Closed CMCDragonkai closed 2 years ago
Bringing in the new changes from TypeScript-Demo-Lib-Native. But without the application builds because this is a pure library. And also removing deployment jobs.
Ok it's time to finally bring in the leveldb source code and start hacking C++.
I'm also going to solve the problem with key path here and probably prepare it for merging by cherry picking into staging, this can go along with a number of other CI/CD changes too.
@emmacasolin
@emmacasolin I'm changing the DBIterator
type to be like this and introducing DBIteratorOptions
:
/**
* Iterator options
* The `keyAsBuffer` property controls
* whether DBIterator returns KeyPath as buffers or as strings
* It should be considered to default to true
* The `valueAsBuffer` property controls value type
* It should be considered to default to true
*/
type DBIteratorOptions = {
gt?: KeyPath | Buffer | string;
gte?: KeyPath | Buffer | string;
lt?: KeyPath | Buffer | string;
lte?: KeyPath | Buffer | string;
limit?: number;
keys?: boolean;
values?: boolean;
keyAsBuffer?: boolean;
valueAsBuffer?: boolean
};
/**
* Iterator
*/
type DBIterator<K extends KeyPath | undefined, V> = {
seek: (k: KeyPath | string | Buffer) => void;
end: () => Promise<void>;
next: () => Promise<[K, V] | undefined>;
[Symbol.asyncIterator]: () => AsyncGenerator<[K, V]>;
};
This means now KeyPath
becomes pre-eminent, and anywhere I have KeyPath | Buffer | string
, the Buffer
or string
is intepreted as a singleton KeyPath
.
This also allows the key returned by the iterator to be later used by seek or the range options.
This will impact downstream EFS and PK usage though. But find and replace should be sufficient.
I've updated the DB._iterator
to use the new DBIteratorOptions
and DBIterator
. I haven't tested yet, only type checked.
I've also created a utils.toKeyPath
function that can be used to easily convert possible keypaths into keypaths. This can be used in our get
, put
, del
functions to Buffer
and string
into KeyPath
.
The keyAsBuffer
option now means that the returned KeyPath
is converted to an array of string compared to an array of buffers as it would normally be.
If the problem is double encoding then couldn't this be solved by having clear barriers to where and when the encoding is applied? We need the encoded form internally but the the user needs the un-encoded form. Doesn't this mean encoding conversion only needs to happen when we pass to and from the user?
If the problem is double encoding then couldn't this be solved by having clear barriers to where and when the encoding is applied? We need the encoded form internally but the the user needs the un-encoded form. Doesn't this mean encoding conversion only needs to happen when we pass to and from the user?
There are clear barriers. It's only applied when user passes input into the system. The problem is only for the iterator. Because iterator returns your the "key" that you're supposed to use later for other operations. Then the solution is not encode the key then again while inside the iterator. Solving the double escaping problem.
Something I recently discovered the effect of the empty key.
That is if you have a KeyPath
of []
this becomes ['']
.
Therefore if you use the empty key, in master
I believe you would not be able to see this key when iterating.
To solve this, I had to change the default iterator option to instead of using gt
to use gte
.
For example:
if (options_.gt == null && options_.gte == null) {
options_.gte = utils.levelPathToKey(levelPath);
}
This now ensures that the empty key shows up within the level during iteration.
Extra test cases were added for empty keys now, and this actually resolves another bug involving empty keys.
describe('utils', () => {
const keyPaths: Array<KeyPath> = [
// Normal keys
['foo'],
['foo', 'bar'],
// Empty keys are possible
[''],
['', ''],
['foo', ''],
['foo', '', ''],
['', 'foo', ''],
['', '', ''],
['', '', 'foo'],
// Separator can be used in key part
['foo', 'bar', Buffer.concat([utils.sep, Buffer.from('key'), utils.sep])],
[utils.sep],
[Buffer.concat([utils.sep, Buffer.from('foobar')])],
[Buffer.concat([Buffer.from('foobar'), utils.sep])],
[Buffer.concat([utils.sep, Buffer.from('foobar'), utils.sep])],
[Buffer.concat([utils.sep, Buffer.from('foobar'), utils.sep, Buffer.from('foobar')])],
[Buffer.concat([Buffer.from('foobar'), utils.sep, Buffer.from('foobar'), utils.sep]),
],
// Escape can be used in key part
[utils.esc],
[Buffer.concat([utils.esc, Buffer.from('foobar')])],
[Buffer.concat([Buffer.from('foobar'), utils.esc])],
[Buffer.concat([utils.esc, Buffer.from('foobar'), utils.esc])],
[Buffer.concat([utils.esc, Buffer.from('foobar'), utils.esc, Buffer.from('foobar')])],
[Buffer.concat([Buffer.from('foobar'), utils.esc, Buffer.from('foobar'), utils.esc])],
// Separator can be used in level parts
[Buffer.concat([utils.sep, Buffer.from('foobar')]), 'key'],
[Buffer.concat([Buffer.from('foobar'), utils.sep]), 'key'],
[Buffer.concat([utils.sep, Buffer.from('foobar'), utils.sep]), 'key'],
[Buffer.concat([utils.sep, Buffer.from('foobar'), utils.sep, Buffer.from('foobar')]), 'key'],
[Buffer.concat([Buffer.from('foobar'), utils.sep, Buffer.from('foobar'), utils.sep]), 'key'],
// Escape can be used in level parts
[Buffer.concat([utils.sep, utils.esc, utils.sep]), 'key'],
[Buffer.concat([utils.esc, utils.esc, utils.esc]), 'key'],
];
test.each(keyPaths.map(kP => [kP]))(
'parse key paths %s',
(keyPath: KeyPath) => {
const key = utils.keyPathToKey(keyPath);
const keyPath_ = utils.parseKey(key);
expect(keyPath.map((b) => b.toString())).toStrictEqual(
keyPath_.map((b) => b.toString()),
);
}
);
});
Needs rebase on top of staging now.
Time to rebase.
I was looking at 2 codebases to understand how to integrate leveldb.
It appears the classic-level is still using leveldb 1.20 which is 5 years old. The rocksdb is bit more recent.
The leveldb codebase has a bit more of a complicated build process.
The top level binding.gyp
includes a lower level gyp file:
"dependencies": [
"<(module_root_dir)/deps/leveldb/leveldb.gyp:leveldb"
],
The deps/leveldb/leveldb.gyp
file contains all the settings to actually compile the leveldb as a shared object.
Note that the binding.cc
does import leveldb headers like:
#include <leveldb/db.h>
These headers are not specified by the binding.gyp
, it's possible that by specifying the dependencies, the inclusion headers made available to the top-level target.
The leveldb.gyp
also specifies a dependency in snappy
:
"dependencies": [
"../snappy/snappy.gyp:snappy"
],
These are all organised under deps
. These are not git submodules. Except for the snappy.
I think for us, we should just copy the structure of deps
, as well as the submodule configuration. We can preserve the leveldb.gyp
and snappy.gyp
, and then just write our own binding.gyp
that uses it. Then things should proceed as normal.
So it seems that leveldb has alot of legacy aspects, rocksdb compilation is alot cleaner. In fact lots of tooling has stopped using gyp file. We can explore that later.
The presence of the snappy submodule means cloning has to be done now with git clone --recursive
. If you have already cloned, setup the git submodule with git submodule update --init --recursive
. It should bring in data into deps/snappy/snappy
.
While porting over the binding.gyp
from classic-level
, we have to be aware of: https://github.com/MatrixAI/TypeScript-Demo-Lib/pull/38#issuecomment-1124494772
The cflags
and cflags_cc
both apply to g++
and g++
is used when the file is cpp
.
However both c
and cpp
files may be used at the same time, so we should be setting relevant flags for both cflags
and cflags_cc
.
I'm not sure if this is true for non-linux platforms. The upstream classic-level does not bother with cflags_cc
. However we will set it just so that we can get the proper standard checks.
I found out what cflags+
means. It is based on:
If the key ends with a plus sign (+), the policy is for the source list contents to be prepended to the destination list. Mnemonic: + for addition or concatenation.
So cflags+
will prepend rather than appending as normal. This sets the visibility=hidden
to be true.
This is required due to: https://github.com/nodejs/node-addon-api/blob/main/doc/setup.md. The reason this is required is documented here: https://github.com/nodejs/node-addon-api/pull/460#issuecomment-544143695. This should go into TS-Demo-Lib-Native too.
I am using this .vscode/c_cpp_properties.json
as configuration, so that vscode finds the right headers, there's no automatic way to generate this atm, it's based on paths that is inside the nix-shell
:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/nix/store/zl4bvsqfxyx5vn9bbhnrmbmpfvzqj4gd-nodejs-16.14.2/include/**"
],
"defines": [],
"compilerPath": "/nix/store/58pwclg9yr437h0pfgrnbd0jis8fqasd-gcc-wrapper-11.2.0/bin/gcc",
"cStandard": "c99",
"cppStandard": "c++17",
"intelliSenseMode": "${default}"
}
],
"version": 4
}
Paths are from:
which gcc
which node
echo $npm_config_nodedir
Noticed that the sync
option for PutOptions
and DelOptions
should be switched to true
. This ensures that when the transaction is committed, it is infact committed to disk. Which means if the OS crashes, we won't be losing data.
Probably makes sense that the DBTransaction
should be using the internal write batch. I'm not sure if they are journaling the write batch, but it probably isn't, and it's in-memory. The only issue is that you cannot dump what's in the batch, which limits its debuggability.
It is possible or the DB to be corrupted in the case of power failure. In that case the repair_db
should be called. There's no guarantee that the DB will return to normal state, but we should have this available in our pk agent start
command and auto-repair in case we cannot open the DB. This is also why it is important to always backup PK agent state to other PK agents.
I believe all the types for src/leveldb
is done.
I've also discovered a nice way of doing phantom types. It relies on the same mechanism as Opaque
type constructor.
Note that I've updated the Opaque
type to use a unique symbol
as the differentiating key.
This only exists at compile-time and is elided at runtime.
Think of it as the "tag" in Tagged Unions used to implement algebraic data types.
So for example:
/**
* Iterator object
* A `napi_external` type
*/
type Iterator<
K extends string | Buffer = string | Buffer,
V extends string | Buffer = string | Buffer,
> = Opaque<'Iterator', object> & {
readonly [brandIteratorK]: K;
readonly [brandIteratorV]: V;
};
declare const brandIteratorK: unique symbol;
declare const brandIteratorV: unique symbol;
Now the K
and V
types are phantom types.
Phantom types don't work normally in TS due to structural typing system, but by telling TS that there would be a special key differentiating it, TS stops complaining about the fact that we aren't using the type parameter. But in reality we never use the type parameter and just use casting to create the specific Iterator
type.
Should update the Opaque
usage in PK to be:
/**
* Opaque types are wrappers of existing types
* that require smart constructors
*/
type Opaque<K, T> = T & { readonly [brand]: K };
declare const brand: unique symbol;
@emmacasolin
With this, we should be able to proceed with integrating src/leveldb
and replace our usage of leveldown
completely.
However the purpose of this is to also get snapshot isolation transactions. I'll do this after first prototyping substitution of leveldown
for our internal src/leveldb
.
LevelDB corruption errors are represented in JS as an exception with LEVEL_CORRUPTION
code. It was introduced here: https://github.com/Level/classic-level/commit/39cad430930e1196a66197de04448392f7e40ef1.
This should mean we can detect if this is the exception and attempt an automatic repair.
It uses https://nodejs.org/api/n-api.html#napi_create_error, which should create an Error
object with a code
property of "LEVEL_CORRUPTION
.
So we should be able to do:
if (e.code === 'LEVEL_CORRUPTION') {
// attempt repair before opening
}
Not sure how this would be tested though.
The src/leveldb
now exports both:
leveldb
- default export of the module's methodsleveldbP
- wrapped promisified version of leveldb
All leveldb types are also prefixed with LevelDB
making it easier to differentiate them from our DB's types.
I've augmented the promisify
function now with additional types that preserve typing info. It however still won't work well on overloaded function signatures.
/**
* Convert callback-style to promise-style
* If this is applied to overloaded function
* it will only choose one of the function signatures to use
*/
function promisify<
T extends Array<unknown>,
P extends Array<unknown>,
R extends (T extends [] ? void : (T extends [unknown] ? T[0] : T)),
>(
f: (...args: [ ...params: P, callback: Callback<T> ]) => unknown
): (...params: P) => Promise<R> {
// Uses a regular function so that `this` can be bound
return function (...params: P): Promise<R> {
return new Promise((resolve, reject) => {
const callback = (error, ...values) => {
if (error != null) {
return reject(error);
}
if (values.length === 0) {
(resolve as () => void)();
} else if (values.length === 1) {
resolve(values[0] as R);
} else {
resolve(values as R);
}
return;
};
params.push(callback);
f.apply(this, params);
});
};
}
This can be ported to EFS and PK @tegefaulkes @emmacasolin but low priority.
I've just had a look at the rocksdb interface, it appears the exported C++ functions are exactly the same.
NAPI_INIT() {
NAPI_EXPORT_FUNCTION(db_init);
NAPI_EXPORT_FUNCTION(db_open);
NAPI_EXPORT_FUNCTION(db_close);
NAPI_EXPORT_FUNCTION(db_put);
NAPI_EXPORT_FUNCTION(db_get);
NAPI_EXPORT_FUNCTION(db_get_many);
NAPI_EXPORT_FUNCTION(db_del);
NAPI_EXPORT_FUNCTION(db_clear);
NAPI_EXPORT_FUNCTION(db_approximate_size);
NAPI_EXPORT_FUNCTION(db_compact_range);
NAPI_EXPORT_FUNCTION(db_get_property);
NAPI_EXPORT_FUNCTION(destroy_db);
NAPI_EXPORT_FUNCTION(repair_db);
NAPI_EXPORT_FUNCTION(iterator_init);
NAPI_EXPORT_FUNCTION(iterator_seek);
NAPI_EXPORT_FUNCTION(iterator_end);
NAPI_EXPORT_FUNCTION(iterator_next);
NAPI_EXPORT_FUNCTION(batch_do);
NAPI_EXPORT_FUNCTION(batch_init);
NAPI_EXPORT_FUNCTION(batch_put);
NAPI_EXPORT_FUNCTION(batch_del);
NAPI_EXPORT_FUNCTION(batch_clear);
NAPI_EXPORT_FUNCTION(batch_write);
}
However some of the types may be slightly different.
Furthermore rocksdb does have support for block level encryption. https://github.com/facebook/rocksdb/search?q=encryption
Seems like it should be relatively simple to port over.
I do want to point out something about the indexing work:
Point is, if you stay at the key-value level, you'd need a deterministic order-preserving encryption scheme to be flexible. There's alot of research on this atm like: https://arxiv.org/abs/1706.00324. However it's quite speculative, I believe it is possible, but it seems having block encryption is just simpler and will solve the problem directly. Order preserving can still be useful if you want others to be able to "work" on the encrypted data without know the plaintext values.
The DB.ts
is only using these functions:
NAPI_EXPORT_FUNCTION(db_init);
NAPI_EXPORT_FUNCTION(db_open);
NAPI_EXPORT_FUNCTION(db_close);
NAPI_EXPORT_FUNCTION(db_put);
NAPI_EXPORT_FUNCTION(db_get);
NAPI_EXPORT_FUNCTION(db_del);
NAPI_EXPORT_FUNCTION(iterator_init);
NAPI_EXPORT_FUNCTION(iterator_seek);
NAPI_EXPORT_FUNCTION(iterator_close);
NAPI_EXPORT_FUNCTION(iterator_nextv);
NAPI_EXPORT_FUNCTION(batch_do);
I'm not currently using:
NAPI_EXPORT_FUNCTION(db_clear);
Because in C++ it appears to be doing the same thing that I'm already doing by relying on the iterator.
I've ported all of those to DB.ts
now.
Note that having ios/android builds of the native addon would only relevant if we intend to stay in the node ecosystem when on mobile. If node itself won't be embeddable (as a library or separate runtime) on mobile, then there's no need for ios/android builds of this addon.
Hit this problem again: https://github.com/facebook/jest/issues/12814.
So will need to use toEqual
when comparing buffers coming out of the leveldb, not toStrictEqual
.
I'm not sure but leveldown and classic level doesn't seem to have this problem, but I'm pretty sure they didn't wrap their buffers. So I wonder how I'm able to test like this.
The DB.test.ts
is working now directly against the native addon.
However I discovered that boolean options that have a default value of false
in the C++ code, if passed as undefined
would instead actually be asserted to being true.
It appears that the BooleanProperty
would:
undefined
ends up being true
This cause some confusion on the reverse
option. This is because I've starting assigning the options even if they are not set, which would set it as undefined
.
This would affect any option that is meant to default to false
, and we would want to ensure that if passed as undefined
, the option should be cast explicitly to true
with !!(options?.[OPTION])
.
If you're logging stuff out from C++, make sure to use STDERR otherwise it will clobber console.log
outputs:
#include <ios>
#include <iostream>
fprintf(stderr, "BOOLEAN RESULT: %s %d\n", key, result);
std::cerr << std::boolalpha << result << "\n";
Note that napi_get_value_bool
will end up casting undefined
values into a number. It's important to ensure that options are strictly defined with optionality. This means x?: boolean
cannot be x: undefined
when passed to the native addon.
I've created a filterUndefined
utility function to remove any undefined
properties to ensure that these properties are removed prior to passing into the native addon.
Before attempting the other tasks, let's see what happens if we use rocksdb
with src/rocksdb
as we may be able to get free SI (and free block encryption) if we just integrate at the C++ level.
I found a fork of leveldb's rocksdb that provides a guide on how to get SI and other advanced functionality out of rocksdb: https://github.com/lu4/rocksdb-ts. Would be worth investigating so potentially we can just use https://github.com/facebook/rocksdb/blob/main/examples/transaction_example.cc for free.
Added a new test iterating sublevels with range
it demonstrates how to use the range options which can be quite tricky.
I tried benchmarking the current code.
It looks like this:
[nix-shell:~/Projects/js-db]$ npm run bench
> @matrixai/db@4.0.5 bench
> rimraf ./benches/results && ts-node -r tsconfig-paths/register ./benches
Running "DB1KiB" suite...
Progress: 100%
get 1 KiB of data:
43 276 ops/s, ±3.66% | fastest
put 1 KiB of data:
28 117 ops/s, ±2.99% | 35.03% slower
put zero data:
35 105 ops/s, ±2.27% | 18.88% slower
put zero data then del:
18 127 ops/s, ±2.42% | slowest, 58.11% slower
Finished 4 cases!
Fastest: get 1 KiB of data
Slowest: put zero data then del
Saved to: benches/results/DB1KiB.json
Saved to: benches/results/DB1KiB.chart.html
Running "DB1MiB" suite...
Progress: 100%
get 1 MiB of data:
1 932 ops/s, ±4.57% | 93.21% slower
put 1 MiB of data:
19 ops/s, ±135.25% | slowest, 99.93% slower
put zero data:
28 456 ops/s, ±4.25% | fastest
put zero data then del:
13 221 ops/s, ±6.31% | 53.54% slower
Finished 4 cases!
Fastest: put zero data
Slowest: put 1 MiB of data
Saved to: benches/results/DB1MiB.json
Saved to: benches/results/DB1MiB.chart.html
Everything looks similar to before EXCEPT the put 1 MiB of data
.
That is now around 20 ops/s.
Whereas previously on staging
branch that's currently around 467 ops/s.
How did it drop so much. Firstly I think it's because the sync
option is true (which I set to false by default). But I've made it false, but it doesn't change anything.
This is quite strange since all other ops are basically the same.
Need to find out why my put
is so slow. Especially since we are just calling leveldbP.put
. I'm going to try the callback first.
Because I pulled this out of classic-level
and not leveldown
, the slowness in put
might have been introduced by classic-level
. The current staging
is still using leveldown
. The native addon appear to be incompatible.
I tried with classic level, it's not that. So now I need to think if it's now due to our promise implementation or something else. Very strange. Maybe there's optimisation flags that aren't turned on.
It's got nothing to do with the C++ code.
I installed the classic-level
node package, and then copied their prebuilt binary to prebuilds/linux-x64
.
So the JS code is then using their library, but for some reason, by directly using the JS/TS that I'm wrtiting, I'm getting 19 ops/s, ±145.79%
.
Not only is the 19
number strange, but it's also the margin of error is so high as well. Perhaps there's something about the way the database is being setup that is different.
It's also not the src/leveldb
code. Benching that gives us 471 ops/s
. So it's something that's happening inside DB.ts
.
It turns out its the iterator.
If the iterator is left open, it appears to cause performance issues for the 1MiB benchmark. This is quite surprising that the existence of an open iterator can cause such issues.
We have a bug where the iterator is not being closed.
I need to refactor the next
call inside the iterator so that it is actually being closed.
The await iterator.close()
must not be called twice. If you call it twice, it's an error.
Ok so there's some missing logic in the iterator implementation atm:
seek
, next
, and close
. That is these calls cannot be run concurrently.ready
decorator currently has a block
parameter that enables blocking these calls during the async lifecycle. So we should use a lock.async-locks
to ensure the methods are called.close
should based on our CreateDestroy
system.So I'm thinking we create a class called DBIterator
to take care of this so it's clearer and fits within our existing async class design.
The DBTransaction
is a bit more complicated since it has to maintain 2 iterators, however this might change once we bring in rocksdb since it might already have its own transaction state that we can get for free.
It is essential to run git submodule update --init --recursive
now if we using submodules. Also use git submodule status
to see what the status of the submodules are.
Currently:
[nix-shell:~/Projects/js-db]$ git submodule status
09c7e96eac1ab983f97ce9e0406730b8014b3398 deps/rocksdb/rocksdb (v5.8-3265-g09c7e96ea)
b02bfa754ebf27921d8da3bd2517eab445b84ff9 deps/snappy/snappy (1.1.7)
We now have DBIterator
, this has taken over the functionality that was specified inside DB.ts
before.
The benchmarks are back to how they were before, and we no longer have the iterator leak. We've made some changes to how the iterator next and seek calls work. Concurrent calls to next will block rather than throwing an error, but if a seek call is made while a next is in progress, that will result in ErrorDBIteratorBusy
.
Although DBIterator
is an CreateDestroy
, it doesn't actually have an asynchronous creation at all, and it is just created from the constructor. This is because the iterator()
call is supposed to just return the iterator object without wrapping it as a promise.
Derived from #18
Description
This implements the snapshot isolation DB transaction.
This means DBTransaction will be automatically snapshot isolated, which means most locking will be unnecessary.
Instead when performing a transaction, there's a chance for a
ErrorDBTransactionConflict
exception which means there was a write conflict with another transaction.Users can then decide on their discretion to retry the operation if they need to (assuming any non-DB side-effects are idempotent, noops or can be compensated). This should reduce the amount of locking overhead we need to do in Polykey. We may bubble up the conflict exception to the user, so the user can re-run their command, or in some cases, in-code we will automatically perform a retry. The user in this case can be the PK client, or the another PK agent or the PK GUI.
There is still one situation where user/application locks are needed, and that's where there may be a write-skew. See snapshot isolation https://en.wikipedia.org/wiki/Snapshot_isolation for more details and also https://www.cockroachlabs.com/blog/what-write-skew-looks-like/.
In the future we may upgrade to SSI (serializable snapshot isolation) which will eliminate this write-skew possibility.
~Additionally this PR will also enable the
keyAsBuffer
andvalueAsBuffer
options on the iterators, enabling easier usage of the iterators without having to usedbUtils.deserialize<T>(value)
where it can be configured ahead of time.~ - already mergedSee this https://www.fluentcpp.com/2019/08/30/how-to-disable-a-warning-in-cpp/ as to how to disable warnings in C++ cross platform.
Also see: https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
Issues Fixed
Tasks
keyAsBuffer
andvalueAsBuffer
~ - already merged in stagingDBTransaction
lifecyclesnapshotLock
to ensure mutual exclusion when using the snapshot iteratorgetSnapshot
as the last-resort getter forDBTransaction.get
DBTransaction.iterator
to use the snapshot iteratorkeyAsBuffer
andvalueAsBuffer
usage on the iterator, expectstring
andV
types. Existing tests should still work. Since by default iterator returns buffers. This is different fromget
which by default does not return raw buffers.~ - already merged into stagingErrorDBTransactionConflict
sync
option when committingDBTransaction
by default, allow it to be set offdb_destroy
can replace our removal of the db path~ - no need for this, because we may have more interesting state in the db pathCORRUPTION
error code and attempt automatic repair and reopen, or at least provide users the ability to call the command with a special exception for this - https://github.com/MatrixAI/js-db/pull/19#issuecomment-1143114335~ - To be addressed later in #35LevelDB
LevelDBP
DB.ts
directlyDBIterator
to maintain async lifecycle of the iterator - https://github.com/MatrixAI/js-db/pull/19#issuecomment-1145852220 (it was a good thing we caught this during benchmarking, a serious performance regression that led us to discover a resource leak)ErrorDBLiveReference
to mean that an iterator or transaction object is still alive whendb.stop()
is called, and therefore it must prevent any stopping, we must there maintain a weakset for every transaction/iterator objects that we create. This can also be used for subdatabases, where you create a prefixed DB in the future.~WeakSet
does not support size or length, there's no way to know how many of these objects are still aliveDB
and subtract an allocation counter instead, or just maintain reference to objects and remove themSet
then, anddestroy
removes them from the setErrorDBLiveReference
, just auto-closes the same as in C++transaction_*
native functions:transactionInit
transactionCommit
transactionRollback
transactionGet
transationGetForUpdate
transactionPut
transactionDel
transactionMultiGet
transactionClear
transactionIteratorInit
transactionSnapshot
transactionMultiGetForUpdate
GetForUpdate
, this "materializes the conflict" in a write skew so that it becomes a write write conflict. Although rocksdb calls this a read write conflcit. SSI is not provided by rocksdb, it is however available in cockroachdb and badgerdb, but hopefully someone backports that to rocksdb.DBIterator
doesn't seem to work when no level path is specified, it iterates over no records at all, it's possible that our iterator options don't make sense when thedata
sublevel isn't used.levelPath
andoptions
parameters, becauselevelPath
is a far more used thanoptions
, this does imply an API break for EFS... etc, but it should be a quick find and replaceFinal checklist