@rikalabs/akter 0.1.0-alpha.0 → 0.1.0-alpha.3

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 (308) hide show
  1. package/CHANGELOG.md +46 -3
  2. package/README.md +2 -2
  3. package/dist/actor/definition.d.ts +13 -9
  4. package/dist/actor/descriptor.d.ts +2 -2
  5. package/dist/actor/descriptor.js +4 -4
  6. package/dist/client/calls.js +1 -1
  7. package/dist/client/index.d.ts +1 -1
  8. package/dist/client/index.js +1 -1
  9. package/dist/errors/actor.d.ts +12 -42
  10. package/dist/errors/actor.js +13 -42
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/members/payload.d.ts +1 -1
  14. package/dist/protocol/inspection.d.ts +221 -1
  15. package/dist/protocol/inspection.js +151 -2
  16. package/dist/protocol/wire.d.ts +6 -49
  17. package/dist/protocol/wire.js +1 -10
  18. package/dist/runtime/actors.d.ts +3 -0
  19. package/dist/runtime/adoption/backfill.js +1 -1
  20. package/dist/runtime/adoption/enforce.js +2 -2
  21. package/dist/runtime/adoption/observe.js +1 -1
  22. package/dist/runtime/adoption/plan.d.ts +1 -1
  23. package/dist/runtime/adoption/startup.js +1 -1
  24. package/dist/runtime/adoption/status.d.ts +1 -1
  25. package/dist/runtime/app.d.ts +65 -0
  26. package/dist/runtime/app.js +38 -0
  27. package/dist/runtime/connections/holder.d.ts +11 -1
  28. package/dist/runtime/connections/holder.js +8 -4
  29. package/dist/runtime/connections/streams.d.ts +4 -4
  30. package/dist/runtime/content/store.js +15 -11
  31. package/dist/runtime/database/bounded.d.ts +4 -18
  32. package/dist/runtime/database/bounded.js +36 -22
  33. package/dist/runtime/database/coordination.d.ts +12 -3
  34. package/dist/runtime/database/coordination.js +29 -36
  35. package/dist/runtime/database/migrations.d.ts +61 -29
  36. package/dist/runtime/database/migrations.js +375 -139
  37. package/dist/runtime/database/replica.d.ts +4 -1
  38. package/dist/runtime/database/replica.js +4 -1
  39. package/dist/runtime/database/schema.d.ts +13 -0
  40. package/dist/runtime/database/schema.js +13 -0
  41. package/dist/runtime/database/shards.d.ts +31 -13
  42. package/dist/runtime/database/shards.js +38 -26
  43. package/dist/runtime/entity/register.d.ts +8 -1
  44. package/dist/runtime/entity/register.js +58 -7
  45. package/dist/runtime/fleet/checks.js +4 -4
  46. package/dist/runtime/fleet/maintainer.d.ts +1 -1
  47. package/dist/runtime/fleet/maintainer.js +1 -1
  48. package/dist/runtime/index.d.ts +2 -0
  49. package/dist/runtime/index.js +1 -0
  50. package/dist/runtime/inspector/layer.d.ts +20 -0
  51. package/dist/runtime/inspector/layer.js +224 -5
  52. package/dist/runtime/inspector/queries.d.ts +54 -8
  53. package/dist/runtime/inspector/queries.js +84 -15
  54. package/dist/runtime/layer.d.ts +48 -39
  55. package/dist/runtime/layer.js +124 -56
  56. package/dist/runtime/operators/audit.d.ts +3 -0
  57. package/dist/runtime/operators/audit.js +6 -2
  58. package/dist/runtime/operators/repair.js +30 -10
  59. package/dist/runtime/operators/seed.js +4 -2
  60. package/dist/runtime/payloads/versions.d.ts +4 -4
  61. package/dist/runtime/payloads/versions.js +24 -15
  62. package/dist/runtime/queries.js +4 -2
  63. package/dist/runtime/registration.d.ts +5 -2
  64. package/dist/runtime/registration.js +3 -3
  65. package/dist/runtime/runner.d.ts +12 -0
  66. package/dist/runtime/runner.js +12 -0
  67. package/dist/runtime/storage/codec.d.ts +19 -7
  68. package/dist/runtime/storage/codec.js +23 -7
  69. package/dist/runtime/storage/placements.d.ts +6 -1
  70. package/dist/runtime/storage/placements.js +98 -6
  71. package/dist/runtime/storage/retention.d.ts +6 -0
  72. package/dist/runtime/storage/retention.js +25 -13
  73. package/dist/runtime/subscriptions/relay.js +47 -44
  74. package/dist/runtime/subscriptions/storage.d.ts +3 -2
  75. package/dist/runtime/subscriptions/storage.js +9 -6
  76. package/dist/runtime/telemetry/live.d.ts +255 -0
  77. package/dist/runtime/telemetry/live.js +540 -0
  78. package/dist/runtime/telemetry/metrics.d.ts +2 -0
  79. package/dist/runtime/telemetry/metrics.js +4 -1
  80. package/dist/runtime/telemetry/sampler.js +33 -13
  81. package/dist/runtime/telemetry/spans.d.ts +1 -1
  82. package/dist/runtime/telemetry/spans.js +1 -1
  83. package/dist/runtime/topology/locks.d.ts +6 -3
  84. package/dist/runtime/topology/locks.js +27 -4
  85. package/dist/runtime/turn/execute.d.ts +2 -0
  86. package/dist/runtime/turn/execute.js +5 -2
  87. package/dist/runtime/turn/pipeline.d.ts +5 -12
  88. package/dist/runtime/turn/pipeline.js +11 -11
  89. package/dist/runtime/turn/relay.d.ts +13 -3
  90. package/dist/runtime/turn/relay.js +27 -14
  91. package/dist/runtime/turn/rows.js +4 -4
  92. package/dist/runtime/workflows/compatibility.d.ts +1 -1
  93. package/dist/runtime/workflows/compatibility.js +85 -31
  94. package/dist/runtime/workflows/engine.js +20 -17
  95. package/dist/serve/layer.js +3 -2
  96. package/dist/tables/owned.d.ts +1 -1
  97. package/dist/tables/owned.js +1 -1
  98. package/dist/testing/actor-test.d.ts +2 -24
  99. package/dist/testing/actor-test.js +1 -27
  100. package/dist/testing/index.d.ts +0 -12
  101. package/dist/testing/index.js +0 -6
  102. package/package.json +1 -1
  103. package/dist/runtime/database/neki/migrations.d.ts +0 -31
  104. package/dist/runtime/database/neki/migrations.js +0 -276
  105. package/dist/runtime/database/neki/session.d.ts +0 -23
  106. package/dist/runtime/database/neki/session.js +0 -31
  107. package/dist/testing/cluster.d.ts +0 -93
  108. package/dist/testing/cluster.js +0 -296
  109. package/dist/testing/conformance/access.d.ts +0 -5
  110. package/dist/testing/conformance/access.js +0 -531
  111. package/dist/testing/conformance/admission.d.ts +0 -11
  112. package/dist/testing/conformance/admission.js +0 -182
  113. package/dist/testing/conformance/adoption.d.ts +0 -2
  114. package/dist/testing/conformance/adoption.js +0 -1019
  115. package/dist/testing/conformance/assertions.d.ts +0 -202
  116. package/dist/testing/conformance/assertions.js +0 -849
  117. package/dist/testing/conformance/batches.d.ts +0 -41
  118. package/dist/testing/conformance/batches.js +0 -388
  119. package/dist/testing/conformance/blobs.d.ts +0 -14
  120. package/dist/testing/conformance/blobs.js +0 -405
  121. package/dist/testing/conformance/capacity.d.ts +0 -8
  122. package/dist/testing/conformance/capacity.js +0 -433
  123. package/dist/testing/conformance/client.d.ts +0 -5
  124. package/dist/testing/conformance/client.js +0 -719
  125. package/dist/testing/conformance/cold-serve.d.ts +0 -16
  126. package/dist/testing/conformance/cold-serve.js +0 -254
  127. package/dist/testing/conformance/connections/actors.d.ts +0 -1143
  128. package/dist/testing/conformance/connections/actors.js +0 -180
  129. package/dist/testing/conformance/connections/harness.d.ts +0 -1754
  130. package/dist/testing/conformance/connections/harness.js +0 -142
  131. package/dist/testing/conformance/connections/holders.d.ts +0 -4
  132. package/dist/testing/conformance/connections/holders.js +0 -165
  133. package/dist/testing/conformance/connections/lifecycle.d.ts +0 -4
  134. package/dist/testing/conformance/connections/lifecycle.js +0 -113
  135. package/dist/testing/conformance/connections/limits.d.ts +0 -4
  136. package/dist/testing/conformance/connections/limits.js +0 -127
  137. package/dist/testing/conformance/connections/reauthorization.d.ts +0 -4
  138. package/dist/testing/conformance/connections/reauthorization.js +0 -170
  139. package/dist/testing/conformance/connections/resync.d.ts +0 -4
  140. package/dist/testing/conformance/connections/resync.js +0 -360
  141. package/dist/testing/conformance/connections/takeover.d.ts +0 -4
  142. package/dist/testing/conformance/connections/takeover.js +0 -287
  143. package/dist/testing/conformance/connections.d.ts +0 -4
  144. package/dist/testing/conformance/connections.js +0 -15
  145. package/dist/testing/conformance/content-blobs.d.ts +0 -16
  146. package/dist/testing/conformance/content-blobs.js +0 -534
  147. package/dist/testing/conformance/counter.d.ts +0 -16
  148. package/dist/testing/conformance/counter.js +0 -813
  149. package/dist/testing/conformance/cron.d.ts +0 -5
  150. package/dist/testing/conformance/cron.js +0 -665
  151. package/dist/testing/conformance/drain.d.ts +0 -26
  152. package/dist/testing/conformance/drain.js +0 -540
  153. package/dist/testing/conformance/effect-control.d.ts +0 -38
  154. package/dist/testing/conformance/effect-control.js +0 -1059
  155. package/dist/testing/conformance/effects.d.ts +0 -23
  156. package/dist/testing/conformance/effects.js +0 -432
  157. package/dist/testing/conformance/events.d.ts +0 -28
  158. package/dist/testing/conformance/events.js +0 -335
  159. package/dist/testing/conformance/export.d.ts +0 -2
  160. package/dist/testing/conformance/export.js +0 -368
  161. package/dist/testing/conformance/fleet.d.ts +0 -444
  162. package/dist/testing/conformance/fleet.js +0 -1009
  163. package/dist/testing/conformance/groups.d.ts +0 -7
  164. package/dist/testing/conformance/groups.js +0 -353
  165. package/dist/testing/conformance/heap.d.ts +0 -3
  166. package/dist/testing/conformance/heap.js +0 -149
  167. package/dist/testing/conformance/http.d.ts +0 -2809
  168. package/dist/testing/conformance/http.js +0 -1234
  169. package/dist/testing/conformance/inspection-views.d.ts +0 -8
  170. package/dist/testing/conformance/inspection-views.js +0 -272
  171. package/dist/testing/conformance/inspector.d.ts +0 -7
  172. package/dist/testing/conformance/inspector.js +0 -650
  173. package/dist/testing/conformance/mint.d.ts +0 -58
  174. package/dist/testing/conformance/mint.js +0 -692
  175. package/dist/testing/conformance/multi-runner.d.ts +0 -8
  176. package/dist/testing/conformance/multi-runner.js +0 -403
  177. package/dist/testing/conformance/neki/plan.d.ts +0 -19
  178. package/dist/testing/conformance/neki/plan.js +0 -30
  179. package/dist/testing/conformance/observability.d.ts +0 -3
  180. package/dist/testing/conformance/observability.js +0 -343
  181. package/dist/testing/conformance/offline.d.ts +0 -4
  182. package/dist/testing/conformance/offline.js +0 -474
  183. package/dist/testing/conformance/operator-harness.d.ts +0 -57
  184. package/dist/testing/conformance/operator-harness.js +0 -78
  185. package/dist/testing/conformance/operator.d.ts +0 -3
  186. package/dist/testing/conformance/operator.js +0 -379
  187. package/dist/testing/conformance/outbox-cross-shard.d.ts +0 -14
  188. package/dist/testing/conformance/outbox-cross-shard.js +0 -160
  189. package/dist/testing/conformance/outbox.d.ts +0 -36
  190. package/dist/testing/conformance/outbox.js +0 -437
  191. package/dist/testing/conformance/payload-migrations.d.ts +0 -3
  192. package/dist/testing/conformance/payload-migrations.js +0 -612
  193. package/dist/testing/conformance/pipeline.d.ts +0 -3
  194. package/dist/testing/conformance/pipeline.js +0 -1040
  195. package/dist/testing/conformance/placement.d.ts +0 -22
  196. package/dist/testing/conformance/placement.js +0 -539
  197. package/dist/testing/conformance/platform.d.ts +0 -40
  198. package/dist/testing/conformance/platform.js +0 -101
  199. package/dist/testing/conformance/progress.d.ts +0 -39
  200. package/dist/testing/conformance/progress.js +0 -666
  201. package/dist/testing/conformance/properties.d.ts +0 -6
  202. package/dist/testing/conformance/properties.js +0 -295
  203. package/dist/testing/conformance/protocols.d.ts +0 -4
  204. package/dist/testing/conformance/protocols.js +0 -618
  205. package/dist/testing/conformance/provider/advertise.d.ts +0 -21
  206. package/dist/testing/conformance/provider/advertise.js +0 -44
  207. package/dist/testing/conformance/read-your-writes.d.ts +0 -20
  208. package/dist/testing/conformance/read-your-writes.js +0 -192
  209. package/dist/testing/conformance/reducers.d.ts +0 -8
  210. package/dist/testing/conformance/reducers.js +0 -258
  211. package/dist/testing/conformance/relay.d.ts +0 -42
  212. package/dist/testing/conformance/relay.js +0 -1115
  213. package/dist/testing/conformance/restore.d.ts +0 -43
  214. package/dist/testing/conformance/restore.js +0 -341
  215. package/dist/testing/conformance/retention.d.ts +0 -14
  216. package/dist/testing/conformance/retention.js +0 -463
  217. package/dist/testing/conformance/rls.d.ts +0 -24
  218. package/dist/testing/conformance/rls.js +0 -576
  219. package/dist/testing/conformance/simulation.d.ts +0 -31
  220. package/dist/testing/conformance/simulation.js +0 -167
  221. package/dist/testing/conformance/single-shard.d.ts +0 -2
  222. package/dist/testing/conformance/single-shard.js +0 -125
  223. package/dist/testing/conformance/singleton.d.ts +0 -3
  224. package/dist/testing/conformance/singleton.js +0 -293
  225. package/dist/testing/conformance/statements.d.ts +0 -60
  226. package/dist/testing/conformance/statements.js +0 -108
  227. package/dist/testing/conformance/streams.d.ts +0 -7
  228. package/dist/testing/conformance/streams.js +0 -280
  229. package/dist/testing/conformance/subscriptions/access.d.ts +0 -4
  230. package/dist/testing/conformance/subscriptions/access.js +0 -98
  231. package/dist/testing/conformance/subscriptions/actors.d.ts +0 -4910
  232. package/dist/testing/conformance/subscriptions/actors.js +0 -372
  233. package/dist/testing/conformance/subscriptions/cluster.d.ts +0 -4
  234. package/dist/testing/conformance/subscriptions/cluster.js +0 -89
  235. package/dist/testing/conformance/subscriptions/delivery.d.ts +0 -4
  236. package/dist/testing/conformance/subscriptions/delivery.js +0 -253
  237. package/dist/testing/conformance/subscriptions/deploys.d.ts +0 -4
  238. package/dist/testing/conformance/subscriptions/deploys.js +0 -162
  239. package/dist/testing/conformance/subscriptions/epochs.d.ts +0 -4
  240. package/dist/testing/conformance/subscriptions/epochs.js +0 -169
  241. package/dist/testing/conformance/subscriptions/failures.d.ts +0 -4
  242. package/dist/testing/conformance/subscriptions/failures.js +0 -108
  243. package/dist/testing/conformance/subscriptions/feeds.d.ts +0 -4
  244. package/dist/testing/conformance/subscriptions/feeds.js +0 -218
  245. package/dist/testing/conformance/subscriptions/harness.d.ts +0 -124
  246. package/dist/testing/conformance/subscriptions/harness.js +0 -143
  247. package/dist/testing/conformance/subscriptions/operator.d.ts +0 -4
  248. package/dist/testing/conformance/subscriptions/operator.js +0 -128
  249. package/dist/testing/conformance/subscriptions/retention.d.ts +0 -4
  250. package/dist/testing/conformance/subscriptions/retention.js +0 -288
  251. package/dist/testing/conformance/subscriptions/routing.d.ts +0 -4
  252. package/dist/testing/conformance/subscriptions/routing.js +0 -179
  253. package/dist/testing/conformance/subscriptions.d.ts +0 -4
  254. package/dist/testing/conformance/subscriptions.js +0 -19
  255. package/dist/testing/conformance/tables.d.ts +0 -125
  256. package/dist/testing/conformance/tables.js +0 -664
  257. package/dist/testing/conformance/transports/actors.d.ts +0 -1907
  258. package/dist/testing/conformance/transports/actors.js +0 -163
  259. package/dist/testing/conformance/transports/client.d.ts +0 -3
  260. package/dist/testing/conformance/transports/client.js +0 -237
  261. package/dist/testing/conformance/transports/feeds.d.ts +0 -3
  262. package/dist/testing/conformance/transports/feeds.js +0 -232
  263. package/dist/testing/conformance/transports/harness.d.ts +0 -93
  264. package/dist/testing/conformance/transports/harness.js +0 -84
  265. package/dist/testing/conformance/transports/streams.d.ts +0 -3
  266. package/dist/testing/conformance/transports/streams.js +0 -96
  267. package/dist/testing/conformance/transports/websocket.d.ts +0 -3
  268. package/dist/testing/conformance/transports/websocket.js +0 -384
  269. package/dist/testing/conformance/transports/wire.d.ts +0 -141
  270. package/dist/testing/conformance/transports/wire.js +0 -204
  271. package/dist/testing/conformance/transports.d.ts +0 -3
  272. package/dist/testing/conformance/transports.js +0 -11
  273. package/dist/testing/conformance/watch.d.ts +0 -59
  274. package/dist/testing/conformance/watch.js +0 -879
  275. package/dist/testing/conformance/workflow-engine.d.ts +0 -97
  276. package/dist/testing/conformance/workflow-engine.js +0 -253
  277. package/dist/testing/conformance/workflow-versions.d.ts +0 -3
  278. package/dist/testing/conformance/workflow-versions.js +0 -545
  279. package/dist/testing/conformance/workflows/access.d.ts +0 -4
  280. package/dist/testing/conformance/workflows/access.js +0 -147
  281. package/dist/testing/conformance/workflows/actors.d.ts +0 -2317
  282. package/dist/testing/conformance/workflows/actors.js +0 -305
  283. package/dist/testing/conformance/workflows/basics.d.ts +0 -4
  284. package/dist/testing/conformance/workflows/basics.js +0 -144
  285. package/dist/testing/conformance/workflows/engine.d.ts +0 -4
  286. package/dist/testing/conformance/workflows/engine.js +0 -80
  287. package/dist/testing/conformance/workflows/guards.d.ts +0 -4
  288. package/dist/testing/conformance/workflows/guards.js +0 -172
  289. package/dist/testing/conformance/workflows/harness.d.ts +0 -14
  290. package/dist/testing/conformance/workflows/harness.js +0 -44
  291. package/dist/testing/conformance/workflows/recovery.d.ts +0 -4
  292. package/dist/testing/conformance/workflows/recovery.js +0 -181
  293. package/dist/testing/conformance/workflows/starts.d.ts +0 -4
  294. package/dist/testing/conformance/workflows/starts.js +0 -57
  295. package/dist/testing/conformance/workflows/suspension.d.ts +0 -4
  296. package/dist/testing/conformance/workflows/suspension.js +0 -106
  297. package/dist/testing/conformance/workflows/waits.d.ts +0 -4
  298. package/dist/testing/conformance/workflows/waits.js +0 -284
  299. package/dist/testing/conformance/workflows.d.ts +0 -4
  300. package/dist/testing/conformance/workflows.js +0 -19
  301. package/dist/testing/conformance.d.ts +0 -330
  302. package/dist/testing/conformance.js +0 -364
  303. package/dist/testing/foundation.d.ts +0 -41
  304. package/dist/testing/foundation.js +0 -517
  305. package/dist/testing/simulate-cluster.d.ts +0 -112
  306. package/dist/testing/simulate-cluster.js +0 -257
  307. package/dist/testing/simulate.d.ts +0 -90
  308. package/dist/testing/simulate.js +0 -127
@@ -22,7 +22,7 @@ const ROUTED_SUBSCRIBER_WAIT_MS = 5000;
22
22
  * connection holder, and the sweeps, and forgotten when its layer's scope
23
23
  * closes.
24
24
  */
25
- export const actorRegistration = ({ registrations, queryRegistrations, jobRegistrations, residency, diagnostics, owners, heldTypes, heldType, sweepsWorkflows, checked, services, frameworkClock, content, retryWindowMs, tableRole, adoptionRole, enforcedTypes, writerDeclarations, refreshPayloadWriters, subscriptions, transport, authorize, gate, writable, defectLog, outbox, }) => {
25
+ export const actorRegistration = ({ registrations, queryRegistrations, jobRegistrations, residency, diagnostics, owners, heldTypes, heldType, sweepsWorkflows, checked, services, frameworkClock, content, retryWindowMs, tableRole, adoptionRole, enforcedTypes, writerDeclarations, refreshPayloadWriters, subscriptions, transport, authorize, gate, writable, defectLog, outbox, recorder, }) => {
26
26
  /** Concurrent layers can exceed checkout capacity; only a refused checkout is safe to retry here. */
27
27
  const startupSql = (effect) => retryPoolRefusal(effect).pipe(Effect.orDie);
28
28
  /**
@@ -150,9 +150,9 @@ export const actorRegistration = ({ registrations, queryRegistrations, jobRegist
150
150
  heldTypes.delete(registration.name);
151
151
  sweepsWorkflows.delete(registration.name);
152
152
  }));
153
- const { isResident, owner, diagnose, restarting } = yield* registerActor(registration, transport, authorize, gate, writable).pipe(Effect.provideService(DefectLog, defectLog), Effect.provideContext(services), Effect.provideService(OutboxRuntime, outbox));
153
+ const { isResident, owner, diagnose, restarting, resident } = yield* registerActor(registration, transport, authorize, gate, writable, recorder).pipe(Effect.provideService(DefectLog, defectLog), Effect.provideContext(services), Effect.provideService(OutboxRuntime, outbox));
154
154
  residency.set(registration.name, isResident);
155
- diagnostics.set(registration.name, { diagnose, restarting });
155
+ diagnostics.set(registration.name, { diagnose, restarting, resident });
156
156
  owners.set(registration.name, owner);
157
157
  }),
158
158
  registerQueries: Effect.fnUntraced(function* (registration) {
@@ -29,6 +29,18 @@ declare const RunnerReadiness_base: Context.ServiceClass<RunnerReadiness, "@rika
29
29
  /** The public runner's readiness checks acquired shards, not just a reachable database. */
30
30
  export declare class RunnerReadiness extends RunnerReadiness_base {
31
31
  }
32
+ declare const RunnerPeers_base: Context.ServiceClass<RunnerPeers, "@rikalabs/akter/runtime/runner/RunnerPeers", {
33
+ /** Healthy or not; undefined when the storage could not be read. */
34
+ readonly peers: Effect.Effect<number | undefined>;
35
+ }>;
36
+ /** How many runners other than this one the cluster's runner storage lists. */
37
+ export declare class RunnerPeers extends RunnerPeers_base {
38
+ }
39
+ /** Counts the runners the storage lists under any address but this runner's own. */
40
+ export declare const runnerPeers: Effect.Effect<{
41
+ /** Healthy or not; undefined when the storage could not be read. */
42
+ readonly peers: Effect.Effect<number | undefined>;
43
+ }, never, RunnerStorage.RunnerStorage | ShardingConfig.ShardingConfig>;
32
44
  /** Builds assignment expectations independently of the runner's local acquisition state. */
33
45
  export declare const acquiredShards: Effect.Effect<{
34
46
  readonly acquired: (sharding: Pick<Sharding.Sharding["Service"], "hasShardId">) => Effect.Effect<boolean>;
@@ -20,6 +20,18 @@ export class RunnerWiring extends Context.Service()("@rikalabs/akter/runtime/run
20
20
  /** The public runner's readiness checks acquired shards, not just a reachable database. */
21
21
  export class RunnerReadiness extends Context.Service()("@rikalabs/akter/runtime/runner/RunnerReadiness") {
22
22
  }
23
+ /** How many runners other than this one the cluster's runner storage lists. */
24
+ export class RunnerPeers extends Context.Service()("@rikalabs/akter/runtime/runner/RunnerPeers") {
25
+ }
26
+ /** Counts the runners the storage lists under any address but this runner's own. */
27
+ export const runnerPeers = Effect.gen(function* () {
28
+ const config = yield* ShardingConfig.ShardingConfig;
29
+ const storage = yield* RunnerStorage.RunnerStorage;
30
+ const self = Option.map(config.runnerAddress, PrimaryKey.value);
31
+ return RunnerPeers.of({
32
+ peers: storage.getRunners.pipe(Effect.map((runners) => runners.filter(([runner]) => Option.isNone(self) || PrimaryKey.value(runner.address) !== self.value).length), Effect.orElseSucceed(() => undefined)),
33
+ });
34
+ });
23
35
  /** Builds assignment expectations independently of the runner's local acquisition state. */
24
36
  export const acquiredShards = Effect.gen(function* () {
25
37
  const config = yield* ShardingConfig.ShardingConfig;
@@ -5,24 +5,36 @@ export declare const PLACEMENT_ENCODING = 1;
5
5
  * An actor type's placement. A parent-placed type names its parent type and
6
6
  * that type's own placement, down to an actor-placed root.
7
7
  */
8
- export type Placement = "tenant" | "actor" | {
8
+ export type Placement = "tenant" | "actor" | "authority" | {
9
9
  readonly parent: string;
10
10
  readonly placement: Placement;
11
11
  };
12
+ /**
13
+ * The bucket every authority-placed key falls in. A topology that splits a
14
+ * database keeps it on the authoritative shard, beside the tables it does not
15
+ * route, so a turn of an authority-placed actor reads and writes the real
16
+ * control tables in a transaction that reaches one shard.
17
+ */
18
+ export declare const AUTHORITY_BUCKET = -128;
19
+ /** `key` moved into `AUTHORITY_BUCKET`, keeping its low 56 bits, so distinct keys stay distinct. */
20
+ export declare const authorityKey: (key: bigint) => bigint;
12
21
  /** A parent-placed type's parent and the parent's placement; `undefined` for any other. */
13
22
  export declare const parentPlacement: (placement: Placement) => {
14
23
  readonly parent: string;
15
24
  readonly placement: Placement;
16
25
  } | undefined;
17
26
  /** The value `actor_placements.placement` records for `placement`. */
18
- export declare const placementKind: (placement: Placement) => "actor" | "parent" | "tenant";
27
+ export declare const placementKind: (placement: Placement) => "actor" | "authority" | "parent" | "tenant";
28
+ /** The placement at the root of `placement`'s parent chain. */
29
+ export declare const rootPlacement: (placement: Placement) => "tenant" | "actor" | "authority";
19
30
  /**
20
31
  * The 64-bit shard key shared by every row an actor owns. Tenant
21
- * placement colocates a tenant's actors; actor placement spreads them; a
22
- * parent-placed actor takes its root's key, read from the parent id its own
23
- * id carries, so a family shares one shard. A parent-placed id without a
24
- * parent id throws: ids are decoded against their actor's key before any
25
- * routing, so that is a defect, not an input error.
32
+ * placement colocates a tenant's actors; actor placement spreads them;
33
+ * authority placement colocates a tenant's authority-placed actors in
34
+ * `AUTHORITY_BUCKET`; a parent-placed actor takes its root's key, read from
35
+ * the parent id its own id carries, so a family shares one shard. A
36
+ * parent-placed id without a parent id throws: ids are decoded against their
37
+ * actor's key before any routing, so that is a defect, not an input error.
26
38
  */
27
39
  export declare const routingKey: ({ ref, placement, }: {
28
40
  readonly ref: ActorRef;
@@ -2,17 +2,31 @@ import { parseChildId } from "../../identity/child.js";
2
2
  import { compressBytes, decompressBytes, hash64 } from "./platform.js";
3
3
  /** Increment only with a migration: every stored `routing_key` depends on this encoding. */
4
4
  export const PLACEMENT_ENCODING = 1;
5
+ /**
6
+ * The bucket every authority-placed key falls in. A topology that splits a
7
+ * database keeps it on the authoritative shard, beside the tables it does not
8
+ * route, so a turn of an authority-placed actor reads and writes the real
9
+ * control tables in a transaction that reaches one shard.
10
+ */
11
+ export const AUTHORITY_BUCKET = -128;
12
+ const LOW_56 = (1n << 56n) - 1n;
13
+ /** `key` moved into `AUTHORITY_BUCKET`, keeping its low 56 bits, so distinct keys stay distinct. */
14
+ export const authorityKey = (key) => BigInt.asIntN(64, (key & LOW_56) | (BigInt(AUTHORITY_BUCKET + 256) << 56n));
15
+ const isRoot = (placement) => placement === "tenant" || placement === "actor" || placement === "authority";
5
16
  /** A parent-placed type's parent and the parent's placement; `undefined` for any other. */
6
- export const parentPlacement = (placement) => placement === "tenant" || placement === "actor" ? undefined : placement;
17
+ export const parentPlacement = (placement) => (isRoot(placement) ? undefined : placement);
7
18
  /** The value `actor_placements.placement` records for `placement`. */
8
- export const placementKind = (placement) => placement === "tenant" || placement === "actor" ? placement : "parent";
19
+ export const placementKind = (placement) => (isRoot(placement) ? placement : "parent");
20
+ /** The placement at the root of `placement`'s parent chain. */
21
+ export const rootPlacement = (placement) => isRoot(placement) ? placement : rootPlacement(placement.placement);
9
22
  /**
10
23
  * The 64-bit shard key shared by every row an actor owns. Tenant
11
- * placement colocates a tenant's actors; actor placement spreads them; a
12
- * parent-placed actor takes its root's key, read from the parent id its own
13
- * id carries, so a family shares one shard. A parent-placed id without a
14
- * parent id throws: ids are decoded against their actor's key before any
15
- * routing, so that is a defect, not an input error.
24
+ * placement colocates a tenant's actors; actor placement spreads them;
25
+ * authority placement colocates a tenant's authority-placed actors in
26
+ * `AUTHORITY_BUCKET`; a parent-placed actor takes its root's key, read from
27
+ * the parent id its own id carries, so a family shares one shard. A
28
+ * parent-placed id without a parent id throws: ids are decoded against their
29
+ * actor's key before any routing, so that is a defect, not an input error.
16
30
  */
17
31
  export const routingKey = ({ ref, placement, }) => {
18
32
  const above = parentPlacement(placement);
@@ -25,6 +39,8 @@ export const routingKey = ({ ref, placement, }) => {
25
39
  placement: above.placement,
26
40
  });
27
41
  }
42
+ if (placement === "authority")
43
+ return authorityKey(tenantRoutingKey(ref.tenant));
28
44
  return placement === "tenant"
29
45
  ? tenantRoutingKey(ref.tenant)
30
46
  : placementHash(["actor", ref.tenant, ref.actor, ref.id]);
@@ -1,11 +1,16 @@
1
1
  import { Effect } from "effect";
2
2
  import { SqlClient, type SqlError } from "effect/sql";
3
3
  import { type Placement } from "./codec.ts";
4
+ /** The per-actor tables `moveToAuthority` rewrites, for the catalog check that none is missed. */
5
+ export declare const AUTHORITY_MOVED_TABLES: ReadonlyArray<string>;
4
6
  /**
5
7
  * Records `name`'s placement on first registration and refuses one that
6
8
  * differs from the record, for the type and every ancestor it routes through:
7
9
  * a changed placement, encoding, or parent would read and write under
8
- * different routing keys.
10
+ * different routing keys. The one change it makes instead of refusing is a
11
+ * tenant-placed type that now declares authority placement: its rows move to
12
+ * the new keys and the record changes in one transaction, under the record's
13
+ * row lock, so concurrent starts move them once.
9
14
  */
10
15
  export declare const checkPlacement: ({ name, placement, }: {
11
16
  readonly name: string;
@@ -1,11 +1,89 @@
1
1
  import { Effect } from "effect";
2
2
  import { SqlClient } from "effect/sql";
3
- import { parentPlacement, PLACEMENT_ENCODING, placementKind } from "./codec.js";
3
+ import { AUTHORITY_BUCKET, authorityKey, parentPlacement, PLACEMENT_ENCODING, placementKind, } from "./codec.js";
4
+ /**
5
+ * The per-actor tables that keep an actor's rows under its routing key, each
6
+ * with the column that names the actor's type and whether it carries the
7
+ * key's `bucket`. `actor_generations` and `actor_workflow_executions` are
8
+ * referenced by the others' foreign keys, so their rows are copied to the new
9
+ * key and deleted afterwards instead of updated.
10
+ */
11
+ const KEYED = [
12
+ { table: "actor_state", type: "actor_type" },
13
+ { table: "actor_receipts", type: "actor_type" },
14
+ { table: "actor_events", type: "actor_type" },
15
+ { table: "actor_outbox", type: "actor_type", bucket: true },
16
+ { table: "actor_dead_letters", type: "actor_type" },
17
+ { table: "actor_blobs", type: "actor_type" },
18
+ { table: "actor_connections", type: "actor_type", bucket: true },
19
+ { table: "actor_content_refs", type: "actor_type" },
20
+ { table: "actor_subscription_cursors", type: "actor_type" },
21
+ { table: "actor_subscriptions", type: "source_type", bucket: true },
22
+ { table: "actor_subscription_tags", type: "source_type" },
23
+ { table: "actor_operator_audit", type: "actor_type" },
24
+ ];
25
+ /** The per-actor tables `moveToAuthority` rewrites, for the catalog check that none is missed. */
26
+ export const AUTHORITY_MOVED_TABLES = [
27
+ "actor_generations",
28
+ "actor_workflow_executions",
29
+ "actor_workflow_step",
30
+ ...KEYED.map(({ table }) => table),
31
+ ];
32
+ const LOW_56 = (1n << 56n) - 1n;
33
+ /**
34
+ * Moves every row of `name`'s actors from its tenant-placed keys to its
35
+ * authority-placed keys, in the caller's transaction: the framework's
36
+ * per-actor rows and the rows of every table `actor_tables` records it as
37
+ * owning. Rows already in `AUTHORITY_BUCKET` keep their key, which is the
38
+ * same under both placements. It returns how many actors moved.
39
+ *
40
+ * A runner that still computes the old keys would recreate an actor under
41
+ * them, so this runs only at the start of a release that replaced every
42
+ * runner of the previous one.
43
+ */
44
+ const moveToAuthority = Effect.fnUntraced(function* (name) {
45
+ const sql = yield* SqlClient.SqlClient;
46
+ const high = authorityKey(0n);
47
+ const moved = sql `(routing_key & ${LOW_56}) | ${high}`;
48
+ const stale = sql `(routing_key >> 56) <> ${AUTHORITY_BUCKET}`;
49
+ const copy = (table) => Effect.gen(function* () {
50
+ const columns = yield* sql `SELECT attname AS name FROM pg_attribute
51
+ WHERE attrelid = ${table}::regclass AND attnum > 0 AND NOT attisdropped ORDER BY attnum`;
52
+ return yield* sql `INSERT INTO ${sql(table)} (${sql.csv(columns.map(({ name }) => sql `${sql(name)}`))})
53
+ SELECT ${sql.csv(columns.map(({ name }) => (name === "routing_key" ? moved : sql `${sql(name)}`)))}
54
+ FROM ${sql(table)} WHERE actor_type = ${name} AND ${stale}
55
+ RETURNING actor_id`;
56
+ });
57
+ const generations = yield* copy("actor_generations");
58
+ yield* copy("actor_workflow_executions");
59
+ yield* sql `UPDATE actor_workflow_step SET routing_key = ${moved}
60
+ WHERE actor_type = ${name} AND ${stale}`;
61
+ yield* sql `DELETE FROM actor_workflow_executions WHERE actor_type = ${name} AND ${stale}`;
62
+ for (const keyed of KEYED)
63
+ yield* sql `UPDATE ${sql(keyed.table)} SET routing_key = ${moved}
64
+ ${keyed.bucket === true ? sql `, bucket = ${AUTHORITY_BUCKET}` : sql.literal("")}
65
+ WHERE ${sql(keyed.type)} = ${name} AND ${stale}`;
66
+ yield* sql `DELETE FROM actor_generations WHERE actor_type = ${name} AND ${stale}`;
67
+ const owned = yield* sql `
68
+ SELECT t.table_schema AS schema, t.table_name AS table FROM actor_tables t
69
+ WHERE t.actor_type = ${name} AND EXISTS (
70
+ SELECT 1 FROM pg_attribute a
71
+ WHERE a.attrelid = to_regclass(format('%I.%I', t.table_schema, t.table_name))
72
+ AND a.attname = 'routing_key' AND NOT a.attisdropped)
73
+ ORDER BY t.table_schema, t.table_name`;
74
+ for (const { schema, table } of owned)
75
+ yield* sql `UPDATE ${sql(schema)}.${sql(table)} SET routing_key = ${moved}
76
+ WHERE routing_key IS NOT NULL AND ${stale}`;
77
+ return generations.length;
78
+ });
4
79
  /**
5
80
  * Records `name`'s placement on first registration and refuses one that
6
81
  * differs from the record, for the type and every ancestor it routes through:
7
82
  * a changed placement, encoding, or parent would read and write under
8
- * different routing keys.
83
+ * different routing keys. The one change it makes instead of refusing is a
84
+ * tenant-placed type that now declares authority placement: its rows move to
85
+ * the new keys and the record changes in one transaction, under the record's
86
+ * row lock, so concurrent starts move them once.
9
87
  */
10
88
  export const checkPlacement = ({ name, placement, }) => Effect.gen(function* () {
11
89
  const sql = yield* SqlClient.SqlClient;
@@ -15,10 +93,24 @@ export const checkPlacement = ({ name, placement, }) => Effect.gen(function* ()
15
93
  yield* sql `INSERT INTO actor_placements (actor_type, placement, encoding, parent_type)
16
94
  VALUES (${name}, ${kind}, ${PLACEMENT_ENCODING}, ${parent})
17
95
  ON CONFLICT DO NOTHING`;
18
- const [recorded] = yield* sql `SELECT placement, encoding, parent_type FROM actor_placements WHERE actor_type = ${name}`;
19
- if (recorded?.placement !== kind ||
20
- recorded.encoding !== PLACEMENT_ENCODING ||
21
- recorded.parent_type !== parent)
96
+ const read = (lock) => Effect.map(sql `SELECT placement, encoding, parent_type FROM actor_placements WHERE actor_type = ${name}
97
+ ${lock ? sql.literal("FOR UPDATE") : sql.literal("")}`, ([row]) => row);
98
+ const matches = (recorded) => recorded?.placement === kind &&
99
+ recorded.encoding === PLACEMENT_ENCODING &&
100
+ recorded.parent_type === parent;
101
+ const recorded = yield* read(false);
102
+ if (!matches(recorded) &&
103
+ kind === "authority" &&
104
+ recorded?.placement === "tenant" &&
105
+ recorded.encoding === PLACEMENT_ENCODING)
106
+ yield* sql.withTransaction(Effect.gen(function* () {
107
+ if (matches(yield* read(true)))
108
+ return;
109
+ const moved = yield* moveToAuthority(name);
110
+ yield* sql `UPDATE actor_placements SET placement = ${kind} WHERE actor_type = ${name}`;
111
+ yield* Effect.logInfo(`Moved ${moved} actors of ${name} to authority placement`);
112
+ }));
113
+ if (!matches(yield* read(false)))
22
114
  return yield* Effect.die(new Error(`Actor ${name} placement differs from the deployment; migrate explicitly`));
23
115
  if (above !== undefined)
24
116
  yield* checkPlacement({ name: above.parent, placement: above.placement });
@@ -64,6 +64,12 @@ export interface Swept {
64
64
  *
65
65
  * The sweep yields after each batch, so a turn waiting for PGlite's one
66
66
  * connection runs between batches instead of after the whole sweep.
67
+ *
68
+ * With data shards, every prune runs once per shard on a session targeted at
69
+ * it, under the same authoritative lock and that shard's local fence. A
70
+ * manifest is then pruned on the authority once no shard reports an open
71
+ * execution under it; an execution that starts under it after that read
72
+ * restores it, as a runner of an older deployment does.
67
73
  */
68
74
  export declare const sweep: (policies: Iterable<RetentionPolicy>, retryWindowMs: number) => Effect.Effect<{
69
75
  receipts: number;
@@ -3,6 +3,7 @@ import { SqlClient } from "effect/sql";
3
3
  import { databaseTime } from "../turn/admission.js";
4
4
  import { CleanupHooks } from "../turn/hooks.js";
5
5
  import { count, Metrics } from "../telemetry/metrics.js";
6
+ import { forEachRange, withinRange } from "../database/shards.js";
6
7
  import { coordinated } from "../database/coordination.js";
7
8
  /**
8
9
  * How long after its id expires a receipt stays. An id expires one retry
@@ -45,6 +46,12 @@ export const receiptMarginMs = (horizon) => Math.max(horizon.keepReceiptsMs - ho
45
46
  *
46
47
  * The sweep yields after each batch, so a turn waiting for PGlite's one
47
48
  * connection runs between batches instead of after the whole sweep.
49
+ *
50
+ * With data shards, every prune runs once per shard on a session targeted at
51
+ * it, under the same authoritative lock and that shard's local fence. A
52
+ * manifest is then pruned on the authority once no shard reports an open
53
+ * execution under it; an execution that starts under it after that read
54
+ * restores it, as a runner of an older deployment does.
48
55
  */
49
56
  export const sweep = Effect.fnUntraced(function* (policies, retryWindowMs) {
50
57
  const sql = yield* SqlClient.SqlClient;
@@ -74,11 +81,13 @@ export const sweep = Effect.fnUntraced(function* (policies, retryWindowMs) {
74
81
  yield* Effect.yieldNow;
75
82
  }
76
83
  });
77
- receipts += yield* pruneAll((from) => sql `
84
+ const everyRange = (prune) => Effect.map(forEachRange(prune), (counts) => counts.reduce((sum, one) => sum + one, 0));
85
+ receipts += yield* everyRange((range) => pruneAll((from) => sql `
78
86
  WITH doomed AS (
79
87
  SELECT r.routing_key, r.tenant_id, r.actor_type, r.actor_id, r.command_id, r.expires_at_ms
80
88
  FROM actor_receipts r
81
89
  WHERE r.actor_type = ${policy.actorType} AND r.expires_at_ms >= ${from}::bigint
90
+ ${withinRange({ sql, range, column: "r.routing_key" })}
82
91
  AND r.expires_at_ms <= ${receiptCutoff}
83
92
  AND NOT EXISTS (
84
93
  SELECT 1 FROM actor_outbox o
@@ -94,11 +103,12 @@ export const sweep = Effect.fnUntraced(function* (policies, retryWindowMs) {
94
103
  AND r.actor_type = d.actor_type AND r.actor_id = d.actor_id AND r.command_id = d.command_id
95
104
  RETURNING 1)
96
105
  SELECT count(*)::integer AS count, (SELECT max(expires_at_ms)::text FROM doomed) AS last
97
- FROM gone`, Metrics.receiptsPruned);
98
- events += yield* pruneAll((from) => sql `
106
+ FROM gone`, Metrics.receiptsPruned));
107
+ events += yield* everyRange((range) => pruneAll((from) => sql `
99
108
  WITH picked AS (
100
109
  SELECT routing_key, tenant_id, actor_type, actor_id, sequence, emitted_at_ms FROM actor_events
101
110
  WHERE actor_type = ${policy.actorType} AND emitted_at_ms >= ${from}::bigint
111
+ ${withinRange({ sql, range, column: "routing_key" })}
102
112
  AND emitted_at_ms <= ${eventCutoff}
103
113
  ORDER BY emitted_at_ms
104
114
  LIMIT ${hooks.batchSize}),
@@ -128,13 +138,14 @@ export const sweep = Effect.fnUntraced(function* (policies, retryWindowMs) {
128
138
  AND e.actor_type = u.actor_type AND e.actor_id = u.actor_id AND e.sequence <= u.last
129
139
  RETURNING 1)
130
140
  SELECT count(*)::integer AS count, (SELECT max(emitted_at_ms)::text FROM picked) AS last
131
- FROM gone`, Metrics.eventsPruned);
141
+ FROM gone`, Metrics.eventsPruned));
132
142
  if (policy.workflows) {
133
143
  const workflowCutoff = now - policy.keepWorkflowsMs;
134
- workflows += yield* pruneAll(() => sql `
144
+ workflows += yield* everyRange((range) => pruneAll(() => sql `
135
145
  WITH doomed AS (
136
146
  SELECT routing_key, execution_id FROM actor_workflow_executions
137
147
  WHERE actor_type = ${policy.actorType} AND status = 'finished'
148
+ ${withinRange({ sql, range, column: "routing_key" })}
138
149
  AND finished_at_ms <= ${workflowCutoff}
139
150
  ORDER BY finished_at_ms
140
151
  LIMIT ${hooks.batchSize}
@@ -143,15 +154,16 @@ export const sweep = Effect.fnUntraced(function* (policies, retryWindowMs) {
143
154
  DELETE FROM actor_workflow_executions x USING doomed d
144
155
  WHERE x.routing_key = d.routing_key AND x.execution_id = d.execution_id
145
156
  RETURNING 1)
146
- SELECT count(*)::integer AS count FROM gone`);
157
+ SELECT count(*)::integer AS count FROM gone`));
158
+ const superseded = sql `m.actor_type = ${policy.actorType}
159
+ AND m.accepted_at_ms < (SELECT max(l.accepted_at_ms) FROM actor_workflow_manifests l
160
+ WHERE l.actor_type = m.actor_type AND l.workflow = m.workflow)`;
147
161
  yield* batch(sql `
148
- DELETE FROM actor_workflow_manifests m
149
- WHERE m.actor_type = ${policy.actorType}
150
- AND m.accepted_at_ms < (SELECT max(l.accepted_at_ms) FROM actor_workflow_manifests l
151
- WHERE l.actor_type = m.actor_type AND l.workflow = m.workflow)
152
- AND NOT EXISTS (SELECT 1 FROM actor_workflow_executions x
153
- WHERE x.actor_type = m.actor_type AND x.workflow = m.workflow
154
- AND x.manifest_hash = m.manifest_hash AND x.status <> 'finished')`);
162
+ DELETE FROM actor_workflow_manifests m
163
+ WHERE ${superseded}
164
+ AND NOT EXISTS (SELECT 1 FROM actor_workflow_executions x
165
+ WHERE x.actor_type = m.actor_type AND x.workflow = m.workflow
166
+ AND x.manifest_hash = m.manifest_hash AND x.status <> 'finished')`);
155
167
  }
156
168
  }
157
169
  return { receipts, events, workflows };
@@ -1,6 +1,6 @@
1
1
  import { DateTime, Effect, Match, Result, Schema } from "effect";
2
2
  import { SqlClient, SqlError } from "effect/sql";
3
- import {} from "../database/shards.js";
3
+ import { forEachRange, withinRange } from "../database/shards.js";
4
4
  import { SubscriptionFailure } from "../../errors/subscription.js";
5
5
  import { Outcome, Request } from "../request.js";
6
6
  import {} from "../members.js";
@@ -9,7 +9,7 @@ import { decompress, routingKey } from "../storage/codec.js";
9
9
  import { databaseTime, FrameworkClock } from "../turn/admission.js";
10
10
  import { TurnHooks } from "../turn/hooks.js";
11
11
  import { ControlPayload, StringsJson, textArray } from "../turn/outbox.js";
12
- import { candidates, outboxNow } from "../turn/relay.js";
12
+ import { candidates, outboxClock, outboxNow } from "../turn/relay.js";
13
13
  import { changeRows, rowColumns } from "./storage.js";
14
14
  import { deliveryCommandId } from "./identity.js";
15
15
  import { count, Metrics } from "../telemetry/metrics.js";
@@ -135,6 +135,7 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
135
135
  const clock = yield* FrameworkClock;
136
136
  const { settings } = options;
137
137
  const now = () => outboxNow({ sql, offsetMillis: clock.offsetMillis() });
138
+ const statementClock = outboxClock({ sql });
138
139
  const sourceWhere = (alias, key, source) => sql `${sql(alias)}.routing_key = ${key} AND ${sql(alias)}.tenant_id = ${source.tenant}
139
140
  AND ${sql(alias)}.source_type = ${source.actor} AND ${sql(alias)}.source_id = ${source.id}`;
140
141
  const eventsOf = (alias, key, source) => sql `${sql(alias)}.routing_key = ${key} AND ${sql(alias)}.tenant_id = ${source.tenant}
@@ -308,7 +309,7 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
308
309
  AND (s.subscriber_type, s.subscription, s.subscriber_id)
309
310
  <= (SELECT subscriber_type, subscription, subscriber_id FROM last)`;
310
311
  const [page] = yield* sql `
311
- WITH head AS (
312
+ WITH ${statementClock}, head AS (
312
313
  SELECT event_sequence AS h FROM actor_generations g
313
314
  WHERE ${eventsOf("g", key, source)}),
314
315
  page AS MATERIALIZED (
@@ -409,14 +410,14 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
409
410
  AND s.subscription = ${row.command} AND s.subscriber_id = ${subscriber.id}`;
410
411
  const rejected = yield* sql.withTransaction(Effect.gen(function* () {
411
412
  yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
412
- VALUES (${key}, ${source.tenant}, ${source.actor}, ${source.id}) ON CONFLICT DO NOTHING`;
413
+ VALUES (${key}, ${source.tenant}, ${source.actor}, ${source.id}) ON CONFLICT DO NOTHING`;
413
414
  const [generation] = yield* sql `
414
- SELECT event_sequence::text AS head FROM actor_generations g
415
- WHERE ${eventsOf("g", key, source)} FOR SHARE`;
415
+ SELECT event_sequence::text AS head FROM actor_generations g
416
+ WHERE ${eventsOf("g", key, source)} FOR SHARE`;
416
417
  const head = BigInt(generation.head);
417
418
  const [existing] = yield* sql `
418
- SELECT s.epoch::text AS epoch, s.active FROM actor_subscriptions s
419
- WHERE ${target} FOR UPDATE`;
419
+ SELECT s.epoch::text AS epoch, s.active FROM actor_subscriptions s
420
+ WHERE ${target} FOR UPDATE`;
420
421
  const refused = change.op === "subscribe" &&
421
422
  change.start !== "now" &&
422
423
  change.start !== "start" &&
@@ -424,36 +425,36 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
424
425
  if (existing !== undefined && BigInt(existing.epoch) >= epoch)
425
426
  return refused && BigInt(existing.epoch) === epoch && !existing.active;
426
427
  const old = sql `SELECT ${rowColumns({ sql, alias: "s" })} FROM actor_subscriptions s
427
- WHERE ${target} FOR UPDATE`;
428
+ WHERE ${target} FOR UPDATE`;
428
429
  if (change.op === "remove" || refused) {
429
430
  yield* changeRows(old, sql `INSERT INTO actor_subscriptions AS s (routing_key, tenant_id, source_type, source_id,
430
- subscriber_type, subscription, subscriber_id, events, epoch, active, delivered, bucket)
431
- VALUES (${key}, ${source.tenant}, ${source.actor}, ${source.id}, ${subscriber.actor},
432
- ${row.command}, ${subscriber.id}, ${textArray({ sql, values: change.events })}, ${epoch}, false,
433
- ${head}, ${Number(key >> 56n)})
434
- ON CONFLICT (routing_key, tenant_id, source_type, source_id, subscriber_type, subscription, subscriber_id)
435
- DO UPDATE SET epoch = EXCLUDED.epoch, active = false, due_at_ms = NULL, attempts = 0,
436
- last_error = NULL, gap_at_ms = NULL, gap_through = NULL
437
- RETURNING ${rowColumns({ sql, alias: "s" })}`);
431
+ subscriber_type, subscription, subscriber_id, events, epoch, active, delivered, bucket)
432
+ VALUES (${key}, ${source.tenant}, ${source.actor}, ${source.id}, ${subscriber.actor},
433
+ ${row.command}, ${subscriber.id}, ${textArray({ sql, values: change.events })}, ${epoch}, false,
434
+ ${head}, ${Number(key >> 56n)})
435
+ ON CONFLICT (routing_key, tenant_id, source_type, source_id, subscriber_type, subscription, subscriber_id)
436
+ DO UPDATE SET epoch = EXCLUDED.epoch, active = false, due_at_ms = NULL, attempts = 0,
437
+ last_error = NULL, gap_at_ms = NULL, gap_through = NULL
438
+ RETURNING ${rowColumns({ sql, alias: "s" })}`);
438
439
  return refused;
439
440
  }
440
441
  const delivered = Match.value(change.start).pipe(Match.when("now", () => head), Match.when("start", () => 0n), Match.orElse((cursor) => BigInt(cursor)));
441
442
  yield* changeRows(old, sql `INSERT INTO actor_subscriptions AS s (routing_key, tenant_id, source_type, source_id,
442
- subscriber_type, subscription, subscriber_id, events, epoch, active, delivered, bucket,
443
- due_at_ms)
444
- SELECT ${key}, ${source.tenant}, ${source.actor}, ${source.id}, ${subscriber.actor},
445
- ${row.command}, ${subscriber.id}, x.events, ${epoch}, true, ${delivered},
446
- ${Number(key >> 56n)},
447
- CASE WHEN EXISTS (SELECT 1 FROM actor_events e
448
- WHERE ${eventsOf("e", key, source)}
449
- AND e.sequence > ${delivered} AND e.sequence <= ${head} AND e.event = ANY(x.events))
450
- THEN ${now()} END
451
- FROM (SELECT ${textArray({ sql, values: change.events })} AS events) AS x
452
- ON CONFLICT (routing_key, tenant_id, source_type, source_id, subscriber_type, subscription, subscriber_id)
453
- DO UPDATE SET events = EXCLUDED.events, epoch = EXCLUDED.epoch, active = true,
454
- delivered = EXCLUDED.delivered, marked = 0, due_at_ms = EXCLUDED.due_at_ms, attempts = 0,
455
- last_error = NULL, gap_at_ms = NULL, gap_through = NULL
456
- RETURNING ${rowColumns({ sql, alias: "s" })}`);
443
+ subscriber_type, subscription, subscriber_id, events, epoch, active, delivered, bucket,
444
+ due_at_ms)
445
+ SELECT ${key}, ${source.tenant}, ${source.actor}, ${source.id}, ${subscriber.actor},
446
+ ${row.command}, ${subscriber.id}, x.events, ${epoch}, true, ${delivered},
447
+ ${Number(key >> 56n)},
448
+ CASE WHEN EXISTS (SELECT 1 FROM actor_events e
449
+ WHERE ${eventsOf("e", key, source)}
450
+ AND e.sequence > ${delivered} AND e.sequence <= ${head} AND e.event = ANY(x.events))
451
+ THEN ${now()} END
452
+ FROM (SELECT ${textArray({ sql, values: change.events })} AS events) AS x
453
+ ON CONFLICT (routing_key, tenant_id, source_type, source_id, subscriber_type, subscription, subscriber_id)
454
+ DO UPDATE SET events = EXCLUDED.events, epoch = EXCLUDED.epoch, active = true,
455
+ delivered = EXCLUDED.delivered, marked = 0, due_at_ms = EXCLUDED.due_at_ms, attempts = 0,
456
+ last_error = NULL, gap_at_ms = NULL, gap_through = NULL
457
+ RETURNING ${rowColumns({ sql, alias: "s" })}`, statementClock);
457
458
  return false;
458
459
  }));
459
460
  const claim = sql `routing_key = ${BigInt(row.routing_key)} AND intent_id = ${row.intent_id}
@@ -677,7 +678,7 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
677
678
  events = CASE WHEN s.events @> ${declared} THEN s.events
678
679
  ELSE ARRAY(SELECT DISTINCT x FROM unnest(s.events || ${declared}) AS u(x) ORDER BY x) END
679
680
  WHERE ${held()}
680
- RETURNING ${rowColumns({ sql, alias: "s" })}`);
681
+ RETURNING ${rowColumns({ sql, alias: "s" })}`, statementClock);
681
682
  if (settled === 0)
682
683
  return yield* lostClaim("settle");
683
684
  yield* count(Metrics.relayDelivered, { kind: "subscription" }, 1);
@@ -698,18 +699,18 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
698
699
  }).pipe(Effect.ignore);
699
700
  /**
700
701
  * Widens the rows of one dynamic subscription to its declaration's event
701
- * classes, 1,000 rows per transaction, with their tag summary. It keeps every cursor and epoch, never
702
+ * classes on every range, 1,000 rows per transaction, with their tag summary. It keeps every cursor and epoch, never
702
703
  * narrows a row, and is idempotent, so every runner of a rolling deploy may
703
704
  * run it. A widened row with an event of an added class after its
704
705
  * `delivered` becomes due. Rows claimed right now are skipped; their settle
705
706
  * widens them.
706
707
  */
707
- const widen = Effect.fnUntraced(function* (subscriberType, declared) {
708
+ const widenRange = Effect.fnUntraced(function* (subscriberType, declared, range) {
708
709
  const tags = textArray({ sql, values: declared.events });
709
710
  for (;;) {
710
711
  const widened = yield* changeRows(sql `SELECT ${rowColumns({ sql, alias: "s" })} FROM actor_subscriptions s
711
712
  WHERE s.subscriber_type = ${subscriberType} AND s.subscription = ${declared.tag}
712
- AND s.active AND NOT s.events @> ${tags}
713
+ AND s.active AND NOT s.events @> ${tags} ${withinRange({ sql, range, column: "s.routing_key" })}
713
714
  LIMIT ${EXPANSION_PAGE}
714
715
  FOR UPDATE SKIP LOCKED`, sql `UPDATE actor_subscriptions s
715
716
  SET events = ARRAY(SELECT DISTINCT u.x FROM unnest(s.events || ${tags}) AS u(x) ORDER BY u.x),
@@ -725,23 +726,25 @@ export const subscriptionRelay = Effect.fnUntraced(function* (options) {
725
726
  AND s.source_type = o.source_type AND s.source_id = o.source_id
726
727
  AND s.subscriber_type = o.subscriber_type AND s.subscription = o.subscription
727
728
  AND s.subscriber_id = o.subscriber_id
728
- RETURNING ${rowColumns({ sql, alias: "s" })}`);
729
+ RETURNING ${rowColumns({ sql, alias: "s" })}`, statementClock);
729
730
  if (widened < EXPANSION_PAGE)
730
731
  break;
731
732
  }
732
733
  });
734
+ const widen = (subscriberType, declared) => Effect.asVoid(forEachRange((range) => widenRange(subscriberType, declared, range)));
733
735
  /**
734
- * Deletes rows of `subscriberType` whose subscription this runner doesn't
735
- * declare once they have been due for a day. Such a row is never claimed
736
+ * Deletes rows of `subscriberType` on every range whose subscription this
737
+ * runner doesn't declare once they have been due for a day. Such a row is never claimed
736
738
  * here, so a rolling deploy's newer runners get a day to take it first.
737
739
  */
738
740
  const cleanupRemoved = Effect.fnUntraced(function* (subscriberType, declared) {
739
741
  const cutoff = (yield* databaseTime) - REMOVED_AFTER_MS;
740
- return yield* changeRows(sql `SELECT ${rowColumns({ sql, alias: "s" })} FROM actor_subscriptions s
741
- WHERE s.subscriber_type = ${subscriberType}
742
- AND NOT s.subscription = ANY(${textArray({ sql, values: declared })})
743
- AND s.due_at_ms < ${cutoff}
744
- FOR UPDATE`, deleteLocked);
742
+ const counts = yield* forEachRange((range) => changeRows(sql `SELECT ${rowColumns({ sql, alias: "s" })} FROM actor_subscriptions s
743
+ WHERE s.subscriber_type = ${subscriberType}
744
+ AND NOT s.subscription = ANY(${textArray({ sql, values: declared })})
745
+ AND s.due_at_ms < ${cutoff} ${withinRange({ sql, range, column: "s.routing_key" })}
746
+ FOR UPDATE`, deleteLocked));
747
+ return counts.reduce((sum, one) => sum + one, 0);
745
748
  });
746
749
  const run = (work, handoff) => {
747
750
  const claim = { lease: BigInt(work.claimed_until) };
@@ -35,6 +35,7 @@ export declare const summarize: ({ sql, changed, }: {
35
35
  * version of each row, so a concurrent change that committed first is never
36
36
  * counted twice. A summary count the change lowered to 0 is deleted in the
37
37
  * same transaction, so no committed summary row counts 0. Returns how many
38
- * rows `change` wrote.
38
+ * rows `change` wrote. A `change` that reads the outbox clock passes its
39
+ * `clock` CTE, which leads the statement's `WITH`.
39
40
  */
40
- export declare const changeRows: (old: Statement.Fragment, change: Statement.Fragment) => Effect.Effect<number, import("effect/sql/SqlError").SqlError, SqlClient.SqlClient>;
41
+ export declare const changeRows: (old: Statement.Fragment, change: Statement.Fragment, clock?: Statement.Fragment | undefined) => Effect.Effect<number, import("effect/sql/SqlError").SqlError, SqlClient.SqlClient>;
@@ -64,15 +64,16 @@ export const summarize = ({ sql, changed, }) => {
64
64
  * version of each row, so a concurrent change that committed first is never
65
65
  * counted twice. A summary count the change lowered to 0 is deleted in the
66
66
  * same transaction, so no committed summary row counts 0. Returns how many
67
- * rows `change` wrote.
67
+ * rows `change` wrote. A `change` that reads the outbox clock passes its
68
+ * `clock` CTE, which leads the statement's `WITH`.
68
69
  */
69
- export const changeRows = Effect.fnUntraced(function* (old, change) {
70
+ export const changeRows = Effect.fnUntraced(function* (old, change, clock) {
70
71
  const sql = yield* SqlClient.SqlClient;
71
72
  return yield* sql.withTransaction(Effect.gen(function* () {
72
73
  const [locked] = yield* sql `
73
74
  SELECT coalesce(json_agg(o), '[]')::text AS rows FROM (${old}) o`;
74
75
  const [result] = yield* sql `
75
- WITH old_rows AS (
76
+ WITH ${clock === undefined ? sql `` : sql `${clock},`} old_rows AS (
76
77
  SELECT * FROM json_to_recordset(${locked.rows}::json) AS o(routing_key bigint,
77
78
  tenant_id text, source_type text, source_id text, subscriber_type text,
78
79
  subscription text, subscriber_id text, active boolean, events text[])),
@@ -89,9 +90,11 @@ export const changeRows = Effect.fnUntraced(function* (old, change) {
89
90
  event)) FROM tags_moved WHERE rows = 0)::text AS emptied`;
90
91
  if (result.emptied !== null)
91
92
  yield* sql `DELETE FROM actor_subscription_tags t
92
- USING json_array_elements(${result.emptied}::json) AS e
93
- WHERE t.routing_key = (e->>0)::bigint AND t.tenant_id = e->>1
94
- AND t.source_type = e->>2 AND t.source_id = e->>3 AND t.event = e->>4
93
+ USING (SELECT (e->>0)::bigint AS routing_key, e->>1 AS tenant_id, e->>2 AS source_type,
94
+ e->>3 AS source_id, e->>4 AS event
95
+ FROM json_array_elements(${result.emptied}::json) AS e) AS k
96
+ WHERE t.routing_key = k.routing_key AND t.tenant_id = k.tenant_id
97
+ AND t.source_type = k.source_type AND t.source_id = k.source_id AND t.event = k.event
95
98
  AND t.rows = 0`;
96
99
  return result.rows;
97
100
  }));