Repository navigation
fix(table): make TTL eviction path errors non-fatal on both storage engines (#1287) #1361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| require('../testUtils'); | ||
| const assert = require('assert'); | ||
| const { setupTestDBPath } = require('../testUtils'); | ||
| const { table } = require('#src/resources/databases'); | ||
| const { setMainIsWorker } = require('#js/server/threads/manageThreads'); | ||
|
|
||
| // Regression for #1287 (RocksDB leg). The cleanup scan's batcher retries ERR_BUSY, but the two | ||
| // fire-and-forget evict() call sites (the read-path expiry check and runRecordExpirationEviction on | ||
| // worker 0) commit the raw transaction directly, bypassing DatabaseTransaction's ERR_BUSY retry. A | ||
| // concurrent write to the same record makes that raw commit reject with TransactionIsBusyError, which | ||
| // escaped as an unhandledRejection on [main/0] and could exit the process. evict() must swallow the | ||
| // conflict (eviction is best-effort; lazy-expiry keeps reads correct) so it can never reject. | ||
| describe('evict() swallows a commit conflict instead of rejecting (#1287)', () => { | ||
| if (process.env.HARPER_STORAGE_ENGINE === 'lmdb') return; | ||
| let BusyTable; | ||
|
|
||
| before(async function () { | ||
| setupTestDBPath(); | ||
| setMainIsWorker(true); | ||
| BusyTable = table({ | ||
| table: 'EvictBusyTable', | ||
| database: 'test', | ||
| attributes: [{ name: 'id', isPrimaryKey: true }], | ||
| }); | ||
| }); | ||
|
|
||
| it('returns a thenable that resolves when the eviction commit hits ERR_BUSY', async function () { | ||
| const { Transaction } = require('@harperfast/rocksdb-js'); | ||
| const originalCommit = Transaction.prototype.commit; | ||
| await BusyTable.put('b1', { id: 'b1' }); | ||
| const entry = BusyTable.primaryStore.getEntry('b1'); | ||
|
|
||
| let injected = false; | ||
| // Arm a single ERR_BUSY into the next commit — the eviction commit issued synchronously by evict(). | ||
| Transaction.prototype.commit = async function (...args) { | ||
| if (!injected) { | ||
| injected = true; | ||
| const error = new Error('Resource busy'); | ||
| error.code = 'ERR_BUSY'; | ||
| throw error; | ||
| } | ||
| return originalCommit.apply(this, args); | ||
| }; | ||
|
|
||
| try { | ||
| const resolution = BusyTable.evict('b1', entry.value, entry.version); | ||
| assert.equal(typeof resolution?.then, 'function', 'evict() must return a thenable'); | ||
| // Must resolve, not reject: a rejection here is the unhandledRejection that crashed [main/0]. | ||
| await resolution; | ||
| assert.ok(injected, 'the injected ERR_BUSY conflict should have fired on the eviction commit'); | ||
| } finally { | ||
| Transaction.prototype.commit = originalCommit; | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| require('../testUtils'); | ||
| const assert = require('assert'); | ||
| const { setupTestDBPath } = require('../testUtils'); | ||
| const { table } = require('#src/resources/databases'); | ||
| const { setMainIsWorker } = require('#js/server/threads/manageThreads'); | ||
|
|
||
| // Regression for #1287 (LMDB leg). The cleanup scan keeps the per-record TableResource.evict() path | ||
| // for LMDB (RocksDB coalesces into a batcher instead). evict()'s LMDB branch returned | ||
| // DatabaseTransaction.commit()'s plain resolution object — the optimistic remove goes straight to the | ||
| // store, so the wrapper transaction has no tracked writes and commit() returns `{ txnTime }`, not a | ||
| // promise. The scan then did `resolution.catch(...)` and threw `TypeError: resolution.catch is not a | ||
| // function` every cycle, degrading eviction to roughly one record per scan and growing the store. | ||
| // | ||
| // evict() must return a thenable that resolves once the removal is durable (RocksDB has its own | ||
| // coverage in evictionBatch.test.js, so this leg is LMDB-only). | ||
| describe('evict() returns a thenable on the LMDB path (#1287)', () => { | ||
| if (process.env.HARPER_STORAGE_ENGINE !== 'lmdb') return; | ||
| let EvictTable; | ||
|
|
||
| before(async function () { | ||
| setupTestDBPath(); | ||
| setMainIsWorker(true); | ||
| EvictTable = table({ | ||
| table: 'EvictLmdbThenableTable', | ||
| database: 'test', | ||
| attributes: [ | ||
| { name: 'id', isPrimaryKey: true }, | ||
| { name: 'name', indexed: true }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it('evict() resolves a thenable and physically removes the record + its index entry', async function () { | ||
| await EvictTable.put('a1', { id: 'a1', name: 'group 7' }); | ||
| const entry = EvictTable.primaryStore.getEntry('a1'); | ||
| assert.ok(entry, 'record should be resident before eviction'); | ||
| assert.equal(EvictTable.indices['name'].getValuesCount('group 7'), 1, 'index should be populated'); | ||
|
|
||
| const resolution = EvictTable.evict('a1', entry.value, entry.version); | ||
| // The core regression: the cleanup scan does `resolution.catch(...)`, so a non-thenable return | ||
| // throws and aborts the scan. evict() must hand back a real promise. | ||
| assert.equal(typeof resolution?.then, 'function', 'evict() must return a thenable'); | ||
| assert.equal(typeof resolution?.catch, 'function', 'evict() thenable must expose catch()'); | ||
| await resolution; | ||
|
|
||
| assert.equal(EvictTable.primaryStore.getEntry('a1'), undefined, 'record should be physically evicted'); | ||
| assert.equal( | ||
| EvictTable.indices['name'].getValuesCount('group 7'), | ||
| 0, | ||
| 'index entry must be removed with the record' | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.