@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- 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 +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// intercepted in workers.
|
|
5
5
|
const { realExit } = require("./workerProcessGuard.js");
|
|
6
6
|
const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
|
|
7
|
-
const {
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
|
|
9
9
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
10
|
+
const { confirmWindowsProcessTreeGone, ROOT_SPAWN_ALLOWANCE_MS } = require("./windowsProcessTree.js");
|
|
10
11
|
const { join, isAbsolute, extname } = require('path');
|
|
11
12
|
const { pathToFileURL } = require('url');
|
|
12
13
|
const { server } = require("../Server.js");
|
|
@@ -22,6 +23,7 @@ const { resolvePreloadModules } = require("./resolvePreload.js");
|
|
|
22
23
|
const { resolveThreadHeapMemoryMb } = require("./threadHeapMemory.js");
|
|
23
24
|
const { getConfigPath } = require("../../config/configUtils.js");
|
|
24
25
|
const { resolveWatchTarget } = require("../../utility/watchPath.js");
|
|
26
|
+
const { databaseDropPreparationSnapshot, handleDatabaseDropPreparationOwnerExit, } = require("../../resources/databaseDropPreparation.js");
|
|
25
27
|
const { DIRECTORY_POLLING_FALLBACK_OPTIONS, claimLostNativeWatchError, guardedWatch, isWatcherExhaustionError, warnWatcherFallback, } = require("../../utility/watcherFallback.js");
|
|
26
28
|
let importModules;
|
|
27
29
|
function getImportModules() {
|
|
@@ -125,6 +127,14 @@ function restoreShutdownDeadline() {
|
|
|
125
127
|
}
|
|
126
128
|
catch { }
|
|
127
129
|
}
|
|
130
|
+
function notifyJobCleanupComplete() {
|
|
131
|
+
for (const port of connectedPorts) {
|
|
132
|
+
try {
|
|
133
|
+
port.postMessage({ type: hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE });
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
128
138
|
const listenersByType = new Map();
|
|
129
139
|
const messagesQueuedByType = new Map();
|
|
130
140
|
module.exports = {
|
|
@@ -151,6 +161,7 @@ module.exports = {
|
|
|
151
161
|
onMessageByType,
|
|
152
162
|
broadcast,
|
|
153
163
|
broadcastWithAcknowledgement,
|
|
164
|
+
broadcastWithStrictAcknowledgement,
|
|
154
165
|
getWorkerIndex,
|
|
155
166
|
getWorkerCount,
|
|
156
167
|
getEligibleBroadcastRecipientThreadIds,
|
|
@@ -159,6 +170,7 @@ module.exports = {
|
|
|
159
170
|
setTerminateTimeout,
|
|
160
171
|
extendShutdownDeadline,
|
|
161
172
|
restoreShutdownDeadline,
|
|
173
|
+
notifyJobCleanupComplete,
|
|
162
174
|
beginProcessShutdown,
|
|
163
175
|
registerWorkerDataProvider,
|
|
164
176
|
onThreadExit,
|
|
@@ -166,9 +178,11 @@ module.exports = {
|
|
|
166
178
|
notifyThreadExit,
|
|
167
179
|
registerProcessGroup,
|
|
168
180
|
unregisterProcessGroup,
|
|
181
|
+
addProcessGroup,
|
|
182
|
+
removeProcessGroup,
|
|
183
|
+
terminateProcessGroupsForThread,
|
|
169
184
|
isProcessGroupAlive,
|
|
170
185
|
isThreadRunning,
|
|
171
|
-
waitUntilConfirmedGone,
|
|
172
186
|
restartNumber: workerData?.restartNumber || 1,
|
|
173
187
|
// Identifies this process incarnation, where the PID cannot: a container reuses PID 1. Minted once
|
|
174
188
|
// on the main thread and carried to workers, so live siblings agree on it — one derived per thread
|
|
@@ -347,6 +361,7 @@ const RESERVED_WORKER_DATA_KEYS = [
|
|
|
347
361
|
'restartNumber',
|
|
348
362
|
'processIncarnation',
|
|
349
363
|
'ticketKeys',
|
|
364
|
+
'databaseDropPreparations',
|
|
350
365
|
'noServerStart',
|
|
351
366
|
'isolatedApplication',
|
|
352
367
|
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
@@ -565,6 +580,7 @@ function startWorker(path, options = {}) {
|
|
|
565
580
|
restartNumber: module.exports.restartNumber,
|
|
566
581
|
processIncarnation: module.exports.processIncarnation,
|
|
567
582
|
ticketKeys: getTicketKeys(),
|
|
583
|
+
databaseDropPreparations: databaseDropPreparationSnapshot(),
|
|
568
584
|
},
|
|
569
585
|
transferList: portsToSend,
|
|
570
586
|
...options,
|
|
@@ -1045,14 +1061,31 @@ async function broadcast(message, includeSelf) {
|
|
|
1045
1061
|
const awaitingResponses = new Map();
|
|
1046
1062
|
let nextId = 1;
|
|
1047
1063
|
// Backstop so a wedged-but-alive worker (one whose event loop is blocked and never acks, yet
|
|
1048
|
-
// whose port hasn't closed) can't hang a mutating admin/DDL op forever.
|
|
1049
|
-
//
|
|
1050
|
-
// worker (its port close fires the same ack handlers), so on timeout we proceed best-effort.
|
|
1064
|
+
// whose port hasn't closed) can't hang a mutating admin/DDL op forever. Ordinary broadcasts happen
|
|
1065
|
+
// after the durable write and proceed best-effort; strict preparation broadcasts reject on timeout.
|
|
1051
1066
|
const DEFAULT_ACK_TIMEOUT_MS = 30000;
|
|
1052
|
-
function
|
|
1053
|
-
|
|
1067
|
+
function settleAcknowledgementsForClosedWorker(matches, jobCleanupComplete, exitConfirmed) {
|
|
1068
|
+
for (const [, ackHandler] of awaitingResponses) {
|
|
1069
|
+
if (!matches(ackHandler.port))
|
|
1070
|
+
continue;
|
|
1071
|
+
if (ackHandler.allowNormalJobExit && !jobCleanupComplete && !exitConfirmed)
|
|
1072
|
+
continue;
|
|
1073
|
+
ackHandler(ackHandler.allowNormalJobExit && jobCleanupComplete ? undefined : ackHandler.closeResponse);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function settleAcknowledgementsForClosedPort(port, jobCleanupComplete = false, exitConfirmed = false) {
|
|
1077
|
+
settleAcknowledgementsForClosedWorker((candidate) => candidate === port, jobCleanupComplete, exitConfirmed);
|
|
1078
|
+
}
|
|
1079
|
+
function settleAcknowledgementsForClosedThread(threadId, jobCleanupComplete = false, exitConfirmed = false) {
|
|
1080
|
+
settleAcknowledgementsForClosedWorker((candidate) => candidate.threadId === threadId, jobCleanupComplete, exitConfirmed);
|
|
1081
|
+
}
|
|
1082
|
+
/** @param {boolean|'active'} includeJobWorkers */
|
|
1083
|
+
function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, strict = false, includeJobWorkers = false) {
|
|
1084
|
+
return new Promise((resolve, reject) => {
|
|
1054
1085
|
let waitingCount = 0;
|
|
1055
1086
|
let timer;
|
|
1087
|
+
let initializing = true;
|
|
1088
|
+
const failures = [];
|
|
1056
1089
|
// Tracks the handlers still awaiting an ack for THIS broadcast. Doubles as an
|
|
1057
1090
|
// idempotency guard: a port's handler runs at most once whether it's driven by an ack,
|
|
1058
1091
|
// the close listener, or the timeout below.
|
|
@@ -1062,22 +1095,48 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1062
1095
|
clearTimeout(timer);
|
|
1063
1096
|
timer = undefined;
|
|
1064
1097
|
}
|
|
1065
|
-
|
|
1098
|
+
if (strict && failures.length > 0) {
|
|
1099
|
+
if (failures.length === 1 &&
|
|
1100
|
+
(failures[0].name === 'DatabaseDroppingError' || failures[0].name === 'DatabaseDrainTimeoutError')) {
|
|
1101
|
+
reject(failures[0]);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const error = new AggregateError(failures, 'A worker could not prepare for the schema change');
|
|
1105
|
+
for (const property of ['name', 'code', 'statusCode', 'retryable']) {
|
|
1106
|
+
const value = failures[0][property];
|
|
1107
|
+
if (property === 'name' && value === 'Error')
|
|
1108
|
+
continue;
|
|
1109
|
+
if (value != null && failures.every((failure) => failure[property] === value))
|
|
1110
|
+
error[property] = value;
|
|
1111
|
+
}
|
|
1112
|
+
reject(error);
|
|
1113
|
+
}
|
|
1114
|
+
else
|
|
1115
|
+
resolve();
|
|
1066
1116
|
};
|
|
1067
1117
|
for (let port of connectedPorts) {
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (!isEligibleBroadcastRecipient(port))
|
|
1118
|
+
// Ordinary schema gossip excludes job workers to avoid re-entrant waits. Destructive
|
|
1119
|
+
// preparation opts them in because every native handle must be closed before deletion.
|
|
1120
|
+
if (port.isJobWorker &&
|
|
1121
|
+
(!includeJobWorkers || (includeJobWorkers === 'active' && port.jobCleanupComplete === true)))
|
|
1073
1122
|
continue;
|
|
1123
|
+
let ackHandler;
|
|
1074
1124
|
try {
|
|
1075
1125
|
let requestId = nextId++;
|
|
1076
|
-
|
|
1126
|
+
ackHandler = (response) => {
|
|
1077
1127
|
if (!pending.delete(ackHandler))
|
|
1078
1128
|
return; // already settled for this port
|
|
1129
|
+
if (response?.error) {
|
|
1130
|
+
const error = new Error(`Worker ${port.threadId} could not prepare for the schema change: ${response.error.message ?? response.error}`);
|
|
1131
|
+
error.cause = response.error;
|
|
1132
|
+
for (const property of ['name', 'code', 'statusCode', 'retryable']) {
|
|
1133
|
+
if (response.error[property] != null)
|
|
1134
|
+
error[property] = response.error[property];
|
|
1135
|
+
}
|
|
1136
|
+
failures.push(error);
|
|
1137
|
+
}
|
|
1079
1138
|
awaitingResponses.delete(requestId);
|
|
1080
|
-
if (--waitingCount === 0) {
|
|
1139
|
+
if (--waitingCount === 0 && !initializing) {
|
|
1081
1140
|
finish();
|
|
1082
1141
|
}
|
|
1083
1142
|
if (port !== parentPort && --port.refCount === 0) {
|
|
@@ -1085,39 +1144,39 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1085
1144
|
}
|
|
1086
1145
|
};
|
|
1087
1146
|
ackHandler.port = port;
|
|
1147
|
+
ackHandler.closeResponse = strict
|
|
1148
|
+
? { error: { message: 'exited before acknowledging preparation' } }
|
|
1149
|
+
: undefined;
|
|
1150
|
+
ackHandler.allowNormalJobExit = strict && includeJobWorkers && port.isJobWorker;
|
|
1088
1151
|
pending.add(ackHandler);
|
|
1089
|
-
|
|
1152
|
+
waitingCount++;
|
|
1090
1153
|
port.refCount = (port.refCount || 0) + 1;
|
|
1154
|
+
port.ref();
|
|
1091
1155
|
awaitingResponses.set((message.requestId = requestId), ackHandler);
|
|
1092
1156
|
if (!port.hasAckCloseListener) {
|
|
1093
1157
|
// just set a single close listener that can clean up all the ack handlers for a port that is closed
|
|
1094
1158
|
port.hasAckCloseListener = true;
|
|
1095
|
-
port.on(port.close ? 'close' : 'exit', () =>
|
|
1096
|
-
for (let [, ackHandler] of awaitingResponses) {
|
|
1097
|
-
if (ackHandler.port === port) {
|
|
1098
|
-
ackHandler();
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1159
|
+
port.on(port.close ? 'close' : 'exit', () => settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true));
|
|
1102
1160
|
}
|
|
1103
1161
|
port.postMessage(message);
|
|
1104
|
-
waitingCount++;
|
|
1105
1162
|
}
|
|
1106
1163
|
catch (error) {
|
|
1107
1164
|
harperLogger.error(`Unable to send message to worker`, error);
|
|
1165
|
+
ackHandler?.({ error: { message: error.message ?? String(error) } });
|
|
1108
1166
|
}
|
|
1109
1167
|
}
|
|
1168
|
+
initializing = false;
|
|
1110
1169
|
if (waitingCount === 0)
|
|
1111
|
-
return
|
|
1170
|
+
return finish();
|
|
1112
1171
|
if (timeout > 0) {
|
|
1113
1172
|
timer = setTimeout(() => {
|
|
1114
1173
|
timer = undefined;
|
|
1115
1174
|
const stuck = [];
|
|
1116
1175
|
for (let ackHandler of [...pending]) {
|
|
1117
1176
|
stuck.push(ackHandler.port);
|
|
1118
|
-
ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and
|
|
1177
|
+
ackHandler(strict ? { error: { message: `did not acknowledge within ${timeout}ms` } } : undefined); // same cleanup path as an ack/close; drives waitingCount to 0 and settles
|
|
1119
1178
|
}
|
|
1120
|
-
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`);
|
|
1179
|
+
harperLogger.warn(`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; ${strict ? 'failing the coordinated operation' : 'proceeding best-effort'}`);
|
|
1121
1180
|
if (isMainThread)
|
|
1122
1181
|
for (let port of stuck)
|
|
1123
1182
|
logStuckWorkerDiagnostics(port);
|
|
@@ -1132,6 +1191,10 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1132
1191
|
}
|
|
1133
1192
|
});
|
|
1134
1193
|
}
|
|
1194
|
+
/** @param {boolean|'active'} includeJobWorkers */
|
|
1195
|
+
function broadcastWithStrictAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, includeJobWorkers = false) {
|
|
1196
|
+
return broadcastWithAcknowledgement(message, timeout, true, includeJobWorkers);
|
|
1197
|
+
}
|
|
1135
1198
|
// Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
|
|
1136
1199
|
function getOsThreadId() {
|
|
1137
1200
|
try {
|
|
@@ -1472,6 +1535,10 @@ const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
|
|
|
1472
1535
|
const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
|
|
1473
1536
|
const zombieGroupScanTimes = new Map();
|
|
1474
1537
|
const processGroupLivenessStates = new Map();
|
|
1538
|
+
// When each group's root was registered — by then it was already running, which is what lets the
|
|
1539
|
+
// Windows scan tell our root from a later process that recycled its PID.
|
|
1540
|
+
const processGroupSpawnedAt = new Map();
|
|
1541
|
+
let processGroupRegistrationGeneration = 0;
|
|
1475
1542
|
function processGroupExists(processGroupId) {
|
|
1476
1543
|
try {
|
|
1477
1544
|
process.kill(-processGroupId, 0);
|
|
@@ -1604,102 +1671,111 @@ function isProcessGroupAlive(processGroupId, options) {
|
|
|
1604
1671
|
function processGroupIsAlive(processGroupId) {
|
|
1605
1672
|
return isProcessGroupAlive(processGroupId);
|
|
1606
1673
|
}
|
|
1607
|
-
async function waitForProcessGroupExit(processGroupId) {
|
|
1608
|
-
while (
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
// Mirrors Application.ts's windowsProcessTreeIsAlive: a descendant retains its ParentProcessId
|
|
1612
|
-
// after its parent exits, which is exactly the taskkill "process not found" race, so query the
|
|
1613
|
-
// process table by walking parentage rather than trusting only the root pid.
|
|
1614
|
-
function windowsProcessTreeIsAlive(rootPid) {
|
|
1615
|
-
// Exit code 1 must mean "queried the process table and positively found nothing" — never
|
|
1616
|
-
// "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
|
|
1617
|
-
// otherwise read identically to a confirmed-gone tree and release the lock while a descendant
|
|
1618
|
-
// may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
|
|
1619
|
-
// failure into its own exit code (2), which the caller below already treats as unknown.
|
|
1620
|
-
const script = "$ErrorActionPreference = 'Stop'; try { " +
|
|
1621
|
-
`$rootPid = ${rootPid}; ` +
|
|
1622
|
-
'$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
|
|
1623
|
-
'$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
|
|
1624
|
-
'while ($frontier.Count -gt 0) { ' +
|
|
1625
|
-
'$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
|
|
1626
|
-
'$seen[$parentPid] = $true; foreach ($p in $all) { ' +
|
|
1627
|
-
'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
|
|
1628
|
-
'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
|
|
1629
|
-
'$frontier = $next }; ' +
|
|
1630
|
-
'if ($found) { exit 0 } else { exit 1 } ' +
|
|
1631
|
-
'} catch { exit 2 }';
|
|
1632
|
-
return new Promise((resolve) => {
|
|
1633
|
-
const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1634
|
-
stdio: 'ignore',
|
|
1635
|
-
windowsHide: true,
|
|
1636
|
-
});
|
|
1637
|
-
query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
|
|
1638
|
-
query.once('error', () => resolve(null));
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
// The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
|
|
1642
|
-
// that call still works from a process `exit` handler) but its result is not checked there, since
|
|
1643
|
-
// a nonzero exit is ambiguous between a real failure and the target having already exited. Confirm
|
|
1644
|
-
// via the process table and keep retrying taskkill until the whole tree is positively gone —
|
|
1645
|
-
// reclamation must not proceed on an unconfirmed guess either way. Exported so a test can drive the
|
|
1646
|
-
// retry logic with injected callbacks instead of real Windows processes.
|
|
1647
|
-
async function waitUntilConfirmedGone(attemptTermination, treeIsAlive, pollMs) {
|
|
1648
|
-
for (;;) {
|
|
1649
|
-
// A successful taskkill exit only proves the request was accepted, not that the whole tree
|
|
1650
|
-
// has actually exited — Windows termination is asynchronous, and taskkill can report overall
|
|
1651
|
-
// success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
|
|
1652
|
-
// treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
|
|
1653
|
-
// `true` or `null` (unknown) must keep the loop retrying.
|
|
1654
|
-
await attemptTermination();
|
|
1655
|
-
if ((await treeIsAlive()) === false)
|
|
1674
|
+
async function waitForProcessGroupExit(processGroupId, registration) {
|
|
1675
|
+
while (true) {
|
|
1676
|
+
const currentRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1677
|
+
if (currentRegistration !== undefined && currentRegistration !== registration)
|
|
1656
1678
|
return;
|
|
1657
|
-
|
|
1679
|
+
if (!processGroupIsAlive(processGroupId))
|
|
1680
|
+
return;
|
|
1681
|
+
await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
|
|
1658
1682
|
}
|
|
1659
1683
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1684
|
+
// The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
|
|
1685
|
+
// that call still works from a process `exit` handler). A nonzero exit there is ambiguous between
|
|
1686
|
+
// a real failure and the target having already exited, so only a reported success bounds the
|
|
1687
|
+
// root's lifetime up front (a terminated process cannot spawn); otherwise the scan latches the
|
|
1688
|
+
// root's exit itself, re-terminating the root while it is still found running as ours. Either
|
|
1689
|
+
// way the wait confirms via the process table — reclamation must not proceed on a guess. The
|
|
1690
|
+
// root was created inside the spawner's spawn() call, whose start and return times travel with the
|
|
1691
|
+
// registration so the cross-thread hop adds nothing to the window before it.
|
|
1692
|
+
function waitForWindowsGroupExit(processGroupId, spawn, killedAt) {
|
|
1693
|
+
return confirmWindowsProcessTreeGone({
|
|
1694
|
+
rootPid: processGroupId,
|
|
1695
|
+
rootKnownAt: spawn?.spawnedAt ?? killedAt ?? Date.now(),
|
|
1696
|
+
rootStartedWithinMs: spawn?.spawnStartedAt !== undefined ? spawn.spawnedAt - spawn.spawnStartedAt : ROOT_SPAWN_ALLOWANCE_MS,
|
|
1697
|
+
rootExitedAt: killedAt,
|
|
1698
|
+
}, { pollMs: PROCESS_GROUP_TERMINATION_POLL_MS, label: `process group ${processGroupId}` });
|
|
1699
|
+
}
|
|
1700
|
+
function addProcessGroup(ownerThreadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration) {
|
|
1671
1701
|
if (!Number.isInteger(processGroupId) || processGroupId <= 0)
|
|
1672
1702
|
return;
|
|
1703
|
+
const previousRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1704
|
+
if (previousRegistration &&
|
|
1705
|
+
(previousRegistration.ownerThreadId !== ownerThreadId ||
|
|
1706
|
+
previousRegistration.registrationGeneration !== registrationGeneration)) {
|
|
1707
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1708
|
+
}
|
|
1673
1709
|
let processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1674
1710
|
if (!processGroups)
|
|
1675
1711
|
processGroupsByThread.set(ownerThreadId, (processGroups = new Set()));
|
|
1676
1712
|
processGroups.add(processGroupId);
|
|
1713
|
+
// The map is keyed by PID, which the OS reuses the instant a process exits. Owner distinguishes
|
|
1714
|
+
// threads; generation distinguishes two children of the same thread that receive the same PID.
|
|
1715
|
+
processGroupSpawnedAt.set(processGroupId, {
|
|
1716
|
+
ownerThreadId,
|
|
1717
|
+
registrationGeneration,
|
|
1718
|
+
spawnedAt: Number.isFinite(spawnedAt) ? spawnedAt : Date.now(),
|
|
1719
|
+
spawnStartedAt: Number.isFinite(spawnStartedAt) && spawnStartedAt <= spawnedAt ? spawnStartedAt : undefined,
|
|
1720
|
+
});
|
|
1677
1721
|
}
|
|
1678
|
-
function removeProcessGroup(ownerThreadId, processGroupId) {
|
|
1679
|
-
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1722
|
+
function removeProcessGroup(ownerThreadId, processGroupId, registrationGeneration) {
|
|
1723
|
+
const currentRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1724
|
+
const sameOwnerNewerGeneration = currentRegistration?.ownerThreadId === ownerThreadId &&
|
|
1725
|
+
currentRegistration.registrationGeneration !== registrationGeneration;
|
|
1726
|
+
if (!sameOwnerNewerGeneration) {
|
|
1727
|
+
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1728
|
+
if (processGroups?.delete(processGroupId) && processGroups.size === 0) {
|
|
1729
|
+
processGroupsByThread.delete(ownerThreadId);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (currentRegistration?.ownerThreadId !== ownerThreadId || sameOwnerNewerGeneration)
|
|
1682
1733
|
return;
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
processGroupsByThread.delete(ownerThreadId);
|
|
1734
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1735
|
+
processGroupSpawnedAt.delete(processGroupId);
|
|
1686
1736
|
}
|
|
1687
1737
|
// Returns a promise that resolves once every process group tracked for `ownerThreadId` is
|
|
1688
1738
|
// confirmed terminated. Callers that only need to fire the termination (e.g. the `exit` handler
|
|
1689
1739
|
// below) can ignore the returned promise; isThreadRunning awaits it before declaring a dead
|
|
1690
|
-
// owner reclaimable.
|
|
1691
|
-
// this runs from a process `exit` handler too, where nothing queued after a suspension
|
|
1692
|
-
// guaranteed to run
|
|
1693
|
-
|
|
1740
|
+
// owner reclaimable. On POSIX the kill signal for every group is sent synchronously, before any
|
|
1741
|
+
// `await` — this runs from a process `exit` handler too, where nothing queued after a suspension
|
|
1742
|
+
// point is guaranteed to run, and a process-group SIGKILL is directed at the group id, not a PID
|
|
1743
|
+
// Windows could have already reissued. On Windows a synchronous `taskkill /pid` has no such
|
|
1744
|
+
// process-group semantics — a PID whose process already exited is indistinguishable from one that
|
|
1745
|
+
// never will be, so an unconditional pre-kill can hit whatever now holds a recycled PID (the exact
|
|
1746
|
+
// harper#2273 unrelated-process kill this module exists to prevent). `fromExitHandler` restricts
|
|
1747
|
+
// that blind pre-kill to the one caller that genuinely cannot await anything first (a synchronous
|
|
1748
|
+
// scan via spawnSync before killing is possible there too, just not done); every other
|
|
1749
|
+
// caller lets the identity-checked confirmation loop below issue the first kill, after its own
|
|
1750
|
+
// scan has verified who the PID currently belongs to — UNLESS the process itself is already
|
|
1751
|
+
// shutting down (`processShuttingDown`, set before a restart tears its workers down too): that
|
|
1752
|
+
// path has no guarantee the async loop gets even one scan in before `process.exit()` runs, and by
|
|
1753
|
+
// then this function has already dropped the registration the exit handler would otherwise have
|
|
1754
|
+
// caught, so the blind kill has to fire here instead.
|
|
1755
|
+
function terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler = false } = {}) {
|
|
1694
1756
|
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1695
1757
|
if (!processGroups)
|
|
1696
1758
|
return pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
|
|
1697
1759
|
processGroupsByThread.delete(ownerThreadId);
|
|
1698
|
-
|
|
1760
|
+
// Membership is not ownership: a PID is reusable the moment its process exits.
|
|
1761
|
+
const groupIds = [...processGroups].filter((processGroupId) => {
|
|
1762
|
+
if (processGroupSpawnedAt.get(processGroupId)?.ownerThreadId === ownerThreadId)
|
|
1763
|
+
return true;
|
|
1764
|
+
harperLogger.warn(`Not terminating process group ${processGroupId} for thread ${ownerThreadId}: it is no longer registered to this thread`);
|
|
1765
|
+
return false;
|
|
1766
|
+
});
|
|
1767
|
+
const killedAt = new Map();
|
|
1699
1768
|
for (const processGroupId of groupIds) {
|
|
1700
1769
|
try {
|
|
1701
1770
|
if (process.platform === 'win32') {
|
|
1702
|
-
|
|
1771
|
+
if (!fromExitHandler && !processShuttingDown)
|
|
1772
|
+
continue;
|
|
1773
|
+
const result = spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
|
|
1774
|
+
stdio: 'ignore',
|
|
1775
|
+
windowsHide: true,
|
|
1776
|
+
});
|
|
1777
|
+
if (result.status === 0)
|
|
1778
|
+
killedAt.set(processGroupId, Date.now());
|
|
1703
1779
|
}
|
|
1704
1780
|
else {
|
|
1705
1781
|
process.kill(-processGroupId, 'SIGKILL');
|
|
@@ -1710,7 +1786,17 @@ function terminateProcessGroupsForThread(ownerThreadId) {
|
|
|
1710
1786
|
harperLogger.warn(`Failed to terminate process group ${processGroupId}:`, error);
|
|
1711
1787
|
}
|
|
1712
1788
|
}
|
|
1713
|
-
const termination = Promise.all(groupIds.map((processGroupId) =>
|
|
1789
|
+
const termination = Promise.all(groupIds.map((processGroupId) => {
|
|
1790
|
+
const registration = processGroupSpawnedAt.get(processGroupId);
|
|
1791
|
+
const wait = process.platform === 'win32'
|
|
1792
|
+
? waitForWindowsGroupExit(processGroupId, registration, killedAt.get(processGroupId))
|
|
1793
|
+
: waitForProcessGroupExit(processGroupId, registration);
|
|
1794
|
+
return wait.finally(() => {
|
|
1795
|
+
if (processGroupSpawnedAt.get(processGroupId) === registration) {
|
|
1796
|
+
processGroupSpawnedAt.delete(processGroupId);
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
})).finally(() => {
|
|
1714
1800
|
if (pendingProcessGroupTerminations.get(ownerThreadId) === termination) {
|
|
1715
1801
|
pendingProcessGroupTerminations.delete(ownerThreadId);
|
|
1716
1802
|
}
|
|
@@ -1718,17 +1804,28 @@ function terminateProcessGroupsForThread(ownerThreadId) {
|
|
|
1718
1804
|
pendingProcessGroupTerminations.set(ownerThreadId, termination);
|
|
1719
1805
|
return termination;
|
|
1720
1806
|
}
|
|
1721
|
-
|
|
1807
|
+
// `spawnedAt` / `spawnStartedAt`: the caller's clock as its spawn() of the group's root returned and
|
|
1808
|
+
// as it was called — the root was created between the two.
|
|
1809
|
+
function registerProcessGroup(processGroupId, spawnedAt = Date.now(), spawnStartedAt) {
|
|
1810
|
+
const registrationGeneration = ++processGroupRegistrationGeneration;
|
|
1722
1811
|
if (isMainThread)
|
|
1723
|
-
addProcessGroup(threadId, processGroupId);
|
|
1724
|
-
else
|
|
1725
|
-
parentPort?.postMessage({
|
|
1812
|
+
addProcessGroup(threadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration);
|
|
1813
|
+
else {
|
|
1814
|
+
parentPort?.postMessage({
|
|
1815
|
+
type: REGISTER_PROCESS_GROUP,
|
|
1816
|
+
processGroupId,
|
|
1817
|
+
spawnedAt,
|
|
1818
|
+
spawnStartedAt,
|
|
1819
|
+
registrationGeneration,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
return registrationGeneration;
|
|
1726
1823
|
}
|
|
1727
|
-
function unregisterProcessGroup(processGroupId) {
|
|
1824
|
+
function unregisterProcessGroup(processGroupId, registrationGeneration) {
|
|
1728
1825
|
if (isMainThread)
|
|
1729
|
-
removeProcessGroup(threadId, processGroupId);
|
|
1826
|
+
removeProcessGroup(threadId, processGroupId, registrationGeneration);
|
|
1730
1827
|
else
|
|
1731
|
-
parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
|
|
1828
|
+
parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId, registrationGeneration });
|
|
1732
1829
|
}
|
|
1733
1830
|
class ProcessGroupTerminationUnconfirmedError extends Error {
|
|
1734
1831
|
code = 'ERR_PROCESS_GROUP_TERMINATION_UNCONFIRMED';
|
|
@@ -1776,7 +1873,7 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
|
|
|
1776
1873
|
if (isMainThread) {
|
|
1777
1874
|
process.on('exit', () => {
|
|
1778
1875
|
for (const ownerThreadId of [...processGroupsByThread.keys()])
|
|
1779
|
-
terminateProcessGroupsForThread(ownerThreadId);
|
|
1876
|
+
terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler: true });
|
|
1780
1877
|
});
|
|
1781
1878
|
}
|
|
1782
1879
|
/**
|
|
@@ -1791,6 +1888,7 @@ function notifyThreadExit(deadThreadId) {
|
|
|
1791
1888
|
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId))
|
|
1792
1889
|
return;
|
|
1793
1890
|
notifiedDeadThreadIds.add(deadThreadId);
|
|
1891
|
+
handleDatabaseDropPreparationOwnerExit(deadThreadId);
|
|
1794
1892
|
for (const listener of threadExitListeners) {
|
|
1795
1893
|
try {
|
|
1796
1894
|
listener(deadThreadId);
|
|
@@ -1805,6 +1903,8 @@ function removePort(port, deadThreadId) {
|
|
|
1805
1903
|
// cleanup must still run when the authoritative close/exit event reaches this thread.
|
|
1806
1904
|
if (deadThreadId != null)
|
|
1807
1905
|
terminateProcessGroupsForThread(deadThreadId);
|
|
1906
|
+
const exitConfirmed = !port.close;
|
|
1907
|
+
settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true, exitConfirmed);
|
|
1808
1908
|
const idx = connectedPorts.indexOf(port);
|
|
1809
1909
|
if (idx === -1)
|
|
1810
1910
|
return;
|
|
@@ -1819,7 +1919,12 @@ function removePort(port, deadThreadId) {
|
|
|
1819
1919
|
if (deadThreadId != null) {
|
|
1820
1920
|
for (let remainingPort of connectedPorts) {
|
|
1821
1921
|
try {
|
|
1822
|
-
remainingPort.postMessage({
|
|
1922
|
+
remainingPort.postMessage({
|
|
1923
|
+
type: REMOVE_PORT,
|
|
1924
|
+
threadId: deadThreadId,
|
|
1925
|
+
jobCleanupComplete: port.jobCleanupComplete === true,
|
|
1926
|
+
exitConfirmed,
|
|
1927
|
+
});
|
|
1823
1928
|
}
|
|
1824
1929
|
catch {
|
|
1825
1930
|
// port may already be dead; ignore
|
|
@@ -1836,10 +1941,14 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
1836
1941
|
port
|
|
1837
1942
|
.on('message', (message) => {
|
|
1838
1943
|
if (message.type === REGISTER_PROCESS_GROUP) {
|
|
1839
|
-
addProcessGroup(portThreadId, message.processGroupId);
|
|
1944
|
+
addProcessGroup(portThreadId, message.processGroupId, message.spawnedAt, message.spawnStartedAt, message.registrationGeneration);
|
|
1840
1945
|
}
|
|
1841
1946
|
else if (message.type === UNREGISTER_PROCESS_GROUP) {
|
|
1842
|
-
removeProcessGroup(portThreadId, message.processGroupId);
|
|
1947
|
+
removeProcessGroup(portThreadId, message.processGroupId, message.registrationGeneration);
|
|
1948
|
+
}
|
|
1949
|
+
else if (message.type === hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE) {
|
|
1950
|
+
port.jobCleanupComplete = true;
|
|
1951
|
+
settleAcknowledgementsForClosedPort(port, true);
|
|
1843
1952
|
}
|
|
1844
1953
|
else if (message.type === ADDED_PORT) {
|
|
1845
1954
|
message.port.threadId = message.threadId;
|
|
@@ -1848,13 +1957,20 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
1848
1957
|
else if (message.type === ACKNOWLEDGEMENT) {
|
|
1849
1958
|
let completion = awaitingResponses.get(message.id);
|
|
1850
1959
|
if (completion) {
|
|
1851
|
-
completion();
|
|
1960
|
+
completion(message);
|
|
1852
1961
|
}
|
|
1853
1962
|
}
|
|
1854
1963
|
else if (message.type === REMOVE_PORT) {
|
|
1855
|
-
const
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
1964
|
+
const removedPort = connectedPorts.find((candidate) => candidate.threadId === message.threadId);
|
|
1965
|
+
if (removedPort && !removedPort.close && !message.exitConfirmed)
|
|
1966
|
+
return;
|
|
1967
|
+
settleAcknowledgementsForClosedThread(message.threadId, message.jobCleanupComplete === true, message.exitConfirmed === true);
|
|
1968
|
+
if (removedPort) {
|
|
1969
|
+
if (message.jobCleanupComplete)
|
|
1970
|
+
removedPort.jobCleanupComplete = true;
|
|
1971
|
+
settleAcknowledgementsForClosedPort(removedPort, removedPort.jobCleanupComplete === true);
|
|
1972
|
+
connectedPorts.splice(connectedPorts.indexOf(removedPort), 1);
|
|
1973
|
+
}
|
|
1858
1974
|
// A sibling's port-to-the-dead-worker can close (and broadcast this) before this
|
|
1859
1975
|
// thread's OWN port to that worker fires its 'close'/'exit' — at which point
|
|
1860
1976
|
// removePort() would no-op (already spliced) and threadExitListeners would never
|
|
@@ -1866,10 +1982,16 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
1866
1982
|
}
|
|
1867
1983
|
})
|
|
1868
1984
|
.on('close', () => {
|
|
1869
|
-
|
|
1985
|
+
// A worker's parentPort closing is this worker leaving, not main exiting (server/DESIGN.md).
|
|
1986
|
+
removePort(port, port === parentPort ? undefined : portThreadId);
|
|
1870
1987
|
})
|
|
1871
1988
|
.on('exit', () => {
|
|
1872
|
-
|
|
1989
|
+
// Let a cleanup proof already queued by the worker reach this port before exit becomes
|
|
1990
|
+
// authoritative. The next turn still fails closed if no proof arrives.
|
|
1991
|
+
if (port.isJobWorker && !port.jobCleanupComplete)
|
|
1992
|
+
setImmediate(() => removePort(port, portThreadId));
|
|
1993
|
+
else
|
|
1994
|
+
removePort(port, portThreadId);
|
|
1873
1995
|
});
|
|
1874
1996
|
if (keepRef)
|
|
1875
1997
|
port.refCount = 100;
|