pixl-server-storage 3.2.9 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -49
- package/docs/Transactions.md +6 -6
- package/engines/SQLite.js +125 -139
- package/package.json +1 -1
- package/test/config.json +7 -0
- package/test/data/backups/backup-2026-02-15-18-10-21.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-33.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-45.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-58.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-10.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-23.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-35.db.gz +0 -0
- package/test/test-indexer.js +12 -2
- package/test/test.js +12 -0
- package/test/data/sqlite.db +0 -0
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Overview
|
|
2
2
|
|
|
3
|
-
This module is a component for use in [pixl-server](https://www.github.com/jhuckaby/pixl-server). It implements a simple key/value storage system that can use multiple back-ends, such as [Amazon S3](https://aws.amazon.com/s3/), [
|
|
3
|
+
This module is a component for use in [pixl-server](https://www.github.com/jhuckaby/pixl-server). It implements a simple key/value storage system that can use multiple back-ends, such as [Amazon S3](https://aws.amazon.com/s3/), [Redis](https://redis.io/), or a local filesystem. It introduces the concept of a "chunked linked list", which supports extremely fast push, pop, shift, unshift, and random reads/writes. Also provided is a fast hash table implementation with key iteration, a transaction system, and an indexing and search system.
|
|
4
4
|
|
|
5
5
|
## Features at a Glance
|
|
6
6
|
|
|
7
7
|
* Uses very little memory in most cases.
|
|
8
8
|
* Store JSON or binary (raw) data records.
|
|
9
|
-
* Supports multiple back-ends including Amazon S3,
|
|
9
|
+
* Supports multiple back-ends including Amazon S3, Redis, and local filesystem.
|
|
10
10
|
* Linked lists with very fast push, pop, shift, unshift, and random reads/writes.
|
|
11
11
|
* Hash tables with key iterators, and very fast reads / writes.
|
|
12
12
|
* Advisory locking system with shared and exclusive locks.
|
|
@@ -55,7 +55,6 @@ Here is the table of contents for this current document:
|
|
|
55
55
|
+ [S3 Key Template](#s3-key-template)
|
|
56
56
|
+ [S3 Cache](#s3-cache)
|
|
57
57
|
+ [S3 Compatible Services](#s3-compatible-services)
|
|
58
|
-
* [Couchbase](#couchbase)
|
|
59
58
|
* [Redis](#redis)
|
|
60
59
|
+ [RedisCluster](#rediscluster)
|
|
61
60
|
* [SQLite](#sqlite)
|
|
@@ -267,7 +266,7 @@ The `debug` property is only used when using [Standalone Mode](#standalone-mode)
|
|
|
267
266
|
|
|
268
267
|
# Engines
|
|
269
268
|
|
|
270
|
-
The storage system can be backed by a number of different "engines", which actually perform the reads and writes. A simple local filesystem implementation is included, as well as modules for Amazon S3
|
|
269
|
+
The storage system can be backed by a number of different "engines", which actually perform the reads and writes. A simple local filesystem implementation is included, as well as modules for Amazon S3 and Redis. Each one requires a bit of extra configuration.
|
|
271
270
|
|
|
272
271
|
## Local Filesystem
|
|
273
272
|
|
|
@@ -573,44 +572,6 @@ Here is a complete example with the new parameters added:
|
|
|
573
572
|
}
|
|
574
573
|
```
|
|
575
574
|
|
|
576
|
-
## Couchbase
|
|
577
|
-
|
|
578
|
-
Please note that as of this writing (April 2022), pixl-server-storage only supports Couchbase Client v2, so you need to force install version `2.6.12` (see instructions below). Work is underway to support the v3 API, which has many breaking changes.
|
|
579
|
-
|
|
580
|
-
If you want to use [Couchbase](http://www.couchbase.com/nosql-databases/couchbase-server) as a backing store, here is how to do so. First, you need to manually install the [couchbase](https://www.npmjs.com/package/couchbase) module into your app, and it **must be v2**:
|
|
581
|
-
|
|
582
|
-
```sh
|
|
583
|
-
npm install --save couchbase@2.6.12
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
Then configure your storage thusly:
|
|
587
|
-
|
|
588
|
-
```json
|
|
589
|
-
{
|
|
590
|
-
"engine": "Couchbase",
|
|
591
|
-
"Couchbase": {
|
|
592
|
-
"connectString": "couchbase://127.0.0.1",
|
|
593
|
-
"bucket": "default",
|
|
594
|
-
"username": "",
|
|
595
|
-
"password": "",
|
|
596
|
-
"serialize": false,
|
|
597
|
-
"keyPrefix": ""
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
Set the `connectString` for your own Couchbase server setup. You can embed a username and password into the string if they are required to connect (this is different from the bucket password), and use `couchbases://` for SSL, if desired.
|
|
603
|
-
|
|
604
|
-
The `bucket` property should be set to the bucket name. If you don't know this then `default` is probably correct. The `password` property is the bucket password, and may or may not be required, depending on your Couchbase server setup.
|
|
605
|
-
|
|
606
|
-
The `serialize` property, when set to `true`, will cause all object values to be serialized to JSON before storing, and they will also be parsed from JSON when fetching. When set to `false` (the default), this is left up to Couchbase to handle.
|
|
607
|
-
|
|
608
|
-
The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key-prefix) feature. It allows you to prefix all the Couchbase keys with a common string, to separate your application's data in a shared bucket situation.
|
|
609
|
-
|
|
610
|
-
The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
|
|
611
|
-
|
|
612
|
-
Note that for Couchbase Server v5.0+ (Couchbase Node SDK 2.5+), you will have to supply both a `username` and `password` for a valid user created in the Couchbase UI. Prior to v5+ you could omit the `username` and only specify a `password`, or no password at all if your bucket has no authentication.
|
|
613
|
-
|
|
614
575
|
## Redis
|
|
615
576
|
|
|
616
577
|
If you want to use [Redis](https://redis.io/) as a backing store, here is how to do so. First, you need to manually install the [ioredis](https://www.npmjs.com/package/ioredis) module into your app:
|
|
@@ -681,10 +642,10 @@ The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3
|
|
|
681
642
|
|
|
682
643
|
## SQLite
|
|
683
644
|
|
|
684
|
-
If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [sqlite3](https://www.npmjs.com/package/sqlite3) module into your app:
|
|
645
|
+
If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) module into your app:
|
|
685
646
|
|
|
686
647
|
```sh
|
|
687
|
-
npm install --save sqlite3
|
|
648
|
+
npm install --save better-sqlite3
|
|
688
649
|
```
|
|
689
650
|
|
|
690
651
|
Then configure your storage thusly:
|
|
@@ -852,8 +813,6 @@ storage.head( 'test1', function(err, data) {
|
|
|
852
813
|
} );
|
|
853
814
|
```
|
|
854
815
|
|
|
855
|
-
Note that the [Couchbase](#couchbase) engine does not support `head`, but the [Amazon S3](#amazon-s3) and [Local Filesystem](#local-filesystem) engines both do.
|
|
856
|
-
|
|
857
816
|
You can fetch multiple records at once by calling `getMulti()` and passing in array of keys. Example:
|
|
858
817
|
|
|
859
818
|
```js
|
|
@@ -951,7 +910,7 @@ storage.getStream( 'test1.gif', function(err, readStream) {
|
|
|
951
910
|
} );
|
|
952
911
|
```
|
|
953
912
|
|
|
954
|
-
Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the
|
|
913
|
+
Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the Redis APIs do not support streams, so they are currently simulated in those engines. Streams *are* supported natively in both the Filesystem and Amazon S3 engines.
|
|
955
914
|
|
|
956
915
|
# Expiring Data
|
|
957
916
|
|
|
@@ -1322,7 +1281,7 @@ If you install the [pixl-unit](https://www.github.com/jhuckaby/pixl-unit) module
|
|
|
1322
1281
|
pixl-unit test/test.js --verbose
|
|
1323
1282
|
```
|
|
1324
1283
|
|
|
1325
|
-
This also allows you to specify an alternate test configuration file via the `--configFile` option. Using this you can load your own test config, which may use a different engine (e.g. S3,
|
|
1284
|
+
This also allows you to specify an alternate test configuration file via the `--configFile` option. Using this you can load your own test config, which may use a different engine (e.g. S3, etc.):
|
|
1326
1285
|
|
|
1327
1286
|
```sh
|
|
1328
1287
|
pixl-unit test/test.js --configFile /path/to/my/config.json
|
|
@@ -1332,7 +1291,7 @@ pixl-unit test/test.js --configFile /path/to/my/config.json
|
|
|
1332
1291
|
|
|
1333
1292
|
**The MIT License (MIT)**
|
|
1334
1293
|
|
|
1335
|
-
Copyright (c) 2015 -
|
|
1294
|
+
Copyright (c) 2015 - 2026 Joseph Huckaby.
|
|
1336
1295
|
|
|
1337
1296
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
1338
1297
|
of this software and associated documentation files (the "Software"), to deal
|
package/docs/Transactions.md
CHANGED
|
@@ -54,9 +54,9 @@ Choose your `trans_dir` carefully. This directory is used to store temporary fi
|
|
|
54
54
|
|----------------|----------------|
|
|
55
55
|
| Local Filesystem | Make sure your `trans_dir` is on the *same mount* as your local storage data, to ensure speed and safety. This is the default. |
|
|
56
56
|
| NFS Filesystem | Make sure your `trans_dir` is **NOT** on the NFS mount, but instead points to a local fast SSD mount. |
|
|
57
|
-
| S3
|
|
57
|
+
| S3 | Make sure your `trans_dir` points to a local fast SSD mount. |
|
|
58
58
|
|
|
59
|
-
So basically, if you plan to use the [Filesystem](../README.md#local-filesystem) engine with a local (i.e. non-network) disk, you don't need to make any adjustments. The default setting for `trans_dir` is a subdirectory just inside your `base_dir` engine property. But if you are going with a networked (i.e. NFS) filesystem, or you're going to use [S3](../README.md#amazon-s3)
|
|
59
|
+
So basically, if you plan to use the [Filesystem](../README.md#local-filesystem) engine with a local (i.e. non-network) disk, you don't need to make any adjustments. The default setting for `trans_dir` is a subdirectory just inside your `base_dir` engine property. But if you are going with a networked (i.e. NFS) filesystem, or you're going to use [S3](../README.md#amazon-s3), then you should set `trans_dir` to a local filesystem path on a fast disk (ideally SSD).
|
|
60
60
|
|
|
61
61
|
**Pro-Tip:** When using transactions with the Amazon S3 engine, consider setting the `maxAttempts` property in your `S3` configuration object, so the AWS client library makes multiple attempts before failing an operation. Network hiccups can happen.
|
|
62
62
|
|
|
@@ -180,7 +180,7 @@ storage.begin( 'some_path', function(err, trans) {
|
|
|
180
180
|
|
|
181
181
|
It should be noted that if you receive an error from a [commit()](API.md#commit) call, it is *vital* that you call [abort()](API.md#abort) to undo whatever operations may have already executed. A commit error is typically very bad, and your storage system will be in an unknown state. Only by calling [abort()](API.md#abort) can you restore it to before the transaction started.
|
|
182
182
|
|
|
183
|
-
If the [abort()](API.md#abort) also fails, then the database raises a fatal error and exits immediately. See [Emergency Shutdown](#emergency-shutdown) below for details about what this means, and [Recovery](#recovery) for how to get back up and running. Examples of fatal errors include your disk running completely out of space, or a major network failure when using NFS,
|
|
183
|
+
If the [abort()](API.md#abort) also fails, then the database raises a fatal error and exits immediately. See [Emergency Shutdown](#emergency-shutdown) below for details about what this means, and [Recovery](#recovery) for how to get back up and running. Examples of fatal errors include your disk running completely out of space, or a major network failure when using NFS, or S3.
|
|
184
184
|
|
|
185
185
|
## Automatic API Wrappers
|
|
186
186
|
|
|
@@ -215,7 +215,7 @@ You can still use these methods inside of your own transaction, and they will "j
|
|
|
215
215
|
|
|
216
216
|
## Emergency Shutdown
|
|
217
217
|
|
|
218
|
-
If a fatal storage error is encountered in the middle of an abort (rollback) operation, the database immediately shuts itself down. This is because your data will be in an undefined state, stuck in the middle of partial transaction. This should be a very rare event, only occurring when the underlying storage completely fails to write records. Examples include a disk running out of space (local or NFS filesystem), or a hard network failure with S3
|
|
218
|
+
If a fatal storage error is encountered in the middle of an abort (rollback) operation, the database immediately shuts itself down. This is because your data will be in an undefined state, stuck in the middle of partial transaction. This should be a very rare event, only occurring when the underlying storage completely fails to write records. Examples include a disk running out of space (local or NFS filesystem), or a hard network failure with S3. When this happens, the Node.js process will exit (by default), and will need to be recovered (see [Recovery](#recovery) below).
|
|
219
219
|
|
|
220
220
|
Upon fatal error, your application can hook the event and provide its own emergency shutdown procedure. Simply add an event listener on the storage object for the `fatal` event. It will be passed an `Error` object describing the actual error that occurred. It is then up to your code to call `process.exit()`. Example:
|
|
221
221
|
|
|
@@ -271,7 +271,7 @@ The transaction system is implemented by temporarily "branching" the storage sys
|
|
|
271
271
|
|
|
272
272
|
### Temporary Directory
|
|
273
273
|
|
|
274
|
-
A local temporary directory on disk is always used for transactions, regardless of the storage engine. Even if you are using S3
|
|
274
|
+
A local temporary directory on disk is always used for transactions, regardless of the storage engine. Even if you are using S3 for primary storage, transactions still use temp files on disk before and during commit. This is to insure both speed and data safety. You can control where the temporary files live by setting the `trans_dir` configuration property.
|
|
275
275
|
|
|
276
276
|
Inside the temp directory, two subdirectories are created: `data` and `logs`. The `data` directory is used to hold all modified records during a transaction (before commit). Each is named using a unique transaction ID (see below) and a hash of the original key. This ensures no files will collide with each other, even with many concurrent transactions. The `logs` directory is for rollback logs. When a transaction is committed, the original state of the mutated records is written to the log, so it can be applied in reverse during a recovery event.
|
|
277
277
|
|
|
@@ -409,7 +409,7 @@ So here is the commit process, step by step:
|
|
|
409
409
|
|
|
410
410
|
The idea here is that no matter where a crash or sudden power loss occurs during the commit process, a full rollback can always be achieved. The actual changes on the main storage system are only made once the rollback log is fully written and flushed to disk, and the log itself is only deleted when the record changes are also made (and flushed, where possible).
|
|
411
411
|
|
|
412
|
-
This is not a 100% guarantee of data durability, as corruption can happen during a crash or power loss. The rollback log may only be partially written, or corrupted in some way where a replay is not possible. This can happen because fsync calls are not guaranteed on all operating systems, filesystems or disk media (e.g. certain SSDs), and really all bets are off if you are using S3
|
|
412
|
+
This is not a 100% guarantee of data durability, as corruption can happen during a crash or power loss. The rollback log may only be partially written, or corrupted in some way where a replay is not possible. This can happen because fsync calls are not guaranteed on all operating systems, filesystems or disk media (e.g. certain SSDs), and really all bets are off if you are using S3 for primary storage.
|
|
413
413
|
|
|
414
414
|
Basically it's all just a big crap shoot, but we're trying to cover as many error cases as possible.
|
|
415
415
|
|
package/engines/SQLite.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// SQLite Storage Plugin
|
|
2
|
-
// Copyright (c) 2023 Joseph Huckaby
|
|
2
|
+
// Copyright (c) 2023 - 2026 Joseph Huckaby
|
|
3
3
|
// Released under the MIT License
|
|
4
4
|
|
|
5
|
-
// Requires the 'sqlite3' module
|
|
6
|
-
// npm install --save sqlite3
|
|
5
|
+
// Requires the 'better-sqlite3' module
|
|
7
6
|
|
|
8
7
|
const fs = require('fs');
|
|
9
8
|
const Path = require('path');
|
|
10
9
|
const zlib = require('zlib');
|
|
11
10
|
const Class = require("pixl-class");
|
|
12
11
|
const Component = require("pixl-server/component");
|
|
13
|
-
const
|
|
12
|
+
const BetterSqlite3 = require('better-sqlite3');
|
|
14
13
|
const Tools = require("pixl-tools");
|
|
15
14
|
const Perf = require("pixl-perf");
|
|
16
15
|
const Cache = require("pixl-cache");
|
|
@@ -81,36 +80,34 @@ module.exports = Class.create({
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
var db_file = Path.join( this.baseDir, sql_config.filename );
|
|
84
|
-
this.logDebug(3, "Opening database file: " + db_file);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
callback
|
|
99
|
-
); // eachSeries
|
|
100
|
-
},
|
|
101
|
-
function(callback) {
|
|
102
|
-
// create our table if necessary
|
|
103
|
-
self.db.run( 'CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )', callback );
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
function(err) {
|
|
107
|
-
if (err) {
|
|
108
|
-
self.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
|
|
109
|
-
return callback(err);
|
|
83
|
+
this.logDebug(3, "Opening database file: " + db_file, sql_config.pragmas || {});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// open sync connection
|
|
87
|
+
this.db = new BetterSqlite3(db_file);
|
|
88
|
+
|
|
89
|
+
// optionally set pragmas on the db
|
|
90
|
+
if (sql_config.pragmas) {
|
|
91
|
+
Object.keys(sql_config.pragmas).forEach(function(key) {
|
|
92
|
+
var value = sql_config.pragmas[key];
|
|
93
|
+
// value may be string (e.g. WAL) or number
|
|
94
|
+
self.db.pragma(`${key} = ${value}`);
|
|
95
|
+
});
|
|
110
96
|
}
|
|
97
|
+
|
|
98
|
+
// create our table if necessary
|
|
99
|
+
this.db.prepare('CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )').run();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
|
|
103
|
+
return callback(err);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// behave asynchronously for API compatibility
|
|
107
|
+
process.nextTick(function() {
|
|
111
108
|
self.logDebug(3, "Setup complete");
|
|
112
109
|
callback();
|
|
113
|
-
});
|
|
110
|
+
});
|
|
114
111
|
},
|
|
115
112
|
|
|
116
113
|
prepKey: function(key) {
|
|
@@ -148,20 +145,23 @@ module.exports = Class.create({
|
|
|
148
145
|
value = Buffer.from( JSON.stringify( value ) );
|
|
149
146
|
}
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
process.nextTick(function() {
|
|
149
|
+
try {
|
|
150
|
+
self.db.prepare(self.commands.put).run({ key: key, value: value, now: now });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
153
|
err.message = "Failed to store object: " + key + ": " + err;
|
|
154
154
|
self.logError('sqlite', '' + err);
|
|
155
|
+
if (callback) callback(err);
|
|
155
156
|
}
|
|
156
|
-
else self.logDebug(9, "Store complete: " + key);
|
|
157
157
|
|
|
158
|
+
self.logDebug(9, "Store complete: " + key);
|
|
158
159
|
// possibly cache in LRU
|
|
159
160
|
if (self.cache && !is_binary) {
|
|
160
161
|
self.cache.set( key, value, { date: Tools.timeNow(true) } );
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} ); // put
|
|
163
|
+
if (callback) callback(null);
|
|
164
|
+
});
|
|
165
165
|
},
|
|
166
166
|
|
|
167
167
|
putStream: function(key, inp, callback) {
|
|
@@ -200,24 +200,24 @@ module.exports = Class.create({
|
|
|
200
200
|
return;
|
|
201
201
|
} // cache
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
process.nextTick(function() {
|
|
204
|
+
var row = null;
|
|
205
|
+
try {
|
|
206
|
+
row = self.db.prepare(self.commands.head).get({ key: key });
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
206
209
|
err.message = "Failed to head key: " + key + ": " + err;
|
|
207
210
|
self.logError('sqlite', '' + err);
|
|
208
211
|
callback(err);
|
|
209
212
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// always use "NoSuchKey" in error code
|
|
213
|
+
|
|
214
|
+
if (!row) {
|
|
213
215
|
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
214
216
|
err.code = "NoSuchKey";
|
|
215
|
-
callback(
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
callback( null, { mod: row.modified, len: row['length(value)'] } );
|
|
217
|
+
return callback(err, null);
|
|
219
218
|
}
|
|
220
|
-
|
|
219
|
+
callback(null, { mod: row.modified, len: row['length(value)'] });
|
|
220
|
+
});
|
|
221
221
|
},
|
|
222
222
|
|
|
223
223
|
get: function(key, callback) {
|
|
@@ -247,44 +247,38 @@ module.exports = Class.create({
|
|
|
247
247
|
|
|
248
248
|
this.logDebug(9, "Fetching SQLite Object: " + key);
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
process.nextTick(function() {
|
|
251
|
+
var row = null;
|
|
252
|
+
try {
|
|
253
|
+
row = self.db.prepare(self.commands.get).get({ key: key });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
253
256
|
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
254
257
|
self.logError('sqlite', '' + err);
|
|
255
258
|
callback(err);
|
|
256
259
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// always use "NoSuchKey" in error code
|
|
260
|
+
|
|
261
|
+
if (!row) {
|
|
260
262
|
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
261
263
|
err.code = "NoSuchKey";
|
|
262
|
-
callback(
|
|
264
|
+
return callback(err, null);
|
|
263
265
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
var json = null;
|
|
277
|
-
try { json = JSON.parse( row.value.toString() ); }
|
|
278
|
-
catch (err) {
|
|
279
|
-
self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
|
|
280
|
-
callback( err, null );
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
|
|
284
|
-
callback( null, json );
|
|
285
|
-
}
|
|
266
|
+
if (is_binary) {
|
|
267
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
268
|
+
return callback(null, row.value);
|
|
269
|
+
}
|
|
270
|
+
if (self.cache) {
|
|
271
|
+
self.cache.set( key, row.value, { date: Tools.timeNow(true) } );
|
|
272
|
+
}
|
|
273
|
+
var json = null;
|
|
274
|
+
try { json = JSON.parse( row.value.toString() ); }
|
|
275
|
+
catch (err) {
|
|
276
|
+
self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
|
|
277
|
+
return callback(err, null);
|
|
286
278
|
}
|
|
287
|
-
|
|
279
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
|
|
280
|
+
callback(null, json);
|
|
281
|
+
});
|
|
288
282
|
},
|
|
289
283
|
|
|
290
284
|
getBuffer: function(key, callback) {
|
|
@@ -294,26 +288,25 @@ module.exports = Class.create({
|
|
|
294
288
|
|
|
295
289
|
this.logDebug(9, "Fetching SQLite Object: " + key);
|
|
296
290
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
291
|
+
process.nextTick(function() {
|
|
292
|
+
var row = null;
|
|
293
|
+
try {
|
|
294
|
+
row = self.db.prepare(self.commands.get).get({ key: key });
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
300
297
|
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
301
298
|
self.logError('sqlite', '' + err);
|
|
302
299
|
callback(err);
|
|
303
300
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// always use "NoSuchKey" in error code
|
|
301
|
+
|
|
302
|
+
if (!row) {
|
|
307
303
|
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
308
304
|
err.code = "NoSuchKey";
|
|
309
|
-
callback(
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
// success
|
|
313
|
-
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
314
|
-
callback( null, row.value );
|
|
305
|
+
return callback(err, null);
|
|
315
306
|
}
|
|
316
|
-
|
|
307
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
308
|
+
callback(null, row.value);
|
|
309
|
+
});
|
|
317
310
|
},
|
|
318
311
|
|
|
319
312
|
getStream: function(key, callback) {
|
|
@@ -390,25 +383,27 @@ module.exports = Class.create({
|
|
|
390
383
|
|
|
391
384
|
this.logDebug(9, "Deleting SQLite Object: " + key);
|
|
392
385
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
err.code = "NoSuchKey";
|
|
386
|
+
process.nextTick(function() {
|
|
387
|
+
var info = null;
|
|
388
|
+
try {
|
|
389
|
+
info = self.db.prepare(self.commands.delete).run({ key: key });
|
|
398
390
|
}
|
|
399
|
-
|
|
391
|
+
catch (err) {
|
|
400
392
|
self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
|
|
393
|
+
callback(err);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!info.changes) {
|
|
397
|
+
var err = new Error("Failed to delete object: " + key + ": Not found");
|
|
398
|
+
err.code = "NoSuchKey";
|
|
401
399
|
return callback(err);
|
|
402
400
|
}
|
|
403
401
|
self.logDebug(9, "Delete complete: " + key);
|
|
404
|
-
|
|
405
|
-
// possibly delete from LRU cache as well
|
|
406
402
|
if (self.cache && self.cache.has(key)) {
|
|
407
403
|
self.cache.delete(key);
|
|
408
404
|
}
|
|
409
|
-
|
|
410
405
|
callback();
|
|
411
|
-
});
|
|
406
|
+
});
|
|
412
407
|
},
|
|
413
408
|
|
|
414
409
|
runMaintenance: function(callback) {
|
|
@@ -528,39 +523,30 @@ module.exports = Class.create({
|
|
|
528
523
|
} );
|
|
529
524
|
}; // compress
|
|
530
525
|
|
|
531
|
-
// Perform the backup
|
|
532
|
-
//
|
|
533
|
-
// see: https://sqlite.org/forum/forumpost/95c0e12f4c
|
|
526
|
+
// Perform the backup using better-sqlite3's async backup API
|
|
527
|
+
// As long as WAL mode is enabled, this should only acquire a shared lock
|
|
534
528
|
perf.begin('backup');
|
|
535
|
-
|
|
536
529
|
var db = this.db;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (backup.idle) {
|
|
542
|
-
backup.step(-1);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (backup.completed) {
|
|
546
|
-
perf.end('backup');
|
|
547
|
-
clearTimeout(timer);
|
|
548
|
-
self.logDebug(6, "SQLite backup operation completed: " + file);
|
|
549
|
-
|
|
550
|
-
// now compress or keep or done
|
|
551
|
-
if (backups.compress) compress();
|
|
552
|
-
else if (backups.keep) keep();
|
|
553
|
-
else finish();
|
|
530
|
+
db.backup(file, {
|
|
531
|
+
progress: function(info) {
|
|
532
|
+
// transfer pages per tick (tuneable)
|
|
533
|
+
return 100;
|
|
554
534
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
535
|
+
})
|
|
536
|
+
.then(function() {
|
|
537
|
+
perf.end('backup');
|
|
538
|
+
self.logDebug(6, "SQLite backup operation completed: " + file);
|
|
539
|
+
// now compress or keep or done
|
|
540
|
+
if (backups.compress) compress();
|
|
541
|
+
else if (backups.keep) keep();
|
|
542
|
+
else finish();
|
|
543
|
+
})
|
|
544
|
+
.catch(function(err) {
|
|
545
|
+
perf.end('backup');
|
|
546
|
+
self.logError('backup', "SQLite backup operation failed. Please check permissions and available disk space. " + err);
|
|
547
|
+
fs.unlink(file, noop);
|
|
548
|
+
finish();
|
|
549
|
+
});
|
|
564
550
|
},
|
|
565
551
|
|
|
566
552
|
shutdown: function(callback) {
|
|
@@ -569,15 +555,15 @@ module.exports = Class.create({
|
|
|
569
555
|
this.logDebug(2, "Shutting down SQLite");
|
|
570
556
|
|
|
571
557
|
if (this.db) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
callback();
|
|
577
|
-
} );
|
|
578
|
-
self.db = null;
|
|
579
|
-
self.commands = null;
|
|
558
|
+
try { this.db.close(); }
|
|
559
|
+
catch (err) { this.logError('sqlite', "Failed to shutdown database cleanly: " + err); }
|
|
560
|
+
this.db = null;
|
|
561
|
+
this.commands = null;
|
|
580
562
|
}
|
|
563
|
+
process.nextTick(function() {
|
|
564
|
+
self.logDebug(3, "Shutdown complete");
|
|
565
|
+
callback();
|
|
566
|
+
});
|
|
581
567
|
}
|
|
582
568
|
|
|
583
569
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "A key/value/list storage component for the pixl-server framework.",
|
|
5
5
|
"author": "Joseph Huckaby <jhuckaby@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/jhuckaby/pixl-server-storage",
|
package/test/config.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/test/test-indexer.js
CHANGED
|
@@ -9,8 +9,11 @@ var cp = require('child_process');
|
|
|
9
9
|
var async = require('async');
|
|
10
10
|
var Tools = require('pixl-tools');
|
|
11
11
|
|
|
12
|
+
var sample_tickets = null;
|
|
13
|
+
var fixtures = null;
|
|
14
|
+
|
|
12
15
|
var sample_data = require('./sample-data.json');
|
|
13
|
-
var
|
|
16
|
+
var orig_sample_tickets = sample_data.Ticket;
|
|
14
17
|
|
|
15
18
|
var index_config = {
|
|
16
19
|
base_path: "/index/ontrack",
|
|
@@ -50,7 +53,7 @@ var index_config = {
|
|
|
50
53
|
remove_words: ["the","of","and","a","to","in","is","you","that","it","he","was","for","on","are","as","with","his","they","I","at","be","this","have","from","or","one","had","by","word","but","not","what","all","were","we","when","your","can","said","there","use","an","each","which","she","do","how","their","if","will","up","other","about","out","many","then","them","these","so","some","her","would","make","like","him","into","time","has","look","two","more","write","go","see","number","no","way","could","people","my","than","first","water","been","call","who","oil","its","now","find","long","down","day","did","get","come","made","may","part"]
|
|
51
54
|
};
|
|
52
55
|
|
|
53
|
-
var
|
|
56
|
+
var orig_fixtures = {
|
|
54
57
|
searchRecordsExact2: {
|
|
55
58
|
'title:"Released to Preproduction"': { "2653": 1, "2654": 1, "2659": 1, "2662": 1, "2665": 1 },
|
|
56
59
|
'status:open title:"Released to Preproduction"': { "2653": 1, "2654": 1 },
|
|
@@ -317,6 +320,13 @@ var fixtures = {
|
|
|
317
320
|
|
|
318
321
|
module.exports = {
|
|
319
322
|
tests: [
|
|
323
|
+
|
|
324
|
+
function resetFixtures(test) {
|
|
325
|
+
fixtures = Tools.copyHash( orig_fixtures, true );
|
|
326
|
+
sample_tickets = Tools.copyHash( orig_sample_tickets, true );
|
|
327
|
+
test.ok( true );
|
|
328
|
+
test.done();
|
|
329
|
+
},
|
|
320
330
|
|
|
321
331
|
// insert record
|
|
322
332
|
|
package/test/test.js
CHANGED
|
@@ -51,6 +51,7 @@ module.exports = {
|
|
|
51
51
|
|
|
52
52
|
// override Filesystem base dir to go somewhere more sane
|
|
53
53
|
if (storage_config.Filesystem) storage_config.Filesystem.base_dir = base_data_dir;
|
|
54
|
+
if (storage_config.SQLite) storage_config.SQLite.base_dir = base_data_dir;
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
// delete old unit test log
|
|
@@ -58,6 +59,7 @@ module.exports = {
|
|
|
58
59
|
// startup mock server
|
|
59
60
|
server.startup( function() {
|
|
60
61
|
// startup complete
|
|
62
|
+
server.logger.debug(9, "BASE DIR: " + base_data_dir);
|
|
61
63
|
|
|
62
64
|
// write log in sync mode, for troubleshooting
|
|
63
65
|
server.logger.set('sync', true);
|
|
@@ -90,6 +92,16 @@ module.exports = {
|
|
|
90
92
|
// finally add indexer tests
|
|
91
93
|
self.tests = self.tests.concat( indexerTests.tests );
|
|
92
94
|
|
|
95
|
+
// log memory usage at end
|
|
96
|
+
self.tests.push( function(test) {
|
|
97
|
+
if (global.gc) { global.gc(); global.gc(); }
|
|
98
|
+
test.ok( true );
|
|
99
|
+
setTimeout( function() {
|
|
100
|
+
self.storage.logDebug(9, "MEMORY USAGE: " + Tools.getTextFromBytes( process.memoryUsage.rss() ), process.memoryUsage() );
|
|
101
|
+
test.done();
|
|
102
|
+
}, 1 );
|
|
103
|
+
} );
|
|
104
|
+
|
|
93
105
|
// startup complete
|
|
94
106
|
// delay this by 1ms so the log is in the correct order (pre-start is async)
|
|
95
107
|
setTimeout( function() { callback(); }, 1 );
|
package/test/data/sqlite.db
DELETED
|
Binary file
|