@shipfox/api-runners 5.0.0 → 6.0.0

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 (414) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +61 -0
  3. package/dist/config.d.ts +5 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +53 -6
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/capacity-assignments.d.ts +7 -0
  8. package/dist/core/capacity-assignments.d.ts.map +1 -0
  9. package/dist/core/capacity-assignments.js +6 -0
  10. package/dist/core/capacity-assignments.js.map +1 -0
  11. package/dist/core/demand.d.ts +6 -6
  12. package/dist/core/demand.d.ts.map +1 -1
  13. package/dist/core/demand.js +10 -10
  14. package/dist/core/demand.js.map +1 -1
  15. package/dist/core/entities/capacity-assignment.d.ts +9 -0
  16. package/dist/core/entities/capacity-assignment.d.ts.map +1 -0
  17. package/dist/core/entities/capacity-assignment.js +3 -0
  18. package/dist/core/entities/capacity-assignment.js.map +1 -0
  19. package/dist/core/entities/ephemeral-registration-token.d.ts +1 -1
  20. package/dist/core/entities/ephemeral-registration-token.d.ts.map +1 -1
  21. package/dist/core/entities/ephemeral-registration-token.js.map +1 -1
  22. package/dist/core/entities/provisioner-capability-snapshot.d.ts +13 -0
  23. package/dist/core/entities/provisioner-capability-snapshot.d.ts.map +1 -0
  24. package/dist/core/entities/provisioner-capability-snapshot.js +3 -0
  25. package/dist/core/entities/provisioner-capability-snapshot.js.map +1 -0
  26. package/dist/core/entities/provisioner-token.d.ts +10 -2
  27. package/dist/core/entities/provisioner-token.d.ts.map +1 -1
  28. package/dist/core/entities/provisioner-token.js.map +1 -1
  29. package/dist/core/entities/{provisioned-runner.d.ts → runner-instance.d.ts} +6 -6
  30. package/dist/core/entities/runner-instance.d.ts.map +1 -0
  31. package/dist/core/entities/runner-instance.js +3 -0
  32. package/dist/core/entities/runner-instance.js.map +1 -0
  33. package/dist/core/entities/runner-session.d.ts +4 -2
  34. package/dist/core/entities/runner-session.d.ts.map +1 -1
  35. package/dist/core/entities/runner-session.js.map +1 -1
  36. package/dist/core/ephemeral-registration-tokens.d.ts +5 -5
  37. package/dist/core/ephemeral-registration-tokens.d.ts.map +1 -1
  38. package/dist/core/ephemeral-registration-tokens.js +12 -12
  39. package/dist/core/ephemeral-registration-tokens.js.map +1 -1
  40. package/dist/core/errors.d.ts +16 -4
  41. package/dist/core/errors.d.ts.map +1 -1
  42. package/dist/core/errors.js +22 -4
  43. package/dist/core/errors.js.map +1 -1
  44. package/dist/core/index.d.ts +10 -4
  45. package/dist/core/index.d.ts.map +1 -1
  46. package/dist/core/index.js +6 -2
  47. package/dist/core/index.js.map +1 -1
  48. package/dist/core/job-executions.d.ts +2 -0
  49. package/dist/core/job-executions.d.ts.map +1 -1
  50. package/dist/core/job-executions.js +1 -2
  51. package/dist/core/job-executions.js.map +1 -1
  52. package/dist/core/maintenance.d.ts +1 -1
  53. package/dist/core/maintenance.d.ts.map +1 -1
  54. package/dist/core/maintenance.js +5 -5
  55. package/dist/core/maintenance.js.map +1 -1
  56. package/dist/core/provisioner-capability-snapshots.d.ts +5 -0
  57. package/dist/core/provisioner-capability-snapshots.d.ts.map +1 -0
  58. package/dist/core/provisioner-capability-snapshots.js +10 -0
  59. package/dist/core/provisioner-capability-snapshots.js.map +1 -0
  60. package/dist/core/provisioner-tokens.d.ts +10 -0
  61. package/dist/core/provisioner-tokens.d.ts.map +1 -1
  62. package/dist/core/provisioner-tokens.js +23 -1
  63. package/dist/core/provisioner-tokens.js.map +1 -1
  64. package/dist/core/rate-limit.d.ts.map +1 -1
  65. package/dist/core/rate-limit.js +2 -7
  66. package/dist/core/rate-limit.js.map +1 -1
  67. package/dist/core/runner-activation.d.ts +13 -0
  68. package/dist/core/runner-activation.d.ts.map +1 -0
  69. package/dist/core/runner-activation.js +36 -0
  70. package/dist/core/runner-activation.js.map +1 -0
  71. package/dist/core/runner-control-sessions.d.ts +46 -0
  72. package/dist/core/runner-control-sessions.d.ts.map +1 -0
  73. package/dist/core/runner-control-sessions.js +121 -0
  74. package/dist/core/runner-control-sessions.js.map +1 -0
  75. package/dist/core/runner-instances.d.ts +79 -0
  76. package/dist/core/runner-instances.d.ts.map +1 -0
  77. package/dist/core/runner-instances.js +168 -0
  78. package/dist/core/runner-instances.js.map +1 -0
  79. package/dist/core/runner-sessions.d.ts +8 -2
  80. package/dist/core/runner-sessions.d.ts.map +1 -1
  81. package/dist/core/runner-sessions.js +8 -5
  82. package/dist/core/runner-sessions.js.map +1 -1
  83. package/dist/db/capacity-assignments.d.ts +7 -0
  84. package/dist/db/capacity-assignments.d.ts.map +1 -0
  85. package/dist/db/capacity-assignments.js +72 -0
  86. package/dist/db/capacity-assignments.js.map +1 -0
  87. package/dist/db/db.d.ts +2837 -1009
  88. package/dist/db/db.d.ts.map +1 -1
  89. package/dist/db/db.js +11 -2
  90. package/dist/db/db.js.map +1 -1
  91. package/dist/db/ephemeral-registration-tokens.d.ts +2 -2
  92. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
  93. package/dist/db/ephemeral-registration-tokens.js +21 -21
  94. package/dist/db/ephemeral-registration-tokens.js.map +1 -1
  95. package/dist/db/index.d.ts +7 -4
  96. package/dist/db/index.d.ts.map +1 -1
  97. package/dist/db/index.js +5 -2
  98. package/dist/db/index.js.map +1 -1
  99. package/dist/db/job-executions.d.ts +6 -6
  100. package/dist/db/job-executions.d.ts.map +1 -1
  101. package/dist/db/job-executions.js +20 -19
  102. package/dist/db/job-executions.js.map +1 -1
  103. package/dist/db/provisioner-capability-snapshots.d.ts +21 -0
  104. package/dist/db/provisioner-capability-snapshots.d.ts.map +1 -0
  105. package/dist/db/provisioner-capability-snapshots.js +57 -0
  106. package/dist/db/provisioner-capability-snapshots.js.map +1 -0
  107. package/dist/db/provisioner-tokens.d.ts +7 -2
  108. package/dist/db/provisioner-tokens.d.ts.map +1 -1
  109. package/dist/db/provisioner-tokens.js +59 -10
  110. package/dist/db/provisioner-tokens.js.map +1 -1
  111. package/dist/db/reservations.d.ts +18 -0
  112. package/dist/db/reservations.d.ts.map +1 -1
  113. package/dist/db/reservations.js +88 -1
  114. package/dist/db/reservations.js.map +1 -1
  115. package/dist/db/runner-instances.d.ts +91 -0
  116. package/dist/db/runner-instances.d.ts.map +1 -0
  117. package/dist/db/runner-instances.js +511 -0
  118. package/dist/db/runner-instances.js.map +1 -0
  119. package/dist/db/runner-sessions.d.ts +5 -0
  120. package/dist/db/runner-sessions.d.ts.map +1 -1
  121. package/dist/db/runner-sessions.js +48 -2
  122. package/dist/db/runner-sessions.js.map +1 -1
  123. package/dist/db/schema/capacity-assignments.d.ts +113 -0
  124. package/dist/db/schema/capacity-assignments.d.ts.map +1 -0
  125. package/dist/db/schema/capacity-assignments.js +29 -0
  126. package/dist/db/schema/capacity-assignments.js.map +1 -0
  127. package/dist/db/schema/ephemeral-registration-tokens.d.ts +2 -2
  128. package/dist/db/schema/ephemeral-registration-tokens.js +3 -3
  129. package/dist/db/schema/ephemeral-registration-tokens.js.map +1 -1
  130. package/dist/db/schema/provisioner-capability-snapshots.d.ts +207 -0
  131. package/dist/db/schema/provisioner-capability-snapshots.d.ts.map +1 -0
  132. package/dist/db/schema/provisioner-capability-snapshots.js +38 -0
  133. package/dist/db/schema/provisioner-capability-snapshots.js.map +1 -0
  134. package/dist/db/schema/provisioner-tokens.d.ts +19 -1
  135. package/dist/db/schema/provisioner-tokens.d.ts.map +1 -1
  136. package/dist/db/schema/provisioner-tokens.js +29 -3
  137. package/dist/db/schema/provisioner-tokens.js.map +1 -1
  138. package/dist/db/schema/runner-activation-tokens.d.ts +161 -0
  139. package/dist/db/schema/runner-activation-tokens.d.ts.map +1 -0
  140. package/dist/db/schema/runner-activation-tokens.js +27 -0
  141. package/dist/db/schema/runner-activation-tokens.js.map +1 -0
  142. package/dist/db/schema/runner-control-sessions.d.ts +338 -0
  143. package/dist/db/schema/runner-control-sessions.d.ts.map +1 -0
  144. package/dist/db/schema/runner-control-sessions.js +52 -0
  145. package/dist/db/schema/runner-control-sessions.js.map +1 -0
  146. package/dist/db/schema/{provisioned-runners.d.ts → runner-instances.d.ts} +88 -34
  147. package/dist/db/schema/runner-instances.d.ts.map +1 -0
  148. package/dist/db/schema/{provisioned-runners.js → runner-instances.js} +19 -14
  149. package/dist/db/schema/runner-instances.js.map +1 -0
  150. package/dist/db/schema/runner-sessions.d.ts +39 -5
  151. package/dist/db/schema/runner-sessions.d.ts.map +1 -1
  152. package/dist/db/schema/runner-sessions.js +13 -6
  153. package/dist/db/schema/runner-sessions.js.map +1 -1
  154. package/dist/db/schema/running-job-executions.d.ts +2 -2
  155. package/dist/db/schema/running-job-executions.js +4 -4
  156. package/dist/db/schema/running-job-executions.js.map +1 -1
  157. package/dist/index.d.ts +7 -2
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/index.js +45 -38
  160. package/dist/index.js.map +1 -1
  161. package/dist/installation-provisioning.d.ts +14 -0
  162. package/dist/installation-provisioning.d.ts.map +1 -0
  163. package/dist/installation-provisioning.js +7 -0
  164. package/dist/installation-provisioning.js.map +1 -0
  165. package/dist/metrics/index.d.ts +1 -1
  166. package/dist/metrics/index.d.ts.map +1 -1
  167. package/dist/metrics/index.js +1 -1
  168. package/dist/metrics/index.js.map +1 -1
  169. package/dist/metrics/instance.d.ts +11 -7
  170. package/dist/metrics/instance.d.ts.map +1 -1
  171. package/dist/metrics/instance.js +13 -7
  172. package/dist/metrics/instance.js.map +1 -1
  173. package/dist/presentation/auth/index.d.ts +1 -0
  174. package/dist/presentation/auth/index.d.ts.map +1 -1
  175. package/dist/presentation/auth/index.js +1 -0
  176. package/dist/presentation/auth/index.js.map +1 -1
  177. package/dist/presentation/auth/provisioner-token-auth.d.ts.map +1 -1
  178. package/dist/presentation/auth/provisioner-token-auth.js +5 -1
  179. package/dist/presentation/auth/provisioner-token-auth.js.map +1 -1
  180. package/dist/presentation/auth/runner-control-session-auth.d.ts +4 -0
  181. package/dist/presentation/auth/runner-control-session-auth.d.ts.map +1 -0
  182. package/dist/presentation/auth/runner-control-session-auth.js +29 -0
  183. package/dist/presentation/auth/runner-control-session-auth.js.map +1 -0
  184. package/dist/presentation/auth/runner-registration-token-auth.d.ts +5 -1
  185. package/dist/presentation/auth/runner-registration-token-auth.d.ts.map +1 -1
  186. package/dist/presentation/auth/runner-registration-token-auth.js +21 -1
  187. package/dist/presentation/auth/runner-registration-token-auth.js.map +1 -1
  188. package/dist/presentation/dto/index.d.ts +2 -2
  189. package/dist/presentation/dto/index.d.ts.map +1 -1
  190. package/dist/presentation/dto/index.js +2 -2
  191. package/dist/presentation/dto/index.js.map +1 -1
  192. package/dist/presentation/dto/mint-registration-tokens.d.ts +5 -3
  193. package/dist/presentation/dto/mint-registration-tokens.d.ts.map +1 -1
  194. package/dist/presentation/dto/mint-registration-tokens.js +4 -4
  195. package/dist/presentation/dto/mint-registration-tokens.js.map +1 -1
  196. package/dist/presentation/dto/poll-demand.d.ts.map +1 -1
  197. package/dist/presentation/dto/poll-demand.js +7 -1
  198. package/dist/presentation/dto/poll-demand.js.map +1 -1
  199. package/dist/presentation/dto/provisioner-token.d.ts +1 -0
  200. package/dist/presentation/dto/provisioner-token.d.ts.map +1 -1
  201. package/dist/presentation/dto/provisioner-token.js +5 -1
  202. package/dist/presentation/dto/provisioner-token.js.map +1 -1
  203. package/dist/presentation/dto/{provisioned-runners.d.ts → runner-instances.d.ts} +9 -9
  204. package/dist/presentation/dto/runner-instances.d.ts.map +1 -0
  205. package/dist/presentation/dto/{provisioned-runners.js → runner-instances.js} +6 -6
  206. package/dist/presentation/dto/runner-instances.js.map +1 -0
  207. package/dist/presentation/index.d.ts +2 -2
  208. package/dist/presentation/index.d.ts.map +1 -1
  209. package/dist/presentation/index.js +2 -2
  210. package/dist/presentation/index.js.map +1 -1
  211. package/dist/presentation/inter-module.d.ts +5 -0
  212. package/dist/presentation/inter-module.d.ts.map +1 -0
  213. package/dist/presentation/inter-module.js +43 -0
  214. package/dist/presentation/inter-module.js.map +1 -0
  215. package/dist/presentation/routes/assign-capacity.d.ts +2 -0
  216. package/dist/presentation/routes/assign-capacity.d.ts.map +1 -0
  217. package/dist/presentation/routes/assign-capacity.js +44 -0
  218. package/dist/presentation/routes/assign-capacity.js.map +1 -0
  219. package/dist/presentation/routes/get-provisioner-me.d.ts.map +1 -1
  220. package/dist/presentation/routes/get-provisioner-me.js +5 -1
  221. package/dist/presentation/routes/get-provisioner-me.js.map +1 -1
  222. package/dist/presentation/routes/heartbeat.d.ts +2 -1
  223. package/dist/presentation/routes/heartbeat.d.ts.map +1 -1
  224. package/dist/presentation/routes/heartbeat.js +60 -50
  225. package/dist/presentation/routes/heartbeat.js.map +1 -1
  226. package/dist/presentation/routes/index.d.ts +3 -1
  227. package/dist/presentation/routes/index.d.ts.map +1 -1
  228. package/dist/presentation/routes/index.js +83 -50
  229. package/dist/presentation/routes/index.js.map +1 -1
  230. package/dist/presentation/routes/mint-registration-tokens.d.ts.map +1 -1
  231. package/dist/presentation/routes/mint-registration-tokens.js +5 -5
  232. package/dist/presentation/routes/mint-registration-tokens.js.map +1 -1
  233. package/dist/presentation/routes/planned-capacity.d.ts +3 -0
  234. package/dist/presentation/routes/planned-capacity.d.ts.map +1 -0
  235. package/dist/presentation/routes/planned-capacity.js +54 -0
  236. package/dist/presentation/routes/planned-capacity.js.map +1 -0
  237. package/dist/presentation/routes/poll-demand.d.ts +2 -0
  238. package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
  239. package/dist/presentation/routes/poll-demand.js +86 -45
  240. package/dist/presentation/routes/poll-demand.js.map +1 -1
  241. package/dist/presentation/routes/reconcile-runner-instances.d.ts +2 -0
  242. package/dist/presentation/routes/reconcile-runner-instances.d.ts.map +1 -0
  243. package/dist/presentation/routes/reconcile-runner-instances.js +27 -0
  244. package/dist/presentation/routes/reconcile-runner-instances.js.map +1 -0
  245. package/dist/presentation/routes/register.d.ts +2 -1
  246. package/dist/presentation/routes/register.d.ts.map +1 -1
  247. package/dist/presentation/routes/register.js +55 -52
  248. package/dist/presentation/routes/register.js.map +1 -1
  249. package/dist/presentation/routes/report-runner-instances.d.ts +2 -0
  250. package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -0
  251. package/dist/presentation/routes/report-runner-instances.js +37 -0
  252. package/dist/presentation/routes/report-runner-instances.js.map +1 -0
  253. package/dist/presentation/routes/request-job.d.ts +2 -1
  254. package/dist/presentation/routes/request-job.d.ts.map +1 -1
  255. package/dist/presentation/routes/request-job.js +39 -36
  256. package/dist/presentation/routes/request-job.js.map +1 -1
  257. package/dist/presentation/routes/runner-enrollment.d.ts +7 -0
  258. package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -0
  259. package/dist/presentation/routes/runner-enrollment.js +185 -0
  260. package/dist/presentation/routes/runner-enrollment.js.map +1 -0
  261. package/dist/temporal/activities/index.d.ts +2 -2
  262. package/dist/temporal/activities/index.d.ts.map +1 -1
  263. package/dist/temporal/activities/index.js +2 -2
  264. package/dist/temporal/activities/index.js.map +1 -1
  265. package/dist/temporal/activities/maintenance-activities.d.ts +1 -1
  266. package/dist/temporal/activities/maintenance-activities.d.ts.map +1 -1
  267. package/dist/temporal/activities/maintenance-activities.js +3 -3
  268. package/dist/temporal/activities/maintenance-activities.js.map +1 -1
  269. package/dist/temporal/workflows/index.bundle.js +3 -3
  270. package/dist/temporal/workflows/stuck-job-detector.js +2 -2
  271. package/dist/temporal/workflows/stuck-job-detector.js.map +1 -1
  272. package/dist/tsconfig.test.tsbuildinfo +1 -1
  273. package/drizzle/0000_initial.sql +102 -24
  274. package/drizzle/meta/0000_snapshot.json +483 -63
  275. package/package.json +21 -16
  276. package/src/config.test.ts +0 -26
  277. package/src/config.ts +47 -6
  278. package/src/core/capacity-assignments.ts +10 -0
  279. package/src/core/demand.test.ts +29 -29
  280. package/src/core/demand.ts +22 -22
  281. package/src/core/entities/capacity-assignment.ts +8 -0
  282. package/src/core/entities/ephemeral-registration-token.ts +1 -1
  283. package/src/core/entities/provisioner-capability-snapshot.ts +12 -0
  284. package/src/core/entities/provisioner-token.ts +8 -2
  285. package/src/core/entities/{provisioned-runner.ts → runner-instance.ts} +5 -5
  286. package/src/core/entities/runner-session.ts +4 -2
  287. package/src/core/ephemeral-registration-tokens.test.ts +11 -11
  288. package/src/core/ephemeral-registration-tokens.ts +18 -20
  289. package/src/core/errors.ts +23 -4
  290. package/src/core/index.ts +39 -17
  291. package/src/core/job-executions.ts +3 -2
  292. package/src/core/maintenance.test.ts +13 -17
  293. package/src/core/maintenance.ts +5 -5
  294. package/src/core/provisioner-capability-snapshots.test.ts +35 -0
  295. package/src/core/provisioner-capability-snapshots.ts +12 -0
  296. package/src/core/provisioner-tokens.test.ts +20 -0
  297. package/src/core/provisioner-tokens.ts +33 -0
  298. package/src/core/rate-limit.test.ts +11 -27
  299. package/src/core/rate-limit.ts +3 -11
  300. package/src/core/runner-activation.ts +72 -0
  301. package/src/core/runner-control-sessions.ts +190 -0
  302. package/src/core/{provisioned-runners.test.ts → runner-instances.test.ts} +24 -24
  303. package/src/core/runner-instances.ts +310 -0
  304. package/src/core/runner-sessions.test.ts +112 -22
  305. package/src/core/runner-sessions.ts +25 -14
  306. package/src/db/capacity-assignments.test.ts +159 -0
  307. package/src/db/capacity-assignments.ts +116 -0
  308. package/src/db/db.ts +11 -2
  309. package/src/db/ephemeral-registration-tokens.test.ts +9 -9
  310. package/src/db/ephemeral-registration-tokens.ts +26 -26
  311. package/src/db/index.ts +31 -21
  312. package/src/db/job-executions.test.ts +13 -11
  313. package/src/db/job-executions.ts +33 -27
  314. package/src/db/provisioner-capability-snapshots.test.ts +175 -0
  315. package/src/db/provisioner-capability-snapshots.ts +130 -0
  316. package/src/db/provisioner-tokens.test.ts +139 -2
  317. package/src/db/provisioner-tokens.ts +123 -15
  318. package/src/db/reservations.test.ts +49 -0
  319. package/src/db/reservations.ts +146 -1
  320. package/src/db/{provisioned-runners.test.ts → runner-instances.test.ts} +429 -353
  321. package/src/db/runner-instances.ts +942 -0
  322. package/src/db/runner-sessions.test.ts +19 -6
  323. package/src/db/runner-sessions.ts +78 -2
  324. package/src/db/schema/capacity-assignments.ts +34 -0
  325. package/src/db/schema/ephemeral-registration-tokens.ts +4 -4
  326. package/src/db/schema/provisioner-capability-snapshots.ts +46 -0
  327. package/src/db/schema/provisioner-tokens.ts +33 -2
  328. package/src/db/schema/runner-activation-tokens.ts +22 -0
  329. package/src/db/schema/runner-control-sessions.ts +46 -0
  330. package/src/db/schema/{provisioned-runners.ts → runner-instances.ts} +26 -27
  331. package/src/db/schema/runner-sessions.ts +11 -7
  332. package/src/db/schema/running-job-executions.ts +5 -5
  333. package/src/index.test.ts +26 -0
  334. package/src/index.ts +39 -19
  335. package/src/installation-provisioning.ts +14 -0
  336. package/src/metrics/index.ts +5 -5
  337. package/src/metrics/instance.ts +25 -14
  338. package/src/presentation/auth/index.ts +4 -0
  339. package/src/presentation/auth/provisioner-token-auth.ts +8 -4
  340. package/src/presentation/auth/runner-control-session-auth.ts +31 -0
  341. package/src/presentation/auth/runner-registration-token-auth.ts +39 -3
  342. package/src/presentation/dto/index.ts +5 -5
  343. package/src/presentation/dto/mint-registration-tokens.ts +8 -15
  344. package/src/presentation/dto/poll-demand.ts +3 -1
  345. package/src/presentation/dto/provisioner-token.ts +5 -0
  346. package/src/presentation/dto/{provisioned-runners.ts → runner-instances.ts} +13 -15
  347. package/src/presentation/index.ts +2 -1
  348. package/src/presentation/inter-module.test.ts +15 -0
  349. package/src/presentation/inter-module.ts +53 -0
  350. package/src/presentation/routes/assign-capacity.ts +48 -0
  351. package/src/presentation/routes/get-provisioner-me.ts +3 -4
  352. package/src/presentation/routes/heartbeat.test.ts +4 -3
  353. package/src/presentation/routes/heartbeat.ts +67 -64
  354. package/src/presentation/routes/index.ts +93 -43
  355. package/src/presentation/routes/list-active-runners.test.ts +23 -23
  356. package/src/presentation/routes/manual-registration-tokens.test.ts +4 -3
  357. package/src/presentation/routes/mint-registration-tokens.test.ts +12 -12
  358. package/src/presentation/routes/mint-registration-tokens.ts +5 -7
  359. package/src/presentation/routes/planned-capacity.ts +49 -0
  360. package/src/presentation/routes/poll-demand-release.test.ts +4 -4
  361. package/src/presentation/routes/poll-demand.test.ts +161 -22
  362. package/src/presentation/routes/poll-demand.ts +105 -45
  363. package/src/presentation/routes/provisioner-me.test.ts +37 -3
  364. package/src/presentation/routes/{reconcile-provisioned-runners.test.ts → reconcile-runner-instances.test.ts} +66 -61
  365. package/src/presentation/routes/reconcile-runner-instances.ts +30 -0
  366. package/src/presentation/routes/register.test.ts +9 -5
  367. package/src/presentation/routes/register.ts +61 -57
  368. package/src/presentation/routes/{report-provisioned-runners.test.ts → report-runner-instances.test.ts} +28 -28
  369. package/src/presentation/routes/{report-provisioned-runners.ts → report-runner-instances.ts} +14 -14
  370. package/src/presentation/routes/request-job.test.ts +5 -3
  371. package/src/presentation/routes/request-job.ts +41 -37
  372. package/src/presentation/routes/runner-enrollment.test.ts +259 -0
  373. package/src/presentation/routes/runner-enrollment.ts +174 -0
  374. package/src/temporal/activities/index.ts +2 -2
  375. package/src/temporal/activities/maintenance-activities.test.ts +7 -7
  376. package/src/temporal/activities/maintenance-activities.ts +3 -3
  377. package/src/temporal/workflows/stuck-job-detector.test.ts +3 -3
  378. package/src/temporal/workflows/stuck-job-detector.ts +2 -2
  379. package/test/env.ts +1 -3
  380. package/test/factories/ephemeral-registration-token.ts +2 -2
  381. package/test/factories/provisioner-token.ts +3 -1
  382. package/test/factories/runner-instance.ts +57 -0
  383. package/test/factories/runner-session.ts +3 -1
  384. package/test/fixtures/auth-inter-module.ts +30 -0
  385. package/test/globalSetup.ts +5 -1
  386. package/test/index.ts +2 -1
  387. package/tsconfig.build.tsbuildinfo +1 -1
  388. package/dist/core/entities/provisioned-runner.d.ts.map +0 -1
  389. package/dist/core/entities/provisioned-runner.js +0 -3
  390. package/dist/core/entities/provisioned-runner.js.map +0 -1
  391. package/dist/core/provisioned-runners.d.ts +0 -67
  392. package/dist/core/provisioned-runners.d.ts.map +0 -1
  393. package/dist/core/provisioned-runners.js +0 -162
  394. package/dist/core/provisioned-runners.js.map +0 -1
  395. package/dist/db/provisioned-runners.d.ts +0 -79
  396. package/dist/db/provisioned-runners.d.ts.map +0 -1
  397. package/dist/db/provisioned-runners.js +0 -466
  398. package/dist/db/provisioned-runners.js.map +0 -1
  399. package/dist/db/schema/provisioned-runners.d.ts.map +0 -1
  400. package/dist/db/schema/provisioned-runners.js.map +0 -1
  401. package/dist/presentation/dto/provisioned-runners.d.ts.map +0 -1
  402. package/dist/presentation/dto/provisioned-runners.js.map +0 -1
  403. package/dist/presentation/routes/reconcile-provisioned-runners.d.ts +0 -2
  404. package/dist/presentation/routes/reconcile-provisioned-runners.d.ts.map +0 -1
  405. package/dist/presentation/routes/reconcile-provisioned-runners.js +0 -27
  406. package/dist/presentation/routes/reconcile-provisioned-runners.js.map +0 -1
  407. package/dist/presentation/routes/report-provisioned-runners.d.ts +0 -2
  408. package/dist/presentation/routes/report-provisioned-runners.d.ts.map +0 -1
  409. package/dist/presentation/routes/report-provisioned-runners.js +0 -37
  410. package/dist/presentation/routes/report-provisioned-runners.js.map +0 -1
  411. package/src/core/provisioned-runners.ts +0 -301
  412. package/src/db/provisioned-runners.ts +0 -886
  413. package/src/presentation/routes/reconcile-provisioned-runners.ts +0 -30
  414. package/test/factories/provisioned-runner.ts +0 -78
@@ -1,5 +1,5 @@
1
1
  import {hashOpaqueToken} from '@shipfox/node-tokens';
2
- import {sql} from 'drizzle-orm';
2
+ import {eq, sql} from 'drizzle-orm';
3
3
  import {db} from '#db/db.js';
4
4
  import {
5
5
  createProvisionerToken,
@@ -9,7 +9,12 @@ import {
9
9
  revokeProvisionerToken,
10
10
  touchProvisionerLastSeen,
11
11
  } from '#db/provisioner-tokens.js';
12
- import {provisionerTokenFactory} from '#test/index.js';
12
+ import {provisionerTokens} from '#db/schema/provisioner-tokens.js';
13
+ import {runnerActivationTokens} from '#db/schema/runner-activation-tokens.js';
14
+ import {runnerBootstrapTokens, runnerControlSessions} from '#db/schema/runner-control-sessions.js';
15
+ import {providerRunners} from '#db/schema/runner-instances.js';
16
+ import {runnerSessions} from '#db/schema/runner-sessions.js';
17
+ import {providerRunnerFactory, provisionerTokenFactory} from '#test/index.js';
13
18
 
14
19
  describe('provisioner token db', () => {
15
20
  it('creates and resolves a token', async () => {
@@ -17,6 +22,7 @@ describe('provisioner token db', () => {
17
22
  const rawToken = 'sf_pt_test-token';
18
23
  const createdByUserId = crypto.randomUUID();
19
24
  const token = await createProvisionerToken({
25
+ scope: 'workspace',
20
26
  workspaceId,
21
27
  hashedToken: hashOpaqueToken(rawToken),
22
28
  prefix: rawToken.slice(0, 12),
@@ -37,6 +43,28 @@ describe('provisioner token db', () => {
37
43
  expect(result).toBeUndefined();
38
44
  });
39
45
 
46
+ it('creates installation factory tokens without a workspace', async () => {
47
+ const token = await provisionerTokenFactory.create({scope: 'installation'});
48
+
49
+ expect(token).toMatchObject({scope: 'installation', workspaceId: null});
50
+ });
51
+
52
+ it('enforces valid scope and workspace combinations in PostgreSQL', async () => {
53
+ const insert = (scope: 'installation' | 'workspace', workspaceId: string | null) =>
54
+ db()
55
+ .insert(provisionerTokens)
56
+ .values({
57
+ scope,
58
+ workspaceId,
59
+ hashedToken: hashOpaqueToken(`${scope}-${workspaceId}`),
60
+ prefix: 'sf_pt_invalid',
61
+ createdByUserId: crypto.randomUUID(),
62
+ });
63
+
64
+ await expect(insert('installation', crypto.randomUUID())).rejects.toThrow();
65
+ await expect(insert('workspace', null)).rejects.toThrow();
66
+ });
67
+
40
68
  it('lists usable tokens for a workspace', async () => {
41
69
  const workspaceId = crypto.randomUUID();
42
70
  const usable = await provisionerTokenFactory.create({workspaceId, name: 'usable'});
@@ -74,6 +102,115 @@ describe('provisioner token db', () => {
74
102
  expect(revoked?.revokedByUserId).toBe(revokedByUserId);
75
103
  });
76
104
 
105
+ it("cascades revocation to the provisioner's unclaimed runner credentials and sessions", async () => {
106
+ const workspaceId = crypto.randomUUID();
107
+ const provisioner = await provisionerTokenFactory.create({workspaceId});
108
+ const runner = await providerRunnerFactory.create({
109
+ workspaceId,
110
+ provisionerId: provisioner.id,
111
+ runnerSessionId: null,
112
+ });
113
+ const future = new Date(Date.now() + 60_000);
114
+ const tokenValue = (name: string) => hashOpaqueToken(`${name}-${crypto.randomUUID()}`);
115
+ await db()
116
+ .insert(runnerBootstrapTokens)
117
+ .values({
118
+ runnerInstanceId: runner.id,
119
+ provisionerId: provisioner.id,
120
+ hashedToken: tokenValue('bootstrap'),
121
+ prefix: 'bootstrap',
122
+ expiresAt: future,
123
+ });
124
+ await db()
125
+ .insert(runnerControlSessions)
126
+ .values({
127
+ runnerInstanceId: runner.id,
128
+ provisionerId: provisioner.id,
129
+ hashedToken: tokenValue('control'),
130
+ prefix: 'control',
131
+ expiresAt: future,
132
+ });
133
+ await db()
134
+ .insert(runnerActivationTokens)
135
+ .values({
136
+ runnerInstanceId: runner.id,
137
+ hashedToken: tokenValue('activation'),
138
+ prefix: 'activation',
139
+ expiresAt: future,
140
+ });
141
+ const [unclaimedSession] = await db()
142
+ .insert(runnerSessions)
143
+ .values({
144
+ workspaceId,
145
+ scope: 'workspace',
146
+ registrationTokenId: crypto.randomUUID(),
147
+ registrationTokenKind: 'activation',
148
+ runnerInstanceId: runner.id,
149
+ provisionerId: provisioner.id,
150
+ providerRunnerId: runner.providerRunnerId,
151
+ labels: ['linux'],
152
+ maxClaims: 1,
153
+ claimsUsed: 0,
154
+ })
155
+ .returning();
156
+ const [claimedSession] = await db()
157
+ .insert(runnerSessions)
158
+ .values({
159
+ workspaceId,
160
+ scope: 'workspace',
161
+ registrationTokenId: crypto.randomUUID(),
162
+ registrationTokenKind: 'activation',
163
+ runnerInstanceId: crypto.randomUUID(),
164
+ provisionerId: provisioner.id,
165
+ providerRunnerId: crypto.randomUUID(),
166
+ labels: ['linux'],
167
+ maxClaims: 1,
168
+ claimsUsed: 1,
169
+ })
170
+ .returning();
171
+ if (!unclaimedSession || !claimedSession) throw new Error('Runner sessions were not created');
172
+
173
+ await revokeProvisionerToken({
174
+ tokenId: provisioner.id,
175
+ workspaceId,
176
+ revokedByUserId: crypto.randomUUID(),
177
+ });
178
+
179
+ const [bootstrap] = await db()
180
+ .select()
181
+ .from(runnerBootstrapTokens)
182
+ .where(eq(runnerBootstrapTokens.runnerInstanceId, runner.id));
183
+ const [control] = await db()
184
+ .select()
185
+ .from(runnerControlSessions)
186
+ .where(eq(runnerControlSessions.runnerInstanceId, runner.id));
187
+ const [activation] = await db()
188
+ .select()
189
+ .from(runnerActivationTokens)
190
+ .where(eq(runnerActivationTokens.runnerInstanceId, runner.id));
191
+ const [terminatedRunner] = await db()
192
+ .select()
193
+ .from(providerRunners)
194
+ .where(eq(providerRunners.id, runner.id));
195
+ const [revokedSession] = await db()
196
+ .select()
197
+ .from(runnerSessions)
198
+ .where(eq(runnerSessions.id, unclaimedSession.id));
199
+ const [preservedSession] = await db()
200
+ .select()
201
+ .from(runnerSessions)
202
+ .where(eq(runnerSessions.id, claimedSession.id));
203
+
204
+ expect(bootstrap?.revokedAt).toBeInstanceOf(Date);
205
+ expect(control).toMatchObject({closeReason: 'provisioner-revoked'});
206
+ expect(control?.closedAt).toBeInstanceOf(Date);
207
+ expect(activation?.revokedAt).toBeInstanceOf(Date);
208
+ expect(terminatedRunner).toMatchObject({state: 'terminated'});
209
+ expect(terminatedRunner?.terminatedAt).toBeInstanceOf(Date);
210
+ expect(revokedSession?.revokedAt).toBeInstanceOf(Date);
211
+ expect(preservedSession?.revokedAt).toBeNull();
212
+ });
213
+
77
214
  it('preserves the original revocation audit fields on repeat revoke', async () => {
78
215
  const workspaceId = crypto.randomUUID();
79
216
  const token = await provisionerTokenFactory.create({workspaceId});
@@ -1,10 +1,19 @@
1
- import {and, desc, eq, gt, isNull, or, sql} from 'drizzle-orm';
2
- import type {ActiveProvisionerToken, ProvisionerToken} from '#core/entities/provisioner-token.js';
1
+ import {and, desc, eq, gt, inArray, isNull, or, sql} from 'drizzle-orm';
2
+ import type {
3
+ ActiveProvisionerToken,
4
+ ProvisionerScope,
5
+ ProvisionerToken,
6
+ } from '#core/entities/provisioner-token.js';
3
7
  import {db} from './db.js';
4
8
  import {provisionerTokens, toProvisionerToken} from './schema/provisioner-tokens.js';
9
+ import {runnerActivationTokens} from './schema/runner-activation-tokens.js';
10
+ import {runnerBootstrapTokens, runnerControlSessions} from './schema/runner-control-sessions.js';
11
+ import {providerRunners} from './schema/runner-instances.js';
12
+ import {runnerSessions} from './schema/runner-sessions.js';
5
13
 
6
14
  export interface CreateProvisionerTokenParams {
7
- workspaceId: string;
15
+ scope: ProvisionerScope;
16
+ workspaceId?: string | undefined;
8
17
  hashedToken: string;
9
18
  prefix: string;
10
19
  createdByUserId: string;
@@ -12,13 +21,68 @@ export interface CreateProvisionerTokenParams {
12
21
  expiresAt?: Date | undefined;
13
22
  }
14
23
 
24
+ async function cascadeProvisionerRevocation(
25
+ tx: Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0],
26
+ provisionerId: string,
27
+ ) {
28
+ await tx
29
+ .update(runnerBootstrapTokens)
30
+ .set({revokedAt: sql`now()`})
31
+ .where(eq(runnerBootstrapTokens.provisionerId, provisionerId));
32
+ await tx
33
+ .update(runnerControlSessions)
34
+ .set({closedAt: sql`now()`, closeReason: 'provisioner-revoked'})
35
+ .where(
36
+ and(
37
+ eq(runnerControlSessions.provisionerId, provisionerId),
38
+ isNull(runnerControlSessions.closedAt),
39
+ ),
40
+ );
41
+ await tx
42
+ .update(runnerActivationTokens)
43
+ .set({revokedAt: sql`now()`})
44
+ .where(
45
+ and(
46
+ isNull(runnerActivationTokens.consumedAt),
47
+ isNull(runnerActivationTokens.revokedAt),
48
+ inArray(
49
+ runnerActivationTokens.runnerInstanceId,
50
+ tx
51
+ .select({id: providerRunners.id})
52
+ .from(providerRunners)
53
+ .where(eq(providerRunners.provisionerId, provisionerId)),
54
+ ),
55
+ ),
56
+ );
57
+ await tx
58
+ .update(providerRunners)
59
+ .set({state: 'terminated', terminatedAt: sql`now()`, updatedAt: sql`now()`})
60
+ .where(
61
+ and(
62
+ eq(providerRunners.provisionerId, provisionerId),
63
+ isNull(providerRunners.runnerSessionId),
64
+ ),
65
+ );
66
+ await tx
67
+ .update(runnerSessions)
68
+ .set({revokedAt: sql`now()`})
69
+ .where(
70
+ and(
71
+ eq(runnerSessions.provisionerId, provisionerId),
72
+ eq(runnerSessions.claimsUsed, 0),
73
+ isNull(runnerSessions.revokedAt),
74
+ ),
75
+ );
76
+ }
77
+
15
78
  export async function createProvisionerToken(
16
79
  params: CreateProvisionerTokenParams,
17
80
  ): Promise<ProvisionerToken> {
18
81
  const rows = await db()
19
82
  .insert(provisionerTokens)
20
83
  .values({
21
- workspaceId: params.workspaceId,
84
+ scope: params.scope,
85
+ workspaceId: params.workspaceId ?? null,
22
86
  hashedToken: params.hashedToken,
23
87
  prefix: params.prefix,
24
88
  createdByUserId: params.createdByUserId,
@@ -32,6 +96,42 @@ export async function createProvisionerToken(
32
96
  return toProvisionerToken(row);
33
97
  }
34
98
 
99
+ export async function revokeInstallationProvisionerToken(params: {
100
+ tokenId: string;
101
+ revokedByUserId: string;
102
+ }): Promise<ProvisionerToken | undefined> {
103
+ const rows = await db().transaction(async (tx) => {
104
+ const rows = await tx
105
+ .update(provisionerTokens)
106
+ .set({revokedAt: new Date(), revokedByUserId: params.revokedByUserId, updatedAt: new Date()})
107
+ .where(
108
+ and(
109
+ eq(provisionerTokens.id, params.tokenId),
110
+ eq(provisionerTokens.scope, 'installation'),
111
+ isNull(provisionerTokens.revokedAt),
112
+ ),
113
+ )
114
+ .returning();
115
+ if (rows[0]) {
116
+ await cascadeProvisionerRevocation(tx, params.tokenId);
117
+ }
118
+ return rows;
119
+ });
120
+
121
+ const row = rows[0];
122
+ if (row) return toProvisionerToken(row);
123
+
124
+ const existingRows = await db()
125
+ .select()
126
+ .from(provisionerTokens)
127
+ .where(
128
+ and(eq(provisionerTokens.id, params.tokenId), eq(provisionerTokens.scope, 'installation')),
129
+ )
130
+ .limit(1);
131
+ const existingRow = existingRows[0];
132
+ return existingRow ? toProvisionerToken(existingRow) : undefined;
133
+ }
134
+
35
135
  export async function listUsableProvisionerTokensByWorkspaceId(
36
136
  workspaceId: string,
37
137
  ): Promise<ProvisionerToken[]> {
@@ -42,6 +142,7 @@ export async function listUsableProvisionerTokensByWorkspaceId(
42
142
  .where(
43
143
  and(
44
144
  eq(provisionerTokens.workspaceId, workspaceId),
145
+ eq(provisionerTokens.scope, 'workspace'),
45
146
  isNull(provisionerTokens.revokedAt),
46
147
  or(isNull(provisionerTokens.expiresAt), gt(provisionerTokens.expiresAt, now)),
47
148
  ),
@@ -70,17 +171,22 @@ export async function revokeProvisionerToken(params: {
70
171
  workspaceId: string;
71
172
  revokedByUserId: string;
72
173
  }): Promise<ProvisionerToken | undefined> {
73
- const rows = await db()
74
- .update(provisionerTokens)
75
- .set({revokedAt: new Date(), revokedByUserId: params.revokedByUserId, updatedAt: new Date()})
76
- .where(
77
- and(
78
- eq(provisionerTokens.id, params.tokenId),
79
- eq(provisionerTokens.workspaceId, params.workspaceId),
80
- isNull(provisionerTokens.revokedAt),
81
- ),
82
- )
83
- .returning();
174
+ const rows = await db().transaction(async (tx) => {
175
+ const rows = await tx
176
+ .update(provisionerTokens)
177
+ .set({revokedAt: new Date(), revokedByUserId: params.revokedByUserId, updatedAt: new Date()})
178
+ .where(
179
+ and(
180
+ eq(provisionerTokens.id, params.tokenId),
181
+ eq(provisionerTokens.workspaceId, params.workspaceId),
182
+ eq(provisionerTokens.scope, 'workspace'),
183
+ isNull(provisionerTokens.revokedAt),
184
+ ),
185
+ )
186
+ .returning();
187
+ if (rows[0]) await cascadeProvisionerRevocation(tx, params.tokenId);
188
+ return rows;
189
+ });
84
190
 
85
191
  const row = rows[0];
86
192
  if (row) return toProvisionerToken(row);
@@ -92,6 +198,7 @@ export async function revokeProvisionerToken(params: {
92
198
  and(
93
199
  eq(provisionerTokens.id, params.tokenId),
94
200
  eq(provisionerTokens.workspaceId, params.workspaceId),
201
+ eq(provisionerTokens.scope, 'workspace'),
95
202
  ),
96
203
  )
97
204
  .limit(1);
@@ -129,6 +236,7 @@ export async function listActiveProvisionerTokens(params: {
129
236
  .where(
130
237
  and(
131
238
  eq(provisionerTokens.workspaceId, params.workspaceId),
239
+ eq(provisionerTokens.scope, 'workspace'),
132
240
  isNull(provisionerTokens.revokedAt),
133
241
  or(isNull(provisionerTokens.expiresAt), gt(provisionerTokens.expiresAt, sql`now()`)),
134
242
  sql`${provisionerTokens.lastSeenAt} > now() - (${params.windowSeconds} || ' seconds')::interval`,
@@ -3,6 +3,7 @@ import {db} from '#db/db.js';
3
3
  import {
4
4
  deleteReservationsByIds,
5
5
  pollDemandAndReserve,
6
+ pollInstallationDemandAndReserve,
6
7
  releaseReservationUnits,
7
8
  } from '#db/reservations.js';
8
9
  import {reservations} from '#db/schema/reservations.js';
@@ -75,6 +76,54 @@ describe('pollDemandAndReserve', () => {
75
76
  expect(reserved).toBeLessThanOrEqual(5);
76
77
  });
77
78
 
79
+ it('allocates eligible workspace heads in oldest-demand order without overselling templates', async () => {
80
+ const olderWorkspaceId = crypto.randomUUID();
81
+ const newerWorkspaceId = crypto.randomUUID();
82
+ await pendingJobFactory.create({workspaceId: olderWorkspaceId, requiredLabels: ['linux']});
83
+ await pendingJobFactory.create({workspaceId: newerWorkspaceId, requiredLabels: ['linux']});
84
+
85
+ const result = await pollInstallationDemandAndReserve({
86
+ provisionerId,
87
+ maxReservations: 5,
88
+ ttlSeconds: 60,
89
+ templates: [template('linux', ['linux'], 1)],
90
+ capabilityWindowSeconds: 60,
91
+ eligibleWorkspaceIds: new Set([olderWorkspaceId, newerWorkspaceId]),
92
+ });
93
+
94
+ expect(result.reservations).toEqual([
95
+ expect.objectContaining({workspaceId: olderWorkspaceId, labels: ['linux'], count: 1}),
96
+ ]);
97
+ });
98
+
99
+ it('reports committed installation grants before an aborted poll stops allocating', async () => {
100
+ const firstWorkspaceId = crypto.randomUUID();
101
+ const secondWorkspaceId = crypto.randomUUID();
102
+ const abortController = new AbortController();
103
+ const reportedReservations: string[] = [];
104
+ await pendingJobFactory.create({workspaceId: firstWorkspaceId, requiredLabels: ['linux']});
105
+ await pendingJobFactory.create({workspaceId: secondWorkspaceId, requiredLabels: ['linux']});
106
+
107
+ const result = await pollInstallationDemandAndReserve({
108
+ provisionerId,
109
+ maxReservations: 2,
110
+ ttlSeconds: 60,
111
+ templates: [template('linux', ['linux'], 2)],
112
+ capabilityWindowSeconds: 60,
113
+ eligibleWorkspaceIds: new Set([firstWorkspaceId, secondWorkspaceId]),
114
+ signal: abortController.signal,
115
+ onReservations: (reservations) => {
116
+ reportedReservations.push(...reservations.map((reservation) => reservation.reservationId));
117
+ abortController.abort();
118
+ },
119
+ });
120
+
121
+ expect(result.reservations).toHaveLength(1);
122
+ expect(reportedReservations).toEqual(
123
+ result.reservations.map((reservation) => reservation.reservationId),
124
+ );
125
+ });
126
+
78
127
  it('does not count expired reservations against demand', async () => {
79
128
  await createPendingJobs(1, ['linux']);
80
129
  await reservationFactory.create({
@@ -3,6 +3,8 @@ import {and, asc, eq, gt, inArray, lt, sql} from 'drizzle-orm';
3
3
  import type {Tx} from './db.js';
4
4
  import {db} from './db.js';
5
5
  import {pendingJobExecutions} from './schema/pending-job-executions.js';
6
+ import {provisionerCapabilitySnapshots} from './schema/provisioner-capability-snapshots.js';
7
+ import {provisionerTokens} from './schema/provisioner-tokens.js';
6
8
  import {reservations} from './schema/reservations.js';
7
9
 
8
10
  export interface ReservationTemplate {
@@ -14,6 +16,7 @@ export interface ReservationTemplate {
14
16
  }
15
17
 
16
18
  export interface DemandStat {
19
+ workspaceId?: string;
17
20
  labels: string[];
18
21
  queued: number;
19
22
  reserved: number;
@@ -22,6 +25,7 @@ export interface DemandStat {
22
25
 
23
26
  export interface ReservationGrant {
24
27
  reservationId: string;
28
+ workspaceId?: string;
25
29
  labels: string[];
26
30
  count: number;
27
31
  expiresAt: Date;
@@ -33,6 +37,18 @@ export interface PollDemandAndReserveParams {
33
37
  maxReservations: number;
34
38
  ttlSeconds: number;
35
39
  templates: ReservationTemplate[];
40
+ capabilityWindowSeconds?: number;
41
+ }
42
+
43
+ export interface InstallationPollDemandAndReserveParams {
44
+ provisionerId: string;
45
+ maxReservations: number;
46
+ ttlSeconds: number;
47
+ templates: ReservationTemplate[];
48
+ capabilityWindowSeconds: number;
49
+ eligibleWorkspaceIds: ReadonlySet<string>;
50
+ signal?: AbortSignal;
51
+ onReservations?: (reservations: ReservationGrant[]) => void;
36
52
  }
37
53
 
38
54
  interface NormalizedTemplate {
@@ -60,8 +76,75 @@ export async function pollDemandAndReserveTx(
60
76
  params: PollDemandAndReserveParams,
61
77
  ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
62
78
  await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);
79
+ return await pollDemandAndReserveLockedTx(tx, params);
80
+ }
81
+
82
+ export async function pollInstallationDemandAndReserve(
83
+ params: InstallationPollDemandAndReserveParams,
84
+ ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
85
+ const candidateWorkspaceIds = await listInstallationDemandWorkspaceIds(
86
+ params.eligibleWorkspaceIds,
87
+ );
88
+ const results: Array<{stats: DemandStat[]; reservations: ReservationGrant[]}> = [];
89
+ let remainingMaxReservations = params.maxReservations;
90
+ const remainingTemplates = params.templates.map((template) => ({
91
+ ...template,
92
+ labels: [...canonicalizeLabels(template.labels)],
93
+ remainingSlots: template.availableSlots,
94
+ }));
95
+ for (const workspaceId of candidateWorkspaceIds) {
96
+ if (params.signal?.aborted || remainingMaxReservations === 0) break;
97
+ const result = await db().transaction(async (tx) => {
98
+ const lockResult = await tx.execute<{locked: boolean}>(
99
+ sql`select pg_try_advisory_xact_lock(hashtext(${workspaceId})) as locked`,
100
+ );
101
+ const locked = lockResult.rows[0];
102
+ if (!locked?.locked) return {stats: [], reservations: []};
103
+ return await pollDemandAndReserveLockedTx(tx, {
104
+ workspaceId,
105
+ provisionerId: params.provisionerId,
106
+ maxReservations: remainingMaxReservations,
107
+ ttlSeconds: params.ttlSeconds,
108
+ templates: remainingTemplates.map((template) => ({
109
+ ...template,
110
+ availableSlots: template.remainingSlots,
111
+ })),
112
+ capabilityWindowSeconds: params.capabilityWindowSeconds,
113
+ });
114
+ });
115
+ results.push(result);
116
+ consumeInstallationTemplateSlots(remainingTemplates, result.reservations);
117
+ params.onReservations?.(result.reservations);
118
+ remainingMaxReservations -= result.reservations.reduce(
119
+ (total, reservation) => total + reservation.count,
120
+ 0,
121
+ );
122
+ }
123
+ return {
124
+ stats: results.flatMap((result) => result.stats),
125
+ reservations: results.flatMap((result) => result.reservations),
126
+ };
127
+ }
63
128
 
64
- const demandRows = (
129
+ function consumeInstallationTemplateSlots(
130
+ templates: NormalizedTemplate[],
131
+ reservations: ReservationGrant[],
132
+ ): void {
133
+ for (const reservation of reservations) {
134
+ const satisfyingTemplates = templates
135
+ .filter((template) => isSubset(reservation.labels, template.labels))
136
+ .sort(
137
+ (a, b) => a.labels.length - b.labels.length || a.templateKey.localeCompare(b.templateKey),
138
+ );
139
+ drawSlots(satisfyingTemplates, reservation.count);
140
+ }
141
+ }
142
+
143
+ async function pollDemandAndReserveLockedTx(
144
+ tx: Tx,
145
+ params: PollDemandAndReserveParams,
146
+ ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
147
+ let demandRows = (
65
148
  await tx
66
149
  .select({
67
150
  requiredLabels: pendingJobExecutions.requiredLabels,
@@ -75,6 +158,15 @@ export async function pollDemandAndReserveTx(
75
158
  ...row,
76
159
  oldestQueuedAt: new Date(row.oldestQueuedAt),
77
160
  }));
161
+ if (params.capabilityWindowSeconds !== undefined) {
162
+ const capabilityLabels = await listActiveWorkspaceCapabilityLabelsTx(tx, {
163
+ workspaceId: params.workspaceId,
164
+ windowSeconds: params.capabilityWindowSeconds,
165
+ });
166
+ demandRows = demandRows.filter(
167
+ (demand) => !capabilityLabels.some((labels) => isSubset(demand.requiredLabels, labels)),
168
+ );
169
+ }
78
170
 
79
171
  const activeReservationRows = await tx
80
172
  .select({
@@ -114,6 +206,8 @@ export async function pollDemandAndReserveTx(
114
206
  const stats: DemandStat[] = [];
115
207
  const grants: ReservationGrant[] = [];
116
208
  let remainingMaxReservations = params.maxReservations;
209
+ const workspaceIdField =
210
+ params.capabilityWindowSeconds === undefined ? {} : {workspaceId: params.workspaceId};
117
211
 
118
212
  for (const demand of sortDemandRows(demandRows)) {
119
213
  const satisfyingTemplates = templates
@@ -152,6 +246,7 @@ export async function pollDemandAndReserveTx(
152
246
  reservedAfterGrant += grant;
153
247
  grants.push({
154
248
  reservationId: inserted.id,
249
+ ...workspaceIdField,
155
250
  labels: demand.requiredLabels,
156
251
  count: grant,
157
252
  expiresAt: inserted.expiresAt,
@@ -159,6 +254,7 @@ export async function pollDemandAndReserveTx(
159
254
  }
160
255
 
161
256
  stats.push({
257
+ ...workspaceIdField,
162
258
  labels: demand.requiredLabels,
163
259
  queued: demand.queued,
164
260
  reserved: reservedAfterGrant,
@@ -169,6 +265,55 @@ export async function pollDemandAndReserveTx(
169
265
  return {stats, reservations: grants};
170
266
  }
171
267
 
268
+ async function listInstallationDemandWorkspaceIds(eligibleWorkspaceIds: ReadonlySet<string>) {
269
+ if (eligibleWorkspaceIds.size === 0) return [];
270
+ const rows = await db()
271
+ .select({
272
+ workspaceId: pendingJobExecutions.workspaceId,
273
+ oldestQueuedAt: sql<Date>`min(${pendingJobExecutions.createdAt})`,
274
+ })
275
+ .from(pendingJobExecutions)
276
+ .where(inArray(pendingJobExecutions.workspaceId, [...eligibleWorkspaceIds]))
277
+ .groupBy(pendingJobExecutions.workspaceId)
278
+ .orderBy(
279
+ asc(sql`min(${pendingJobExecutions.createdAt})`),
280
+ asc(pendingJobExecutions.workspaceId),
281
+ );
282
+ return rows.map((row) => row.workspaceId);
283
+ }
284
+
285
+ export async function listQueuedDemandWorkspaceIds(): Promise<string[]> {
286
+ const rows = await db()
287
+ .select({workspaceId: pendingJobExecutions.workspaceId})
288
+ .from(pendingJobExecutions)
289
+ .groupBy(pendingJobExecutions.workspaceId);
290
+ return rows.map((row) => row.workspaceId);
291
+ }
292
+
293
+ async function listActiveWorkspaceCapabilityLabelsTx(
294
+ tx: Tx,
295
+ params: {workspaceId: string; windowSeconds: number},
296
+ ): Promise<string[][]> {
297
+ const rows = await tx
298
+ .select({labels: provisionerCapabilitySnapshots.labels})
299
+ .from(provisionerCapabilitySnapshots)
300
+ .innerJoin(
301
+ provisionerTokens,
302
+ eq(provisionerTokens.id, provisionerCapabilitySnapshots.provisionerId),
303
+ )
304
+ .where(
305
+ and(
306
+ eq(provisionerCapabilitySnapshots.workspaceId, params.workspaceId),
307
+ eq(provisionerTokens.workspaceId, params.workspaceId),
308
+ eq(provisionerTokens.scope, 'workspace'),
309
+ sql`${provisionerCapabilitySnapshots.advertisedAt} > now() - (${params.windowSeconds} || ' seconds')::interval`,
310
+ sql`${provisionerTokens.revokedAt} is null`,
311
+ sql`(${provisionerTokens.expiresAt} is null or ${provisionerTokens.expiresAt} > now())`,
312
+ ),
313
+ );
314
+ return rows.map((row) => row.labels);
315
+ }
316
+
172
317
  export async function deleteExpiredReservations(params?: {limit?: number}): Promise<number> {
173
318
  const expiredIds = db()
174
319
  .select({id: reservations.id})