Skip to content

Commit c84d216

Browse files
committed
fix(knowledge): raise the connector sync ceiling and tie it to the stale lock
A 2,600-document library exhausted the 30-minute budget and the run was killed mid-listing, leaving the connector's `syncing` lock set until the scheduler reclaimed it. Raising the ceiling is not a lone constant, because reclaiming a stale lock flips the connector to `error` and frees it for another sync. A TTL at or below the run ceiling would hand the lock to a successor while the first sync is still writing — two syncs racing the same `(connectorId, externalId)` rows. The previous values, a 1800s run against a hard-coded 120-minute TTL declared in a different file, held that invariant only by coincidence. Both now derive from one another, with a test pinning the margin so the next raise cannot silently break it.
1 parent 3ea5905 commit c84d216

4 files changed

Lines changed: 46 additions & 3 deletions

File tree

apps/sim/app/api/knowledge/connectors/sync/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
99
import { generateRequestId } from '@/lib/core/utils/request'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { dispatchSync } from '@/lib/knowledge/connectors/queue'
12+
import { CONNECTOR_SYNC_STALE_LOCK_TTL_MS } from '@/lib/knowledge/connectors/sync-limits'
1213

1314
export const dynamic = 'force-dynamic'
1415

@@ -39,8 +40,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3940
try {
4041
const now = new Date()
4142

42-
const STALE_SYNC_TTL_MS = 120 * 60 * 1000
43-
const staleCutoff = new Date(now.getTime() - STALE_SYNC_TTL_MS)
43+
const staleCutoff = new Date(now.getTime() - CONNECTOR_SYNC_STALE_LOCK_TTL_MS)
4444

4545
const recoveredConnectors = await db
4646
.update(knowledgeConnector)

apps/sim/background/knowledge-connector-sync.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type ConnectorSyncPayload,
66
} from '@/lib/knowledge/connectors/queue'
77
import { executeSync } from '@/lib/knowledge/connectors/sync-engine'
8+
import { CONNECTOR_SYNC_MAX_DURATION_SECONDS } from '@/lib/knowledge/connectors/sync-limits'
89

910
const logger = createLogger('TriggerKnowledgeConnectorSync')
1011

@@ -39,7 +40,7 @@ export async function executeConnectorSyncJob(payload: unknown) {
3940

4041
export const knowledgeConnectorSync = task({
4142
id: 'knowledge-connector-sync',
42-
maxDuration: 1800,
43+
maxDuration: CONNECTOR_SYNC_MAX_DURATION_SECONDS,
4344
machine: 'large-2x',
4445
retry: {
4546
maxAttempts: 3,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import {
6+
CONNECTOR_SYNC_MAX_DURATION_SECONDS,
7+
CONNECTOR_SYNC_STALE_LOCK_TTL_MS,
8+
} from '@/lib/knowledge/connectors/sync-limits'
9+
10+
describe('connector sync limits', () => {
11+
/**
12+
* Reclaiming a stale lock frees it for another sync, so a TTL at or below the
13+
* run ceiling would start a second sync while the first is still writing. This
14+
* guards the invariant against a future hard-coded TTL, not the derivation.
15+
*/
16+
it('keeps at least a 2x margin between the run ceiling and the reclaim', () => {
17+
expect(CONNECTOR_SYNC_STALE_LOCK_TTL_MS).toBeGreaterThanOrEqual(
18+
CONNECTOR_SYNC_MAX_DURATION_SECONDS * 2 * 1000
19+
)
20+
})
21+
22+
/** A 2,600-document library exhausted the previous 1800s budget mid-listing. */
23+
it('allows a run longer than the half hour that timed out in production', () => {
24+
expect(CONNECTOR_SYNC_MAX_DURATION_SECONDS).toBeGreaterThan(1800)
25+
})
26+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Wall-clock ceiling for a single connector sync run. A large document library
3+
* needs more than the half hour this used to allow: a 2,600-document site
4+
* exhausted the old budget and was killed mid-listing, leaving its `syncing`
5+
* lock set until the scheduler reclaimed it.
6+
*/
7+
export const CONNECTOR_SYNC_MAX_DURATION_SECONDS = 3600
8+
9+
/**
10+
* How long a connector may sit in `syncing` before the scheduler reclaims its lock.
11+
*
12+
* MUST stay above {@link CONNECTOR_SYNC_MAX_DURATION_SECONDS}: reclaiming frees the
13+
* lock for another sync, so a TTL at or below the run ceiling would start a second
14+
* sync while the first is still writing, both racing the same documents.
15+
*/
16+
export const CONNECTOR_SYNC_STALE_LOCK_TTL_MS = CONNECTOR_SYNC_MAX_DURATION_SECONDS * 2 * 1000

0 commit comments

Comments
 (0)