Skip to content

Commit 68ca2f8

Browse files
authored
Merge branch 'HarperFast:main' into main
2 parents dd590d4 + 2790aee commit 68ca2f8

25 files changed

Lines changed: 618 additions & 97 deletions

.github/workflows/create-release.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
echo "Package.json version: v${{ steps.package-version.outputs.packageVersion }}"
4444
test "${{ steps.tag-version.outputs.tagVersion }}" == "v${{ steps.package-version.outputs.packageVersion }}"
4545
- name: Notify release in progress in Slack
46-
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
46+
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
4747
with:
4848
method: chat.postMessage
4949
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -103,7 +103,7 @@ jobs:
103103
needs: [create-release]
104104
runs-on: ubuntu-latest
105105
steps:
106-
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
106+
- uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
107107
with:
108108
method: chat.postMessage
109109
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -144,7 +144,7 @@ jobs:
144144
needs: [create-release]
145145
runs-on: ubuntu-latest
146146
steps:
147-
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
147+
- uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
148148
with:
149149
method: chat.postMessage
150150
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/notify-release-published.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Send Slack release published notification
16-
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
16+
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
1717
with:
1818
method: chat.postMessage
1919
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/publish-docker.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2424
- name: Setup Docker metadata
2525
id: meta
26-
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
26+
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
2727
with:
2828
images: harperfast/harper
2929
- name: Login to Docker Hub
@@ -61,14 +61,14 @@ jobs:
6161
docker-image-tag: ${{ steps.meta.outputs.version }}
6262
steps:
6363
- name: Download digests
64-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
64+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
6565
with:
6666
path: /tmp/digests
6767
pattern: digest-*
6868
merge-multiple: true
6969
- name: Setup Docker metadata
7070
id: meta
71-
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
71+
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
7272
with:
7373
images: harperfast/harper
7474
tags: |
@@ -96,7 +96,7 @@ jobs:
9696
runs-on: ubuntu-latest
9797
steps:
9898
- name: Send Slack published notification
99-
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
99+
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
100100
with:
101101
method: chat.postMessage
102102
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -137,7 +137,7 @@ jobs:
137137
needs: [merge]
138138
runs-on: ubuntu-latest
139139
steps:
140-
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
140+
- uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
141141
with:
142142
method: chat.postMessage
143143
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/publish-npm.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
runs-on: ubuntu-latest
9898
steps:
9999
- name: Send Slack published notification
100-
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
100+
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
101101
with:
102102
method: chat.postMessage
103103
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -144,7 +144,7 @@ jobs:
144144
- publish-harper-npm-package
145145
runs-on: ubuntu-latest
146146
steps:
147-
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
147+
- uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
148148
with:
149149
method: chat.postMessage
150150
token: ${{ secrets.SLACK_BOT_TOKEN }}

bin/copyDb.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getDatabases, getDefaultCompression, resetDatabases } from '../resource
22
import { open } from 'lmdb';
33
import { join } from 'path';
44
import { move, remove } from 'fs-extra';
5+
import { existsSync, mkdirSync } from 'node:fs';
56
import { get } from '../utility/environment/environmentManager.js';
67
import OpenEnvironmentObject from '../utility/lmdb/OpenEnvironmentObject.js';
78
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.js';
@@ -11,6 +12,8 @@ import { AUDIT_STORE_OPTIONS } from '../resources/auditStore.ts';
1112
import { describeSchema } from '../dataLayer/schemaDescribe.js';
1213
import { updateConfigValue } from '../config/configUtils.js';
1314
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';
1417

1518
export async function compactOnStart() {
1619
hdbLogger.notify('Running compact on start');
@@ -278,3 +281,208 @@ export async function copyDb(sourceDatabase: string, targetDatabasePath: string)
278281
targetEnv.close();
279282
}
280283
}
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+
}

bin/restart.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,6 @@ async function restart(req) {
6161

6262
if (envMgr.get(hdbTerms.CONFIG_PARAMS.STORAGE_COMPACTONSTART)) await compactOnStart();
6363

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 process to trigger a container restart');
68-
process.exit(0);
69-
}
7064
setTimeout(async () => {
7165
// It seems like you should just be able to start the other process and kill this process and everything should
7266
// 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) {
7973
// remove pid file so it doesn't trip up the launch
8074
await unlinkSync(path.join(envMgr.get(hdbTerms.CONFIG_PARAMS.ROOTPATH), hdbTerms.HDB_PID_FILE), `${process.pid}`);
8175
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 process to trigger a container restart');
80+
process.exit(0);
81+
}
8282
// now launch the new process and exit this process
8383
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
8586
} else {
8687
// Post msg to main parent thread requesting it restart (so the main thread can process.exit())
8788
parentPort.postMessage({

bin/run.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const installation = require('../utility/installation.ts');
1919
const configUtils = require('../config/configUtils.js');
2020
const assignCMDENVVariables = require('../utility/assignCmdEnvVariables.js');
2121
const upgrade = require('./upgrade.js');
22-
const { compactOnStart } = require('./copyDb.ts');
22+
const { compactOnStart, migrateOnStart } = require('./copyDb.ts');
2323
const minimist = require('minimist');
2424
const keys = require('../security/keys.js');
2525
const { startHTTPThreads } = require('../server/threads/socketRouter.ts');
@@ -192,6 +192,7 @@ async function main(calledByInstall = false) {
192192
await initialize(calledByInstall, true);
193193

194194
if (env.get(terms.CONFIG_PARAMS.STORAGE_COMPACTONSTART)) await compactOnStart();
195+
if (env.get(terms.CONFIG_PARAMS.STORAGE_MIGRATEONSTART)) await migrateOnStart();
195196

196197
const isScripted = process.env.IS_SCRIPTED_SERVICE && !cmdArgs.service;
197198

0 commit comments

Comments
 (0)