Skip to content

Commit e82a00a

Browse files
committed
fix: retry flags only on transport errors
1 parent 106c8f6 commit e82a00a

5 files changed

Lines changed: 168 additions & 226 deletions

File tree

posthog/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
FlagDefinitionCacheProvider as FlagDefinitionCacheProvider,
6868
)
6969
from posthog.request import (
70+
DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES as DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES,
7071
disable_connection_reuse as disable_connection_reuse,
7172
enable_keep_alive as enable_keep_alive,
7273
set_socket_options as set_socket_options,
@@ -322,6 +323,9 @@ def get_tags() -> Dict[str, Any]:
322323
attributed to the person normally.
323324
feature_flags_request_timeout_seconds: Timeout in seconds for feature flag
324325
and remote config requests.
326+
feature_flags_request_max_retries: Number of retries for feature flag
327+
requests after network, transport, or timeout failures. Defaults to 1.
328+
Set to 0 to disable retries.
325329
super_properties: Properties merged into every captured event.
326330
enable_exception_autocapture: Automatically capture uncaught exceptions.
327331
log_captured_exceptions: Also log exceptions captured by error tracking.
@@ -365,6 +369,7 @@ def get_tags() -> Dict[str, Any]:
365369
disable_geoip = True # type: bool
366370
is_server = True # type: bool
367371
feature_flags_request_timeout_seconds = 3 # type: int
372+
feature_flags_request_max_retries = DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES # type: int
368373
super_properties = None # type: Optional[Dict]
369374
enable_exception_autocapture = False # type: bool
370375
log_captured_exceptions = False # type: bool
@@ -1156,6 +1161,7 @@ def setup() -> Client:
11561161
disable_geoip=disable_geoip,
11571162
is_server=is_server,
11581163
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
1164+
feature_flags_request_max_retries=feature_flags_request_max_retries,
11591165
super_properties=super_properties,
11601166
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)
11611167
# This kind of initialisation is very annoying for exception capture. We need to figure out a way around this,

posthog/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
AI_EVENTS_ENDPOINT,
7070
EVENTS_ENDPOINT,
7171
APIError,
72+
DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES,
7273
QuotaLimitError,
7374
RequestsConnectionError,
7475
RequestsTimeout,
@@ -240,6 +241,7 @@ def __init__(
240241
is_server=True,
241242
historical_migration=False,
242243
feature_flags_request_timeout_seconds=3,
244+
feature_flags_request_max_retries=DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES,
243245
super_properties=None,
244246
enable_exception_autocapture=False,
245247
log_captured_exceptions=False,
@@ -296,6 +298,9 @@ def __init__(
296298
historical_migration: Mark events as historical migration imports.
297299
feature_flags_request_timeout_seconds: Timeout in seconds for feature
298300
flag and remote config requests.
301+
feature_flags_request_max_retries: Number of retries for feature flag
302+
requests after network, transport, or timeout failures. Defaults
303+
to 1. Set to 0 to disable retries.
299304
super_properties: Properties merged into every captured event.
300305
enable_exception_autocapture: Automatically capture uncaught
301306
exceptions.
@@ -376,6 +381,9 @@ def __init__(
376381
self.feature_flags_request_timeout_seconds = (
377382
feature_flags_request_timeout_seconds
378383
)
384+
self.feature_flags_request_max_retries = max(
385+
0, feature_flags_request_max_retries
386+
)
379387
self.poller: Optional[Poller] = None
380388
self.distinct_ids_feature_flags_reported = SizeLimitedDict(MAX_DICT_SIZE, set)
381389
self.flag_fallback_cache_url = flag_fallback_cache_url
@@ -895,10 +903,18 @@ def _get_flags_decision(
895903
if flag_keys_to_evaluate:
896904
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
897905

906+
flag_request_options: Dict[str, Any] = {}
907+
if (
908+
self.feature_flags_request_max_retries
909+
!= DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES
910+
):
911+
flag_request_options["max_retries"] = self.feature_flags_request_max_retries
912+
898913
resp_data = flags(
899914
self.api_key,
900915
self.host,
901916
timeout=self.feature_flags_request_timeout_seconds,
917+
**flag_request_options,
902918
**request_data,
903919
)
904920

posthog/request.py

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import re
44
import socket
5+
import time
56
from dataclasses import dataclass
67
from datetime import date, datetime, timezone
78
from gzip import GzipFile
@@ -41,8 +42,8 @@
4142
if hasattr(socket, attr):
4243
KEEP_ALIVE_SOCKET_OPTIONS.append((socket.SOL_TCP, getattr(socket, attr), value))
4344

44-
# Status codes that indicate transient server errors worth retrying
45-
RETRY_STATUS_FORCELIST = [408, 500, 502, 503, 504]
45+
DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES = 1
46+
FEATURE_FLAGS_RETRY_BACKOFF_SECONDS = 0.3
4647

4748

4849
def _mask_tokens_in_url(url: str) -> str:
@@ -90,23 +91,14 @@ def _build_session(socket_options: Optional[SocketOptions] = None) -> requests.S
9091
def _build_flags_session(
9192
socket_options: Optional[SocketOptions] = None,
9293
) -> requests.Session:
93-
"""
94-
Build a session for feature flag requests with POST retries.
94+
"""Build a session for feature flag requests.
9595
96-
Feature flag requests are idempotent (read-only), so retrying POST
97-
requests is safe. This session retries on transient server errors
98-
(408, 5xx) and network failures with exponential backoff
99-
(0.5s, 1s delays between retries).
96+
/flags retries are handled explicitly in ``flags()`` so that only
97+
transport failures are retried. HTTP status responses must surface as API
98+
errors without retrying.
10099
"""
101100
adapter = HTTPAdapterWithSocketOptions(
102-
max_retries=Retry(
103-
total=2,
104-
connect=2,
105-
read=2,
106-
backoff_factor=0.5,
107-
status_forcelist=RETRY_STATUS_FORCELIST,
108-
allowed_methods=["POST"],
109-
),
101+
max_retries=Retry(total=0, connect=0, read=0, status=0),
110102
socket_options=socket_options,
111103
)
112104
session = requests.Session()
@@ -306,26 +298,41 @@ def _process_response(
306298
raise APIError(res.status_code, res.text, retry_after=retry_after)
307299

308300

301+
def _feature_flags_retry_delay(failed_attempt: int) -> float:
302+
return FEATURE_FLAGS_RETRY_BACKOFF_SECONDS * (2**failed_attempt)
303+
304+
309305
def flags(
310306
api_key: str,
311307
host: Optional[str] = None,
312308
gzip: bool = False,
313309
timeout: int = 15,
310+
max_retries: int = DEFAULT_FEATURE_FLAGS_REQUEST_MAX_RETRIES,
314311
**kwargs,
315312
) -> Any:
316-
"""Post the kwargs to the flags API endpoint with automatic retries."""
317-
res = post(
318-
api_key,
319-
host,
320-
"/flags/?v=2",
321-
gzip,
322-
timeout,
323-
session=_get_flags_session(),
324-
**kwargs,
325-
)
326-
return _process_response(
327-
res, success_message="Feature flags evaluated successfully"
328-
)
313+
"""Post the kwargs to the flags API endpoint with transport retries."""
314+
retries = max(0, max_retries)
315+
failed_attempt = 0
316+
317+
while True:
318+
try:
319+
res = post(
320+
api_key,
321+
host,
322+
"/flags/?v=2",
323+
gzip,
324+
timeout,
325+
session=_get_flags_session(),
326+
**kwargs,
327+
)
328+
return _process_response(
329+
res, success_message="Feature flags evaluated successfully"
330+
)
331+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
332+
if failed_attempt >= retries:
333+
raise
334+
time.sleep(_feature_flags_retry_delay(failed_attempt))
335+
failed_attempt += 1
329336

330337

331338
def remote_config(

posthog/test/test_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,21 @@ def test_basic_capture_with_feature_flags_returns_active_only(self, patch_flags)
984984
device_id=None,
985985
)
986986

987+
@mock.patch("posthog.client.flags")
988+
def test_feature_flags_request_max_retries_is_forwarded_when_configured(
989+
self, patch_flags
990+
):
991+
patch_flags.return_value = {"featureFlags": {}, "featureFlagPayloads": {}}
992+
client = Client(
993+
FAKE_TEST_API_KEY,
994+
feature_flags_request_max_retries=0,
995+
personal_api_key=FAKE_TEST_API_KEY,
996+
)
997+
998+
client.get_all_flags("distinct_id")
999+
1000+
self.assertEqual(patch_flags.call_args.kwargs["max_retries"], 0)
1001+
9871002
@mock.patch("posthog.client.flags")
9881003
def test_basic_capture_with_feature_flags_and_disable_geoip_returns_correctly(
9891004
self, patch_flags

0 commit comments

Comments
 (0)