Repository navigation
feat(feature-flags): Evaluate feature flags locally with a polling runtime client - #765
Open
stanleyphu wants to merge 5 commits into
Open
stanleyphu wants to merge 5 commits into
stanleyphu wants to merge 5 commits into
Conversation
…ntime client FeatureFlagsRuntimeClient polls GET /sdk/feature-flags?payload_version=2 on a daemon thread, keeps the configuration in memory and evaluates flags locally from it. A failed poll keeps the last good configuration and backs off, transient failures are retried within request_timeout, and a 401 stops polling. It supports bootstrap, change, error and status listeners, status and a manual refresh(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 4, 2026
Contributor
[Medium impact] The reviewed changes appear safe to merge. SummaryAdds a synchronous feature flag client that polls in the background and evaluates cached flags locally.
Reviews (4) · Last reviewed commit: "fix(feature-flags): Make every close() c..." · Reviewed by Greptile |
…e-flags-runtime-sync-client # Conflicts: # tests/feature_flags_runtime/test_configuration.py
A retry inside a refresh waited for whatever Retry-After the server sent, uncapped and possibly negative, while the wait before the next poll was capped at 60 seconds. Both now use the same capped value, as Node does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
close() only joins the background thread, so a manual refresh() that passed its closed check just before close() could still install a new configuration after close() returned, without a change notification. The check and the snapshot update now happen under the lock close() takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@greptileai review |
…stop A second close() returned as soon as it saw the client was closed, while the first was still waiting for an in-flight poll, so the second caller could close the WorkOSClient under it. Every caller now waits for the poller thread, as the async client does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@greptileai review |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack. This is #762, split into four PRs for review. Merge them in order; after each merge I'll rebase the next one onto
main.Description
FeatureFlagsRuntimeClientpollsGET /sdk/feature-flags?payload_version=2on a daemon thread and keeps the configuration from #764 in memory. It evaluates flags locally, with no request per check.Retry-Afteris honored up to 60 seconds, by retries inside a refresh and before the next poll.max_retriesand withinrequest_timeout.request_timeoutplus at most one read of a body that stalls.bootstrap,on_change,on_errorandon_statuslisteners,status,get_flag(), and a manualrefresh()that works withoutstart().refresh()itself.close()stops polling, and nothing a refresh still in flight does changes the configuration or status after it returns. Every caller waits for the poller, including concurrent ones.on_changeper refresh that installs a new configuration, including the first load.Where to look, in order:
_core.py: state, evaluation and listeners._sync.py:_poll_forever,_refresh_lockedand_drain._transport.py: the time budget._policy.py: backoff and retry decisions.Not in this PR: fork handling comes in #767. Until then, the README says to create the runtime client in each worker after forking. Merge #765 through #767 before the next release PR, so the first release includes fork handling.
How I checked it:
I ran it in our feature flags runtime lab, side by side with the Node SDK, against the same fixture responses. All 80 side-by-side checks pass: 40 for Python, 40 for Node. They cover:
The lab's asyncio checks need feat(feature-flags): Add an async feature flags runtime client #766.
test_transport.pysends the poll to a real local HTTP server. That includes one server that stalls before answering and one that drips the body a byte at a time. In both cases the refresh ends within its budget.nox -s cipasses locally: 3,203 tests.Documentation
Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.
The feature flags SDK docs describe the runtime client as Node only. A docs PR adding Python, covering both clients, is still to do.
🤖 Generated with Claude Code