@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
@@ -1,1059 +0,0 @@
1
- import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, Layer, Schedule, Schema, } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import { Actor, Intent } from "../../index.js";
4
- import { TurnHooks } from "../../runtime/turn/hooks.js";
5
- import { wakeWaiting } from "../../runtime/jobs/attempt.js";
6
- import { claimCapped } from "../../runtime/turn/relay.js";
7
- import { ActorTest } from "../actor-test.js";
8
- import { ActorCluster } from "../cluster.js";
9
- export const effectControlFixture = () => ({
10
- attempts: [],
11
- provider: (attempt) => Effect.succeed(attempt.label),
12
- hook: () => Effect.void,
13
- });
14
- const reset = (fixture) => Effect.sync(() => {
15
- fixture.attempts.length = 0;
16
- fixture.provider = (attempt) => Effect.succeed(attempt.label);
17
- fixture.hook = () => Effect.void;
18
- });
19
- class ControlDown extends Schema.TaggedError()("ControlDown", {}) {
20
- }
21
- const Job = Actor.job("Job", {
22
- payload: { label: Schema.String },
23
- success: Schema.String,
24
- });
25
- const Capped = Actor.job("Capped", {
26
- payload: { label: Schema.String },
27
- success: Schema.String,
28
- });
29
- const Serial = Actor.job("Serial", {
30
- payload: { label: Schema.String },
31
- success: Schema.String,
32
- });
33
- const Picky = Actor.job("Picky", {
34
- payload: { label: Schema.String },
35
- success: Schema.String,
36
- });
37
- const EffectName = Schema.Literals(["Job", "Capped", "Serial", "Picky"]);
38
- const Perform = Actor.command("Perform", {
39
- payload: Schema.Struct({
40
- effect: EffectName,
41
- labels: Schema.Array(Schema.String),
42
- keyed: Schema.optional(Schema.Boolean),
43
- afterMs: Schema.optional(Schema.Int),
44
- }),
45
- });
46
- const CancelEffect = Actor.command("CancelEffect", { payload: Schema.Array(Schema.String) });
47
- const PerformThenCancel = Actor.command("PerformThenCancel", { payload: Schema.String });
48
- const CancelThenRefuse = Actor.command("CancelThenRefuse", {
49
- payload: Schema.String,
50
- error: ControlDown,
51
- });
52
- const CancelThenDie = Actor.command("CancelThenDie", { payload: Schema.String });
53
- const CaptureCancel = Actor.command("CaptureCancel", {});
54
- const UseCaptured = Actor.command("UseCaptured", { payload: Schema.String });
55
- const Done = Actor.command("Done", { payload: Schema.String });
56
- const JobCancelled = Actor.command("JobCancelled", { payload: Actor.Cancelled(Job) });
57
- const CappedCancelled = Actor.command("CappedCancelled", { payload: Actor.Cancelled(Capped) });
58
- const SerialFailed = Actor.command("SerialFailed", { payload: Actor.DeadLetter(Serial) });
59
- const PickyDone = Actor.command("PickyDone", {
60
- payload: Schema.String.check(Schema.isPattern(/^accepted$/u)),
61
- });
62
- const PickyCancelled = Actor.command("PickyCancelled", { payload: Actor.Cancelled(Picky) });
63
- const Report = Schema.Struct({
64
- label: Schema.String,
65
- effectId: Schema.String,
66
- outcome: Schema.String,
67
- ambiguous: Schema.Boolean,
68
- value: Schema.optional(Schema.String),
69
- cause: Schema.optional(Schema.String),
70
- });
71
- const Controlled = Actor.make("Controlled", {
72
- key: Schema.String,
73
- state: Actor.state({
74
- done: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
75
- cancelled: Schema.Array(Report).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
76
- letters: Schema.Array(Report).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
77
- }),
78
- jobs: {
79
- Job: {
80
- job: Job,
81
- retry: { times: 2, backoff: { base: "1 second", max: "1 second" } },
82
- onSuccess: Done,
83
- onCancelled: JobCancelled,
84
- },
85
- Capped: {
86
- job: Capped,
87
- retry: { times: 1, backoff: { base: "1 second", max: "1 second" } },
88
- concurrency: { perActor: 2 },
89
- onSuccess: Done,
90
- onCancelled: CappedCancelled,
91
- },
92
- Serial: {
93
- job: Serial,
94
- retry: { times: 1, backoff: { base: "1 second", max: "1 second" } },
95
- concurrency: { perActor: 1 },
96
- onSuccess: Done,
97
- onDeadLetter: SerialFailed,
98
- },
99
- Picky: { job: Picky, retry: { times: 0 }, onSuccess: PickyDone, onCancelled: PickyCancelled },
100
- },
101
- api: {
102
- Perform,
103
- CancelEffect,
104
- PerformThenCancel,
105
- CancelThenRefuse,
106
- CancelThenDie,
107
- CaptureCancel,
108
- UseCaptured,
109
- },
110
- internal: { Done, JobCancelled, CappedCancelled, SerialFailed, PickyDone, PickyCancelled },
111
- });
112
- const reportCancelled = (cancelled) => Effect.gen(function* () {
113
- const turn = yield* Controlled.Turn;
114
- const { outcome } = cancelled;
115
- yield* turn.state.set({
116
- cancelled: [
117
- ...turn.state.cancelled,
118
- {
119
- label: cancelled.job.label,
120
- effectId: cancelled.jobId,
121
- outcome: outcome._tag,
122
- ambiguous: cancelled.ambiguous,
123
- ...("value" in outcome ? { value: String(outcome.value) } : { cause: outcome.cause }),
124
- },
125
- ],
126
- });
127
- });
128
- let captured;
129
- /** The effect-control actors' command layers, which every runner builds. */
130
- export const effectControlLayer = Controlled.toLayer(Effect.succeed({
131
- Perform: Effect.fnUntraced(function* ({ effect, labels, keyed, afterMs }) {
132
- const turn = yield* Controlled.Turn;
133
- for (const label of labels) {
134
- const instance = { Job, Capped, Serial, Picky }[effect].make({ label });
135
- const options = {};
136
- if (keyed === true)
137
- options.key = label;
138
- if (afterMs !== undefined)
139
- options.after = Duration.millis(afterMs);
140
- yield* turn.enqueue(instance, options);
141
- }
142
- }),
143
- CancelEffect: Effect.fnUntraced(function* (keys) {
144
- const turn = yield* Controlled.Turn;
145
- for (const key of keys)
146
- yield* turn.cancelJob(key);
147
- }),
148
- PerformThenCancel: Effect.fnUntraced(function* (label) {
149
- const turn = yield* Controlled.Turn;
150
- yield* turn.enqueue(Job.make({ label }), { key: label });
151
- yield* turn.cancelJob(label);
152
- }),
153
- CancelThenRefuse: Effect.fnUntraced(function* (key) {
154
- yield* (yield* Controlled.Turn).cancelJob(key);
155
- return yield* ControlDown.make({});
156
- }),
157
- CancelThenDie: Effect.fnUntraced(function* (key) {
158
- yield* (yield* Controlled.Turn).cancelJob(key);
159
- return yield* Effect.die(new Error("Canceller defect"));
160
- }),
161
- CaptureCancel: Effect.fnUntraced(function* () {
162
- captured = (yield* Controlled.Turn).cancelJob;
163
- }),
164
- UseCaptured: Effect.fnUntraced(function* (key) {
165
- if (captured !== undefined)
166
- yield* captured(key);
167
- }),
168
- Done: Effect.fnUntraced(function* (value) {
169
- const turn = yield* Controlled.Turn;
170
- yield* turn.state.set({ done: [...turn.state.done, value] });
171
- }),
172
- JobCancelled: reportCancelled,
173
- CappedCancelled: (cancelled) => reportCancelled(cancelled),
174
- PickyDone: Effect.fnUntraced(function* (value) {
175
- const turn = yield* Controlled.Turn;
176
- yield* turn.state.set({ done: [...turn.state.done, value] });
177
- }),
178
- PickyCancelled: (cancelled) => reportCancelled(cancelled),
179
- SerialFailed: Effect.fnUntraced(function* (letter) {
180
- const turn = yield* Controlled.Turn;
181
- yield* turn.state.set({
182
- letters: [
183
- ...turn.state.letters,
184
- {
185
- label: letter.job.label,
186
- effectId: letter.jobId,
187
- outcome: "DeadLetter",
188
- ambiguous: letter.ambiguous,
189
- cause: letter.cause,
190
- },
191
- ],
192
- });
193
- }),
194
- }));
195
- /** The executors of `runner`. */
196
- const runnerEffects = (fixture, runner) => {
197
- const execute = (effect, label) => Effect.gen(function* () {
198
- const exec = yield* Controlled.Executor;
199
- const attempt = {
200
- label,
201
- actor: exec.ref.id,
202
- effect,
203
- jobId: exec.jobId,
204
- attempt: exec.attempt,
205
- runner,
206
- startedAt: yield* Clock.currentTimeMillis,
207
- endedAt: undefined,
208
- interrupted: false,
209
- };
210
- fixture.attempts.push(attempt);
211
- return yield* Effect.suspend(() => fixture.provider(attempt)).pipe(Effect.onExit((exit) => Effect.map(Clock.currentTimeMillis, (now) => {
212
- attempt.endedAt = now;
213
- attempt.interrupted = Exit.hasInterrupts(exit);
214
- })));
215
- });
216
- return Controlled.toJobLayer(Effect.succeed({
217
- Job: ({ label }) => execute("Job", label),
218
- Capped: ({ label }) => execute("Capped", label),
219
- Serial: ({ label }) => execute("Serial", label),
220
- Picky: ({ label }) => execute("Picky", label),
221
- }));
222
- };
223
- /** The executors of the conformance environment's single runtime. */
224
- export const effectControlEffects = (fixture) => runnerEffects(fixture, 0);
225
- /** A poll far longer than any case, so only wakes after commit and `advance` claim rows. */
226
- const NO_POLL = { poll: "1 hour" };
227
- const SHORT_LEASE = { lease: "3 seconds" };
228
- /** Builds a fresh database and `runners` runners on it for one case. */
229
- const withCluster = (environment, fixture, runners, settings, body) => environment.run(Effect.gen(function* () {
230
- yield* reset(fixture);
231
- const database = yield* environment.freshDatabase;
232
- const context = yield* Layer.build(ActorTest.cluster({
233
- database,
234
- runners,
235
- shardLockExpiration: "3 seconds",
236
- actors: effectControlLayer,
237
- runnerActors: (runner) => runnerEffects(fixture, runner),
238
- relay: settings.relay,
239
- executors: settings.executors,
240
- })).pipe(Effect.provideService(TurnHooks, {
241
- at: (point, request) => Effect.suspend(() => fixture.hook(point, request)),
242
- }));
243
- return yield* body.pipe(Effect.provideContext(context));
244
- }));
245
- const on = (runner, effect) => ActorCluster.use((cluster) => cluster.on(runner)(effect));
246
- const perform = (id, effect, labels, options = {}) => Controlled.get(id).pipe(Effect.flatMap((actor) => actor.Perform({ effect, labels, ...options })), Effect.orDie);
247
- const cancel = (id, keys) => Controlled.get(id).pipe(Effect.flatMap((actor) => actor.CancelEffect(keys)), Effect.orDie);
248
- const stateOf = (id) => Effect.gen(function* () {
249
- const actor = yield* Controlled.get(id);
250
- const { state } = yield* (yield* ActorTest).inspect(actor.ref);
251
- return state;
252
- });
253
- const effectRows = (sql) => sql `SELECT command, kind, attempts, running, cancelled_at_ms IS NOT NULL AS cancelled,
254
- waiting, timer_key
255
- FROM actor_outbox WHERE actor_type = 'Controlled' ORDER BY ready_at_ms, intent_id`;
256
- const deadLetters = (sql) => sql `SELECT attempts, ambiguous, cause
257
- FROM actor_dead_letters WHERE actor_type = 'Controlled'`;
258
- /** Runs a query on `runner`'s database pool. */
259
- const query = (runner, statement) => on(runner, Effect.gen(function* () {
260
- return yield* statement(yield* SqlClient.SqlClient);
261
- })).pipe(Effect.orDie);
262
- /** Polls `check` until it holds, dying after `timeout`. */
263
- const eventually = (check, timeout = "20 seconds", what = "a condition") => check.pipe(Effect.repeat({ schedule: Schedule.spaced("25 millis"), until: (held) => held }), Effect.timeoutOrElse({
264
- duration: timeout,
265
- orElse: () => Effect.die(new Error(`Timed out waiting for ${what}`)),
266
- }), Effect.asVoid);
267
- /**
268
- * Waits until no outbox row is left. The relay commits a route's turn before
269
- * it deletes the route's row, so a route can show in state while its row
270
- * still exists.
271
- */
272
- const drained = (rows) => eventually(rows.pipe(Effect.map((left) => left.length === 0)), "5 seconds", "the delivered route's outbox row to be deleted");
273
- const advance = (runner, duration) => on(runner, ActorTest.use((test) => test.advance(duration)));
274
- /** The runner that owns `ref`'s shard, and another one. */
275
- const ownerAndOther = (ref) => Effect.gen(function* () {
276
- const cluster = yield* ActorCluster;
277
- const owner = (yield* cluster.owner(ref));
278
- return { owner, other: (owner + 1) % cluster.runners };
279
- });
280
- const refOf = (id) => on(0, Controlled.get(id).pipe(Effect.map((actor) => actor.ref)));
281
- /** The most attempts of one actor's `effect` whose runs overlapped in time. */
282
- const maxInFlight = (attempts, actor, effect) => {
283
- const mine = attempts.filter((attempt) => attempt.actor === actor && attempt.effect === effect);
284
- return Math.max(0, ...mine.map((attempt) => 1 +
285
- mine.filter((other) => other !== attempt &&
286
- other.startedAt <= attempt.startedAt &&
287
- (other.endedAt ?? Number.POSITIVE_INFINITY) > attempt.startedAt).length));
288
- };
289
- const thrown = (build) => {
290
- try {
291
- build();
292
- return "";
293
- }
294
- catch (error) {
295
- return error instanceof Error ? error.message : String(error);
296
- }
297
- };
298
- /** Single-runtime cases, on every backend. */
299
- export const effectControlConformance = [
300
- {
301
- name: "never runs a pending keyed effect cancelled by a later turn or in its own turn",
302
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
303
- yield* reset(fixture);
304
- const sql = yield* SqlClient.SqlClient;
305
- const test = yield* ActorTest;
306
- yield* perform("pending", "Job", ["later"], { keyed: true, afterMs: 60_000 });
307
- expect(yield* effectRows(sql)).toMatchObject([
308
- { command: "Job", timer_key: "$job:later", attempts: 0 },
309
- ]);
310
- yield* cancel("pending", ["later", "never-performed"]);
311
- const actor = yield* Controlled.get("pending");
312
- yield* actor.PerformThenCancel("same-turn").pipe(Effect.orDie);
313
- yield* test.advance("2 minutes");
314
- expect(fixture.attempts).toEqual([]);
315
- expect(yield* effectRows(sql)).toEqual([]);
316
- const pending = yield* stateOf("pending");
317
- expect([...(pending.done ?? []), ...(pending.cancelled ?? [])]).toEqual([]);
318
- })),
319
- },
320
- {
321
- name: "replaces a pending keyed effect performed again under its key",
322
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
323
- yield* reset(fixture);
324
- const test = yield* ActorTest;
325
- fixture.provider = (attempt) => Effect.succeed(`${attempt.label}@${attempt.attempt}`);
326
- const actor = yield* Controlled.get("replaced");
327
- yield* actor
328
- .Perform({ effect: "Job", labels: ["k"], keyed: true, afterMs: 60_000 })
329
- .pipe(Effect.orDie);
330
- yield* actor
331
- .Perform({ effect: "Job", labels: ["k"], keyed: true, afterMs: 30_000 })
332
- .pipe(Effect.orDie);
333
- yield* test.advance("2 minutes");
334
- expect(fixture.attempts.map(({ label }) => label)).toEqual(["k"]);
335
- expect((yield* stateOf("replaced")).done).toEqual(["k@1"]);
336
- })),
337
- },
338
- {
339
- name: "reports Failed when cancelled while backing off after a typed failure, and never retries",
340
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
341
- yield* reset(fixture);
342
- const sql = yield* SqlClient.SqlClient;
343
- const test = yield* ActorTest;
344
- fixture.provider = () => Effect.fail(ControlDown.make({}));
345
- yield* perform("typed", "Job", ["typed"], { keyed: true });
346
- yield* eventually(Effect.sync(() => fixture.attempts[0]?.endedAt !== undefined));
347
- yield* eventually(effectRows(sql).pipe(Effect.map((rows) => rows[0]?.running === false)), "5 seconds", "the failure's backoff");
348
- yield* cancel("typed", ["typed"]);
349
- yield* test.advance("5 seconds");
350
- expect(fixture.attempts.length).toBe(1);
351
- expect((yield* stateOf("typed")).cancelled).toMatchObject([
352
- { label: "typed", outcome: "Failed", ambiguous: false },
353
- ]);
354
- expect(yield* effectRows(sql)).toEqual([]);
355
- })),
356
- },
357
- {
358
- name: "reports Unknown when cancelled while backing off after an attempt that may have applied",
359
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
360
- yield* reset(fixture);
361
- const sql = yield* SqlClient.SqlClient;
362
- const test = yield* ActorTest;
363
- fixture.provider = () => Effect.die(new Error("provider connection reset"));
364
- yield* perform("maybe", "Job", ["maybe"], { keyed: true });
365
- yield* eventually(effectRows(sql).pipe(Effect.map((rows) => rows[0]?.attempts === 1 && rows[0].running === false)), "5 seconds", "the defect's backoff");
366
- yield* cancel("maybe", ["maybe"]);
367
- yield* test.advance("5 seconds");
368
- expect(fixture.attempts.length).toBe(1);
369
- expect((yield* stateOf("maybe")).cancelled).toMatchObject([
370
- { label: "maybe", outcome: "Unknown", ambiguous: true },
371
- ]);
372
- })),
373
- },
374
- {
375
- name: "interrupts a running attempt its own runner cancels and reports Unknown, never Failed",
376
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
377
- yield* reset(fixture);
378
- const sql = yield* SqlClient.SqlClient;
379
- const test = yield* ActorTest;
380
- fixture.provider = () => Effect.never;
381
- yield* perform("running", "Job", ["running"], { keyed: true });
382
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
383
- const cancelledAt = yield* Clock.currentTimeMillis;
384
- yield* cancel("running", ["running"]);
385
- yield* eventually(Effect.sync(() => fixture.attempts[0]?.interrupted === true), "5 seconds", "the interruption");
386
- expect(fixture.attempts[0].endedAt - cancelledAt < 5000).toBe(true);
387
- yield* eventually(stateOf("running").pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)), "5 seconds", "the cancellation report");
388
- yield* test.advance("5 minutes");
389
- expect(fixture.attempts.length).toBe(1);
390
- expect((yield* stateOf("running")).cancelled).toMatchObject([
391
- { label: "running", outcome: "Unknown", ambiguous: true },
392
- ]);
393
- expect(yield* effectRows(sql)).toEqual([]);
394
- })),
395
- },
396
- {
397
- name: "interrupts at once an attempt its own runner cancels between its claim and its call",
398
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
399
- yield* reset(fixture);
400
- const sql = yield* SqlClient.SqlClient;
401
- const test = yield* ActorTest;
402
- fixture.provider = () => Effect.never;
403
- const paused = yield* test.pauseNext("beforeExecute");
404
- yield* perform("claimed", "Job", ["claimed"], { keyed: true });
405
- yield* paused.reached;
406
- const cancelledAt = yield* Clock.currentTimeMillis;
407
- yield* cancel("claimed", ["claimed"]);
408
- yield* paused.release;
409
- yield* eventually(Effect.sync(() => fixture.attempts[0]?.interrupted === true), "5 seconds", "the interruption");
410
- expect(fixture.attempts[0].endedAt - cancelledAt < 5000).toBe(true);
411
- yield* eventually(stateOf("claimed").pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)), "5 seconds", "the cancellation report");
412
- expect(fixture.attempts.length).toBe(1);
413
- expect((yield* stateOf("claimed")).cancelled).toMatchObject([
414
- { label: "claimed", outcome: "Unknown", ambiguous: true },
415
- ]);
416
- yield* drained(effectRows(sql));
417
- })),
418
- },
419
- {
420
- name: "dead-letters an ambiguous cancellation as ambiguous and drops a failed one without onCancelled",
421
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
422
- yield* reset(fixture);
423
- const sql = yield* SqlClient.SqlClient;
424
- const test = yield* ActorTest;
425
- fixture.provider = (attempt) => attempt.label === "hung" ? Effect.never : Effect.fail(ControlDown.make({}));
426
- yield* perform("unrouted", "Serial", ["hung"], { keyed: true });
427
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
428
- yield* cancel("unrouted", ["hung"]);
429
- yield* eventually(stateOf("unrouted").pipe(Effect.map((state) => (state.letters ?? []).length === 1)), "5 seconds", "the dead letter");
430
- expect(yield* deadLetters(sql)).toMatchObject([{ attempts: 1, ambiguous: true }]);
431
- yield* perform("unrouted", "Serial", ["refused"], { keyed: true });
432
- yield* eventually(effectRows(sql).pipe(Effect.map((rows) => rows[0]?.attempts === 1 && rows[0].running === false)), "5 seconds", "the typed failure's backoff");
433
- yield* cancel("unrouted", ["refused"]);
434
- yield* test.advance("5 seconds");
435
- expect(fixture.attempts.map(({ label }) => label)).toEqual(["hung", "refused"]);
436
- expect((yield* stateOf("unrouted")).letters?.length).toBe(1);
437
- expect((yield* deadLetters(sql)).length).toBe(1);
438
- expect(yield* effectRows(sql)).toEqual([]);
439
- })),
440
- },
441
- {
442
- name: "runs one capped attempt at a time per actor, in perform order, without holding other actors back",
443
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
444
- yield* reset(fixture);
445
- fixture.provider = (attempt) => Effect.sleep("20 millis").pipe(Effect.as(attempt.label));
446
- const labels = Array.from({ length: 6 }, (_, index) => `hot-${index}`);
447
- yield* perform("hot", "Serial", labels.slice(0, 3));
448
- for (const label of labels.slice(3))
449
- yield* perform("hot", "Serial", [label]);
450
- yield* perform("cold", "Serial", ["cold-0"]);
451
- yield* eventually(stateOf("hot").pipe(Effect.map((state) => (state.done ?? []).length === 6)), "20 seconds", "the hot actor's effects");
452
- yield* eventually(stateOf("cold").pipe(Effect.map((state) => (state.done ?? []).length === 1)));
453
- expect(maxInFlight(fixture.attempts, "hot", "Serial")).toBe(1);
454
- expect(fixture.attempts.filter(({ actor }) => actor === "hot").map(({ label }) => label)).toEqual(labels);
455
- const cold = fixture.attempts.find(({ actor }) => actor === "cold");
456
- const lastHot = fixture.attempts.filter(({ actor }) => actor === "hot").at(-1);
457
- expect(cold.startedAt <= lastHot.startedAt).toBe(true);
458
- })),
459
- },
460
- {
461
- name: "rejects effect keys, intent keys in the effect namespace, and caps out of range",
462
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
463
- const actor = yield* Controlled.get("keys");
464
- const failure = yield* actor
465
- .Perform({ effect: "Job", labels: ["x".repeat(201)], keyed: true })
466
- .pipe(Effect.exit);
467
- expect(Exit.isFailure(failure)).toBe(true);
468
- for (const perActor of [0, 65, 1.5])
469
- expect(thrown(() => Actor.make("BadCap", {
470
- jobs: { Job: { job: Job, concurrency: { perActor } } },
471
- api: { Done },
472
- }))).toContain("perActor");
473
- })),
474
- },
475
- {
476
- name: "cancels nothing when the cancelling turn rolls back",
477
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
478
- yield* reset(fixture);
479
- const test = yield* ActorTest;
480
- const actor = yield* Controlled.get("rollback");
481
- yield* perform("rollback", "Job", ["kept"], { keyed: true, afterMs: 60_000 });
482
- expect(yield* actor.CancelThenRefuse("kept").pipe(Effect.flip)).toBeInstanceOf(ControlDown);
483
- const died = yield* actor.CancelThenDie("kept").pipe(Effect.exit);
484
- expect(Exit.isFailure(died) && Cause.pretty(died.cause)).toContain("Canceller defect");
485
- yield* perform("rollback", "Job", ["retried"], { keyed: true, afterMs: 60_000 });
486
- yield* test.crashNext("beforeCommit");
487
- yield* actor.CancelEffect(["retried"]).pipe(Effect.orDie);
488
- yield* test.advance("2 minutes");
489
- expect(fixture.attempts.map(({ label }) => label)).toEqual(["kept"]);
490
- const state = yield* stateOf("rollback");
491
- expect(state.done).toEqual(["kept"]);
492
- expect(state.cancelled ?? []).toEqual([]);
493
- })),
494
- },
495
- {
496
- name: "does nothing when cancelling an effect that already completed",
497
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
498
- yield* reset(fixture);
499
- const sql = yield* SqlClient.SqlClient;
500
- const test = yield* ActorTest;
501
- yield* perform("completed", "Job", ["done"], { keyed: true });
502
- yield* eventually(stateOf("completed").pipe(Effect.map((state) => (state.done ?? []).length === 1)));
503
- yield* cancel("completed", ["done"]);
504
- yield* test.advance("1 minute");
505
- expect(fixture.attempts.length).toBe(1);
506
- const state = yield* stateOf("completed");
507
- expect(state.done).toEqual(["done"]);
508
- expect(state.cancelled ?? []).toEqual([]);
509
- expect(yield* effectRows(sql)).toEqual([]);
510
- yield* perform("completed", "Job", ["done"], { keyed: true });
511
- yield* eventually(stateOf("completed").pipe(Effect.map((state) => (state.done ?? []).length === 2)));
512
- })),
513
- },
514
- {
515
- name: "replaces a running keyed effect: reports the old one and runs the new one under a new id",
516
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
517
- yield* reset(fixture);
518
- fixture.provider = (attempt) => fixture.attempts.indexOf(attempt) === 0 ? Effect.never : Effect.succeed("second");
519
- yield* perform("rerun", "Job", ["k"], { keyed: true });
520
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
521
- yield* perform("rerun", "Job", ["k"], { keyed: true });
522
- yield* eventually(stateOf("rerun").pipe(Effect.map((state) => (state.done ?? []).length === 1 && (state.cancelled ?? []).length === 1)));
523
- const [first, second] = fixture.attempts;
524
- expect(first.interrupted).toBe(true);
525
- expect(second.jobId === first.jobId).toBe(false);
526
- const state = yield* stateOf("rerun");
527
- expect(state.done).toEqual(["second"]);
528
- expect(state.cancelled).toMatchObject([
529
- { effectId: first.jobId, outcome: "Unknown", ambiguous: true },
530
- ]);
531
- })),
532
- },
533
- {
534
- name: "rejects reserved intent keys and a captured cancelJob",
535
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
536
- expect(thrown(() => Intent.key("$job:x"))).toContain('Intent key "$job:x" is reserved for jobs');
537
- const inTurn = yield* Intent.cancel("$job:x").pipe(Effect.provideService(Actor.InTurn, Actor.InTurn.of({ turn: Symbol() })), Effect.exit);
538
- expect(Exit.isFailure(inTurn) && Cause.pretty(inTurn.cause)).toContain("is reserved for jobs");
539
- const actor = yield* Controlled.get("escape");
540
- yield* actor.CaptureCancel().pipe(Effect.orDie);
541
- const stolen = yield* actor.UseCaptured("k").pipe(Effect.exit);
542
- expect(Exit.isFailure(stolen) && Cause.pretty(stolen.cause)).toContain("Job capability escaped its turn");
543
- })),
544
- },
545
- ];
546
- /** Multi-runner cases: real Postgres only, each on a fresh database and cluster. */
547
- export const effectControlClusterConformance = [
548
- {
549
- name: "never shortens the lease of a waiting row a claim takes while a settle wakes it",
550
- requiresIndependentConnections: true,
551
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
552
- const sql = yield* SqlClient.SqlClient;
553
- const group = {
554
- routing_key: "7",
555
- tenant_id: "wake-race",
556
- actor_type: "Controlled",
557
- actor_id: "raced",
558
- command: "Serial",
559
- };
560
- const now = yield* Clock.currentTimeMillis;
561
- const leaseEnd = now + 60_000;
562
- yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
563
- VALUES (7, 'wake-race', 'Controlled', 'raced')`;
564
- yield* sql `INSERT INTO actor_outbox ${sql.insert(["oldest", "next"].map((intentId, index) => ({
565
- routing_key: 7,
566
- intent_id: intentId,
567
- kind: "job",
568
- bucket: 0,
569
- due_at_ms: leaseEnd,
570
- scheduled_at_ms: now + index,
571
- ready_at_ms: now + index,
572
- waiting: true,
573
- tenant_id: "wake-race",
574
- actor_type: "Controlled",
575
- actor_id: "raced",
576
- target_type: "Controlled",
577
- target_id: "raced",
578
- command: "Serial",
579
- payload: "{}",
580
- caller: "{}",
581
- })))}`;
582
- const locked = yield* Deferred.make();
583
- const release = yield* Deferred.make();
584
- const claim = yield* sql
585
- .withTransaction(Effect.gen(function* () {
586
- yield* sql `SELECT 1 FROM actor_outbox
587
- WHERE routing_key = 7 AND intent_id = 'oldest' FOR UPDATE`;
588
- yield* sql `UPDATE actor_outbox SET running = true, waiting = false,
589
- attempts = 1, due_at_ms = ${leaseEnd}
590
- WHERE routing_key = 7 AND intent_id = 'oldest'`;
591
- yield* Deferred.succeed(locked, undefined);
592
- yield* Deferred.await(release);
593
- }))
594
- .pipe(Effect.forkChild);
595
- yield* Deferred.await(locked);
596
- const wake = yield* wakeWaiting({ sql, group, at: now }).pipe(Effect.forkChild);
597
- yield* eventually(Effect.gen(function* () {
598
- if (wake.pollUnsafe() !== undefined)
599
- return true;
600
- const [blocked] = yield* sql `SELECT count(*)::int AS n
601
- FROM pg_stat_activity
602
- WHERE datname = current_database()
603
- AND wait_event_type = 'Lock' AND query LIKE 'UPDATE actor_outbox SET due_at_ms%'`;
604
- return blocked.n > 0;
605
- }).pipe(Effect.orDie), "10 seconds", "the wake to finish or wait on the claim's row lock");
606
- yield* Deferred.succeed(release, undefined);
607
- yield* Fiber.join(claim);
608
- yield* Fiber.join(wake);
609
- const rows = yield* sql `SELECT intent_id AS id, running, waiting, due_at_ms::text AS due FROM actor_outbox
610
- WHERE routing_key = 7 ORDER BY intent_id`;
611
- expect(rows).toEqual([
612
- { id: "next", running: false, waiting: false, due: String(now) },
613
- { id: "oldest", running: true, waiting: false, due: String(leaseEnd) },
614
- ]);
615
- })),
616
- },
617
- {
618
- name: "wakes the oldest waiting row only after a claim of its group has committed",
619
- requiresIndependentConnections: true,
620
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
621
- const sql = yield* SqlClient.SqlClient;
622
- const group = {
623
- routing_key: "8",
624
- tenant_id: "wake-order",
625
- actor_type: "Controlled",
626
- actor_id: "ordered",
627
- command: "Serial",
628
- };
629
- const now = yield* Clock.currentTimeMillis;
630
- yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
631
- VALUES (8, 'wake-order', 'Controlled', 'ordered')`;
632
- yield* sql `INSERT INTO actor_outbox ${sql.insert({
633
- routing_key: 8,
634
- intent_id: "oldest",
635
- kind: "job",
636
- bucket: 0,
637
- due_at_ms: now + 60_000,
638
- scheduled_at_ms: now,
639
- ready_at_ms: now,
640
- waiting: true,
641
- tenant_id: "wake-order",
642
- actor_type: "Controlled",
643
- actor_id: "ordered",
644
- target_type: "Controlled",
645
- target_id: "ordered",
646
- command: "Serial",
647
- payload: "{}",
648
- caller: "{}",
649
- })}`;
650
- const locked = yield* Deferred.make();
651
- const release = yield* Deferred.make();
652
- const claim = yield* sql
653
- .withTransaction(Effect.gen(function* () {
654
- yield* sql `SELECT 1 FROM actor_generations
655
- WHERE routing_key = 8 AND tenant_id = 'wake-order'
656
- AND actor_type = 'Controlled' AND actor_id = 'ordered' FOR UPDATE`;
657
- yield* Deferred.succeed(locked, undefined);
658
- yield* Deferred.await(release);
659
- }))
660
- .pipe(Effect.forkChild);
661
- yield* Deferred.await(locked);
662
- const wake = yield* wakeWaiting({ sql, group, at: now }).pipe(Effect.forkChild);
663
- yield* eventually(Effect.gen(function* () {
664
- const [blocked] = yield* sql `SELECT count(*)::int AS n
665
- FROM pg_stat_activity
666
- WHERE datname = current_database()
667
- AND wait_event_type = 'Lock' AND wait_event IN ('tuple', 'transactionid')`;
668
- return blocked.n > 0;
669
- }).pipe(Effect.orDie), "10 seconds", "the wake to wait on the group's lock");
670
- expect(wake.pollUnsafe()).toBe(undefined);
671
- yield* Deferred.succeed(release, undefined);
672
- yield* Fiber.join(claim);
673
- yield* Fiber.join(wake);
674
- const [row] = yield* sql `SELECT waiting,
675
- due_at_ms::text AS due FROM actor_outbox WHERE routing_key = 8 AND intent_id = 'oldest'`;
676
- expect(row).toEqual({ waiting: false, due: String(now) });
677
- })),
678
- },
679
- {
680
- name: "never starts a younger capped row while an older attempt's lost lease is being renewed",
681
- requiresIndependentConnections: true,
682
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
683
- const sql = yield* SqlClient.SqlClient;
684
- const group = {
685
- routing_key: "9",
686
- tenant_id: "claim-renew",
687
- actor_type: "Controlled",
688
- actor_id: "renewed",
689
- command: "Serial",
690
- };
691
- const now = yield* Clock.currentTimeMillis;
692
- const renewedUntil = now + 60_000;
693
- yield* sql `INSERT INTO actor_generations (routing_key, tenant_id, actor_type, actor_id)
694
- VALUES (9, 'claim-renew', 'Controlled', 'renewed')`;
695
- yield* sql `INSERT INTO actor_outbox ${sql.insert([
696
- { intent_id: "oldest", running: true, waiting: false, attempts: 1, due: now - 1 },
697
- {
698
- intent_id: "next",
699
- running: false,
700
- waiting: true,
701
- attempts: 0,
702
- due: now + 60_000,
703
- },
704
- ].map(({ due, ...row }, index) => ({
705
- ...row,
706
- routing_key: 9,
707
- kind: "job",
708
- bucket: 0,
709
- due_at_ms: due,
710
- scheduled_at_ms: now + index,
711
- ready_at_ms: now + index,
712
- tenant_id: "claim-renew",
713
- actor_type: "Controlled",
714
- actor_id: "renewed",
715
- target_type: "Controlled",
716
- target_id: "renewed",
717
- command: "Serial",
718
- payload: "{}",
719
- caller: "{}",
720
- })))}`;
721
- const locked = yield* Deferred.make();
722
- const release = yield* Deferred.make();
723
- const renewal = yield* sql
724
- .withTransaction(Effect.gen(function* () {
725
- yield* sql `UPDATE actor_outbox SET due_at_ms = ${renewedUntil}
726
- WHERE routing_key = 9 AND intent_id = 'oldest' AND attempts = 1`;
727
- yield* Deferred.succeed(locked, undefined);
728
- yield* Deferred.await(release);
729
- }))
730
- .pipe(Effect.forkChild);
731
- yield* Deferred.await(locked);
732
- const claim = yield* claimCapped({
733
- sql,
734
- now: sql `${now}::bigint`,
735
- group,
736
- cap: 1,
737
- maxAttempts: 3,
738
- permits: 1,
739
- leaseMs: 60_000,
740
- }).pipe(Effect.forkChild);
741
- yield* eventually(Effect.gen(function* () {
742
- if (claim.pollUnsafe() !== undefined)
743
- return true;
744
- const [blocked] = yield* sql `SELECT count(*)::int AS n
745
- FROM pg_stat_activity
746
- WHERE datname = current_database() AND wait_event_type = 'Lock'`;
747
- return blocked.n > 0;
748
- }).pipe(Effect.orDie), "10 seconds", "the claim to finish or wait on the renewal's row lock");
749
- yield* Deferred.succeed(release, undefined);
750
- yield* Fiber.join(renewal);
751
- expect((yield* Fiber.join(claim)).map(({ intent_id }) => intent_id)).toEqual([]);
752
- const rows = yield* sql `
753
- SELECT intent_id AS id, running, due_at_ms::text AS due FROM actor_outbox
754
- WHERE routing_key = 9 ORDER BY intent_id`;
755
- expect(rows).toEqual([
756
- { id: "next", running: false, due: String(now + 60_000) },
757
- { id: "oldest", running: true, due: String(renewedUntil) },
758
- ]);
759
- })),
760
- },
761
- {
762
- name: "caps one actor's running attempts across three runners while other actors proceed",
763
- requiresFreshDatabase: true,
764
- requiresIndependentConnections: true,
765
- timeoutMs: 120_000,
766
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, {}, Effect.gen(function* () {
767
- fixture.provider = (attempt) => Effect.sleep("100 millis").pipe(Effect.as(attempt.label));
768
- const labels = Array.from({ length: 12 }, (_, index) => `capped-${index}`);
769
- yield* on(1, perform("capped", "Capped", labels));
770
- yield* on(2, Effect.forEach(Array.from({ length: 6 }, (_, index) => `other-${index}`), (id) => perform(id, "Capped", [id]), { concurrency: 6, discard: true }));
771
- yield* eventually(on(0, stateOf("capped")).pipe(Effect.map((state) => (state.done ?? []).length === 12)), "60 seconds", "every capped effect");
772
- expect(maxInFlight(fixture.attempts, "capped", "Capped") <= 2).toBe(true);
773
- expect(new Set(fixture.attempts.map(({ jobId }) => jobId)).size).toBe(18);
774
- expect(fixture.attempts.every(({ attempt }) => attempt === 1)).toBe(true);
775
- })),
776
- },
777
- {
778
- name: "keeps a killed runner's capped attempt counted until its lease ends, then retries it first",
779
- requiresFreshDatabase: true,
780
- requiresIndependentConnections: true,
781
- timeoutMs: 120_000,
782
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, { executors: { lease: "9 seconds" } }, Effect.gen(function* () {
783
- fixture.provider = (attempt) => attempt.label === "first" && attempt.attempt === 1
784
- ? Effect.never
785
- : Effect.succeed(`${attempt.label}@${attempt.attempt}`);
786
- const { owner, other } = yield* ownerAndOther(yield* refOf("killed"));
787
- yield* on(other, perform("killed", "Serial", ["first"]));
788
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
789
- yield* on(other, perform("killed", "Serial", ["second"]));
790
- const holder = fixture.attempts[0].runner;
791
- const cluster = yield* ActorCluster;
792
- yield* cluster.kill(holder);
793
- const killedAt = yield* Clock.currentTimeMillis;
794
- yield* cluster.ready;
795
- const survivor = [owner, other, (owner + 2) % 3].find((runner) => runner !== holder);
796
- yield* eventually(on(survivor, stateOf("killed")).pipe(Effect.map((state) => (state.done ?? []).length === 2)), "60 seconds", "both effects");
797
- const later = fixture.attempts.slice(1);
798
- expect(later.map(({ label, attempt }) => `${label}@${attempt}`)).toEqual([
799
- "first@2",
800
- "second@1",
801
- ]);
802
- expect(later.every(({ startedAt }) => startedAt >= killedAt)).toBe(true);
803
- expect(maxInFlight(later, "killed", "Serial")).toBe(1);
804
- expect((yield* on(survivor, stateOf("killed"))).done).toEqual(["first@2", "second@1"]);
805
- })),
806
- },
807
- {
808
- name: "frees a capped slot when an attempt loses its lease, interrupting it before the takeover starts",
809
- requiresFreshDatabase: true,
810
- requiresIndependentConnections: true,
811
- timeoutMs: 90_000,
812
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { executors: SHORT_LEASE }, Effect.gen(function* () {
813
- fixture.provider = (attempt) => attempt.attempt === 1 ? Effect.never : Effect.succeed(attempt.label);
814
- fixture.hook = (point) => point === "beforeRenew" && fixture.attempts.length === 1
815
- ? Effect.die(new Error("Database unreachable"))
816
- : Effect.void;
817
- yield* on(0, perform("lost", "Serial", ["lost"]));
818
- yield* eventually(on(0, stateOf("lost")).pipe(Effect.map((state) => (state.done ?? []).length === 1)), "30 seconds", "the takeover");
819
- const [first, second] = fixture.attempts;
820
- expect(first.interrupted).toBe(true);
821
- expect(first.endedAt <= second.startedAt).toBe(true);
822
- expect(maxInFlight(fixture.attempts, "lost", "Serial")).toBe(1);
823
- })),
824
- },
825
- {
826
- name: "keeps a newer capped attempt's slot when an attempt that lost its lease settles late",
827
- requiresFreshDatabase: true,
828
- requiresIndependentConnections: true,
829
- timeoutMs: 90_000,
830
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
831
- const held = yield* Deferred.make();
832
- const newer = yield* Deferred.make();
833
- const settling = yield* Deferred.make();
834
- fixture.provider = (attempt) => attempt.label === "slow" && attempt.attempt === 2
835
- ? Deferred.await(newer).pipe(Effect.as("slow@2"))
836
- : Effect.succeed(`${attempt.label}@${attempt.attempt}`);
837
- fixture.hook = (point) => point === "afterExecute" && fixture.attempts.length === 1
838
- ? Deferred.succeed(settling, undefined).pipe(Effect.andThen(Deferred.await(held)))
839
- : Effect.void;
840
- const { owner, other } = yield* ownerAndOther(yield* refOf("overlap"));
841
- yield* on(owner, perform("overlap", "Serial", ["slow"]));
842
- yield* Deferred.await(settling);
843
- expect(fixture.attempts[0].runner).toBe(owner);
844
- yield* advance(other, "4 seconds").pipe(Effect.forkChild);
845
- yield* eventually(Effect.sync(() => fixture.attempts.length === 2), "10 seconds");
846
- expect(fixture.attempts[1]).toMatchObject({ runner: other, attempt: 2 });
847
- yield* Deferred.succeed(held, undefined);
848
- yield* Effect.sleep("1 second");
849
- yield* on(owner, perform("overlap", "Serial", ["next"]));
850
- yield* Effect.sleep("2 seconds");
851
- expect(fixture.attempts.map(({ label }) => label)).toEqual(["slow", "slow"]);
852
- expect((yield* query(other, effectRows)).find((row) => row.command === "Serial" && row.running)).toMatchObject({ attempts: 2 });
853
- yield* Deferred.succeed(newer, undefined);
854
- yield* eventually(on(other, stateOf("overlap")).pipe(Effect.map((state) => (state.done ?? []).length === 2)), "20 seconds", "both effects");
855
- expect(maxInFlight(fixture.attempts, "overlap", "Serial")).toBe(1);
856
- const { done } = yield* on(other, stateOf("overlap"));
857
- expect(done.filter((label) => label.startsWith("slow")).length).toBe(1);
858
- expect(done).toContain("next@1");
859
- })),
860
- },
861
- {
862
- name: "reaches a running attempt on another runner within cancelCheck and reports Unknown",
863
- requiresFreshDatabase: true,
864
- requiresIndependentConnections: true,
865
- timeoutMs: 90_000,
866
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: { lease: "3 seconds", cancelCheck: "1 second" } }, Effect.gen(function* () {
867
- fixture.provider = () => Effect.never;
868
- const { owner, other } = yield* ownerAndOther(yield* refOf("remote"));
869
- yield* on(owner, perform("remote", "Job", ["remote"], { keyed: true, afterMs: 60_000 }));
870
- yield* advance(other, "1 minute");
871
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
872
- expect(fixture.attempts[0].runner).toBe(other);
873
- const cancelledAt = yield* Clock.currentTimeMillis;
874
- yield* on(owner, cancel("remote", ["remote"]));
875
- yield* eventually(Effect.sync(() => fixture.attempts[0].interrupted), "5 seconds", "the remote interruption");
876
- expect(fixture.attempts[0].endedAt - cancelledAt <= 2500).toBe(true);
877
- yield* eventually(on(owner, stateOf("remote")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
878
- expect((yield* on(owner, stateOf("remote"))).cancelled).toMatchObject([
879
- { outcome: "Unknown", ambiguous: true },
880
- ]);
881
- expect(fixture.attempts.length).toBe(1);
882
- })),
883
- },
884
- {
885
- name: "routes a success that finishes before the cancellation is seen to onCancelled as Succeeded",
886
- requiresFreshDatabase: true,
887
- requiresIndependentConnections: true,
888
- timeoutMs: 90_000,
889
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL }, Effect.gen(function* () {
890
- const gate = yield* Deferred.make();
891
- fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.as(attempt.label));
892
- const { owner, other } = yield* ownerAndOther(yield* refOf("raced"));
893
- yield* on(owner, perform("raced", "Job", ["raced"], { keyed: true, afterMs: 60_000 }));
894
- yield* advance(other, "1 minute").pipe(Effect.forkChild);
895
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
896
- yield* on(owner, cancel("raced", ["raced"]));
897
- yield* Deferred.succeed(gate, undefined);
898
- yield* eventually(on(owner, stateOf("raced")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
899
- const state = yield* on(owner, stateOf("raced"));
900
- expect(state.cancelled).toMatchObject([
901
- { outcome: "Succeeded", value: "raced", ambiguous: false },
902
- ]);
903
- expect(state.done ?? []).toEqual([]);
904
- })),
905
- },
906
- {
907
- name: "routes a capped success that finishes before the cancellation is seen to onCancelled",
908
- requiresFreshDatabase: true,
909
- requiresIndependentConnections: true,
910
- timeoutMs: 90_000,
911
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL }, Effect.gen(function* () {
912
- const gate = yield* Deferred.make();
913
- fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.as(attempt.label));
914
- const { owner, other } = yield* ownerAndOther(yield* refOf("capped-raced"));
915
- yield* on(owner, perform("capped-raced", "Capped", ["capped"], { keyed: true, afterMs: 60_000 }));
916
- yield* advance(other, "1 minute").pipe(Effect.forkChild);
917
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
918
- yield* on(owner, cancel("capped-raced", ["capped"]));
919
- yield* Deferred.succeed(gate, undefined);
920
- yield* eventually(on(owner, stateOf("capped-raced")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
921
- const state = yield* on(owner, stateOf("capped-raced"));
922
- expect(state.cancelled).toMatchObject([
923
- { outcome: "Succeeded", value: "capped", ambiguous: false },
924
- ]);
925
- expect(state.done ?? []).toEqual([]);
926
- yield* drained(query(owner, effectRows));
927
- })),
928
- },
929
- {
930
- name: "routes a cancelled success that onSuccess cannot accept to onCancelled as Succeeded",
931
- requiresFreshDatabase: true,
932
- requiresIndependentConnections: true,
933
- timeoutMs: 90_000,
934
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL }, Effect.gen(function* () {
935
- const gate = yield* Deferred.make();
936
- fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.as(attempt.label));
937
- const { owner, other } = yield* ownerAndOther(yield* refOf("picky"));
938
- yield* on(owner, perform("picky", "Picky", ["rejected"], { keyed: true, afterMs: 60_000 }));
939
- yield* advance(other, "1 minute").pipe(Effect.forkChild);
940
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
941
- yield* on(owner, cancel("picky", ["rejected"]));
942
- yield* Deferred.succeed(gate, undefined);
943
- yield* eventually(on(owner, stateOf("picky")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
944
- const state = yield* on(owner, stateOf("picky"));
945
- expect(state.cancelled).toMatchObject([
946
- { outcome: "Succeeded", value: "rejected", ambiguous: false },
947
- ]);
948
- expect(state.done ?? []).toEqual([]);
949
- yield* drained(query(owner, effectRows));
950
- expect(fixture.attempts.length).toBe(1);
951
- })),
952
- },
953
- {
954
- name: "records a late success after its cancellation settled as ambiguous and routes it once",
955
- requiresFreshDatabase: true,
956
- requiresIndependentConnections: true,
957
- timeoutMs: 90_000,
958
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
959
- const gate = yield* Deferred.make();
960
- fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.as(attempt.label));
961
- const { owner, other } = yield* ownerAndOther(yield* refOf("late"));
962
- const renewal = yield* on(owner, ActorTest.use((test) => test.pauseNext("beforeRenew")));
963
- yield* on(owner, perform("late", "Job", ["late"], { keyed: true }));
964
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
965
- expect(fixture.attempts[0].runner).toBe(owner);
966
- yield* renewal.reached;
967
- yield* on(other, cancel("late", ["late"]));
968
- yield* advance(other, "4 seconds");
969
- yield* eventually(on(other, stateOf("late")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
970
- yield* Deferred.succeed(gate, undefined);
971
- yield* eventually(query(other, deadLetters).pipe(Effect.map((letters) => letters.length === 1)), "10 seconds", "the late success's record");
972
- yield* renewal.release;
973
- expect(yield* query(other, deadLetters)).toMatchObject([
974
- { ambiguous: true, cause: "Succeeded after it was cancelled" },
975
- ]);
976
- const state = yield* on(other, stateOf("late"));
977
- expect(state.cancelled).toMatchObject([{ outcome: "Unknown", ambiguous: true }]);
978
- expect(state.done ?? []).toEqual([]);
979
- })),
980
- },
981
- {
982
- name: "records a late success onSuccess cannot accept after its cancellation settled",
983
- requiresFreshDatabase: true,
984
- requiresIndependentConnections: true,
985
- timeoutMs: 90_000,
986
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, { relay: NO_POLL, executors: SHORT_LEASE }, Effect.gen(function* () {
987
- const gate = yield* Deferred.make();
988
- fixture.provider = (attempt) => Deferred.await(gate).pipe(Effect.as(attempt.label));
989
- const { owner, other } = yield* ownerAndOther(yield* refOf("late-picky"));
990
- const renewal = yield* on(owner, ActorTest.use((test) => test.pauseNext("beforeRenew")));
991
- yield* on(owner, perform("late-picky", "Picky", ["rejected"], { keyed: true }));
992
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
993
- expect(fixture.attempts[0].runner).toBe(owner);
994
- yield* renewal.reached;
995
- yield* on(other, cancel("late-picky", ["rejected"]));
996
- yield* advance(other, "4 seconds");
997
- yield* eventually(on(other, stateOf("late-picky")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)));
998
- yield* Deferred.succeed(gate, undefined);
999
- yield* eventually(query(other, deadLetters).pipe(Effect.map((letters) => letters.length === 1)), "10 seconds", "the late success's record");
1000
- yield* renewal.release;
1001
- expect(yield* query(other, deadLetters)).toMatchObject([
1002
- { ambiguous: true, cause: "Succeeded after it was cancelled" },
1003
- ]);
1004
- const state = yield* on(other, stateOf("late-picky"));
1005
- expect(state.cancelled).toMatchObject([{ outcome: "Unknown", ambiguous: true }]);
1006
- expect(state.done ?? []).toEqual([]);
1007
- })),
1008
- },
1009
- {
1010
- name: "gives each effect exactly one fate when cancellation races its claim",
1011
- requiresFreshDatabase: true,
1012
- requiresIndependentConnections: true,
1013
- timeoutMs: 120_000,
1014
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, {}, Effect.gen(function* () {
1015
- fixture.provider = (attempt) => Effect.sleep("5 millis").pipe(Effect.as(attempt.label));
1016
- const ids = Array.from({ length: 40 }, (_, index) => `race-${index}`);
1017
- yield* Effect.forEach(ids, (id, index) => on(index % 3, perform(id, "Job", [id], { keyed: true }).pipe(Effect.andThen(cancel(id, [id])))), { concurrency: 8, discard: true });
1018
- yield* eventually(query(0, (sql) => sql `SELECT count(*)::int AS n FROM actor_outbox
1019
- WHERE actor_type = 'Controlled'`.pipe(Effect.map((rows) => rows[0].n === 0))), "30 seconds", "the outbox to settle");
1020
- for (const id of ids) {
1021
- const state = yield* on(0, stateOf(id));
1022
- const ran = fixture.attempts.filter(({ actor }) => actor === id);
1023
- const fates = (state.done ?? []).length + (state.cancelled ?? []).length;
1024
- expect(ran.length <= 1).toBe(true);
1025
- expect(fates).toBe(ran.length === 0 ? 0 : 1);
1026
- expect((state.cancelled ?? []).every(({ outcome }) => outcome !== "Failed")).toBe(true);
1027
- }
1028
- })),
1029
- },
1030
- {
1031
- name: "settles a cancelled effect whose runner was killed as ambiguous without running it",
1032
- requiresFreshDatabase: true,
1033
- requiresIndependentConnections: true,
1034
- timeoutMs: 120_000,
1035
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, { executors: { lease: "9 seconds" } }, Effect.gen(function* () {
1036
- fixture.provider = () => Effect.never;
1037
- const { owner, other } = yield* ownerAndOther(yield* refOf("orphan"));
1038
- yield* on(other, perform("orphan", "Job", ["orphan"], { keyed: true }));
1039
- yield* eventually(Effect.sync(() => fixture.attempts.length === 1));
1040
- const holder = fixture.attempts[0].runner;
1041
- const cluster = yield* ActorCluster;
1042
- yield* cluster.kill(holder);
1043
- yield* cluster.ready;
1044
- const survivor = [owner, other, (owner + 2) % 3].find((runner) => runner !== holder);
1045
- yield* on(survivor, cancel("orphan", ["orphan"]));
1046
- yield* eventually(on(survivor, stateOf("orphan")).pipe(Effect.map((state) => (state.cancelled ?? []).length === 1)), "60 seconds", "the orphan's cancellation report");
1047
- expect(fixture.attempts.length).toBe(1);
1048
- expect((yield* on(survivor, stateOf("orphan"))).cancelled).toMatchObject([
1049
- { outcome: "Unknown", ambiguous: true },
1050
- ]);
1051
- yield* eventually(query(survivor, effectRows).pipe(Effect.map((rows) => rows.length === 0)), "20 seconds", "the delivered route's row to be deleted");
1052
- })),
1053
- },
1054
- ];
1055
- /** Effect-control actors and the executors of runner 0. */
1056
- export const effectControlSuite = {
1057
- fixture: effectControlFixture,
1058
- layer: (fixture) => Layer.merge(effectControlLayer, effectControlEffects(fixture)),
1059
- };