diff --git a/services/cloud-agent-next/src/server.test.ts b/services/cloud-agent-next/src/server.test.ts index 1b7c10587f..498a2cb3ef 100644 --- a/services/cloud-agent-next/src/server.test.ts +++ b/services/cloud-agent-next/src/server.test.ts @@ -151,6 +151,7 @@ const { default: worker, REPORT_RETENTION_CRON, OUTCOME_AGGREGATE_CRON, + OUTCOME_AGGREGATE_CRONS, } = await import('./server.js'); const secret = 'test-secret'; @@ -424,6 +425,26 @@ describe('server background reporting', () => { expect(removeExpiredCloudAgentReportDataMock).not.toHaveBeenCalled(); }); + it('runs the collections on the tolerated legacy 3-minute cron', async () => { + const env = createEnv(); + const scheduledTime = Date.parse('2026-02-01T00:10:42.000Z'); + + await worker.scheduled( + { cron: '*/3 * * * *', scheduledTime } as ScheduledController, + env as unknown as Env + ); + + expect(runCloudAgentOutcomeCollectionMock).toHaveBeenCalledTimes(1); + expect(runCloudAgentOutcomeCollectionMock).toHaveBeenCalledWith( + env, + expect.any(Date), + scheduledTime + ); + expect(runCloudAgentOpenStockCollectionMock).toHaveBeenCalledTimes(1); + expect(runCloudAgentOpenStockCollectionMock).toHaveBeenCalledWith(env); + expect(loggerWarnMock).not.toHaveBeenCalled(); + }); + it('still runs the open-stock collection and preserves the outcome error when outcome collection rejects', async () => { const env = createEnv(); const outcomeError = new Error('outcome collection failed'); @@ -464,6 +485,7 @@ describe('server background reporting', () => { expect((config.triggers?.crons ?? []).slice().sort()).toEqual(expected); expect((config.env?.dev?.triggers?.crons ?? []).slice().sort()).toEqual(expected); expect(OUTCOME_AGGREGATE_CRON).toBe('*/5 * * * *'); + expect([...OUTCOME_AGGREGATE_CRONS]).toEqual([OUTCOME_AGGREGATE_CRON, '*/3 * * * *']); }); }); diff --git a/services/cloud-agent-next/src/server.ts b/services/cloud-agent-next/src/server.ts index a4b7415cce..4e82799442 100644 --- a/services/cloud-agent-next/src/server.ts +++ b/services/cloud-agent-next/src/server.ts @@ -1096,6 +1096,10 @@ app.onError(createErrorHandler(logger, { includeMessage: false })); export const REPORT_RETENTION_CRON = '17 2 * * *'; export const OUTCOME_AGGREGATE_CRON = '*/5 * * * *'; +// Cloudflare has been observed to keep firing a previously configured `*/3` trigger after a +// deploy changed it to `*/5`. Accept both so a stale trigger cannot silently disable collection; +// the collector floors `scheduledTime` to 5-minute buckets, so extra ticks only repeat a bucket. +export const OUTCOME_AGGREGATE_CRONS = new Set([OUTCOME_AGGREGATE_CRON, '*/3 * * * *']); export default { fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise { @@ -1125,7 +1129,7 @@ export default { await removeExpiredCloudAgentReportData(env); return; } - if (controller.cron === OUTCOME_AGGREGATE_CRON) { + if (OUTCOME_AGGREGATE_CRONS.has(controller.cron)) { try { await runCloudAgentOutcomeCollection(env, new Date(), controller.scheduledTime); } finally {