Skip to content

Commit c4c74da

Browse files
authored
Merge pull request #371 from HarperFast/destroy
Add `db.destroy()`
2 parents 1354876 + cca1f90 commit c4c74da

21 files changed

+523
-162
lines changed

README.md

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ RocksDatabase.config({
9191
});
9292
```
9393

94+
### `db.isOpen(): boolean`
95+
96+
Returns `true` if the database is open, otherwise false.
97+
98+
```typescript
99+
console.log(db.isOpen()); // true or false
100+
```
101+
102+
### `db.name: string`
103+
104+
Returns the database column family's name.
105+
106+
```typescript
107+
const db = new RocksDatabase('path/to/db');
108+
console.log(db.name); // 'default'
109+
110+
const db2 = new RocksDatabase('path/to/db', { name: 'users' });
111+
console.log(db.name); // 'users'
112+
```
113+
94114
### `db.open(): RocksDatabase`
95115

96116
Opens the database at the given path. This must be called before performing any data operations.
@@ -108,16 +128,12 @@ There's also a static `open()` method for convenience that performs the same thi
108128
const db = RocksDatabase.open('path/to/db');
109129
```
110130

111-
### `db.name: string`
131+
### `db.status: 'opened' | 'closed'`
112132

113-
Returns the database column family's name.
133+
Returns a string `'opened'` or `'closed'` indicating if the database is opened or closed.
114134

115135
```typescript
116-
const db = new RocksDatabase('path/to/db');
117-
console.log(db.name); // 'default'
118-
119-
const db2 = new RocksDatabase('path/to/db', { name: 'users' });
120-
console.log(db.name); // 'users'
136+
console.log(db.status);
121137
```
122138

123139
## Data Operations
@@ -156,6 +172,16 @@ const entriesRemoved = db.clearSync();
156172
console.log(entriesRemoved); // 10
157173
```
158174

175+
### `db.destroy(): void`
176+
177+
Completely removes a database based on the `db` instance's path including all data, column families,
178+
and files on disk.
179+
180+
```typescript
181+
db.destroy();
182+
console.log(fs.existsSync(db.path)); // false
183+
```
184+
159185
### `db.drop(): Promise<void>`
160186

161187
Removes all entries in the database. If the database was opened with a `name`, the database will be
@@ -847,6 +873,24 @@ Returns an object containing all of the information in the log file.
847873
- `length: number` The size of the entry data.
848874
- `timestamp: number` The entry timestamp.
849875

876+
### `registryStatus(): RegistryStatus`
877+
878+
Returns an array containing that status of all active RocksDB instances.
879+
880+
- `path: string` The database path.
881+
- `refCount: number` The number of JavaScript database instances plus the registry's reference.
882+
- `columnFamiles: string[]` A list of the database's column families.
883+
- `transactions: number` The count of active transactions.
884+
- `closables: number` The count of active database, transactions, and iterators.
885+
- `locks: number` The count of active locks.
886+
- `userSharedBuffers: number` The count of active user shared buffers.
887+
- `listenerCallbacks: number` The count of in-flight callbacks.
888+
889+
```typescript
890+
import { registryStatus } from '@harperfast/rocksdb-js';
891+
console.log(registryStatus());
892+
```
893+
850894
### `shutdown(): void`
851895

852896
The `shutdown()` will flush all in-memory data to disk and wait for any outstanding compactions to
@@ -858,6 +902,15 @@ import { shutdown } from '@harperfast/rocksdb-js';
858902
process.on('exit', shutdown);
859903
```
860904

905+
### `versions: { 'rocksdb': string; 'rocksdb-js': string }`
906+
907+
Returns the `rocksdb-js` and RocksDB version.
908+
909+
```typescript
910+
import { versions } from '@harperfast/rocksdb-js';
911+
console.log(versions); // { "rocksdb": "10.10.1", "rocksdb-js": "0.1.2" }
912+
```
913+
861914
## Custom Store
862915

863916
The store is a class that sits between the `RocksDatabase` or `Transaction` instance and the native

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@
2525
"build:bundle:minify": "cross-env MINIFY=1 pnpm build:bundle",
2626
"check": "pnpm type-check && pnpm lint",
2727
"clean": "node-gyp clean",
28-
"coverage": "tsx scripts/coverage/main.ts",
28+
"coverage": "tsx scripts/coverage/main.ts --no-file-parallelism",
2929
"format": "dprint check",
3030
"format:fix": "dprint fmt",
3131
"lint": "oxlint",
3232
"prepublishOnly": "pnpm build",
3333
"rebuild": "node-gyp rebuild",
3434
"rebuild:debug": "node-gyp rebuild --coverage --debug --verbose",
35-
"test": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose",
36-
"test:bun": "cross-env CI=1 bun --bun --config=bunfig.toml ./node_modules/vitest/vitest.mjs --reporter=verbose",
37-
"test:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports ./node_modules/vitest/vitest.mjs",
38-
"test:stress": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose --config=vitest-stress.config.ts",
35+
"test": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose --no-file-parallelism",
36+
"test:bun": "cross-env CI=1 bun --bun --config=bunfig.toml ./node_modules/vitest/vitest.mjs --reporter=verbose --no-file-parallelism",
37+
"test:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports ./node_modules/vitest/vitest.mjs --no-file-parallelism",
38+
"test:stress": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose --config=vitest-stress.config.ts --no-file-parallelism",
3939
"type-check": "tsc --noEmit"
4040
},
4141
"files": [

src/binding/binding.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ NAPI_MODULE_INIT() {
5151
DEBUG_LOG("Binding::Init Module ref count: %d\n", refCount);
5252

5353
// initialize the registry
54-
DBRegistry::Init();
54+
rocksdb_js::DBRegistry::Init(env, exports);
5555

5656
// registry cleanup
5757
NAPI_STATUS_THROWS(::napi_add_env_cleanup_hook(env, [](void* data) {

src/binding/database.cpp

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,45 @@ napi_value Database::Close(napi_env env, napi_callback_info info) {
169169
UNWRAP_DB_HANDLE();
170170

171171
if (*dbHandle && (*dbHandle)->descriptor) {
172-
DEBUG_LOG("%p Database::Close closing database: %s\n", dbHandle->get(), (*dbHandle)->descriptor->path.c_str());
172+
DEBUG_LOG("%p Database::Close Closing database: \"%s\"\n", dbHandle->get(), (*dbHandle)->path.c_str());
173173
DBRegistry::CloseDB(*dbHandle);
174-
DEBUG_LOG("%p Database::Close closed database\n", dbHandle->get());
174+
DEBUG_LOG("%p Database::Close Closed database\n", dbHandle->get());
175175
} else {
176176
DEBUG_LOG("%p Database::Close Database not opened\n", dbHandle->get());
177177
}
178178

179179
NAPI_RETURN_UNDEFINED();
180180
}
181181

182+
/**
183+
* Destroys the RocksDB database.
184+
*
185+
* @example
186+
* ```typescript
187+
* const db = new NativeDatabase();
188+
* db.destroy();
189+
* ```
190+
*/
191+
napi_value Database::Destroy(napi_env env, napi_callback_info info) {
192+
NAPI_METHOD_ARGV(1);
193+
UNWRAP_DB_HANDLE();
194+
195+
if (*dbHandle) {
196+
try {
197+
DBRegistry::DestroyDB((*dbHandle)->path);
198+
} catch (const std::exception& e) {
199+
DEBUG_LOG("%p Database::Destroy Error: %s\n", dbHandle->get(), e.what());
200+
::napi_throw_error(env, nullptr, e.what());
201+
return nullptr;
202+
}
203+
} else {
204+
::napi_throw_error(env, nullptr, "Invalid database handle");
205+
return nullptr;
206+
}
207+
208+
NAPI_RETURN_UNDEFINED();
209+
}
210+
182211
/**
183212
* Drops the RocksDB database column family asynchronously. If the column family
184213
* is the default, it will clear the database instead.
@@ -639,6 +668,7 @@ napi_value Database::GetDBIntProperty(napi_env env, napi_callback_info info) {
639668
napi_value Database::GetSync(napi_env env, napi_callback_info info) {
640669
NAPI_METHOD_ARGV(3);
641670
UNWRAP_DB_HANDLE_AND_OPEN();
671+
642672
// we store this in key slice (no copying) because we are synchronously using the key
643673
rocksdb::Slice keySlice;
644674
if (!rocksdb_js::getSliceFromArg(env, argv[0], keySlice, (*dbHandle)->defaultKeyBufferPtr, "Key must be a buffer")) {
@@ -906,6 +936,11 @@ napi_value Database::Open(napi_env env, napi_callback_info info) {
906936

907937
try {
908938
(*dbHandle)->open(path, dbHandleOptions);
939+
940+
// now that the database is open and the dbHandle has a reference to
941+
// the descriptor, we can attach the database instance's smart_ptr to
942+
// the descriptor so it gets cleaned up when the descriptor is closed
943+
(*dbHandle)->descriptor->attach(*dbHandle);
909944
} catch (const std::exception& e) {
910945
DEBUG_LOG("%p Database::Open Error: %s\n", dbHandle->get(), e.what());
911946
::napi_throw_error(env, nullptr, e.what());
@@ -1156,6 +1191,7 @@ void Database::Init(napi_env env, napi_value exports) {
11561191
{ "clear", nullptr, Clear, nullptr, nullptr, nullptr, napi_default, nullptr },
11571192
{ "clearSync", nullptr, ClearSync, nullptr, nullptr, nullptr, napi_default, nullptr },
11581193
{ "close", nullptr, Close, nullptr, nullptr, nullptr, napi_default, nullptr },
1194+
{ "destroy", nullptr, Destroy, nullptr, nullptr, nullptr, napi_default, nullptr },
11591195
{ "drop", nullptr, Drop, nullptr, nullptr, nullptr, napi_default, nullptr },
11601196
{ "dropSync", nullptr, DropSync, nullptr, nullptr, nullptr, napi_default, nullptr },
11611197
{ "flush", nullptr, Flush, nullptr, nullptr, nullptr, napi_default, nullptr },

src/binding/database.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ struct Database final {
4343
static napi_value Clear(napi_env env, napi_callback_info info);
4444
static napi_value ClearSync(napi_env env, napi_callback_info info);
4545
static napi_value Close(napi_env env, napi_callback_info info);
46+
static napi_value Destroy(napi_env env, napi_callback_info info);
4647
static napi_value Drop(napi_env env, napi_callback_info info);
4748
static napi_value DropSync(napi_env env, napi_callback_info info);
4849
static napi_value Flush(napi_env env, napi_callback_info info);

src/binding/db_descriptor.cpp

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -130,37 +130,14 @@ DBDescriptor::DBDescriptor(
130130
* (transactions, iterators, etc).
131131
*/
132132
DBDescriptor::~DBDescriptor() {
133-
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing \"%s\" (closables=%zu columns=%zu transactions=%zu transactionLogStores=%zu)\n",
134-
this, this->path.c_str(), this->closables.size(), this->columns.size(), this->transactions.size(), this->transactionLogStores.size());
135-
136-
std::unique_lock<std::mutex> txnsLock(this->txnsMutex);
137-
138-
while (!this->closables.empty()) {
139-
Closable* handle = *this->closables.begin();
140-
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing closable %p\n", this, handle);
141-
this->closables.erase(handle);
142-
143-
// release the mutex before calling close() to avoid a deadlock
144-
txnsLock.unlock();
145-
handle->close();
146-
txnsLock.lock();
147-
}
148-
149-
if (!this->transactionLogStores.empty()) {
150-
std::lock_guard<std::mutex> logLock(this->transactionLogMutex);
151-
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing transaction log stores (size=%zu)\n", this, this->transactionLogStores.size());
152-
for (auto& [name, transactionLogStore] : this->transactionLogStores) {
153-
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing transaction log store \"%s\"\n", this, name.c_str());
154-
transactionLogStore->close();
155-
}
156-
this->transactionLogStores.clear();
157-
}
158-
159-
this->transactions.clear();
160-
this->columns.clear();
161-
this->db.reset();
133+
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing \"%s\"\n", this, this->path.c_str());
134+
this->close();
162135
}
163136

137+
/**
138+
* Close the database descriptor and any resources associated with it
139+
* (transactions, iterators, etc).
140+
*/
164141
void DBDescriptor::close() {
165142
// check if already closing
166143
if (this->closing.exchange(true)) {
@@ -181,23 +158,58 @@ void DBDescriptor::close() {
181158
// trigger a flush
182159
rocksdb::WaitForCompactOptions options;
183160
this->db->WaitForCompact(options);
161+
162+
std::unique_lock<std::mutex> txnsLock(this->txnsMutex);
163+
164+
// Close all handles that still exist and reset their descriptor references
165+
for (auto it = this->closables.begin(); it != this->closables.end(); ) {
166+
if (auto closable = it->second.lock()) {
167+
// Remove from map before closing to avoid re-entrant detach() calls
168+
it = this->closables.erase(it);
169+
170+
// Release mutex during close to avoid deadlocks
171+
txnsLock.unlock();
172+
closable->close();
173+
txnsLock.lock();
174+
} else {
175+
// Handle was already GC'd, just remove the expired weak_ptr
176+
it = this->closables.erase(it);
177+
}
178+
}
179+
180+
if (!this->transactionLogStores.empty()) {
181+
std::lock_guard<std::mutex> logLock(this->transactionLogMutex);
182+
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing transaction log stores (size=%zu)\n", this, this->transactionLogStores.size());
183+
for (auto& [name, transactionLogStore] : this->transactionLogStores) {
184+
DEBUG_LOG("%p DBDescriptor::~DBDescriptor Closing transaction log store \"%s\"\n", this, name.c_str());
185+
transactionLogStore->close();
186+
}
187+
this->transactionLogStores.clear();
188+
}
189+
190+
this->transactions.clear();
191+
this->columns.clear();
192+
this->db.reset();
184193
}
185194

186195
/**
187196
* Registers a database resource to be closed when the descriptor is closed.
197+
*
198+
* Important: The closable must be same smart_ptr that is napi-wrapped and
199+
* bound to the JavaScript class counterpart.
188200
*/
189-
void DBDescriptor::attach(Closable* closable) {
201+
void DBDescriptor::attach(std::shared_ptr<Closable> closable) {
190202
std::lock_guard<std::mutex> lock(this->txnsMutex);
191-
this->closables.insert(closable);
203+
this->closables[closable.get()] = std::weak_ptr<Closable>(closable);
192204
}
193205

194206
/**
195207
* Unregisters a database resource from being closed when the descriptor is
196208
* closed.
197209
*/
198-
void DBDescriptor::detach(Closable* closable) {
210+
void DBDescriptor::detach(std::shared_ptr<Closable> closable) {
199211
std::lock_guard<std::mutex> lock(this->txnsMutex);
200-
this->closables.erase(closable);
212+
this->closables.erase(closable.get());
201213
}
202214

203215
/**
@@ -573,7 +585,7 @@ void DBDescriptor::transactionAdd(std::shared_ptr<TransactionHandle> txnHandle)
573585
auto id = txnHandle->id;
574586
std::lock_guard<std::mutex> lock(this->txnsMutex);
575587
this->transactions.emplace(id, txnHandle);
576-
this->closables.insert(txnHandle.get());
588+
this->closables[txnHandle.get()] = std::weak_ptr<Closable>(txnHandle);
577589
}
578590

579591
/**

src/binding/db_descriptor.h

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ struct DBDescriptor final : public std::enable_shared_from_this<DBDescriptor> {
9393
/**
9494
* Set of closables to be closed when the descriptor is closed.
9595
*/
96-
std::set<Closable*> closables;
96+
std::map<Closable*, std::weak_ptr<Closable>> closables;
9797

9898
/**
9999
* Mutex to protect the locks map.
@@ -166,24 +166,24 @@ struct DBDescriptor final : public std::enable_shared_from_this<DBDescriptor> {
166166
std::mutex transactionLogMutex;
167167

168168
private:
169-
DBDescriptor(
170-
const std::string& path,
171-
const DBOptions& options,
172-
std::shared_ptr<rocksdb::DB> db,
173-
std::unordered_map<std::string, std::shared_ptr<rocksdb::ColumnFamilyHandle>>&& columns
174-
);
169+
DBDescriptor(
170+
const std::string& path,
171+
const DBOptions& options,
172+
std::shared_ptr<rocksdb::DB> db,
173+
std::unordered_map<std::string, std::shared_ptr<rocksdb::ColumnFamilyHandle>>&& columns
174+
);
175175

176176
void discoverTransactionLogStores();
177177

178178
public:
179-
static std::shared_ptr<DBDescriptor> open(const std::string& path, const DBOptions& options);
180-
~DBDescriptor();
179+
static std::shared_ptr<DBDescriptor> open(const std::string& path, const DBOptions& options);
180+
~DBDescriptor();
181181

182182
void close();
183183
bool isClosing() const { return this->closing.load(); }
184184

185-
void attach(Closable* closable);
186-
void detach(Closable* closable);
185+
void attach(std::shared_ptr<Closable> closable);
186+
void detach(std::shared_ptr<Closable> closable);
187187

188188
void lockCall(
189189
napi_env env,
@@ -388,8 +388,8 @@ struct ListenerCallback final {
388388
*/
389389
std::weak_ptr<DBHandle> owner;
390390

391-
ListenerCallback(napi_env env, napi_threadsafe_function tsfn, napi_ref callbackRef, std::weak_ptr<DBHandle> owner = {})
392-
: env(env), threadsafeCallback(tsfn), callbackRef(callbackRef), owner(owner) {}
391+
ListenerCallback(napi_env env, napi_threadsafe_function tsfn, napi_ref callbackRef, std::weak_ptr<DBHandle> owner = {})
392+
: env(env), threadsafeCallback(tsfn), callbackRef(callbackRef), owner(owner) {}
393393

394394
// move constructor
395395
ListenerCallback(ListenerCallback&& other) noexcept

0 commit comments

Comments
 (0)