@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +21 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +624 -176
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +217 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +143 -24
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
package/components/operations.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { isMainThread, parentPort } = require('node:worker_threads');
|
|
5
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
5
6
|
const fs = require('fs-extra');
|
|
6
7
|
const fg = require('fast-glob');
|
|
7
8
|
const normalize = require('normalize-path');
|
|
@@ -48,6 +49,7 @@ const {
|
|
|
48
49
|
const { ProgressEmitter } = require('../server/serverHelpers/progressEmitter.ts');
|
|
49
50
|
|
|
50
51
|
const DROP_COMPONENT_LOCK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
52
|
+
const ISOLATED_TOPOLOGY_REQUEST_TIMEOUT_MS = 5000;
|
|
51
53
|
|
|
52
54
|
function componentDropLockOptions(project) {
|
|
53
55
|
return {
|
|
@@ -588,13 +590,67 @@ async function deployComponent(req) {
|
|
|
588
590
|
// replicated ciphertext (reference, not embed); already-reference entries pass through, and with
|
|
589
591
|
// no custody a literal token stays as a transient, this-node-only fallback (#1158). Peers
|
|
590
592
|
// re-running a replicated deploy already carry references and never re-ingest.
|
|
593
|
+
const { isIsolatedApplication } = require('../server/threads/isolatedApplications.ts');
|
|
594
|
+
const requestedIsolation = req.isolated;
|
|
595
|
+
const isReplicatedExecution = typeof req._deploymentId === 'string';
|
|
596
|
+
const mode = req.deployment_id ? 'activate' : req.activate === false ? 'stage' : 'deploy';
|
|
597
|
+
const isActivation = mode === 'activate';
|
|
598
|
+
// this thread's cached config may predate an earlier deploy that changed the entry without a restart
|
|
599
|
+
env.initSync(true);
|
|
600
|
+
const initiallyIsolated = isIsolatedApplication(req.project);
|
|
601
|
+
if (requestedIsolation !== undefined && !req.package) {
|
|
602
|
+
throw handleHDBError(
|
|
603
|
+
new Error(),
|
|
604
|
+
`'isolated' is only supported for package deployments; set it on the application's root config entry instead`,
|
|
605
|
+
HTTP_STATUS_CODES.BAD_REQUEST
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
// Only the originating node rejects admission: replicated peers must commit the same desired config,
|
|
609
|
+
// then report any node-local inability to run it through component lifecycle status.
|
|
610
|
+
const assertIsolationAdmission = async (isolated) => {
|
|
611
|
+
if (!isolated || isReplicatedExecution) return;
|
|
612
|
+
const {
|
|
613
|
+
isolatedApplicationRefusal,
|
|
614
|
+
isolatedApplicationCapacityRefusal,
|
|
615
|
+
presentIsolatedApplicationNames,
|
|
616
|
+
} = require('../server/threads/isolatedApplications.ts');
|
|
617
|
+
let runningApplications;
|
|
618
|
+
try {
|
|
619
|
+
runningApplications = await manageThreads.getRunningIsolatedApplications(ISOLATED_TOPOLOGY_REQUEST_TIMEOUT_MS);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
throw handleHDBError(
|
|
622
|
+
error,
|
|
623
|
+
`Cannot deploy '${req.project}' as an isolated application: the main thread's worker topology is unavailable: ${error.message}`,
|
|
624
|
+
HTTP_STATUS_CODES.SERVICE_UNAVAILABLE
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
runningApplications = new Set([
|
|
628
|
+
...runningApplications,
|
|
629
|
+
...presentIsolatedApplicationNames().filter((application) => !isolatedApplicationRefusal(application)),
|
|
630
|
+
]);
|
|
631
|
+
const refusal =
|
|
632
|
+
isolatedApplicationRefusal(req.project) ?? isolatedApplicationCapacityRefusal(req.project, runningApplications);
|
|
633
|
+
if (refusal) {
|
|
634
|
+
throw handleHDBError(
|
|
635
|
+
new Error(),
|
|
636
|
+
`Cannot deploy '${req.project}' as an isolated application: ${refusal}`,
|
|
637
|
+
HTTP_STATUS_CODES.CONFLICT
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
// Skipped for an activation: the artifact's descriptor is the authority for the isolation it wants, and
|
|
642
|
+
// `admitIsolation` runs the real check under the preparation lock. Admitting the CURRENT value here would
|
|
643
|
+
// refuse an activation whose artifact turns isolation OFF for a component whose present isolated state is
|
|
644
|
+
// itself the refusal — a 409 for the request that would fix it.
|
|
645
|
+
if (!isActivation) await assertIsolationAdmission(requestedIsolation ?? initiallyIsolated);
|
|
591
646
|
const { ingestCredentials, resolveCredentials } = require('./secretOperations.ts');
|
|
592
|
-
|
|
647
|
+
// An activation resolves, fetches and installs nothing, so there is no credential for it to carry.
|
|
648
|
+
// The validator rejects one; this keeps the ingest itself off the path rather than relying on that.
|
|
649
|
+
if (!isActivation) req.credentials = await ingestCredentials(req, req.credentials, req.project);
|
|
593
650
|
// References are safe to persist (config + deployment row) and replicate; a no-custody literal
|
|
594
651
|
// token is not — it is used only for this node's install below, then stripped before replication.
|
|
595
652
|
const credentialReferences = (req.credentials ?? []).filter((entry) => entry && entry.secret !== undefined);
|
|
596
653
|
|
|
597
|
-
// Write to root config if the request contains a package identifier
|
|
598
654
|
if (req.package) {
|
|
599
655
|
// Check if trying to overwrite a core component (requires force)
|
|
600
656
|
// Lazy-load to avoid circular dependency with componentLoader
|
|
@@ -606,29 +662,9 @@ async function deployComponent(req) {
|
|
|
606
662
|
HTTP_STATUS_CODES.CONFLICT
|
|
607
663
|
);
|
|
608
664
|
}
|
|
609
|
-
|
|
610
|
-
const applicationConfig = { package: req.package };
|
|
611
|
-
// Avoid writing an empty `install:` block
|
|
612
|
-
if (req.install_command || req.install_timeout || req.install_allow_scripts !== undefined) {
|
|
613
|
-
applicationConfig.install = {
|
|
614
|
-
command: req.install_command,
|
|
615
|
-
timeout: req.install_timeout,
|
|
616
|
-
allowInstallScripts: req.install_allow_scripts,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
|
|
620
|
-
if (req.host !== undefined) applicationConfig.host = req.host;
|
|
621
|
-
// Same convention as host/urlPath above, and for the same reason: a package redeploy rebuilds
|
|
622
|
-
// this application's whole root-config entry from request fields, so a deployment-level key that
|
|
623
|
-
// isn't re-supplied here is silently dropped -- for branchedDatabases specifically, that means an
|
|
624
|
-
// application which believed it had a private fork resumes sharing the base after the next
|
|
625
|
-
// restart, with nothing in this operation to say so.
|
|
626
|
-
if (req.branchedDatabases !== undefined) applicationConfig.branchedDatabases = req.branchedDatabases;
|
|
627
|
-
// Persist credential references (never tokens) so every cold install of this component —
|
|
628
|
-
// reboot, new peer, rollback — re-resolves the credential from the store.
|
|
629
|
-
if (credentialReferences.length) applicationConfig.credentials = credentialReferences;
|
|
630
|
-
await configUtils.addConfig(req.project, applicationConfig);
|
|
631
665
|
}
|
|
666
|
+
let wasIsolated;
|
|
667
|
+
let nowIsolated;
|
|
632
668
|
|
|
633
669
|
// Create a hdb_deployment row up front so the deploy is observable and auditable
|
|
634
670
|
// even if the CLI disconnects. The row also holds the payload in a Blob attribute,
|
|
@@ -638,7 +674,6 @@ async function deployComponent(req) {
|
|
|
638
674
|
// recording so we don't accumulate one row per node for the same deploy. The row
|
|
639
675
|
// reaches peers via the table's standard replication; the peer-side branch below
|
|
640
676
|
// reads payload_blob back from there.
|
|
641
|
-
const isReplicatedExecution = typeof req._deploymentId === 'string';
|
|
642
677
|
// An SSE-bound caller already attached a ProgressEmitter (created in the server
|
|
643
678
|
// handler so it can also drive the response stream). Reuse it; otherwise spin up a
|
|
644
679
|
// fresh emitter so the recorder still gets phase events for non-SSE deploys.
|
|
@@ -650,6 +685,7 @@ async function deployComponent(req) {
|
|
|
650
685
|
project: req.project,
|
|
651
686
|
package_identifier: req.package ?? null,
|
|
652
687
|
user: req.hdb_user?.username,
|
|
688
|
+
activated_from: req.deployment_id ?? null,
|
|
653
689
|
restart_mode: req.restart === 'rolling' ? 'rolling' : req.restart ? 'immediate' : null,
|
|
654
690
|
// Reference form only — the rollback source for re-resolving the credential.
|
|
655
691
|
credentials: credentialReferences.length ? credentialReferences : null,
|
|
@@ -674,12 +710,18 @@ async function deployComponent(req) {
|
|
|
674
710
|
// Bounded ring buffer of install stdout/stderr so a non-SSE caller sees the tail
|
|
675
711
|
// in the thrown error. SSE callers still stream every line live.
|
|
676
712
|
const installCapture = createInstallCapture();
|
|
713
|
+
let stagedOnOrigin = false;
|
|
677
714
|
try {
|
|
678
715
|
// On the origin, tee the tarball (Buffer or Readable from the multipart parser)
|
|
679
716
|
// through a hash-and-size tap into the row's payload_blob, then re-source extraction
|
|
680
717
|
// from the persisted blob. When `system` replicates, the blob becomes the channel
|
|
681
718
|
// peers read from; when it doesn't, the blob stays local for audit and rollback.
|
|
682
|
-
if (
|
|
719
|
+
if (isActivation) {
|
|
720
|
+
// A replicated activation has exactly the shape the peer-side blob branch below keys on — neither
|
|
721
|
+
// `package` nor `payload` — so without this branch every peer would wait out `deployment_timeout`
|
|
722
|
+
// for a payload its row never had while the origin swapped successfully.
|
|
723
|
+
extractionPayload = undefined;
|
|
724
|
+
} else if (recorder && req.payload != null) {
|
|
683
725
|
await recorder.ingestPayload(req.payload);
|
|
684
726
|
extractionPayload = recorder.row.payload_blob.stream();
|
|
685
727
|
} else if (isReplicatedExecution && req.payload == null && !req.package) {
|
|
@@ -715,9 +757,13 @@ async function deployComponent(req) {
|
|
|
715
757
|
if (isReplicatedExecution) {
|
|
716
758
|
credentialsWaitMs = coerceTimeoutMs(req.deployment_timeout, DEFAULT_AWAIT_ROW_TIMEOUT_MS);
|
|
717
759
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
760
|
+
// Skipped for an activation, which runs no pack, clone or install: waiting for an hdb_secret row to
|
|
761
|
+
// replicate in would only delay a swap that has nothing to authenticate to.
|
|
762
|
+
const resolvedCredentials = isActivation
|
|
763
|
+
? undefined
|
|
764
|
+
: await resolveCredentials(req.credentials, req.project, {
|
|
765
|
+
waitMs: credentialsWaitMs,
|
|
766
|
+
});
|
|
721
767
|
|
|
722
768
|
const application = new Application({
|
|
723
769
|
name: req.project,
|
|
@@ -755,12 +801,90 @@ async function deployComponent(req) {
|
|
|
755
801
|
// committed" — so a later failure arrives after both phases reported success. The operation's error
|
|
756
802
|
// is the authority on whether the deploy landed, not the phase stream.
|
|
757
803
|
emit('phase', { phase: 'prepare', status: 'start' });
|
|
804
|
+
// The root-config entry a package deploy publishes. A stage records it with the artifact instead of
|
|
805
|
+
// publishing it, so a staged release nobody activated cannot leave config naming it — and the
|
|
806
|
+
// activation that eventually publishes it does so in the same order a normal deploy does.
|
|
807
|
+
let stagedRootConfig = null;
|
|
758
808
|
await prepareApplication(application, {
|
|
809
|
+
// `.deploy-staging/<artifactId>`. The public deployment id, so the id the caller was handed is
|
|
810
|
+
// the id a later `deployment_id` request can name; an activation names the artifact's own id,
|
|
811
|
+
// not the new row's.
|
|
812
|
+
artifactId: req.deployment_id ?? req._deploymentId,
|
|
813
|
+
mode,
|
|
814
|
+
describeArtifact: () => ({ rootConfig: stagedRootConfig, isolated: Boolean(nowIsolated) }),
|
|
815
|
+
// Returns its own undo. Publication happens BEFORE the swap, in the same order a normal deploy
|
|
816
|
+
// uses, so an activation that fails pre-commit would otherwise leave config naming a release that
|
|
817
|
+
// is not live — and `installApplications()` would resolve and install that package from scratch at
|
|
818
|
+
// the next boot, over a component whose certified artifact is sitting right there. Restoring is
|
|
819
|
+
// best-effort and not durable (that is #2315 step 3's work); it is still the difference between a
|
|
820
|
+
// failed activation that changed nothing and one that re-points the component.
|
|
821
|
+
publishRootConfig: async (entry) => {
|
|
822
|
+
const previous = configUtils.getConfigObj()?.[req.project];
|
|
823
|
+
await configUtils.addConfig(req.project, entry);
|
|
824
|
+
env.initSync(true);
|
|
825
|
+
// Read BACK, not `entry` — see `publishedEntryStillStands`.
|
|
826
|
+
const published = configUtils.getConfigObj()?.[req.project];
|
|
827
|
+
return async () => {
|
|
828
|
+
if (
|
|
829
|
+
!publishedEntryStillStands(
|
|
830
|
+
published,
|
|
831
|
+
() => env.initSync(true),
|
|
832
|
+
() => configUtils.getConfigObj()?.[req.project]
|
|
833
|
+
)
|
|
834
|
+
) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (previous === undefined) configUtils.deleteConfigFromFile([req.project]);
|
|
838
|
+
else await configUtils.addConfig(req.project, previous);
|
|
839
|
+
env.initSync(true);
|
|
840
|
+
};
|
|
841
|
+
},
|
|
842
|
+
admitIsolation: async (descriptor) => {
|
|
843
|
+
env.initSync(true);
|
|
844
|
+
wasIsolated = isIsolatedApplication(req.project);
|
|
845
|
+
// A package artifact restores its own root-config entry, so its recorded intent is what will
|
|
846
|
+
// be in force. A payload artifact publishes no config, so the effective configuration is the
|
|
847
|
+
// only authority there — a saved `false` must not admit past an isolation the operator set
|
|
848
|
+
// between the stage and this call.
|
|
849
|
+
nowIsolated = descriptor.rootConfig ? descriptor.isolated : wasIsolated;
|
|
850
|
+
await assertIsolationAdmission(nowIsolated);
|
|
851
|
+
},
|
|
852
|
+
beforePrepare: async () => {
|
|
853
|
+
if (isActivation) return;
|
|
854
|
+
env.initSync(true);
|
|
855
|
+
wasIsolated = isIsolatedApplication(req.project);
|
|
856
|
+
nowIsolated = requestedIsolation ?? wasIsolated;
|
|
857
|
+
await assertIsolationAdmission(nowIsolated);
|
|
858
|
+
if (!isReplicatedExecution && req.package) req.isolated = nowIsolated;
|
|
859
|
+
if (!req.package) return;
|
|
860
|
+
const applicationConfig = { package: req.package };
|
|
861
|
+
if (req.install_command || req.install_timeout || req.install_allow_scripts !== undefined) {
|
|
862
|
+
applicationConfig.install = {
|
|
863
|
+
command: req.install_command,
|
|
864
|
+
timeout: req.install_timeout,
|
|
865
|
+
allowInstallScripts: req.install_allow_scripts,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
|
|
869
|
+
if (req.host !== undefined) applicationConfig.host = req.host;
|
|
870
|
+
if (req.branchedDatabases !== undefined) applicationConfig.branchedDatabases = req.branchedDatabases;
|
|
871
|
+
if (nowIsolated) applicationConfig.isolated = true;
|
|
872
|
+
if (credentialReferences.length) applicationConfig.credentials = credentialReferences;
|
|
873
|
+
if (mode === 'stage') {
|
|
874
|
+
stagedRootConfig = applicationConfig;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
await configUtils.addConfig(req.project, applicationConfig);
|
|
878
|
+
env.initSync(true);
|
|
879
|
+
},
|
|
759
880
|
validateCandidate: async (candidateDirPath) => {
|
|
760
881
|
emit('phase', { phase: 'prepare', status: 'done' });
|
|
761
882
|
await validateComponentLoads(candidateDirPath, emit);
|
|
762
883
|
},
|
|
763
884
|
});
|
|
885
|
+
// The build is certified on disk from here on, so every later failure — a peer result, or a rejection
|
|
886
|
+
// thrown by the replication layer itself — still leaves an artifact this id can activate.
|
|
887
|
+
if (mode === 'stage') stagedOnOrigin = true;
|
|
764
888
|
const rollingRestart = req.restart === 'rolling';
|
|
765
889
|
// if doing a rolling restart set restart to false so that other nodes don't also restart.
|
|
766
890
|
req.restart = rollingRestart ? false : req.restart;
|
|
@@ -803,7 +927,18 @@ async function deployComponent(req) {
|
|
|
803
927
|
// when the per-peer callback already fired for these.
|
|
804
928
|
recorder.recordPeers(response.replicated);
|
|
805
929
|
}
|
|
806
|
-
|
|
930
|
+
// A still-isolated application restarts only its own dedicated worker. Everything else restarts the
|
|
931
|
+
// pool: a shared application, and either direction of an isolation flip, where the reconcile in
|
|
932
|
+
// restartWorkers starts or stops the moving application's own worker.
|
|
933
|
+
const restartScope = wasIsolated && nowIsolated ? application.name : undefined;
|
|
934
|
+
if (mode === 'stage') {
|
|
935
|
+
// No restart and no restart-required flag: nothing about the running component changed. The
|
|
936
|
+
// marker is what tells the origin which peers understood the request — see the confirmation
|
|
937
|
+
// check below — and it is set on every node, origin included, since a peer returns this same
|
|
938
|
+
// object as its operation response.
|
|
939
|
+
response.staged = true;
|
|
940
|
+
response.message = `Staged: ${application.name}`;
|
|
941
|
+
} else if (req.restart === true) {
|
|
807
942
|
emit('phase', { phase: 'restart', status: 'start' });
|
|
808
943
|
// Workers not yet replaced keep serving the pre-deploy component set, and where the OS lets
|
|
809
944
|
// replacements share a port they keep accepting connections for the whole rolling restart, so
|
|
@@ -811,7 +946,7 @@ async function deployComponent(req) {
|
|
|
811
946
|
// never heard of it.
|
|
812
947
|
const { awaitRestart } = require('./awaitRestart.ts');
|
|
813
948
|
const restart = await awaitRestart((onProgress) =>
|
|
814
|
-
manageThreads.restartWorkers('http', undefined, undefined, onProgress)
|
|
949
|
+
manageThreads.restartWorkers('http', undefined, undefined, onProgress, restartScope)
|
|
815
950
|
);
|
|
816
951
|
emit('phase', { phase: 'restart', status: 'done' });
|
|
817
952
|
logRestartOutcome(restart, `deploying ${application.name}`);
|
|
@@ -822,6 +957,8 @@ async function deployComponent(req) {
|
|
|
822
957
|
const jobResponse = await serverUtilities.executeJob({
|
|
823
958
|
operation: 'restart_service',
|
|
824
959
|
service: 'http',
|
|
960
|
+
scope: manageThreads.encodeRestartScope(restartScope),
|
|
961
|
+
scopeFallback: restartScope === undefined ? undefined : manageThreads.encodeRestartScope(undefined),
|
|
825
962
|
replicated: true,
|
|
826
963
|
});
|
|
827
964
|
emit('phase', { phase: 'restart', status: 'done' });
|
|
@@ -841,10 +978,13 @@ async function deployComponent(req) {
|
|
|
841
978
|
// An existing component's watched files are handled by Scope/EntryHandler. Package
|
|
842
979
|
// metadata is deliberately outside most plugin globs, so compare it across the atomic
|
|
843
980
|
// swap as well: a dependency or module-entry change also invalidates loaded code.
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
981
|
+
const { requestRestartAfterDeploy } = require('./requestRestart.ts');
|
|
982
|
+
requestRestartAfterDeploy(
|
|
983
|
+
application.isNewComponent,
|
|
984
|
+
application.packageMetadataChanged,
|
|
985
|
+
wasIsolated,
|
|
986
|
+
nowIsolated
|
|
987
|
+
);
|
|
848
988
|
response.message = `Successfully deployed: ${application.name}`;
|
|
849
989
|
}
|
|
850
990
|
|
|
@@ -860,12 +1000,37 @@ async function deployComponent(req) {
|
|
|
860
1000
|
const detail = failedPeers
|
|
861
1001
|
.map((peer) => `${peer.node ?? 'unknown'} (${peer.error?.message ?? 'unknown error'})`)
|
|
862
1002
|
.join(', ');
|
|
863
|
-
|
|
864
|
-
`Component '${application.name}' was deployed on the origin node
|
|
865
|
-
|
|
866
|
-
`See deployment ${recorder.deploymentId} (get_deployment) for details, or
|
|
867
|
-
`ignore_replication_errors: true to treat replication failures as non-fatal.`
|
|
1003
|
+
const replicationError = new ServerError(
|
|
1004
|
+
`Component '${application.name}' was ${mode === 'stage' ? 'staged' : 'deployed'} on the origin node ` +
|
|
1005
|
+
`but failed to replicate to ${failedPeers.length} of ${recorder.row.peer_results.length} peer ` +
|
|
1006
|
+
`node(s): ${detail}. See deployment ${recorder.deploymentId} (get_deployment) for details, or ` +
|
|
1007
|
+
`pass ignore_replication_errors: true to treat replication failures as non-fatal.`
|
|
868
1008
|
);
|
|
1009
|
+
throw replicationError;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// A peer running a build that predates staged deploys accepts `activate: false` as an unknown
|
|
1013
|
+
// field and performs its ordinary deploy, so it is now serving a release the operator asked only
|
|
1014
|
+
// to stage. Nothing can prevent that — the operations channel carries no peer version or
|
|
1015
|
+
// capability — so the origin reports it, from the one signal that separates the two cases: a node
|
|
1016
|
+
// that staged says so in its response. Read from the raw aggregate, because the recorder's peer
|
|
1017
|
+
// normalization keeps only node/status/error.
|
|
1018
|
+
//
|
|
1019
|
+
// The wording stays at "did not confirm". An unreachable peer and one that dropped the request
|
|
1020
|
+
// look identical from here, and telling an operator those nodes went live would be a guess.
|
|
1021
|
+
if (mode === 'stage') {
|
|
1022
|
+
const unconfirmed = unconfirmedStagingPeers(response?.replicated);
|
|
1023
|
+
if (unconfirmed.length > 0) {
|
|
1024
|
+
const detail = unconfirmed.map((peer) => peer.node ?? 'unknown').join(', ');
|
|
1025
|
+
const unconfirmedError = new ServerError(
|
|
1026
|
+
`Component '${application.name}' was staged on the origin node, but ${unconfirmed.length} peer ` +
|
|
1027
|
+
`node(s) did not confirm staging: ${detail}. Either they are unreachable, or they run a build ` +
|
|
1028
|
+
`that predates staged deploys — which treats this request as an ordinary deploy, so the ` +
|
|
1029
|
+
`release is already serving there. Check those nodes before activating deployment ` +
|
|
1030
|
+
`${recorder.deploymentId}, or pass ignore_replication_errors: true to accept the difference.`
|
|
1031
|
+
);
|
|
1032
|
+
throw unconfirmedError;
|
|
1033
|
+
}
|
|
869
1034
|
}
|
|
870
1035
|
}
|
|
871
1036
|
|
|
@@ -879,14 +1044,28 @@ async function deployComponent(req) {
|
|
|
879
1044
|
// the tarball when it's still the artifact you'd debug or retry with: failed deploys
|
|
880
1045
|
// don't reach this branch, and a deploy that reached here only because
|
|
881
1046
|
// ignore_replication_errors masked failed peers keeps its payload for those peers.
|
|
1047
|
+
// Never for a stage: the artifact on disk is not yet a release anyone can re-derive, and retention
|
|
1048
|
+
// can evict it (it shares `deployment_stagingRetention_maxCount` with incidental dormant builds),
|
|
1049
|
+
// so dropping the tarball here is how a staged release becomes unrecoverable — no tree, no bytes,
|
|
1050
|
+
// and a row still reporting `staged`. An operator can still reclaim it deliberately with
|
|
1051
|
+
// delete_deployment_payload once they accept that.
|
|
882
1052
|
const payloadSize = recorder.row.payload_size;
|
|
883
1053
|
const retentionMaxSize = getPayloadRetentionMaxSize();
|
|
884
|
-
if (
|
|
1054
|
+
if (
|
|
1055
|
+
mode !== 'stage' &&
|
|
1056
|
+
typeof payloadSize === 'number' &&
|
|
1057
|
+
payloadSize > retentionMaxSize &&
|
|
1058
|
+
recorder.getFailedPeers().length === 0
|
|
1059
|
+
) {
|
|
885
1060
|
const freed = recorder.dropPayload();
|
|
886
1061
|
if (freed > 0) emit('payload_dropped', { payload_size: freed, max_size: retentionMaxSize });
|
|
887
1062
|
}
|
|
888
|
-
emit('phase', { phase: 'success', status: 'done' });
|
|
889
|
-
await recorder.finish('success');
|
|
1063
|
+
emit('phase', { phase: mode === 'stage' ? 'staged' : 'success', status: 'done' });
|
|
1064
|
+
await recorder.finish(mode === 'stage' ? 'staged' : 'success');
|
|
1065
|
+
if (mode === 'stage') {
|
|
1066
|
+
response.message =
|
|
1067
|
+
`Staged: ${application.name}. Deploy it with deploy_component ` + `deployment_id=${recorder.deploymentId}`;
|
|
1068
|
+
}
|
|
890
1069
|
}
|
|
891
1070
|
return response;
|
|
892
1071
|
} catch (err) {
|
|
@@ -927,7 +1106,7 @@ async function deployComponent(req) {
|
|
|
927
1106
|
// install output, and failed_peers the caller needs.
|
|
928
1107
|
if (recorder) {
|
|
929
1108
|
try {
|
|
930
|
-
await recorder.finish('failed', err);
|
|
1109
|
+
await recorder.finish(stagedOnOrigin ? 'staged' : 'failed', err);
|
|
931
1110
|
} catch (finishErr) {
|
|
932
1111
|
log.warn('Failed to record deployment failure row', finishErr);
|
|
933
1112
|
}
|
|
@@ -936,6 +1115,43 @@ async function deployComponent(req) {
|
|
|
936
1115
|
}
|
|
937
1116
|
}
|
|
938
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Whether the root-config entry an activation published is still the one on disk — the condition for that
|
|
1120
|
+
* activation's failure to take it back. A `set_configuration` acknowledged while the swap was retrying is
|
|
1121
|
+
* not this operation's to overwrite, and restoring a snapshot taken before it would silently drop it.
|
|
1122
|
+
*
|
|
1123
|
+
* Split out from the caller so it can be tested without a deploy: this is the only guard on a hazard the
|
|
1124
|
+
* preparation lock does not cover, and both ways it can regress fail silently. `refreshConfig` is called
|
|
1125
|
+
* BEFORE the read and is load-bearing — `set_configuration` writes the config file without refreshing this
|
|
1126
|
+
* process's config object, so a cached read compares against a value that predates the very change the
|
|
1127
|
+
* guard exists to protect and always concludes nothing moved. It narrows the window rather than closing
|
|
1128
|
+
* it; serializing config publication is #2315 step 3.
|
|
1129
|
+
*
|
|
1130
|
+
* `published` must be the entry as read back after publication, not as passed in: only two values that went
|
|
1131
|
+
* through the same write-and-parse round trip are comparable, and comparing against the argument would read
|
|
1132
|
+
* any serialization difference as a concurrent change and skip every undo.
|
|
1133
|
+
*/
|
|
1134
|
+
function publishedEntryStillStands(published, refreshConfig, readCurrentEntry) {
|
|
1135
|
+
refreshConfig();
|
|
1136
|
+
return isDeepStrictEqual(readCurrentEntry(), published);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Peers that did not answer a stage with `staged: true` — either unreachable, or running a build that
|
|
1141
|
+
* predates staged deploys and therefore treated the request as an ordinary deploy.
|
|
1142
|
+
*
|
|
1143
|
+
* Split out from the caller so it can be tested without a cluster: this is the only safety net for the
|
|
1144
|
+
* mixed-version hazard #2315 records as accepted, and a regression in it fails silently. The entry shape
|
|
1145
|
+
* belongs to harper-pro's replicator rather than this repo — `normalizePeerResult` already tolerates more
|
|
1146
|
+
* than one — so the marker is read flat or from a wrapped body; assuming flat would report every peer in a
|
|
1147
|
+
* fully-upgraded cluster as unconfirmed.
|
|
1148
|
+
*/
|
|
1149
|
+
function unconfirmedStagingPeers(replicated) {
|
|
1150
|
+
if (!Array.isArray(replicated)) return [];
|
|
1151
|
+
const confirmed = (peer) => peer?.staged === true || peer?.value?.staged === true || peer?.body?.staged === true;
|
|
1152
|
+
return replicated.filter((peer) => peer && !confirmed(peer));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
939
1155
|
// Ring buffer of install stdout/stderr lines, capped by both line count and bytes so
|
|
940
1156
|
// a chatty install can't unbounded-grow the error response. snapshot() reports whether
|
|
941
1157
|
// the head was dropped so callers can flag truncation.
|
|
@@ -1311,13 +1527,29 @@ async function dropComponent(req) {
|
|
|
1311
1527
|
const componentsRoot = configUtils.getConfigPath(hdbTerms.CONFIG_PARAMS.COMPONENTSROOT);
|
|
1312
1528
|
const componentPath = path.join(componentsRoot, project);
|
|
1313
1529
|
const pathToComponent = path.join(componentsRoot, projectPath);
|
|
1530
|
+
let restartScope;
|
|
1314
1531
|
|
|
1315
1532
|
let response;
|
|
1316
1533
|
await withComponentPreparationLock(
|
|
1317
1534
|
componentPath,
|
|
1318
1535
|
async () => {
|
|
1536
|
+
if (req.restart === true) {
|
|
1537
|
+
let runningApplications;
|
|
1538
|
+
try {
|
|
1539
|
+
runningApplications = await manageThreads.getRunningIsolatedApplications(
|
|
1540
|
+
ISOLATED_TOPOLOGY_REQUEST_TIMEOUT_MS
|
|
1541
|
+
);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
throw handleHDBError(
|
|
1544
|
+
error,
|
|
1545
|
+
`Cannot drop '${project}' with a restart: the main thread's worker topology is unavailable: ${error.message}`,
|
|
1546
|
+
HTTP_STATUS_CODES.SERVICE_UNAVAILABLE
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (runningApplications.includes(project)) restartScope = project;
|
|
1550
|
+
}
|
|
1319
1551
|
const componentSymlink = path.join(env.get(hdbTerms.CONFIG_PARAMS.ROOTPATH), 'node_modules', project);
|
|
1320
|
-
if (await fs.pathExists(componentSymlink)) {
|
|
1552
|
+
if (!file && (await fs.pathExists(componentSymlink))) {
|
|
1321
1553
|
await fs.unlink(componentSymlink);
|
|
1322
1554
|
}
|
|
1323
1555
|
|
|
@@ -1328,7 +1560,7 @@ async function dropComponent(req) {
|
|
|
1328
1560
|
}
|
|
1329
1561
|
|
|
1330
1562
|
const packageJsonPath = path.join(env.get(hdbTerms.CONFIG_PARAMS.ROOTPATH), 'package.json');
|
|
1331
|
-
if (await fs.pathExists(packageJsonPath)) {
|
|
1563
|
+
if (!file && (await fs.pathExists(packageJsonPath))) {
|
|
1332
1564
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
1333
1565
|
if (packageJson?.dependencies?.[project]) {
|
|
1334
1566
|
delete packageJson.dependencies[project];
|
|
@@ -1336,11 +1568,8 @@ async function dropComponent(req) {
|
|
|
1336
1568
|
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
|
|
1337
1569
|
}
|
|
1338
1570
|
|
|
1339
|
-
configUtils.deleteConfigFromFile([project]);
|
|
1340
|
-
|
|
1341
|
-
// package application in that cache, and installing this one would take the lock held here.
|
|
1342
|
-
// (A worker's RESTART message refreshes main's config the same way.)
|
|
1343
|
-
if (isMainThread) env.initSync(true);
|
|
1571
|
+
if (!file) configUtils.deleteConfigFromFile([project]);
|
|
1572
|
+
if (!file && isMainThread) env.initSync(true);
|
|
1344
1573
|
response = await server.replication.replicateOperation(req);
|
|
1345
1574
|
const { applicationHasBranchStorage, removeBranchesForApplication } = require('../resources/branchDatabase.ts');
|
|
1346
1575
|
const branched = !file && applicationHasBranchStorage(project);
|
|
@@ -1356,6 +1585,7 @@ async function dropComponent(req) {
|
|
|
1356
1585
|
parentPort.postMessage({
|
|
1357
1586
|
type: hdbTerms.ITC_EVENT_TYPES.RESTART,
|
|
1358
1587
|
workerType: 'http',
|
|
1588
|
+
scope: manageThreads.encodeRestartScope(restartScope),
|
|
1359
1589
|
removeBranchesFor: file ? undefined : project,
|
|
1360
1590
|
});
|
|
1361
1591
|
if (branched) {
|
|
@@ -1366,7 +1596,7 @@ async function dropComponent(req) {
|
|
|
1366
1596
|
}
|
|
1367
1597
|
const { awaitRestart } = require('./awaitRestart.ts');
|
|
1368
1598
|
const restart = await awaitRestart((onProgress) =>
|
|
1369
|
-
manageThreads.restartWorkers('http', undefined, undefined, onProgress)
|
|
1599
|
+
manageThreads.restartWorkers('http', undefined, undefined, onProgress, restartScope)
|
|
1370
1600
|
);
|
|
1371
1601
|
logRestartOutcome(restart, `dropping ${projectPath}`);
|
|
1372
1602
|
if (!branched) return;
|
|
@@ -1399,6 +1629,8 @@ exports.addComponent = addComponent;
|
|
|
1399
1629
|
exports.dropCustomFunctionProject = dropCustomFunctionProject;
|
|
1400
1630
|
exports.packageComponent = packageComponent;
|
|
1401
1631
|
exports.deployComponent = deployComponent;
|
|
1632
|
+
exports.unconfirmedStagingPeers = unconfirmedStagingPeers;
|
|
1633
|
+
exports.publishedEntryStillStands = publishedEntryStillStands;
|
|
1402
1634
|
exports.getComponents = getComponents;
|
|
1403
1635
|
exports.getComponentFile = getComponentFile;
|
|
1404
1636
|
exports.setComponentFile = setComponentFile;
|
|
@@ -29,6 +29,12 @@ const ENV_KEY_REGEX = /^[\w.-]+$/;
|
|
|
29
29
|
const HOSTNAME_SCHEMA = Joi.string().hostname();
|
|
30
30
|
const IPV6_SCHEMA = Joi.string().ip({ version: 'ipv6' });
|
|
31
31
|
|
|
32
|
+
// A deployment id names both an hdb_deployment row and a staging directory, so it is held to the shape
|
|
33
|
+
// the recorder mints — `randomUUID()`, which is lowercase. LOWERCASE ONLY, because a case-insensitive
|
|
34
|
+
// filesystem resolves an uppercase id to the real directory while every string compare against it fails:
|
|
35
|
+
// the retention pin would not recognise the artifact the request is about to activate, and would evict it.
|
|
36
|
+
const DEPLOYMENT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
37
|
+
|
|
32
38
|
module.exports = {
|
|
33
39
|
getDropCustomFunctionValidator,
|
|
34
40
|
setCustomFunctionValidator,
|
|
@@ -492,7 +498,32 @@ function deployComponentValidator(req) {
|
|
|
492
498
|
.required()
|
|
493
499
|
.messages({ 'string.pattern.base': HDB_ERROR_MSGS.BAD_PROJECT_NAME }),
|
|
494
500
|
package: Joi.string().optional(),
|
|
495
|
-
|
|
501
|
+
// `activate: false` builds and certifies the component, then stops, leaving an artifact a later
|
|
502
|
+
// request activates by `deployment_id`. STRICT: `validateBySchema` throws away Joi's converted
|
|
503
|
+
// value, so a coercing boolean would let `activate: "false"` validate here and then read as truthy
|
|
504
|
+
// in the handler — deploying the very release the caller asked not to make live.
|
|
505
|
+
activate: Joi.boolean().strict().optional(),
|
|
506
|
+
// Activate a build staged earlier by `activate: false`. It becomes a path segment
|
|
507
|
+
// (`.deploy-staging/<id>/<component>`), so the charset is pinned as tightly as the value it names.
|
|
508
|
+
deployment_id: Joi.string().pattern(DEPLOYMENT_ID_REGEX).optional().messages({
|
|
509
|
+
'string.pattern.base': `'deployment_id' must be a UUID`,
|
|
510
|
+
}),
|
|
511
|
+
// Set by deployComponent from the origin's deployment row and carried to peers by replication.
|
|
512
|
+
// Validated because it is a path segment on EVERY deploy and unknown keys are admitted, so an
|
|
513
|
+
// operator-supplied one would otherwise reach the filesystem unchecked.
|
|
514
|
+
_deploymentId: Joi.string().pattern(DEPLOYMENT_ID_REGEX).optional().messages({
|
|
515
|
+
'string.pattern.base': `'_deploymentId' must be a UUID`,
|
|
516
|
+
}),
|
|
517
|
+
restart: Joi.alternatives()
|
|
518
|
+
.try(Joi.boolean(), Joi.string().valid('rolling'))
|
|
519
|
+
.optional()
|
|
520
|
+
// Nothing goes live, so there is nothing to restart into.
|
|
521
|
+
.when('activate', {
|
|
522
|
+
is: false,
|
|
523
|
+
then: Joi.forbidden().messages({
|
|
524
|
+
'any.unknown': `'restart' cannot be combined with 'activate: false': staging does not change the running component`,
|
|
525
|
+
}),
|
|
526
|
+
}),
|
|
496
527
|
install_command: Joi.string().optional(),
|
|
497
528
|
install_timeout: Joi.number().optional(),
|
|
498
529
|
install_allow_scripts: Joi.boolean().optional(),
|
|
@@ -534,6 +565,7 @@ function deployComponentValidator(req) {
|
|
|
534
565
|
// like urlPath/host above, not component config: reusing assertBranchedDatabases here (rather
|
|
535
566
|
// than re-deriving its rules in Joi) gives a synchronous 400 at deploy time instead of a load
|
|
536
567
|
// failure discovered much later and disconnected from the request that caused it.
|
|
568
|
+
isolated: Joi.boolean().optional(),
|
|
537
569
|
branchedDatabases: Joi.custom((value, helpers) => {
|
|
538
570
|
try {
|
|
539
571
|
assertBranchedDatabases(req.project, value);
|
|
@@ -575,7 +607,22 @@ function deployComponentValidator(req) {
|
|
|
575
607
|
}),
|
|
576
608
|
})
|
|
577
609
|
.with('urlPath', 'package')
|
|
578
|
-
.with('host', 'package')
|
|
610
|
+
.with('host', 'package')
|
|
611
|
+
// An activation has no build inputs and takes no routing or packaging opinions: the artifact's own
|
|
612
|
+
// descriptor is the authority for what it publishes, and silently ignoring these would hide that.
|
|
613
|
+
.without('deployment_id', [
|
|
614
|
+
'package',
|
|
615
|
+
'payload',
|
|
616
|
+
'activate',
|
|
617
|
+
'install_command',
|
|
618
|
+
'install_timeout',
|
|
619
|
+
'install_allow_scripts',
|
|
620
|
+
'credentials',
|
|
621
|
+
'urlPath',
|
|
622
|
+
'host',
|
|
623
|
+
'branchedDatabases',
|
|
624
|
+
'isolated',
|
|
625
|
+
]);
|
|
579
626
|
|
|
580
627
|
return validator.validateBySchema(req, deployProjSchema);
|
|
581
628
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, relative, sep } from 'node:path';
|
|
1
|
+
import { basename, join, relative, sep } from 'node:path';
|
|
2
2
|
import { stat, readdir } from 'node:fs/promises';
|
|
3
3
|
import { Readable, pipeline } from 'node:stream';
|
|
4
4
|
import tar from 'tar-fs';
|
|
@@ -13,6 +13,27 @@ const DEFAULT_OPTIONS: PackageOptions = { skip_node_modules: false, skip_symlink
|
|
|
13
13
|
|
|
14
14
|
const WEBPACK_CACHE_SEGMENT = join('cache', 'webpack');
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* `node_modules/harper` is Harper's, whatever is on disk there.
|
|
18
|
+
*
|
|
19
|
+
* Every non-root component load runs `symlinkHarperModule`, which does `rmSync(harperModule, {
|
|
20
|
+
* recursive: true, force: true })` and then links the running install (`components/componentLoader.ts`).
|
|
21
|
+
* It does that whether or not the path already existed, so component content there survives no load and
|
|
22
|
+
* packaging it cannot be preserving component source — while packaging it walks the whole install, since
|
|
23
|
+
* the packer dereferences symlinks and recurses into linked directories.
|
|
24
|
+
*
|
|
25
|
+
* `harperdb` is deliberately NOT here, and the asymmetry is the reason: the loader only *repairs*
|
|
26
|
+
* `node_modules/harperdb` when that path already exists. A legacy component shipping a real
|
|
27
|
+
* npm-installed `harperdb` that has never been loaded on this host therefore owns it — and excluding it
|
|
28
|
+
* would strip it from the tarball, after which `installApplication` skips installing (it returns early
|
|
29
|
+
* when `node_modules` exists), the loader's present-gate never fires, and `import from 'harperdb'`
|
|
30
|
+
* fails on the target — and that holds whether the path is skipped by name or by resolved identity,
|
|
31
|
+
* because an archive that simply OMITS it cannot restore it. So a `harperdb` link to the install is
|
|
32
|
+
* still followed. Closing that means packing a placeholder at the path so the loader's present-gate can
|
|
33
|
+
* fire on the target, not skipping it; tracked in harper#2577.
|
|
34
|
+
*/
|
|
35
|
+
const INSTALL_OWNED_PATH = join('node_modules', 'harper');
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Whether `fullPath` (an absolute path under `directory`) should be excluded from the package when
|
|
18
39
|
* `skip_node_modules` is set. The path is first made relative to `directory`, so packaging a component
|
|
@@ -21,6 +42,9 @@ const WEBPACK_CACHE_SEGMENT = join('cache', 'webpack');
|
|
|
21
42
|
* excluded the whole tree. Shared by the stream packer and the directory walk so they cannot diverge.
|
|
22
43
|
*/
|
|
23
44
|
function isExcluded(directory: string, fullPath: string, options: PackageOptions): boolean {
|
|
45
|
+
// Unconditional, including under `skip_symlinks`: packed literally, the link ships an absolute path to
|
|
46
|
+
// the packaging host's install root, which arrives dangling.
|
|
47
|
+
if (basename(fullPath) === 'harper' && relative(directory, fullPath) === INSTALL_OWNED_PATH) return true;
|
|
24
48
|
if (!options.skip_node_modules) return false;
|
|
25
49
|
const rel = relative(directory, fullPath);
|
|
26
50
|
return rel.split(sep).includes('node_modules') || rel.includes(WEBPACK_CACHE_SEGMENT);
|
|
@@ -24,6 +24,17 @@ export function requestRestart() {
|
|
|
24
24
|
restartArrayBuffer.notify();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function requestRestartAfterDeploy(
|
|
28
|
+
isNewComponent: boolean,
|
|
29
|
+
packageMetadataChanged: boolean,
|
|
30
|
+
wasIsolated: boolean,
|
|
31
|
+
isIsolated: boolean
|
|
32
|
+
): boolean {
|
|
33
|
+
if (!isNewComponent && !packageMetadataChanged && wasIsolated === isIsolated) return false;
|
|
34
|
+
requestRestart();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function restartNeeded() {
|
|
28
39
|
ensureInitialized();
|
|
29
40
|
return restartNeededArray[0] === 1;
|