Skip to content

Commit

Permalink
feat: implement transaction context
Browse files Browse the repository at this point in the history
Signed-off-by: Federico Bond <federicobond@gmail.com>
  • Loading branch information
federicobond committed Apr 25, 2024
1 parent 44f1223 commit c31a2fa
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ print("Value: " + str(flag_value))
|| [Domains](#domains) | Logically bind clients with providers. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ️️⚠️ | [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) |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
Expand Down
25 changes: 25 additions & 0 deletions openfeature/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from openfeature.provider import FeatureProvider
from openfeature.provider._registry import provider_registry
from openfeature.provider.metadata import Metadata
from openfeature.transaction_context import (
NoopTransactionContextPropagator,
TransactionContextPropagator,
)

__all__ = [
"get_client",
Expand All @@ -26,12 +30,19 @@
"shutdown",
"add_handler",
"remove_handler",
"set_transaction_context_propagator",
"set_transaction_context",
"get_transaction_context",
]

_evaluation_context = EvaluationContext()

_hooks: typing.List[Hook] = []

_transaction_context_propagator: TransactionContextPropagator = (
NoopTransactionContextPropagator()
)


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

def remove_handler(event: ProviderEvent, handler: EventHandler) -> None:
_event_support.remove_global_handler(event, handler)


def set_transaction_context_propagator(
propagator: TransactionContextPropagator,
) -> None:
_transaction_context_propagator = propagator

Check warning on line 113 in openfeature/api.py

View check run for this annotation

Codecov / codecov/patch

openfeature/api.py#L113

Added line #L113 was not covered by tests


def set_transaction_context(context: EvaluationContext) -> None:
_transaction_context_propagator.set_transaction_context(context)

Check warning on line 117 in openfeature/api.py

View check run for this annotation

Codecov / codecov/patch

openfeature/api.py#L117

Added line #L117 was not covered by tests


def get_transaction_context() -> EvaluationContext:
return _transaction_context_propagator.get_transaction_context()
3 changes: 2 additions & 1 deletion openfeature/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,10 @@ def evaluate_flag_details( # noqa: PLR0915
)
invocation_context = invocation_context.merge(ctx2=evaluation_context)

# Requirement 3.2.2 merge: API.context->client.context->invocation.context
# Requirement 3.2.3 merge: API.context->transaction.context->client.context->invocation.context
merged_context = (
api.get_evaluation_context()
.merge(api.get_transaction_context())
.merge(self.context)
.merge(invocation_context)
)
Expand Down
37 changes: 37 additions & 0 deletions openfeature/transaction_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import typing
from contextvars import ContextVar

from openfeature.evaluation_context import EvaluationContext

__all__ = [
"TransactionContextPropagator",
"NoopTransactionContextPropagator",
"ContextVarTransactionContextPropagator",
]


class TransactionContextPropagator(typing.Protocol):
def get_transaction_context(self) -> EvaluationContext: ...

def set_transaction_context(self, context: EvaluationContext) -> None: ...


class NoopTransactionContextPropagator(TransactionContextPropagator):
def get_transaction_context(self) -> EvaluationContext:
return EvaluationContext()

def set_transaction_context(self, context: EvaluationContext) -> None:
pass

Check warning on line 24 in openfeature/transaction_context.py

View check run for this annotation

Codecov / codecov/patch

openfeature/transaction_context.py#L24

Added line #L24 was not covered by tests


class ContextVarTransactionContextPropagator(TransactionContextPropagator):
def __init__(self) -> None:
self._contextvar = ContextVar(
"transaction_context", default=EvaluationContext()
)

def get_transaction_context(self) -> EvaluationContext:
return self._contextvar.get()

def set_transaction_context(self, context: EvaluationContext) -> None:
self._contextvar.set(context)
28 changes: 28 additions & 0 deletions tests/test_transaction_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from concurrent.futures import ThreadPoolExecutor

from openfeature.evaluation_context import EvaluationContext
from openfeature.transaction_context import ContextVarTransactionContextPropagator


def test_contextvar_transaction_context_propagator():
propagator = ContextVarTransactionContextPropagator()

context = propagator.get_transaction_context()
assert isinstance(context, EvaluationContext)

context = EvaluationContext(targeting_key="foo", attributes={"key": "value"})
propagator.set_transaction_context(context)
transaction_context = propagator.get_transaction_context()

assert transaction_context.targeting_key == "foo"
assert transaction_context.attributes == {"key": "value"}

def thread_fn():
thread_context = propagator.get_transaction_context()
assert thread_context.targeting_key is None
assert thread_context.attributes == {}

with ThreadPoolExecutor() as executor:
future = executor.submit(thread_fn)

future.result()

0 comments on commit c31a2fa

Please sign in to comment.