diff --git a/packages/cli/src/__tests__/license.test.ts b/packages/cli/src/__tests__/license.test.ts index fb6868e02e..c4170279ce 100644 --- a/packages/cli/src/__tests__/license.test.ts +++ b/packages/cli/src/__tests__/license.test.ts @@ -214,6 +214,64 @@ describe('License', () => { const mainPlan = license.getMainPlan(); expect(mainPlan).toBeUndefined(); }); + + describe('onExpirySoon', () => { + it.each([ + { + instanceType: 'main' as const, + isLeader: true, + shouldReload: false, + description: 'Leader main should not reload', + }, + { + instanceType: 'main' as const, + isLeader: false, + shouldReload: true, + description: 'Follower main should reload', + }, + { + instanceType: 'worker' as const, + isLeader: false, + shouldReload: true, + description: 'Worker should reload', + }, + { + instanceType: 'webhook' as const, + isLeader: false, + shouldReload: true, + description: 'Webhook should reload', + }, + ])('$description', async ({ instanceType, isLeader, shouldReload }) => { + const logger = mockLogger(); + const reloadSpy = jest.spyOn(License.prototype, 'reload').mockResolvedValueOnce(); + const instanceSettings = mock({ instanceType }); + Object.defineProperty(instanceSettings, 'isLeader', { get: () => isLeader }); + + license = new License( + logger, + instanceSettings, + mock(), + mock(), + mock({ license: licenseConfig }), + ); + + await license.init(); + + const licenseManager = LicenseManager as jest.MockedClass; + const calls = licenseManager.mock.calls; + const licenseManagerCall = calls[calls.length - 1][0]; + const onExpirySoon = licenseManagerCall.onExpirySoon; + + if (shouldReload) { + expect(onExpirySoon).toBeDefined(); + onExpirySoon!(); + expect(reloadSpy).toHaveBeenCalled(); + } else { + expect(onExpirySoon).toBeUndefined(); + expect(reloadSpy).not.toHaveBeenCalled(); + } + }); + }); }); describe('License', () => { diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index eec82fe9e8..fb6fb111e2 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -78,6 +78,8 @@ export class License implements LicenseProvider { const collectPassthroughData = isMainInstance ? async () => await this.licenseMetricsService.collectPassthroughData() : async () => ({}); + const onExpirySoon = !this.instanceSettings.isLeader ? () => this.onExpirySoon() : undefined; + const expirySoonOffsetMins = !this.instanceSettings.isLeader ? 120 : undefined; const { isLeader } = this.instanceSettings; const { autoRenewalEnabled } = this.globalConfig.license; @@ -107,6 +109,8 @@ export class License implements LicenseProvider { collectPassthroughData, onFeatureChange, onLicenseRenewed, + onExpirySoon, + expirySoonOffsetMins, }); await this.manager.initialize(); @@ -433,4 +437,20 @@ export class License implements LicenseProvider { disableAutoRenewals() { this.manager?.disableAutoRenewals(); } + + private onExpirySoon() { + this.logger.info('License is about to expire soon, reloading license...'); + + // reload in background to avoid blocking SDK + + void this.reload() + .then(() => { + this.logger.info('Reloaded license on expiry soon'); + }) + .catch((error) => { + this.logger.error('Failed to reload license on expiry soon', { + error: error instanceof Error ? error.message : error, + }); + }); + } }