@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
|
@@ -50,6 +50,7 @@ function getIdMappingRecord(auditStore) {
|
|
|
50
50
|
// now we can take over the local node id
|
|
51
51
|
nameToId[node_name] = 0;
|
|
52
52
|
auditStore.putSync(REMOTE_NODE_IDS, pack(idMappingRecord));
|
|
53
|
+
invalidateNodeNames(auditStore);
|
|
53
54
|
}
|
|
54
55
|
return idMappingRecord;
|
|
55
56
|
}
|
|
@@ -84,6 +85,7 @@ export function remoteToLocalNodeId(remoteMapping: any, auditStore: any) {
|
|
|
84
85
|
}
|
|
85
86
|
if (hasChanges) {
|
|
86
87
|
auditStore.putSync(REMOTE_NODE_IDS, pack(idMappingRecord));
|
|
88
|
+
invalidateNodeNames(auditStore);
|
|
87
89
|
}
|
|
88
90
|
return remoteToLocalId;
|
|
89
91
|
}
|
|
@@ -103,6 +105,7 @@ export function getIdOfRemoteNode(remoteNodeName, auditStore) {
|
|
|
103
105
|
id = lastId + 1;
|
|
104
106
|
nameToId[remoteNodeName] = id;
|
|
105
107
|
auditStore.putSync(REMOTE_NODE_IDS, pack(idMappingRecord));
|
|
108
|
+
invalidateNodeNames(auditStore);
|
|
106
109
|
}
|
|
107
110
|
logger.trace?.('The remote node name map', remoteNodeName, nameToId, id);
|
|
108
111
|
return id;
|
|
@@ -123,3 +126,50 @@ export function lastTimeInAuditStore(auditStore: Database) {
|
|
|
123
126
|
export function getThisNodeId(auditStore: any) {
|
|
124
127
|
return exportIdMapping(auditStore)?.[server.hostname];
|
|
125
128
|
}
|
|
129
|
+
|
|
130
|
+
// Inverted id -> name map. exportIdMapping() re-reads and unpacks the mapping record on every call,
|
|
131
|
+
// which is far too much per replicated entry on the apply thread; ids are stable once minted, so the
|
|
132
|
+
// inversion is cached and dropped wherever nameToId is written. Keyed per audit store because short
|
|
133
|
+
// ids are minted per database in first-seen order — id 1 names a different node in each one.
|
|
134
|
+
// Refresh window for a MISS. A hit is always trusted, but a miss cannot be: ids are minted by
|
|
135
|
+
// whichever worker first talks to a peer, and invalidation only reaches that worker's own copy — so
|
|
136
|
+
// a newly admitted node is absent from every other worker's map until it is rebuilt. Rebuilding on
|
|
137
|
+
// every miss would put a store read and unpack back on the apply thread for each unmapped entry, so
|
|
138
|
+
// misses re-read at most this often — short enough that a joining node loses at most a stray entry,
|
|
139
|
+
// long enough that a burst of unmapped ids cannot drive the store. `onMiss` records whether the
|
|
140
|
+
// cached map was built by a `rebuildOnMiss` caller, which is what bounds that caller to one read per
|
|
141
|
+
// window too rather than exempting it from the interval entirely.
|
|
142
|
+
const NODE_NAME_REFRESH_MS = 50;
|
|
143
|
+
const idToNodeName = new WeakMap<object, { names: Map<number, string>; refreshedAt: number; onMiss: boolean }>();
|
|
144
|
+
function invalidateNodeNames(auditStore: any) {
|
|
145
|
+
idToNodeName.delete(auditStore);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The node name a local short id refers to. Callers that must attribute a replicated entry to its
|
|
150
|
+
* origin node need the globally stable name, not the id, which is assigned per node.
|
|
151
|
+
*/
|
|
152
|
+
export function getNodeNameForId(
|
|
153
|
+
auditStore: any,
|
|
154
|
+
nodeId: number | undefined,
|
|
155
|
+
rebuildOnMiss = false
|
|
156
|
+
): string | undefined {
|
|
157
|
+
if (typeof nodeId !== 'number' || !auditStore) return undefined;
|
|
158
|
+
const cached = idToNodeName.get(auditStore);
|
|
159
|
+
const hit = cached?.names.get(nodeId);
|
|
160
|
+
if (hit !== undefined) return hit;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
// `rebuildOnMiss` is for entry classes whose loss costs more than the read: a dropped record lock
|
|
163
|
+
// release leaves the home holding a grant until its own deadline, so a just-minted id is worth one
|
|
164
|
+
// store read rather than up to `NODE_NAME_REFRESH_MS` of dropped entries. ONE read, though, not one
|
|
165
|
+
// per entry — an id this map will never resolve (a node whose mapping was purged, an origin
|
|
166
|
+
// relayed from a database that never minted one) is exactly the burst the interval exists to keep
|
|
167
|
+
// off the apply thread, and a rebuild inside the window is not stale: a write to the mapping drops
|
|
168
|
+
// this entry outright (`invalidateNodeNames`).
|
|
169
|
+
if (cached && now - cached.refreshedAt < NODE_NAME_REFRESH_MS && (!rebuildOnMiss || cached.onMiss)) return undefined;
|
|
170
|
+
const nameToId = exportIdMapping(auditStore);
|
|
171
|
+
const names = new Map<number, string>();
|
|
172
|
+
for (const name in nameToId) names.set(nameToId[name], name);
|
|
173
|
+
idToNodeName.set(auditStore, { names, refreshedAt: now, onMiss: rebuildOnMiss });
|
|
174
|
+
return names.get(nodeId);
|
|
175
|
+
}
|
package/resources/recordLock.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
1
2
|
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
2
3
|
import { ClientError } from '../utility/errors/hdbError.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Exclusive per-record locks (harper#483
|
|
6
|
+
* Exclusive per-record locks (harper#483).
|
|
6
7
|
*
|
|
7
|
-
* The
|
|
8
|
-
* shared by every worker thread's handle.
|
|
9
|
-
*
|
|
8
|
+
* The intra-node authority is the rocksdb-js process-wide key lock: one `locks` map per DBDescriptor
|
|
9
|
+
* shared by every worker thread's handle. Acquiring and releasing it write nothing to the store, and
|
|
10
|
+
* the record's version and bytes are unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Phase 1 layers cluster-wide exclusion on top without changing that: once the native key is held,
|
|
13
|
+
* `Table.lock()` obtains a delegation for the key from its home node (see `recordLockCoordinator.ts`)
|
|
14
|
+
* and calls `joinClusterRound()` on the handle. A live delegation serves repeat locks with no cluster
|
|
15
|
+
* message at all. A node-scoped lock (`{ scope: 'node' }`), or any lock on a database with no
|
|
16
|
+
* transport registered, skips the cluster step entirely and behaves exactly as it did in Phase 0.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
export const DEFAULT_LOCK_LEASE_MS = 30_000;
|
|
@@ -24,12 +31,20 @@ export interface RecordLockOptions {
|
|
|
24
31
|
timeout?: number;
|
|
25
32
|
/** A held lock outlives the acquiring transaction; it is released by `unlock()` or by its lease */
|
|
26
33
|
hold?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* `'cluster'` (default) is exclusive across every participating node; `'node'` keeps Phase 0
|
|
36
|
+
* semantics and is exclusive only across this node's worker threads.
|
|
37
|
+
*/
|
|
38
|
+
scope?: 'cluster' | 'node';
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export interface ResolvedRecordLockOptions {
|
|
30
42
|
lease: number;
|
|
31
43
|
timeout: number;
|
|
32
44
|
hold: boolean;
|
|
45
|
+
scope: 'cluster' | 'node';
|
|
46
|
+
/** True when the caller named `scope` explicitly, which makes `'cluster'` fail-closed. */
|
|
47
|
+
scopeRequested: boolean;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/**
|
|
@@ -55,6 +70,35 @@ export interface RecordLockHandle {
|
|
|
55
70
|
noteCandidateFloor(version: number): void;
|
|
56
71
|
/** Compute a holder-write version without advancing the floor until that write commits. */
|
|
57
72
|
holderVersionCandidate(floor?: number): number;
|
|
73
|
+
/** `ts_R` of the granted cluster round; undefined while the hold is node-scoped (Phase 0). */
|
|
74
|
+
clusterTsR?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Whether this handle may still authorize a write, evaluated NOW rather than trusting the lease
|
|
77
|
+
* timer to have run. A holder whose event loop stalled past its deadline would otherwise commit
|
|
78
|
+
* between the deadline and its own timer callback, while peers had already expired the hold.
|
|
79
|
+
* Expiry is detected on a monotonic clock, so a wall-clock jump cannot extend a lease.
|
|
80
|
+
*/
|
|
81
|
+
isExpired(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether the LEASE elapsed. Distinct from `isExpired()` in that a deliberate release does NOT
|
|
84
|
+
* make it true on its own: a write staged while the lock was held stays valid at commit if the
|
|
85
|
+
* caller merely unlocked. Once the deadline passes it is true either way, because by then peers
|
|
86
|
+
* have passed their own bound and the key may already belong to someone else.
|
|
87
|
+
*/
|
|
88
|
+
isLeaseExpired(): boolean;
|
|
89
|
+
/** Revoke authority ahead of the lease, so a staged-but-uncommitted write is fenced at commit. */
|
|
90
|
+
revokeLease(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Re-anchor a granted cluster round: the hold now runs from `tsR` (no margin, so this node
|
|
93
|
+
* always expires before any participant does) and `onRelease` emits the durable LOCK_RELEASE.
|
|
94
|
+
* Returns false when the round completed after the lease had already elapsed.
|
|
95
|
+
*
|
|
96
|
+
* `mintedMono` is `performance.now()` at the instant `tsR` was minted. The remaining lease is
|
|
97
|
+
* measured from it rather than from `tsR - Date.now()`: `tsR` comes from a never-decreasing
|
|
98
|
+
* monotonic source, so after a backward wall-clock step that subtraction would hand back MORE
|
|
99
|
+
* lease than the round was granted, past what every peer bounded from its own observation.
|
|
100
|
+
*/
|
|
101
|
+
joinClusterRound(tsR: number, leaseMs: number, mintedMono: number, onRelease: () => void): boolean;
|
|
58
102
|
/**
|
|
59
103
|
* Return the next version to stamp a holder write with. Starts at acquiredAt when no transaction
|
|
60
104
|
* version has been noted; otherwise advances by the minimum monotonic step from the recorded floor.
|
|
@@ -83,10 +127,15 @@ export function resolveLockOptions(options?: RecordLockOptions | null): Resolved
|
|
|
83
127
|
throw new ClientError(`Lock options must be an object, but received ${typeof options}`);
|
|
84
128
|
const hold = options?.hold ?? false;
|
|
85
129
|
if (typeof hold !== 'boolean') throw new ClientError(`Lock option hold must be a boolean, but received ${hold}`);
|
|
130
|
+
const scope = options?.scope ?? 'cluster';
|
|
131
|
+
if (scope !== 'cluster' && scope !== 'node')
|
|
132
|
+
throw new ClientError(`Lock option scope must be 'cluster' or 'node', but received ${scope}`);
|
|
86
133
|
return {
|
|
87
134
|
lease: requireDuration('lease', options?.lease, DEFAULT_LOCK_LEASE_MS, MIN_LOCK_LEASE_MS, MAX_LOCK_LEASE_MS),
|
|
88
135
|
timeout: requireDuration('timeout', options?.timeout, DEFAULT_LOCK_TIMEOUT_MS, 1, MAX_LOCK_TIMEOUT_MS),
|
|
89
136
|
hold,
|
|
137
|
+
scope,
|
|
138
|
+
scopeRequested: options?.scope !== undefined,
|
|
90
139
|
};
|
|
91
140
|
}
|
|
92
141
|
|
|
@@ -105,6 +154,19 @@ export function lockAttemptKey(tableId: number, id: any): any[] {
|
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
const warnedLeaseTimerStores = new WeakSet();
|
|
157
|
+
/**
|
|
158
|
+
* Rate-limited rather than latched, for the reason `warnOnce` in the coordinator gives: a release
|
|
159
|
+
* that cannot be written leaves peers waiting out the lease, and a latch would show an operator the
|
|
160
|
+
* first occurrence and then hide the condition for as long as it lasts.
|
|
161
|
+
*/
|
|
162
|
+
const CLUSTER_RELEASE_WARN_INTERVAL_MS = 60_000;
|
|
163
|
+
let lastClusterReleaseWarn = -Infinity;
|
|
164
|
+
function warnClusterReleaseFailure(err: unknown) {
|
|
165
|
+
const now = performance.now();
|
|
166
|
+
if (now - lastClusterReleaseWarn < CLUSTER_RELEASE_WARN_INTERVAL_MS) return;
|
|
167
|
+
lastClusterReleaseWarn = now;
|
|
168
|
+
harperLogger.warn?.('cluster record lock release could not be written; peers will expire the hold', err);
|
|
169
|
+
}
|
|
108
170
|
|
|
109
171
|
class KeyLockHandle implements RecordLockHandle {
|
|
110
172
|
store: any;
|
|
@@ -115,9 +177,15 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
115
177
|
released = false;
|
|
116
178
|
expired = false;
|
|
117
179
|
acquiredAt: number;
|
|
180
|
+
clusterTsR: number | undefined;
|
|
118
181
|
#lastHolderVersion: number | undefined;
|
|
119
182
|
#candidateFloor: number | undefined;
|
|
120
183
|
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
184
|
+
// Monotonic mirror of expiresAt. The wall-clock field stays public (callers and the cluster
|
|
185
|
+
// protocol both speak in timestamps) but the enforcement compares monotonic readings, so neither
|
|
186
|
+
// an NTP step nor a manual clock change can extend a lease past what peers assumed.
|
|
187
|
+
#deadlineMono: number | undefined;
|
|
188
|
+
#onRelease: (() => void) | undefined;
|
|
121
189
|
|
|
122
190
|
constructor(store: any, key: any[], keyId: unknown, lease?: number, hold = false, acquiredAt?: number) {
|
|
123
191
|
this.store = store;
|
|
@@ -127,6 +195,7 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
127
195
|
this.hold = hold;
|
|
128
196
|
this.acquiredAt = acquiredAt ?? store.getMonotonicTimestamp();
|
|
129
197
|
if (lease != null) {
|
|
198
|
+
this.#deadlineMono = performance.now() + lease;
|
|
130
199
|
this.#timer = setTimeout(() => this.#onLeaseExpire(), lease).unref();
|
|
131
200
|
}
|
|
132
201
|
}
|
|
@@ -150,7 +219,70 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
150
219
|
);
|
|
151
220
|
}
|
|
152
221
|
|
|
222
|
+
isExpired(): boolean {
|
|
223
|
+
return this.released || this.expired || this.#leaseLapsed();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
isLeaseExpired(): boolean {
|
|
227
|
+
return this.expired || this.#leaseLapsed();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The single place the deadline is evaluated, so the two predicates cannot disagree about it. A
|
|
231
|
+
// deliberate release does not make a lapsed lease valid again: by then peers have passed their own
|
|
232
|
+
// bound and the key may belong to someone else.
|
|
233
|
+
#leaseLapsed(): boolean {
|
|
234
|
+
if (this.#deadlineMono === undefined || performance.now() < this.#deadlineMono) return false;
|
|
235
|
+
// Finish the expiry rather than only reporting it: the native key must actually go back so a
|
|
236
|
+
// stalled holder does not keep a key every participant has already written off.
|
|
237
|
+
if (!this.released) this.#onLeaseExpire();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Revoke this handle's authority immediately, ahead of its lease. A recall must stop a delegate
|
|
243
|
+
* from COMMITTING, not merely from admitting: a caller that already staged a write and then called
|
|
244
|
+
* `unlock()` has nothing outstanding for a drain to wait on, but its write is still in the
|
|
245
|
+
* transaction and would land after the successor was admitted. Expiring the handle is what makes
|
|
246
|
+
* the commit-time fence reject it (§6).
|
|
247
|
+
*/
|
|
248
|
+
revokeLease(): void {
|
|
249
|
+
if (this.expired) return;
|
|
250
|
+
// `expired` is what the commit-time fence reads, and it must be set even when the caller has
|
|
251
|
+
// ALREADY unlocked: a write staged before `unlock()` is still in its transaction, and that is
|
|
252
|
+
// precisely the handoff §6 exists to fence. Gating this on `released` made the revocation a
|
|
253
|
+
// no-op in the one state it was written for.
|
|
254
|
+
this.expired = true;
|
|
255
|
+
if (this.released) {
|
|
256
|
+
// The native key is already back and `onRelease` already fired; only the fence flag and the
|
|
257
|
+
// lease timer are left to settle.
|
|
258
|
+
clearTimeout(this.#timer);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.#onLeaseExpire();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
joinClusterRound(tsR: number, leaseMs: number, mintedMono: number, onRelease: () => void): boolean {
|
|
265
|
+
// A round that completed after the native lease already fired yields no usable hold: the key
|
|
266
|
+
// has gone back and another worker may hold it, so every write through this handle would 409.
|
|
267
|
+
if (this.isLeaseExpired()) return false;
|
|
268
|
+
const remaining = leaseMs - (performance.now() - mintedMono);
|
|
269
|
+
if (remaining <= 0) return false;
|
|
270
|
+
this.clusterTsR = tsR;
|
|
271
|
+
this.acquiredAt = tsR;
|
|
272
|
+
this.#lastHolderVersion = undefined;
|
|
273
|
+
this.#onRelease = onRelease;
|
|
274
|
+
this.expiresAt = tsR + leaseMs;
|
|
275
|
+
// From the mint origin, not a second clock read: the design says this bound has no margin.
|
|
276
|
+
this.#deadlineMono = mintedMono + leaseMs;
|
|
277
|
+
clearTimeout(this.#timer);
|
|
278
|
+
this.#timer = setTimeout(() => this.#onLeaseExpire(), remaining).unref();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
153
282
|
nextHolderVersion(): number {
|
|
283
|
+
// TODO(harper#2412): under the settled dual-clock model the lock-ordered stamp belongs in the
|
|
284
|
+
// distinct-version second word (HAS_DISTINCT_VERSION_FLAG), not the transaction timestamp.
|
|
285
|
+
// Until that lands, Phase 1 keeps this Phase 0 mechanism, anchored at ts_R in cluster mode.
|
|
154
286
|
const next = this.holderVersionCandidate();
|
|
155
287
|
this.#lastHolderVersion = next;
|
|
156
288
|
return next;
|
|
@@ -160,6 +292,7 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
160
292
|
if (this.released) return;
|
|
161
293
|
this.released = true;
|
|
162
294
|
this.expired = true;
|
|
295
|
+
clearTimeout(this.#timer);
|
|
163
296
|
try {
|
|
164
297
|
this.store.unlock(this.key);
|
|
165
298
|
} catch (err) {
|
|
@@ -168,6 +301,24 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
168
301
|
harperLogger.warn?.('record lock lease timer failed (store may have been dropped)', err);
|
|
169
302
|
}
|
|
170
303
|
}
|
|
304
|
+
this.#emitRelease();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The cluster release is a durability optimization, not the safety mechanism — peers expire the
|
|
308
|
+
// hold on their own lease bound — so neither a throw nor a rejected promise may escape here. This
|
|
309
|
+
// runs from a lease timer and from post-commit cleanup, where an escaping rejection would take
|
|
310
|
+
// down the worker or turn a committed transaction into a 500.
|
|
311
|
+
#emitRelease() {
|
|
312
|
+
const onRelease = this.#onRelease;
|
|
313
|
+
if (!onRelease) return;
|
|
314
|
+
this.#onRelease = undefined;
|
|
315
|
+
try {
|
|
316
|
+
const result = onRelease() as unknown;
|
|
317
|
+
if (result && typeof (result as Promise<void>).then === 'function')
|
|
318
|
+
(result as Promise<void>).then(undefined, warnClusterReleaseFailure);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
warnClusterReleaseFailure(err);
|
|
321
|
+
}
|
|
171
322
|
}
|
|
172
323
|
|
|
173
324
|
release(): boolean {
|
|
@@ -182,14 +333,29 @@ class KeyLockHandle implements RecordLockHandle {
|
|
|
182
333
|
harperLogger.warn?.('record lock release failed (store may have been dropped)', err);
|
|
183
334
|
}
|
|
184
335
|
}
|
|
336
|
+
this.#emitRelease();
|
|
185
337
|
return true;
|
|
186
338
|
}
|
|
187
339
|
|
|
188
340
|
upgradeToHold(lease: number): void {
|
|
189
341
|
this.hold = true;
|
|
190
342
|
clearTimeout(this.#timer);
|
|
191
|
-
|
|
192
|
-
|
|
343
|
+
// A granted cluster round may not be extended locally: peers bound the hold from the request
|
|
344
|
+
// they saw, and a longer local lease is exactly the two-holder window this protocol removes.
|
|
345
|
+
// Clamped on the monotonic deadline, not on expiresAt, so a backward wall-clock step cannot
|
|
346
|
+
// turn the upgrade into the extension this is here to refuse.
|
|
347
|
+
const now = performance.now();
|
|
348
|
+
const extended = now + lease;
|
|
349
|
+
const deadline = this.clusterTsR === undefined ? extended : Math.min(extended, this.#deadlineMono ?? extended);
|
|
350
|
+
const remaining = deadline - now;
|
|
351
|
+
this.#deadlineMono = deadline;
|
|
352
|
+
this.expiresAt = Date.now() + remaining;
|
|
353
|
+
this.#timer = setTimeout(() => this.#onLeaseExpire(), remaining).unref();
|
|
354
|
+
// Prime the nextHolderVersion counter so a later CLOSED-path save (e.g. after the
|
|
355
|
+
// enclosing transaction commits) gets acquiredAt+MIN_STEP rather than acquiredAt again
|
|
356
|
+
// — the same priming the fresh-hold path performs. Without this, a post-commit save
|
|
357
|
+
// and any in-transaction save both get acquiredAt, causing a LWW tie that drops the write.
|
|
358
|
+
this.nextHolderVersion();
|
|
193
359
|
}
|
|
194
360
|
}
|
|
195
361
|
|
|
@@ -219,7 +385,7 @@ export async function acquireRecordKey(
|
|
|
219
385
|
hold = false
|
|
220
386
|
): Promise<RecordLockHandle> {
|
|
221
387
|
const existing = txn.recordLockFor(store, keyId);
|
|
222
|
-
if (existing && !existing.
|
|
388
|
+
if (existing && !existing.isExpired()) return existing;
|
|
223
389
|
|
|
224
390
|
const deadline = Date.now() + waitMs;
|
|
225
391
|
// One persistent wake slot: at most one onUnlocked callback is registered per waiter at any
|