@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,398 +1,297 @@
1
+ import type { CommonObjectMeta, UnitWorker } from "@highstate/contract"
1
2
  import type { Logger } from "pino"
2
- import type { LockManager } from "../lock"
3
+ import type {
4
+ DatabaseManager,
5
+ ProjectTransaction,
6
+ Worker,
7
+ WorkerVersion,
8
+ WorkerVersionLog,
9
+ } from "../database"
10
+ import type { PubSubManager } from "../pubsub"
3
11
  import type { WorkerManager } from "../worker"
4
- import type { RandomProvider } from "../common"
12
+ import { randomBytes } from "node:crypto"
13
+ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/client"
14
+ import { createProjectLogger } from "../common"
5
15
  import {
16
+ extractDigestFromImage,
6
17
  getWorkerIdentity,
7
- type ProjectApiKey,
8
- type ServiceAccount,
9
- type UnitWorker,
10
- type Worker,
11
- type WorkerUnitRegistration,
18
+ type WorkerUnitRegistrationEvent,
12
19
  } from "../shared"
13
- import { SAME_KEY, type StateManager } from "../state"
20
+ import { WorkerVersionNotFoundError } from "../shared/models/errors"
14
21
 
15
22
  export class WorkerService {
16
23
  constructor(
17
- private readonly stateManager: StateManager,
24
+ private readonly database: DatabaseManager,
18
25
  private readonly workerManager: WorkerManager,
19
- private readonly lockManager: LockManager,
20
- private readonly random: RandomProvider,
26
+ private readonly pubsubManager: PubSubManager,
21
27
  private readonly logger: Logger,
22
28
  ) {}
23
29
 
24
30
  /**
25
- * Updates the worker registrations for the given project and instance.
26
- * It creates new registrations for each unit worker, updates existing ones,
27
- * and deletes registrations that are no longer present.
31
+ * Updates unit worker registrations for an instance in a single transaction
32
+ * creates workers and worker versions as needed and cleans up removed registrations
28
33
  *
29
- * @param projectId The ID of the project.
30
- * @param instanceId The ID of the instance.
31
- * @param existingRegistrationIds A mapping of unit worker names to existing registration IDs.
32
- * @param unitWorkers The list of unit workers to register.
33
- * @returns A new mapping of unit worker names to their registration IDs.
34
+ * @param tx The transaction to use for database operations.
35
+ * @param projectId The ID of the project containing the instance.
36
+ * @param stateId The ID of the instance state to update registrations for.
37
+ * @param unitWorkers The list workers provided by the unit.
34
38
  */
35
39
  async updateUnitRegistrations(
40
+ tx: ProjectTransaction,
36
41
  projectId: string,
37
- instanceId: string,
38
- existingRegistrationIds: Record<string, string>,
42
+ stateId: string,
39
43
  unitWorkers: UnitWorker[],
40
- ): Promise<Record<string, string>> {
41
- // we will lock on the worker images to ensure that
42
- // concurrent units cannot create duplicate workers or leave dangling worker resources when removing them
43
- // we will not lock on the registrations
44
- // since it they are only updated within an operation
45
- const lockKeys = unitWorkers.map(
46
- unitWorker => ["worker-image", projectId, unitWorker.image] as const,
47
- )
48
-
49
- const newRegistrationIds: Record<string, string> = {}
50
-
51
- return await this.lockManager.acquire(lockKeys, async () => {
52
- const existingRegistrations = await this.stateManager
53
- .getWorkerRegistrationRepository(projectId)
54
- .getManyRecord(Object.values(existingRegistrationIds))
55
-
56
- const nameToExistingRegistration = new Map<string, WorkerUnitRegistration>()
57
- for (const [name, registrationId] of Object.entries(existingRegistrationIds)) {
58
- const existingRegistration = existingRegistrations[registrationId]
59
- if (existingRegistration) {
60
- nameToExistingRegistration.set(name, existingRegistration)
61
- }
62
- }
63
-
64
- const batch = this.stateManager.batch()
65
-
66
- // create or update registrations for each unit worker
67
- for (const unitWorker of unitWorkers) {
68
- const workerId = await this.ensureWorkerCreated(projectId, unitWorker)
69
- const existingRegistration = nameToExistingRegistration.get(unitWorker.name)
70
-
71
- if (
72
- existingRegistration &&
73
- existingRegistration.workerId === workerId &&
74
- JSON.stringify(existingRegistration.params) === JSON.stringify(unitWorker.params)
75
- ) {
76
- // no changes
77
- newRegistrationIds[unitWorker.name] = existingRegistration.id
78
- continue
79
- }
80
-
81
- const registration: WorkerUnitRegistration = {
82
- id: existingRegistration?.id ?? this.random.uuidv7(),
83
- meta: existingRegistration?.meta ?? {},
84
- image: unitWorker.image,
85
- params: unitWorker.params,
86
- instanceId,
87
- workerId,
88
- }
44
+ ): Promise<void> {
45
+ // parse images first
46
+ const parsedWorkers = unitWorkers.map(w => {
47
+ const digest = extractDigestFromImage(w.image)
48
+ const identity = getWorkerIdentity(w.image)
89
49
 
90
- const isNewRegistration = !existingRegistration
91
- const isWorkerChange = existingRegistration?.workerId !== workerId
92
- const isParamsChange =
93
- existingRegistration &&
94
- JSON.stringify(existingRegistration.params) !== JSON.stringify(unitWorker.params)
95
-
96
- if (isNewRegistration) {
97
- this.logger.info(
98
- {
99
- projectId,
100
- registrationId: registration.id,
101
- unitWorkerName: unitWorker.name,
102
- workerId,
103
- image: unitWorker.image,
104
- },
105
- `creating worker registration "%s" for unit worker "%s" in project "%s"`,
106
- registration.id,
107
- unitWorker.name,
108
- projectId,
109
- )
110
- } else if (isWorkerChange) {
111
- this.logger.info(
112
- {
113
- projectId,
114
- registrationId: registration.id,
115
- unitWorkerName: unitWorker.name,
116
- oldWorkerId: existingRegistration?.workerId,
117
- newWorkerId: workerId,
118
- oldImage: existingRegistration?.image,
119
- newImage: unitWorker.image,
120
- },
121
- `updating worker registration "%s" for unit worker "%s" in project "%s" (worker changed from "%s" to "%s")`,
122
- registration.id,
123
- unitWorker.name,
124
- projectId,
125
- existingRegistration?.workerId,
126
- workerId,
127
- )
128
- } else if (isParamsChange) {
129
- this.logger.info(
130
- {
131
- projectId,
132
- registrationId: registration.id,
133
- unitWorkerName: unitWorker.name,
134
- workerId,
135
- },
136
- `updating worker registration "%s" for unit worker "%s" in project "%s" (params changed)`,
137
- registration.id,
138
- unitWorker.name,
139
- projectId,
140
- )
141
- }
50
+ return { ...w, digest, identity }
51
+ })
142
52
 
143
- await this.stateManager
144
- .getWorkerRegistrationRepository(projectId)
145
- .putItem(registration, batch)
53
+ const logger = createProjectLogger(this.logger, projectId)
146
54
 
147
- // update the registration indexes for new and old workers
148
- if (existingRegistration?.workerId !== workerId) {
149
- await this.stateManager
150
- .getWorkerRegistrationIndexRepository(projectId, workerId)
151
- .indexRepository.put(registration.id, SAME_KEY)
55
+ const eventsToPublish: { workerVersionId: string; event: WorkerUnitRegistrationEvent }[] = []
152
56
 
153
- if (existingRegistration?.workerId) {
154
- await this.stateManager
155
- .getWorkerRegistrationIndexRepository(projectId, existingRegistration.workerId)
156
- .indexRepository.delete(registration.id)
57
+ // query all registrations for the instance
58
+ const existingRegistrations = await tx.workerUnitRegistration.findMany({
59
+ where: { stateId },
60
+ select: { stateId: true, name: true, params: true, workerVersionId: true },
61
+ })
157
62
 
158
- // check if the old worker should be deleted
159
- await this.deleteWorkerIfHasNoRegistrations(projectId, existingRegistration.workerId)
160
- }
161
- }
63
+ // the set of names we want to keep
64
+ const desiredNames = new Set(parsedWorkers.map(w => w.name))
162
65
 
163
- newRegistrationIds[unitWorker.name] = registration.id
164
- }
66
+ for (const worker of parsedWorkers) {
67
+ const workerRecord = await this.ensureWorker(tx, worker.identity)
68
+ const workerVersionRecord = await this.ensureWorkerVersion(tx, workerRecord, worker.digest)
165
69
 
166
- // delete registrations that are no longer present
167
- for (const [name, existingRegistration] of nameToExistingRegistration.entries()) {
168
- if (unitWorkers.some(unitWorker => unitWorker.name === name)) {
169
- continue
170
- }
70
+ const existing = existingRegistrations.find(r => r.name === worker.name)
71
+ const stringifiedParams = JSON.stringify(worker.params)
171
72
 
172
- this.logger.info(
173
- {
174
- projectId,
175
- registrationId: existingRegistration.id,
176
- unitWorkerName: name,
177
- workerId: existingRegistration.workerId,
73
+ // create a new registration if it doesn't exist
74
+ if (!existing) {
75
+ await tx.workerUnitRegistration.create({
76
+ data: {
77
+ stateId,
78
+ name: worker.name,
79
+ params: worker.params,
80
+ workerVersionId: workerVersionRecord.id,
178
81
  },
179
- `deleting worker registration "%s" for unit worker "%s" in project "%s" (unit worker no longer present)`,
180
- existingRegistration.id,
181
- name,
182
- projectId,
183
- )
184
-
185
- await this.stateManager
186
- .getWorkerRegistrationRepository(projectId)
187
- .delete(existingRegistration.id, batch)
188
-
189
- await this.stateManager
190
- .getWorkerRegistrationIndexRepository(projectId, existingRegistration.workerId)
191
- .indexRepository.delete(existingRegistration.id)
192
-
193
- // ensure the worker is deleted if it has no registrations left
194
- await this.deleteWorkerIfHasNoRegistrations(projectId, existingRegistration.workerId)
195
- }
196
-
197
- await batch.write()
82
+ })
198
83
 
199
- return newRegistrationIds
200
- })
201
- }
84
+ eventsToPublish.push({
85
+ workerVersionId: workerVersionRecord.id,
86
+ event: { type: "registered", instanceId: stateId, params: worker.params },
87
+ })
202
88
 
203
- private async ensureWorkerCreated(projectId: string, unitWorker: UnitWorker): Promise<string> {
204
- const identity = getWorkerIdentity(unitWorker.image)
205
-
206
- return await this.lockManager.acquire(["worker-image", projectId, identity], async () => {
207
- // it is not critical to fetch all workers since their count is not expected to be high
208
- const workers = await this.stateManager.getWorkerRepository(projectId).getAllItems()
209
-
210
- const existingWorker = workers.find(worker => worker.image === unitWorker.image)
211
- if (existingWorker) {
212
- this.logger.debug(
213
- { projectId, workerId: existingWorker.id, image: unitWorker.image },
214
- `worker with image "%s" already exists, reusing it`,
215
- unitWorker.image,
216
- )
217
- return existingWorker.id
89
+ continue
218
90
  }
219
91
 
220
- this.logger.info(
221
- { projectId, image: unitWorker.image, identity },
222
- `creating new worker for image "%s" with identity "%s" in project "%s"`,
223
- unitWorker.image,
224
- identity,
225
- projectId,
226
- )
227
-
228
- let serviceAccountId: string | undefined
92
+ const paramsChanged = JSON.stringify(existing.params) !== stringifiedParams
93
+ const versionChanged = existing.workerVersionId !== workerVersionRecord.id
229
94
 
230
- const siblingWorker = workers.find(worker => worker.identity === identity)
231
- if (siblingWorker) {
232
- this.logger.debug(
233
- `sibling worker with identity "%s" already exists, using its service account`,
234
- identity,
235
- )
95
+ // update existing registration if params or version changed
96
+ if (paramsChanged || versionChanged) {
97
+ await tx.workerUnitRegistration.update({
98
+ where: { stateId_name: { stateId, name: worker.name } },
99
+ data: {
100
+ params: worker.params,
101
+ workerVersionId: workerVersionRecord.id,
102
+ },
103
+ })
104
+
105
+ // deregister from old worker version if it changed
106
+ if (versionChanged) {
107
+ eventsToPublish.push({
108
+ workerVersionId: existing.workerVersionId,
109
+ event: { type: "deregistered", instanceId: stateId },
110
+ })
111
+ }
236
112
 
237
- serviceAccountId = siblingWorker.serviceAccountId
113
+ eventsToPublish.push({
114
+ workerVersionId: workerVersionRecord.id,
115
+ event: { type: "registered", instanceId: stateId, params: worker.params },
116
+ })
238
117
  }
118
+ }
119
+
120
+ // remove registrations that are no longer desired
121
+ for (const registration of existingRegistrations) {
122
+ if (!desiredNames.has(registration.name)) {
123
+ await tx.workerUnitRegistration.delete({
124
+ where: { stateId_name: { stateId, name: registration.name } },
125
+ })
126
+
127
+ eventsToPublish.push({
128
+ workerVersionId: registration.workerVersionId,
129
+ event: { type: "deregistered", instanceId: stateId },
130
+ })
131
+ }
132
+ }
239
133
 
240
- const batch = this.stateManager.batch()
134
+ await this.cleanupUnusedWorkerVersions(tx)
241
135
 
242
- // create an empty service account if it is the first worker with this identity
243
- // the meta of the account will be updated by the worker itself
244
- if (!serviceAccountId) {
245
- const serviceAccount: ServiceAccount = {
246
- id: this.random.uuidv7(),
247
- meta: {},
248
- }
136
+ // publish events after transaction commits
137
+ for (const { workerVersionId, event } of eventsToPublish) {
138
+ void this.pubsubManager.publish(
139
+ ["worker-unit-registration", projectId, workerVersionId],
140
+ event,
141
+ )
142
+ }
249
143
 
250
- serviceAccountId = serviceAccount.id
144
+ // ensure all worker versions are started
145
+ void this.workerManager.syncWorkers(projectId)
251
146
 
252
- this.logger.info(
253
- { projectId, serviceAccountId, identity },
254
- `creating service account "%s" for worker identity "%s" in project "%s"`,
255
- serviceAccountId,
256
- identity,
257
- projectId,
258
- )
147
+ logger.info(`updated worker registrations for instance state "%s"`, stateId)
148
+ }
259
149
 
260
- await this.stateManager
261
- .getServiceAccountRepository(projectId)
262
- .putItem(serviceAccount, batch)
263
- }
150
+ private async ensureWorker(tx: ProjectTransaction, identity: string): Promise<Worker> {
151
+ const existing = await tx.worker.findUnique({ where: { identity } })
152
+ if (existing) {
153
+ return existing
154
+ }
155
+
156
+ // create a new service account for the worker
157
+ const serviceAccount = await tx.serviceAccount.create({
158
+ select: { id: true },
159
+ data: {
160
+ meta: {
161
+ // this generic meta should be replaced by the worker when it starts
162
+ title: "Worker Service Account",
163
+ description: `Automatically created for worker "${identity}".`,
164
+ },
165
+ },
166
+ })
264
167
 
265
- const apiKey: ProjectApiKey = {
266
- id: this.random.uuidv7(),
267
- meta: {},
268
- token: Buffer.from(this.random.bytes(32)).toString("hex"),
269
- scopes: [
270
- {
271
- type: "service-account",
272
- actions: ["full"],
273
- serviceAccountIds: [serviceAccountId],
274
- },
275
- ],
276
- }
168
+ return await tx.worker.create({
169
+ data: {
170
+ identity,
171
+ serviceAccountId: serviceAccount.id,
172
+ },
173
+ })
174
+ }
277
175
 
278
- await this.stateManager.getApiKeyRepository(projectId).putItem(apiKey, batch)
176
+ private async ensureWorkerVersion(
177
+ tx: ProjectTransaction,
178
+ worker: Worker,
179
+ digest: string,
180
+ ): Promise<WorkerVersion> {
181
+ const existing = await tx.workerVersion.findUnique({ where: { digest } })
182
+ if (existing) {
183
+ return existing
184
+ }
185
+
186
+ // create an API key for the worker granting full access to its service account
187
+ const apiKey = await tx.apiKey.create({
188
+ data: {
189
+ meta: {
190
+ title: `Worker API Key for "${worker.identity}"`,
191
+ description: `Automatically created for worker "${worker.identity}" with digest "${digest}".`,
192
+ },
193
+ serviceAccountId: worker.serviceAccountId,
194
+ token: randomBytes(32).toString("hex"),
195
+ },
196
+ })
279
197
 
280
- const worker: Worker = {
281
- id: this.random.uuidv7(),
282
- meta: {},
283
- status: "starting",
284
- failedStartAttempts: 5,
285
- identity,
286
- image: unitWorker.image,
287
- serviceAccountId,
198
+ return await tx.workerVersion.create({
199
+ data: {
200
+ workerId: worker.id,
201
+ digest,
202
+ meta: {
203
+ title: "Worker Version",
204
+ description: `Worker version with digest ${digest}`,
205
+ },
288
206
  apiKeyId: apiKey.id,
289
- }
207
+ },
208
+ })
209
+ }
290
210
 
291
- this.logger.info(
292
- { projectId, workerId: worker.id, image: unitWorker.image, identity, serviceAccountId },
293
- `creating worker "%s" for image "%s" in project "%s"`,
294
- worker.id,
295
- unitWorker.image,
296
- projectId,
297
- )
211
+ /**
212
+ * Performs cleanup of unused worker versions and syncs workers for a project.
213
+ * This method should be called after operations that may leave unused workers.
214
+ *
215
+ * @param projectId The ID of the project to cleanup and sync workers for.
216
+ */
217
+ async cleanupWorkerUsageAndSync(projectId: string): Promise<void> {
218
+ const database = await this.database.forProject(projectId)
219
+
220
+ await database.$transaction(async tx => {
221
+ await this.cleanupUnusedWorkerVersions(tx)
222
+ })
298
223
 
299
- await this.stateManager.getWorkerRepository(projectId).putItem(worker, batch)
300
- await batch.write()
224
+ // ensure all worker versions are started
225
+ void this.workerManager.syncWorkers(projectId)
226
+ }
301
227
 
302
- void this.workerManager.startWorker(projectId, worker.id)
228
+ private async cleanupUnusedWorkerVersions(tx: ProjectTransaction): Promise<void> {
229
+ const unused = await tx.workerVersion.findMany({
230
+ where: {
231
+ unitRegistrations: { none: {} },
232
+ },
233
+ select: { id: true },
234
+ })
303
235
 
304
- return worker.id
236
+ if (unused.length === 0) {
237
+ return
238
+ }
239
+
240
+ await tx.workerVersion.deleteMany({
241
+ where: { id: { in: unused.map(u => u.id) } },
305
242
  })
306
243
  }
307
244
 
308
- private async deleteWorkerIfHasNoRegistrations(
245
+ /**
246
+ * Updates the metadata for a worker version.
247
+ *
248
+ * @param projectId The ID of the project.
249
+ * @param workerVersionId The ID of the worker version to update.
250
+ * @param meta The new metadata to set.
251
+ */
252
+ async updateWorkerVersionMeta(
309
253
  projectId: string,
310
- workerId: string,
254
+ workerVersionId: string,
255
+ meta: CommonObjectMeta,
311
256
  ): Promise<void> {
312
- await this.lockManager.acquire(["worker", workerId], async () => {
313
- const registrations = await this.stateManager
314
- .getWorkerRegistrationIndexRepository(projectId, workerId)
315
- .getAllItems()
316
-
317
- if (registrations.length > 0) {
318
- // still has registrations, no need to delete
319
- this.logger.debug(
320
- { projectId, workerId, registrationCount: registrations.length },
321
- `worker "%s" still has %d registrations, not deleting`,
322
- workerId,
323
- registrations.length,
324
- )
325
- return
257
+ const database = await this.database.forProject(projectId)
258
+ const logger = createProjectLogger(this.logger, projectId)
259
+
260
+ try {
261
+ await database.workerVersion.update({
262
+ where: { id: workerVersionId },
263
+ data: { meta },
264
+ })
265
+ } catch (error) {
266
+ if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") {
267
+ throw new WorkerVersionNotFoundError(projectId, workerVersionId)
326
268
  }
327
269
 
328
- const worker = await this.stateManager.getWorkerRepository(projectId).get(workerId)
270
+ throw error
271
+ }
329
272
 
330
- if (!worker) {
331
- this.logger.warn(
332
- { projectId, workerId },
333
- `worker "%s" not found in project "%s" while deleting`,
334
- workerId,
335
- projectId,
336
- )
337
- return
338
- }
339
-
340
- const batch = this.stateManager.batch()
341
-
342
- this.logger.info(
343
- { projectId, workerId, image: worker.image, identity: worker.identity },
344
- `deleting worker "%s" with image "%s" in project "%s" (no registrations remaining)`,
345
- workerId,
346
- worker.image,
347
- projectId,
348
- )
349
-
350
- await this.stateManager.getWorkerRepository(projectId).delete(workerId, batch)
351
- await this.stateManager.getApiKeyRepository(projectId).delete(worker.apiKeyId, batch)
352
-
353
- const workers = await this.stateManager.getWorkerRepository(projectId).getAllItems()
354
- const hasSiblingWorker = workers.some(
355
- siblingWorker =>
356
- siblingWorker.identity === worker.identity && siblingWorker.id !== workerId,
357
- )
358
-
359
- if (!hasSiblingWorker) {
360
- this.logger.info(
361
- {
362
- projectId,
363
- workerId,
364
- serviceAccountId: worker.serviceAccountId,
365
- identity: worker.identity,
366
- },
367
- `deleting service account "%s" for worker "%s" in project "%s" (no sibling workers remaining)`,
368
- worker.serviceAccountId,
369
- workerId,
370
- projectId,
371
- )
372
-
373
- await this.stateManager
374
- .getServiceAccountRepository(projectId)
375
- .delete(worker.serviceAccountId, batch)
376
- } else {
377
- this.logger.debug(
378
- {
379
- projectId,
380
- workerId,
381
- serviceAccountId: worker.serviceAccountId,
382
- identity: worker.identity,
383
- },
384
- `not deleting service account "%s" for worker "%s" in project "%s" (has sibling workers)`,
385
- worker.serviceAccountId,
386
- workerId,
387
- projectId,
388
- )
389
- }
390
-
391
- // for now, we will keep the service account even if the last sibling worker is deleted
392
- await batch.write()
273
+ logger.info(`updated worker version metadata for "%s"`, workerVersionId)
274
+ }
393
275
 
394
- // stop the worker and clear logs after it
395
- this.workerManager.stopWorker(projectId, workerId)
276
+ /**
277
+ * Gets logs for a worker version.
278
+ *
279
+ * @param projectId The ID of the project.
280
+ * @param workerVersionId The ID of the worker version to get logs for.
281
+ */
282
+ async getWorkerVersionLogs(
283
+ projectId: string,
284
+ workerVersionId: string,
285
+ ): Promise<WorkerVersionLog[]> {
286
+ const database = await this.database.forProject(projectId)
287
+
288
+ return await database.workerVersionLog.findMany({
289
+ where: {
290
+ workerVersionId,
291
+ },
292
+ orderBy: {
293
+ id: "asc",
294
+ },
396
295
  })
397
296
  }
398
297
  }