1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/src/lib/services/proxy-service.ts

212 lines
6.1 KiB
TypeScript
Raw Normal View History

import { IUnleashConfig, IUnleashServices, IUnleashStores } from '../types';
import { Logger } from '../logger';
import { ClientMetricsSchema, ProxyFeatureSchema } from '../openapi';
import ApiUser from '../types/api-user';
import {
Context,
InMemStorageProvider,
Unleash,
UnleashEvents,
} from 'unleash-client';
import { ProxyRepository } from '../proxy';
import { ApiTokenType } from '../types/models/api-token';
import {
FrontendSettings,
frontendSettingsKey,
} from '../types/settings/frontend-settings';
import { validateOrigins } from '../util';
import { BadDataError, InvalidTokenError } from '../error';
import { minutesToMilliseconds } from 'date-fns';
type Config = Pick<
IUnleashConfig,
'getLogger' | 'frontendApi' | 'frontendApiOrigins'
>;
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
type Services = Pick<
IUnleashServices,
| 'featureToggleServiceV2'
| 'segmentService'
| 'clientMetricsServiceV2'
| 'settingService'
| 'configurationRevisionService'
>;
export class ProxyService {
private readonly config: Config;
private readonly logger: Logger;
private readonly stores: Stores;
private readonly services: Services;
/**
* This is intentionally a Promise becasue we want to be able to await
* until the client (which might be being created by a different request) is ready
* Check this test that fails if we don't use a Promise: src/test/e2e/api/proxy/proxy.concurrency.e2e.test.ts
*/
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
new Map();
private cachedFrontendSettings?: FrontendSettings;
fix: polling in proxy repository now stops correctly (#3268) ### What This patches two very subtle bugs in the proxy repository that cause it to never actually stop polling the db in the background ## Details - Issue 1 We've recently started to get the following output when running `yarn test`: ` Attempted to log "Error: Unable to acquire a connection at Object.queryBuilder (/home/simon/dev/unleash/node_modules/knex/lib/knex-builder/make-knex.js:111:26)` This seems to occur for every test suite after running the proxy tests and the full stack trace doesn't point to anything related to the running tests that produce this output. Running a `git bisect` points to this commit: https://github.com/Unleash/unleash/commit/6e44a65c58d8e28668f0d3459b62c0ce0b84849a being the culprit but I believe that this may have surfaced the bug rather than causing it. Layering in a few console logs and running Unleash, seems to point to the proxy repository setting up data polling but never actually terminating it when `stop` was called, which is inline with the output here - effectively the tests were continuing to run the polling in the background after the suite had exited and jest freaks out that an async task is running when it shouldn't be. This is easy to reproduce once the console logs are in place in the `dataPolling` function, by running Unleash - creating and deleting a front end token never terminates the poll cycle. I believe the cause here is some subtlety around using async functions with timers - stop was being called, which results in the timer being cleared but a scheduled async call was already on the stack, causing the recursive call to resolve after stop, resurrecting the timer and reinitializing the poll cycle. I've moved the terminating code into the async callback. Which seems to solve the problem here. ## Details - Issue 2 Related to the first issue, when the proxy service stops the underlying Unleash Client, it never actually calls destroy on the client, it only removes it from its internal map. That in turn means that the Client never calls stop on the injected repository, it only removes it from memory. However, the scheduled task is `async` and `unref`, meaning it continues to spin in the background until every other process also exits. This is patched by simply calling destroy on the client when cleaning up ## The Ugly This is really hard to test effectively, mostly because this is an issue caused by internals within NodeJS and async. I've added a test that reads the output from the debug log (and also placed a debug log in the termination code). This also requires the test code to wait until the async task completes. This is horribly fragile so if someone has a better idea on how to prove this I would be a very happy human. The second ugly part is that this is a subtle issue in complex code that really, really needs to work correctly. I'm nervous about making changes here without lots of eyes on this
2023-03-10 09:03:32 +01:00
private timer: NodeJS.Timeout | null;
constructor(config: Config, stores: Stores, services: Services) {
this.config = config;
this.logger = config.getLogger('services/proxy-service.ts');
this.stores = stores;
this.services = services;
this.timer = setInterval(
() => this.fetchFrontendSettings(),
minutesToMilliseconds(2),
).unref();
}
async getProxyFeatures(
token: ApiUser,
context: Context,
): Promise<ProxyFeatureSchema[]> {
const client = await this.clientForProxyToken(token);
const definitions = client.getFeatureToggleDefinitions() || [];
const sessionId = context.sessionId || String(Math.random());
return definitions
.filter((feature) =>
client.isEnabled(feature.name, { ...context, sessionId }),
)
.map((feature) => ({
name: feature.name,
enabled: Boolean(feature.enabled),
variant: client.getVariant(feature.name, {
...context,
sessionId,
}),
impressionData: Boolean(feature.impressionData),
}));
}
async registerProxyMetrics(
token: ApiUser,
metrics: ClientMetricsSchema,
ip: string,
): Promise<void> {
ProxyService.assertExpectedTokenType(token);
const environment =
this.services.clientMetricsServiceV2.resolveMetricsEnvironment(
token,
metrics,
);
await this.services.clientMetricsServiceV2.registerClientMetrics(
{ ...metrics, environment },
ip,
);
}
private async clientForProxyToken(token: ApiUser): Promise<Unleash> {
ProxyService.assertExpectedTokenType(token);
let client = this.clients.get(token.secret);
if (!client) {
client = this.createClientForProxyToken(token);
this.clients.set(token.secret, client);
}
return client;
}
private async createClientForProxyToken(token: ApiUser): Promise<Unleash> {
const repository = new ProxyRepository(
this.config,
this.stores,
this.services,
token,
);
fix(deps): update dependency unleash-client to v3.18.0 (#2956) [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [unleash-client](https://togithub.com/Unleash/unleash-client-node) | [`3.16.1` -> `3.18.0`](https://renovatebot.com/diffs/npm/unleash-client/3.16.1/3.18.0) | [![age](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/compatibility-slim/3.16.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/confidence-slim/3.16.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes <details> <summary>Unleash/unleash-client-node</summary> ### [`v3.18.0`](https://togithub.com/Unleash/unleash-client-node/blob/HEAD/CHANGELOG.md#&#8203;3180) [Compare Source](https://togithub.com/Unleash/unleash-client-node/compare/v3.17.0...v3.18.0) feat: gracefully handle unsuccessful metrics post ([#&#8203;414](https://togithub.com/Unleash/unleash-client-node/issues/414)) feat/flush metrics ([#&#8203;415](https://togithub.com/Unleash/unleash-client-node/issues/415)) feat: add metrics jitter support ([#&#8203;412](https://togithub.com/Unleash/unleash-client-node/issues/412)) fix: Allow SDK to startup when backup data is corrupt ([#&#8203;418](https://togithub.com/Unleash/unleash-client-node/issues/418)) fix: flexible-rollout random stickiness is not random enough ([#&#8203;417](https://togithub.com/Unleash/unleash-client-node/issues/417)) fix: build correct version on npm version chore(deps): update dependency eslint-plugin-import to v2.27.5 ([#&#8203;416](https://togithub.com/Unleash/unleash-client-node/issues/416)) chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.48.2 ([#&#8203;413](https://togithub.com/Unleash/unleash-client-node/issues/413)) chore(deps): update dependency eslint to v8.32.0 ([#&#8203;410](https://togithub.com/Unleash/unleash-client-node/issues/410)) chore(deps): update dependency prettier to v2.8.3 ([#&#8203;406](https://togithub.com/Unleash/unleash-client-node/issues/406)) chore(deps): update dependency eslint-plugin-import to v2.27.4 ([#&#8203;404](https://togithub.com/Unleash/unleash-client-node/issues/404)) ### [`v3.17.0`](https://togithub.com/Unleash/unleash-client-node/blob/HEAD/CHANGELOG.md#&#8203;3170) [Compare Source](https://togithub.com/Unleash/unleash-client-node/compare/v3.16.1...v3.17.0) - feat: Only initialize the SDK once. ([#&#8203;368](https://togithub.com/Unleash/unleash-client-node/issues/368)) - fix: upgrade semver to 7.3.8 - fix: add resolution for debug - fix: add resolution for minimatch - fix: add resolution for qs - fix: add resolution for json5 - fix: update yarn.lock - docs: Update the readme with info from docs.getunleash ([#&#8203;399](https://togithub.com/Unleash/unleash-client-node/issues/399)) - docs: minor fix in README - chore(deps): update dependency debug to v4 ([#&#8203;402](https://togithub.com/Unleash/unleash-client-node/issues/402)) - chore(deps): update dependency json5 to v2 ([#&#8203;401](https://togithub.com/Unleash/unleash-client-node/issues/401)) - chore(deps): update dependency eslint to v8.31.0 ([#&#8203;394](https://togithub.com/Unleash/unleash-client-node/issues/394)) - chore(deps): update dependency nock to v13.3.0 ([#&#8203;400](https://togithub.com/Unleash/unleash-client-node/issues/400)) - chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.48.1 ([#&#8203;395](https://togithub.com/Unleash/unleash-client-node/issues/395)) - chore(deps): update dependency eslint-config-prettier to v8.6.0 ([#&#8203;396](https://togithub.com/Unleash/unleash-client-node/issues/396)) - chore(deps): update dependency prettier to v2.8.2 ([#&#8203;398](https://togithub.com/Unleash/unleash-client-node/issues/398)) - chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.47.1 ([#&#8203;346](https://togithub.com/Unleash/unleash-client-node/issues/346)) - chore(deps): update dependency typescript to v4.9.4 ([#&#8203;386](https://togithub.com/Unleash/unleash-client-node/issues/386)) - chore(deps): update dependency sinon to v15 ([#&#8203;391](https://togithub.com/Unleash/unleash-client-node/issues/391)) - chore(deps): update dependency [@&#8203;types/node](https://togithub.com/types/node) to v18 ([#&#8203;380](https://togithub.com/Unleash/unleash-client-node/issues/380)) - chore(deps): update dependency [@&#8203;types/node](https://togithub.com/types/node) to v14.18.36 ([#&#8203;382](https://togithub.com/Unleash/unleash-client-node/issues/382)) - chore(deps): update dependency eslint to v8.30.0 ([#&#8203;367](https://togithub.com/Unleash/unleash-client-node/issues/367)) - chore(deps): update dependency prettier to v2.8.1 ([#&#8203;387](https://togithub.com/Unleash/unleash-client-node/issues/387)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/Unleash/unleash). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xMDUuNCIsInVwZGF0ZWRJblZlciI6IjM0LjExNy4xIn0=--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ivar Conradi Østhus <ivar@getunleash.ai>
2023-02-10 10:51:53 +01:00
const client = new Unleash({
appName: 'proxy',
url: 'unused',
storageProvider: new InMemStorageProvider(),
disableMetrics: true,
repository,
});
client.on(UnleashEvents.Error, (error) => {
this.logger.error(error);
});
fix(deps): update dependency unleash-client to v3.18.0 (#2956) [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [unleash-client](https://togithub.com/Unleash/unleash-client-node) | [`3.16.1` -> `3.18.0`](https://renovatebot.com/diffs/npm/unleash-client/3.16.1/3.18.0) | [![age](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/compatibility-slim/3.16.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/unleash-client/3.18.0/confidence-slim/3.16.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes <details> <summary>Unleash/unleash-client-node</summary> ### [`v3.18.0`](https://togithub.com/Unleash/unleash-client-node/blob/HEAD/CHANGELOG.md#&#8203;3180) [Compare Source](https://togithub.com/Unleash/unleash-client-node/compare/v3.17.0...v3.18.0) feat: gracefully handle unsuccessful metrics post ([#&#8203;414](https://togithub.com/Unleash/unleash-client-node/issues/414)) feat/flush metrics ([#&#8203;415](https://togithub.com/Unleash/unleash-client-node/issues/415)) feat: add metrics jitter support ([#&#8203;412](https://togithub.com/Unleash/unleash-client-node/issues/412)) fix: Allow SDK to startup when backup data is corrupt ([#&#8203;418](https://togithub.com/Unleash/unleash-client-node/issues/418)) fix: flexible-rollout random stickiness is not random enough ([#&#8203;417](https://togithub.com/Unleash/unleash-client-node/issues/417)) fix: build correct version on npm version chore(deps): update dependency eslint-plugin-import to v2.27.5 ([#&#8203;416](https://togithub.com/Unleash/unleash-client-node/issues/416)) chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.48.2 ([#&#8203;413](https://togithub.com/Unleash/unleash-client-node/issues/413)) chore(deps): update dependency eslint to v8.32.0 ([#&#8203;410](https://togithub.com/Unleash/unleash-client-node/issues/410)) chore(deps): update dependency prettier to v2.8.3 ([#&#8203;406](https://togithub.com/Unleash/unleash-client-node/issues/406)) chore(deps): update dependency eslint-plugin-import to v2.27.4 ([#&#8203;404](https://togithub.com/Unleash/unleash-client-node/issues/404)) ### [`v3.17.0`](https://togithub.com/Unleash/unleash-client-node/blob/HEAD/CHANGELOG.md#&#8203;3170) [Compare Source](https://togithub.com/Unleash/unleash-client-node/compare/v3.16.1...v3.17.0) - feat: Only initialize the SDK once. ([#&#8203;368](https://togithub.com/Unleash/unleash-client-node/issues/368)) - fix: upgrade semver to 7.3.8 - fix: add resolution for debug - fix: add resolution for minimatch - fix: add resolution for qs - fix: add resolution for json5 - fix: update yarn.lock - docs: Update the readme with info from docs.getunleash ([#&#8203;399](https://togithub.com/Unleash/unleash-client-node/issues/399)) - docs: minor fix in README - chore(deps): update dependency debug to v4 ([#&#8203;402](https://togithub.com/Unleash/unleash-client-node/issues/402)) - chore(deps): update dependency json5 to v2 ([#&#8203;401](https://togithub.com/Unleash/unleash-client-node/issues/401)) - chore(deps): update dependency eslint to v8.31.0 ([#&#8203;394](https://togithub.com/Unleash/unleash-client-node/issues/394)) - chore(deps): update dependency nock to v13.3.0 ([#&#8203;400](https://togithub.com/Unleash/unleash-client-node/issues/400)) - chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.48.1 ([#&#8203;395](https://togithub.com/Unleash/unleash-client-node/issues/395)) - chore(deps): update dependency eslint-config-prettier to v8.6.0 ([#&#8203;396](https://togithub.com/Unleash/unleash-client-node/issues/396)) - chore(deps): update dependency prettier to v2.8.2 ([#&#8203;398](https://togithub.com/Unleash/unleash-client-node/issues/398)) - chore(deps): update dependency [@&#8203;typescript-eslint/eslint-plugin](https://togithub.com/typescript-eslint/eslint-plugin) to v5.47.1 ([#&#8203;346](https://togithub.com/Unleash/unleash-client-node/issues/346)) - chore(deps): update dependency typescript to v4.9.4 ([#&#8203;386](https://togithub.com/Unleash/unleash-client-node/issues/386)) - chore(deps): update dependency sinon to v15 ([#&#8203;391](https://togithub.com/Unleash/unleash-client-node/issues/391)) - chore(deps): update dependency [@&#8203;types/node](https://togithub.com/types/node) to v18 ([#&#8203;380](https://togithub.com/Unleash/unleash-client-node/issues/380)) - chore(deps): update dependency [@&#8203;types/node](https://togithub.com/types/node) to v14.18.36 ([#&#8203;382](https://togithub.com/Unleash/unleash-client-node/issues/382)) - chore(deps): update dependency eslint to v8.30.0 ([#&#8203;367](https://togithub.com/Unleash/unleash-client-node/issues/367)) - chore(deps): update dependency prettier to v2.8.1 ([#&#8203;387](https://togithub.com/Unleash/unleash-client-node/issues/387)) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/Unleash/unleash). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xMDUuNCIsInVwZGF0ZWRJblZlciI6IjM0LjExNy4xIn0=--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ivar Conradi Østhus <ivar@getunleash.ai>
2023-02-10 10:51:53 +01:00
await client.start();
return client;
}
async deleteClientForProxyToken(secret: string): Promise<void> {
const clientPromise = this.clients.get(secret);
if (clientPromise) {
const client = await clientPromise;
client.destroy();
this.clients.delete(secret);
}
}
stopAll(): void {
this.clients.forEach((promise) => promise.then((c) => c.destroy()));
}
private static assertExpectedTokenType({ type }: ApiUser) {
if (!(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN)) {
throw new InvalidTokenError();
}
}
async setFrontendSettings(
value: FrontendSettings,
createdBy: string,
): Promise<void> {
const error = validateOrigins(value.frontendApiOrigins);
if (error) {
throw new BadDataError(error);
}
await this.services.settingService.insert(
frontendSettingsKey,
value,
createdBy,
);
}
private async fetchFrontendSettings(): Promise<FrontendSettings> {
try {
this.cachedFrontendSettings =
await this.services.settingService.get(frontendSettingsKey, {
frontendApiOrigins: this.config.frontendApiOrigins,
});
} catch (error) {
this.logger.debug('Unable to fetch frontend settings');
}
return this.cachedFrontendSettings;
}
async getFrontendSettings(
useCache: boolean = true,
): Promise<FrontendSettings> {
if (useCache && this.cachedFrontendSettings) {
return this.cachedFrontendSettings;
}
return this.fetchFrontendSettings();
}
destroy(): void {
fix: polling in proxy repository now stops correctly (#3268) ### What This patches two very subtle bugs in the proxy repository that cause it to never actually stop polling the db in the background ## Details - Issue 1 We've recently started to get the following output when running `yarn test`: ` Attempted to log "Error: Unable to acquire a connection at Object.queryBuilder (/home/simon/dev/unleash/node_modules/knex/lib/knex-builder/make-knex.js:111:26)` This seems to occur for every test suite after running the proxy tests and the full stack trace doesn't point to anything related to the running tests that produce this output. Running a `git bisect` points to this commit: https://github.com/Unleash/unleash/commit/6e44a65c58d8e28668f0d3459b62c0ce0b84849a being the culprit but I believe that this may have surfaced the bug rather than causing it. Layering in a few console logs and running Unleash, seems to point to the proxy repository setting up data polling but never actually terminating it when `stop` was called, which is inline with the output here - effectively the tests were continuing to run the polling in the background after the suite had exited and jest freaks out that an async task is running when it shouldn't be. This is easy to reproduce once the console logs are in place in the `dataPolling` function, by running Unleash - creating and deleting a front end token never terminates the poll cycle. I believe the cause here is some subtlety around using async functions with timers - stop was being called, which results in the timer being cleared but a scheduled async call was already on the stack, causing the recursive call to resolve after stop, resurrecting the timer and reinitializing the poll cycle. I've moved the terminating code into the async callback. Which seems to solve the problem here. ## Details - Issue 2 Related to the first issue, when the proxy service stops the underlying Unleash Client, it never actually calls destroy on the client, it only removes it from its internal map. That in turn means that the Client never calls stop on the injected repository, it only removes it from memory. However, the scheduled task is `async` and `unref`, meaning it continues to spin in the background until every other process also exits. This is patched by simply calling destroy on the client when cleaning up ## The Ugly This is really hard to test effectively, mostly because this is an issue caused by internals within NodeJS and async. I've added a test that reads the output from the debug log (and also placed a debug log in the termination code). This also requires the test code to wait until the async task completes. This is horribly fragile so if someone has a better idea on how to prove this I would be a very happy human. The second ugly part is that this is a subtle issue in complex code that really, really needs to work correctly. I'm nervous about making changes here without lots of eyes on this
2023-03-10 09:03:32 +01:00
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}