Skip to content

Commit 999c2be

Browse files
fix(table): enforce shared concurrencyKey cap on database batchEnqueueAndWait
1 parent 5a34d68 commit 999c2be

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

apps/sim/lib/core/async-jobs/backends/database.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
5+
import { sleep } from '@sim/utils/helpers'
56
import { beforeEach, describe, expect, it, vi } from 'vitest'
67

78
vi.mock('@sim/db', () => ({
@@ -78,3 +79,36 @@ describe('DatabaseJobQueue enqueue', () => {
7879
})
7980
})
8081
})
82+
83+
describe('DatabaseJobQueue batchEnqueueAndWait', () => {
84+
beforeEach(() => {
85+
vi.clearAllMocks()
86+
resetDbChainMock()
87+
})
88+
89+
it('caps overlapping batches sharing a concurrencyKey at the shared limit', async () => {
90+
const queue = new DatabaseJobQueue()
91+
let inFlight = 0
92+
let maxInFlight = 0
93+
const makeItem = () => ({
94+
payload: {},
95+
options: {
96+
concurrencyKey: 'table-1',
97+
concurrencyLimit: 2,
98+
runner: async () => {
99+
inFlight += 1
100+
maxInFlight = Math.max(maxInFlight, inFlight)
101+
await sleep(1)
102+
inFlight -= 1
103+
},
104+
},
105+
})
106+
107+
await Promise.all([
108+
queue.batchEnqueueAndWait('workflow-group-cell', [makeItem(), makeItem()]),
109+
queue.batchEnqueueAndWait('workflow-group-cell', [makeItem(), makeItem()]),
110+
])
111+
112+
expect(maxInFlight).toBe(2)
113+
})
114+
})

apps/sim/lib/core/async-jobs/backends/database.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,23 @@ export class DatabaseJobQueue implements JobQueueBackend {
207207
inlineCancelKeyControllers.set(cancelKey, controller)
208208
tracked.push({ key: cancelKey, controller })
209209
}
210-
return runner(item.payload, controller.signal).catch((err) => {
210+
// Same shared-key semaphore as `runInline`: without it, overlapping
211+
// batches on one concurrencyKey (e.g. two dispatches on one table) would
212+
// each run their full window concurrently instead of sharing the cap.
213+
const { concurrencyKey, concurrencyLimit } = item.options ?? {}
214+
const run = async () => {
215+
if (concurrencyKey && concurrencyLimit && concurrencyLimit > 0) {
216+
await acquireSlot(concurrencyKey, concurrencyLimit)
217+
try {
218+
await runner(item.payload, controller.signal)
219+
} finally {
220+
releaseSlot(concurrencyKey)
221+
}
222+
return
223+
}
224+
await runner(item.payload, controller.signal)
225+
}
226+
return run().catch((err) => {
211227
logger.error(`[${type}] Inline run failed`, {
212228
cancelKey,
213229
error: toError(err).message,

0 commit comments

Comments
 (0)