Skip to content

Retry with sentinel instead of error#531

Closed
kriszyp wants to merge 2 commits into
mainfrom
feat/coordinated-retry
Closed

Retry with sentinel instead of error#531
kriszyp wants to merge 2 commits into
mainfrom
feat/coordinated-retry

Conversation

@kriszyp
Copy link
Copy Markdown
Member

@kriszyp kriszyp commented Apr 28, 2026

This is the only way I have figured out how to address HarperFast/harper#415
And I've spent a lot of cycles (mine and Claude's) trying to figure out how ERR_BUSY errors are escaping our catch handlers to no avail. I think this is cleaner, more straightforward and... it works.

This specifically anticipates #526, which certainly will take more review/testing, and pulls out the retry change, but should be API compatible with this future change.

When coordinatedRetry: true is set on TransactionOptions, an IsBusy
conflict at commit time resolves the commit promise with RETRY_NOW_VALUE
(a new exported constant) instead of rejecting with ERR_BUSY. The
transaction() retry loop detects this sentinel and retries immediately
without backoff, while the existing retryOnBusy/ERR_BUSY path is
unchanged for callers that don't opt in.

Changes:

  • C++: parse coordinatedRetry option in Transaction::New, store on handle;
    Commit async complete resolves with RETRY_NOW_VALUE on IsBusy when set
  • C++: define RETRY_NOW_VALUE (0x04000000) in database.h, export from binding
  • TS: add coordinatedRetry to TransactionOptions; update NativeTransaction.commit
    type; add RETRY_NOW_VALUE to constants shape
  • TS: Transaction.commit() returns Promise<RETRY_NOW | void>; export RETRY_NOW
  • TS: database.ts transaction() loop handles RETRY_NOW with immediate retry
  • Tests: new coordinated-retry.test.ts covering sentinel value, no-conflict
    path, write-write conflict retry, and raw Transaction.commit() usage
  • README: document coordinatedRetry option, RETRY_NOW sentinel, and usage examples

kriszyp and others added 2 commits April 28, 2026 07:01
…TRY_NOW

When coordinatedRetry: true is set on TransactionOptions, an IsBusy
conflict at commit time resolves the commit promise with RETRY_NOW_VALUE
(a new exported constant) instead of rejecting with ERR_BUSY. The
transaction() retry loop detects this sentinel and retries immediately
without backoff, while the existing retryOnBusy/ERR_BUSY path is
unchanged for callers that don't opt in.

Changes:
- C++: parse coordinatedRetry option in Transaction::New, store on handle;
  Commit async complete resolves with RETRY_NOW_VALUE on IsBusy when set
- C++: define RETRY_NOW_VALUE (0x04000000) in database.h, export from binding
- TS: add coordinatedRetry to TransactionOptions; update NativeTransaction.commit
  type; add RETRY_NOW_VALUE to constants shape
- TS: Transaction.commit() returns Promise<RETRY_NOW | void>; export RETRY_NOW
- TS: database.ts transaction() loop handles RETRY_NOW with immediate retry
- Tests: new coordinated-retry.test.ts covering sentinel value, no-conflict
  path, write-write conflict retry, and raw Transaction.commit() usage
- README: document coordinatedRetry option, RETRY_NOW sentinel, and usage examples

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

📊 Benchmark Results

get-sync.bench.ts

getSync() > random keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 23.79K ops/sec 42.04 40.70 631.666 0.109 118,931
🥈 rocksdb 2 12.52K ops/sec 79.86 77.20 22,576.866 0.895 62,608

getSync() > sequential keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 27.30K ops/sec 36.64 35.31 643.874 0.109 136,482
🥈 rocksdb 2 12.93K ops/sec 77.33 75.04 691.404 0.053 64,659

ranges.bench.ts

getRange() > small range (100 records, 50 range)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 25.71K ops/sec 38.90 36.88 497.949 0.153 128,531
🥈 rocksdb 2 3.60K ops/sec 277.601 242.773 1,401.593 0.565 18,012

realistic-load.bench.ts

Realistic write load with workers > write variable records with transaction log

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 182.76 ops/sec 5,471.719 58.86 139,933.124 40.31 366
🥈 lmdb 2 26.48 ops/sec 37,767.777 408.677 1,187,849.686 136.238 64.00

transaction-log.bench.ts

Transaction log > read 100 iterators while write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 35.51K ops/sec 28.16 14.06 14,211.729 0.599 177,563
🥈 lmdb 2 442.91 ops/sec 2,257.773 143.136 13,061.211 1.39 2,215

Transaction log > read one entry from random position from log with 1000 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 723.08K ops/sec 1.38 1.20 4,429.796 0.188 3,615,410
🥈 lmdb 2 460.64K ops/sec 2.17 1.11 8,441.714 0.502 2,303,197

worker-put-sync.bench.ts

putSync() > random keys - small key size (100 records, 10 workers)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 860.97 ops/sec 1,161.481 989.04 1,939.986 0.391 1,722
🥈 lmdb 2 1.18 ops/sec 846,236.084 806,672.977 905,498.568 2.59 10.00

worker-transaction-log.bench.ts

Transaction log with workers > write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 18.43K ops/sec 54.25 30.85 602.338 0.520 36,867
🥈 lmdb 2 797.58 ops/sec 1,253.797 40.98 12,186.466 5.57 1,596

Results from commit 6255d4d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant