Skip to content

Commit 058be5e

Browse files
fix: infinite loop on error (#1529)
1 parent b23ca6f commit 058be5e

4 files changed

Lines changed: 129 additions & 4 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.10.36"
3+
version = "2.10.37"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/telemetry/_track.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,18 @@ def send(self, data_to_send: Any) -> None:
202202
except Exception as e:
203203
_logger.warning("AppInsights send: %s (%s)", type(e).__name__, e)
204204

205-
# Re-queue unsent data
205+
# Re-queue unsent data up to 2 attempts, then discard
206+
max_retries = 2
206207
for data in data_to_send:
207-
self._queue.put(data)
208+
attempt = getattr(data, "_send_attempts", 0) + 1
209+
if attempt < max_retries:
210+
data._send_attempts = attempt
211+
self._queue.put(data)
212+
else:
213+
_logger.warning(
214+
"AppInsights send: discarding item after %d failed attempts",
215+
attempt,
216+
)
208217

209218

210219
class _AppInsightsEventClient:

packages/uipath/tests/telemetry/test_track.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from uipath.telemetry._track import (
77
_AppInsightsEventClient,
8+
_DiagnosticSender,
89
_parse_connection_string,
910
_TelemetryClient,
1011
flush_events,
@@ -706,3 +707,118 @@ def test_initialize_handles_exception(
706707

707708
assert _AppInsightsEventClient._initialized is True
708709
assert _AppInsightsEventClient._client is None
710+
711+
712+
class TestDiagnosticSender:
713+
"""Test _DiagnosticSender retry, re-queue, and discard logic."""
714+
715+
def _make_sender(self):
716+
"""Create a _DiagnosticSender with a mock queue."""
717+
sender = _DiagnosticSender.__new__(_DiagnosticSender)
718+
sender._service_endpoint_uri = "https://example.com/v2/track"
719+
sender._timeout = 10
720+
sender._queue = MagicMock()
721+
return sender
722+
723+
def _make_data_item(self, send_attempts=None):
724+
item = MagicMock()
725+
item.write.return_value = {"name": "test"}
726+
if send_attempts is not None:
727+
item._send_attempts = send_attempts
728+
else:
729+
del item._send_attempts
730+
return item
731+
732+
@patch("urllib.request.urlopen")
733+
def test_successful_send_does_not_requeue(self, mock_urlopen):
734+
"""Test that a 2xx response returns early without re-queuing."""
735+
sender = self._make_sender()
736+
mock_response = MagicMock()
737+
mock_response.getcode.return_value = 200
738+
mock_urlopen.return_value = mock_response
739+
740+
data = [self._make_data_item()]
741+
sender.send(data)
742+
743+
sender._queue.put.assert_not_called()
744+
745+
@patch("urllib.request.urlopen")
746+
def test_http_400_discards_without_requeue(self, mock_urlopen):
747+
"""Test that HTTP 400 logs a warning and returns before retry logic."""
748+
from urllib.error import HTTPError
749+
750+
sender = self._make_sender()
751+
mock_urlopen.side_effect = HTTPError(
752+
url="https://example.com",
753+
code=400,
754+
msg="Bad Request",
755+
hdrs=MagicMock(),
756+
fp=None,
757+
)
758+
759+
data = [self._make_data_item()]
760+
sender.send(data)
761+
762+
sender._queue.put.assert_not_called()
763+
764+
@patch("urllib.request.urlopen")
765+
def test_multiple_fresh_items_all_requeued_on_failure(self, mock_urlopen):
766+
"""Test that all fresh items in a batch are re-queued on failure."""
767+
from urllib.error import HTTPError
768+
769+
sender = self._make_sender()
770+
mock_urlopen.side_effect = HTTPError(
771+
url="https://example.com",
772+
code=503,
773+
msg="Unavailable",
774+
hdrs=MagicMock(),
775+
fp=None,
776+
)
777+
778+
items = [self._make_data_item() for _ in range(3)]
779+
sender.send(items)
780+
781+
assert sender._queue.put.call_count == 3
782+
for item in items:
783+
assert item._send_attempts == 1
784+
785+
@patch("urllib.request.urlopen")
786+
def test_item_with_one_prior_attempt_is_discarded(self, mock_urlopen):
787+
"""Test that an already-retried item (attempt=1) is discarded on next failure."""
788+
from urllib.error import HTTPError
789+
790+
sender = self._make_sender()
791+
mock_urlopen.side_effect = HTTPError(
792+
url="https://example.com",
793+
code=500,
794+
msg="Server Error",
795+
hdrs=MagicMock(),
796+
fp=None,
797+
)
798+
799+
item = self._make_data_item(send_attempts=1) # already retried once
800+
sender.send([item])
801+
802+
sender._queue.put.assert_not_called()
803+
804+
@patch("urllib.request.urlopen")
805+
def test_mixed_batch_requeues_fresh_discards_retried(self, mock_urlopen):
806+
"""Test a batch with both fresh and already-retried items."""
807+
from urllib.error import HTTPError
808+
809+
sender = self._make_sender()
810+
mock_urlopen.side_effect = HTTPError(
811+
url="https://example.com",
812+
code=502,
813+
msg="Bad Gateway",
814+
hdrs=MagicMock(),
815+
fp=None,
816+
)
817+
818+
fresh_item = self._make_data_item() # no prior attempts
819+
retried_item = self._make_data_item(send_attempts=1) # already retried
820+
821+
sender.send([fresh_item, retried_item])
822+
823+
sender._queue.put.assert_called_once_with(fresh_item)
824+
assert fresh_item._send_attempts == 1

packages/uipath/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)