@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
|
@@ -34,12 +34,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ImmediateTransaction = exports.DatabaseTransaction = exports.TRANSACTION_STATE = void 0;
|
|
37
|
+
exports.trackOutstandingCommit = trackOutstandingCommit;
|
|
38
|
+
exports.getOutstandingCommits = getOutstandingCommits;
|
|
37
39
|
exports.setCommitLatencyRecorder = setCommitLatencyRecorder;
|
|
38
40
|
exports.getTransactionQueueDepths = getTransactionQueueDepths;
|
|
39
41
|
exports.replicationConfirmation = replicationConfirmation;
|
|
40
42
|
exports.transactionOpenTooLongError = transactionOpenTooLongError;
|
|
41
43
|
exports.priorStagedWrite = priorStagedWrite;
|
|
42
44
|
exports.writeKeyId = writeKeyId;
|
|
45
|
+
exports.resetReplayedWritesWarning = resetReplayedWritesWarning;
|
|
43
46
|
exports.setTxnExpiration = setTxnExpiration;
|
|
44
47
|
const blob_ts_1 = require("./blob.js");
|
|
45
48
|
const commonUtility_ts_1 = require("../utility/lmdb/commonUtility.js");
|
|
@@ -69,22 +72,91 @@ const MAX_RETRIES = 40;
|
|
|
69
72
|
// Cap the per-retry backoff so replication-applied transactions, which retry conflicts without a
|
|
70
73
|
// cap (see the commit rejection handler), don't grow the delay unbounded.
|
|
71
74
|
const MAX_RETRY_DELAY_MS = 1000;
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
75
|
+
let oldestOutstandingCommit;
|
|
76
|
+
let newestOutstandingCommit;
|
|
77
|
+
let outstandingCommitCount = 0;
|
|
78
|
+
// Caps the stuck-commit log (checkOverloaded() below) to at most one line per this interval across
|
|
79
|
+
// the whole thread, regardless of how many distinct commits individually cross the threshold — see
|
|
80
|
+
// the comment at the log site for why a per-commit-only dedup isn't enough under sustained overload.
|
|
81
|
+
const OVERLOAD_LOG_MIN_INTERVAL_MS = 1000;
|
|
82
|
+
let lastOverloadLogAt = -Infinity;
|
|
83
|
+
// Track a submitted commit until it settles. Every attempt is tracked unconditionally: a
|
|
84
|
+
// coordinated retry round and a chained second-store commit are both issued from inside the
|
|
85
|
+
// preceding commit's own resolve handler, which runs before any reaction that could release a
|
|
86
|
+
// single shared slot — so anything conditional on "is something already outstanding" skips them
|
|
87
|
+
// and leaves a wedged retry or chain invisible to checkOverloaded() forever. Each attempt is timed
|
|
88
|
+
// from its own submission, keeping the overload window per-attempt rather than cumulative over a
|
|
89
|
+
// retry ladder. `.then(untrack, untrack)` also marks an ERR_BUSY rejection handled, so this
|
|
90
|
+
// tracking never surfaces as an unhandled rejection alongside the caller's own handler.
|
|
91
|
+
// Exported only so unit tests can drive the list with controllable promises: the unlink order that
|
|
92
|
+
// matters (a middle or tail node settling first) cannot be forced through real writes, and a node
|
|
93
|
+
// left linked would 503 every write on this thread forever. commit() below is the sole caller.
|
|
94
|
+
// Also the single source for write-queue-depth accounting (getTransactionQueueDepths below): every
|
|
95
|
+
// native commit this function tracks is, by definition, exactly the write-queue backlog — see the
|
|
96
|
+
// comment there for why that used to be a second, separately-maintained counter.
|
|
97
|
+
// `store`/`startedFrom`/`nativeTransaction` are the identity snapshot for checkOverloaded()'s stuck-
|
|
98
|
+
// commit log (harper#2001); omit them (as the test seam below does) when a caller has none to give.
|
|
99
|
+
function trackOutstandingCommit(commitResolution, store, startedFrom, nativeTransaction) {
|
|
100
|
+
// Guards against a future caller passing a non-Promise: today commit() always hands this a real
|
|
101
|
+
// Promise, but an unguarded link here would leave a node permanently wedged in the list (503ing
|
|
102
|
+
// every write on this thread) with no settlement to ever unlink it.
|
|
103
|
+
if (typeof commitResolution?.then !== 'function')
|
|
104
|
+
return;
|
|
105
|
+
const outstanding = {
|
|
106
|
+
start: performance.now(),
|
|
107
|
+
prev: newestOutstandingCommit,
|
|
108
|
+
next: undefined,
|
|
109
|
+
store,
|
|
110
|
+
startedFrom,
|
|
111
|
+
nativeTransaction,
|
|
112
|
+
logged: false,
|
|
113
|
+
};
|
|
114
|
+
if (newestOutstandingCommit != null)
|
|
115
|
+
newestOutstandingCommit.next = outstanding;
|
|
116
|
+
else
|
|
117
|
+
oldestOutstandingCommit = outstanding;
|
|
118
|
+
newestOutstandingCommit = outstanding;
|
|
119
|
+
outstandingCommitCount++;
|
|
120
|
+
// Doubles as the write-queue-depth high-water mark (see getTransactionQueueDepths): every
|
|
121
|
+
// outstanding commit is a write-queue entry, so the peak of one is the peak of the other.
|
|
122
|
+
if (outstandingCommitCount > writeTxnQueueDepthHighWater)
|
|
123
|
+
writeTxnQueueDepthHighWater = outstandingCommitCount;
|
|
124
|
+
// Guards against double-untracking the same node: `.then(untrack, untrack)` below means a promise
|
|
125
|
+
// that both resolves and later has its rejection handler independently triggered (or is tracked via
|
|
126
|
+
// a shared/misused resolution) could otherwise run the unlink twice, corrupting the list or driving
|
|
127
|
+
// outstandingCommitCount negative.
|
|
128
|
+
let untracked = false;
|
|
129
|
+
const untrack = () => {
|
|
130
|
+
if (untracked)
|
|
131
|
+
return;
|
|
132
|
+
untracked = true;
|
|
133
|
+
if (outstanding.prev != null)
|
|
134
|
+
outstanding.prev.next = outstanding.next;
|
|
135
|
+
else
|
|
136
|
+
oldestOutstandingCommit = outstanding.next;
|
|
137
|
+
if (outstanding.next != null)
|
|
138
|
+
outstanding.next.prev = outstanding.prev;
|
|
139
|
+
else
|
|
140
|
+
newestOutstandingCommit = outstanding.prev;
|
|
141
|
+
outstandingCommitCount--;
|
|
142
|
+
};
|
|
143
|
+
commitResolution.then(untrack, untrack);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How many write commits are outstanding on this thread and how long the oldest has been waiting
|
|
147
|
+
* (`oldestAgeMs` is undefined when none is). `oldestAgeMs` is exactly the value checkOverloaded()
|
|
148
|
+
* rejects on, exposed so a commit that never settles (harper#2001) can be observed directly rather
|
|
149
|
+
* than inferred from the 503s it eventually produces.
|
|
150
|
+
*/
|
|
151
|
+
function getOutstandingCommits() {
|
|
152
|
+
return {
|
|
153
|
+
count: outstandingCommitCount,
|
|
154
|
+
oldestAgeMs: oldestOutstandingCommit ? performance.now() - oldestOutstandingCommit.start : undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Once per process: committing under open read iterators forces a write replay, so the warning is
|
|
158
|
+
// about the caller's pattern, not the individual commit.
|
|
159
|
+
let replayedWritesWarned = false;
|
|
88
160
|
// The analytics module registers a recorder here at load (dependency inversion, mirroring
|
|
89
161
|
// `replicationConfirmation` below) so the storage layer doesn't statically import the analytics/server
|
|
90
162
|
// modules. Unset until analytics loads, and when analytics is disabled the recorder call is cheap.
|
|
@@ -117,29 +189,19 @@ function recordCommitLatency(commitResolution, submittedAt) {
|
|
|
117
189
|
}
|
|
118
190
|
// Queue-depth gauges surfaced through the analytics pipeline (write-transaction-queue-depth /
|
|
119
191
|
// read-transaction-queue-depth). Per-thread state; the analytics aggregator sums across threads.
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// (
|
|
124
|
-
//
|
|
192
|
+
// The write depth is `outstandingCommitCount` itself (maintained above by trackOutstandingCommit) —
|
|
193
|
+
// write commits handed to the storage engine but not yet resolved are exactly the same set of native
|
|
194
|
+
// commits the overload check tracks, and keeping one counter instead of two removes the duplicate
|
|
195
|
+
// per-commit bookkeeping (and the drift risk: a code path that updates one but not the other, as the
|
|
196
|
+
// replay path did before this fix). Read depth is derived from the live `trackedTxns` set (every
|
|
197
|
+
// tracked transaction holds an open read snapshot). We also retain a high-water mark per sampling
|
|
198
|
+
// window because the queue can fill and drain within a single (~1s) analytics period, so an
|
|
125
199
|
// instantaneous sample taken at emit time would routinely miss the spike operators need to see.
|
|
126
200
|
// RocksDB-write-path only: LMDB routes through the separate LMDBTransaction.commit()/getReadTxn()
|
|
127
201
|
// overrides (resources/LMDBTransaction.ts), which maintain their own unrelated `trackedTxns` set and
|
|
128
202
|
// do not call into this accounting.
|
|
129
|
-
let writeTxnQueueDepth = 0;
|
|
130
203
|
let writeTxnQueueDepthHighWater = 0;
|
|
131
204
|
let readTxnQueueDepthHighWater = 0;
|
|
132
|
-
function enterWriteQueue() {
|
|
133
|
-
if (++writeTxnQueueDepth > writeTxnQueueDepthHighWater)
|
|
134
|
-
writeTxnQueueDepthHighWater = writeTxnQueueDepth;
|
|
135
|
-
}
|
|
136
|
-
function leaveWriteQueue() {
|
|
137
|
-
// Floor at zero: accounting is balanced by construction (every enterWriteQueue has exactly one
|
|
138
|
-
// matching settlement), but the guard is cheap insurance against a future call-site imbalance
|
|
139
|
-
// producing a negative depth that would corrupt every subsequent sample.
|
|
140
|
-
if (writeTxnQueueDepth > 0)
|
|
141
|
-
writeTxnQueueDepth--;
|
|
142
|
-
}
|
|
143
205
|
/**
|
|
144
206
|
* Returns the current write/read transaction queue depths for this thread along with the high-water
|
|
145
207
|
* mark observed since the previous call, then resets the high-water marks to the current depth so the
|
|
@@ -150,12 +212,12 @@ function getTransactionQueueDepths() {
|
|
|
150
212
|
// dominates the current size here — no need to reconcile against `readDepth` before reporting.
|
|
151
213
|
const readDepth = trackedTxns.size;
|
|
152
214
|
const depths = {
|
|
153
|
-
writeDepth:
|
|
215
|
+
writeDepth: outstandingCommitCount,
|
|
154
216
|
writeMaxDepth: writeTxnQueueDepthHighWater,
|
|
155
217
|
readDepth,
|
|
156
218
|
readMaxDepth: readTxnQueueDepthHighWater,
|
|
157
219
|
};
|
|
158
|
-
writeTxnQueueDepthHighWater =
|
|
220
|
+
writeTxnQueueDepthHighWater = outstandingCommitCount;
|
|
159
221
|
readTxnQueueDepthHighWater = readDepth;
|
|
160
222
|
return depths;
|
|
161
223
|
}
|
|
@@ -227,7 +289,18 @@ class DatabaseTransaction {
|
|
|
227
289
|
replicatedConfirmation;
|
|
228
290
|
getReadTxn(disableSnapshot) {
|
|
229
291
|
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
230
|
-
|
|
292
|
+
// The limit is an IDLE limit. Writes always re-arm it (see addWrite), but reads only do so
|
|
293
|
+
// while no uncommitted writes are held: staged writes hold write intents that other writers'
|
|
294
|
+
// coordinated-retry commits park on, so a handler that wrote once and then only reads — an
|
|
295
|
+
// orphaned long-poll whose client had already gone, in harper#2001 — must not keep those
|
|
296
|
+
// intents alive by reading. A transaction that keeps writing stays alive; so does a purely
|
|
297
|
+
// read-only one. A committed transaction re-arms too: its intents went with the commit, and
|
|
298
|
+
// the monitor bounds its retained read snapshot separately.
|
|
299
|
+
// `writes`/`next` are checked inline first: this is a hot path and the dominant case is a
|
|
300
|
+
// single-store transaction that has never written.
|
|
301
|
+
if ((this.writes.length === 0 && !this.next) || this.open !== exports.TRANSACTION_STATE.OPEN || !this.hasPendingWrites()) {
|
|
302
|
+
this.timeout = txnExpiration;
|
|
303
|
+
}
|
|
231
304
|
if (this.transaction) {
|
|
232
305
|
if (this.transaction.openTimer)
|
|
233
306
|
this.transaction.openTimer = 0;
|
|
@@ -275,6 +348,7 @@ class DatabaseTransaction {
|
|
|
275
348
|
trackedTxns.delete(this);
|
|
276
349
|
this.transaction?.abort();
|
|
277
350
|
this.transaction = null;
|
|
351
|
+
this.completeDeferredContextRelease();
|
|
278
352
|
}
|
|
279
353
|
}
|
|
280
354
|
/**
|
|
@@ -293,6 +367,20 @@ class DatabaseTransaction {
|
|
|
293
367
|
harperLogger.debug?.('releasing timed-out read transaction', error);
|
|
294
368
|
}
|
|
295
369
|
this.transaction = null;
|
|
370
|
+
this.completeDeferredContextRelease();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Complete a context release that releaseContext() deferred because outstanding read iterators
|
|
374
|
+
* were still using this transaction (see releaseContext()) — called once the last one drains,
|
|
375
|
+
* whether that happens naturally (doneReadTxn()) or is forced by the long-transaction monitor
|
|
376
|
+
* (releaseReadTxn()).
|
|
377
|
+
*/
|
|
378
|
+
completeDeferredContextRelease() {
|
|
379
|
+
if (!this.pendingContextRelease)
|
|
380
|
+
return;
|
|
381
|
+
this.pendingContextRelease = false;
|
|
382
|
+
if (this.#context?.transaction === this)
|
|
383
|
+
this.#context.transaction = null;
|
|
296
384
|
}
|
|
297
385
|
disregardReadTxn() {
|
|
298
386
|
if (--this.readTxnRefCount === 0 && this.readTxnsUsed === 1) {
|
|
@@ -324,22 +412,69 @@ class DatabaseTransaction {
|
|
|
324
412
|
this.writes = [];
|
|
325
413
|
this.writesByKey = undefined;
|
|
326
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Drop this transaction's back-reference from its context once completed (commit or abort),
|
|
417
|
+
* so a long-lived context (e.g. an MQTT subscription context held open for the life of a
|
|
418
|
+
* suspended delivery loop) doesn't keep pinning a finished transaction in memory. Guarded by
|
|
419
|
+
* identity: a context already re-pointed at a different (e.g. reused) transaction is untouched.
|
|
420
|
+
*
|
|
421
|
+
* `final` must be false for an in-callback explicit `context.transaction.commit()` — the
|
|
422
|
+
* "commit in the middle" pattern intentionally keeps recommitting and adding writes to the
|
|
423
|
+
* SAME instance (see the comment above about a transaction being "reused and committed
|
|
424
|
+
* again"), so releasing here would strand those later writes with no transaction to join.
|
|
425
|
+
* Only resources/transaction.ts's own wrapper commit (`{ doneWriting: true }`, once the
|
|
426
|
+
* caller's callback has fully returned) and abort() are truly final.
|
|
427
|
+
*
|
|
428
|
+
* A final commit can still have outstanding read iterators streaming through this.transaction
|
|
429
|
+
* (see the outstanding-iterators branch in commit()) — those keep this instance meaningfully
|
|
430
|
+
* alive (a fresh write on the same context must not join a DIFFERENT, already-replayed
|
|
431
|
+
* transaction) until doneReadTxn() drains the last one, so the release is deferred to there.
|
|
432
|
+
*
|
|
433
|
+
* Sets `.transaction` to `null` rather than deleting the property: `Context.transaction` is
|
|
434
|
+
* typed `DatabaseTransaction | null | undefined` precisely to document this released state, and
|
|
435
|
+
* `delete` would repeatedly force a long-lived, hot context (e.g. an MQTT subscription context
|
|
436
|
+
* releasing/reattaching a transaction per message) into V8's slower dictionary-mode property
|
|
437
|
+
* storage.
|
|
438
|
+
*/
|
|
439
|
+
releaseContext(final) {
|
|
440
|
+
if (!final)
|
|
441
|
+
return;
|
|
442
|
+
if (this.readTxnsUsed > 0) {
|
|
443
|
+
this.pendingContextRelease = true;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (this.#context?.transaction === this)
|
|
447
|
+
this.#context.transaction = null;
|
|
448
|
+
}
|
|
327
449
|
checkOverloaded() {
|
|
328
|
-
if (
|
|
450
|
+
if (oldestOutstandingCommit &&
|
|
329
451
|
!this.overloadChecked &&
|
|
330
|
-
performance.now() -
|
|
331
|
-
|
|
452
|
+
performance.now() - oldestOutstandingCommit.start > MAX_OUTSTANDING_TXN_DURATION) {
|
|
453
|
+
const now = performance.now();
|
|
454
|
+
// Also rate-limited across the whole overload episode (not just deduped per commit): under
|
|
455
|
+
// sustained heavy load, many distinct commits can each individually age past the limit in
|
|
456
|
+
// quick succession as earlier ones finally settle, which without this cap would turn one
|
|
457
|
+
// overload episode into a growing stream of ERROR lines — the same "flood" harper#2001's
|
|
458
|
+
// original per-request log was fixed to avoid, just shifted from per-request to per-commit.
|
|
459
|
+
// A commit skipped by the cooldown is NOT marked `logged`, so it still gets a log later if
|
|
460
|
+
// it's still the oldest once the cooldown clears, rather than going silent forever.
|
|
461
|
+
if (!oldestOutstandingCommit.logged && now - lastOverloadLogAt > OVERLOAD_LOG_MIN_INTERVAL_MS) {
|
|
332
462
|
// Log once per stuck commit (not once per rejected request, harper#2001): a wedged
|
|
333
463
|
// thread otherwise logs nothing at all server-side while rejecting every write with a
|
|
334
|
-
// 503, which was the single biggest obstacle to root-causing a recurrence.
|
|
335
|
-
|
|
336
|
-
|
|
464
|
+
// 503, which was the single biggest obstacle to root-causing a recurrence. The flag lives
|
|
465
|
+
// on the node itself, so if THIS commit settles while still over the limit and a
|
|
466
|
+
// different one is now oldest, that one logs too instead of staying silent forever.
|
|
467
|
+
oldestOutstandingCommit.logged = true;
|
|
468
|
+
lastOverloadLogAt = now;
|
|
469
|
+
const nativeTransactionId = oldestOutstandingCommit.nativeTransaction?.id;
|
|
470
|
+
const store = oldestOutstandingCommit.store;
|
|
471
|
+
const startedFrom = oldestOutstandingCommit.startedFrom;
|
|
337
472
|
harperLogger.error(`Rejecting writes on this thread: a commit has been outstanding for ` +
|
|
338
|
-
`${Math.round(
|
|
339
|
-
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${
|
|
473
|
+
`${Math.round(now - oldestOutstandingCommit.start)}ms (exceeds the ` +
|
|
474
|
+
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${store?.rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
|
|
340
475
|
(nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
|
|
341
|
-
(
|
|
342
|
-
? `, started from ${
|
|
476
|
+
(startedFrom?.resourceName
|
|
477
|
+
? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
|
|
343
478
|
: '') +
|
|
344
479
|
`. Further record updates and publishes from new application requests on this thread ` +
|
|
345
480
|
`will be rejected with 503 until the commit settles or the process is restarted (deletes, ` +
|
|
@@ -352,6 +487,13 @@ class DatabaseTransaction {
|
|
|
352
487
|
addWrite(operation) {
|
|
353
488
|
if (this.timedOut)
|
|
354
489
|
throw transactionOpenTooLongError();
|
|
490
|
+
// A write is activity: it re-arms the idle limit on this link even though the reads it
|
|
491
|
+
// performs no longer do (see getReadTxn), so a transaction that keeps writing stays alive
|
|
492
|
+
// and only an idle one holding write intents is reaped.
|
|
493
|
+
this.timeout = txnExpiration;
|
|
494
|
+
// Independent write-recency signal for chainStillActive (see the field comment) — reads never
|
|
495
|
+
// touch this, only writes do.
|
|
496
|
+
this.writeTimeout = txnExpiration;
|
|
355
497
|
this.linkWrite(operation);
|
|
356
498
|
this.writes.push(operation);
|
|
357
499
|
if (!operation.deferSave) {
|
|
@@ -481,6 +623,12 @@ class DatabaseTransaction {
|
|
|
481
623
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
482
624
|
if (this.writes.length > 0) {
|
|
483
625
|
if (!options.transaction) {
|
|
626
|
+
if (!replayedWritesWarned) {
|
|
627
|
+
replayedWritesWarned = true;
|
|
628
|
+
harperLogger.warn?.(`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` +
|
|
629
|
+
(this.startedFrom ? `, from ${this.startedFrom.resourceName}.${this.startedFrom.method}` : '') +
|
|
630
|
+
`. Fully consume (or close) iterators before committing to avoid this. Logged once per process.`);
|
|
631
|
+
}
|
|
484
632
|
// Deliberately NOT marked isRetry and NOT carrying over the original's onCommit:
|
|
485
633
|
// audit/txn-log entries batch natively on the transaction they were staged into and
|
|
486
634
|
// are only durably written by that transaction's commit attempt (an abort discards
|
|
@@ -501,6 +649,29 @@ class DatabaseTransaction {
|
|
|
501
649
|
// re-staged the writes into it
|
|
502
650
|
commitResolution = transaction.commit();
|
|
503
651
|
recordCommitLatency(commitResolution, performance.now());
|
|
652
|
+
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
653
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
|
|
654
|
+
// dedicated accounting here (as a prior version of this replay path did) used to leave
|
|
655
|
+
// write-transaction-queue-depth, the one metric that can observe a commit that never
|
|
656
|
+
// settles (harper#2001), reading zero for exactly this path.
|
|
657
|
+
}
|
|
658
|
+
// No commit will ever run on the retained handle — the replay above owns these
|
|
659
|
+
// writes — so this is the only place its write intents can be released. Left in
|
|
660
|
+
// place, other writers' coordinated-retry commits park on them until the last
|
|
661
|
+
// iterator finishes (harper#2001). Reads through the handle, including
|
|
662
|
+
// read-your-own-writes, keep working. Once only: a coordinated-retry or backoff
|
|
663
|
+
// round re-enters this branch on the same retained handle. Fenced like the other
|
|
664
|
+
// post-submit steps here: the replay commit is already in flight, so a throw must
|
|
665
|
+
// not skip onCommit/the chain-store commit below. Optional: rocksdb-js < 2.7
|
|
666
|
+
// lacks the method.
|
|
667
|
+
if (!this.writesAbandoned) {
|
|
668
|
+
this.writesAbandoned = true;
|
|
669
|
+
try {
|
|
670
|
+
this.transaction?.abandonWrites?.();
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
harperLogger.warn?.('Failed to release write intents on a retained read transaction', error);
|
|
674
|
+
}
|
|
504
675
|
}
|
|
505
676
|
}
|
|
506
677
|
else {
|
|
@@ -516,31 +687,19 @@ class DatabaseTransaction {
|
|
|
516
687
|
// Promise<number | void>; it is handled in the resolve callback below.
|
|
517
688
|
commitResolution = transaction.commit();
|
|
518
689
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
519
|
-
// metric. This is the same clock the overload check uses (
|
|
520
|
-
//
|
|
690
|
+
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
691
|
+
// stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
|
|
521
692
|
// "Outstanding write transactions have too long of queue" (503) rejection. A transient-
|
|
522
|
-
// conflict retry rejects this promise and issues a fresh commit(),
|
|
523
|
-
//
|
|
524
|
-
// retries<=2 paths re-commit synchronously while outstandingCommit is still set, so
|
|
525
|
-
// recording per attempt here counts more attempts than the overload check ever arms for.
|
|
693
|
+
// conflict retry rejects this promise and issues a fresh commit(), which is tracked as
|
|
694
|
+
// its own attempt, so recording per attempt matches the overload semantics.
|
|
526
695
|
// commitResolution's declared type (Promise<number | void> | void) doesn't narrow to
|
|
527
696
|
// Promise<void> here because the widening union defeats flow analysis on the prior
|
|
528
697
|
// cast assignment; re-assert it — this branch's commit() result is always a Promise.
|
|
529
698
|
recordCommitLatency(commitResolution, performance.now());
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
// The thenable guard protects against a future caller passing a non-Promise
|
|
535
|
-
// `commitResolution` (today it is always rocksdb-js's async Transaction.commit()
|
|
536
|
-
// result, guaranteed to be a Promise).
|
|
537
|
-
enterWriteQueue();
|
|
538
|
-
if (commitResolution && typeof commitResolution.then === 'function') {
|
|
539
|
-
commitResolution.then(leaveWriteQueue, leaveWriteQueue);
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
leaveWriteQueue();
|
|
543
|
-
}
|
|
699
|
+
// Write-queue-depth accounting for this commit happens uniformly below, via
|
|
700
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. A
|
|
701
|
+
// transient-conflict retry rejects this promise and issues a fresh commit()
|
|
702
|
+
// (re-entering here), which trackOutstandingCommit tracks as its own attempt.
|
|
544
703
|
}
|
|
545
704
|
else {
|
|
546
705
|
try {
|
|
@@ -556,43 +715,10 @@ class DatabaseTransaction {
|
|
|
556
715
|
}
|
|
557
716
|
}
|
|
558
717
|
if (commitResolution) {
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
// — so a retry or a second store's commit that itself wedges is never armed, and
|
|
564
|
-
// neither checkOverloaded() nor this log will ever see it. Only the first store's
|
|
565
|
-
// first commit attempt is covered.
|
|
566
|
-
if (!outstandingCommit) {
|
|
567
|
-
outstandingCommit = commitResolution;
|
|
568
|
-
outstandingCommitStart = performance.now();
|
|
569
|
-
// Snapshot the store/origin now rather than reading them off `this` lazily in
|
|
570
|
-
// checkOverloaded(): `this` (a DatabaseTransaction) can be reused for a later immediate
|
|
571
|
-
// commit while this native commit is still wedged, and that later commit's resolve
|
|
572
|
-
// handler runs clearWrites() on this SAME object — a lazy read would then blank or
|
|
573
|
-
// mis-attribute the identity out from under the deferred log. Read the table off the
|
|
574
|
-
// write itself, not this.db, which is whichever table first claimed this per-database
|
|
575
|
-
// transaction in txnForContext and so can name the wrong table when a transaction spans
|
|
576
|
-
// more than one table in the same database. Both are stable references (a Store, and an
|
|
577
|
-
// object set once and never mutated), so this holds no allocation and no lingering
|
|
578
|
-
// reference to the transaction/writes/entries graph.
|
|
579
|
-
outstandingCommitStore = this.writes[0]?.store;
|
|
580
|
-
outstandingCommitStartedFrom = this.startedFrom;
|
|
581
|
-
outstandingCommitNativeTransaction = transaction;
|
|
582
|
-
outstandingCommitLogged = false;
|
|
583
|
-
outstandingCommit
|
|
584
|
-
// if `commitResolution` rejects with and `ERR_BUSY` error, the retry logic
|
|
585
|
-
// will correct course, but the reject will still be propagated on the
|
|
586
|
-
// `outstandingCommit` promise and needs to be caught and silenced
|
|
587
|
-
.catch(() => { })
|
|
588
|
-
.finally(() => {
|
|
589
|
-
outstandingCommit = null;
|
|
590
|
-
outstandingCommitStore = undefined;
|
|
591
|
-
outstandingCommitStartedFrom = undefined;
|
|
592
|
-
outstandingCommitNativeTransaction = undefined;
|
|
593
|
-
outstandingCommitLogged = false;
|
|
594
|
-
});
|
|
595
|
-
}
|
|
718
|
+
// Read the table off the write itself, not this.db, which is whichever table first
|
|
719
|
+
// claimed this per-database transaction in txnForContext and so can name the wrong
|
|
720
|
+
// table when a transaction spans more than one table in the same database.
|
|
721
|
+
trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
|
|
596
722
|
const completions = [];
|
|
597
723
|
return commitResolution.then((commitResult) => {
|
|
598
724
|
if (commitResult === RETRY_NOW_VALUE) {
|
|
@@ -656,8 +782,7 @@ class DatabaseTransaction {
|
|
|
656
782
|
// now reset transactions tracking; this transaction be reused and committed again
|
|
657
783
|
this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
|
|
658
784
|
this.clearWrites();
|
|
659
|
-
|
|
660
|
-
this.#context.resourceCache = null;
|
|
785
|
+
this.releaseContext(!!options.doneWriting);
|
|
661
786
|
this.next = null;
|
|
662
787
|
let txnTime = this.timestamp;
|
|
663
788
|
this.timestamp = 0; // reset the timestamp as well
|
|
@@ -734,6 +859,11 @@ class DatabaseTransaction {
|
|
|
734
859
|
catch (abortError) {
|
|
735
860
|
harperLogger.debug?.('aborting transaction after failed commit', abortError);
|
|
736
861
|
}
|
|
862
|
+
// A terminal failure is just as final as a success — release the context's
|
|
863
|
+
// back-reference here too, or transaction.ts's onComplete() (which has no
|
|
864
|
+
// rejection handler of its own) would leave a long-lived context pinning this
|
|
865
|
+
// CLOSED wrapper forever.
|
|
866
|
+
this.releaseContext(!!options.doneWriting);
|
|
737
867
|
throw error;
|
|
738
868
|
}
|
|
739
869
|
});
|
|
@@ -743,8 +873,7 @@ class DatabaseTransaction {
|
|
|
743
873
|
(0, blob_ts_1.cleanupUnusedBlobs)(write.savedBlobs, (0, blob_ts_1.collectRetainedFileIds)(write.store.getEntry(write.key)?.value));
|
|
744
874
|
}
|
|
745
875
|
this.clearWrites();
|
|
746
|
-
|
|
747
|
-
this.#context.resourceCache = null;
|
|
876
|
+
this.releaseContext(!!options.doneWriting);
|
|
748
877
|
const txnResolution = {
|
|
749
878
|
txnTime: this.timestamp,
|
|
750
879
|
};
|
|
@@ -776,8 +905,13 @@ class DatabaseTransaction {
|
|
|
776
905
|
}
|
|
777
906
|
// reset the transaction
|
|
778
907
|
this.clearWrites();
|
|
779
|
-
|
|
780
|
-
|
|
908
|
+
// A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
|
|
909
|
+
// Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
|
|
910
|
+
// starting a fresh one) so the rest of the logical operation fails atomically via the
|
|
911
|
+
// poison check in addWrite()/commit(), rather than silently landing a later write on a
|
|
912
|
+
// brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
|
|
913
|
+
// make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
|
|
914
|
+
this.releaseContext(!this.timedOut);
|
|
781
915
|
}
|
|
782
916
|
/**
|
|
783
917
|
* Give up on a chain of linked transactions after exhausting conflict retries: poison every link
|
|
@@ -899,9 +1033,33 @@ class ImmediateTransaction extends DatabaseTransaction {
|
|
|
899
1033
|
}
|
|
900
1034
|
exports.ImmediateTransaction = ImmediateTransaction;
|
|
901
1035
|
let timer;
|
|
1036
|
+
/**
|
|
1037
|
+
* True when a link other than `txn` in the same multi-store chain was written recently enough to
|
|
1038
|
+
* still be active — i.e. its `writeTimeout` (set only by addWrite, see the field comment) hasn't
|
|
1039
|
+
* decayed to zero. Writes re-arm only the link that receives them, so a chain writing database B
|
|
1040
|
+
* while its head only reads A would otherwise be aborted by the head's own decay.
|
|
1041
|
+
*/
|
|
1042
|
+
function chainStillActive(txn) {
|
|
1043
|
+
for (let link = txn.next; link; link = link.next) {
|
|
1044
|
+
// A write-only link (e.g. a blind write to a second database, never itself read) never calls
|
|
1045
|
+
// getReadTxn, so it's never added to trackedTxns and the main loop below never decays it.
|
|
1046
|
+
// Decay it here instead, so an idle write-only link eventually expires rather than keeping the
|
|
1047
|
+
// whole chain immortal (harper#2001's blind-write shape).
|
|
1048
|
+
if (!trackedTxns.has(link) && link.writeTimeout > 0)
|
|
1049
|
+
link.writeTimeout -= txnExpiration;
|
|
1050
|
+
if (link.writeTimeout > 0)
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
902
1055
|
function startMonitoringTxns() {
|
|
903
1056
|
timer = setInterval(function () {
|
|
904
1057
|
for (const txn of trackedTxns) {
|
|
1058
|
+
// Decay write recency once per tick for every tracked link, independent of the `timeout`
|
|
1059
|
+
// branches below — a tracked link that keeps its own idle limit alive by reading must not
|
|
1060
|
+
// thereby keep chainStillActive believing it was written recently too.
|
|
1061
|
+
if (txn.writeTimeout > 0)
|
|
1062
|
+
txn.writeTimeout -= txnExpiration;
|
|
905
1063
|
if (txn.timeout <= 0) {
|
|
906
1064
|
const url = txn.getContext()?.url;
|
|
907
1065
|
if (txn.open === exports.TRANSACTION_STATE.CLOSED) {
|
|
@@ -914,6 +1072,13 @@ function startMonitoringTxns() {
|
|
|
914
1072
|
harperLogger.warn?.(`Read iterators held a committed transaction's snapshot past the open-transaction limit; releasing it, from table: ${txn.db?.name + (url ? ' path: ' + url : '')}`);
|
|
915
1073
|
txn.releaseReadTxn();
|
|
916
1074
|
}
|
|
1075
|
+
else if (txn.hasPendingWrites() && chainStillActive(txn)) {
|
|
1076
|
+
// A later link in the chain was written recently (writes re-arm only the link that
|
|
1077
|
+
// receives them, and a multi-store transaction can be writing database B while this
|
|
1078
|
+
// head only reads A). The logical transaction is still active, so re-arm this link
|
|
1079
|
+
// rather than aborting the whole chain out from under it.
|
|
1080
|
+
txn.timeout = txnExpiration;
|
|
1081
|
+
}
|
|
917
1082
|
else if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
|
|
918
1083
|
// Abort and surface an error rather than force-committing a partial write set: silently
|
|
919
1084
|
// committing on the application's behalf breaks atomicity and can leave orphaned
|
|
@@ -956,6 +1121,14 @@ function startMonitoringTxns() {
|
|
|
956
1121
|
}, txnExpiration).unref();
|
|
957
1122
|
}
|
|
958
1123
|
startMonitoringTxns();
|
|
1124
|
+
/**
|
|
1125
|
+
* Test seam: re-arms the once-per-process replay warning. The whole unit suite shares one process,
|
|
1126
|
+
* so whichever test first drives a commit under open iterators consumes the warning for every test
|
|
1127
|
+
* after it.
|
|
1128
|
+
*/
|
|
1129
|
+
function resetReplayedWritesWarning() {
|
|
1130
|
+
replayedWritesWarned = false;
|
|
1131
|
+
}
|
|
959
1132
|
function setTxnExpiration(ms) {
|
|
960
1133
|
clearInterval(timer);
|
|
961
1134
|
txnExpiration = ms;
|