feat: scaled transact budgets + labeled timeout diagnostics + envelope docs
- The transact apply budget scales with the batch — max(30s, opCount x 2s) — or is exactly the caller's new TransactOptions.timeoutMs. A flat 30s cap silently limited honest bulk work to ~15 operations on network-attached storage (~2s/op measured in a production import) while looking generous for small batches. Internal batch paths (removeMany chunks) get the same scaling via the shared transactTimeoutBudget helper. - TransactionTimeoutError now reports the operation it stopped at as i/N with the operation's name, elapsed vs budgeted time, and states the batch rolled back atomically and is retryable; context carries the fields programmatically. - The transact operational envelope is documented (optimistic-concurrency guide): budget math, chunking with ifAbsent idempotency, when to reach for addMany vs transact, and the precompute pattern (embedBatch + per-op vector) that keeps inference out of the commit path. - Embedding claims made honest per an end-to-end probe of the built dist: batch and single embedding paths produce bit-identical vectors and a vector-supplied add is fully searchable, but batch throughput on the default WASM engine measures comparable to sequential (~160ms/text) — the 5-10x speedup claim in the addMany JSDoc is replaced with the measured reality and the actual win (inference outside the budgeted write path).
This commit is contained in:
parent
e0f6e7722f
commit
6ef9fcb7a2
7 changed files with 175 additions and 15 deletions
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { TransactionManager } from '../../src/transaction/TransactionManager.js'
|
||||
import { transactTimeoutBudget } from '../../src/transaction/Transaction.js'
|
||||
import { TransactionError } from '../../src/transaction/errors.js'
|
||||
|
||||
describe('TransactionManager', () => {
|
||||
|
|
@ -328,4 +329,45 @@ describe('TransactionManager', () => {
|
|||
expect(stats1).toEqual(stats2) // Same values
|
||||
})
|
||||
})
|
||||
|
||||
describe('Timeout budget + telemetry', () => {
|
||||
it('transactTimeoutBudget: explicit override wins; default scales with batch size', () => {
|
||||
expect(transactTimeoutBudget(1)).toBe(30_000) // small batches keep the 30s floor
|
||||
expect(transactTimeoutBudget(15)).toBe(30_000) // the old flat cap's break-even point
|
||||
expect(transactTimeoutBudget(100)).toBe(200_000) // 100 ops × 2s — bulk gets an honest budget
|
||||
expect(transactTimeoutBudget(1000, 5_000)).toBe(5_000) // caller override is untouched
|
||||
})
|
||||
|
||||
it('a tripped budget rolls back and names the operation, progress, and budget', async () => {
|
||||
const rolledBack: string[] = []
|
||||
|
||||
const failing = manager.executeTransaction(
|
||||
async (tx) => {
|
||||
tx.addOperation({
|
||||
name: 'slow-first-op',
|
||||
execute: async () => {
|
||||
await new Promise((r) => setTimeout(r, 30))
|
||||
return async () => {
|
||||
rolledBack.push('slow-first-op')
|
||||
}
|
||||
}
|
||||
})
|
||||
tx.addOperation({
|
||||
name: 'never-reached',
|
||||
execute: async () => undefined
|
||||
})
|
||||
},
|
||||
{ timeout: 5 } // the first op's 30ms sleep guarantees the pre-op-2 check trips
|
||||
)
|
||||
|
||||
await expect(failing).rejects.toThrow(TransactionError)
|
||||
const err = await failing.catch((e) => e)
|
||||
expect(err.name).toBe('TransactionTimeoutError')
|
||||
expect(err.message).toContain('operation 1/2') // which op, of how many
|
||||
expect(err.message).toContain("('never-reached')") // its name
|
||||
expect(err.message).toContain('budget 5ms') // the budget that tripped
|
||||
expect(err.message).toContain('rolled back') // the retryability statement
|
||||
expect(rolledBack).toEqual(['slow-first-op']) // the applied op was undone
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue