From b3fab1fb0dfa441c99a98aaca996bb368d279fe5 Mon Sep 17 00:00:00 2001 From: Evgeniy Shangin Date: Tue, 25 Jul 2023 13:21:29 +0300 Subject: [PATCH] feat: add dynamic config pooler --- src/index.ts | 3 +- src/lib/context.ts | 6 ++- src/lib/dynamic-config-poller.ts | 68 ++++++++++++++++++++++++++++++++ src/nodekit.ts | 5 +++ src/types.ts | 2 + 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/lib/dynamic-config-poller.ts diff --git a/src/index.ts b/src/index.ts index a119eaa..48eae63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ export {NodeKit} from './nodekit'; export {AppContext} from './lib/context'; -export {AppConfig} from './types'; -export {AppContextParams} from './types'; +export {AppConfig, AppContextParams, AppDynamicConfig} from './types'; export {AppError} from './lib/app-error'; diff --git a/src/lib/context.ts b/src/lib/context.ts index 70500b2..122cf0a 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -2,7 +2,7 @@ import pino from 'pino'; import {JaegerTracer} from 'jaeger-client'; import {Span, Tags, SpanContext, FORMAT_HTTP_HEADERS} from 'opentracing'; import {NodeKit} from '../nodekit'; -import {AppConfig, AppContextParams, Dict} from '../types'; +import {AppConfig, AppContextParams, Dict, AppDynamicConfig} from '../types'; import {AppError} from './app-error'; import {extractErrorInfo} from './error-parser'; import {IncomingHttpHeaders} from 'http'; @@ -17,6 +17,7 @@ interface ContextInitialParams { stats: AppTelemetrySendStats; parentSpanContext?: SpanContext; utils: NodeKit['utils']; + dynamicConfig?: AppDynamicConfig; loggerPostfix?: string; tags?: Dict; } @@ -42,6 +43,7 @@ export class AppContext { parentContext?: AppContext; utils: NodeKit['utils']; stats: AppTelemetrySendStats; + dynamicConfig: AppDynamicConfig; protected appParams: AppContextParams; protected name: string; @@ -62,6 +64,7 @@ export class AppContext { this.logger = params.parentContext.logger; this.tracer = params.parentContext.tracer; this.utils = params.parentContext.utils; + this.dynamicConfig = params.parentContext.dynamicConfig; this.appParams = Object.assign({}, params.parentContext?.appParams); this.loggerPrefix = `${params.parentContext.loggerPrefix} [${this.name}]`.trim(); this.loggerPostfix = params.loggerPostfix || params.parentContext.loggerPostfix; @@ -77,6 +80,7 @@ export class AppContext { this.logger = params.logger; this.tracer = params.tracer; this.utils = params.utils; + this.dynamicConfig = {}; this.loggerPrefix = ''; this.loggerPostfix = params.loggerPostfix || ''; this.stats = params.stats; diff --git a/src/lib/dynamic-config-poller.ts b/src/lib/dynamic-config-poller.ts new file mode 100644 index 0000000..268e588 --- /dev/null +++ b/src/lib/dynamic-config-poller.ts @@ -0,0 +1,68 @@ +import axios, {AxiosError} from 'axios'; +import type {AppContext} from './context'; + +const DYNAMIC_CONFIG_POLL_INTERVAL = 30000; + +export interface DynamicConfigSetup { + url: string; + interval?: number; +} + +export class DynamicConfigPoller { + ctx: AppContext; + + private namespace: string; + private dynamicConfigSetup: DynamicConfigSetup; + + constructor(ctx: AppContext, namespace: string, dynamicConfigSetup: DynamicConfigSetup) { + this.ctx = ctx; + this.namespace = namespace; + this.dynamicConfigSetup = dynamicConfigSetup; + } + + startPolling = () => { + const {dynamicConfigSetup, namespace} = this; + + if (process.env.APP_DEBUG_DYNAMIC_CONFIG) { + this.ctx.log('Dynamic config: fetching started', { + namespace, + }); + } + + axios + .get(`${dynamicConfigSetup.url}?cacheInvalidation=${Date.now()}`) + .then(this.onSuccess, this.onError); + }; + + private getPollTimeout() { + return this.dynamicConfigSetup.interval || DYNAMIC_CONFIG_POLL_INTERVAL; + } + + private onSuccess = (response: {data: Record}) => { + const {namespace} = this; + + if (process.env.APP_DEBUG_DYNAMIC_CONFIG) { + this.ctx.log('Dynamic config: fetch complete', { + oldDynamicConfig: (this.ctx.dynamicConfig as Record)[namespace], + fetchedDynamicConfig: response.data, + namespace, + }); + } + + (this.ctx.dynamicConfig as Record)[namespace] = response.data; + + setTimeout(this.startPolling, this.getPollTimeout()); + }; + + private onError = (error: AxiosError) => { + const timeout = this.getPollTimeout(); + const {namespace} = this; + + this.ctx.logError('Dynamic config: fetch failed', error, { + timeout, + namespace, + }); + + setTimeout(this.startPolling, timeout); + }; +} diff --git a/src/nodekit.ts b/src/nodekit.ts index c7a2840..8e6ad80 100644 --- a/src/nodekit.ts +++ b/src/nodekit.ts @@ -12,6 +12,7 @@ import { SensitiveKeysRedacter, } from './lib/utils/redact-sensitive-keys'; import {prepareClickhouseClient} from './lib/telemetry/clickhouse'; +import {DynamicConfigSetup, DynamicConfigPoller} from './lib/dynamic-config-poller'; interface InitOptions { disableDotEnv?: boolean; @@ -104,6 +105,10 @@ export class NodeKit { this.shutdownHandlers.push(handler); } + setupDynamicConfig(namespace: string, dynamicConfigSetup: DynamicConfigSetup) { + new DynamicConfigPoller(this.ctx, namespace, dynamicConfigSetup).startPolling(); + } + private setupShutdownSignals() { const signals = ['SIGTERM', 'SIGINT'] as const; diff --git a/src/types.ts b/src/types.ts index 3c7d0f9..bc36004 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,8 @@ export interface AppConfig { export interface AppContextParams {} +export interface AppDynamicConfig {} + export type Dict = {[key: string]: unknown}; export interface ShutdownHandler {