@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.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/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +21 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +624 -176
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +217 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +143 -24
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './longLivedTransactions.ts';
|
|
27
27
|
|
|
28
28
|
const trackedTxns = new Set<DatabaseTransaction>();
|
|
29
|
+
const readTransactionOwners = new WeakMap<ReadTransaction, DatabaseTransaction>();
|
|
29
30
|
// Read options for a rotated generation's native transactions; shared because they never vary.
|
|
30
31
|
const SNAPSHOT_FREE = Object.freeze({ disableSnapshot: true });
|
|
31
32
|
// Logical transactions the monitor supervises for their WRITES, kept apart from trackedTxns because the
|
|
@@ -292,6 +293,82 @@ export function transactionOpenTooLongError(): ServerError {
|
|
|
292
293
|
);
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
class ReadSnapshotExpiredError extends ServerError {
|
|
297
|
+
constructor() {
|
|
298
|
+
super('Read scan snapshot expired; retry the read without replaying previously committed writes', 503);
|
|
299
|
+
this.name = 'ReadSnapshotExpiredError';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function getReadTransactionGuard(transaction: ReadTransaction): (() => void) | undefined {
|
|
304
|
+
const owner = readTransactionOwners.get(transaction);
|
|
305
|
+
if (!owner) return;
|
|
306
|
+
return function checkActive() {
|
|
307
|
+
if (owner.timedOut) throw transactionOpenTooLongError();
|
|
308
|
+
if (owner.transaction !== transaction) {
|
|
309
|
+
throw new ReadSnapshotExpiredError();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function trackReadRange(transaction: ReadTransaction, createRange: () => any): any {
|
|
315
|
+
const owner = readTransactionOwners.get(transaction);
|
|
316
|
+
if (!owner) return createRange();
|
|
317
|
+
const checkActive = getReadTransactionGuard(transaction)!;
|
|
318
|
+
checkActive();
|
|
319
|
+
const range = createRange();
|
|
320
|
+
const iterate = range.iterate;
|
|
321
|
+
range.iterate = function (options) {
|
|
322
|
+
const iterator = iterate.call(this, options);
|
|
323
|
+
let done = false;
|
|
324
|
+
// Closing the underlying iterator is the one step here that can throw for a reason the caller
|
|
325
|
+
// must not see: `next()` only reaches it once the snapshot is already gone, which is the
|
|
326
|
+
// likeliest moment for the native layer to object, and an error from cleanup would replace the
|
|
327
|
+
// named 503 with exactly the raw iterator error this wrapper exists to stop surfacing. The
|
|
328
|
+
// closure reference rather than `this` so a destructured `next` still cleans up.
|
|
329
|
+
const wrapper = {
|
|
330
|
+
[Symbol.iterator]() {
|
|
331
|
+
return this;
|
|
332
|
+
},
|
|
333
|
+
next() {
|
|
334
|
+
if (done) return { done: true, value: undefined };
|
|
335
|
+
try {
|
|
336
|
+
checkActive();
|
|
337
|
+
owner.rangeReadActive = true;
|
|
338
|
+
const result = iterator.next();
|
|
339
|
+
done = result.done === true;
|
|
340
|
+
return result;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
closeQuietly();
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
return(value?: any) {
|
|
347
|
+
if (!done) {
|
|
348
|
+
done = true;
|
|
349
|
+
iterator.return?.(value);
|
|
350
|
+
}
|
|
351
|
+
return { done: true, value };
|
|
352
|
+
},
|
|
353
|
+
throw(error) {
|
|
354
|
+
// Not delegated to `iterator.throw`: it closes and rethrows the same error anyway, and
|
|
355
|
+
// delegating after the close below would run it against an iterator already closed.
|
|
356
|
+
closeQuietly();
|
|
357
|
+
throw error;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
function closeQuietly() {
|
|
361
|
+
try {
|
|
362
|
+
wrapper.return();
|
|
363
|
+
} catch {
|
|
364
|
+
// the error being propagated is the actionable one
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return wrapper;
|
|
368
|
+
};
|
|
369
|
+
return range;
|
|
370
|
+
}
|
|
371
|
+
|
|
295
372
|
type MaybePromise<T> = T | Promise<T>;
|
|
296
373
|
|
|
297
374
|
export type CommitOptions = {
|
|
@@ -309,6 +386,11 @@ type ReadTransaction = (LMDBTransaction | RocksTransaction) & {
|
|
|
309
386
|
isCommitted?: boolean;
|
|
310
387
|
};
|
|
311
388
|
|
|
389
|
+
export type WriteGeneration = {
|
|
390
|
+
closed: boolean;
|
|
391
|
+
internalWrites: number;
|
|
392
|
+
};
|
|
393
|
+
|
|
312
394
|
export type TransactionWrite = {
|
|
313
395
|
key: Id;
|
|
314
396
|
store: any; // using any here because of circular dependency and complex RootDatabaseKind
|
|
@@ -381,8 +463,30 @@ export type TransactionWrite = {
|
|
|
381
463
|
// this settles (fire-and-forget from the if-branch), so Table.save()'s lock-writable path awaits
|
|
382
464
|
// it to ensure the write is durable before resolving to the caller.
|
|
383
465
|
innerCommit?: MaybePromise<CommitResolution>;
|
|
466
|
+
// the commit derives stored state (folds, index diffs, residency) from its base entry, so
|
|
467
|
+
// save() must reload that base through the committing transaction's snapshot
|
|
468
|
+
reloadCommitBase?: boolean;
|
|
469
|
+
writeGeneration?: WriteGeneration;
|
|
470
|
+
instanceClosed?: boolean;
|
|
384
471
|
};
|
|
385
472
|
|
|
473
|
+
export function closeWriteInstance(operation: TransactionWrite | null | undefined): void {
|
|
474
|
+
if (operation && !operation.instanceClosed) {
|
|
475
|
+
operation.instanceClosed = true;
|
|
476
|
+
if (operation.writeGeneration) operation.writeGeneration.closed = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function validateWrite(operation: TransactionWrite, txnTime: number, transaction: DatabaseTransaction): any {
|
|
481
|
+
const generation = operation.writeGeneration;
|
|
482
|
+
if (generation) generation.internalWrites++;
|
|
483
|
+
try {
|
|
484
|
+
return operation.validate?.(txnTime, transaction);
|
|
485
|
+
} finally {
|
|
486
|
+
if (generation) generation.internalWrites--;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
386
490
|
export function getAppliedWriteVersion(recordVersion: number | undefined, txnLogKey: number): number {
|
|
387
491
|
return recordVersion == null ? txnLogKey : Math.min(recordVersion, txnLogKey);
|
|
388
492
|
}
|
|
@@ -431,6 +535,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
431
535
|
this.#scopeOwned = options?.scopeOwned === true;
|
|
432
536
|
}
|
|
433
537
|
writes: TransactionWrite[] = []; // the set of writes to commit if the conditions are met
|
|
538
|
+
ownedWrites?: WeakSet<TransactionWrite>;
|
|
434
539
|
// the last staged write per store and key, used to chain repeat writes to the same key (linkWrite)
|
|
435
540
|
declare writesByKey?: Map<any, Map<unknown, TransactionWrite>>;
|
|
436
541
|
completions: Promise<void>[] = []; // the set of outstanding async operations to complete
|
|
@@ -521,6 +626,15 @@ export class DatabaseTransaction implements Transaction {
|
|
|
521
626
|
declare commitChainHead?: DatabaseTransaction;
|
|
522
627
|
// O(1) lookup in recordLockFor; only lock() handles are registered here (no gate handles).
|
|
523
628
|
declare recordLocks?: Map<any, Map<unknown, RecordLockHandle>>;
|
|
629
|
+
/**
|
|
630
|
+
* Whether any staged write was made through a record lock handle. The commit-time lease fence
|
|
631
|
+
* below is skipped entirely when this is false, which is every transaction in a core-only
|
|
632
|
+
* deployment: without it a bulk transaction of 100,000 plain writes pays 100,000 property checks
|
|
633
|
+
* before submission, on a path that is supposed to be untouched when no lock is involved. It
|
|
634
|
+
* latches rather than tracking a count, because a write whose handle was released or expired after
|
|
635
|
+
* staging is exactly what the fence exists to catch.
|
|
636
|
+
*/
|
|
637
|
+
declare hasLeaseProtectedWrite?: boolean;
|
|
524
638
|
// Tracks in-flight acquireRecordKey calls so concurrent lock() calls for the same key in one
|
|
525
639
|
// link (e.g. Promise.all([T.lock(id), T.lock(id)])) can coalesce rather than self-block.
|
|
526
640
|
declare pendingLocks?: Map<any, Map<unknown, Promise<RecordLockHandle>>>;
|
|
@@ -534,8 +648,9 @@ export class DatabaseTransaction implements Transaction {
|
|
|
534
648
|
}
|
|
535
649
|
}
|
|
536
650
|
|
|
537
|
-
|
|
538
|
-
|
|
651
|
+
rangeReadActive = false;
|
|
652
|
+
|
|
653
|
+
renewReadTimeout(): void {
|
|
539
654
|
// The limit is an IDLE limit. Writes always re-arm it (see addWrite), but reads only do so
|
|
540
655
|
// while no uncommitted writes are held: staged writes hold write intents that other writers'
|
|
541
656
|
// coordinated-retry commits park on, so a handler that wrote once and then only reads — an
|
|
@@ -548,6 +663,25 @@ export class DatabaseTransaction implements Transaction {
|
|
|
548
663
|
if ((this.writes.length === 0 && !this.next) || this.open !== TRANSACTION_STATE.OPEN || !this.hasPendingWrites()) {
|
|
549
664
|
this.timeout = Math.max(txnExpiration, this.timeoutBudget);
|
|
550
665
|
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Each engine keeps its own expiration; LMDBTransaction overrides this with its own.
|
|
669
|
+
renewIdleTimeout(): void {
|
|
670
|
+
this.timeout = Math.max(txnExpiration, this.timeoutBudget ?? 0);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// The links after this one wait for its native commit before their own commit() is entered; each
|
|
674
|
+
// hop starts with a full idle window and a stalled native commit is still bounded by one.
|
|
675
|
+
renewChainForNativeCommit(): void {
|
|
676
|
+
for (let txn: DatabaseTransaction = this; txn; txn = txn.next) {
|
|
677
|
+
if (txn.timedOut || txn.open === TRANSACTION_STATE.CLOSED) continue;
|
|
678
|
+
txn.renewIdleTimeout();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
getReadTxn(disableSnapshot?: boolean): ReadTransaction {
|
|
683
|
+
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
684
|
+
this.renewReadTimeout();
|
|
551
685
|
if (this.transaction) {
|
|
552
686
|
if ((this.transaction as any).openTimer) (this.transaction as any).openTimer = 0;
|
|
553
687
|
return this.transaction;
|
|
@@ -582,6 +716,8 @@ export class DatabaseTransaction implements Transaction {
|
|
|
582
716
|
// Monitor state is not ownership state: it stays with `trackedTxns.add` in getReadTxn().
|
|
583
717
|
private attachOwnedTransaction(transaction: RocksTransactionWithRetry): void {
|
|
584
718
|
this.transaction = transaction;
|
|
719
|
+
readTransactionOwners.set(transaction, this);
|
|
720
|
+
this.rangeReadActive = false;
|
|
585
721
|
this.readTxnsUsed = 1;
|
|
586
722
|
this.baseReadRefConsumed = false;
|
|
587
723
|
this.handleOpenedAt = performance.now();
|
|
@@ -623,6 +759,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
623
759
|
trackedTxns.delete(this);
|
|
624
760
|
this.endWriteSupervision();
|
|
625
761
|
this.transaction = null;
|
|
762
|
+
this.rangeReadActive = false;
|
|
626
763
|
this.readTxnsUsed = 0;
|
|
627
764
|
this.readTxnRefCount = 0;
|
|
628
765
|
this.handleOpenedAt = 0;
|
|
@@ -721,6 +858,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
721
858
|
detachWrite(operation: TransactionWrite): void {
|
|
722
859
|
const index = this.writes.indexOf(operation);
|
|
723
860
|
if (index > -1) this.writes[index] = null;
|
|
861
|
+
this.ownedWrites?.delete(operation);
|
|
724
862
|
if (operation.key === undefined) return;
|
|
725
863
|
const writesForStore = this.writesByKey?.get(operation.store);
|
|
726
864
|
if (!writesForStore) return;
|
|
@@ -728,7 +866,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
728
866
|
// Membership, not `stagedIn`, which every commit handler clears and so cannot tell a takeover from a
|
|
729
867
|
// write done in place; a prior already taken over must not become this transaction's basis again.
|
|
730
868
|
let prior = operation.priorWrite;
|
|
731
|
-
while (prior && !this.
|
|
869
|
+
while (prior && !this.ownedWrites?.has(prior)) prior = prior.priorWrite;
|
|
732
870
|
const tail = writesForStore.get(keyId);
|
|
733
871
|
if (tail === operation) {
|
|
734
872
|
if (prior) writesForStore.set(keyId, prior);
|
|
@@ -757,9 +895,9 @@ export class DatabaseTransaction implements Transaction {
|
|
|
757
895
|
if (!storeMap) return undefined;
|
|
758
896
|
const h = storeMap.get(keyId);
|
|
759
897
|
if (!h) return undefined;
|
|
760
|
-
if (!h.
|
|
761
|
-
// Prune released handles so they don't accumulate;
|
|
762
|
-
// would otherwise be seen as the holder and incorrectly granted access.
|
|
898
|
+
if (!h.isExpired()) return h;
|
|
899
|
+
// Prune released/expired handles so they don't accumulate; a handle past its deadline checking
|
|
900
|
+
// re-entrancy would otherwise be seen as the holder and incorrectly granted access.
|
|
763
901
|
storeMap.delete(keyId);
|
|
764
902
|
return undefined;
|
|
765
903
|
}
|
|
@@ -818,9 +956,16 @@ export class DatabaseTransaction implements Transaction {
|
|
|
818
956
|
// fire after a commit or abort, and routing it back here would revive a write this transaction
|
|
819
957
|
// already rolled back — whose blobs abort() has reclaimed. Cleared here, save() resolves the
|
|
820
958
|
// context's current transaction as it did before `stagedIn` existed.
|
|
821
|
-
for (const write of this.writes)
|
|
959
|
+
for (const write of this.writes) {
|
|
960
|
+
if (write?.stagedIn === this) write.stagedIn = undefined;
|
|
961
|
+
if (write) this.ownedWrites?.delete(write);
|
|
962
|
+
}
|
|
822
963
|
this.writes = [];
|
|
823
964
|
this.writesByKey = undefined;
|
|
965
|
+
// The commit-fence latch belongs to the discarded batch. A reused transaction that once staged
|
|
966
|
+
// a locked write would otherwise re-scan every write of every later unlocked batch. Assigned
|
|
967
|
+
// only when it was set, so an ordinary transaction never gains the property at all.
|
|
968
|
+
if (this.hasLeaseProtectedWrite) this.hasLeaseProtectedWrite = false;
|
|
824
969
|
}
|
|
825
970
|
|
|
826
971
|
/**
|
|
@@ -956,6 +1101,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
956
1101
|
this.writeTick = monitorTick;
|
|
957
1102
|
this.linkWrite(operation);
|
|
958
1103
|
this.writes.push(operation);
|
|
1104
|
+
(this.ownedWrites ??= new WeakSet()).add(operation);
|
|
959
1105
|
operation.stagedIn = this;
|
|
960
1106
|
// Hold this write back while any earlier same-key write has not run — out of staging order both
|
|
961
1107
|
// diff against the pre-transaction record (harper#2211, DESIGN.md). The whole chain, not just the
|
|
@@ -987,15 +1133,26 @@ export class DatabaseTransaction implements Transaction {
|
|
|
987
1133
|
const lockHandle = operation.lockHandle;
|
|
988
1134
|
// Guard: a write staged through an expired or released lock handle must not land.
|
|
989
1135
|
// The handle's lease timer already unlocked the native key; another holder may have taken it.
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1136
|
+
// isExpired() re-evaluates the deadline here rather than trusting the timer to have run: a
|
|
1137
|
+
// holder whose event loop stalled past its lease would otherwise commit in the window between
|
|
1138
|
+
// the deadline and its own timer callback, after peers had already granted the key onward.
|
|
1139
|
+
//
|
|
1140
|
+
// A RE-save is judged by the lease alone. `released` belongs to the first stage — a caller that
|
|
1141
|
+
// unlocked and then saved is writing through a handle it gave back — but an operation already
|
|
1142
|
+
// staged was staged while the lock was held, and an `unlock()` inside the lease leaves it valid
|
|
1143
|
+
// (the same rule the pre-submit fence applies below). Without the distinction,
|
|
1144
|
+
// `lock(); save(); unlock(); commit()` succeeded normally and threw 409 only when a conflict
|
|
1145
|
+
// retry or an open read iterator forced the replay to re-save it.
|
|
1146
|
+
if (operation.saved ? lockHandle?.isLeaseExpired() : lockHandle?.isExpired()) {
|
|
1147
|
+
// Through `detachWrite`, which also repairs the per-key chain: dropping it from `writes` alone
|
|
1148
|
+
// leaves `writesByKey` pointing at a write that was refused, so a caller that catches this 409
|
|
1149
|
+
// and stages the same key again would take the rejected operation as its merge basis.
|
|
1150
|
+
this.detachWrite(operation);
|
|
995
1151
|
throw lockNotHeldError(lockHandle);
|
|
996
1152
|
}
|
|
997
1153
|
// Lock-write timestamp rules.
|
|
998
1154
|
if (lockHandle) {
|
|
1155
|
+
this.hasLeaseProtectedWrite = true;
|
|
999
1156
|
if (this.open === TRANSACTION_STATE.CLOSED || this.saveCommits) {
|
|
1000
1157
|
// CLOSED path (second+ write per ImmediateTransaction cycle) OR the first write in
|
|
1001
1158
|
// an ImmediateTransaction (open=OPEN until commit sets it CLOSED, but saveCommits
|
|
@@ -1054,22 +1211,44 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1054
1211
|
(transaction as RocksTransactionWithRetry).isRetry = true;
|
|
1055
1212
|
}
|
|
1056
1213
|
if (!txnTime) txnTime = this.timestamp = transaction.getTimestamp();
|
|
1214
|
+
if (!operation.saved && operation.pendingPriorWrite) {
|
|
1215
|
+
const pendingWrites = [];
|
|
1216
|
+
for (let pending = operation.pendingPriorWrite; pending;) {
|
|
1217
|
+
if (!pending.saved && this.ownedWrites?.has(pending)) pendingWrites.push(pending);
|
|
1218
|
+
pending = pending.pendingPriorWrite !== undefined ? pending.pendingPriorWrite : pending.priorWrite;
|
|
1219
|
+
}
|
|
1220
|
+
for (let index = pendingWrites.length - 1; index >= 0; index--)
|
|
1221
|
+
this.save(pendingWrites[index], transaction, false, options);
|
|
1222
|
+
operation.pendingPriorWrite = null;
|
|
1223
|
+
}
|
|
1057
1224
|
// `txnTime` is this transaction's timestamp — the key its entries take in the per-origin log.
|
|
1058
1225
|
// A write applied from elsewhere carries the origin's record version too, and that is what the
|
|
1059
1226
|
// record is stored at; the two coincide for every locally-originated write. Gated on the apply
|
|
1060
1227
|
// flags so an ordinary write never reads the property (harper#2412).
|
|
1061
1228
|
const writeVersion =
|
|
1062
1229
|
this.sourceApply || this.isReplay ? getAppliedWriteVersion(operation.recordVersion, txnTime) : txnTime;
|
|
1063
|
-
|
|
1064
|
-
|
|
1230
|
+
// A base that feeds stored state must come from this transaction's snapshot, never the
|
|
1231
|
+
// cross-worker cache vouch (stale when a resequenced write reused a version). That closes the
|
|
1232
|
+
// lost-update window only when this transaction holds a snapshot to validate the later Put
|
|
1233
|
+
// against; a snapshot-free transaction (this.snapshotFree, after a mid-scope-commit rotation)
|
|
1234
|
+
// has no snapshot for rocksdb-js to validate the Put against, so it only narrows the window to
|
|
1235
|
+
// the read-to-put span rather than closing it (open follow-up, tracked in the PR description).
|
|
1236
|
+
// Replays keep their pre-read base — their convergence contract is the replay pass itself.
|
|
1237
|
+
const reloadsCommitBase = operation.reloadCommitBase && !operation.saved && !this.isReplay;
|
|
1238
|
+
if (reloadEntry || operation.entry === undefined || reloadsCommitBase) {
|
|
1239
|
+
const uncachedRead = (!!operation.reloadCommitBase && !this.isReplay) || reloadEntry;
|
|
1240
|
+
operation.entry = operation.store.getEntry(operation.key, { transaction, uncachedRead });
|
|
1065
1241
|
}
|
|
1066
1242
|
if (!operation.saved) {
|
|
1067
|
-
operation.saved = true;
|
|
1068
1243
|
// immediately execute in this transaction
|
|
1069
|
-
|
|
1244
|
+
const validated = validateWrite(operation, writeVersion, this);
|
|
1245
|
+
if ((validated as any) === false) {
|
|
1246
|
+
operation.saved = true;
|
|
1070
1247
|
operation.commit = () => {}; // noop if we try again
|
|
1248
|
+
closeWriteInstance(operation);
|
|
1071
1249
|
return;
|
|
1072
1250
|
}
|
|
1251
|
+
operation.saved = true;
|
|
1073
1252
|
let result: Promise<void> = operation.before?.() as Promise<void>;
|
|
1074
1253
|
if (result?.then) this.stageCompletion(result);
|
|
1075
1254
|
result = operation.beforeIntermediate?.() as Promise<void>;
|
|
@@ -1077,7 +1256,12 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1077
1256
|
}
|
|
1078
1257
|
if (lockHandle || this.recordLocks) operation.trackRecordVersion = true;
|
|
1079
1258
|
if (operation.trackRecordVersion) operation.recordVersionApplied = false;
|
|
1080
|
-
|
|
1259
|
+
let completion: Promise<void>;
|
|
1260
|
+
try {
|
|
1261
|
+
completion = operation.commit(writeVersion, operation.entry, this.retries > 0, transaction) as Promise<void>;
|
|
1262
|
+
} finally {
|
|
1263
|
+
closeWriteInstance(operation);
|
|
1264
|
+
}
|
|
1081
1265
|
if (operation.trackRecordVersion)
|
|
1082
1266
|
operation.appliedRecordVersion = operation.recordVersionApplied ? writeVersion : undefined;
|
|
1083
1267
|
if (typeof completion?.then === 'function') this.stageCompletion(completion);
|
|
@@ -1149,6 +1333,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1149
1333
|
// the local is empty: a truthy one is what the loop staged into, and the retained-handle
|
|
1150
1334
|
// and replay branches below deliberately commit a handle other than this.transaction.
|
|
1151
1335
|
if (!transaction) transaction = this.transaction;
|
|
1336
|
+
if (!options.transaction && this.writes.some((write) => write)) this.renewChainForNativeCommit();
|
|
1152
1337
|
this.open = TRANSACTION_STATE.CLOSED;
|
|
1153
1338
|
// RocksTransaction.commit() resolves with RETRY_NOW_VALUE (a number) under
|
|
1154
1339
|
// coordinatedRetry, or void on a normal commit/abort.
|
|
@@ -1234,6 +1419,30 @@ export class DatabaseTransaction implements Transaction {
|
|
|
1234
1419
|
if (transaction) {
|
|
1235
1420
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
1236
1421
|
if (this.writes.length > 0) {
|
|
1422
|
+
// Commit retries can construct fresh ranges on this live handle after read ownership ends.
|
|
1423
|
+
readTransactionOwners.delete(transaction);
|
|
1424
|
+
// Re-fence before submitting: the loop above skips operations already marked saved, so
|
|
1425
|
+
// save()'s own check cannot see a holder that stalled between staging and commit. The
|
|
1426
|
+
// retry/replay path re-saves every operation and is fenced there instead. Lease expiry
|
|
1427
|
+
// only: an unlock() inside the lease leaves the staged write valid, an elapsed lease
|
|
1428
|
+
// does not, whether or not the caller also unlocked.
|
|
1429
|
+
for (let i = 0; this.hasLeaseProtectedWrite && i < this.writes.length; i++) {
|
|
1430
|
+
const lapsed = this.writes[i].lockHandle;
|
|
1431
|
+
if (!lapsed?.isLeaseExpired()) continue;
|
|
1432
|
+
try {
|
|
1433
|
+
transaction.abort();
|
|
1434
|
+
} catch {}
|
|
1435
|
+
// Every other terminal exit from commit() runs the logical cleanup too. Throwing
|
|
1436
|
+
// straight out would strand the OTHER locks this transaction holds, its staged
|
|
1437
|
+
// blobs, and the context's back-reference to a CLOSED transaction — and
|
|
1438
|
+
// transaction()'s onComplete has no rejection path to run it later.
|
|
1439
|
+
try {
|
|
1440
|
+
this.abort();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
harperLogger.debug?.('cleaning up a transaction whose record lock lapsed', error);
|
|
1443
|
+
}
|
|
1444
|
+
throw lockNotHeldError(lapsed);
|
|
1445
|
+
}
|
|
1237
1446
|
// The transaction was created with coordinatedRetry:true (see
|
|
1238
1447
|
// getReadTxn), so commit() can resolve to RETRY_NOW_VALUE. That
|
|
1239
1448
|
// sentinel (a number) is why commitResolution is typed
|
|
@@ -2032,6 +2241,10 @@ function startMonitoringTxns() {
|
|
|
2032
2241
|
reportNow: number,
|
|
2033
2242
|
reportBudget: LongLivedHolderReportBudget
|
|
2034
2243
|
) {
|
|
2244
|
+
if (txn.rangeReadActive) {
|
|
2245
|
+
txn.rangeReadActive = false;
|
|
2246
|
+
txn.renewReadTimeout();
|
|
2247
|
+
}
|
|
2035
2248
|
reportIfLongLived(txn, reportThresholdMs, reportNow, reportBudget);
|
|
2036
2249
|
{
|
|
2037
2250
|
const commitChainHead = txn.commitChainHead ?? txn;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DatabaseTransaction,
|
|
3
|
+
closeWriteInstance,
|
|
4
|
+
validateWrite,
|
|
3
5
|
shouldSpareCommitPhase,
|
|
4
6
|
transactionOpenTooLongError,
|
|
5
7
|
type CommitOptions,
|
|
@@ -37,10 +39,14 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
37
39
|
declare overloadChecked: boolean;
|
|
38
40
|
open = TRANSACTION_STATE.OPEN;
|
|
39
41
|
|
|
42
|
+
renewIdleTimeout(): void {
|
|
43
|
+
this.timeout = Math.max(txnExpiration, this.timeoutBudget ?? 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
getReadTxn(): any {
|
|
41
47
|
// used optimistically
|
|
42
48
|
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
43
|
-
this.
|
|
49
|
+
this.renewIdleTimeout();
|
|
44
50
|
if (this.stale) this.stale = false;
|
|
45
51
|
if (this.readTxn) {
|
|
46
52
|
if ((this.readTxn as any).openTimer) (this.readTxn as any).openTimer = 0;
|
|
@@ -113,12 +119,14 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
113
119
|
|
|
114
120
|
this.linkWrite(operation);
|
|
115
121
|
this.writes.push(operation); // standard path, add to current transaction
|
|
122
|
+
(this.ownedWrites ??= new WeakSet()).add(operation);
|
|
116
123
|
operation.stagedIn = this;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
removeWrite(operation: TransactionWrite) {
|
|
120
127
|
const index = this.writes.indexOf(operation);
|
|
121
128
|
if (index > -1) this.writes[index] = null;
|
|
129
|
+
this.ownedWrites?.delete(operation);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
/**
|
|
@@ -140,7 +148,11 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
140
148
|
this.validated = this.writes.length;
|
|
141
149
|
for (let i = start; i < this.validated; i++) {
|
|
142
150
|
const write = this.writes[i];
|
|
143
|
-
|
|
151
|
+
try {
|
|
152
|
+
if (write) validateWrite(write, this.timestamp, this);
|
|
153
|
+
} finally {
|
|
154
|
+
closeWriteInstance(write);
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
let hasBefore;
|
|
146
158
|
for (let i = start; i < this.validated; i++) {
|
|
@@ -194,6 +206,7 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
194
206
|
throw error;
|
|
195
207
|
}
|
|
196
208
|
}
|
|
209
|
+
if (!retries && this.writes.some((write) => write)) this.renewChainForNativeCommit();
|
|
197
210
|
// release the read snapshot so we don't keep it open longer than necessary
|
|
198
211
|
if (!retries) this.doneReadTxn();
|
|
199
212
|
this.open = options?.doneWriting ? TRANSACTION_STATE.LINGERING : TRANSACTION_STATE.OPEN;
|
|
@@ -203,7 +216,12 @@ export class LMDBTransaction extends DatabaseTransaction {
|
|
|
203
216
|
let writeIndex = 0;
|
|
204
217
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
205
218
|
const doWrite = (write) => {
|
|
206
|
-
|
|
219
|
+
let completion;
|
|
220
|
+
try {
|
|
221
|
+
completion = write.commit(txnTime, write.entry, retries);
|
|
222
|
+
} finally {
|
|
223
|
+
closeWriteInstance(write);
|
|
224
|
+
}
|
|
207
225
|
if (typeof completion?.then === 'function') {
|
|
208
226
|
// the aggregating Promise.all is attached a turn or more later (after the conditional batch
|
|
209
227
|
// or the exclusive transaction resolves), so handle rejection here to keep the gap from
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { trackReadRange } from './DatabaseTransaction.ts';
|
|
1
2
|
import { RocksDatabase, type RocksDatabaseOptions, constants, type Store, Transaction } from '@harperfast/rocksdb-js';
|
|
2
3
|
|
|
3
4
|
const FRESH_VERSION_FLAG = constants.FRESH_VERSION_FLAG;
|
|
4
5
|
import { WeakLRUCache } from 'weak-lru-cache';
|
|
5
6
|
import { when } from '../utility/when.ts';
|
|
6
|
-
import { assignStoredFields, entryMap, METADATA, type Entry } from './RecordEncoder.ts';
|
|
7
|
+
import { assignStoredFields, entryMap, METADATA, VERSION_REUSED, type Entry } from './RecordEncoder.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* RocksDatabase subclass that owns all primary-store behaviour for Harper tables:
|
|
@@ -121,6 +122,13 @@ export class PrimaryRocksDatabase extends RocksDatabase {
|
|
|
121
122
|
*/
|
|
122
123
|
getEntry(id: any, options?: any): any {
|
|
123
124
|
this.readCount++;
|
|
125
|
+
// Commit-path base reads must reflect the caller's transaction snapshot; the cache vouch
|
|
126
|
+
// answers "latest committed" — the wrong question there, and wrong outright for a version a
|
|
127
|
+
// resequenced write reused — so read the store directly, touching neither cache nor VT.
|
|
128
|
+
if (options?.uncachedRead) {
|
|
129
|
+
if (options.async) return when(super.get(id, options), (result) => this.#processEntry(result, id));
|
|
130
|
+
return this.#processEntry(super.getSync(id, options), id);
|
|
131
|
+
}
|
|
124
132
|
const cache = this.#cache;
|
|
125
133
|
// The cache stores the record *value* (weakly, via setValue) rather than
|
|
126
134
|
// the Entry: a WeakRef-wrapped value lets the LRFU expirer release it once
|
|
@@ -158,11 +166,16 @@ export class PrimaryRocksDatabase extends RocksDatabase {
|
|
|
158
166
|
if (cache && cachedValue !== undefined) cache.delete(id);
|
|
159
167
|
return undefined;
|
|
160
168
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
if (entry.version != null && cache) {
|
|
170
|
+
// its version no longer identifies its value, so drop any copy already held
|
|
171
|
+
if (entry.metadataFlags & VERSION_REUSED) {
|
|
172
|
+
if (cachedValue !== undefined) cache.delete(id);
|
|
173
|
+
} else if (entry.value != null && typeof entry.value === 'object') {
|
|
174
|
+
// Only object values can be weakly cached and mapped back to their Entry;
|
|
175
|
+
// primitive/empty values fall through uncached (no fast path, still correct).
|
|
176
|
+
entryMap.set(entry.value, entry);
|
|
177
|
+
cache.setValue(id, entry.value, (entry.size ?? 0) >> 10);
|
|
178
|
+
}
|
|
166
179
|
}
|
|
167
180
|
return entry;
|
|
168
181
|
});
|
|
@@ -178,7 +191,7 @@ export class PrimaryRocksDatabase extends RocksDatabase {
|
|
|
178
191
|
}
|
|
179
192
|
|
|
180
193
|
getRange(options?: any): any {
|
|
181
|
-
const iterable = super.getRange(options);
|
|
194
|
+
const iterable = trackReadRange(options?.transaction, () => super.getRange(options));
|
|
182
195
|
if (options?.valuesForKey) return iterable.map((v: any) => v?.value);
|
|
183
196
|
if (options?.values === false || options?.onlyCount) return iterable;
|
|
184
197
|
if (!this.#enc.isRocksDB) return iterable;
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from './blob.ts';
|
|
31
31
|
import { getThisNodeId } from './nodeIdMapping.ts';
|
|
32
32
|
import { recordAction } from './analytics/write.ts';
|
|
33
|
-
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
33
|
+
import { constants, RocksDatabase } from '@harperfast/rocksdb-js';
|
|
34
34
|
import { when } from '../utility/when.ts';
|
|
35
35
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
36
36
|
import * as envMngr from '../utility/environment/environmentManager.js';
|
|
@@ -124,7 +124,26 @@ export const HAS_NODE_ID = 64;
|
|
|
124
124
|
export const PENDING_LOCAL_TIME = 1;
|
|
125
125
|
export const HAS_STRUCTURE_UPDATE = 0x100;
|
|
126
126
|
export const HAS_ADDITIONAL_AUDIT_REFS = 0x80;
|
|
127
|
-
|
|
127
|
+
// A resequenced write keeps the (newer) version it merged onto, so one version identifies two
|
|
128
|
+
// different stored values and version equality proves nothing. The metadata word this is set in is
|
|
129
|
+
// the same header word the VerificationTable reads at value offset 8 — ACTION_32_BIT is the tag
|
|
130
|
+
// byte its predicate requires — so the bit is what stops the native layer vouching for the version.
|
|
131
|
+
export const VERSION_REUSED = constants.VERSION_NOT_UNIQUE_FLAG;
|
|
132
|
+
// The bit is persisted in every resequenced record, so a rocksdb-js that moved it down onto one of
|
|
133
|
+
// the flags above (or up into the tag byte) would silently change what records already on disk
|
|
134
|
+
// mean: it must stay a single bit strictly between them.
|
|
135
|
+
if (
|
|
136
|
+
typeof VERSION_REUSED !== 'number' ||
|
|
137
|
+
(VERSION_REUSED & (VERSION_REUSED - 1)) !== 0 ||
|
|
138
|
+
VERSION_REUSED < 0x10000 ||
|
|
139
|
+
VERSION_REUSED > 0x800000
|
|
140
|
+
)
|
|
141
|
+
throw new Error(
|
|
142
|
+
`rocksdb-js VERSION_NOT_UNIQUE_FLAG (${VERSION_REUSED}) is not a single bit in the range Harper record metadata reserves for it — requires @harperfast/rocksdb-js >= 2.8.0`
|
|
143
|
+
);
|
|
144
|
+
function versionIsReused(newVersion: number, existingEntry: { version?: number } | undefined): boolean {
|
|
145
|
+
return existingEntry?.version != null && newVersion <= existingEntry.version;
|
|
146
|
+
}
|
|
128
147
|
|
|
129
148
|
const TRACKED_WRITE_TYPES = new Set(['put', 'patch', 'delete', 'message', 'publish']);
|
|
130
149
|
// For now we use this as the private property mechanism for mapping records to entries.
|
|
@@ -885,11 +904,12 @@ export function recordUpdater(store, tableId, auditStore) {
|
|
|
885
904
|
: NO_TIMESTAMP;
|
|
886
905
|
const expiresAt = options?.expiresAt;
|
|
887
906
|
if (expiresAt >= 0) assignMetadata |= HAS_EXPIRATION;
|
|
888
|
-
if (isRocksDB && record !== undefined && existingEntry?.version != null && newVersion <= existingEntry.version) {
|
|
889
|
-
assignMetadata = Math.max(assignMetadata, 0) | VERSION_NOT_UNIQUE_FLAG;
|
|
890
|
-
}
|
|
891
907
|
metadataInNextEncoding = assignMetadata;
|
|
892
908
|
expiresAtNextEncoding = expiresAt;
|
|
909
|
+
// Math.max normalizes the -1 "no metadata word" sentinel to 0 first: OR-ing the flag into
|
|
910
|
+
// -1 directly would stay -1 and silently drop the metadata word (and the flag with it).
|
|
911
|
+
if (isRocksDB && record !== undefined && versionIsReused(newVersion, existingEntry))
|
|
912
|
+
metadataInNextEncoding = Math.max(metadataInNextEncoding, 0) | VERSION_REUSED;
|
|
893
913
|
const putOptions: {
|
|
894
914
|
version: number;
|
|
895
915
|
instructedWrite?: boolean;
|