@harperfast/harper 5.2.13 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +58 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +120 -4
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +3436 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +440 -98
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +56 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +381 -32
- package/components/componentPreparationLock.ts +16 -5
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +537 -113
- package/components/operationsValidation.js +98 -3
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +240 -40
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +357 -48
- package/config/harperConfigEnvVars.ts +170 -27
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +33 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +61 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +91 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +235 -17
- package/dist/components/Application.js +3038 -238
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -0
- package/dist/components/OptionsWatcher.js +440 -99
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +52 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +313 -24
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +476 -113
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +97 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +9 -1
- package/dist/config/RootConfigWatcher.js +228 -37
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +11 -2
- package/dist/config/configUtils.js +304 -47
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +55 -0
- package/dist/resources/DatabaseTransaction.d.ts +77 -0
- package/dist/resources/DatabaseTransaction.js +586 -54
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +43 -6
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +49 -5
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +110 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +117 -24
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +28 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -9
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
- package/dist/resources/RocksTransactionLogStore.js +160 -49
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +165 -10
- package/dist/resources/Table.js +1805 -303
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +181 -2
- package/dist/resources/auditStore.js +640 -29
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +157 -14
- package/dist/resources/databases.js +1462 -325
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +52 -16
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +123 -0
- package/dist/resources/recordLock.js +315 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +157 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +283 -37
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +77 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +106 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +16 -1
- package/dist/server/http.js +122 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +27 -0
- package/dist/server/serverHelpers/Headers.js +145 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
- package/dist/server/serverHelpers/contentTypes.js +221 -40
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +178 -29
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +17 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +83 -3
- package/dist/server/threads/manageThreads.js +769 -75
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +277 -31
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +76 -22
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +33 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +18 -0
- package/dist/utility/hdbTerms.js +20 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +286 -30
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +192 -85
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +11 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +41 -0
- package/dist/utility/watcherFallback.js +153 -0
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +205 -75
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +8 -0
- package/json/systemSchema.json +55 -0
- package/npm-shrinkwrap.json +286 -194
- package/package.json +15 -7
- package/resources/DESIGN.md +219 -52
- package/resources/DatabaseTransaction.ts +657 -52
- package/resources/LMDBTransaction.ts +46 -6
- package/resources/PrimaryRocksDatabase.ts +46 -7
- package/resources/RecordEncoder.ts +124 -9
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +114 -22
- package/resources/ResourceInterface.ts +31 -0
- package/resources/RocksIndexStore.ts +30 -9
- package/resources/RocksTransactionLogStore.ts +188 -51
- package/resources/Table.ts +1960 -301
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +647 -32
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +1584 -320
- package/resources/defineTable.ts +18 -2
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +72 -17
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +419 -0
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +152 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +288 -46
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +83 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +194 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +115 -4
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +133 -20
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +135 -0
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +217 -36
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +232 -40
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +15 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +771 -66
- package/server/threads/socketRouter.ts +291 -30
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +73 -22
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +57 -0
- package/utility/hdbTerms.ts +19 -0
- package/utility/logging/harper_logger.ts +278 -26
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +213 -81
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +12 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +148 -0
- package/validation/configValidator.ts +215 -75
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
|
@@ -7,7 +7,7 @@ const { realExit } = require('./workerProcessGuard.ts');
|
|
|
7
7
|
|
|
8
8
|
const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
|
|
9
9
|
const { spawn, spawnSync } = require('node:child_process');
|
|
10
|
-
const { readFileSync } = require('node:fs');
|
|
10
|
+
const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
|
|
11
11
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
12
12
|
const { join, isAbsolute, extname } = require('path');
|
|
13
13
|
const { pathToFileURL } = require('url');
|
|
@@ -21,7 +21,16 @@ const { randomBytes } = require('crypto');
|
|
|
21
21
|
const { _assignPackageExport } = require('../../globals.js');
|
|
22
22
|
const { PACKAGE_ROOT } = require('../../utility/packageUtils.js');
|
|
23
23
|
const { resolvePreloadModules } = require('./resolvePreload.ts');
|
|
24
|
+
const { resolveThreadHeapMemoryMb } = require('./threadHeapMemory.ts');
|
|
24
25
|
const { getConfigPath } = require('../../config/configUtils.ts');
|
|
26
|
+
const { resolveWatchTarget } = require('../../utility/watchPath.ts');
|
|
27
|
+
const {
|
|
28
|
+
DIRECTORY_POLLING_FALLBACK_OPTIONS,
|
|
29
|
+
claimLostNativeWatchError,
|
|
30
|
+
guardedWatch,
|
|
31
|
+
isWatcherExhaustionError,
|
|
32
|
+
warnWatcherFallback,
|
|
33
|
+
} = require('../../utility/watcherFallback.ts');
|
|
25
34
|
let importModules;
|
|
26
35
|
function getImportModules() {
|
|
27
36
|
if (importModules === undefined)
|
|
@@ -42,7 +51,6 @@ function getRequireModules() {
|
|
|
42
51
|
);
|
|
43
52
|
return requireModules;
|
|
44
53
|
}
|
|
45
|
-
const chokidar = require('chokidar');
|
|
46
54
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
47
55
|
const MB = 1024 * 1024;
|
|
48
56
|
const workers = []; // these are our child workers that we are managing
|
|
@@ -54,9 +62,14 @@ const MAX_UNEXPECTED_RESTARTS = 50;
|
|
|
54
62
|
// down mid-disposal crashes the process, so give that teardown more room before the forced backstop.
|
|
55
63
|
let threadTerminationTimeout = process.env.DEV_MODE === 'true' || process.env.DEV_MODE === '1' ? 30000 : 10000;
|
|
56
64
|
const RESTART_TYPE = 'restart';
|
|
65
|
+
const RESTART_PROGRESS_HEARTBEAT_MS = 15000;
|
|
57
66
|
const REQUEST_THREAD_INFO = 'request_thread_info';
|
|
67
|
+
const REQUEST_RUNNING_ISOLATED_APPLICATIONS = 'request-running-isolated-applications';
|
|
58
68
|
const RESOURCE_REPORT = 'resource_report';
|
|
69
|
+
const OS_THREAD_ID = 'os-thread-id';
|
|
70
|
+
const STUCK_WORKER_REPORT = 'stuck-worker-report';
|
|
59
71
|
const THREAD_INFO = 'thread_info';
|
|
72
|
+
const RUNNING_ISOLATED_APPLICATIONS = 'running-isolated-applications';
|
|
60
73
|
const ADDED_PORT = 'added-port';
|
|
61
74
|
const ACKNOWLEDGEMENT = 'ack';
|
|
62
75
|
const REMOVE_PORT = 'remove-port';
|
|
@@ -74,6 +87,7 @@ const AWAIT_PROCESS_GROUP_TERMINATION = 'await-process-group-termination';
|
|
|
74
87
|
const PROCESS_GROUP_TERMINATION_CONFIRMED = 'process-group-termination-confirmed';
|
|
75
88
|
const THREAD_INFO_REQUEST_TIMEOUT_MS = 1000;
|
|
76
89
|
let getThreadInfo;
|
|
90
|
+
let getRunningIsolatedApplications;
|
|
77
91
|
let awaitProcessGroupTermination;
|
|
78
92
|
// Worker-side backstop that force-exits if the graceful shutdown sequence doesn't finish in time.
|
|
79
93
|
let selfExitTimer;
|
|
@@ -140,6 +154,19 @@ module.exports = {
|
|
|
140
154
|
workers,
|
|
141
155
|
setMonitorListener,
|
|
142
156
|
onMessageFromWorkers,
|
|
157
|
+
setIsolatedWorkerReconciler,
|
|
158
|
+
setRunningIsolatedApplicationsGetter,
|
|
159
|
+
isApplicationPrimaryWorker,
|
|
160
|
+
applicationWorkerIndex,
|
|
161
|
+
runsApplicationCodeSingletons,
|
|
162
|
+
isDedicatedWorker,
|
|
163
|
+
markBranchStorePath,
|
|
164
|
+
ownsStoreMaintenance,
|
|
165
|
+
ownsStoreExpiration,
|
|
166
|
+
workersForApplication,
|
|
167
|
+
stopWorker,
|
|
168
|
+
encodeRestartScope,
|
|
169
|
+
decodeRestartScope,
|
|
143
170
|
onMessageByType,
|
|
144
171
|
broadcast,
|
|
145
172
|
broadcastWithAcknowledgement,
|
|
@@ -154,8 +181,11 @@ module.exports = {
|
|
|
154
181
|
beginProcessShutdown,
|
|
155
182
|
registerWorkerDataProvider,
|
|
156
183
|
onThreadExit,
|
|
184
|
+
hasThreadExited,
|
|
185
|
+
notifyThreadExit,
|
|
157
186
|
registerProcessGroup,
|
|
158
187
|
unregisterProcessGroup,
|
|
188
|
+
isProcessGroupAlive,
|
|
159
189
|
isThreadRunning,
|
|
160
190
|
waitUntilConfirmedGone,
|
|
161
191
|
restartNumber: workerData?.restartNumber || 1,
|
|
@@ -182,6 +212,9 @@ connectedPorts.sendToThread = function (threadId, message) {
|
|
|
182
212
|
throw err;
|
|
183
213
|
}
|
|
184
214
|
};
|
|
215
|
+
// Direct thread-to-thread send, so a worker can reach a sibling (e.g. the record lock owner worker)
|
|
216
|
+
// without a hop through main. Returns false when no port for the thread is connected.
|
|
217
|
+
module.exports.sendToThread = connectedPorts.sendToThread;
|
|
185
218
|
module.exports.whenThreadsStarted = new Promise((resolve) => {
|
|
186
219
|
module.exports.threadsHaveStarted = resolve;
|
|
187
220
|
});
|
|
@@ -197,6 +230,101 @@ function setTerminateTimeout(newTimeout) {
|
|
|
197
230
|
function getWorkerIndex() {
|
|
198
231
|
return workerData ? workerData.workerIndex : isMainWorker ? 0 : undefined;
|
|
199
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The worker that owns `applicationName`'s per-application singletons (its scheduled jobs, its data
|
|
235
|
+
* files): pool worker 0 for a shared application and for root-level plugins (which pass no name), the
|
|
236
|
+
* dedicated worker for an isolated one -- never index 0, so node-wide duties stay on the pool, and
|
|
237
|
+
* never a dedicated worker for anything but its own application.
|
|
238
|
+
*/
|
|
239
|
+
function isApplicationPrimaryWorker(applicationName) {
|
|
240
|
+
if (workerData?.isolatedApplication !== undefined) return applicationName === workerData.isolatedApplication;
|
|
241
|
+
return getWorkerIndex() === 0;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Stores that exist only on this thread: the branch stores an isolated application opened in its
|
|
245
|
+
* dedicated worker. Registered by openBranchDatabase, so the ownership predicates below can tell them
|
|
246
|
+
* from the shared stores every thread has open.
|
|
247
|
+
*/
|
|
248
|
+
const branchStorePaths = new Set();
|
|
249
|
+
function markBranchStorePath(path, isBranch = true) {
|
|
250
|
+
if (isBranch) branchStorePaths.add(path);
|
|
251
|
+
else branchStorePaths.delete(path);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Whether this thread runs the "last worker" per-store maintenance for `storePath` (TTL scans, storage
|
|
255
|
+
* reclamation, audit cleanup). Shared stores belong to the last pool worker; a dedicated worker
|
|
256
|
+
* maintains only its own branch stores, which no other thread has open.
|
|
257
|
+
*/
|
|
258
|
+
function ownsStoreMaintenance(storePath) {
|
|
259
|
+
if (workerData?.isolatedApplication !== undefined) return branchStorePaths.has(storePath);
|
|
260
|
+
return getWorkerIndex() === getWorkerCount() - 1;
|
|
261
|
+
}
|
|
262
|
+
/** The "worker 0" counterpart: expiration eviction for `storePath`. */
|
|
263
|
+
function ownsStoreExpiration(storePath) {
|
|
264
|
+
if (workerData?.isolatedApplication !== undefined) return branchStorePaths.has(storePath);
|
|
265
|
+
return getWorkerIndex() === 0;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Whether a singleton set up by APPLICATION CODE runs here (a caching table's `sourcedFrom`
|
|
269
|
+
* subscription): pool worker 0, or a dedicated worker -- the only code that runs on one is its own
|
|
270
|
+
* application's, so whatever it sets up is that application's to run. Not for root-level plugins,
|
|
271
|
+
* which load on every thread and must use isApplicationPrimaryWorker(name).
|
|
272
|
+
*/
|
|
273
|
+
function runsApplicationCodeSingletons() {
|
|
274
|
+
return getWorkerIndex() === 0 || workerData?.isolatedApplication !== undefined;
|
|
275
|
+
}
|
|
276
|
+
/** Whether this thread is a worker dedicated to one isolated application. */
|
|
277
|
+
function isDedicatedWorker() {
|
|
278
|
+
return workerData?.isolatedApplication !== undefined;
|
|
279
|
+
}
|
|
280
|
+
/** The worker index as the application sees it: a dedicated worker is its application's worker 0. */
|
|
281
|
+
function applicationWorkerIndex() {
|
|
282
|
+
return workerData?.isolatedApplication !== undefined ? 0 : getWorkerIndex();
|
|
283
|
+
}
|
|
284
|
+
/** Every started worker dedicated to `application`, including a replacement still booting. */
|
|
285
|
+
function workersForApplication(application) {
|
|
286
|
+
return workers.filter((worker) => worker.application === application);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stop one worker for good: shut it down, honour the drain extension it may ask for, force it after
|
|
290
|
+
* the same backstop the rolling restart uses (FORCE_EXIT on Bun, where terminate() segfaults), and
|
|
291
|
+
* resolve once it has exited. Marked as shut down first so its exit does not start a replacement.
|
|
292
|
+
*/
|
|
293
|
+
function stopWorker(worker) {
|
|
294
|
+
worker.wasShutdown = true;
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
const armTerminate = (delay) =>
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
harperLogger.warn('Thread did not voluntarily terminate, terminating from the outside', worker.threadId);
|
|
299
|
+
if (isBun) {
|
|
300
|
+
try {
|
|
301
|
+
worker.postMessage({ type: FORCE_EXIT });
|
|
302
|
+
} catch {}
|
|
303
|
+
} else {
|
|
304
|
+
worker.terminate();
|
|
305
|
+
}
|
|
306
|
+
}, delay).unref();
|
|
307
|
+
let timeout = armTerminate(threadTerminationTimeout * 2);
|
|
308
|
+
worker.extendTerminateDeadline = (deadlineMs) => {
|
|
309
|
+
clearTimeout(timeout);
|
|
310
|
+
const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require('../../components/shutdownDrain.ts');
|
|
311
|
+
timeout = armTerminate(
|
|
312
|
+
boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs())
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
worker.on('exit', () => {
|
|
316
|
+
clearTimeout(timeout);
|
|
317
|
+
worker.extendTerminateDeadline = undefined;
|
|
318
|
+
resolve();
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
worker.postMessage({ restartNumber: module.exports.restartNumber, type: hdbTerms.ITC_EVENT_TYPES.SHUTDOWN });
|
|
322
|
+
} catch {
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
resolve(); // already gone
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
200
328
|
function getWorkerCount() {
|
|
201
329
|
return workerData ? workerData.workerCount : isMainWorker ? 1 : undefined;
|
|
202
330
|
}
|
|
@@ -233,6 +361,7 @@ const RESERVED_WORKER_DATA_KEYS = [
|
|
|
233
361
|
'processIncarnation',
|
|
234
362
|
'ticketKeys',
|
|
235
363
|
'noServerStart',
|
|
364
|
+
'isolatedApplication',
|
|
236
365
|
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
237
366
|
];
|
|
238
367
|
const workerDataProviders = new Map();
|
|
@@ -286,23 +415,43 @@ function getTicketKeys() {
|
|
|
286
415
|
ticketKeys = isMainThread ? randomBytes(48) : workerData.ticketKeys;
|
|
287
416
|
return ticketKeys;
|
|
288
417
|
}
|
|
418
|
+
// What application code sees: a dedicated worker is its application's only worker, index 0 of 1, so
|
|
419
|
+
// the documented partitioning idiom (`hash % server.workerCount === server.workerIndex`) covers
|
|
420
|
+
// everything there. Node-wide duties keep using the raw getWorkerIndex/getWorkerCount.
|
|
289
421
|
Object.defineProperty(server, 'workerIndex', {
|
|
290
422
|
get() {
|
|
291
|
-
return
|
|
423
|
+
return applicationWorkerIndex();
|
|
292
424
|
},
|
|
293
425
|
});
|
|
294
426
|
Object.defineProperty(server, 'workerCount', {
|
|
295
427
|
get() {
|
|
296
|
-
return getWorkerCount();
|
|
428
|
+
return workerData?.isolatedApplication !== undefined ? 1 : getWorkerCount();
|
|
297
429
|
},
|
|
298
430
|
});
|
|
299
431
|
if (!parentPort) {
|
|
300
432
|
onMessageByType(REQUEST_THREAD_INFO, (message, worker) => {
|
|
301
433
|
if (worker) sendThreadInfo(worker);
|
|
302
434
|
});
|
|
435
|
+
onMessageByType(REQUEST_RUNNING_ISOLATED_APPLICATIONS, (message, worker) => {
|
|
436
|
+
if (worker)
|
|
437
|
+
worker.postMessage({
|
|
438
|
+
type: RUNNING_ISOLATED_APPLICATIONS,
|
|
439
|
+
requestId: message.requestId,
|
|
440
|
+
applications: runningIsolatedApplicationsGetter(),
|
|
441
|
+
});
|
|
442
|
+
});
|
|
303
443
|
onMessageByType(RESOURCE_REPORT, (message, worker) => {
|
|
304
444
|
if (worker) recordResourceReport(worker, message);
|
|
305
445
|
});
|
|
446
|
+
onMessageByType(OS_THREAD_ID, (message, worker) => {
|
|
447
|
+
if (worker) worker.osThreadId = message.osThreadId;
|
|
448
|
+
});
|
|
449
|
+
onMessageByType(STUCK_WORKER_REPORT, (message) => {
|
|
450
|
+
for (const threadId of message.threadIds) {
|
|
451
|
+
const worker = workers.find((worker) => worker.threadId === threadId);
|
|
452
|
+
if (worker) logStuckWorkerDiagnostics(worker);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
306
455
|
onMessageByType(AWAIT_PROCESS_GROUP_TERMINATION, async (message, worker) => {
|
|
307
456
|
if (!worker) return;
|
|
308
457
|
await (pendingProcessGroupTerminations.get(message.ownerThreadId) ?? Promise.resolve());
|
|
@@ -314,6 +463,7 @@ if (!parentPort) {
|
|
|
314
463
|
}
|
|
315
464
|
// postMessage type listeners that are registered in other ways or can be registered later
|
|
316
465
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED, null);
|
|
466
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTUP_PHASE, null);
|
|
317
467
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.SCHEMA, null);
|
|
318
468
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.USER, null);
|
|
319
469
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.COMPONENT_STATUS_REQUEST, null);
|
|
@@ -324,11 +474,12 @@ listenersByType.set(hdbTerms.ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_RESPONSE, null);
|
|
|
324
474
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_REGISTERED, null);
|
|
325
475
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_REQUEST, null);
|
|
326
476
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_RESPONSE, null);
|
|
327
|
-
//
|
|
477
|
+
// These request/response functions register their own one-shot parentPort listener per
|
|
328
478
|
// call rather than going through onMessageByType, so without this, every reply would also reach
|
|
329
479
|
// addPort's permanent dispatcher as an "unregistered" type: notifyMessageListeners would warn and
|
|
330
480
|
// queue each one in messagesQueuedByType forever, and a lock-wait polls every 50ms.
|
|
331
481
|
listenersByType.set(THREAD_INFO, null);
|
|
482
|
+
listenersByType.set(RUNNING_ISOLATED_APPLICATIONS, null);
|
|
332
483
|
listenersByType.set(PROCESS_GROUP_TERMINATION_CONFIRMED, null);
|
|
333
484
|
|
|
334
485
|
function startWorker(path, options = {}) {
|
|
@@ -351,8 +502,8 @@ function startWorker(path, options = {}) {
|
|
|
351
502
|
// and lower than total memory
|
|
352
503
|
availableMemory = Math.min(availableMemory, totalmem(), 20000 * MB);
|
|
353
504
|
const maxOldMemory =
|
|
354
|
-
envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY) ??
|
|
355
|
-
Math.max(Math.floor(availableMemory / MB / (10 + (options.threadCount || 1) / 4)), 512);
|
|
505
|
+
resolveThreadHeapMemoryMb(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY)) ??
|
|
506
|
+
Math.max(Math.floor(availableMemory / MB / (10 + (options.heapShareCount || options.threadCount || 1) / 4)), 512);
|
|
356
507
|
// Max young memory space (semi-space for scavenger) is 1/128 of max memory (limited to 16-64). For most of our m5
|
|
357
508
|
// machines this will be 64MB (less for t3's). This is based on recommendations from:
|
|
358
509
|
// https://www.alibabacloud.com/blog/node-js-application-troubleshooting-manual---comprehensive-gc-problems-and-optimization594965
|
|
@@ -398,6 +549,9 @@ function startWorker(path, options = {}) {
|
|
|
398
549
|
for (const requirePath of getRequireModules()) execArgv.push('--require', requirePath);
|
|
399
550
|
}
|
|
400
551
|
|
|
552
|
+
// Only a start that declares the serving topology may write it; see DESIGN.md on the two workerCounts.
|
|
553
|
+
if (typeof options.threadCount === 'number') workerCount = options.threadCount;
|
|
554
|
+
|
|
401
555
|
const worker = new Worker(isAbsolute(path) ? path : join(PACKAGE_ROOT, path), {
|
|
402
556
|
resourceLimits: {
|
|
403
557
|
maxOldGenerationSizeMb: maxOldMemory,
|
|
@@ -412,8 +566,9 @@ function startWorker(path, options = {}) {
|
|
|
412
566
|
addThreadIds: channelsToConnect.map((channel) => channel.existingPort.threadId),
|
|
413
567
|
addPortIsJobWorkers: channelsToConnect.map((channel) => channel.existingPort.isJobWorker === true),
|
|
414
568
|
workerIndex: options.workerIndex,
|
|
415
|
-
workerCount:
|
|
569
|
+
workerCount: options.threadCount,
|
|
416
570
|
name: options.name,
|
|
571
|
+
isolatedApplication: options.application,
|
|
417
572
|
restartNumber: module.exports.restartNumber,
|
|
418
573
|
processIncarnation: module.exports.processIncarnation,
|
|
419
574
|
ticketKeys: getTicketKeys(),
|
|
@@ -449,18 +604,27 @@ function startWorker(path, options = {}) {
|
|
|
449
604
|
});
|
|
450
605
|
worker.on('exit', (_code) => {
|
|
451
606
|
workers.splice(workers.indexOf(worker), 1);
|
|
452
|
-
if (
|
|
607
|
+
if (
|
|
608
|
+
!processShuttingDown &&
|
|
609
|
+
!worker.wasShutdown &&
|
|
610
|
+
options.autoRestart !== false &&
|
|
611
|
+
options.shouldAutoRestart?.(worker) !== false
|
|
612
|
+
) {
|
|
453
613
|
// if this wasn't an intentional shutdown, restart now (unless we have tried too many times)
|
|
454
614
|
if (worker.unexpectedRestarts < MAX_UNEXPECTED_RESTARTS) {
|
|
455
615
|
options.unexpectedRestarts = worker.unexpectedRestarts + 1;
|
|
456
616
|
startWorker(path, options);
|
|
457
|
-
} else
|
|
617
|
+
} else {
|
|
618
|
+
harperLogger.error(`Thread has been restarted ${worker.unexpectedRestarts} times and will not be restarted`);
|
|
619
|
+
options.onRestartExhausted?.(worker);
|
|
620
|
+
}
|
|
458
621
|
}
|
|
459
622
|
});
|
|
460
623
|
workers.push(worker);
|
|
461
624
|
startMonitoring();
|
|
462
625
|
if (options.onStarted) options.onStarted(worker); // notify that it is ready
|
|
463
626
|
worker.name = options.name;
|
|
627
|
+
worker.application = options.application; // the isolated application this worker is dedicated to, if any
|
|
464
628
|
return worker;
|
|
465
629
|
}
|
|
466
630
|
|
|
@@ -474,16 +638,31 @@ const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
|
|
|
474
638
|
* threads at the same time we shutdown new ones. However, we usually want to limit how many we do at once to avoid
|
|
475
639
|
* excessive load and to keep things responsive. This parameter throttles the restarts to minimize load from
|
|
476
640
|
* thread startups.
|
|
477
|
-
* @
|
|
641
|
+
* @param onProgress Called each time a worker has been replaced (or its replacement has been given
|
|
642
|
+
* up on), so a caller waiting on a wide pool can tell a slow restart from a stalled one.
|
|
643
|
+
* @returns {Promise<{workersKeptOnOldCode: number, replacementsNotStarted: number}|{declined: true}|undefined>}
|
|
644
|
+
* from the main thread, how many workers were left running the old code and how many replacements
|
|
645
|
+
* never reported that they started — or `{declined: true}` when the process is already shutting down;
|
|
646
|
+
* from a worker, nothing — the restart is handed to the main thread.
|
|
478
647
|
*/
|
|
479
648
|
|
|
649
|
+
/**
|
|
650
|
+
* `application` selects which workers of the type restart: omitted preserves the historic all-worker
|
|
651
|
+
* behavior, explicit undefined restarts the shared pool only, a name restarts only that application's
|
|
652
|
+
* dedicated worker, and '*' restarts all.
|
|
653
|
+
*/
|
|
480
654
|
async function restartWorkers(
|
|
481
655
|
name = null,
|
|
482
656
|
maxWorkersDown = Math.max(Math.floor(workerCount / 8), 1), // restart 1/8 of the threads at a time, but at least 1
|
|
483
|
-
startReplacementThreads = true
|
|
657
|
+
startReplacementThreads = true,
|
|
658
|
+
onProgress = null,
|
|
659
|
+
application = undefined
|
|
484
660
|
) {
|
|
661
|
+
if (arguments.length < 5) application = '*';
|
|
485
662
|
if (isMainThread) {
|
|
486
|
-
|
|
663
|
+
// Declining is not the same as delegating: a caller reporting on the restart must not read this
|
|
664
|
+
// as "another thread is completing it".
|
|
665
|
+
if (processShuttingDown && startReplacementThreads) return { declined: true };
|
|
487
666
|
try {
|
|
488
667
|
// we do this because it is possible for a component to chdir to itself, get re-deployed and then the cwd
|
|
489
668
|
// inode link is invalid and it can cause a lot of problems. But process.cwd() still returns the path, for
|
|
@@ -492,23 +671,62 @@ async function restartWorkers(
|
|
|
492
671
|
} catch (e) {
|
|
493
672
|
harperLogger.error('Unable to reestablish current working directory', e);
|
|
494
673
|
}
|
|
674
|
+
const freshlyStarted = new Set(); // dedicated workers the reconcile just started: already on the new code
|
|
495
675
|
// problematic cyclic dependency, bind late
|
|
496
676
|
const { resetRestartNeeded } = require('../../components/requestRestart.ts');
|
|
497
|
-
|
|
677
|
+
// One process-wide bit cannot say WHICH application is pending, so it may only be cleared by a
|
|
678
|
+
// restart that demonstrably covers every worker the bit could stand for. A restart scoped to one
|
|
679
|
+
// application leaves the whole pool on its old code; a pool restart replaces every pool worker and
|
|
680
|
+
// reconciles dedicated slots, but leaves an already-running dedicated worker on its old modules.
|
|
681
|
+
// Clearing from either would report restartRequired: false while a deployed component is still
|
|
682
|
+
// loaded nowhere. So: all workers always, the pool only while no dedicated worker is running --
|
|
683
|
+
// which is every node that uses no isolated application, i.e. the historic behavior unchanged.
|
|
684
|
+
const coversEveryWorker =
|
|
685
|
+
application === '*' || (application === undefined && !workers.some((worker) => worker.application));
|
|
686
|
+
if (coversEveryWorker) resetRestartNeeded();
|
|
498
687
|
// This is here to prevent circular dependencies
|
|
499
688
|
if (startReplacementThreads) {
|
|
500
689
|
const { loadRootComponents } = require('../loadRootComponents.js');
|
|
501
|
-
|
|
690
|
+
// Installing and loading every root component reports nothing and can outlast a caller's idle
|
|
691
|
+
// window on its own (a cold npm cache, a large dependency graph), so beat while it runs. The
|
|
692
|
+
// caller's absolute ceiling is what bounds a load that never finishes.
|
|
693
|
+
const loading = setInterval(() => onProgress?.(), RESTART_PROGRESS_HEARTBEAT_MS).unref();
|
|
694
|
+
try {
|
|
695
|
+
await loadRootComponents();
|
|
696
|
+
// isolated applications added or removed by the reload get their dedicated worker started or
|
|
697
|
+
// stopped; a crash-looping newcomer can take a while, so the heartbeat runs through this too
|
|
698
|
+
try {
|
|
699
|
+
for (const application of (await isolatedWorkerReconciler?.()) ?? []) freshlyStarted.add(application);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
harperLogger.error('Could not reconcile isolated application workers', error);
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
clearInterval(loading);
|
|
705
|
+
}
|
|
706
|
+
onProgress?.();
|
|
502
707
|
}
|
|
503
708
|
|
|
504
709
|
module.exports.restartNumber++;
|
|
505
|
-
|
|
710
|
+
// `Infinity` is shutdownWorkers' "all at once" sentinel and must survive.
|
|
711
|
+
if (typeof maxWorkersDown !== 'number' || Number.isNaN(maxWorkersDown)) {
|
|
712
|
+
maxWorkersDown = 1;
|
|
713
|
+
} else if (maxWorkersDown < 1) {
|
|
506
714
|
// we accept a ratio of workers, and compute absolute maximum being down at a time from the total number of
|
|
507
715
|
// threads
|
|
508
716
|
maxWorkersDown = maxWorkersDown * workers.length;
|
|
509
717
|
}
|
|
510
718
|
// make a copy of the workers before iterating them, as the workers array mutates a lot during this
|
|
511
|
-
let waitingToFinish = []; // promises for workers we
|
|
719
|
+
let waitingToFinish = []; // promises for workers we are replacing, spliced as each is replaced
|
|
720
|
+
// Every replacement that was started without being awaited first, so this function can still
|
|
721
|
+
// resolve only once each one is accepting connections.
|
|
722
|
+
let replacementsStarting = [];
|
|
723
|
+
// A replacement that never came up leaves the pool in one of two very different states, and a
|
|
724
|
+
// caller waiting on this restart needs them apart: the pre-start path keeps the *old* worker
|
|
725
|
+
// serving old code, while a replacement that exits after its predecessor is already gone only
|
|
726
|
+
// costs capacity until startWorker's auto-restart brings a fresh one up.
|
|
727
|
+
let workersKeptOnOldCode = 0;
|
|
728
|
+
let replacementsNotStarted = 0;
|
|
729
|
+
let replacementsFailedToStart = 0;
|
|
512
730
|
// We can only start the replacement *before* the old worker releases its port when the OS lets
|
|
513
731
|
// both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT),
|
|
514
732
|
// macOS (unreliable SO_REUSEPORT, so workers bind exclusively), and Bun — the replacement can't
|
|
@@ -519,12 +737,17 @@ async function restartWorkers(
|
|
|
519
737
|
// replacement — an unavoidable brief gap, but only for worker-owned listeners, since the main
|
|
520
738
|
// thread keeps serving the HTTP ports throughout. This ordering is also what lets
|
|
521
739
|
// listenOnPorts() treat a dedicated listener's EADDRINUSE as an external conflict.
|
|
522
|
-
const
|
|
523
|
-
|
|
740
|
+
const platformCanPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
|
|
741
|
+
const restarting = workers.slice(0);
|
|
742
|
+
for (let index = 0; index < restarting.length; index++) {
|
|
743
|
+
const worker = restarting[index];
|
|
524
744
|
// Terminal shutdown: stop replacing workers mid-loop — the guard for every replacement start below.
|
|
525
745
|
if (processShuttingDown && startReplacementThreads) break;
|
|
526
746
|
if ((name && worker.name !== name) || worker.wasShutdown) continue; // filter by type, if specified
|
|
747
|
+
if (application !== '*' && worker.application !== application) continue; // and by isolated application
|
|
748
|
+
if (worker.application && freshlyStarted.has(worker.application)) continue;
|
|
527
749
|
const overlapping = OVERLAPPING_RESTART_TYPES.indexOf(worker.name) > -1;
|
|
750
|
+
const canPreStartReplacement = platformCanPreStartReplacement && !worker.application;
|
|
528
751
|
if (overlapping && startReplacementThreads && canPreStartReplacement) {
|
|
529
752
|
// Overlapping restart: start the replacement and wait until it is accepting connections
|
|
530
753
|
// *before* shutting down the worker it replaces. The replacement joins the (SO_REUSEPORT)
|
|
@@ -587,6 +810,8 @@ async function restartWorkers(
|
|
|
587
810
|
// Replacement didn't come up — keep the existing worker serving. Restore its auto-restart
|
|
588
811
|
// protection if it is still alive (it may have exited on its own during the wait).
|
|
589
812
|
if (workers.includes(worker)) worker.wasShutdown = false;
|
|
813
|
+
workersKeptOnOldCode++;
|
|
814
|
+
onProgress?.();
|
|
590
815
|
continue;
|
|
591
816
|
}
|
|
592
817
|
}
|
|
@@ -607,7 +832,16 @@ async function restartWorkers(
|
|
|
607
832
|
// Overlapping types we couldn't pre-start (Windows/Bun): start the replacement now that the old
|
|
608
833
|
// worker is releasing its port. server.close() stops accepting immediately, so the port frees up
|
|
609
834
|
// well before the replacement finishes booting and binds.
|
|
610
|
-
|
|
835
|
+
let replacementStarting;
|
|
836
|
+
if (overlapping && startReplacementThreads && !canPreStartReplacement && !processShuttingDown) {
|
|
837
|
+
replacementStarting = whenWorkerStarted(worker.startCopy()).then((started) => {
|
|
838
|
+
if (!started) replacementsFailedToStart++;
|
|
839
|
+
onProgress?.();
|
|
840
|
+
return started;
|
|
841
|
+
});
|
|
842
|
+
replacementsStarting.push(replacementStarting);
|
|
843
|
+
}
|
|
844
|
+
|
|
611
845
|
let whenDone = new Promise((resolve) => {
|
|
612
846
|
// in case the exit inside the thread doesn't timeout, force it from the outside
|
|
613
847
|
const armTerminate = (delay) =>
|
|
@@ -634,38 +868,117 @@ async function restartWorkers(
|
|
|
634
868
|
// buggy/rogue message can't defer the force-kill unboundedly; a shrink (drain-done reset)
|
|
635
869
|
// passes through untouched. See boundedTerminateDelay for the arithmetic + its unit tests.
|
|
636
870
|
const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require('../../components/shutdownDrain.ts');
|
|
637
|
-
|
|
638
|
-
|
|
871
|
+
const delay = boundedTerminateDelay(
|
|
872
|
+
deadlineMs,
|
|
873
|
+
Date.now(),
|
|
874
|
+
threadTerminationTimeout * 2,
|
|
875
|
+
getShutdownDrainCeilingMs()
|
|
639
876
|
);
|
|
877
|
+
timeout = armTerminate(delay);
|
|
878
|
+
// The worker is telling us it has work still moving and how long it may take, so pass that
|
|
879
|
+
// on: a caller waiting on the restart must not treat a live drain as a stalled one.
|
|
880
|
+
onProgress?.(Date.now() + delay);
|
|
640
881
|
};
|
|
641
882
|
worker.on('exit', () => {
|
|
642
883
|
clearTimeout(timeout);
|
|
884
|
+
onProgress?.();
|
|
643
885
|
worker.extendTerminateDeadline = undefined;
|
|
644
|
-
const index = waitingToFinish.indexOf(whenDone);
|
|
645
|
-
if (index > -1) waitingToFinish.splice(index, 1);
|
|
646
886
|
// non-overlapping types have no advance replacement, so start it once the old one is gone
|
|
647
887
|
if (!overlapping && startReplacementThreads && !processShuttingDown) worker.startCopy();
|
|
648
888
|
resolve();
|
|
649
889
|
});
|
|
650
890
|
});
|
|
651
|
-
|
|
891
|
+
// A worker counts as replaced only once its replacement is accepting connections, not merely
|
|
892
|
+
// once it has exited. This promise is held unawaited between throttle points, so it must not
|
|
893
|
+
// be able to reject.
|
|
894
|
+
const replaced = (replacementStarting ? Promise.all([whenDone, replacementStarting]) : whenDone)
|
|
895
|
+
.catch((error) => harperLogger.warn('Error waiting for a worker to be replaced', error))
|
|
896
|
+
.then(() => {
|
|
897
|
+
const at = waitingToFinish.indexOf(replaced);
|
|
898
|
+
if (at > -1) waitingToFinish.splice(at, 1);
|
|
899
|
+
});
|
|
900
|
+
waitingToFinish.push(replaced);
|
|
652
901
|
if (waitingToFinish.length >= maxWorkersDown) {
|
|
653
|
-
// throttle how many workers are
|
|
902
|
+
// throttle how many workers are down at once to limit load
|
|
654
903
|
await Promise.race(waitingToFinish);
|
|
655
904
|
}
|
|
905
|
+
// Readiness throttling bounds how many workers are down at once, but not how many *fail*: with
|
|
906
|
+
// replacements that never come up, walking the rest of the pool would leave nothing serving.
|
|
907
|
+
// The workers not yet touched are still running the old code, which beats none running at all.
|
|
908
|
+
if (replacementsFailedToStart >= maxWorkersDown) {
|
|
909
|
+
const untouched = restarting
|
|
910
|
+
.slice(index + 1)
|
|
911
|
+
// a worker that exited on its own mid-restart is spliced out of `workers` and
|
|
912
|
+
// auto-restarted onto the new code (see the exit handler above); it is not still on
|
|
913
|
+
// the previous code even though this loop never got to it.
|
|
914
|
+
.filter((other) => (!name || other.name === name) && !other.wasShutdown && workers.includes(other)).length;
|
|
915
|
+
harperLogger.error(
|
|
916
|
+
`${replacementsFailedToStart} replacement worker thread(s) did not start; stopping this restart with ${untouched} worker(s) still on the previous code`
|
|
917
|
+
);
|
|
918
|
+
workersKeptOnOldCode += untouched;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
656
921
|
}
|
|
657
|
-
// seems appropriate to wait for this to finish, but the API doesn't actually wait for this function
|
|
658
|
-
// to finish, so not that important
|
|
659
922
|
await Promise.all(waitingToFinish);
|
|
923
|
+
// A caller awaiting this needs it to mean "the pool is serving the new code", so wait out the
|
|
924
|
+
// replacements that could only be started once their predecessor released its exclusive ports.
|
|
925
|
+
replacementsNotStarted = (await Promise.all(replacementsStarting)).filter((started) => !started).length;
|
|
926
|
+
return { workersKeptOnOldCode, replacementsNotStarted };
|
|
660
927
|
} else {
|
|
661
928
|
parentPort.postMessage({
|
|
662
929
|
type: RESTART_TYPE,
|
|
663
930
|
workerType: name,
|
|
931
|
+
scope: encodeRestartScope(application),
|
|
664
932
|
});
|
|
665
933
|
}
|
|
666
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Resolve once a newly started worker reports that it is accepting connections, or gives up on it.
|
|
937
|
+
* There is no old worker left to fall back on here, so a replacement that fails to start is left to
|
|
938
|
+
* startWorker's own auto-restart handling; this only stops waiting on it.
|
|
939
|
+
* @param newWorker The replacement worker
|
|
940
|
+
* @returns {Promise<boolean>} whether the worker reported that it started
|
|
941
|
+
*/
|
|
942
|
+
function whenWorkerStarted(newWorker) {
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
const cleanup = () => {
|
|
945
|
+
clearTimeout(timeout);
|
|
946
|
+
newWorker.off('message', startListener);
|
|
947
|
+
newWorker.off('exit', exitListener);
|
|
948
|
+
};
|
|
949
|
+
const timeout = setTimeout(
|
|
950
|
+
() => {
|
|
951
|
+
harperLogger.error('Replacement worker did not start in time', newWorker.threadId);
|
|
952
|
+
cleanup();
|
|
953
|
+
// Its predecessor is already gone, so a replacement wedged in boot is a worker slot serving
|
|
954
|
+
// nothing until the process restarts. Stop it and let startWorker's exit handling replace it.
|
|
955
|
+
if (isBun) {
|
|
956
|
+
// terminate() triggers a NAPI segfault in Bun; ask the worker to self-exit instead.
|
|
957
|
+
try {
|
|
958
|
+
newWorker.postMessage({ type: FORCE_EXIT });
|
|
959
|
+
} catch {}
|
|
960
|
+
} else newWorker.terminate();
|
|
961
|
+
resolve(false);
|
|
962
|
+
},
|
|
963
|
+
Math.max(threadTerminationTimeout * 2, 60000)
|
|
964
|
+
).unref();
|
|
965
|
+
const startListener = (message) => {
|
|
966
|
+
if (message.type === hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED) {
|
|
967
|
+
cleanup();
|
|
968
|
+
resolve(true);
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
const exitListener = () => {
|
|
972
|
+
harperLogger.warn('Replacement worker exited before starting', newWorker.threadId);
|
|
973
|
+
cleanup();
|
|
974
|
+
resolve(false);
|
|
975
|
+
};
|
|
976
|
+
newWorker.on('message', startListener);
|
|
977
|
+
newWorker.on('exit', exitListener);
|
|
978
|
+
});
|
|
979
|
+
}
|
|
667
980
|
function shutdownWorkers(name) {
|
|
668
|
-
return restartWorkers(name, Infinity, false);
|
|
981
|
+
return restartWorkers(name, Infinity, false, null, '*');
|
|
669
982
|
}
|
|
670
983
|
function beginProcessShutdown() {
|
|
671
984
|
processShuttingDown = true;
|
|
@@ -685,6 +998,28 @@ async function shutdownWorkersNow(name) {
|
|
|
685
998
|
}
|
|
686
999
|
}
|
|
687
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* The restart scope on the wire: a worker cannot post `undefined` and have it mean "the pool" (a
|
|
1003
|
+
* message with no scope at all means "everything", as every pre-existing sender intends).
|
|
1004
|
+
*/
|
|
1005
|
+
function encodeRestartScope(application) {
|
|
1006
|
+
return application === undefined ? '' : application; // '' is not a legal application name
|
|
1007
|
+
}
|
|
1008
|
+
function decodeRestartScope(message) {
|
|
1009
|
+
if (message.scope === undefined) return '*';
|
|
1010
|
+
return message.scope === '' ? undefined : message.scope;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
let isolatedWorkerReconciler = null;
|
|
1014
|
+
let runningIsolatedApplicationsGetter = () => [];
|
|
1015
|
+
/** Registered by socketRouter: starts and stops dedicated workers to match the root config's isolated applications. */
|
|
1016
|
+
function setIsolatedWorkerReconciler(reconcile) {
|
|
1017
|
+
isolatedWorkerReconciler = reconcile;
|
|
1018
|
+
}
|
|
1019
|
+
function setRunningIsolatedApplicationsGetter(getter) {
|
|
1020
|
+
runningIsolatedApplicationsGetter = getter;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
688
1023
|
const messageListeners = [];
|
|
689
1024
|
function onMessageFromWorkers(listener) {
|
|
690
1025
|
messageListeners.push(listener);
|
|
@@ -791,18 +1126,154 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
791
1126
|
timer = undefined;
|
|
792
1127
|
const stuck = [];
|
|
793
1128
|
for (let ackHandler of [...pending]) {
|
|
794
|
-
stuck.push(ackHandler.port
|
|
1129
|
+
stuck.push(ackHandler.port);
|
|
795
1130
|
ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
|
|
796
1131
|
}
|
|
797
1132
|
harperLogger.warn(
|
|
798
|
-
`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.join(', ')} within ${timeout}ms; proceeding best-effort`
|
|
1133
|
+
`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`
|
|
799
1134
|
);
|
|
1135
|
+
if (isMainThread) for (let port of stuck) logStuckWorkerDiagnostics(port);
|
|
1136
|
+
else if (parentPort) {
|
|
1137
|
+
// Only main holds the Worker objects (and tids), so a worker-originated timeout is sampled there.
|
|
1138
|
+
const threadIds = stuck.map((port) => port?.threadId).filter((threadId) => threadId > 0);
|
|
1139
|
+
if (threadIds.length > 0) parentPort.postMessage({ type: STUCK_WORKER_REPORT, threadIds });
|
|
1140
|
+
}
|
|
800
1141
|
}, timeout);
|
|
801
1142
|
timer.unref?.();
|
|
802
1143
|
}
|
|
803
1144
|
});
|
|
804
1145
|
}
|
|
805
1146
|
|
|
1147
|
+
// Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
|
|
1148
|
+
function getOsThreadId() {
|
|
1149
|
+
try {
|
|
1150
|
+
const tid = Number(readlinkSync('/proc/thread-self').split('/').pop());
|
|
1151
|
+
return Number.isInteger(tid) && tid > 0 ? tid : undefined;
|
|
1152
|
+
} catch {
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function readTaskFile(tid, name) {
|
|
1158
|
+
try {
|
|
1159
|
+
return readFileSync(`/proc/self/task/${tid}/${name}`, 'utf8').trim();
|
|
1160
|
+
} catch {
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Kernel-side view of one thread. Every field is best-effort and reported individually, since
|
|
1166
|
+
// wchan/syscall need ptrace read access that a hardened container may deny while stat is open.
|
|
1167
|
+
function finiteOrUndefined(value) {
|
|
1168
|
+
const number = Number(value);
|
|
1169
|
+
return Number.isFinite(number) ? number : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function readOsThreadState(tid) {
|
|
1173
|
+
const thread = { tid };
|
|
1174
|
+
const stat = readTaskFile(tid, 'stat');
|
|
1175
|
+
if (stat !== undefined) {
|
|
1176
|
+
// Fields after the parenthesized comm, so state is [0], utime/stime [11]/[12], starttime [19].
|
|
1177
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1178
|
+
thread.state = fields[0];
|
|
1179
|
+
const utime = finiteOrUndefined(fields[11]);
|
|
1180
|
+
const stime = finiteOrUndefined(fields[12]);
|
|
1181
|
+
if (utime !== undefined && stime !== undefined) thread.cpuTicks = utime + stime;
|
|
1182
|
+
thread.startTime = fields[19];
|
|
1183
|
+
}
|
|
1184
|
+
const wchan = readTaskFile(tid, 'wchan');
|
|
1185
|
+
if (wchan !== undefined) thread.wchan = wchan;
|
|
1186
|
+
// First token only: the syscall number, "running", or -1 (blocked outside a syscall). The
|
|
1187
|
+
// remainder is argument registers and the stack/instruction pointers, which don't belong in a log.
|
|
1188
|
+
const syscall = readTaskFile(tid, 'syscall');
|
|
1189
|
+
if (syscall !== undefined) thread.syscall = syscall.split(' ')[0];
|
|
1190
|
+
const status = readTaskFile(tid, 'status');
|
|
1191
|
+
if (status !== undefined) {
|
|
1192
|
+
thread.voluntarySwitches = finiteOrUndefined(/^voluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
1193
|
+
thread.nonvoluntarySwitches = finiteOrUndefined(/^nonvoluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
1194
|
+
}
|
|
1195
|
+
return thread;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function snapshotWorkerThread(worker) {
|
|
1199
|
+
const snapshot = { at: Date.now() };
|
|
1200
|
+
if (worker.resources?.updated) snapshot.sinceResourceReport = snapshot.at - worker.resources.updated;
|
|
1201
|
+
const eventLoop = worker.performance?.eventLoopUtilization?.();
|
|
1202
|
+
if (eventLoop) snapshot.eventLoop = { idle: eventLoop.idle, active: eventLoop.active };
|
|
1203
|
+
if (worker.osThreadId) snapshot.osThread = readOsThreadState(worker.osThreadId);
|
|
1204
|
+
return snapshot;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function describeThreadState(thread) {
|
|
1208
|
+
return `state=${thread.state ?? '?'} wchan=${thread.wchan ?? '?'} syscall=${thread.syscall ?? '?'}`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function describeSnapshot(snapshot) {
|
|
1212
|
+
const parts = [
|
|
1213
|
+
snapshot.sinceResourceReport === undefined
|
|
1214
|
+
? 'no resource report received'
|
|
1215
|
+
: `last resource report ${snapshot.sinceResourceReport}ms ago`,
|
|
1216
|
+
];
|
|
1217
|
+
if (snapshot.eventLoop)
|
|
1218
|
+
parts.push(
|
|
1219
|
+
`event loop active ${Math.round(snapshot.eventLoop.active)}ms idle ${Math.round(snapshot.eventLoop.idle)}ms`
|
|
1220
|
+
);
|
|
1221
|
+
const thread = snapshot.osThread;
|
|
1222
|
+
if (!thread) parts.push('os thread state unavailable');
|
|
1223
|
+
else
|
|
1224
|
+
parts.push(
|
|
1225
|
+
`os tid ${thread.tid} ${describeThreadState(thread)} cpuTicks=${thread.cpuTicks ?? '?'} ctxtSwitches=${thread.voluntarySwitches ?? '?'}/${thread.nonvoluntarySwitches ?? '?'}`
|
|
1226
|
+
);
|
|
1227
|
+
return parts.join('; ');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function describeDelta(before, after) {
|
|
1231
|
+
return Number.isFinite(before) && Number.isFinite(after) ? `+${after - before}` : '?';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function describeProgress(first, second) {
|
|
1235
|
+
const parts = [];
|
|
1236
|
+
if (first.eventLoop && second.eventLoop)
|
|
1237
|
+
parts.push(
|
|
1238
|
+
`event loop active +${Math.round(second.eventLoop.active - first.eventLoop.active)}ms idle +${Math.round(second.eventLoop.idle - first.eventLoop.idle)}ms`
|
|
1239
|
+
);
|
|
1240
|
+
const before = first.osThread;
|
|
1241
|
+
const after = second.osThread;
|
|
1242
|
+
if (before && after)
|
|
1243
|
+
parts.push(
|
|
1244
|
+
`cpuTicks ${describeDelta(before.cpuTicks, after.cpuTicks)} ctxtSwitches ${describeDelta(before.voluntarySwitches, after.voluntarySwitches)}/${describeDelta(before.nonvoluntarySwitches, after.nonvoluntarySwitches)} ${describeThreadState(after)}`
|
|
1245
|
+
);
|
|
1246
|
+
return parts.join('; ');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// A blocked event loop cannot report itself; two kernel-state samples a second apart separate
|
|
1250
|
+
// "parked on a lock" (no CPU ticks, no context switches) from "spinning".
|
|
1251
|
+
const STUCK_WORKER_SAMPLE_INTERVAL_MS = 1000;
|
|
1252
|
+
const STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS = 30000;
|
|
1253
|
+
function logStuckWorkerDiagnostics(worker) {
|
|
1254
|
+
if (!worker || !workers.includes(worker)) return;
|
|
1255
|
+
const now = Date.now();
|
|
1256
|
+
if (worker.stuckDiagnosticAt !== undefined && now - worker.stuckDiagnosticAt < STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS)
|
|
1257
|
+
return;
|
|
1258
|
+
worker.stuckDiagnosticAt = now;
|
|
1259
|
+
const threadId = worker.threadId; // Node resets it to -1 once the worker exits
|
|
1260
|
+
const first = snapshotWorkerThread(worker);
|
|
1261
|
+
harperLogger.warn(`Worker thread ${threadId} at ack timeout: ${describeSnapshot(first)}`);
|
|
1262
|
+
if (!first.eventLoop && !first.osThread) return;
|
|
1263
|
+
setTimeout(() => {
|
|
1264
|
+
if (!workers.includes(worker)) {
|
|
1265
|
+
harperLogger.warn(`Worker thread ${threadId} exited before its follow-up sample`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const second = snapshotWorkerThread(worker);
|
|
1269
|
+
// A recycled tid after a thread exit would attribute another thread's activity to this worker.
|
|
1270
|
+
if (second.osThread && second.osThread.startTime !== first.osThread?.startTime) second.osThread = undefined;
|
|
1271
|
+
harperLogger.warn(
|
|
1272
|
+
`Worker thread ${threadId} over the next ${second.at - first.at}ms: ${describeProgress(first, second) || 'no further state available'}`
|
|
1273
|
+
);
|
|
1274
|
+
}, STUCK_WORKER_SAMPLE_INTERVAL_MS).unref();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
806
1277
|
function sendThreadInfo(targetWorker) {
|
|
807
1278
|
targetWorker.postMessage({
|
|
808
1279
|
type: THREAD_INFO,
|
|
@@ -815,6 +1286,7 @@ function getChildWorkerInfo() {
|
|
|
815
1286
|
return workers.map((worker) => ({
|
|
816
1287
|
threadId: worker.threadId,
|
|
817
1288
|
name: worker.name,
|
|
1289
|
+
application: worker.application,
|
|
818
1290
|
heapTotal: worker.resources?.heapTotal,
|
|
819
1291
|
heapUsed: worker.resources?.heapUsed,
|
|
820
1292
|
externalMemory: worker.resources?.external,
|
|
@@ -875,6 +1347,8 @@ if (parentPort && workerData?.addPorts) {
|
|
|
875
1347
|
// parentPort so sendToThread(0, ...) and similar lookups can route back to main.
|
|
876
1348
|
parentPort.threadId = 0;
|
|
877
1349
|
addPort(parentPort);
|
|
1350
|
+
const osThreadId = getOsThreadId();
|
|
1351
|
+
if (osThreadId !== undefined) parentPort.postMessage({ type: OS_THREAD_ID, osThreadId });
|
|
878
1352
|
for (let i = 0, l = workerData.addPorts.length; i < l; i++) {
|
|
879
1353
|
let port = workerData.addPorts[i];
|
|
880
1354
|
port.threadId = workerData.addThreadIds[i];
|
|
@@ -922,28 +1396,74 @@ if (parentPort && workerData?.addPorts) {
|
|
|
922
1396
|
}, timeoutMs);
|
|
923
1397
|
}
|
|
924
1398
|
});
|
|
1399
|
+
let nextRunningIsolatedApplicationsRequestId = 0;
|
|
1400
|
+
getRunningIsolatedApplications = (timeoutMs) =>
|
|
1401
|
+
new Promise((resolve, reject) => {
|
|
1402
|
+
const requestId = ++nextRunningIsolatedApplicationsRequestId;
|
|
1403
|
+
let timeout;
|
|
1404
|
+
parentPort.on('message', receiveApplications);
|
|
1405
|
+
try {
|
|
1406
|
+
parentPort.postMessage({ type: REQUEST_RUNNING_ISOLATED_APPLICATIONS, requestId });
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
cleanup();
|
|
1409
|
+
reject(error);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
function receiveApplications(message) {
|
|
1413
|
+
if (message.type === RUNNING_ISOLATED_APPLICATIONS && message.requestId === requestId) {
|
|
1414
|
+
cleanup();
|
|
1415
|
+
resolve(message.applications);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function cleanup() {
|
|
1419
|
+
if (timeout) clearTimeout(timeout);
|
|
1420
|
+
parentPort.off('message', receiveApplications);
|
|
1421
|
+
}
|
|
1422
|
+
if (timeoutMs != null) {
|
|
1423
|
+
timeout = setTimeout(() => {
|
|
1424
|
+
cleanup();
|
|
1425
|
+
const error = new Error(`Timed out waiting for isolated application topology after ${timeoutMs}ms`);
|
|
1426
|
+
error.code = 'ERR_ISOLATED_APPLICATIONS_TIMEOUT';
|
|
1427
|
+
reject(error);
|
|
1428
|
+
}, timeoutMs);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
925
1431
|
let awaitTerminationRequestId = 0;
|
|
926
|
-
awaitProcessGroupTermination = (ownerThreadId) =>
|
|
1432
|
+
awaitProcessGroupTermination = (ownerThreadId, signal) =>
|
|
927
1433
|
new Promise((resolve) => {
|
|
928
1434
|
// Deliberately no timeout: a contender must not reclaim a dead owner's lock while its
|
|
929
1435
|
// process group might still be alive and mutating files, so this mirrors the unbounded
|
|
930
|
-
// wait Application.ts's waitForConfirmedTermination uses for the same reason.
|
|
1436
|
+
// wait Application.ts's waitForConfirmedTermination uses for the same reason. A caller
|
|
1437
|
+
// that needs its own bound (isThreadRunning) passes `signal` so this listener still gets
|
|
1438
|
+
// torn down when that caller gives up, instead of leaking for the life of the worker.
|
|
931
1439
|
const requestId = ++awaitTerminationRequestId;
|
|
932
1440
|
parentPort.on('message', receiveConfirmation);
|
|
1441
|
+
signal?.addEventListener('abort', cleanup, { once: true });
|
|
933
1442
|
parentPort.postMessage({ type: AWAIT_PROCESS_GROUP_TERMINATION, ownerThreadId, requestId });
|
|
1443
|
+
function cleanup() {
|
|
1444
|
+
parentPort.off('message', receiveConfirmation);
|
|
1445
|
+
}
|
|
934
1446
|
function receiveConfirmation(message) {
|
|
935
1447
|
if (message.type === PROCESS_GROUP_TERMINATION_CONFIRMED && message.requestId === requestId) {
|
|
936
|
-
|
|
1448
|
+
cleanup();
|
|
937
1449
|
resolve();
|
|
938
1450
|
}
|
|
939
1451
|
}
|
|
940
1452
|
});
|
|
941
1453
|
} else {
|
|
942
1454
|
getThreadInfo = getChildWorkerInfo;
|
|
1455
|
+
getRunningIsolatedApplications = isMainThread
|
|
1456
|
+
? () => runningIsolatedApplicationsGetter()
|
|
1457
|
+
: () => {
|
|
1458
|
+
const error = new Error('No channel to the main thread for isolated application topology');
|
|
1459
|
+
error.code = 'ERR_ISOLATED_APPLICATIONS_UNAVAILABLE';
|
|
1460
|
+
return Promise.reject(error);
|
|
1461
|
+
};
|
|
943
1462
|
awaitProcessGroupTermination = (ownerThreadId) =>
|
|
944
1463
|
pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
|
|
945
1464
|
}
|
|
946
1465
|
module.exports.getThreadInfo = getThreadInfo;
|
|
1466
|
+
module.exports.getRunningIsolatedApplications = getRunningIsolatedApplications;
|
|
947
1467
|
|
|
948
1468
|
// Listeners notified when a connected thread's port closes (worker exit/restart), so
|
|
949
1469
|
// modules holding per-thread state (e.g. registeredOperations' registry and in-flight
|
|
@@ -966,33 +1486,142 @@ const processGroupsByThread = new Map();
|
|
|
966
1486
|
// window component-preparation locking exists to close.
|
|
967
1487
|
const pendingProcessGroupTerminations = new Map();
|
|
968
1488
|
const PROCESS_GROUP_TERMINATION_POLL_MS = 25;
|
|
1489
|
+
const ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS = 1000;
|
|
1490
|
+
const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
|
|
1491
|
+
// Bounds isThreadRunning's own wait (below); Application.ts's waitForConfirmedTermination stays
|
|
1492
|
+
// deliberately unbounded.
|
|
1493
|
+
const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
|
|
1494
|
+
const zombieGroupScanTimes = new Map();
|
|
1495
|
+
const processGroupLivenessStates = new Map();
|
|
969
1496
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1497
|
+
function processGroupExists(processGroupId) {
|
|
1498
|
+
try {
|
|
1499
|
+
process.kill(-processGroupId, 0);
|
|
1500
|
+
return true;
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
return error.code === 'EPERM';
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function processProbeError(error) {
|
|
1507
|
+
return error?.code ?? error?.message ?? String(error ?? 'unknown error');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function processGroupLeaderState(processGroupId, platform, readStat) {
|
|
1511
|
+
if (platform !== 'linux') {
|
|
1512
|
+
return { state: 'unknown', reason: `zombie-process detection is unavailable on ${platform}` };
|
|
1513
|
+
}
|
|
979
1514
|
let stat;
|
|
980
1515
|
try {
|
|
981
|
-
stat =
|
|
982
|
-
} catch {
|
|
983
|
-
return
|
|
1516
|
+
stat = readStat(`/proc/${processGroupId}/stat`, 'utf8');
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH') return { state: 'missing' };
|
|
1519
|
+
return {
|
|
1520
|
+
state: 'unknown',
|
|
1521
|
+
reason: `reading /proc/${processGroupId}/stat failed (${processProbeError(error)})`,
|
|
1522
|
+
};
|
|
984
1523
|
}
|
|
985
|
-
|
|
986
|
-
|
|
1524
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1525
|
+
if (Number(fields[2]) !== processGroupId) return { state: 'missing' };
|
|
1526
|
+
return { state: fields[0] === 'Z' ? 'zombie' : 'alive' };
|
|
987
1527
|
}
|
|
988
1528
|
|
|
989
|
-
function
|
|
1529
|
+
function scanLinuxProcessGroup(processGroupId, readDirectory, readStat) {
|
|
1530
|
+
let processIds;
|
|
990
1531
|
try {
|
|
991
|
-
|
|
1532
|
+
processIds = readDirectory('/proc');
|
|
992
1533
|
} catch (error) {
|
|
993
|
-
return
|
|
1534
|
+
return {
|
|
1535
|
+
isAlive: null,
|
|
1536
|
+
reason: `reading /proc failed (${processProbeError(error)})`,
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
for (const processId of processIds) {
|
|
1540
|
+
if (!/^\d+$/.test(processId)) continue;
|
|
1541
|
+
let stat;
|
|
1542
|
+
try {
|
|
1543
|
+
stat = readStat(`/proc/${processId}/stat`, 'utf8');
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH') continue; // already gone
|
|
1546
|
+
// EACCES/EPERM (e.g. hidepid/ProtectProc) can only happen on a pid we don't own — we
|
|
1547
|
+
// spawned our own group's members as this same user, so their stat is always readable —
|
|
1548
|
+
// so it's not one of ours. Anything else is genuinely unknown, not "not ours".
|
|
1549
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') continue;
|
|
1550
|
+
return {
|
|
1551
|
+
isAlive: null,
|
|
1552
|
+
reason: `reading /proc/${processId}/stat failed (${processProbeError(error)})`,
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1556
|
+
const pgrp = Number(fields[2]);
|
|
1557
|
+
if (!Number.isFinite(pgrp)) {
|
|
1558
|
+
return { isAlive: null, reason: `/proc/${processId}/stat had an unparseable process-group field` };
|
|
1559
|
+
}
|
|
1560
|
+
if (pgrp !== processGroupId) continue;
|
|
1561
|
+
if (fields[0] !== 'Z') return { isAlive: true, reason: `a live member was observed at pid ${processId}` };
|
|
1562
|
+
}
|
|
1563
|
+
return { isAlive: false };
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function keepProcessGroupAlive(processGroupId, reason, observationTime, warn) {
|
|
1567
|
+
let livenessState = processGroupLivenessStates.get(processGroupId);
|
|
1568
|
+
if (!livenessState) {
|
|
1569
|
+
livenessState = { observedAt: observationTime, reason, warned: false };
|
|
1570
|
+
processGroupLivenessStates.set(processGroupId, livenessState);
|
|
1571
|
+
} else {
|
|
1572
|
+
livenessState.reason = reason;
|
|
994
1573
|
}
|
|
995
|
-
|
|
1574
|
+
if (!livenessState.warned && observationTime - livenessState.observedAt >= PROCESS_GROUP_LIVENESS_WARNING_MS) {
|
|
1575
|
+
livenessState.warned = true;
|
|
1576
|
+
warn(
|
|
1577
|
+
`Process group ${processGroupId} termination remains unconfirmed after ${PROCESS_GROUP_LIVENESS_WARNING_MS}ms: ${livenessState.reason}`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function clearProcessGroupLivenessState(processGroupId) {
|
|
1584
|
+
zombieGroupScanTimes.delete(processGroupId);
|
|
1585
|
+
processGroupLivenessStates.delete(processGroupId);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function isProcessGroupAlive(processGroupId, options) {
|
|
1589
|
+
const platform = options?.platform ?? process.platform;
|
|
1590
|
+
const groupExists = options?.processGroupExists ?? processGroupExists;
|
|
1591
|
+
const readDirectory = options?.readDirectory ?? readdirSync;
|
|
1592
|
+
const readStat = options?.readStat ?? readFileSync;
|
|
1593
|
+
const observationTime = options?.now?.() ?? performance.now();
|
|
1594
|
+
const warn = options?.warn ?? ((message) => harperLogger.warn(message));
|
|
1595
|
+
if (!groupExists(processGroupId)) {
|
|
1596
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
const leaderState = processGroupLeaderState(processGroupId, platform, readStat);
|
|
1600
|
+
if (leaderState.state === 'alive')
|
|
1601
|
+
return keepProcessGroupAlive(processGroupId, 'the process-group leader is still alive', observationTime, warn);
|
|
1602
|
+
if (leaderState.state === 'unknown')
|
|
1603
|
+
return keepProcessGroupAlive(processGroupId, leaderState.reason, observationTime, warn);
|
|
1604
|
+
const scanTime = observationTime;
|
|
1605
|
+
const lastScan = zombieGroupScanTimes.get(processGroupId);
|
|
1606
|
+
if (lastScan !== undefined && scanTime >= lastScan && scanTime - lastScan < ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS) {
|
|
1607
|
+
const reason = processGroupLivenessStates.get(processGroupId)?.reason ?? 'the previous Linux scan is still current';
|
|
1608
|
+
return keepProcessGroupAlive(processGroupId, reason, observationTime, warn);
|
|
1609
|
+
}
|
|
1610
|
+
zombieGroupScanTimes.set(processGroupId, scanTime);
|
|
1611
|
+
let scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1612
|
+
if (scanResult.isAlive !== false)
|
|
1613
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1614
|
+
// A second snapshot catches a member omitted after forking inside the first readdir/stat window.
|
|
1615
|
+
// It narrows rather than closes the race because the second synchronous pass has the same gap.
|
|
1616
|
+
scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1617
|
+
if (scanResult.isAlive !== false)
|
|
1618
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1619
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function processGroupIsAlive(processGroupId) {
|
|
1624
|
+
return isProcessGroupAlive(processGroupId);
|
|
996
1625
|
}
|
|
997
1626
|
|
|
998
1627
|
async function waitForProcessGroupExit(processGroupId) {
|
|
@@ -1074,6 +1703,7 @@ function addProcessGroup(ownerThreadId, processGroupId) {
|
|
|
1074
1703
|
}
|
|
1075
1704
|
|
|
1076
1705
|
function removeProcessGroup(ownerThreadId, processGroupId) {
|
|
1706
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1077
1707
|
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1078
1708
|
if (!processGroups) return;
|
|
1079
1709
|
processGroups.delete(processGroupId);
|
|
@@ -1125,12 +1755,48 @@ function unregisterProcessGroup(processGroupId) {
|
|
|
1125
1755
|
else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
|
|
1126
1756
|
}
|
|
1127
1757
|
|
|
1758
|
+
class ProcessGroupTerminationUnconfirmedError extends Error {
|
|
1759
|
+
code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
|
|
1760
|
+
constructor(ownerThreadId, timeoutMs) {
|
|
1761
|
+
super(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${timeoutMs}ms`);
|
|
1762
|
+
this.name = 'ProcessGroupTerminationUnconfirmedError';
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Bounds awaitProcessGroupTermination for isThreadRunning below. Throws rather than resolving to
|
|
1767
|
+
// "not running": componentPreparationLock's ownerIsAlive/ownerLivenessConfirmed already treat a
|
|
1768
|
+
// throwing isOwnerAlive as "can't confirm — don't steal the claim, don't renew the deadline", so
|
|
1769
|
+
// the lock's own bounded wait is what eventually fails the waiter.
|
|
1770
|
+
async function awaitConfirmedProcessGroupTermination(ownerThreadId) {
|
|
1771
|
+
const abortController = new AbortController();
|
|
1772
|
+
let timedOut = false;
|
|
1773
|
+
const timer = setTimeout(() => {
|
|
1774
|
+
timedOut = true;
|
|
1775
|
+
abortController.abort();
|
|
1776
|
+
}, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1777
|
+
timer.unref?.();
|
|
1778
|
+
try {
|
|
1779
|
+
await Promise.race([
|
|
1780
|
+
awaitProcessGroupTermination(ownerThreadId, abortController.signal),
|
|
1781
|
+
new Promise((resolve) => abortController.signal.addEventListener('abort', resolve, { once: true })),
|
|
1782
|
+
]);
|
|
1783
|
+
} finally {
|
|
1784
|
+
clearTimeout(timer);
|
|
1785
|
+
}
|
|
1786
|
+
if (timedOut) {
|
|
1787
|
+
harperLogger.warn(
|
|
1788
|
+
`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${THREAD_RUNNING_TERMINATION_BACKSTOP_MS}ms`
|
|
1789
|
+
);
|
|
1790
|
+
throw new ProcessGroupTerminationUnconfirmedError(ownerThreadId, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1128
1794
|
async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TIMEOUT_MS) {
|
|
1129
1795
|
if (ownerThreadId === threadId || ownerThreadId === 0) return true;
|
|
1130
1796
|
if ((await getThreadInfo(timeoutMs)).some((worker) => worker.threadId === ownerThreadId)) return true;
|
|
1131
1797
|
// The thread itself is gone, but it may still own process groups whose forced termination is
|
|
1132
1798
|
// in flight — wait for that to be confirmed before reporting the owner as reclaimable.
|
|
1133
|
-
await
|
|
1799
|
+
await awaitConfirmedProcessGroupTermination(ownerThreadId);
|
|
1134
1800
|
return false;
|
|
1135
1801
|
}
|
|
1136
1802
|
|
|
@@ -1140,6 +1806,15 @@ if (isMainThread) {
|
|
|
1140
1806
|
});
|
|
1141
1807
|
}
|
|
1142
1808
|
|
|
1809
|
+
/**
|
|
1810
|
+
* Whether a thread has already been reported dead. Sync, unlike `isThreadRunning`, because it only
|
|
1811
|
+
* reads the tombstone `notifyThreadExit` records below — callers on the exit path need an answer
|
|
1812
|
+
* without awaiting process-group confirmation.
|
|
1813
|
+
*/
|
|
1814
|
+
function hasThreadExited(threadId) {
|
|
1815
|
+
return notifiedDeadThreadIds.has(threadId);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1143
1818
|
function notifyThreadExit(deadThreadId) {
|
|
1144
1819
|
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId)) return;
|
|
1145
1820
|
notifiedDeadThreadIds.add(deadThreadId);
|
|
@@ -1247,23 +1922,49 @@ if (isMainThread) {
|
|
|
1247
1922
|
const ignoredPaths = ['node_modules', '.git'];
|
|
1248
1923
|
const watchDir = async (dir, beforeRestartCallback) => {
|
|
1249
1924
|
if (beforeRestartCallback) beforeRestart = beforeRestartCallback;
|
|
1250
|
-
|
|
1251
|
-
|
|
1925
|
+
const watchTarget = resolveWatchTarget(dir);
|
|
1926
|
+
let usingPolling = watchTarget.mustPoll;
|
|
1927
|
+
let liveWatcher;
|
|
1928
|
+
const openWatcher = () => {
|
|
1929
|
+
const opened = (liveWatcher = guardedWatch(watchTarget.path, {
|
|
1252
1930
|
persistent: false,
|
|
1931
|
+
...(usingPolling ? DIRECTORY_POLLING_FALLBACK_OPTIONS : {}),
|
|
1253
1932
|
ignored: (path) => {
|
|
1254
1933
|
return ignoredPaths.some((ignoredPath) => path.includes(ignoredPath));
|
|
1255
1934
|
},
|
|
1256
|
-
})
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1935
|
+
}));
|
|
1936
|
+
opened
|
|
1937
|
+
// This runs on the thread that owns every worker, and chokidar emits 'error' unguarded for
|
|
1938
|
+
// anything but ENOENT/ENOTDIR.
|
|
1939
|
+
.on('error', (error) => {
|
|
1940
|
+
if (claimLostNativeWatchError(error)) return;
|
|
1941
|
+
if (isWatcherExhaustionError(error)) {
|
|
1942
|
+
if (usingPolling || liveWatcher !== opened) return;
|
|
1943
|
+
warnWatcherFallback(dir);
|
|
1944
|
+
usingPolling = true;
|
|
1945
|
+
Promise.resolve()
|
|
1946
|
+
.then(() => opened.close())
|
|
1947
|
+
.catch(() => {})
|
|
1948
|
+
.then(openWatcher)
|
|
1949
|
+
.catch((reopenError) =>
|
|
1950
|
+
console.error(`Could not reopen the ${dir} component-reload watch on polling:`, reopenError)
|
|
1951
|
+
);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
console.error(`Error watching ${dir} for component reloads:`, error);
|
|
1955
|
+
})
|
|
1956
|
+
.on('change', (path) => {
|
|
1957
|
+
changedFiles.add(path);
|
|
1958
|
+
if (queuedRestart) clearTimeout(queuedRestart);
|
|
1959
|
+
queuedRestart = setTimeout(async () => {
|
|
1960
|
+
if (beforeRestart) await beforeRestart();
|
|
1961
|
+
await restartWorkers(undefined, undefined, true, null, '*');
|
|
1962
|
+
console.log('Reloaded Harper components, changed files:', Array.from(changedFiles));
|
|
1963
|
+
changedFiles.clear();
|
|
1964
|
+
}, 100);
|
|
1965
|
+
});
|
|
1966
|
+
};
|
|
1967
|
+
openWatcher();
|
|
1267
1968
|
};
|
|
1268
1969
|
module.exports.watchDir = watchDir;
|
|
1269
1970
|
if (process.env.WATCH_DIR) watchDir(process.env.WATCH_DIR);
|
|
@@ -1279,3 +1980,7 @@ if (isMainThread) {
|
|
|
1279
1980
|
realExit(0);
|
|
1280
1981
|
});
|
|
1281
1982
|
}
|
|
1983
|
+
|
|
1984
|
+
// Required here, not from logging, which must never import this module; last in the file because it
|
|
1985
|
+
// calls onThreadExit.
|
|
1986
|
+
require('./logRotationTransport.ts');
|