@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
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
const { realExit } = require('./workerProcessGuard.ts');
|
|
7
7
|
|
|
8
8
|
const { Worker, MessageChannel, parentPort, isMainThread, threadId, workerData } = require('worker_threads');
|
|
9
|
-
const {
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
10
|
const { readdirSync, readFileSync, readlinkSync } = require('node:fs');
|
|
11
11
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
12
|
+
const { confirmWindowsProcessTreeGone, ROOT_SPAWN_ALLOWANCE_MS } = require('./windowsProcessTree.ts');
|
|
12
13
|
const { join, isAbsolute, extname } = require('path');
|
|
13
14
|
const { pathToFileURL } = require('url');
|
|
14
15
|
const { server } = require('../Server.ts');
|
|
@@ -24,6 +25,10 @@ const { resolvePreloadModules } = require('./resolvePreload.ts');
|
|
|
24
25
|
const { resolveThreadHeapMemoryMb } = require('./threadHeapMemory.ts');
|
|
25
26
|
const { getConfigPath } = require('../../config/configUtils.ts');
|
|
26
27
|
const { resolveWatchTarget } = require('../../utility/watchPath.ts');
|
|
28
|
+
const {
|
|
29
|
+
databaseDropPreparationSnapshot,
|
|
30
|
+
handleDatabaseDropPreparationOwnerExit,
|
|
31
|
+
} = require('../../resources/databaseDropPreparation.ts');
|
|
27
32
|
const {
|
|
28
33
|
DIRECTORY_POLLING_FALLBACK_OPTIONS,
|
|
29
34
|
claimLostNativeWatchError,
|
|
@@ -143,6 +148,14 @@ function restoreShutdownDeadline() {
|
|
|
143
148
|
} catch {}
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
function notifyJobCleanupComplete() {
|
|
152
|
+
for (const port of connectedPorts) {
|
|
153
|
+
try {
|
|
154
|
+
port.postMessage({ type: hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE });
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
const listenersByType = new Map();
|
|
147
160
|
const messagesQueuedByType = new Map();
|
|
148
161
|
|
|
@@ -170,6 +183,7 @@ module.exports = {
|
|
|
170
183
|
onMessageByType,
|
|
171
184
|
broadcast,
|
|
172
185
|
broadcastWithAcknowledgement,
|
|
186
|
+
broadcastWithStrictAcknowledgement,
|
|
173
187
|
getWorkerIndex,
|
|
174
188
|
getWorkerCount,
|
|
175
189
|
getEligibleBroadcastRecipientThreadIds,
|
|
@@ -178,6 +192,7 @@ module.exports = {
|
|
|
178
192
|
setTerminateTimeout,
|
|
179
193
|
extendShutdownDeadline,
|
|
180
194
|
restoreShutdownDeadline,
|
|
195
|
+
notifyJobCleanupComplete,
|
|
181
196
|
beginProcessShutdown,
|
|
182
197
|
registerWorkerDataProvider,
|
|
183
198
|
onThreadExit,
|
|
@@ -185,9 +200,11 @@ module.exports = {
|
|
|
185
200
|
notifyThreadExit,
|
|
186
201
|
registerProcessGroup,
|
|
187
202
|
unregisterProcessGroup,
|
|
203
|
+
addProcessGroup,
|
|
204
|
+
removeProcessGroup,
|
|
205
|
+
terminateProcessGroupsForThread,
|
|
188
206
|
isProcessGroupAlive,
|
|
189
207
|
isThreadRunning,
|
|
190
|
-
waitUntilConfirmedGone,
|
|
191
208
|
restartNumber: workerData?.restartNumber || 1,
|
|
192
209
|
// Identifies this process incarnation, where the PID cannot: a container reuses PID 1. Minted once
|
|
193
210
|
// on the main thread and carried to workers, so live siblings agree on it — one derived per thread
|
|
@@ -360,6 +377,7 @@ const RESERVED_WORKER_DATA_KEYS = [
|
|
|
360
377
|
'restartNumber',
|
|
361
378
|
'processIncarnation',
|
|
362
379
|
'ticketKeys',
|
|
380
|
+
'databaseDropPreparations',
|
|
363
381
|
'noServerStart',
|
|
364
382
|
'isolatedApplication',
|
|
365
383
|
'__proto__', // never a legitimate payload name; spread would define it as an own property
|
|
@@ -571,6 +589,7 @@ function startWorker(path, options = {}) {
|
|
|
571
589
|
restartNumber: module.exports.restartNumber,
|
|
572
590
|
processIncarnation: module.exports.processIncarnation,
|
|
573
591
|
ticketKeys: getTicketKeys(),
|
|
592
|
+
databaseDropPreparations: databaseDropPreparationSnapshot(),
|
|
574
593
|
},
|
|
575
594
|
transferList: portsToSend,
|
|
576
595
|
...options,
|
|
@@ -1060,14 +1079,41 @@ async function broadcast(message, includeSelf) {
|
|
|
1060
1079
|
const awaitingResponses = new Map();
|
|
1061
1080
|
let nextId = 1;
|
|
1062
1081
|
// Backstop so a wedged-but-alive worker (one whose event loop is blocked and never acks, yet
|
|
1063
|
-
// whose port hasn't closed) can't hang a mutating admin/DDL op forever.
|
|
1064
|
-
//
|
|
1065
|
-
// worker (its port close fires the same ack handlers), so on timeout we proceed best-effort.
|
|
1082
|
+
// whose port hasn't closed) can't hang a mutating admin/DDL op forever. Ordinary broadcasts happen
|
|
1083
|
+
// after the durable write and proceed best-effort; strict preparation broadcasts reject on timeout.
|
|
1066
1084
|
const DEFAULT_ACK_TIMEOUT_MS = 30000;
|
|
1067
|
-
function
|
|
1068
|
-
|
|
1085
|
+
function settleAcknowledgementsForClosedWorker(matches, jobCleanupComplete, exitConfirmed) {
|
|
1086
|
+
for (const [, ackHandler] of awaitingResponses) {
|
|
1087
|
+
if (!matches(ackHandler.port)) continue;
|
|
1088
|
+
if (ackHandler.allowNormalJobExit && !jobCleanupComplete && !exitConfirmed) continue;
|
|
1089
|
+
ackHandler(ackHandler.allowNormalJobExit && jobCleanupComplete ? undefined : ackHandler.closeResponse);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function settleAcknowledgementsForClosedPort(port, jobCleanupComplete = false, exitConfirmed = false) {
|
|
1094
|
+
settleAcknowledgementsForClosedWorker((candidate) => candidate === port, jobCleanupComplete, exitConfirmed);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function settleAcknowledgementsForClosedThread(threadId, jobCleanupComplete = false, exitConfirmed = false) {
|
|
1098
|
+
settleAcknowledgementsForClosedWorker(
|
|
1099
|
+
(candidate) => candidate.threadId === threadId,
|
|
1100
|
+
jobCleanupComplete,
|
|
1101
|
+
exitConfirmed
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** @param {boolean|'active'} includeJobWorkers */
|
|
1106
|
+
function broadcastWithAcknowledgement(
|
|
1107
|
+
message,
|
|
1108
|
+
timeout = DEFAULT_ACK_TIMEOUT_MS,
|
|
1109
|
+
strict = false,
|
|
1110
|
+
includeJobWorkers = false
|
|
1111
|
+
) {
|
|
1112
|
+
return new Promise((resolve, reject) => {
|
|
1069
1113
|
let waitingCount = 0;
|
|
1070
1114
|
let timer;
|
|
1115
|
+
let initializing = true;
|
|
1116
|
+
const failures = [];
|
|
1071
1117
|
// Tracks the handlers still awaiting an ack for THIS broadcast. Doubles as an
|
|
1072
1118
|
// idempotency guard: a port's handler runs at most once whether it's driven by an ack,
|
|
1073
1119
|
// the close listener, or the timeout below.
|
|
@@ -1077,20 +1123,48 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1077
1123
|
clearTimeout(timer);
|
|
1078
1124
|
timer = undefined;
|
|
1079
1125
|
}
|
|
1080
|
-
|
|
1126
|
+
if (strict && failures.length > 0) {
|
|
1127
|
+
if (
|
|
1128
|
+
failures.length === 1 &&
|
|
1129
|
+
(failures[0].name === 'DatabaseDroppingError' || failures[0].name === 'DatabaseDrainTimeoutError')
|
|
1130
|
+
) {
|
|
1131
|
+
reject(failures[0]);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const error = new AggregateError(failures, 'A worker could not prepare for the schema change');
|
|
1135
|
+
for (const property of ['name', 'code', 'statusCode', 'retryable']) {
|
|
1136
|
+
const value = failures[0][property];
|
|
1137
|
+
if (property === 'name' && value === 'Error') continue;
|
|
1138
|
+
if (value != null && failures.every((failure) => failure[property] === value)) error[property] = value;
|
|
1139
|
+
}
|
|
1140
|
+
reject(error);
|
|
1141
|
+
} else resolve();
|
|
1081
1142
|
};
|
|
1082
1143
|
for (let port of connectedPorts) {
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1144
|
+
// Ordinary schema gossip excludes job workers to avoid re-entrant waits. Destructive
|
|
1145
|
+
// preparation opts them in because every native handle must be closed before deletion.
|
|
1146
|
+
if (
|
|
1147
|
+
port.isJobWorker &&
|
|
1148
|
+
(!includeJobWorkers || (includeJobWorkers === 'active' && port.jobCleanupComplete === true))
|
|
1149
|
+
)
|
|
1150
|
+
continue;
|
|
1151
|
+
let ackHandler;
|
|
1088
1152
|
try {
|
|
1089
1153
|
let requestId = nextId++;
|
|
1090
|
-
|
|
1154
|
+
ackHandler = (response) => {
|
|
1091
1155
|
if (!pending.delete(ackHandler)) return; // already settled for this port
|
|
1156
|
+
if (response?.error) {
|
|
1157
|
+
const error = new Error(
|
|
1158
|
+
`Worker ${port.threadId} could not prepare for the schema change: ${response.error.message ?? response.error}`
|
|
1159
|
+
);
|
|
1160
|
+
error.cause = response.error;
|
|
1161
|
+
for (const property of ['name', 'code', 'statusCode', 'retryable']) {
|
|
1162
|
+
if (response.error[property] != null) error[property] = response.error[property];
|
|
1163
|
+
}
|
|
1164
|
+
failures.push(error);
|
|
1165
|
+
}
|
|
1092
1166
|
awaitingResponses.delete(requestId);
|
|
1093
|
-
if (--waitingCount === 0) {
|
|
1167
|
+
if (--waitingCount === 0 && !initializing) {
|
|
1094
1168
|
finish();
|
|
1095
1169
|
}
|
|
1096
1170
|
if (port !== parentPort && --port.refCount === 0) {
|
|
@@ -1098,38 +1172,40 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1098
1172
|
}
|
|
1099
1173
|
};
|
|
1100
1174
|
ackHandler.port = port;
|
|
1175
|
+
ackHandler.closeResponse = strict
|
|
1176
|
+
? { error: { message: 'exited before acknowledging preparation' } }
|
|
1177
|
+
: undefined;
|
|
1178
|
+
ackHandler.allowNormalJobExit = strict && includeJobWorkers && port.isJobWorker;
|
|
1101
1179
|
pending.add(ackHandler);
|
|
1102
|
-
|
|
1180
|
+
waitingCount++;
|
|
1103
1181
|
port.refCount = (port.refCount || 0) + 1;
|
|
1182
|
+
port.ref();
|
|
1104
1183
|
awaitingResponses.set((message.requestId = requestId), ackHandler);
|
|
1105
1184
|
if (!port.hasAckCloseListener) {
|
|
1106
1185
|
// just set a single close listener that can clean up all the ack handlers for a port that is closed
|
|
1107
1186
|
port.hasAckCloseListener = true;
|
|
1108
|
-
port.on(port.close ? 'close' : 'exit', () =>
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
ackHandler();
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
});
|
|
1187
|
+
port.on(port.close ? 'close' : 'exit', () =>
|
|
1188
|
+
settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true)
|
|
1189
|
+
);
|
|
1115
1190
|
}
|
|
1116
1191
|
port.postMessage(message);
|
|
1117
|
-
waitingCount++;
|
|
1118
1192
|
} catch (error) {
|
|
1119
1193
|
harperLogger.error(`Unable to send message to worker`, error);
|
|
1194
|
+
ackHandler?.({ error: { message: error.message ?? String(error) } });
|
|
1120
1195
|
}
|
|
1121
1196
|
}
|
|
1122
|
-
|
|
1197
|
+
initializing = false;
|
|
1198
|
+
if (waitingCount === 0) return finish();
|
|
1123
1199
|
if (timeout > 0) {
|
|
1124
1200
|
timer = setTimeout(() => {
|
|
1125
1201
|
timer = undefined;
|
|
1126
1202
|
const stuck = [];
|
|
1127
1203
|
for (let ackHandler of [...pending]) {
|
|
1128
1204
|
stuck.push(ackHandler.port);
|
|
1129
|
-
ackHandler(); // same cleanup path as an ack/close; drives waitingCount to 0 and
|
|
1205
|
+
ackHandler(strict ? { error: { message: `did not acknowledge within ${timeout}ms` } } : undefined); // same cleanup path as an ack/close; drives waitingCount to 0 and settles
|
|
1130
1206
|
}
|
|
1131
1207
|
harperLogger.warn(
|
|
1132
|
-
`ITC broadcast (type ${message.type}) not acknowledged by worker thread(s) ${stuck.map((port) => port?.threadId).join(', ')} within ${timeout}ms; proceeding best-effort`
|
|
1208
|
+
`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'}`
|
|
1133
1209
|
);
|
|
1134
1210
|
if (isMainThread) for (let port of stuck) logStuckWorkerDiagnostics(port);
|
|
1135
1211
|
else if (parentPort) {
|
|
@@ -1143,6 +1219,11 @@ function broadcastWithAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS)
|
|
|
1143
1219
|
});
|
|
1144
1220
|
}
|
|
1145
1221
|
|
|
1222
|
+
/** @param {boolean|'active'} includeJobWorkers */
|
|
1223
|
+
function broadcastWithStrictAcknowledgement(message, timeout = DEFAULT_ACK_TIMEOUT_MS, includeJobWorkers = false) {
|
|
1224
|
+
return broadcastWithAcknowledgement(message, timeout, true, includeJobWorkers);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1146
1227
|
// Linux only: /proc/thread-self resolves to <pid>/task/<tid> for the calling thread.
|
|
1147
1228
|
function getOsThreadId() {
|
|
1148
1229
|
try {
|
|
@@ -1492,6 +1573,10 @@ const PROCESS_GROUP_LIVENESS_WARNING_MS = 30000;
|
|
|
1492
1573
|
const THREAD_RUNNING_TERMINATION_BACKSTOP_MS = PROCESS_GROUP_LIVENESS_WARNING_MS;
|
|
1493
1574
|
const zombieGroupScanTimes = new Map();
|
|
1494
1575
|
const processGroupLivenessStates = new Map();
|
|
1576
|
+
// When each group's root was registered — by then it was already running, which is what lets the
|
|
1577
|
+
// Windows scan tell our root from a later process that recycled its PID.
|
|
1578
|
+
const processGroupSpawnedAt = new Map();
|
|
1579
|
+
let processGroupRegistrationGeneration = 0;
|
|
1495
1580
|
|
|
1496
1581
|
function processGroupExists(processGroupId) {
|
|
1497
1582
|
try {
|
|
@@ -1623,107 +1708,115 @@ function processGroupIsAlive(processGroupId) {
|
|
|
1623
1708
|
return isProcessGroupAlive(processGroupId);
|
|
1624
1709
|
}
|
|
1625
1710
|
|
|
1626
|
-
async function waitForProcessGroupExit(processGroupId) {
|
|
1627
|
-
while (
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
// process table by walking parentage rather than trusting only the root pid.
|
|
1633
|
-
function windowsProcessTreeIsAlive(rootPid) {
|
|
1634
|
-
// Exit code 1 must mean "queried the process table and positively found nothing" — never
|
|
1635
|
-
// "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
|
|
1636
|
-
// otherwise read identically to a confirmed-gone tree and release the lock while a descendant
|
|
1637
|
-
// may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
|
|
1638
|
-
// failure into its own exit code (2), which the caller below already treats as unknown.
|
|
1639
|
-
const script =
|
|
1640
|
-
"$ErrorActionPreference = 'Stop'; try { " +
|
|
1641
|
-
`$rootPid = ${rootPid}; ` +
|
|
1642
|
-
'$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
|
|
1643
|
-
'$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
|
|
1644
|
-
'while ($frontier.Count -gt 0) { ' +
|
|
1645
|
-
'$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
|
|
1646
|
-
'$seen[$parentPid] = $true; foreach ($p in $all) { ' +
|
|
1647
|
-
'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
|
|
1648
|
-
'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
|
|
1649
|
-
'$frontier = $next }; ' +
|
|
1650
|
-
'if ($found) { exit 0 } else { exit 1 } ' +
|
|
1651
|
-
'} catch { exit 2 }';
|
|
1652
|
-
return new Promise((resolve) => {
|
|
1653
|
-
const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1654
|
-
stdio: 'ignore',
|
|
1655
|
-
windowsHide: true,
|
|
1656
|
-
});
|
|
1657
|
-
query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
|
|
1658
|
-
query.once('error', () => resolve(null));
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
|
|
1663
|
-
// that call still works from a process `exit` handler) but its result is not checked there, since
|
|
1664
|
-
// a nonzero exit is ambiguous between a real failure and the target having already exited. Confirm
|
|
1665
|
-
// via the process table and keep retrying taskkill until the whole tree is positively gone —
|
|
1666
|
-
// reclamation must not proceed on an unconfirmed guess either way. Exported so a test can drive the
|
|
1667
|
-
// retry logic with injected callbacks instead of real Windows processes.
|
|
1668
|
-
async function waitUntilConfirmedGone(attemptTermination, treeIsAlive, pollMs) {
|
|
1669
|
-
for (;;) {
|
|
1670
|
-
// A successful taskkill exit only proves the request was accepted, not that the whole tree
|
|
1671
|
-
// has actually exited — Windows termination is asynchronous, and taskkill can report overall
|
|
1672
|
-
// success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
|
|
1673
|
-
// treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
|
|
1674
|
-
// `true` or `null` (unknown) must keep the loop retrying.
|
|
1675
|
-
await attemptTermination();
|
|
1676
|
-
if ((await treeIsAlive()) === false) return;
|
|
1677
|
-
await delay(pollMs);
|
|
1711
|
+
async function waitForProcessGroupExit(processGroupId, registration) {
|
|
1712
|
+
while (true) {
|
|
1713
|
+
const currentRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1714
|
+
if (currentRegistration !== undefined && currentRegistration !== registration) return;
|
|
1715
|
+
if (!processGroupIsAlive(processGroupId)) return;
|
|
1716
|
+
await delay(PROCESS_GROUP_TERMINATION_POLL_MS);
|
|
1678
1717
|
}
|
|
1679
1718
|
}
|
|
1680
1719
|
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1720
|
+
// The initial taskkill in terminateProcessGroupsForThread is fired synchronously (required so
|
|
1721
|
+
// that call still works from a process `exit` handler). A nonzero exit there is ambiguous between
|
|
1722
|
+
// a real failure and the target having already exited, so only a reported success bounds the
|
|
1723
|
+
// root's lifetime up front (a terminated process cannot spawn); otherwise the scan latches the
|
|
1724
|
+
// root's exit itself, re-terminating the root while it is still found running as ours. Either
|
|
1725
|
+
// way the wait confirms via the process table — reclamation must not proceed on a guess. The
|
|
1726
|
+
// root was created inside the spawner's spawn() call, whose start and return times travel with the
|
|
1727
|
+
// registration so the cross-thread hop adds nothing to the window before it.
|
|
1728
|
+
function waitForWindowsGroupExit(processGroupId, spawn, killedAt) {
|
|
1729
|
+
return confirmWindowsProcessTreeGone(
|
|
1730
|
+
{
|
|
1731
|
+
rootPid: processGroupId,
|
|
1732
|
+
rootKnownAt: spawn?.spawnedAt ?? killedAt ?? Date.now(),
|
|
1733
|
+
rootStartedWithinMs:
|
|
1734
|
+
spawn?.spawnStartedAt !== undefined ? spawn.spawnedAt - spawn.spawnStartedAt : ROOT_SPAWN_ALLOWANCE_MS,
|
|
1735
|
+
rootExitedAt: killedAt,
|
|
1736
|
+
},
|
|
1737
|
+
{ pollMs: PROCESS_GROUP_TERMINATION_POLL_MS, label: `process group ${processGroupId}` }
|
|
1694
1738
|
);
|
|
1695
1739
|
}
|
|
1696
1740
|
|
|
1697
|
-
function addProcessGroup(ownerThreadId, processGroupId) {
|
|
1741
|
+
function addProcessGroup(ownerThreadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration) {
|
|
1698
1742
|
if (!Number.isInteger(processGroupId) || processGroupId <= 0) return;
|
|
1743
|
+
const previousRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1744
|
+
if (
|
|
1745
|
+
previousRegistration &&
|
|
1746
|
+
(previousRegistration.ownerThreadId !== ownerThreadId ||
|
|
1747
|
+
previousRegistration.registrationGeneration !== registrationGeneration)
|
|
1748
|
+
) {
|
|
1749
|
+
clearProcessGroupLivenessState(processGroupId);
|
|
1750
|
+
}
|
|
1699
1751
|
let processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1700
1752
|
if (!processGroups) processGroupsByThread.set(ownerThreadId, (processGroups = new Set()));
|
|
1701
1753
|
processGroups.add(processGroupId);
|
|
1754
|
+
// The map is keyed by PID, which the OS reuses the instant a process exits. Owner distinguishes
|
|
1755
|
+
// threads; generation distinguishes two children of the same thread that receive the same PID.
|
|
1756
|
+
processGroupSpawnedAt.set(processGroupId, {
|
|
1757
|
+
ownerThreadId,
|
|
1758
|
+
registrationGeneration,
|
|
1759
|
+
spawnedAt: Number.isFinite(spawnedAt) ? spawnedAt : Date.now(),
|
|
1760
|
+
spawnStartedAt: Number.isFinite(spawnStartedAt) && spawnStartedAt <= spawnedAt ? spawnStartedAt : undefined,
|
|
1761
|
+
});
|
|
1702
1762
|
}
|
|
1703
1763
|
|
|
1704
|
-
function removeProcessGroup(ownerThreadId, processGroupId) {
|
|
1764
|
+
function removeProcessGroup(ownerThreadId, processGroupId, registrationGeneration) {
|
|
1765
|
+
const currentRegistration = processGroupSpawnedAt.get(processGroupId);
|
|
1766
|
+
const sameOwnerNewerGeneration =
|
|
1767
|
+
currentRegistration?.ownerThreadId === ownerThreadId &&
|
|
1768
|
+
currentRegistration.registrationGeneration !== registrationGeneration;
|
|
1769
|
+
if (!sameOwnerNewerGeneration) {
|
|
1770
|
+
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1771
|
+
if (processGroups?.delete(processGroupId) && processGroups.size === 0) {
|
|
1772
|
+
processGroupsByThread.delete(ownerThreadId);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
if (currentRegistration?.ownerThreadId !== ownerThreadId || sameOwnerNewerGeneration) return;
|
|
1705
1776
|
clearProcessGroupLivenessState(processGroupId);
|
|
1706
|
-
|
|
1707
|
-
if (!processGroups) return;
|
|
1708
|
-
processGroups.delete(processGroupId);
|
|
1709
|
-
if (processGroups.size === 0) processGroupsByThread.delete(ownerThreadId);
|
|
1777
|
+
processGroupSpawnedAt.delete(processGroupId);
|
|
1710
1778
|
}
|
|
1711
1779
|
|
|
1712
1780
|
// Returns a promise that resolves once every process group tracked for `ownerThreadId` is
|
|
1713
1781
|
// confirmed terminated. Callers that only need to fire the termination (e.g. the `exit` handler
|
|
1714
1782
|
// below) can ignore the returned promise; isThreadRunning awaits it before declaring a dead
|
|
1715
|
-
// owner reclaimable.
|
|
1716
|
-
// this runs from a process `exit` handler too, where nothing queued after a suspension
|
|
1717
|
-
// guaranteed to run
|
|
1718
|
-
|
|
1783
|
+
// owner reclaimable. On POSIX the kill signal for every group is sent synchronously, before any
|
|
1784
|
+
// `await` — this runs from a process `exit` handler too, where nothing queued after a suspension
|
|
1785
|
+
// point is guaranteed to run, and a process-group SIGKILL is directed at the group id, not a PID
|
|
1786
|
+
// Windows could have already reissued. On Windows a synchronous `taskkill /pid` has no such
|
|
1787
|
+
// process-group semantics — a PID whose process already exited is indistinguishable from one that
|
|
1788
|
+
// never will be, so an unconditional pre-kill can hit whatever now holds a recycled PID (the exact
|
|
1789
|
+
// harper#2273 unrelated-process kill this module exists to prevent). `fromExitHandler` restricts
|
|
1790
|
+
// that blind pre-kill to the one caller that genuinely cannot await anything first (a synchronous
|
|
1791
|
+
// scan via spawnSync before killing is possible there too, just not done); every other
|
|
1792
|
+
// caller lets the identity-checked confirmation loop below issue the first kill, after its own
|
|
1793
|
+
// scan has verified who the PID currently belongs to — UNLESS the process itself is already
|
|
1794
|
+
// shutting down (`processShuttingDown`, set before a restart tears its workers down too): that
|
|
1795
|
+
// path has no guarantee the async loop gets even one scan in before `process.exit()` runs, and by
|
|
1796
|
+
// then this function has already dropped the registration the exit handler would otherwise have
|
|
1797
|
+
// caught, so the blind kill has to fire here instead.
|
|
1798
|
+
function terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler = false } = {}) {
|
|
1719
1799
|
const processGroups = processGroupsByThread.get(ownerThreadId);
|
|
1720
1800
|
if (!processGroups) return pendingProcessGroupTerminations.get(ownerThreadId) ?? Promise.resolve();
|
|
1721
1801
|
processGroupsByThread.delete(ownerThreadId);
|
|
1722
|
-
|
|
1802
|
+
// Membership is not ownership: a PID is reusable the moment its process exits.
|
|
1803
|
+
const groupIds = [...processGroups].filter((processGroupId) => {
|
|
1804
|
+
if (processGroupSpawnedAt.get(processGroupId)?.ownerThreadId === ownerThreadId) return true;
|
|
1805
|
+
harperLogger.warn(
|
|
1806
|
+
`Not terminating process group ${processGroupId} for thread ${ownerThreadId}: it is no longer registered to this thread`
|
|
1807
|
+
);
|
|
1808
|
+
return false;
|
|
1809
|
+
});
|
|
1810
|
+
const killedAt = new Map();
|
|
1723
1811
|
for (const processGroupId of groupIds) {
|
|
1724
1812
|
try {
|
|
1725
1813
|
if (process.platform === 'win32') {
|
|
1726
|
-
|
|
1814
|
+
if (!fromExitHandler && !processShuttingDown) continue;
|
|
1815
|
+
const result = spawnSync('taskkill', ['/pid', String(processGroupId), '/T', '/F'], {
|
|
1816
|
+
stdio: 'ignore',
|
|
1817
|
+
windowsHide: true,
|
|
1818
|
+
});
|
|
1819
|
+
if (result.status === 0) killedAt.set(processGroupId, Date.now());
|
|
1727
1820
|
} else {
|
|
1728
1821
|
process.kill(-processGroupId, 'SIGKILL');
|
|
1729
1822
|
}
|
|
@@ -1732,9 +1825,18 @@ function terminateProcessGroupsForThread(ownerThreadId) {
|
|
|
1732
1825
|
}
|
|
1733
1826
|
}
|
|
1734
1827
|
const termination = Promise.all(
|
|
1735
|
-
groupIds.map((processGroupId) =>
|
|
1736
|
-
|
|
1737
|
-
|
|
1828
|
+
groupIds.map((processGroupId) => {
|
|
1829
|
+
const registration = processGroupSpawnedAt.get(processGroupId);
|
|
1830
|
+
const wait =
|
|
1831
|
+
process.platform === 'win32'
|
|
1832
|
+
? waitForWindowsGroupExit(processGroupId, registration, killedAt.get(processGroupId))
|
|
1833
|
+
: waitForProcessGroupExit(processGroupId, registration);
|
|
1834
|
+
return wait.finally(() => {
|
|
1835
|
+
if (processGroupSpawnedAt.get(processGroupId) === registration) {
|
|
1836
|
+
processGroupSpawnedAt.delete(processGroupId);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
})
|
|
1738
1840
|
).finally(() => {
|
|
1739
1841
|
if (pendingProcessGroupTerminations.get(ownerThreadId) === termination) {
|
|
1740
1842
|
pendingProcessGroupTerminations.delete(ownerThreadId);
|
|
@@ -1744,14 +1846,26 @@ function terminateProcessGroupsForThread(ownerThreadId) {
|
|
|
1744
1846
|
return termination;
|
|
1745
1847
|
}
|
|
1746
1848
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1849
|
+
// `spawnedAt` / `spawnStartedAt`: the caller's clock as its spawn() of the group's root returned and
|
|
1850
|
+
// as it was called — the root was created between the two.
|
|
1851
|
+
function registerProcessGroup(processGroupId, spawnedAt = Date.now(), spawnStartedAt) {
|
|
1852
|
+
const registrationGeneration = ++processGroupRegistrationGeneration;
|
|
1853
|
+
if (isMainThread) addProcessGroup(threadId, processGroupId, spawnedAt, spawnStartedAt, registrationGeneration);
|
|
1854
|
+
else {
|
|
1855
|
+
parentPort?.postMessage({
|
|
1856
|
+
type: REGISTER_PROCESS_GROUP,
|
|
1857
|
+
processGroupId,
|
|
1858
|
+
spawnedAt,
|
|
1859
|
+
spawnStartedAt,
|
|
1860
|
+
registrationGeneration,
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
return registrationGeneration;
|
|
1750
1864
|
}
|
|
1751
1865
|
|
|
1752
|
-
function unregisterProcessGroup(processGroupId) {
|
|
1753
|
-
if (isMainThread) removeProcessGroup(threadId, processGroupId);
|
|
1754
|
-
else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId });
|
|
1866
|
+
function unregisterProcessGroup(processGroupId, registrationGeneration) {
|
|
1867
|
+
if (isMainThread) removeProcessGroup(threadId, processGroupId, registrationGeneration);
|
|
1868
|
+
else parentPort?.postMessage({ type: UNREGISTER_PROCESS_GROUP, processGroupId, registrationGeneration });
|
|
1755
1869
|
}
|
|
1756
1870
|
|
|
1757
1871
|
class ProcessGroupTerminationUnconfirmedError extends Error {
|
|
@@ -1801,7 +1915,8 @@ async function isThreadRunning(ownerThreadId, timeoutMs = THREAD_INFO_REQUEST_TI
|
|
|
1801
1915
|
|
|
1802
1916
|
if (isMainThread) {
|
|
1803
1917
|
process.on('exit', () => {
|
|
1804
|
-
for (const ownerThreadId of [...processGroupsByThread.keys()])
|
|
1918
|
+
for (const ownerThreadId of [...processGroupsByThread.keys()])
|
|
1919
|
+
terminateProcessGroupsForThread(ownerThreadId, { fromExitHandler: true });
|
|
1805
1920
|
});
|
|
1806
1921
|
}
|
|
1807
1922
|
|
|
@@ -1817,6 +1932,7 @@ function hasThreadExited(threadId) {
|
|
|
1817
1932
|
function notifyThreadExit(deadThreadId) {
|
|
1818
1933
|
if (deadThreadId == null || notifiedDeadThreadIds.has(deadThreadId)) return;
|
|
1819
1934
|
notifiedDeadThreadIds.add(deadThreadId);
|
|
1935
|
+
handleDatabaseDropPreparationOwnerExit(deadThreadId);
|
|
1820
1936
|
for (const listener of threadExitListeners) {
|
|
1821
1937
|
try {
|
|
1822
1938
|
listener(deadThreadId);
|
|
@@ -1830,6 +1946,8 @@ function removePort(port, deadThreadId) {
|
|
|
1830
1946
|
// A sibling may already have announced this dead thread and removed its port. Process-group
|
|
1831
1947
|
// cleanup must still run when the authoritative close/exit event reaches this thread.
|
|
1832
1948
|
if (deadThreadId != null) terminateProcessGroupsForThread(deadThreadId);
|
|
1949
|
+
const exitConfirmed = !port.close;
|
|
1950
|
+
settleAcknowledgementsForClosedPort(port, port.jobCleanupComplete === true, exitConfirmed);
|
|
1833
1951
|
const idx = connectedPorts.indexOf(port);
|
|
1834
1952
|
if (idx === -1) return;
|
|
1835
1953
|
connectedPorts.splice(idx, 1);
|
|
@@ -1842,7 +1960,12 @@ function removePort(port, deadThreadId) {
|
|
|
1842
1960
|
if (deadThreadId != null) {
|
|
1843
1961
|
for (let remainingPort of connectedPorts) {
|
|
1844
1962
|
try {
|
|
1845
|
-
remainingPort.postMessage({
|
|
1963
|
+
remainingPort.postMessage({
|
|
1964
|
+
type: REMOVE_PORT,
|
|
1965
|
+
threadId: deadThreadId,
|
|
1966
|
+
jobCleanupComplete: port.jobCleanupComplete === true,
|
|
1967
|
+
exitConfirmed,
|
|
1968
|
+
});
|
|
1846
1969
|
} catch {
|
|
1847
1970
|
// port may already be dead; ignore
|
|
1848
1971
|
}
|
|
@@ -1858,20 +1981,39 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
1858
1981
|
port
|
|
1859
1982
|
.on('message', (message) => {
|
|
1860
1983
|
if (message.type === REGISTER_PROCESS_GROUP) {
|
|
1861
|
-
addProcessGroup(
|
|
1984
|
+
addProcessGroup(
|
|
1985
|
+
portThreadId,
|
|
1986
|
+
message.processGroupId,
|
|
1987
|
+
message.spawnedAt,
|
|
1988
|
+
message.spawnStartedAt,
|
|
1989
|
+
message.registrationGeneration
|
|
1990
|
+
);
|
|
1862
1991
|
} else if (message.type === UNREGISTER_PROCESS_GROUP) {
|
|
1863
|
-
removeProcessGroup(portThreadId, message.processGroupId);
|
|
1992
|
+
removeProcessGroup(portThreadId, message.processGroupId, message.registrationGeneration);
|
|
1993
|
+
} else if (message.type === hdbTerms.ITC_EVENT_TYPES.JOB_CLEANUP_COMPLETE) {
|
|
1994
|
+
port.jobCleanupComplete = true;
|
|
1995
|
+
settleAcknowledgementsForClosedPort(port, true);
|
|
1864
1996
|
} else if (message.type === ADDED_PORT) {
|
|
1865
1997
|
message.port.threadId = message.threadId;
|
|
1866
1998
|
addPort(message.port, false, message.isJobWorker);
|
|
1867
1999
|
} else if (message.type === ACKNOWLEDGEMENT) {
|
|
1868
2000
|
let completion = awaitingResponses.get(message.id);
|
|
1869
2001
|
if (completion) {
|
|
1870
|
-
completion();
|
|
2002
|
+
completion(message);
|
|
1871
2003
|
}
|
|
1872
2004
|
} else if (message.type === REMOVE_PORT) {
|
|
1873
|
-
const
|
|
1874
|
-
if (
|
|
2005
|
+
const removedPort = connectedPorts.find((candidate) => candidate.threadId === message.threadId);
|
|
2006
|
+
if (removedPort && !removedPort.close && !message.exitConfirmed) return;
|
|
2007
|
+
settleAcknowledgementsForClosedThread(
|
|
2008
|
+
message.threadId,
|
|
2009
|
+
message.jobCleanupComplete === true,
|
|
2010
|
+
message.exitConfirmed === true
|
|
2011
|
+
);
|
|
2012
|
+
if (removedPort) {
|
|
2013
|
+
if (message.jobCleanupComplete) removedPort.jobCleanupComplete = true;
|
|
2014
|
+
settleAcknowledgementsForClosedPort(removedPort, removedPort.jobCleanupComplete === true);
|
|
2015
|
+
connectedPorts.splice(connectedPorts.indexOf(removedPort), 1);
|
|
2016
|
+
}
|
|
1875
2017
|
// A sibling's port-to-the-dead-worker can close (and broadcast this) before this
|
|
1876
2018
|
// thread's OWN port to that worker fires its 'close'/'exit' — at which point
|
|
1877
2019
|
// removePort() would no-op (already spliced) and threadExitListeners would never
|
|
@@ -1882,10 +2024,14 @@ function addPort(port, keepRef, isJobWorker) {
|
|
|
1882
2024
|
}
|
|
1883
2025
|
})
|
|
1884
2026
|
.on('close', () => {
|
|
1885
|
-
|
|
2027
|
+
// A worker's parentPort closing is this worker leaving, not main exiting (server/DESIGN.md).
|
|
2028
|
+
removePort(port, port === parentPort ? undefined : portThreadId);
|
|
1886
2029
|
})
|
|
1887
2030
|
.on('exit', () => {
|
|
1888
|
-
|
|
2031
|
+
// Let a cleanup proof already queued by the worker reach this port before exit becomes
|
|
2032
|
+
// authoritative. The next turn still fails closed if no proof arrives.
|
|
2033
|
+
if (port.isJobWorker && !port.jobCleanupComplete) setImmediate(() => removePort(port, portThreadId));
|
|
2034
|
+
else removePort(port, portThreadId);
|
|
1889
2035
|
});
|
|
1890
2036
|
if (keepRef) port.refCount = 100;
|
|
1891
2037
|
else port.unref();
|
|
@@ -8,6 +8,8 @@ let componentsLoadedResolve;
|
|
|
8
8
|
exports.whenComponentsLoaded = new Promise((resolve) => {
|
|
9
9
|
componentsLoadedResolve = resolve;
|
|
10
10
|
});
|
|
11
|
+
let bootLoadStarted = false;
|
|
12
|
+
exports.bootLoadsComponents = () => bootLoadStarted;
|
|
11
13
|
|
|
12
14
|
const harperLogger = require('../../utility/logging/harper_logger.ts');
|
|
13
15
|
const env = require('../../utility/environment/environmentManager.ts');
|
|
@@ -174,6 +176,7 @@ function closeServers() {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
function startServers() {
|
|
179
|
+
bootLoadStarted = true;
|
|
177
180
|
// A worker that has not yet posted child_started owns no ref'd handle: addPort()
|
|
178
181
|
// (manageThreads) unrefs parentPort, component watchers are persistent:false, and the
|
|
179
182
|
// reporting timers are unref'd. An await inside loadRootComponents whose completion
|
|
@@ -230,7 +233,16 @@ function startServers() {
|
|
|
230
233
|
})
|
|
231
234
|
.then(() => closeServers())
|
|
232
235
|
.then(() => whenScopesClosed())
|
|
233
|
-
.then(() =>
|
|
236
|
+
.then(async () => {
|
|
237
|
+
const { branchDatabasesHaveWork, closeBranchDatabases } = require('../../resources/databases.ts');
|
|
238
|
+
if (!branchDatabasesHaveWork()) return closeBranchDatabases();
|
|
239
|
+
extendShutdownDeadline(Date.now() + getShutdownDrainCeilingMs());
|
|
240
|
+
try {
|
|
241
|
+
await closeBranchDatabases();
|
|
242
|
+
} finally {
|
|
243
|
+
restoreShutdownDeadline();
|
|
244
|
+
}
|
|
245
|
+
})
|
|
234
246
|
.then(() => {
|
|
235
247
|
realExit(0);
|
|
236
248
|
});
|