From 6ef2c4f3797e4ef964d064875e72b022787a600f Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Thu, 13 Aug 2026 12:22:00 -0400 Subject: [PATCH 1/2] chore: schedule FDv2 recovery orchestration after fallback TTL expires --- .../datasource/fdv2/FDv2DataSource.test.ts | 176 ++++++++++++++++++ .../src/datasource/fdv2/FDv2DataSource.ts | 47 ++++- 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts index 815ec9261f..6f2e1774b4 100644 --- a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts +++ b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts @@ -1180,3 +1180,179 @@ it('stops initializer chain when a transfer-none changeSet triggers fdv1 fallbac expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); ds.close(); }); + +// -- FDv2 recovery after TTL (regression: SDK-2617) -- + +it('re-engages FDv2 after the fdv1 fallback TTL expires', async () => { + const dataCallback = jest.fn(); + const statusManager = makeStatusManager(); + const logger = makeLogger(); + + const fdv2Payload = makePayload({ state: 'fdv2-selector' }); + const fdv1Payload = makePayload({ state: 'fdv1-selector' }); + const fdv2RecoveryPayload = makePayload({ state: 'fdv2-recovered' }); + + let fdv2Created = 0; + const fdv2Factory = jest.fn(() => { + fdv2Created += 1; + return fdv2Created === 1 + ? makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 50 })]) + : makeMockSynchronizer([changeSet(fdv2RecoveryPayload, { fdv1Fallback: false })]); + }); + const fdv1Sync = makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]); + + const slots: SynchronizerSlot[] = [ + createSynchronizerSlot({ create: fdv2Factory }), + createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), + ]; + + const ds = createFDv2DataSource({ + initializerFactories: [], + synchronizerSlots: slots, + dataCallback, + statusManager, + selectorGetter: noSelector, + logger, + }); + + await ds.start(); + + await statusManager.waitForState('VALID', 2); + expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); + + await statusManager.waitForState('VALID', 3); + expect(dataCallback).toHaveBeenCalledWith(fdv2RecoveryPayload); + expect(fdv2Factory).toHaveBeenCalledTimes(2); + + ds.close(); +}); + +it('does not re-engage FDv2 when the fdv1 fallback TTL is 0 (indefinite)', async () => { + const dataCallback = jest.fn(); + const statusManager = makeStatusManager(); + const logger = makeLogger(); + + const fdv2Payload = makePayload({ state: 'fdv2-selector' }); + const fdv1Payload = makePayload({ state: 'fdv1-selector' }); + + let fdv2Created = 0; + const fdv2Factory = jest.fn(() => { + fdv2Created += 1; + return makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 0 })]); + }); + const fdv1Sync = makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]); + + const slots: SynchronizerSlot[] = [ + createSynchronizerSlot({ create: fdv2Factory }), + createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), + ]; + + const ds = createFDv2DataSource({ + initializerFactories: [], + synchronizerSlots: slots, + dataCallback, + statusManager, + selectorGetter: noSelector, + logger, + }); + + await ds.start(); + await statusManager.waitForState('VALID', 2); + expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); + + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + expect(fdv2Created).toBe(1); + + ds.close(); +}); + +it('does not re-trigger fallback when the fdv1 synchronizer itself yields a fallback-flagged result', async () => { + const dataCallback = jest.fn(); + const statusManager = makeStatusManager(); + const logger = makeLogger(); + + const fdv2Payload = makePayload({ state: 'fdv2-selector' }); + const fdv1PayloadA = makePayload({ state: 'fdv1-a' }); + const fdv1PayloadB = makePayload({ state: 'fdv1-b' }); + + let fdv2Created = 0; + const fdv2Factory = jest.fn(() => { + fdv2Created += 1; + return makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 0 })]); + }); + const fdv1Sync = makeMockSynchronizer([ + changeSet(fdv1PayloadA, { fdv1Fallback: true }), + changeSet(fdv1PayloadB, { fdv1Fallback: false }), + ]); + + const slots: SynchronizerSlot[] = [ + createSynchronizerSlot({ create: fdv2Factory }), + createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), + ]; + + const ds = createFDv2DataSource({ + initializerFactories: [], + synchronizerSlots: slots, + dataCallback, + statusManager, + selectorGetter: noSelector, + logger, + }); + + await ds.start(); + + await statusManager.waitForState('VALID', 3); + expect(dataCallback).toHaveBeenCalledWith(fdv1PayloadA); + expect(dataCallback).toHaveBeenCalledWith(fdv1PayloadB); + expect(fdv2Created).toBe(1); + + ds.close(); +}); + +it('uses a default recovery interval (not zero) when fdv1FallbackTtlMs is absent (regression: SDK-2617 §8.3.2)', async () => { + // §8.3.2: when x-ld-fd-fallback-ttl header is absent, the SDK MUST use a default + // retry interval of 1 hour. Absence must NOT suppress recovery (unlike TTL=0). + const dataCallback = jest.fn(); + const statusManager = makeStatusManager(); + const fdv2Payload = makePayload({ state: 'fdv2-selector' }); + const fdv1Payload = makePayload({ state: 'fdv1-selector' }); + const fdv2RecoveryPayload = makePayload({ state: 'fdv2-recovered' }); + + let fdv2Created = 0; + const fdv2Factory = jest.fn(() => { + fdv2Created += 1; + // changeSet with fdv1Fallback=true and no TTL (undefined) — should use 1-hour default + return fdv2Created === 1 + ? makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true })]) + : makeMockSynchronizer([changeSet(fdv2RecoveryPayload, { fdv1Fallback: false })]); + }); + + const slots: SynchronizerSlot[] = [ + createSynchronizerSlot({ create: fdv2Factory }), + createSynchronizerSlot( + { create: () => makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]) }, + { isFDv1Fallback: true }, + ), + ]; + + const ds = createFDv2DataSource({ + initializerFactories: [], + synchronizerSlots: slots, + dataCallback, + statusManager, + selectorGetter: noSelector, + fdv2RecoveryTimeoutMs: 50, + }); + + await ds.start(); + await statusManager.waitForState('VALID', 2); + expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); + // Recovery must be scheduled (not skipped as with TTL=0). + await statusManager.waitForState('VALID', 3); + expect(dataCallback).toHaveBeenCalledWith(fdv2RecoveryPayload); + expect(fdv2Created).toBe(2); + + ds.close(); +}); diff --git a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts index fba90989a7..d8ed7d81ea 100644 --- a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts +++ b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts @@ -2,8 +2,10 @@ import { internal, LDLogger } from '@launchdarkly/js-sdk-common'; import { DataSourceStatusManager } from '../DataSourceStatusManager'; import { + Condition, ConditionGroup, ConditionType, + createFDv2RecoveryCondition, DEFAULT_FALLBACK_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, getConditions, @@ -11,6 +13,9 @@ import { import { ChangeSetResult, FDv2SourceResult, StatusResult } from './FDv2SourceResult'; import { createSourceManager, InitializerFactory, SynchronizerSlot } from './SourceManager'; +/** Default time to remain on FDv1 before attempting FDv2 recovery (1 hour). */ +export const DEFAULT_FDV2_RECOVERY_TIMEOUT_MS = 60 * 60 * 1000; + /** * Callback invoked when the orchestrator produces a changeSet payload. */ @@ -47,6 +52,9 @@ export interface FDv2DataSourceConfig { /** Recovery condition timeout in ms (default 300s). */ recoveryTimeoutMs?: number; + + /** FDv2 recovery timeout override in ms. When absent, DEFAULT_FDV2_RECOVERY_TIMEOUT_MS is used. */ + fdv2RecoveryTimeoutMs?: number; } /** @@ -83,11 +91,13 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour logger, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS, + fdv2RecoveryTimeoutMs, } = config; let initialized = false; let closed = false; let dataReceived = false; + let pendingFdv1FallbackTtlMs: number | undefined; let initResolve: (() => void) | undefined; let initReject: ((err: Error) => void) | undefined; @@ -132,7 +142,15 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } function handleFdv1Fallback(result: FDv2SourceResult): boolean { + // Guard: if the FDv1 fallback synchronizer itself produces a result flagged + // fdv1Fallback, do not re-run the fallback machinery - we are already on FDv1. + if (sourceManager.isCurrentSynchronizerFDv1Fallback) { + return false; + } if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) { + // Remember the TTL (including 0 = indefinite) so runSynchronizers can + // schedule FDv2 recovery once the FDv1 synchronizer is active. + pendingFdv1FallbackTtlMs = result.fdv1FallbackTtlMs; sourceManager.fdv1Fallback(); return true; } @@ -255,7 +273,22 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour logger?.debug('Fallback condition active for current synchronizer.'); } - // try/finally ensures conditions are closed on all code paths. + // When the FDv1 fallback synchronizer is active, schedule an FDv2 recovery + // attempt after the fallback TTL. A TTL of 0 means indefinite fallback; + // no recovery condition is created. An absent TTL uses the 1-hour default. + let fdv2RecoveryCondition: Condition | undefined; + if (sourceManager.isCurrentSynchronizerFDv1Fallback) { + // Priority: server-supplied TTL (from the fallback directive) wins; + // fall back to the caller-configured override, then the 1-hour hardcoded default. + const ttlMs = pendingFdv1FallbackTtlMs ?? fdv2RecoveryTimeoutMs ?? DEFAULT_FDV2_RECOVERY_TIMEOUT_MS; + pendingFdv1FallbackTtlMs = undefined; + if (ttlMs !== 0) { + fdv2RecoveryCondition = createFDv2RecoveryCondition(ttlMs); + logger?.debug(`FDv2 recovery scheduled in ${ttlMs} ms.`); + } + } + + // Conditions hold timers; close them even if the inner loop throws or breaks early. let synchronizerRunning = true; try { while (!closed && synchronizerRunning) { @@ -269,6 +302,14 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour conditions.promise.then((value) => ({ source: 'condition' as const, value })), ); } + if (fdv2RecoveryCondition !== undefined) { + racers.push( + fdv2RecoveryCondition.promise.then((value) => ({ + source: 'condition' as const, + value, + })), + ); + } // eslint-disable-next-line no-await-in-loop const winner = await Promise.race(racers); @@ -284,6 +325,9 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } else if (conditionType === 'recovery') { logger?.info('Recovery condition fired, resetting to primary synchronizer.'); sourceManager.resetSourceIndex(); + } else if (conditionType === 'fdv2Recovery') { + logger?.info('FDv2 recovery timer fired, re-engaging FDv2 synchronizers.'); + sourceManager.fdv2Recovery(); } synchronizerRunning = false; @@ -331,6 +375,7 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } } finally { conditions.close(); + fdv2RecoveryCondition?.close(); } } } From 63d2f3dacabe344f5d9674704ba9436ae0fc500f Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Tue, 18 Aug 2026 16:42:46 -0400 Subject: [PATCH 2/2] chore: remove dead FDv2 recovery condition wiring createFDv2RecoveryCondition was removed from Conditions.ts in PR #1856; this drops the now-broken caller and its config/tracking from FDv2DataSource.ts. --- .../datasource/fdv2/FDv2DataSource.test.ts | 132 +----------------- .../src/datasource/fdv2/FDv2DataSource.ts | 40 ------ 2 files changed, 1 insertion(+), 171 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts index 6f2e1774b4..285ee0833f 100644 --- a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts +++ b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts @@ -1181,92 +1181,7 @@ it('stops initializer chain when a transfer-none changeSet triggers fdv1 fallbac ds.close(); }); -// -- FDv2 recovery after TTL (regression: SDK-2617) -- - -it('re-engages FDv2 after the fdv1 fallback TTL expires', async () => { - const dataCallback = jest.fn(); - const statusManager = makeStatusManager(); - const logger = makeLogger(); - - const fdv2Payload = makePayload({ state: 'fdv2-selector' }); - const fdv1Payload = makePayload({ state: 'fdv1-selector' }); - const fdv2RecoveryPayload = makePayload({ state: 'fdv2-recovered' }); - - let fdv2Created = 0; - const fdv2Factory = jest.fn(() => { - fdv2Created += 1; - return fdv2Created === 1 - ? makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 50 })]) - : makeMockSynchronizer([changeSet(fdv2RecoveryPayload, { fdv1Fallback: false })]); - }); - const fdv1Sync = makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]); - - const slots: SynchronizerSlot[] = [ - createSynchronizerSlot({ create: fdv2Factory }), - createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), - ]; - - const ds = createFDv2DataSource({ - initializerFactories: [], - synchronizerSlots: slots, - dataCallback, - statusManager, - selectorGetter: noSelector, - logger, - }); - - await ds.start(); - - await statusManager.waitForState('VALID', 2); - expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); - - await statusManager.waitForState('VALID', 3); - expect(dataCallback).toHaveBeenCalledWith(fdv2RecoveryPayload); - expect(fdv2Factory).toHaveBeenCalledTimes(2); - - ds.close(); -}); - -it('does not re-engage FDv2 when the fdv1 fallback TTL is 0 (indefinite)', async () => { - const dataCallback = jest.fn(); - const statusManager = makeStatusManager(); - const logger = makeLogger(); - - const fdv2Payload = makePayload({ state: 'fdv2-selector' }); - const fdv1Payload = makePayload({ state: 'fdv1-selector' }); - - let fdv2Created = 0; - const fdv2Factory = jest.fn(() => { - fdv2Created += 1; - return makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 0 })]); - }); - const fdv1Sync = makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]); - - const slots: SynchronizerSlot[] = [ - createSynchronizerSlot({ create: fdv2Factory }), - createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), - ]; - - const ds = createFDv2DataSource({ - initializerFactories: [], - synchronizerSlots: slots, - dataCallback, - statusManager, - selectorGetter: noSelector, - logger, - }); - - await ds.start(); - await statusManager.waitForState('VALID', 2); - expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); - - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - expect(fdv2Created).toBe(1); - - ds.close(); -}); +// -- FDv1 fallback re-trigger guard (regression: SDK-2617) -- it('does not re-trigger fallback when the fdv1 synchronizer itself yields a fallback-flagged result', async () => { const dataCallback = jest.fn(); @@ -1311,48 +1226,3 @@ it('does not re-trigger fallback when the fdv1 synchronizer itself yields a fall ds.close(); }); -it('uses a default recovery interval (not zero) when fdv1FallbackTtlMs is absent (regression: SDK-2617 §8.3.2)', async () => { - // §8.3.2: when x-ld-fd-fallback-ttl header is absent, the SDK MUST use a default - // retry interval of 1 hour. Absence must NOT suppress recovery (unlike TTL=0). - const dataCallback = jest.fn(); - const statusManager = makeStatusManager(); - const fdv2Payload = makePayload({ state: 'fdv2-selector' }); - const fdv1Payload = makePayload({ state: 'fdv1-selector' }); - const fdv2RecoveryPayload = makePayload({ state: 'fdv2-recovered' }); - - let fdv2Created = 0; - const fdv2Factory = jest.fn(() => { - fdv2Created += 1; - // changeSet with fdv1Fallback=true and no TTL (undefined) — should use 1-hour default - return fdv2Created === 1 - ? makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true })]) - : makeMockSynchronizer([changeSet(fdv2RecoveryPayload, { fdv1Fallback: false })]); - }); - - const slots: SynchronizerSlot[] = [ - createSynchronizerSlot({ create: fdv2Factory }), - createSynchronizerSlot( - { create: () => makeMockSynchronizer([changeSet(fdv1Payload, { fdv1Fallback: false })]) }, - { isFDv1Fallback: true }, - ), - ]; - - const ds = createFDv2DataSource({ - initializerFactories: [], - synchronizerSlots: slots, - dataCallback, - statusManager, - selectorGetter: noSelector, - fdv2RecoveryTimeoutMs: 50, - }); - - await ds.start(); - await statusManager.waitForState('VALID', 2); - expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); - // Recovery must be scheduled (not skipped as with TTL=0). - await statusManager.waitForState('VALID', 3); - expect(dataCallback).toHaveBeenCalledWith(fdv2RecoveryPayload); - expect(fdv2Created).toBe(2); - - ds.close(); -}); diff --git a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts index d8ed7d81ea..76fa2fbac0 100644 --- a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts +++ b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts @@ -2,10 +2,8 @@ import { internal, LDLogger } from '@launchdarkly/js-sdk-common'; import { DataSourceStatusManager } from '../DataSourceStatusManager'; import { - Condition, ConditionGroup, ConditionType, - createFDv2RecoveryCondition, DEFAULT_FALLBACK_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, getConditions, @@ -13,9 +11,6 @@ import { import { ChangeSetResult, FDv2SourceResult, StatusResult } from './FDv2SourceResult'; import { createSourceManager, InitializerFactory, SynchronizerSlot } from './SourceManager'; -/** Default time to remain on FDv1 before attempting FDv2 recovery (1 hour). */ -export const DEFAULT_FDV2_RECOVERY_TIMEOUT_MS = 60 * 60 * 1000; - /** * Callback invoked when the orchestrator produces a changeSet payload. */ @@ -52,9 +47,6 @@ export interface FDv2DataSourceConfig { /** Recovery condition timeout in ms (default 300s). */ recoveryTimeoutMs?: number; - - /** FDv2 recovery timeout override in ms. When absent, DEFAULT_FDV2_RECOVERY_TIMEOUT_MS is used. */ - fdv2RecoveryTimeoutMs?: number; } /** @@ -91,13 +83,11 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour logger, fallbackTimeoutMs = DEFAULT_FALLBACK_TIMEOUT_MS, recoveryTimeoutMs = DEFAULT_RECOVERY_TIMEOUT_MS, - fdv2RecoveryTimeoutMs, } = config; let initialized = false; let closed = false; let dataReceived = false; - let pendingFdv1FallbackTtlMs: number | undefined; let initResolve: (() => void) | undefined; let initReject: ((err: Error) => void) | undefined; @@ -148,9 +138,6 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour return false; } if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) { - // Remember the TTL (including 0 = indefinite) so runSynchronizers can - // schedule FDv2 recovery once the FDv1 synchronizer is active. - pendingFdv1FallbackTtlMs = result.fdv1FallbackTtlMs; sourceManager.fdv1Fallback(); return true; } @@ -273,21 +260,6 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour logger?.debug('Fallback condition active for current synchronizer.'); } - // When the FDv1 fallback synchronizer is active, schedule an FDv2 recovery - // attempt after the fallback TTL. A TTL of 0 means indefinite fallback; - // no recovery condition is created. An absent TTL uses the 1-hour default. - let fdv2RecoveryCondition: Condition | undefined; - if (sourceManager.isCurrentSynchronizerFDv1Fallback) { - // Priority: server-supplied TTL (from the fallback directive) wins; - // fall back to the caller-configured override, then the 1-hour hardcoded default. - const ttlMs = pendingFdv1FallbackTtlMs ?? fdv2RecoveryTimeoutMs ?? DEFAULT_FDV2_RECOVERY_TIMEOUT_MS; - pendingFdv1FallbackTtlMs = undefined; - if (ttlMs !== 0) { - fdv2RecoveryCondition = createFDv2RecoveryCondition(ttlMs); - logger?.debug(`FDv2 recovery scheduled in ${ttlMs} ms.`); - } - } - // Conditions hold timers; close them even if the inner loop throws or breaks early. let synchronizerRunning = true; try { @@ -302,14 +274,6 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour conditions.promise.then((value) => ({ source: 'condition' as const, value })), ); } - if (fdv2RecoveryCondition !== undefined) { - racers.push( - fdv2RecoveryCondition.promise.then((value) => ({ - source: 'condition' as const, - value, - })), - ); - } // eslint-disable-next-line no-await-in-loop const winner = await Promise.race(racers); @@ -325,9 +289,6 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } else if (conditionType === 'recovery') { logger?.info('Recovery condition fired, resetting to primary synchronizer.'); sourceManager.resetSourceIndex(); - } else if (conditionType === 'fdv2Recovery') { - logger?.info('FDv2 recovery timer fired, re-engaging FDv2 synchronizers.'); - sourceManager.fdv2Recovery(); } synchronizerRunning = false; @@ -375,7 +336,6 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } } finally { conditions.close(); - fdv2RecoveryCondition?.close(); } } }