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 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/), [Couchbase](http://www.couchbase.com/nosql-databases/couchbase-server), [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.
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, Couchbase, Redis, and local filesystem.
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, Couchbase and Redis. Each one requires a bit of extra configuration.
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 Couchbase and 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.
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, Couchbase, etc.):
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 - 2018 Joseph Huckaby.
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
@@ -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 or Couchbase | Make sure your `trans_dir` points to a local fast SSD mount. |
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) or [Couchbase](../README.md#couchbase), then you should set `trans_dir` to a local filesystem path on a fast disk (ideally SSD).
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, S3 or Couchbase.
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 or Couchbase. When this happens, the Node.js process will exit (by default), and will need to be recovered (see [Recovery](#recovery) below).
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 or Couchbase 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.
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 or Couchbase for primary storage.
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 from npm
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 SQLite3 = require('sqlite3');
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
- async.series([
87
- function(callback) {
88
- self.db = new SQLite3.Database( db_file, callback );
89
- },
90
- function(callback) {
91
- // optionally set pragmas on the db
92
- if (!sql_config.pragmas) return process.nextTick(callback);
93
- async.eachSeries( Object.keys(sql_config.pragmas),
94
- function(key, callback) {
95
- var value = sql_config.pragmas[key];
96
- self.db.run(`PRAGMA ${key} = ${value};`, callback);
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
- }); // async.series
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
- this.db.run( this.commands.put, { $key: key, $value: value, $now: now }, function(err) {
152
- if (err) {
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
- if (callback) callback(err);
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
- this.db.get( this.commands.head, { $key: key }, function(err, row) {
204
- if (err) {
205
- // an actual error
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
- else if (!row) {
211
- // record not found
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( err, null );
216
- }
217
- else {
218
- callback( null, { mod: row.modified, len: row['length(value)'] } );
217
+ return callback(err, null);
219
218
  }
220
- }); // head
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
- this.db.get( this.commands.get, { $key: key }, function(err, row) {
251
- if (err) {
252
- // an actual error
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
- else if (!row) {
258
- // record not found
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( err, null );
264
+ return callback(err, null);
263
265
  }
264
- else {
265
- // success
266
- if (is_binary) {
267
- self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
268
- callback( null, row.value );
269
- }
270
- else {
271
- // possibly cache in LRU
272
- if (self.cache) {
273
- self.cache.set( key, row.value, { date: Tools.timeNow(true) } );
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
- }); // get
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
- this.db.get( this.commands.get, { $key: key }, function(err, row) {
298
- if (err) {
299
- // an actual error
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
- else if (!row) {
305
- // record not found
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( err, null );
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
- }); // get
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
- this.db.run( this.commands.delete, { $key: key }, function(err) {
394
- // In sqlite3 callbacks `this` is special, and contains `changes`
395
- if (!err && !this.changes) {
396
- err = new Error("Failed to delete object: " + key + ": Not found");
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
- if (err) {
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
- }); // delete
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 -- as long as WAL mode is enabled, this should only acquire a shared lock on the DB
532
- // allowing reads and writes to continue (albeit slowly), and NOT be included in the backup
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
- var backup = db.backup(file);
538
- backup.step(-1);
539
-
540
- var timer = setInterval( function() {
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
- if (backup.failed) {
557
- perf.end('backup');
558
- clearTimeout(timer);
559
- self.logError('backup', "SQLite backup operation failed. Please check permissions and available disk space.");
560
- fs.unlink(file, noop);
561
- finish();
562
- }
563
- }, 10 );
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
- // close db
573
- self.db.close( function(err) {
574
- if (err) self.logError('sqlite', "Failed to shutdown database cleanly: " + err);
575
- else self.logDebug(3, "Shutdown complete");
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.2.9",
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
@@ -76,6 +76,13 @@
76
76
  "enabled": true,
77
77
  "maxItems": 1000,
78
78
  "maxBytes": 10485760
79
+ },
80
+ "backups": {
81
+ "enabled": true,
82
+ "dir": "data/backups",
83
+ "filename": "backup-[yyyy]-[mm]-[dd]-[hh]-[mi]-[ss].db",
84
+ "compress": true,
85
+ "keep": 7
79
86
  }
80
87
  }
81
88
  }
@@ -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 sample_tickets = sample_data.Ticket;
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 fixtures = {
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 );
Binary file