@harperfast/harper 5.2.0 → 5.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/copyDb.ts +40 -11
- package/bin/harper.ts +20 -51
- 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 +68 -24
- package/components/componentLoader.ts +70 -16
- package/components/deployLifecycle.ts +119 -33
- package/components/deploymentRecorder.ts +102 -11
- package/components/mcp/tools/operations.ts +6 -0
- package/components/operations.js +8 -6
- package/config/configUtils.ts +78 -20
- package/config/harperConfigEnvVars.ts +114 -17
- package/config-root.schema.json +10 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/copyDb.js +33 -9
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +6 -0
- package/dist/bin/harper.js +18 -50
- 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 +1 -0
- package/dist/components/Scope.js +69 -22
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +69 -10
- 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/deploymentRecorder.d.ts +6 -0
- package/dist/components/deploymentRecorder.js +94 -12
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +6 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/operations.js +8 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +78 -18
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.js +108 -16
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +57 -0
- package/dist/resources/DatabaseTransaction.js +284 -104
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -2
- 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 +8 -6
- package/dist/resources/Table.js +158 -31
- 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/auditStore.js +27 -1
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +0 -1
- package/dist/resources/blob.js +15 -7
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +22 -5
- package/dist/resources/databases.js +118 -9
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.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/search.d.ts +1 -1
- package/dist/resources/search.js +7 -3
- package/dist/resources/search.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/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.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 +19 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +10 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +57 -18
- 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/manageThreads.js +8 -0
- package/dist/server/threads/manageThreads.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/sqlEngine/parser/normalizer.js +37 -9
- package/dist/sqlEngine/parser/normalizer.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/environment/environmentManager.d.ts +11 -0
- package/dist/utility/environment/environmentManager.js +154 -0
- package/dist/utility/environment/environmentManager.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 +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- 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/lmdb/environmentUtility.js +2 -1
- package/dist/utility/lmdb/environmentUtility.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +7 -0
- package/dist/utility/logging/harper_logger.js +57 -24
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/npm-shrinkwrap.json +208 -198
- package/package.json +10 -8
- package/resources/DESIGN.md +1 -0
- package/resources/DatabaseTransaction.ts +313 -102
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +398 -259
- package/resources/analytics/write.ts +22 -20
- package/resources/auditStore.ts +32 -1
- package/resources/blob.ts +15 -8
- package/resources/databases.ts +123 -12
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
- 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/search.ts +8 -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 +45 -34
- package/server/http.ts +100 -13
- 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 +20 -3
- package/server/serverHelpers/serverUtilities.ts +10 -7
- package/server/static.ts +75 -20
- package/server/storageReclamation.ts +104 -8
- package/server/threads/manageThreads.js +8 -0
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +2 -0
- package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
- package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
- package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
- package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
- package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
- package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
- package/studio/web/assets/index-CDeqVx71.css +1 -0
- package/studio/web/assets/index-Sevt3MVx.js +819 -0
- package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
- package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
- package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
- package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
- package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
- package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
- package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
- package/studio/web/assets/notifications-vehqyOLM.js +2 -0
- package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
- package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
- package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
- package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
- package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
- package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
- package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
- package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
- package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
- package/studio/web/assets/register-BDy01-q3.js +3 -0
- package/studio/web/assets/register-BDy01-q3.js.map +1 -0
- package/studio/web/assets/register-CQHRwNP4.js +3 -0
- package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
- package/studio/web/assets/setup-I15FAF9H.js +3 -0
- package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
- package/studio/web/assets/status-BF1eJshJ.js +62 -0
- package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
- package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
- package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
- package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
- package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
- package/studio/web/index.html +6 -7
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/environment/environmentManager.ts +152 -0
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/install/installer.ts +37 -0
- package/utility/lmdb/environmentUtility.ts +2 -1
- package/utility/logging/harper_logger.ts +57 -26
- package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
- package/studio/web/assets/index-Bh_CNAHr.js +0 -809
- package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
- package/studio/web/assets/index-C8Mv9kHV.css +0 -1
- package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
- package/studio/web/assets/notifications-CwKhipK7.js +0 -2
- package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
- package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
- package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
- package/studio/web/assets/setup-B56Oz1_u.js +0 -3
- package/studio/web/assets/status-BAod7p3o.js +0 -62
- package/studio/web/assets/status-BAod7p3o.js.map +0 -1
- package/studio/web/assets/textarea-DhLMZ3PA.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,13 @@ 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;
|
|
338
|
+
timeoutBudget = 0;
|
|
251
339
|
validated = 0;
|
|
252
340
|
timestamp = 0;
|
|
253
341
|
retries = 0;
|
|
@@ -256,6 +344,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
256
344
|
// Whether this read handle's base reference (readTxnsUsed starts at 1 in getReadTxn) has been
|
|
257
345
|
// consumed by a commit round; iterator references are consumed only by doneReadTxn().
|
|
258
346
|
declare baseReadRefConsumed?: boolean;
|
|
347
|
+
// Set when a final commit/abort wanted to release the context's back-reference (see
|
|
348
|
+
// releaseContext()) but outstanding read iterators were still using this transaction —
|
|
349
|
+
// doneReadTxn() completes the release once the last iterator drains.
|
|
350
|
+
declare pendingContextRelease?: boolean;
|
|
259
351
|
declare startedFrom?: {
|
|
260
352
|
resourceName: string;
|
|
261
353
|
method: string;
|
|
@@ -278,10 +370,24 @@ export class DatabaseTransaction implements Transaction {
|
|
|
278
370
|
// open-transaction limit. Once poisoned, any further addWrite/commit throws transactionOpenTooLongError
|
|
279
371
|
// so the request rolls back cleanly instead of silently committing a partial write set (issue #1407).
|
|
280
372
|
declare timedOut?: boolean;
|
|
373
|
+
// Set once the retained read handle's write intents have been released (see commit()'s
|
|
374
|
+
// outstanding-iterators branch), so a retry round cannot re-fire the release.
|
|
375
|
+
declare writesAbandoned?: boolean;
|
|
281
376
|
|
|
282
377
|
getReadTxn(disableSnapshot?: boolean): ReadTransaction {
|
|
283
378
|
this.readTxnRefCount = (this.readTxnRefCount || 0) + 1;
|
|
284
|
-
|
|
379
|
+
// The limit is an IDLE limit. Writes always re-arm it (see addWrite), but reads only do so
|
|
380
|
+
// while no uncommitted writes are held: staged writes hold write intents that other writers'
|
|
381
|
+
// coordinated-retry commits park on, so a handler that wrote once and then only reads — an
|
|
382
|
+
// orphaned long-poll whose client had already gone, in harper#2001 — must not keep those
|
|
383
|
+
// intents alive by reading. A transaction that keeps writing stays alive; so does a purely
|
|
384
|
+
// read-only one. A committed transaction re-arms too: its intents went with the commit, and
|
|
385
|
+
// the monitor bounds its retained read snapshot separately.
|
|
386
|
+
// `writes`/`next` are checked inline first: this is a hot path and the dominant case is a
|
|
387
|
+
// single-store transaction that has never written.
|
|
388
|
+
if ((this.writes.length === 0 && !this.next) || this.open !== TRANSACTION_STATE.OPEN || !this.hasPendingWrites()) {
|
|
389
|
+
this.timeout = Math.max(txnExpiration, this.timeoutBudget);
|
|
390
|
+
}
|
|
285
391
|
if (this.transaction) {
|
|
286
392
|
if ((this.transaction as any).openTimer) (this.transaction as any).openTimer = 0;
|
|
287
393
|
return this.transaction;
|
|
@@ -328,6 +434,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
328
434
|
trackedTxns.delete(this);
|
|
329
435
|
this.transaction?.abort();
|
|
330
436
|
this.transaction = null;
|
|
437
|
+
this.completeDeferredContextRelease();
|
|
331
438
|
}
|
|
332
439
|
}
|
|
333
440
|
|
|
@@ -346,6 +453,19 @@ export class DatabaseTransaction implements Transaction {
|
|
|
346
453
|
harperLogger.debug?.('releasing timed-out read transaction', error);
|
|
347
454
|
}
|
|
348
455
|
this.transaction = null;
|
|
456
|
+
this.completeDeferredContextRelease();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Complete a context release that releaseContext() deferred because outstanding read iterators
|
|
461
|
+
* were still using this transaction (see releaseContext()) — called once the last one drains,
|
|
462
|
+
* whether that happens naturally (doneReadTxn()) or is forced by the long-transaction monitor
|
|
463
|
+
* (releaseReadTxn()).
|
|
464
|
+
*/
|
|
465
|
+
private completeDeferredContextRelease(): void {
|
|
466
|
+
if (!this.pendingContextRelease) return;
|
|
467
|
+
this.pendingContextRelease = false;
|
|
468
|
+
if (this.#context?.transaction === this) this.#context.transaction = null;
|
|
349
469
|
}
|
|
350
470
|
|
|
351
471
|
disregardReadTxn(): void {
|
|
@@ -378,25 +498,71 @@ export class DatabaseTransaction implements Transaction {
|
|
|
378
498
|
this.writesByKey = undefined;
|
|
379
499
|
}
|
|
380
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Drop this transaction's back-reference from its context once completed (commit or abort),
|
|
503
|
+
* so a long-lived context (e.g. an MQTT subscription context held open for the life of a
|
|
504
|
+
* suspended delivery loop) doesn't keep pinning a finished transaction in memory. Guarded by
|
|
505
|
+
* identity: a context already re-pointed at a different (e.g. reused) transaction is untouched.
|
|
506
|
+
*
|
|
507
|
+
* `final` must be false for an in-callback explicit `context.transaction.commit()` — the
|
|
508
|
+
* "commit in the middle" pattern intentionally keeps recommitting and adding writes to the
|
|
509
|
+
* SAME instance (see the comment above about a transaction being "reused and committed
|
|
510
|
+
* again"), so releasing here would strand those later writes with no transaction to join.
|
|
511
|
+
* Only resources/transaction.ts's own wrapper commit (`{ doneWriting: true }`, once the
|
|
512
|
+
* caller's callback has fully returned) and abort() are truly final.
|
|
513
|
+
*
|
|
514
|
+
* A final commit can still have outstanding read iterators streaming through this.transaction
|
|
515
|
+
* (see the outstanding-iterators branch in commit()) — those keep this instance meaningfully
|
|
516
|
+
* alive (a fresh write on the same context must not join a DIFFERENT, already-replayed
|
|
517
|
+
* transaction) until doneReadTxn() drains the last one, so the release is deferred to there.
|
|
518
|
+
*
|
|
519
|
+
* Sets `.transaction` to `null` rather than deleting the property: `Context.transaction` is
|
|
520
|
+
* typed `DatabaseTransaction | null | undefined` precisely to document this released state, and
|
|
521
|
+
* `delete` would repeatedly force a long-lived, hot context (e.g. an MQTT subscription context
|
|
522
|
+
* releasing/reattaching a transaction per message) into V8's slower dictionary-mode property
|
|
523
|
+
* storage.
|
|
524
|
+
*/
|
|
525
|
+
private releaseContext(final: boolean): void {
|
|
526
|
+
if (!final) return;
|
|
527
|
+
if (this.readTxnsUsed > 0) {
|
|
528
|
+
this.pendingContextRelease = true;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (this.#context?.transaction === this) this.#context.transaction = null;
|
|
532
|
+
}
|
|
533
|
+
|
|
381
534
|
checkOverloaded() {
|
|
382
535
|
if (
|
|
383
|
-
|
|
536
|
+
oldestOutstandingCommit &&
|
|
384
537
|
!this.overloadChecked &&
|
|
385
|
-
performance.now() -
|
|
538
|
+
performance.now() - oldestOutstandingCommit.start > MAX_OUTSTANDING_TXN_DURATION
|
|
386
539
|
) {
|
|
387
|
-
|
|
540
|
+
const now = performance.now();
|
|
541
|
+
// Also rate-limited across the whole overload episode (not just deduped per commit): under
|
|
542
|
+
// sustained heavy load, many distinct commits can each individually age past the limit in
|
|
543
|
+
// quick succession as earlier ones finally settle, which without this cap would turn one
|
|
544
|
+
// overload episode into a growing stream of ERROR lines — the same "flood" harper#2001's
|
|
545
|
+
// original per-request log was fixed to avoid, just shifted from per-request to per-commit.
|
|
546
|
+
// A commit skipped by the cooldown is NOT marked `logged`, so it still gets a log later if
|
|
547
|
+
// it's still the oldest once the cooldown clears, rather than going silent forever.
|
|
548
|
+
if (!oldestOutstandingCommit.logged && now - lastOverloadLogAt > OVERLOAD_LOG_MIN_INTERVAL_MS) {
|
|
388
549
|
// Log once per stuck commit (not once per rejected request, harper#2001): a wedged
|
|
389
550
|
// 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
|
-
|
|
551
|
+
// 503, which was the single biggest obstacle to root-causing a recurrence. The flag lives
|
|
552
|
+
// on the node itself, so if THIS commit settles while still over the limit and a
|
|
553
|
+
// different one is now oldest, that one logs too instead of staying silent forever.
|
|
554
|
+
oldestOutstandingCommit.logged = true;
|
|
555
|
+
lastOverloadLogAt = now;
|
|
556
|
+
const nativeTransactionId = oldestOutstandingCommit.nativeTransaction?.id;
|
|
557
|
+
const store = oldestOutstandingCommit.store;
|
|
558
|
+
const startedFrom = oldestOutstandingCommit.startedFrom;
|
|
393
559
|
harperLogger.error(
|
|
394
560
|
`Rejecting writes on this thread: a commit has been outstanding for ` +
|
|
395
|
-
`${Math.round(
|
|
396
|
-
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${
|
|
561
|
+
`${Math.round(now - oldestOutstandingCommit.start)}ms (exceeds the ` +
|
|
562
|
+
`${MAX_OUTSTANDING_TXN_DURATION}ms limit), from table: ${store?.rootStore?.databaseName ?? '?'}.${store?.name ?? '?'}` +
|
|
397
563
|
(nativeTransactionId !== undefined ? ` (transaction ${nativeTransactionId})` : '') +
|
|
398
|
-
(
|
|
399
|
-
? `, started from ${
|
|
564
|
+
(startedFrom?.resourceName
|
|
565
|
+
? `, started from ${startedFrom.resourceName}${startedFrom.method ? '.' + startedFrom.method : ''}`
|
|
400
566
|
: '') +
|
|
401
567
|
`. Further record updates and publishes from new application requests on this thread ` +
|
|
402
568
|
`will be rejected with 503 until the commit settles or the process is restarted (deletes, ` +
|
|
@@ -410,6 +576,13 @@ export class DatabaseTransaction implements Transaction {
|
|
|
410
576
|
|
|
411
577
|
addWrite(operation: TransactionWrite) {
|
|
412
578
|
if (this.timedOut) throw transactionOpenTooLongError();
|
|
579
|
+
// A write is activity: it re-arms the idle limit on this link even though the reads it
|
|
580
|
+
// performs no longer do (see getReadTxn), so a transaction that keeps writing stays alive
|
|
581
|
+
// and only an idle one holding write intents is reaped.
|
|
582
|
+
this.timeout = Math.max(txnExpiration, this.timeoutBudget ?? 0);
|
|
583
|
+
// Independent write-recency signal for chainStillActive (see the field comment) — reads never
|
|
584
|
+
// touch this, only writes do.
|
|
585
|
+
this.writeTimeout = this.timeout;
|
|
413
586
|
this.linkWrite(operation);
|
|
414
587
|
this.writes.push(operation);
|
|
415
588
|
if (!operation.deferSave) {
|
|
@@ -535,6 +708,14 @@ export class DatabaseTransaction implements Transaction {
|
|
|
535
708
|
this.writes = this.writes.filter((write) => write); // filter out removed entries
|
|
536
709
|
if (this.writes.length > 0) {
|
|
537
710
|
if (!options.transaction) {
|
|
711
|
+
if (!replayedWritesWarned) {
|
|
712
|
+
replayedWritesWarned = true;
|
|
713
|
+
harperLogger.warn?.(
|
|
714
|
+
`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` +
|
|
715
|
+
(this.startedFrom ? `, from ${this.startedFrom.resourceName}.${this.startedFrom.method}` : '') +
|
|
716
|
+
`. Fully consume (or close) iterators before committing to avoid this. Logged once per process.`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
538
719
|
// Deliberately NOT marked isRetry and NOT carrying over the original's onCommit:
|
|
539
720
|
// audit/txn-log entries batch natively on the transaction they were staged into and
|
|
540
721
|
// are only durably written by that transaction's commit attempt (an abort discards
|
|
@@ -557,6 +738,28 @@ export class DatabaseTransaction implements Transaction {
|
|
|
557
738
|
// re-staged the writes into it
|
|
558
739
|
commitResolution = transaction.commit() as Promise<void>;
|
|
559
740
|
recordCommitLatency(commitResolution, performance.now());
|
|
741
|
+
// Write-queue-depth accounting for this replay commit happens uniformly below, via
|
|
742
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. Omitting
|
|
743
|
+
// dedicated accounting here (as a prior version of this replay path did) used to leave
|
|
744
|
+
// write-transaction-queue-depth, the one metric that can observe a commit that never
|
|
745
|
+
// settles (harper#2001), reading zero for exactly this path.
|
|
746
|
+
}
|
|
747
|
+
// No commit will ever run on the retained handle — the replay above owns these
|
|
748
|
+
// writes — so this is the only place its write intents can be released. Left in
|
|
749
|
+
// place, other writers' coordinated-retry commits park on them until the last
|
|
750
|
+
// iterator finishes (harper#2001). Reads through the handle, including
|
|
751
|
+
// read-your-own-writes, keep working. Once only: a coordinated-retry or backoff
|
|
752
|
+
// round re-enters this branch on the same retained handle. Fenced like the other
|
|
753
|
+
// post-submit steps here: the replay commit is already in flight, so a throw must
|
|
754
|
+
// not skip onCommit/the chain-store commit below. Optional: rocksdb-js < 2.7
|
|
755
|
+
// lacks the method.
|
|
756
|
+
if (!this.writesAbandoned) {
|
|
757
|
+
this.writesAbandoned = true;
|
|
758
|
+
try {
|
|
759
|
+
(this.transaction as { abandonWrites?: () => void } | null)?.abandonWrites?.();
|
|
760
|
+
} catch (error) {
|
|
761
|
+
harperLogger.warn?.('Failed to release write intents on a retained read transaction', error);
|
|
762
|
+
}
|
|
560
763
|
}
|
|
561
764
|
} else {
|
|
562
765
|
// no more reads need to be performed, just commit/abort based if there are any writes
|
|
@@ -571,30 +774,19 @@ export class DatabaseTransaction implements Transaction {
|
|
|
571
774
|
// Promise<number | void>; it is handled in the resolve callback below.
|
|
572
775
|
commitResolution = transaction.commit();
|
|
573
776
|
// Record how long this commit stays outstanding (submit → settle) as a distribution
|
|
574
|
-
// metric. This is the same clock the overload check uses (
|
|
575
|
-
//
|
|
777
|
+
// metric. This is the same clock the overload check uses (trackOutstandingCommit
|
|
778
|
+
// stamps each attempt at submit), so a rising p99/p999 is the leading indicator for the
|
|
576
779
|
// "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.
|
|
780
|
+
// conflict retry rejects this promise and issues a fresh commit(), which is tracked as
|
|
781
|
+
// its own attempt, so recording per attempt matches the overload semantics.
|
|
581
782
|
// commitResolution's declared type (Promise<number | void> | void) doesn't narrow to
|
|
582
783
|
// Promise<void> here because the widening union defeats flow analysis on the prior
|
|
583
784
|
// cast assignment; re-assert it — this branch's commit() result is always a Promise.
|
|
584
785
|
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
|
-
}
|
|
786
|
+
// Write-queue-depth accounting for this commit happens uniformly below, via
|
|
787
|
+
// trackOutstandingCommit(commitResolution) — see that function's comment. A
|
|
788
|
+
// transient-conflict retry rejects this promise and issues a fresh commit()
|
|
789
|
+
// (re-entering here), which trackOutstandingCommit tracks as its own attempt.
|
|
598
790
|
} else {
|
|
599
791
|
try {
|
|
600
792
|
commitResolution = transaction.abort();
|
|
@@ -609,43 +801,10 @@ export class DatabaseTransaction implements Transaction {
|
|
|
609
801
|
}
|
|
610
802
|
|
|
611
803
|
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
|
-
}
|
|
804
|
+
// Read the table off the write itself, not this.db, which is whichever table first
|
|
805
|
+
// claimed this per-database transaction in txnForContext and so can name the wrong
|
|
806
|
+
// table when a transaction spans more than one table in the same database.
|
|
807
|
+
trackOutstandingCommit(commitResolution, this.writes[0]?.store, this.startedFrom, transaction);
|
|
649
808
|
const completions = [];
|
|
650
809
|
return commitResolution.then(
|
|
651
810
|
(commitResult) => {
|
|
@@ -721,6 +880,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
721
880
|
// now reset transactions tracking; this transaction be reused and committed again
|
|
722
881
|
this.retries = 0; // reset per-native-transaction retry counter so a reused DatabaseTransaction's next batch starts fresh
|
|
723
882
|
this.clearWrites();
|
|
883
|
+
this.releaseContext(!!options.doneWriting);
|
|
724
884
|
this.next = null;
|
|
725
885
|
let txnTime = this.timestamp;
|
|
726
886
|
this.timestamp = 0; // reset the timestamp as well
|
|
@@ -802,6 +962,11 @@ export class DatabaseTransaction implements Transaction {
|
|
|
802
962
|
} catch (abortError) {
|
|
803
963
|
harperLogger.debug?.('aborting transaction after failed commit', abortError);
|
|
804
964
|
}
|
|
965
|
+
// A terminal failure is just as final as a success — release the context's
|
|
966
|
+
// back-reference here too, or transaction.ts's onComplete() (which has no
|
|
967
|
+
// rejection handler of its own) would leave a long-lived context pinning this
|
|
968
|
+
// CLOSED wrapper forever.
|
|
969
|
+
this.releaseContext(!!options.doneWriting);
|
|
805
970
|
throw error;
|
|
806
971
|
}
|
|
807
972
|
}
|
|
@@ -812,6 +977,7 @@ export class DatabaseTransaction implements Transaction {
|
|
|
812
977
|
cleanupUnusedBlobs(write.savedBlobs, collectRetainedFileIds(write.store.getEntry(write.key)?.value));
|
|
813
978
|
}
|
|
814
979
|
this.clearWrites();
|
|
980
|
+
this.releaseContext(!!options.doneWriting);
|
|
815
981
|
const txnResolution: CommitResolution = {
|
|
816
982
|
txnTime: this.timestamp,
|
|
817
983
|
};
|
|
@@ -846,6 +1012,13 @@ export class DatabaseTransaction implements Transaction {
|
|
|
846
1012
|
}
|
|
847
1013
|
// reset the transaction
|
|
848
1014
|
this.clearWrites();
|
|
1015
|
+
// A timeout-poisoned abort (abortDueToTimeout()) is the one abort that is NOT "reuse-free":
|
|
1016
|
+
// Resource.ts's dispatcher deliberately keeps joining a `timedOut` transaction (instead of
|
|
1017
|
+
// starting a fresh one) so the rest of the logical operation fails atomically via the
|
|
1018
|
+
// poison check in addWrite()/commit(), rather than silently landing a later write on a
|
|
1019
|
+
// brand-new transaction after an earlier one was rolled back (#1411). Releasing here would
|
|
1020
|
+
// make that check see `undefined?.timedOut` and take the "start fresh" branch instead.
|
|
1021
|
+
this.releaseContext(!this.timedOut);
|
|
849
1022
|
}
|
|
850
1023
|
/**
|
|
851
1024
|
* Give up on a chain of linked transactions after exhausting conflict retries: poison every link
|
|
@@ -935,6 +1108,7 @@ export interface CommitResolution {
|
|
|
935
1108
|
next?: CommitResolution;
|
|
936
1109
|
}
|
|
937
1110
|
export interface Transaction {
|
|
1111
|
+
timeoutBudget?: number;
|
|
938
1112
|
commit(options): MaybePromise<CommitResolution>;
|
|
939
1113
|
abort?(): any;
|
|
940
1114
|
}
|
|
@@ -973,9 +1147,31 @@ export class ImmediateTransaction extends DatabaseTransaction {
|
|
|
973
1147
|
|
|
974
1148
|
let timer;
|
|
975
1149
|
|
|
1150
|
+
/**
|
|
1151
|
+
* True when a link other than `txn` in the same multi-store chain was written recently enough to
|
|
1152
|
+
* still be active — i.e. its `writeTimeout` (set only by addWrite, see the field comment) hasn't
|
|
1153
|
+
* decayed to zero. Writes re-arm only the link that receives them, so a chain writing database B
|
|
1154
|
+
* while its head only reads A would otherwise be aborted by the head's own decay.
|
|
1155
|
+
*/
|
|
1156
|
+
function chainStillActive(txn: DatabaseTransaction): boolean {
|
|
1157
|
+
for (let link: DatabaseTransaction = txn.next; link; link = link.next) {
|
|
1158
|
+
// A write-only link (e.g. a blind write to a second database, never itself read) never calls
|
|
1159
|
+
// getReadTxn, so it's never added to trackedTxns and the main loop below never decays it.
|
|
1160
|
+
// Decay it here instead, so an idle write-only link eventually expires rather than keeping the
|
|
1161
|
+
// whole chain immortal (harper#2001's blind-write shape).
|
|
1162
|
+
if (!trackedTxns.has(link) && link.writeTimeout > 0) link.writeTimeout -= txnExpiration;
|
|
1163
|
+
if (link.writeTimeout > 0) return true;
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
976
1168
|
function startMonitoringTxns() {
|
|
977
1169
|
timer = setInterval(function () {
|
|
978
1170
|
for (const txn of trackedTxns) {
|
|
1171
|
+
// Decay write recency once per tick for every tracked link, independent of the `timeout`
|
|
1172
|
+
// branches below — a tracked link that keeps its own idle limit alive by reading must not
|
|
1173
|
+
// thereby keep chainStillActive believing it was written recently too.
|
|
1174
|
+
if (txn.writeTimeout > 0) txn.writeTimeout -= txnExpiration;
|
|
979
1175
|
if (txn.timeout <= 0) {
|
|
980
1176
|
const url = (txn.getContext() as any)?.url;
|
|
981
1177
|
if (txn.open === TRANSACTION_STATE.CLOSED) {
|
|
@@ -991,6 +1187,12 @@ function startMonitoringTxns() {
|
|
|
991
1187
|
}`
|
|
992
1188
|
);
|
|
993
1189
|
txn.releaseReadTxn();
|
|
1190
|
+
} else if (txn.hasPendingWrites() && chainStillActive(txn)) {
|
|
1191
|
+
// A later link in the chain was written recently (writes re-arm only the link that
|
|
1192
|
+
// receives them, and a multi-store transaction can be writing database B while this
|
|
1193
|
+
// head only reads A). The logical transaction is still active, so re-arm this link
|
|
1194
|
+
// rather than aborting the whole chain out from under it.
|
|
1195
|
+
txn.timeout = Math.max(txnExpiration, txn.timeoutBudget ?? 0);
|
|
994
1196
|
} else if (txn.hasPendingWrites() && !txn.sourceApply && !txn.isReplay) {
|
|
995
1197
|
// Abort and surface an error rather than force-committing a partial write set: silently
|
|
996
1198
|
// committing on the application's behalf breaks atomicity and can leave orphaned
|
|
@@ -1026,7 +1228,7 @@ function startMonitoringTxns() {
|
|
|
1026
1228
|
} catch (error) {
|
|
1027
1229
|
harperLogger.debug?.(`Error committing timed out transaction: ${error.message}`);
|
|
1028
1230
|
}
|
|
1029
|
-
txn.timeout = txnExpiration;
|
|
1231
|
+
txn.timeout = Math.max(txnExpiration, txn.timeoutBudget ?? 0);
|
|
1030
1232
|
}
|
|
1031
1233
|
} else {
|
|
1032
1234
|
txn.timeout -= txnExpiration;
|
|
@@ -1037,6 +1239,15 @@ function startMonitoringTxns() {
|
|
|
1037
1239
|
|
|
1038
1240
|
startMonitoringTxns();
|
|
1039
1241
|
|
|
1242
|
+
/**
|
|
1243
|
+
* Test seam: re-arms the once-per-process replay warning. The whole unit suite shares one process,
|
|
1244
|
+
* so whichever test first drives a commit under open iterators consumes the warning for every test
|
|
1245
|
+
* after it.
|
|
1246
|
+
*/
|
|
1247
|
+
export function resetReplayedWritesWarning() {
|
|
1248
|
+
replayedWritesWarned = false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1040
1251
|
export function setTxnExpiration(ms) {
|
|
1041
1252
|
clearInterval(timer);
|
|
1042
1253
|
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 */
|