@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const { realExit } = require("./workerProcessGuard.js");
|
|
6
6
|
const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
|
|
7
7
|
const { spawn, spawnSync } = require('node:child_process');
|
|
8
|
-
const { readFileSync } = require('node:fs');
|
|
8
|
+
const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
|
|
9
9
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
10
10
|
const { join, isAbsolute, extname } = require('path');
|
|
11
11
|
const { pathToFileURL } = require('url');
|
|
@@ -19,7 +19,10 @@ const { randomBytes } = require('crypto');
|
|
|
19
19
|
const { _assignPackageExport } = require('../../globals.js');
|
|
20
20
|
const { PACKAGE_ROOT } = require('../../utility/packageUtils.js');
|
|
21
21
|
const { resolvePreloadModules } = require("./resolvePreload.js");
|
|
22
|
+
const { resolveThreadHeapMemoryMb } = require("./threadHeapMemory.js");
|
|
22
23
|
const { getConfigPath } = require("../../config/configUtils.js");
|
|
24
|
+
const { resolveWatchTarget } = require("../../utility/watchPath.js");
|
|
25
|
+
const { DIRECTORY_POLLING_FALLBACK_OPTIONS, claimLostNativeWatchError, guardedWatch, isWatcherExhaustionError, warnWatcherFallback, } = require("../../utility/watcherFallback.js");
|
|
23
26
|
let importModules;
|
|
24
27
|
function getImportModules() {
|
|
25
28
|
if (importModules === undefined)
|
|
@@ -32,7 +35,6 @@ function getRequireModules() {
|
|
|
32
35
|
requireModules = resolvePreloadModules(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_PRELOADREQUIRE), getConfigPath(hdbTerms.CONFIG_PARAMS.COMPONENTSROOT), 'threads.preloadRequire');
|
|
33
36
|
return requireModules;
|
|
34
37
|
}
|
|
35
|
-
const chokidar = require('chokidar');
|
|
36
38
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
37
39
|
const MB = 1024 * 1024;
|
|
38
40
|
const workers = []; // these are our child workers that we are managing
|
|
@@ -44,9 +46,14 @@ const MAX_UNEXPECTED_RESTARTS = 50;
|
|
|
44
46
|
// down mid-disposal crashes the process, so give that teardown more room before the forced backstop.
|
|
45
47
|
let threadTerminationTimeout = process.env.DEV_MODE === 'true' || process.env.DEV_MODE === '1' ? 30000 : 10000;
|
|
46
48
|
const RESTART_TYPE = 'restart';
|
|
49
|
+
const RESTART_PROGRESS_HEARTBEAT_MS = 15000;
|
|
47
50
|
const REQUEST_THREAD_INFO = 'request_thread_info';
|
|
51
|
+
const REQUEST_RUNNING_ISOLATED_APPLICATIONS = 'request-running-isolated-applications';
|
|
48
52
|
const RESOURCE_REPORT = 'resource_report';
|
|
53
|
+
const OS_THREAD_ID = 'os-thread-id';
|
|
54
|
+
const STUCK_WORKER_REPORT = 'stuck-worker-report';
|
|
49
55
|
const THREAD_INFO = 'thread_info';
|
|
56
|
+
const RUNNING_ISOLATED_APPLICATIONS = 'running-isolated-applications';
|
|
50
57
|
const ADDED_PORT = 'added-port';
|
|
51
58
|
const ACKNOWLEDGEMENT = 'ack';
|
|
52
59
|
const REMOVE_PORT = 'remove-port';
|
|
@@ -64,6 +71,7 @@ const AWAIT_PROCESS_GROUP_TERMINATION = 'await-process-group-termination';
|
|
|
64
71
|
const PROCESS_GROUP_TERMINATION_CONFIRMED = 'process-group-termination-confirmed';
|
|
65
72
|
const THREAD_INFO_REQUEST_TIMEOUT_MS = 1000;
|
|
66
73
|
let getThreadInfo;
|
|
74
|
+
let getRunningIsolatedApplications;
|
|
67
75
|
let awaitProcessGroupTermination;
|
|
68
76
|
// Worker-side backstop that force-exits if the graceful shutdown sequence doesn't finish in time.
|
|
69
77
|
let selfExitTimer;
|
|
@@ -127,6 +135,19 @@ module.exports = {
|
|
|
127
135
|
workers,
|
|
128
136
|
setMonitorListener,
|
|
129
137
|
onMessageFromWorkers,
|
|
138
|
+
setIsolatedWorkerReconciler,
|
|
139
|
+
setRunningIsolatedApplicationsGetter,
|
|
140
|
+
isApplicationPrimaryWorker,
|
|
141
|
+
applicationWorkerIndex,
|
|
142
|
+
runsApplicationCodeSingletons,
|
|
143
|
+
isDedicatedWorker,
|
|
144
|
+
markBranchStorePath,
|
|
145
|
+
ownsStoreMaintenance,
|
|
146
|
+
ownsStoreExpiration,
|
|
147
|
+
workersForApplication,
|
|
148
|
+
stopWorker,
|
|
149
|
+
encodeRestartScope,
|
|
150
|
+
decodeRestartScope,
|
|
130
151
|
onMessageByType,
|
|
131
152
|
broadcast,
|
|
132
153
|
broadcastWithAcknowledgement,
|
|
@@ -141,8 +162,11 @@ module.exports = {
|
|
|
141
162
|
beginProcessShutdown,
|
|
142
163
|
registerWorkerDataProvider,
|
|
143
164
|
onThreadExit,
|
|
165
|
+
hasThreadExited,
|
|
166
|
+
notifyThreadExit,
|
|
144
167
|
registerProcessGroup,
|
|
145
168
|
unregisterProcessGroup,
|
|
169
|
+
isProcessGroupAlive,
|
|
146
170
|
isThreadRunning,
|
|
147
171
|
waitUntilConfirmedGone,
|
|
148
172
|
restartNumber: workerData?.restartNumber || 1,
|
|
@@ -172,6 +196,9 @@ connectedPorts.sendToThread = function (threadId, message) {
|
|
|
172
196
|
throw err;
|
|
173
197
|
}
|
|
174
198
|
};
|
|
199
|
+
// Direct thread-to-thread send, so a worker can reach a sibling (e.g. the record lock owner worker)
|
|
200
|
+
// without a hop through main. Returns false when no port for the thread is connected.
|
|
201
|
+
module.exports.sendToThread = connectedPorts.sendToThread;
|
|
175
202
|
module.exports.whenThreadsStarted = new Promise((resolve) => {
|
|
176
203
|
module.exports.threadsHaveStarted = resolve;
|
|
177
204
|
});
|
|
@@ -186,6 +213,106 @@ function setTerminateTimeout(newTimeout) {
|
|
|
186
213
|
function getWorkerIndex() {
|
|
187
214
|
return workerData ? workerData.workerIndex : isMainWorker ? 0 : undefined;
|
|
188
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* The worker that owns `applicationName`'s per-application singletons (its scheduled jobs, its data
|
|
218
|
+
* files): pool worker 0 for a shared application and for root-level plugins (which pass no name), the
|
|
219
|
+
* dedicated worker for an isolated one -- never index 0, so node-wide duties stay on the pool, and
|
|
220
|
+
* never a dedicated worker for anything but its own application.
|
|
221
|
+
*/
|
|
222
|
+
function isApplicationPrimaryWorker(applicationName) {
|
|
223
|
+
if (workerData?.isolatedApplication !== undefined)
|
|
224
|
+
return applicationName === workerData.isolatedApplication;
|
|
225
|
+
return getWorkerIndex() === 0;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Stores that exist only on this thread: the branch stores an isolated application opened in its
|
|
229
|
+
* dedicated worker. Registered by openBranchDatabase, so the ownership predicates below can tell them
|
|
230
|
+
* from the shared stores every thread has open.
|
|
231
|
+
*/
|
|
232
|
+
const branchStorePaths = new Set();
|
|
233
|
+
function markBranchStorePath(path, isBranch = true) {
|
|
234
|
+
if (isBranch)
|
|
235
|
+
branchStorePaths.add(path);
|
|
236
|
+
else
|
|
237
|
+
branchStorePaths.delete(path);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether this thread runs the "last worker" per-store maintenance for `storePath` (TTL scans, storage
|
|
241
|
+
* reclamation, audit cleanup). Shared stores belong to the last pool worker; a dedicated worker
|
|
242
|
+
* maintains only its own branch stores, which no other thread has open.
|
|
243
|
+
*/
|
|
244
|
+
function ownsStoreMaintenance(storePath) {
|
|
245
|
+
if (workerData?.isolatedApplication !== undefined)
|
|
246
|
+
return branchStorePaths.has(storePath);
|
|
247
|
+
return getWorkerIndex() === getWorkerCount() - 1;
|
|
248
|
+
}
|
|
249
|
+
/** The "worker 0" counterpart: expiration eviction for `storePath`. */
|
|
250
|
+
function ownsStoreExpiration(storePath) {
|
|
251
|
+
if (workerData?.isolatedApplication !== undefined)
|
|
252
|
+
return branchStorePaths.has(storePath);
|
|
253
|
+
return getWorkerIndex() === 0;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether a singleton set up by APPLICATION CODE runs here (a caching table's `sourcedFrom`
|
|
257
|
+
* subscription): pool worker 0, or a dedicated worker -- the only code that runs on one is its own
|
|
258
|
+
* application's, so whatever it sets up is that application's to run. Not for root-level plugins,
|
|
259
|
+
* which load on every thread and must use isApplicationPrimaryWorker(name).
|
|
260
|
+
*/
|
|
261
|
+
function runsApplicationCodeSingletons() {
|
|
262
|
+
return getWorkerIndex() === 0 || workerData?.isolatedApplication !== undefined;
|
|
263
|
+
}
|
|
264
|
+
/** Whether this thread is a worker dedicated to one isolated application. */
|
|
265
|
+
function isDedicatedWorker() {
|
|
266
|
+
return workerData?.isolatedApplication !== undefined;
|
|
267
|
+
}
|
|
268
|
+
/** The worker index as the application sees it: a dedicated worker is its application's worker 0. */
|
|
269
|
+
function applicationWorkerIndex() {
|
|
270
|
+
return workerData?.isolatedApplication !== undefined ? 0 : getWorkerIndex();
|
|
271
|
+
}
|
|
272
|
+
/** Every started worker dedicated to `application`, including a replacement still booting. */
|
|
273
|
+
function workersForApplication(application) {
|
|
274
|
+
return workers.filter((worker) => worker.application === application);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Stop one worker for good: shut it down, honour the drain extension it may ask for, force it after
|
|
278
|
+
* the same backstop the rolling restart uses (FORCE_EXIT on Bun, where terminate() segfaults), and
|
|
279
|
+
* resolve once it has exited. Marked as shut down first so its exit does not start a replacement.
|
|
280
|
+
*/
|
|
281
|
+
function stopWorker(worker) {
|
|
282
|
+
worker.wasShutdown = true;
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const armTerminate = (delay) => setTimeout(() => {
|
|
285
|
+
harperLogger.warn('Thread did not voluntarily terminate, terminating from the outside', worker.threadId);
|
|
286
|
+
if (isBun) {
|
|
287
|
+
try {
|
|
288
|
+
worker.postMessage({ type: FORCE_EXIT });
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
worker.terminate();
|
|
294
|
+
}
|
|
295
|
+
}, delay).unref();
|
|
296
|
+
let timeout = armTerminate(threadTerminationTimeout * 2);
|
|
297
|
+
worker.extendTerminateDeadline = (deadlineMs) => {
|
|
298
|
+
clearTimeout(timeout);
|
|
299
|
+
const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require("../../components/shutdownDrain.js");
|
|
300
|
+
timeout = armTerminate(boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs()));
|
|
301
|
+
};
|
|
302
|
+
worker.on('exit', () => {
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
worker.extendTerminateDeadline = undefined;
|
|
305
|
+
resolve();
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
worker.postMessage({ restartNumber: module.exports.restartNumber, type: hdbTerms.ITC_EVENT_TYPES.SHUTDOWN });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
resolve(); // already gone
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
189
316
|
function getWorkerCount() {
|
|
190
317
|
return workerData ? workerData.workerCount : isMainWorker ? 1 : undefined;
|
|
191
318
|
}
|
|
@@ -221,6 +348,7 @@ const RESERVED_WORKER_DATA_KEYS = [
|
|
|
221
348
|
'processIncarnation',
|
|
222
349
|
'ticketKeys',
|
|
223
350
|
'noServerStart',
|
|
351
|
+
'isolatedApplication',
|
|
224
352
|
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
225
353
|
];
|
|
226
354
|
const workerDataProviders = new Map();
|
|
@@ -280,14 +408,17 @@ function getTicketKeys() {
|
|
|
280
408
|
ticketKeys = isMainThread ? randomBytes(48) : workerData.ticketKeys;
|
|
281
409
|
return ticketKeys;
|
|
282
410
|
}
|
|
411
|
+
// What application code sees: a dedicated worker is its application's only worker, index 0 of 1, so
|
|
412
|
+
// the documented partitioning idiom (`hash % server.workerCount === server.workerIndex`) covers
|
|
413
|
+
// everything there. Node-wide duties keep using the raw getWorkerIndex/getWorkerCount.
|
|
283
414
|
Object.defineProperty(server, 'workerIndex', {
|
|
284
415
|
get() {
|
|
285
|
-
return
|
|
416
|
+
return applicationWorkerIndex();
|
|
286
417
|
},
|
|
287
418
|
});
|
|
288
419
|
Object.defineProperty(server, 'workerCount', {
|
|
289
420
|
get() {
|
|
290
|
-
return getWorkerCount();
|
|
421
|
+
return workerData?.isolatedApplication !== undefined ? 1 : getWorkerCount();
|
|
291
422
|
},
|
|
292
423
|
});
|
|
293
424
|
if (!parentPort) {
|
|
@@ -295,10 +426,29 @@ if (!parentPort) {
|
|
|
295
426
|
if (worker)
|
|
296
427
|
sendThreadInfo(worker);
|
|
297
428
|
});
|
|
429
|
+
onMessageByType(REQUEST_RUNNING_ISOLATED_APPLICATIONS, (message, worker) => {
|
|
430
|
+
if (worker)
|
|
431
|
+
worker.postMessage({
|
|
432
|
+
type: RUNNING_ISOLATED_APPLICATIONS,
|
|
433
|
+
requestId: message.requestId,
|
|
434
|
+
applications: runningIsolatedApplicationsGetter(),
|
|
435
|
+
});
|
|
436
|
+
});
|
|
298
437
|
onMessageByType(RESOURCE_REPORT, (message, worker) => {
|
|
299
438
|
if (worker)
|
|
300
439
|
recordResourceReport(worker, message);
|
|
301
440
|
});
|
|
441
|
+
onMessageByType(OS_THREAD_ID, (message, worker) => {
|
|
442
|
+
if (worker)
|
|
443
|
+
worker.osThreadId = message.osThreadId;
|
|
444
|
+
});
|
|
445
|
+
onMessageByType(STUCK_WORKER_REPORT, (message) => {
|
|
446
|
+
for (const threadId of message.threadIds) {
|
|
447
|
+
const worker = workers.find((worker) => worker.threadId === threadId);
|
|
448
|
+
if (worker)
|
|
449
|
+
logStuckWorkerDiagnostics(worker);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
302
452
|
onMessageByType(AWAIT_PROCESS_GROUP_TERMINATION, async (message, worker) => {
|
|
303
453
|
if (!worker)
|
|
304
454
|
return;
|
|
@@ -311,6 +461,7 @@ if (!parentPort) {
|
|
|
311
461
|
}
|
|
312
462
|
// postMessage type listeners that are registered in other ways or can be registered later
|
|
313
463
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED, null);
|
|
464
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTUP_PHASE, null);
|
|
314
465
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.SCHEMA, null);
|
|
315
466
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.USER, null);
|
|
316
467
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.COMPONENT_STATUS_REQUEST, null);
|
|
@@ -321,11 +472,12 @@ listenersByType.set(hdbTerms.ITC_EVENT_TYPES.MIDDLEWARE_CHAINS_RESPONSE, null);
|
|
|
321
472
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_REGISTERED, null);
|
|
322
473
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_REQUEST, null);
|
|
323
474
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.OPERATION_EXECUTE_RESPONSE, null);
|
|
324
|
-
//
|
|
475
|
+
// These request/response functions register their own one-shot parentPort listener per
|
|
325
476
|
// call rather than going through onMessageByType, so without this, every reply would also reach
|
|
326
477
|
// addPort's permanent dispatcher as an "unregistered" type: notifyMessageListeners would warn and
|
|
327
478
|
// queue each one in messagesQueuedByType forever, and a lock-wait polls every 50ms.
|
|
328
479
|
listenersByType.set(THREAD_INFO, null);
|
|
480
|
+
listenersByType.set(RUNNING_ISOLATED_APPLICATIONS, null);
|
|
329
481
|
listenersByType.set(PROCESS_GROUP_TERMINATION_CONFIRMED, null);
|
|
330
482
|
function startWorker(path, options = {}) {
|
|
331
483
|
if (processShuttingDown) {
|
|
@@ -346,8 +498,8 @@ function startWorker(path, options = {}) {
|
|
|
346
498
|
let availableMemory = process.constrainedMemory?.() || totalmem(); // used constrained memory if it is available
|
|
347
499
|
// and lower than total memory
|
|
348
500
|
availableMemory = Math.min(availableMemory, totalmem(), 20000 * MB);
|
|
349
|
-
const maxOldMemory = envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY) ??
|
|
350
|
-
Math.max(Math.floor(availableMemory / MB / (10 + (options.threadCount || 1) / 4)), 512);
|
|
501
|
+
const maxOldMemory = resolveThreadHeapMemoryMb(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY)) ??
|
|
502
|
+
Math.max(Math.floor(availableMemory / MB / (10 + (options.heapShareCount || options.threadCount || 1) / 4)), 512);
|
|
351
503
|
// Max young memory space (semi-space for scavenger) is 1/128 of max memory (limited to 16-64). For most of our m5
|
|
352
504
|
// machines this will be 64MB (less for t3's). This is based on recommendations from:
|
|
353
505
|
// https://www.alibabacloud.com/blog/node-js-application-troubleshooting-manual---comprehensive-gc-problems-and-optimization594965
|
|
@@ -391,6 +543,9 @@ function startWorker(path, options = {}) {
|
|
|
391
543
|
for (const requirePath of getRequireModules())
|
|
392
544
|
execArgv.push('--require', requirePath);
|
|
393
545
|
}
|
|
546
|
+
// Only a start that declares the serving topology may write it; see DESIGN.md on the two workerCounts.
|
|
547
|
+
if (typeof options.threadCount === 'number')
|
|
548
|
+
workerCount = options.threadCount;
|
|
394
549
|
const worker = new Worker(isAbsolute(path) ? path : join(PACKAGE_ROOT, path), {
|
|
395
550
|
resourceLimits: {
|
|
396
551
|
maxOldGenerationSizeMb: maxOldMemory,
|
|
@@ -405,8 +560,9 @@ function startWorker(path, options = {}) {
|
|
|
405
560
|
addThreadIds: channelsToConnect.map((channel) => channel.existingPort.threadId),
|
|
406
561
|
addPortIsJobWorkers: channelsToConnect.map((channel) => channel.existingPort.isJobWorker === true),
|
|
407
562
|
workerIndex: options.workerIndex,
|
|
408
|
-
workerCount:
|
|
563
|
+
workerCount: options.threadCount,
|
|
409
564
|
name: options.name,
|
|
565
|
+
isolatedApplication: options.application,
|
|
410
566
|
restartNumber: module.exports.restartNumber,
|
|
411
567
|
processIncarnation: module.exports.processIncarnation,
|
|
412
568
|
ticketKeys: getTicketKeys(),
|
|
@@ -439,14 +595,19 @@ function startWorker(path, options = {}) {
|
|
|
439
595
|
});
|
|
440
596
|
worker.on('exit', (_code) => {
|
|
441
597
|
workers.splice(workers.indexOf(worker), 1);
|
|
442
|
-
if (!processShuttingDown &&
|
|
598
|
+
if (!processShuttingDown &&
|
|
599
|
+
!worker.wasShutdown &&
|
|
600
|
+
options.autoRestart !== false &&
|
|
601
|
+
options.shouldAutoRestart?.(worker) !== false) {
|
|
443
602
|
// if this wasn't an intentional shutdown, restart now (unless we have tried too many times)
|
|
444
603
|
if (worker.unexpectedRestarts < MAX_UNEXPECTED_RESTARTS) {
|
|
445
604
|
options.unexpectedRestarts = worker.unexpectedRestarts + 1;
|
|
446
605
|
startWorker(path, options);
|
|
447
606
|
}
|
|
448
|
-
else
|
|
449
|
-
harperLogger.error(`Thread has been restarted ${worker.
|
|
607
|
+
else {
|
|
608
|
+
harperLogger.error(`Thread has been restarted ${worker.unexpectedRestarts} times and will not be restarted`);
|
|
609
|
+
options.onRestartExhausted?.(worker);
|
|
610
|
+
}
|
|
450
611
|
}
|
|
451
612
|
});
|
|
452
613
|
workers.push(worker);
|
|
@@ -454,6 +615,7 @@ function startWorker(path, options = {}) {
|
|
|
454
615
|
if (options.onStarted)
|
|
455
616
|
options.onStarted(worker); // notify that it is ready
|
|
456
617
|
worker.name = options.name;
|
|
618
|
+
worker.application = options.application; // the isolated application this worker is dedicated to, if any
|
|
457
619
|
return worker;
|
|
458
620
|
}
|
|
459
621
|
const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
|
|
@@ -465,13 +627,27 @@ const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
|
|
|
465
627
|
* threads at the same time we shutdown new ones. However, we usually want to limit how many we do at once to avoid
|
|
466
628
|
* excessive load and to keep things responsive. This parameter throttles the restarts to minimize load from
|
|
467
629
|
* thread startups.
|
|
468
|
-
* @
|
|
630
|
+
* @param onProgress Called each time a worker has been replaced (or its replacement has been given
|
|
631
|
+
* up on), so a caller waiting on a wide pool can tell a slow restart from a stalled one.
|
|
632
|
+
* @returns {Promise<{workersKeptOnOldCode: number, replacementsNotStarted: number}|{declined: true}|undefined>}
|
|
633
|
+
* from the main thread, how many workers were left running the old code and how many replacements
|
|
634
|
+
* never reported that they started — or `{declined: true}` when the process is already shutting down;
|
|
635
|
+
* from a worker, nothing — the restart is handed to the main thread.
|
|
636
|
+
*/
|
|
637
|
+
/**
|
|
638
|
+
* `application` selects which workers of the type restart: omitted preserves the historic all-worker
|
|
639
|
+
* behavior, explicit undefined restarts the shared pool only, a name restarts only that application's
|
|
640
|
+
* dedicated worker, and '*' restarts all.
|
|
469
641
|
*/
|
|
470
642
|
async function restartWorkers(name = null, maxWorkersDown = Math.max(Math.floor(workerCount / 8), 1), // restart 1/8 of the threads at a time, but at least 1
|
|
471
|
-
startReplacementThreads = true) {
|
|
643
|
+
startReplacementThreads = true, onProgress = null, application = undefined) {
|
|
644
|
+
if (arguments.length < 5)
|
|
645
|
+
application = '*';
|
|
472
646
|
if (isMainThread) {
|
|
647
|
+
// Declining is not the same as delegating: a caller reporting on the restart must not read this
|
|
648
|
+
// as "another thread is completing it".
|
|
473
649
|
if (processShuttingDown && startReplacementThreads)
|
|
474
|
-
return;
|
|
650
|
+
return { declined: true };
|
|
475
651
|
try {
|
|
476
652
|
// we do this because it is possible for a component to chdir to itself, get re-deployed and then the cwd
|
|
477
653
|
// inode link is invalid and it can cause a lot of problems. But process.cwd() still returns the path, for
|
|
@@ -481,22 +657,65 @@ startReplacementThreads = true) {
|
|
|
481
657
|
catch (e) {
|
|
482
658
|
harperLogger.error('Unable to reestablish current working directory', e);
|
|
483
659
|
}
|
|
660
|
+
const freshlyStarted = new Set(); // dedicated workers the reconcile just started: already on the new code
|
|
484
661
|
// problematic cyclic dependency, bind late
|
|
485
662
|
const { resetRestartNeeded } = require("../../components/requestRestart.js");
|
|
486
|
-
|
|
663
|
+
// One process-wide bit cannot say WHICH application is pending, so it may only be cleared by a
|
|
664
|
+
// restart that demonstrably covers every worker the bit could stand for. A restart scoped to one
|
|
665
|
+
// application leaves the whole pool on its old code; a pool restart replaces every pool worker and
|
|
666
|
+
// reconciles dedicated slots, but leaves an already-running dedicated worker on its old modules.
|
|
667
|
+
// Clearing from either would report restartRequired: false while a deployed component is still
|
|
668
|
+
// loaded nowhere. So: all workers always, the pool only while no dedicated worker is running --
|
|
669
|
+
// which is every node that uses no isolated application, i.e. the historic behavior unchanged.
|
|
670
|
+
const coversEveryWorker = application === '*' || (application === undefined && !workers.some((worker) => worker.application));
|
|
671
|
+
if (coversEveryWorker)
|
|
672
|
+
resetRestartNeeded();
|
|
487
673
|
// This is here to prevent circular dependencies
|
|
488
674
|
if (startReplacementThreads) {
|
|
489
675
|
const { loadRootComponents } = require('../loadRootComponents.js');
|
|
490
|
-
|
|
676
|
+
// Installing and loading every root component reports nothing and can outlast a caller's idle
|
|
677
|
+
// window on its own (a cold npm cache, a large dependency graph), so beat while it runs. The
|
|
678
|
+
// caller's absolute ceiling is what bounds a load that never finishes.
|
|
679
|
+
const loading = setInterval(() => onProgress?.(), RESTART_PROGRESS_HEARTBEAT_MS).unref();
|
|
680
|
+
try {
|
|
681
|
+
await loadRootComponents();
|
|
682
|
+
// isolated applications added or removed by the reload get their dedicated worker started or
|
|
683
|
+
// stopped; a crash-looping newcomer can take a while, so the heartbeat runs through this too
|
|
684
|
+
try {
|
|
685
|
+
for (const application of (await isolatedWorkerReconciler?.()) ?? [])
|
|
686
|
+
freshlyStarted.add(application);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
harperLogger.error('Could not reconcile isolated application workers', error);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
clearInterval(loading);
|
|
694
|
+
}
|
|
695
|
+
onProgress?.();
|
|
491
696
|
}
|
|
492
697
|
module.exports.restartNumber++;
|
|
493
|
-
|
|
698
|
+
// `Infinity` is shutdownWorkers' "all at once" sentinel and must survive.
|
|
699
|
+
if (typeof maxWorkersDown !== 'number' || Number.isNaN(maxWorkersDown)) {
|
|
700
|
+
maxWorkersDown = 1;
|
|
701
|
+
}
|
|
702
|
+
else if (maxWorkersDown < 1) {
|
|
494
703
|
// we accept a ratio of workers, and compute absolute maximum being down at a time from the total number of
|
|
495
704
|
// threads
|
|
496
705
|
maxWorkersDown = maxWorkersDown * workers.length;
|
|
497
706
|
}
|
|
498
707
|
// make a copy of the workers before iterating them, as the workers array mutates a lot during this
|
|
499
|
-
let waitingToFinish = []; // promises for workers we
|
|
708
|
+
let waitingToFinish = []; // promises for workers we are replacing, spliced as each is replaced
|
|
709
|
+
// Every replacement that was started without being awaited first, so this function can still
|
|
710
|
+
// resolve only once each one is accepting connections.
|
|
711
|
+
let replacementsStarting = [];
|
|
712
|
+
// A replacement that never came up leaves the pool in one of two very different states, and a
|
|
713
|
+
// caller waiting on this restart needs them apart: the pre-start path keeps the *old* worker
|
|
714
|
+
// serving old code, while a replacement that exits after its predecessor is already gone only
|
|
715
|
+
// costs capacity until startWorker's auto-restart brings a fresh one up.
|
|
716
|
+
let workersKeptOnOldCode = 0;
|
|
717
|
+
let replacementsNotStarted = 0;
|
|
718
|
+
let replacementsFailedToStart = 0;
|
|
500
719
|
// We can only start the replacement *before* the old worker releases its port when the OS lets
|
|
501
720
|
// both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT),
|
|
502
721
|
// macOS (unreliable SO_REUSEPORT, so workers bind exclusively), and Bun — the replacement can't
|
|
@@ -507,14 +726,21 @@ startReplacementThreads = true) {
|
|
|
507
726
|
// replacement — an unavoidable brief gap, but only for worker-owned listeners, since the main
|
|
508
727
|
// thread keeps serving the HTTP ports throughout. This ordering is also what lets
|
|
509
728
|
// listenOnPorts() treat a dedicated listener's EADDRINUSE as an external conflict.
|
|
510
|
-
const
|
|
511
|
-
|
|
729
|
+
const platformCanPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
|
|
730
|
+
const restarting = workers.slice(0);
|
|
731
|
+
for (let index = 0; index < restarting.length; index++) {
|
|
732
|
+
const worker = restarting[index];
|
|
512
733
|
// Terminal shutdown: stop replacing workers mid-loop — the guard for every replacement start below.
|
|
513
734
|
if (processShuttingDown && startReplacementThreads)
|
|
514
735
|
break;
|
|
515
736
|
if ((name && worker.name !== name) || worker.wasShutdown)
|
|
516
737
|
continue; // filter by type, if specified
|
|
738
|
+
if (application !== '*' && worker.application !== application)
|
|
739
|
+
continue; // and by isolated application
|
|
740
|
+
if (worker.application && freshlyStarted.has(worker.application))
|
|
741
|
+
continue;
|
|
517
742
|
const overlapping = OVERLAPPING_RESTART_TYPES.indexOf(worker.name) > -1;
|
|
743
|
+
const canPreStartReplacement = platformCanPreStartReplacement && !worker.application;
|
|
518
744
|
if (overlapping && startReplacementThreads && canPreStartReplacement) {
|
|
519
745
|
// Overlapping restart: start the replacement and wait until it is accepting connections
|
|
520
746
|
// *before* shutting down the worker it replaces. The replacement joins the (SO_REUSEPORT)
|
|
@@ -569,6 +795,8 @@ startReplacementThreads = true) {
|
|
|
569
795
|
// protection if it is still alive (it may have exited on its own during the wait).
|
|
570
796
|
if (workers.includes(worker))
|
|
571
797
|
worker.wasShutdown = false;
|
|
798
|
+
workersKeptOnOldCode++;
|
|
799
|
+
onProgress?.();
|
|
572
800
|
continue;
|
|
573
801
|
}
|
|
574
802
|
}
|
|
@@ -591,8 +819,16 @@ startReplacementThreads = true) {
|
|
|
591
819
|
// Overlapping types we couldn't pre-start (Windows/Bun): start the replacement now that the old
|
|
592
820
|
// worker is releasing its port. server.close() stops accepting immediately, so the port frees up
|
|
593
821
|
// well before the replacement finishes booting and binds.
|
|
594
|
-
|
|
595
|
-
|
|
822
|
+
let replacementStarting;
|
|
823
|
+
if (overlapping && startReplacementThreads && !canPreStartReplacement && !processShuttingDown) {
|
|
824
|
+
replacementStarting = whenWorkerStarted(worker.startCopy()).then((started) => {
|
|
825
|
+
if (!started)
|
|
826
|
+
replacementsFailedToStart++;
|
|
827
|
+
onProgress?.();
|
|
828
|
+
return started;
|
|
829
|
+
});
|
|
830
|
+
replacementsStarting.push(replacementStarting);
|
|
831
|
+
}
|
|
596
832
|
let whenDone = new Promise((resolve) => {
|
|
597
833
|
// in case the exit inside the thread doesn't timeout, force it from the outside
|
|
598
834
|
const armTerminate = (delay) => setTimeout(() => {
|
|
@@ -620,39 +856,113 @@ startReplacementThreads = true) {
|
|
|
620
856
|
// buggy/rogue message can't defer the force-kill unboundedly; a shrink (drain-done reset)
|
|
621
857
|
// passes through untouched. See boundedTerminateDelay for the arithmetic + its unit tests.
|
|
622
858
|
const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require("../../components/shutdownDrain.js");
|
|
623
|
-
|
|
859
|
+
const delay = boundedTerminateDelay(deadlineMs, Date.now(), threadTerminationTimeout * 2, getShutdownDrainCeilingMs());
|
|
860
|
+
timeout = armTerminate(delay);
|
|
861
|
+
// The worker is telling us it has work still moving and how long it may take, so pass that
|
|
862
|
+
// on: a caller waiting on the restart must not treat a live drain as a stalled one.
|
|
863
|
+
onProgress?.(Date.now() + delay);
|
|
624
864
|
};
|
|
625
865
|
worker.on('exit', () => {
|
|
626
866
|
clearTimeout(timeout);
|
|
867
|
+
onProgress?.();
|
|
627
868
|
worker.extendTerminateDeadline = undefined;
|
|
628
|
-
const index = waitingToFinish.indexOf(whenDone);
|
|
629
|
-
if (index > -1)
|
|
630
|
-
waitingToFinish.splice(index, 1);
|
|
631
869
|
// non-overlapping types have no advance replacement, so start it once the old one is gone
|
|
632
870
|
if (!overlapping && startReplacementThreads && !processShuttingDown)
|
|
633
871
|
worker.startCopy();
|
|
634
872
|
resolve();
|
|
635
873
|
});
|
|
636
874
|
});
|
|
637
|
-
|
|
875
|
+
// A worker counts as replaced only once its replacement is accepting connections, not merely
|
|
876
|
+
// once it has exited. This promise is held unawaited between throttle points, so it must not
|
|
877
|
+
// be able to reject.
|
|
878
|
+
const replaced = (replacementStarting ? Promise.all([whenDone, replacementStarting]) : whenDone)
|
|
879
|
+
.catch((error) => harperLogger.warn('Error waiting for a worker to be replaced', error))
|
|
880
|
+
.then(() => {
|
|
881
|
+
const at = waitingToFinish.indexOf(replaced);
|
|
882
|
+
if (at > -1)
|
|
883
|
+
waitingToFinish.splice(at, 1);
|
|
884
|
+
});
|
|
885
|
+
waitingToFinish.push(replaced);
|
|
638
886
|
if (waitingToFinish.length >= maxWorkersDown) {
|
|
639
|
-
// throttle how many workers are
|
|
887
|
+
// throttle how many workers are down at once to limit load
|
|
640
888
|
await Promise.race(waitingToFinish);
|
|
641
889
|
}
|
|
890
|
+
// Readiness throttling bounds how many workers are down at once, but not how many *fail*: with
|
|
891
|
+
// replacements that never come up, walking the rest of the pool would leave nothing serving.
|
|
892
|
+
// The workers not yet touched are still running the old code, which beats none running at all.
|
|
893
|
+
if (replacementsFailedToStart >= maxWorkersDown) {
|
|
894
|
+
const untouched = restarting
|
|
895
|
+
.slice(index + 1)
|
|
896
|
+
// a worker that exited on its own mid-restart is spliced out of `workers` and
|
|
897
|
+
// auto-restarted onto the new code (see the exit handler above); it is not still on
|
|
898
|
+
// the previous code even though this loop never got to it.
|
|
899
|
+
.filter((other) => (!name || other.name === name) && !other.wasShutdown && workers.includes(other)).length;
|
|
900
|
+
harperLogger.error(`${replacementsFailedToStart} replacement worker thread(s) did not start; stopping this restart with ${untouched} worker(s) still on the previous code`);
|
|
901
|
+
workersKeptOnOldCode += untouched;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
642
904
|
}
|
|
643
|
-
// seems appropriate to wait for this to finish, but the API doesn't actually wait for this function
|
|
644
|
-
// to finish, so not that important
|
|
645
905
|
await Promise.all(waitingToFinish);
|
|
906
|
+
// A caller awaiting this needs it to mean "the pool is serving the new code", so wait out the
|
|
907
|
+
// replacements that could only be started once their predecessor released its exclusive ports.
|
|
908
|
+
replacementsNotStarted = (await Promise.all(replacementsStarting)).filter((started) => !started).length;
|
|
909
|
+
return { workersKeptOnOldCode, replacementsNotStarted };
|
|
646
910
|
}
|
|
647
911
|
else {
|
|
648
912
|
parentPort.postMessage({
|
|
649
913
|
type: RESTART_TYPE,
|
|
650
914
|
workerType: name,
|
|
915
|
+
scope: encodeRestartScope(application),
|
|
651
916
|
});
|
|
652
917
|
}
|
|
653
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Resolve once a newly started worker reports that it is accepting connections, or gives up on it.
|
|
921
|
+
* There is no old worker left to fall back on here, so a replacement that fails to start is left to
|
|
922
|
+
* startWorker's own auto-restart handling; this only stops waiting on it.
|
|
923
|
+
* @param newWorker The replacement worker
|
|
924
|
+
* @returns {Promise<boolean>} whether the worker reported that it started
|
|
925
|
+
*/
|
|
926
|
+
function whenWorkerStarted(newWorker) {
|
|
927
|
+
return new Promise((resolve) => {
|
|
928
|
+
const cleanup = () => {
|
|
929
|
+
clearTimeout(timeout);
|
|
930
|
+
newWorker.off('message', startListener);
|
|
931
|
+
newWorker.off('exit', exitListener);
|
|
932
|
+
};
|
|
933
|
+
const timeout = setTimeout(() => {
|
|
934
|
+
harperLogger.error('Replacement worker did not start in time', newWorker.threadId);
|
|
935
|
+
cleanup();
|
|
936
|
+
// Its predecessor is already gone, so a replacement wedged in boot is a worker slot serving
|
|
937
|
+
// nothing until the process restarts. Stop it and let startWorker's exit handling replace it.
|
|
938
|
+
if (isBun) {
|
|
939
|
+
// terminate() triggers a NAPI segfault in Bun; ask the worker to self-exit instead.
|
|
940
|
+
try {
|
|
941
|
+
newWorker.postMessage({ type: FORCE_EXIT });
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
}
|
|
945
|
+
else
|
|
946
|
+
newWorker.terminate();
|
|
947
|
+
resolve(false);
|
|
948
|
+
}, Math.max(threadTerminationTimeout * 2, 60000)).unref();
|
|
949
|
+
const startListener = (message) => {
|
|
950
|
+
if (message.type === hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED) {
|
|
951
|
+
cleanup();
|
|
952
|
+
resolve(true);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const exitListener = () => {
|
|
956
|
+
harperLogger.warn('Replacement worker exited before starting', newWorker.threadId);
|
|
957
|
+
cleanup();
|
|
958
|
+
resolve(false);
|
|
959
|
+
};
|
|
960
|
+
newWorker.on('message', startListener);
|
|
961
|
+
newWorker.on('exit', exitListener);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
654
964
|
function shutdownWorkers(name) {
|
|
655
|
-
return restartWorkers(name, Infinity, false);
|
|
965
|
+
return restartWorkers(name, Infinity, false, null, '*');
|
|
656
966
|
}
|
|
657
967
|
function beginProcessShutdown() {
|
|
658
968
|
processShuttingDown = true;
|
|
@@ -674,6 +984,27 @@ async function shutdownWorkersNow(name) {
|
|
|
674
984
|
await Promise.all(workers.map((worker) => worker.terminate()));
|
|
675
985
|
}
|
|
676
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* The restart scope on the wire: a worker cannot post `undefined` and have it mean "the pool" (a
|
|
989
|
+
* message with no scope at all means "everything", as every pre-existing sender intends).
|
|
990
|
+
*/
|
|
991
|
+
function encodeRestartScope(application) {
|
|
992
|
+
return application === undefined ? '' : application; // '' is not a legal application name
|
|
993
|
+
}
|
|
994
|
+
function decodeRestartScope(message) {
|
|
995
|
+
if (message.scope === undefined)
|
|
996
|
+
return '*';
|
|
997
|
+
return message.scope === '' ? undefined : message.scope;
|
|
998
|
+
}
|
|
999
|
+
let isolatedWorkerReconciler = null;
|
|
1000
|
+
let runningIsolatedApplicationsGetter = () => [];
|
|
1001
|
+
/** Registered by socketRouter: starts and stops dedicated workers to match the root config's isolated applications. */
|
|
1002
|
+
function setIsolatedWorkerReconciler(reconcile) {
|
|
1003
|
+
isolatedWorkerReconciler = reconcile;
|
|
1004
|
+
}
|
|
1005
|
+
function setRunningIsolatedApplicationsGetter(getter) {
|
|
1006
|
+
runningIsolatedApplicationsGetter = getter;
|
|
1007
|
+
}
|
|
677
1008
|
const messageListeners = [];
|
|
678
1009
|
function onMessageFromWorkers(listener) {
|
|
679
1010
|
messageListeners.push(listener);
|
|
@@ -784,15 +1115,146 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
784
1115
|
timer = undefined;
|
|
785
1116
|
const stuck = [];
|
|
786
1117
|
for (let ackHandler of [...pending]) {
|
|
787
|
-
stuck.push(ackHandler.port
|
|
1118
|
+
stuck.push(ackHandler.port);
|
|
788
1119
|
ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
|
|
789
1120
|
}
|
|
790
|
-
harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.join(', ')} within ${timeout}ms; proceeding best-effort`);
|
|
1121
|
+
harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`);
|
|
1122
|
+
if (isMainThread)
|
|
1123
|
+
for (let port of stuck)
|
|
1124
|
+
logStuckWorkerDiagnostics(port);
|
|
1125
|
+
else if (parentPort) {
|
|
1126
|
+
// Only main holds the Worker objects (and tids), so a worker-originated timeout is sampled there.
|
|
1127
|
+
const threadIds = stuck.map((port) => port?.threadId).filter((threadId) => threadId > 0);
|
|
1128
|
+
if (threadIds.length > 0)
|
|
1129
|
+
parentPort.postMessage({ type: STUCK_WORKER_REPORT, threadIds });
|
|
1130
|
+
}
|
|
791
1131
|
}, timeout);
|
|
792
1132
|
timer.unref?.();
|
|
793
1133
|
}
|
|
794
1134
|
});
|
|
795
1135
|
}
|
|
1136
|
+
// Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
|
|
1137
|
+
function getOsThreadId() {
|
|
1138
|
+
try {
|
|
1139
|
+
const tid = Number(readlinkSync('/proc/thread-self').split('/').pop());
|
|
1140
|
+
return Number.isInteger(tid) && tid > 0 ? tid : undefined;
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
function readTaskFile(tid, name) {
|
|
1147
|
+
try {
|
|
1148
|
+
return readFileSync(`/proc/self/task/${tid}/${name}`, 'utf8').trim();
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
// Kernel-side view of one thread. Every field is best-effort and reported individually, since
|
|
1155
|
+
// wchan/syscall need ptrace read access that a hardened container may deny while stat is open.
|
|
1156
|
+
function finiteOrUndefined(value) {
|
|
1157
|
+
const number = Number(value);
|
|
1158
|
+
return Number.isFinite(number) ? number : undefined;
|
|
1159
|
+
}
|
|
1160
|
+
function readOsThreadState(tid) {
|
|
1161
|
+
const thread = { tid };
|
|
1162
|
+
const stat = readTaskFile(tid, 'stat');
|
|
1163
|
+
if (stat !== undefined) {
|
|
1164
|
+
// Fields after the parenthesized comm, so state is [0], utime/stime [11]/[12], starttime [19].
|
|
1165
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1166
|
+
thread.state = fields[0];
|
|
1167
|
+
const utime = finiteOrUndefined(fields[11]);
|
|
1168
|
+
const stime = finiteOrUndefined(fields[12]);
|
|
1169
|
+
if (utime !== undefined && stime !== undefined)
|
|
1170
|
+
thread.cpuTicks = utime + stime;
|
|
1171
|
+
thread.startTime = fields[19];
|
|
1172
|
+
}
|
|
1173
|
+
const wchan = readTaskFile(tid, 'wchan');
|
|
1174
|
+
if (wchan !== undefined)
|
|
1175
|
+
thread.wchan = wchan;
|
|
1176
|
+
// First token only: the syscall number, "running", or -1 (blocked outside a syscall). The
|
|
1177
|
+
// remainder is argument registers and the stack/instruction pointers, which don't belong in a log.
|
|
1178
|
+
const syscall = readTaskFile(tid, 'syscall');
|
|
1179
|
+
if (syscall !== undefined)
|
|
1180
|
+
thread.syscall = syscall.split(' ')[0];
|
|
1181
|
+
const status = readTaskFile(tid, 'status');
|
|
1182
|
+
if (status !== undefined) {
|
|
1183
|
+
thread.voluntarySwitches = finiteOrUndefined(/^voluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
1184
|
+
thread.nonvoluntarySwitches = finiteOrUndefined(/^nonvoluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
1185
|
+
}
|
|
1186
|
+
return thread;
|
|
1187
|
+
}
|
|
1188
|
+
function snapshotWorkerThread(worker) {
|
|
1189
|
+
const snapshot = { at: Date.now() };
|
|
1190
|
+
if (worker.resources?.updated)
|
|
1191
|
+
snapshot.sinceResourceReport = snapshot.at - worker.resources.updated;
|
|
1192
|
+
const eventLoop = worker.performance?.eventLoopUtilization?.();
|
|
1193
|
+
if (eventLoop)
|
|
1194
|
+
snapshot.eventLoop = { idle: eventLoop.idle, active: eventLoop.active };
|
|
1195
|
+
if (worker.osThreadId)
|
|
1196
|
+
snapshot.osThread = readOsThreadState(worker.osThreadId);
|
|
1197
|
+
return snapshot;
|
|
1198
|
+
}
|
|
1199
|
+
function describeThreadState(thread) {
|
|
1200
|
+
return `state=${thread.state ?? '?'} wchan=${thread.wchan ?? '?'} syscall=${thread.syscall ?? '?'}`;
|
|
1201
|
+
}
|
|
1202
|
+
function describeSnapshot(snapshot) {
|
|
1203
|
+
const parts = [
|
|
1204
|
+
snapshot.sinceResourceReport === undefined
|
|
1205
|
+
? 'no resource report received'
|
|
1206
|
+
: `last resource report ${snapshot.sinceResourceReport}ms ago`,
|
|
1207
|
+
];
|
|
1208
|
+
if (snapshot.eventLoop)
|
|
1209
|
+
parts.push(`event loop active ${Math.round(snapshot.eventLoop.active)}ms idle ${Math.round(snapshot.eventLoop.idle)}ms`);
|
|
1210
|
+
const thread = snapshot.osThread;
|
|
1211
|
+
if (!thread)
|
|
1212
|
+
parts.push('os thread state unavailable');
|
|
1213
|
+
else
|
|
1214
|
+
parts.push(`os tid ${thread.tid} ${describeThreadState(thread)} cpuTicks=${thread.cpuTicks ?? '?'} ctxtSwitches=${thread.voluntarySwitches ?? '?'}/${thread.nonvoluntarySwitches ?? '?'}`);
|
|
1215
|
+
return parts.join('; ');
|
|
1216
|
+
}
|
|
1217
|
+
function describeDelta(before, after) {
|
|
1218
|
+
return Number.isFinite(before) && Number.isFinite(after) ? `+${after - before}` : '?';
|
|
1219
|
+
}
|
|
1220
|
+
function describeProgress(first, second) {
|
|
1221
|
+
const parts = [];
|
|
1222
|
+
if (first.eventLoop && second.eventLoop)
|
|
1223
|
+
parts.push(`event loop active +${Math.round(second.eventLoop.active - first.eventLoop.active)}ms idle +${Math.round(second.eventLoop.idle - first.eventLoop.idle)}ms`);
|
|
1224
|
+
const before = first.osThread;
|
|
1225
|
+
const after = second.osThread;
|
|
1226
|
+
if (before && after)
|
|
1227
|
+
parts.push(`cpuTicks ${describeDelta(before.cpuTicks, after.cpuTicks)} ctxtSwitches ${describeDelta(before.voluntarySwitches, after.voluntarySwitches)}/${describeDelta(before.nonvoluntarySwitches, after.nonvoluntarySwitches)} ${describeThreadState(after)}`);
|
|
1228
|
+
return parts.join('; ');
|
|
1229
|
+
}
|
|
1230
|
+
// A blocked event loop cannot report itself; two kernel-state samples a second apart separate
|
|
1231
|
+
// "parked on a lock" (no CPU ticks, no context switches) from "spinning".
|
|
1232
|
+
const STUCK_WORKER_SAMPLE_INTERVAL_MS = 1000;
|
|
1233
|
+
const STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS = 30000;
|
|
1234
|
+
function logStuckWorkerDiagnostics(worker) {
|
|
1235
|
+
if (!worker || !workers.includes(worker))
|
|
1236
|
+
return;
|
|
1237
|
+
const now = Date.now();
|
|
1238
|
+
if (worker.stuckDiagnosticAt !== undefined && now - worker.stuckDiagnosticAt < STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS)
|
|
1239
|
+
return;
|
|
1240
|
+
worker.stuckDiagnosticAt = now;
|
|
1241
|
+
const threadId = worker.threadId; // Node resets it to -1 once the worker exits
|
|
1242
|
+
const first = snapshotWorkerThread(worker);
|
|
1243
|
+
harperLogger.warn(`Worker thread ${threadId} at ack timeout: ${describeSnapshot(first)}`);
|
|
1244
|
+
if (!first.eventLoop && !first.osThread)
|
|
1245
|
+
return;
|
|
1246
|
+
setTimeout(() => {
|
|
1247
|
+
if (!workers.includes(worker)) {
|
|
1248
|
+
harperLogger.warn(`Worker thread ${threadId} exited before its follow-up sample`);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const second = snapshotWorkerThread(worker);
|
|
1252
|
+
// A recycled tid after a thread exit would attribute another thread's activity to this worker.
|
|
1253
|
+
if (second.osThread && second.osThread.startTime !== first.osThread?.startTime)
|
|
1254
|
+
second.osThread = undefined;
|
|
1255
|
+
harperLogger.warn(`Worker thread ${threadId} over the next ${second.at - first.at}ms: ${describeProgress(first, second) || 'no further state available'}`);
|
|
1256
|
+
}, STUCK_WORKER_SAMPLE_INTERVAL_MS).unref();
|
|
1257
|
+
}
|
|
796
1258
|
function sendThreadInfo(targetWorker) {
|
|
797
1259
|
targetWorker.postMessage({
|
|
798
1260
|
type: THREAD_INFO,
|
|
@@ -804,6 +1266,7 @@ function getChildWorkerInfo() {
|
|
|
804
1266
|
return workers.map((worker) => ({
|
|
805
1267
|
threadId: worker.threadId,
|
|
806
1268
|
name: worker.name,
|
|
1269
|
+
application: worker.application,
|
|
807
1270
|
heapTotal: worker.resources?.heapTotal,
|
|
808
1271
|
heapUsed: worker.resources?.heapUsed,
|
|
809
1272
|
externalMemory: worker.resources?.external,
|
|
@@ -864,6 +1327,9 @@ if (parentPort && workerData?.addPorts) {
|
|
|
864
1327
|
// parentPort so sendToThread(0, ...) and similar lookups can route back to main.
|
|
865
1328
|
parentPort.threadId = 0;
|
|
866
1329
|
addPort(parentPort);
|
|
1330
|
+
const osThreadId = getOsThreadId();
|
|
1331
|
+
if (osThreadId !== undefined)
|
|
1332
|
+
parentPort.postMessage({ type: OS_THREAD_ID, osThreadId });
|
|
867
1333
|
for (let i = 0, l = workerData.addPorts.length; i < l; i++) {
|
|
868
1334
|
let port = workerData.addPorts[i];
|
|
869
1335
|
port.threadId = workerData.addThreadIds[i];
|
|
@@ -912,17 +1378,56 @@ if (parentPort && workerData?.addPorts) {
|
|
|
912
1378
|
}, timeoutMs);
|
|
913
1379
|
}
|
|
914
1380
|
});
|
|
1381
|
+
let nextRunningIsolatedApplicationsRequestId = 0;
|
|
1382
|
+
getRunningIsolatedApplications = (timeoutMs) => new Promise((resolve, reject) => {
|
|
1383
|
+
const requestId = ++nextRunningIsolatedApplicationsRequestId;
|
|
1384
|
+
let timeout;
|
|
1385
|
+
parentPort.on('message', receiveApplications);
|
|
1386
|
+
try {
|
|
1387
|
+
parentPort.postMessage({ type: REQUEST_RUNNING_ISOLATED_APPLICATIONS, requestId });
|
|
1388
|
+
}
|
|
1389
|
+
catch (error) {
|
|
1390
|
+
cleanup();
|
|
1391
|
+
reject(error);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
function receiveApplications(message) {
|
|
1395
|
+
if (message.type === RUNNING_ISOLATED_APPLICATIONS && message.requestId === requestId) {
|
|
1396
|
+
cleanup();
|
|
1397
|
+
resolve(message.applications);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function cleanup() {
|
|
1401
|
+
if (timeout)
|
|
1402
|
+
clearTimeout(timeout);
|
|
1403
|
+
parentPort.off('message', receiveApplications);
|
|
1404
|
+
}
|
|
1405
|
+
if (timeoutMs != null) {
|
|
1406
|
+
timeout = setTimeout(() => {
|
|
1407
|
+
cleanup();
|
|
1408
|
+
const error = new Error(`Timed out waiting for isolated application topology after ${timeoutMs}ms`);
|
|
1409
|
+
error.code = 'ERR_ISOLATED_APPLICATIONS_TIMEOUT';
|
|
1410
|
+
reject(error);
|
|
1411
|
+
}, timeoutMs);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
915
1414
|
let awaitTerminationRequestId = 0;
|
|
916
|
-
awaitProcessGroupTermination = (ownerThreadId) => new Promise((resolve) => {
|
|
1415
|
+
awaitProcessGroupTermination = (ownerThreadId, signal) => new Promise((resolve) => {
|
|
917
1416
|
// Deliberately no timeout: a contender must not reclaim a dead owner's lock while its
|
|
918
1417
|
// process group might still be alive and mutating files, so this mirrors the unbounded
|
|
919
|
-
// wait Application.ts's waitForConfirmedTermination uses for the same reason.
|
|
1418
|
+
// wait Application.ts's waitForConfirmedTermination uses for the same reason. A caller
|
|
1419
|
+
// that needs its own bound (isThreadRunning) passes `signal` so this listener still gets
|
|
1420
|
+
// torn down when that caller gives up, instead of leaking for the life of the worker.
|
|
920
1421
|
const requestId = ++awaitTerminationRequestId;
|
|
921
1422
|
parentPort.on('message', receiveConfirmation);
|
|
1423
|
+
signal?.addEventListener('abort', cleanup, { once: true });
|
|
922
1424
|
parentPort.postMessage({ type: AWAIT_PROCESS_GROUP_TERMINATION, ownerThreadId, requestId });
|
|
1425
|
+
function cleanup() {
|
|
1426
|
+
parentPort.off('message', receiveConfirmation);
|
|
1427
|
+
}
|
|
923
1428
|
function receiveConfirmation(message) {
|
|
924
1429
|
if (message.type === PROCESS_GROUP_TERMINATION_CONFIRMED && message.requestId === requestId) {
|
|
925
|
-
|
|
1430
|
+
cleanup();
|
|
926
1431
|
resolve();
|
|
927
1432
|
}
|
|
928
1433
|
}
|
|
@@ -930,9 +1435,17 @@ if (parentPort && workerData?.addPorts) {
|
|
|
930
1435
|
}
|
|
931
1436
|
else {
|
|
932
1437
|
getThreadInfo = getChildWorkerInfo;
|
|
1438
|
+
getRunningIsolatedApplications = isMainThread
|
|
1439
|
+
? () => runningIsolatedApplicationsGetter()
|
|
1440
|
+
: () => {
|
|
1441
|
+
const error = new Error('No channel to the main thread for isolated application topology');
|
|
1442
|
+
error.code = 'ERR_ISOLATED_APPLICATIONS_UNAVAILABLE';
|
|
1443
|
+
return Promise.reject(error);
|
|
1444
|
+
};
|
|
933
1445
|
awaitProcessGroupTermination = (ownerThreadId) => pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
|
|
934
1446
|
}
|
|
935
1447
|
module.exports.getThreadInfo = getThreadInfo;
|
|
1448
|
+
module.exports.getRunningIsolatedApplications = getRunningIsolatedApplications;
|
|
936
1449
|
// Listeners notified when a connected thread's port closes (worker exit/restart), so
|
|
937
1450
|
// modules holding per-thread state (e.g. registeredOperations' registry and in-flight
|
|
938
1451
|
// forwards) can clean up.
|
|
@@ -953,34 +1466,144 @@ const processGroupsByThread = new Map();
|
|
|
953
1466
|
// window component-preparation locking exists to close.
|
|
954
1467
|
const pendingProcessGroupTerminations = new Map();
|
|
955
1468
|
const PROCESS_GROUP_TERMINATION_POLL_MS = 25;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
function
|
|
964
|
-
|
|
965
|
-
|
|
1469
|
+
const ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS = 1000;
|
|
1470
|
+
const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
|
|
1471
|
+
// Bounds isThreadRunning's own wait (below); Application.ts's waitForConfirmedTermination stays
|
|
1472
|
+
// deliberately unbounded.
|
|
1473
|
+
const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
|
|
1474
|
+
const zombieGroupScanTimes = new Map();
|
|
1475
|
+
const processGroupLivenessStates = new Map();
|
|
1476
|
+
function processGroupExists(processGroupId) {
|
|
1477
|
+
try {
|
|
1478
|
+
process.kill(-processGroupId, 0);
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
return error.code === 'EPERM';
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
function processProbeError(error) {
|
|
1486
|
+
return error?.code ?? error?.message ?? String(error ?? 'unknown error');
|
|
1487
|
+
}
|
|
1488
|
+
function processGroupLeaderState(processGroupId, platform, readStat) {
|
|
1489
|
+
if (platform !== 'linux') {
|
|
1490
|
+
return { state: 'unknown', reason: `zombie-process detection is unavailable on ${platform}` };
|
|
1491
|
+
}
|
|
966
1492
|
let stat;
|
|
967
1493
|
try {
|
|
968
|
-
stat =
|
|
1494
|
+
stat = readStat(`/proc/${processGroupId}/stat`, 'utf8');
|
|
969
1495
|
}
|
|
970
|
-
catch {
|
|
971
|
-
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH')
|
|
1498
|
+
return { state: 'missing' };
|
|
1499
|
+
return {
|
|
1500
|
+
state: 'unknown',
|
|
1501
|
+
reason: `reading /proc/${processGroupId}/stat failed (${processProbeError(error)})`,
|
|
1502
|
+
};
|
|
972
1503
|
}
|
|
973
|
-
|
|
974
|
-
|
|
1504
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1505
|
+
if (Number(fields[2]) !== processGroupId)
|
|
1506
|
+
return { state: 'missing' };
|
|
1507
|
+
return { state: fields[0] === 'Z' ? 'zombie' : 'alive' };
|
|
975
1508
|
}
|
|
976
|
-
function
|
|
1509
|
+
function scanLinuxProcessGroup(processGroupId, readDirectory, readStat) {
|
|
1510
|
+
let processIds;
|
|
977
1511
|
try {
|
|
978
|
-
|
|
1512
|
+
processIds = readDirectory('/proc');
|
|
979
1513
|
}
|
|
980
1514
|
catch (error) {
|
|
981
|
-
return
|
|
1515
|
+
return {
|
|
1516
|
+
isAlive: null,
|
|
1517
|
+
reason: `reading /proc failed (${processProbeError(error)})`,
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
for (const processId of processIds) {
|
|
1521
|
+
if (!/^\d+$/.test(processId))
|
|
1522
|
+
continue;
|
|
1523
|
+
let stat;
|
|
1524
|
+
try {
|
|
1525
|
+
stat = readStat(`/proc/${processId}/stat`, 'utf8');
|
|
1526
|
+
}
|
|
1527
|
+
catch (error) {
|
|
1528
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH')
|
|
1529
|
+
continue; // already gone
|
|
1530
|
+
// EACCES/EPERM (e.g. hidepid/ProtectProc) can only happen on a pid we don't own — we
|
|
1531
|
+
// spawned our own group's members as this same user, so their stat is always readable —
|
|
1532
|
+
// so it's not one of ours. Anything else is genuinely unknown, not "not ours".
|
|
1533
|
+
if (error.code === 'EACCES' || error.code === 'EPERM')
|
|
1534
|
+
continue;
|
|
1535
|
+
return {
|
|
1536
|
+
isAlive: null,
|
|
1537
|
+
reason: `reading /proc/${processId}/stat failed (${processProbeError(error)})`,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1541
|
+
const pgrp = Number(fields[2]);
|
|
1542
|
+
if (!Number.isFinite(pgrp)) {
|
|
1543
|
+
return { isAlive: null, reason: `/proc/${processId}/stat had an unparseable process-group field` };
|
|
1544
|
+
}
|
|
1545
|
+
if (pgrp !== processGroupId)
|
|
1546
|
+
continue;
|
|
1547
|
+
if (fields[0] !== 'Z')
|
|
1548
|
+
return { isAlive: true, reason: `a live member was observed at pid ${processId}` };
|
|
982
1549
|
}
|
|
983
|
-
return
|
|
1550
|
+
return { isAlive: false };
|
|
1551
|
+
}
|
|
1552
|
+
function keepProcessGroupAlive(processGroupId, reason, observationTime, warn) {
|
|
1553
|
+
let livenessState = processGroupLivenessStates.get(processGroupId);
|
|
1554
|
+
if (!livenessState) {
|
|
1555
|
+
livenessState = { observedAt: observationTime, reason, warned: false };
|
|
1556
|
+
processGroupLivenessStates.set(processGroupId, livenessState);
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
livenessState.reason = reason;
|
|
1560
|
+
}
|
|
1561
|
+
if (!livenessState.warned && observationTime - livenessState.observedAt >= PROCESS_GROUP_LIVENESS_WARNING_MS) {
|
|
1562
|
+
livenessState.warned = true;
|
|
1563
|
+
warn(`Process group ${processGroupId} termination remains unconfirmed after ${PROCESS_GROUP_LIVENESS_WARNING_MS}ms: ${livenessState.reason}`);
|
|
1564
|
+
}
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
function clearProcessGroupLivenessState(processGroupId) {
|
|
1568
|
+
zombieGroupScanTimes.delete(processGroupId);
|
|
1569
|
+
processGroupLivenessStates.delete(processGroupId);
|
|
1570
|
+
}
|
|
1571
|
+
function isProcessGroupAlive(processGroupId, options) {
|
|
1572
|
+
const platform = options?.platform ?? process.platform;
|
|
1573
|
+
const groupExists = options?.processGroupExists ?? processGroupExists;
|
|
1574
|
+
const readDirectory = options?.readDirectory ?? readdirSync;
|
|
1575
|
+
const readStat = options?.readStat ?? readFileSync;
|
|
1576
|
+
const observationTime = options?.now?.() ?? performance.now();
|
|
1577
|
+
const warn = options?.warn ?? ((message) => harperLogger.warn(message));
|
|
1578
|
+
if (!groupExists(processGroupId)) {
|
|
1579
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
const leaderState = processGroupLeaderState(processGroupId, platform, readStat);
|
|
1583
|
+
if (leaderState.state === 'alive')
|
|
1584
|
+
return keepProcessGroupAlive(processGroupId, 'the process-group leader is still alive', observationTime, warn);
|
|
1585
|
+
if (leaderState.state === 'unknown')
|
|
1586
|
+
return keepProcessGroupAlive(processGroupId, leaderState.reason, observationTime, warn);
|
|
1587
|
+
const scanTime = observationTime;
|
|
1588
|
+
const lastScan = zombieGroupScanTimes.get(processGroupId);
|
|
1589
|
+
if (lastScan !== undefined && scanTime >= lastScan && scanTime - lastScan < ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS) {
|
|
1590
|
+
const reason = processGroupLivenessStates.get(processGroupId)?.reason ?? 'the previous Linux scan is still current';
|
|
1591
|
+
return keepProcessGroupAlive(processGroupId, reason, observationTime, warn);
|
|
1592
|
+
}
|
|
1593
|
+
zombieGroupScanTimes.set(processGroupId, scanTime);
|
|
1594
|
+
let scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1595
|
+
if (scanResult.isAlive !== false)
|
|
1596
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1597
|
+
// A second snapshot catches a member omitted after forking inside the first readdir/stat window.
|
|
1598
|
+
// It narrows rather than closes the race because the second synchronous pass has the same gap.
|
|
1599
|
+
scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1600
|
+
if (scanResult.isAlive !== false)
|
|
1601
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1602
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
function processGroupIsAlive(processGroupId) {
|
|
1606
|
+
return isProcessGroupAlive(processGroupId);
|
|
984
1607
|
}
|
|
985
1608
|
async function waitForProcessGroupExit(processGroupId) {
|
|
986
1609
|
while (processGroupIsAlive(processGroupId))
|
|
@@ -1054,6 +1677,7 @@ function addProcessGroup(ownerThreadId, processGroupId) {
|
|
|
1054
1677
|
processGroups.add(processGroupId);
|
|
1055
1678
|
}
|
|
1056
1679
|
function removeProcessGroup(ownerThreadId, processGroupId) {
|
|
1680
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1057
1681
|
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1058
1682
|
if (!processGroups)
|
|
1059
1683
|
return;
|
|
@@ -1107,6 +1731,39 @@ function unregisterProcessGroup(processGroupId) {
|
|
|
1107
1731
|
else
|
|
1108
1732
|
parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
|
|
1109
1733
|
}
|
|
1734
|
+
class ProcessGroupTerminationUnconfirmedError extends Error {
|
|
1735
|
+
code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
|
|
1736
|
+
constructor(ownerThreadId, timeoutMs) {
|
|
1737
|
+
super(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${timeoutMs}ms`);
|
|
1738
|
+
this.name = 'ProcessGroupTerminationUnconfirmedError';
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// Bounds awaitProcessGroupTermination for isThreadRunning below. Throws rather than resolving to
|
|
1742
|
+
// "not running": componentPreparationLock's ownerIsAlive/ownerLivenessConfirmed already treat a
|
|
1743
|
+
// throwing isOwnerAlive as "can't confirm — don't steal the claim, don't renew the deadline", so
|
|
1744
|
+
// the lock's own bounded wait is what eventually fails the waiter.
|
|
1745
|
+
async function awaitConfirmedProcessGroupTermination(ownerThreadId) {
|
|
1746
|
+
const abortController = new AbortController();
|
|
1747
|
+
let timedOut = false;
|
|
1748
|
+
const timer = setTimeout(() => {
|
|
1749
|
+
timedOut = true;
|
|
1750
|
+
abortController.abort();
|
|
1751
|
+
}, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1752
|
+
timer.unref?.();
|
|
1753
|
+
try {
|
|
1754
|
+
await Promise.race([
|
|
1755
|
+
awaitProcessGroupTermination(ownerThreadId, abortController.signal),
|
|
1756
|
+
new Promise((resolve) => abortController.signal.addEventListener('abort', resolve, { once: true })),
|
|
1757
|
+
]);
|
|
1758
|
+
}
|
|
1759
|
+
finally {
|
|
1760
|
+
clearTimeout(timer);
|
|
1761
|
+
}
|
|
1762
|
+
if (timedOut) {
|
|
1763
|
+
harperLogger.warn(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${THREAD_RUNNING_TERMINATION_BACKSTOP_MS}ms`);
|
|
1764
|
+
throw new ProcessGroupTerminationUnconfirmedError(ownerThreadId, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1110
1767
|
async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TIMEOUT_MS) {
|
|
1111
1768
|
if (ownerThreadId === threadId || ownerThreadId === 0)
|
|
1112
1769
|
return true;
|
|
@@ -1114,7 +1771,7 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
|
|
|
1114
1771
|
return true;
|
|
1115
1772
|
// The thread itself is gone, but it may still own process groups whose forced termination is
|
|
1116
1773
|
// in flight — wait for that to be confirmed before reporting the owner as reclaimable.
|
|
1117
|
-
await
|
|
1774
|
+
await awaitConfirmedProcessGroupTermination(ownerThreadId);
|
|
1118
1775
|
return false;
|
|
1119
1776
|
}
|
|
1120
1777
|
if (isMainThread) {
|
|
@@ -1123,6 +1780,14 @@ if (isMainThread) {
|
|
|
1123
1780
|
terminateProcessGroupsForThread(ownerThreadId);
|
|
1124
1781
|
});
|
|
1125
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Whether a thread has already been reported dead. Sync, unlike `isThreadRunning`, because it only
|
|
1785
|
+
* reads the tombstone `notifyThreadExit` records below — callers on the exit path need an answer
|
|
1786
|
+
* without awaiting process-group confirmation.
|
|
1787
|
+
*/
|
|
1788
|
+
function hasThreadExited(threadId) {
|
|
1789
|
+
return notifiedDeadThreadIds.has(threadId);
|
|
1790
|
+
}
|
|
1126
1791
|
function notifyThreadExit(deadThreadId) {
|
|
1127
1792
|
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId))
|
|
1128
1793
|
return;
|
|
@@ -1246,25 +1911,51 @@ if (isMainThread) {
|
|
|
1246
1911
|
const watchDir = async (dir, beforeRestartCallback) => {
|
|
1247
1912
|
if (beforeRestartCallback)
|
|
1248
1913
|
beforeRestart = beforeRestartCallback;
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1914
|
+
const watchTarget = resolveWatchTarget(dir);
|
|
1915
|
+
let usingPolling = watchTarget.mustPoll;
|
|
1916
|
+
let liveWatcher;
|
|
1917
|
+
const openWatcher = () => {
|
|
1918
|
+
const opened = (liveWatcher = guardedWatch(watchTarget.path, {
|
|
1919
|
+
persistent: false,
|
|
1920
|
+
...(usingPolling ? DIRECTORY_POLLING_FALLBACK_OPTIONS : {}),
|
|
1921
|
+
ignored: (path) => {
|
|
1922
|
+
return ignoredPaths.some((ignoredPath) => path.includes(ignoredPath));
|
|
1923
|
+
},
|
|
1924
|
+
}));
|
|
1925
|
+
opened
|
|
1926
|
+
// This runs on the thread that owns every worker, and chokidar emits 'error' unguarded for
|
|
1927
|
+
// anything but ENOENT/ENOTDIR.
|
|
1928
|
+
.on('error', (error) => {
|
|
1929
|
+
if (claimLostNativeWatchError(error))
|
|
1930
|
+
return;
|
|
1931
|
+
if (isWatcherExhaustionError(error)) {
|
|
1932
|
+
if (usingPolling || liveWatcher !== opened)
|
|
1933
|
+
return;
|
|
1934
|
+
warnWatcherFallback(dir);
|
|
1935
|
+
usingPolling = true;
|
|
1936
|
+
Promise.resolve()
|
|
1937
|
+
.then(() => opened.close())
|
|
1938
|
+
.catch(() => { })
|
|
1939
|
+
.then(openWatcher)
|
|
1940
|
+
.catch((reopenError) => console.error(`Could not reopen the ${dir} component-reload watch on polling:`, reopenError));
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
console.error(`Error watching ${dir} for component reloads:`, error);
|
|
1944
|
+
})
|
|
1945
|
+
.on('change', (path) => {
|
|
1946
|
+
changedFiles.add(path);
|
|
1947
|
+
if (queuedRestart)
|
|
1948
|
+
clearTimeout(queuedRestart);
|
|
1949
|
+
queuedRestart = setTimeout(async () => {
|
|
1950
|
+
if (beforeRestart)
|
|
1951
|
+
await beforeRestart();
|
|
1952
|
+
await restartWorkers(undefined, undefined, true, null, '*');
|
|
1953
|
+
console.log('Reloaded Harper components, changed files:', Array.from(changedFiles));
|
|
1954
|
+
changedFiles.clear();
|
|
1955
|
+
}, 100);
|
|
1956
|
+
});
|
|
1957
|
+
};
|
|
1958
|
+
openWatcher();
|
|
1268
1959
|
};
|
|
1269
1960
|
module.exports.watchDir = watchDir;
|
|
1270
1961
|
if (process.env.WATCH_DIR)
|
|
@@ -1282,4 +1973,7 @@ else {
|
|
|
1282
1973
|
realExit(0);
|
|
1283
1974
|
});
|
|
1284
1975
|
}
|
|
1976
|
+
// Required here, not from logging, which must never import this module; last in the file because it
|
|
1977
|
+
// calls onThreadExit.
|
|
1978
|
+
require("./logRotationTransport.js");
|
|
1285
1979
|
//# sourceMappingURL=manageThreads.js.map
|