Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,13 @@ flags.get_all_flags({"organization": "org_123"}) # {"new-checkout": True, ...}
flags.close() # on shutdown; or use `with workos.create_feature_flags_runtime_client() as flags:`
```

With `AsyncWorkOSClient`, polling runs in an asyncio task: `await flags.start()`, `await flags.wait_until_ready(timeout=5)` and `await flags.close()` (or `async with`). Evaluation stays synchronous.

**Evaluation.** The context maps a target type to the exact, case-sensitive ID being evaluated. `user` and `organization` are target types like any [custom target](https://workos.com/docs/feature-flags/custom-targets) type, and an ID of `None` is skipped. A disabled flag serves its off value. For an enabled flag, the first rule whose conditions all match wins, so a rule serving `False` can exclude a user before a later rule includes their organization; otherwise the flag's default value applies. A missing flag, an unready client, or a flag without a boolean answer returns `default` (`False` unless you pass one), and `get_all_flags` omits flags without an answer. Evaluation never raises: invalid context entries are ignored with a logged warning and the rest is evaluated.

**Failures.** A failed poll keeps the last good configuration and retries with exponential backoff. Transient failures (429, 500, 502, 503, 504, timeouts, connection errors) are also retried within one refresh, up to the client's `max_retries` and within `request_timeout`. A 429's `Retry-After` is honored up to 60 seconds, both by those retries and before the next poll. The client streams the response and gives every network wait only the time left, so a refresh takes at most `request_timeout` plus one read of a response body that stalls; a custom `HTTPBackend` that is not an httpx client receives the remaining time as its per-request timeout instead. A 401, from a poll or from `refresh()`, stops polling for good: create a new client with a valid API key. Failures are logged to the `workos.feature_flags.runtime` logger (pass `logger=` to use your own) and reported to error listeners. `flags.status` shows readiness, the last error, refresh counts and timestamps, `polling_stopped`, `flag_count` and `cache_age()`.
**Failures.** A failed poll keeps the last good configuration and retries with exponential backoff. Transient failures (429, 500, 502, 503, 504, timeouts, connection errors) are also retried within one refresh, up to the client's `max_retries` and within `request_timeout`. A 429's `Retry-After` is honored up to 60 seconds, both by those retries and before the next poll. The async client enforces `request_timeout` exactly. The sync client streams the response and gives every network wait only the time left, so a refresh takes at most `request_timeout` plus one read of a response body that stalls; a custom `HTTPBackend` that is not an httpx client receives the remaining time as its per-request timeout instead. A 401, from a poll or from `refresh()`, stops polling for good: create a new client with a valid API key. Failures are logged to the `workos.feature_flags.runtime` logger (pass `logger=` to use your own) and reported to error listeners. `flags.status` shows readiness, the last error, refresh counts and timestamps, `polling_stopped`, `flag_count` and `cache_age()`.

**Notifications.** `on_change` (or `add_change_listener`) fires after a refresh installs a different configuration, including the first load, with the previous and current snapshots and the changed slugs. `on_error` fires for failed background polls and `on_status` after every refresh. Listeners run one at a time; their exceptions are logged and never affect polling.
**Notifications.** `on_change` (or `add_change_listener`) fires after a refresh installs a different configuration, including the first load, with the previous and current snapshots and the changed slugs. `on_error` fires for failed background polls and `on_status` after every refresh. Listeners run one at a time; their exceptions are logged and never affect polling. Async clients also accept coroutine listeners.

**Bootstrap.** Persist the configuration and pass it back to make a new client ready before its first poll:

Expand All @@ -277,7 +279,7 @@ flags = workos.create_feature_flags_runtime_client(

**Inspection and manual refresh.** `flags.get_flag(slug)` returns a flag's configuration as received, with its rules in order. `flags.refresh()` fetches the configuration immediately, raising on failure, and works without `start()`, e.g. in scripts or serverless functions.

**Processes.** The runtime client polls through the `WorkOSClient`'s HTTP client, so close it before closing the `WorkOSClient` or an HTTP client you supplied. Create the `WorkOSClient` and the runtime client in each worker process, after forking, rather than in an app gunicorn loads with `--preload`. Background threads must be enabled, so run uWSGI with `--enable-threads`.
**Processes.** The runtime client polls through its `WorkOSClient` or `AsyncWorkOSClient`, so close it before closing that client or an HTTP client you supplied. Create the client and the runtime client in each worker process, after forking, rather than in an app gunicorn loads with `--preload`. Background threads must be enabled, so run uWSGI with `--enable-threads`.

## Type Safety

Expand Down
2 changes: 2 additions & 0 deletions src/workos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)
from ._pagination import AsyncPage, ListMetadata, SyncPage
from .feature_flags.runtime import (
AsyncFeatureFlagsRuntimeClient,
FeatureFlagsConfiguration,
FeatureFlagsRuntimeClient,
)
Expand Down Expand Up @@ -56,5 +57,6 @@
"TransportTimeout",
"TransportConnectError",
"FeatureFlagsRuntimeClient",
"AsyncFeatureFlagsRuntimeClient",
"FeatureFlagsConfiguration",
]
6 changes: 6 additions & 0 deletions src/workos/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,10 @@ def pkce(self) -> PKCE:
"""PKCE (Proof Key for Code Exchange) utilities."""
return PKCE()

# A plain function bound as a method, so its signature and docs live next
# to the runtime client's constructor.
from .feature_flags.runtime._factory import (
create_async_feature_flags_runtime_client as create_feature_flags_runtime_client,
)

# @oagen-ignore-end
5 changes: 4 additions & 1 deletion src/workos/feature_flags/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

"""Local evaluation of WorkOS feature flags from a polled configuration.

Use :class:`FeatureFlagsRuntimeClient` with :class:`workos.WorkOSClient`.
Use :class:`FeatureFlagsRuntimeClient` with :class:`workos.WorkOSClient` and
:class:`AsyncFeatureFlagsRuntimeClient` with :class:`workos.AsyncWorkOSClient`.
"""

from ._async import AsyncFeatureFlagsRuntimeClient
from ._configuration import (
ConditionDefinition,
FeatureFlagsConfiguration,
Expand All @@ -19,6 +21,7 @@
from ._types import FlagsChangedEvent, RuntimeStatus

__all__ = [
"AsyncFeatureFlagsRuntimeClient",
"ConditionDefinition",
"EvaluationContext",
"FeatureFlagsConfiguration",
Expand Down
Loading
Loading