@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
|
@@ -9,25 +9,39 @@
|
|
|
9
9
|
//
|
|
10
10
|
// This module solves that by broadcasting a structured "deploy:start" /
|
|
11
11
|
// "deploy:end" signal to every Harper thread. Each thread's deploy emitter
|
|
12
|
-
// fires locally so Scopes (and any plugin subscribers) can react: Scopes
|
|
13
|
-
// their EntryHandlers on start
|
|
12
|
+
// fires locally so Scopes (and any plugin subscribers) can react: Scopes pause
|
|
13
|
+
// their EntryHandlers on start and resume them on end. The result is that
|
|
14
14
|
// during a deploy, no watcher fires events for the deployed component, and
|
|
15
|
-
// after the deploy the
|
|
16
|
-
//
|
|
15
|
+
// after the deploy the watcher compares the new tree with its retained snapshot
|
|
16
|
+
// and emits the same logical add/change/unlink events consumers saw before #1806.
|
|
17
17
|
//
|
|
18
|
-
// State sharing across threads is intentionally narrow:
|
|
19
|
-
//
|
|
20
|
-
// need to gate their own work on deploy progress
|
|
21
|
-
// listen to the events directly.
|
|
18
|
+
// State sharing across threads is intentionally narrow: local deployment-owner
|
|
19
|
+
// records are rebuilt from the broadcast stream and reclaimed when their owner
|
|
20
|
+
// thread exits. Plugins that need to gate their own work on deploy progress
|
|
21
|
+
// import `deployLifecycle` and listen to the events directly.
|
|
22
22
|
|
|
23
23
|
import { EventEmitter } from 'node:events';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
26
|
+
import { isMainThread, threadId } from 'node:worker_threads';
|
|
27
|
+
import {
|
|
28
|
+
broadcast,
|
|
29
|
+
broadcastWithAcknowledgement,
|
|
30
|
+
isThreadRunning,
|
|
31
|
+
onMessageByType,
|
|
32
|
+
onThreadExit,
|
|
33
|
+
} from '../server/threads/manageThreads.js';
|
|
34
|
+
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
26
35
|
|
|
27
36
|
const DEPLOY_LIFECYCLE_MSG = 'harper:deploy:lifecycle';
|
|
28
37
|
|
|
29
38
|
export type DeployPhase = 'start' | 'end';
|
|
30
|
-
export type DeployLifecycleEvent = {
|
|
39
|
+
export type DeployLifecycleEvent = {
|
|
40
|
+
name: string;
|
|
41
|
+
phase: DeployPhase;
|
|
42
|
+
deploymentId?: string;
|
|
43
|
+
ownerThreadId?: number;
|
|
44
|
+
};
|
|
31
45
|
|
|
32
46
|
type DeployLifecycleEventsMap = {
|
|
33
47
|
'deploy:start': [componentName: string];
|
|
@@ -35,44 +49,109 @@ type DeployLifecycleEventsMap = {
|
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
class DeployLifecycle extends EventEmitter<DeployLifecycleEventsMap> {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#inFlight = new Map<string, number>();
|
|
52
|
+
#deployments = new Map<string, { name: string; ownerThreadId: number }>();
|
|
53
|
+
#byComponent = new Map<string, Set<string>>();
|
|
54
|
+
#deadOwners = new Set<number>();
|
|
55
|
+
#legacyDeployments = new Map<string, string[]>();
|
|
56
|
+
#legacySequence = 0;
|
|
44
57
|
|
|
45
58
|
isDeployInFlight(componentName: string): boolean {
|
|
46
|
-
return (this.#
|
|
59
|
+
return (this.#byComponent.get(componentName)?.size ?? 0) > 0;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
// Process a deploy lifecycle event in-process. Called both from the
|
|
50
63
|
// broadcast receiver (for events originating on another thread) and from
|
|
51
64
|
// the broadcaster (so the originating thread also reacts locally).
|
|
52
65
|
_handle(event: DeployLifecycleEvent): void {
|
|
53
|
-
const
|
|
66
|
+
const ownerThreadId = event.ownerThreadId ?? threadId;
|
|
67
|
+
let deploymentId = event.deploymentId;
|
|
54
68
|
if (event.phase === 'start') {
|
|
55
|
-
this.#
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
if (this.#deadOwners.has(ownerThreadId)) return;
|
|
70
|
+
if (!deploymentId) {
|
|
71
|
+
deploymentId = `legacy:${threadId}:${++this.#legacySequence}`;
|
|
72
|
+
const legacy = this.#legacyDeployments.get(event.name) ?? [];
|
|
73
|
+
legacy.push(deploymentId);
|
|
74
|
+
this.#legacyDeployments.set(event.name, legacy);
|
|
75
|
+
}
|
|
76
|
+
if (this.#deployments.has(deploymentId)) return;
|
|
77
|
+
const active = this.#byComponent.get(event.name) ?? new Set<string>();
|
|
78
|
+
this.#deployments.set(deploymentId, { name: event.name, ownerThreadId });
|
|
79
|
+
this.#byComponent.set(event.name, active);
|
|
80
|
+
active.add(deploymentId);
|
|
81
|
+
if (active.size === 1) this.#emitSafely('deploy:start', event.name);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!deploymentId) deploymentId = this.#legacyDeployments.get(event.name)?.pop();
|
|
85
|
+
if (deploymentId) this.#removeDeployment(deploymentId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_reclaimOwner(ownerThreadId: number): void {
|
|
89
|
+
this.#deadOwners.add(ownerThreadId);
|
|
90
|
+
for (const [deploymentId, deployment] of this.#deployments) {
|
|
91
|
+
if (deployment.ownerThreadId === ownerThreadId) this.#removeDeployment(deploymentId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _reclaimOwnerAfterTermination(
|
|
96
|
+
ownerThreadId: number,
|
|
97
|
+
waitForTermination: (ownerThreadId: number) => Promise<void> = waitForOwnerTermination
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
if (ownerThreadId === 0 || ownerThreadId === threadId) return;
|
|
100
|
+
this.#deadOwners.add(ownerThreadId);
|
|
101
|
+
await waitForTermination(ownerThreadId);
|
|
102
|
+
this._reclaimOwner(ownerThreadId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#removeDeployment(deploymentId: string): void {
|
|
106
|
+
const deployment = this.#deployments.get(deploymentId);
|
|
107
|
+
if (!deployment) return;
|
|
108
|
+
this.#deployments.delete(deploymentId);
|
|
109
|
+
const active = this.#byComponent.get(deployment.name);
|
|
110
|
+
if (!active) return;
|
|
111
|
+
active.delete(deploymentId);
|
|
112
|
+
if (active.size === 0) {
|
|
113
|
+
this.#byComponent.delete(deployment.name);
|
|
114
|
+
this.#emitSafely('deploy:end', deployment.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#emitSafely(event: 'deploy:start' | 'deploy:end', componentName: string): void {
|
|
119
|
+
for (const listener of this.rawListeners(event)) {
|
|
120
|
+
try {
|
|
121
|
+
listener.call(this, componentName);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
harperLogger.error(`Listener for ${event} threw for ${componentName}:`, error);
|
|
64
124
|
}
|
|
65
125
|
}
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
// Test-only: clear in-flight state without firing events.
|
|
69
129
|
_clearForTests(): void {
|
|
70
|
-
this.#
|
|
130
|
+
this.#deployments.clear();
|
|
131
|
+
this.#byComponent.clear();
|
|
132
|
+
this.#deadOwners.clear();
|
|
133
|
+
this.#legacyDeployments.clear();
|
|
134
|
+
this.#legacySequence = 0;
|
|
71
135
|
}
|
|
72
136
|
}
|
|
73
137
|
|
|
74
138
|
export const deployLifecycle = new DeployLifecycle();
|
|
75
139
|
|
|
140
|
+
async function waitForOwnerTermination(ownerThreadId: number): Promise<void> {
|
|
141
|
+
let timeoutReported = false;
|
|
142
|
+
for (;;) {
|
|
143
|
+
try {
|
|
144
|
+
if (!(await isThreadRunning(ownerThreadId))) return;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!timeoutReported) {
|
|
147
|
+
timeoutReported = true;
|
|
148
|
+
harperLogger.warn(`Retrying liveness check for exited thread ${ownerThreadId}:`, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
await delay(25, undefined, { ref: false });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
let receiverInstalled = false;
|
|
77
156
|
function ensureReceiver() {
|
|
78
157
|
if (receiverInstalled) return;
|
|
@@ -88,6 +167,11 @@ function ensureReceiver() {
|
|
|
88
167
|
// suppress only the main thread's watchers and the worker watchers would keep
|
|
89
168
|
// firing restart storms (harper#488).
|
|
90
169
|
ensureReceiver();
|
|
170
|
+
onThreadExit((deadThreadId: number) => {
|
|
171
|
+
void deployLifecycle
|
|
172
|
+
._reclaimOwnerAfterTermination(deadThreadId)
|
|
173
|
+
.catch((error) => harperLogger.error(`Could not reclaim deployments from thread ${deadThreadId}:`, error));
|
|
174
|
+
});
|
|
91
175
|
|
|
92
176
|
/**
|
|
93
177
|
* Announce the start of a deploy for `componentName`. Awaits acknowledgement
|
|
@@ -98,9 +182,10 @@ ensureReceiver();
|
|
|
98
182
|
* component compose correctly (each call must be paired with exactly one
|
|
99
183
|
* broadcastDeployEnd).
|
|
100
184
|
*/
|
|
101
|
-
export async function broadcastDeployStart(componentName: string): Promise<
|
|
185
|
+
export async function broadcastDeployStart(componentName: string): Promise<string> {
|
|
102
186
|
ensureReceiver();
|
|
103
|
-
const
|
|
187
|
+
const deploymentId = randomUUID();
|
|
188
|
+
const event: DeployLifecycleEvent = { name: componentName, phase: 'start', deploymentId, ownerThreadId: threadId };
|
|
104
189
|
deployLifecycle._handle(event); // local thread first
|
|
105
190
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
106
191
|
try {
|
|
@@ -125,6 +210,7 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
|
|
|
125
210
|
} finally {
|
|
126
211
|
if (timer) clearTimeout(timer);
|
|
127
212
|
}
|
|
213
|
+
return deploymentId;
|
|
128
214
|
}
|
|
129
215
|
|
|
130
216
|
/**
|
|
@@ -135,9 +221,9 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
|
|
|
135
221
|
* Must be called exactly once per matching broadcastDeployStart, even if the
|
|
136
222
|
* deploy errored — typically from a `finally` block.
|
|
137
223
|
*/
|
|
138
|
-
export function broadcastDeployEnd(componentName: string): void {
|
|
224
|
+
export function broadcastDeployEnd(componentName: string, deploymentId: string): void {
|
|
139
225
|
ensureReceiver();
|
|
140
|
-
const event: DeployLifecycleEvent = { name: componentName, phase: 'end' };
|
|
226
|
+
const event: DeployLifecycleEvent = { name: componentName, phase: 'end', deploymentId, ownerThreadId: threadId };
|
|
141
227
|
deployLifecycle._handle(event);
|
|
142
228
|
try {
|
|
143
229
|
void broadcast({ type: DEPLOY_LIFECYCLE_MSG, event }, false);
|
|
@@ -13,6 +13,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { Readable, Transform, pipeline } from 'node:stream';
|
|
15
15
|
import { databases } from '../resources/databases.ts';
|
|
16
|
+
import { transaction, contextStorage } from '../resources/transaction.ts';
|
|
16
17
|
import { createBlob, isSaving, deleteBlob, BLOB_UNAVAILABLE_STATUS } from '../resources/blob.ts';
|
|
17
18
|
import * as terms from '../utility/hdbTerms.ts';
|
|
18
19
|
import type { CredentialReference } from './secretOperations.ts';
|
|
@@ -68,6 +69,39 @@ interface CreateOptions {
|
|
|
68
69
|
// no-custody transient token.
|
|
69
70
|
credentials?: CredentialReference[] | null;
|
|
70
71
|
emitter?: ProgressEmitter;
|
|
72
|
+
// Open-transaction budget (ms) for the payload-ingest write, below. Mirrors the
|
|
73
|
+
// `deployment_timeout` operation parameter already used for the peer-side row/blob waits
|
|
74
|
+
// (DEFAULT_AWAIT_ROW_TIMEOUT_MS) so operators have one existing knob for all of a deploy's
|
|
75
|
+
// size-driven timeouts, rather than a new config surface for this one.
|
|
76
|
+
ingestTimeoutMs?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run `write()` in a fresh transaction while preserving audit attribution. Recorder writes must
|
|
81
|
+
* commit independently of a caller's transaction so an older staged row cannot overwrite them;
|
|
82
|
+
* authorization is enforced by the deploy operation rather than these internal writes.
|
|
83
|
+
*
|
|
84
|
+
* `timeoutBudget` is sticky across RocksDB `getReadTxn()` calls, so reads inside `write()` cannot
|
|
85
|
+
* silently restore the generic default. The transaction uses the larger of this budget and the
|
|
86
|
+
* configured global limit.
|
|
87
|
+
*/
|
|
88
|
+
function withIsolatedTransaction<T>(write: () => Promise<T>, timeoutMs?: number): Promise<T> {
|
|
89
|
+
const ambient = contextStorage.getStore();
|
|
90
|
+
const context = {
|
|
91
|
+
user: ambient?.user,
|
|
92
|
+
originatingOperation: ambient?.originatingOperation,
|
|
93
|
+
session: ambient?.session,
|
|
94
|
+
signal: ambient?.signal,
|
|
95
|
+
};
|
|
96
|
+
return transaction(context, (txn) => {
|
|
97
|
+
if (timeoutMs != null) {
|
|
98
|
+
// TODO(harper#2057): only extends the RocksDB DatabaseTransaction head; on
|
|
99
|
+
// HARPER_STORAGE_ENGINE=lmdb, Table.txnForContext() chains a separate LMDBTransaction
|
|
100
|
+
// that this budget doesn't reach.
|
|
101
|
+
txn.timeoutBudget = timeoutMs;
|
|
102
|
+
}
|
|
103
|
+
return write();
|
|
104
|
+
});
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
export class DeploymentRecorder {
|
|
@@ -78,10 +112,13 @@ export class DeploymentRecorder {
|
|
|
78
112
|
private pendingPut: Promise<void> | null = null;
|
|
79
113
|
private dirty = false;
|
|
80
114
|
private sealed = false;
|
|
115
|
+
private flushSuppressed = false;
|
|
116
|
+
private readonly ingestTimeoutMs?: number;
|
|
81
117
|
|
|
82
|
-
private constructor(deploymentId: string, initial: Record<string, any
|
|
118
|
+
private constructor(deploymentId: string, initial: Record<string, any>, ingestTimeoutMs?: number) {
|
|
83
119
|
this.deploymentId = deploymentId;
|
|
84
120
|
this.record = initial;
|
|
121
|
+
this.ingestTimeoutMs = ingestTimeoutMs;
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
static async create(options: CreateOptions): Promise<DeploymentRecorder> {
|
|
@@ -107,7 +144,7 @@ export class DeploymentRecorder {
|
|
|
107
144
|
credentials: options.credentials ?? null,
|
|
108
145
|
error: null,
|
|
109
146
|
};
|
|
110
|
-
const recorder = new DeploymentRecorder(deploymentId, record);
|
|
147
|
+
const recorder = new DeploymentRecorder(deploymentId, record, options.ingestTimeoutMs);
|
|
111
148
|
await recorder.put();
|
|
112
149
|
if (options.emitter) {
|
|
113
150
|
recorder.subscribeTo(options.emitter);
|
|
@@ -158,9 +195,8 @@ export class DeploymentRecorder {
|
|
|
158
195
|
// the record dirty; the chained continuation issues a follow-up put once the prior one
|
|
159
196
|
// settles. This keeps event_log writes O(1) puts per burst rather than O(N) per event.
|
|
160
197
|
private scheduleFlush(): void {
|
|
161
|
-
if (this.sealed) {
|
|
162
|
-
//
|
|
163
|
-
// terminal write. See seal() for why. The emitter still emits live SSE events.
|
|
198
|
+
if (this.sealed || this.flushSuppressed) {
|
|
199
|
+
// Accumulate state until finish() or the active ingest writes it without a competing put.
|
|
164
200
|
this.dirty = true;
|
|
165
201
|
return;
|
|
166
202
|
}
|
|
@@ -175,6 +211,7 @@ export class DeploymentRecorder {
|
|
|
175
211
|
this.scheduleFlush();
|
|
176
212
|
}
|
|
177
213
|
});
|
|
214
|
+
this.pendingPut.catch((error) => logger.warn?.('Failed to persist deployment progress', error));
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
/**
|
|
@@ -191,6 +228,26 @@ export class DeploymentRecorder {
|
|
|
191
228
|
* materialized by the time they reach us, so they take the simpler buffer path.
|
|
192
229
|
*/
|
|
193
230
|
async ingestPayload(source: Readable | Buffer | string): Promise<void> {
|
|
231
|
+
this.flushSuppressed = true;
|
|
232
|
+
try {
|
|
233
|
+
while (this.pendingPut) {
|
|
234
|
+
try {
|
|
235
|
+
await this.pendingPut;
|
|
236
|
+
} catch {
|
|
237
|
+
/* already logged; the ingest write carries the current row state */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await this.ingestPayloadWithoutFlush(source);
|
|
241
|
+
} finally {
|
|
242
|
+
this.flushSuppressed = false;
|
|
243
|
+
if (this.dirty && !this.sealed) {
|
|
244
|
+
this.dirty = false;
|
|
245
|
+
this.scheduleFlush();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async ingestPayloadWithoutFlush(source: Readable | Buffer | string): Promise<void> {
|
|
194
251
|
const hash = createHash('sha256');
|
|
195
252
|
|
|
196
253
|
// In-memory sources: the bytes are already resident, so hashing them and creating a
|
|
@@ -202,7 +259,10 @@ export class DeploymentRecorder {
|
|
|
202
259
|
this.record.payload_blob = createBlob(buffer, { type: 'application/gzip' });
|
|
203
260
|
this.record.payload_hash = hash.digest('hex');
|
|
204
261
|
this.record.payload_size = buffer.length;
|
|
205
|
-
|
|
262
|
+
// Same blob-durability gate as the streaming path below applies here too (the commit
|
|
263
|
+
// waits for the buffer to land on disk), so a large base64-in-JSON/CBOR body needs the
|
|
264
|
+
// same extended budget.
|
|
265
|
+
await this.put(ingestTransactionTimeoutMs(this.ingestTimeoutMs));
|
|
206
266
|
return;
|
|
207
267
|
}
|
|
208
268
|
|
|
@@ -263,6 +323,16 @@ export class DeploymentRecorder {
|
|
|
263
323
|
});
|
|
264
324
|
const blob = createBlob(tap, { type: 'application/gzip' });
|
|
265
325
|
this.record.payload_blob = blob;
|
|
326
|
+
// This row's commit is gated on the blob's durable file write (resources/blob.ts,
|
|
327
|
+
// startPreCommitBlobsForRecord's local-write branch) so a peer never sees a row that
|
|
328
|
+
// references a not-yet-durable blob — the write transaction is legitimately held open for
|
|
329
|
+
// as long as the payload takes to land on disk. The deploy system is explicitly designed
|
|
330
|
+
// to carry arbitrarily large, uncapped components (see the module comment above), so that
|
|
331
|
+
// duration scales with payload size, not with anything bounded. Run this one put() under
|
|
332
|
+
// an extended open-time budget instead of the generic storage.maxTransactionOpenTime meant
|
|
333
|
+
// for ordinary short requests (a large-but-otherwise-healthy ingest tripped that generic
|
|
334
|
+
// guard on a loaded self-hosted runner — see the Large Deploy Payload Test regression).
|
|
335
|
+
//
|
|
266
336
|
// Persisting the row encodes the blob, which synchronously starts the file write that
|
|
267
337
|
// drains the tap, so `isSaving(blob)` is set as soon as put() is *called*. Await the put,
|
|
268
338
|
// the source draining (tapDone), and the file flush (saving) together: handing all three
|
|
@@ -270,7 +340,7 @@ export class DeploymentRecorder {
|
|
|
270
340
|
// a large upload mid-stream (which rejects both tapDone and the blob write) fails the
|
|
271
341
|
// deploy loudly here instead of leaking an unhandledRejection. This also makes hash/size
|
|
272
342
|
// correctness independent of the put()/store write timing and of isSaving() being defined.
|
|
273
|
-
const putDone = this.put();
|
|
343
|
+
const putDone = this.put(ingestTransactionTimeoutMs(this.ingestTimeoutMs));
|
|
274
344
|
const saving = isSaving(blob) ?? Promise.resolve();
|
|
275
345
|
try {
|
|
276
346
|
await Promise.all([putDone, tapDone, saving]);
|
|
@@ -288,7 +358,7 @@ export class DeploymentRecorder {
|
|
|
288
358
|
// ensures the null-reference write lands after the original reference write
|
|
289
359
|
// (so it wins) and the blob's file lock is released before we unlink it.
|
|
290
360
|
await Promise.allSettled([putDone, saving]);
|
|
291
|
-
await this.put();
|
|
361
|
+
await this.put(ingestTransactionTimeoutMs(this.ingestTimeoutMs));
|
|
292
362
|
deleteBlob(blob);
|
|
293
363
|
} catch (cleanupError) {
|
|
294
364
|
logger.warn?.('Failed to clean up partial deploy payload blob', cleanupError);
|
|
@@ -301,7 +371,7 @@ export class DeploymentRecorder {
|
|
|
301
371
|
this.record.payload_size = blob.size ?? byteCount;
|
|
302
372
|
// Persist the now-known hash + size. The blob is already saved, so this re-put does
|
|
303
373
|
// not re-stream — saveBlob short-circuits on the existing fileId.
|
|
304
|
-
await this.put();
|
|
374
|
+
await this.put(ingestTransactionTimeoutMs(this.ingestTimeoutMs));
|
|
305
375
|
}
|
|
306
376
|
|
|
307
377
|
/**
|
|
@@ -436,14 +506,14 @@ export class DeploymentRecorder {
|
|
|
436
506
|
return this.record;
|
|
437
507
|
}
|
|
438
508
|
|
|
439
|
-
private async put(): Promise<void> {
|
|
509
|
+
private async put(timeoutMs?: number): Promise<void> {
|
|
440
510
|
const table = (databases as any).system?.[terms.SYSTEM_TABLE_NAMES.DEPLOYMENT_TABLE_NAME];
|
|
441
511
|
if (!table) {
|
|
442
512
|
// Table missing means the upgrade directive hasn't run yet (or the table got dropped).
|
|
443
513
|
// We tolerate this — tracking is observability; the deploy itself must still succeed.
|
|
444
514
|
return;
|
|
445
515
|
}
|
|
446
|
-
await table.put(this.record);
|
|
516
|
+
await withIsolatedTransaction(() => table.put(this.record), timeoutMs);
|
|
447
517
|
}
|
|
448
518
|
}
|
|
449
519
|
|
|
@@ -455,11 +525,32 @@ export class DeploymentRecorder {
|
|
|
455
525
|
// via the `deployment_timeout` operation parameter.
|
|
456
526
|
export const DEFAULT_AWAIT_ROW_TIMEOUT_MS = 120_000;
|
|
457
527
|
|
|
528
|
+
// Default open-time budget for the origin's payload-ingest write transaction (ingestPayload,
|
|
529
|
+
// below). Distinct from DEFAULT_AWAIT_ROW_TIMEOUT_MS (that one bounds a *replication* wait;
|
|
530
|
+
// this one bounds *local disk I/O* for a component the deploy system explicitly supports at
|
|
531
|
+
// arbitrarily large, uncapped sizes — see the module comment above and the large-payload
|
|
532
|
+
// regression test). 10 minutes comfortably covers a multi-GB ingest even on a slow or loaded
|
|
533
|
+
// disk while still being a bounded safety net, not a disabled one. Overridable via the same
|
|
534
|
+
// `deployment_timeout` operation parameter as the peer-side waits, so operators sizing for
|
|
535
|
+
// even larger components (or a known-slow disk) have one knob, not two.
|
|
536
|
+
export const DEFAULT_INGEST_TRANSACTION_TIMEOUT_MS = 600_000;
|
|
537
|
+
|
|
458
538
|
export function coerceTimeoutMs(value: unknown, fallback: number): number {
|
|
459
539
|
const requested = Number(value);
|
|
460
540
|
return Number.isFinite(requested) && requested >= 0 ? requested : fallback;
|
|
461
541
|
}
|
|
462
542
|
|
|
543
|
+
// Floored at the default, never lowered by it: `deployment_timeout` also governs the
|
|
544
|
+
// peer-side row/blob wait (awaitDeploymentRow), where 0 means "poll once, don't wait" — a
|
|
545
|
+
// legitimate fast-fail setting for peers that must never also starve the origin's own disk
|
|
546
|
+
// write down to 0ms.
|
|
547
|
+
export function ingestTransactionTimeoutMs(deploymentTimeout: unknown): number {
|
|
548
|
+
return Math.max(
|
|
549
|
+
coerceTimeoutMs(deploymentTimeout, DEFAULT_INGEST_TRANSACTION_TIMEOUT_MS),
|
|
550
|
+
DEFAULT_INGEST_TRANSACTION_TIMEOUT_MS
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
463
554
|
/**
|
|
464
555
|
* Peer-side helper — wait for the hdb_deployment row to arrive via table replication,
|
|
465
556
|
* then return it. The row is committed on origin before `replicateOperation` is
|
|
@@ -303,6 +303,12 @@ export function makeOperationToolHandler(operationName: string) {
|
|
|
303
303
|
operation: operationName,
|
|
304
304
|
hdb_user: context.user,
|
|
305
305
|
};
|
|
306
|
+
// Authorization bypass is internal dispatch state, never client-supplied tool
|
|
307
|
+
// arguments — mirrors handlePostRequest's stripping in serverHandlers.js. Without
|
|
308
|
+
// this, a caller-supplied `bypass_auth`/`bypassAuth` argument would reach handlers
|
|
309
|
+
// (e.g. createTokens) that trust it directly from the body.
|
|
310
|
+
delete body.bypass_auth;
|
|
311
|
+
delete body.bypassAuth;
|
|
306
312
|
if (STREAMING_OPERATIONS.has(operationName)) {
|
|
307
313
|
// Reject before the handler runs at all — see STREAMING_OPERATIONS' comment for why
|
|
308
314
|
// destroying an already-returned stream isn't sufficient cleanup for these producers.
|
package/components/operations.js
CHANGED
|
@@ -441,6 +441,10 @@ async function deployComponent(req) {
|
|
|
441
441
|
// Reference form only — the rollback source for re-resolving the credential.
|
|
442
442
|
credentials: credentialReferences.length ? credentialReferences : null,
|
|
443
443
|
emitter,
|
|
444
|
+
// Reuse the same `deployment_timeout` knob operators already have for the
|
|
445
|
+
// peer-side row/blob waits to size the origin's payload-ingest write
|
|
446
|
+
// transaction too — see DEFAULT_INGEST_TRANSACTION_TIMEOUT_MS in deploymentRecorder.
|
|
447
|
+
ingestTimeoutMs: req.deployment_timeout,
|
|
444
448
|
});
|
|
445
449
|
if (recorder) req._deploymentId = recorder.deploymentId;
|
|
446
450
|
|
|
@@ -646,12 +650,10 @@ async function deployComponent(req) {
|
|
|
646
650
|
// deploy checks its own local isNewComponent, since directory state (and therefore
|
|
647
651
|
// whether the component was already active) can differ per node.
|
|
648
652
|
//
|
|
649
|
-
// An existing
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
// changes and requests the restart itself.
|
|
654
|
-
if (application.isNewComponent) {
|
|
653
|
+
// An existing component's watched files are handled by Scope/EntryHandler. Package
|
|
654
|
+
// metadata is deliberately outside most plugin globs, so compare it across the atomic
|
|
655
|
+
// swap as well: a dependency or module-entry change also invalidates loaded code.
|
|
656
|
+
if (application.isNewComponent || application.packageMetadataChanged) {
|
|
655
657
|
const { requestRestart } = require('./requestRestart.ts');
|
|
656
658
|
requestRestart();
|
|
657
659
|
}
|
package/config/configUtils.ts
CHANGED
|
@@ -7,7 +7,6 @@ import YAML from 'yaml';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { threadId } from 'node:worker_threads';
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
|
-
import { performance } from 'node:perf_hooks';
|
|
11
10
|
import isNumber from 'is-number';
|
|
12
11
|
import propertiesReaderModule from 'properties-reader';
|
|
13
12
|
import _ from 'lodash';
|
|
@@ -50,6 +49,14 @@ let flatDefaultConfigObj;
|
|
|
50
49
|
let flatConfigObj;
|
|
51
50
|
let configObj;
|
|
52
51
|
|
|
52
|
+
// Canonical param names that live in CONFIG_PARAM_MAP but do NOT correspond to a path in the
|
|
53
|
+
// harper-config.yaml schema (BOOT_PROP_PARAMS is boot-props-file-only bookkeeping — see its own
|
|
54
|
+
// comment). Splitting one of these on '_' and writing it into the nested configObj tree would
|
|
55
|
+
// silently create a bogus top-level section (e.g. 'settings_path' -> configObj.settings.path),
|
|
56
|
+
// which componentLoader.ts treats as a real component to load, since it iterates every truthy
|
|
57
|
+
// top-level key of the root config.
|
|
58
|
+
const NON_NESTED_CONFIG_PARAMS = new Set<string>(Object.values(hdbTerms.BOOT_PROP_PARAMS));
|
|
59
|
+
|
|
53
60
|
export function resolvePath(relativePath: string) {
|
|
54
61
|
if (relativePath?.startsWith('~/')) {
|
|
55
62
|
return path.join(hdbUtils.getHomeDir(), relativePath.slice(1));
|
|
@@ -87,12 +94,17 @@ export function getConfigPath(param: string) {
|
|
|
87
94
|
// Every worker thread runs its own RootConfigWatcher (chokidar), so a write on one thread
|
|
88
95
|
// routinely races a hot-reload read on another; Windows Defender / AV real-time scanning can
|
|
89
96
|
// hold a similar transient handle. Retry with exponential backoff to ride out the race -
|
|
90
|
-
// callers are synchronous, so the wait is a synchronous
|
|
91
|
-
|
|
97
|
+
// callers are synchronous, so the wait is a synchronous sleep rather than an async one.
|
|
98
|
+
// The budget must outlast a single AV real-time scan pass (seconds, not hundreds of ms):
|
|
99
|
+
// the previous ~910ms budget was exhausted twice in a row by the same test on a CI runner
|
|
100
|
+
// (harper#2036), so the worst case is now ~3.6s.
|
|
101
|
+
const RENAME_RETRY_MAX_ATTEMPTS = 12;
|
|
92
102
|
const RENAME_RETRY_INITIAL_DELAY_MS = 10;
|
|
93
|
-
const RENAME_RETRY_MAX_DELAY_MS =
|
|
103
|
+
const RENAME_RETRY_MAX_DELAY_MS = 500;
|
|
104
|
+
// Never notified; exists only so Atomics.wait can time out (a synchronous, CPU-idle sleep).
|
|
105
|
+
const renameRetrySleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
94
106
|
|
|
95
|
-
function atomicWriteFile(
|
|
107
|
+
export function atomicWriteFile(
|
|
96
108
|
filePath,
|
|
97
109
|
content,
|
|
98
110
|
{
|
|
@@ -112,11 +124,10 @@ function atomicWriteFile(
|
|
|
112
124
|
} catch (err) {
|
|
113
125
|
if (retries > 0 && (err.code === 'EPERM' || err.code === 'EACCES')) {
|
|
114
126
|
retries--;
|
|
115
|
-
// Sleep synchronously (all call sites are sync) to allow the
|
|
116
|
-
// file.
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
while (performance.now() - start < delayMs) {}
|
|
127
|
+
// Sleep synchronously (all call sites are sync) to allow the holder to close the
|
|
128
|
+
// file. Atomics.wait yields the thread to the OS instead of spinning the CPU,
|
|
129
|
+
// which is what makes a multi-second worst-case budget affordable.
|
|
130
|
+
if (delayMs > 0) Atomics.wait(renameRetrySleepBuffer, 0, 0, delayMs);
|
|
120
131
|
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
121
132
|
continue;
|
|
122
133
|
}
|
|
@@ -190,8 +201,7 @@ export function createConfigFile(args, skipFsValidation = false) {
|
|
|
190
201
|
// Validates config doc and if required sets default values for some parameters.
|
|
191
202
|
validateConfig(configDoc, skipFsValidation);
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
flatConfigObj = flattenConfig(configObj);
|
|
204
|
+
flatConfigObj = setActiveConfig(configDoc.toJSON());
|
|
195
205
|
|
|
196
206
|
// Create new config file and write config doc to it.
|
|
197
207
|
const hdbRoot = configDoc.getIn(['rootPath']) as string;
|
|
@@ -497,9 +507,9 @@ export function initConfig(force = false) {
|
|
|
497
507
|
|
|
498
508
|
// Validates config doc and if required sets default values for some parameters.
|
|
499
509
|
validateConfig(configDoc);
|
|
500
|
-
const
|
|
501
|
-
(server as any).config =
|
|
502
|
-
flatConfigObj =
|
|
510
|
+
const parsedConfig = configDoc.toJSON();
|
|
511
|
+
(server as any).config = parsedConfig;
|
|
512
|
+
flatConfigObj = setActiveConfig(parsedConfig);
|
|
503
513
|
|
|
504
514
|
// If config has old version of logrotate enabled let user know it has been deprecated.
|
|
505
515
|
if (flatConfigObj['logging_rotation_rotate']) {
|
|
@@ -687,6 +697,41 @@ export function updateConfigObject(param: string, value: any) {
|
|
|
687
697
|
}
|
|
688
698
|
|
|
689
699
|
flatConfigObj[configObjKey.toLowerCase()] = value;
|
|
700
|
+
|
|
701
|
+
// Keep the nested config tree in sync too: componentLoader (root components) and other
|
|
702
|
+
// nested-path readers derive behavior — e.g. a component's network port/protocol — from
|
|
703
|
+
// getConfigObj()'s tree, not the flattened map, so an override that only touched flatConfigObj
|
|
704
|
+
// was invisible to them. configObj is unset only before a live config has ever been installed
|
|
705
|
+
// (setActiveConfig), i.e. during install; don't auto-vivify it, since getConfigObj() reads its
|
|
706
|
+
// falsiness as "not yet initialized" and an empty object would short-circuit that lazy init
|
|
707
|
+
// permanently.
|
|
708
|
+
if (configObj != null && !NON_NESTED_CONFIG_PARAMS.has(configObjKey)) {
|
|
709
|
+
const pathSegments = configObjKey.split('_');
|
|
710
|
+
let node = configObj;
|
|
711
|
+
for (let i = 0; i < pathSegments.length - 1; i++) {
|
|
712
|
+
const segment = pathSegments[i];
|
|
713
|
+
if (node[segment] === undefined) {
|
|
714
|
+
// Auto-vivifying ancestors only to delete the leaf would leave empty sections behind,
|
|
715
|
+
// which componentLoader.ts would treat as components to load.
|
|
716
|
+
if (value === undefined) return;
|
|
717
|
+
node[segment] = {};
|
|
718
|
+
} else if (typeof node[segment] !== 'object' || node[segment] === null) {
|
|
719
|
+
// A legacy scalar shorthand for this key (e.g. `threads: 4`); descending would replace
|
|
720
|
+
// it with {} out from under other readers. Leaves the two views disagreeing on this
|
|
721
|
+
// param, so say so rather than diverging silently.
|
|
722
|
+
logger.trace(
|
|
723
|
+
`Config param '${configObjKey}' not mirrored into the nested config: '${segment}' holds a scalar value`
|
|
724
|
+
);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
node = node[segment];
|
|
728
|
+
}
|
|
729
|
+
// squashObj never writes an undefined value as an enumerable key either; root-config readers
|
|
730
|
+
// (e.g. bin/run.ts) iterate configObj's own keys and assume every one holds a real value.
|
|
731
|
+
const leaf = pathSegments[pathSegments.length - 1];
|
|
732
|
+
if (value === undefined) delete node[leaf];
|
|
733
|
+
else node[leaf] = value;
|
|
734
|
+
}
|
|
690
735
|
}
|
|
691
736
|
|
|
692
737
|
/**
|
|
@@ -852,7 +897,7 @@ export function updateConfigValue(
|
|
|
852
897
|
}
|
|
853
898
|
atomicWriteFile(configFileLocation, String(configDoc));
|
|
854
899
|
if (update_config_obj) {
|
|
855
|
-
flatConfigObj =
|
|
900
|
+
flatConfigObj = setActiveConfig(configDoc.toJSON());
|
|
856
901
|
}
|
|
857
902
|
logger.trace(`Config parameter: ${param} updated with value: ${value}`);
|
|
858
903
|
}
|
|
@@ -871,9 +916,25 @@ function backupConfigFile(configPath, hdbRoot) {
|
|
|
871
916
|
}
|
|
872
917
|
}
|
|
873
918
|
|
|
919
|
+
/**
|
|
920
|
+
* Flattens `obj` and installs it as the live config — the nested tree getConfigObj() hands out and
|
|
921
|
+
* updateConfigObject() mirrors overrides into. Only callers that own the live config may do this:
|
|
922
|
+
* flattenConfig() also runs on docs that are discarded moments later (the defaults doc, the
|
|
923
|
+
* user-supplied doc installer.ts reads for HDB_CONFIG), and no reader of configObj can tell one of
|
|
924
|
+
* those from the real thing.
|
|
925
|
+
* @param obj
|
|
926
|
+
* @returns the flattened config
|
|
927
|
+
*/
|
|
928
|
+
function setActiveConfig(obj) {
|
|
929
|
+
const flatObj = flattenConfig(obj);
|
|
930
|
+
configObj = obj;
|
|
931
|
+
return flatObj;
|
|
932
|
+
}
|
|
933
|
+
|
|
874
934
|
const PRESERVED_PROPERTIES = ['databases'];
|
|
875
935
|
/**
|
|
876
936
|
* Flattens the JSON version of Harper config with underscores separating each parent/child key.
|
|
937
|
+
* Does NOT install `obj` as the live config — see setActiveConfig() for that.
|
|
877
938
|
* @param obj
|
|
878
939
|
* @returns {null}
|
|
879
940
|
*/
|
|
@@ -882,10 +943,7 @@ export function flattenConfig(obj) {
|
|
|
882
943
|
if (obj?.operationsApi?.network) obj.operationsApi.network = { ...obj.http, ...obj.operationsApi.network };
|
|
883
944
|
if (obj?.operationsApi) obj.operationsApi.tls = { ...obj.tls, ...obj.operationsApi.tls };
|
|
884
945
|
|
|
885
|
-
|
|
886
|
-
const flatObj = squashObj(obj);
|
|
887
|
-
|
|
888
|
-
return flatObj;
|
|
946
|
+
return squashObj(obj);
|
|
889
947
|
|
|
890
948
|
function squashObj(obj) {
|
|
891
949
|
let result = {};
|