diff --git a/specification.json b/specification.json index 35ce7efc..8362a3a8 100644 --- a/specification.json +++ b/specification.json @@ -17,7 +17,7 @@ { "id": "Requirement 1.1.2.2", "machine_id": "requirement_1_1_2_2", - "content": "The `provider mutator` function MUST invoke the `initialize` function on the newly registered provider before using it to resolve flag values.", + "content": "The `provider mutator` function MUST invoke the `initialize` function on the newly registered provider before using it to resolve flag values, supplying the bound `domain`, if any.", "RFC 2119 keyword": "MUST", "children": [] }, @@ -70,6 +70,21 @@ "RFC 2119 keyword": "MUST NOT", "children": [] }, + { + "id": "Condition 1.1.8", + "machine_id": "condition_1_1_8", + "content": "The `provider` declares that it is `domain-scoped`.", + "RFC 2119 keyword": null, + "children": [ + { + "id": "Conditional Requirement 1.1.8.1", + "machine_id": "conditional_requirement_1_1_8_1", + "content": "The `provider mutator` MUST NOT bind a `domain-scoped` provider instance to more than one `domain`, rejecting any attempt to bind an already-bound instance to an additional `domain`.", + "RFC 2119 keyword": "MUST NOT", + "children": [] + } + ] + }, { "id": "Requirement 1.2.1", "machine_id": "requirement_1_2_1", @@ -510,7 +525,7 @@ { "id": "Requirement 2.4.1", "machine_id": "requirement_2_4_1", - "content": "The `provider` MAY define an initialization function which accepts the global `evaluation context` as an argument and performs initialization logic relevant to the provider.", + "content": "The `provider` MAY define an initialization function which accepts the global `evaluation context` and an optional bound `domain`, which performs initialization logic relevant to the provider.", "RFC 2119 keyword": "MAY", "children": [] }, @@ -529,6 +544,20 @@ } ] }, + { + "id": "Requirement 2.4.3", + "machine_id": "requirement_2_4_3", + "content": "The `provider` MAY declare that it is `domain-scoped`, indicating that it maintains state specific to a single `domain`, such as a persistent cache, that cannot be shared across `domains`.", + "RFC 2119 keyword": "MAY", + "children": [] + }, + { + "id": "Requirement 2.4.4", + "machine_id": "requirement_2_4_4", + "content": "A `provider` that declares itself `domain-scoped` MUST accept the bound `domain` during initialization.", + "RFC 2119 keyword": "MUST", + "children": [] + }, { "id": "Requirement 2.5.1", "machine_id": "requirement_2_5_1", diff --git a/specification/sections/01-flag-evaluation.md b/specification/sections/01-flag-evaluation.md index cb47dffe..88ff29a2 100644 --- a/specification/sections/01-flag-evaluation.md +++ b/specification/sections/01-flag-evaluation.md @@ -38,7 +38,7 @@ See [provider](./02-providers.md), [creating clients](#creating-clients) for det #### Requirement 1.1.2.2 -> The `provider mutator` function **MUST** invoke the `initialize` function on the newly registered provider before using it to resolve flag values. +> The `provider mutator` function **MUST** invoke the `initialize` function on the newly registered provider before using it to resolve flag values, supplying the bound `domain`, if any. Application authors can await the newly set `provider's` readiness using the `PROVIDER_READY` event. Provider instances which are already active (because they have been bound to another `domain` or otherwise) need not be initialized again. @@ -147,6 +147,19 @@ See [setting a provider](#setting-a-provider), [domain](../glossary.md#domain) f Clients may be created in critical code paths, and even per-request in server-side HTTP contexts. Therefore, in keeping with the principle that OpenFeature should never cause abnormal execution of the first party application, this function should never throw. Abnormal execution in initialization should instead occur during provider registration. +#### Condition 1.1.8 + +> The `provider` declares that it is `domain-scoped`. + +see: [Requirement 2.4.3](./02-providers.md#requirement-243) + +##### Conditional Requirement 1.1.8.1 + +> The `provider mutator` **MUST NOT** bind a `domain-scoped` provider instance to more than one `domain`, rejecting any attempt to bind an already-bound instance to an additional `domain`. + +A `domain-scoped` provider keys per-`domain` state on the single `domain` supplied to its `initialize` function. +Rejection should occur in a manner idiomatic to the implementation language (throwing, returning an error, etc.), and leaves any existing binding intact. + ### 1.2. Client Usage #### Requirement 1.2.1 @@ -577,7 +590,7 @@ import { createIsolatedOpenFeatureAPI } from '@openfeature/web-sdk/isolated'; > A `provider` instance **SHOULD NOT** be registered with more than one `API` instance simultaneously. Because the `API` instance manages the [lifecycle](./02-providers.md) of its associated providers (including initialization, shutdown, and event handling), binding a `provider` to more than one `API` instance could result in undefined behavior. -A `provider` instance can be registered with multiple `domains` within a single `API` instance. +A `provider` instance can be registered with multiple `domains` within a single `API` instance, unless it declares itself `domain-scoped` (see [Requirement 2.4.3](./02-providers.md#requirement-243)), in which case it can be bound to at most one `domain`. When a provider is no longer associated with an `API` instance, it can be registered to another. See [setting a provider](#setting-a-provider), [domain](../glossary.md#domain) for details. diff --git a/specification/sections/02-providers.md b/specification/sections/02-providers.md index baa7d87e..6a5e2ac1 100644 --- a/specification/sections/02-providers.md +++ b/specification/sections/02-providers.md @@ -170,19 +170,25 @@ class MyProvider implements Provider { #### Requirement 2.4.1 -> The `provider` **MAY** define an initialization function which accepts the global `evaluation context` as an argument and performs initialization logic relevant to the provider. +> The `provider` **MAY** define an initialization function which accepts the global `evaluation context` and an optional bound `domain`, which performs initialization logic relevant to the provider. Many feature flag frameworks or SDKs require some initialization before they can be used. They might require the completion of an HTTP request, establishing persistent connections, or starting timers or worker threads. The initialization function is an ideal place for such logic. +The `domain` the provider is registered under is also supplied, allowing the provider to scope domain-specific behavior, such as partitioning a persistent cache, so that multiple providers sharing the same storage do not collide. +A `provider` instance is initialized only once, even when bound to multiple `domains`; in that case the `domain` supplied is the one under which it was first registered. +A provider that maintains `domain`-specific state can instead declare itself `domain-scoped` (see [Requirement 2.4.3](#requirement-243)), in which case it is restricted to a single `domain` and this ambiguity does not arise. +The default provider, which is not bound to a domain, is initialized without one. + ```java // MyProvider implementation of the initialize function defined in Provider class MyProvider implements Provider { //... - // the global context is passed to the initialization function - void initialize(EvaluationContext initialContext) { + // the global context and the bound domain are passed to the initialization function + void initialize(EvaluationContext initialContext, @Nullable String domain) { + this.domain = domain; /* A hypothetical initialization function: make an initial call doing some bulk initial evaluation, start a worker to do periodic updates */ @@ -207,6 +213,21 @@ If the error is irrecoverable (perhaps due to bad credentials or invalid configu see: [error codes](../types.md#error-code) +#### Requirement 2.4.3 + +> The `provider` **MAY** declare that it is `domain-scoped`, indicating that it maintains state specific to a single `domain`, such as a persistent cache, that cannot be shared across `domains`. + +Most providers are stateless with respect to their `domain` and can safely back multiple `domains` from a single instance. +Providers that persist or cache `domain`-specific data need a stable, unambiguous `domain` to key that state on. +By declaring itself `domain-scoped`, such a provider signals that the `API` must bind it to at most one `domain` (see [Requirement 1.1.8](./01-flag-evaluation.md#condition-118)), guaranteeing the `domain` supplied to `initialize` is the only one the instance will ever serve. + +#### Requirement 2.4.4 + +> A `provider` that declares itself `domain-scoped` **MUST** accept the bound `domain` during initialization. + +A `domain-scoped` declaration is only meaningful if the provider consumes the `domain` it is given to scope its state. +This is a contract on the provider; implementations may not be able to detect or reject a violation automatically, so it is not guaranteed to surface as a runtime error. + ### 2.5. Shutdown [![hardening](https://img.shields.io/static/v1?label=Status&message=hardening&color=yellow)](https://github.com/open-feature/spec/tree/main/specification#hardening)