@highstate/backend 0.19.1 → 0.21.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/dist/chunk-b05q6fm2.js +37 -0
- package/dist/{chunk-V2NILDHS.js → chunk-gxjwa93h.js} +704 -604
- package/dist/{chunk-X2WG3WGL.js → chunk-vzdz6chj.js} +18 -15
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7350 -3514
- package/dist/library/package-resolution-worker.js +121 -10
- package/dist/library/worker/main.js +31 -17
- package/dist/shared/index.js +254 -4
- package/package.json +19 -20
- package/prisma/backend/_schema/object.prisma +12 -0
- package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
- package/prisma/project/artifact.prisma +3 -0
- package/prisma/project/entity.prisma +125 -0
- package/prisma/project/instance.prisma +6 -0
- package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
- package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
- package/prisma/project/operation.prisma +3 -0
- package/src/artifact/factory.ts +3 -2
- package/src/business/artifact.test.ts +22 -2
- package/src/business/artifact.ts +7 -1
- package/src/business/entity-snapshot.test.ts +684 -0
- package/src/business/entity-snapshot.ts +904 -0
- package/src/business/evaluation.test.ts +56 -0
- package/src/business/evaluation.ts +102 -22
- package/src/business/global-search.test.ts +344 -0
- package/src/business/global-search.ts +902 -0
- package/src/business/index.ts +4 -0
- package/src/business/instance-lock.ts +58 -74
- package/src/business/instance-state.test.ts +15 -1
- package/src/business/instance-state.ts +37 -14
- package/src/business/object-ref-index.test.ts +140 -0
- package/src/business/object-ref-index.ts +193 -0
- package/src/business/operation.test.ts +15 -1
- package/src/business/operation.ts +4 -0
- package/src/business/project-model.ts +154 -13
- package/src/business/project-unlock.ts +25 -2
- package/src/business/project.ts +9 -0
- package/src/business/secret.test.ts +35 -2
- package/src/business/secret.ts +32 -9
- package/src/business/settings.ts +761 -0
- package/src/business/unit-output.test.ts +477 -0
- package/src/business/unit-output.ts +461 -0
- package/src/business/worker.ts +55 -4
- package/src/database/_generated/backend/postgresql/browser.ts +6 -0
- package/src/database/_generated/backend/postgresql/client.ts +6 -0
- package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
- package/src/database/_generated/backend/postgresql/models.ts +1 -0
- package/src/database/_generated/backend/sqlite/browser.ts +6 -0
- package/src/database/_generated/backend/sqlite/client.ts +6 -0
- package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
- package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
- package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
- package/src/database/_generated/backend/sqlite/models.ts +1 -0
- package/src/database/_generated/project/browser.ts +23 -0
- package/src/database/_generated/project/client.ts +23 -0
- package/src/database/_generated/project/commonInputTypes.ts +87 -53
- package/src/database/_generated/project/enums.ts +8 -0
- package/src/database/_generated/project/internal/class.ts +53 -5
- package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
- package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
- package/src/database/_generated/project/models/Artifact.ts +199 -11
- package/src/database/_generated/project/models/Entity.ts +1274 -0
- package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
- package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
- package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
- package/src/database/_generated/project/models/InstanceState.ts +361 -1
- package/src/database/_generated/project/models/Operation.ts +148 -3
- package/src/database/_generated/project/models/OperationLog.ts +0 -4
- package/src/database/_generated/project/models.ts +4 -0
- package/src/database/migration.ts +3 -0
- package/src/library/find-package-json.test.ts +77 -0
- package/src/library/find-package-json.ts +149 -0
- package/src/library/package-resolution-worker.ts +7 -3
- package/src/library/worker/evaluator.ts +7 -1
- package/src/orchestrator/manager.ts +7 -0
- package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
- package/src/orchestrator/operation-context.ts +154 -16
- package/src/orchestrator/operation-plan.destroy.test.md +33 -12
- package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
- package/src/orchestrator/operation-plan.fixtures.ts +2 -0
- package/src/orchestrator/operation-plan.md +4 -1
- package/src/orchestrator/operation-plan.ts +286 -92
- package/src/orchestrator/operation-plan.update.test.md +286 -11
- package/src/orchestrator/operation-plan.update.test.ts +656 -5
- package/src/orchestrator/operation-workset.ts +72 -22
- package/src/orchestrator/operation.cancel.test.ts +4 -0
- package/src/orchestrator/operation.composite.test.ts +341 -0
- package/src/orchestrator/operation.destroy.test.ts +4 -0
- package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
- package/src/orchestrator/operation.preview.test.ts +4 -0
- package/src/orchestrator/operation.refresh.test.ts +4 -0
- package/src/orchestrator/operation.test-utils.ts +52 -13
- package/src/orchestrator/operation.ts +230 -68
- package/src/orchestrator/operation.update.failure.test.ts +4 -0
- package/src/orchestrator/operation.update.skip.test.ts +196 -0
- package/src/orchestrator/operation.update.test.ts +4 -0
- package/src/orchestrator/plan-test-builder.ts +1 -0
- package/src/orchestrator/unit-input-values.test.ts +450 -0
- package/src/orchestrator/unit-input-values.ts +281 -0
- package/src/pubsub/manager.ts +3 -0
- package/src/runner/abstractions.ts +23 -54
- package/src/runner/factory.ts +3 -3
- package/src/runner/force-abort.ts +7 -2
- package/src/runner/local.ts +116 -87
- package/src/runner/pulumi.ts +3 -5
- package/src/services.ts +53 -2
- package/src/shared/models/prisma.ts +1 -0
- package/src/shared/models/project/entity.ts +121 -0
- package/src/shared/models/project/index.ts +1 -0
- package/src/shared/models/project/operation.ts +61 -3
- package/src/shared/models/project/state.ts +10 -0
- package/src/shared/models/project/worker.ts +7 -0
- package/src/shared/resolvers/effective-output-type.test.ts +494 -0
- package/src/shared/resolvers/effective-output-type.ts +162 -0
- package/src/shared/resolvers/index.ts +1 -0
- package/src/shared/resolvers/input.ts +59 -9
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/stable-json.ts +41 -0
- package/src/terminal/manager.ts +6 -0
- package/src/terminal/run.sh.ts +9 -4
- package/src/worker/manager.ts +97 -1
- package/LICENSE +0 -21
- package/dist/chunk-I7BWSAN6.js +0 -49
- package/dist/chunk-I7BWSAN6.js.map +0 -1
- package/dist/chunk-V2NILDHS.js.map +0 -1
- package/dist/chunk-X2WG3WGL.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/library/package-resolution-worker.js.map +0 -1
- package/dist/library/worker/main.js.map +0 -1
- package/dist/shared/index.js.map +0 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ComponentModel,
|
|
3
|
+
type EntityModel,
|
|
3
4
|
type HubInput,
|
|
4
5
|
type HubModel,
|
|
5
6
|
type InstanceInput,
|
|
6
7
|
type InstanceModel,
|
|
8
|
+
inputKey,
|
|
7
9
|
isUnitModel,
|
|
8
10
|
} from "@highstate/contract"
|
|
9
11
|
import { fromEntries, mapValues } from "remeda"
|
|
12
|
+
import { resolveEffectiveOutputType } from "./effective-output-type"
|
|
10
13
|
import { GraphResolver } from "./graph-resolver"
|
|
11
14
|
|
|
12
15
|
export type InputResolverNode =
|
|
@@ -14,10 +17,12 @@ export type InputResolverNode =
|
|
|
14
17
|
kind: "instance"
|
|
15
18
|
instance: InstanceModel
|
|
16
19
|
component: ComponentModel
|
|
20
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
17
21
|
}
|
|
18
22
|
| {
|
|
19
23
|
kind: "hub"
|
|
20
24
|
hub: HubModel
|
|
25
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export type ResolvedInstanceInput = {
|
|
@@ -30,6 +35,7 @@ export type InputResolverOutput =
|
|
|
30
35
|
kind: "instance"
|
|
31
36
|
instance: InstanceModel
|
|
32
37
|
component: ComponentModel
|
|
38
|
+
entities: Readonly<Record<string, EntityModel>>
|
|
33
39
|
resolvedInputs: Record<string, ResolvedInstanceInput[]>
|
|
34
40
|
resolvedOutputs: Record<string, InstanceInput[]> | undefined
|
|
35
41
|
resolvedInjectionInputs: ResolvedInstanceInput[]
|
|
@@ -79,6 +85,25 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
79
85
|
return dependencies
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
private resolveOutputTypeForInput(input: InstanceInput, fallbackType: string): string {
|
|
89
|
+
return resolveEffectiveOutputType({
|
|
90
|
+
input,
|
|
91
|
+
fallbackType,
|
|
92
|
+
getInstanceContext: instanceId => {
|
|
93
|
+
const output = this.outputs.get(`instance:${instanceId}`)
|
|
94
|
+
if (!output || output.kind !== "instance") {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
instance: output.instance,
|
|
100
|
+
component: output.component,
|
|
101
|
+
entities: output.entities,
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
processNode(node: InputResolverNode): InputResolverOutput {
|
|
83
108
|
const getHubOutput = (input: HubInput) => {
|
|
84
109
|
const output = this.outputs.get(`hub:${input.hubId}`)
|
|
@@ -119,7 +144,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
119
144
|
const hubResult: Map<string, ResolvedInstanceInput> = new Map()
|
|
120
145
|
|
|
121
146
|
const addHubResult = (input: ResolvedInstanceInput) => {
|
|
122
|
-
hubResult.set(
|
|
147
|
+
hubResult.set(inputKey(input.input), input)
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
for (const input of node.hub.inputs ?? []) {
|
|
@@ -131,7 +156,10 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
131
156
|
continue
|
|
132
157
|
}
|
|
133
158
|
|
|
134
|
-
addHubResult({
|
|
159
|
+
addHubResult({
|
|
160
|
+
input,
|
|
161
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
162
|
+
})
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
for (const injectionInput of node.hub.injectionInputs ?? []) {
|
|
@@ -162,6 +190,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
162
190
|
kind: "instance",
|
|
163
191
|
instance: node.instance,
|
|
164
192
|
component: node.component,
|
|
193
|
+
entities: node.entities,
|
|
165
194
|
resolvedInputs: mapValues(node.instance.resolvedInputs, (inputs, inputName) => {
|
|
166
195
|
const componentInput = node.component.inputs[inputName]
|
|
167
196
|
if (!componentInput) {
|
|
@@ -191,7 +220,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
191
220
|
resolvedInputsMap.set(inputName, inputs)
|
|
192
221
|
}
|
|
193
222
|
|
|
194
|
-
inputs.set(
|
|
223
|
+
inputs.set(inputKey(input.input), input)
|
|
195
224
|
}
|
|
196
225
|
|
|
197
226
|
const addInstanceInput = (inputName: string, input: InstanceInput) => {
|
|
@@ -213,17 +242,37 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
213
242
|
}
|
|
214
243
|
|
|
215
244
|
if (isUnitModel(component)) {
|
|
216
|
-
addInstanceResult(inputName, {
|
|
245
|
+
addInstanceResult(inputName, {
|
|
246
|
+
input,
|
|
247
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
248
|
+
})
|
|
217
249
|
return
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
if (resolvedOutputs) {
|
|
221
253
|
for (const output of resolvedOutputs) {
|
|
222
|
-
addInstanceResult(inputName, {
|
|
254
|
+
addInstanceResult(inputName, {
|
|
255
|
+
input: {
|
|
256
|
+
...output,
|
|
257
|
+
// keep explicit path from the edge while preserving already-resolved output path
|
|
258
|
+
path: input.path ?? output.path,
|
|
259
|
+
},
|
|
260
|
+
type: this.resolveOutputTypeForInput(
|
|
261
|
+
{
|
|
262
|
+
instanceId: input.instanceId,
|
|
263
|
+
output: input.output,
|
|
264
|
+
path: input.path ?? output.path,
|
|
265
|
+
},
|
|
266
|
+
componentInput.type,
|
|
267
|
+
),
|
|
268
|
+
})
|
|
223
269
|
}
|
|
224
270
|
} else {
|
|
225
271
|
// if the instance is not evaluated, we a forced to use the input as is
|
|
226
|
-
addInstanceResult(inputName, {
|
|
272
|
+
addInstanceResult(inputName, {
|
|
273
|
+
input,
|
|
274
|
+
type: this.resolveOutputTypeForInput(input, componentInput.type),
|
|
275
|
+
})
|
|
227
276
|
}
|
|
228
277
|
}
|
|
229
278
|
|
|
@@ -239,7 +288,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
239
288
|
for (const injectionInput of node.instance.injectionInputs ?? []) {
|
|
240
289
|
const { resolvedInputs } = getHubOutput(injectionInput)
|
|
241
290
|
for (const input of resolvedInputs) {
|
|
242
|
-
injectionInputs.set(
|
|
291
|
+
injectionInputs.set(inputKey(input.input), input)
|
|
243
292
|
}
|
|
244
293
|
}
|
|
245
294
|
|
|
@@ -250,7 +299,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
250
299
|
for (const hubInput of hubInputs) {
|
|
251
300
|
const { resolvedInputs } = getHubOutput(hubInput)
|
|
252
301
|
for (const input of resolvedInputs) {
|
|
253
|
-
allInputs.set(
|
|
302
|
+
allInputs.set(inputKey(input.input), input)
|
|
254
303
|
}
|
|
255
304
|
}
|
|
256
305
|
|
|
@@ -258,7 +307,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
258
307
|
if (input.type === componentInput.type) {
|
|
259
308
|
addInstanceInput(inputName, input.input)
|
|
260
309
|
|
|
261
|
-
const key =
|
|
310
|
+
const key = inputKey(input.input)
|
|
262
311
|
if (injectionInputs.has(key)) {
|
|
263
312
|
matchedInjectionInputs.set(key, input)
|
|
264
313
|
}
|
|
@@ -277,6 +326,7 @@ export class InputResolver extends GraphResolver<InputResolverNode, InputResolve
|
|
|
277
326
|
kind: "instance",
|
|
278
327
|
instance: node.instance,
|
|
279
328
|
component: node.component,
|
|
329
|
+
entities: node.entities,
|
|
280
330
|
resolvedInputs,
|
|
281
331
|
resolvedOutputs: node.instance.resolvedOutputs,
|
|
282
332
|
resolvedInjectionInputs: Array.from(injectionInputs.values()),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function stableJsonStringify(value: unknown): string {
|
|
2
|
+
if (value === null) {
|
|
3
|
+
return "null"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return `[${value.map(stableJsonStringify).join(",")}]`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
switch (typeof value) {
|
|
11
|
+
case "string":
|
|
12
|
+
return JSON.stringify(value)
|
|
13
|
+
case "number": {
|
|
14
|
+
if (!Number.isFinite(value)) {
|
|
15
|
+
throw new Error("Snapshot content contains a non-finite number")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.stringify(value)
|
|
19
|
+
}
|
|
20
|
+
case "boolean":
|
|
21
|
+
return value ? "true" : "false"
|
|
22
|
+
case "object": {
|
|
23
|
+
const record = value as Record<string, unknown>
|
|
24
|
+
const keys = Object.keys(record).sort()
|
|
25
|
+
const parts: string[] = []
|
|
26
|
+
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const item = record[key]
|
|
29
|
+
if (item === undefined) {
|
|
30
|
+
throw new Error("Snapshot content contains undefined")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parts.push(`${JSON.stringify(key)}:${stableJsonStringify(item)}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `{${parts.join(",")}}`
|
|
37
|
+
}
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Snapshot content contains non-JSON value of type "${typeof value}"`)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/terminal/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Logger } from "pino"
|
|
2
2
|
import type { ProjectUnlockService } from "../business"
|
|
3
|
+
import type { ObjectRefIndexService } from "../business/object-ref-index"
|
|
3
4
|
import type { DatabaseManager, Terminal, TerminalSession } from "../database"
|
|
4
5
|
import type { PubSubManager } from "../pubsub"
|
|
5
6
|
import type { TerminalSessionOutput } from "../shared/models/project/terminal"
|
|
@@ -35,6 +36,7 @@ export class TerminalManager {
|
|
|
35
36
|
private readonly database: DatabaseManager,
|
|
36
37
|
private readonly pubsubManager: PubSubManager,
|
|
37
38
|
private readonly projectUnlockService: ProjectUnlockService,
|
|
39
|
+
private readonly objectRefIndexService: ObjectRefIndexService,
|
|
38
40
|
private readonly logger: Logger,
|
|
39
41
|
) {
|
|
40
42
|
this.projectUnlockService.registerUnlockTask(
|
|
@@ -113,6 +115,8 @@ export class TerminalManager {
|
|
|
113
115
|
},
|
|
114
116
|
})
|
|
115
117
|
|
|
118
|
+
await this.objectRefIndexService.track(projectId, [session.id])
|
|
119
|
+
|
|
116
120
|
const output = toTerminalSessionOutput(terminal, session)
|
|
117
121
|
|
|
118
122
|
this.logger.info({ msg: "terminal session created", id: output.id })
|
|
@@ -319,6 +323,7 @@ export class TerminalManager {
|
|
|
319
323
|
database: DatabaseManager,
|
|
320
324
|
pubsubManager: PubSubManager,
|
|
321
325
|
projectUnlockService: ProjectUnlockService,
|
|
326
|
+
objectRefIndexService: ObjectRefIndexService,
|
|
322
327
|
logger: Logger,
|
|
323
328
|
): TerminalManager {
|
|
324
329
|
return new TerminalManager(
|
|
@@ -326,6 +331,7 @@ export class TerminalManager {
|
|
|
326
331
|
database,
|
|
327
332
|
pubsubManager,
|
|
328
333
|
projectUnlockService,
|
|
334
|
+
objectRefIndexService,
|
|
329
335
|
logger.child({ service: "TerminalManager" }),
|
|
330
336
|
)
|
|
331
337
|
}
|
package/src/terminal/run.sh.ts
CHANGED
|
@@ -23,10 +23,15 @@ for key in "\${filesKeys[@]}"; do
|
|
|
23
23
|
continue
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
# Handle embedded content
|
|
27
|
-
if [ "$contentType" = "embedded" ]; then
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
# Handle embedded and embedded-secret content
|
|
27
|
+
if [ "$contentType" = "embedded" ] || [ "$contentType" = "embedded-secret" ]; then
|
|
28
|
+
if [ "$contentType" = "embedded-secret" ]; then
|
|
29
|
+
content=$(jq -r ".files[\\"$key\\"].content.value.value" <<<"$data")
|
|
30
|
+
else
|
|
31
|
+
content=$(jq -r ".files[\\"$key\\"].content.value" <<<"$data")
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
isBinary=$(jq -r ".files[\\"$key\\"].content.isBinary // .files[\\"$key\\"].meta.isBinary // false" <<<"$data")
|
|
30
35
|
mode=$(jq -r ".files[\\"$key\\"].meta.mode // 0" <<<"$data")
|
|
31
36
|
|
|
32
37
|
mkdir -p "$(dirname "$key")"
|
package/src/worker/manager.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Logger } from "pino"
|
|
|
2
2
|
import type { ApiKeyService, ProjectUnlockService } from "../business"
|
|
3
3
|
import type { DatabaseManager, Worker, WorkerVersion } from "../database"
|
|
4
4
|
import type { PubSubManager } from "../pubsub"
|
|
5
|
+
import type { WorkerVersionStatus } from "../shared"
|
|
5
6
|
import type { WorkerBackend } from "./abstractions"
|
|
6
7
|
import { PassThrough } from "node:stream"
|
|
7
8
|
import { z } from "zod"
|
|
@@ -9,6 +10,9 @@ import { type AsyncBatcher, createAsyncBatcher } from "../shared"
|
|
|
9
10
|
|
|
10
11
|
export const workerManagerConfig = z.object({
|
|
11
12
|
HIGHSTATE_WORKER_API_PATH: z.string().default("/var/run/highstate.sock"),
|
|
13
|
+
HIGHSTATE_WORKER_START_MAX_ATTEMPTS: z.coerce.number().int().positive().default(5),
|
|
14
|
+
HIGHSTATE_WORKER_RESTART_BACKOFF_BASE_MS: z.coerce.number().int().nonnegative().default(1000),
|
|
15
|
+
HIGHSTATE_WORKER_RESTART_BACKOFF_MAX_MS: z.coerce.number().int().nonnegative().default(30000),
|
|
12
16
|
})
|
|
13
17
|
|
|
14
18
|
type RunningWorkerInfo = {
|
|
@@ -22,7 +26,40 @@ type RunningWorkerInfo = {
|
|
|
22
26
|
lineBuffer?: string
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
function getWorkerRestartBackoffMs(failedAttempts: number, baseMs: number, maxMs: number): number {
|
|
30
|
+
if (failedAttempts <= 0) {
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const exponent = Math.max(0, failedAttempts - 1)
|
|
35
|
+
const uncapped = baseMs * 2 ** exponent
|
|
36
|
+
|
|
37
|
+
return Math.min(maxMs, uncapped)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function waitForAbortableDelay(delayMs: number, signal: AbortSignal): Promise<boolean> {
|
|
41
|
+
if (delayMs <= 0) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (signal.aborted) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return await new Promise(resolve => {
|
|
50
|
+
const onAbort = () => {
|
|
51
|
+
clearTimeout(timer)
|
|
52
|
+
resolve(false)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
signal.removeEventListener("abort", onAbort)
|
|
57
|
+
resolve(true)
|
|
58
|
+
}, delayMs)
|
|
59
|
+
|
|
60
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
61
|
+
})
|
|
62
|
+
}
|
|
26
63
|
|
|
27
64
|
export class WorkerManager {
|
|
28
65
|
constructor(
|
|
@@ -50,6 +87,17 @@ export class WorkerManager {
|
|
|
50
87
|
|
|
51
88
|
private readonly runningWorkers = new Map<string, RunningWorkerInfo>()
|
|
52
89
|
|
|
90
|
+
private async publishWorkerVersionStatus(
|
|
91
|
+
projectId: string,
|
|
92
|
+
workerVersionId: string,
|
|
93
|
+
status: WorkerVersionStatus,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await this.pubsubManager.publish(["worker-version-status", projectId], {
|
|
96
|
+
workerVersionId,
|
|
97
|
+
status,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
53
101
|
private async startWorkerVersion(
|
|
54
102
|
projectId: string,
|
|
55
103
|
workerVersion: WorkerVersion & { worker: Worker },
|
|
@@ -70,6 +118,7 @@ export class WorkerManager {
|
|
|
70
118
|
// calculate attempt number
|
|
71
119
|
const previousFailedAttempts = existingInfo?.failedAttempts ?? 0
|
|
72
120
|
const failedAttempts = restart ? previousFailedAttempts + 1 : 0
|
|
121
|
+
const maxWorkerStartAttempts = this.config.HIGHSTATE_WORKER_START_MAX_ATTEMPTS
|
|
73
122
|
|
|
74
123
|
// check if max attempts reached
|
|
75
124
|
if (failedAttempts >= maxWorkerStartAttempts) {
|
|
@@ -95,6 +144,8 @@ export class WorkerManager {
|
|
|
95
144
|
},
|
|
96
145
|
})
|
|
97
146
|
|
|
147
|
+
await this.publishWorkerVersionStatus(projectId, workerVersion.id, "error")
|
|
148
|
+
|
|
98
149
|
// clean up from running workers map
|
|
99
150
|
if (existingInfo) {
|
|
100
151
|
existingInfo.logBatcher && void existingInfo.logBatcher.flush()
|
|
@@ -104,6 +155,45 @@ export class WorkerManager {
|
|
|
104
155
|
return
|
|
105
156
|
}
|
|
106
157
|
|
|
158
|
+
if (restart && existingInfo) {
|
|
159
|
+
const restartBackoffMs = getWorkerRestartBackoffMs(
|
|
160
|
+
failedAttempts,
|
|
161
|
+
this.config.HIGHSTATE_WORKER_RESTART_BACKOFF_BASE_MS,
|
|
162
|
+
this.config.HIGHSTATE_WORKER_RESTART_BACKOFF_MAX_MS,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (restartBackoffMs > 0) {
|
|
166
|
+
this.logger.debug(
|
|
167
|
+
{ projectId, workerVersionId: workerVersion.id, restartBackoffMs, failedAttempts },
|
|
168
|
+
`delaying worker version "%s" restart for %s ms after attempt %s`,
|
|
169
|
+
workerVersion.id,
|
|
170
|
+
restartBackoffMs,
|
|
171
|
+
failedAttempts,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await this.writeWorkerLog(
|
|
175
|
+
projectId,
|
|
176
|
+
workerVersion.id,
|
|
177
|
+
`worker restart scheduled in ${restartBackoffMs}ms`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const canContinue = await waitForAbortableDelay(
|
|
181
|
+
restartBackoffMs,
|
|
182
|
+
existingInfo.abortController.signal,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!canContinue) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const currentInfo = this.runningWorkers.get(workerVersion.id)
|
|
190
|
+
|
|
191
|
+
if (currentInfo !== existingInfo || currentInfo.abortController.signal.aborted) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
107
197
|
// regenerate API token
|
|
108
198
|
const apiKey = await this.apiKeyService.regenerateToken(projectId, workerVersion.apiKeyId)
|
|
109
199
|
const stdout = new PassThrough()
|
|
@@ -184,6 +274,8 @@ export class WorkerManager {
|
|
|
184
274
|
},
|
|
185
275
|
})
|
|
186
276
|
|
|
277
|
+
await this.publishWorkerVersionStatus(projectId, workerVersion.id, "starting")
|
|
278
|
+
|
|
187
279
|
void this.workerBackend
|
|
188
280
|
.run({
|
|
189
281
|
projectId,
|
|
@@ -272,6 +364,8 @@ export class WorkerManager {
|
|
|
272
364
|
},
|
|
273
365
|
})
|
|
274
366
|
|
|
367
|
+
await this.publishWorkerVersionStatus(projectId, workerVersionId, "running")
|
|
368
|
+
|
|
275
369
|
this.logger.debug(
|
|
276
370
|
{ projectId, workerVersionId },
|
|
277
371
|
`worker version "%s" is now running in project "%s"`,
|
|
@@ -311,6 +405,8 @@ export class WorkerManager {
|
|
|
311
405
|
},
|
|
312
406
|
})
|
|
313
407
|
|
|
408
|
+
await this.publishWorkerVersionStatus(info.projectId, workerVersionId, "stopped")
|
|
409
|
+
|
|
314
410
|
this.logger.debug(
|
|
315
411
|
{ projectId: info.projectId, workerVersionId },
|
|
316
412
|
`stopped worker version "%s"`,
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Exeteres
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/chunk-I7BWSAN6.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
2
|
-
var __typeError = (msg) => {
|
|
3
|
-
throw TypeError(msg);
|
|
4
|
-
};
|
|
5
|
-
var __using = (stack, value, async) => {
|
|
6
|
-
if (value != null) {
|
|
7
|
-
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
8
|
-
var dispose, inner;
|
|
9
|
-
if (async) dispose = value[__knownSymbol("asyncDispose")];
|
|
10
|
-
if (dispose === void 0) {
|
|
11
|
-
dispose = value[__knownSymbol("dispose")];
|
|
12
|
-
if (async) inner = dispose;
|
|
13
|
-
}
|
|
14
|
-
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
15
|
-
if (inner) dispose = function() {
|
|
16
|
-
try {
|
|
17
|
-
inner.call(this);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
return Promise.reject(e);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
stack.push([async, dispose, value]);
|
|
23
|
-
} else if (async) {
|
|
24
|
-
stack.push([async]);
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
};
|
|
28
|
-
var __callDispose = (stack, error, hasError) => {
|
|
29
|
-
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
30
|
-
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
31
|
-
};
|
|
32
|
-
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
33
|
-
var next = (it) => {
|
|
34
|
-
while (it = stack.pop()) {
|
|
35
|
-
try {
|
|
36
|
-
var result = it[1] && it[1].call(it[2]);
|
|
37
|
-
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
38
|
-
} catch (e) {
|
|
39
|
-
fail(e);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (hasError) throw error;
|
|
43
|
-
};
|
|
44
|
-
return next();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export { __callDispose, __using };
|
|
48
|
-
//# sourceMappingURL=chunk-I7BWSAN6.js.map
|
|
49
|
-
//# sourceMappingURL=chunk-I7BWSAN6.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-I7BWSAN6.js"}
|