Serialize database destruction with concurrent opens - #787
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces robust database lifecycle management for RocksDB JS bindings. It implements a timed-wait mechanism (lifecycleWaitSeconds) for open, destroy, and shutdown operations to prevent concurrent lifecycle conflicts. It also introduces a "quarantine" state for database paths when a native close, flush, compaction, or physical directory cleanup fails, preventing subsequent opens until the cleanup is retried via destroy() or shutdown(). Additionally, it ensures that in-flight operations (like backups and checkpoints) are safely awaited before destruction, and that thread-affine N-API references are cleaned up safely. There are no review comments, so I have no feedback to provide.
📊 Benchmark Resultsget-sync.bench.tsgetSync() > random keys - small key size (100 records)
getSync() > sequential keys - small key size (100 records)
ranges.bench.tsgetRange() > small range (100 records, 50 range)
realistic-load.bench.tsRealistic write load with workers > write variable records with transaction log
transaction-log.bench.tsTransaction log > read 100 iterators while write log with 100 byte records
Transaction log > read one entry from random position from log with 1000 100 byte records
worker-put-sync.bench.tsputSync() > random keys - small key size (100 records, 10 workers)
worker-transaction-log.bench.tsTransaction log with workers > write log with 100 byte records
Results from commit a972fec |
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
- shutdown() no longer permanently throws once a destroy-cleanup tombstone exists; it stays non-destructive (per AGENTS.md) and skips the entry instead of poisoning every later call - binding.cpp always releases global listener threadsafe functions, even when DBRegistry::Shutdown() throws - compactSync() cancels its manual compaction when finishClose() is draining in-flight operations, instead of blocking the untimed drain (and cascading OpenDB timeouts) for the compaction's full duration - Iterator Return()/Throw() are idempotent again on an already-closed iterator, matching close() elsewhere, instead of throwing over a clean loop exit or the caller's real error - narrow the AGENTS.md VT fast-path claim to what's actually true - log the retained path on a benchmark teardown failure instead of leaking it silently - add a deterministic test for iteratorMutex serializing Next() against a foreign forced close, plus a return()/throw() idempotency unit test
Summary
Harper currently works around a rocksdb-js lifecycle race by locking root opens in JavaScript. This moves the invariant into the native registry: physical destruction now owns a database path across read-write/read-only descriptors, concurrent opens wait, and shutdown is serialized with both operations.
The change also makes teardown failures observable and recoverable, and prevents destroy/shutdown from releasing the native database beneath directory backups, streaming backups, or checkpoints.
For the human reviewer
destroy()intentionally changes from refusing while peer handles exist to closing every in-process handle for the physical path before removing it. This is what allows Harper schema propagation to race safely with a database drop. Cross-process coordination remains RocksDB's lock responsibility.database:closeFailed. A failed post-destroy directory cleanup is visible inregistryStatus()and can only be retried by the explicit destructive verbs,destroy()orshutdown();open()remains non-destructive.close()in afinally, but silently ignoring a failed flush would hide possible data loss.DBHandle; shutdown already exercised that path before this change. N-API reference deletion is now owner-thread-only, and worker lifecycle fixtures pass, but the remaining shared-handle synchronization is a follow-up decision for the storage maintainer.This is the rocksdb-js root-cause fix for Harper PR #2169, "Prevent job wedges on runtime database opens". Harper's JavaScript
.openlock should remain out of the released path once a package containing this change is available.Verification
node_modules/.bin/node-gyp buildnode_modules/.bin/tsc --noEmitnode_modules/.bin/oxlintnode_modules/.bin/oxfmt --checkMADV_COLDskipsReview coverage
— GPT-5 Codex
Human-Review-Need: 4 (decisions: close-throws-vs-reports, quarantine-blocks-reopen, global-shutdown-as-the-retry-api, shutdown-throws-while-tombstone-exists, destroy-forces-foreign-teardown, unbounded-inflight-wait-outside-the-deadline, one-global-lifecycle-timeout, retry-skips-the-inflight-drain, iterator-serialized-by-mutex-not-lifetime-guard, destroy-readonly-flag-crosses-the-js-boundary) @ e9bc308