@highstate/backend 0.9.15 → 0.9.18
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-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/chunk-Y7DXREVO.js +1745 -0
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +7227 -2501
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +76 -185
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -98
- package/dist/shared/index.js.map +1 -1
- package/package.json +31 -10
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +109 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +138 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +289 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +10 -0
- package/src/business/instance-lock.ts +125 -0
- package/src/business/instance-state.ts +434 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +260 -0
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +281 -0
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +398 -0
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +5 -1
- package/src/common/performance.ts +44 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +19 -11
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +100 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +24 -28
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +91 -111
- package/src/library/worker/evaluator.ts +36 -73
- package/src/library/worker/loader.lite.ts +54 -0
- package/src/library/worker/main.ts +15 -66
- package/src/library/worker/protocol.ts +6 -33
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/lock/index.ts +4 -0
- package/src/lock/manager.ts +97 -0
- package/src/lock/memory.ts +19 -0
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +118 -90
- package/src/orchestrator/operation-workset.ts +181 -93
- package/src/orchestrator/operation.ts +1021 -283
- package/src/project/abstractions.ts +27 -38
- package/src/project/evaluation.ts +248 -0
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +1 -2
- package/src/project/local.ts +107 -103
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/{workspace → pubsub}/index.ts +1 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +108 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +155 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +292 -372
- package/src/{common → runner}/pulumi.ts +89 -37
- package/src/services.ts +251 -40
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/{library.ts → models/backend/library.ts} +4 -4
- package/src/shared/models/backend/project.ts +82 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +68 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +65 -0
- package/src/shared/models/project/artifact.ts +83 -0
- package/src/shared/models/project/index.ts +13 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +98 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +449 -0
- package/src/shared/models/project/terminal.ts +98 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +38 -0
- package/src/shared/models/project/worker.ts +107 -0
- package/src/shared/resolvers/graph-resolver.ts +61 -18
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +47 -13
- package/src/shared/resolvers/registry.ts +3 -2
- package/src/shared/resolvers/state.ts +2 -2
- package/src/shared/resolvers/validation.ts +82 -25
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +4 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +199 -131
- package/src/state/encryption.ts +98 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +4 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +106 -0
- package/src/state/local/collection.ts +361 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +875 -18
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +507 -0
- package/src/state/test.ts +457 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +131 -79
- package/src/terminal/run.sh.ts +21 -11
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +167 -0
- package/dist/chunk-KTGKNSKM.js +0 -979
- package/dist/chunk-KTGKNSKM.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/project/manager.ts +0 -433
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/project.ts +0 -62
- package/src/shared/state.ts +0 -247
- package/src/shared/terminal.ts +0 -14
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
import type { InstanceModel } from "@highstate/contract"
|
|
2
|
-
import type {
|
|
3
|
-
import type { LibraryModel, ResolvedInstanceInput } from "../../shared"
|
|
2
|
+
import type { ResolvedInstanceInput } from "../../shared"
|
|
4
3
|
|
|
5
4
|
export type WorkerData = {
|
|
6
|
-
|
|
5
|
+
libraryModulePaths: string[]
|
|
7
6
|
logLevel?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type WorkerRequest =
|
|
11
|
-
| {
|
|
12
|
-
type: "evaluate-composite-instances"
|
|
13
|
-
allInstances: InstanceModel[]
|
|
14
|
-
resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>
|
|
15
|
-
instanceIds: string[]
|
|
16
|
-
}
|
|
17
|
-
| {
|
|
18
|
-
type: "evaluate-modules"
|
|
19
|
-
modulePaths: string[]
|
|
20
|
-
}
|
|
21
7
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
| {
|
|
28
|
-
type: "instance-evaluation-results"
|
|
29
|
-
results: InstanceEvaluationResult[]
|
|
30
|
-
}
|
|
31
|
-
| {
|
|
32
|
-
type: "module-evaluation-result"
|
|
33
|
-
result: ModuleEvaluationResult
|
|
34
|
-
}
|
|
35
|
-
| {
|
|
36
|
-
type: "error"
|
|
37
|
-
error: string
|
|
38
|
-
}
|
|
8
|
+
allInstances: InstanceModel[]
|
|
9
|
+
resolvedInputs: Record<string, Record<string, ResolvedInstanceInput[]>>
|
|
10
|
+
instanceIds: string[]
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LockBackend } from "./abstractions"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { MemoryLockBackend } from "./memory"
|
|
4
|
+
|
|
5
|
+
export const lockBackendConfig = z.object({
|
|
6
|
+
HIGHSTATE_LOCK_BACKEND_TYPE: z.enum(["memory"]).default("memory"),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export function createLockBackend(config: z.infer<typeof lockBackendConfig>): LockBackend {
|
|
10
|
+
switch (config.HIGHSTATE_LOCK_BACKEND_TYPE) {
|
|
11
|
+
case "memory": {
|
|
12
|
+
return MemoryLockBackend.create()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { LockBackend } from "./abstractions"
|
|
2
|
+
import { join } from "remeda"
|
|
3
|
+
|
|
4
|
+
export type LockKeyMap = {
|
|
5
|
+
/**
|
|
6
|
+
* Lock for the project names to ensure uniqueness.
|
|
7
|
+
*/
|
|
8
|
+
"project-name": [name: string]
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lock for instances and hubs of the project.
|
|
12
|
+
*/
|
|
13
|
+
"project-nodes": [projectId: string]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Locks for the evaluation of the project.
|
|
17
|
+
*/
|
|
18
|
+
"project-evaluation": [projectId: string]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lock for all instance states of the project.
|
|
22
|
+
*/
|
|
23
|
+
"project-instance-states": [projectId: string]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lock for a specific instance within a project.
|
|
27
|
+
*/
|
|
28
|
+
instance: [projectId: string, instanceId: string]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lock for a specific instance lock within a project.
|
|
32
|
+
*/
|
|
33
|
+
"instance-lock": [projectId: string, instanceId: string]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lock for a specific instance state within a project.
|
|
37
|
+
*/
|
|
38
|
+
"instance-state": [projectId: string, instanceId: string]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lock for a specific artifact within a project.
|
|
42
|
+
*/
|
|
43
|
+
artifact: [projectId: string, artifactId: string]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lock for a specific artifact hash within a project.
|
|
47
|
+
*/
|
|
48
|
+
"artifact-hash": [projectId: string, hash: string]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lock for a specific operation within a project.
|
|
52
|
+
*/
|
|
53
|
+
operation: [projectId: string, operationId: string]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lock for a specific api key within a project.
|
|
57
|
+
*/
|
|
58
|
+
"api-key": [projectId: string, key: string]
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Lock for a specific worker within a project.
|
|
62
|
+
*/
|
|
63
|
+
worker: [workerId: string]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Lock for a specific worker image within a project.
|
|
67
|
+
*/
|
|
68
|
+
"worker-image": [projectId: string, image: string]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type LockKey = Readonly<
|
|
72
|
+
{
|
|
73
|
+
[K in keyof LockKeyMap]: [type: K, ...LockKeyMap[K]]
|
|
74
|
+
}[keyof LockKeyMap]
|
|
75
|
+
>
|
|
76
|
+
|
|
77
|
+
export class LockManager {
|
|
78
|
+
constructor(private readonly lockBackend: LockBackend) {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Acquires locks for the given keys and executes the function.
|
|
82
|
+
*
|
|
83
|
+
* This method provides a clean interface for acquiring distributed locks across
|
|
84
|
+
* multiple keys and ensures they are properly released after execution.
|
|
85
|
+
*
|
|
86
|
+
* @param keys The keys to acquire locks for.
|
|
87
|
+
* @param fn The function to execute while holding the locks.
|
|
88
|
+
* @returns The result of the executed function.
|
|
89
|
+
*/
|
|
90
|
+
public async acquire<T>(keys: LockKey | LockKey[], fn: () => Promise<T> | T): Promise<T> {
|
|
91
|
+
if (typeof keys[0] === "string") {
|
|
92
|
+
return this.lockBackend.acquire([keys.join(":")], fn)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.lockBackend.acquire(keys.map(join(":")), fn)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LockBackend } from "./abstractions"
|
|
2
|
+
import type { BetterLock as BetterLockType } from "better-lock/dist/better_lock"
|
|
3
|
+
import { BetterLock } from "better-lock"
|
|
4
|
+
|
|
5
|
+
export class MemoryLockBackend implements LockBackend {
|
|
6
|
+
private readonly lock: BetterLockType
|
|
7
|
+
|
|
8
|
+
private constructor() {
|
|
9
|
+
this.lock = new BetterLock()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async acquire<T>(keys: string[], fn: () => Promise<T> | T): Promise<T> {
|
|
13
|
+
return this.lock.acquire(keys, fn)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static create(): LockBackend {
|
|
17
|
+
return new MemoryLockBackend()
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/lock/test.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Fixtures } from "@vitest/runner"
|
|
2
|
+
import type { LockKey } from "./manager"
|
|
3
|
+
import { linkTraceEntry, renderTraceEntry, type TestTracer, type TraceEntry } from "../common"
|
|
4
|
+
import { LockManager } from "./manager"
|
|
5
|
+
import { MemoryLockBackend } from "./memory"
|
|
6
|
+
|
|
7
|
+
class LockAcquireEntry implements TraceEntry {
|
|
8
|
+
releaseEntry?: TraceEntry
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
readonly id: number,
|
|
12
|
+
private readonly keys: string[],
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
render(): string {
|
|
16
|
+
return renderTraceEntry({
|
|
17
|
+
icon: "🔒",
|
|
18
|
+
title: this.releaseEntry
|
|
19
|
+
? `locked, released at ${linkTraceEntry(this.releaseEntry)}`
|
|
20
|
+
: "locked",
|
|
21
|
+
fields: {
|
|
22
|
+
keys: {
|
|
23
|
+
value: this.keys,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class LockReleaseEntry implements TraceEntry {
|
|
31
|
+
constructor(
|
|
32
|
+
readonly id: number,
|
|
33
|
+
private readonly keys: string[],
|
|
34
|
+
private readonly acquireEntry: TraceEntry,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
render(): string {
|
|
38
|
+
return renderTraceEntry({
|
|
39
|
+
icon: "🔓",
|
|
40
|
+
title: `released, locked at ${linkTraceEntry(this.acquireEntry)}`,
|
|
41
|
+
fields: {
|
|
42
|
+
keys: {
|
|
43
|
+
value: this.keys,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class TestLockManager {
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly lockManager: LockManager,
|
|
53
|
+
private readonly tracer: TestTracer,
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
async acquire<T>(keys: LockKey | LockKey[], fn: () => Promise<T> | T): Promise<T> {
|
|
57
|
+
const keyStrings = this.formatKeys(keys)
|
|
58
|
+
|
|
59
|
+
// add acquire trace entry
|
|
60
|
+
const acquireEntry = new LockAcquireEntry(this.tracer.nextEntryId(), keyStrings)
|
|
61
|
+
this.tracer.addEntry(acquireEntry)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.lockManager.acquire(keys, fn)
|
|
65
|
+
|
|
66
|
+
// add release trace entry
|
|
67
|
+
const releaseEntry = new LockReleaseEntry(this.tracer.nextEntryId(), keyStrings, acquireEntry)
|
|
68
|
+
this.tracer.addEntry(releaseEntry)
|
|
69
|
+
|
|
70
|
+
// link acquire entry to release entry
|
|
71
|
+
acquireEntry.releaseEntry = releaseEntry
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// add release trace entry even on error
|
|
76
|
+
const releaseEntry = new LockReleaseEntry(this.tracer.nextEntryId(), keyStrings, acquireEntry)
|
|
77
|
+
this.tracer.addEntry(releaseEntry)
|
|
78
|
+
|
|
79
|
+
// link acquire entry to release entry
|
|
80
|
+
acquireEntry.releaseEntry = releaseEntry
|
|
81
|
+
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private formatKeys(keys: LockKey | LockKey[]): string[] {
|
|
87
|
+
if (typeof keys[0] === "string") {
|
|
88
|
+
return [keys.join(":")]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return keys.map(key => key.join(":"))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const lockFixtures: Fixtures<
|
|
96
|
+
{
|
|
97
|
+
lockManager: LockManager
|
|
98
|
+
},
|
|
99
|
+
{ tracer: TestTracer }
|
|
100
|
+
> = {
|
|
101
|
+
lockManager: async ({ tracer }, use) => {
|
|
102
|
+
const backend = MemoryLockBackend.create()
|
|
103
|
+
const baseLockManager = new LockManager(backend)
|
|
104
|
+
const testLockManager = new TestLockManager(baseLockManager, tracer) as unknown as LockManager
|
|
105
|
+
|
|
106
|
+
await use(testLockManager)
|
|
107
|
+
},
|
|
108
|
+
}
|
|
@@ -1,76 +1,61 @@
|
|
|
1
1
|
import type { LibraryBackend } from "../library"
|
|
2
|
-
import type { ProjectBackend
|
|
2
|
+
import type { ProjectBackend } from "../project"
|
|
3
3
|
import type { RunnerBackend } from "../runner"
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
4
|
+
import type { StateManager } from "../state"
|
|
5
|
+
import type { ArtifactService } from "../business/artifact"
|
|
6
6
|
import type { Logger } from "pino"
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import type {
|
|
8
|
+
InstanceLockService,
|
|
9
|
+
OperationService,
|
|
10
|
+
SecretService,
|
|
11
|
+
ProjectUnlockService,
|
|
12
|
+
InstanceStateService,
|
|
13
|
+
WorkerService,
|
|
14
|
+
} from "../business"
|
|
15
|
+
import type { PubSubManager } from "../pubsub"
|
|
16
|
+
import { v7 as uuidv7 } from "uuid"
|
|
17
|
+
import {
|
|
18
|
+
isFinalOperationStatus,
|
|
19
|
+
type Operation,
|
|
20
|
+
type OperationRequest,
|
|
21
|
+
type Project,
|
|
22
|
+
} from "../shared"
|
|
10
23
|
import { RuntimeOperation } from "./operation"
|
|
11
24
|
|
|
12
|
-
export type OperationEvents = Record<string, [ProjectOperation]>
|
|
13
|
-
export type InstanceLogsEvents = Record<string, [string]>
|
|
14
|
-
|
|
15
25
|
export class OperationManager {
|
|
16
26
|
constructor(
|
|
17
27
|
private readonly runnerBackend: RunnerBackend,
|
|
18
|
-
private readonly stateBackend: StateBackend,
|
|
19
28
|
private readonly libraryBackend: LibraryBackend,
|
|
20
29
|
private readonly projectBackend: ProjectBackend,
|
|
21
|
-
private readonly
|
|
22
|
-
private readonly projectLockManager: ProjectLockManager,
|
|
30
|
+
private readonly artifactManager: ArtifactService,
|
|
23
31
|
private readonly stateManager: StateManager,
|
|
32
|
+
private readonly instanceLockService: InstanceLockService,
|
|
33
|
+
private readonly stateUnlockService: ProjectUnlockService,
|
|
34
|
+
private readonly operationService: OperationService,
|
|
35
|
+
private readonly secretService: SecretService,
|
|
36
|
+
private readonly instanceStateService: InstanceStateService,
|
|
37
|
+
private readonly pubsubManager: PubSubManager,
|
|
38
|
+
private readonly workerService: WorkerService,
|
|
24
39
|
private readonly logger: Logger,
|
|
25
|
-
) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Watches for all project operations in the project.
|
|
34
|
-
*
|
|
35
|
-
* @param projectId The project ID to watch.
|
|
36
|
-
* @param signal The signal to abort the operation.
|
|
37
|
-
*/
|
|
38
|
-
public async *watchOperations(
|
|
39
|
-
projectId: string,
|
|
40
|
-
signal?: AbortSignal,
|
|
41
|
-
): AsyncIterable<ProjectOperation> {
|
|
42
|
-
for await (const [operation] of on(this.operationEE, projectId, { signal })) {
|
|
43
|
-
yield operation
|
|
44
|
-
}
|
|
40
|
+
) {
|
|
41
|
+
this.stateUnlockService.registerUnlockTask(
|
|
42
|
+
//
|
|
43
|
+
"process-lost-operations",
|
|
44
|
+
projectId => this.processLostOperations(projectId),
|
|
45
|
+
)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
* Watches for logs of the instance in the operation.
|
|
49
|
-
*
|
|
50
|
-
* @param operationId The operation ID to watch.
|
|
51
|
-
* @param instanceId The instance ID to watch.
|
|
52
|
-
* @param signal The signal to abort the operation.
|
|
53
|
-
*/
|
|
54
|
-
async *watchInstanceLogs(
|
|
55
|
-
operationId: string,
|
|
56
|
-
instanceId: string,
|
|
57
|
-
signal?: AbortSignal,
|
|
58
|
-
): AsyncIterable<string> {
|
|
59
|
-
const eventKey = `${operationId}/${instanceId}`
|
|
60
|
-
for await (const [log] of on(this.instanceLogsEE, eventKey, { signal })) {
|
|
61
|
-
yield log
|
|
62
|
-
}
|
|
63
|
-
}
|
|
48
|
+
private readonly runtimeOperations = new Map<string, RuntimeOperation>()
|
|
64
49
|
|
|
65
50
|
/**
|
|
66
51
|
* Launches the project operation.
|
|
67
52
|
*
|
|
68
53
|
* @param request The operation request to launch.
|
|
69
54
|
*/
|
|
70
|
-
async launch(request:
|
|
71
|
-
const operation:
|
|
55
|
+
async launch(request: OperationRequest): Promise<Operation> {
|
|
56
|
+
const operation: Operation = {
|
|
72
57
|
id: uuidv7(),
|
|
73
|
-
|
|
58
|
+
meta: {},
|
|
74
59
|
type: request.type,
|
|
75
60
|
requestedInstanceIds: request.instanceIds,
|
|
76
61
|
instanceIdsToUpdate: [],
|
|
@@ -94,9 +79,14 @@ export class OperationManager {
|
|
|
94
79
|
|
|
95
80
|
this.logger.info({ operation }, "launching operation")
|
|
96
81
|
|
|
97
|
-
await this.
|
|
82
|
+
await this.operationService.updateOperation(request.projectId, operation)
|
|
98
83
|
|
|
99
|
-
this.
|
|
84
|
+
const project = await this.stateManager.getProjectRepository().get(request.projectId)
|
|
85
|
+
if (!project) {
|
|
86
|
+
throw new Error(`Project with ID "${request.projectId}" not found`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.startOperation(project, operation)
|
|
100
90
|
|
|
101
91
|
return operation
|
|
102
92
|
}
|
|
@@ -112,59 +102,97 @@ export class OperationManager {
|
|
|
112
102
|
}
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
105
|
+
cancelInstance(operationId: string, instanceId: string): void {
|
|
106
|
+
const runtimeOperation = this.runtimeOperations.get(operationId)
|
|
107
|
+
if (runtimeOperation) {
|
|
108
|
+
runtimeOperation.cancelInstance(instanceId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private startOperation(project: Project, operation: Operation): void {
|
|
116
113
|
const runtimeOperation = new RuntimeOperation(
|
|
114
|
+
project,
|
|
117
115
|
operation,
|
|
118
116
|
this.runnerBackend,
|
|
119
|
-
this.stateBackend,
|
|
120
117
|
this.libraryBackend,
|
|
121
118
|
this.projectBackend,
|
|
122
|
-
this.
|
|
123
|
-
this.projectLockManager.getLock(operation.projectId),
|
|
119
|
+
this.artifactManager,
|
|
124
120
|
this.stateManager,
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
121
|
+
this.instanceLockService,
|
|
122
|
+
this.operationService,
|
|
123
|
+
this.secretService,
|
|
124
|
+
this.instanceStateService,
|
|
125
|
+
this.pubsubManager,
|
|
126
|
+
this.workerService,
|
|
127
|
+
this.logger.child({ operationId: operation.id }),
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
this.runtimeOperations.set(operation.id, runtimeOperation)
|
|
131
131
|
void runtimeOperation.operateSafe().finally(() => this.runtimeOperations.delete(operation.id))
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
projectBackend: ProjectBackend,
|
|
139
|
-
secretBackend: SecretBackend,
|
|
140
|
-
projectLockManager: ProjectLockManager,
|
|
141
|
-
stateManager: StateManager,
|
|
142
|
-
logger: Logger,
|
|
143
|
-
) {
|
|
144
|
-
const operator = new OperationManager(
|
|
145
|
-
runnerBackend,
|
|
146
|
-
stateBackend,
|
|
147
|
-
libraryBackend,
|
|
148
|
-
projectBackend,
|
|
149
|
-
secretBackend,
|
|
150
|
-
projectLockManager,
|
|
151
|
-
stateManager,
|
|
152
|
-
logger.child({ service: "OperationManager" }),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
// relaunch all active operations
|
|
156
|
-
const activeOperations = await stateBackend.getActiveOperations()
|
|
134
|
+
private async processLostOperations(projectId: string): Promise<void> {
|
|
135
|
+
const activeOperations = await this.stateManager
|
|
136
|
+
.getActiveOperationIndexRepository(projectId)
|
|
137
|
+
.getAllItems()
|
|
157
138
|
|
|
158
139
|
for (const operation of activeOperations) {
|
|
159
|
-
|
|
140
|
+
if (isFinalOperationStatus(operation.status)) {
|
|
141
|
+
this.logger.warn(
|
|
142
|
+
{ projectId, operationId: operation.id },
|
|
143
|
+
"operation is in final state but still marked as active, removing from index",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await this.stateManager
|
|
147
|
+
.getActiveOperationIndexRepository(projectId)
|
|
148
|
+
.indexRepository.delete(operation.id)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
160
151
|
|
|
161
|
-
|
|
152
|
+
const errorMessagePrefix = operation.error ? `${operation.error}\n\n` : ""
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
operation.
|
|
165
|
-
|
|
154
|
+
operation.status = "failed"
|
|
155
|
+
operation.error = `${errorMessagePrefix}Operation was unexpectedly interrupted. Please restart it.`
|
|
156
|
+
|
|
157
|
+
await this.operationService.updateOperation(projectId, operation)
|
|
166
158
|
}
|
|
167
159
|
|
|
168
|
-
|
|
160
|
+
// unlock instances that were locked by lost operations
|
|
161
|
+
const activeOperationIds = new Set(activeOperations.map(op => op.id))
|
|
162
|
+
|
|
163
|
+
const allLocks = await this.stateManager.getInstanceLockRepository(projectId).getAllItems()
|
|
164
|
+
const lockIdsToRemvoe: string[] = []
|
|
165
|
+
|
|
166
|
+
// clean up unexpected operation locks
|
|
167
|
+
for (const lock of allLocks) {
|
|
168
|
+
if (lock.spec.type !== "operation") {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (activeOperationIds.has(lock.spec.operationId)) {
|
|
173
|
+
// remove locks for lost operation which is expected
|
|
174
|
+
lockIdsToRemvoe.push(lock.id)
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// unexpected operation lock found, also remove it
|
|
179
|
+
this.logger.warn(
|
|
180
|
+
{ projectId, lockId: lock.id },
|
|
181
|
+
`unexpected operation lock found for completed operation "${lock.spec.operationId}", removing lock`,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
lockIdsToRemvoe.push(lock.id)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (lockIdsToRemvoe.length > 0) {
|
|
188
|
+
await this.instanceLockService.unlockInstancesUnconditionally(projectId, lockIdsToRemvoe)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.logger.debug(
|
|
192
|
+
{ projectId, operationCount: activeOperations.length },
|
|
193
|
+
`processed %s lost operations for project "%s"`,
|
|
194
|
+
activeOperations.length,
|
|
195
|
+
projectId,
|
|
196
|
+
)
|
|
169
197
|
}
|
|
170
198
|
}
|