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
84 changes: 53 additions & 31 deletions specification.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,57 +325,36 @@
{
"id": "Conditional Requirement 1.7.2.1",
"machine_id": "conditional_requirement_1_7_2_1",
"content": "In addition to `NOT_READY`, `READY`, `STALE`, or `ERROR`, the `provider status` accessor must support possible value `RECONCILING`.",
"RFC 2119 keyword": null,
"content": "In addition to `NOT_READY`, `READY`, `STALE`, `ERROR`, or `FATAL`, the `provider status` accessor MUST support possible value `RECONCILING`.",
"RFC 2119 keyword": "MUST",
"children": []
}
]
},
{
"id": "Requirement 1.7.3",
"machine_id": "requirement_1_7_3",
"content": "The client's `provider status` accessor MUST indicate `READY` if the `initialize` function of the associated provider terminates normally.",
"content": "The client's `provider status` accessor MUST indicate `READY` after the provider emits `PROVIDER_READY` following initialization.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.4",
"machine_id": "requirement_1_7_4",
"content": "The client's `provider status` accessor MUST indicate `ERROR` if the `initialize` function of the associated provider terminates abnormally.",
"content": "The client's `provider status` accessor MUST indicate `ERROR` after the provider emits `PROVIDER_ERROR` following initialization.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.5",
"machine_id": "requirement_1_7_5",
"content": "The client's `provider status` accessor MUST indicate `FATAL` if the `initialize` function of the associated provider terminates abnormally and indicates `error code` `PROVIDER_FATAL`.",
"content": "The client's `provider status` accessor MUST indicate `FATAL` after the provider emits `PROVIDER_ERROR` with error code `PROVIDER_FATAL` following initialization.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.6",
"machine_id": "requirement_1_7_6",
"content": "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `NOT_READY`.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.7",
"machine_id": "requirement_1_7_7",
"content": "The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `FATAL`.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.8",
"machine_id": "requirement_1_7_8",
"content": "Implementations SHOULD propagate the `error code` returned from any provider lifecycle methods.",
"RFC 2119 keyword": "SHOULD",
"children": []
},
{
"id": "Requirement 1.7.9",
"machine_id": "requirement_1_7_9",
"content": "The client's `provider status` accessor MUST indicate `NOT_READY` once the `shutdown` function of the associated provider terminates.",
"RFC 2119 keyword": "MUST",
"children": []
Expand Down Expand Up @@ -593,6 +572,49 @@
"RFC 2119 keyword": "MAY",
"children": []
},
{
"id": "Requirement 2.8.1",
"machine_id": "requirement_2_8_1",
"content": "The provider MUST emit an event to signal each status transition, including transitions resulting from lifecycle methods (`initialize`, `on context changed`) and spontaneous transitions.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 2.8.2",
"machine_id": "requirement_2_8_2",
"content": "The provider MUST emit `PROVIDER_READY` if its `initialize` function terminates normally.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 2.8.3",
"machine_id": "requirement_2_8_3",
"content": "The provider MUST emit `PROVIDER_ERROR` if its `initialize` function terminates abnormally.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 2.8.4",
"machine_id": "requirement_2_8_4",
"content": "The provider MUST emit `PROVIDER_CONTEXT_CHANGED` if its `on context changed` function terminates normally, and `PROVIDER_ERROR` if it terminates abnormally.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Condition 2.8.5",
"machine_id": "condition_2_8_5",
"content": "The provider does not define an `initialize` function.",
"RFC 2119 keyword": null,
"children": [
{
"id": "Conditional Requirement 2.8.5.1",
"machine_id": "conditional_requirement_2_8_5_1",
"content": "The SDK MUST treat such providers as `READY` from registration and MUST run `PROVIDER_READY` handlers on their behalf.",
"RFC 2119 keyword": "MUST",
"children": []
}
]
},
{
"id": "Requirement 3.1.1",
"machine_id": "requirement_3_1_1",
Expand Down Expand Up @@ -1013,8 +1035,8 @@
{
"id": "Requirement 5.1.1",
"machine_id": "requirement_5_1_1",
"content": "The `provider` MAY define a mechanism for signaling the occurrence of one of a set of events, including `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_CONFIGURATION_CHANGED` and `PROVIDER_STALE`, with a `provider event details` payload.",
"RFC 2119 keyword": "MAY",
"content": "The `feature provider` interface MUST define a mechanism for signaling the occurrence of one of a set of events, including `PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_CONFIGURATION_CHANGED`, `PROVIDER_STALE`, `PROVIDER_RECONCILING`, and `PROVIDER_CONTEXT_CHANGED`, with a `provider event details` payload.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
Expand Down Expand Up @@ -1097,14 +1119,14 @@
{
"id": "Requirement 5.3.1",
"machine_id": "requirement_5_3_1",
"content": "If the provider's `initialize` function terminates normally, `PROVIDER_READY` handlers MUST run.",
"content": "When the provider emits `PROVIDER_READY`, associated `PROVIDER_READY` handlers MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 5.3.2",
"machine_id": "requirement_5_3_2",
"content": "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.",
"content": "When the provider emits `PROVIDER_ERROR`, associated `PROVIDER_ERROR` handlers MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
},
Expand Down Expand Up @@ -1147,7 +1169,7 @@
{
"id": "Requirement 5.3.5",
"machine_id": "requirement_5_3_5",
"content": "If the provider emits an event, the value of the client's `provider status` MUST be updated to the status associated with that event **before** the SDK invokes any event handlers for that event, so that handlers observe a consistent status.",
"content": "When a provider emits an event, the SDK MUST update the `provider status` to the status associated with that event **before** invoking any event handlers for that event, so that handlers observe a consistent status.",
"RFC 2119 keyword": "MUST",
"children": []
},
Expand Down
1 change: 1 addition & 0 deletions specification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ sidebar_position: 0
- [Appendix B: Gherkin Suites](./appendix-b-gherkin-suites.md)
- [Appendix C: OFREP](./appendix-c/index.md)
- [Appendix D: Observability](./appendix-d-observability.md)
- [Appendix E: Migrations](./appendix-e-migrations.md)

## Conformance

Expand Down
76 changes: 76 additions & 0 deletions specification/appendix-e-migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
id: appendix-e
title: "Appendix E: Migrations"
description: Migration guidance for breaking spec changes
sidebar_position: 6
---

# Appendix E: Migrations

This appendix provides non-normative guidance for provider authors and SDK authors on migrating to new or changed specification requirements.

## Provider Lifecycle Event Emission

### Background

Prior to `v0.9.0`, the SDK emitted synthetic lifecycle events (`PROVIDER_READY`, `PROVIDER_ERROR`) on behalf of providers after lifecycle methods (`initialize`, `shutdown`, `on context changed`) returned.
This created a race condition in multi-threaded SDKs: the provider could emit events from background threads concurrently with SDK-emitted synthetic events, resulting in incorrect event ordering and inconsistent status.

The spec now requires providers to emit their own lifecycle events.
The SDK derives provider status entirely from the provider's event stream (see [provider status](./sections/02-providers.md#28-provider-status), [requirement 5.3.5](./sections/05-events.md#requirement-535)).

### For provider authors

Providers must now emit events for all state transitions, including those resulting from lifecycle methods:

- `PROVIDER_READY` after successful initialization
- `PROVIDER_ERROR` after failed initialization (with appropriate error code)
- `PROVIDER_RECONCILING` when beginning context reconciliation (static-context paradigm)
- `PROVIDER_CONTEXT_CHANGED` after successful context reconciliation (static-context paradigm)
- `PROVIDER_ERROR` after failed context reconciliation (static-context paradigm)

To signal to the SDK that your provider emits its own lifecycle events, implement the opt-in marker defined by the SDK (e.g. an interface, boolean property, or type-level tag).

Providers that do not implement this marker will continue to work via the SDK's legacy compatibility path (see below).

Providers with no `initialize` function are handled per [Condition 2.8.5](./sections/02-providers.md#condition-285) and require no changes.

#### Common migration patterns

**Blocking initialization.**
If `initialize` performs setup synchronously and returns when ready, emit `PROVIDER_READY` immediately before returning (or `PROVIDER_ERROR` before throwing).

**Event-driven initialization.**
If setup completes asynchronously (e.g. the underlying vendor SDK signals readiness via its own event), forward that signal as `PROVIDER_READY`.
`initialize` may return before the event is emitted; the SDK treats the return as a synchronization signal only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recommendation looks like it may break "set provider and wait" methods? What does "synchronization signal" mean?


**Context reconciliation.**
The provider now owns the coalescing behavior previously handled by the SDK.
If `on context changed` may be invoked simultaneously or in quick succession, emit `PROVIDER_CONTEXT_CHANGED` (or `PROVIDER_ERROR`) only after the last reentrant invocation terminates.

#### Anti-patterns

- Do not emit `PROVIDER_READY` before initialization work is complete.
- Do not rely on `initialize`'s return being observed by the SDK for status transitions; it is a synchronization signal only.

### For SDK authors

SDKs must detect whether a provider emits its own lifecycle events, via an opt-in marker (e.g. an interface, boolean property, or type-level tag).

- **Marker present:** the SDK does not emit synthetic lifecycle events. It derives status solely from the provider's event stream.
- **Marker absent (legacy):** the SDK emits synthetic lifecycle events after lifecycle methods return, as before. This path is deprecated.

The legacy path should be deprecated in the release that introduces the marker, with removal targeted for the next major version.
SDK authors should update any first-party providers and provider base classes to emit their own lifecycle events.
Comment thread
toddbaert marked this conversation as resolved.

#### Legacy path behavior

For providers without the marker, the SDK preserves pre-`v0.9.0` behavior:

- Emit `PROVIDER_READY` after `initialize` returns normally.
- Emit `PROVIDER_ERROR` (with the returned error code, if any) after `initialize` returns abnormally.
- For static-context providers: coalesce reentrant `on context changed` invocations and emit `PROVIDER_CONTEXT_CHANGED` or `PROVIDER_ERROR` after the last one terminates.

If a legacy provider also emits its own events, the SDK processes them per [requirement 5.3.5](./sections/05-events.md#requirement-535); duplicates are possible in this mixed mode and are expected legacy behavior.

SDKs should log a deprecation warning on registration of a legacy provider.
12 changes: 6 additions & 6 deletions specification/assets/gherkin/evaluation_v2.feature
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ Feature: Flag Evaluations - Complete OpenFeature Specification Coverage
| key | type | default |
| boolean-flag | Object | {\"a\": 1} |

# Spec 1.7.6: Testing PROVIDER_NOT_READY error when provider isn't initialized
# Testing: client must short-circuit and return error when provider not ready
# Spec 2.2.7: Testing PROVIDER_NOT_READY error when provider isn't initialized
# Testing: provider indicates error abnormally; client returns default per 1.4.10
# Implicitly tests: 1.4.10 (client never throws exceptions - returns default instead)
@provider-status @spec-1.7.6 @spec-1.4.10
@provider-status @spec-2.2.7 @spec-1.4.10
Scenario Outline: Provider not ready error
Given a not ready provider
And a <type>-flag with key "<key>" and a fallback value "<default>"
Expand Down Expand Up @@ -222,10 +222,10 @@ Feature: Flag Evaluations - Complete OpenFeature Specification Coverage
| key | type | default |
| object-flag | Object | {\"a\": 1} |

# Spec 1.7.7: Testing PROVIDER_FATAL error when provider is in fatal state
# Testing: client must short-circuit and return error when provider is fatal
# Spec 2.2.7: Testing PROVIDER_FATAL error when provider is in fatal state
# Testing: provider indicates error abnormally; client returns default per 1.4.10
# Implicitly tests: 1.4.10 (client never throws exceptions - returns default instead), 1.7.5 (FATAL status)
@provider-status @provider-status-fatal @spec-1.7.7 @spec-1.4.10 @spec-1.7.5
@provider-status @provider-status-fatal @spec-2.2.7 @spec-1.4.10 @spec-1.7.5
Scenario Outline: Provider in fatal state error
Given a fatal provider
And a <type>-flag with key "<key>" and a fallback value "<default>"
Expand Down
45 changes: 10 additions & 35 deletions specification/sections/01-flag-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,9 +476,9 @@ stateDiagram-v2

> The `client` **MUST** define a `provider status` accessor which indicates the readiness of the associated provider, with possible values `NOT_READY`, `READY`, `STALE`, `ERROR`, or `FATAL`.

The SDK at all times maintains an up-to-date state corresponding to the success/failure of the last lifecycle method (`initialize`, `shutdown`, `on context change`) or emitted event.
The SDK derives provider status from events emitted by the provider and keeps it up-to-date as events are received.

see [provider status](../types.md#provider-status)
see [provider status](../types.md#provider-status), [provider events](./05-events.md#51-provider-events)

#### Condition 1.7.2

Expand All @@ -490,59 +490,34 @@ see: [static-context paradigm](../glossary.md#static-context-paradigm)

##### Conditional Requirement 1.7.2.1

> In addition to `NOT_READY`, `READY`, `STALE`, or `ERROR`, the `provider status` accessor must support possible value `RECONCILING`.
> In addition to `NOT_READY`, `READY`, `STALE`, `ERROR`, or `FATAL`, the `provider status` accessor **MUST** support possible value `RECONCILING`.

In the static context paradigm, the implementation must define a `provider status` indicating that a provider is reconciling its internal state due to a context change.

#### Requirement 1.7.3

> The client's `provider status` accessor **MUST** indicate `READY` if the `initialize` function of the associated provider terminates normally.
> The client's `provider status` accessor **MUST** indicate `READY` after the provider emits `PROVIDER_READY` following initialization.

Once the provider has initialized, the `provider status` should indicate the provider is ready to be used to evaluate flags.
see: [provider status requirements](./02-providers.md#28-provider-status), [event-status mapping](./05-events.md#requirement-535)

#### Requirement 1.7.4

> The client's `provider status` accessor **MUST** indicate `ERROR` if the `initialize` function of the associated provider terminates abnormally.
> The client's `provider status` accessor **MUST** indicate `ERROR` after the provider emits `PROVIDER_ERROR` following initialization.

If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.
see: [provider status requirements](./02-providers.md#28-provider-status), [event-status mapping](./05-events.md#requirement-535)

#### Requirement 1.7.5

> The client's `provider status` accessor **MUST** indicate `FATAL` if the `initialize` function of the associated provider terminates abnormally and indicates `error code` `PROVIDER_FATAL`.
> The client's `provider status` accessor **MUST** indicate `FATAL` after the provider emits `PROVIDER_ERROR` with error code `PROVIDER_FATAL` following initialization.

If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.
see: [provider status requirements](./02-providers.md#28-provider-status), [event-status mapping](./05-events.md#requirement-535)

#### Requirement 1.7.6

> The client **MUST** default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `NOT_READY`.

The client defaults and returns the `PROVIDER_NOT_READY` `error code` if evaluation is attempted before the provider is initialized (the provider is still in a `NOT_READY` state).
The SDK avoids calling the provider's resolver functions entirely ("short-circuits") if the provider is in this state.

see: [error codes](../types.md#error-code), [flag value resolution](./02-providers.md#22-flag-value-resolution)

#### Requirement 1.7.7

> The client **MUST** default, run error hooks, and indicate an error if flag resolution is attempted while the provider is in `FATAL`.

The client defaults and returns the `PROVIDER_FATAL` `error code` if evaluation is attempted after the provider has transitioned to an irrecoverable error state.
The SDK avoids calling the provider's resolver functions entirely ("short-circuits") if the provider is in this state.

see: [error codes](../types.md#error-code), [flag value resolution](./02-providers.md#22-flag-value-resolution)
Comment on lines -517 to -531

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have the client defaults case on evaluation during NOT_READY/FATAL defined somewhere else?
I guess we'll need to drop the "short-circuit" parts here but just want to make sure we keep the definitions on the defaults.

We could consider moving this into 1.4 alongside 1.4.10, without the short circuit of course.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this inherently becomes a provider requirement; it's basically an extension of 2.2.7. We need to get rid of the short-circuit in the SDK because it re-introduces the race (the SDK has to check state to short-circuit, and that state can lag the provider's actual state). The cleanest fix is pushing the responsibility down to the provider, which knows its own state without lag.

I'd lean toward keeping this in section 2 rather than 1.4. The provider is the only party that can answer "am I ready?" without a race, so it makes sense to put the obligation there. We could either add a non-normative note to 2.2.7 or add a new requirement for it. WDYT?


#### Requirement 1.7.8

> Implementations **SHOULD** propagate the `error code` returned from any provider lifecycle methods.

The SDK ensures that if the provider's lifecycle methods terminate with an `error code`, that error code is included in any associated error events and returned/thrown errors/exceptions.

see: [error codes](../types.md#error-code)

#### Requirement 1.7.9

> The client's `provider status` accessor **MUST** indicate `NOT_READY` once the `shutdown` function of the associated provider terminates.

Regardless of the success of the provider's `shutdown` function, the `provider status` should convey the provider is no longer ready to use once the shutdown function terminates.
Unlike other status transitions, this shutdown is inferred by the SDK; because the SDK initiates the shutdown call, no event from the provider is required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly related to the change here but not having a "not ready" event creates an asymmetry where users can't be notified when a provider becomes not ready. If application author subscribes too all events, they'd still not have the up-to-date status of the provider. #366


### 1.8. Isolated API Instances

Expand Down
Loading
Loading