Skip to content

Commit 1c72782

Browse files
committed
feat: implement transaction context
Signed-off-by: Federico Bond <[email protected]>
1 parent 34ac91c commit 1c72782

File tree

5 files changed

+93
-1
lines changed

5 files changed

+93
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ print("Value: " + str(flag_value))
108108
|| [Domains](#domains) | Logically bind clients with providers. |
109109
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
110110
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
111+
| ️️⚠️ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
111112
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
112113

113114
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>

openfeature/api.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
from openfeature.provider import FeatureProvider
1313
from openfeature.provider._registry import provider_registry
1414
from openfeature.provider.metadata import Metadata
15+
from openfeature.transaction_context import (
16+
NoopTransactionContextPropagator,
17+
TransactionContextPropagator,
18+
)
1519

1620
__all__ = [
1721
"get_client",
@@ -26,12 +30,19 @@
2630
"shutdown",
2731
"add_handler",
2832
"remove_handler",
33+
"set_transaction_context_propagator",
34+
"set_transaction_context",
35+
"get_transaction_context",
2936
]
3037

3138
_evaluation_context = EvaluationContext()
3239

3340
_hooks: typing.List[Hook] = []
3441

42+
_transaction_context_propagator: TransactionContextPropagator = (
43+
NoopTransactionContextPropagator()
44+
)
45+
3546

3647
def get_client(
3748
domain: typing.Optional[str] = None, version: typing.Optional[str] = None
@@ -94,3 +105,17 @@ def add_handler(event: ProviderEvent, handler: EventHandler) -> None:
94105

95106
def remove_handler(event: ProviderEvent, handler: EventHandler) -> None:
96107
_event_support.remove_global_handler(event, handler)
108+
109+
110+
def set_transaction_context_propagator(
111+
propagator: TransactionContextPropagator,
112+
) -> None:
113+
_transaction_context_propagator = propagator
114+
115+
116+
def set_transaction_context(context: EvaluationContext) -> None:
117+
_transaction_context_propagator.set_transaction_context(context)
118+
119+
120+
def get_transaction_context() -> EvaluationContext:
121+
return _transaction_context_propagator.get_transaction_context()

openfeature/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,10 @@ def evaluate_flag_details( # noqa: PLR0915
335335
)
336336
invocation_context = invocation_context.merge(ctx2=evaluation_context)
337337

338-
# Requirement 3.2.2 merge: API.context->client.context->invocation.context
338+
# Requirement 3.2.3 merge: API.context->transaction.context->client.context->invocation.context
339339
merged_context = (
340340
api.get_evaluation_context()
341+
.merge(api.get_transaction_context())
341342
.merge(self.context)
342343
.merge(invocation_context)
343344
)

openfeature/transaction_context.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import typing
2+
from contextvars import ContextVar
3+
4+
from openfeature.evaluation_context import EvaluationContext
5+
6+
__all__ = [
7+
"TransactionContextPropagator",
8+
"NoopTransactionContextPropagator",
9+
"ContextVarTransactionContextPropagator",
10+
]
11+
12+
13+
class TransactionContextPropagator(typing.Protocol):
14+
def get_transaction_context(self) -> EvaluationContext: ...
15+
16+
def set_transaction_context(self, context: EvaluationContext) -> None: ...
17+
18+
19+
class NoopTransactionContextPropagator(TransactionContextPropagator):
20+
def get_transaction_context(self) -> EvaluationContext:
21+
return EvaluationContext()
22+
23+
def set_transaction_context(self, context: EvaluationContext) -> None:
24+
pass
25+
26+
27+
class ContextVarTransactionContextPropagator(TransactionContextPropagator):
28+
def __init__(self) -> None:
29+
self._contextvar = ContextVar(
30+
"transaction_context", default=EvaluationContext()
31+
)
32+
33+
def get_transaction_context(self) -> EvaluationContext:
34+
return self._contextvar.get()
35+
36+
def set_transaction_context(self, context: EvaluationContext) -> None:
37+
self._contextvar.set(context)

tests/test_transaction_context.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from concurrent.futures import ThreadPoolExecutor
2+
3+
from openfeature.evaluation_context import EvaluationContext
4+
from openfeature.transaction_context import ContextVarTransactionContextPropagator
5+
6+
7+
def test_contextvar_transaction_context_propagator():
8+
propagator = ContextVarTransactionContextPropagator()
9+
10+
context = propagator.get_transaction_context()
11+
assert isinstance(context, EvaluationContext)
12+
13+
context = EvaluationContext(targeting_key="foo", attributes={"key": "value"})
14+
propagator.set_transaction_context(context)
15+
transaction_context = propagator.get_transaction_context()
16+
17+
assert transaction_context.targeting_key == "foo"
18+
assert transaction_context.attributes == {"key": "value"}
19+
20+
def thread_fn():
21+
thread_context = propagator.get_transaction_context()
22+
assert thread_context.targeting_key is None
23+
assert thread_context.attributes == {}
24+
25+
with ThreadPoolExecutor() as executor:
26+
future = executor.submit(thread_fn)
27+
28+
future.result()

0 commit comments

Comments
 (0)