Skip to content

Commit 0b3da55

Browse files
authored
Merge pull request #148 from tharropoulos/fix/144-mark-answering-node
fix: mark the node that answered as healthy (#144)
2 parents 51caa32 + 3d0d70c commit 0b3da55

3 files changed

Lines changed: 78 additions & 10 deletions

File tree

‎src/typesense/async_/api_call.py‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ async def _execute_request(
487487
try:
488488
return await self._make_request_and_process_response(
489489
method,
490+
node,
490491
url,
491492
entity_type,
492493
as_json,
@@ -511,12 +512,13 @@ async def _execute_request(
511512
async def _make_request_and_process_response(
512513
self,
513514
method: str,
515+
node: Node,
514516
url: str,
515517
entity_type: typing.Type[TEntityDict],
516518
as_json: bool,
517519
**kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]],
518520
) -> typing.Union[TEntityDict, str]:
519-
"""Make the async API request and process the response."""
521+
"""Make the async API request to `node` and process the response."""
520522
request_response = await self.request_handler.make_request(
521523
method=method,
522524
url=url,
@@ -525,10 +527,7 @@ async def _make_request_and_process_response(
525527
client=self._client,
526528
**kwargs,
527529
)
528-
self.node_manager.set_node_health(
529-
self.node_manager.get_node(),
530-
is_healthy=True,
531-
)
530+
self.node_manager.set_node_health(node, is_healthy=True)
532531
return (
533532
typing.cast(TEntityDict, request_response)
534533
if as_json

‎src/typesense/sync/api_call.py‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ def _execute_request(
487487
try:
488488
return self._make_request_and_process_response(
489489
method,
490+
node,
490491
url,
491492
entity_type,
492493
as_json,
@@ -511,12 +512,13 @@ def _execute_request(
511512
def _make_request_and_process_response(
512513
self,
513514
method: str,
515+
node: Node,
514516
url: str,
515517
entity_type: typing.Type[TEntityDict],
516518
as_json: bool,
517519
**kwargs: typing.Unpack[SessionFunctionKwargs[TParams, TBody]],
518520
) -> typing.Union[TEntityDict, str]:
519-
"""Make the async API request and process the response."""
521+
"""Make the async API request to `node` and process the response."""
520522
request_response = self.request_handler.make_request(
521523
method=method,
522524
url=url,
@@ -525,10 +527,7 @@ def _make_request_and_process_response(
525527
client=self._client,
526528
**kwargs,
527529
)
528-
self.node_manager.set_node_health(
529-
self.node_manager.get_node(),
530-
is_healthy=True,
531-
)
530+
self.node_manager.set_node_health(node, is_healthy=True)
532531
return (
533532
typing.cast(TEntityDict, request_response)
534533
if as_json

‎tests/api_call_test.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,3 +742,73 @@ async def test_async_client_side_error_does_not_mark_node_unhealthy(
742742
assert not node0_route.called
743743

744744
assert fake_async_api_call.config.nearest_node.healthy is True
745+
746+
747+
def test_round_robin_visits_each_node_in_turn(fake_api_call: ApiCall) -> None:
748+
"""Test that successful requests advance the round-robin by one node each."""
749+
fake_api_call.config.nearest_node = None
750+
751+
with respx.mock:
752+
for host in ("node0", "node1", "node2"):
753+
respx.get(f"http://{host}:8108/").mock(
754+
return_value=httpx.Response(200, json={"key": "value"}),
755+
)
756+
757+
for _ in range(6):
758+
fake_api_call.get("/", entity_type=typing.Dict[str, str])
759+
760+
assert [str(call.request.url) for call in respx.calls] == [
761+
"http://node0:8108/",
762+
"http://node1:8108/",
763+
"http://node2:8108/",
764+
"http://node0:8108/",
765+
"http://node1:8108/",
766+
"http://node2:8108/",
767+
]
768+
769+
770+
async def test_async_round_robin_visits_each_node_in_turn(
771+
fake_async_api_call: AsyncApiCall,
772+
) -> None:
773+
"""Test that successful requests advance the round-robin by one node each (async)."""
774+
fake_async_api_call.config.nearest_node = None
775+
776+
with respx.mock:
777+
for host in ("node0", "node1", "node2"):
778+
respx.get(f"http://{host}:8108/").mock(
779+
return_value=httpx.Response(200, json={"key": "value"}),
780+
)
781+
782+
for _ in range(6):
783+
await fake_async_api_call.get("/", entity_type=typing.Dict[str, str])
784+
785+
assert [str(call.request.url) for call in respx.calls] == [
786+
"http://node0:8108/",
787+
"http://node1:8108/",
788+
"http://node2:8108/",
789+
"http://node0:8108/",
790+
"http://node1:8108/",
791+
"http://node2:8108/",
792+
]
793+
794+
795+
def test_success_marks_only_the_answering_node_healthy(
796+
fake_api_call: ApiCall,
797+
) -> None:
798+
"""Test that a success refreshes the node that answered and no other."""
799+
fake_api_call.config.nearest_node = None
800+
answering_node, unhealthy_node, _ = fake_api_call.node_manager.nodes
801+
answering_node.last_access_ts = 0
802+
unhealthy_node.healthy = False
803+
unhealthy_node.last_access_ts = int(time.time())
804+
805+
with respx.mock:
806+
respx.get("http://node0:8108/").mock(
807+
return_value=httpx.Response(200, json={"key": "value"}),
808+
)
809+
810+
fake_api_call.get("/", entity_type=typing.Dict[str, str])
811+
812+
assert answering_node.healthy is True
813+
assert answering_node.last_access_ts > 0
814+
assert unhealthy_node.healthy is False

0 commit comments

Comments
 (0)