From 9906f2ad23cab8b08252ebf327451fcf661b86cf Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Wed, 6 Mar 2024 11:12:15 +0100 Subject: [PATCH] fix: debounce kubernetes listeners channels individually (#6280) * fix: debounce channels individually Signed-off-by: Philippe Martin * refactor: dispatch Signed-off-by: Philippe Martin --- .../plugin/kubernetes-context-state.spec.ts | 88 +++++++++++++++++++ .../src/plugin/kubernetes-context-state.ts | 74 +++++++++------- 2 files changed, 131 insertions(+), 31 deletions(-) diff --git a/packages/main/src/plugin/kubernetes-context-state.spec.ts b/packages/main/src/plugin/kubernetes-context-state.spec.ts index bfc2b0bb8b55a..020ed35288d82 100644 --- a/packages/main/src/plugin/kubernetes-context-state.spec.ts +++ b/packages/main/src/plugin/kubernetes-context-state.spec.ts @@ -1383,6 +1383,94 @@ test('changing context should not start service informer on current context if n expect(makeInformerMock).not.toHaveBeenCalled(); }); +test('should not ignore events sent a short time before', async () => { + vi.useFakeTimers(); + vi.mocked(makeInformer).mockImplementation( + ( + kubeconfig: kubeclient.KubeConfig, + path: string, + _listPromiseFn: kubeclient.ListPromise, + ) => { + const connectResult = undefined; + switch (path) { + case '/apis/networking.k8s.io/v1/namespaces/ns1/ingresses': + return new FakeInformer( + kubeconfig.currentContext, + path, + 0, + connectResult, + [ + { + delayMs: 1, + verb: 'add', + object: {}, + }, + ], + [], + ); + case '/api/v1/namespaces/ns1/services': + return new FakeInformer( + kubeconfig.currentContext, + path, + 0, + connectResult, + [ + { + delayMs: 2, + verb: 'add', + object: {}, + }, + ], + [], + ); + } + return new FakeInformer(kubeconfig.currentContext, path, 0, connectResult, [], []); + }, + ); + const client = new ContextsManager(apiSender); + const kubeConfig = new kubeclient.KubeConfig(); + const config = { + clusters: [ + { + name: 'cluster1', + server: 'server1', + }, + ], + users: [ + { + name: 'user1', + }, + ], + contexts: [ + { + name: 'context1', + cluster: 'cluster1', + user: 'user1', + namespace: 'ns1', + }, + { + name: 'context2', + cluster: 'cluster1', + user: 'user1', + namespace: 'ns2', + }, + ], + currentContext: 'context1', + }; + kubeConfig.loadFromOptions(config); + client.registerGetCurrentContextResources('services'); + client.registerGetCurrentContextResources('ingresses'); + await client.update(kubeConfig); + vi.advanceTimersByTime(20); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-contexts-general-state-update', expect.anything()); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-general-state-update', expect.anything()); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-pods-update', []); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-deployments-update', []); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-services-update', [{}]); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-ingresses-update', [{}]); + expect(apiSenderSendMock).toHaveBeenCalledWith('kubernetes-current-context-routes-update', []); +}); + describe('ContextsStates tests', () => { test('hasInformer should check if informer exists for context', () => { const client = new ContextsStates(); diff --git a/packages/main/src/plugin/kubernetes-context-state.ts b/packages/main/src/plugin/kubernetes-context-state.ts index 3ddb9ea325009..b615861a92a20 100644 --- a/packages/main/src/plugin/kubernetes-context-state.ts +++ b/packages/main/src/plugin/kubernetes-context-state.ts @@ -772,51 +772,63 @@ export class ContextsManager { }); } - dispatchTimer: NodeJS.Timeout | undefined; + dispatchContextsGeneralStateTimer: NodeJS.Timeout | undefined; + dispatchCurrentContextGeneralStateTimer: NodeJS.Timeout | undefined; + dispatchCurrentContextResourceTimers = new Map(); + private dispatch(options: DispatchOptions): void { - clearTimeout(this.dispatchTimer); - this.dispatchTimer = setTimeout(() => { - if (options.contextsGeneralState) { - this.dispatchContextsGeneralState(); - } - if (options.currentContextGeneralState) { - this.dispatchCurrentContextGeneralState(); + if (options.contextsGeneralState) { + this.dispatchDebounce( + `kubernetes-contexts-general-state-update`, + this.states.getContextsGeneralState(), + this.dispatchContextsGeneralStateTimer, + dispatchTimeout, + ); + } + if (options.currentContextGeneralState) { + this.dispatchCurrentContextGeneralStateTimer = this.dispatchDebounce( + `kubernetes-current-context-general-state-update`, + this.states.getCurrentContextGeneralState(this.kubeConfig.currentContext), + this.dispatchCurrentContextGeneralStateTimer, + dispatchTimeout, + ); + } + Object.keys(options.resources).forEach(res => { + const resname = res as ResourceName; + if (options.resources[resname]) { + this.dispatchCurrentContextResourceTimers.set( + resname, + this.dispatchDebounce( + `kubernetes-current-context-${resname}-update`, + this.states.getContextResources(this.kubeConfig.currentContext, resname), + this.dispatchCurrentContextResourceTimers.get(resname), + dispatchTimeout, + ), + ); } - Object.keys(options.resources).forEach(res => { - const resname = res as ResourceName; - if (options.resources[resname]) { - this.dispatchCurrentContextResource(resname); - } - }); - }, dispatchTimeout); + }); } - private dispatchContextsGeneralState(): void { - this.apiSender.send(`kubernetes-contexts-general-state-update`, this.states.getContextsGeneralState()); + private dispatchDebounce( + eventName: string, + value: Map | ContextGeneralState | KubernetesObject[], + timer: NodeJS.Timeout | undefined, + timeout: number, + ): NodeJS.Timeout { + clearTimeout(timer); + return setTimeout(() => { + this.apiSender.send(eventName, value); + }, timeout); } public getContextsGeneralState(): Map { return this.states.getContextsGeneralState(); } - private dispatchCurrentContextGeneralState(): void { - this.apiSender.send( - `kubernetes-current-context-general-state-update`, - this.states.getCurrentContextGeneralState(this.kubeConfig.currentContext), - ); - } - public getCurrentContextGeneralState(): ContextGeneralState { return this.states.getCurrentContextGeneralState(this.kubeConfig.currentContext); } - private dispatchCurrentContextResource(resourceName: ResourceName): void { - this.apiSender.send( - `kubernetes-current-context-${resourceName}-update`, - this.states.getContextResources(this.kubeConfig.currentContext, resourceName), - ); - } - public registerGetCurrentContextResources(resourceName: ResourceName): KubernetesObject[] { if (isSecondaryResourceName(resourceName)) { this.secondaryWatchers.subscribe(resourceName);