diff --git a/src/azureMonitor/metrics/quickpulse/liveMetrics.ts b/src/azureMonitor/metrics/quickpulse/liveMetrics.ts index ee5bf77..5428b70 100644 --- a/src/azureMonitor/metrics/quickpulse/liveMetrics.ts +++ b/src/azureMonitor/metrics/quickpulse/liveMetrics.ts @@ -137,6 +137,7 @@ export class LiveMetrics { private derivedMetricProjection: Projection = new Projection(); private validator: Validator = new Validator(); private filter: Filter = new Filter(); + private _isShutdown: boolean = false; // type: Map> private validDocumentFilterConjuctionGroupInfos: Map< string, @@ -203,10 +204,15 @@ export class LiveMetrics { } public shutdown(): void { - this.meterProvider?.shutdown(); + this._isShutdown = true; + clearTimeout(this.handle as any); + this.deactivateMetrics(); } private async goQuickpulse(): Promise { + if (this._isShutdown) { + return; + } if (!this.isCollectingData) { // If not collecting, Ping try { @@ -223,15 +229,20 @@ export class LiveMetrics { this.quickPulseDone(undefined); } - this.handle = setTimeout(this.goQuickpulse.bind(this), this.pingInterval); - this.handle.unref(); + if (!this._isShutdown) { + this.handle = setTimeout(this.goQuickpulse.bind(this), this.pingInterval); + this.handle.unref(); + } } - if (this.isCollectingData) { + if (this.isCollectingData && !this._isShutdown) { this.activateMetrics({ collectionInterval: this.postInterval }); } } private async quickPulseDone(response: QuickpulseResponse | undefined): Promise { + if (this._isShutdown) { + return; + } if (!response) { if (!this.isCollectingData) { if (Date.now() - this.lastSuccessTime >= MAX_PING_WAIT_TIME) { @@ -259,8 +270,10 @@ export class LiveMetrics { this.etag = ""; this.deactivateMetrics(); - this.handle = setTimeout(this.goQuickpulse.bind(this), this.pingInterval); - this.handle.unref(); + if (!this._isShutdown) { + this.handle = setTimeout(this.goQuickpulse.bind(this), this.pingInterval); + this.handle.unref(); + } } const endpointRedirect = response.xMsQpsServiceEndpointRedirectV2; diff --git a/test/internal/unit/metrics/liveMetrics.test.ts b/test/internal/unit/metrics/liveMetrics.test.ts index 44857c2..e203393 100644 --- a/test/internal/unit/metrics/liveMetrics.test.ts +++ b/test/internal/unit/metrics/liveMetrics.test.ts @@ -671,4 +671,69 @@ describe("#LiveMetrics", () => { ["testScope1"], ); }); + + it("shutdown should clear the polling timer", () => { + const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); + const handle = autoCollect["handle"]; + autoCollect.shutdown(); + expect(clearTimeoutSpy).toHaveBeenCalledWith(handle); + clearTimeoutSpy.mockRestore(); + }); + + it("shutdown should set _isShutdown flag", () => { + expect(autoCollect["_isShutdown"]).toBe(false); + autoCollect.shutdown(); + expect(autoCollect["_isShutdown"]).toBe(true); + }); + + it("shutdown should deactivate metrics", () => { + autoCollect.activateMetrics({ collectionInterval: 100 }); + expect(autoCollect["meterProvider"]).toBeDefined(); + autoCollect.shutdown(); + expect(autoCollect["meterProvider"]).toBeUndefined(); + }); + + it("goQuickpulse should not reschedule after shutdown", async () => { + autoCollect.shutdown(); + const setTimeoutSpy = vi.spyOn(global, "setTimeout"); + await autoCollect["goQuickpulse"](); + // goQuickpulse should early-return without scheduling a new timer + expect(setTimeoutSpy).not.toHaveBeenCalled(); + setTimeoutSpy.mockRestore(); + }); + + it("goQuickpulse should not reschedule if shutdown called during ping", async () => { + // Simulate shutdown happening while the ping request is in-flight + vi.spyOn(autoCollect["pingSender"], "isSubscribed").mockImplementation(async () => { + // Shutdown is called while we are awaiting the ping + autoCollect.shutdown(); + return undefined as any; + }); + autoCollect["_isShutdown"] = false; // reset so goQuickpulse enters the body + autoCollect["isCollectingData"] = false; + const setTimeoutSpy = vi.spyOn(global, "setTimeout"); + await autoCollect["goQuickpulse"](); + expect(setTimeoutSpy).not.toHaveBeenCalled(); + setTimeoutSpy.mockRestore(); + }); + + it("quickPulseDone should not reschedule after shutdown", async () => { + autoCollect.shutdown(); + const setTimeoutSpy = vi.spyOn(global, "setTimeout"); + await autoCollect["quickPulseDone"]({ + xMsQpsSubscribed: "false", + xMsQpsConfigurationEtag: undefined, + } as any); + expect(setTimeoutSpy).not.toHaveBeenCalled(); + setTimeoutSpy.mockRestore(); + }); + + it("shutdown should be idempotent", () => { + autoCollect.activateMetrics({ collectionInterval: 100 }); + autoCollect.shutdown(); + // Second shutdown should not throw + expect(() => autoCollect.shutdown()).not.toThrow(); + expect(autoCollect["_isShutdown"]).toBe(true); + expect(autoCollect["meterProvider"]).toBeUndefined(); + }); });