From 2666125f8f0a5bf394c089dcc53e4fbdd85d73c2 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Tue, 20 Oct 2026 12:41:40 +0000 Subject: [PATCH] feat: diff 50 shadow channels at a time with a four-hour run budget Refs linuxfoundation/lfx-cdp-insights-ops#833 Signed-off-by: Mouad BANI --- .../src/schedules/shadowDiff.ts | 23 +++++---- .../src/workflows/shadowDiff.test.ts | 50 ++++++++++++++++++- .../src/workflows/shadowDiff.ts | 2 +- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/services/apps/connectors_worker/src/schedules/shadowDiff.ts b/services/apps/connectors_worker/src/schedules/shadowDiff.ts index 0209389d7b..2ad2fc97d6 100644 --- a/services/apps/connectors_worker/src/schedules/shadowDiff.ts +++ b/services/apps/connectors_worker/src/schedules/shadowDiff.ts @@ -3,10 +3,20 @@ import { ScheduleAlreadyRunning, ScheduleOverlapPolicy } from '@temporalio/clien import { svc } from '../main' import { shadowDiff } from '../workflows/shadowDiff' +const SCHEDULE_ID = 'connectors-shadow-diff' + +const action = { + type: 'startWorkflow', + workflowType: shadowDiff, + taskQueue: 'connectors', + args: [], + workflowExecutionTimeout: '4 hours', +} as const + export async function scheduleShadowDiff(): Promise { try { await svc.temporal.schedule.create({ - scheduleId: 'connectors-shadow-diff', + scheduleId: SCHEDULE_ID, spec: { calendars: [{ hour: { start: 0, end: 0 } }], }, @@ -14,17 +24,12 @@ export async function scheduleShadowDiff(): Promise { overlap: ScheduleOverlapPolicy.SKIP, catchupWindow: '1 hour', }, - action: { - type: 'startWorkflow', - workflowType: shadowDiff, - taskQueue: 'connectors', - args: [], - workflowExecutionTimeout: '30 minutes', - }, + action, }) } catch (err) { if (err instanceof ScheduleAlreadyRunning) { - svc.log.info('Shadow diff schedule already registered in Temporal.') + await svc.temporal.schedule.getHandle(SCHEDULE_ID).update((prev) => ({ ...prev, action })) + svc.log.info('Shadow diff schedule already registered in Temporal, action reconciled.') } else { throw new Error(err) } diff --git a/services/apps/connectors_worker/src/workflows/shadowDiff.test.ts b/services/apps/connectors_worker/src/workflows/shadowDiff.test.ts index 6e4852c03d..ccc935eac0 100644 --- a/services/apps/connectors_worker/src/workflows/shadowDiff.test.ts +++ b/services/apps/connectors_worker/src/workflows/shadowDiff.test.ts @@ -1,6 +1,20 @@ -import { describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' -import { describeChannelError } from './shadowDiff' +import { CHANNEL_CONCURRENCY, describeChannelError, shadowDiff } from './shadowDiff' + +const mocks = vi.hoisted(() => ({ + listShadowDiffChannels: vi.fn(), + runShadowDiffForChannel: vi.fn(), + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})) + +vi.mock('@temporalio/workflow', () => ({ + proxyActivities: () => ({ + listShadowDiffChannels: mocks.listShadowDiffChannels, + runShadowDiffForChannel: mocks.runShadowDiffForChannel, + }), + log: mocks.log, +})) describe('describeChannelError', () => { it('extracts the message from an Error so operators can see why a channel failed', () => { @@ -18,3 +32,35 @@ describe('describeChannelError', () => { expect(describeChannelError(circular)).toBe(String(circular)) }) }) + +describe('shadowDiff', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('diffs channels 50 at a time and keeps counting after a failed channel', async () => { + const channels = Array.from({ length: 120 }, (_, i) => ({ + channelName: `c${i}`, + integrationId: 'i', + units: [], + })) + mocks.listShadowDiffChannels.mockResolvedValue(channels) + let inFlight = 0 + let peak = 0 + mocks.runShadowDiffForChannel.mockImplementation(async (channel) => { + inFlight += 1 + peak = Math.max(peak, inFlight) + await Promise.resolve() + inFlight -= 1 + if (channel.channelName === 'c7') throw new Error('boom') + return { channelName: channel.channelName, integrationId: 'i', status: 'ok' } + }) + await shadowDiff() + expect(peak).toBe(CHANNEL_CONCURRENCY) + expect(CHANNEL_CONCURRENCY).toBe(50) + expect(mocks.log.info).toHaveBeenCalledWith( + 'shadow diff run complete', + expect.objectContaining({ channelCount: 120, okCount: 119, errorCount: 1 }), + ) + }) +}) diff --git a/services/apps/connectors_worker/src/workflows/shadowDiff.ts b/services/apps/connectors_worker/src/workflows/shadowDiff.ts index 901909cf00..946be64fca 100644 --- a/services/apps/connectors_worker/src/workflows/shadowDiff.ts +++ b/services/apps/connectors_worker/src/workflows/shadowDiff.ts @@ -7,7 +7,7 @@ const activity = proxyActivities({ retry: { maximumAttempts: 3, backoffCoefficient: 2 }, }) -const CHANNEL_CONCURRENCY = 10 +export const CHANNEL_CONCURRENCY = 50 export function describeChannelError(err: unknown): string { if (err instanceof Error) {