@harperfast/harper 5.3.0 → 5.3.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/bin/copyDb.ts +5 -1
- package/bin/deployRenderer.ts +3 -0
- package/components/Application.ts +66 -22
- package/components/DESIGN.md +89 -36
- package/components/componentLoader.ts +2 -0
- package/components/deploymentRecorder.ts +18 -1
- package/components/installFingerprint.ts +123 -0
- package/components/mcp/DESIGN.md +4 -4
- package/components/operations.js +29 -0
- package/config/DESIGN.md +5 -5
- package/dataLayer/DESIGN.md +37 -22
- package/dataLayer/backupRepository.ts +248 -0
- package/dataLayer/restoreMarker.ts +30 -75
- package/dataLayer/rocksdbBackup.ts +253 -110
- package/dist/bin/copyDb.js +3 -0
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deployRenderer.js +3 -0
- package/dist/bin/deployRenderer.js.map +1 -1
- package/dist/components/Application.d.ts +5 -0
- package/dist/components/Application.js +51 -19
- package/dist/components/Application.js.map +1 -1
- package/dist/components/componentLoader.js +2 -0
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentRecorder.js +18 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/installFingerprint.d.ts +39 -0
- package/dist/components/installFingerprint.js +113 -0
- package/dist/components/installFingerprint.js.map +1 -0
- package/dist/components/operations.d.ts +2 -0
- package/dist/components/operations.js +31 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/dataLayer/backupRepository.d.ts +33 -0
- package/dist/dataLayer/backupRepository.js +232 -0
- package/dist/dataLayer/backupRepository.js.map +1 -0
- package/dist/dataLayer/restoreMarker.d.ts +4 -1
- package/dist/dataLayer/restoreMarker.js +29 -68
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.js +221 -112
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +14 -0
- package/dist/resources/DatabaseTransaction.d.ts +140 -11
- package/dist/resources/DatabaseTransaction.js +1055 -464
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/IterableEventQueue.d.ts +2 -1
- package/dist/resources/IterableEventQueue.js +16 -8
- package/dist/resources/IterableEventQueue.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +13 -2
- package/dist/resources/LMDBTransaction.js +219 -93
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/RecordEncoder.js +2 -3
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +5 -6
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +3 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +2 -1
- package/dist/resources/RocksIndexStore.js +3 -0
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +116 -12
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -2
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +71 -17
- package/dist/resources/auditStore.js +248 -64
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/branchDatabase.js +2 -0
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databases.js +8 -2
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +6 -2
- package/dist/resources/indexes/fullTextNativeBinding.js +12 -2
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +4 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +207 -10
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -0
- package/dist/resources/models/Models.js +95 -4
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +8 -0
- package/dist/resources/models/backendRegistry.js +15 -1
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +2 -0
- package/dist/resources/models/bootstrap.js +6 -0
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/calibration.d.ts +88 -0
- package/dist/resources/models/calibration.js +244 -0
- package/dist/resources/models/calibration.js.map +1 -0
- package/dist/resources/models/calibrationStore.d.ts +163 -0
- package/dist/resources/models/calibrationStore.js +986 -0
- package/dist/resources/models/calibrationStore.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +24 -0
- package/dist/resources/models/decisionStore.js +45 -0
- package/dist/resources/models/decisionStore.js.map +1 -1
- package/dist/resources/models/generativeDecision.js +44 -15
- package/dist/resources/models/generativeDecision.js.map +1 -1
- package/dist/resources/models/types.d.ts +7 -0
- package/dist/resources/recordLock.d.ts +8 -5
- package/dist/resources/recordLock.js +6 -4
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/replayLogs.js +8 -1
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/scheduler/engine.d.ts +4 -0
- package/dist/resources/scheduler/engine.js +30 -2
- package/dist/resources/scheduler/engine.js.map +1 -1
- package/dist/resources/search.d.ts +16 -4
- package/dist/resources/search.js +117 -13
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transaction.js +85 -5
- package/dist/resources/transaction.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +8 -0
- package/dist/resources/transactionBroadcast.js +65 -0
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/keys.d.ts +1 -1
- package/dist/security/keys.js +170 -120
- package/dist/security/keys.js.map +1 -1
- package/dist/server/http.js +10 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Request.js +11 -2
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -3
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +38 -7
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -3
- package/dist/server/threads/manageThreads.js +15 -13
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +3 -0
- package/dist/server/threads/threadServer.js +24 -6
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +9 -3
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/durableFile.d.ts +36 -0
- package/dist/utility/durableFile.js +106 -0
- package/dist/utility/durableFile.js.map +1 -0
- package/dist/utility/errors/hdbError.d.ts +4 -0
- package/dist/utility/errors/hdbError.js +10 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/validation/configValidator.js +29 -0
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +7 -0
- package/json/systemSchema.json +14 -0
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -2
- package/resources/DESIGN.md +57 -272
- package/resources/DatabaseTransaction.ts +708 -87
- package/resources/FULL_TEXT.md +6 -2
- package/resources/IterableEventQueue.ts +14 -7
- package/resources/LMDBTransaction.ts +212 -84
- package/resources/RecordEncoder.ts +2 -3
- package/resources/Resource.ts +5 -6
- package/resources/ResourceInterface.ts +3 -1
- package/resources/RocksIndexStore.ts +5 -0
- package/resources/Table.ts +118 -15
- package/resources/analytics/write.ts +6 -2
- package/resources/audit-retention.md +203 -0
- package/resources/auditStore.ts +306 -84
- package/resources/branchDatabase.ts +2 -0
- package/resources/databases.ts +6 -2
- package/resources/indexes/DESIGN.md +120 -177
- package/resources/indexes/fullTextNativeBinding.ts +22 -3
- package/resources/indexes/fullTextQueryIndex.ts +274 -11
- package/resources/models/DESIGN.md +11 -3
- package/resources/models/Models.ts +127 -4
- package/resources/models/backendRegistry.ts +17 -0
- package/resources/models/bootstrap.ts +9 -1
- package/resources/models/calibration.ts +298 -0
- package/resources/models/calibrationStore.ts +1167 -0
- package/resources/models/decisionStore.ts +57 -0
- package/resources/models/generativeDecision.ts +54 -16
- package/resources/models/types.ts +6 -0
- package/resources/record-locks.md +155 -141
- package/resources/recordLock.ts +12 -6
- package/resources/replayLogs.ts +9 -2
- package/resources/scheduler/DESIGN.md +4 -3
- package/resources/scheduler/engine.ts +35 -2
- package/resources/search.ts +154 -16
- package/resources/transaction.ts +50 -5
- package/resources/transactionBroadcast.ts +69 -0
- package/security/DESIGN.md +9 -30
- package/security/keys.ts +175 -127
- package/server/DESIGN.md +70 -36
- package/server/http.ts +10 -0
- package/server/serverHelpers/Request.ts +10 -2
- package/server/serverHelpers/contentTypes.ts +5 -3
- package/server/serverHelpers/uwsServer.ts +34 -7
- package/server/threads/manageThreads.js +16 -14
- package/server/threads/threadServer.js +24 -6
- package/studio/web/assets/{Chat-CNEKMllJ.js → Chat-4LMxI0yB.js} +1 -1
- package/studio/web/assets/{FloatingChat-CEnWhHeR.js → FloatingChat-kWHApEtF.js} +3 -3
- package/studio/web/assets/{SearchDialog-CwhJdEdY.js → SearchDialog-B7ymna97.js} +1 -1
- package/studio/web/assets/{apiToken-DC5vsaaO.js → apiToken-CE3AAt9f.js} +1 -1
- package/studio/web/assets/{applications-DQZxDy0x.js → applications-DdN7CqsB.js} +1 -1
- package/studio/web/assets/{cssMode-Dz2DxF4e.js → cssMode-BpySZUSt.js} +1 -1
- package/studio/web/assets/{editor-B3mjd3tz.js → editor-myLCVv9C.js} +1 -1
- package/studio/web/assets/{html-BsRGM7tp.js → html-DGNYO55O.js} +1 -1
- package/studio/web/assets/{htmlMode-tUKJaA3o.js → htmlMode-BUgpQr41.js} +1 -1
- package/studio/web/assets/{index-BuqFjN5K.js → index-FN4KC9va.js} +39 -39
- package/studio/web/assets/index-QLZw6xrU.css +1 -0
- package/studio/web/assets/{index.lazy-CcpfQFY_.js → index.lazy-DZFuaumG.js} +1 -1
- package/studio/web/assets/{javascript-BZ_uRGHl.js → javascript-DiBzh4pj.js} +1 -1
- package/studio/web/assets/{jsonMode-DgB-aFMj.js → jsonMode-Bm312nmu.js} +1 -1
- package/studio/web/assets/{languageServices-BleuBw17.js → languageServices-Dz2K9rZN.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-ByVYLSsQ.js → lspLanguageFeatures-CNu1L5xn.js} +1 -1
- package/studio/web/assets/{notifications-TuzN0IMi.js → notifications--nwFLGmM.js} +1 -1
- package/studio/web/assets/{notifications-D1Nf-Edn.js → notifications-BelKw5bR.js} +1 -1
- package/studio/web/assets/{profile-D7C7Lupz.js → profile-AU5FjOBx.js} +1 -1
- package/studio/web/assets/{regions-nGQCm-v8.js → regions-C0sTSEHH.js} +1 -1
- package/studio/web/assets/{register-C3x9Ze6P.js → register-DyNR2rGk.js} +2 -2
- package/studio/web/assets/{setComponentFile-DpjrRqeG.js → setComponentFile-BkWx8AN3.js} +1 -1
- package/studio/web/assets/{setup-CGiKLZ9e.js → setup-D6LZS78d.js} +2 -2
- package/studio/web/assets/{status-CGJwrNPH.js → status-BCWnn70o.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-C-Ne6P7T.js → toggleHighContrast-pyeGJ6W1.js} +1 -1
- package/studio/web/assets/{tsMode-CSreYTys.js → tsMode-CGV3iaWC.js} +1 -1
- package/studio/web/assets/{typescript-MnxBk89W.js → typescript-Ci7Khvwo.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-TNrh77-s.js → useEntityRestURL-DNiz3aZ0.js} +1 -1
- package/studio/web/assets/{workers-BfZlUNVI.js → workers-byzxjbqy.js} +1 -1
- package/studio/web/assets/{xml-BHd6qGH7.js → xml-DGnHQ3CX.js} +1 -1
- package/studio/web/assets/{yaml-C53R8OQq.js → yaml-BUoTnPWb.js} +1 -1
- package/studio/web/index.html +2 -2
- package/upgrade/directives/5-3-0.ts +10 -3
- package/utility/durableFile.ts +96 -0
- package/utility/errors/hdbError.ts +9 -0
- package/utility/hdbTerms.ts +1 -0
- package/validation/configValidator.ts +31 -0
- package/studio/web/assets/index-72cEH474.css +0 -1
|
@@ -62,6 +62,7 @@ exports.purgeBackupsOffline = purgeBackupsOffline;
|
|
|
62
62
|
const node_crypto_1 = require("node:crypto");
|
|
63
63
|
const node_fs_1 = require("node:fs");
|
|
64
64
|
const promises_1 = require("node:fs/promises");
|
|
65
|
+
const node_crypto_2 = require("node:crypto");
|
|
65
66
|
const node_path_1 = require("node:path");
|
|
66
67
|
const node_stream_1 = require("node:stream");
|
|
67
68
|
const promises_2 = require("node:stream/promises");
|
|
@@ -70,6 +71,7 @@ const promises_3 = require("node:timers/promises");
|
|
|
70
71
|
const tar_stream_1 = require("tar-stream");
|
|
71
72
|
const rocksdb_js_1 = require("@harperfast/rocksdb-js");
|
|
72
73
|
const databases_ts_1 = require("../resources/databases.js");
|
|
74
|
+
const auditStore_ts_1 = require("../resources/auditStore.js");
|
|
73
75
|
const blob_ts_1 = require("../resources/blob.js");
|
|
74
76
|
const environmentManager_ts_1 = require("../utility/environment/environmentManager.js");
|
|
75
77
|
const configUtils_ts_1 = require("../config/configUtils.js");
|
|
@@ -79,6 +81,8 @@ const hdbError_ts_1 = require("../utility/errors/hdbError.js");
|
|
|
79
81
|
const signalling = __importStar(require("../utility/signalling.js"));
|
|
80
82
|
const itc_js_1 = require("../server/threads/itc.js");
|
|
81
83
|
const restoreMarker_ts_1 = require("./restoreMarker.js");
|
|
84
|
+
const backupRepository_ts_1 = require("./backupRepository.js");
|
|
85
|
+
const durableFile_ts_1 = require("../utility/durableFile.js");
|
|
82
86
|
const blobBackup_ts_1 = require("./blobBackup.js");
|
|
83
87
|
const backupManifest_ts_1 = require("./backupManifest.js");
|
|
84
88
|
const harper_logger_ts_1 = __importDefault(require("../utility/logging/harper_logger.js"));
|
|
@@ -184,6 +188,31 @@ function requireRocksRootStore(databaseName, operation) {
|
|
|
184
188
|
}
|
|
185
189
|
return rootStore;
|
|
186
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Apply the engine gate only when there is a loaded database to gate.
|
|
193
|
+
*
|
|
194
|
+
* A repository outlives its database. A restore that failed leaves the database blocked by its
|
|
195
|
+
* restoring marker with the repository intact, and a backup can be imported for a name that does not
|
|
196
|
+
* exist yet — both are exactly the states an operator needs list/verify/delete/purge for. Refusing
|
|
197
|
+
* them because no root store is loaded turns a recoverable situation into a filesystem intervention,
|
|
198
|
+
* which on Fabric the operator cannot perform at all. A name that is neither a loaded database nor a
|
|
199
|
+
* repository is still a 404, so a typo does not silently answer with an empty list.
|
|
200
|
+
*/
|
|
201
|
+
function requireBackupRepositoryAccess(databaseName, operation) {
|
|
202
|
+
const loaded = (0, databases_ts_1.getDatabases)()[databaseName];
|
|
203
|
+
if (loaded != null && Object.keys(loaded).length > 0) {
|
|
204
|
+
requireRocksRootStore(databaseName, operation);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
requireBackupRepositoryDirectory(databaseName);
|
|
208
|
+
}
|
|
209
|
+
function requireBackupRepositoryDirectory(databaseName) {
|
|
210
|
+
const backupDir = backupDirForDatabase(databaseName);
|
|
211
|
+
if (!(0, node_fs_1.existsSync)(backupDir)) {
|
|
212
|
+
throw new BackupNotFoundError(`Database '${databaseName}' is not loaded and has no backup repository at ${backupDir}`);
|
|
213
|
+
}
|
|
214
|
+
return backupDir;
|
|
215
|
+
}
|
|
187
216
|
function requireBackupId(backupId) {
|
|
188
217
|
if (!Number.isSafeInteger(backupId) || backupId <= 0) {
|
|
189
218
|
throw new hdbError_ts_1.ClientError(`'backup_id' must be a positive integer`);
|
|
@@ -208,10 +237,24 @@ function mapLockedError(error, databaseName) {
|
|
|
208
237
|
}
|
|
209
238
|
// --- directory helpers (operate on a backup directory only; no open database, usable offline) ---
|
|
210
239
|
async function listBackupsInDir(backupDir) {
|
|
211
|
-
// the backup dir doesn't exist until the first create_backup
|
|
212
|
-
|
|
240
|
+
// the backup dir doesn't exist until the first create_backup. pathPresent, not existsSync: an
|
|
241
|
+
// empty listing is what reconcileHarperManagedBackupFiles reads as "keep nothing", so a
|
|
242
|
+
// permission or I/O fault answering "absent" would license deleting every manifest and blob
|
|
243
|
+
// snapshot in a repository that is merely unreadable.
|
|
244
|
+
if (!(0, durableFile_ts_1.pathPresent)(backupDir))
|
|
213
245
|
return [];
|
|
214
|
-
|
|
246
|
+
try {
|
|
247
|
+
return await rocksdb_js_1.backups.list(backupDir);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
// Taking the management lock creates the repository directory before anything has written
|
|
251
|
+
// engine metadata into it, so a directory with no metadata is an empty repository, not a
|
|
252
|
+
// failure — otherwise a purge on a database that never had a backup reports the binding's
|
|
253
|
+
// "meta is missing" instead of "no backups found".
|
|
254
|
+
if (!(0, durableFile_ts_1.pathPresent)((0, node_path_1.join)(backupDir, 'meta')))
|
|
255
|
+
return [];
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
215
258
|
}
|
|
216
259
|
async function findBackup(backupDir, backupId, databaseName) {
|
|
217
260
|
requireBackupId(backupId); // every id-taking path flows through here, including the offline CLI
|
|
@@ -280,6 +323,16 @@ async function resolveCompleteBackup(backupDir, requestedId, databaseName) {
|
|
|
280
323
|
const latest = complete.reduce((a, b) => (b.backupId > a.backupId ? b : a));
|
|
281
324
|
return { backupId: latest.backupId, manifest: { backupId: latest.backupId, blobs: latest.blobs, completedAt: 0 } };
|
|
282
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Drop the Harper-managed files — blob snapshots and completion manifests — of every backup the
|
|
328
|
+
* engine no longer has. Derived from what survives rather than from what a caller believes it
|
|
329
|
+
* removed, so a delete or purge that failed partway through still leaves no orphans behind.
|
|
330
|
+
*/
|
|
331
|
+
async function reconcileHarperManagedBackupFiles(backupDir) {
|
|
332
|
+
const keepIds = new Set((await listBackupsInDir(backupDir)).map((backup) => backup.backupId));
|
|
333
|
+
await (0, blobBackup_ts_1.purgeBlobSnapshots)(backupDir, keepIds);
|
|
334
|
+
await (0, backupManifest_ts_1.purgeBackupManifests)(backupDir, keepIds);
|
|
335
|
+
}
|
|
283
336
|
/**
|
|
284
337
|
* Publish a backup's completion manifest after the engine backup and (when included) blob snapshot
|
|
285
338
|
* are durable. On failure, best-effort roll back the just-created engine backup, its partial blob
|
|
@@ -289,6 +342,13 @@ async function finalizeBackup(backupDir, backupId, databaseName, blobs) {
|
|
|
289
342
|
try {
|
|
290
343
|
if (blobs)
|
|
291
344
|
await (0, blobBackup_ts_1.snapshotBlobs)(backupDir, backupId, (0, blob_ts_1.getBlobPathsForDatabaseName)(databaseName));
|
|
345
|
+
// Guards writers that do not take Harper's lock — an older binary, or a direct binding call.
|
|
346
|
+
// Harper's own purge cannot reach here: the whole create is one critical section.
|
|
347
|
+
// The manifest is what publishes a backup as usable, so writing one for engine files that are
|
|
348
|
+
// gone is the #2031 false-green shape: a backup that lists and verifies with nothing to restore.
|
|
349
|
+
if (!(await listBackupsInDir(backupDir)).some((backup) => backup.backupId === backupId)) {
|
|
350
|
+
throw new BackupNotFoundError(`Backup ${backupId} of database '${databaseName}' was removed while it was being finalized; rerun create_backup`);
|
|
351
|
+
}
|
|
292
352
|
await (0, backupManifest_ts_1.writeBackupManifest)(backupDir, backupId, blobs);
|
|
293
353
|
}
|
|
294
354
|
catch (error) {
|
|
@@ -303,53 +363,20 @@ async function listBackups(request) {
|
|
|
303
363
|
requireSuperUser(request, hdbTerms_ts_1.OPERATIONS_ENUM.LIST_BACKUPS);
|
|
304
364
|
const databaseName = getDatabaseName(request);
|
|
305
365
|
harper_logger_ts_1.default.info(`Listing backups for database '${databaseName}'`);
|
|
306
|
-
|
|
307
|
-
return (
|
|
366
|
+
requireBackupRepositoryAccess(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.LIST_BACKUPS);
|
|
367
|
+
return listBackupsOffline(databaseName);
|
|
308
368
|
}
|
|
309
369
|
async function deleteBackup(request) {
|
|
310
370
|
requireSuperUser(request, hdbTerms_ts_1.OPERATIONS_ENUM.DELETE_BACKUP);
|
|
311
371
|
const databaseName = getDatabaseName(request);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const backupDir = backupDirForDatabase(databaseName);
|
|
315
|
-
await findBackup(backupDir, backupId, databaseName);
|
|
316
|
-
try {
|
|
317
|
-
await rocksdb_js_1.backups.delete(backupDir, backupId);
|
|
318
|
-
}
|
|
319
|
-
catch (error) {
|
|
320
|
-
throw mapLockedError(error, databaseName);
|
|
321
|
-
}
|
|
322
|
-
// the engine's delete leaves the (Harper-managed) blob snapshot + manifest behind — remove them too
|
|
323
|
-
await (0, blobBackup_ts_1.deleteBlobSnapshot)(backupDir, backupId);
|
|
324
|
-
await (0, backupManifest_ts_1.deleteBackupManifest)(backupDir, backupId);
|
|
325
|
-
return { ok: true };
|
|
372
|
+
requireBackupRepositoryAccess(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.DELETE_BACKUP);
|
|
373
|
+
return deleteBackupOffline(databaseName, requireBackupId(request.backup_id));
|
|
326
374
|
}
|
|
327
375
|
async function purgeBackups(request) {
|
|
328
376
|
requireSuperUser(request, hdbTerms_ts_1.OPERATIONS_ENUM.PURGE_BACKUPS);
|
|
329
377
|
const databaseName = getDatabaseName(request);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (!Number.isSafeInteger(keepCount) || keepCount < 0) {
|
|
333
|
-
throw new hdbError_ts_1.ClientError(`'keep_count' must be a non-negative integer`);
|
|
334
|
-
}
|
|
335
|
-
const backupDir = backupDirForDatabase(databaseName);
|
|
336
|
-
const before = await listBackupsInDir(backupDir);
|
|
337
|
-
if (before.length === 0) {
|
|
338
|
-
throw new BackupNotFoundError(`No backups found for database '${databaseName}'`);
|
|
339
|
-
}
|
|
340
|
-
try {
|
|
341
|
-
await rocksdb_js_1.backups.purge(backupDir, keepCount);
|
|
342
|
-
}
|
|
343
|
-
catch (error) {
|
|
344
|
-
throw mapLockedError(error, databaseName);
|
|
345
|
-
}
|
|
346
|
-
const remainingBackups = await listBackupsInDir(backupDir);
|
|
347
|
-
// drop blob snapshots + manifests for every id the engine purged (keep only the survivors')
|
|
348
|
-
const keepIds = new Set(remainingBackups.map((backup) => backup.backupId));
|
|
349
|
-
await (0, blobBackup_ts_1.purgeBlobSnapshots)(backupDir, keepIds);
|
|
350
|
-
await (0, backupManifest_ts_1.purgeBackupManifests)(backupDir, keepIds);
|
|
351
|
-
// clamp: a concurrent create between the two lists can otherwise make this negative
|
|
352
|
-
return { deleted: Math.max(0, before.length - remainingBackups.length), remaining: remainingBackups.length };
|
|
378
|
+
requireBackupRepositoryAccess(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.PURGE_BACKUPS);
|
|
379
|
+
return purgeBackupsOffline(databaseName, request.keep_count);
|
|
353
380
|
}
|
|
354
381
|
// --- job operations: create_backup / verify_backup / restore_backup ---
|
|
355
382
|
// Each has a synchronous-validation function (run by jobs.addJob before the job record is
|
|
@@ -366,15 +393,19 @@ async function createBackup(request) {
|
|
|
366
393
|
const excludeBlobs = requireBooleanOption(request.exclude_blobs, 'exclude_blobs');
|
|
367
394
|
const rootStore = requireRocksRootStore(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.CREATE_BACKUP);
|
|
368
395
|
const backupDir = backupDirForDatabase(databaseName);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
// One critical section, management lock before the engine call: a purge can neither remove the
|
|
397
|
+
// engine backup before the manifest publishes it nor shift which ids it would remove.
|
|
398
|
+
const backupId = await (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
399
|
+
let id;
|
|
400
|
+
try {
|
|
401
|
+
id = await rootStore.backup(backupDir, { transactionLogs: true });
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
throw mapLockedError(error, databaseName);
|
|
405
|
+
}
|
|
406
|
+
await finalizeBackup(backupDir, id, databaseName, !excludeBlobs);
|
|
407
|
+
return id;
|
|
408
|
+
}, true);
|
|
378
409
|
await writeBackupReadme(backupDir, databaseName);
|
|
379
410
|
return {
|
|
380
411
|
database: databaseName,
|
|
@@ -457,7 +488,7 @@ Both remove the RocksDB backup and its blob snapshot.
|
|
|
457
488
|
async function validateVerifyBackup(request) {
|
|
458
489
|
requireSuperUser(request, hdbTerms_ts_1.OPERATIONS_ENUM.VERIFY_BACKUP);
|
|
459
490
|
const databaseName = getDatabaseName(request);
|
|
460
|
-
|
|
491
|
+
requireBackupRepositoryAccess(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.VERIFY_BACKUP);
|
|
461
492
|
requireBooleanOption(request.verify_checksum, 'verify_checksum');
|
|
462
493
|
const backupDir = backupDirForDatabase(databaseName);
|
|
463
494
|
const backupId = requireBackupId(request.backup_id);
|
|
@@ -466,19 +497,8 @@ async function validateVerifyBackup(request) {
|
|
|
466
497
|
}
|
|
467
498
|
async function verifyBackup(request) {
|
|
468
499
|
const databaseName = getDatabaseName(request);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const verifyWithChecksum = requireBooleanOption(request.verify_checksum, 'verify_checksum');
|
|
472
|
-
const backupDir = backupDirForDatabase(databaseName);
|
|
473
|
-
await findBackup(backupDir, backupId, databaseName);
|
|
474
|
-
const manifest = await requireBackupComplete(backupDir, backupId, databaseName);
|
|
475
|
-
await rocksdb_js_1.backups.verify(backupDir, backupId, { verifyWithChecksum });
|
|
476
|
-
// verification covers the declared blob snapshot: a backup that recorded blobs must still have its
|
|
477
|
-
// snapshot directory (a manifest without its blobs is a corrupt backup)
|
|
478
|
-
if (manifest.blobs && !(0, node_fs_1.existsSync)((0, blobBackup_ts_1.blobSnapshotDir)(backupDir, backupId))) {
|
|
479
|
-
throw new hdbError_ts_1.ClientError(`Backup ${backupId} of database '${databaseName}' declares captured blobs but its blob snapshot is missing (corrupt backup)`);
|
|
480
|
-
}
|
|
481
|
-
return { database: databaseName, backup_id: backupId, ok: true, blobs: manifest.blobs };
|
|
500
|
+
requireBackupRepositoryAccess(databaseName, hdbTerms_ts_1.OPERATIONS_ENUM.VERIFY_BACKUP);
|
|
501
|
+
return verifyBackupOffline(databaseName, requireBackupId(request.backup_id), request.verify_checksum);
|
|
482
502
|
}
|
|
483
503
|
async function validateRestoreBackup(request) {
|
|
484
504
|
requireSuperUser(request, hdbTerms_ts_1.OPERATIONS_ENUM.RESTORE_BACKUP);
|
|
@@ -543,9 +563,15 @@ async function restoreBackup(request) {
|
|
|
543
563
|
// Once is enough: the decision reads the manifest and the opt-in, never the destination, so no
|
|
544
564
|
// concurrent writer can change the answer between here and the purge.
|
|
545
565
|
(0, blobBackup_ts_1.assertEngineOnlyRestoreAllowed)(databaseName, { backupHasBlobs: manifest.blobs, allowEngineOnly });
|
|
546
|
-
const
|
|
566
|
+
const pinId = restorePinId(databaseDir);
|
|
547
567
|
const restoreToken = (0, node_crypto_1.randomUUID)();
|
|
548
568
|
let destructionStarted = false;
|
|
569
|
+
// Re-check before replacing a previous attempt's claim; publish the new claim before the marker
|
|
570
|
+
// under both locks, so a crash cannot leave a marked database with an unprotected source.
|
|
571
|
+
const lock = await (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
572
|
+
await findBackup(backupDir, backupId, databaseName);
|
|
573
|
+
return beginRestoreForDatabase(databaseDir, databaseName, () => (0, backupRepository_ts_1.pinBackup)(backupDir, pinId, backupId, `restore of database '${databaseName}'`, databaseDir));
|
|
574
|
+
});
|
|
549
575
|
try {
|
|
550
576
|
// Block new blob saves, drain in-flight saves, and close the database across all worker threads.
|
|
551
577
|
// Each thread also rescans, and the restoring marker keeps it from reloading mid-restore.
|
|
@@ -568,6 +594,7 @@ async function restoreBackup(request) {
|
|
|
568
594
|
if (manifest.blobs) {
|
|
569
595
|
await (0, blobBackup_ts_1.restoreBlobSnapshot)(backupDir, backupId, databaseName, (0, blob_ts_1.getBlobPathsForDatabaseName)(databaseName));
|
|
570
596
|
}
|
|
597
|
+
await (0, auditStore_ts_1.stampDatabaseDirectory)(databaseDir, { carriesLog: true });
|
|
571
598
|
}
|
|
572
599
|
catch (error) {
|
|
573
600
|
// Leave the marker (so startup/rescan detection reports an incomplete restore until a rerun
|
|
@@ -577,6 +604,8 @@ async function restoreBackup(request) {
|
|
|
577
604
|
// partial/corrupt data. Only a *fresh* marker on a *previously healthy* database that failed
|
|
578
605
|
// before any destruction is safe to clear.
|
|
579
606
|
if (destructionStarted || lock.preexisting) {
|
|
607
|
+
// The marker stays, so the database is unloadable until a rerun — and the rerun needs this
|
|
608
|
+
// backup. The pin stays with it, and lapses on its own once the marker is gone.
|
|
580
609
|
(0, restoreMarker_ts_1.abandonRestore)(lock);
|
|
581
610
|
// The restore is over even though it failed, so release the workers' blob fence. Without this
|
|
582
611
|
// the fence outlives the attempt: the marker keeps the database from loading, but an operator
|
|
@@ -598,15 +627,36 @@ async function restoreBackup(request) {
|
|
|
598
627
|
}
|
|
599
628
|
// nothing destructive happened and the marker was fresh — clear it and let every thread reload
|
|
600
629
|
// the intact database
|
|
601
|
-
(
|
|
630
|
+
releaseRestoreClaim(backupDir, pinId, lock, databaseName);
|
|
602
631
|
await signalling.signalSchemaChange(restoreSchemaEvent(databaseName, 'reload', restoreToken, false));
|
|
603
632
|
throw error;
|
|
604
633
|
}
|
|
605
|
-
(
|
|
634
|
+
releaseRestoreClaim(backupDir, pinId, lock, databaseName);
|
|
606
635
|
// signal again: with the marker gone, every thread's rescan reloads the restored database
|
|
607
636
|
await signalling.signalSchemaChange(restoreSchemaEvent(databaseName, 'reload', restoreToken));
|
|
608
637
|
return { database: databaseName, backup_id: backupId, ...(allowEngineOnly ? { allow_engine_only: true } : {}) };
|
|
609
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Retire a finished restore: marker, then claim, then the destination lock. While the database is
|
|
641
|
+
* still marked a rerun is required and the rerun needs this backup, so the claim has to outlive the
|
|
642
|
+
* marker — a crash the other way round leaves an unloadable database whose source a purge may
|
|
643
|
+
* delete. Both happen under the restore lock, so no later attempt can have written a claim at this
|
|
644
|
+
* same id yet; one stranded after the marker is gone simply lapses and is swept.
|
|
645
|
+
*/
|
|
646
|
+
function releaseRestoreClaim(backupDir, pinId, lock, databaseName) {
|
|
647
|
+
try {
|
|
648
|
+
(0, restoreMarker_ts_1.clearRestoringMarker)(lock);
|
|
649
|
+
try {
|
|
650
|
+
(0, backupRepository_ts_1.unpinBackup)(backupDir, pinId);
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
harper_logger_ts_1.default.error(`Could not release the backup claim after restoring '${databaseName}'; it lapses with the marker`, error);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
finally {
|
|
657
|
+
(0, restoreMarker_ts_1.releaseRestoreLock)(lock);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
610
660
|
/**
|
|
611
661
|
* `restoreToken` identifies the restore that owns the blob fence. Two restores of the same database
|
|
612
662
|
* can overlap, so a worker releases the fence only for the token that established it -- see
|
|
@@ -654,9 +704,16 @@ async function verifyDatabaseClosed(databaseDir, databaseName) {
|
|
|
654
704
|
* beginRestore's own error message carries the filesystem path (useful in CLI/server logs);
|
|
655
705
|
* client-facing operations report by database name instead.
|
|
656
706
|
*/
|
|
657
|
-
|
|
707
|
+
/**
|
|
708
|
+
* One pin per target database, not per attempt: two attempts can never collide on it, and a rerun
|
|
709
|
+
* after a failed restore reuses the claim the failed attempt left protecting its source.
|
|
710
|
+
*/
|
|
711
|
+
function restorePinId(databaseDir) {
|
|
712
|
+
return `restore-${(0, node_crypto_2.createHash)('sha256').update((0, node_path_1.resolve)(databaseDir)).digest('hex').slice(0, 32)}`;
|
|
713
|
+
}
|
|
714
|
+
function beginRestoreForDatabase(databaseDir, databaseName, beforePublishMarker) {
|
|
658
715
|
try {
|
|
659
|
-
return (0, restoreMarker_ts_1.beginRestore)(databaseDir);
|
|
716
|
+
return (0, restoreMarker_ts_1.beginRestore)(databaseDir, beforePublishMarker);
|
|
660
717
|
}
|
|
661
718
|
catch (error) {
|
|
662
719
|
if (error.statusCode === 409) {
|
|
@@ -943,14 +1000,17 @@ async function createBackupOffline(databaseName, excludeBlobs = false) {
|
|
|
943
1000
|
const database = rocksdb_js_1.RocksDatabase.open(databaseDir);
|
|
944
1001
|
try {
|
|
945
1002
|
const backupDir = backupDirForDatabase(databaseName);
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1003
|
+
const backupId = await (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
1004
|
+
let id;
|
|
1005
|
+
try {
|
|
1006
|
+
id = await database.backup(backupDir, { transactionLogs: true });
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
throw mapLockedError(error, databaseName);
|
|
1010
|
+
}
|
|
1011
|
+
await finalizeBackup(backupDir, id, databaseName, !excludeBlobs);
|
|
1012
|
+
return id;
|
|
1013
|
+
}, true);
|
|
954
1014
|
await writeBackupReadme(backupDir, databaseName);
|
|
955
1015
|
return {
|
|
956
1016
|
database: databaseName,
|
|
@@ -992,10 +1052,13 @@ async function restoreBackupOffline(databaseName, backupId, targetDatabase, allo
|
|
|
992
1052
|
const blobRoots = (0, blob_ts_1.getBlobPathsForDatabaseName)(targetDatabase ?? databaseName);
|
|
993
1053
|
await (0, blobBackup_ts_1.assertBlobSnapshotRestorable)(backupDir, backupId, blobRoots);
|
|
994
1054
|
(0, blobBackup_ts_1.assertEngineOnlyRestoreAllowed)(targetDatabase ?? databaseName, { backupHasBlobs: manifest.blobs, allowEngineOnly });
|
|
995
|
-
|
|
996
|
-
// marker and refuses to load the database (closing the window between the probe and the purge).
|
|
997
|
-
const lock = beginRestoreForDatabase(databaseDir, targetDatabase ?? databaseName);
|
|
1055
|
+
const pinId = restorePinId(databaseDir);
|
|
998
1056
|
let destructionStarted = false;
|
|
1057
|
+
// As online, claim before marking under both locks, and mark before probing the destination.
|
|
1058
|
+
const lock = await (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
1059
|
+
await findBackup(backupDir, backupId, databaseName);
|
|
1060
|
+
return beginRestoreForDatabase(databaseDir, targetDatabase ?? databaseName, () => (0, backupRepository_ts_1.pinBackup)(backupDir, pinId, backupId, `restore of database '${targetDatabase ?? databaseName}'`, databaseDir));
|
|
1061
|
+
});
|
|
999
1062
|
try {
|
|
1000
1063
|
// The offline path is entered only when the CLI sees no running server (getHdbPid), but that is
|
|
1001
1064
|
// a heuristic: the PID file is briefly absent mid-`harper restart`, and backups.restore's
|
|
@@ -1023,15 +1086,19 @@ async function restoreBackupOffline(databaseName, backupId, targetDatabase, allo
|
|
|
1023
1086
|
if (manifest.blobs) {
|
|
1024
1087
|
await (0, blobBackup_ts_1.restoreBlobSnapshot)(backupDir, backupId, databaseName, (0, blob_ts_1.getBlobPathsForDatabaseName)(targetDatabase ?? databaseName));
|
|
1025
1088
|
}
|
|
1089
|
+
await (0, auditStore_ts_1.stampDatabaseDirectory)(databaseDir, { carriesLog: true });
|
|
1026
1090
|
}
|
|
1027
1091
|
catch (error) {
|
|
1028
1092
|
// Preserve the marker on a destructive failure or a recovery over a pre-existing marker (see
|
|
1029
1093
|
// the online restoreBackup for the rationale); otherwise clear the fresh marker so an intact,
|
|
1030
1094
|
// merely-locked database is not left flagged as an incomplete restore.
|
|
1095
|
+
// The pin stays exactly as long as the marker does: a retained marker means a rerun is required,
|
|
1096
|
+
// and the rerun needs this backup to still be there.
|
|
1031
1097
|
if (destructionStarted || lock.preexisting)
|
|
1032
1098
|
(0, restoreMarker_ts_1.abandonRestore)(lock);
|
|
1033
|
-
else
|
|
1034
|
-
(
|
|
1099
|
+
else {
|
|
1100
|
+
releaseRestoreClaim(backupDir, pinId, lock, databaseName);
|
|
1101
|
+
}
|
|
1035
1102
|
// preserve typed client errors (e.g. the 409 lock probe) unwrapped; only wrap an opaque restore
|
|
1036
1103
|
// failure after destruction has begun
|
|
1037
1104
|
if (destructionStarted && !(error instanceof hdbError_ts_1.ClientError)) {
|
|
@@ -1039,7 +1106,7 @@ async function restoreBackupOffline(databaseName, backupId, targetDatabase, allo
|
|
|
1039
1106
|
}
|
|
1040
1107
|
throw error;
|
|
1041
1108
|
}
|
|
1042
|
-
(
|
|
1109
|
+
releaseRestoreClaim(backupDir, pinId, lock, databaseName);
|
|
1043
1110
|
return {
|
|
1044
1111
|
database: databaseName,
|
|
1045
1112
|
backup_id: backupId,
|
|
@@ -1057,7 +1124,10 @@ function isMissingOrEmptyDir(path) {
|
|
|
1057
1124
|
throw error;
|
|
1058
1125
|
}
|
|
1059
1126
|
}
|
|
1060
|
-
// ---
|
|
1127
|
+
// --- directory-only management implementations ---
|
|
1128
|
+
// What the CLI runs when the local server is stopped, and what the online operations delegate to
|
|
1129
|
+
// once their engine gate has passed. One implementation, so the two paths cannot drift, and so a
|
|
1130
|
+
// repository whose database is absent or blocked by a restore stays manageable.
|
|
1061
1131
|
async function listBackupsOffline(databaseName) {
|
|
1062
1132
|
validateDatabaseName(databaseName);
|
|
1063
1133
|
// map to the same snake_case response shape as the online list_backups operation
|
|
@@ -1077,38 +1147,77 @@ async function verifyBackupOffline(databaseName, backupId, verifyChecksum) {
|
|
|
1077
1147
|
}
|
|
1078
1148
|
async function deleteBackupOffline(databaseName, backupId) {
|
|
1079
1149
|
validateDatabaseName(databaseName);
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
await
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1150
|
+
requireBackupId(backupId);
|
|
1151
|
+
const backupDir = requireBackupRepositoryDirectory(databaseName);
|
|
1152
|
+
return (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
1153
|
+
await findBackup(backupDir, backupId, databaseName);
|
|
1154
|
+
(0, backupRepository_ts_1.assertBackupsUnpinned)(backupDir, [backupId], databaseName);
|
|
1155
|
+
let engineSucceeded = false;
|
|
1156
|
+
try {
|
|
1157
|
+
await rocksdb_js_1.backups.delete(backupDir, backupId);
|
|
1158
|
+
engineSucceeded = true;
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
throw mapLockedError(error, databaseName);
|
|
1162
|
+
}
|
|
1163
|
+
finally {
|
|
1164
|
+
// The engine's delete leaves the Harper-managed blob snapshot + manifest behind, and it can
|
|
1165
|
+
// fail after removing engine files — so reconcile against what survives rather than assuming
|
|
1166
|
+
// this id was the only thing that changed.
|
|
1167
|
+
// Preserve a primary engine error, but never report success when cleanup failed.
|
|
1168
|
+
await reconcileHarperManagedBackupFiles(backupDir).catch((error) => {
|
|
1169
|
+
if (engineSucceeded)
|
|
1170
|
+
throw error;
|
|
1171
|
+
harper_logger_ts_1.default.warn(`Could not reconcile Harper-managed backup files in ${backupDir}`, error);
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
return { ok: true };
|
|
1175
|
+
});
|
|
1091
1176
|
}
|
|
1092
1177
|
async function purgeBackupsOffline(databaseName, keepCount) {
|
|
1093
1178
|
validateDatabaseName(databaseName);
|
|
1094
1179
|
if (!Number.isSafeInteger(keepCount) || keepCount < 0) {
|
|
1095
1180
|
throw new hdbError_ts_1.ClientError(`'keep_count' must be a non-negative integer`);
|
|
1096
1181
|
}
|
|
1097
|
-
const backupDir =
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1182
|
+
const backupDir = requireBackupRepositoryDirectory(databaseName);
|
|
1183
|
+
return (0, backupRepository_ts_1.withBackupRepositoryLock)(backupDir, databaseName, async () => {
|
|
1184
|
+
const before = await listBackupsInDir(backupDir);
|
|
1185
|
+
if (before.length === 0) {
|
|
1186
|
+
throw new BackupNotFoundError(`No backups found for database '${databaseName}'`);
|
|
1187
|
+
}
|
|
1188
|
+
// Every Harper writer is excluded by this lock, so `before` is exactly what the binding will
|
|
1189
|
+
// see, and `purge` keeps the newest `keepCount` by id — so the ids it will remove can be named
|
|
1190
|
+
// here and checked against the claims before it runs. One bulk call then does the removal.
|
|
1191
|
+
// Sorted rather than sliced off the listing as returned: this set gates claim protection, so it
|
|
1192
|
+
// does not rest on the documented list order holding, matching resolveCompleteBackup below.
|
|
1193
|
+
const removing = [...before]
|
|
1194
|
+
.sort((first, second) => first.backupId - second.backupId)
|
|
1195
|
+
.slice(0, Math.max(0, before.length - keepCount))
|
|
1196
|
+
.map((backup) => backup.backupId);
|
|
1197
|
+
(0, backupRepository_ts_1.assertBackupsUnpinned)(backupDir, removing, databaseName);
|
|
1198
|
+
let engineSucceeded = false;
|
|
1199
|
+
try {
|
|
1200
|
+
await rocksdb_js_1.backups.purge(backupDir, keepCount);
|
|
1201
|
+
engineSucceeded = true;
|
|
1202
|
+
}
|
|
1203
|
+
catch (error) {
|
|
1204
|
+
throw mapLockedError(error, databaseName);
|
|
1205
|
+
}
|
|
1206
|
+
finally {
|
|
1207
|
+
// Reconciled from what actually survives, in a finally: a purge that failed partway through
|
|
1208
|
+
// still removed engine backups, and their blob snapshots would otherwise be orphaned on disk
|
|
1209
|
+
// — invisible to list_backups and still charged to the tenant's quota.
|
|
1210
|
+
// Preserve a primary engine error, but never report success when cleanup failed.
|
|
1211
|
+
await reconcileHarperManagedBackupFiles(backupDir).catch((error) => {
|
|
1212
|
+
if (engineSucceeded)
|
|
1213
|
+
throw error;
|
|
1214
|
+
harper_logger_ts_1.default.warn(`Could not reconcile Harper-managed backup files in ${backupDir}`, error);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
// Counted from what actually survives rather than from `removing`: no create can land inside
|
|
1218
|
+
// this lock, so the difference is exactly what the purge removed.
|
|
1219
|
+
const remaining = (await listBackupsInDir(backupDir)).length;
|
|
1220
|
+
return { deleted: before.length - remaining, remaining };
|
|
1221
|
+
});
|
|
1113
1222
|
}
|
|
1114
1223
|
//# sourceMappingURL=rocksdbBackup.js.map
|