Skip to content

Commit 4e1dc16

Browse files
committed
chore(db): regenerate the sync lock token migration with drizzle-kit
The migration was hand-written, which left it inconsistent with every other migration in the repo and, more importantly, without a schema snapshot. Drizzle diffs against the latest snapshot to decide what a migration needs to contain, so the next generate would have seen the column as still missing and emitted it a second time. Regenerated properly: drizzle-kit now owns the SQL, the journal entry, and 0297_snapshot.json. The emitted statement matches the house pattern for an additive nullable column, and check:migrations still reports backward-compatible.
1 parent 1dffbf7 commit 4e1dc16

7 files changed

Lines changed: 20107 additions & 43 deletions

File tree

apps/sim/lib/knowledge/connectors/sync-engine.test.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,7 @@ describe('writeTerminalConnectorState', () => {
13321332
})
13331333
})
13341334

1335-
describe('applySupersededOutcome', () => {
1335+
describe('markSyncSuperseded', () => {
13361336
const result = {
13371337
docsAdded: 3,
13381338
docsUpdated: 1,
@@ -1341,29 +1341,28 @@ describe('applySupersededOutcome', () => {
13411341
docsFailed: 0,
13421342
}
13431343

1344-
it('leaves a run that kept its lock untouched', async () => {
1345-
const { applySupersededOutcome } = await import('@/lib/knowledge/connectors/sync-engine')
1346-
1347-
expect(applySupersededOutcome(result, true)).toEqual(result)
1348-
})
1349-
13501344
it('flags a discarded run so the task wrapper does not report it as clean', async () => {
1351-
const { applySupersededOutcome, SUPERSEDED_SYNC_ERROR } = await import(
1345+
const { markSyncSuperseded, SUPERSEDED_SYNC_ERROR } = await import(
13521346
'@/lib/knowledge/connectors/sync-engine'
13531347
)
13541348

1355-
const superseded = applySupersededOutcome(result, false)
1356-
13571349
// The task wrapper reports `success: !result.error`.
1358-
expect(superseded.error).toBe(SUPERSEDED_SYNC_ERROR)
1359-
expect(Boolean(superseded.error)).toBe(true)
1350+
expect(markSyncSuperseded(result).error).toBe(SUPERSEDED_SYNC_ERROR)
13601351
})
13611352

13621353
it('preserves the document counters of the discarded run', async () => {
1363-
const { applySupersededOutcome } = await import('@/lib/knowledge/connectors/sync-engine')
1354+
const { markSyncSuperseded } = await import('@/lib/knowledge/connectors/sync-engine')
13641355

13651356
// Those writes landed — only the connector-level bookkeeping was discarded.
1366-
expect(applySupersededOutcome(result, false)).toMatchObject(result)
1357+
expect(markSyncSuperseded(result)).toMatchObject(result)
1358+
})
1359+
1360+
it('does not mutate the result it was handed', async () => {
1361+
const { markSyncSuperseded } = await import('@/lib/knowledge/connectors/sync-engine')
1362+
1363+
markSyncSuperseded(result)
1364+
1365+
expect(result).not.toHaveProperty('error')
13671366
})
13681367
})
13691368

apps/sim/lib/knowledge/connectors/sync-engine.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,11 @@ export async function completeSyncLog(
364364
* proves the lock is still this run's. `status` is kept alongside as defence in
365365
* depth and to cover a user pausing the connector mid-run.
366366
*
367-
* Guards both terminal paths. The failure path needs it as much as the success
368-
* path: a reclaimed run's failure would double-increment a counter the sweep
369-
* already advanced and overwrite its backoff with a shorter one.
367+
* Guards every write a run makes to its own connector row: both terminal paths
368+
* and the mid-run heartbeat. The failure path needs it as much as the success
369+
* path — a reclaimed run's failure would double-increment a counter the sweep
370+
* already advanced and overwrite its backoff with a shorter one — and reusing it
371+
* for the heartbeat is what turns a beat into an ownership probe.
370372
*/
371373
export function stillHoldsSyncLock(connectorId: string, syncLockToken: string) {
372374
return and(
@@ -396,7 +398,7 @@ export function buildSyncLockAcquisition(syncLogId: string, now: Date) {
396398
/**
397399
* Whether a running sync is due to refresh its lock.
398400
*
399-
* Time-based rather than batch-count-based: batches vary hugely in cost, so a
401+
* Time-based rather than batch-count-based: batches vary hugely in cost, so an
400402
* every-N-batches beat would fire constantly on small documents and barely at
401403
* all on large ones — exactly the runs that need it.
402404
*/
@@ -457,9 +459,10 @@ export async function writeTerminalConnectorState(
457459
}
458460

459461
/**
460-
* Reported when a run's terminal write matched no rows because the run no longer
461-
* held its lock. Its document writes still landed; only its connector-level
462-
* bookkeeping was discarded, in favour of whoever reclaimed the row.
462+
* Reported when a run loses its connector's lock mid-flight — either because a
463+
* heartbeat found the lock reclaimed, or because its terminal write matched no
464+
* rows. Its document writes still landed; only its connector-level bookkeeping
465+
* was discarded, in favour of whoever reclaimed the row.
463466
*/
464467
export const SUPERSEDED_SYNC_ERROR = 'sync_superseded'
465468

@@ -468,11 +471,7 @@ export const SUPERSEDED_SYNC_ERROR = 'sync_superseded'
468471
* report a discarded run as a clean sync — the same reason a lock-contended run
469472
* returns `sync_in_progress` rather than an empty success.
470473
*/
471-
export function applySupersededOutcome(
472-
result: SyncResult,
473-
terminalWriteLanded: boolean
474-
): SyncResult {
475-
if (terminalWriteLanded) return result
474+
export function markSyncSuperseded(result: SyncResult): SyncResult {
476475
return { ...result, error: SUPERSEDED_SYNC_ERROR }
477476
}
478477

@@ -595,8 +594,8 @@ export function classifySuspectListing(
595594
* immediately. A genuinely emptied source keeps reconciling: its second sync
596595
* corroborates the first and tombstones everything, and a later sync — once the
597596
* tombstoned set is again absent — completes the two-strike purge, subject to
598-
* {@link capReconciliationDeletions}, which holds a pass whose deletion count
599-
* exceeds the per-sync blast-radius cap.
597+
* {@link capReconciliationDeletions}, which withholds any generation whose
598+
* deletion count exceeds the per-sync blast-radius cap.
600599
*
601600
* A forced `fullSync` overrides the guard, matching its existing meaning
602601
* elsewhere here — an explicit human request to reconcile against this listing
@@ -2033,7 +2032,7 @@ export async function executeSync(
20332032
syncLogId,
20342033
...result,
20352034
})
2036-
return applySupersededOutcome(result, false)
2035+
return markSyncSuperseded(result)
20372036
}
20382037

20392038
logger.info('Sync completed', { connectorId, ...result })
@@ -2050,7 +2049,7 @@ export async function executeSync(
20502049
syncLogId,
20512050
...result,
20522051
})
2053-
return applySupersededOutcome(result, false)
2052+
return markSyncSuperseded(result)
20542053
}
20552054

20562055
if (error instanceof ConnectorDeletedException) {
@@ -2108,7 +2107,7 @@ export async function executeSync(
21082107
)
21092108

21102109
/**
2111-
* Deliberately does NOT get {@link applySupersededOutcome}. `result.error`
2110+
* Deliberately does NOT get {@link markSyncSuperseded}. `result.error`
21122111
* is set to the real failure cause below and the task wrapper already
21132112
* reports this run as unsuccessful, so overwriting it with
21142113
* `sync_superseded` would destroy the diagnostic without changing the

apps/sim/lib/knowledge/connectors/sync-limits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export const CONNECTOR_SYNC_STALE_LOCK_TTL_MS = CONNECTOR_SYNC_MAX_DURATION_SECO
3838
*
3939
* Shared because two independent writers advance this counter: `executeSync`'s
4040
* in-process failure path, and the scheduler's out-of-process stale-lock
41-
* reclaim (a SIGKILL skips `catch`/`finally`, so only the reaper ever sees that
42-
* failure). A connector that only ever dies hard must still reach the threshold,
41+
* reclaim (a SIGKILL unwinds nothing, so the in-process `catch` never runs and
42+
* only the reaper ever sees that failure). A connector that only ever dies hard must still reach the threshold,
4343
* which it cannot if the two disagree on what the threshold is.
4444
*/
4545
export const MAX_CONSECUTIVE_FAILURES = 10

packages/db/migrations/0297_connector_sync_lock_token.sql

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "knowledge_connector" ADD COLUMN "sync_lock_token" text;

0 commit comments

Comments
 (0)