Skip to content

Commit ca544a4

Browse files
committed
fixup: improve readme and use all the settings everywhere
Signed-off-by: Simon Schrottner <[email protected]>
1 parent c7f81b6 commit ca544a4

File tree

19 files changed

+317
-139
lines changed

19 files changed

+317
-139
lines changed

.github/workflows/build.yml

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,6 @@ jobs:
2727
- "hooks/openfeature-hooks-opentelemetry"
2828
- "providers/openfeature-provider-flagd"
2929

30-
services:
31-
# flagd-testbed for flagd RPC provider e2e tests
32-
flagd:
33-
image: ghcr.io/open-feature/flagd-testbed:v0.5.4
34-
ports:
35-
- 8013:8013
36-
# flagd-testbed for flagd RPC provider reconnect e2e tests
37-
flagd-unstable:
38-
image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.4
39-
ports:
40-
- 8014:8013
41-
# sync-testbed for flagd in-process provider e2e tests
42-
sync:
43-
image: ghcr.io/open-feature/sync-testbed:v0.5.4
44-
ports:
45-
- 9090:9090
46-
# sync-testbed for flagd in-process provider reconnect e2e tests
47-
sync-unstable:
48-
image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.4
49-
ports:
50-
- 9091:9090
51-
5230
steps:
5331
- uses: actions/checkout@v4
5432
with:

CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@ We use `pytest` for our unit testing, making use of `parametrized` to inject cas
2828

2929
The Flagd provider utilizes the [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) to validate against a live, seeded Flagd instance.
3030

31-
To run the integration tests:
31+
To run the integration tests you need to have a container runtime, like docker, ranger, etc. installed.
3232

3333
```bash
34-
cd providers/openfeature-provider-flagd
35-
docker-compose up -d # this runs the flagd sidecars
3634
hatch run test
3735
```
3836

providers/openfeature-provider-flagd/README.md

Lines changed: 126 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# flagd Provider for OpenFeature
22

3-
This provider is designed to use flagd's [evaluation protocol](https://github.com/open-feature/schemas/blob/main/protobuf/schema/v1/schema.proto).
3+
This provider is designed to use
4+
flagd's [evaluation protocol](https://github.com/open-feature/schemas/blob/main/protobuf/schema/v1/schema.proto).
45

56
## Installation
67

@@ -10,6 +11,14 @@ pip install openfeature-provider-flagd
1011

1112
## Configuration and Usage
1213

14+
The flagd provider can operate in two modes: [RPC](#remote-resolver-rpc) (evaluation takes place in flagd, via gRPC calls) or [in-process](#in-process-resolver) (evaluation takes place in-process, with the provider getting a ruleset from a compliant sync-source).
15+
16+
### Remote resolver (RPC)
17+
18+
This is the default mode of operation of the provider.
19+
In this mode, `FlagdProvider` communicates with [flagd](https://github.com/open-feature/flagd) via the gRPC protocol.
20+
Flag evaluations take place remotely at the connected flagd instance.
21+
1322
Instantiate a new FlagdProvider instance and configure the OpenFeature SDK to use it:
1423

1524
```python
@@ -19,8 +28,11 @@ from openfeature.contrib.provider.flagd import FlagdProvider
1928
api.set_provider(FlagdProvider())
2029
```
2130

31+
### In-process resolver
2232

23-
To use in-process evaluation with flagd gRPC sync service:
33+
This mode performs flag evaluations locally (in-process). Flag configurations for evaluation are obtained via gRPC protocol using [sync protobuf schema](https://buf.build/open-feature/flagd/file/main:sync/v1/sync_service.proto) service definition.
34+
35+
Consider the following example to create a `FlagdProvider` with in-process evaluations,
2436

2537
```python
2638
from openfeature import api
@@ -32,7 +44,20 @@ api.set_provider(FlagdProvider(
3244
))
3345
```
3446

35-
To use in-process evaluation in offline mode with a file as source:
47+
In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flags.json).
48+
49+
<!--
50+
#### Sync-metadata
51+
52+
To support the injection of contextual data configured in flagd for in-process evaluation, the provider exposes a `getSyncMetadata` accessor which provides the most recent value returned by the [GetMetadata RPC](https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata).
53+
The value is updated with every (re)connection to the sync implementation.
54+
This can be used to enrich evaluations with such data.
55+
If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
56+
-->
57+
#### Offline mode
58+
59+
In-process resolvers can also work in an offline mode.
60+
To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.
3661

3762
```python
3863
from openfeature import api
@@ -45,21 +70,108 @@ api.set_provider(FlagdProvider(
4570
))
4671
```
4772

73+
Provider will attempt to detect file changes using polling.
74+
Polling happens at 5 second intervals and this is currently unconfigurable.
75+
This mode is useful for local development, tests and offline applications.
76+
4877
### Configuration options
4978

5079
The default options can be defined in the FlagdProvider constructor.
5180

52-
| Option name | Environment Variable Name | Type & Values | Default |
53-
|-------------------------------|-------------------------------------|----------------|-----------|
54-
| resolver_type | FLAGD_RESOLVER_TYPE | enum | grpc |
55-
| host | FLAGD_HOST | str | localhost |
56-
| port | FLAGD_PORT | int | 8013 |
57-
| tls | FLAGD_TLS | bool | false |
58-
| timeout | | int | 5 |
59-
| retry_backoff_seconds | FLAGD_RETRY_BACKOFF_SECONDS | float | 2.0 |
60-
| selector | FLAGD_SELECTOR | str | None |
61-
| offline_flag_source_path | FLAGD_OFFLINE_FLAG_SOURCE_PATH | str | None |
62-
| offline_poll_interval_seconds | FLAGD_OFFLINE_POLL_INTERVAL_SECONDS | float | 1.0 |
81+
| Option name | Environment variable name | Type & Values | Default | Compatible resolver |
82+
|--------------------------|--------------------------------|---------------|-----------|---------------------|
83+
| resolver_type | FLAGD_RESOLVER | enum | grpc | |
84+
| host | FLAGD_HOST | str | localhost | rpc & in-process |
85+
| port | FLAGD_PORT | int | 8013 | rpc & in-process |
86+
| tls | FLAGD_TLS | bool | false | rpc & in-process |
87+
| deadline | FLAGD_DEADLINE_MS | int | 500 | rpc & in-process |
88+
| stream_deadline_ms | FLAGD_STREAM_DEADLINE_MS | int | 600000 | rpc & in-process |
89+
| keep_alive_time | FLAGD_KEEP_ALIVE_TIME_MS | int | 0 | rpc & in-process |
90+
| selector | FLAGD_SOURCE_SELECTOR | str | null | in-process |
91+
| cache_type | FLAGD_CACHE | enum | lru | rpc |
92+
| max_cache_size | FLAGD_MAX_CACHE_SIZE | int | 1000 | rpc |
93+
| retry_backoff_ms | FLAGD_RETRY_BACKOFF_MS | int | 1000 | rpc |
94+
| offline_flag_source_path | FLAGD_OFFLINE_FLAG_SOURCE_PATH | str | null | in-process |
95+
96+
<!--
97+
| target_uri | FLAGD_TARGET_URI | str | null | rpc & in-process |
98+
| socket_path | FLAGD_SOCKET_PATH | str | null | rpc & in-process |
99+
| cert_path | FLAGD_SERVER_CERT_PATH | str | null | rpc & in-process |
100+
| max_event_stream_retries | FLAGD_MAX_EVENT_STREAM_RETRIES | int | 5 | rpc |
101+
-->
102+
103+
> [!NOTE]
104+
> Some configurations are only applicable for RPC resolver.
105+
106+
107+
<!--
108+
### Unix socket support
109+
110+
Unix socket communication with flagd is facilitated by usaging of the linux-native `epoll` library on `linux-x86_64`
111+
only (ARM support is pending the release of `netty-transport-native-epoll` v5).
112+
Unix sockets are not supported on other platforms or architectures.
113+
-->
114+
115+
### Reconnection
116+
117+
Reconnection is supported by the underlying gRPC connections.
118+
If the connection to flagd is lost, it will reconnect automatically.
119+
A failure to connect will result in an [error event](https://openfeature.dev/docs/reference/concepts/events#provider_error) from the provider, though it will attempt to reconnect
120+
indefinitely.
121+
122+
### Deadlines
123+
124+
Deadlines are used to define how long the provider waits to complete initialization or flag evaluations.
125+
They behave differently based on the resolver type.
126+
127+
#### Deadlines with Remote resolver (RPC)
128+
129+
If the remote evaluation call is not completed within this deadline, the gRPC call is terminated with the error `DEADLINE_EXCEEDED`
130+
and the evaluation will default.
131+
132+
#### Deadlines with In-process resolver
133+
134+
In-process resolver with remote evaluation uses the `deadline` for synchronous gRPC calls to fetch metadata from flagd as part of its initialization process.
135+
If fetching metadata fails within this deadline, the provider will try to reconnect.
136+
The `streamDeadlineMs` defines a deadline for the streaming connection that listens to flag configuration updates from
137+
flagd. After the deadline is exceeded, the provider closes the gRPC stream and will attempt to reconnect.
138+
139+
In-process resolver with offline evaluation uses the `deadline` for file reads to fetch flag definitions.
140+
If the provider cannot open and read the file within this deadline, the provider will default the evaluation.
141+
142+
143+
### TLS
144+
145+
TLS is available in situations where flagd is running on another host.
146+
147+
<!--
148+
You may optionally supply an X.509 certificate in PEM format. Otherwise, the default certificate store will be used.
149+
150+
```java
151+
FlagdProvider flagdProvider = new FlagdProvider(
152+
FlagdOptions.builder()
153+
.host("myflagdhost")
154+
.tls(true) // use TLS
155+
.certPath("etc/cert/ca.crt") // PEM cert
156+
.build());
157+
```
158+
-->
159+
160+
### Caching (RPC only)
161+
162+
> [!NOTE]
163+
> The in-process resolver does not benefit from caching since all evaluations are done locally and do not involve I/O.
164+
165+
The provider attempts to establish a connection to flagd's event stream (up to 5 times by default).
166+
If the connection is successful and caching is enabled, each flag returned with the reason `STATIC` is cached until an event is received
167+
concerning the cached flag (at which point it is removed from the cache).
168+
169+
On invocation of a flag evaluation (if caching is available), an attempt is made to retrieve the entry from the cache, if
170+
found the flag is returned with the reason `CACHED`.
171+
172+
By default, the provider is configured to
173+
use [least recently used (lru)](https://pypi.org/project/cachebox/)
174+
caching with up to 1000 entries.
63175

64176
## License
65177

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ packages = ["src/openfeature"]
7373
[tool.coverage.run]
7474
omit = [
7575
# exclude generated files
76-
"src/openfeature/contrib/provider/flagd/proto/*",
76+
"src/schemas/*",
7777
"tests/**",
7878
]

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22
import typing
33
from enum import Enum
44

5-
ENV_VAR_MAX_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE"
5+
ENV_VAR_MAX_EVENT_STREAM_RETRIES = "FLAGD_MAX_EVENT_STREAM_RETRIES"
6+
7+
ENV_VAR_KEEP_ALIVE_TIME_MS = "FLAGD_KEEP_ALIVE_TIME_MS"
8+
9+
ENV_VAR_DEADLINE_MS = "FLAGD_DEADLINE_MS"
10+
ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
11+
612
ENV_VAR_CACHE_TYPE = "FLAGD_CACHE_TYPE"
7-
ENV_VAR_OFFLINE_POLL_INTERVAL_SECONDS = "FLAGD_OFFLINE_POLL_INTERVAL_SECONDS"
13+
ENV_VAR_HOST = "FLAGD_HOST"
14+
ENV_VAR_MAX_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE"
815
ENV_VAR_OFFLINE_FLAG_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH"
16+
ENV_VAR_OFFLINE_POLL_INTERVAL_SECONDS = "FLAGD_OFFLINE_POLL_INTERVAL_SECONDS"
917
ENV_VAR_PORT = "FLAGD_PORT"
1018
ENV_VAR_RESOLVER_TYPE = "FLAGD_RESOLVER_TYPE"
19+
ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS"
20+
ENV_VAR_SELECTOR = "FLAGD_SELECTOR"
1121
ENV_VAR_TLS = "FLAGD_TLS"
12-
ENV_VAR_HOST = "FLAGD_HOST"
1322

1423
T = typing.TypeVar("T")
1524

@@ -43,27 +52,28 @@ def __init__( # noqa: PLR0913
4352
host: typing.Optional[str] = None,
4453
port: typing.Optional[int] = None,
4554
tls: typing.Optional[bool] = None,
46-
timeout: typing.Optional[int] = None,
47-
retry_backoff_seconds: typing.Optional[float] = None,
4855
selector: typing.Optional[str] = None,
4956
resolver_type: typing.Optional[ResolverType] = None,
5057
offline_flag_source_path: typing.Optional[str] = None,
51-
offline_poll_interval_seconds: typing.Optional[float] = None,
58+
retry_backoff_ms: typing.Optional[int] = None,
5259
cache_type: typing.Optional[CacheType] = None,
5360
max_cache_size: typing.Optional[int] = None,
61+
deadline: typing.Optional[int] = None,
62+
stream_deadline_ms: typing.Optional[int] = None,
63+
keep_alive_time: typing.Optional[int] = None,
64+
max_event_stream_retries: typing.Optional[int] = None,
5465
):
5566
self.host = env_or_default(ENV_VAR_HOST, "localhost") if host is None else host
5667
self.tls = (
5768
env_or_default(ENV_VAR_TLS, False, cast=str_to_bool) if tls is None else tls
5869
)
59-
self.timeout = 5 if timeout is None else timeout
60-
self.retry_backoff_seconds: float = (
61-
float(env_or_default("FLAGD_RETRY_BACKOFF_SECONDS", 2.0))
62-
if retry_backoff_seconds is None
63-
else retry_backoff_seconds
70+
self.retry_backoff_ms: int = (
71+
int(env_or_default(ENV_VAR_RETRY_BACKOFF_MS, 1000, cast=int))
72+
if retry_backoff_ms is None
73+
else retry_backoff_ms
6474
)
6575
self.selector = (
66-
env_or_default("FLAGD_SELECTOR", None) if selector is None else selector
76+
env_or_default(ENV_VAR_SELECTOR, None) if selector is None else selector
6777
)
6878
self.resolver_type = (
6979
ResolverType(env_or_default(ENV_VAR_RESOLVER_TYPE, "grpc"))
@@ -72,8 +82,8 @@ def __init__( # noqa: PLR0913
7282
)
7383

7484
default_port = 8013 if self.resolver_type is ResolverType.GRPC else 8015
75-
self.port = (
76-
env_or_default(ENV_VAR_PORT, default_port, cast=int)
85+
self.port: int = (
86+
int(env_or_default(ENV_VAR_PORT, default_port, cast=int))
7787
if port is None
7888
else port
7989
)
@@ -82,20 +92,39 @@ def __init__( # noqa: PLR0913
8292
if offline_flag_source_path is None
8393
else offline_flag_source_path
8494
)
85-
self.offline_poll_interval_seconds = (
86-
float(env_or_default(ENV_VAR_OFFLINE_POLL_INTERVAL_SECONDS, 1.0))
87-
if offline_poll_interval_seconds is None
88-
else offline_poll_interval_seconds
89-
)
9095

9196
self.cache_type = (
92-
CacheType(env_or_default(ENV_VAR_CACHE_TYPE, CacheType.DISABLED))
97+
CacheType(env_or_default(ENV_VAR_CACHE_TYPE, CacheType.LRU))
9398
if cache_type is None
9499
else cache_type
95100
)
96101

97-
self.max_cache_size = (
98-
env_or_default(ENV_VAR_MAX_CACHE_SIZE, 16, cast=int)
102+
self.max_cache_size: int = (
103+
int(env_or_default(ENV_VAR_MAX_CACHE_SIZE, 1000, cast=int))
99104
if max_cache_size is None
100105
else max_cache_size
101106
)
107+
108+
self.deadline: int = (
109+
int(env_or_default(ENV_VAR_DEADLINE_MS, 500, cast=int))
110+
if deadline is None
111+
else deadline
112+
)
113+
114+
self.stream_deadline_ms: int = (
115+
int(env_or_default(ENV_VAR_STREAM_DEADLINE_MS, 600000, cast=int))
116+
if stream_deadline_ms is None
117+
else stream_deadline_ms
118+
)
119+
120+
self.keep_alive_time: int = (
121+
int(env_or_default(ENV_VAR_KEEP_ALIVE_TIME_MS, 0, cast=int))
122+
if keep_alive_time is None
123+
else keep_alive_time
124+
)
125+
126+
self.max_event_stream_retries: int = (
127+
int(env_or_default(ENV_VAR_MAX_EVENT_STREAM_RETRIES, 5, cast=int))
128+
if max_event_stream_retries is None
129+
else max_event_stream_retries
130+
)

0 commit comments

Comments
 (0)