@harperfast/harper-pro 5.0.2 → 5.0.3
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/core/bin/cliOperations.js +6 -4
- package/core/bin/copyDb.ts +208 -0
- package/core/bin/restart.js +8 -7
- package/core/bin/run.js +2 -1
- package/core/resources/DatabaseTransaction.ts +19 -2
- package/core/resources/RecordEncoder.ts +2 -2
- package/core/resources/ResourceInterface.ts +1 -1
- package/core/resources/RocksIndexStore.ts +20 -15
- package/core/resources/Table.ts +38 -24
- package/core/resources/databases.ts +29 -14
- package/core/resources/indexes/HierarchicalNavigableSmallWorld.ts +44 -23
- package/core/security/certificateVerification/ocspVerification.ts +1 -1
- package/core/security/keys.js +7 -7
- package/core/server/itc/serverHandlers.js +0 -4
- package/core/utility/hdbTerms.ts +1 -0
- package/core/utility/install/installer.js +14 -10
- package/dist/cloneNode/cloneNode.js +5 -5
- package/dist/cloneNode/cloneNode.js.map +1 -1
- package/dist/core/bin/cliOperations.js +6 -4
- package/dist/core/bin/cliOperations.js.map +1 -1
- package/dist/core/bin/copyDb.js +197 -0
- package/dist/core/bin/copyDb.js.map +1 -1
- package/dist/core/bin/restart.js +8 -7
- package/dist/core/bin/restart.js.map +1 -1
- package/dist/core/bin/run.js +3 -1
- package/dist/core/bin/run.js.map +1 -1
- package/dist/core/resources/DatabaseTransaction.js +17 -2
- package/dist/core/resources/DatabaseTransaction.js.map +1 -1
- package/dist/core/resources/RecordEncoder.js +2 -2
- package/dist/core/resources/RecordEncoder.js.map +1 -1
- package/dist/core/resources/RocksIndexStore.js +19 -12
- package/dist/core/resources/RocksIndexStore.js.map +1 -1
- package/dist/core/resources/Table.js +43 -28
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/resources/databases.js +18 -14
- package/dist/core/resources/databases.js.map +1 -1
- package/dist/core/resources/indexes/HierarchicalNavigableSmallWorld.js +14 -12
- package/dist/core/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/core/security/certificateVerification/ocspVerification.js +1 -1
- package/dist/core/security/certificateVerification/ocspVerification.js.map +1 -1
- package/dist/core/security/keys.js +7 -7
- package/dist/core/security/keys.js.map +1 -1
- package/dist/core/server/itc/serverHandlers.js +0 -4
- package/dist/core/server/itc/serverHandlers.js.map +1 -1
- package/dist/core/utility/hdbTerms.js +1 -0
- package/dist/core/utility/hdbTerms.js.map +1 -1
- package/dist/core/utility/install/installer.js +11 -8
- package/dist/core/utility/install/installer.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/studio/web/assets/{index-f5-e8ocl.js → index-CxTavHFE.js} +5 -5
- package/studio/web/assets/{index-f5-e8ocl.js.map → index-CxTavHFE.js.map} +1 -1
- package/studio/web/assets/{index.lazy-CCd1vMot.js → index.lazy-CfiR1tvq.js} +2 -2
- package/studio/web/assets/{index.lazy-CCd1vMot.js.map → index.lazy-CfiR1tvq.js.map} +1 -1
- package/studio/web/assets/{profile-gjpePJuu.js → profile-C-uokAal.js} +2 -2
- package/studio/web/assets/{profile-gjpePJuu.js.map → profile-C-uokAal.js.map} +1 -1
- package/studio/web/assets/{status-CmoVx0A5.js → status-D6xeT4ss.js} +2 -2
- package/studio/web/assets/{status-CmoVx0A5.js.map → status-D6xeT4ss.js.map} +1 -1
- package/studio/web/index.html +1 -1
|
@@ -63,7 +63,7 @@ function buildRequest() {
|
|
|
63
63
|
*/
|
|
64
64
|
async function cliOperations(req) {
|
|
65
65
|
if (!req.target) {
|
|
66
|
-
req.target = process.env.CLI_TARGET;
|
|
66
|
+
req.target = process.env.HARPER_CLI_TARGET || process.env.CLI_TARGET;
|
|
67
67
|
}
|
|
68
68
|
let target;
|
|
69
69
|
if (req.target) {
|
|
@@ -73,20 +73,22 @@ async function cliOperations(req) {
|
|
|
73
73
|
try {
|
|
74
74
|
target = new URL(`https://${req.target}:9925`);
|
|
75
75
|
} catch {
|
|
76
|
-
throw error;
|
|
76
|
+
throw error;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
target = {
|
|
80
80
|
protocol: target.protocol,
|
|
81
81
|
hostname: target.hostname,
|
|
82
82
|
port: target.port,
|
|
83
|
-
username: req.username || target.username || process.env.CLI_TARGET_USERNAME,
|
|
84
|
-
password: req.password || target.password || process.env.CLI_TARGET_PASSWORD,
|
|
83
|
+
username: req.username || target.username || process.env.HARPER_CLI_USERNAME || process.env.CLI_TARGET_USERNAME,
|
|
84
|
+
password: req.password || target.password || process.env.HARPER_CLI_PASSWORD || process.env.CLI_TARGET_PASSWORD,
|
|
85
85
|
rejectUnauthorized: req.rejectUnauthorized,
|
|
86
86
|
};
|
|
87
|
+
console.error(`Connecting to ${target.protocol}//${target.hostname}:${target.port}`);
|
|
87
88
|
} else {
|
|
88
89
|
// if we aren't doing a targeted operation (like deploy), we initialize the config and verify that local harper
|
|
89
90
|
// is running and that we can communicate with it.
|
|
91
|
+
console.error('Connecting to local Harper Pro instance');
|
|
90
92
|
initConfig();
|
|
91
93
|
if (!getHdbPid()) {
|
|
92
94
|
console.error('Harper Pro must be running to perform this operation');
|
package/core/bin/copyDb.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDatabases, getDefaultCompression, resetDatabases } from '../resource
|
|
|
2
2
|
import { open } from 'lmdb';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { move, remove } from 'fs-extra';
|
|
5
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
6
|
import { get } from '../utility/environment/environmentManager.js';
|
|
6
7
|
import OpenEnvironmentObject from '../utility/lmdb/OpenEnvironmentObject.js';
|
|
7
8
|
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.js';
|
|
@@ -11,6 +12,8 @@ import { AUDIT_STORE_OPTIONS } from '../resources/auditStore.ts';
|
|
|
11
12
|
import { describeSchema } from '../dataLayer/schemaDescribe.js';
|
|
12
13
|
import { updateConfigValue } from '../config/configUtils.js';
|
|
13
14
|
import * as hdbLogger from '../utility/logging/harper_logger.js';
|
|
15
|
+
import { RocksDatabase, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
16
|
+
import { RocksIndexStore } from '../resources/RocksIndexStore.ts';
|
|
14
17
|
|
|
15
18
|
export async function compactOnStart() {
|
|
16
19
|
hdbLogger.notify('Running compact on start');
|
|
@@ -278,3 +281,208 @@ export async function copyDb(sourceDatabase: string, targetDatabasePath: string)
|
|
|
278
281
|
targetEnv.close();
|
|
279
282
|
}
|
|
280
283
|
}
|
|
284
|
+
|
|
285
|
+
function openRocksDb(path: string, options: RocksDatabaseOptions & { dupSort?: boolean } = {}) {
|
|
286
|
+
options.disableWAL ??= false;
|
|
287
|
+
if (!existsSync(path)) {
|
|
288
|
+
mkdirSync(path, { recursive: true });
|
|
289
|
+
}
|
|
290
|
+
let db;
|
|
291
|
+
if (options.dupSort) {
|
|
292
|
+
db = RocksDatabase.open(new RocksIndexStore(path, options));
|
|
293
|
+
} else {
|
|
294
|
+
db = RocksDatabase.open(path, options);
|
|
295
|
+
db.encoder.name = options.name;
|
|
296
|
+
}
|
|
297
|
+
return db;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function migrateOnStart() {
|
|
301
|
+
hdbLogger.notify('Running migrate on start (LMDB to RocksDB)');
|
|
302
|
+
console.log('Running migrate on start (LMDB to RocksDB)');
|
|
303
|
+
|
|
304
|
+
const rootPath = get(CONFIG_PARAMS.ROOTPATH);
|
|
305
|
+
const databases = getDatabases();
|
|
306
|
+
|
|
307
|
+
updateConfigValue(CONFIG_PARAMS.STORAGE_MIGRATEONSTART, false);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
for (const databaseName in databases) {
|
|
311
|
+
if (databaseName === 'system') continue;
|
|
312
|
+
if (databaseName.endsWith('-copy')) continue;
|
|
313
|
+
let rootStore;
|
|
314
|
+
for (const tableName in databases[databaseName]) {
|
|
315
|
+
const table = databases[databaseName][tableName];
|
|
316
|
+
table.primaryStore.put = noop;
|
|
317
|
+
table.primaryStore.remove = noop;
|
|
318
|
+
for (const attributeName in table.indices) {
|
|
319
|
+
const index = table.indices[attributeName];
|
|
320
|
+
index.put = noop;
|
|
321
|
+
index.remove = noop;
|
|
322
|
+
}
|
|
323
|
+
if (table.auditStore) {
|
|
324
|
+
table.auditStore.put = noop;
|
|
325
|
+
table.auditStore.remove = noop;
|
|
326
|
+
}
|
|
327
|
+
rootStore = table.primaryStore.rootStore;
|
|
328
|
+
}
|
|
329
|
+
if (!rootStore) {
|
|
330
|
+
console.log("Couldn't find any tables in database", databaseName);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (rootStore instanceof RocksDatabase) {
|
|
334
|
+
console.log('Database', databaseName, 'is already RocksDB, skipping');
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const targetPath = join(rootPath, DATABASES_DIR_NAME, databaseName);
|
|
339
|
+
const lmdbPath = rootStore.path;
|
|
340
|
+
const backupDest = join(rootPath, 'backup', databaseName + '.mdb');
|
|
341
|
+
|
|
342
|
+
console.log('Migrating', databaseName, 'from LMDB to RocksDB at', targetPath);
|
|
343
|
+
|
|
344
|
+
await copyDbToRocks(rootStore, databaseName, targetPath);
|
|
345
|
+
|
|
346
|
+
// Back up the original LMDB file
|
|
347
|
+
console.log('Backing up LMDB', databaseName, 'to', backupDest);
|
|
348
|
+
try {
|
|
349
|
+
await move(lmdbPath, backupDest, { overwrite: true });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.log('Error moving database', lmdbPath, 'to', backupDest, error);
|
|
352
|
+
}
|
|
353
|
+
// Remove the lock file
|
|
354
|
+
try {
|
|
355
|
+
await remove(lmdbPath + '-lock');
|
|
356
|
+
} catch {
|
|
357
|
+
// lock file may not exist
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
resetDatabases();
|
|
363
|
+
} catch (err) {
|
|
364
|
+
hdbLogger.error('Error resetting databases after migration', err);
|
|
365
|
+
console.error('Error resetting databases after migration', err);
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
hdbLogger.error('Error migrating database', err);
|
|
369
|
+
console.error('Error migrating database', err);
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function copyDbToRocks(sourceRootStore, sourceDatabase: string, targetPath: string) {
|
|
375
|
+
console.log(`Migrating database ${sourceDatabase} to RocksDB at ${targetPath}`);
|
|
376
|
+
const sourceDbisDb = sourceRootStore.dbisDb;
|
|
377
|
+
|
|
378
|
+
const targetRootStore = openRocksDb(targetPath, { disableWAL: false });
|
|
379
|
+
const targetDbisDb = openRocksDb(targetPath, {
|
|
380
|
+
disableWAL: false,
|
|
381
|
+
name: INTERNAL_DBIS_NAME,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
let written;
|
|
385
|
+
let outstandingWrites = 0;
|
|
386
|
+
const transaction = sourceDbisDb.useReadTransaction();
|
|
387
|
+
try {
|
|
388
|
+
for (const { key, value: attribute } of sourceDbisDb.getRange({ transaction })) {
|
|
389
|
+
const isPrimary = attribute.isPrimaryKey;
|
|
390
|
+
targetDbisDb.put(key, attribute);
|
|
391
|
+
if (!(isPrimary || attribute.indexed)) continue;
|
|
392
|
+
|
|
393
|
+
// Open source LMDB dbi with default encoding so values are decoded
|
|
394
|
+
const dbiInit = new OpenDBIObject(!isPrimary, isPrimary);
|
|
395
|
+
const sourceDbi = sourceRootStore.openDB(key, dbiInit);
|
|
396
|
+
|
|
397
|
+
let targetDbi;
|
|
398
|
+
if (!isPrimary) {
|
|
399
|
+
targetDbi = openRocksDb(targetPath, { dupSort: true, name: key });
|
|
400
|
+
} else {
|
|
401
|
+
targetDbi = openRocksDb(targetPath, { name: key });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log('migrating', key, 'from', sourceDatabase, 'to RocksDB');
|
|
405
|
+
await copyDbiToRocks(sourceDbi, targetDbi, isPrimary, transaction);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Note: audit store is not migrated because LMDB and RocksDB use fundamentally different
|
|
409
|
+
// audit store formats (LMDB uses a custom binary encoding in a regular DB, RocksDB uses TransactionLog).
|
|
410
|
+
// A new audit store will be created automatically when the RocksDB database is opened.
|
|
411
|
+
|
|
412
|
+
await written;
|
|
413
|
+
console.log('migrated database ' + sourceDatabase + ' to RocksDB');
|
|
414
|
+
} finally {
|
|
415
|
+
transaction.done();
|
|
416
|
+
targetRootStore.close();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function copyDbiToRocks(sourceDbi, targetDbi, isPrimary, transaction) {
|
|
420
|
+
let recordsCopied = 0;
|
|
421
|
+
let skippedRecord = 0;
|
|
422
|
+
let retries = 1000000;
|
|
423
|
+
let start = null;
|
|
424
|
+
while (retries-- > 0) {
|
|
425
|
+
try {
|
|
426
|
+
if (isPrimary) {
|
|
427
|
+
for (const { key, value, version } of sourceDbi.getRange({ start, transaction, versions: true })) {
|
|
428
|
+
try {
|
|
429
|
+
start = key;
|
|
430
|
+
if (value == null) {
|
|
431
|
+
skippedRecord++;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
written = targetDbi.put(key, value, version);
|
|
435
|
+
recordsCopied++;
|
|
436
|
+
if (transaction.openTimer) transaction.openTimer = 0;
|
|
437
|
+
if (outstandingWrites++ > 5000) {
|
|
438
|
+
await written;
|
|
439
|
+
console.log('migrated', recordsCopied, 'entries, skipped', skippedRecord, 'delete records');
|
|
440
|
+
outstandingWrites = 0;
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(
|
|
444
|
+
'Error migrating record',
|
|
445
|
+
typeof key === 'symbol' ? 'symbol' : key,
|
|
446
|
+
'from',
|
|
447
|
+
sourceDatabase,
|
|
448
|
+
error
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
for (const { key, value } of sourceDbi.getRange({ start, transaction })) {
|
|
454
|
+
try {
|
|
455
|
+
start = key;
|
|
456
|
+
written = targetDbi.put(key, value);
|
|
457
|
+
recordsCopied++;
|
|
458
|
+
if (transaction.openTimer) transaction.openTimer = 0;
|
|
459
|
+
if (outstandingWrites++ > 5000) {
|
|
460
|
+
await written;
|
|
461
|
+
console.log('migrated', recordsCopied, 'index entries');
|
|
462
|
+
outstandingWrites = 0;
|
|
463
|
+
}
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error(
|
|
466
|
+
'Error migrating index record',
|
|
467
|
+
typeof key === 'symbol' ? 'symbol' : key,
|
|
468
|
+
'from',
|
|
469
|
+
sourceDatabase,
|
|
470
|
+
error
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
console.log('finish migrating, copied', recordsCopied, 'entries, skipped', skippedRecord, 'delete records');
|
|
476
|
+
return;
|
|
477
|
+
} catch {
|
|
478
|
+
if (typeof start === 'string') {
|
|
479
|
+
if (start === 'z') {
|
|
480
|
+
return console.error('Reached end of dbi', start, 'for', sourceDatabase);
|
|
481
|
+
}
|
|
482
|
+
start = start.slice(0, -2) + 'z';
|
|
483
|
+
} else if (typeof start === 'number') start++;
|
|
484
|
+
else return console.error('Unknown key type', start, 'for', sourceDatabase);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
package/core/bin/restart.js
CHANGED
|
@@ -61,12 +61,6 @@ async function restart(req) {
|
|
|
61
61
|
|
|
62
62
|
if (envMgr.get(hdbTerms.CONFIG_PARAMS.STORAGE_COMPACTONSTART)) await compactOnStart();
|
|
63
63
|
|
|
64
|
-
if (process.env.HARPER_EXIT_ON_RESTART) {
|
|
65
|
-
// use this to exit the process so that it will be restarted by the
|
|
66
|
-
// PM/container/orchestrator.
|
|
67
|
-
hdbLogger.warn('Exiting Harper Pro process to trigger a container restart');
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
64
|
setTimeout(async () => {
|
|
71
65
|
// It seems like you should just be able to start the other process and kill this process and everything should
|
|
72
66
|
// be cleaned up, however that doesn't work for some reason; the socket listening fds somehow get transferred to the
|
|
@@ -79,9 +73,16 @@ async function restart(req) {
|
|
|
79
73
|
// remove pid file so it doesn't trip up the launch
|
|
80
74
|
await unlinkSync(path.join(envMgr.get(hdbTerms.CONFIG_PARAMS.ROOTPATH), hdbTerms.HDB_PID_FILE), `${process.pid}`);
|
|
81
75
|
hdbLogger.debug('Starting new process...');
|
|
76
|
+
if (process.env.HARPER_EXIT_ON_RESTART) {
|
|
77
|
+
// use this to exit the process so that it will be restarted by the
|
|
78
|
+
// PM/container/orchestrator.
|
|
79
|
+
hdbLogger.warn('Exiting Harper Pro process to trigger a container restart');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
82
|
// now launch the new process and exit this process
|
|
83
83
|
require('./run.js').launch(true);
|
|
84
|
-
}, 50); // can't await this because it is going to do an exit()
|
|
84
|
+
}, 50); // can't await this because it is going to do an exit(), but wait for 50ms so we give the HTTP thread a
|
|
85
|
+
// chance to return a response
|
|
85
86
|
} else {
|
|
86
87
|
// Post msg to main parent thread requesting it restart (so the main thread can process.exit())
|
|
87
88
|
parentPort.postMessage({
|
package/core/bin/run.js
CHANGED
|
@@ -19,7 +19,7 @@ const installation = require('../utility/installation.ts');
|
|
|
19
19
|
const configUtils = require('../config/configUtils.js');
|
|
20
20
|
const assignCMDENVVariables = require('../utility/assignCmdEnvVariables.js');
|
|
21
21
|
const upgrade = require('./upgrade.js');
|
|
22
|
-
const { compactOnStart } = require('./copyDb.ts');
|
|
22
|
+
const { compactOnStart, migrateOnStart } = require('./copyDb.ts');
|
|
23
23
|
const minimist = require('minimist');
|
|
24
24
|
const keys = require('../security/keys.js');
|
|
25
25
|
const { startHTTPThreads } = require('../server/threads/socketRouter.ts');
|
|
@@ -192,6 +192,7 @@ async function main(calledByInstall = false) {
|
|
|
192
192
|
await initialize(calledByInstall, true);
|
|
193
193
|
|
|
194
194
|
if (env.get(terms.CONFIG_PARAMS.STORAGE_COMPACTONSTART)) await compactOnStart();
|
|
195
|
+
if (env.get(terms.CONFIG_PARAMS.STORAGE_MIGRATEONSTART)) await migrateOnStart();
|
|
195
196
|
|
|
196
197
|
const isScripted = process.env.IS_SCRIPTED_SERVICE && !cmdArgs.service;
|
|
197
198
|
|
|
@@ -7,6 +7,7 @@ import * as envMngr from '../utility/environment/environmentManager.js';
|
|
|
7
7
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
8
8
|
import { convertToMS } from '../utility/common_utils.js';
|
|
9
9
|
import { when } from '../utility/when.ts';
|
|
10
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
11
|
import { Transaction as RocksTransaction, type Store as RocksStore } from '@harperfast/rocksdb-js';
|
|
11
12
|
import type { RootDatabaseKind } from './databases.ts';
|
|
12
13
|
import type { Entry } from './RecordEncoder.ts';
|
|
@@ -19,6 +20,7 @@ export const TRANSACTION_STATE = {
|
|
|
19
20
|
OPEN: 1, // the transaction is open and can be used for reads and writes
|
|
20
21
|
LINGERING: 2, // the transaction has completed a read, but can be used for immediate writes
|
|
21
22
|
};
|
|
23
|
+
const MAX_RETRIES = 40;
|
|
22
24
|
let outstandingCommit, outstandingCommitStart;
|
|
23
25
|
let confirmReplication;
|
|
24
26
|
export function replicationConfirmation(callback) {
|
|
@@ -93,6 +95,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
93
95
|
if (this.open !== TRANSACTION_STATE.OPEN) return; // can not start a new read transaction as there is no future commit that will take place, just have to allow the read to latest database state
|
|
94
96
|
|
|
95
97
|
this.transaction = new RocksTransaction(this.db.store);
|
|
98
|
+
|
|
96
99
|
if (this.timestamp) {
|
|
97
100
|
this.transaction.setTimestamp(this.timestamp);
|
|
98
101
|
}
|
|
@@ -158,7 +161,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
158
161
|
transaction ??= this.transaction;
|
|
159
162
|
let immediateCommit = false;
|
|
160
163
|
if (!transaction) {
|
|
161
|
-
transaction = new RocksTransaction(
|
|
164
|
+
transaction = new RocksTransaction(operation.store.store as RocksStore);
|
|
165
|
+
if (operation.store.rootStore !== this.db.rootStore) {
|
|
166
|
+
harperLogger.warn?.('Created new transaction in save, but the store does match existing store', transaction.id);
|
|
167
|
+
}
|
|
162
168
|
if (this.open === TRANSACTION_STATE.OPEN) {
|
|
163
169
|
this.transaction = transaction;
|
|
164
170
|
} else {
|
|
@@ -168,9 +174,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
168
174
|
if (txnTime) {
|
|
169
175
|
transaction.setTimestamp(txnTime);
|
|
170
176
|
}
|
|
177
|
+
} else {
|
|
171
178
|
}
|
|
172
179
|
if (this.retries > 0) {
|
|
173
|
-
//
|
|
180
|
+
// This marks the Rocks transaction as a retry so we don't write the transaction log again
|
|
174
181
|
transaction.isRetry = true;
|
|
175
182
|
}
|
|
176
183
|
if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
|
|
@@ -295,6 +302,16 @@ export class DatabaseTransaction implements Transaction {
|
|
|
295
302
|
// if the transaction failed due to concurrent changes, we need to retry. First record this as an increased risk of contention/retry
|
|
296
303
|
// for future transactions
|
|
297
304
|
this.retries++;
|
|
305
|
+
harperLogger.debug?.('retrying', transaction.id, this.retries);
|
|
306
|
+
if (this.retries > 2) {
|
|
307
|
+
if (this.retries > MAX_RETRIES) {
|
|
308
|
+
throw new ServerError(
|
|
309
|
+
`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
// start delaying, back off to try to space out transactions and avoid excessive conflicts
|
|
313
|
+
return delay(this.retries * this.retries).then(() => this.commit({ transaction }));
|
|
314
|
+
}
|
|
298
315
|
return this.commit({ transaction }); // try again
|
|
299
316
|
} else throw error;
|
|
300
317
|
}
|
|
@@ -688,13 +688,13 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
688
688
|
export function setAdditionalAuditRefs(refs: Array<{ version: number; nodeId: number }> | undefined) {
|
|
689
689
|
additionalAuditRefsNextEncoding = refs;
|
|
690
690
|
}
|
|
691
|
-
export function removeEntry(store: any, entry: any,
|
|
691
|
+
export function removeEntry(store: any, entry: any, options?: any) {
|
|
692
692
|
if (!entry) return;
|
|
693
693
|
if (entry.value && entry.metadataFlags & HAS_BLOBS) {
|
|
694
694
|
// if it used to have blobs, we need to delete the old blobs
|
|
695
695
|
deleteBlobsInObject(entry.value);
|
|
696
696
|
}
|
|
697
|
-
return store.remove(entry.key,
|
|
697
|
+
return store.remove(entry.key, options);
|
|
698
698
|
}
|
|
699
699
|
export interface RecordObject {
|
|
700
700
|
getUpdatedTime(): number;
|
|
@@ -69,7 +69,7 @@ export interface Context {
|
|
|
69
69
|
/** The user making the request */
|
|
70
70
|
user?: User;
|
|
71
71
|
/** Check the username and password against the core user table to verify user identity */
|
|
72
|
-
login
|
|
72
|
+
login?: (username: string, password: string) => Promise<string>;
|
|
73
73
|
/** Describes the current cookie-based session if it is present and grants the capacity to delete it. authentication.enableSessions must be turned on in the harperdb-config.yaml */
|
|
74
74
|
session?: Session;
|
|
75
75
|
/** The database transaction object */
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DBI,
|
|
3
|
-
Store,
|
|
4
|
-
type StoreContext,
|
|
5
3
|
type StoreIteratorOptions,
|
|
6
4
|
type StorePutOptions,
|
|
7
5
|
type StoreRemoveOptions,
|
|
6
|
+
RocksDatabase,
|
|
8
7
|
} from '@harperfast/rocksdb-js';
|
|
9
8
|
import { Id } from './ResourceInterface.ts';
|
|
10
9
|
import { MAXIMUM_KEY } from 'ordered-binary';
|
|
11
10
|
|
|
12
11
|
declare module '@harperfast/rocksdb-js' {
|
|
13
|
-
// eslint-disable-next-line no-unused-vars
|
|
14
12
|
interface DBI<T> {
|
|
15
13
|
getValuesCount(indexedValue: any): number;
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* A specialized RocksDB-based index store that maintains indexed references to primary keys.
|
|
19
|
+
* This store uses composite keys consisting of indexed values and primary keys, enabling
|
|
20
|
+
* efficient range queries over indexed data. The actual data values are stored as null since
|
|
21
|
+
* this is purely an index structure pointing to primary records elsewhere. This extends
|
|
22
|
+
* RocksDatabase rather than a store because it actually alters the interface
|
|
23
|
+
*/
|
|
24
|
+
export class RocksIndexStore extends RocksDatabase {
|
|
20
25
|
/**
|
|
21
26
|
* Get all entries matching the range
|
|
22
27
|
* @param options
|
|
23
28
|
*/
|
|
24
|
-
getRange(
|
|
29
|
+
getRange(options: StoreIteratorOptions): Iterable<any> {
|
|
25
30
|
let { start, end, exclusiveStart, inclusiveEnd, reverse } = options;
|
|
26
31
|
if ((reverse ? !exclusiveStart : exclusiveStart) && start !== undefined) {
|
|
27
32
|
start = [start, MAXIMUM_KEY];
|
|
@@ -30,7 +35,7 @@ export class RocksIndexStore extends Store {
|
|
|
30
35
|
end = [end, MAXIMUM_KEY];
|
|
31
36
|
}
|
|
32
37
|
const translatedOptions = { ...options, start, end };
|
|
33
|
-
return super.getRange(
|
|
38
|
+
return super.getRange(translatedOptions).map(({ key }) => {
|
|
34
39
|
return { key: key[0], value: key.length > 2 ? key.slice(1) : key[1] };
|
|
35
40
|
});
|
|
36
41
|
}
|
|
@@ -41,20 +46,20 @@ export class RocksIndexStore extends Store {
|
|
|
41
46
|
* @param primaryKey
|
|
42
47
|
* @param txnId
|
|
43
48
|
*/
|
|
44
|
-
put(
|
|
45
|
-
return super.putSync(
|
|
49
|
+
put(indexedValue: any, primaryKey: Id, options: StorePutOptions) {
|
|
50
|
+
return super.putSync([indexedValue, primaryKey], null, options);
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
putSync(
|
|
49
|
-
return super.putSync(
|
|
53
|
+
putSync(indexedValue: any, primaryKey: Id, options: StorePutOptions) {
|
|
54
|
+
return super.putSync([indexedValue, primaryKey], null, options);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
remove(
|
|
53
|
-
return super.removeSync(
|
|
57
|
+
remove(indexedValue: any, primaryKey: Id, options?: StoreRemoveOptions) {
|
|
58
|
+
return super.removeSync([indexedValue, primaryKey], options);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
removeSync(
|
|
57
|
-
super.removeSync(
|
|
61
|
+
removeSync(indexedValue: any, primaryKey: Id, options?: StoreRemoveOptions) {
|
|
62
|
+
super.removeSync([indexedValue, primaryKey], options);
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -63,7 +68,7 @@ export class RocksIndexStore extends Store {
|
|
|
63
68
|
* classes.
|
|
64
69
|
*/
|
|
65
70
|
DBI.prototype.getValuesCount = function getValuesCount(indexedValue: any) {
|
|
66
|
-
if (this
|
|
71
|
+
if (this instanceof RocksIndexStore) {
|
|
67
72
|
return this.store.getCount(this._context, { start: indexedValue, end: [indexedValue, MAXIMUM_KEY] });
|
|
68
73
|
}
|
|
69
74
|
throw new Error('getValuesCount is only supported if dupSort=true');
|
package/core/resources/Table.ts
CHANGED
|
@@ -1424,23 +1424,35 @@ export function makeTable(options) {
|
|
|
1424
1424
|
*/
|
|
1425
1425
|
static evict(id, existingRecord, existingVersion) {
|
|
1426
1426
|
let entry;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1427
|
+
let transaction = txnForContext({ transaction: new DatabaseTransaction() }).getReadTxn();
|
|
1428
|
+
let options = { transaction };
|
|
1429
|
+
try {
|
|
1430
|
+
if (hasSourceGet || audit) {
|
|
1431
|
+
if (!existingRecord) return;
|
|
1432
|
+
entry = primaryStore.getEntry(id, options);
|
|
1433
|
+
if (!entry || !existingRecord) return;
|
|
1434
|
+
if (entry.version !== existingVersion) return;
|
|
1435
|
+
}
|
|
1436
|
+
if (hasSourceGet) {
|
|
1437
|
+
// if there is a resolution in-progress, abandon the eviction
|
|
1438
|
+
if (primaryStore.hasLock(id, entry.version)) return;
|
|
1439
|
+
}
|
|
1440
|
+
// evictions never go in the audit log, so we can not record a deletion entry for the eviction
|
|
1441
|
+
// as there is no corresponding audit entry and it would never get cleaned up. So we must simply
|
|
1442
|
+
// removed the entry entirely, but first cleanup indices
|
|
1443
|
+
if (primaryStore.ifVersion) {
|
|
1444
|
+
// lmdb
|
|
1445
|
+
primaryStore.ifVersion?.(id, existingVersion, () => {
|
|
1446
|
+
updateIndices(id, existingRecord, null);
|
|
1447
|
+
});
|
|
1448
|
+
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), existingVersion);
|
|
1449
|
+
} else {
|
|
1450
|
+
updateIndices(id, existingRecord, null, options);
|
|
1451
|
+
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), options);
|
|
1452
|
+
}
|
|
1453
|
+
} finally {
|
|
1454
|
+
return transaction.commit();
|
|
1436
1455
|
}
|
|
1437
|
-
primaryStore.ifVersion?.(id, existingVersion, () => {
|
|
1438
|
-
updateIndices(id, existingRecord, null);
|
|
1439
|
-
});
|
|
1440
|
-
// evictions never go in the audit log, so we can not record a deletion entry for the eviction
|
|
1441
|
-
// as there is no corresponding audit entry and it would never get cleaned up. So we must simply
|
|
1442
|
-
// removed the entry entirely
|
|
1443
|
-
return removeEntry(primaryStore, entry ?? primaryStore.getEntry(id), existingVersion);
|
|
1444
1456
|
}
|
|
1445
1457
|
/**
|
|
1446
1458
|
* This is intended to acquire a lock on a record from the whole cluster.
|
|
@@ -1951,9 +1963,10 @@ export function makeTable(options) {
|
|
|
1951
1963
|
context.lastModified = existingEntry.version;
|
|
1952
1964
|
TableResource._updateResource(this, existingEntry);
|
|
1953
1965
|
}
|
|
1954
|
-
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId)
|
|
1955
|
-
|
|
1956
|
-
|
|
1966
|
+
if (precedesExistingVersion(txnTime, existingEntry, options?.nodeId) < 0) {
|
|
1967
|
+
return;
|
|
1968
|
+
} // a newer record exists locally
|
|
1969
|
+
updateIndices(id, existingRecord, null, transaction && { transaction });
|
|
1957
1970
|
if (audit || trackDeletes) {
|
|
1958
1971
|
updateRecord(
|
|
1959
1972
|
id,
|
|
@@ -3511,6 +3524,7 @@ export function makeTable(options) {
|
|
|
3511
3524
|
// determine what index values need to be removed and added
|
|
3512
3525
|
let valuesToAdd = getIndexedValues(value, indexNulls) as any[];
|
|
3513
3526
|
let valuesToRemove = getIndexedValues(existingValue, indexNulls) as any[];
|
|
3527
|
+
let isLMDB = !!index.prefetch;
|
|
3514
3528
|
if (valuesToRemove?.length > 0) {
|
|
3515
3529
|
// put this in a conditional so we can do a faster version for new records
|
|
3516
3530
|
// determine the changes/diff from new values and old values
|
|
@@ -3527,18 +3541,18 @@ export function makeTable(options) {
|
|
|
3527
3541
|
})
|
|
3528
3542
|
: [];
|
|
3529
3543
|
valuesToRemove = Array.from(setToRemove);
|
|
3530
|
-
if ((valuesToRemove.length > 0 || valuesToAdd.length > 0) && LMDB_PREFETCH_WRITES) {
|
|
3544
|
+
if (isLMDB && (valuesToRemove.length > 0 || valuesToAdd.length > 0) && LMDB_PREFETCH_WRITES) {
|
|
3531
3545
|
// prefetch any values that have been removed or added
|
|
3532
3546
|
const valuesToPrefetch = valuesToRemove.concat(valuesToAdd).map((v) => ({ key: v, value: id }));
|
|
3533
|
-
index.prefetch
|
|
3547
|
+
index.prefetch(valuesToPrefetch, noop);
|
|
3534
3548
|
}
|
|
3535
3549
|
//if the update cleared out the attribute value we need to delete it from the index
|
|
3536
3550
|
for (let i = 0, l = valuesToRemove.length; i < l; i++) {
|
|
3537
3551
|
index.remove(valuesToRemove[i], id, options);
|
|
3538
3552
|
}
|
|
3539
|
-
} else if (valuesToAdd?.length > 0 && LMDB_PREFETCH_WRITES) {
|
|
3553
|
+
} else if (isLMDB && valuesToAdd?.length > 0 && LMDB_PREFETCH_WRITES) {
|
|
3540
3554
|
// no old values, just new
|
|
3541
|
-
index.prefetch
|
|
3555
|
+
index.prefetch(
|
|
3542
3556
|
valuesToAdd.map((v) => ({ key: v, value: id })),
|
|
3543
3557
|
noop
|
|
3544
3558
|
);
|
|
@@ -4124,7 +4138,7 @@ export function makeTable(options) {
|
|
|
4124
4138
|
// don't do anything if the version has changed
|
|
4125
4139
|
return;
|
|
4126
4140
|
}
|
|
4127
|
-
updateIndices(id, existingRecord, updatedRecord);
|
|
4141
|
+
updateIndices(id, existingRecord, updatedRecord, transaction && { transaction });
|
|
4128
4142
|
if (updatedRecord) {
|
|
4129
4143
|
if (existingEntry) {
|
|
4130
4144
|
context.previousResidency = TableResource.getResidencyRecord(existingEntry.residencyId);
|