@highstate/backend 0.9.18 → 0.9.19

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-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  4. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  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 +7587 -7291
  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 +27 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +35 -0
  35. package/prisma/project/instance.prisma +160 -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 +41 -0
  72. package/prisma/project/secret.prisma +42 -0
  73. package/prisma/project/service-account.prisma +36 -0
  74. package/prisma/project/terminal.prisma +90 -0
  75. package/prisma/project/trigger.prisma +31 -0
  76. package/prisma/project/unlock-method.prisma +32 -0
  77. package/prisma/project/worker.prisma +138 -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 +465 -130
  97. package/src/business/secret.ts +186 -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 +435 -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 +33 -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 +64 -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 +222 -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 +1 -1
  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,80 @@ 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,
502
365
  })
503
366
 
504
- perfLogger.log("unit update requested")
505
-
506
- await this.watchStateStream(instance.type, instance.name, logger)
507
- perfLogger.log("unit update completed")
367
+ await this.watchStateStream(state, instance.type, instance.name, logger)
508
368
  logger.info("unit updated")
509
369
  })
510
370
  }
511
371
 
512
- private async updateUnitDependencies(instanceId: string, logger: Logger): Promise<void> {
372
+ private async updateUnitDependencies(instanceId: InstanceId, logger: Logger): Promise<void> {
513
373
  try {
514
- const dependencies = this.getInstanceDependencyIds(instanceId)
374
+ const dependencies = this.context.getDependencies(instanceId)
515
375
  const dependencyPromises: Promise<void>[] = []
516
376
 
517
- for (const dependencyId of dependencies) {
518
- if (!this.operation.instanceIdsToUpdate.includes(dependencyId)) {
377
+ for (const dependency of dependencies) {
378
+ if (!this.workset.phaseAffectedInstanceIds.has(dependency.id)) {
519
379
  // skip dependencies that are not affected by the operation
520
380
  continue
521
381
  }
522
382
 
523
- logger.debug(`waiting for dependency "${dependencyId}"`)
524
- dependencyPromises.push(this.getInstancePromiseForOperation(dependencyId))
383
+ const state = this.context.getState(dependency.id)
384
+
385
+ logger.debug(`waiting for dependency "%s"`, dependency.id)
386
+ dependencyPromises.push(this.getInstancePromiseForOperation(dependency, state))
525
387
  }
526
388
 
527
- await Promise.all(dependencyPromises)
389
+ await waitAll(dependencyPromises)
528
390
 
529
391
  if (dependencies.length > 0) {
530
392
  logger.info("all dependencies completed")
531
393
  }
532
- } catch {
394
+ } catch (error) {
533
395
  // abort the instance if any dependency fails
534
- throw new AbortError()
396
+ throw new AbortError("One of the dependencies failed", { cause: error })
535
397
  }
536
398
  }
537
399
 
538
400
  private async processBeforeDestroyTriggers(
401
+ instance: InstanceModel,
539
402
  state: InstanceState,
540
403
  logger: Logger,
541
404
  signal: AbortSignal,
@@ -546,15 +409,7 @@ export class RuntimeOperation {
546
409
  return
547
410
  }
548
411
 
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)
412
+ const allTriggers = await this.unitExtraService.getInstanceTriggers(this.project.id, state.id)
558
413
 
559
414
  const beforeDestroyTriggers = allTriggers.filter(
560
415
  trigger => trigger.spec.type === "before-destroy",
@@ -564,95 +419,72 @@ export class RuntimeOperation {
564
419
  return
565
420
  }
566
421
 
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
- })
422
+ const invokedTriggers = beforeDestroyTriggers.map(trigger => ({
423
+ name: trigger.name,
424
+ }))
575
425
 
576
- await this.workset.patchState({
577
- id: instance.id,
578
- operationStatus: {
579
- status: "processing-triggers",
580
- },
426
+ await this.workset.updateState(instance.id, {
427
+ operationState: { status: "processing_triggers" },
581
428
  })
582
429
 
583
430
  logger.info("updating unit to process before-destroy triggers...")
584
431
 
585
- const secrets = await this.secretService.getInstanceSecretValues(this.project, instance.id)
432
+ const secrets = await this.secretService.getInstanceSecretValues(this.project.id, state.id)
586
433
 
587
434
  await this.runnerBackend.update({
588
435
  projectId: this.project.id,
436
+ stateId: state.id,
589
437
  libraryId: this.project.libraryId,
590
438
  instanceType: instance.type,
591
439
  instanceName: instance.name,
592
- config: this.prepareUnitConfig(instance, component, invokedTriggers),
440
+ config: this.prepareUnitConfig(instance, secrets, invokedTriggers),
593
441
  refresh: this.operation.options.refresh,
594
- secrets: mapValues(secrets, value => valueToString(value)),
442
+ secrets,
595
443
  signal,
596
444
  forceSignal,
597
445
  })
598
446
 
599
447
  logger.debug("unit update requested")
600
448
 
601
- await this.watchStateStream(instance.type, instance.name, logger)
449
+ await this.watchStateStream(state, instance.type, instance.name, logger)
602
450
  logger.debug("before-destroy triggers processed")
603
451
  }
604
452
 
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
-
453
+ private async destroyUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
454
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
622
455
  const dependentPromises: Promise<void>[] = []
623
- const dependents = this.workset.getDependentStates(state.id)
456
+ const dependents = this.context.getDependentStates(instance.id)
624
457
 
625
458
  for (const dependent of dependents) {
626
- if (
627
- !this.operation.options.destroyDependentInstances &&
628
- !this.operation.instanceIdsToDestroy.includes(dependent.id)
629
- ) {
459
+ if (!this.workset.phaseAffectedInstanceIds.has(dependent.instanceId)) {
630
460
  // skip dependents that are not affected by the operation
631
461
  continue
632
462
  }
633
463
 
634
- dependentPromises.push(this.getInstancePromiseForOperation(dependent.id))
464
+ const instance = this.context.getInstance(dependent.instanceId)
465
+ dependentPromises.push(this.getInstancePromiseForOperation(instance, dependent))
635
466
  }
636
467
 
637
- await Promise.all(dependentPromises)
468
+ await waitAll(dependentPromises)
638
469
  signal.throwIfAborted()
639
470
 
640
- await this.processBeforeDestroyTriggers(state, logger, signal, forceSignal)
471
+ await this.processBeforeDestroyTriggers(instance, state, logger, signal, forceSignal)
641
472
  signal.throwIfAborted()
642
473
 
643
474
  logger.info("destroying unit...")
644
475
 
645
- await this.workset.patchState({
646
- id: instanceId,
647
- operationStatus: {
476
+ await this.workset.updateState(instance.id, {
477
+ operationState: {
648
478
  status: "destroying",
479
+ startedAt: new Date(),
649
480
  },
650
481
  })
651
482
 
652
- const [type, name] = parseInstanceId(instanceId)
483
+ const [type, name] = parseInstanceId(instance.id)
653
484
 
654
485
  await this.runnerBackend.destroy({
655
486
  projectId: this.project.id,
487
+ stateId: state.id,
656
488
  libraryId: this.project.libraryId,
657
489
  instanceType: type,
658
490
  instanceName: name,
@@ -665,43 +497,31 @@ export class RuntimeOperation {
665
497
 
666
498
  logger.debug("destroy request sent")
667
499
 
668
- await this.watchStateStream(type, name, logger)
500
+ await this.watchStateStream(state, type, name, logger)
669
501
 
670
502
  // 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
- }
503
+ await this.artifactService.clearInstanceArtifactReferences(this.project.id, instance.id)
683
504
 
684
505
  logger.info("unit destroyed")
685
506
  })
686
507
  }
687
508
 
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: {
509
+ private async refreshUnit(instance: InstanceModel, state: InstanceState): Promise<void> {
510
+ return this.getInstancePromise(instance.id, async (logger, signal, forceSignal) => {
511
+ await this.workset.updateState(instance.id, {
512
+ operationState: {
693
513
  status: "refreshing",
694
- currentResourceCount: 0,
695
- totalResourceCount: 0,
514
+ startedAt: new Date(),
696
515
  },
697
516
  })
698
517
 
699
518
  logger.info("refreshing unit...")
700
519
 
701
- const [type, name] = parseInstanceId(instanceId)
520
+ const [type, name] = parseInstanceId(instance.id)
702
521
 
703
522
  await this.runnerBackend.refresh({
704
523
  projectId: this.project.id,
524
+ stateId: state.id,
705
525
  libraryId: this.project.libraryId,
706
526
  instanceType: type,
707
527
  instanceName: name,
@@ -711,18 +531,20 @@ export class RuntimeOperation {
711
531
 
712
532
  logger.debug("unit refresh requested")
713
533
 
714
- await this.watchStateStream(type, name, logger)
534
+ await this.watchStateStream(state, type, name, logger)
715
535
  logger.info("unit refreshed")
716
536
  })
717
537
  }
718
538
 
719
539
  private async watchStateStream(
720
- instanceType: string,
540
+ state: InstanceState,
541
+ instanceType: VersionedName,
721
542
  instanceName: string,
722
543
  logger: Logger,
723
544
  ): Promise<void> {
724
545
  const stream = this.runnerBackend.watch({
725
546
  projectId: this.project.id,
547
+ stateId: state.id,
726
548
  libraryId: this.project.libraryId,
727
549
  instanceType,
728
550
  instanceName,
@@ -732,18 +554,12 @@ export class RuntimeOperation {
732
554
 
733
555
  for await (update of stream) {
734
556
  try {
735
- await this.handleUnitStateUpdate(update)
557
+ await this.handleUnitStateUpdate(update, state)
736
558
  } catch (error) {
737
559
  logger.error({ error }, "failed to handle unit state update")
738
560
  }
739
561
 
740
562
  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
563
  // rethrow the error to stop the execution of dependent units
748
564
  throw new Error(`An error occurred while processing the unit "${update.unitId}"`, {
749
565
  cause: update.message,
@@ -762,204 +578,155 @@ export class RuntimeOperation {
762
578
 
763
579
  private prepareUnitConfig(
764
580
  instance: InstanceModel,
765
- component: ComponentModel,
581
+ secrets: Record<string, unknown>,
766
582
  invokedTriggers: TriggerInvocation[] = [],
767
- ): Record<string, string> {
768
- const config: Record<string, string> = {}
583
+ ): UnitConfig {
584
+ const resolvedInputs = this.context.getResolvedInputs(instance.id)
769
585
 
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) : ""
586
+ return {
587
+ instanceId: instance.id,
588
+ args: instance.args ?? {},
589
+ inputs: mapValues(resolvedInputs ?? {}, input => input.map(value => value.input)),
590
+ invokedTriggers,
591
+ secretNames: Object.keys(secrets),
592
+ stateIdMap: this.context.getInstanceIdToStateIdMap(),
774
593
  }
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
594
  }
788
595
 
789
- private async handleUnitStateUpdate(update: UnitStateUpdate): Promise<void> {
596
+ private async handleUnitStateUpdate(
597
+ update: UnitStateUpdate,
598
+ state: InstanceState,
599
+ ): Promise<void> {
790
600
  switch (update.type) {
791
601
  case "message":
792
- this.handleUnitMessage(update)
602
+ this.handleUnitMessage(update, state)
793
603
  return
794
604
  case "progress":
795
605
  await this.handleUnitProgress(update)
796
606
  return
797
607
  case "error":
798
- await this.handleUnitError(update)
608
+ await this.handleUnitError(update, state)
799
609
  return
800
610
  case "completion":
801
- await this.handleUnitCompletion(update)
611
+ await this.handleUnitCompletion(update, state)
802
612
  return
803
613
  }
804
614
  }
805
615
 
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
-
616
+ private handleUnitMessage(update: TypedUnitStateUpdate<"message">, state: InstanceState): void {
617
+ // append logs to the instance and all its parents
811
618
  for (;;) {
812
- instanceIdsInHierarchy.push(instance.id)
619
+ this.promiseTracker.track(
620
+ this.operationService.appendLog(
621
+ this.project.id,
622
+ this.operation.id,
623
+ state.id,
624
+ update.message,
625
+ ),
626
+ )
813
627
 
814
- if (!instance.parentId) {
628
+ if (!state.parentInstanceId) {
815
629
  break
816
630
  }
817
631
 
818
- instance = this.workset.getInstance(instance.parentId)
632
+ state = this.context.getState(state.parentInstanceId)
819
633
  }
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
634
  }
830
635
 
831
636
  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)
637
+ await this.workset.updateState(update.unitId, {
638
+ operationState: {
639
+ currentResourceCount: update.currentResourceCount,
640
+ totalResourceCount: update.totalResourceCount,
641
+ },
642
+ })
844
643
  }
845
644
 
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",
645
+ private async handleUnitError(
646
+ update: TypedUnitStateUpdate<"error">,
647
+ state: InstanceState,
648
+ ): Promise<void> {
649
+ await this.workset.updateState(update.unitId, {
650
+ instanceState: {
651
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
652
+ status: state.status === "deployed" ? "deployed" : "failed",
855
653
  message: update.message,
856
654
  },
857
- }
858
-
859
- await this.workset.patchState(patch)
655
+ operationState: {
656
+ status: isAbortErrorLike(update.message) ? "cancelled" : "failed",
657
+ finishedAt: new Date(),
658
+ },
659
+ })
860
660
  }
861
661
 
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)
662
+ private async handleUnitCompletion(
663
+ update: TypedUnitStateUpdate<"completion">,
664
+ state: InstanceState,
665
+ ): Promise<void> {
666
+ const instance = this.context.getInstance(update.unitId)
877
667
 
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
- })
668
+ const data: InstanceStatePatch = {
669
+ status: this.workset.getNextStableInstanceStatus(instance.id),
670
+ message: update.message,
671
+ statusFields: update.statusFields ?? null,
884
672
  }
885
673
 
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,
674
+ if (update.operationType !== "destroy") {
675
+ // давайте еще больше усложним и без того сложную штуку
676
+ // set output hash before calculating input hash to capture up-to-date output hash for dependencies
677
+ state.outputHash = update.outputHash ?? null
905
678
 
906
- pageIds: update.pages
907
- ? this.processPages(update.unitId, update.pages, state.extra?.pageIds)
908
- : null,
679
+ // recalculate the input and output hashes for the instance
680
+ const { inputHash, dependencyOutputHash } =
681
+ await this.context.getUpToDateInputHashOutput(instance)
909
682
 
910
- triggerIds: update.triggers
911
- ? this.processTriggers(update.unitId, update.triggers, state.extra?.triggerIds)
912
- : null,
683
+ data.inputHash = inputHash
684
+ data.dependencyOutputHash = dependencyOutputHash
685
+ data.outputHash = update.outputHash
913
686
 
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,
687
+ // also update the parent ID
688
+ if (instance.parentId) {
689
+ const parentState = this.context.getState(instance.parentId)
690
+ data.parentId = parentState.id
691
+ } else {
692
+ data.parentId = null
693
+ }
694
+ } else {
695
+ data.message = null
696
+ data.inputHash = null
697
+ data.dependencyOutputHash = null
698
+ data.outputHash = null
699
+ data.parentId = null
700
+ }
701
+
702
+ // update the operation state
703
+ await this.workset.updateState(instance.id, {
704
+ // TODO: honestly, it is not correct
705
+ // may be we should track operation phases separately
706
+ // or introduce status like "destroyed-before-recreation" (quite ugly though)
707
+ operationState: {
708
+ status: this.workset.getStableStatusByOperationPhase(),
709
+ finishedAt: new Date(),
934
710
  },
935
- }
936
-
937
- if (update.secrets) {
938
- const instance = this.workset.getInstance(update.unitId)
939
711
 
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)
712
+ // do not write instance state for non-last phases of the instance
713
+ instanceState: this.workset.isLastPhaseForInstance(instance.id) ? data : undefined,
714
+
715
+ // also do not write unit extra data for non-last phases of the instance
716
+ unitExtra: this.workset.isLastPhaseForInstance(instance.id)
717
+ ? {
718
+ pages: update.pages ?? [],
719
+ terminals: update.terminals ?? [],
720
+ triggers: update.triggers ?? [],
721
+ workers: update.workers ?? [],
722
+ secrets: update.secrets ?? {},
723
+ }
724
+ : undefined,
725
+ })
959
726
  }
960
727
 
961
728
  private getInstancePromise(
962
- instanceId: string,
729
+ instanceId: InstanceId,
963
730
  fn: (logger: Logger, signal: AbortSignal, forceSignal: AbortSignal) => Promise<void>,
964
731
  ): Promise<void> {
965
732
  let instancePromise = this.instancePromiseMap.get(instanceId)
@@ -967,30 +734,54 @@ export class RuntimeOperation {
967
734
  return instancePromise
968
735
  }
969
736
 
970
- const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
971
- const abortController = this.instanceAbortControllers.get(instanceId)
972
- const forceAbortController = this.instanceForceAbortControllers.get(instanceId)
737
+ // "pending" -> "running" if at least one instance is running
738
+ if (this.operation.status === "pending") {
739
+ this.operation.status = "running"
740
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
741
+ }
742
+
743
+ const state = this.context.getState(instanceId)
973
744
 
974
- if (!abortController || !forceAbortController) {
745
+ const logger = this.logger.child({ instanceId }, { msgPrefix: `[${instanceId}] ` })
746
+ const abortControllerPair = this.workset.instanceAbortControllers.get(instanceId)
747
+ if (!abortControllerPair) {
975
748
  throw new Error(`Abort controllers for instance "${instanceId}" are not initialized`)
976
749
  }
977
750
 
751
+ const { abortController, forceAbortController } = abortControllerPair
752
+
978
753
  instancePromise = this.workset
979
- .ensureInstanceLocked(instanceId, abortController.signal)
754
+ .waitForInstanceLock(state.id, abortController.signal)
980
755
  .then(() => fn(logger, abortController.signal, forceAbortController.signal))
981
756
  .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
757
  if (this.operation.status !== "failing") {
990
758
  // report the failing status of the operation
991
759
  this.operation.status = "failing"
760
+ this.promiseTracker.track(this.updateOperation({ status: this.operation.status }))
761
+ }
762
+
763
+ if (isTransientInstanceOperationStatus(state.lastOperationState?.status)) {
764
+ // if the underlying method did not correctly update the instance status, do it here
992
765
  this.promiseTracker.track(
993
- this.operationService.updateOperation(this.project.id, this.operation),
766
+ this.workset.updateState(instanceId, {
767
+ operationState: {
768
+ status: isAbortErrorLike(error) ? "cancelled" : "failed",
769
+ finishedAt: new Date(),
770
+ },
771
+ instanceState: {
772
+ // keep "deployed" status for initially deployed instances even if the operation was failed or cancelled
773
+ status: state.status === "deployed" ? "deployed" : "failed",
774
+ },
775
+ }),
776
+ )
777
+
778
+ this.promiseTracker.track(
779
+ this.operationService.appendLog(
780
+ this.project.id,
781
+ this.operation.id,
782
+ state.id,
783
+ errorToString(error),
784
+ ),
994
785
  )
995
786
  }
996
787
 
@@ -998,48 +789,67 @@ export class RuntimeOperation {
998
789
  throw error
999
790
  })
1000
791
  .finally(() => {
792
+ if (!this.workset.isLastPhaseForInstance(instanceId)) {
793
+ // do not finalize the instance if it has more phases to run
794
+ return
795
+ }
796
+
1001
797
  this.instancePromiseMap.delete(instanceId)
1002
798
 
1003
799
  // TODO: ideally we should defer unlocking until all direct dependents are completed,
1004
800
  // to ensure that they are received expected inputs from this instance
1005
801
  this.promiseTracker.track(
1006
- this.instanceLockService.unlockInstancesUnconditionally(this.project.id, [instanceId]),
802
+ this.instanceLockService
803
+ .unlockInstances(this.project.id, [state.id], this.unlockToken)
804
+ .then(() => this.workset.markInstanceUnlocked(state.id)),
1007
805
  )
806
+
807
+ this.logger.debug(`promise for instance "%s" completed`, instanceId)
1008
808
  })
1009
809
 
1010
810
  this.instancePromiseMap.set(instanceId, instancePromise)
811
+ this.logger.trace(`created new promise for instance "%s"`, instanceId)
1011
812
 
1012
813
  return instancePromise
1013
814
  }
1014
815
 
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
- }
816
+ private async ensureInstancesUnlocked(): Promise<void> {
817
+ const lockedStateIds = Array.from(this.workset.getLockedStateIds())
818
+ if (lockedStateIds.length === 0) {
819
+ return
820
+ }
1031
821
 
1032
- private getInstanceDependencyIds(instanceId: string): string[] {
1033
- const dependencies = new Set<string>()
1034
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instanceId) ?? {}
822
+ this.logger.warn("unlocking %d locked instances before shutting down", lockedStateIds.length)
1035
823
 
1036
- for (const inputs of Object.values(instanceInputs)) {
1037
- for (const input of inputs) {
1038
- dependencies.add(input.input.instanceId)
1039
- }
824
+ await this.instanceLockService.unlockInstances(
825
+ this.project.id,
826
+ lockedStateIds,
827
+ this.unlockToken,
828
+ )
829
+ }
830
+
831
+ private async ensureOperationStatesFinalized(): Promise<void> {
832
+ const unfinishedStates = this.context.getUnfinishedOperationStates()
833
+ if (unfinishedStates.length === 0) {
834
+ return
1040
835
  }
1041
836
 
1042
- return Array.from(dependencies)
837
+ this.logger.warn(
838
+ "finalizing %d unfinished operation states before shutting down",
839
+ unfinishedStates.length,
840
+ )
841
+
842
+ for (const state of unfinishedStates) {
843
+ await this.workset.updateState(state.instanceId, {
844
+ operationState: {
845
+ status: "failed",
846
+ finishedAt: new Date(),
847
+ },
848
+ instanceState: {
849
+ status: state.status === "deployed" ? "deployed" : "failed",
850
+ },
851
+ })
852
+ }
1043
853
  }
1044
854
 
1045
855
  /**
@@ -1048,22 +858,22 @@ export class RuntimeOperation {
1048
858
  */
1049
859
  private collectArtifactIdsForInstance(instance: InstanceModel): string[] {
1050
860
  const artifactIds = new Set<string>()
1051
- const instanceInputs = this.workset.resolvedInstanceInputs.get(instance.id) ?? {}
861
+ const instanceInputs = this.context.getResolvedInputs(instance.id) ?? {}
1052
862
 
1053
863
  for (const inputs of Object.values(instanceInputs)) {
1054
864
  for (const input of inputs) {
1055
- const dependencyState = this.workset.getState(input.input.instanceId)
1056
- if (!dependencyState?.extra?.exportedArtifactIds) {
865
+ const dependencyState = this.context.getState(input.input.instanceId)
866
+ if (!dependencyState.exportedArtifactIds) {
1057
867
  continue
1058
868
  }
1059
869
 
1060
870
  const outputKey = input.input.output
1061
- const outputArtifacts = dependencyState.extra.exportedArtifactIds[outputKey]
1062
- if (!outputArtifacts) {
871
+ const outputArtifactIds = dependencyState.exportedArtifactIds[outputKey]
872
+ if (!outputArtifactIds) {
1063
873
  continue
1064
874
  }
1065
875
 
1066
- for (const hash of outputArtifacts) {
876
+ for (const hash of outputArtifactIds) {
1067
877
  artifactIds.add(hash)
1068
878
  }
1069
879
  }
@@ -1071,327 +881,4 @@ export class RuntimeOperation {
1071
881
 
1072
882
  return Array.from(artifactIds)
1073
883
  }
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
884
  }