[8.16] [Feature Flags] Retry provider setup (#214200) (#214300)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Feature Flags] Retry provider setup
(#214200)](https://github.com/elastic/kibana/pull/214200)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Alejandro Fernández
Haro","email":"alejandro.haro@elastic.co"},"sourceCommit":{"committedDate":"2025-03-12T21:45:38Z","message":"[Feature
Flags] Retry provider setup (#214200)\n\n## Summary\n\nWe identified
that on some occasions, the Feature Flags provider times\nout when
setting up, and, since we don't restart the Kibana server, it\nnever
sets it up.\n\nThis PR adds a retry logic to try to set the provider in
case there's an\nerror.\n\ncc @pmuellr as he found out about this
bug\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1337c11ac3c4c94c828db52a9ab9768ccf9a1c45","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","release_note:skip","backport:prev-minor","backport:prev-major","v9.1.0"],"title":"[Feature
Flags] Retry provider
setup","number":214200,"url":"https://github.com/elastic/kibana/pull/214200","mergeCommit":{"message":"[Feature
Flags] Retry provider setup (#214200)\n\n## Summary\n\nWe identified
that on some occasions, the Feature Flags provider times\nout when
setting up, and, since we don't restart the Kibana server, it\nnever
sets it up.\n\nThis PR adds a retry logic to try to set the provider in
case there's an\nerror.\n\ncc @pmuellr as he found out about this
bug\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1337c11ac3c4c94c828db52a9ab9768ccf9a1c45"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214200","number":214200,"mergeCommit":{"message":"[Feature
Flags] Retry provider setup (#214200)\n\n## Summary\n\nWe identified
that on some occasions, the Feature Flags provider times\nout when
setting up, and, since we don't restart the Kibana server, it\nnever
sets it up.\n\nThis PR adds a retry logic to try to set the provider in
case there's an\nerror.\n\ncc @pmuellr as he found out about this
bug\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1337c11ac3c4c94c828db52a9ab9768ccf9a1c45"}},{"url":"https://github.com/elastic/kibana/pull/214288","number":214288,"branch":"9.0","state":"OPEN"}]}]
BACKPORT-->
This commit is contained in:
Alejandro Fernández Haro 2025-03-13 11:30:18 +01:00 committed by GitHub
parent d648da2ded
commit a17cb1c163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 168 additions and 2 deletions

View file

@ -36,7 +36,9 @@ describe('FeatureFlagsService Server', () => {
});
afterEach(async () => {
jest.useRealTimers();
await featureFlagsService.stop();
jest.spyOn(OpenFeature, 'setProviderAndWait').mockRestore(); // Make sure that we clean up any previous mocked implementations
jest.clearAllMocks();
await OpenFeature.clearProviders();
});
@ -45,7 +47,7 @@ describe('FeatureFlagsService Server', () => {
test('appends a provider (no async operation)', () => {
expect.assertions(1);
const { setProvider } = featureFlagsService.setup();
const spy = jest.spyOn(OpenFeature, 'setProvider');
const spy = jest.spyOn(OpenFeature, 'setProviderAndWait');
const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
setProvider(fakeProvider);
expect(spy).toHaveBeenCalledWith(fakeProvider);

View file

@ -24,6 +24,7 @@ import {
} from '@openfeature/server-sdk';
import deepMerge from 'deepmerge';
import { filter, switchMap, startWith, Subject } from 'rxjs';
import { setProviderWithRetries } from './set_provider_with_retries';
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
/**
@ -76,7 +77,7 @@ export class FeatureFlagsService {
if (OpenFeature.providerMetadata !== NOOP_PROVIDER.metadata) {
throw new Error('A provider has already been set. This API cannot be called twice.');
}
OpenFeature.setProvider(provider);
setProviderWithRetries(provider, this.logger);
},
appendContext: (contextToAppend) => this.appendContext(contextToAppend),
};

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { OpenFeature, type Provider } from '@openfeature/server-sdk';
import { setProviderWithRetries } from './set_provider_with_retries';
import { loggerMock, type MockedLogger } from '@kbn/logging-mocks';
describe('setProviderWithRetries', () => {
const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
let logger: MockedLogger;
beforeEach(() => {
jest.useFakeTimers();
logger = loggerMock.create();
});
afterEach(() => {
jest.clearAllTimers();
jest.clearAllMocks();
jest.useRealTimers();
});
test('sets the provider and logs the success', async () => {
expect.assertions(3);
const spy = jest.spyOn(OpenFeature, 'setProviderAndWait');
setProviderWithRetries(fakeProvider, logger);
expect(spy).toHaveBeenCalledWith(fakeProvider);
expect(spy).toHaveBeenCalledTimes(1);
await jest.runAllTimersAsync();
expect(logger.info.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"Feature flags provider successfully set up.",
],
]
`);
});
test('should retry up to 5 times (and does not throw/reject)', async () => {
expect.assertions(15);
const spy = jest
.spyOn(OpenFeature, 'setProviderAndWait')
.mockRejectedValue(new Error('Something went terribly wrong!'));
setProviderWithRetries(fakeProvider, logger);
expect(spy).toHaveBeenCalledWith(fakeProvider);
// Initial attempt
expect(spy).toHaveBeenCalledTimes(1);
// 5 retries
for (let i = 0; i < 5; i++) {
await jest.advanceTimersByTimeAsync(1000 * Math.pow(2, i)); // exponential backoff of factor 2
expect(spy).toHaveBeenCalledTimes(i + 2);
expect(logger.warn).toHaveBeenCalledTimes(i + 2);
}
// Given up retrying
await jest.advanceTimersByTimeAsync(32000);
expect(spy).toHaveBeenCalledTimes(6);
expect(logger.warn.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 5 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 4 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 3 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 2 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 1 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!. Retrying 0 times more...",
Object {
"error": [Error: Something went terribly wrong!],
},
],
]
`);
expect(logger.error.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"Failed to set up the feature flags provider: Something went terribly wrong!",
Object {
"error": [Error: Something went terribly wrong!],
},
],
]
`);
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { Logger } from '@kbn/logging';
import { type Provider, OpenFeature } from '@openfeature/server-sdk';
import pRetry from 'p-retry';
/**
* Handles the setting of the Feature Flags provider and any retries that may be required.
* This method is intentionally synchronous (no async/await) to avoid holding Kibana's startup on the feature flags setup.
* @param provider The OpenFeature provider to set up.
* @param logger You know, for logging.
*/
export function setProviderWithRetries(provider: Provider, logger: Logger): void {
pRetry(() => OpenFeature.setProviderAndWait(provider), {
retries: 5,
onFailedAttempt: (error) => {
logger.warn(
`Failed to set up the feature flags provider: ${error.message}. Retrying ${error.retriesLeft} times more...`,
{ error }
);
},
})
.then(() => {
logger.info('Feature flags provider successfully set up.');
})
.catch((error) => {
logger.error(`Failed to set up the feature flags provider: ${error.message}`, {
error,
});
});
}

View file

@ -20,5 +20,6 @@
"@kbn/core-base-server-mocks",
"@kbn/config-schema",
"@kbn/config-mocks",
"@kbn/logging-mocks",
]
}