@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.
- package/CHANGELOG.md +46 -3
- package/README.md +2 -2
- package/dist/actor/definition.d.ts +13 -9
- package/dist/actor/descriptor.d.ts +2 -2
- package/dist/actor/descriptor.js +4 -4
- package/dist/client/calls.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors/actor.d.ts +12 -42
- package/dist/errors/actor.js +13 -42
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/members/payload.d.ts +1 -1
- package/dist/protocol/inspection.d.ts +221 -1
- package/dist/protocol/inspection.js +151 -2
- package/dist/protocol/wire.d.ts +6 -49
- package/dist/protocol/wire.js +1 -10
- package/dist/runtime/actors.d.ts +3 -0
- package/dist/runtime/adoption/backfill.js +1 -1
- package/dist/runtime/adoption/enforce.js +2 -2
- package/dist/runtime/adoption/observe.js +1 -1
- package/dist/runtime/adoption/plan.d.ts +1 -1
- package/dist/runtime/adoption/startup.js +1 -1
- package/dist/runtime/adoption/status.d.ts +1 -1
- package/dist/runtime/app.d.ts +65 -0
- package/dist/runtime/app.js +38 -0
- package/dist/runtime/connections/holder.d.ts +11 -1
- package/dist/runtime/connections/holder.js +8 -4
- package/dist/runtime/connections/streams.d.ts +4 -4
- package/dist/runtime/content/store.js +15 -11
- package/dist/runtime/database/bounded.d.ts +4 -18
- package/dist/runtime/database/bounded.js +36 -22
- package/dist/runtime/database/coordination.d.ts +12 -3
- package/dist/runtime/database/coordination.js +29 -36
- package/dist/runtime/database/migrations.d.ts +61 -29
- package/dist/runtime/database/migrations.js +375 -139
- package/dist/runtime/database/replica.d.ts +4 -1
- package/dist/runtime/database/replica.js +4 -1
- package/dist/runtime/database/schema.d.ts +13 -0
- package/dist/runtime/database/schema.js +13 -0
- package/dist/runtime/database/shards.d.ts +31 -13
- package/dist/runtime/database/shards.js +38 -26
- package/dist/runtime/entity/register.d.ts +8 -1
- package/dist/runtime/entity/register.js +58 -7
- package/dist/runtime/fleet/checks.js +4 -4
- package/dist/runtime/fleet/maintainer.d.ts +1 -1
- package/dist/runtime/fleet/maintainer.js +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/inspector/layer.d.ts +20 -0
- package/dist/runtime/inspector/layer.js +224 -5
- package/dist/runtime/inspector/queries.d.ts +54 -8
- package/dist/runtime/inspector/queries.js +84 -15
- package/dist/runtime/layer.d.ts +48 -39
- package/dist/runtime/layer.js +124 -56
- package/dist/runtime/operators/audit.d.ts +3 -0
- package/dist/runtime/operators/audit.js +6 -2
- package/dist/runtime/operators/repair.js +30 -10
- package/dist/runtime/operators/seed.js +4 -2
- package/dist/runtime/payloads/versions.d.ts +4 -4
- package/dist/runtime/payloads/versions.js +24 -15
- package/dist/runtime/queries.js +4 -2
- package/dist/runtime/registration.d.ts +5 -2
- package/dist/runtime/registration.js +3 -3
- package/dist/runtime/runner.d.ts +12 -0
- package/dist/runtime/runner.js +12 -0
- package/dist/runtime/storage/codec.d.ts +19 -7
- package/dist/runtime/storage/codec.js +23 -7
- package/dist/runtime/storage/placements.d.ts +6 -1
- package/dist/runtime/storage/placements.js +98 -6
- package/dist/runtime/storage/retention.d.ts +6 -0
- package/dist/runtime/storage/retention.js +25 -13
- package/dist/runtime/subscriptions/relay.js +47 -44
- package/dist/runtime/subscriptions/storage.d.ts +3 -2
- package/dist/runtime/subscriptions/storage.js +9 -6
- package/dist/runtime/telemetry/live.d.ts +255 -0
- package/dist/runtime/telemetry/live.js +540 -0
- package/dist/runtime/telemetry/metrics.d.ts +2 -0
- package/dist/runtime/telemetry/metrics.js +4 -1
- package/dist/runtime/telemetry/sampler.js +33 -13
- package/dist/runtime/telemetry/spans.d.ts +1 -1
- package/dist/runtime/telemetry/spans.js +1 -1
- package/dist/runtime/topology/locks.d.ts +6 -3
- package/dist/runtime/topology/locks.js +27 -4
- package/dist/runtime/turn/execute.d.ts +2 -0
- package/dist/runtime/turn/execute.js +5 -2
- package/dist/runtime/turn/pipeline.d.ts +5 -12
- package/dist/runtime/turn/pipeline.js +11 -11
- package/dist/runtime/turn/relay.d.ts +13 -3
- package/dist/runtime/turn/relay.js +27 -14
- package/dist/runtime/turn/rows.js +4 -4
- package/dist/runtime/workflows/compatibility.d.ts +1 -1
- package/dist/runtime/workflows/compatibility.js +85 -31
- package/dist/runtime/workflows/engine.js +20 -17
- package/dist/serve/layer.js +3 -2
- package/dist/tables/owned.d.ts +1 -1
- package/dist/tables/owned.js +1 -1
- package/dist/testing/actor-test.d.ts +2 -24
- package/dist/testing/actor-test.js +1 -27
- package/dist/testing/index.d.ts +0 -12
- package/dist/testing/index.js +0 -6
- package/package.json +1 -1
- package/dist/runtime/database/neki/migrations.d.ts +0 -31
- package/dist/runtime/database/neki/migrations.js +0 -276
- package/dist/runtime/database/neki/session.d.ts +0 -23
- package/dist/runtime/database/neki/session.js +0 -31
- package/dist/testing/cluster.d.ts +0 -93
- package/dist/testing/cluster.js +0 -296
- package/dist/testing/conformance/access.d.ts +0 -5
- package/dist/testing/conformance/access.js +0 -531
- package/dist/testing/conformance/admission.d.ts +0 -11
- package/dist/testing/conformance/admission.js +0 -182
- package/dist/testing/conformance/adoption.d.ts +0 -2
- package/dist/testing/conformance/adoption.js +0 -1019
- package/dist/testing/conformance/assertions.d.ts +0 -202
- package/dist/testing/conformance/assertions.js +0 -849
- package/dist/testing/conformance/batches.d.ts +0 -41
- package/dist/testing/conformance/batches.js +0 -388
- package/dist/testing/conformance/blobs.d.ts +0 -14
- package/dist/testing/conformance/blobs.js +0 -405
- package/dist/testing/conformance/capacity.d.ts +0 -8
- package/dist/testing/conformance/capacity.js +0 -433
- package/dist/testing/conformance/client.d.ts +0 -5
- package/dist/testing/conformance/client.js +0 -719
- package/dist/testing/conformance/cold-serve.d.ts +0 -16
- package/dist/testing/conformance/cold-serve.js +0 -254
- package/dist/testing/conformance/connections/actors.d.ts +0 -1143
- package/dist/testing/conformance/connections/actors.js +0 -180
- package/dist/testing/conformance/connections/harness.d.ts +0 -1754
- package/dist/testing/conformance/connections/harness.js +0 -142
- package/dist/testing/conformance/connections/holders.d.ts +0 -4
- package/dist/testing/conformance/connections/holders.js +0 -165
- package/dist/testing/conformance/connections/lifecycle.d.ts +0 -4
- package/dist/testing/conformance/connections/lifecycle.js +0 -113
- package/dist/testing/conformance/connections/limits.d.ts +0 -4
- package/dist/testing/conformance/connections/limits.js +0 -127
- package/dist/testing/conformance/connections/reauthorization.d.ts +0 -4
- package/dist/testing/conformance/connections/reauthorization.js +0 -170
- package/dist/testing/conformance/connections/resync.d.ts +0 -4
- package/dist/testing/conformance/connections/resync.js +0 -360
- package/dist/testing/conformance/connections/takeover.d.ts +0 -4
- package/dist/testing/conformance/connections/takeover.js +0 -287
- package/dist/testing/conformance/connections.d.ts +0 -4
- package/dist/testing/conformance/connections.js +0 -15
- package/dist/testing/conformance/content-blobs.d.ts +0 -16
- package/dist/testing/conformance/content-blobs.js +0 -534
- package/dist/testing/conformance/counter.d.ts +0 -16
- package/dist/testing/conformance/counter.js +0 -813
- package/dist/testing/conformance/cron.d.ts +0 -5
- package/dist/testing/conformance/cron.js +0 -665
- package/dist/testing/conformance/drain.d.ts +0 -26
- package/dist/testing/conformance/drain.js +0 -540
- package/dist/testing/conformance/effect-control.d.ts +0 -38
- package/dist/testing/conformance/effect-control.js +0 -1059
- package/dist/testing/conformance/effects.d.ts +0 -23
- package/dist/testing/conformance/effects.js +0 -432
- package/dist/testing/conformance/events.d.ts +0 -28
- package/dist/testing/conformance/events.js +0 -335
- package/dist/testing/conformance/export.d.ts +0 -2
- package/dist/testing/conformance/export.js +0 -368
- package/dist/testing/conformance/fleet.d.ts +0 -444
- package/dist/testing/conformance/fleet.js +0 -1009
- package/dist/testing/conformance/groups.d.ts +0 -7
- package/dist/testing/conformance/groups.js +0 -353
- package/dist/testing/conformance/heap.d.ts +0 -3
- package/dist/testing/conformance/heap.js +0 -149
- package/dist/testing/conformance/http.d.ts +0 -2809
- package/dist/testing/conformance/http.js +0 -1234
- package/dist/testing/conformance/inspection-views.d.ts +0 -8
- package/dist/testing/conformance/inspection-views.js +0 -272
- package/dist/testing/conformance/inspector.d.ts +0 -7
- package/dist/testing/conformance/inspector.js +0 -650
- package/dist/testing/conformance/mint.d.ts +0 -58
- package/dist/testing/conformance/mint.js +0 -692
- package/dist/testing/conformance/multi-runner.d.ts +0 -8
- package/dist/testing/conformance/multi-runner.js +0 -403
- package/dist/testing/conformance/neki/plan.d.ts +0 -19
- package/dist/testing/conformance/neki/plan.js +0 -30
- package/dist/testing/conformance/observability.d.ts +0 -3
- package/dist/testing/conformance/observability.js +0 -343
- package/dist/testing/conformance/offline.d.ts +0 -4
- package/dist/testing/conformance/offline.js +0 -474
- package/dist/testing/conformance/operator-harness.d.ts +0 -57
- package/dist/testing/conformance/operator-harness.js +0 -78
- package/dist/testing/conformance/operator.d.ts +0 -3
- package/dist/testing/conformance/operator.js +0 -379
- package/dist/testing/conformance/outbox-cross-shard.d.ts +0 -14
- package/dist/testing/conformance/outbox-cross-shard.js +0 -160
- package/dist/testing/conformance/outbox.d.ts +0 -36
- package/dist/testing/conformance/outbox.js +0 -437
- package/dist/testing/conformance/payload-migrations.d.ts +0 -3
- package/dist/testing/conformance/payload-migrations.js +0 -612
- package/dist/testing/conformance/pipeline.d.ts +0 -3
- package/dist/testing/conformance/pipeline.js +0 -1040
- package/dist/testing/conformance/placement.d.ts +0 -22
- package/dist/testing/conformance/placement.js +0 -539
- package/dist/testing/conformance/platform.d.ts +0 -40
- package/dist/testing/conformance/platform.js +0 -101
- package/dist/testing/conformance/progress.d.ts +0 -39
- package/dist/testing/conformance/progress.js +0 -666
- package/dist/testing/conformance/properties.d.ts +0 -6
- package/dist/testing/conformance/properties.js +0 -295
- package/dist/testing/conformance/protocols.d.ts +0 -4
- package/dist/testing/conformance/protocols.js +0 -618
- package/dist/testing/conformance/provider/advertise.d.ts +0 -21
- package/dist/testing/conformance/provider/advertise.js +0 -44
- package/dist/testing/conformance/read-your-writes.d.ts +0 -20
- package/dist/testing/conformance/read-your-writes.js +0 -192
- package/dist/testing/conformance/reducers.d.ts +0 -8
- package/dist/testing/conformance/reducers.js +0 -258
- package/dist/testing/conformance/relay.d.ts +0 -42
- package/dist/testing/conformance/relay.js +0 -1115
- package/dist/testing/conformance/restore.d.ts +0 -43
- package/dist/testing/conformance/restore.js +0 -341
- package/dist/testing/conformance/retention.d.ts +0 -14
- package/dist/testing/conformance/retention.js +0 -463
- package/dist/testing/conformance/rls.d.ts +0 -24
- package/dist/testing/conformance/rls.js +0 -576
- package/dist/testing/conformance/simulation.d.ts +0 -31
- package/dist/testing/conformance/simulation.js +0 -167
- package/dist/testing/conformance/single-shard.d.ts +0 -2
- package/dist/testing/conformance/single-shard.js +0 -125
- package/dist/testing/conformance/singleton.d.ts +0 -3
- package/dist/testing/conformance/singleton.js +0 -293
- package/dist/testing/conformance/statements.d.ts +0 -60
- package/dist/testing/conformance/statements.js +0 -108
- package/dist/testing/conformance/streams.d.ts +0 -7
- package/dist/testing/conformance/streams.js +0 -280
- package/dist/testing/conformance/subscriptions/access.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/access.js +0 -98
- package/dist/testing/conformance/subscriptions/actors.d.ts +0 -4910
- package/dist/testing/conformance/subscriptions/actors.js +0 -372
- package/dist/testing/conformance/subscriptions/cluster.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/cluster.js +0 -89
- package/dist/testing/conformance/subscriptions/delivery.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/delivery.js +0 -253
- package/dist/testing/conformance/subscriptions/deploys.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/deploys.js +0 -162
- package/dist/testing/conformance/subscriptions/epochs.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/epochs.js +0 -169
- package/dist/testing/conformance/subscriptions/failures.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/failures.js +0 -108
- package/dist/testing/conformance/subscriptions/feeds.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/feeds.js +0 -218
- package/dist/testing/conformance/subscriptions/harness.d.ts +0 -124
- package/dist/testing/conformance/subscriptions/harness.js +0 -143
- package/dist/testing/conformance/subscriptions/operator.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/operator.js +0 -128
- package/dist/testing/conformance/subscriptions/retention.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/retention.js +0 -288
- package/dist/testing/conformance/subscriptions/routing.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/routing.js +0 -179
- package/dist/testing/conformance/subscriptions.d.ts +0 -4
- package/dist/testing/conformance/subscriptions.js +0 -19
- package/dist/testing/conformance/tables.d.ts +0 -125
- package/dist/testing/conformance/tables.js +0 -664
- package/dist/testing/conformance/transports/actors.d.ts +0 -1907
- package/dist/testing/conformance/transports/actors.js +0 -163
- package/dist/testing/conformance/transports/client.d.ts +0 -3
- package/dist/testing/conformance/transports/client.js +0 -237
- package/dist/testing/conformance/transports/feeds.d.ts +0 -3
- package/dist/testing/conformance/transports/feeds.js +0 -232
- package/dist/testing/conformance/transports/harness.d.ts +0 -93
- package/dist/testing/conformance/transports/harness.js +0 -84
- package/dist/testing/conformance/transports/streams.d.ts +0 -3
- package/dist/testing/conformance/transports/streams.js +0 -96
- package/dist/testing/conformance/transports/websocket.d.ts +0 -3
- package/dist/testing/conformance/transports/websocket.js +0 -384
- package/dist/testing/conformance/transports/wire.d.ts +0 -141
- package/dist/testing/conformance/transports/wire.js +0 -204
- package/dist/testing/conformance/transports.d.ts +0 -3
- package/dist/testing/conformance/transports.js +0 -11
- package/dist/testing/conformance/watch.d.ts +0 -59
- package/dist/testing/conformance/watch.js +0 -879
- package/dist/testing/conformance/workflow-engine.d.ts +0 -97
- package/dist/testing/conformance/workflow-engine.js +0 -253
- package/dist/testing/conformance/workflow-versions.d.ts +0 -3
- package/dist/testing/conformance/workflow-versions.js +0 -545
- package/dist/testing/conformance/workflows/access.d.ts +0 -4
- package/dist/testing/conformance/workflows/access.js +0 -147
- package/dist/testing/conformance/workflows/actors.d.ts +0 -2317
- package/dist/testing/conformance/workflows/actors.js +0 -305
- package/dist/testing/conformance/workflows/basics.d.ts +0 -4
- package/dist/testing/conformance/workflows/basics.js +0 -144
- package/dist/testing/conformance/workflows/engine.d.ts +0 -4
- package/dist/testing/conformance/workflows/engine.js +0 -80
- package/dist/testing/conformance/workflows/guards.d.ts +0 -4
- package/dist/testing/conformance/workflows/guards.js +0 -172
- package/dist/testing/conformance/workflows/harness.d.ts +0 -14
- package/dist/testing/conformance/workflows/harness.js +0 -44
- package/dist/testing/conformance/workflows/recovery.d.ts +0 -4
- package/dist/testing/conformance/workflows/recovery.js +0 -181
- package/dist/testing/conformance/workflows/starts.d.ts +0 -4
- package/dist/testing/conformance/workflows/starts.js +0 -57
- package/dist/testing/conformance/workflows/suspension.d.ts +0 -4
- package/dist/testing/conformance/workflows/suspension.js +0 -106
- package/dist/testing/conformance/workflows/waits.d.ts +0 -4
- package/dist/testing/conformance/workflows/waits.js +0 -284
- package/dist/testing/conformance/workflows.d.ts +0 -4
- package/dist/testing/conformance/workflows.js +0 -19
- package/dist/testing/conformance.d.ts +0 -330
- package/dist/testing/conformance.js +0 -364
- package/dist/testing/foundation.d.ts +0 -41
- package/dist/testing/foundation.js +0 -517
- package/dist/testing/simulate-cluster.d.ts +0 -112
- package/dist/testing/simulate-cluster.js +0 -257
- package/dist/testing/simulate.d.ts +0 -90
- 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) {
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -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>;
|
package/dist/runtime/runner.js
CHANGED
|
@@ -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;
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* parent id
|
|
25
|
-
*
|
|
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
|
|
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
|
|
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;
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* parent id
|
|
15
|
-
*
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
recorded.
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
413
|
+
VALUES (${key}, ${source.tenant}, ${source.actor}, ${source.id}) ON CONFLICT DO NOTHING`;
|
|
413
414
|
const [generation] = yield* sql `
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
}));
|