diff --git a/optimizely/event_dispatcher.py b/optimizely/event_dispatcher.py index 55209dc8..b06b9e18 100644 --- a/optimizely/event_dispatcher.py +++ b/optimizely/event_dispatcher.py @@ -49,7 +49,7 @@ def dispatch_event(event: event_builder.Event) -> None: session = requests.Session() retries = Retry(total=EventDispatchConfig.RETRIES, - backoff_factor=0.1, + backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]) adapter = HTTPAdapter(max_retries=retries) diff --git a/optimizely/odp/odp_event_manager.py b/optimizely/odp/odp_event_manager.py index 85512e90..3fb961ac 100644 --- a/optimizely/odp/odp_event_manager.py +++ b/optimizely/odp/odp_event_manager.py @@ -163,6 +163,8 @@ def _flush_batch(self) -> None: self.logger.debug(f'ODP event queue: flushing batch size {batch_len}.') should_retry = False + initial_retry_interval = 0.2 # 200ms + max_retry_interval = 1.0 # 1 second for i in range(1 + self.retry_count): try: @@ -176,7 +178,12 @@ def _flush_batch(self) -> None: if not should_retry: break if i < self.retry_count: - self.logger.debug('Error dispatching ODP events, scheduled to retry.') + # Exponential backoff: 200ms, 400ms, 800ms, ... capped at 1s + delay = initial_retry_interval * (2 ** i) + if delay > max_retry_interval: + delay = max_retry_interval + self.logger.debug(f'Error dispatching ODP events, retrying after {delay}s.') + time.sleep(delay) if should_retry: self.logger.error(Errors.ODP_EVENT_FAILED.format(f'Failed after {i} retries: {self._current_batch}')) diff --git a/tests/test_odp_event_manager.py b/tests/test_odp_event_manager.py index d9d29eab..acec396f 100644 --- a/tests/test_odp_event_manager.py +++ b/tests/test_odp_event_manager.py @@ -265,7 +265,7 @@ def test_odp_event_manager_retry_failure(self, *args): with mock.patch.object( event_manager.api_manager, 'send_odp_events', new_callable=CopyingMock, return_value=True - ) as mock_send: + ) as mock_send, mock.patch('time.sleep') as mock_sleep: event_manager.send_event(**self.events[0]) event_manager.send_event(**self.events[1]) event_manager.flush() @@ -275,7 +275,9 @@ def test_odp_event_manager_retry_failure(self, *args): [mock.call(self.api_key, self.api_host, self.processed_events)] * number_of_tries ) self.assertEqual(len(event_manager._current_batch), 0) - mock_logger.debug.assert_any_call('Error dispatching ODP events, scheduled to retry.') + # Verify exponential backoff delays: 0.2s, 0.4s, 0.8s + mock_sleep.assert_has_calls([mock.call(0.2), mock.call(0.4), mock.call(0.8)]) + mock_logger.debug.assert_any_call('Error dispatching ODP events, retrying after 0.2s.') mock_logger.error.assert_called_once_with( f'ODP event send failed (Failed after 3 retries: {self.processed_events}).' ) @@ -288,7 +290,7 @@ def test_odp_event_manager_retry_success(self, *args): with mock.patch.object( event_manager.api_manager, 'send_odp_events', new_callable=CopyingMock, side_effect=[True, True, False] - ) as mock_send: + ) as mock_send, mock.patch('time.sleep') as mock_sleep: event_manager.send_event(**self.events[0]) event_manager.send_event(**self.events[1]) event_manager.flush() @@ -296,7 +298,9 @@ def test_odp_event_manager_retry_success(self, *args): mock_send.assert_has_calls([mock.call(self.api_key, self.api_host, self.processed_events)] * 3) self.assertEqual(len(event_manager._current_batch), 0) - mock_logger.debug.assert_any_call('Error dispatching ODP events, scheduled to retry.') + # Verify exponential backoff delays: 0.2s, 0.4s (only 2 delays for 3 attempts) + mock_sleep.assert_has_calls([mock.call(0.2), mock.call(0.4)]) + mock_logger.debug.assert_any_call('Error dispatching ODP events, retrying after 0.2s.') mock_logger.error.assert_not_called() self.assertStrictTrue(event_manager.is_running) event_manager.stop()