@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.
Files changed (187) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/chunk-Y7DXREVO.js +1745 -0
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +7227 -2501
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +76 -185
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -98
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +31 -10
  19. package/src/artifact/abstractions.ts +46 -0
  20. package/src/artifact/encryption.ts +109 -0
  21. package/src/artifact/factory.ts +36 -0
  22. package/src/artifact/index.ts +3 -0
  23. package/src/artifact/local.ts +138 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  27. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  28. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  40. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  41. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  42. package/src/business/api-key.ts +65 -0
  43. package/src/business/artifact.ts +289 -0
  44. package/src/business/backend-unlock.ts +10 -0
  45. package/src/business/index.ts +10 -0
  46. package/src/business/instance-lock.ts +125 -0
  47. package/src/business/instance-state.ts +434 -0
  48. package/src/business/operation.ts +251 -0
  49. package/src/business/project-unlock.ts +260 -0
  50. package/src/business/project.ts +299 -0
  51. package/src/business/secret.test.ts +178 -0
  52. package/src/business/secret.ts +281 -0
  53. package/src/business/worker.test.ts +614 -0
  54. package/src/business/worker.ts +398 -0
  55. package/src/common/clock.ts +18 -0
  56. package/src/common/index.ts +5 -1
  57. package/src/common/performance.ts +44 -0
  58. package/src/common/random.ts +68 -0
  59. package/src/common/test/index.ts +2 -0
  60. package/src/common/test/render.ts +98 -0
  61. package/src/common/test/tracer.ts +359 -0
  62. package/src/common/tree.ts +33 -0
  63. package/src/common/utils.ts +40 -1
  64. package/src/config.ts +19 -11
  65. package/src/hotstate/abstractions.ts +48 -0
  66. package/src/hotstate/factory.ts +17 -0
  67. package/src/{secret → hotstate}/index.ts +1 -0
  68. package/src/hotstate/manager.ts +192 -0
  69. package/src/hotstate/memory.ts +100 -0
  70. package/src/hotstate/validation.ts +100 -0
  71. package/src/index.ts +2 -1
  72. package/src/library/abstractions.ts +24 -28
  73. package/src/library/factory.ts +2 -2
  74. package/src/library/local.ts +91 -111
  75. package/src/library/worker/evaluator.ts +36 -73
  76. package/src/library/worker/loader.lite.ts +54 -0
  77. package/src/library/worker/main.ts +15 -66
  78. package/src/library/worker/protocol.ts +6 -33
  79. package/src/lock/abstractions.ts +6 -0
  80. package/src/lock/factory.ts +15 -0
  81. package/src/lock/index.ts +4 -0
  82. package/src/lock/manager.ts +97 -0
  83. package/src/lock/memory.ts +19 -0
  84. package/src/lock/test.ts +108 -0
  85. package/src/orchestrator/manager.ts +118 -90
  86. package/src/orchestrator/operation-workset.ts +181 -93
  87. package/src/orchestrator/operation.ts +1021 -283
  88. package/src/project/abstractions.ts +27 -38
  89. package/src/project/evaluation.ts +248 -0
  90. package/src/project/factory.ts +1 -1
  91. package/src/project/index.ts +1 -2
  92. package/src/project/local.ts +107 -103
  93. package/src/pubsub/abstractions.ts +13 -0
  94. package/src/pubsub/factory.ts +19 -0
  95. package/src/{workspace → pubsub}/index.ts +1 -0
  96. package/src/pubsub/local.ts +36 -0
  97. package/src/pubsub/manager.ts +108 -0
  98. package/src/pubsub/validation.ts +33 -0
  99. package/src/runner/abstractions.ts +155 -68
  100. package/src/runner/artifact-env.ts +160 -0
  101. package/src/runner/factory.ts +20 -5
  102. package/src/runner/force-abort.ts +117 -0
  103. package/src/runner/local.ts +292 -372
  104. package/src/{common → runner}/pulumi.ts +89 -37
  105. package/src/services.ts +251 -40
  106. package/src/shared/index.ts +3 -11
  107. package/src/shared/models/backend/index.ts +3 -0
  108. package/src/shared/{library.ts → models/backend/library.ts} +4 -4
  109. package/src/shared/models/backend/project.ts +82 -0
  110. package/src/shared/models/backend/unlock-method.ts +20 -0
  111. package/src/shared/models/base.ts +68 -0
  112. package/src/shared/models/errors.ts +5 -0
  113. package/src/shared/models/index.ts +4 -0
  114. package/src/shared/models/project/api-key.ts +65 -0
  115. package/src/shared/models/project/artifact.ts +83 -0
  116. package/src/shared/models/project/index.ts +13 -0
  117. package/src/shared/models/project/lock.ts +91 -0
  118. package/src/shared/models/project/model.ts +14 -0
  119. package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
  120. package/src/shared/models/project/page.ts +57 -0
  121. package/src/shared/models/project/secret.ts +98 -0
  122. package/src/shared/models/project/service-account.ts +22 -0
  123. package/src/shared/models/project/state.ts +449 -0
  124. package/src/shared/models/project/terminal.ts +98 -0
  125. package/src/shared/models/project/trigger.ts +56 -0
  126. package/src/shared/models/project/unlock-method.ts +38 -0
  127. package/src/shared/models/project/worker.ts +107 -0
  128. package/src/shared/resolvers/graph-resolver.ts +61 -18
  129. package/src/shared/resolvers/index.ts +5 -0
  130. package/src/shared/resolvers/input-hash.ts +53 -15
  131. package/src/shared/resolvers/input.ts +47 -13
  132. package/src/shared/resolvers/registry.ts +3 -2
  133. package/src/shared/resolvers/state.ts +2 -2
  134. package/src/shared/resolvers/validation.ts +82 -25
  135. package/src/shared/utils/args.ts +25 -0
  136. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  137. package/src/shared/utils/hash.ts +6 -0
  138. package/src/shared/utils/index.ts +4 -0
  139. package/src/shared/utils/promise-tracker.ts +23 -0
  140. package/src/state/abstractions.ts +199 -131
  141. package/src/state/encryption.ts +98 -0
  142. package/src/state/factory.ts +3 -5
  143. package/src/state/index.ts +4 -0
  144. package/src/state/keyring.ts +22 -0
  145. package/src/state/local/backend.ts +106 -0
  146. package/src/state/local/collection.ts +361 -0
  147. package/src/state/local/index.ts +2 -0
  148. package/src/state/manager.ts +875 -18
  149. package/src/state/memory/backend.ts +70 -0
  150. package/src/state/memory/collection.ts +270 -0
  151. package/src/state/memory/index.ts +2 -0
  152. package/src/state/repository/index.ts +2 -0
  153. package/src/state/repository/repository.index.ts +193 -0
  154. package/src/state/repository/repository.ts +507 -0
  155. package/src/state/test.ts +457 -0
  156. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  157. package/src/terminal/docker.ts +18 -14
  158. package/src/terminal/factory.ts +3 -3
  159. package/src/terminal/index.ts +1 -1
  160. package/src/terminal/manager.ts +131 -79
  161. package/src/terminal/run.sh.ts +21 -11
  162. package/src/unlock/abstractions.ts +49 -0
  163. package/src/unlock/index.ts +2 -0
  164. package/src/unlock/memory.ts +32 -0
  165. package/src/worker/abstractions.ts +42 -0
  166. package/src/worker/docker.ts +83 -0
  167. package/src/worker/factory.ts +20 -0
  168. package/src/worker/index.ts +3 -0
  169. package/src/worker/manager.ts +167 -0
  170. package/dist/chunk-KTGKNSKM.js +0 -979
  171. package/dist/chunk-KTGKNSKM.js.map +0 -1
  172. package/dist/chunk-WXDYCRTT.js +0 -234
  173. package/dist/chunk-WXDYCRTT.js.map +0 -1
  174. package/src/library/worker/loader.ts +0 -114
  175. package/src/preferences/shared.ts +0 -1
  176. package/src/project/lock.ts +0 -39
  177. package/src/project/manager.ts +0 -433
  178. package/src/secret/abstractions.ts +0 -59
  179. package/src/secret/factory.ts +0 -22
  180. package/src/secret/local.ts +0 -152
  181. package/src/shared/project.ts +0 -62
  182. package/src/shared/state.ts +0 -247
  183. package/src/shared/terminal.ts +0 -14
  184. package/src/state/local.ts +0 -612
  185. package/src/workspace/abstractions.ts +0 -41
  186. package/src/workspace/factory.ts +0 -14
  187. package/src/workspace/local.ts +0 -54
@@ -1,38 +1,11 @@
1
1
  import type { InstanceModel } from "@highstate/contract"
2
- import type { InstanceEvaluationResult, ModuleEvaluationResult } from "../abstractions"
3
- import type { LibraryModel, ResolvedInstanceInput } from "../../shared"
2
+ import type { ResolvedInstanceInput } from "../../shared"
4
3
 
5
4
  export type WorkerData = {
6
- modulePaths: string[]
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
- export type WorkerResponse =
23
- | {
24
- type: "library"
25
- library: LibraryModel
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,6 @@
1
+ export interface LockBackend {
2
+ /**
3
+ * Acquires locks for the given keys and executes the function.
4
+ */
5
+ acquire<T>(keys: string[], fn: () => Promise<T> | T): Promise<T>
6
+ }
@@ -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,4 @@
1
+ export * from "./abstractions"
2
+ export * from "./factory"
3
+ export * from "./manager"
4
+ export * from "./test"
@@ -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
+ }
@@ -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, ProjectLockManager } from "../project"
2
+ import type { ProjectBackend } from "../project"
3
3
  import type { RunnerBackend } from "../runner"
4
- import type { SecretBackend } from "../secret"
5
- import type { StateBackend, StateManager } from "../state"
4
+ import type { StateManager } from "../state"
5
+ import type { ArtifactService } from "../business/artifact"
6
6
  import type { Logger } from "pino"
7
- import { EventEmitter, on } from "node:events"
8
- import { uuidv7 } from "uuidv7"
9
- import { type ProjectOperation, type ProjectOperationRequest } from "../shared"
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 secretBackend: SecretBackend,
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
- private readonly operationEE = new EventEmitter<OperationEvents>()
28
- private readonly instanceLogsEE = new EventEmitter<InstanceLogsEvents>()
29
-
30
- private readonly runtimeOperations = new Map<string, RuntimeOperation>()
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: ProjectOperationRequest): Promise<ProjectOperation> {
71
- const operation: ProjectOperation = {
55
+ async launch(request: OperationRequest): Promise<Operation> {
56
+ const operation: Operation = {
72
57
  id: uuidv7(),
73
- projectId: request.projectId,
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.stateBackend.putOperation(operation)
82
+ await this.operationService.updateOperation(request.projectId, operation)
98
83
 
99
- this.startOperation(operation)
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
- private startOperation(operation: ProjectOperation): void {
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.secretBackend,
123
- this.projectLockManager.getLock(operation.projectId),
119
+ this.artifactManager,
124
120
  this.stateManager,
125
- this.operationEE,
126
- this.instanceLogsEE,
127
- this.logger.child({ service: "RuntimeOperation", operationId: operation.id }),
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
- static async create(
135
- runnerBackend: RunnerBackend,
136
- stateBackend: StateBackend,
137
- libraryBackend: LibraryBackend,
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
- logger.info({ msg: "relaunching operation", operationId: operation.id })
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
- // operator.startOperation(operation)
152
+ const errorMessagePrefix = operation.error ? `${operation.error}\n\n` : ""
162
153
 
163
- // cancel the operation for now
164
- operation.status = "cancelled"
165
- await stateBackend.putOperation(operation)
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
- return operator
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
  }