Skip to content

Commit c548150

Browse files
committed
fix(file-search): rotate a woken owner whose slots are full by dispatch age
An owner already holding both of its outstanding claims can claim nothing, but a write between passes kept marking it woken, so with little global capacity left it took every owner turn and the free slot went unused. Woken owners now go first only while they have a free slot; a full owner keeps the dispatch-age rotation it had before. Also gives the due-reconcile work-check case the same one-minute margin as its not-yet-due counterpart: exactly at the interval it compared a database timestamp with the application clock and flaked on skew.
1 parent 3a57638 commit c548150

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎apps/sim/lib/workspace-files/search/dispatcher.integration.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
FILE_SEARCH_INDEX_DISPATCH_WORKSPACES,
3232
FILE_SEARCH_INDEX_MAX_OUTSTANDING,
3333
FILE_SEARCH_INDEX_STALE_DISPATCH_MS,
34+
FILE_SEARCH_INDEX_WORKSPACE_OUTSTANDING,
3435
FILE_SEARCH_RECONCILE_INTERVAL_MS,
3536
} from '@/lib/workspace-files/search/constants'
3637
import {
@@ -454,6 +455,20 @@ describe('workspace file search dispatch PostgreSQL deadlines', () => {
454455
expect(result.payloads.map((payload) => payload.fileId)).toEqual(['woken-workspace-000001'])
455456
})
456457

458+
it('rotates a woken owner whose slots are full behind an older waiting owner', async () => {
459+
await leaveSlots(1 + FILE_SEARCH_INDEX_WORKSPACE_OUTSTANDING)
460+
// Both of its slots are taken and it has more files; a write woke it after its last dispatch.
461+
await seedQueue('busy-workspace', 3, FILE_SEARCH_INDEX_WORKSPACE_OUTSTANDING)
462+
await connection`UPDATE file_search_dispatch_queue
463+
SET last_dispatched_at = now() - interval '10 minutes' WHERE entity_id = 'busy-workspace'`
464+
await seedQueue('waiting-workspace', 1)
465+
await connection`UPDATE file_search_dispatch_queue SET last_dispatched_at = now() - interval '1 hour',
466+
updated_at = now() - interval '1 hour' WHERE entity_id = 'waiting-workspace'`
467+
468+
const result = await prepareWorkspaceFileSearchDispatch()
469+
expect(result.payloads.map((payload) => payload.fileId)).toEqual(['waiting-workspace-000001'])
470+
})
471+
457472
it('serves a fresh write before work the backfill found, then the backfill work', async () => {
458473
await leaveSlots(0)
459474
await connection`UPDATE workspace_file_search_backfill SET completed_at = NULL`
@@ -1027,8 +1042,10 @@ describe('workspace file search dispatch PostgreSQL deadlines', () => {
10271042
},
10281043
{
10291044
work: 'a backfill reconcile that is due',
1045+
// A minute past the interval, mirroring the not-yet-due case: the check compares a database
1046+
// timestamp with the application clock, so exactly at the boundary it depends on clock skew.
10301047
seed: () => connection`UPDATE workspace_file_search_backfill
1031-
SET completed_at = now() - make_interval(secs => ${reconcileSeconds})`,
1048+
SET completed_at = now() - make_interval(secs => ${reconcileSeconds + 60})`,
10321049
},
10331050
{
10341051
work: 'a queued workspace',

‎apps/sim/lib/workspace-files/search/dispatcher.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,20 @@ const transactionNow = sql`now()`
186186
*/
187187
const wokenSinceDispatch = sql`(${fileSearchDispatchQueue.lastDispatchedAt} IS NULL OR ${fileSearchDispatchQueue.updatedAt} > ${fileSearchDispatchQueue.lastDispatchedAt})`
188188

189+
/**
190+
* Whether the queued owner already holds every claim it may have outstanding. Such an owner can
191+
* claim nothing this pass, so being woken must not put it ahead of owners that can: it keeps
192+
* rotating by dispatch age, or a busy owner written to between passes would take every turn while
193+
* the free global capacity goes unused.
194+
*/
195+
const ownerSlotsFull = sql`(SELECT count(*) FROM (
196+
SELECT 1 FROM workspace_file_search_revision active
197+
WHERE coalesce(active.entity_type, 'workspace') = ${fileSearchDispatchQueue.entityType}
198+
AND coalesce(active.entity_id, active.workspace_id) = ${fileSearchDispatchQueue.entityId}
199+
AND active.status = 'pending' AND active.dispatched_at IS NOT NULL
200+
LIMIT ${FILE_SEARCH_INDEX_WORKSPACE_OUTSTANDING}
201+
) active_claims) >= ${FILE_SEARCH_INDEX_WORKSPACE_OUTSTANDING}`
202+
189203
/**
190204
* Queues owners for the claim pass. A `reconciliation` wake-up, from the backfill walk, enters as
191205
* if just dispatched and never touches an owner already queued, so a page of owners with nothing
@@ -687,7 +701,7 @@ export async function prepareWorkspaceFileSearchDispatch(
687701
})
688702
.from(fileSearchDispatchQueue)
689703
.orderBy(
690-
sql`${wokenSinceDispatch} DESC`,
704+
sql`(${wokenSinceDispatch} AND NOT ${ownerSlotsFull}) DESC`,
691705
sql`${fileSearchDispatchQueue.lastDispatchedAt} ASC NULLS FIRST`,
692706
asc(fileSearchDispatchQueue.enqueuedAt),
693707
asc(fileSearchDispatchQueue.entityType),

0 commit comments

Comments
 (0)