@harperfast/harper 5.2.0-beta.4 → 5.2.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/backup.ts +169 -0
- package/bin/cliOperations.ts +92 -73
- package/bin/copyDb.ts +21 -4
- package/bin/harper.ts +43 -55
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +120 -30
- package/components/componentLoader.ts +177 -25
- package/components/deployLifecycle.ts +119 -33
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +5 -6
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +14 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/backup.d.ts +9 -0
- package/dist/bin/backup.js +192 -0
- package/dist/bin/backup.js.map +1 -0
- package/dist/bin/cliOperations.d.ts +13 -0
- package/dist/bin/cliOperations.js +89 -70
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +13 -2
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +13 -0
- package/dist/bin/harper.js +45 -56
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +38 -1
- package/dist/components/Scope.js +117 -28
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +160 -18
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +7 -0
- package/dist/components/mcp/toolRegistry.js +10 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.js +11 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +9 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/operations.js +6 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +34 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/scopeMount.d.ts +86 -0
- package/dist/components/scopeMount.js +131 -0
- package/dist/components/scopeMount.js.map +1 -0
- package/dist/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/backupManifest.d.ts +26 -0
- package/dist/dataLayer/backupManifest.js +97 -0
- package/dist/dataLayer/backupManifest.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +87 -0
- package/dist/dataLayer/blobBackup.js +282 -0
- package/dist/dataLayer/blobBackup.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +122 -0
- package/dist/dataLayer/restoreMarker.js +261 -0
- package/dist/dataLayer/restoreMarker.js.map +1 -0
- package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
- package/dist/dataLayer/rocksdbBackup.js +1039 -0
- package/dist/dataLayer/rocksdbBackup.js.map +1 -0
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -109
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -5
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -5
- package/dist/resources/Table.js +114 -37
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -1
- package/dist/resources/blob.js +31 -14
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +59 -1
- package/dist/resources/databases.js +386 -41
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.js.map +1 -1
- package/dist/server/REST.js +25 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +15 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +7 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +20 -1
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobRunner.js +10 -0
- package/dist/server/jobs/jobRunner.js.map +1 -1
- package/dist/server/jobs/jobs.js +11 -0
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +10 -1
- package/dist/server/middlewareChain.js +81 -21
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +27 -7
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +21 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +68 -21
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +13 -1
- package/dist/utility/hdbTerms.js +13 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +47 -0
- package/dist/utility/logging/harper_logger.js +803 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/operation_authorization.js +13 -3
- package/dist/utility/operation_authorization.js.map +1 -1
- package/npm-shrinkwrap.json +198 -193
- package/package.json +12 -7
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +310 -104
- package/resources/ResourceInterface.ts +8 -5
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +341 -263
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +31 -14
- package/resources/databases.ts +387 -46
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/transaction.ts +0 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +61 -34
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/http.ts +100 -13
- package/server/itc/serverHandlers.js +7 -1
- package/server/jobs/jobProcess.ts +18 -1
- package/server/jobs/jobRunner.ts +10 -0
- package/server/jobs/jobs.ts +11 -0
- package/server/middlewareChain.ts +79 -20
- package/server/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +28 -7
- package/server/serverHelpers/serverUtilities.ts +29 -7
- package/server/static.ts +87 -23
- package/server/storageReclamation.ts +104 -8
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +3 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +13 -1
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +786 -41
- package/utility/operation_authorization.ts +31 -3
- package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
|
@@ -31,22 +31,112 @@ const MAX_RETRIES = 40;
|
|
|
31
31
|
// Cap the per-retry backoff so replication-applied transactions, which retry conflicts without a
|
|
32
32
|
// cap (see the commit rejection handler), don't grow the delay unbounded.
|
|
33
33
|
const MAX_RETRY_DELAY_MS = 1000;
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
34
|
+
// Every native write commit currently outstanding on this worker thread, oldest first.
|
|
35
|
+
// checkOverloaded() sheds new application writes once the OLDEST of these has been outstanding
|
|
36
|
+
// past MAX_OUTSTANDING_TXN_DURATION, so a commit whose promise never settles (harper#2001) is
|
|
37
|
+
// detected whichever transaction submitted it. A linked list rather than a Set because the write
|
|
38
|
+
// path reads the oldest on every write and must not allocate an iterator to do it, and because
|
|
39
|
+
// commits settle out of order and so have to unlink in constant time.
|
|
40
|
+
interface OutstandingCommit {
|
|
41
|
+
start: number;
|
|
42
|
+
prev: OutstandingCommit | undefined;
|
|
43
|
+
next: OutstandingCommit | undefined;
|
|
44
|
+
// Identity for the one-time checkOverloaded() log below (harper#2001) — otherwise a stuck commit
|
|
45
|
+
// gives no indication of which database/table/resource to investigate. Snapshotted at arm time
|
|
46
|
+
// (not read from `this.writes`/`this.startedFrom` lazily off the DatabaseTransaction object)
|
|
47
|
+
// because that object can be reused for a later immediate commit while this native commit is
|
|
48
|
+
// still wedged — its resolve handler runs clearWrites() on the SAME object, which would blank or
|
|
49
|
+
// replace the identity out from under a deferred read. Per-node (not a single module-level slot)
|
|
50
|
+
// so that every outstanding commit carries its own identity and its own `logged` flag: a second
|
|
51
|
+
// commit that is still stuck once the first settles becomes the new oldest and logs on its own.
|
|
52
|
+
store: any;
|
|
53
|
+
startedFrom: { resourceName: string; method: string } | undefined;
|
|
54
|
+
nativeTransaction: any;
|
|
55
|
+
logged: boolean;
|
|
56
|
+
}
|
|
57
|
+
let oldestOutstandingCommit: OutstandingCommit | undefined;
|
|
58
|
+
let newestOutstandingCommit: OutstandingCommit | undefined;
|
|
59
|
+
let outstandingCommitCount = 0;
|
|
60
|
+
// Caps the stuck-commit log (checkOverloaded() below) to at most one line per this interval across
|
|
61
|
+
// the whole thread, regardless of how many distinct commits individually cross the threshold — see
|
|
62
|
+
// the comment at the log site for why a per-commit-only dedup isn't enough under sustained overload.
|
|
63
|
+
const OVERLOAD_LOG_MIN_INTERVAL_MS = 1000;
|
|
64
|
+
let lastOverloadLogAt = -Infinity;
|
|
65
|
+
|
|
66
|
+
// Track a submitted commit until it settles. Every attempt is tracked unconditionally: a
|
|
67
|
+
// coordinated retry round and a chained second-store commit are both issued from inside the
|
|
68
|
+
// preceding commit's own resolve handler, which runs before any reaction that could release a
|
|
69
|
+
// single shared slot — so anything conditional on "is something already outstanding" skips them
|
|
70
|
+
// and leaves a wedged retry or chain invisible to checkOverloaded() forever. Each attempt is timed
|
|
71
|
+
// from its own submission, keeping the overload window per-attempt rather than cumulative over a
|
|
72
|
+
// retry ladder. `.then(untrack, untrack)` also marks an ERR_BUSY rejection handled, so this
|
|
73
|
+
// tracking never surfaces as an unhandled rejection alongside the caller's own handler.
|
|
74
|
+
// Exported only so unit tests can drive the list with controllable promises: the unlink order that
|
|
75
|
+
// matters (a middle or tail node settling first) cannot be forced through real writes, and a node
|
|
76
|
+
// left linked would 503 every write on this thread forever. commit() below is the sole caller.
|
|
77
|
+
// Also the single source for write-queue-depth accounting (getTransactionQueueDepths below): every
|
|
78
|
+
// native commit this function tracks is, by definition, exactly the write-queue backlog — see the
|
|
79
|
+
// comment there for why that used to be a second, separately-maintained counter.
|
|
80
|
+
// `store`/`startedFrom`/`nativeTransaction` are the identity snapshot for checkOverloaded()'s stuck-
|
|
81
|
+
// commit log (harper#2001); omit them (as the test seam below does) when a caller has none to give.
|
|
82
|
+
export function trackOutstandingCommit(
|
|
83
|
+
commitResolution: Promise<number | void>,
|
|
84
|
+
store?: any,
|
|
85
|
+
startedFrom?: { resourceName: string; method: string },
|
|
86
|
+
nativeTransaction?: any
|
|
87
|
+
): void {
|
|
88
|
+
// Guards against a future caller passing a non-Promise: today commit() always hands this a real
|
|
89
|
+
// Promise, but an unguarded link here would leave a node permanently wedged in the list (503ing
|
|
90
|
+
// every write on this thread) with no settlement to ever unlink it.
|
|
91
|
+
if (typeof commitResolution?.then !== 'function') return;
|
|
92
|
+
const outstanding: OutstandingCommit = {
|
|
93
|
+
start: performance.now(),
|
|
94
|
+
prev: newestOutstandingCommit,
|
|
95
|
+
next: undefined,
|
|
96
|
+
store,
|
|
97
|
+
startedFrom,
|
|
98
|
+
nativeTransaction,
|
|
99
|
+
logged: false,
|
|
100
|
+
};
|
|
101
|
+
if (newestOutstandingCommit != null) newestOutstandingCommit.next = outstanding;
|
|
102
|
+
else oldestOutstandingCommit = outstanding;
|
|
103
|
+
newestOutstandingCommit = outstanding;
|
|
104
|
+
outstandingCommitCount++;
|
|
105
|
+
// Doubles as the write-queue-depth high-water mark (see getTransactionQueueDepths): every
|
|
106
|
+
// outstanding commit is a write-queue entry, so the peak of one is the peak of the other.
|
|
107
|
+
if (outstandingCommitCount > writeTxnQueueDepthHighWater) writeTxnQueueDepthHighWater = outstandingCommitCount;
|
|
108
|
+
// Guards against double-untracking the same node: `.then(untrack, untrack)` below means a promise
|
|
109
|
+
// that both resolves and later has its rejection handler independently triggered (or is tracked via
|
|
110
|
+
// a shared/misused resolution) could otherwise run the unlink twice, corrupting the list or driving
|
|
111
|
+
// outstandingCommitCount negative.
|
|
112
|
+
let untracked = false;
|
|
113
|
+
const untrack = () => {
|
|
114
|
+
if (untracked) return;
|
|
115
|
+
untracked = true;
|
|
116
|
+
if (outstanding.prev != null) outstanding.prev.next = outstanding.next;
|
|
117
|
+
else oldestOutstandingCommit = outstanding.next;
|
|
118
|
+
if (outstanding.next != null) outstanding.next.prev = outstanding.prev;
|
|
119
|
+
else newestOutstandingCommit = outstanding.prev;
|
|
120
|
+
outstandingCommitCount--;
|
|
121
|
+
};
|
|
122
|
+
commitResolution.then(untrack, untrack);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How many write commits are outstanding on this thread and how long the oldest has been waiting
|
|
127
|
+
* (`oldestAgeMs` is undefined when none is). `oldestAgeMs` is exactly the value checkOverloaded()
|
|
128
|
+
* rejects on, exposed so a commit that never settles (harper#2001) can be observed directly rather
|
|
129
|
+
* than inferred from the 503s it eventually produces.
|
|
130
|
+
*/
|
|
131
|
+
export function getOutstandingCommits(): { count: number; oldestAgeMs: number | undefined } {
|
|
132
|
+
return {
|
|
133
|
+
count: outstandingCommitCount,
|
|
134
|
+
oldestAgeMs: oldestOutstandingCommit ? performance.now() - oldestOutstandingCommit.start : undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Once per process: committing under open read iterators forces a write replay, so the warning is
|
|
138
|
+
// about the caller's pattern, not the individual commit.
|
|
139
|
+
let replayedWritesWarned = false;
|
|
50
140
|
|
|
51
141
|
// The analytics module registers a recorder here at load (dependency inversion, mirroring
|
|
52
142
|
// `replicationConfirmation` below) so the storage layer doesn't statically import the analytics/server
|
|
@@ -80,29 +170,20 @@ function recordCommitLatency(commitResolution: Promise<number | void>, submitted
|
|
|
80
170
|
|
|
81
171
|
// Queue-depth gauges surfaced through the analytics pipeline (write-transaction-queue-depth /
|
|
82
172
|
// read-transaction-queue-depth). Per-thread state; the analytics aggregator sums across threads.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// (
|
|
87
|
-
//
|
|
173
|
+
// The write depth is `outstandingCommitCount` itself (maintained above by trackOutstandingCommit) —
|
|
174
|
+
// write commits handed to the storage engine but not yet resolved are exactly the same set of native
|
|
175
|
+
// commits the overload check tracks, and keeping one counter instead of two removes the duplicate
|
|
176
|
+
// per-commit bookkeeping (and the drift risk: a code path that updates one but not the other, as the
|
|
177
|
+
// replay path did before this fix). Read depth is derived from the live `trackedTxns` set (every
|
|
178
|
+
// tracked transaction holds an open read snapshot). We also retain a high-water mark per sampling
|
|
179
|
+
// window because the queue can fill and drain within a single (~1s) analytics period, so an
|
|
88
180
|
// instantaneous sample taken at emit time would routinely miss the spike operators need to see.
|
|
89
181
|
// RocksDB-write-path only: LMDB routes through the separate LMDBTransaction.commit()/getReadTxn()
|
|
90
182
|
// overrides (resources/LMDBTransaction.ts), which maintain their own unrelated `trackedTxns` set and
|
|
91
183
|
// do not call into this accounting.
|
|
92
|
-
let writeTxnQueueDepth = 0;
|
|
93
184
|
let writeTxnQueueDepthHighWater = 0;
|
|
94
185
|
let readTxnQueueDepthHighWater = 0;
|
|
95
186
|
|
|
96
|
-
function enterWriteQueue() {
|
|
97
|
-
if (++writeTxnQueueDepth > writeTxnQueueDepthHighWater) writeTxnQueueDepthHighWater = writeTxnQueueDepth;
|
|
98
|
-
}
|
|
99
|
-
function leaveWriteQueue() {
|
|
100
|
-
// Floor at zero: accounting is balanced by construction (every enterWriteQueue has exactly one
|
|
101
|
-
// matching settlement), but the guard is cheap insurance against a future call-site imbalance
|
|
102
|
-
// producing a negative depth that would corrupt every subsequent sample.
|
|
103
|
-
if (writeTxnQueueDepth > 0) writeTxnQueueDepth--;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
187
|
/**
|
|
107
188
|
* Returns the current write/read transaction queue depths for this thread along with the high-water
|
|
108
189
|
* mark observed since the previous call, then resets the high-water marks to the current depth so the
|
|
@@ -113,12 +194,12 @@ export function getTransactionQueueDepths() {
|
|
|
113
194
|
// dominates the current size here — no need to reconcile against `readDepth` before reporting.
|
|
114
195
|
const readDepth = trackedTxns.size;
|
|
115
196
|
const depths = {
|
|
116
|
-
writeDepth:
|
|
197
|
+
writeDepth: outstandingCommitCount,
|
|
117
198
|
writeMaxDepth: writeTxnQueueDepthHighWater,
|
|
118
199
|
readDepth,
|
|
119
200
|
readMaxDepth: readTxnQueueDepthHighWater,
|
|
120
201
|
};
|
|
121
|
-
writeTxnQueueDepthHighWater =
|
|
202
|
+
writeTxnQueueDepthHighWater = outstandingCommitCount;
|
|
122
203
|
readTxnQueueDepthHighWater = readDepth;
|
|
123
204
|
return depths;
|
|
124
205
|
}
|
|
@@ -248,6 +329,12 @@ export class DatabaseTransaction implements Transaction {
|
|
|
248
329
|
readTxnRefCount: number;
|
|
249
330
|
readTxnsUsed: number;
|
|
250
331
|
timeout: number;
|
|
332
|
+
// Write recency, tracked separately from `timeout`: set only by addWrite, never by a read. `timeout`
|
|
333
|
+
// is re-armed by reads too (on a link with no pending writes of its own, see the fast path in
|
|
334
|
+
// getReadTxn), so chainStillActive can't use it to mean "this link was written recently" — a `.next`
|
|
335
|
+
// link with no writes that is being read in a loop would otherwise masquerade as write activity and
|
|
336
|
+
// keep a write-holding head immortal.
|
|
337
|
+
declare writeTimeout: number;
|
|
251
338
|
validated = 0;
|
|
252
339
|
timestamp = 0;
|
|
253
340
|
retries = 0;
|
|
@@ -256,6 +343,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
256
343
|
// Whether this read handle's base reference (readTxnsUsed starts at 1 in getReadTxn) has been
|
|
257
344
|
// consumed by a commit round; iterator references are consumed only by doneReadTxn().
|
|
258
345
|
declare baseReadRefConsumed?: boolean;
|
|
346
|
+
// Set when a final commit/abort wanted to release the context's back-reference (see
|
|
347
|
+
// releaseContext()) but outstanding read iterators were still using this transaction —
|
|
348
|
+
// doneReadTxn() completes the release once the last iterator drains.
|
|
349
|
+
declare pendingContextRelease?: boolean;
|
|
259
350
|
declare startedFrom?: {
|
|
260
351
|
resourceName: string;
|
|
261
352
|
method: string;
|
|
@@ -278,10 +369,24 @@ export class DatabaseTransaction implements Transaction {
|
|
|
278
369
|
// open-transaction limit. Once poisoned, any further addWrite/commit throws transactionOpenTooLongError
|
|
279
370
|
// so the request rolls back cleanly instead of silently committing a partial write set (issue #1407).
|
|
280
371
|
declare timedOut?: boolean;
|
|
372
|
+
// Set once the retained read handle's write intents have been released (see commit()'s
|
|
373
|
+
// outstanding-iterators branch), so a retry round cannot re-fire the release.
|
|
374
|
+
declare writesAbandoned?: boolean;
|
|
281
375
|
|
|
282
376
|
getReadTxn(disableSnapshot?: boolean): ReadTransaction {
|
|
283
377
|
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
284
|
-
|
|
378
|
+
// The limit is an IDLE limit. Writes always re-arm it (see addWrite), but reads only do so
|
|
379
|
+
// while no uncommitted writes are held: staged writes hold write intents that other writers'
|
|
380
|
+
// coordinated-retry commits park on, so a handler that wrote once and then only reads — an
|
|
381
|
+
// orphaned long-poll whose client had already gone, in harper#2001 — must not keep those
|
|
382
|
+
// intents alive by reading. A transaction that keeps writing stays alive; so does a purely
|
|
383
|
+
// read-only one. A committed transaction re-arms too: its intents went with the commit, and
|
|
384
|
+
// the monitor bounds its retained read snapshot separately.
|
|
385
|
+
// `writes`/`next` are checked inline first: this is a hot path and the dominant case is a
|
|
386
|
+
// single-store transaction that has never written.
|
|
387
|
+
if ((this.writes.length === 0 && !this.next) || this.open !== TRANSACTION_STATE.OPEN || !this.hasPendingWrites()) {
|
|
388
|
+
this.timeout = txnExpiration;
|
|
389
|
+
}
|
|
285
390
|
if (this.transaction) {
|
|
286
391
|
if ((this.transaction as any).openTimer) (this.transaction as any).openTimer = 0;
|
|
287
392
|
return this.transaction;
|
|
@@ -328,6 +433,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
328
433
|
trackedTxns.delete(this);
|
|
329
434
|
this.transaction?.abort();
|
|
330
435
|
this.transaction = null;
|
|
436
|
+
this.completeDeferredContextRelease();
|
|
331
437
|
}
|
|
332
438
|
}
|
|
333
439
|
|
|
@@ -346,6 +452,19 @@ export class DatabaseTransaction implements Transaction {
|
|
|
346
452
|
harperLogger.debug?.('releasing timed-out read transaction', error);
|
|
347
453
|
}
|
|
348
454
|
this.transaction = null;
|
|
455
|
+
this.completeDeferredContextRelease();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Complete a context release that releaseContext() deferred because outstanding read iterators
|
|
460
|
+
* were still using this transaction (see releaseContext()) — called once the last one drains,
|
|
461
|
+
* whether that happens naturally (doneReadTxn()) or is forced by the long-transaction monitor
|
|
462
|
+
* (releaseReadTxn()).
|
|
463
|
+
*/
|
|
464
|
+
private completeDeferredContextRelease(): void {
|
|
465
|
+
if (!this.pendingContextRelease) return;
|
|
466
|
+
this.pendingContextRelease = false;
|
|
467
|
+
if (this.#context?.transaction === this) this.#context.transaction = null;
|
|
349
468
|
}
|
|
350
469
|
|
|
351
470
|
disregardReadTxn(): void {
|
|
@@ -378,25 +497,71 @@ export class DatabaseTransaction implements Transaction {
|
|
|
378
497
|
this.writesByKey = undefined;
|
|
379
498
|
}
|
|
380
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Drop this transaction's back-reference from its context once completed (commit or abort),
|
|
502
|
+
* so a long-lived context (e.g. an MQTT subscription context held open for the life of a
|
|
503
|
+
* suspended delivery loop) doesn't keep pinning a finished transaction in memory. Guarded by
|
|
504
|
+
* identity: a context already re-pointed at a different (e.g. reused) transaction is untouched.
|
|
505
|
+
*
|
|
506
|
+
* `final` must be false for an in-callback explicit `context.transaction.commit()` — the
|
|
507
|
+
* "commit in the middle" pattern intentionally keeps recommitting and adding writes to the
|
|
508
|
+
* SAME instance (see the comment above about a transaction being "reused and committed
|
|
509
|
+
* again"), so releasing here would strand those later writes with no transaction to join.
|
|
510
|
+
* Only resources/transaction.ts's own wrapper commit (`{ doneWriting: true }`, once the
|
|
511
|
+
* caller's callback has fully returned) and abort() are truly final.
|
|
512
|
+
*
|
|
513
|
+
* A final commit can still have outstanding read iterators streaming through this.transaction
|
|
514
|
+
* (see the outstanding-iterators branch in commit()) — those keep this instance meaningfully
|
|
515
|
+
* alive (a fresh write on the same context must not join a DIFFERENT, already-replayed
|
|
516
|
+
* transaction) until doneReadTxn() drains the last one, so the release is deferred to there.
|
|
517
|
+
*
|
|
518
|
+
* Sets `.transaction` to `null` rather than deleting the property: `Context.transaction` is
|
|
519
|
+
* typed `DatabaseTransaction | null | undefined` precisely to document this released state, and
|
|
520
|
+
* `delete` would repeatedly force a long-lived, hot context (e.g. an MQTT subscription context
|
|
521
|
+
* releasing/reattaching a transaction per message) into V8's slower dictionary-mode property
|
|
522
|
+
* storage.
|
|
523
|
+
*/
|
|
524
|
+
private releaseContext(final: boolean): void {
|
|
525
|
+
if (!final) return;
|
|
526
|
+
if (this.readTxnsUsed > 0) {
|
|
527
|
+
this.pendingContextRelease = true;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (this.#context?.transaction === this) this.#context.transaction = null;
|
|
531
|
+
}
|
|
532
|
+
|
|
381
533
|
checkOverloaded() {
|
|
382
534
|
if (
|
|
383
|
-
|
|
535
|
+
oldestOutstandingCommit &&
|
|
384
536
|
!this.overloadChecked &&
|
|
385
|
-
performance.now() -
|
|
537
|
+
performance.now() - oldestOutstandingCommit.start > MAX_OUTSTANDING_TXN_DURATION
|
|
386
538
|
) {
|
|
387
|
-
|
|
539
|
+
const now = performance.now();
|
|
540
|
+
// Also rate-limited across the whole overload episode (not just deduped per commit): under
|
|
541
|
+
// sustained heavy load, many distinct commits can each individually age past the limit in
|
|
542
|
+
// quick succession as earlier ones finally settle, which without this cap would turn one
|
|
543
|
+
// overload episode into a growing stream of ERROR lines — the same "flood" harper#2001's
|
|
544
|
+
// original per-request log was fixed to avoid, just shifted from per-request to per-commit.
|
|
545
|
+
// A commit skipped by the cooldown is NOT marked `logged`, so it still gets a log later if
|
|
546
|
+
// it's still the oldest once the cooldown clears, rather than going silent forever.
|
|
547
|
+
if (!oldestOutstandingCommit.logged && now - lastOverloadLogAt > OVERLOAD_LOG_MIN_INTERVAL_MS) {
|
|
388
548
|
// Log once per stuck commit (not once per rejected request, harper#2001): a wedged
|
|
389
549
|
// thread otherwise logs nothing at all server-side while rejecting every write with a
|
|
390
|
-
// 503, which was the single biggest obstacle to root-causing a recurrence.
|
|
391
|
-
|
|
392
|
-
|
|
550
|
+
// 503, which was the single biggest obstacle to root-causing a recurrence. The flag lives
|
|
551
|
+
// on the node itself, so if THIS commit settles while still over the limit and a
|
|
552
|
+
// different one is now oldest, that one logs too instead of staying silent forever.
|
|
553
|
+
oldestOutstandingCommit.logged = true;
|
|
554
|
+
lastOverloadLogAt = now;
|
|
555
|
+
const nativeTransactionId = oldestOutstandingCommit.nativeTransaction?.id;
|
|
556
|
+
const store = oldestOutstandingCommit.store;
|
|
557
|
+
const startedFrom = oldestOutstandingCommit.startedFrom;
|
|
393
558
|
harperLogger.error(
|
|
394
559
|
`Rejecting writes on this thread: a commit has been outstanding for ` +
|
|
395
|
-
`${Math.round(
|
|
396
|
-
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${
|
|
560
|
+
`${Math.round(now - oldestOutstandingCommit.start)}ms (exceeds the ` +
|
|
561
|
+
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${store?.rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
|
|
397
562
|
(nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
|
|
398
|
-
(
|
|
399
|
-
? `, started from ${
|
|
563
|
+
(startedFrom?.resourceName
|
|
564
|
+
? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
|
|
400
565
|
: '') +
|
|
401
566
|
`. Further record updates and publishes from new application requests on this thread ` +
|
|
402
567
|
`will be rejected with 503 until the commit settles or the process is restarted (deletes, ` +
|
|
@@ -410,6 +575,13 @@ export class DatabaseTransaction implements Transaction {
|
|
|
410
575
|
|
|
411
576
|
addWrite(operation: TransactionWrite) {
|
|
412
577
|
if (this.timedOut) throw transactionOpenTooLongError();
|
|
578
|
+
// A write is activity: it re-arms the idle limit on this link even though the reads it
|
|
579
|
+
// performs no longer do (see getReadTxn), so a transaction that keeps writing stays alive
|
|
580
|
+
// and only an idle one holding write intents is reaped.
|
|
581
|
+
this.timeout = txnExpiration;
|
|
582
|
+
// Independent write-recency signal for chainStillActive (see the field comment) — reads never
|
|
583
|
+
// touch this, only writes do.
|
|
584
|
+
this.writeTimeout = txnExpiration;
|
|
413
585
|
this.linkWrite(operation);
|
|
414
586
|
this.writes.push(operation);
|
|
415
587
|
if (!operation.deferSave) {
|
|
@@ -535,6 +707,14 @@ export class DatabaseTransaction implements Transaction {
|
|
|
535
707
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
536
708
|
if (this.writes.length > 0) {
|
|
537
709
|
if (!options.transaction) {
|
|
710
|
+
if (!replayedWritesWarned) {
|
|
711
|
+
replayedWritesWarned = true;
|
|
712
|
+
harperLogger.warn?.(
|
|
713
|
+
`Committing while read iterators are still open: ${this.writes.length} staged write(s) must be re-staged and committed on a second transaction, doubling their write work` +
|
|
714
|
+
(this.startedFrom ? `, from ${this.startedFrom.resourceName}.${this.startedFrom.method}` : '') +
|
|
715
|
+
`. Fully consume (or close) iterators before committing to avoid this. Logged once per process.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
538
718
|
// Deliberately NOT marked isRetry and NOT carrying over the original's onCommit:
|
|
539
719
|
// audit/txn-log entries batch natively on the transaction they were staged into and
|
|
540
720
|
// are only durably written by that transaction's commit attempt (an abort discards
|
|
@@ -557,6 +737,28 @@ export class DatabaseTransaction implements Transaction {
|
|
|
557
737
|
// re-staged the writes into it
|
|
558
738
|
commitResolution = transaction.commit() as Promise<void>;
|
|
559
739
|
recordCommitLatency(commitResolution, performance.now());
|
|
740
|
+
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
741
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
|
|
742
|
+
// dedicated accounting here (as a prior version of this replay path did) used to leave
|
|
743
|
+
// write-transaction-queue-depth, the one metric that can observe a commit that never
|
|
744
|
+
// settles (harper#2001), reading zero for exactly this path.
|
|
745
|
+
}
|
|
746
|
+
// No commit will ever run on the retained handle — the replay above owns these
|
|
747
|
+
// writes — so this is the only place its write intents can be released. Left in
|
|
748
|
+
// place, other writers' coordinated-retry commits park on them until the last
|
|
749
|
+
// iterator finishes (harper#2001). Reads through the handle, including
|
|
750
|
+
// read-your-own-writes, keep working. Once only: a coordinated-retry or backoff
|
|
751
|
+
// round re-enters this branch on the same retained handle. Fenced like the other
|
|
752
|
+
// post-submit steps here: the replay commit is already in flight, so a throw must
|
|
753
|
+
// not skip onCommit/the chain-store commit below. Optional: rocksdb-js < 2.7
|
|
754
|
+
// lacks the method.
|
|
755
|
+
if (!this.writesAbandoned) {
|
|
756
|
+
this.writesAbandoned = true;
|
|
757
|
+
try {
|
|
758
|
+
(this.transaction as { abandonWrites?: () => void } | null)?.abandonWrites?.();
|
|
759
|
+
} catch (error) {
|
|
760
|
+
harperLogger.warn?.('Failed to release write intents on a retained read transaction', error);
|
|
761
|
+
}
|
|
560
762
|
}
|
|
561
763
|
} else {
|
|
562
764
|
// no more reads need to be performed, just commit/abort based if there are any writes
|
|
@@ -571,30 +773,19 @@ export class DatabaseTransaction implements Transaction {
|
|
|
571
773
|
// Promise<number | void>; it is handled in the resolve callback below.
|
|
572
774
|
commitResolution = transaction.commit();
|
|
573
775
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
574
|
-
// metric. This is the same clock the overload check uses (
|
|
575
|
-
//
|
|
776
|
+
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
777
|
+
// stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
|
|
576
778
|
// "Outstanding write transactions have too long of queue" (503) rejection. A transient-
|
|
577
|
-
// conflict retry rejects this promise and issues a fresh commit(),
|
|
578
|
-
//
|
|
579
|
-
// retries<=2 paths re-commit synchronously while outstandingCommit is still set, so
|
|
580
|
-
// recording per attempt here counts more attempts than the overload check ever arms for.
|
|
779
|
+
// conflict retry rejects this promise and issues a fresh commit(), which is tracked as
|
|
780
|
+
// its own attempt, so recording per attempt matches the overload semantics.
|
|
581
781
|
// commitResolution's declared type (Promise<number | void> | void) doesn't narrow to
|
|
582
782
|
// Promise<void> here because the widening union defeats flow analysis on the prior
|
|
583
783
|
// cast assignment; re-assert it — this branch's commit() result is always a Promise.
|
|
584
784
|
recordCommitLatency(commitResolution as Promise<void>, performance.now());
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
// The thenable guard protects against a future caller passing a non-Promise
|
|
590
|
-
// `commitResolution` (today it is always rocksdb-js's async Transaction.commit()
|
|
591
|
-
// result, guaranteed to be a Promise).
|
|
592
|
-
enterWriteQueue();
|
|
593
|
-
if (commitResolution && typeof (commitResolution as any).then === 'function') {
|
|
594
|
-
commitResolution.then(leaveWriteQueue, leaveWriteQueue);
|
|
595
|
-
} else {
|
|
596
|
-
leaveWriteQueue();
|
|
597
|
-
}
|
|
785
|
+
// Write-queue-depth accounting for this commit happens uniformly below, via
|
|
786
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. A
|
|
787
|
+
// transient-conflict retry rejects this promise and issues a fresh commit()
|
|
788
|
+
// (re-entering here), which trackOutstandingCommit tracks as its own attempt.
|
|
598
789
|
} else {
|
|
599
790
|
try {
|
|
600
791
|
commitResolution = transaction.abort();
|
|
@@ -609,43 +800,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
609
800
|
}
|
|
610
801
|
|
|
611
802
|
if (commitResolution) {
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
// — so a retry or a second store's commit that itself wedges is never armed, and
|
|
617
|
-
// neither checkOverloaded() nor this log will ever see it. Only the first store's
|
|
618
|
-
// first commit attempt is covered.
|
|
619
|
-
if (!outstandingCommit) {
|
|
620
|
-
outstandingCommit = commitResolution;
|
|
621
|
-
outstandingCommitStart = performance.now();
|
|
622
|
-
// Snapshot the store/origin now rather than reading them off `this` lazily in
|
|
623
|
-
// checkOverloaded(): `this` (a DatabaseTransaction) can be reused for a later immediate
|
|
624
|
-
// commit while this native commit is still wedged, and that later commit's resolve
|
|
625
|
-
// handler runs clearWrites() on this SAME object — a lazy read would then blank or
|
|
626
|
-
// mis-attribute the identity out from under the deferred log. Read the table off the
|
|
627
|
-
// write itself, not this.db, which is whichever table first claimed this per-database
|
|
628
|
-
// transaction in txnForContext and so can name the wrong table when a transaction spans
|
|
629
|
-
// more than one table in the same database. Both are stable references (a Store, and an
|
|
630
|
-
// object set once and never mutated), so this holds no allocation and no lingering
|
|
631
|
-
// reference to the transaction/writes/entries graph.
|
|
632
|
-
outstandingCommitStore = this.writes[0]?.store;
|
|
633
|
-
outstandingCommitStartedFrom = this.startedFrom;
|
|
634
|
-
outstandingCommitNativeTransaction = transaction;
|
|
635
|
-
outstandingCommitLogged = false;
|
|
636
|
-
outstandingCommit
|
|
637
|
-
// if `commitResolution` rejects with and `ERR_BUSY` error, the retry logic
|
|
638
|
-
// will correct course, but the reject will still be propagated on the
|
|
639
|
-
// `outstandingCommit` promise and needs to be caught and silenced
|
|
640
|
-
.catch(() => {})
|
|
641
|
-
.finally(() => {
|
|
642
|
-
outstandingCommit = null;
|
|
643
|
-
outstandingCommitStore = undefined;
|
|
644
|
-
outstandingCommitStartedFrom = undefined;
|
|
645
|
-
outstandingCommitNativeTransaction = undefined;
|
|
646
|
-
outstandingCommitLogged = false;
|
|
647
|
-
});
|
|
648
|
-
}
|
|
803
|
+
// Read the table off the write itself, not this.db, which is whichever table first
|
|
804
|
+
// claimed this per-database transaction in txnForContext and so can name the wrong
|
|
805
|
+
// table when a transaction spans more than one table in the same database.
|
|
806
|
+
trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
|
|
649
807
|
const completions = [];
|
|
650
808
|
return commitResolution.then(
|
|
651
809
|
(commitResult) => {
|
|
@@ -721,7 +879,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
721
879
|
// now reset transactions tracking; this transaction be reused and committed again
|
|
722
880
|
this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
|
|
723
881
|
this.clearWrites();
|
|
724
|
-
|
|
882
|
+
this.releaseContext(!!options.doneWriting);
|
|
725
883
|
this.next = null;
|
|
726
884
|
let txnTime = this.timestamp;
|
|
727
885
|
this.timestamp = 0; // reset the timestamp as well
|
|
@@ -803,6 +961,11 @@ export class DatabaseTransaction implements Transaction {
|
|
|
803
961
|
} catch (abortError) {
|
|
804
962
|
harperLogger.debug?.('aborting transaction after failed commit', abortError);
|
|
805
963
|
}
|
|
964
|
+
// A terminal failure is just as final as a success — release the context's
|
|
965
|
+
// back-reference here too, or transaction.ts's onComplete() (which has no
|
|
966
|
+
// rejection handler of its own) would leave a long-lived context pinning this
|
|
967
|
+
// CLOSED wrapper forever.
|
|
968
|
+
this.releaseContext(!!options.doneWriting);
|
|
806
969
|
throw error;
|
|
807
970
|
}
|
|
808
971
|
}
|
|
@@ -813,7 +976,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
813
976
|
cleanupUnusedBlobs(write.savedBlobs, collectRetainedFileIds(write.store.getEntry(write.key)?.value));
|
|
814
977
|
}
|
|
815
978
|
this.clearWrites();
|
|
816
|
-
|
|
979
|
+
this.releaseContext(!!options.doneWriting);
|
|
817
980
|
const txnResolution: CommitResolution = {
|
|
818
981
|
txnTime: this.timestamp,
|
|
819
982
|
};
|
|
@@ -848,7 +1011,13 @@ export class DatabaseTransaction implements Transaction {
|
|
|
848
1011
|
}
|
|
849
1012
|
// reset the transaction
|
|
850
1013
|
this.clearWrites();
|
|
851
|
-
|
|
1014
|
+
// A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
|
|
1015
|
+
// Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
|
|
1016
|
+
// starting a fresh one) so the rest of the logical operation fails atomically via the
|
|
1017
|
+
// poison check in addWrite()/commit(), rather than silently landing a later write on a
|
|
1018
|
+
// brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
|
|
1019
|
+
// make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
|
|
1020
|
+
this.releaseContext(!this.timedOut);
|
|
852
1021
|
}
|
|
853
1022
|
/**
|
|
854
1023
|
* Give up on a chain of linked transactions after exhausting conflict retries: poison every link
|
|
@@ -976,9 +1145,31 @@ export class ImmediateTransaction extends DatabaseTransaction {
|
|
|
976
1145
|
|
|
977
1146
|
let timer;
|
|
978
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* True when a link other than `txn` in the same multi-store chain was written recently enough to
|
|
1150
|
+
* still be active — i.e. its `writeTimeout` (set only by addWrite, see the field comment) hasn't
|
|
1151
|
+
* decayed to zero. Writes re-arm only the link that receives them, so a chain writing database B
|
|
1152
|
+
* while its head only reads A would otherwise be aborted by the head's own decay.
|
|
1153
|
+
*/
|
|
1154
|
+
function chainStillActive(txn: DatabaseTransaction): boolean {
|
|
1155
|
+
for (let link: DatabaseTransaction = txn.next; link; link = link.next) {
|
|
1156
|
+
// A write-only link (e.g. a blind write to a second database, never itself read) never calls
|
|
1157
|
+
// getReadTxn, so it's never added to trackedTxns and the main loop below never decays it.
|
|
1158
|
+
// Decay it here instead, so an idle write-only link eventually expires rather than keeping the
|
|
1159
|
+
// whole chain immortal (harper#2001's blind-write shape).
|
|
1160
|
+
if (!trackedTxns.has(link) && link.writeTimeout > 0) link.writeTimeout -= txnExpiration;
|
|
1161
|
+
if (link.writeTimeout > 0) return true;
|
|
1162
|
+
}
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
979
1166
|
function startMonitoringTxns() {
|
|
980
1167
|
timer = setInterval(function () {
|
|
981
1168
|
for (const txn of trackedTxns) {
|
|
1169
|
+
// Decay write recency once per tick for every tracked link, independent of the `timeout`
|
|
1170
|
+
// branches below — a tracked link that keeps its own idle limit alive by reading must not
|
|
1171
|
+
// thereby keep chainStillActive believing it was written recently too.
|
|
1172
|
+
if (txn.writeTimeout > 0) txn.writeTimeout -= txnExpiration;
|
|
982
1173
|
if (txn.timeout <= 0) {
|
|
983
1174
|
const url = (txn.getContext() as any)?.url;
|
|
984
1175
|
if (txn.open === TRANSACTION_STATE.CLOSED) {
|
|
@@ -994,6 +1185,12 @@ function startMonitoringTxns() {
|
|
|
994
1185
|
}`
|
|
995
1186
|
);
|
|
996
1187
|
txn.releaseReadTxn();
|
|
1188
|
+
} else if (txn.hasPendingWrites() && chainStillActive(txn)) {
|
|
1189
|
+
// A later link in the chain was written recently (writes re-arm only the link that
|
|
1190
|
+
// receives them, and a multi-store transaction can be writing database B while this
|
|
1191
|
+
// head only reads A). The logical transaction is still active, so re-arm this link
|
|
1192
|
+
// rather than aborting the whole chain out from under it.
|
|
1193
|
+
txn.timeout = txnExpiration;
|
|
997
1194
|
} else if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
|
|
998
1195
|
// Abort and surface an error rather than force-committing a partial write set: silently
|
|
999
1196
|
// committing on the application's behalf breaks atomicity and can leave orphaned
|
|
@@ -1040,6 +1237,15 @@ function startMonitoringTxns() {
|
|
|
1040
1237
|
|
|
1041
1238
|
startMonitoringTxns();
|
|
1042
1239
|
|
|
1240
|
+
/**
|
|
1241
|
+
* Test seam: re-arms the once-per-process replay warning. The whole unit suite shares one process,
|
|
1242
|
+
* so whichever test first drives a commit under open iterators consumes the warning for every test
|
|
1243
|
+
* after it.
|
|
1244
|
+
*/
|
|
1245
|
+
export function resetReplayedWritesWarning() {
|
|
1246
|
+
replayedWritesWarned = false;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1043
1249
|
export function setTxnExpiration(ms) {
|
|
1044
1250
|
clearInterval(timer);
|
|
1045
1251
|
txnExpiration = ms;
|
|
@@ -67,8 +67,14 @@ export interface Context {
|
|
|
67
67
|
login?: (username: string, password: string) => Promise<string>;
|
|
68
68
|
/** Describes the current cookie-based session if it is present and grants the capacity to delete it. authentication.enableSessions must be turned on in the harperdb-config.yaml */
|
|
69
69
|
session?: Session;
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The database transaction object. `undefined` means none was ever attached; `null` means one
|
|
72
|
+
* was attached and has since completed and released its back-reference (DatabaseTransaction.ts's
|
|
73
|
+
* releaseContext()) — kept `null` rather than deleting the property so a long-lived, hot context
|
|
74
|
+
* (e.g. an MQTT subscription context releasing/reattaching a transaction per message) doesn't
|
|
75
|
+
* repeatedly force V8 to deoptimize it into dictionary-mode property storage.
|
|
76
|
+
*/
|
|
77
|
+
transaction?: DatabaseTransaction | null;
|
|
72
78
|
/** If the operation that will be performed with this context should check user authorization */
|
|
73
79
|
authorize?: boolean;
|
|
74
80
|
/** The last modification time of any data that has been accessed with this context */
|
|
@@ -95,7 +101,6 @@ export interface Context {
|
|
|
95
101
|
originatingOperation?: OperationFunctionName;
|
|
96
102
|
previousResidency?: string[];
|
|
97
103
|
nodeName?: string;
|
|
98
|
-
resourceCache?: Map<Id, any>;
|
|
99
104
|
_freezeRecords?: boolean; // until v5, we conditionally freeze records for back-compat
|
|
100
105
|
timestamp?: number;
|
|
101
106
|
includeExpensiveRecordCountEstimates?: boolean;
|
|
@@ -127,8 +132,6 @@ export interface SourceContext<TRequestContext = Context, Record extends object
|
|
|
127
132
|
noCacheStore?: boolean;
|
|
128
133
|
/** Reference to the source Resource instance */
|
|
129
134
|
source?: ResourceInterface<Record>;
|
|
130
|
-
/** Shared resource cache from parent context for visibility of modifications */
|
|
131
|
-
resourceCache?: Map<Id, any>;
|
|
132
135
|
/** Database transaction for the context */
|
|
133
136
|
transaction?: DatabaseTransaction;
|
|
134
137
|
/** The time at which the cached entry should expire (ms since epoch) */
|