Skip to content

Commit 3b85970

Browse files
committed
fix: mark the node that answered as healthy (#144)
1 parent 6a82649 commit 3b85970

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
@@ -684,3 +684,73 @@ async def test_async_sleeps_retry_interval_between_retries(
684684
assert sleep_call == mocker.call(
685685
fake_async_api_call.config.retry_interval_seconds,
686686
)
687+
688+
689+
def test_round_robin_visits_each_node_in_turn(fake_api_call: ApiCall) -> None:
690+
"""Test that successful requests advance the round-robin by one node each."""
691+
fake_api_call.config.nearest_node = None
692+
693+
with respx.mock:
694+
for host in ("node0", "node1", "node2"):
695+
respx.get(f"http://{host}:8108/").mock(
696+
return_value=httpx.Response(200, json={"key": "value"}),
697+
)
698+
699+
for _ in range(6):
700+
fake_api_call.get("/", entity_type=typing.Dict[str, str])
701+
702+
assert [str(call.request.url) for call in respx.calls] == [
703+
"http://node0:8108/",
704+
"http://node1:8108/",
705+
"http://node2:8108/",
706+
"http://node0:8108/",
707+
"http://node1:8108/",
708+
"http://node2:8108/",
709+
]
710+
711+
712+
async def test_async_round_robin_visits_each_node_in_turn(
713+
fake_async_api_call: AsyncApiCall,
714+
) -> None:
715+
"""Test that successful requests advance the round-robin by one node each (async)."""
716+
fake_async_api_call.config.nearest_node = None
717+
718+
with respx.mock:
719+
for host in ("node0", "node1", "node2"):
720+
respx.get(f"http://{host}:8108/").mock(
721+
return_value=httpx.Response(200, json={"key": "value"}),
722+
)
723+
724+
for _ in range(6):
725+
await fake_async_api_call.get("/", entity_type=typing.Dict[str, str])
726+
727+
assert [str(call.request.url) for call in respx.calls] == [
728+
"http://node0:8108/",
729+
"http://node1:8108/",
730+
"http://node2:8108/",
731+
"http://node0:8108/",
732+
"http://node1:8108/",
733+
"http://node2:8108/",
734+
]
735+
736+
737+
def test_success_marks_only_the_answering_node_healthy(
738+
fake_api_call: ApiCall,
739+
) -> None:
740+
"""Test that a success refreshes the node that answered and no other."""
741+
fake_api_call.config.nearest_node = None
742+
answering_node, unhealthy_node, _ = fake_api_call.node_manager.nodes
743+
answering_node.last_access_ts = 0
744+
unhealthy_node.healthy = False
745+
unhealthy_node.last_access_ts = int(time.time())
746+
747+
with respx.mock:
748+
respx.get("http://node0:8108/").mock(
749+
return_value=httpx.Response(200, json={"key": "value"}),
750+
)
751+
752+
fake_api_call.get("/", entity_type=typing.Dict[str, str])
753+
754+
assert answering_node.healthy is True
755+
assert answering_node.last_access_ts > 0
756+
assert unhealthy_node.healthy is False

0 commit comments

Comments
 (0)