@nocobase/app-plugin-releases 0.1.0-beta.2 → 0.1.0-beta.21
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/CHANGELOG.md +349 -0
- package/LICENSE.txt +129 -0
- package/README.md +91 -16
- package/dist/client/components/environment-form.d.ts +1 -1
- package/dist/client/components/environment-form.d.ts.map +1 -1
- package/dist/client/components/environment-form.js +9 -2
- package/dist/client/components/environment-form.js.map +1 -1
- package/dist/client/components/page-container.js +1 -1
- package/dist/client/components/page-container.js.map +1 -1
- package/dist/client/components/page-header.d.ts.map +1 -1
- package/dist/client/components/page-header.js +1 -1
- package/dist/client/components/page-header.js.map +1 -1
- package/dist/client/components/release-badges.d.ts.map +1 -1
- package/dist/client/components/release-badges.js +13 -1
- package/dist/client/components/release-badges.js.map +1 -1
- package/dist/client/components/route-overlay.d.ts +4 -1
- package/dist/client/components/route-overlay.d.ts.map +1 -1
- package/dist/client/components/route-overlay.js +32 -3
- package/dist/client/components/route-overlay.js.map +1 -1
- package/dist/client/components/runtime-policy-fields.d.ts +7 -1
- package/dist/client/components/runtime-policy-fields.d.ts.map +1 -1
- package/dist/client/components/runtime-policy-fields.js +19 -5
- package/dist/client/components/runtime-policy-fields.js.map +1 -1
- package/dist/client/components/runtime-state.d.ts +6 -0
- package/dist/client/components/runtime-state.d.ts.map +1 -0
- package/dist/client/components/runtime-state.js +67 -0
- package/dist/client/components/runtime-state.js.map +1 -0
- package/dist/client/components/shared-host.d.ts +20 -0
- package/dist/client/components/shared-host.d.ts.map +1 -0
- package/dist/client/components/shared-host.js +88 -0
- package/dist/client/components/shared-host.js.map +1 -0
- package/dist/client/components/ui/progress.d.ts +9 -0
- package/dist/client/components/ui/progress.d.ts.map +1 -0
- package/dist/client/components/ui/progress.js +20 -0
- package/dist/client/components/ui/progress.js.map +1 -0
- package/dist/client/components/use-route-overlay.d.ts +2 -9
- package/dist/client/components/use-route-overlay.d.ts.map +1 -1
- package/dist/client/components/use-route-overlay.js +5 -3
- package/dist/client/components/use-route-overlay.js.map +1 -1
- package/dist/client/components/variable-alerts.d.ts.map +1 -1
- package/dist/client/components/variable-alerts.js +6 -18
- package/dist/client/components/variable-alerts.js.map +1 -1
- package/dist/client/hooks/use-app-summary.d.ts +1 -1
- package/dist/client/hooks/use-app-summary.d.ts.map +1 -1
- package/dist/client/hooks/use-app-summary.js +4 -1
- package/dist/client/hooks/use-app-summary.js.map +1 -1
- package/dist/client/hooks/use-releases.d.ts +4 -1
- package/dist/client/hooks/use-releases.d.ts.map +1 -1
- package/dist/client/hooks/use-releases.js +6 -2
- package/dist/client/hooks/use-releases.js.map +1 -1
- package/dist/client/lib/app-state.d.ts +2 -2
- package/dist/client/lib/app-state.d.ts.map +1 -1
- package/dist/client/lib/app-state.js +10 -3
- package/dist/client/lib/app-state.js.map +1 -1
- package/dist/client/lib/errors.d.ts.map +1 -1
- package/dist/client/lib/errors.js +7 -0
- package/dist/client/lib/errors.js.map +1 -1
- package/dist/client/lib/format.d.ts +2 -0
- package/dist/client/lib/format.d.ts.map +1 -1
- package/dist/client/lib/format.js +7 -0
- package/dist/client/lib/format.js.map +1 -1
- package/dist/client/lib/paths.d.ts +5 -0
- package/dist/client/lib/paths.d.ts.map +1 -1
- package/dist/client/lib/paths.js +10 -0
- package/dist/client/lib/paths.js.map +1 -1
- package/dist/client/lib/permissions.d.ts +5 -0
- package/dist/client/lib/permissions.d.ts.map +1 -1
- package/dist/client/lib/permissions.js +8 -0
- package/dist/client/lib/permissions.js.map +1 -1
- package/dist/client/lib/runtime-state.d.ts +25 -0
- package/dist/client/lib/runtime-state.d.ts.map +1 -0
- package/dist/client/lib/runtime-state.js +58 -0
- package/dist/client/lib/runtime-state.js.map +1 -0
- package/dist/client/locales/de-DE.d.ts +5 -0
- package/dist/client/locales/de-DE.d.ts.map +1 -0
- package/dist/client/locales/de-DE.js +876 -0
- package/dist/client/locales/de-DE.js.map +1 -0
- package/dist/client/locales/en-US.d.ts +83 -9
- package/dist/client/locales/en-US.d.ts.map +1 -1
- package/dist/client/locales/en-US.js +80 -9
- package/dist/client/locales/en-US.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts +5 -0
- package/dist/client/locales/es-ES.d.ts.map +1 -0
- package/dist/client/locales/es-ES.js +882 -0
- package/dist/client/locales/es-ES.js.map +1 -0
- package/dist/client/locales/fr-FR.d.ts +5 -0
- package/dist/client/locales/fr-FR.d.ts.map +1 -0
- package/dist/client/locales/fr-FR.js +882 -0
- package/dist/client/locales/fr-FR.js.map +1 -0
- package/dist/client/locales/id-ID.d.ts +5 -0
- package/dist/client/locales/id-ID.d.ts.map +1 -0
- package/dist/client/locales/id-ID.js +876 -0
- package/dist/client/locales/id-ID.js.map +1 -0
- package/dist/client/locales/index.d.ts.map +1 -1
- package/dist/client/locales/index.js +10 -0
- package/dist/client/locales/index.js.map +1 -1
- package/dist/client/locales/ja-JP.d.ts +5 -0
- package/dist/client/locales/ja-JP.d.ts.map +1 -0
- package/dist/client/locales/ja-JP.js +876 -0
- package/dist/client/locales/ja-JP.js.map +1 -0
- package/dist/client/locales/ko-KR.d.ts +5 -0
- package/dist/client/locales/ko-KR.d.ts.map +1 -0
- package/dist/client/locales/ko-KR.js +879 -0
- package/dist/client/locales/ko-KR.js.map +1 -0
- package/dist/client/locales/pt-BR.d.ts +5 -0
- package/dist/client/locales/pt-BR.d.ts.map +1 -0
- package/dist/client/locales/pt-BR.js +882 -0
- package/dist/client/locales/pt-BR.js.map +1 -0
- package/dist/client/locales/ru-RU.d.ts +5 -0
- package/dist/client/locales/ru-RU.d.ts.map +1 -0
- package/dist/client/locales/ru-RU.js +891 -0
- package/dist/client/locales/ru-RU.js.map +1 -0
- package/dist/client/locales/vi-VN.d.ts +5 -0
- package/dist/client/locales/vi-VN.d.ts.map +1 -0
- package/dist/client/locales/vi-VN.js +876 -0
- package/dist/client/locales/vi-VN.js.map +1 -0
- package/dist/client/locales/zh-CN.d.ts.map +1 -1
- package/dist/client/locales/zh-CN.js +83 -9
- package/dist/client/locales/zh-CN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts +5 -0
- package/dist/client/locales/zh-TW.d.ts.map +1 -0
- package/dist/client/locales/zh-TW.js +876 -0
- package/dist/client/locales/zh-TW.js.map +1 -0
- package/dist/client/pages/app-page.d.ts.map +1 -1
- package/dist/client/pages/app-page.js +14 -8
- package/dist/client/pages/app-page.js.map +1 -1
- package/dist/client/pages/apps-outlet.d.ts +8 -0
- package/dist/client/pages/apps-outlet.d.ts.map +1 -0
- package/dist/client/pages/apps-outlet.js +9 -0
- package/dist/client/pages/apps-outlet.js.map +1 -0
- package/dist/client/pages/environment-page.d.ts.map +1 -1
- package/dist/client/pages/environment-page.js +112 -24
- package/dist/client/pages/environment-page.js.map +1 -1
- package/dist/client/pages/environments-page.d.ts +8 -3
- package/dist/client/pages/environments-page.d.ts.map +1 -1
- package/dist/client/pages/environments-page.js +58 -10
- package/dist/client/pages/environments-page.js.map +1 -1
- package/dist/client/pages/new-app-page.d.ts +2 -4
- package/dist/client/pages/new-app-page.d.ts.map +1 -1
- package/dist/client/pages/new-app-page.js +29 -19
- package/dist/client/pages/new-app-page.js.map +1 -1
- package/dist/client/pages/new-app-redirect.d.ts +7 -0
- package/dist/client/pages/new-app-redirect.d.ts.map +1 -0
- package/dist/client/pages/new-app-redirect.js +8 -0
- package/dist/client/pages/new-app-redirect.js.map +1 -0
- package/dist/client/pages/request-page.js +1 -1
- package/dist/client/pages.d.ts +6 -2
- package/dist/client/pages.d.ts.map +1 -1
- package/dist/client/pages.js +8 -4
- package/dist/client/pages.js.map +1 -1
- package/dist/client/routes.d.ts.map +1 -1
- package/dist/client/routes.js +37 -10
- package/dist/client/routes.js.map +1 -1
- package/dist/database/migrations/.manifest.json +10 -2
- package/dist/database/migrations/202610020001_rel_create_tables.js +1 -1
- package/dist/database/migrations/202610020001_rel_create_tables.js.map +1 -1
- package/dist/database/migrations/202610260002_rel_bound_artifact_keys.d.ts +4 -0
- package/dist/database/migrations/202610260002_rel_bound_artifact_keys.d.ts.map +1 -0
- package/dist/database/migrations/202610260002_rel_bound_artifact_keys.js +51 -0
- package/dist/database/migrations/202610260002_rel_bound_artifact_keys.js.map +1 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts +8 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.d.ts.map +1 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.js +30 -0
- package/dist/database/migrations/202611020001_rel_environment_purpose.js.map +1 -0
- package/dist/server/access/caller.d.ts +6 -0
- package/dist/server/access/caller.d.ts.map +1 -1
- package/dist/server/access/caller.js +36 -0
- package/dist/server/access/caller.js.map +1 -1
- package/dist/server/artifact-config.d.ts +9 -0
- package/dist/server/artifact-config.d.ts.map +1 -0
- package/dist/server/artifact-config.js +49 -0
- package/dist/server/artifact-config.js.map +1 -0
- package/dist/server/composition.d.ts +6 -0
- package/dist/server/composition.d.ts.map +1 -1
- package/dist/server/composition.js +7 -1
- package/dist/server/composition.js.map +1 -1
- package/dist/server/config.d.ts +18 -2
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/drivers/host/driver.d.ts +13 -2
- package/dist/server/drivers/host/driver.d.ts.map +1 -1
- package/dist/server/drivers/host/driver.js +427 -81
- package/dist/server/drivers/host/driver.js.map +1 -1
- package/dist/server/drivers/types.d.ts +79 -1
- package/dist/server/drivers/types.d.ts.map +1 -1
- package/dist/server/drivers/types.js +6 -0
- package/dist/server/drivers/types.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/locales/de-DE.d.ts +5 -0
- package/dist/server/locales/de-DE.d.ts.map +1 -0
- package/dist/server/locales/de-DE.js +133 -0
- package/dist/server/locales/de-DE.js.map +1 -0
- package/dist/server/locales/es-ES.d.ts +5 -0
- package/dist/server/locales/es-ES.d.ts.map +1 -0
- package/dist/server/locales/es-ES.js +133 -0
- package/dist/server/locales/es-ES.js.map +1 -0
- package/dist/server/locales/fr-FR.d.ts +5 -0
- package/dist/server/locales/fr-FR.d.ts.map +1 -0
- package/dist/server/locales/fr-FR.js +133 -0
- package/dist/server/locales/fr-FR.js.map +1 -0
- package/dist/server/locales/id-ID.d.ts +5 -0
- package/dist/server/locales/id-ID.d.ts.map +1 -0
- package/dist/server/locales/id-ID.js +133 -0
- package/dist/server/locales/id-ID.js.map +1 -0
- package/dist/server/locales/index.d.ts.map +1 -1
- package/dist/server/locales/index.js +10 -0
- package/dist/server/locales/index.js.map +1 -1
- package/dist/server/locales/ja-JP.d.ts +5 -0
- package/dist/server/locales/ja-JP.d.ts.map +1 -0
- package/dist/server/locales/ja-JP.js +133 -0
- package/dist/server/locales/ja-JP.js.map +1 -0
- package/dist/server/locales/ko-KR.d.ts +5 -0
- package/dist/server/locales/ko-KR.d.ts.map +1 -0
- package/dist/server/locales/ko-KR.js +133 -0
- package/dist/server/locales/ko-KR.js.map +1 -0
- package/dist/server/locales/pt-BR.d.ts +5 -0
- package/dist/server/locales/pt-BR.d.ts.map +1 -0
- package/dist/server/locales/pt-BR.js +133 -0
- package/dist/server/locales/pt-BR.js.map +1 -0
- package/dist/server/locales/ru-RU.d.ts +5 -0
- package/dist/server/locales/ru-RU.d.ts.map +1 -0
- package/dist/server/locales/ru-RU.js +133 -0
- package/dist/server/locales/ru-RU.js.map +1 -0
- package/dist/server/locales/vi-VN.d.ts +5 -0
- package/dist/server/locales/vi-VN.d.ts.map +1 -0
- package/dist/server/locales/vi-VN.js +133 -0
- package/dist/server/locales/vi-VN.js.map +1 -0
- package/dist/server/locales/zh-TW.d.ts +5 -0
- package/dist/server/locales/zh-TW.d.ts.map +1 -0
- package/dist/server/locales/zh-TW.js +133 -0
- package/dist/server/locales/zh-TW.js.map +1 -0
- package/dist/server/providers/releases.d.ts.map +1 -1
- package/dist/server/providers/releases.js +12 -5
- package/dist/server/providers/releases.js.map +1 -1
- package/dist/server/routes/api.d.ts +1 -1
- package/dist/server/routes/api.d.ts.map +1 -1
- package/dist/server/routes/api.js +120 -20
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +10 -2
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +7 -2
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +320 -89
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/services/environments.d.ts +2 -2
- package/dist/server/services/environments.d.ts.map +1 -1
- package/dist/server/services/environments.js +21 -1
- package/dist/server/services/environments.js.map +1 -1
- package/dist/server/services/releases.d.ts +69 -8
- package/dist/server/services/releases.d.ts.map +1 -1
- package/dist/server/services/releases.js +574 -128
- package/dist/server/services/releases.js.map +1 -1
- package/dist/server/services/validation.d.ts +2 -0
- package/dist/server/services/validation.d.ts.map +1 -1
- package/dist/server/services/validation.js +8 -0
- package/dist/server/services/validation.js.map +1 -1
- package/dist/server/tokens.d.ts +15 -2
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js.map +1 -1
- package/dist/shared/releases.d.ts +94 -3
- package/dist/shared/releases.d.ts.map +1 -1
- package/dist/shared/releases.js +37 -0
- package/dist/shared/releases.js.map +1 -1
- package/package.json +18 -18
- package/dist/client/pages/apps-page.d.ts +0 -4
- package/dist/client/pages/apps-page.d.ts.map +0 -1
- package/dist/client/pages/apps-page.js +0 -116
- package/dist/client/pages/apps-page.js.map +0 -1
|
@@ -19,7 +19,8 @@ import { createDiagnosticLogger, } from '@nocobase/logging';
|
|
|
19
19
|
import { stringify as stringifyYaml } from 'yaml';
|
|
20
20
|
import { ACCESS_NAMESPACE } from '../../../shared/access.js';
|
|
21
21
|
import { writeTextAtomic } from '../../services/config-file.js';
|
|
22
|
-
|
|
22
|
+
export { UNRESTORED_APP_ERROR } from '../types.js';
|
|
23
|
+
import { expandPublicUrl, UNRESTORED_APP_ERROR, } from '../types.js';
|
|
23
24
|
/** The backend of Apps that run in the Host process. */
|
|
24
25
|
export const IN_PROCESS_BACKEND = 'in-process';
|
|
25
26
|
export const DOCKER_BACKEND = 'docker';
|
|
@@ -61,6 +62,14 @@ export function createHostDriver(options) {
|
|
|
61
62
|
const inProcess = options.hosts[IN_PROCESS_BACKEND];
|
|
62
63
|
if (!inProcess)
|
|
63
64
|
throw new Error('The Host driver needs the in-process Host.');
|
|
65
|
+
for (const [name, value] of Object.entries({
|
|
66
|
+
restartMaxWaitMs: inProcess.restartMaxWaitMs,
|
|
67
|
+
restartDrainMs: inProcess.restartDrainMs,
|
|
68
|
+
})) {
|
|
69
|
+
if (value !== undefined &&
|
|
70
|
+
(!Number.isInteger(value) || value < 0 || value > 2_147_483_647))
|
|
71
|
+
throw new Error(`Invalid ${name}: expected a non-negative timer interval`);
|
|
72
|
+
}
|
|
64
73
|
const scopes = new Map();
|
|
65
74
|
/** The latest revision sent per scope; the Host refuses an older one. */
|
|
66
75
|
const revisions = new Map();
|
|
@@ -72,7 +81,23 @@ export function createHostDriver(options) {
|
|
|
72
81
|
let runtimeChurn = 0;
|
|
73
82
|
let inFlight = 0;
|
|
74
83
|
let restartScheduled = null;
|
|
84
|
+
let rotationPreparing = null;
|
|
85
|
+
let readyRestoring = null;
|
|
75
86
|
let churnTimer = null;
|
|
87
|
+
let restartDeadline = null;
|
|
88
|
+
let checkingChurn = false;
|
|
89
|
+
let recheckChurn = false;
|
|
90
|
+
let retryCheckAt = 0;
|
|
91
|
+
const capacityWaiting = new Set();
|
|
92
|
+
const batchBusy = () => [...scopes.values()].some((scope) => scope.host === inProcess &&
|
|
93
|
+
scope.deploymentBatchBusy?.(capacityWaiting));
|
|
94
|
+
let hostGeneration = 0;
|
|
95
|
+
let rotationSince = null;
|
|
96
|
+
let capacityRotation = false;
|
|
97
|
+
const rotationWaiters = new Set();
|
|
98
|
+
const heapRatio = inProcess.restartHeapRatio ?? (inProcess.restartAfterChurn ? 0.55 : 0);
|
|
99
|
+
if (!Number.isFinite(heapRatio) || heapRatio < 0 || heapRatio >= 1)
|
|
100
|
+
throw new Error('Invalid Host restart heap ratio');
|
|
76
101
|
const hostOf = (backend) => {
|
|
77
102
|
const host = options.hosts[backend];
|
|
78
103
|
if (!host)
|
|
@@ -164,18 +189,11 @@ export function createHostDriver(options) {
|
|
|
164
189
|
: {}),
|
|
165
190
|
};
|
|
166
191
|
};
|
|
167
|
-
const track = (scope, spec) => {
|
|
168
|
-
if (spec.activation === 'onDemand')
|
|
169
|
-
scope.onDemand.add(spec.appId);
|
|
170
|
-
else
|
|
171
|
-
scope.onDemand.delete(spec.appId);
|
|
172
|
-
};
|
|
173
192
|
/** Sends one environment's complete desired set as a new revision of its scope. */
|
|
174
193
|
const sendSet = async (scope) => {
|
|
175
194
|
const management = await managementOf(scope.host);
|
|
176
195
|
const specs = await scope.desired();
|
|
177
|
-
|
|
178
|
-
track(scope, spec);
|
|
196
|
+
scope.desiredAppIds = new Set(specs.map((spec) => spec.appId));
|
|
179
197
|
const deployments = specs.map((spec) => toHostSpec(scope, spec));
|
|
180
198
|
const send = async (revision) => {
|
|
181
199
|
revisions.set(scope.environmentId, revision);
|
|
@@ -191,8 +209,22 @@ export function createHostDriver(options) {
|
|
|
191
209
|
await send(first.revision + 1);
|
|
192
210
|
};
|
|
193
211
|
const scheduleRestore = (scope) => {
|
|
212
|
+
if (scope.restoring)
|
|
213
|
+
return scope.restoring;
|
|
194
214
|
const current = sendSet(scope)
|
|
195
|
-
.
|
|
215
|
+
.then(() => {
|
|
216
|
+
scope.restoreFailed = false;
|
|
217
|
+
scope.restoreError = null;
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
scope.restoreFailed = true;
|
|
221
|
+
scope.restoreError =
|
|
222
|
+
error instanceof Error ? error.message : String(error);
|
|
223
|
+
if (scopes.get(scope.environmentId) === scope)
|
|
224
|
+
watchChurn(true);
|
|
225
|
+
diagnostic.error(`Failed to restore the Apps of environment ${scope.environmentId}`, error);
|
|
226
|
+
throw error;
|
|
227
|
+
})
|
|
196
228
|
.finally(() => {
|
|
197
229
|
if (scope.restoring === current)
|
|
198
230
|
scope.restoring = null;
|
|
@@ -201,28 +233,49 @@ export function createHostDriver(options) {
|
|
|
201
233
|
return current;
|
|
202
234
|
};
|
|
203
235
|
const restoreHost = async (host) => {
|
|
236
|
+
const failures = [];
|
|
204
237
|
for (const scope of scopes.values())
|
|
205
238
|
if (scope.host === host)
|
|
206
|
-
await scheduleRestore(scope)
|
|
239
|
+
await scheduleRestore(scope).catch((error) => {
|
|
240
|
+
failures.push(error);
|
|
241
|
+
});
|
|
242
|
+
// A broken environment must not prevent the remaining environments from being restored.
|
|
243
|
+
if (failures.length)
|
|
244
|
+
throw failures[0];
|
|
207
245
|
};
|
|
208
246
|
const watchReady = (host) => {
|
|
209
247
|
if (watched.has(host))
|
|
210
248
|
return;
|
|
211
249
|
watched.add(host);
|
|
212
250
|
host.controller.onReady(() => {
|
|
213
|
-
|
|
251
|
+
if (host === inProcess && !restartScheduled) {
|
|
252
|
+
hostGeneration += 1;
|
|
253
|
+
churn = 0;
|
|
254
|
+
runtimeChurn = 0;
|
|
255
|
+
if (restartDeadline)
|
|
256
|
+
clearTimeout(restartDeadline);
|
|
257
|
+
restartDeadline = null;
|
|
258
|
+
rotationSince = null;
|
|
259
|
+
capacityRotation = false;
|
|
260
|
+
}
|
|
261
|
+
if (!restartScheduled) {
|
|
262
|
+
const restoring = restoreHost(host);
|
|
263
|
+
if (host === inProcess)
|
|
264
|
+
readyRestoring = restoring;
|
|
265
|
+
void restoring
|
|
266
|
+
.catch(() => undefined)
|
|
267
|
+
.finally(() => {
|
|
268
|
+
if (readyRestoring === restoring)
|
|
269
|
+
readyRestoring = null;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
214
272
|
});
|
|
215
273
|
};
|
|
216
274
|
// --- Recycling the in-process Host (`restartAfterChurn`) ---------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.filter((scope) => scope.host === host)
|
|
222
|
-
.map(async (scope) => [
|
|
223
|
-
scope,
|
|
224
|
-
await management.getStatus({ scope: scope.environmentId }),
|
|
225
|
-
]));
|
|
275
|
+
const finishRotation = (ok) => {
|
|
276
|
+
for (const resolve of rotationWaiters)
|
|
277
|
+
resolve(ok);
|
|
278
|
+
rotationWaiters.clear();
|
|
226
279
|
};
|
|
227
280
|
const restartHost = async (reason) => {
|
|
228
281
|
const { controller } = inProcess;
|
|
@@ -230,10 +283,28 @@ export function createHostDriver(options) {
|
|
|
230
283
|
throw new Error('This Host runtime cannot be restarted by the driver.');
|
|
231
284
|
restartScheduled ??= (async () => {
|
|
232
285
|
try {
|
|
233
|
-
|
|
286
|
+
hostGeneration += 1;
|
|
287
|
+
if (restartDeadline)
|
|
288
|
+
clearTimeout(restartDeadline);
|
|
289
|
+
restartDeadline = null;
|
|
290
|
+
await controller.restart(reason, {
|
|
291
|
+
shutdownTimeoutMs: inProcess.restartDrainMs ?? 30_000,
|
|
292
|
+
});
|
|
234
293
|
churn = 0;
|
|
235
294
|
runtimeChurn = 0;
|
|
295
|
+
rotationSince = null;
|
|
296
|
+
capacityRotation = false;
|
|
236
297
|
await restoreHost(inProcess);
|
|
298
|
+
finishRotation(true);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
// If restart failed before the child exited, serving traffic can resume. New revisions remain paused.
|
|
302
|
+
const service = await managementOf(inProcess).catch(() => null);
|
|
303
|
+
await service
|
|
304
|
+
?.prepareRotation?.({ resume: true })
|
|
305
|
+
.catch(() => undefined);
|
|
306
|
+
finishRotation(false);
|
|
307
|
+
throw error;
|
|
237
308
|
}
|
|
238
309
|
finally {
|
|
239
310
|
restartScheduled = null;
|
|
@@ -241,61 +312,190 @@ export function createHostDriver(options) {
|
|
|
241
312
|
})();
|
|
242
313
|
await restartScheduled;
|
|
243
314
|
};
|
|
244
|
-
/** Whether an on-demand App runs or starts, which a restart would cut off. */
|
|
245
|
-
const onDemandBusy = (statuses) => statuses.some(([scope, status]) => status.deployments.some((deployment) => {
|
|
246
|
-
const state = observed(deployment).state;
|
|
247
|
-
return (scope.onDemand.has(deployment.appId) &&
|
|
248
|
-
(state === 'running' || state === 'starting'));
|
|
249
|
-
}));
|
|
250
|
-
/**
|
|
251
|
-
* Restarts the in-process Host when the churn reached `restartAfterChurn`, nothing is in flight and no on-demand App
|
|
252
|
-
* is in use; otherwise a later check tries again.
|
|
253
|
-
*/
|
|
254
315
|
const maybeRestart = async () => {
|
|
255
|
-
const limit = inProcess.restartAfterChurn;
|
|
256
316
|
const { controller } = inProcess;
|
|
257
|
-
if (
|
|
317
|
+
if (restartScheduled || rotationPreparing)
|
|
258
318
|
return;
|
|
259
|
-
if (controller.getInfo().status !== 'ready')
|
|
319
|
+
if (controller.getInfo().status !== 'ready' || Date.now() < retryCheckAt)
|
|
260
320
|
return;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
321
|
+
if ((!inProcess.restartAfterChurn && !heapRatio && !capacityRotation) ||
|
|
322
|
+
!controller.restart)
|
|
323
|
+
return;
|
|
324
|
+
if (batchBusy() && rotationSince === null && !capacityRotation)
|
|
325
|
+
return;
|
|
326
|
+
const generation = hostGeneration;
|
|
327
|
+
const service = await managementOf(inProcess);
|
|
328
|
+
// The unfiltered status includes resident, staging and unscoped Apps sharing the process.
|
|
329
|
+
const status = await service.getStatus();
|
|
330
|
+
if (restartScheduled || generation !== hostGeneration)
|
|
331
|
+
return;
|
|
332
|
+
const counters = status.counters;
|
|
333
|
+
runtimeChurn =
|
|
334
|
+
(counters?.idleStops ?? 0) +
|
|
335
|
+
(counters?.dormancies ?? 0) +
|
|
336
|
+
(counters?.capacityStops ?? 0) +
|
|
337
|
+
(counters?.startupTimeouts ?? 0);
|
|
266
338
|
const total = churn + runtimeChurn;
|
|
267
|
-
|
|
339
|
+
const memory = counters?.memory;
|
|
340
|
+
const threshold = total > 0 &&
|
|
341
|
+
(capacityRotation ||
|
|
342
|
+
(inProcess.restartAfterChurn && total >= inProcess.restartAfterChurn) ||
|
|
343
|
+
(heapRatio > 0 &&
|
|
344
|
+
memory &&
|
|
345
|
+
memory.heapUsed >= memory.heapLimit * heapRatio));
|
|
346
|
+
if (!threshold && rotationSince === null)
|
|
347
|
+
return;
|
|
348
|
+
rotationSince ??= Date.now();
|
|
349
|
+
const wait = inProcess.restartMaxWaitMs ?? 5 * 60_000;
|
|
350
|
+
const expired = Date.now() - rotationSince >= wait;
|
|
351
|
+
if (!expired && !restartDeadline) {
|
|
352
|
+
restartDeadline = setTimeout(() => {
|
|
353
|
+
restartDeadline = null;
|
|
354
|
+
checkChurn();
|
|
355
|
+
}, Math.max(1, wait - (Date.now() - rotationSince)));
|
|
356
|
+
restartDeadline.unref?.();
|
|
357
|
+
}
|
|
358
|
+
if (expired)
|
|
359
|
+
await service.prepareRotation?.({ pauseOnly: true });
|
|
360
|
+
if (inFlight > 0 ||
|
|
361
|
+
batchBusy() ||
|
|
362
|
+
[...scopes.values()].some((scope) => scope.host === inProcess && scope.restoring !== null) ||
|
|
363
|
+
(counters?.operationsInFlight ?? 0) > 0)
|
|
364
|
+
return;
|
|
365
|
+
const busy = status.deployments.some((deployment) => deployment.lifecycle?.state === 'starting' ||
|
|
366
|
+
(!expired && (deployment.app?.activeRequests ?? 0) > 0) ||
|
|
367
|
+
(!expired && (deployment.app?.activeConnections ?? 0) > 0));
|
|
368
|
+
if (busy)
|
|
268
369
|
return;
|
|
269
|
-
|
|
370
|
+
// Before the deadline reserve only an idle child. At the deadline gate ingress first, then give existing
|
|
371
|
+
// responses a bounded drain. The old proxy target remains available to return 503 + Retry-After while draining.
|
|
372
|
+
if (!service.prepareRotation)
|
|
270
373
|
return;
|
|
271
|
-
|
|
374
|
+
const preparing = service.prepareRotation({
|
|
375
|
+
ignoreConnections: expired,
|
|
376
|
+
...(expired
|
|
377
|
+
? { drainTimeoutMs: inProcess.restartDrainMs ?? 30_000 }
|
|
378
|
+
: {}),
|
|
379
|
+
});
|
|
380
|
+
rotationPreparing = preparing;
|
|
381
|
+
let reserved;
|
|
382
|
+
try {
|
|
383
|
+
reserved = await preparing;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
await service.prepareRotation({ resume: true }).catch(() => undefined);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
if (rotationPreparing === preparing)
|
|
391
|
+
rotationPreparing = null;
|
|
392
|
+
}
|
|
393
|
+
if (!reserved)
|
|
394
|
+
return;
|
|
395
|
+
// New tracked work waits for the handoff. New control-plane dispatches have not entered the Host yet;
|
|
396
|
+
// the batch was settled before the handoff began. Existing operations remain protected by the child recheck.
|
|
397
|
+
if (inFlight > 0 || generation !== hostGeneration) {
|
|
398
|
+
await service.prepareRotation({ resume: true });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
await restartHost(`${total} applications deployed, replaced, removed, stopped or made dormant; recycling Host memory`);
|
|
272
402
|
};
|
|
273
403
|
const checkChurn = () => {
|
|
274
|
-
|
|
404
|
+
if (checkingChurn) {
|
|
405
|
+
recheckChurn = true;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
checkingChurn = true;
|
|
409
|
+
void maybeRestart()
|
|
410
|
+
.catch((error) => {
|
|
411
|
+
retryCheckAt = Date.now() + (options.churnCheckIntervalMs ?? 60_000);
|
|
412
|
+
diagnostic.error('Failed to recycle the App Host', error);
|
|
413
|
+
})
|
|
414
|
+
.finally(() => {
|
|
415
|
+
checkingChurn = false;
|
|
416
|
+
// Retry failed restores after evaluating rotation, never as a foreground management prerequisite. A pending
|
|
417
|
+
// restore remains protected work, including the interval spent reading its desired set before Host submission.
|
|
418
|
+
if (!inFlight &&
|
|
419
|
+
!batchBusy() &&
|
|
420
|
+
!rotationPreparing &&
|
|
421
|
+
!restartScheduled &&
|
|
422
|
+
inProcess.controller.getInfo().status === 'ready' &&
|
|
423
|
+
Date.now() >= retryCheckAt)
|
|
424
|
+
for (const scope of scopes.values())
|
|
425
|
+
if (scope.restoreFailed && !scope.restoring)
|
|
426
|
+
void scheduleRestore(scope).catch(() => undefined);
|
|
427
|
+
if (recheckChurn) {
|
|
428
|
+
recheckChurn = false;
|
|
429
|
+
queueMicrotask(checkChurn);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
275
432
|
};
|
|
276
433
|
const countChurn = (scope) => {
|
|
277
434
|
if (scope.host !== inProcess)
|
|
278
435
|
return;
|
|
279
436
|
churn += 1;
|
|
280
|
-
checkChurn();
|
|
281
437
|
};
|
|
282
|
-
const watchChurn = () => {
|
|
283
|
-
if (churnTimer ||
|
|
438
|
+
const watchChurn = (forRecovery = false) => {
|
|
439
|
+
if (churnTimer ||
|
|
440
|
+
(!forRecovery && !inProcess.restartAfterChurn && !heapRatio))
|
|
284
441
|
return;
|
|
285
442
|
churnTimer = setInterval(checkChurn, options.churnCheckIntervalMs ?? 60_000);
|
|
286
443
|
churnTimer.unref?.();
|
|
287
444
|
};
|
|
445
|
+
/** Several failed admissions share one rotation, never hold inFlight while waiting for it. */
|
|
446
|
+
const recycleForCapacity = async (signal) => {
|
|
447
|
+
if (!inProcess.controller.restart || signal.aborted)
|
|
448
|
+
return false;
|
|
449
|
+
const status = await (await managementOf(inProcess)).getStatus();
|
|
450
|
+
const counters = status.counters;
|
|
451
|
+
runtimeChurn =
|
|
452
|
+
(counters?.idleStops ?? 0) +
|
|
453
|
+
(counters?.dormancies ?? 0) +
|
|
454
|
+
(counters?.capacityStops ?? 0) +
|
|
455
|
+
(counters?.startupTimeouts ?? 0);
|
|
456
|
+
if (churn + runtimeChurn <= 0)
|
|
457
|
+
return false;
|
|
458
|
+
capacityRotation = true;
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
const done = (ok) => {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
rotationWaiters.delete(done);
|
|
463
|
+
signal.removeEventListener('abort', cancelled);
|
|
464
|
+
resolve(ok);
|
|
465
|
+
};
|
|
466
|
+
const cancelled = () => done(false);
|
|
467
|
+
signal.addEventListener('abort', cancelled, { once: true });
|
|
468
|
+
const timer = setTimeout(() => done(false), (inProcess.restartMaxWaitMs ?? 300_000) +
|
|
469
|
+
2 * (inProcess.restartDrainMs ?? 30_000) +
|
|
470
|
+
(options.churnCheckIntervalMs ?? 60_000));
|
|
471
|
+
rotationWaiters.add(done);
|
|
472
|
+
if (signal.aborted)
|
|
473
|
+
cancelled();
|
|
474
|
+
else
|
|
475
|
+
checkChurn();
|
|
476
|
+
});
|
|
477
|
+
};
|
|
288
478
|
const tracked = async (scope, work) => {
|
|
289
479
|
if (scope.host !== inProcess)
|
|
290
480
|
return await work();
|
|
481
|
+
if (rotationPreparing)
|
|
482
|
+
await rotationPreparing.catch(() => false);
|
|
291
483
|
if (restartScheduled)
|
|
292
484
|
await restartScheduled.catch(() => undefined);
|
|
485
|
+
if (readyRestoring)
|
|
486
|
+
await readyRestoring.catch(() => undefined);
|
|
487
|
+
// A pending replay may still hold an older desired set. Let it finish before submitting any mutation.
|
|
488
|
+
// Failure leaves management usable; only the background poll initiates another recovery attempt.
|
|
489
|
+
if (scope.restoring)
|
|
490
|
+
await scope.restoring.catch(() => undefined);
|
|
293
491
|
inFlight += 1;
|
|
294
492
|
try {
|
|
295
493
|
return await work();
|
|
296
494
|
}
|
|
297
495
|
finally {
|
|
298
496
|
inFlight -= 1;
|
|
497
|
+
if (inFlight === 0)
|
|
498
|
+
queueMicrotask(checkChurn);
|
|
299
499
|
}
|
|
300
500
|
};
|
|
301
501
|
const capabilitiesOf = (backend) => HOST_BACKEND_CAPABILITIES[backend] ??
|
|
@@ -343,6 +543,10 @@ export function createHostDriver(options) {
|
|
|
343
543
|
churn: () => churn + runtimeChurn,
|
|
344
544
|
restartHost: (reason = 'restart requested') => restartHost(reason),
|
|
345
545
|
proxyTarget() {
|
|
546
|
+
// Only the initial replay after the child becomes ready gates ingress. Failed environments and their
|
|
547
|
+
// background retries must not take healthy applications on this shared Host offline.
|
|
548
|
+
if (restartScheduled || readyRestoring)
|
|
549
|
+
return null;
|
|
346
550
|
const info = inProcess.controller.getInfo();
|
|
347
551
|
return info.status === 'ready' && info.targetUrl
|
|
348
552
|
? new URL(info.targetUrl)
|
|
@@ -361,11 +565,15 @@ export function createHostDriver(options) {
|
|
|
361
565
|
scope: scopeOf(environment, settings),
|
|
362
566
|
publicUrl: environment.publicUrl,
|
|
363
567
|
desired: () => context.desired(),
|
|
364
|
-
onDemand: new Set(),
|
|
365
568
|
restoring: null,
|
|
569
|
+
restoreFailed: false,
|
|
570
|
+
restoreError: null,
|
|
571
|
+
desiredAppIds: new Set(),
|
|
572
|
+
deploymentBatchBusy: (excluding) => context.deploymentBatchBusy?.(excluding) ?? false,
|
|
366
573
|
};
|
|
367
574
|
scopes.set(environment.id, scope);
|
|
368
575
|
watchReady(host);
|
|
576
|
+
// Recovery is retried by the background poll. A broken desired set must not block management or repair.
|
|
369
577
|
const management = () => managementOf(host);
|
|
370
578
|
const statusOf = async (appId) => {
|
|
371
579
|
const status = await (await management()).getStatus({ scope: environment.id, appIds: [appId] });
|
|
@@ -380,7 +588,9 @@ export function createHostDriver(options) {
|
|
|
380
588
|
error: null,
|
|
381
589
|
};
|
|
382
590
|
};
|
|
591
|
+
const pendingDeployments = new Map();
|
|
383
592
|
return {
|
|
593
|
+
deploymentSettled: checkChurn,
|
|
384
594
|
async check() {
|
|
385
595
|
try {
|
|
386
596
|
const url = (await host.controller.ensureStarted()).toString();
|
|
@@ -398,57 +608,122 @@ export function createHostDriver(options) {
|
|
|
398
608
|
};
|
|
399
609
|
}
|
|
400
610
|
},
|
|
611
|
+
async cancel(deploymentId, appId, queuedOnly) {
|
|
612
|
+
const pending = pendingDeployments.get(deploymentId);
|
|
613
|
+
if (pending && pending.appId === appId && !pending.submitted) {
|
|
614
|
+
pending.cancelled = true;
|
|
615
|
+
pending.abortController.abort();
|
|
616
|
+
return 'cancelled';
|
|
617
|
+
}
|
|
618
|
+
return (await management()).cancelDeployment(deploymentId, appId, {
|
|
619
|
+
scope: scope.scope,
|
|
620
|
+
queuedOnly,
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
async progress(deploymentId, appId) {
|
|
624
|
+
if (pendingDeployments.get(deploymentId)?.appId === appId &&
|
|
625
|
+
capacityWaiting.has(deploymentId))
|
|
626
|
+
return { state: 'queued', queueAhead: 0, cancellable: true };
|
|
627
|
+
return (await management()).deploymentProgress(deploymentId, appId, {
|
|
628
|
+
scope: scope.scope,
|
|
629
|
+
});
|
|
630
|
+
},
|
|
401
631
|
async apply(spec, onEvent) {
|
|
402
|
-
|
|
403
|
-
|
|
632
|
+
const pending = {
|
|
633
|
+
appId: spec.appId,
|
|
634
|
+
submitted: false,
|
|
635
|
+
cancelled: false,
|
|
636
|
+
abortController: new AbortController(),
|
|
637
|
+
};
|
|
638
|
+
pendingDeployments.set(spec.deploymentId, pending);
|
|
639
|
+
let sequence = 0;
|
|
640
|
+
const forward = (entry) => {
|
|
641
|
+
onEvent?.({ ...entry, sequence: ++sequence });
|
|
642
|
+
};
|
|
404
643
|
try {
|
|
405
|
-
const
|
|
644
|
+
const submit = (retryCapacity) => tracked(scope, async () => {
|
|
406
645
|
const service = await management();
|
|
407
|
-
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
646
|
+
if (pending.cancelled)
|
|
647
|
+
throw Object.assign(new Error('Deployment cancelled.'), {
|
|
648
|
+
code: 'DEPLOYMENT_CANCELLED',
|
|
649
|
+
});
|
|
650
|
+
pending.submitted = true;
|
|
651
|
+
const status = await service.applyDeployment({
|
|
413
652
|
...toHostSpec(scope, spec, 'running', 'eager'),
|
|
414
653
|
scope: scope.scope,
|
|
415
|
-
|
|
654
|
+
...(retryCapacity ? { retryCapacity: true } : {}),
|
|
655
|
+
}, onEvent ? forward : undefined);
|
|
656
|
+
if (status.deployments.some((deployment) => deployment.appId === spec.appId &&
|
|
657
|
+
deployment.observedState !== 'failed'))
|
|
658
|
+
countChurn(scope);
|
|
659
|
+
return status;
|
|
416
660
|
});
|
|
417
|
-
|
|
418
|
-
|
|
661
|
+
let status = await submit(false);
|
|
662
|
+
const capacity = status.deployments.find((deployment) => deployment.appId === spec.appId)?.errorCode === 'APP_CAPACITY_EXCEEDED';
|
|
663
|
+
if (capacity && scope.host === inProcess) {
|
|
664
|
+
pending.submitted = false;
|
|
665
|
+
capacityWaiting.add(spec.deploymentId);
|
|
666
|
+
forward({
|
|
667
|
+
phase: 'queued',
|
|
668
|
+
msg: 'Waiting for Host memory recycling',
|
|
669
|
+
queueAhead: 0,
|
|
670
|
+
cancellable: true,
|
|
671
|
+
});
|
|
672
|
+
try {
|
|
673
|
+
if (await recycleForCapacity(pending.abortController.signal)) {
|
|
674
|
+
capacityWaiting.delete(spec.deploymentId);
|
|
675
|
+
status = await submit(true);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
capacityWaiting.delete(spec.deploymentId);
|
|
680
|
+
}
|
|
681
|
+
if (pending.cancelled)
|
|
682
|
+
throw Object.assign(new Error('Deployment cancelled.'), {
|
|
683
|
+
code: 'DEPLOYMENT_CANCELLED',
|
|
684
|
+
});
|
|
685
|
+
}
|
|
419
686
|
const deployed = status.deployments.find((deployment) => deployment.appId === spec.appId);
|
|
420
687
|
if (!deployed)
|
|
421
688
|
return failed(spec, 'The Host did not report the deployment.');
|
|
689
|
+
scope.desiredAppIds.add(spec.appId);
|
|
422
690
|
return outcomeOf(deployed, spec.deploymentId);
|
|
423
691
|
}
|
|
424
692
|
catch (error) {
|
|
693
|
+
if (error.code === 'DEPLOYMENT_CANCELLED')
|
|
694
|
+
return {
|
|
695
|
+
...failed(spec, 'Deployment cancelled.'),
|
|
696
|
+
state: 'cancelled',
|
|
697
|
+
};
|
|
425
698
|
return failed(spec, error instanceof Error ? error.message : String(error));
|
|
426
699
|
}
|
|
700
|
+
finally {
|
|
701
|
+
pendingDeployments.delete(spec.deploymentId);
|
|
702
|
+
}
|
|
427
703
|
},
|
|
428
704
|
async start(spec) {
|
|
429
|
-
|
|
430
|
-
await (await management()).startDeployment({
|
|
705
|
+
await tracked(scope, async () => (await management()).startDeployment({
|
|
431
706
|
...toHostSpec(scope, spec, 'running'),
|
|
432
707
|
scope: scope.scope,
|
|
433
|
-
});
|
|
708
|
+
}));
|
|
434
709
|
return await statusOf(spec.appId);
|
|
435
710
|
},
|
|
436
711
|
async stop(appId) {
|
|
437
|
-
await (await management()).stopDeployment(appId);
|
|
712
|
+
await tracked(scope, async () => (await management()).stopDeployment(appId));
|
|
438
713
|
return await statusOf(appId);
|
|
439
714
|
},
|
|
440
715
|
async restart(appId) {
|
|
441
|
-
await (await management()).restartApp(appId);
|
|
716
|
+
await tracked(scope, async () => (await management()).restartApp(appId));
|
|
442
717
|
return await statusOf(appId);
|
|
443
718
|
},
|
|
444
719
|
async remove(appId, { purgeData }) {
|
|
445
|
-
await tracked(scope, async () =>
|
|
446
|
-
|
|
447
|
-
|
|
720
|
+
await tracked(scope, async () => {
|
|
721
|
+
await (await management()).removeDeployment(appId, { purgeData });
|
|
722
|
+
scope.desiredAppIds.delete(appId);
|
|
723
|
+
countChurn(scope);
|
|
724
|
+
});
|
|
448
725
|
},
|
|
449
726
|
async status(appIds) {
|
|
450
|
-
if (scope.restoring)
|
|
451
|
-
await Promise.race([scope.restoring, delay(5_000)]);
|
|
452
727
|
const status = await (await management()).getStatus({
|
|
453
728
|
scope: environment.id,
|
|
454
729
|
...(appIds ? { appIds: [...appIds] } : {}),
|
|
@@ -456,6 +731,18 @@ export function createHostDriver(options) {
|
|
|
456
731
|
const result = new Map();
|
|
457
732
|
for (const deployment of status.deployments)
|
|
458
733
|
result.set(deployment.appId, observed(deployment));
|
|
734
|
+
// A failed restore left these Apps off the Host until a background retry succeeds.
|
|
735
|
+
if (scope.restoreFailed && !scope.restoring && appIds)
|
|
736
|
+
for (const appId of appIds)
|
|
737
|
+
if (!result.has(appId))
|
|
738
|
+
result.set(appId, {
|
|
739
|
+
state: 'failed',
|
|
740
|
+
version: null,
|
|
741
|
+
deploymentId: null,
|
|
742
|
+
startedAt: null,
|
|
743
|
+
error: UNRESTORED_APP_ERROR,
|
|
744
|
+
unrestored: true,
|
|
745
|
+
});
|
|
459
746
|
if ((scope.restoring || !status.scope) && appIds)
|
|
460
747
|
for (const appId of appIds)
|
|
461
748
|
if (!result.has(appId))
|
|
@@ -469,11 +756,61 @@ export function createHostDriver(options) {
|
|
|
469
756
|
return result;
|
|
470
757
|
},
|
|
471
758
|
async restore() {
|
|
472
|
-
await
|
|
473
|
-
|
|
759
|
+
await tracked(scope, async () => {
|
|
760
|
+
await host.controller.ensureStarted();
|
|
761
|
+
await scheduleRestore(scope);
|
|
762
|
+
});
|
|
474
763
|
},
|
|
475
764
|
async reloadConfig(appId, content) {
|
|
476
|
-
await (await management()).publishAppConfig(appId, content);
|
|
765
|
+
await tracked(scope, async () => (await management()).publishAppConfig(appId, content));
|
|
766
|
+
},
|
|
767
|
+
async occupancy() {
|
|
768
|
+
if (host !== inProcess)
|
|
769
|
+
return null;
|
|
770
|
+
// The unfiltered status: every scope sharing the process, and Apps deployed without one.
|
|
771
|
+
const status = await (await management()).getStatus();
|
|
772
|
+
const failedScopes = [...scopes.values()].filter((other) => other.host === inProcess &&
|
|
773
|
+
other.restoreFailed &&
|
|
774
|
+
!other.restoring);
|
|
775
|
+
const present = new Set(status.deployments.map((deployment) => `${deployment.scopeId ?? ''}\u0000${deployment.appId}`));
|
|
776
|
+
// A failed restore leaves an environment's Apps off the Host until a background retry succeeds: they are
|
|
777
|
+
// still deployed, and failed, as `status()` reports them. Apps still on the Host keep their own state.
|
|
778
|
+
const missing = failedScopes.flatMap((other) => [...other.desiredAppIds]
|
|
779
|
+
.filter((appId) => !present.has(`${other.environmentId}\u0000${appId}`))
|
|
780
|
+
.map((appId) => ({
|
|
781
|
+
state: 'failed',
|
|
782
|
+
version: null,
|
|
783
|
+
deploymentId: null,
|
|
784
|
+
startedAt: null,
|
|
785
|
+
error: UNRESTORED_APP_ERROR,
|
|
786
|
+
unrestored: true,
|
|
787
|
+
appId,
|
|
788
|
+
environmentId: other.environmentId,
|
|
789
|
+
})));
|
|
790
|
+
return {
|
|
791
|
+
id: IN_PROCESS_BACKEND,
|
|
792
|
+
memory: status.counters?.memory ?? null,
|
|
793
|
+
maxActive: status.counters?.maxActiveApps ?? null,
|
|
794
|
+
recycling: restartScheduled ||
|
|
795
|
+
readyRestoring ||
|
|
796
|
+
inProcess.controller.getInfo().status !== 'ready'
|
|
797
|
+
? 'restarting'
|
|
798
|
+
: rotationSince !== null || status.counters?.rotationPending
|
|
799
|
+
? 'pending'
|
|
800
|
+
: null,
|
|
801
|
+
apps: [
|
|
802
|
+
...status.deployments.map((deployment) => ({
|
|
803
|
+
...observed(deployment),
|
|
804
|
+
appId: deployment.appId,
|
|
805
|
+
environmentId: deployment.scopeId ?? null,
|
|
806
|
+
})),
|
|
807
|
+
...missing,
|
|
808
|
+
],
|
|
809
|
+
failedEnvironments: failedScopes.map((other) => ({
|
|
810
|
+
environmentId: other.environmentId,
|
|
811
|
+
error: other.restoreError,
|
|
812
|
+
})),
|
|
813
|
+
};
|
|
477
814
|
},
|
|
478
815
|
async logs(appId, query) {
|
|
479
816
|
return await (await management()).readAppLogs(appId, query);
|
|
@@ -524,6 +861,10 @@ function observed(item) {
|
|
|
524
861
|
item.lifecycle?.lastFailure?.error ??
|
|
525
862
|
null),
|
|
526
863
|
lastAccessedAt: item.lifecycle?.lastAccessedAt ?? null,
|
|
864
|
+
...(item.lifecycle?.holds?.length ? { holds: item.lifecycle.holds } : {}),
|
|
865
|
+
...(item.lifecycle?.waiting && !active
|
|
866
|
+
? { waiting: item.lifecycle.waiting }
|
|
867
|
+
: {}),
|
|
527
868
|
};
|
|
528
869
|
}
|
|
529
870
|
/** How a deployment ended, as the plugin records it: `running` succeeded, anything else failed. */
|
|
@@ -535,7 +876,10 @@ function outcomeOf(item, deploymentId) {
|
|
|
535
876
|
...status,
|
|
536
877
|
state: 'failed',
|
|
537
878
|
deploymentId,
|
|
538
|
-
error: item.
|
|
879
|
+
error: item.errorCode
|
|
880
|
+
? `[${item.errorCode}] ${item.error ?? status.error ?? 'The deployment failed.'}`
|
|
881
|
+
: (item.error ?? status.error ?? 'The deployment failed.'),
|
|
882
|
+
...(item.errorCode ? { errorCode: item.errorCode } : {}),
|
|
539
883
|
};
|
|
540
884
|
return { ...status, state: 'running', deploymentId, error: null };
|
|
541
885
|
}
|
|
@@ -550,11 +894,14 @@ function operationOutcome(result) {
|
|
|
550
894
|
lastAccessedAt: result.status?.lifecycle?.lastAccessedAt ?? null,
|
|
551
895
|
};
|
|
552
896
|
return {
|
|
553
|
-
state: 'failed',
|
|
897
|
+
state: result.state === 'cancelled' ? 'cancelled' : 'failed',
|
|
898
|
+
...(result.status?.errorCode ? { errorCode: result.status.errorCode } : {}),
|
|
554
899
|
version: result.status?.version ?? null,
|
|
555
900
|
deploymentId: result.operationId,
|
|
556
901
|
startedAt: null,
|
|
557
|
-
error: result.
|
|
902
|
+
error: (result.state === 'interrupted'
|
|
903
|
+
? 'Deployment was interrupted by a restart. Deploy the release again.'
|
|
904
|
+
: result.error) ??
|
|
558
905
|
(result.state === 'running'
|
|
559
906
|
? `The deployment of ${result.appId} has not finished on the Host.`
|
|
560
907
|
: 'The deployment failed.'),
|
|
@@ -597,16 +944,15 @@ function unionCapabilities(list) {
|
|
|
597
944
|
images: list.some((item) => item.images === true),
|
|
598
945
|
};
|
|
599
946
|
}
|
|
600
|
-
function delay(ms) {
|
|
601
|
-
return new Promise((resolve) => {
|
|
602
|
-
setTimeout(resolve, ms).unref?.();
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
947
|
/** Writes a managed Host child's configuration file (`AppHostSupervisor` `configPath`) before it starts. */
|
|
606
948
|
export async function writeHostConfig(configPath, config) {
|
|
607
949
|
await writeTextAtomic(configPath, stringifyYaml({
|
|
608
950
|
host: {
|
|
609
951
|
mode: 'managed',
|
|
952
|
+
...(config.memoryBudget ? { memoryBudget: config.memoryBudget } : {}),
|
|
953
|
+
...(config.maxActiveApps !== undefined
|
|
954
|
+
? { maxActiveApps: config.maxActiveApps }
|
|
955
|
+
: {}),
|
|
610
956
|
...(config.logging ? { logging: config.logging } : {}),
|
|
611
957
|
server: { host: '127.0.0.1', port: 3000 },
|
|
612
958
|
artifact: config.artifact.driver === 'fs'
|