@highstate/backend 0.9.18 → 0.9.20

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 (331) hide show
  1. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  2. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  3. package/dist/chunk-RC6Q3XQQ.js +1547 -0
  4. package/dist/chunk-RC6Q3XQQ.js.map +1 -0
  5. package/dist/chunk-VB4YL327.js +139 -0
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +2 -1
  10. package/dist/index.js +7590 -7289
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +1 -1
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +35 -29
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +2 -2
  17. package/package.json +18 -9
  18. package/prisma/backend/_schema/layout.prisma +7 -0
  19. package/prisma/backend/_schema/library.prisma +17 -0
  20. package/prisma/backend/_schema/project.prisma +101 -0
  21. package/prisma/backend/_schema/pulumi.prisma +17 -0
  22. package/prisma/backend/postgresql/main.prisma +17 -0
  23. package/prisma/backend/sqlite/main.prisma +17 -0
  24. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  25. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  26. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  27. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  28. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  29. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  30. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  31. package/prisma/project/api-key.prisma +32 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +45 -0
  35. package/prisma/project/instance.prisma +157 -0
  36. package/prisma/project/layout.prisma +23 -0
  37. package/prisma/project/lock.prisma +18 -0
  38. package/prisma/project/main.prisma +17 -0
  39. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  40. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  41. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  42. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  43. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  45. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  46. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  47. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  48. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  49. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  50. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  51. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  52. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  53. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  54. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  55. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  56. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  59. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  60. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  61. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  62. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  64. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  65. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  66. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  67. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  68. package/prisma/project/migrations/migration_lock.toml +3 -0
  69. package/prisma/project/model.prisma +37 -0
  70. package/prisma/project/operation.prisma +148 -0
  71. package/prisma/project/page.prisma +49 -0
  72. package/prisma/project/secret.prisma +54 -0
  73. package/prisma/project/service-account.prisma +42 -0
  74. package/prisma/project/terminal.prisma +107 -0
  75. package/prisma/project/trigger.prisma +37 -0
  76. package/prisma/project/unlock-method.prisma +46 -0
  77. package/prisma/project/worker.prisma +169 -0
  78. package/src/artifact/abstractions.ts +13 -13
  79. package/src/artifact/encryption.ts +30 -54
  80. package/src/artifact/factory.ts +6 -9
  81. package/src/artifact/local.ts +33 -46
  82. package/src/business/api-key.ts +24 -36
  83. package/src/business/artifact.test.ts +978 -0
  84. package/src/business/artifact.ts +136 -216
  85. package/src/business/evaluation.ts +328 -0
  86. package/src/business/index.ts +5 -2
  87. package/src/business/instance-lock.test.ts +1060 -0
  88. package/src/business/instance-lock.ts +387 -78
  89. package/src/business/instance-state.test.ts +735 -0
  90. package/src/business/instance-state.ts +582 -337
  91. package/src/business/operation.test.ts +439 -0
  92. package/src/business/operation.ts +174 -208
  93. package/src/business/project-model.ts +258 -0
  94. package/src/business/project-unlock.ts +168 -126
  95. package/src/business/project.ts +287 -179
  96. package/src/business/secret.test.ts +469 -130
  97. package/src/business/secret.ts +177 -217
  98. package/src/business/settings.test.ts +695 -0
  99. package/src/business/settings.ts +855 -0
  100. package/src/business/terminal-session.ts +90 -0
  101. package/src/business/unit-extra.test.ts +539 -0
  102. package/src/business/unit-extra.ts +160 -0
  103. package/src/business/worker.test.ts +356 -579
  104. package/src/business/worker.ts +238 -339
  105. package/src/common/codebase.ts +65 -0
  106. package/src/common/index.ts +3 -5
  107. package/src/common/logger.ts +5 -0
  108. package/src/common/utils.ts +4 -3
  109. package/src/config.ts +10 -11
  110. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  111. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  112. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  113. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  114. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  115. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  116. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  117. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  119. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  120. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  121. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  122. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  123. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  124. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  125. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  126. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  127. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  128. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  129. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  130. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  132. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  133. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  134. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  135. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  136. package/src/database/_generated/project/client.ts +204 -0
  137. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  138. package/src/database/_generated/project/enums.ts +104 -0
  139. package/src/database/_generated/project/internal/class.ts +479 -0
  140. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  141. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  142. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  143. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  144. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  145. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  146. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  147. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  148. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  149. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  150. package/src/database/_generated/project/models/Operation.ts +1647 -0
  151. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  152. package/src/database/_generated/project/models/Page.ts +1838 -0
  153. package/src/database/_generated/project/models/Secret.ts +1692 -0
  154. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  155. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  156. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  157. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  158. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  159. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  160. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  161. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  162. package/src/database/_generated/project/models/Worker.ts +1459 -0
  163. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  164. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  165. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  166. package/src/database/_generated/project/models.ts +35 -0
  167. package/src/database/_generated/project/pjtg.ts +182 -0
  168. package/src/database/abstractions.ts +19 -0
  169. package/src/database/factory.ts +37 -0
  170. package/src/database/index.ts +6 -0
  171. package/src/database/local/backend.ts +134 -0
  172. package/src/database/local/index.ts +3 -0
  173. package/src/database/local/meta.ts +46 -0
  174. package/src/database/local/prisma.config.ts +25 -0
  175. package/src/database/local/project.ts +39 -0
  176. package/src/database/manager.ts +181 -0
  177. package/src/database/migrate.ts +35 -0
  178. package/src/database/prisma.ts +56 -0
  179. package/src/database/well-known.ts +38 -0
  180. package/src/index.ts +4 -4
  181. package/src/library/abstractions.ts +3 -5
  182. package/src/library/factory.ts +1 -1
  183. package/src/library/local.ts +81 -26
  184. package/src/library/package-resolution-worker.ts +1 -1
  185. package/src/library/worker/evaluator.ts +40 -23
  186. package/src/library/worker/loader.lite.ts +1 -1
  187. package/src/library/worker/main.ts +3 -10
  188. package/src/library/worker/protocol.ts +0 -1
  189. package/src/lock/index.ts +0 -1
  190. package/src/lock/manager.ts +0 -10
  191. package/src/orchestrator/manager.ts +190 -104
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +233 -578
  203. package/src/orchestrator/operation.ts +440 -948
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +29 -13
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +40 -41
  219. package/src/runner/artifact-env.ts +19 -8
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +74 -67
  223. package/src/runner/pulumi.ts +23 -63
  224. package/src/services.ts +181 -123
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +9 -1
  227. package/src/shared/models/backend/project.ts +43 -42
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -26
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -59
  236. package/src/shared/models/project/artifact.ts +16 -76
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +8 -7
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +19 -1
  241. package/src/shared/models/project/operation.ts +235 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -89
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -407
  246. package/src/shared/models/project/terminal.ts +75 -88
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +20 -26
  249. package/src/shared/models/project/worker.ts +89 -90
  250. package/src/shared/resolvers/graph-resolver.ts +21 -0
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +9 -2
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +7 -3
  257. package/src/shared/utils/index.ts +1 -2
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +5 -23
  266. package/src/unlock/memory.ts +9 -14
  267. package/src/worker/abstractions.ts +7 -4
  268. package/src/worker/docker.ts +14 -19
  269. package/src/worker/manager.ts +366 -97
  270. package/dist/chunk-NAAIDR4U.js +0 -8499
  271. package/dist/chunk-NAAIDR4U.js.map +0 -1
  272. package/dist/chunk-Y7DXREVO.js +0 -1745
  273. package/dist/chunk-Y7DXREVO.js.map +0 -1
  274. package/dist/magic-string.es-5ABAC4JN.js +0 -1292
  275. package/dist/magic-string.es-5ABAC4JN.js.map +0 -1
  276. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +0 -356
  277. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +0 -274
  278. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +0 -223
  279. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +0 -147
  280. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +0 -280
  281. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +0 -360
  282. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +0 -215
  283. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +0 -427
  284. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +0 -217
  285. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +0 -132
  286. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +0 -454
  287. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +0 -426
  288. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +0 -372
  289. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +0 -383
  290. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +0 -245
  291. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +0 -174
  292. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +0 -432
  293. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +0 -220
  294. package/src/business/backend-unlock.ts +0 -10
  295. package/src/common/clock.ts +0 -18
  296. package/src/common/performance.ts +0 -44
  297. package/src/common/random.ts +0 -68
  298. package/src/common/test/index.ts +0 -2
  299. package/src/common/test/render.ts +0 -98
  300. package/src/common/test/tracer.ts +0 -359
  301. package/src/hotstate/abstractions.ts +0 -48
  302. package/src/hotstate/factory.ts +0 -17
  303. package/src/hotstate/index.ts +0 -3
  304. package/src/hotstate/manager.ts +0 -192
  305. package/src/hotstate/memory.ts +0 -100
  306. package/src/hotstate/validation.ts +0 -100
  307. package/src/lock/test.ts +0 -108
  308. package/src/project/abstractions.ts +0 -78
  309. package/src/project/evaluation.ts +0 -248
  310. package/src/project/factory.ts +0 -11
  311. package/src/project/index.ts +0 -3
  312. package/src/project/local.ts +0 -417
  313. package/src/pubsub/local.ts +0 -36
  314. package/src/pubsub/validation.ts +0 -33
  315. package/src/shared/utils/args.ts +0 -25
  316. package/src/state/abstractions.ts +0 -289
  317. package/src/state/encryption.ts +0 -98
  318. package/src/state/factory.ts +0 -20
  319. package/src/state/index.ts +0 -7
  320. package/src/state/local/backend.ts +0 -106
  321. package/src/state/local/collection.ts +0 -361
  322. package/src/state/local/index.ts +0 -2
  323. package/src/state/manager.ts +0 -890
  324. package/src/state/memory/backend.ts +0 -70
  325. package/src/state/memory/collection.ts +0 -270
  326. package/src/state/memory/index.ts +0 -2
  327. package/src/state/repository/index.ts +0 -2
  328. package/src/state/repository/repository.index.ts +0 -193
  329. package/src/state/repository/repository.ts +0 -507
  330. package/src/state/test.ts +0 -457
  331. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -1,88 +1,71 @@
1
- import type { LibraryBackend } from "../library"
2
- import type { StateManager } from "../state"
3
- import type { ProjectBackend } from "../project"
4
- import type {
5
- RunnerArtifact,
6
- RunnerBackend,
7
- TypedUnitStateUpdate,
8
- UnitStateUpdate,
9
- } from "../runner"
10
1
  import type { Logger } from "pino"
2
+ import type { ArtifactService } from "../artifact"
11
3
  import type {
12
4
  InstanceLockService,
5
+ InstanceStatePatch,
13
6
  InstanceStateService,
14
7
  OperationService,
8
+ ProjectModelService,
15
9
  SecretService,
16
- WorkerService,
10
+ UnitExtraService,
17
11
  } from "../business"
18
- import type { PubSubManager } from "../pubsub"
19
- import { randomBytes } from "node:crypto"
20
- import { v7 as uuidv7 } from "uuid"
12
+ import type { Operation, OperationUpdateInput, Project } from "../database"
13
+ import type { LibraryBackend } from "../library"
14
+ import type { RunnerBackend, TypedUnitStateUpdate, UnitStateUpdate } from "../runner"
21
15
  import {
22
- isUnitModel,
23
- parseInstanceId,
24
- type ComponentModel,
16
+ type InstanceId,
25
17
  type InstanceModel,
26
- type UnitSecretModel,
18
+ parseInstanceId,
19
+ type TriggerInvocation,
20
+ type UnitConfig,
21
+ type VersionedName,
27
22
  } from "@highstate/contract"
28
- import { mapValues, unique } from "remeda"
29
- import { ArtifactService } from "../artifact"
23
+ import { createId } from "@paralleldrive/cuid2"
24
+ import { mapValues } from "remeda"
25
+ import { AbortError, errorToString, isAbortErrorLike, waitForAbort } from "../common"
30
26
  import {
31
27
  type InstanceState,
32
- type UnitPage,
33
- type Operation,
34
- type Page,
35
- type Trigger,
36
- type UnitTrigger,
37
- type UnitTerminal,
38
- type Terminal,
39
- type TerminalSpec,
40
- type TriggerInvocation,
28
+ isTransientInstanceOperationStatus,
29
+ type OperationPhase,
41
30
  PromiseTracker,
42
- type InstanceStatePatch,
43
- type PageBlock,
44
- type Project,
31
+ waitAll,
45
32
  } from "../shared"
46
- import {
47
- AbortError,
48
- errorToString,
49
- isAbortErrorLike,
50
- PerformanceLogger,
51
- stringToValue,
52
- valueToString,
53
- waitForAbort,
54
- } from "../common"
55
- import { OperationWorkset, type OperationPhase } from "./operation-workset"
33
+ import { OperationContext } from "./operation-context"
34
+ import { createOperationPlan } from "./operation-plan"
35
+ import { OperationWorkset } from "./operation-workset"
56
36
 
57
37
  export class RuntimeOperation {
58
- private readonly abortController = new AbortController()
59
- private readonly forceAbortController = new AbortController()
60
-
61
- private readonly instanceAbortControllers = new Map<string, AbortController>()
62
- private readonly instanceForceAbortControllers = new Map<string, AbortController>()
63
-
64
- private readonly instancePromiseMap = new Map<string, Promise<void>>()
38
+ private readonly instancePromiseMap = new Map<InstanceId, Promise<void>>()
65
39
  private readonly promiseTracker = new PromiseTracker()
66
40
 
67
41
  private workset!: OperationWorkset
42
+ private context!: OperationContext
43
+
44
+ private readonly unlockToken = createId()
68
45
 
69
46
  constructor(
70
47
  private readonly project: Project,
71
48
  private readonly operation: Operation,
72
49
  private readonly runnerBackend: RunnerBackend,
73
50
  private readonly libraryBackend: LibraryBackend,
74
- private readonly projectBackend: ProjectBackend,
75
51
  private readonly artifactService: ArtifactService,
76
- private readonly stateManager: StateManager,
77
52
  private readonly instanceLockService: InstanceLockService,
78
53
  private readonly operationService: OperationService,
79
54
  private readonly secretService: SecretService,
80
55
  private readonly instanceStateService: InstanceStateService,
81
- private readonly pubsubManager: PubSubManager,
82
- private readonly workerService: WorkerService,
56
+ private readonly projectModelService: ProjectModelService,
57
+ private readonly unitExtraService: UnitExtraService,
83
58
  private readonly logger: Logger,
84
59
  ) {}
85
60
 
61
+ cancel(): void {
62
+ this.workset.cancel()
63
+ }
64
+
65
+ cancelInstance(instanceId: InstanceId): void {
66
+ this.workset.cancelInstance(instanceId)
67
+ }
68
+
86
69
  async operateSafe(): Promise<void> {
87
70
  try {
88
71
  await this.operate()
@@ -92,21 +75,20 @@ export class RuntimeOperation {
92
75
  } catch (error) {
93
76
  if (isAbortErrorLike(error)) {
94
77
  this.logger.info("the operation was cancelled")
95
- this.operation.status = "cancelled"
96
- this.operation.error = null
97
78
 
98
- await this.operationService.updateOperation(this.project.id, this.operation)
79
+ await this.updateOperation({ status: "cancelled" })
99
80
  return
100
81
  }
101
82
 
102
83
  this.logger.error({ error }, "an error occurred while running the operation")
103
84
 
104
- this.operation.status = "failed"
105
- this.operation.error = errorToString(error)
106
-
107
- await this.operationService.updateOperation(this.project.id, this.operation)
85
+ await this.updateOperation({ status: "failed" })
86
+ await this.writeOperationLog(errorToString(error))
108
87
  } finally {
109
88
  try {
89
+ this.promiseTracker.track(this.ensureInstancesUnlocked())
90
+ this.promiseTracker.track(this.ensureOperationStatesFinalized())
91
+
110
92
  // ensure that all promises are resolved even if the operation failed
111
93
  await this.promiseTracker.waitForAll()
112
94
  } catch (error) {
@@ -121,261 +103,181 @@ export class RuntimeOperation {
121
103
  private async operate(): Promise<void> {
122
104
  this.logger.info("starting operation")
123
105
 
124
- // create the workset for the operation
125
- this.workset = await OperationWorkset.load(
126
- this.project,
127
- this.operation,
128
- this.projectBackend,
106
+ // 1. create the context for the operation
107
+ this.context = await OperationContext.load(
108
+ this.project.id,
129
109
  this.libraryBackend,
130
- this.stateManager,
131
- this.instanceLockService,
132
110
  this.instanceStateService,
111
+ this.projectModelService,
133
112
  this.logger,
134
- this.abortController.signal,
135
113
  )
136
114
 
137
- // start the process to lock more instances when they become available
138
- const lockAbortSignal = new AbortController()
139
- this.promiseTracker.track(this.tryLockMore(lockAbortSignal.signal))
140
-
141
- // try to lock as many instances as possible
142
- await this.workset.tryLock()
115
+ // 2. create the plan for the operation (or use provided one)
116
+ let plan: OperationPhase[]
117
+ if (this.operation.phases) {
118
+ plan = this.operation.phases
119
+ } else {
120
+ plan = createOperationPlan(
121
+ this.context,
122
+ this.operation.type,
123
+ this.operation.requestedInstanceIds,
124
+ this.operation.options,
125
+ )
143
126
 
144
- if (this.workset.instanceIdsToLockIds.size === 0) {
145
- this.logger.debug("successfully locked all instances for the operation before starting")
146
- lockAbortSignal.abort()
127
+ // persist the generated plan
128
+ await this.updateOperation({ phases: plan })
147
129
  }
148
130
 
149
- // setup abort controllers for the instances
150
- for (const instanceId of this.workset.getAllAffectedInstanceIds()) {
151
- this.setupInstanceAbortControllers(instanceId)
152
- }
131
+ // 3. create the workset to track the state of the operation
132
+ this.workset = new OperationWorkset(
133
+ this.project,
134
+ this.operation.id,
135
+ plan,
136
+ this.context,
137
+ this.instanceStateService,
138
+ )
153
139
 
154
- try {
155
- // run the operation
156
- await this.processOperation()
157
- } finally {
158
- lockAbortSignal.abort()
140
+ // 4. create operation states in datbase for all affected instances in all phases
141
+ await this.workset.setupOperationStates()
159
142
 
160
- if (this.operation.type === "preview") {
161
- // stream initial states for preview operations
162
- await this.workset.restoreAffectedInitialStates()
163
- }
164
- }
165
- }
143
+ // 5. setup abort controllers for all affected instances in all phases
144
+ this.workset.setupAbortControllersForAllInstances()
166
145
 
167
- private setupInstanceAbortControllers(instanceId: string): void {
168
- const abortController = new AbortController()
169
- this.abortController.signal.addEventListener("abort", () => abortController.abort())
170
- this.instanceAbortControllers.set(instanceId, abortController)
146
+ // 6. progressively lock instances and launch them as they get locked
147
+ this.launchLockAcquisitionSequence()
171
148
 
172
- const forceAbortController = new AbortController()
173
- this.forceAbortController.signal.addEventListener("abort", () => forceAbortController.abort())
174
- this.instanceForceAbortControllers.set(instanceId, forceAbortController)
149
+ // 7. run the operation
150
+ await this.processOperation()
175
151
  }
176
152
 
177
- private async tryLockMore(signal: AbortSignal): Promise<void> {
178
- const eventStream = this.pubsubManager.subscribe(["instance-lock", this.project.id], signal)
179
-
180
- for await (const event of eventStream) {
181
- if (event.type !== "unlocked") {
182
- continue
183
- }
184
-
185
- const instanceIdsToLock = event.instanceIds.filter(
186
- //
187
- instanceId => this.workset.instanceIdsToLockIds.has(instanceId),
188
- )
189
-
190
- if (instanceIdsToLock.length === 0) {
191
- try {
192
- await this.workset.tryLock(instanceIdsToLock)
193
- } catch (error) {
194
- this.logger.error({ error }, "failed to lock more instances during operation")
195
- }
196
- }
197
-
198
- if (this.workset.instanceIdsToLockIds.size === 0) {
199
- // no more instances to lock, stop listening for events
200
- this.logger.debug("successfully locked all instances for the operation")
201
- break
202
- }
203
- }
153
+ private async updateOperation(patch: OperationUpdateInput): Promise<void> {
154
+ Object.assign(this.operation, patch)
155
+ await this.operationService.updateOperation(this.project.id, this.operation.id, patch)
204
156
  }
205
157
 
206
- private async processOperation(): Promise<void> {
207
- this.operation.instanceIdsToUpdate = this.workset.operation.instanceIdsToUpdate
208
- this.operation.instanceIdsToDestroy = this.workset.operation.instanceIdsToDestroy
158
+ private async writeOperationLog(message: string): Promise<void> {
159
+ this.promiseTracker.track(
160
+ this.operationService.appendLog(this.project.id, this.operation.id, null, message),
161
+ )
162
+ }
209
163
 
210
- this.logger.info(
211
- "operation started with %s instances to update and %s to destroy",
212
- this.operation.instanceIdsToUpdate.length,
213
- this.operation.instanceIdsToDestroy.length,
164
+ private launchLockAcquisitionSequence(): void {
165
+ this.promiseTracker.track(
166
+ this.instanceLockService.lockInstances(
167
+ this.project.id,
168
+ Array.from(this.workset.allAffectedStateIds),
169
+ {
170
+ title: "Operation Lock",
171
+ description: `The instance is locked for the ${this.operation.type} operation "${this.operation.id}".`,
172
+ icon: "mdi:cog-sync",
173
+ },
174
+ async (_tx, newlyLockedStateIds) => {
175
+ // trigger all newly locked instances to start their processing
176
+ for (const stateId of newlyLockedStateIds) {
177
+ this.workset.markInstanceLocked(stateId)
178
+ }
179
+ },
180
+ true, // allow partial locks to allow independent free branches to run
181
+ this.workset.abortController.signal,
182
+ 60_000, // wait up to 60 seconds for unlock events before retrying
183
+ this.unlockToken,
184
+ ),
214
185
  )
186
+ }
215
187
 
216
- const phases = this.getOperationPhases()
188
+ private async processOperation(): Promise<void> {
217
189
  const errors: string[] = []
218
- let hasAbortError = false
219
190
 
220
- for (const phase of phases) {
221
- this.workset.currentPhase = phase
191
+ while (this.workset.hasRemainingPhases()) {
192
+ this.workset.nextPhase()
193
+ this.instancePromiseMap.clear()
222
194
 
223
- const promises: Promise<void>[] = []
224
- for (const instanceId of this.workset.getAffectedInstanceIds()) {
225
- const parentId = this.workset.getParentId(instanceId)
226
- if (parentId && this.workset.isAffected(parentId)) {
227
- // do not call the operation for child instances of affected composites,
228
- // they will be called by their parent instance
229
- continue
230
- }
195
+ // lauch all instances in this phase
196
+ for (const instanceId of this.workset.phaseAffectedInstanceIds) {
197
+ const instance = this.context.getInstance(instanceId)
198
+ const state = this.context.getState(instanceId)
199
+ const promise = this.getInstancePromiseForOperation(instance, state)
231
200
 
232
- promises.push(this.getInstancePromiseForOperation(instanceId))
201
+ this.instancePromiseMap.set(instanceId, promise)
202
+ this.promiseTracker.track(promise)
233
203
  }
234
204
 
235
- this.logger.info(`all operations for phase "%s" started`, phase)
236
- this.operation.status = "running"
237
- await this.operationService.updateOperation(this.project.id, this.operation)
238
-
239
- const phaseResults = await Promise.allSettled(promises)
240
- for (const result of phaseResults) {
241
- if (result.status !== "rejected") {
242
- continue
243
- }
244
-
245
- if (isAbortErrorLike(result.reason)) {
246
- hasAbortError = true
247
- } else {
248
- errors.push(errorToString(result.reason))
249
- }
250
- }
205
+ // wait for all instances in this phase to complete
206
+ await this.promiseTracker.waitForAll()
251
207
 
252
- this.logger.info(`all operations for phase "%s" completed`, phase)
208
+ this.logger.info(`phase "%s" completed`, this.workset.currentPhase)
253
209
  }
254
210
 
255
211
  if (errors.length > 0) {
256
212
  this.operation.status = "failed"
257
- // TODO: map errors to instances inside operation
258
- this.operation.error = errors.join("\n\n")
259
- } else if (hasAbortError) {
260
- this.operation.status = "cancelled"
261
- this.operation.error = null
213
+
214
+ this.operationService.appendLog(
215
+ this.project.id,
216
+ this.operation.id,
217
+ null,
218
+ `Operation failed with the following errors:\n${errors.join("\n")}`,
219
+ )
262
220
  } else {
263
221
  this.operation.status = "completed"
264
- this.operation.error = null
265
222
  }
266
223
 
267
- await this.operationService.updateOperation(this.project.id, this.operation)
224
+ await this.operationService.markOperationFinished(
225
+ this.project.id,
226
+ this.operation.id,
227
+ this.operation.status,
228
+ )
268
229
 
269
230
  this.logger.info("operation completed")
270
231
  }
271
232
 
272
- cancelInstance(instanceId: string): void {
273
- const abortController = this.instanceAbortControllers.get(instanceId)
274
- if (!abortController) {
275
- throw new Error(`No abort controller found for instance "${instanceId}"`)
276
- }
277
-
278
- if (!abortController.signal.aborted) {
279
- this.logger.info(`cancelling operation for instance "${instanceId}"`)
280
- abortController.abort()
281
-
282
- // just the UX feature to indicate that we are trying to cancel the operation
283
- this.promiseTracker.track(
284
- this.workset.patchState({
285
- id: instanceId,
286
- operationStatus: {
287
- status: "cancelling",
288
- },
289
- }),
290
- )
291
-
292
- return
293
- }
294
-
295
- // then try to force cancel the operation
296
- const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
297
- if (!forceAbortController) {
298
- throw new Error(`No force abort controller found for instance "${instanceId}"`)
299
- }
300
-
301
- if (!forceAbortController.signal.aborted) {
302
- this.logger.info(`force cancelling operation for instance "${instanceId}"`)
303
- forceAbortController.abort()
304
- return
305
- }
306
-
307
- this.logger.warn(`operation for instance "${instanceId}" is already force cancelled`)
308
- }
309
-
310
- cancel(): void {
311
- if (!this.abortController.signal.aborted) {
312
- this.logger.info("cancelling the operation")
313
- this.abortController.abort()
314
- return
315
- }
316
-
317
- // then try to force cancel the operation
318
- if (!this.forceAbortController.signal.aborted) {
319
- this.logger.info("force cancelling the operation")
320
- this.forceAbortController.abort()
321
- return
322
- }
323
-
324
- this.logger.warn("the operation is already cancelled or force cancelled")
325
- }
326
-
327
- private getInstancePromiseForOperation(instanceId: string): Promise<void> {
328
- const [instanceType] = parseInstanceId(instanceId)
329
- const component = this.workset.library.components[instanceType]
330
-
331
- if (isUnitModel(component)) {
332
- return this.getUnitPromise(instanceId)
233
+ private getInstancePromiseForOperation(
234
+ instance: InstanceModel,
235
+ state: InstanceState,
236
+ ): Promise<void> {
237
+ if (instance.kind === "unit") {
238
+ return this.getUnitPromise(instance, state)
333
239
  }
334
240
 
335
- return this.getCompositePromise(instanceId)
336
- }
337
-
338
- private getOperationPhases(): OperationPhase[] {
339
- switch (this.operation.type) {
340
- case "update": {
341
- if (this.operation.instanceIdsToDestroy.length > 0) {
342
- return ["destroy", "update"]
343
- }
344
-
345
- return ["update"]
346
- }
347
- case "preview":
348
- return ["update"]
349
- case "recreate":
350
- return ["destroy", "update"]
351
- case "destroy":
352
- return ["destroy"]
353
- case "refresh":
354
- return ["refresh"]
355
- }
241
+ return this.getCompositePromise(instance)
356
242
  }
357
243
 
358
- private async getUnitPromise(instanceId: string): Promise<void> {
244
+ private getUnitPromise(instance: InstanceModel, state: InstanceState): Promise<void> {
359
245
  switch (this.workset.currentPhase) {
360
246
  case "update": {
361
- return this.updateUnit(instanceId)
247
+ return this.updateUnit(instance, state)
362
248
  }
363
249
  case "destroy": {
364
- return this.destroyUnit(instanceId)
250
+ return this.destroyUnit(instance, state)
365
251
  }
366
252
  case "refresh": {
367
- return this.refreshUnit(instanceId)
253
+ return this.refreshUnit(instance, state)
368
254
  }
369
255
  }
370
256
  }
371
257
 
372
- private getCompositePromise(instanceId: string): Promise<void> {
373
- return this.getInstancePromise(instanceId, async logger => {
374
- const instance = this.workset.getInstance(instanceId)
258
+ private getCompositePromise(instance: InstanceModel): Promise<void> {
259
+ return this.getInstancePromise(instance.id, async logger => {
260
+ let instanceState: InstanceStatePatch | undefined
375
261
 
376
- await this.setInitialOperationStatus(instance)
262
+ // set parent ID on update operation
263
+ if (this.workset.currentPhase === "update") {
264
+ if (instance.parentId) {
265
+ const parentState = this.context.getState(instance.parentId)
266
+ instanceState = { parentId: parentState.id }
267
+ } else {
268
+ instanceState = { parentId: null }
269
+ }
270
+ }
271
+
272
+ await this.workset.updateState(instance.id, {
273
+ operationState: {
274
+ status: this.workset.getTransientStatusByOperationPhase(),
275
+ startedAt: new Date(),
276
+ },
277
+ instanceState,
278
+ })
377
279
 
378
- const children = this.workset.getAffectedCompositeChildren(instanceId)
280
+ const children = this.workset.getAffectedCompositeChildren(instance.id)
379
281
  const childPromises: Promise<void>[] = []
380
282
 
381
283
  if (children.length) {
@@ -385,65 +287,37 @@ export class RuntimeOperation {
385
287
  }
386
288
 
387
289
  for (const child of children) {
388
- if (!this.operation.options.forceUpdateChildren && !this.workset.isAffected(child.id)) {
389
- // skip children that are not affected by the operation
390
- continue
391
- }
290
+ logger.debug(`waiting for child "%s"`, child)
392
291
 
393
- logger.debug(`waiting for child "%s"`, child.id)
394
- childPromises.push(this.getInstancePromiseForOperation(child.id))
395
- }
396
-
397
- try {
398
- await Promise.all(childPromises)
292
+ const instance = this.context.getInstance(child)
293
+ const state = this.context.getState(child)
294
+ const promise = this.getInstancePromiseForOperation(instance, state)
399
295
 
400
- if (children.length > 0) {
401
- logger.debug("all children completed")
402
- }
296
+ childPromises.push(promise)
297
+ }
403
298
 
404
- if (this.operation.type === "destroy") {
405
- await this.clearOperationState(instanceId)
406
- return
407
- }
299
+ await waitAll(childPromises)
408
300
 
409
- const { inputHash, dependencyOutputHash } =
410
- await this.workset.getUpToDateInputHashOutput(instance)
301
+ logger.debug("all children completed")
411
302
 
412
- await this.workset.patchState({
413
- id: instanceId,
414
- operationStatus: {
415
- status: "created",
416
- inputHash,
417
- dependencyOutputHash,
303
+ // update the instance and operation state after all children are completed in last phase
304
+ if (this.workset.isLastPhaseForInstance(instance.id)) {
305
+ await this.workset.updateState(instance.id, {
306
+ operationState: {
307
+ status: this.workset.getStableStatusByOperationPhase(),
308
+ finishedAt: new Date(),
418
309
  },
419
- })
420
- } catch (error) {
421
- if (isAbortErrorLike(error)) {
422
- await this.workset.restoreInitialState(instanceId)
423
- return
424
- }
425
-
426
- await this.workset.patchState({
427
- id: instanceId,
428
- operationStatus: {
429
- status: "error",
430
- message: errorToString(error),
310
+ instanceState: {
311
+ status: this.workset.getNextStableInstanceStatus(instance.id),
312
+ message: null,
431
313
  },
432
314
  })
433
315
  }
434
316
  })
435
317
  }
436
318
 
437
- private updateUnit(instanceId: string): Promise<void> {
438
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
439
- const instance = this.workset.getInstance(instanceId)
440
- const component = this.workset.library.components[instance.type]
441
- const perfLogger = new PerformanceLogger(logger)
442
- perfLogger.log("starting update promise for instance")
443
-
444
- await this.setInitialOperationStatus(instance)
445
- perfLogger.log("initial operation status set")
446
-
319
+ private updateUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
320
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
447
321
  await Promise.race([
448
322
  this.updateUnitDependencies(instance.id, logger),
449
323
 
@@ -451,91 +325,81 @@ export class RuntimeOperation {
451
325
  waitForAbort(signal),
452
326
  ])
453
327
 
454
- if (!signal.aborted) {
455
- perfLogger.log("dependencies updated")
456
- }
457
-
458
328
  signal.throwIfAborted()
459
329
 
460
330
  logger.info("updating unit")
461
331
 
462
- await this.workset.patchState({
463
- id: instance.id,
464
- operationStatus: {
332
+ await this.workset.updateState(instance.id, {
333
+ operationState: {
465
334
  status: "updating",
466
- currentResourceCount: 0,
467
- totalResourceCount: 0,
335
+ startedAt: new Date(),
468
336
  },
469
337
  })
470
338
 
471
- perfLogger.log("set 'updating' operation status")
472
339
  signal.throwIfAborted()
473
340
 
474
- const secrets = await this.secretService.getInstanceSecretValues(this.project, instance.id)
341
+ const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
475
342
 
476
- perfLogger.log("secrets loaded")
477
343
  signal.throwIfAborted()
478
344
 
479
- const config = this.prepareUnitConfig(instance, component)
480
- perfLogger.log("unit config prepared")
345
+ const config = this.prepareUnitConfig(instance, secrets)
481
346
 
482
- // extract artifact hashes from dependencies based on instance inputs
483
- const artifactHashes = this.collectArtifactIdsForInstance(instance)
484
- const artifacts = await this.stateManager
485
- .getArtifactHashIndexRepository(this.project.id)
486
- .getManyItems(artifactHashes)
347
+ // collect artifacts authorized for this instance
348
+ const artifactIds = this.collectArtifactIdsForInstance(instance)
349
+ const artifacts = await this.artifactService.getArtifactsByIds(this.project.id, artifactIds)
487
350
 
488
- logger.debug({ count: artifactHashes.length }, "artifact hashes collected from dependencies")
489
- perfLogger.log("artifact hashes collected")
351
+ logger.debug({ count: artifactIds.length }, "artifact ids collected from dependencies")
490
352
 
491
353
  await this.runnerBackend[this.operation.type === "preview" ? "preview" : "update"]({
492
354
  projectId: this.project.id,
493
355
  libraryId: this.project.libraryId,
356
+ stateId: state.id,
494
357
  instanceType: instance.type,
495
358
  instanceName: instance.name,
496
359
  config,
497
360
  refresh: this.operation.options.refresh,
498
- secrets: mapValues(secrets, value => valueToString(value)),
361
+ secrets,
499
362
  artifacts,
500
363
  signal,
501
364
  forceSignal,
365
+ debug: this.operation.options.debug,
502
366
  })
503
367
 
504
- perfLogger.log("unit update requested")
505
-
506
- await this.watchStateStream(instance.type, instance.name, logger)
507
- perfLogger.log("unit update completed")
368
+ await this.watchStateStream(state, instance.type, instance.name, logger)
508
369
  logger.info("unit updated")
509
370
  })
510
371
  }
511
372
 
512
- private async updateUnitDependencies(instanceId: string, logger: Logger): Promise<void> {
373
+ private async updateUnitDependencies(instanceId: InstanceId, logger: Logger): Promise<void> {
513
374
  try {
514
- const dependencies = this.getInstanceDependencyIds(instanceId)
375
+ const dependencies = this.context.getDependencies(instanceId)
515
376
  const dependencyPromises: Promise<void>[] = []
516
377
 
517
- for (const dependencyId of dependencies) {
518
- if (!this.operation.instanceIdsToUpdate.includes(dependencyId)) {
378
+ for (const dependency of dependencies) {
379
+ if (!this.workset.phaseAffectedInstanceIds.has(dependency.id)) {
519
380
  // skip dependencies that are not affected by the operation
520
381
  continue
521
382
  }
522
383
 
523
- logger.debug(`waiting for dependency "${dependencyId}"`)
524
- dependencyPromises.push(this.getInstancePromiseForOperation(dependencyId))
384
+ const state = this.context.getState(dependency.id)
385
+
386
+ logger.debug(`waiting for dependency "%s"`, dependency.id)
387
+ dependencyPromises.push(this.getInstancePromiseForOperation(dependency, state))
525
388
  }
526
389
 
527
- await Promise.all(dependencyPromises)
390
+ await waitAll(dependencyPromises)
528
391
 
529
392
  if (dependencies.length > 0) {
530
393
  logger.info("all dependencies completed")
531
394
  }
532
- } catch {
395
+ } catch (error) {
533
396
  // abort the instance if any dependency fails
534
- throw new AbortError()
397
+ throw new AbortError("One of the dependencies failed", { cause: error })
535
398
  }
536
399
  }
537
400
 
538
401
  private async processBeforeDestroyTriggers(
402
+ instance: InstanceModel,
539
403
  state: InstanceState,
540
404
  logger: Logger,
541
405
  signal: AbortSignal,
@@ -546,15 +410,7 @@ export class RuntimeOperation {
546
410
  return
547
411
  }
548
412
 
549
- const instance = this.workset.getInstance(state.id)
550
- const component = this.workset.library.components[instance.type]
551
-
552
- // fetch triggers from state backend by their IDs
553
- const triggerIds = Object.values(state.extra?.triggerIds ?? {})
554
-
555
- const allTriggers = await this.stateManager
556
- .getTriggerRepository(this.project.id)
557
- .getManyItems(triggerIds)
413
+ const allTriggers = await this.unitExtraService.getInstanceTriggers(this.project.id, state.id)
558
414
 
559
415
  const beforeDestroyTriggers = allTriggers.filter(
560
416
  trigger => trigger.spec.type === "before-destroy",
@@ -564,95 +420,73 @@ export class RuntimeOperation {
564
420
  return
565
421
  }
566
422
 
567
- // get trigger names from state mapping by finding the key for each trigger ID
568
- const invokedTriggers = beforeDestroyTriggers.map(trigger => {
569
- const triggerName = Object.keys(state?.extra?.triggerIds ?? {}).find(
570
- name => state?.extra?.triggerIds?.[name] === trigger.id,
571
- )
572
-
573
- return { name: triggerName || "unknown" }
574
- })
423
+ const invokedTriggers = beforeDestroyTriggers.map(trigger => ({
424
+ name: trigger.name,
425
+ }))
575
426
 
576
- await this.workset.patchState({
577
- id: instance.id,
578
- operationStatus: {
579
- status: "processing-triggers",
580
- },
427
+ await this.workset.updateState(instance.id, {
428
+ operationState: { status: "processing_triggers" },
581
429
  })
582
430
 
583
431
  logger.info("updating unit to process before-destroy triggers...")
584
432
 
585
- const secrets = await this.secretService.getInstanceSecretValues(this.project, instance.id)
433
+ const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
586
434
 
587
435
  await this.runnerBackend.update({
588
436
  projectId: this.project.id,
437
+ stateId: state.id,
589
438
  libraryId: this.project.libraryId,
590
439
  instanceType: instance.type,
591
440
  instanceName: instance.name,
592
- config: this.prepareUnitConfig(instance, component, invokedTriggers),
441
+ config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
593
442
  refresh: this.operation.options.refresh,
594
- secrets: mapValues(secrets, value => valueToString(value)),
443
+ secrets,
595
444
  signal,
596
445
  forceSignal,
446
+ debug: this.operation.options.debug,
597
447
  })
598
448
 
599
449
  logger.debug("unit update requested")
600
450
 
601
- await this.watchStateStream(instance.type, instance.name, logger)
451
+ await this.watchStateStream(state, instance.type, instance.name, logger)
602
452
  logger.debug("before-destroy triggers processed")
603
453
  }
604
454
 
605
- private async destroyUnit(instanceId: string): Promise<void> {
606
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
607
- await this.workset.patchState({
608
- id: instanceId,
609
- operationStatus: {
610
- status: "pending",
611
- },
612
- })
613
-
614
- signal.throwIfAborted()
615
-
616
- const state = this.workset.getState(instanceId)
617
- if (!state) {
618
- logger.warn("state not found for unit, but destroy was requested")
619
- return
620
- }
621
-
455
+ private async destroyUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
456
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
622
457
  const dependentPromises: Promise<void>[] = []
623
- const dependents = this.workset.getDependentStates(state.id)
458
+ const dependents = this.context.getDependentStates(instance.id)
624
459
 
625
460
  for (const dependent of dependents) {
626
- if (
627
- !this.operation.options.destroyDependentInstances &&
628
- !this.operation.instanceIdsToDestroy.includes(dependent.id)
629
- ) {
461
+ if (!this.workset.phaseAffectedInstanceIds.has(dependent.instanceId)) {
630
462
  // skip dependents that are not affected by the operation
631
463
  continue
632
464
  }
633
465
 
634
- dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
466
+ const instance = this.context.getInstance(dependent.instanceId)
467
+ dependentPromises.push(this.getInstancePromiseForOperation(instance, dependent))
635
468
  }
636
469
 
637
- await Promise.all(dependentPromises)
470
+ await waitAll(dependentPromises)
638
471
  signal.throwIfAborted()
639
472
 
640
- await this.processBeforeDestroyTriggers(state, logger, signal, forceSignal)
473
+ await this.processBeforeDestroyTriggers(instance, state, logger, signal, forceSignal)
641
474
  signal.throwIfAborted()
642
475
 
643
476
  logger.info("destroying unit...")
644
477
 
645
- await this.workset.patchState({
646
- id: instanceId,
647
- operationStatus: {
478
+ await this.workset.updateState(instance.id, {
479
+ operationState: {
648
480
  status: "destroying",
481
+ startedAt: new Date(),
649
482
  },
650
483
  })
651
484
 
652
- const [type, name] = parseInstanceId(instanceId)
485
+ const [type, name] = parseInstanceId(instance.id)
653
486
 
654
487
  await this.runnerBackend.destroy({
655
488
  projectId: this.project.id,
489
+ stateId: state.id,
656
490
  libraryId: this.project.libraryId,
657
491
  instanceType: type,
658
492
  instanceName: name,
@@ -661,89 +495,76 @@ export class RuntimeOperation {
661
495
  forceSignal,
662
496
  deleteUnreachable: this.operation.options.deleteUnreachableResources,
663
497
  forceDeleteState: this.operation.options.forceDeleteState,
498
+ debug: this.operation.options.debug,
664
499
  })
665
500
 
666
501
  logger.debug("destroy request sent")
667
502
 
668
- await this.watchStateStream(type, name, logger)
503
+ await this.watchStateStream(state, type, name, logger)
669
504
 
670
505
  // clean up artifacts after successful destruction
671
- try {
672
- const artifactIds = unique(Object.values(state.extra?.exportedArtifactIds ?? {}).flat())
673
-
674
- await this.artifactService.removeUsages(
675
- //
676
- this.project.id,
677
- artifactIds,
678
- [{ type: "instance", instanceId: state.id }],
679
- )
680
- } catch (error) {
681
- logger.warn({ error }, "failed to cleanup artifacts for destroyed instance")
682
- }
506
+ await this.artifactService.clearInstanceArtifactReferences(this.project.id, instance.id)
683
507
 
684
508
  logger.info("unit destroyed")
685
509
  })
686
510
  }
687
511
 
688
- private async refreshUnit(instanceId: string) {
689
- return this.getInstancePromise(instanceId, async (logger, signal, forceSignal) => {
690
- await this.workset.patchState({
691
- id: instanceId,
692
- operationStatus: {
512
+ private async refreshUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
513
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
514
+ await this.workset.updateState(instance.id, {
515
+ operationState: {
693
516
  status: "refreshing",
694
- currentResourceCount: 0,
695
- totalResourceCount: 0,
517
+ startedAt: new Date(),
696
518
  },
697
519
  })
698
520
 
699
521
  logger.info("refreshing unit...")
700
522
 
701
- const [type, name] = parseInstanceId(instanceId)
523
+ const [type, name] = parseInstanceId(instance.id)
702
524
 
703
525
  await this.runnerBackend.refresh({
704
526
  projectId: this.project.id,
527
+ stateId: state.id,
705
528
  libraryId: this.project.libraryId,
706
529
  instanceType: type,
707
530
  instanceName: name,
708
531
  signal,
709
532
  forceSignal,
533
+ debug: this.operation.options.debug,
710
534
  })
711
535
 
712
536
  logger.debug("unit refresh requested")
713
537
 
714
- await this.watchStateStream(type, name, logger)
538
+ await this.watchStateStream(state, type, name, logger)
715
539
  logger.info("unit refreshed")
716
540
  })
717
541
  }
718
542
 
719
543
  private async watchStateStream(
720
- instanceType: string,
544
+ state: InstanceState,
545
+ instanceType: VersionedName,
721
546
  instanceName: string,
722
547
  logger: Logger,
723
548
  ): Promise<void> {
724
549
  const stream = this.runnerBackend.watch({
725
550
  projectId: this.project.id,
551
+ stateId: state.id,
726
552
  libraryId: this.project.libraryId,
727
553
  instanceType,
728
554
  instanceName,
555
+ debug: this.operation.options.debug,
729
556
  })
730
557
 
731
558
  let update: UnitStateUpdate | undefined
732
559
 
733
560
  for await (update of stream) {
734
561
  try {
735
- await this.handleUnitStateUpdate(update)
562
+ await this.handleUnitStateUpdate(update, state)
736
563
  } catch (error) {
737
564
  logger.error({ error }, "failed to handle unit state update")
738
565
  }
739
566
 
740
567
  if (update.type === "error") {
741
- if (isAbortErrorLike(update.message)) {
742
- // abort the unit if the returned error contains some abort-like pattern
743
- // generally, this might not be safe, but for now, we assume that
744
- throw new AbortError()
745
- }
746
-
747
568
  // rethrow the error to stop the execution of dependent units
748
569
  throw new Error(`An error occurred while processing the unit "${update.unitId}"`, {
749
570
  cause: update.message,
@@ -762,204 +583,155 @@ export class RuntimeOperation {
762
583
 
763
584
  private prepareUnitConfig(
764
585
  instance: InstanceModel,
765
- component: ComponentModel,
586
+ secrets: Record<string, unknown>,
766
587
  invokedTriggers: TriggerInvocation[] = [],
767
- ): Record<string, string> {
768
- const config: Record<string, string> = {}
588
+ ): UnitConfig {
589
+ const resolvedInputs = this.context.getResolvedInputs(instance.id)
769
590
 
770
- for (const key of Object.keys(component.args)) {
771
- // explicitly set empty values to remove old values in the config
772
- const value = instance.args?.[key]
773
- config[key] = value ? valueToString(value) : ""
591
+ return {
592
+ instanceId: instance.id,
593
+ args: instance.args ?? {},
594
+ inputs: mapValues(resolvedInputs ?? {}, input => input.map(value => value.input)),
595
+ invokedTriggers,
596
+ secretNames: Object.keys(secrets),
597
+ stateIdMap: this.context.getInstanceIdToStateIdMap(),
774
598
  }
775
-
776
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
777
-
778
- for (const key of Object.keys(component.inputs)) {
779
- const inputs = instanceInputs[key] ?? []
780
-
781
- config[`input.${key}`] = JSON.stringify(inputs.map(input => input.input))
782
- }
783
-
784
- config["$invokedTriggers"] = JSON.stringify(invokedTriggers)
785
-
786
- return config
787
599
  }
788
600
 
789
- private async handleUnitStateUpdate(update: UnitStateUpdate): Promise<void> {
601
+ private async handleUnitStateUpdate(
602
+ update: UnitStateUpdate,
603
+ state: InstanceState,
604
+ ): Promise<void> {
790
605
  switch (update.type) {
791
606
  case "message":
792
- this.handleUnitMessage(update)
607
+ this.handleUnitMessage(update, state)
793
608
  return
794
609
  case "progress":
795
610
  await this.handleUnitProgress(update)
796
611
  return
797
612
  case "error":
798
- await this.handleUnitError(update)
613
+ await this.handleUnitError(update, state)
799
614
  return
800
615
  case "completion":
801
- await this.handleUnitCompletion(update)
616
+ await this.handleUnitCompletion(update, state)
802
617
  return
803
618
  }
804
619
  }
805
620
 
806
- private handleUnitMessage(update: TypedUnitStateUpdate<"message">): void {
807
- // collect all instances in the hierarchy that should receive this log
808
- const instanceIdsInHierarchy: string[] = []
809
- let instance: InstanceModel | null = this.workset.getInstance(update.unitId)
810
-
621
+ private handleUnitMessage(update: TypedUnitStateUpdate<"message">, state: InstanceState): void {
622
+ // append logs to the instance and all its parents
811
623
  for (;;) {
812
- instanceIdsInHierarchy.push(instance.id)
624
+ this.promiseTracker.track(
625
+ this.operationService.appendLog(
626
+ this.project.id,
627
+ this.operation.id,
628
+ state.id,
629
+ update.message,
630
+ ),
631
+ )
813
632
 
814
- if (!instance.parentId) {
633
+ if (!state.parentInstanceId) {
815
634
  break
816
635
  }
817
636
 
818
- instance = this.workset.getInstance(instance.parentId)
637
+ state = this.context.getState(state.parentInstanceId)
819
638
  }
820
-
821
- // persist the log for all instances in the hierarchy
822
- // TODO: batch
823
- this.promiseTracker.track(
824
- this.operationService.appendLogs(this.project.id, this.operation.id, instanceIdsInHierarchy, [
825
- update.message,
826
- ]),
827
- )
828
- return
829
639
  }
830
640
 
831
641
  private async handleUnitProgress(update: TypedUnitStateUpdate<"progress">): Promise<void> {
832
- const patch: InstanceStatePatch = { id: update.unitId }
833
- patch.operationStatus = {}
834
-
835
- if (update.currentResourceCount !== undefined) {
836
- patch.operationStatus.currentResourceCount = update.currentResourceCount
837
- }
838
-
839
- if (update.totalResourceCount !== undefined) {
840
- patch.operationStatus.totalResourceCount = update.totalResourceCount
841
- }
842
-
843
- await this.workset.patchState(patch)
642
+ await this.workset.updateState(update.unitId, {
643
+ operationState: {
644
+ currentResourceCount: update.currentResourceCount,
645
+ totalResourceCount: update.totalResourceCount,
646
+ },
647
+ })
844
648
  }
845
649
 
846
- private async handleUnitError(update: TypedUnitStateUpdate<"error">): Promise<void> {
847
- if (isAbortErrorLike(update.message)) {
848
- return
849
- }
850
-
851
- const patch: InstanceStatePatch = {
852
- id: update.unitId,
853
- operationStatus: {
854
- status: "error",
650
+ private async handleUnitError(
651
+ update: TypedUnitStateUpdate<"error">,
652
+ state: InstanceState,
653
+ ): Promise<void> {
654
+ await this.workset.updateState(update.unitId, {
655
+ instanceState: {
656
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
657
+ status: state.status === "deployed" ? "deployed" : "failed",
855
658
  message: update.message,
856
659
  },
857
- }
858
-
859
- await this.workset.patchState(patch)
660
+ operationState: {
661
+ status: isAbortErrorLike(update.message) ? "cancelled" : "failed",
662
+ finishedAt: new Date(),
663
+ },
664
+ })
860
665
  }
861
666
 
862
- private async handleUnitCompletion(update: TypedUnitStateUpdate<"completion">): Promise<void> {
863
- if (this.workset.currentPhase === "destroy") {
864
- // if the destroy operation was completed, clear the state from all operation-related fields
865
- await this.clearOperationState(update.unitId)
866
- return
867
- }
868
-
869
- const state = this.workset.getState(update.unitId)
870
- if (!state) {
871
- throw new Error(
872
- `Cannot handle completion for unit "${update.unitId}" because its state is not found.`,
873
- )
874
- }
875
-
876
- const instance = this.workset.getInstance(update.unitId)
667
+ private async handleUnitCompletion(
668
+ update: TypedUnitStateUpdate<"completion">,
669
+ state: InstanceState,
670
+ ): Promise<void> {
671
+ const instance = this.context.getInstance(update.unitId)
877
672
 
878
- // patch the state with output hash first to recalculate the input hash
879
- if (update.outputHash) {
880
- await this.workset.patchState({
881
- id: update.unitId,
882
- operationStatus: { outputHash: update.outputHash },
883
- })
673
+ const data: InstanceStatePatch = {
674
+ status: this.workset.getNextStableInstanceStatus(instance.id),
675
+ message: update.message,
676
+ statusFields: update.statusFields ?? null,
884
677
  }
885
678
 
886
- const { inputHash, dependencyOutputHash } =
887
- await this.workset.getUpToDateInputHashOutput(instance)
888
-
889
- const patch: InstanceStatePatch = {
890
- id: update.unitId,
891
- operationStatus: {
892
- status: "created",
893
- message:
894
- update.message ?? "The operation on this instance has been completed successfully.",
895
- outputHash: update.outputHash ?? undefined,
896
- inputHash,
897
- dependencyOutputHash,
898
- },
899
- extra: {
900
- statusFields: update.statusFields,
901
-
902
- terminalIds: update.terminals
903
- ? this.processTerminals(update.unitId, update.terminals, state.extra?.terminalIds)
904
- : null,
679
+ if (update.operationType !== "destroy") {
680
+ // давайте еще больше усложним и без того сложную штуку
681
+ // set output hash before calculating input hash to capture up-to-date output hash for dependencies
682
+ state.outputHash = update.outputHash ?? null
905
683
 
906
- pageIds: update.pages
907
- ? this.processPages(update.unitId, update.pages, state.extra?.pageIds)
908
- : null,
684
+ // recalculate the input and output hashes for the instance
685
+ const { inputHash, dependencyOutputHash } =
686
+ await this.context.getUpToDateInputHashOutput(instance)
909
687
 
910
- triggerIds: update.triggers
911
- ? this.processTriggers(update.unitId, update.triggers, state.extra?.triggerIds)
912
- : null,
688
+ data.inputHash = inputHash
689
+ data.dependencyOutputHash = dependencyOutputHash
690
+ data.outputHash = update.outputHash
913
691
 
914
- workerRegistrationIds: update.workers
915
- ? await this.workerService.updateUnitRegistrations(
916
- this.project.id,
917
- update.unitId,
918
- state.extra?.workerRegistrationIds ?? {},
919
- update.workers,
920
- )
921
- : null,
922
-
923
- exportedArtifactIds: update.exportedArtifacts
924
- ? this.processArtifacts(
925
- update.unitId,
926
- update.exportedArtifacts,
927
- state.extra?.exportedArtifactIds,
928
- )
929
- : null,
930
-
931
- exportedSecretIds: update.exportedSecrets
932
- ? this.processExportedSecrets(update.unitId, update.exportedSecrets)
933
- : null,
692
+ // also update the parent ID
693
+ if (instance.parentId) {
694
+ const parentState = this.context.getState(instance.parentId)
695
+ data.parentId = parentState.id
696
+ } else {
697
+ data.parentId = null
698
+ }
699
+ } else {
700
+ data.message = null
701
+ data.inputHash = null
702
+ data.dependencyOutputHash = null
703
+ data.outputHash = null
704
+ data.parentId = null
705
+ }
706
+
707
+ // update the operation state
708
+ await this.workset.updateState(instance.id, {
709
+ // TODO: honestly, it is not correct
710
+ // may be we should track operation phases separately
711
+ // or introduce status like "destroyed-before-recreation" (quite ugly though)
712
+ operationState: {
713
+ status: this.workset.getStableStatusByOperationPhase(),
714
+ finishedAt: new Date(),
934
715
  },
935
- }
936
-
937
- if (update.secrets) {
938
- const instance = this.workset.getInstance(update.unitId)
939
716
 
940
- this.promiseTracker.track(
941
- this.updateInstanceSecrets(
942
- instance,
943
- mapValues(update.secrets, value => stringToValue(value)),
944
- ),
945
- )
946
- }
947
-
948
- await this.workset.patchState(patch)
949
- }
950
-
951
- private async clearOperationState(instanceId: string): Promise<void> {
952
- const patch: InstanceStatePatch = {
953
- id: instanceId,
954
- operationStatus: null,
955
- extra: null,
956
- }
957
-
958
- await this.workset.patchState(patch)
717
+ // do not write instance state for non-last phases of the instance
718
+ instanceState: this.workset.isLastPhaseForInstance(instance.id) ? data : undefined,
719
+
720
+ // also do not write unit extra data for non-last phases of the instance
721
+ unitExtra: this.workset.isLastPhaseForInstance(instance.id)
722
+ ? {
723
+ pages: update.pages ?? [],
724
+ terminals: update.terminals ?? [],
725
+ triggers: update.triggers ?? [],
726
+ workers: update.workers ?? [],
727
+ secrets: update.secrets ?? {},
728
+ }
729
+ : undefined,
730
+ })
959
731
  }
960
732
 
961
733
  private getInstancePromise(
962
- instanceId: string,
734
+ instanceId: InstanceId,
963
735
  fn: (logger: Logger, signal: AbortSignal, forceSignal: AbortSignal) => Promise<void>,
964
736
  ): Promise<void> {
965
737
  let instancePromise = this.instancePromiseMap.get(instanceId)
@@ -967,30 +739,54 @@ export class RuntimeOperation {
967
739
  return instancePromise
968
740
  }
969
741
 
970
- const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
971
- const abortController = this.instanceAbortControllers.get(instanceId)
972
- const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
742
+ // "pending" -> "running" if at least one instance is running
743
+ if (this.operation.status === "pending") {
744
+ this.operation.status = "running"
745
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
746
+ }
747
+
748
+ const state = this.context.getState(instanceId)
973
749
 
974
- if (!abortController || !forceAbortController) {
750
+ const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
751
+ const abortControllerPair = this.workset.instanceAbortControllers.get(instanceId)
752
+ if (!abortControllerPair) {
975
753
  throw new Error(`Abort controllers for instance "${instanceId}" are not initialized`)
976
754
  }
977
755
 
756
+ const { abortController, forceAbortController } = abortControllerPair
757
+
978
758
  instancePromise = this.workset
979
- .ensureInstanceLocked(instanceId, abortController.signal)
759
+ .waitForInstanceLock(state.id, abortController.signal)
980
760
  .then(() => fn(logger, abortController.signal, forceAbortController.signal))
981
761
  .catch(error => {
982
- if (isAbortErrorLike(error)) {
983
- // if the operation was aborted, restore the initial state of the instance
984
- this.promiseTracker.track(this.workset.restoreInitialState(instanceId))
985
-
986
- throw error
987
- }
988
-
989
762
  if (this.operation.status !== "failing") {
990
763
  // report the failing status of the operation
991
764
  this.operation.status = "failing"
765
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
766
+ }
767
+
768
+ if (isTransientInstanceOperationStatus(state.lastOperationState?.status)) {
769
+ // if the underlying method did not correctly update the instance status, do it here
992
770
  this.promiseTracker.track(
993
- this.operationService.updateOperation(this.project.id, this.operation),
771
+ this.workset.updateState(instanceId, {
772
+ operationState: {
773
+ status: isAbortErrorLike(error) ? "cancelled" : "failed",
774
+ finishedAt: new Date(),
775
+ },
776
+ instanceState: {
777
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
778
+ status: state.status === "deployed" ? "deployed" : "failed",
779
+ },
780
+ }),
781
+ )
782
+
783
+ this.promiseTracker.track(
784
+ this.operationService.appendLog(
785
+ this.project.id,
786
+ this.operation.id,
787
+ state.id,
788
+ errorToString(error),
789
+ ),
994
790
  )
995
791
  }
996
792
 
@@ -998,48 +794,67 @@ export class RuntimeOperation {
998
794
  throw error
999
795
  })
1000
796
  .finally(() => {
797
+ if (!this.workset.isLastPhaseForInstance(instanceId)) {
798
+ // do not finalize the instance if it has more phases to run
799
+ return
800
+ }
801
+
1001
802
  this.instancePromiseMap.delete(instanceId)
1002
803
 
1003
804
  // TODO: ideally we should defer unlocking until all direct dependents are completed,
1004
805
  // to ensure that they are received expected inputs from this instance
1005
806
  this.promiseTracker.track(
1006
- this.instanceLockService.unlockInstancesUnconditionally(this.project.id, [instanceId]),
807
+ this.instanceLockService
808
+ .unlockInstances(this.project.id, [state.id], this.unlockToken)
809
+ .then(() => this.workset.markInstanceUnlocked(state.id)),
1007
810
  )
811
+
812
+ this.logger.debug(`promise for instance "%s" completed`, instanceId)
1008
813
  })
1009
814
 
1010
815
  this.instancePromiseMap.set(instanceId, instancePromise)
816
+ this.logger.trace(`created new promise for instance "%s"`, instanceId)
1011
817
 
1012
818
  return instancePromise
1013
819
  }
1014
820
 
1015
- private async setInitialOperationStatus(instance: InstanceModel): Promise<void> {
1016
- const state = this.workset.getState(instance.id)
1017
-
1018
- await this.workset.patchState({
1019
- id: instance.id,
1020
- parentId: instance.parentId,
1021
- operationStatus: {
1022
- status: "pending",
1023
- message: "",
1024
- dependencyIds: this.getInstanceDependencyIds(instance.id),
1025
- latestOperationId: this.operation.id,
1026
- inputHashNonce: state?.operationStatus?.inputHashNonce ?? randomBytes(4).readInt32LE(),
1027
- args: instance.args,
1028
- },
1029
- })
1030
- }
821
+ private async ensureInstancesUnlocked(): Promise<void> {
822
+ const lockedStateIds = Array.from(this.workset.getLockedStateIds())
823
+ if (lockedStateIds.length === 0) {
824
+ return
825
+ }
1031
826
 
1032
- private getInstanceDependencyIds(instanceId: string): string[] {
1033
- const dependencies = new Set<string>()
1034
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instanceId) ?? {}
827
+ this.logger.warn("unlocking %d locked instances before shutting down", lockedStateIds.length)
1035
828
 
1036
- for (const inputs of Object.values(instanceInputs)) {
1037
- for (const input of inputs) {
1038
- dependencies.add(input.input.instanceId)
1039
- }
829
+ await this.instanceLockService.unlockInstances(
830
+ this.project.id,
831
+ lockedStateIds,
832
+ this.unlockToken,
833
+ )
834
+ }
835
+
836
+ private async ensureOperationStatesFinalized(): Promise<void> {
837
+ const unfinishedStates = this.context.getUnfinishedOperationStates()
838
+ if (unfinishedStates.length === 0) {
839
+ return
1040
840
  }
1041
841
 
1042
- return Array.from(dependencies)
842
+ this.logger.warn(
843
+ "finalizing %d unfinished operation states before shutting down",
844
+ unfinishedStates.length,
845
+ )
846
+
847
+ for (const state of unfinishedStates) {
848
+ await this.workset.updateState(state.instanceId, {
849
+ operationState: {
850
+ status: "failed",
851
+ finishedAt: new Date(),
852
+ },
853
+ instanceState: {
854
+ status: state.status === "deployed" ? "deployed" : "failed",
855
+ },
856
+ })
857
+ }
1043
858
  }
1044
859
 
1045
860
  /**
@@ -1048,22 +863,22 @@ export class RuntimeOperation {
1048
863
  */
1049
864
  private collectArtifactIdsForInstance(instance: InstanceModel): string[] {
1050
865
  const artifactIds = new Set<string>()
1051
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
866
+ const instanceInputs = this.context.getResolvedInputs(instance.id) ?? {}
1052
867
 
1053
868
  for (const inputs of Object.values(instanceInputs)) {
1054
869
  for (const input of inputs) {
1055
- const dependencyState = this.workset.getState(input.input.instanceId)
1056
- if (!dependencyState?.extra?.exportedArtifactIds) {
870
+ const dependencyState = this.context.getState(input.input.instanceId)
871
+ if (!dependencyState.exportedArtifactIds) {
1057
872
  continue
1058
873
  }
1059
874
 
1060
875
  const outputKey = input.input.output
1061
- const outputArtifacts = dependencyState.extra.exportedArtifactIds[outputKey]
1062
- if (!outputArtifacts) {
876
+ const outputArtifactIds = dependencyState.exportedArtifactIds[outputKey]
877
+ if (!outputArtifactIds) {
1063
878
  continue
1064
879
  }
1065
880
 
1066
- for (const hash of outputArtifacts) {
881
+ for (const hash of outputArtifactIds) {
1067
882
  artifactIds.add(hash)
1068
883
  }
1069
884
  }
@@ -1071,327 +886,4 @@ export class RuntimeOperation {
1071
886
 
1072
887
  return Array.from(artifactIds)
1073
888
  }
1074
-
1075
- /**
1076
- * Collects secret IDs from dependencies based on the direct connections
1077
- * from instance inputs to dependency outputs.
1078
- */
1079
- private collectSecretIdsForInstance(instance: InstanceModel): Set<string> {
1080
- const secretIds = new Set<string>()
1081
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
1082
-
1083
- for (const inputs of Object.values(instanceInputs)) {
1084
- for (const input of inputs) {
1085
- const dependencyState = this.workset.getState(input.input.instanceId)
1086
- if (!dependencyState?.extra?.exportedSecretIds) {
1087
- continue
1088
- }
1089
-
1090
- const outputKey = input.input.output
1091
- const outputSecrets = dependencyState.extra.exportedSecretIds[outputKey]
1092
- if (!outputSecrets) {
1093
- continue
1094
- }
1095
-
1096
- for (const secretId of outputSecrets) {
1097
- secretIds.add(secretId)
1098
- }
1099
- }
1100
- }
1101
-
1102
- return secretIds
1103
- }
1104
-
1105
- /**
1106
- * Updates the secrets for a specific instance.
1107
- * Processes terminals from instance state update.
1108
- *
1109
- * Converts unit terminals to instance terminals and handles cleanup.
1110
- */
1111
- private processTerminals(
1112
- instanceId: string,
1113
- unitTerminals: UnitTerminal[],
1114
- currentTerminalIds: Record<string, string> | undefined,
1115
- ): Record<string, string> {
1116
- const terminals: Terminal[] = unitTerminals.map(unitTerminal => ({
1117
- id: currentTerminalIds?.[unitTerminal.name] ?? uuidv7(),
1118
- instanceId,
1119
- meta: unitTerminal.meta ?? {},
1120
- }))
1121
-
1122
- const terminalSpecEntries: [string, TerminalSpec][] = unitTerminals
1123
- //
1124
- .map((unitTerminal, index) => [terminals[index].id, unitTerminal.spec])
1125
-
1126
- // create mapping from local name to UUID
1127
- const newTerminalIds: Record<string, string> = {}
1128
- for (let i = 0; i < unitTerminals.length; i++) {
1129
- const unitTerminal = unitTerminals[i]
1130
- const instanceTerminal = terminals[i]
1131
- newTerminalIds[unitTerminal.name] = instanceTerminal.id
1132
- }
1133
-
1134
- // find terminals that need to be deleted (old terminals not in new list)
1135
- const terminalsToDelete = Object.values(currentTerminalIds ?? {}).filter(
1136
- id => !Object.values(newTerminalIds).includes(id),
1137
- )
1138
-
1139
- if (terminals.length > 0) {
1140
- this.promiseTracker.track(this.putTerminals(terminals, terminalSpecEntries))
1141
- }
1142
-
1143
- if (terminalsToDelete.length > 0) {
1144
- this.promiseTracker.track(this.deleteTerminals(terminalsToDelete))
1145
- }
1146
-
1147
- return newTerminalIds
1148
- }
1149
-
1150
- private async putTerminals(
1151
- terminals: Terminal[],
1152
- terminalSpecEntries: [string, TerminalSpec][],
1153
- ): Promise<void> {
1154
- if (this.operation.type === "preview") {
1155
- return
1156
- }
1157
-
1158
- await using batch = this.stateManager.batch()
1159
-
1160
- await Promise.all([
1161
- this.stateManager.getTerminalRepository(this.project.id).putManyItems(terminals, batch),
1162
-
1163
- this.stateManager
1164
- .getTerminalSpecRepository(this.project.id)
1165
- .putMany(terminalSpecEntries, batch),
1166
- ])
1167
-
1168
- await batch.write()
1169
- }
1170
-
1171
- private async deleteTerminals(terminalIds: string[]): Promise<void> {
1172
- if (this.operation.type === "preview") {
1173
- return
1174
- }
1175
-
1176
- await using batch = this.stateManager.batch()
1177
-
1178
- await Promise.all([
1179
- this.stateManager.getTerminalRepository(this.project.id).deleteMany(terminalIds, batch),
1180
- this.stateManager.getTerminalSpecRepository(this.project.id).deleteMany(terminalIds, batch),
1181
- ])
1182
-
1183
- await batch.write()
1184
- }
1185
-
1186
- /**
1187
- * Processes pages from instance state update.
1188
- *
1189
- * Converts unit pages to instance pages and handles cleanup.
1190
- */
1191
- private processPages(
1192
- instanceId: string,
1193
- unitPages: UnitPage[],
1194
- currentPageIds: Record<string, string> | undefined,
1195
- ): Record<string, string> {
1196
- const pages: Page[] = unitPages.map(unitPage => ({
1197
- id: currentPageIds?.[unitPage.name] ?? uuidv7(),
1198
- instanceId,
1199
- meta: unitPage.meta,
1200
- }))
1201
-
1202
- const pageContentEntries: [string, PageBlock[]][] = unitPages
1203
- //
1204
- .map((unitPage, index) => [pages[index].id, unitPage.content])
1205
-
1206
- // create mapping from local name to UUID
1207
- const newPageIds: Record<string, string> = {}
1208
- for (let i = 0; i < unitPages.length; i++) {
1209
- const unitPage = unitPages[i]
1210
- const instancePage = pages[i]
1211
- newPageIds[unitPage.name] = instancePage.id
1212
- }
1213
-
1214
- // find pages that need to be deleted (old pages not in new list)
1215
- const pagesToDelete = Object.values(currentPageIds ?? {}).filter(
1216
- id => !Object.values(newPageIds).includes(id),
1217
- )
1218
-
1219
- if (pages.length > 0) {
1220
- this.promiseTracker.track(this.putPages(pages, pageContentEntries))
1221
- }
1222
-
1223
- if (pagesToDelete.length > 0) {
1224
- this.promiseTracker.track(this.deletePages(pagesToDelete))
1225
- }
1226
-
1227
- return newPageIds
1228
- }
1229
-
1230
- private async putPages(
1231
- pages: Page[],
1232
- pageContentEntries: [string, PageBlock[]][],
1233
- ): Promise<void> {
1234
- if (this.operation.type === "preview") {
1235
- return
1236
- }
1237
-
1238
- const batch = this.stateManager.batch()
1239
-
1240
- await Promise.all([
1241
- this.stateManager.getPageRepository(this.project.id).putManyItems(pages, batch),
1242
- this.stateManager
1243
- .getPageContentRepository(this.project.id)
1244
- .putMany(pageContentEntries, batch),
1245
- ])
1246
-
1247
- await batch.write()
1248
- }
1249
-
1250
- private async deletePages(pageIds: string[]): Promise<void> {
1251
- if (this.operation.type === "preview") {
1252
- return
1253
- }
1254
-
1255
- const batch = this.stateManager.batch()
1256
-
1257
- await Promise.all([
1258
- this.stateManager.getPageRepository(this.project.id).deleteMany(pageIds, batch),
1259
- this.stateManager.getPageContentRepository(this.project.id).deleteMany(pageIds, batch),
1260
- ])
1261
-
1262
- await batch.write()
1263
- }
1264
-
1265
- /**
1266
- * Processes triggers from instance state update.
1267
- *
1268
- * Converts unit triggers to instance triggers and handles cleanup.
1269
- */
1270
- private processTriggers(
1271
- instanceId: string,
1272
- unitTriggers: UnitTrigger[],
1273
- currentTriggerIds: Record<string, string> | undefined,
1274
- ): Record<string, string> {
1275
- const triggers: Trigger[] = unitTriggers.map(unitTrigger => ({
1276
- id: currentTriggerIds?.[unitTrigger.name] ?? uuidv7(),
1277
- instanceId,
1278
- meta: unitTrigger.meta ?? {},
1279
- spec: unitTrigger.spec,
1280
- }))
1281
-
1282
- // create mapping from local name to UUID
1283
- const newTriggerIds: Record<string, string> = {}
1284
- for (let i = 0; i < unitTriggers.length; i++) {
1285
- const unitTrigger = unitTriggers[i]
1286
- newTriggerIds[unitTrigger.name] = triggers[i].id
1287
- }
1288
-
1289
- // find triggers that need to be deleted (old triggers not in new list)
1290
- const triggersToDelete = Object.values(currentTriggerIds ?? {}).filter(
1291
- id => !Object.values(newTriggerIds).includes(id),
1292
- )
1293
-
1294
- if (triggers.length > 0) {
1295
- this.promiseTracker.track(this.putTriggers(triggers))
1296
- }
1297
-
1298
- if (triggersToDelete.length > 0) {
1299
- this.promiseTracker.track(this.deleteTriggers(triggersToDelete))
1300
- }
1301
-
1302
- return newTriggerIds
1303
- }
1304
-
1305
- private async putTriggers(triggers: Trigger[]): Promise<void> {
1306
- if (this.operation.type === "preview") {
1307
- return
1308
- }
1309
-
1310
- await this.stateManager.getTriggerRepository(this.project.id).putManyItems(triggers)
1311
- }
1312
-
1313
- private async deleteTriggers(triggerIds: string[]): Promise<void> {
1314
- if (this.operation.type === "preview") {
1315
- return
1316
- }
1317
-
1318
- await this.stateManager.getTriggerRepository(this.project.id).deleteMany(triggerIds)
1319
- }
1320
-
1321
- /**
1322
- * Processes artifacts from instance state update.
1323
- */
1324
- private processArtifacts(
1325
- instanceId: string,
1326
- newOwnedArtifacts: Record<string, RunnerArtifact[]>,
1327
- oldOwnedArtifactIds: Record<string, string[]> | undefined,
1328
- ): Record<string, string[]> {
1329
- if (this.operation.type !== "preview") {
1330
- // persist the new owned artifacts if this is not a preview operation
1331
- this.promiseTracker.track(
1332
- this.artifactService.updateUsage(
1333
- this.project.id,
1334
- { type: "instance", instanceId },
1335
- unique(Object.values(oldOwnedArtifactIds ?? {}).flat()),
1336
- unique(
1337
- Object.values(newOwnedArtifacts).flatMap(artifacts =>
1338
- artifacts.map(artifact => artifact.id),
1339
- ),
1340
- ),
1341
- ),
1342
- )
1343
- }
1344
-
1345
- return mapValues(newOwnedArtifacts, artifacts => artifacts.map(artifact => artifact.id))
1346
- }
1347
-
1348
- /**
1349
- * Processes exported secrets from instance state update.
1350
- *
1351
- * Validates that all secret IDs are either present in the instance secrets or are in "exportedSecrets" of the direct dependencies
1352
- * from connected inputs.
1353
- *
1354
- * Returns a mapping of output key to array of secret IDs exported via this output.
1355
- */
1356
- private processExportedSecrets(
1357
- instanceId: string,
1358
- exportedSecrets: Record<string, Omit<UnitSecretModel, "value">[]>,
1359
- ): Record<string, string[]> {
1360
- const instance = this.workset.getInstance(instanceId)
1361
- const allowedSecretIds = this.collectSecretIdsForInstance(instance)
1362
-
1363
- return mapValues(exportedSecrets, secrets => {
1364
- const exportedSecretIds = secrets.map(secret => secret.id)
1365
- for (const exportedSecretId of exportedSecretIds) {
1366
- if (!allowedSecretIds.has(exportedSecretId)) {
1367
- throw new Error(
1368
- `The secret "${exportedSecretId}" is not allowed to be exported since it is not present in the instance secrets or in the exported secrets of connected dependencies.`,
1369
- )
1370
- }
1371
- }
1372
-
1373
- return exportedSecretIds
1374
- })
1375
- }
1376
-
1377
- /**
1378
- * Updates the secrets for a specific instance.
1379
- */
1380
- private async updateInstanceSecrets(
1381
- instance: InstanceModel,
1382
- secrets: Record<string, unknown>,
1383
- ): Promise<void> {
1384
- if (this.operation.type === "preview") {
1385
- // do not update secrets in preview mode
1386
- return
1387
- }
1388
-
1389
- await this.secretService.updateInstanceSecrets(
1390
- this.project,
1391
- instance.id,
1392
- secrets,
1393
- [], // no secrets to delete in this context
1394
- false, // do not invalidate state even if the secrets were updated
1395
- )
1396
- }
1397
889
  }