Skip to content

fix: Do not propagate persistent-store errors from the sync FDv2 warm-start check - #506

Open
jsonbailey wants to merge 3 commits into
mainfrom
jb/sdk-60/sync-fdv2-warmstart-throw
Open

fix: Do not propagate persistent-store errors from the sync FDv2 warm-start check#506
jsonbailey wants to merge 3 commits into
mainfrom
jb/sdk-60/sync-fdv2-warmstart-throw

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The bug

Sync FDv2, when configured with a persistent store (e.g. Redis), could throw a store I/O error out of variation() / variation_detail() / all_flags_state() during the warm-start window (before a data source initializes), instead of degrading to CLIENT_NOT_READY / the default. A variation() must never throw.

The path: LDClient._evaluate_internal / all_flags_state read self._data_system.data_availability before their own try/except. For FDv2, data_availability calls self._store.is_initialized() while no basis has arrived, which reaches the persistent store's initialized_internal() — a blocking query (e.g. redis exists()) that raises on a store error and was not caught. __evaluate_with_hooks does not catch exceptions from block(), so the error propagated out of variation(). (The store's is_available() is wrapped safe, but initialized_internal() was not — that asymmetry is the root.)

The fix

Make the availability gate total: FDv2.data_availability now wraps the persistent-store read in a try/except and degrades to DataAvailability.DEFAULTS on error, logging it. A store I/O error therefore never leaves the gate — variation() returns the default with CLIENT_NOT_READY and all_flags_state() returns an invalid state.

The catch lives in the gate rather than at the client call sites so it covers every caller. In particular, is_initialized() reads data_availability directly, so a client-side-only guard would have left is_initialized() able to throw. This mirrors the async gate in #486, keeping the two implementations symmetric.

The catch is scoped to the store read only; the REFRESHED and CACHED checks around it are untouched, so a real logic error there is not masked.

Also: log persistent-store close errors

Store.close() previously returned the close error as Optional[Exception], which its only caller (FDv2.stop) discarded — so a failed close was silently lost. It now logs a warning and returns None. Closing happens at shutdown, where there is no caller left to react to the error. (Pre-existing since 9.16.0, not introduced by recent refactors; the async counterpart is in #486.)

Also corrects a stale docstring on FDv2._consume_synchronizer_results (described a tuple return; returns a single ConditionDirective).

Validation

  • test_variation_does_not_throw_when_persistent_store_errors_during_warm_start: a persistent store whose initialized raises, with a synchronizer configured but no basis yet. Asserts the gate now returns DEFAULTS (rather than raising), and that through the client variation() / variation_detail() return the default with CLIENT_NOT_READY and all_flags_state() is invalid — no exception.
  • test_persistent_store_close_logs_and_swallows_error: a persistent store whose close() raises; Store.close() logs a warning and does not raise.
  • Full unit suite (minus DB integrations) green; mypy, isort, pycodestyle clean.

Scope

Sync FDv2 only. Sync FDv1 reads data_availability the same way; out of scope here.


Note

Overview
Sync FDv2 no longer lets persistent-store I/O failures escape variation(), variation_detail(), or all_flags_state() during warm-start (before a data source has supplied a basis). FDv2.data_availability now catches errors from the persistent-store initialized check, logs them, and reports DataAvailability.DEFAULTS so the client returns defaults with CLIENT_NOT_READY (or an invalid all_flags_state) instead of raising.

Store.close() logs a warning when closing the persistent store fails instead of returning a discarded exception. LDClient evaluation paths cache data_availability in a local variable when branching on readiness (behavior unchanged).

Adds regression tests for throwing initialized during warm-start and for close-error logging; fixes a stale docstring on _consume_synchronizer_results.

Reviewed by Cursor Bugbot for commit 6a5dd9e. Bugbot is set up for automated code reviews on this repo. Configure here.

The persistent-store error catch moves out of the client and into
FDv2.data_availability, which now degrades to DEFAULTS itself instead of
letting a store I/O error propagate. This makes the availability gate
total for every caller: is_initialized() reads the gate directly and so
was not covered by the previous client-side guard.
… them

Store.close() previously returned the close error as Optional[Exception],
which the only caller (FDv2.stop) discarded, so a failed close was
silently lost. It now logs a warning and returns None. Closing happens at
shutdown, where there is no caller left to react to the error.
@jsonbailey
jsonbailey marked this pull request as ready for review August 27, 2026 21:57
@jsonbailey
jsonbailey requested a review from a team as a code owner August 27, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant