Skip to content

Commit

Permalink
feat: internal provider state, new client events (#241)
Browse files Browse the repository at this point in the history
* moves provider state into SDK (SDK now maintains provider
state/lifecycle; provider interface is now "stateless")
* refine semantics around client state when context is
pending/reconciled by using new events/states, instead of STALE
* add PROVIDER_FATAL error code
* add PROVIDER_RECONCILING event (client only)
* add RECONCILING provider status status (client only)

Resolves: #238
---------

Signed-off-by: Todd Baert <[email protected]>
Co-authored-by: Michael Beemer <[email protected]>
Co-authored-by: Lukas Reining <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
Co-authored-by: Fabrizio Demaria <[email protected]>
  • Loading branch information
5 people authored Feb 22, 2024
1 parent a77f18b commit ae286cf
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 113 deletions.
131 changes: 98 additions & 33 deletions specification.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,70 @@
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 1.7.1",
"machine_id": "requirement_1_7_1",
"content": "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`.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Condition 1.7.2",
"machine_id": "condition_1_7_2",
"content": "The implementation uses the static-context paradigm.",
"RFC 2119 keyword": null,
"children": [
{
"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,
"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.",
"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.",
"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`.",
"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 `PROVIDER_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 2.1.1",
"machine_id": "requirement_2_1_1",
Expand Down Expand Up @@ -409,32 +473,19 @@
"children": []
},
{
"id": "Requirement 2.4.2",
"machine_id": "requirement_2_4_2",
"content": "The `provider` MAY define a `status` field/accessor which indicates the readiness of the provider, with possible values `NOT_READY`, `READY`, `STALE`, or `ERROR`.",
"RFC 2119 keyword": "MAY",
"children": []
},
{
"id": "Requirement 2.4.3",
"machine_id": "requirement_2_4_3",
"content": "The provider MUST set its `status` field/accessor to `READY` if its `initialize` function terminates normally.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 2.4.4",
"machine_id": "requirement_2_4_4",
"content": "The provider MUST set its `status` field to `ERROR` if its `initialize` function terminates abnormally.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 2.4.5",
"machine_id": "requirement_2_4_5",
"content": "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready.",
"RFC 2119 keyword": "SHOULD",
"children": []
"id": "Condition 2.4.2",
"machine_id": "condition_2_4_2",
"content": "The provider defines an `initialize` function.",
"RFC 2119 keyword": null,
"children": [
{
"id": "Conditional Requirement 2.4.2.1",
"machine_id": "conditional_requirement_2_4_2_1",
"content": "If the provider's `initialize` function fails to render the provider ready to evaluate flags, it SHOULD abnormally terminate.",
"RFC 2119 keyword": "SHOULD",
"children": []
}
]
},
{
"id": "Requirement 2.5.1",
Expand All @@ -453,7 +504,7 @@
{
"id": "Requirement 2.6.1",
"machine_id": "requirement_2_6_1",
"content": "The provider MAY define an `on context changed` handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.",
"content": "The provider MAY define an `on context changed` function, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.",
"RFC 2119 keyword": "MAY",
"children": []
},
Expand Down Expand Up @@ -552,14 +603,14 @@
{
"id": "Conditional Requirement 3.2.4.1",
"machine_id": "conditional_requirement_3_2_4_1",
"content": "When the global `evaluation context` is set, the `on context changed` handler MUST run.",
"content": "When the global `evaluation context` is set, the `on context changed` function MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Conditional Requirement 3.2.4.2",
"machine_id": "conditional_requirement_3_2_4_2",
"content": "When the `evaluation context` for a specific provider is set, the `on context changed` handler MUST only run on the associated provider.",
"content": "When the `evaluation context` for a specific provider is set, the `on context changed` function MUST only run on the associated provider.",
"RFC 2119 keyword": "MUST",
"children": []
}
Expand Down Expand Up @@ -881,6 +932,13 @@
"RFC 2119 keyword": "SHOULD",
"children": []
},
{
"id": "Requirement 5.1.5",
"machine_id": "requirement_5_1_5",
"content": "`PROVIDER_ERROR` events SHOULD populate the `provider event details`'s `error code` field.",
"RFC 2119 keyword": "SHOULD",
"children": []
},
{
"id": "Requirement 5.2.1",
"machine_id": "requirement_5_2_1",
Expand Down Expand Up @@ -960,25 +1018,32 @@
{
"id": "Conditional Requirement 5.3.4.1",
"machine_id": "conditional_requirement_5_3_4_1",
"content": "When the provider's `on context changed` is called, the provider MAY emit the `PROVIDER_STALE` event, and transition to the `STALE` state.",
"RFC 2119 keyword": "MAY",
"content": "While the provider's `on context changed` function is executing, associated `RECONCILING` handlers MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Conditional Requirement 5.3.4.2",
"machine_id": "conditional_requirement_5_3_4_2",
"content": "If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.",
"content": "If the provider's `on context changed` function terminates normally, and no other invocations have yet to terminate, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Conditional Requirement 5.3.4.3",
"machine_id": "conditional_requirement_5_3_4_3",
"content": "If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.",
"content": "If the provider's `on context changed` function terminates abnormally, and no other invocations have yet to terminate, associated `PROVIDER_ERROR` handlers MUST run.",
"RFC 2119 keyword": "MUST",
"children": []
}
]
},
{
"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 accordingly.",
"RFC 2119 keyword": "MUST",
"children": []
}
]
}
106 changes: 106 additions & 0 deletions specification/sections/01-flag-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,109 @@ The global API object might expose a `shutdown` function, which will call the re
Alternatively, implementations might leverage language idioms such as auto-disposable interfaces or some means of cancellation signal propagation to allow for graceful shutdown.

see: [`shutdown`](./02-providers.md#25-shutdown)

### 1.7. Provider Lifecycle Management

The implementation maintains an internal representation of the state of configured providers, tracking the lifecycle of each provider.
This state of the provider is exposed on associated `clients`.

The diagram below illustrates the possible states and transitions of the `state` field for a provider during the provider lifecycle.

```mermaid
---
title: Provider lifecycle
---
stateDiagram-v2
direction LR
[*] --> NOT_READY
NOT_READY --> READY:initialize()
NOT_READY --> ERROR:initialize()
NOT_READY --> FATAL:initialize()
FATAL --> [*]
READY --> ERROR:*
ERROR --> READY:*
READY --> STALE:*
STALE --> READY:*
STALE --> ERROR:*
READY --> NOT_READY:shutdown()
STALE --> NOT_READY:shutdown()
ERROR --> NOT_READY:shutdown()
READY --> RECONCILING:::client:setContext()
RECONCILING:::client --> READY
RECONCILING:::client --> ERROR
classDef client fill:#888
```

\* transitions occurring when associated events are spontaneously emitted from the provider

<span style="color:#888">█</span> only defined in static-context (client-side) paradigm

> [!NOTE]
> Only SDKs implementing the [static context (client-side) paradigm](../glossary.md#static-context-paradigm) define `RECONCILING` to facilitate [context reconciliation](./02-providers.md#26-provider-context-reconciliation).
#### Requirement 1.7.1

> 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.

see [provider status](../types.md#provider-status)

#### Condition 1.7.2

[![experimental](https://img.shields.io/static/v1?label=Status&message=experimental&color=orange)](https://github.com/open-feature/spec/tree/main/specification#experimental)

> The implementation uses the static-context paradigm.
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 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.
Once the provider has initialized, the `provider status` should indicate the provider is ready to be used to evaluate flags.

#### Requirement 1.7.4

> The client's `provider status` accessor **MUST** indicate `ERROR` if the `initialize` function of the associated provider terminates abnormally.
If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.

#### 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`.
If the provider has failed to initialize, the `provider status` should indicate the provider is in an error state.

#### 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 `PROVIDER_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)

#### 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)
55 changes: 10 additions & 45 deletions specification/sections/02-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,53 +188,18 @@ class MyProvider implements Provider {
}
```

#### Requirement 2.4.2
#### Condition 2.4.2

> The `provider` **MAY** define a `status` field/accessor which indicates the readiness of the provider, with possible values `NOT_READY`, `READY`, `STALE`, or `ERROR`.
> The provider defines an `initialize` function.
Providers without this field can be assumed to be ready immediately.
##### Conditional Requirement 2.4.2.1

The diagram below illustrates the possible states and transitions of the `status` fields.
> If the provider's `initialize` function fails to render the provider ready to evaluate flags, it **SHOULD** abnormally terminate.
```mermaid
---
title: Provider State
---
stateDiagram-v2
direction LR
[*] --> NOT_READY
NOT_READY --> READY:initialize
READY --> ERROR
ERROR --> READY
READY --> STALE
STALE --> READY
STALE --> ERROR
READY --> NOT_READY:shutdown
STALE --> NOT_READY:shutdown
ERROR --> NOT_READY:shutdown
```

see [provider status](../types.md#provider-status)

#### Requirement 2.4.3

> The provider **MUST** set its `status` field/accessor to `READY` if its `initialize` function terminates normally.
If the provider supports the `status` field/accessor and initialization succeeds, setting the `status` to `READY` indicates that the provider is initialized and flag evaluation is proceeding normally.

#### Requirement 2.4.4

> The provider **MUST** set its `status` field to `ERROR` if its `initialize` function terminates abnormally.
If the provider supports the `status` field/accessor and initialization fails, setting the `status` to `ERROR` indicates the provider is in an error state. If the error is transient in nature (ex: a connectivity failure of some kind) the provider can attempt to resolve this state automatically.

#### Requirement 2.4.5

> The provider **SHOULD** indicate an error if flag resolution is attempted before the provider is ready.
It's recommended to set an informative `error code`, such as `PROVIDER_NOT_READY` if evaluation in attempted before the provider is initialized.
If a provider is unable to start up correctly, it should indicate abnormal execution by throwing an exception, returning an error, or otherwise indicating so by means idiomatic to the implementation language.
If the error is irrecoverable (perhaps due to bad credentials or invalid configuration) the `PROVIDER_FATAL` error code should be used.

see: [error codes](https://openfeature.dev/specification/types#error-code)
see: [error codes](../types.md#error-code)

### 2.5. Shutdown

Expand Down Expand Up @@ -266,14 +231,14 @@ see: [initialization](#24-initialization)
[![experimental](https://img.shields.io/static/v1?label=Status&message=experimental&color=orange)](https://github.com/open-feature/spec/tree/main/specification#experimental)
Static-context focused providers may need a mechanism to understand when their cache of evaluated flags must be invalidated or updated. An `on context changed` handler can be defined which performs whatever operations are needed to reconcile the evaluated flags with the new context.
Static-context focused providers may need a mechanism to understand when their cache of evaluated flags must be invalidated or updated. An `on context changed` function can be defined which performs whatever operations are needed to reconcile the evaluated flags with the new context.
#### Requirement 2.6.1
> The provider **MAY** define an `on context changed` handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.
> The provider **MAY** define an `on context changed` function, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.
Especially in static-context implementations, providers and underlying SDKs may maintain state for a particular context.
The `on context changed` handler provides a mechanism to update this state, often by re-evaluating flags in bulk with respect to the new context.
The `on context changed` function provides a mechanism to update this state, often by re-evaluating flags in bulk with respect to the new context.
```java
// MyProvider implementation of the onContextChanged function defined in Provider
Expand Down
Loading

0 comments on commit ae286cf

Please sign in to comment.