@harperfast/harper 5.2.10 → 5.3.0-alpha.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 +46 -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 +59 -3
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +2414 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +150 -74
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +37 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +340 -30
- package/components/componentPreparationLock.ts +16 -5
- 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 +270 -78
- package/components/operationsValidation.js +49 -1
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +80 -34
- package/config/configUtils.ts +291 -34
- package/config/harperConfigEnvVars.ts +170 -27
- package/config-root.schema.json +29 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +52 -8
- 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 +48 -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 +54 -5
- 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 +139 -16
- package/dist/components/Application.js +2215 -267
- 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 +1 -0
- package/dist/components/OptionsWatcher.js +141 -74
- 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 +38 -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 +279 -22
- 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/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.js +231 -77
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +49 -1
- package/dist/components/operationsValidation.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 +7 -1
- package/dist/config/RootConfigWatcher.js +64 -27
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configUtils.d.ts +9 -1
- package/dist/config/configUtils.js +254 -33
- 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/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 +51 -10
- 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 +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +52 -0
- package/dist/resources/DatabaseTransaction.d.ts +55 -3
- package/dist/resources/DatabaseTransaction.js +460 -134
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.js +22 -4
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +30 -2
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +94 -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 +20 -11
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +20 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -10
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +14 -1
- package/dist/resources/RocksTransactionLogStore.js +57 -17
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +112 -8
- package/dist/resources/Table.js +1071 -197
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.d.ts +11 -2
- package/dist/resources/auditStore.js +183 -18
- 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/databases.d.ts +150 -9
- package/dist/resources/databases.js +1247 -525
- 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/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +50 -15
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +1 -0
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +6 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- 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/recordLock.d.ts +80 -0
- package/dist/resources/recordLock.js +184 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +152 -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/search.js +158 -32
- package/dist/resources/search.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 +75 -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 +73 -0
- 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 +11 -0
- package/dist/server/http.js +88 -15
- 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 +25 -0
- package/dist/server/serverHelpers/Headers.js +139 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +9 -0
- package/dist/server/serverHelpers/contentTypes.js +32 -25
- 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 +90 -20
- 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 +16 -0
- package/dist/server/storageReclamation.js.map +1 -1
- 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/manageThreads.d.ts +26 -4
- package/dist/server/threads/manageThreads.js +517 -72
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +89 -26
- 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 +46 -15
- 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 +17 -0
- package/dist/utility/errors/hdbError.js +30 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +16 -0
- package/dist/utility/hdbTerms.js +18 -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 +91 -14
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +76 -49
- 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 +9 -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 +86 -0
- package/dist/utility/watcherFallback.js +278 -1
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +199 -72
- 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 +2 -0
- package/json/systemSchema.json +52 -0
- package/npm-shrinkwrap.json +235 -233
- package/package.json +8 -7
- package/resources/DESIGN.md +114 -52
- package/resources/DatabaseTransaction.ts +522 -130
- package/resources/LMDBTransaction.ts +26 -4
- package/resources/PrimaryRocksDatabase.ts +29 -3
- package/resources/RecordEncoder.ts +103 -8
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +17 -9
- package/resources/ResourceInterface.ts +23 -0
- package/resources/RocksIndexStore.ts +30 -10
- package/resources/RocksTransactionLogStore.ts +78 -21
- package/resources/Table.ts +1107 -164
- package/resources/auditStore.ts +187 -21
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/databases.ts +1354 -522
- package/resources/defineTable.ts +18 -2
- package/resources/graphql.ts +70 -16
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +6 -2
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/recordLock.ts +253 -0
- package/resources/replayLogs.ts +147 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/search.ts +154 -32
- 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 +81 -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 +183 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +79 -1
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +99 -18
- 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 +131 -0
- package/server/serverHelpers/contentTypes.ts +29 -21
- 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 +104 -31
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +13 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/manageThreads.js +526 -63
- package/server/threads/socketRouter.ts +81 -26
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +43 -15
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-Br06zdMA.js → Chat-BnCBegQz.js} +1 -1
- package/studio/web/assets/{FloatingChat-BWImX5fA.js → FloatingChat-CoDW1ySS.js} +3 -3
- package/studio/web/assets/{apiToken-DN0nmDsq.js → apiToken-Bwk5BLXW.js} +1 -1
- package/studio/web/assets/{applications-kSxVoyeU.js → applications-DHxGi7JH.js} +1 -1
- package/studio/web/assets/{cssMode-C1JeufH5.js → cssMode-s0cWI-_M.js} +1 -1
- package/studio/web/assets/{editor-qoo9CrGO.js → editor-DNcRHK54.js} +1 -1
- package/studio/web/assets/{html-Dt4IIy04.js → html-Bdssedlg.js} +1 -1
- package/studio/web/assets/{htmlMode-DXgKKr4C.js → htmlMode-CoDlJ3fw.js} +1 -1
- package/studio/web/assets/{index-6onkYFOG.js → index-D6sxmFLR.js} +5 -5
- package/studio/web/assets/{index.lazy-BrCFnpNJ.js → index.lazy-tmU5BS8s.js} +1 -1
- package/studio/web/assets/{javascript-DNCQGUBc.js → javascript-B8meVSTH.js} +1 -1
- package/studio/web/assets/{jsonMode-CR6HWruP.js → jsonMode-DpIPd35T.js} +1 -1
- package/studio/web/assets/{languageServices-BM4fI4rS.js → languageServices-C_5FMJzQ.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-DSa1ttcD.js → lspLanguageFeatures-BIzNBzPK.js} +1 -1
- package/studio/web/assets/{notifications-BHXLnh6x.js → notifications-CQf18QKb.js} +1 -1
- package/studio/web/assets/{notifications-CUmtIA6z.js → notifications-CvZivSbh.js} +1 -1
- package/studio/web/assets/{profile-8BeFSF3j.js → profile-DdOwtntb.js} +1 -1
- package/studio/web/assets/{regions-C8qR0HhD.js → regions-n69fwagr.js} +1 -1
- package/studio/web/assets/{register-B4n5i0SD.js → register-PfWTCXWB.js} +2 -2
- package/studio/web/assets/{setComponentFile-g0_B0lgX.js → setComponentFile-Bg6O7X0S.js} +1 -1
- package/studio/web/assets/{setup-B0CTj_Q5.js → setup-CUx_aUDl.js} +2 -2
- package/studio/web/assets/{status-C6Yib7-K.js → status-D7BVKqX9.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Dgta7bVi.js → toggleHighContrast-DBSyXzMr.js} +1 -1
- package/studio/web/assets/{tsMode-CH_jHvU-.js → tsMode-BByKCjBS.js} +1 -1
- package/studio/web/assets/{typescript-Co9LCXd5.js → typescript-DDLnLpw9.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-wKC8NsC_.js → useEntityRestURL-31CHGaHk.js} +1 -1
- package/studio/web/assets/{workers-CWeLxCXA.js → workers-pR3jRY9D.js} +1 -1
- package/studio/web/assets/{xml-BadC-0Rk.js → xml-2iRnMhQO.js} +1 -1
- package/studio/web/assets/{yaml-BiUfxPbC.js → yaml-Bf92gJpd.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 +29 -0
- package/utility/hdbTerms.ts +17 -0
- package/utility/logging/harper_logger.ts +87 -14
- package/utility/logging/logRotator.ts +72 -45
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +10 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +270 -0
- package/validation/configValidator.ts +209 -72
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
|
@@ -7,7 +7,7 @@ const { realExit } = require('./workerProcessGuard.ts');
|
|
|
7
7
|
|
|
8
8
|
const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
|
|
9
9
|
const { spawn, spawnSync } = require('node:child_process');
|
|
10
|
-
const { readFileSync } = require('node:fs');
|
|
10
|
+
const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
|
|
11
11
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
12
12
|
const { join, isAbsolute, extname } = require('path');
|
|
13
13
|
const { pathToFileURL } = require('url');
|
|
@@ -21,7 +21,16 @@ const { randomBytes } = require('crypto');
|
|
|
21
21
|
const { _assignPackageExport } = require('../../globals.js');
|
|
22
22
|
const { PACKAGE_ROOT } = require('../../utility/packageUtils.js');
|
|
23
23
|
const { resolvePreloadModules } = require('./resolvePreload.ts');
|
|
24
|
+
const { resolveThreadHeapMemoryMb } = require('./threadHeapMemory.ts');
|
|
24
25
|
const { getConfigPath } = require('../../config/configUtils.ts');
|
|
26
|
+
const { resolveWatchTarget } = require('../../utility/watchPath.ts');
|
|
27
|
+
const {
|
|
28
|
+
DIRECTORY_POLLING_FALLBACK_OPTIONS,
|
|
29
|
+
claimLostNativeWatchError,
|
|
30
|
+
guardedWatch,
|
|
31
|
+
isWatcherExhaustionError,
|
|
32
|
+
warnWatcherFallback,
|
|
33
|
+
} = require('../../utility/watcherFallback.ts');
|
|
25
34
|
let importModules;
|
|
26
35
|
function getImportModules() {
|
|
27
36
|
if (importModules === undefined)
|
|
@@ -42,7 +51,6 @@ function getRequireModules() {
|
|
|
42
51
|
);
|
|
43
52
|
return requireModules;
|
|
44
53
|
}
|
|
45
|
-
const chokidar = require('chokidar');
|
|
46
54
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
47
55
|
const MB = 1024 * 1024;
|
|
48
56
|
const workers = []; // these are our child workers that we are managing
|
|
@@ -54,8 +62,11 @@ const MAX_UNEXPECTED_RESTARTS = 50;
|
|
|
54
62
|
// down mid-disposal crashes the process, so give that teardown more room before the forced backstop.
|
|
55
63
|
let threadTerminationTimeout = process.env.DEV_MODE === 'true' || process.env.DEV_MODE === '1' ? 30000 : 10000;
|
|
56
64
|
const RESTART_TYPE = 'restart';
|
|
65
|
+
const RESTART_PROGRESS_HEARTBEAT_MS = 15000;
|
|
57
66
|
const REQUEST_THREAD_INFO = 'request_thread_info';
|
|
58
67
|
const RESOURCE_REPORT = 'resource_report';
|
|
68
|
+
const OS_THREAD_ID = 'os-thread-id';
|
|
69
|
+
const STUCK_WORKER_REPORT = 'stuck-worker-report';
|
|
59
70
|
const THREAD_INFO = 'thread_info';
|
|
60
71
|
const ADDED_PORT = 'added-port';
|
|
61
72
|
const ACKNOWLEDGEMENT = 'ack';
|
|
@@ -154,16 +165,14 @@ module.exports = {
|
|
|
154
165
|
beginProcessShutdown,
|
|
155
166
|
registerWorkerDataProvider,
|
|
156
167
|
onThreadExit,
|
|
168
|
+
hasThreadExited,
|
|
169
|
+
notifyThreadExit,
|
|
157
170
|
registerProcessGroup,
|
|
158
171
|
unregisterProcessGroup,
|
|
172
|
+
isProcessGroupAlive,
|
|
159
173
|
isThreadRunning,
|
|
160
174
|
waitUntilConfirmedGone,
|
|
161
175
|
restartNumber: workerData?.restartNumber || 1,
|
|
162
|
-
// Identifies this process incarnation, where the PID cannot: a container reuses PID 1. Minted once
|
|
163
|
-
// on the main thread and carried to workers, so live siblings agree on it — one derived per thread
|
|
164
|
-
// would not. `undefined` on a worker started without it; consumers must fall back, not treat that
|
|
165
|
-
// as a mismatch.
|
|
166
|
-
processIncarnation: workerData ? workerData.processIncarnation : randomBytes(8).toString('hex'),
|
|
167
176
|
};
|
|
168
177
|
|
|
169
178
|
connectedPorts.onMessageByType = onMessageByType;
|
|
@@ -230,7 +239,6 @@ const RESERVED_WORKER_DATA_KEYS = [
|
|
|
230
239
|
'workerCount',
|
|
231
240
|
'name',
|
|
232
241
|
'restartNumber',
|
|
233
|
-
'processIncarnation',
|
|
234
242
|
'ticketKeys',
|
|
235
243
|
'noServerStart',
|
|
236
244
|
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
@@ -303,6 +311,15 @@ if (!parentPort) {
|
|
|
303
311
|
onMessageByType(RESOURCE_REPORT, (message, worker) => {
|
|
304
312
|
if (worker) recordResourceReport(worker, message);
|
|
305
313
|
});
|
|
314
|
+
onMessageByType(OS_THREAD_ID, (message, worker) => {
|
|
315
|
+
if (worker) worker.osThreadId = message.osThreadId;
|
|
316
|
+
});
|
|
317
|
+
onMessageByType(STUCK_WORKER_REPORT, (message) => {
|
|
318
|
+
for (const threadId of message.threadIds) {
|
|
319
|
+
const worker = workers.find((worker) => worker.threadId === threadId);
|
|
320
|
+
if (worker) logStuckWorkerDiagnostics(worker);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
306
323
|
onMessageByType(AWAIT_PROCESS_GROUP_TERMINATION, async (message, worker) => {
|
|
307
324
|
if (!worker) return;
|
|
308
325
|
await (pendingProcessGroupTerminations.get(message.ownerThreadId) ?? Promise.resolve());
|
|
@@ -314,6 +331,7 @@ if (!parentPort) {
|
|
|
314
331
|
}
|
|
315
332
|
// postMessage type listeners that are registered in other ways or can be registered later
|
|
316
333
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED, null);
|
|
334
|
+
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.CHILD_STARTUP_PHASE, null);
|
|
317
335
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.SCHEMA, null);
|
|
318
336
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.USER, null);
|
|
319
337
|
listenersByType.set(hdbTerms.ITC_EVENT_TYPES.COMPONENT_STATUS_REQUEST, null);
|
|
@@ -351,7 +369,7 @@ function startWorker(path, options = {}) {
|
|
|
351
369
|
// and lower than total memory
|
|
352
370
|
availableMemory = Math.min(availableMemory, totalmem(), 20000 * MB);
|
|
353
371
|
const maxOldMemory =
|
|
354
|
-
envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY) ??
|
|
372
|
+
resolveThreadHeapMemoryMb(envMgr.get(hdbTerms.CONFIG_PARAMS.THREADS_MAXHEAPMEMORY)) ??
|
|
355
373
|
Math.max(Math.floor(availableMemory / MB / (10 + (options.threadCount || 1) / 4)), 512);
|
|
356
374
|
// Max young memory space (semi-space for scavenger) is 1/128 of max memory (limited to 16-64). For most of our m5
|
|
357
375
|
// machines this will be 64MB (less for t3's). This is based on recommendations from:
|
|
@@ -415,7 +433,6 @@ function startWorker(path, options = {}) {
|
|
|
415
433
|
workerCount: (workerCount = options.threadCount),
|
|
416
434
|
name: options.name,
|
|
417
435
|
restartNumber: module.exports.restartNumber,
|
|
418
|
-
processIncarnation: module.exports.processIncarnation,
|
|
419
436
|
ticketKeys: getTicketKeys(),
|
|
420
437
|
},
|
|
421
438
|
transferList: portsToSend,
|
|
@@ -454,7 +471,10 @@ function startWorker(path, options = {}) {
|
|
|
454
471
|
if (worker.unexpectedRestarts < MAX_UNEXPECTED_RESTARTS) {
|
|
455
472
|
options.unexpectedRestarts = worker.unexpectedRestarts + 1;
|
|
456
473
|
startWorker(path, options);
|
|
457
|
-
} else
|
|
474
|
+
} else {
|
|
475
|
+
harperLogger.error(`Thread has been restarted ${worker.unexpectedRestarts} times and will not be restarted`);
|
|
476
|
+
options.onRestartExhausted?.(worker);
|
|
477
|
+
}
|
|
458
478
|
}
|
|
459
479
|
});
|
|
460
480
|
workers.push(worker);
|
|
@@ -474,16 +494,24 @@ const OVERLAPPING_RESTART_TYPES = [hdbTerms.THREAD_TYPES.HTTP];
|
|
|
474
494
|
* threads at the same time we shutdown new ones. However, we usually want to limit how many we do at once to avoid
|
|
475
495
|
* excessive load and to keep things responsive. This parameter throttles the restarts to minimize load from
|
|
476
496
|
* thread startups.
|
|
477
|
-
* @
|
|
497
|
+
* @param onProgress Called each time a worker has been replaced (or its replacement has been given
|
|
498
|
+
* up on), so a caller waiting on a wide pool can tell a slow restart from a stalled one.
|
|
499
|
+
* @returns {Promise<{workersKeptOnOldCode: number, replacementsNotStarted: number}|{declined: true}|undefined>}
|
|
500
|
+
* from the main thread, how many workers were left running the old code and how many replacements
|
|
501
|
+
* never reported that they started — or `{declined: true}` when the process is already shutting down;
|
|
502
|
+
* from a worker, nothing — the restart is handed to the main thread.
|
|
478
503
|
*/
|
|
479
504
|
|
|
480
505
|
async function restartWorkers(
|
|
481
506
|
name = null,
|
|
482
507
|
maxWorkersDown = Math.max(Math.floor(workerCount / 8), 1), // restart 1/8 of the threads at a time, but at least 1
|
|
483
|
-
startReplacementThreads = true
|
|
508
|
+
startReplacementThreads = true,
|
|
509
|
+
onProgress = null
|
|
484
510
|
) {
|
|
485
511
|
if (isMainThread) {
|
|
486
|
-
|
|
512
|
+
// Declining is not the same as delegating: a caller reporting on the restart must not read this
|
|
513
|
+
// as "another thread is completing it".
|
|
514
|
+
if (processShuttingDown && startReplacementThreads) return { declined: true };
|
|
487
515
|
try {
|
|
488
516
|
// we do this because it is possible for a component to chdir to itself, get re-deployed and then the cwd
|
|
489
517
|
// inode link is invalid and it can cause a lot of problems. But process.cwd() still returns the path, for
|
|
@@ -498,7 +526,16 @@ async function restartWorkers(
|
|
|
498
526
|
// This is here to prevent circular dependencies
|
|
499
527
|
if (startReplacementThreads) {
|
|
500
528
|
const { loadRootComponents } = require('../loadRootComponents.js');
|
|
501
|
-
|
|
529
|
+
// Installing and loading every root component reports nothing and can outlast a caller's idle
|
|
530
|
+
// window on its own (a cold npm cache, a large dependency graph), so beat while it runs. The
|
|
531
|
+
// caller's absolute ceiling is what bounds a load that never finishes.
|
|
532
|
+
const loading = setInterval(() => onProgress?.(), RESTART_PROGRESS_HEARTBEAT_MS).unref();
|
|
533
|
+
try {
|
|
534
|
+
await loadRootComponents();
|
|
535
|
+
} finally {
|
|
536
|
+
clearInterval(loading);
|
|
537
|
+
}
|
|
538
|
+
onProgress?.();
|
|
502
539
|
}
|
|
503
540
|
|
|
504
541
|
module.exports.restartNumber++;
|
|
@@ -508,7 +545,17 @@ async function restartWorkers(
|
|
|
508
545
|
maxWorkersDown = maxWorkersDown * workers.length;
|
|
509
546
|
}
|
|
510
547
|
// make a copy of the workers before iterating them, as the workers array mutates a lot during this
|
|
511
|
-
let waitingToFinish = []; // promises for workers we
|
|
548
|
+
let waitingToFinish = []; // promises for workers we are replacing, spliced as each is replaced
|
|
549
|
+
// Every replacement that was started without being awaited first, so this function can still
|
|
550
|
+
// resolve only once each one is accepting connections.
|
|
551
|
+
let replacementsStarting = [];
|
|
552
|
+
// A replacement that never came up leaves the pool in one of two very different states, and a
|
|
553
|
+
// caller waiting on this restart needs them apart: the pre-start path keeps the *old* worker
|
|
554
|
+
// serving old code, while a replacement that exits after its predecessor is already gone only
|
|
555
|
+
// costs capacity until startWorker's auto-restart brings a fresh one up.
|
|
556
|
+
let workersKeptOnOldCode = 0;
|
|
557
|
+
let replacementsNotStarted = 0;
|
|
558
|
+
let replacementsFailedToStart = 0;
|
|
512
559
|
// We can only start the replacement *before* the old worker releases its port when the OS lets
|
|
513
560
|
// both listen on the same port at once (SO_REUSEPORT). Without that — Windows (no SO_REUSEPORT),
|
|
514
561
|
// macOS (unreliable SO_REUSEPORT, so workers bind exclusively), and Bun — the replacement can't
|
|
@@ -520,7 +567,9 @@ async function restartWorkers(
|
|
|
520
567
|
// thread keeps serving the HTTP ports throughout. This ordering is also what lets
|
|
521
568
|
// listenOnPorts() treat a dedicated listener's EADDRINUSE as an external conflict.
|
|
522
569
|
const canPreStartReplacement = process.platform !== 'win32' && process.platform !== 'darwin' && !isBun;
|
|
523
|
-
|
|
570
|
+
const restarting = workers.slice(0);
|
|
571
|
+
for (let index = 0; index < restarting.length; index++) {
|
|
572
|
+
const worker = restarting[index];
|
|
524
573
|
// Terminal shutdown: stop replacing workers mid-loop — the guard for every replacement start below.
|
|
525
574
|
if (processShuttingDown && startReplacementThreads) break;
|
|
526
575
|
if ((name && worker.name !== name) || worker.wasShutdown) continue; // filter by type, if specified
|
|
@@ -587,6 +636,8 @@ async function restartWorkers(
|
|
|
587
636
|
// Replacement didn't come up — keep the existing worker serving. Restore its auto-restart
|
|
588
637
|
// protection if it is still alive (it may have exited on its own during the wait).
|
|
589
638
|
if (workers.includes(worker)) worker.wasShutdown = false;
|
|
639
|
+
workersKeptOnOldCode++;
|
|
640
|
+
onProgress?.();
|
|
590
641
|
continue;
|
|
591
642
|
}
|
|
592
643
|
}
|
|
@@ -607,7 +658,16 @@ async function restartWorkers(
|
|
|
607
658
|
// Overlapping types we couldn't pre-start (Windows/Bun): start the replacement now that the old
|
|
608
659
|
// worker is releasing its port. server.close() stops accepting immediately, so the port frees up
|
|
609
660
|
// well before the replacement finishes booting and binds.
|
|
610
|
-
|
|
661
|
+
let replacementStarting;
|
|
662
|
+
if (overlapping && startReplacementThreads && !canPreStartReplacement && !processShuttingDown) {
|
|
663
|
+
replacementStarting = whenWorkerStarted(worker.startCopy()).then((started) => {
|
|
664
|
+
if (!started) replacementsFailedToStart++;
|
|
665
|
+
onProgress?.();
|
|
666
|
+
return started;
|
|
667
|
+
});
|
|
668
|
+
replacementsStarting.push(replacementStarting);
|
|
669
|
+
}
|
|
670
|
+
|
|
611
671
|
let whenDone = new Promise((resolve) => {
|
|
612
672
|
// in case the exit inside the thread doesn't timeout, force it from the outside
|
|
613
673
|
const armTerminate = (delay) =>
|
|
@@ -634,29 +694,62 @@ async function restartWorkers(
|
|
|
634
694
|
// buggy/rogue message can't defer the force-kill unboundedly; a shrink (drain-done reset)
|
|
635
695
|
// passes through untouched. See boundedTerminateDelay for the arithmetic + its unit tests.
|
|
636
696
|
const { boundedTerminateDelay, getShutdownDrainCeilingMs } = require('../../components/shutdownDrain.ts');
|
|
637
|
-
|
|
638
|
-
|
|
697
|
+
const delay = boundedTerminateDelay(
|
|
698
|
+
deadlineMs,
|
|
699
|
+
Date.now(),
|
|
700
|
+
threadTerminationTimeout * 2,
|
|
701
|
+
getShutdownDrainCeilingMs()
|
|
639
702
|
);
|
|
703
|
+
timeout = armTerminate(delay);
|
|
704
|
+
// The worker is telling us it has work still moving and how long it may take, so pass that
|
|
705
|
+
// on: a caller waiting on the restart must not treat a live drain as a stalled one.
|
|
706
|
+
onProgress?.(Date.now() + delay);
|
|
640
707
|
};
|
|
641
708
|
worker.on('exit', () => {
|
|
642
709
|
clearTimeout(timeout);
|
|
710
|
+
onProgress?.();
|
|
643
711
|
worker.extendTerminateDeadline = undefined;
|
|
644
|
-
const index = waitingToFinish.indexOf(whenDone);
|
|
645
|
-
if (index > -1) waitingToFinish.splice(index, 1);
|
|
646
712
|
// non-overlapping types have no advance replacement, so start it once the old one is gone
|
|
647
713
|
if (!overlapping && startReplacementThreads && !processShuttingDown) worker.startCopy();
|
|
648
714
|
resolve();
|
|
649
715
|
});
|
|
650
716
|
});
|
|
651
|
-
|
|
717
|
+
// A worker counts as replaced only once its replacement is accepting connections, not merely
|
|
718
|
+
// once it has exited. This promise is held unawaited between throttle points, so it must not
|
|
719
|
+
// be able to reject.
|
|
720
|
+
const replaced = (replacementStarting ? Promise.all([whenDone, replacementStarting]) : whenDone)
|
|
721
|
+
.catch((error) => harperLogger.warn('Error waiting for a worker to be replaced', error))
|
|
722
|
+
.then(() => {
|
|
723
|
+
const at = waitingToFinish.indexOf(replaced);
|
|
724
|
+
if (at > -1) waitingToFinish.splice(at, 1);
|
|
725
|
+
});
|
|
726
|
+
waitingToFinish.push(replaced);
|
|
652
727
|
if (waitingToFinish.length >= maxWorkersDown) {
|
|
653
|
-
// throttle how many workers are
|
|
728
|
+
// throttle how many workers are down at once to limit load
|
|
654
729
|
await Promise.race(waitingToFinish);
|
|
655
730
|
}
|
|
731
|
+
// Readiness throttling bounds how many workers are down at once, but not how many *fail*: with
|
|
732
|
+
// replacements that never come up, walking the rest of the pool would leave nothing serving.
|
|
733
|
+
// The workers not yet touched are still running the old code, which beats none running at all.
|
|
734
|
+
if (replacementsFailedToStart >= maxWorkersDown) {
|
|
735
|
+
const untouched = restarting
|
|
736
|
+
.slice(index + 1)
|
|
737
|
+
// a worker that exited on its own mid-restart is spliced out of `workers` and
|
|
738
|
+
// auto-restarted onto the new code (see the exit handler above); it is not still on
|
|
739
|
+
// the previous code even though this loop never got to it.
|
|
740
|
+
.filter((other) => (!name || other.name === name) && !other.wasShutdown && workers.includes(other)).length;
|
|
741
|
+
harperLogger.error(
|
|
742
|
+
`${replacementsFailedToStart} replacement worker thread(s) did not start; stopping this restart with ${untouched} worker(s) still on the previous code`
|
|
743
|
+
);
|
|
744
|
+
workersKeptOnOldCode += untouched;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
656
747
|
}
|
|
657
|
-
// seems appropriate to wait for this to finish, but the API doesn't actually wait for this function
|
|
658
|
-
// to finish, so not that important
|
|
659
748
|
await Promise.all(waitingToFinish);
|
|
749
|
+
// A caller awaiting this needs it to mean "the pool is serving the new code", so wait out the
|
|
750
|
+
// replacements that could only be started once their predecessor released its exclusive ports.
|
|
751
|
+
replacementsNotStarted = (await Promise.all(replacementsStarting)).filter((started) => !started).length;
|
|
752
|
+
return { workersKeptOnOldCode, replacementsNotStarted };
|
|
660
753
|
} else {
|
|
661
754
|
parentPort.postMessage({
|
|
662
755
|
type: RESTART_TYPE,
|
|
@@ -664,6 +757,51 @@ async function restartWorkers(
|
|
|
664
757
|
});
|
|
665
758
|
}
|
|
666
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Resolve once a newly started worker reports that it is accepting connections, or gives up on it.
|
|
762
|
+
* There is no old worker left to fall back on here, so a replacement that fails to start is left to
|
|
763
|
+
* startWorker's own auto-restart handling; this only stops waiting on it.
|
|
764
|
+
* @param newWorker The replacement worker
|
|
765
|
+
* @returns {Promise<boolean>} whether the worker reported that it started
|
|
766
|
+
*/
|
|
767
|
+
function whenWorkerStarted(newWorker) {
|
|
768
|
+
return new Promise((resolve) => {
|
|
769
|
+
const cleanup = () => {
|
|
770
|
+
clearTimeout(timeout);
|
|
771
|
+
newWorker.off('message', startListener);
|
|
772
|
+
newWorker.off('exit', exitListener);
|
|
773
|
+
};
|
|
774
|
+
const timeout = setTimeout(
|
|
775
|
+
() => {
|
|
776
|
+
harperLogger.error('Replacement worker did not start in time', newWorker.threadId);
|
|
777
|
+
cleanup();
|
|
778
|
+
// Its predecessor is already gone, so a replacement wedged in boot is a worker slot serving
|
|
779
|
+
// nothing until the process restarts. Stop it and let startWorker's exit handling replace it.
|
|
780
|
+
if (isBun) {
|
|
781
|
+
// terminate() triggers a NAPI segfault in Bun; ask the worker to self-exit instead.
|
|
782
|
+
try {
|
|
783
|
+
newWorker.postMessage({ type: FORCE_EXIT });
|
|
784
|
+
} catch {}
|
|
785
|
+
} else newWorker.terminate();
|
|
786
|
+
resolve(false);
|
|
787
|
+
},
|
|
788
|
+
Math.max(threadTerminationTimeout * 2, 60000)
|
|
789
|
+
).unref();
|
|
790
|
+
const startListener = (message) => {
|
|
791
|
+
if (message.type === hdbTerms.ITC_EVENT_TYPES.CHILD_STARTED) {
|
|
792
|
+
cleanup();
|
|
793
|
+
resolve(true);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
const exitListener = () => {
|
|
797
|
+
harperLogger.warn('Replacement worker exited before starting', newWorker.threadId);
|
|
798
|
+
cleanup();
|
|
799
|
+
resolve(false);
|
|
800
|
+
};
|
|
801
|
+
newWorker.on('message', startListener);
|
|
802
|
+
newWorker.on('exit', exitListener);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
667
805
|
function shutdownWorkers(name) {
|
|
668
806
|
return restartWorkers(name, Infinity, false);
|
|
669
807
|
}
|
|
@@ -791,18 +929,154 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
791
929
|
timer = undefined;
|
|
792
930
|
const stuck = [];
|
|
793
931
|
for (let ackHandler of [...pending]) {
|
|
794
|
-
stuck.push(ackHandler.port
|
|
932
|
+
stuck.push(ackHandler.port);
|
|
795
933
|
ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and resolves
|
|
796
934
|
}
|
|
797
935
|
harperLogger.warn(
|
|
798
|
-
`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.join(', ')} within ${timeout}ms; proceeding best-effort`
|
|
936
|
+
`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`
|
|
799
937
|
);
|
|
938
|
+
if (isMainThread) for (let port of stuck) logStuckWorkerDiagnostics(port);
|
|
939
|
+
else if (parentPort) {
|
|
940
|
+
// Only main holds the Worker objects (and tids), so a worker-originated timeout is sampled there.
|
|
941
|
+
const threadIds = stuck.map((port) => port?.threadId).filter((threadId) => threadId > 0);
|
|
942
|
+
if (threadIds.length > 0) parentPort.postMessage({ type: STUCK_WORKER_REPORT, threadIds });
|
|
943
|
+
}
|
|
800
944
|
}, timeout);
|
|
801
945
|
timer.unref?.();
|
|
802
946
|
}
|
|
803
947
|
});
|
|
804
948
|
}
|
|
805
949
|
|
|
950
|
+
// Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
|
|
951
|
+
function getOsThreadId() {
|
|
952
|
+
try {
|
|
953
|
+
const tid = Number(readlinkSync('/proc/thread-self').split('/').pop());
|
|
954
|
+
return Number.isInteger(tid) && tid > 0 ? tid : undefined;
|
|
955
|
+
} catch {
|
|
956
|
+
return undefined;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function readTaskFile(tid, name) {
|
|
961
|
+
try {
|
|
962
|
+
return readFileSync(`/proc/self/task/${tid}/${name}`, 'utf8').trim();
|
|
963
|
+
} catch {
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Kernel-side view of one thread. Every field is best-effort and reported individually, since
|
|
969
|
+
// wchan/syscall need ptrace read access that a hardened container may deny while stat is open.
|
|
970
|
+
function finiteOrUndefined(value) {
|
|
971
|
+
const number = Number(value);
|
|
972
|
+
return Number.isFinite(number) ? number : undefined;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function readOsThreadState(tid) {
|
|
976
|
+
const thread = { tid };
|
|
977
|
+
const stat = readTaskFile(tid, 'stat');
|
|
978
|
+
if (stat !== undefined) {
|
|
979
|
+
// Fields after the parenthesized comm, so state is [0], utime/stime [11]/[12], starttime [19].
|
|
980
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
981
|
+
thread.state = fields[0];
|
|
982
|
+
const utime = finiteOrUndefined(fields[11]);
|
|
983
|
+
const stime = finiteOrUndefined(fields[12]);
|
|
984
|
+
if (utime !== undefined && stime !== undefined) thread.cpuTicks = utime + stime;
|
|
985
|
+
thread.startTime = fields[19];
|
|
986
|
+
}
|
|
987
|
+
const wchan = readTaskFile(tid, 'wchan');
|
|
988
|
+
if (wchan !== undefined) thread.wchan = wchan;
|
|
989
|
+
// First token only: the syscall number, "running", or -1 (blocked outside a syscall). The
|
|
990
|
+
// remainder is argument registers and the stack/instruction pointers, which don't belong in a log.
|
|
991
|
+
const syscall = readTaskFile(tid, 'syscall');
|
|
992
|
+
if (syscall !== undefined) thread.syscall = syscall.split(' ')[0];
|
|
993
|
+
const status = readTaskFile(tid, 'status');
|
|
994
|
+
if (status !== undefined) {
|
|
995
|
+
thread.voluntarySwitches = finiteOrUndefined(/^voluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
996
|
+
thread.nonvoluntarySwitches = finiteOrUndefined(/^nonvoluntary_ctxt_switches:\s*(\d+)/m.exec(status)?.[1]);
|
|
997
|
+
}
|
|
998
|
+
return thread;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function snapshotWorkerThread(worker) {
|
|
1002
|
+
const snapshot = { at: Date.now() };
|
|
1003
|
+
if (worker.resources?.updated) snapshot.sinceResourceReport = snapshot.at - worker.resources.updated;
|
|
1004
|
+
const eventLoop = worker.performance?.eventLoopUtilization?.();
|
|
1005
|
+
if (eventLoop) snapshot.eventLoop = { idle: eventLoop.idle, active: eventLoop.active };
|
|
1006
|
+
if (worker.osThreadId) snapshot.osThread = readOsThreadState(worker.osThreadId);
|
|
1007
|
+
return snapshot;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function describeThreadState(thread) {
|
|
1011
|
+
return `state=${thread.state ?? '?'} wchan=${thread.wchan ?? '?'} syscall=${thread.syscall ?? '?'}`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function describeSnapshot(snapshot) {
|
|
1015
|
+
const parts = [
|
|
1016
|
+
snapshot.sinceResourceReport === undefined
|
|
1017
|
+
? 'no resource report received'
|
|
1018
|
+
: `last resource report ${snapshot.sinceResourceReport}ms ago`,
|
|
1019
|
+
];
|
|
1020
|
+
if (snapshot.eventLoop)
|
|
1021
|
+
parts.push(
|
|
1022
|
+
`event loop active ${Math.round(snapshot.eventLoop.active)}ms idle ${Math.round(snapshot.eventLoop.idle)}ms`
|
|
1023
|
+
);
|
|
1024
|
+
const thread = snapshot.osThread;
|
|
1025
|
+
if (!thread) parts.push('os thread state unavailable');
|
|
1026
|
+
else
|
|
1027
|
+
parts.push(
|
|
1028
|
+
`os tid ${thread.tid} ${describeThreadState(thread)} cpuTicks=${thread.cpuTicks ?? '?'} ctxtSwitches=${thread.voluntarySwitches ?? '?'}/${thread.nonvoluntarySwitches ?? '?'}`
|
|
1029
|
+
);
|
|
1030
|
+
return parts.join('; ');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function describeDelta(before, after) {
|
|
1034
|
+
return Number.isFinite(before) && Number.isFinite(after) ? `+${after - before}` : '?';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function describeProgress(first, second) {
|
|
1038
|
+
const parts = [];
|
|
1039
|
+
if (first.eventLoop && second.eventLoop)
|
|
1040
|
+
parts.push(
|
|
1041
|
+
`event loop active +${Math.round(second.eventLoop.active - first.eventLoop.active)}ms idle +${Math.round(second.eventLoop.idle - first.eventLoop.idle)}ms`
|
|
1042
|
+
);
|
|
1043
|
+
const before = first.osThread;
|
|
1044
|
+
const after = second.osThread;
|
|
1045
|
+
if (before && after)
|
|
1046
|
+
parts.push(
|
|
1047
|
+
`cpuTicks ${describeDelta(before.cpuTicks, after.cpuTicks)} ctxtSwitches ${describeDelta(before.voluntarySwitches, after.voluntarySwitches)}/${describeDelta(before.nonvoluntarySwitches, after.nonvoluntarySwitches)} ${describeThreadState(after)}`
|
|
1048
|
+
);
|
|
1049
|
+
return parts.join('; ');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// A blocked event loop cannot report itself; two kernel-state samples a second apart separate
|
|
1053
|
+
// "parked on a lock" (no CPU ticks, no context switches) from "spinning".
|
|
1054
|
+
const STUCK_WORKER_SAMPLE_INTERVAL_MS = 1000;
|
|
1055
|
+
const STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS = 30000;
|
|
1056
|
+
function logStuckWorkerDiagnostics(worker) {
|
|
1057
|
+
if (!worker || !workers.includes(worker)) return;
|
|
1058
|
+
const now = Date.now();
|
|
1059
|
+
if (worker.stuckDiagnosticAt !== undefined && now - worker.stuckDiagnosticAt < STUCK_WORKER_DIAGNOSTIC_COOLDOWN_MS)
|
|
1060
|
+
return;
|
|
1061
|
+
worker.stuckDiagnosticAt = now;
|
|
1062
|
+
const threadId = worker.threadId; // Node resets it to -1 once the worker exits
|
|
1063
|
+
const first = snapshotWorkerThread(worker);
|
|
1064
|
+
harperLogger.warn(`Worker thread ${threadId} at ack timeout: ${describeSnapshot(first)}`);
|
|
1065
|
+
if (!first.eventLoop && !first.osThread) return;
|
|
1066
|
+
setTimeout(() => {
|
|
1067
|
+
if (!workers.includes(worker)) {
|
|
1068
|
+
harperLogger.warn(`Worker thread ${threadId} exited before its follow-up sample`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
const second = snapshotWorkerThread(worker);
|
|
1072
|
+
// A recycled tid after a thread exit would attribute another thread's activity to this worker.
|
|
1073
|
+
if (second.osThread && second.osThread.startTime !== first.osThread?.startTime) second.osThread = undefined;
|
|
1074
|
+
harperLogger.warn(
|
|
1075
|
+
`Worker thread ${threadId} over the next ${second.at - first.at}ms: ${describeProgress(first, second) || 'no further state available'}`
|
|
1076
|
+
);
|
|
1077
|
+
}, STUCK_WORKER_SAMPLE_INTERVAL_MS).unref();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
806
1080
|
function sendThreadInfo(targetWorker) {
|
|
807
1081
|
targetWorker.postMessage({
|
|
808
1082
|
type: THREAD_INFO,
|
|
@@ -875,6 +1149,8 @@ if (parentPort && workerData?.addPorts) {
|
|
|
875
1149
|
// parentPort so sendToThread(0, ...) and similar lookups can route back to main.
|
|
876
1150
|
parentPort.threadId = 0;
|
|
877
1151
|
addPort(parentPort);
|
|
1152
|
+
const osThreadId = getOsThreadId();
|
|
1153
|
+
if (osThreadId !== undefined) parentPort.postMessage({ type: OS_THREAD_ID, osThreadId });
|
|
878
1154
|
for (let i = 0, l = workerData.addPorts.length; i < l; i++) {
|
|
879
1155
|
let port = workerData.addPorts[i];
|
|
880
1156
|
port.threadId = workerData.addThreadIds[i];
|
|
@@ -923,17 +1199,23 @@ if (parentPort && workerData?.addPorts) {
|
|
|
923
1199
|
}
|
|
924
1200
|
});
|
|
925
1201
|
let awaitTerminationRequestId = 0;
|
|
926
|
-
awaitProcessGroupTermination = (ownerThreadId) =>
|
|
1202
|
+
awaitProcessGroupTermination = (ownerThreadId, signal) =>
|
|
927
1203
|
new Promise((resolve) => {
|
|
928
1204
|
// Deliberately no timeout: a contender must not reclaim a dead owner's lock while its
|
|
929
1205
|
// process group might still be alive and mutating files, so this mirrors the unbounded
|
|
930
|
-
// wait Application.ts's waitForConfirmedTermination uses for the same reason.
|
|
1206
|
+
// wait Application.ts's waitForConfirmedTermination uses for the same reason. A caller
|
|
1207
|
+
// that needs its own bound (isThreadRunning) passes `signal` so this listener still gets
|
|
1208
|
+
// torn down when that caller gives up, instead of leaking for the life of the worker.
|
|
931
1209
|
const requestId = ++awaitTerminationRequestId;
|
|
932
1210
|
parentPort.on('message', receiveConfirmation);
|
|
1211
|
+
signal?.addEventListener('abort', cleanup, { once: true });
|
|
933
1212
|
parentPort.postMessage({ type: AWAIT_PROCESS_GROUP_TERMINATION, ownerThreadId, requestId });
|
|
1213
|
+
function cleanup() {
|
|
1214
|
+
parentPort.off('message', receiveConfirmation);
|
|
1215
|
+
}
|
|
934
1216
|
function receiveConfirmation(message) {
|
|
935
1217
|
if (message.type === PROCESS_GROUP_TERMINATION_CONFIRMED && message.requestId === requestId) {
|
|
936
|
-
|
|
1218
|
+
cleanup();
|
|
937
1219
|
resolve();
|
|
938
1220
|
}
|
|
939
1221
|
}
|
|
@@ -966,33 +1248,142 @@ const processGroupsByThread = new Map();
|
|
|
966
1248
|
// window component-preparation locking exists to close.
|
|
967
1249
|
const pendingProcessGroupTerminations = new Map();
|
|
968
1250
|
const PROCESS_GROUP_TERMINATION_POLL_MS = 25;
|
|
1251
|
+
const ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS = 1000;
|
|
1252
|
+
const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
|
|
1253
|
+
// Bounds isThreadRunning's own wait (below); Application.ts's waitForConfirmedTermination stays
|
|
1254
|
+
// deliberately unbounded.
|
|
1255
|
+
const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
|
|
1256
|
+
const zombieGroupScanTimes = new Map();
|
|
1257
|
+
const processGroupLivenessStates = new Map();
|
|
1258
|
+
|
|
1259
|
+
function processGroupExists(processGroupId) {
|
|
1260
|
+
try {
|
|
1261
|
+
process.kill(-processGroupId, 0);
|
|
1262
|
+
return true;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
return error.code === 'EPERM';
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function processProbeError(error) {
|
|
1269
|
+
return error?.code ?? error?.message ?? String(error ?? 'unknown error');
|
|
1270
|
+
}
|
|
969
1271
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
// can no longer touch the filesystem, so treat a confirmed zombie as dead rather than poll
|
|
975
|
-
// forever for a reap that will never come. Reproduced by killing a worker mid-install: the
|
|
976
|
-
// installer's process-group leader becomes a permanent `Z` in /proc without this check.
|
|
977
|
-
function isZombieProcessGroupLeader(processGroupId) {
|
|
978
|
-
if (process.platform !== 'linux') return false;
|
|
1272
|
+
function processGroupLeaderState(processGroupId, platform, readStat) {
|
|
1273
|
+
if (platform !== 'linux') {
|
|
1274
|
+
return { state: 'unknown', reason: `zombie-process detection is unavailable on ${platform}` };
|
|
1275
|
+
}
|
|
979
1276
|
let stat;
|
|
980
1277
|
try {
|
|
981
|
-
stat =
|
|
982
|
-
} catch {
|
|
983
|
-
return
|
|
1278
|
+
stat = readStat(`/proc/${processGroupId}/stat`, 'utf8');
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH') return { state: 'missing' };
|
|
1281
|
+
return {
|
|
1282
|
+
state: 'unknown',
|
|
1283
|
+
reason: `reading /proc/${processGroupId}/stat failed (${processProbeError(error)})`,
|
|
1284
|
+
};
|
|
984
1285
|
}
|
|
985
|
-
|
|
986
|
-
|
|
1286
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1287
|
+
if (Number(fields[2]) !== processGroupId) return { state: 'missing' };
|
|
1288
|
+
return { state: fields[0] === 'Z' ? 'zombie' : 'alive' };
|
|
987
1289
|
}
|
|
988
1290
|
|
|
989
|
-
function
|
|
1291
|
+
function scanLinuxProcessGroup(processGroupId, readDirectory, readStat) {
|
|
1292
|
+
let processIds;
|
|
990
1293
|
try {
|
|
991
|
-
|
|
1294
|
+
processIds = readDirectory('/proc');
|
|
992
1295
|
} catch (error) {
|
|
993
|
-
return
|
|
1296
|
+
return {
|
|
1297
|
+
isAlive: null,
|
|
1298
|
+
reason: `reading /proc failed (${processProbeError(error)})`,
|
|
1299
|
+
};
|
|
994
1300
|
}
|
|
995
|
-
|
|
1301
|
+
for (const processId of processIds) {
|
|
1302
|
+
if (!/^\d+$/.test(processId)) continue;
|
|
1303
|
+
let stat;
|
|
1304
|
+
try {
|
|
1305
|
+
stat = readStat(`/proc/${processId}/stat`, 'utf8');
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
if (error.code === 'ENOENT' || error.code === 'ESRCH') continue; // already gone
|
|
1308
|
+
// EACCES/EPERM (e.g. hidepid/ProtectProc) can only happen on a pid we don't own — we
|
|
1309
|
+
// spawned our own group's members as this same user, so their stat is always readable —
|
|
1310
|
+
// so it's not one of ours. Anything else is genuinely unknown, not "not ours".
|
|
1311
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') continue;
|
|
1312
|
+
return {
|
|
1313
|
+
isAlive: null,
|
|
1314
|
+
reason: `reading /proc/${processId}/stat failed (${processProbeError(error)})`,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
1318
|
+
const pgrp = Number(fields[2]);
|
|
1319
|
+
if (!Number.isFinite(pgrp)) {
|
|
1320
|
+
return { isAlive: null, reason: `/proc/${processId}/stat had an unparseable process-group field` };
|
|
1321
|
+
}
|
|
1322
|
+
if (pgrp !== processGroupId) continue;
|
|
1323
|
+
if (fields[0] !== 'Z') return { isAlive: true, reason: `a live member was observed at pid ${processId}` };
|
|
1324
|
+
}
|
|
1325
|
+
return { isAlive: false };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function keepProcessGroupAlive(processGroupId, reason, observationTime, warn) {
|
|
1329
|
+
let livenessState = processGroupLivenessStates.get(processGroupId);
|
|
1330
|
+
if (!livenessState) {
|
|
1331
|
+
livenessState = { observedAt: observationTime, reason, warned: false };
|
|
1332
|
+
processGroupLivenessStates.set(processGroupId, livenessState);
|
|
1333
|
+
} else {
|
|
1334
|
+
livenessState.reason = reason;
|
|
1335
|
+
}
|
|
1336
|
+
if (!livenessState.warned && observationTime - livenessState.observedAt >= PROCESS_GROUP_LIVENESS_WARNING_MS) {
|
|
1337
|
+
livenessState.warned = true;
|
|
1338
|
+
warn(
|
|
1339
|
+
`Process group ${processGroupId} termination remains unconfirmed after ${PROCESS_GROUP_LIVENESS_WARNING_MS}ms: ${livenessState.reason}`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function clearProcessGroupLivenessState(processGroupId) {
|
|
1346
|
+
zombieGroupScanTimes.delete(processGroupId);
|
|
1347
|
+
processGroupLivenessStates.delete(processGroupId);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function isProcessGroupAlive(processGroupId, options) {
|
|
1351
|
+
const platform = options?.platform ?? process.platform;
|
|
1352
|
+
const groupExists = options?.processGroupExists ?? processGroupExists;
|
|
1353
|
+
const readDirectory = options?.readDirectory ?? readdirSync;
|
|
1354
|
+
const readStat = options?.readStat ?? readFileSync;
|
|
1355
|
+
const observationTime = options?.now?.() ?? performance.now();
|
|
1356
|
+
const warn = options?.warn ?? ((message) => harperLogger.warn(message));
|
|
1357
|
+
if (!groupExists(processGroupId)) {
|
|
1358
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
const leaderState = processGroupLeaderState(processGroupId, platform, readStat);
|
|
1362
|
+
if (leaderState.state === 'alive')
|
|
1363
|
+
return keepProcessGroupAlive(processGroupId, 'the process-group leader is still alive', observationTime, warn);
|
|
1364
|
+
if (leaderState.state === 'unknown')
|
|
1365
|
+
return keepProcessGroupAlive(processGroupId, leaderState.reason, observationTime, warn);
|
|
1366
|
+
const scanTime = observationTime;
|
|
1367
|
+
const lastScan = zombieGroupScanTimes.get(processGroupId);
|
|
1368
|
+
if (lastScan !== undefined && scanTime >= lastScan && scanTime - lastScan < ZOMBIE_GROUP_MEMBER_SCAN_INTERVAL_MS) {
|
|
1369
|
+
const reason = processGroupLivenessStates.get(processGroupId)?.reason ?? 'the previous Linux scan is still current';
|
|
1370
|
+
return keepProcessGroupAlive(processGroupId, reason, observationTime, warn);
|
|
1371
|
+
}
|
|
1372
|
+
zombieGroupScanTimes.set(processGroupId, scanTime);
|
|
1373
|
+
let scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1374
|
+
if (scanResult.isAlive !== false)
|
|
1375
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1376
|
+
// A second snapshot catches a member omitted after forking inside the first readdir/stat window.
|
|
1377
|
+
// It narrows rather than closes the race because the second synchronous pass has the same gap.
|
|
1378
|
+
scanResult = scanLinuxProcessGroup(processGroupId, readDirectory, readStat);
|
|
1379
|
+
if (scanResult.isAlive !== false)
|
|
1380
|
+
return keepProcessGroupAlive(processGroupId, scanResult.reason, observationTime, warn);
|
|
1381
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function processGroupIsAlive(processGroupId) {
|
|
1386
|
+
return isProcessGroupAlive(processGroupId);
|
|
996
1387
|
}
|
|
997
1388
|
|
|
998
1389
|
async function waitForProcessGroupExit(processGroupId) {
|
|
@@ -1074,6 +1465,7 @@ function addProcessGroup(ownerThreadId, processGroupId) {
|
|
|
1074
1465
|
}
|
|
1075
1466
|
|
|
1076
1467
|
function removeProcessGroup(ownerThreadId, processGroupId) {
|
|
1468
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1077
1469
|
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1078
1470
|
if (!processGroups) return;
|
|
1079
1471
|
processGroups.delete(processGroupId);
|
|
@@ -1125,12 +1517,48 @@ function unregisterProcessGroup(processGroupId) {
|
|
|
1125
1517
|
else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
|
|
1126
1518
|
}
|
|
1127
1519
|
|
|
1520
|
+
class ProcessGroupTerminationUnconfirmedError extends Error {
|
|
1521
|
+
code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
|
|
1522
|
+
constructor(ownerThreadId, timeoutMs) {
|
|
1523
|
+
super(`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${timeoutMs}ms`);
|
|
1524
|
+
this.name = 'ProcessGroupTerminationUnconfirmedError';
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Bounds awaitProcessGroupTermination for isThreadRunning below. Throws rather than resolving to
|
|
1529
|
+
// "not running": componentPreparationLock's ownerIsAlive/ownerLivenessConfirmed already treat a
|
|
1530
|
+
// throwing isOwnerAlive as "can't confirm — don't steal the claim, don't renew the deadline", so
|
|
1531
|
+
// the lock's own bounded wait is what eventually fails the waiter.
|
|
1532
|
+
async function awaitConfirmedProcessGroupTermination(ownerThreadId) {
|
|
1533
|
+
const abortController = new AbortController();
|
|
1534
|
+
let timedOut = false;
|
|
1535
|
+
const timer = setTimeout(() => {
|
|
1536
|
+
timedOut = true;
|
|
1537
|
+
abortController.abort();
|
|
1538
|
+
}, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1539
|
+
timer.unref?.();
|
|
1540
|
+
try {
|
|
1541
|
+
await Promise.race([
|
|
1542
|
+
awaitProcessGroupTermination(ownerThreadId, abortController.signal),
|
|
1543
|
+
new Promise((resolve) => abortController.signal.addEventListener('abort', resolve, { once: true })),
|
|
1544
|
+
]);
|
|
1545
|
+
} finally {
|
|
1546
|
+
clearTimeout(timer);
|
|
1547
|
+
}
|
|
1548
|
+
if (timedOut) {
|
|
1549
|
+
harperLogger.warn(
|
|
1550
|
+
`Thread ${ownerThreadId}'s process-group termination could not be confirmed after ${THREAD_RUNNING_TERMINATION_BACKSTOP_MS}ms`
|
|
1551
|
+
);
|
|
1552
|
+
throw new ProcessGroupTerminationUnconfirmedError(ownerThreadId, THREAD_RUNNING_TERMINATION_BACKSTOP_MS);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1128
1556
|
async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TIMEOUT_MS) {
|
|
1129
1557
|
if (ownerThreadId === threadId || ownerThreadId === 0) return true;
|
|
1130
1558
|
if ((await getThreadInfo(timeoutMs)).some((worker) => worker.threadId === ownerThreadId)) return true;
|
|
1131
1559
|
// The thread itself is gone, but it may still own process groups whose forced termination is
|
|
1132
1560
|
// in flight — wait for that to be confirmed before reporting the owner as reclaimable.
|
|
1133
|
-
await
|
|
1561
|
+
await awaitConfirmedProcessGroupTermination(ownerThreadId);
|
|
1134
1562
|
return false;
|
|
1135
1563
|
}
|
|
1136
1564
|
|
|
@@ -1140,6 +1568,15 @@ if (isMainThread) {
|
|
|
1140
1568
|
});
|
|
1141
1569
|
}
|
|
1142
1570
|
|
|
1571
|
+
/**
|
|
1572
|
+
* Whether a thread has already been reported dead. Sync, unlike `isThreadRunning`, because it only
|
|
1573
|
+
* reads the tombstone `notifyThreadExit` records below — callers on the exit path need an answer
|
|
1574
|
+
* without awaiting process-group confirmation.
|
|
1575
|
+
*/
|
|
1576
|
+
function hasThreadExited(threadId) {
|
|
1577
|
+
return notifiedDeadThreadIds.has(threadId);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1143
1580
|
function notifyThreadExit(deadThreadId) {
|
|
1144
1581
|
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId)) return;
|
|
1145
1582
|
notifiedDeadThreadIds.add(deadThreadId);
|
|
@@ -1247,23 +1684,49 @@ if (isMainThread) {
|
|
|
1247
1684
|
const ignoredPaths = ['node_modules', '.git'];
|
|
1248
1685
|
const watchDir = async (dir, beforeRestartCallback) => {
|
|
1249
1686
|
if (beforeRestartCallback) beforeRestart = beforeRestartCallback;
|
|
1250
|
-
|
|
1251
|
-
|
|
1687
|
+
const watchTarget = resolveWatchTarget(dir);
|
|
1688
|
+
let usingPolling = watchTarget.mustPoll;
|
|
1689
|
+
let liveWatcher;
|
|
1690
|
+
const openWatcher = () => {
|
|
1691
|
+
const opened = (liveWatcher = guardedWatch(watchTarget.path, {
|
|
1252
1692
|
persistent: false,
|
|
1693
|
+
...(usingPolling ? DIRECTORY_POLLING_FALLBACK_OPTIONS : {}),
|
|
1253
1694
|
ignored: (path) => {
|
|
1254
1695
|
return ignoredPaths.some((ignoredPath) => path.includes(ignoredPath));
|
|
1255
1696
|
},
|
|
1256
|
-
})
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1697
|
+
}));
|
|
1698
|
+
opened
|
|
1699
|
+
// This runs on the thread that owns every worker, and chokidar emits 'error' unguarded for
|
|
1700
|
+
// anything but ENOENT/ENOTDIR.
|
|
1701
|
+
.on('error', (error) => {
|
|
1702
|
+
if (claimLostNativeWatchError(error)) return;
|
|
1703
|
+
if (isWatcherExhaustionError(error)) {
|
|
1704
|
+
if (usingPolling || liveWatcher !== opened) return;
|
|
1705
|
+
warnWatcherFallback(dir);
|
|
1706
|
+
usingPolling = true;
|
|
1707
|
+
Promise.resolve()
|
|
1708
|
+
.then(() => opened.close())
|
|
1709
|
+
.catch(() => {})
|
|
1710
|
+
.then(openWatcher)
|
|
1711
|
+
.catch((reopenError) =>
|
|
1712
|
+
console.error(`Could not reopen the ${dir} component-reload watch on polling:`, reopenError)
|
|
1713
|
+
);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
console.error(`Error watching ${dir} for component reloads:`, error);
|
|
1717
|
+
})
|
|
1718
|
+
.on('change', (path) => {
|
|
1719
|
+
changedFiles.add(path);
|
|
1720
|
+
if (queuedRestart) clearTimeout(queuedRestart);
|
|
1721
|
+
queuedRestart = setTimeout(async () => {
|
|
1722
|
+
if (beforeRestart) await beforeRestart();
|
|
1723
|
+
await restartWorkers();
|
|
1724
|
+
console.log('Reloaded Harper components, changed files:', Array.from(changedFiles));
|
|
1725
|
+
changedFiles.clear();
|
|
1726
|
+
}, 100);
|
|
1727
|
+
});
|
|
1728
|
+
};
|
|
1729
|
+
openWatcher();
|
|
1267
1730
|
};
|
|
1268
1731
|
module.exports.watchDir = watchDir;
|
|
1269
1732
|
if (process.env.WATCH_DIR) watchDir(process.env.WATCH_DIR);
|