Skip to content

Commit 1c56363

Browse files
authored
Add multi-provider blog post (#593)
Signed-off-by: Emma Willis <[email protected]>
1 parent a978c83 commit 1c56363

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
---
2+
slug: "openfeature-multi-provider-release"
3+
title: "OpenFeature Multi-Provider Release"
4+
date: 2024-06-14
5+
authors: [emmawillis]
6+
description: "Introducing an OpenFeature Multi-Provider for Node.js and JavaScript"
7+
tags: [node, javascript, providers]
8+
draft: false
9+
---
10+
We are excited to announce the release of the OpenFeature “Multi-Provider” for JavaScript and NodeJS. This new provider allows developers to access multiple OpenFeature providers through a unified interface, with customizable strategies that reconcile inconsistencies between providers. The Multi-Provider supports use cases including provider migrations and long-term support for multiple feature flag sources. It is currently available for the Javascript OpenFeature SDKs [@openfeature/server-sdk](https://www.npmjs.com/package/@openfeature/server-sdk) and [@openfeature/web-sdk](https://www.npmjs.com/package/@openfeature/web-sdk).
11+
12+
<!--truncate-->
13+
14+
## How It Works
15+
16+
```jsx
17+
const multiProvider = new MultiProvider([
18+
{
19+
provider: new ProviderA(),
20+
},
21+
{
22+
provider: new ProviderB(),
23+
},
24+
]);
25+
26+
await OpenFeature.setProviderAndWait(multiProvider);
27+
28+
openFeatureClient = OpenFeature.getClient();
29+
const value = await openFeatureClient.getStringValue('flag-key', 'default value', someUser);
30+
```
31+
32+
The Multi-Provider behaves like any other provider, but under the hood it combines results from any number of providers. It takes a list of providers as its only required parameter, then evaluates flags by progressing through the list of providers in order. By default, it will only evaluate the next provider if the current one indicates the flag was not found. Once there is a flag match, the Multi-Provider will return that result and not evaluate with the remaining providers.
33+
34+
## Strategies
35+
36+
In order to support diverse use cases, the Multi-Provider also accepts a Strategy parameter, which can be customized to control which providers are evaluated and how the final result is determined.
37+
38+
```jsx
39+
const multiProvider = new MultiProvider(
40+
[
41+
{
42+
provider: new ProviderA(),
43+
},
44+
{
45+
provider: new ProviderB(),
46+
},
47+
],
48+
new SomeStrategy(),
49+
);
50+
```
51+
52+
### FirstMatchStrategy
53+
54+
The default Strategy is the `FirstMatchStrategy`. This will evaluate providers in order, moving on to the next provider only if the current provider returns a `FLAG_NOT_FOUND` result. If an error is thrown by any provider, the Multi-Provider will throw that error. The OpenFeature SDK will then catch the error and return the default value.
55+
56+
This strategy is useful for migrating from one provider to another, when some flags have been ported to the new system while others remain in the old one. By putting the new provider first in the provider's list, the Multi-Provider will prefer results from the new system but keep the old as a fallback.
57+
58+
`FirstMatchStrategy` can also be used to pull feature flags from multiple sources, and return as soon as the flag is found.
59+
60+
### **FirstSuccessfulStrategy**
61+
62+
`FirstSuccessfulStrategy` is similar to `FirstMatchStrategy`, except that errors from evaluated providers do not halt execution. Instead, it will return the first successful result from a provider. If no provider successfully responds, it will throw an error result.
63+
64+
One use case for this strategy is if you want the Multi-Provider to fallback to another provider in the case of an error, rather than throwing an error itself. For example, if an external vendor provider goes down, you may want to automatically fall back to a config file.
65+
66+
### **ComparisonStrategy**
67+
68+
The `ComparisonStrategy` requires that all providers agree on a value. If the providers return the same value, the result of the evaluation will be that value. Otherwise, the evaluation result will contain an error in the `errors` field to indicate a mismatch, and the result value will be the value returned by the first provider. The `ComparisonStrategy` also evaluates all providers in parallel, as it is not intended to exit as soon as it finds a match.
69+
70+
One use case for this strategy is validating the success of a provider migration. If the configuration in one provider has been recreated in another, this strategy confirms the flags and user targeting are consistent between those sources.
71+
72+
### **Custom Strategies**
73+
74+
In order to support complex provider behaviour and diverse use cases, users can write custom strategies that extend the abstract `BaseEvaluationStrategy` class. Some key methods to control the Multi-Provider’s behaviour include:
75+
76+
- **runMode:** This property determines whether providers are evaluated sequentially or in parallel.
77+
- **shouldEvaluateThisProvider:** This function is called before a provider is evaluated. If it returns false, the provider is skipped. This is useful for conditional evaluations based on the flag key or the provider's state.
78+
- For example, if you know that all flag keys containing the substring `provider_a_` should only be evaluated using ProviderA, you can skip evaluating with any other providers when that prefix is used.
79+
80+
```tsx
81+
export class MyCustomStrategy extends BaseEvaluationStrategy {
82+
// In this example, we know keys containing 'provider_a_' will only exist in ProviderA
83+
override shouldEvaluateThisProvider(
84+
strategyContext: StrategyPerProviderContext,
85+
evalContext: EvaluationContext,
86+
): boolean {
87+
if (strategyContext.flagKey.includes('provider_a_') && strategyContext.providerName !== 'ProviderA') {
88+
return false;
89+
}
90+
91+
return true;
92+
}
93+
}
94+
```
95+
96+
- **shouldEvaluateNextProvider:** After a provider is evaluated, this function determines whether to proceed with evaluating the next provider. It takes into account the result or any errors encountered. If it returns true, the next provider is evaluated; otherwise, the evaluation stops and the results are passed to `determineFinalResult`. In parallel execution mode, this function is not called.
97+
- If your provider indicates that a result should not be used in a non-standard way, you can customize `shouldEvaluateNextProvider` to continue to the next provider if that condition is met. For example, if your provider result should be skipped when `flagMetadata.unusableResult` is `true`, you can check for that property here.
98+
99+
```tsx
100+
export class MyCustomStrategy extends BaseEvaluationStrategy {
101+
override shouldEvaluateNextProvider<T extends FlagValue>(
102+
strategyContext: StrategyPerProviderContext,
103+
context: EvaluationContext,
104+
details?: ResolutionDetails<T>,
105+
thrownError?: unknown,
106+
): boolean {
107+
// if the provider indicates the result should not be used, continue to next provider
108+
if (details?.flagMetadata.unusableResult === true) {
109+
return true;
110+
}
111+
// otherwise, return this result
112+
return false;
113+
}
114+
}
115+
```
116+
117+
- **determineFinalResult:** This function is invoked after all evaluations are completed (or when no further evaluations are needed). It consolidates the results and determines the final outcome. The function must return a `FinalResult` object, which includes the final resolution details and the corresponding provider, or an array of errors if the result was unsuccessful.
118+
- For example, if you are evaluating several providers in parallel and want to control which result is used in the case of a mismatch, rather than using the built-in `ComparisonStrategy`, you could override this function.
119+
120+
```tsx
121+
export class MyCustomStrategy extends BaseEvaluationStrategy {
122+
override runMode = 'parallel';
123+
124+
override determineFinalResult<T extends FlagValue>(
125+
strategyContext: StrategyPerProviderContext,
126+
context: EvaluationContext,
127+
resolutions: ProviderResolutionResult<T>[],
128+
): FinalResult<T> {
129+
let value: T | undefined;
130+
131+
for (const resolution of resolutions) {
132+
if (typeof value !== 'undefined' && value !== resolution.details.value) {
133+
logger.warn('Mismatch between provider results: ', value, resolution.details.value);
134+
} else {
135+
value = resolution.details.value;
136+
}
137+
}
138+
139+
// we want to always use the first provider's resolution
140+
const finalResolution = resolutions[0];
141+
return this.resolutionToFinalResult(finalResolution);
142+
}
143+
}
144+
```
145+
146+
## Use Cases
147+
148+
Some key use cases supported by the Multi-Provider include:
149+
150+
### Migrating from one provider to another
151+
152+
Let’s say you were using one provider, `OldProvider`, but have decided to switch to `NewProvider` moving forward. You have been creating all new flags in `NewProvider`, while slowly copying over your existing flags. This means your newest flags only exist in `NewProvider` , some of your older flags only exist in `OldProvider`, and an increasing number of flags exist in both. Previously, you would have to instantiate an OpenFeature client for each of your providers, or register the providers under different domains and manage switching between them manually. The Multi-Provider allows you to wrap both providers and prioritize the new one automatically:
153+
154+
```jsx
155+
const multiProvider = new MultiProvider([
156+
{
157+
provider: new NewProvider(),
158+
},
159+
{
160+
provider: new OldProvider(),
161+
},
162+
]);
163+
```
164+
165+
The `NewProvider` now takes precedence and will always be used, unless a flag does not exist in its configuration. In that case, the Multi-Provider will fall back to `OldProvider` automatically.
166+
167+
### Validating the success of a migration
168+
169+
You can also use the Multi-Provider to confirm that two or more providers always agree on all evaluation results. For example, once you are finished migrating all of your feature flags from `OldProvider` to `NewProvider`, you may want to run concurrently for some time and track any instances where the evaluation results differ, to catch errors in the configuration of `NewProvider`.
170+
171+
By using the [ComparisonStrategy](https://www.notion.so/OpenFeature-Multi-Provider-OF-Blog-Post-4cd7d2e9d7f5473093a5a37b4c3ed89b?pvs=21), you can configure the Multi-Provider to run all providers and return an error result if the results don’t match.
172+
173+
```jsx
174+
const newProvider = new NewProvider();
175+
const oldProvider = new OldProvider();
176+
177+
const onMismatch = (_resolutions: ProviderResolutionResult<FlagValue>[]) => {
178+
logger.warn('Provider mismatch!')
179+
}
180+
181+
const multiProvider = new MultiProvider(
182+
[
183+
{
184+
provider: newProvider,
185+
},
186+
{
187+
provider: oldProvider,
188+
}
189+
],
190+
new ComparisonStrategy(
191+
oldProvider, // the fallback provider to trust in the case of a mismatch
192+
onMismatch, // callback method called when provider resolutions don't match
193+
)
194+
)
195+
```
196+
197+
### Aggregating flags from multiple sources
198+
199+
The Multi-Provider can be used to combine feature flags from sources such as environment variables, database entries, and vendor feature flags, without having to instantiate multiple SDK clients. The order of the providers list establishes the precedence order of providers to control which should be trusted if keys exist in both systems. For example, if you want to use environment variables to override flag values from a vendor provider, you could pass the environment variable provider as the first in the providers list.
200+
201+
```jsx
202+
const multiProvider = new MultiProvider([
203+
{
204+
provider: new EnvVarProvider(),
205+
},
206+
{
207+
provider: new VendorProvider(),
208+
},
209+
]);
210+
await OpenFeature.setProviderAndWait(multiProvider);
211+
212+
openFeatureClient = OpenFeature.getClient();
213+
const value = await openFeatureClient.getStringValue('flag-key', 'default value', someUser);
214+
```
215+
216+
If the flag with the key ‘flag-key’ exists in both providers, it will only be evaluated with the `EnvVarProvider`.
217+
218+
### Setting a fallback for cloud providers
219+
220+
You can use the Multi-Provider to automatically fall back to a local configuration if an external vendor provider goes down, rather than using the default values. By using the `FirstSuccessfulStrategy`, the Multi-Provider will move on to the next provider in the list if an error is thrown.
221+
222+
```jsx
223+
const multiProvider = new MultiProvider(
224+
[
225+
{
226+
provider: new VendorProvider(),
227+
},
228+
{
229+
provider: new EnvVarProvider(),
230+
},
231+
],
232+
new FirstSuccessfulStrategy(),
233+
);
234+
```
235+
236+
In this example, if `VendorProvider` is unavailable for any reason, `EnvVarProvider` will be evaluated instead.
237+
238+
## Compatibility
239+
240+
The Multi-Provider offers developers more flexibility and control when managing complex feature flag setups. As a first step, we have implemented it in the [@openfeature/server-sdk](https://www.npmjs.com/package/@openfeature/server-sdk) and [@openfeature/web-sdk](https://www.npmjs.com/package/@openfeature/web-sdk).
241+
242+
## Future Plans
243+
244+
We’re excited to get feedback about the current strategies and hear about other possible use cases. As we validate the Multi-Provider on the initial supported implementations we will gradually expand to support other languages and further strategies. We welcome contributors to bring this functionality to other languages.

blog/authors.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,9 @@ dyladan:
6363
title: Senior Open Source Architect
6464
url: https://github.com/dyladan
6565
image_url: https://github.com/dyladan.png
66+
67+
emmawillis:
68+
name: Emma Willis
69+
title: Software Developer at DevCycle
70+
url: https://github.com/emmawillis
71+
image_url: https://github.com/emmawillis.png

0 commit comments

Comments
 (0)