Skip to content

Commit eaa44da

Browse files
ajwoottotoddbaert
andauthored
chore: add multi provider appendix (#264)
- Adds an appendix section for the Multi-Provider, describing its purpose and implementation --------- Signed-off-by: Adam Wootton <[email protected]> Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 3f133fb commit eaa44da

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed

specification/appendix-a-included-utilities.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,326 @@ flowchart LR
3737
A[e2e Tests] -.-> B[In-memory provider]
3838
end
3939
```
40+
41+
## Multi-Provider
42+
43+
### Introduction
44+
45+
The OpenFeature Multi-Provider wraps multiple underlying providers in a unified interface, allowing the SDK client to transparently interact with all those providers at once.
46+
This allows use cases where a single client and evaluation interface is desired, but where the flag data should come from more than one source.
47+
48+
Some examples:
49+
50+
- A migration from one feature flagging provider to another.
51+
During that process, you may have some flags that have been ported to the new system and others that haven’t.
52+
Therefore you’d want the Multi-Provider to return the result of the “new” system if available otherwise, return the "old" system’s result.
53+
- Long-term use of multiple sources for flags.
54+
For example, someone might want to be able to combine environment variables, database entries, and vendor feature flag results together in a single interface, and define the precedence order in which those sources should be consulted.
55+
56+
Check the [OpenFeature JavaScript Multi-Provider](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider) for a reference implementation.
57+
58+
### Basics
59+
60+
The provider is initialized by passing a list of provider instances it should evaluate.
61+
The order of the array defines the order in which sources should be evaluated.
62+
The provider whose value is ultimately used will depend on the “strategy” that is provided, which can be chosen from a set of pre-defined ones or implemented as custom logic.
63+
64+
For example:
65+
66+
```typescript
67+
const multiProvider = new MultiProvider(
68+
[
69+
{
70+
provider: new ProviderA(),
71+
},
72+
{
73+
provider: new ProviderB()
74+
}
75+
],
76+
new FirstMatchStrategy()
77+
)
78+
```
79+
80+
From the perspective of the SDK client, this provider will now act as a “normal” spec-compliant provider, while handling the complexities of aggregating multiple providers internally.
81+
82+
### Specific Behavior
83+
84+
When dealing with many providers at once, various aspects of those providers need to be “combined” into one unified view.
85+
For example, each internal provider has a “status”, which should influence the Multi-Provider’s overall “status”.
86+
The specific aspects that need to be addressed are described below.
87+
88+
#### Unique Names
89+
90+
In order to identify each provider uniquely, it must have a name associated with it.
91+
The unique name will be used when reporting errors and results in order to indicate from which provider they came from.
92+
93+
Most providers have a `metadata.name` field which could be used, but this would be non-unique in the case where two instances of the same type of provider are used. As a result there would need to be a way to differentiate the two.
94+
95+
When instantiating the Multi-Provider, there will be an option for specifying a name to associate to each provider:
96+
97+
```typescript
98+
const multiProvider = new MultiProvider([
99+
{
100+
provider: new ProviderA(),
101+
name: "ProviderA"
102+
},
103+
{
104+
provider: new ProviderB(),
105+
name: "ProviderB"
106+
}
107+
])
108+
```
109+
110+
Names for each provider are then determined like this:
111+
112+
1. name passed in to constructor if specified
113+
2. `metadata.name` if it is unique among providers
114+
3. `${metadata.name}_${index}` if name is not unique. Eg. the first instance of ProviderA provider might be named “providerA_1” and the second might be “providerA_2”
115+
116+
If multiple names are passed in the constructor which conflict, an error will be thrown.
117+
118+
#### Initialization
119+
120+
Initialization of each provider should be handled in parallel in the Multi-Provider’s `initialize` method.
121+
It should call `initialize` on each provider it is managing, and bubble up any error that is thrown by re-throwing to the client.
122+
123+
#### Status and Event Handling
124+
125+
The status of a provider is tracked in OpenFeature SDKs based on emitted events.
126+
127+
Provider states can be transitioned in the ways represented here:
128+
[https://openfeature.dev/specification/sections/flag-evaluation#17-provider-lifecycle-management](https://openfeature.dev/specification/sections/flag-evaluation#17-provider-lifecycle-management)
129+
130+
The SDK client tracks statuses of a provider as follows:
131+
132+
- Initially the status is NOT_READY
133+
- Initialize function is called (if exists) and result is awaited
134+
- Successful initialize transitions state to READY, error result transitions to either ERROR or FATAL
135+
- From this point onwards, status is only changed as a result of provider emitting events to indicate status-changing things have occurred.
136+
- It can emit events like `FATAL`, `ERROR`, `STALE` and `READY` to transition to those states.
137+
138+
The only statuses which affect evaluation behavior at the SDK client level are `FATAL` and `NOT_READY`.
139+
If a provider is in either of these states, evaluation will be “skipped” by the client and the default value will be returned.
140+
141+
Other statuses are currently “informational”. Nevertheless, the Multi-Provider will represent an overall “status” based on the combined statuses of the providers.
142+
143+
##### Multi-Provider Status
144+
145+
The Multi-Provider mimics the event handling logic that tracks statuses in the SDK, and keeps track of the status of each provider it is managing.
146+
147+
The individual status-changing events from these providers will be “captured” in the Multi-Provider, and not re-emitted to the outer SDK UNLESS they cause the status of the Multi-Provider to change.
148+
149+
The status of the Multi-Provider will change when one of its providers changes to a status that is considered higher “precedence” than the current status.
150+
151+
The precedence order is defined as:
152+
153+
- FATAL
154+
- NOT_READY
155+
- ERROR
156+
- STALE
157+
- READY
158+
159+
For example, if all providers are currently in “READY” status, the Multi-Provider will be in “READY” status.
160+
If one of the providers is “STALE”, the status of the Multi-Provider will be “STALE”. If a different provider now becomes “ERROR”, the status will be “ERROR” even if the other provider is still in “STALE”.
161+
162+
When the Multi-Provider changes status, it does so by emitting the appropriate event to the SDK.
163+
The “details” of that event will be **identical** to the details of the original event from one of the inner providers which triggered this state change.
164+
165+
There is another event called “configuration changed” which does not affect status.
166+
This event should be re-emitted any time it occurs from any provider.
167+
168+
#### Evaluation Result
169+
170+
The evaluation result is based on the results from evaluating each provider.
171+
There are multiple “strategies” configurable in the Multi-Provider to decide how to use the results.
172+
173+
##### Interpreting Errors
174+
175+
Currently, providers have multiple ways of signalling evaluation errors to the SDK.
176+
Particularly in the case of Javascript, a provider can return an evaluation result that contains an error code and message, but still has a “value” for the result. It can also throw an error.
177+
178+
Several providers currently use the former approach for indicating errors in operations, and use the `value` field of the result to return the default value from the provider itself.
179+
180+
For the purposes of aggregating providers, the Multi-Provider treats both thrown and returned errors as an “error” result. If the returned error result has a value, that value will be ignored by all strategies. Only “nominal” evaluation results will be considered by the evaluation.
181+
182+
##### Strategies
183+
184+
The Multi-Provider supports multiple ways of deciding how to evaluate the set of providers it is managing, and how to deal with any errors that are thrown.
185+
186+
Strategies must be adaptable to the various requirements that might be faced in a multi-provider situation.
187+
In some cases, the strategy may want to ignore errors from individual providers as long as one of them successfully responds.
188+
In other cases, it may want to evaluate providers in order and skip the rest if a successful result is obtained.
189+
In still other scenarios, it may be required to always call every provider and decide what to do with the set of results.
190+
191+
The strategy to use is passed in to the Multi-Provider constructor as follows:
192+
193+
```typescript
194+
new MultiProvider(
195+
[
196+
{
197+
provider: new ProviderA()
198+
},
199+
{
200+
provider: new ProviderB()
201+
}
202+
],
203+
new FirstMatchStrategy()
204+
)
205+
```
206+
207+
By default, the Multi-Provider uses the “FirstMatchStrategy”.
208+
209+
Here are some standard strategies that come with the Multi-Provider:
210+
211+
###### First Match
212+
213+
Return the first result returned by a provider.
214+
Skip providers that indicate they had no value due to `FLAG_NOT_FOUND`.
215+
In all other cases, use the value returned by the provider.
216+
If any provider returns an error result other than `FLAG_NOT_FOUND`, the whole evaluation should error and “bubble up” the individual provider’s error in the result.
217+
218+
As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers.
219+
220+
###### First Successful
221+
222+
Similar to “First Match”, except that errors from evaluated providers do not halt execution.
223+
Instead, it will return the first successful result from a provider. If no provider successfully responds, it will throw an error result.
224+
225+
###### Comparison
226+
227+
Require that all providers agree on a value.
228+
If every provider returns a non-error result, and the values do not agree, the Multi-Provider should return the result from a configurable “fallback” provider.
229+
It will also call an optional “onMismatch” callback that can be used to monitor cases where mismatches of evaluation occurred.
230+
Otherwise the value of the result will be the result of the first provider in precedence order.
231+
232+
###### User Defined
233+
234+
Rather than making assumptions about when to use a provider’s result and when not to (which may not hold across all providers) there is also a way for the user to define their own strategy that determines whether or not to use a result or fall through to the next one.
235+
236+
A strategy can be implemented by implementing the `BaseEvaluationStrategy` class as follows:
237+
238+
```typescript
239+
type StrategyEvaluationContext = {
240+
flagKey: string;
241+
flagType: FlagValueType;
242+
};
243+
244+
type StrategyPerProviderContext = StrategyEvaluationContext & {
245+
provider: Provider;
246+
providerName: string;
247+
providerStatus: ProviderStatus;
248+
};
249+
250+
type ProviderResolutionResult<T extends FlagValue> = {
251+
details: ResolutionDetails<T>;
252+
thrownError?: unknown;
253+
provider: Provider;
254+
providerName: string;
255+
}
256+
type FinalResult = {
257+
details?: ResolutionDetails<unknown>;
258+
provider?: Provider;
259+
providerName?: string;
260+
errors?: {
261+
providerName: string;
262+
error: unknown;
263+
}[];
264+
};
265+
266+
abstract class BaseEvaluationStrategy {
267+
runMode: 'parallel' | 'sequential'
268+
269+
abstract shouldEvaluateThisProvider(
270+
strategyContext: StrategyPerProviderContext,
271+
evalContext: EvaluationContext
272+
): boolean;
273+
274+
abstract shouldEvaluateNextProvider<T extends FlagValue>(
275+
strategyContext: StrategyPerProviderContext,
276+
context: EvaluationContext,
277+
result: ProviderResolutionResult<T>
278+
): boolean;
279+
280+
abstract determineFinalResult<T extends FlagValue>(
281+
strategyContext: StrategyEvaluationContext,
282+
context: EvaluationContext,
283+
resolutions: ProviderResolutionResult<T>[],
284+
): FinalResult;
285+
}
286+
```
287+
288+
The `runMode` property defines whether the providers will all be evaluated at once in parallel, or whether they will be evaluated one at a time with each result determining whether to evaluate the next one in order.
289+
290+
The `shouldEvaluateThisProvider` function is called for each provider right before the Multi-Provider would evaluate it.
291+
If the function returns false, the provider will be skipped.
292+
This can be useful in cases where it’s desired to skip a provider based on what flag key is being used, or based on some state from the provider itself that indicates it shouldn’t be evaluated right now.
293+
294+
The `shouldEvaluateNextProvider` function is called right after a provider is evaluated.
295+
It is called with the details of resolution or any error that was thrown (which will be caught).
296+
If the function returns true, the next provider will be called.
297+
Otherwise all remaining providers will be skipped and the results of the ones that have been evaluated so far will be passed to `determineFinalResult` .
298+
If this function throws an error, the Multi-Provider will throw an error and not evaluate further providers.
299+
This function is not called when `runMode` is `parallel`, since all providers will be executed (as long as they individually pass the `shouldEvaluateThisProvider` check)
300+
301+
The `determineFinalResult` function is called after the resolution stage if no further providers will be called.
302+
This function can be used to decide from the set of resolutions which one should ultimately be used.
303+
The function must return a `FinalResult` object which contains the final “ResolutionDetails” and the provider that they correspond to, or an array of “errors” in the case of a non-successful result, with the provider that created each error.
304+
305+
To see reference implementations of the above-mentioned strategies, check out the source
306+
307+
[https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider/src/lib/strategies](https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider/src/lib/strategies)
308+
309+
#### Hooks
310+
311+
Provider hooks are capable of modifying the context before an evaluation takes place.
312+
This behavior must be preserved, but it’s also necessary to prevent these hooks from interfering with the context being passed to other providers.
313+
314+
For this reason, the Multi-Provider manages calling the hooks of each provider itself, at the appropriate time.
315+
It then uses the result of the before hooks for a given provider as the new evaluation context when evaluating *that provider*, without affecting the context used for other providers.
316+
317+
It then calls the after, error and finally hooks using the appropriate context as well.
318+
319+
Errors thrown from these hooks are be bubbled up to the client, depending on how the evaluation “strategy” defines what to do with errors.
320+
321+
#### Shutdown
322+
323+
The shutdown method should ensure that “shutdown” is called in all underlying providers, and bubble up any errors to the client
324+
325+
#### Error Handling
326+
327+
In cases where all providers are being called (Evaluation etc.) there may be more than one error encountered from more than one provider.
328+
The Multi-Provider will collect and throw all errors in an aggregated form as follows:
329+
330+
```javascript
331+
error = {
332+
message: 'some message',
333+
code: SOME_ERROR,
334+
// which provider caused the error
335+
originalErrors: [
336+
{
337+
source: 'ProviderA',
338+
error: {
339+
message: 'something',
340+
}
341+
}
342+
]
343+
}
344+
```
345+
346+
In the case where only one error is thrown by one provider, it will still throw in this form for consistency.
347+
348+
Other errors from the Multi-Provider itself will use standard error types.
349+
350+
#### Metadata
351+
352+
Providers can contain metadata. The Multi-Provider will make that metadata available within its own metadata as follows:
353+
354+
```javascript
355+
{
356+
name: 'multiprovider',
357+
originalMetadata: {
358+
providerA: {...},
359+
providerB: {...}
360+
},
361+
}
362+
```

0 commit comments

Comments
 (0)