@@ -24,13 +24,13 @@ def workers():
2424 redis = FakeRedis ()
2525 clients = []
2626
27- def worker ():
27+ def worker (secret_key = "test-secret" ):
2828 with mock .patch .object (
2929 Client , "_initialize_flag_cache" , return_value = RedisFlagCache (redis )
3030 ):
3131 client = Client (
3232 FAKE_TEST_API_KEY ,
33- secret_key = "test-secret" ,
33+ secret_key = secret_key ,
3434 send = False ,
3535 enable_local_evaluation = False ,
3636 )
@@ -253,7 +253,13 @@ def test_fork_retains_snapshot_binding(workers):
253253
254254
255255def remote_success (client , during_request = None ):
256- details = FeatureFlag .from_json ({"key" : "person" , "enabled" : True })
256+ details = FeatureFlag .from_json (
257+ {
258+ "key" : "person" ,
259+ "enabled" : True ,
260+ "metadata" : {"payload" : '{"source":"remote"}' },
261+ }
262+ )
257263
258264 def request (* args , ** kwargs ):
259265 if during_request :
@@ -274,8 +280,8 @@ def test_remote_only_success_remains_available_for_stale_fallback(workers):
274280 with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
275281 assert remote_success (writer ).get_value () is True
276282 assert fallback (writer ).get_value () is True
277- # Without verified definitions, a new Client cannot reuse this provenance .
278- assert fallback (workers ()) is None
283+ # Remote results remain usable by a worker starting during the outage .
284+ assert fallback (workers ()). get_value () is True
279285
280286
281287@pytest .mark .parametrize ("transition" , ["hydrate" , "empty-hydrate" , 401 , 402 ])
@@ -299,6 +305,7 @@ def change():
299305 with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
300306 assert remote_success (client , change ).get_value () is True
301307 assert fallback (client ) is None
308+ assert fallback (workers ()) is None
302309 load (client , definitions (1 ))
303310 assert evaluate (client ).get_value () is True
304311 assert fallback (client ).get_value () is True
@@ -327,7 +334,7 @@ def test_reset_empty_fingerprint_cannot_cache_or_read_results(workers, status):
327334 assert fallback (client ) is None
328335
329336
330- def test_invalidated_remote_only_result_does_not_revive_after_restart (workers ):
337+ def test_remote_result_requires_matching_snapshot_after_hydration (workers ):
331338 writer = workers ()
332339 with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
333340 assert remote_success (writer ).get_value () is True
@@ -336,10 +343,10 @@ def test_invalidated_remote_only_result_does_not_revive_after_restart(workers):
336343 assert fallback (writer ) is None
337344 writer .shutdown ()
338345 with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
339- assert fallback (workers ()) is None
346+ assert fallback (workers ()). get_value () is True
340347
341348
342- def test_fork_renews_remote_only_provenance (workers ):
349+ def test_fork_retains_remote_fallback (workers ):
343350 client = workers ()
344351 with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
345352 assert remote_success (client ).get_value () is True
@@ -348,6 +355,68 @@ def test_fork_renews_remote_only_provenance(workers):
348355 client , "_initialize_flag_cache" , return_value = RedisFlagCache (redis )
349356 ):
350357 client ._reinit_after_fork ()
351- assert fallback (client ) is None
358+ assert fallback (client ). get_value () is True
352359 assert remote_success (client ).get_value () is True
353360 assert fallback (client ).get_value () is True
361+
362+
363+ @pytest .mark .parametrize ("secret_key" , [None , "test-secret" ])
364+ @pytest .mark .parametrize ("writer_has_definitions" , [False , True ])
365+ def test_cold_worker_uses_shared_remote_result_during_outage (
366+ workers , secret_key , writer_has_definitions
367+ ):
368+ writer = workers (secret_key )
369+ if writer_has_definitions :
370+ load (writer , definitions (1 ))
371+ with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
372+ assert remote_success (writer ).get_value () is True
373+ reader = workers (secret_key )
374+ # Reproduce startup when neither endpoint is available.
375+ reader ._load_feature_flags ()
376+ assert reader .feature_flags is None
377+ result = fallback (reader )
378+ assert result is not None
379+ assert result .get_value () is True
380+ assert result .payload == {"source" : "remote" }
381+
382+
383+ def test_cold_worker_rejects_shared_local_result_during_outage (workers ):
384+ writer = workers ()
385+ load (writer , definitions (1 ))
386+ assert evaluate (writer ).get_value () is True
387+ with mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )):
388+ assert fallback (workers ()) is None
389+
390+
391+ def test_loaded_worker_rejects_remote_result_from_different_snapshot (workers ):
392+ writer = workers ()
393+ load (writer , definitions (1 ))
394+ assert remote_success (writer ).get_value () is True
395+ reader = workers ()
396+ load (reader , definitions (2 ))
397+ assert fallback (reader ) is None
398+
399+
400+ @pytest .mark .parametrize ("status" , [401 , 402 ])
401+ def test_reset_worker_rejects_shared_remote_result (workers , status ):
402+ writer = workers ()
403+ load (writer , definitions (1 ))
404+ assert remote_success (writer ).get_value () is True
405+ reader = workers ()
406+ with mock .patch ("posthog.client.get" , side_effect = APIError (status , "reset" )):
407+ reader ._load_feature_flags ()
408+ assert fallback (reader ) is None
409+
410+
411+ def test_shared_remote_fallback_respects_stale_ttl (workers ):
412+ writer = workers ()
413+ with (
414+ mock .patch ("posthog.client.get" , side_effect = APIError (503 , "offline" )),
415+ mock .patch ("posthog.utils.time.time" , return_value = 100 ) as clock ,
416+ ):
417+ assert remote_success (writer ).get_value () is True
418+ reader = workers ()
419+ clock .return_value = 100 + writer .flag_cache .stale_ttl - 1
420+ assert fallback (reader ).get_value () is True
421+ clock .return_value += 1
422+ assert fallback (reader ) is None
0 commit comments