@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
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
2
|
-
import { HttpRouter } from "effect/http";
|
|
1
|
+
import { Clock, DateTime, Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
|
-
import { ActorError, Unauthorized } from "../../errors/actor.js";
|
|
4
|
+
import { ActorError, RunnerAtCapacity, Unauthorized } from "../../errors/actor.js";
|
|
5
5
|
import { Anonymous, User } from "../../identity/caller.js";
|
|
6
|
+
import { InternalActors } from "../actors.js";
|
|
7
|
+
import { StreamMessage } from "../telemetry/live.js";
|
|
8
|
+
import { message } from "../../serve/sessions/sse.js";
|
|
6
9
|
import { CREDENTIAL_BYTES, oversizedCredential, readsCookies, withinLimits, } from "../../serve/auth.js";
|
|
7
10
|
import { invalidInput, undecodable } from "../../serve/wire.js";
|
|
8
11
|
import { foundOrNotFound, operatorResponse, refuseCrossOrigin } from "./http.js";
|
|
@@ -61,7 +64,26 @@ const WorkflowsParams = Schema.Struct({
|
|
|
61
64
|
afterStartedAtMs: Schema.optional(Millis),
|
|
62
65
|
afterExecutionId: Schema.optional(Schema.String),
|
|
63
66
|
}).check(whole("afterStartedAtMs", "afterExecutionId"));
|
|
67
|
+
const Window = Schema.optional(Schema.Literals(["1h", "24h", "7d"]));
|
|
68
|
+
const LiveTypeParams = Schema.Struct({ type: Schema.optional(Schema.String), window: Window });
|
|
69
|
+
/** The most actors one live actors read names. */
|
|
70
|
+
const MAX_LIVE_ACTORS = 100;
|
|
71
|
+
const LiveActorsParams = Schema.Struct({
|
|
72
|
+
type: Schema.String,
|
|
73
|
+
ids: Schema.fromJsonString(Schema.Array(Schema.String).check(Schema.isMaxLength(MAX_LIVE_ACTORS))),
|
|
74
|
+
});
|
|
75
|
+
const StreamParams = Schema.Struct({
|
|
76
|
+
type: Schema.optional(Schema.String),
|
|
77
|
+
outcome: Schema.optional(Schema.Literals(["Success", "Failure"])),
|
|
78
|
+
after: Schema.optional(Schema.String),
|
|
79
|
+
});
|
|
64
80
|
const isPrincipal = Schema.is(Schema.Union([User, Anonymous]));
|
|
81
|
+
/**
|
|
82
|
+
* How often a command stream with no commands sends a comment. A proxy in
|
|
83
|
+
* front of the inspector, such as the edge or the control plane's client,
|
|
84
|
+
* closes a connection idle for 10 seconds, so this stays well under it.
|
|
85
|
+
*/
|
|
86
|
+
const STREAM_KEEPALIVE_MS = 4_000;
|
|
65
87
|
/**
|
|
66
88
|
* Serves read-only JSON over the `durable` inspection views as routes on the
|
|
67
89
|
* application's `HttpRouter`. A request from another browser origin is
|
|
@@ -87,6 +109,22 @@ const isPrincipal = Schema.is(Schema.Union([User, Anonymous]));
|
|
|
87
109
|
* - `GET /outbox`, `/jobs`, `/dead-letters?afterDeadAtMs&afterJobId`,
|
|
88
110
|
* `/workflows?status=open|all|running|suspended|finished|completed|failed&afterStartedAtMs&afterExecutionId`:
|
|
89
111
|
* tenant-wide lists; the paged ones answer the `next` cursor to pass back.
|
|
112
|
+
*
|
|
113
|
+
* Inside a runtime it also serves the schedules and this runner's own memory,
|
|
114
|
+
* each live answer naming the runner, its region, when it began recording and
|
|
115
|
+
* how many other runners the cluster lists:
|
|
116
|
+
*
|
|
117
|
+
* - `GET /schedules`: each declared cron entry with its pending ticks and last committed tick.
|
|
118
|
+
* - `GET /live/overview`: recent rates, resident activations and the deepest mailbox, by type.
|
|
119
|
+
* - `GET /live/activity?type&window`, `/live/latency?type&window`: one type's, or every type's,
|
|
120
|
+
* committed turns over `1h`, `24h` (default) or `7d`.
|
|
121
|
+
* - `GET /live/actors?type&ids`: whether each named actor is resident, its
|
|
122
|
+
* mailbox, sockets and feeds; `ids` is a JSON array of at most 100.
|
|
123
|
+
* - `GET /live/connections`: open sockets and SSE responses, by type.
|
|
124
|
+
* - `GET /commands/stream?type&outcome&after`: SSE of each command this runner
|
|
125
|
+
* commits for the tenant, `id` its position, resuming after `after` while the
|
|
126
|
+
* runner still holds it and sending `gap` when it does not, until the
|
|
127
|
+
* credential expires.
|
|
90
128
|
*/
|
|
91
129
|
const serve = (options) => HttpRouter.use(Effect.fnUntraced(function* (router) {
|
|
92
130
|
const basePath = (options.basePath ?? "/inspector").replace(/\/+$/, "");
|
|
@@ -109,11 +147,11 @@ const serve = (options) => HttpRouter.use(Effect.fnUntraced(function* (router) {
|
|
|
109
147
|
return yield* ActorError.make({
|
|
110
148
|
reason: Unauthorized.make({ code: "invalid_credentials" }),
|
|
111
149
|
});
|
|
112
|
-
return authenticated.tenant;
|
|
150
|
+
return { tenant: authenticated.tenant, expiresAt: authenticated.expiresAt };
|
|
113
151
|
});
|
|
114
152
|
const route = (path, params, read) => router.add("GET", `${basePath}${path}`, (request) => Effect.gen(function* () {
|
|
115
153
|
yield* refuseCrossOrigin(request);
|
|
116
|
-
const tenant = yield* authenticate(request);
|
|
154
|
+
const { tenant } = yield* authenticate(request);
|
|
117
155
|
const decoded = yield* HttpRouter.schemaParams(params).pipe(Effect.mapError(undecodable));
|
|
118
156
|
return foundOrNotFound(yield* Queries.readOnly(tenant)(read(tenant, decoded)).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.orDie));
|
|
119
157
|
}).pipe(operatorResponse("Inspector request failed")));
|
|
@@ -165,6 +203,184 @@ const serve = (options) => HttpRouter.use(Effect.fnUntraced(function* (router) {
|
|
|
165
203
|
commandId: afterCommandId,
|
|
166
204
|
},
|
|
167
205
|
}));
|
|
206
|
+
/**
|
|
207
|
+
* The live reads, the schedules and the command stream, which need the
|
|
208
|
+
* runtime itself; mounted only where the inspector runs inside one.
|
|
209
|
+
*/
|
|
210
|
+
const mountLive = Effect.fnUntraced(function* (actors) {
|
|
211
|
+
const recorder = yield* actors.live.enable;
|
|
212
|
+
const scope = Effect.gen(function* () {
|
|
213
|
+
const peers = yield* actors.live.peers;
|
|
214
|
+
return {
|
|
215
|
+
runner: options.runner ?? null,
|
|
216
|
+
region: options.region ?? null,
|
|
217
|
+
startedAtMs: recorder.startedAtMs,
|
|
218
|
+
peers: peers ?? null,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
/** A read of this runner's memory alone, answered as JSON with the scope it covers. */
|
|
222
|
+
const live = (path, params, read) => router.add("GET", `${basePath}${path}`, (request) => Effect.gen(function* () {
|
|
223
|
+
yield* refuseCrossOrigin(request);
|
|
224
|
+
const { tenant } = yield* authenticate(request);
|
|
225
|
+
const decoded = yield* HttpRouter.schemaParams(params).pipe(Effect.mapError(undecodable));
|
|
226
|
+
return HttpServerResponse.jsonUnsafe(yield* read(tenant, decoded, yield* Clock.currentTimeMillis, yield* scope));
|
|
227
|
+
}).pipe(operatorResponse("Inspector request failed")));
|
|
228
|
+
yield* live("/live/overview", Schema.Struct({}), (tenant, _params, nowMs, covered) => Effect.sync(() => {
|
|
229
|
+
const resident = actors.live.resident(tenant);
|
|
230
|
+
const types = new Set([
|
|
231
|
+
...recorder.actorTypes(tenant),
|
|
232
|
+
...resident.map((r) => r.actorType),
|
|
233
|
+
]);
|
|
234
|
+
const deepest = (activations) => activations.reduce((found, activation) => activation.mailbox > 0 &&
|
|
235
|
+
(found === undefined || activation.mailbox > found.mailbox)
|
|
236
|
+
? activation
|
|
237
|
+
: found, undefined);
|
|
238
|
+
const rates = (actorType) => {
|
|
239
|
+
const found = recorder.rates(tenant, actorType, nowMs);
|
|
240
|
+
return {
|
|
241
|
+
perSecond: found?.perSecond ?? null,
|
|
242
|
+
p50Ms: found?.p50Ms ?? null,
|
|
243
|
+
p99Ms: found?.p99Ms ?? null,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const max = deepest(resident);
|
|
247
|
+
return {
|
|
248
|
+
scope: covered,
|
|
249
|
+
total: {
|
|
250
|
+
...rates(undefined),
|
|
251
|
+
awake: resident.length,
|
|
252
|
+
maxMailbox: max === undefined
|
|
253
|
+
? null
|
|
254
|
+
: { depth: max.mailbox, actorType: max.actorType, actorId: max.actorId },
|
|
255
|
+
},
|
|
256
|
+
actorTypes: [...types].sort().map((actorType) => {
|
|
257
|
+
const ofType = resident.filter((activation) => activation.actorType === actorType);
|
|
258
|
+
const typeMax = deepest(ofType);
|
|
259
|
+
return {
|
|
260
|
+
actorType,
|
|
261
|
+
...rates(actorType),
|
|
262
|
+
awake: ofType.length,
|
|
263
|
+
maxMailbox: typeMax === undefined
|
|
264
|
+
? null
|
|
265
|
+
: { depth: typeMax.mailbox, actorId: typeMax.actorId },
|
|
266
|
+
};
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
}));
|
|
270
|
+
yield* live("/live/activity", LiveTypeParams, (tenant, { type, window }, nowMs, covered) => Effect.sync(() => {
|
|
271
|
+
const found = recorder.activity(tenant, type, window ?? "24h", nowMs);
|
|
272
|
+
return {
|
|
273
|
+
scope: covered,
|
|
274
|
+
activity: found === undefined
|
|
275
|
+
? null
|
|
276
|
+
: { sinceMs: found.since, points: found.points, commands: found.commands },
|
|
277
|
+
};
|
|
278
|
+
}));
|
|
279
|
+
yield* live("/live/latency", LiveTypeParams, (tenant, { type, window }, nowMs, covered) => Effect.sync(() => {
|
|
280
|
+
const found = recorder.latency(tenant, type, window ?? "24h", nowMs);
|
|
281
|
+
return {
|
|
282
|
+
scope: covered,
|
|
283
|
+
latency: found === undefined
|
|
284
|
+
? null
|
|
285
|
+
: {
|
|
286
|
+
sinceMs: found.since,
|
|
287
|
+
count: found.count,
|
|
288
|
+
buckets: found.buckets,
|
|
289
|
+
p50Ms: found.p50Ms,
|
|
290
|
+
p95Ms: found.p95Ms,
|
|
291
|
+
p99Ms: found.p99Ms,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}));
|
|
295
|
+
yield* live("/live/actors", LiveActorsParams, (tenant, { type, ids }, _nowMs, covered) => Effect.sync(() => {
|
|
296
|
+
const resident = actors.live
|
|
297
|
+
.resident(tenant)
|
|
298
|
+
.filter((activation) => activation.actorType === type);
|
|
299
|
+
const open = actors.live
|
|
300
|
+
.connections(tenant)
|
|
301
|
+
.filter((connection) => connection.actorType === type);
|
|
302
|
+
return {
|
|
303
|
+
scope: covered,
|
|
304
|
+
actors: ids.map((actorId) => {
|
|
305
|
+
const activation = resident.find((found) => found.actorId === actorId);
|
|
306
|
+
const mine = open.filter((connection) => connection.actorId === actorId);
|
|
307
|
+
const feeds = new Map();
|
|
308
|
+
for (const connection of mine)
|
|
309
|
+
if (connection.kind === "feed")
|
|
310
|
+
for (const event of connection.events)
|
|
311
|
+
feeds.set(event, (feeds.get(event) ?? 0) + 1);
|
|
312
|
+
return {
|
|
313
|
+
actorId,
|
|
314
|
+
awake: activation !== undefined,
|
|
315
|
+
mailbox: activation?.mailbox ?? null,
|
|
316
|
+
sockets: mine.filter((connection) => connection.kind === "socket").length,
|
|
317
|
+
feeds: [...feeds]
|
|
318
|
+
.sort(([left], [right]) => (left < right ? -1 : 1))
|
|
319
|
+
.map(([event, subscribers]) => ({ event, subscribers })),
|
|
320
|
+
};
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
}));
|
|
324
|
+
yield* live("/live/connections", Schema.Struct({}), (tenant, _params, _nowMs, covered) => Effect.sync(() => {
|
|
325
|
+
const open = actors.live.connections(tenant);
|
|
326
|
+
const counts = (connections) => ({
|
|
327
|
+
sockets: connections.filter((connection) => connection.kind === "socket").length,
|
|
328
|
+
feeds: connections.filter((connection) => connection.kind === "feed").length,
|
|
329
|
+
streams: connections.filter((connection) => connection.kind === "stream").length,
|
|
330
|
+
watches: connections.filter((connection) => connection.kind === "watch").length,
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
scope: covered,
|
|
334
|
+
...counts(open),
|
|
335
|
+
byActorType: [...new Set(open.map((connection) => connection.actorType))]
|
|
336
|
+
.sort()
|
|
337
|
+
.map((actorType) => ({
|
|
338
|
+
actorType,
|
|
339
|
+
...counts(open.filter((connection) => connection.actorType === actorType)),
|
|
340
|
+
})),
|
|
341
|
+
};
|
|
342
|
+
}));
|
|
343
|
+
yield* route("/schedules", Schema.Struct({}), (tenant) => Effect.asSome(Queries.schedules({ tenant, declared: actors.live.schedules() })));
|
|
344
|
+
yield* router.add("GET", `${basePath}/commands/stream`, (request) => Effect.gen(function* () {
|
|
345
|
+
yield* refuseCrossOrigin(request);
|
|
346
|
+
const { tenant, expiresAt } = yield* authenticate(request);
|
|
347
|
+
const { type, outcome, after } = yield* HttpRouter.schemaParams(StreamParams).pipe(Effect.mapError(undecodable));
|
|
348
|
+
const nowMs = yield* Clock.currentTimeMillis;
|
|
349
|
+
if (recorder.full(tenant))
|
|
350
|
+
return yield* ActorError.make({ reason: RunnerAtCapacity.make({}) });
|
|
351
|
+
const opened = recorder.subscribe(tenant, {
|
|
352
|
+
actorType: type,
|
|
353
|
+
failed: outcome === undefined ? undefined : outcome === "Failure",
|
|
354
|
+
}, after);
|
|
355
|
+
const until = expiresAt === undefined
|
|
356
|
+
? Effect.never
|
|
357
|
+
: Effect.sleep(Math.max(0, DateTime.toEpochMillis(expiresAt) - nowMs));
|
|
358
|
+
const messages = opened.pipe(Stream.mapEffect(StreamMessage.$match({
|
|
359
|
+
gap: () => message({ event: "gap", data: null }),
|
|
360
|
+
command: ({ entry }) => message({
|
|
361
|
+
event: "command",
|
|
362
|
+
id: entry.id,
|
|
363
|
+
data: {
|
|
364
|
+
id: entry.id,
|
|
365
|
+
commandId: entry.commandId,
|
|
366
|
+
atMs: entry.atMs,
|
|
367
|
+
durationMs: entry.durationMs,
|
|
368
|
+
actorType: entry.actorType,
|
|
369
|
+
actorId: entry.actorId,
|
|
370
|
+
command: entry.command,
|
|
371
|
+
callerKey: Queries.decodeText(entry.callerKey),
|
|
372
|
+
outcomeTag: entry.failed ? "Failure" : "Success",
|
|
373
|
+
errorTag: entry.errorTag,
|
|
374
|
+
payloadPreview: entry.payloadPreview,
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
})), Stream.interruptWhen(until), Stream.concat(Stream.fromEffect(message({ event: "end", data: null }))));
|
|
378
|
+
return HttpServerResponse.stream(Stream.concat(Stream.succeed(": open\n\n"), messages).pipe(Stream.merge(Stream.tick(STREAM_KEEPALIVE_MS).pipe(Stream.drop(1), Stream.map(() => ": keepalive\n\n")), { haltStrategy: "left" }), Stream.encodeText), {
|
|
379
|
+
contentType: "text/event-stream",
|
|
380
|
+
headers: { "x-accel-buffering": "no" },
|
|
381
|
+
});
|
|
382
|
+
}).pipe(operatorResponse("Inspector request failed")));
|
|
383
|
+
});
|
|
168
384
|
yield* route("/latest-events", LatestEventsParams, (tenant, { limit, type, id, after }) => Queries.latestEvents({ tenant, limit, actorType: type, actorId: id, after }));
|
|
169
385
|
yield* route("/timeline", TimelineParams, (tenant, { limit, type, id, beforeSequence, beforeKind }) => Queries.timeline({
|
|
170
386
|
tenant,
|
|
@@ -175,6 +391,9 @@ const serve = (options) => HttpRouter.use(Effect.fnUntraced(function* (router) {
|
|
|
175
391
|
? undefined
|
|
176
392
|
: { sequence: beforeSequence, kind: beforeKind },
|
|
177
393
|
}));
|
|
394
|
+
const runtime = yield* Effect.serviceOption(InternalActors);
|
|
395
|
+
if (Option.isSome(runtime))
|
|
396
|
+
yield* mountLive(runtime.value);
|
|
178
397
|
}));
|
|
179
398
|
/** The read-only inspector: `serve` mounts its routes. */
|
|
180
399
|
export const Inspector = { serve };
|
|
@@ -52,19 +52,32 @@ interface ActorsPage extends Page {
|
|
|
52
52
|
readonly after?: ActorIdentity | undefined;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* The
|
|
55
|
+
* The receipts an actor's last command is looked for among: its greatest
|
|
56
|
+
* command ids, which the receipts' primary key reads in order and which for a
|
|
57
|
+
* minted id are the newest issued, so the read is bounded however many
|
|
58
|
+
* receipts the actor retains.
|
|
59
|
+
*/
|
|
60
|
+
export declare const LAST_COMMAND_RECEIPTS = 256;
|
|
61
|
+
/**
|
|
62
|
+
* The tenant's actors in `(actor_type, actor_id)` order, one page after `after`,
|
|
63
|
+
* each with its last command: the receipt with the newest recorded commit
|
|
64
|
+
* time among its `LAST_COMMAND_RECEIPTS` greatest command ids. The cursor
|
|
56
65
|
* comparison uses the same `C` collation as the ORDER BY, or a cursor could skip or repeat actors.
|
|
57
66
|
* A prefix that names a whole type, `type/` and more, matches that type's ids by their own prefix,
|
|
58
67
|
* so it reads one type rather than every actor of the tenant.
|
|
59
68
|
*/
|
|
60
69
|
export declare const actors: (page: ActorsPage) => Effect.Effect<{
|
|
61
|
-
actors:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
actors: {
|
|
71
|
+
actorType: string;
|
|
72
|
+
actorId: string;
|
|
73
|
+
placement: string | null;
|
|
74
|
+
generation: number;
|
|
75
|
+
created: boolean;
|
|
76
|
+
lastEventSequence: number;
|
|
77
|
+
lastCommand: {
|
|
78
|
+
command: string;
|
|
79
|
+
committedAtMs: number;
|
|
80
|
+
} | null;
|
|
68
81
|
}[];
|
|
69
82
|
next: {
|
|
70
83
|
actorType: string;
|
|
@@ -709,3 +722,36 @@ export declare const timeline: (page: TimelinePage) => Effect.Effect<Option.None
|
|
|
709
722
|
kind: "command" | "event";
|
|
710
723
|
} | null;
|
|
711
724
|
}>, import("effect/sql/SqlError").SqlError, SqlClient.SqlClient>;
|
|
725
|
+
/** The newest receipts of a type a schedule's last run is looked for among. */
|
|
726
|
+
export declare const SCHEDULE_RECEIPTS = 10000;
|
|
727
|
+
/**
|
|
728
|
+
* The tenant's state of each declared cron entry: its pending ticks and the
|
|
729
|
+
* soonest one's due time, and its newest committed tick (a receipt of the
|
|
730
|
+
* entry's command whose caller is the cron source) among the type's newest
|
|
731
|
+
* `SCHEDULE_RECEIPTS` receipts by expiry, which the expiry index reads in
|
|
732
|
+
* order, so a type with more receipts than that answers no last run rather
|
|
733
|
+
* than scan them all.
|
|
734
|
+
*/
|
|
735
|
+
export declare const schedules: ({ tenant, declared, }: {
|
|
736
|
+
readonly tenant: string;
|
|
737
|
+
readonly declared: ReadonlyArray<{
|
|
738
|
+
readonly actorType: string;
|
|
739
|
+
readonly key: string;
|
|
740
|
+
readonly command: string;
|
|
741
|
+
}>;
|
|
742
|
+
}) => Effect.Effect<{
|
|
743
|
+
schedules: {
|
|
744
|
+
actorType: string;
|
|
745
|
+
key: string;
|
|
746
|
+
expression: string;
|
|
747
|
+
command: string;
|
|
748
|
+
pending: number;
|
|
749
|
+
nextDueAtMs: number | null;
|
|
750
|
+
lastRun: {
|
|
751
|
+
commandId: string;
|
|
752
|
+
committedAtMs: number;
|
|
753
|
+
durationMs: number | null;
|
|
754
|
+
outcomeTag: string | null;
|
|
755
|
+
} | null;
|
|
756
|
+
}[];
|
|
757
|
+
}, import("effect/sql/SqlError").SqlError, SqlClient.SqlClient>;
|
|
@@ -2,6 +2,7 @@ import { Effect, Option, Predicate, Schema } from "effect";
|
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
3
|
import { inReadOnlySnapshot } from "../database/snapshot.js";
|
|
4
4
|
import { decompress } from "../storage/codec.js";
|
|
5
|
+
import { CRON_PREFIX } from "../cron/key.js";
|
|
5
6
|
const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
|
|
6
7
|
/** Decodes a JSON `text` column such as a payload, caller, or outcome. */
|
|
7
8
|
export const decodeText = (text) => {
|
|
@@ -61,37 +62,62 @@ export const overview = ({ tenant }) => Effect.gen(function* () {
|
|
|
61
62
|
};
|
|
62
63
|
});
|
|
63
64
|
/**
|
|
64
|
-
* The
|
|
65
|
+
* The receipts an actor's last command is looked for among: its greatest
|
|
66
|
+
* command ids, which the receipts' primary key reads in order and which for a
|
|
67
|
+
* minted id are the newest issued, so the read is bounded however many
|
|
68
|
+
* receipts the actor retains.
|
|
69
|
+
*/
|
|
70
|
+
export const LAST_COMMAND_RECEIPTS = 256;
|
|
71
|
+
/**
|
|
72
|
+
* The tenant's actors in `(actor_type, actor_id)` order, one page after `after`,
|
|
73
|
+
* each with its last command: the receipt with the newest recorded commit
|
|
74
|
+
* time among its `LAST_COMMAND_RECEIPTS` greatest command ids. The cursor
|
|
65
75
|
* comparison uses the same `C` collation as the ORDER BY, or a cursor could skip or repeat actors.
|
|
66
76
|
* A prefix that names a whole type, `type/` and more, matches that type's ids by their own prefix,
|
|
67
77
|
* so it reads one type rather than every actor of the tenant.
|
|
68
78
|
*/
|
|
69
79
|
export const actors = (page) => Effect.gen(function* () {
|
|
70
80
|
const sql = yield* SqlClient.SqlClient;
|
|
71
|
-
const byType = page.actorType === undefined ? sql `TRUE` : sql `actor_type = ${page.actorType}`;
|
|
81
|
+
const byType = page.actorType === undefined ? sql `TRUE` : sql `a.actor_type = ${page.actorType}`;
|
|
72
82
|
const slash = page.prefix?.indexOf("/") ?? -1;
|
|
73
83
|
const byPrefix = page.prefix === undefined
|
|
74
84
|
? sql `TRUE`
|
|
75
85
|
: slash === -1
|
|
76
|
-
? sql `starts_with(actor_type, ${page.prefix})`
|
|
77
|
-
: sql `actor_type = ${page.prefix.slice(0, slash)}
|
|
78
|
-
AND starts_with(actor_id, ${page.prefix.slice(slash + 1)})`;
|
|
86
|
+
? sql `starts_with(a.actor_type, ${page.prefix})`
|
|
87
|
+
: sql `a.actor_type = ${page.prefix.slice(0, slash)}
|
|
88
|
+
AND starts_with(a.actor_id, ${page.prefix.slice(slash + 1)})`;
|
|
79
89
|
const after = page.after === undefined
|
|
80
90
|
? sql `TRUE`
|
|
81
|
-
: sql `(actor_type COLLATE "C", actor_id COLLATE "C") > (${page.after.actorType}, ${page.after.actorId})`;
|
|
91
|
+
: sql `(a.actor_type COLLATE "C", a.actor_id COLLATE "C") > (${page.after.actorType}, ${page.after.actorId})`;
|
|
82
92
|
const rows = yield* sql `
|
|
83
|
-
SELECT actor_type AS "actorType", actor_id AS "actorId",
|
|
84
|
-
generation::float8 AS generation, created,
|
|
85
|
-
last_event_sequence::float8 AS "lastEventSequence"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
SELECT a.actor_type AS "actorType", a.actor_id AS "actorId",
|
|
94
|
+
a.generation::float8 AS generation, a.created, a.placement,
|
|
95
|
+
a.last_event_sequence::float8 AS "lastEventSequence",
|
|
96
|
+
l.command AS "lastCommand", l.committed_at_ms::float8 AS "lastCommittedAtMs"
|
|
97
|
+
FROM durable.actors a
|
|
98
|
+
LEFT JOIN LATERAL (
|
|
99
|
+
SELECT n.command, n.committed_at_ms FROM (
|
|
100
|
+
SELECT r.command, r.command_id, r.committed_at_ms FROM durable.receipts r
|
|
101
|
+
WHERE r.routing_key = a.routing_key AND r.tenant_id = a.tenant_id
|
|
102
|
+
AND r.actor_type = a.actor_type AND r.actor_id = a.actor_id
|
|
103
|
+
ORDER BY r.command_id DESC
|
|
104
|
+
LIMIT ${LAST_COMMAND_RECEIPTS}) n
|
|
105
|
+
WHERE n.committed_at_ms IS NOT NULL
|
|
106
|
+
ORDER BY n.committed_at_ms DESC, n.command_id COLLATE "C" DESC
|
|
107
|
+
LIMIT 1) l ON TRUE
|
|
108
|
+
WHERE a.tenant_id = ${page.tenant} AND ${byType} AND ${byPrefix} AND ${after}
|
|
109
|
+
ORDER BY a.actor_type COLLATE "C", a.actor_id COLLATE "C"
|
|
89
110
|
LIMIT ${page.limit + 1}`;
|
|
90
111
|
const more = rows.length > page.limit;
|
|
91
112
|
const items = more ? rows.slice(0, page.limit) : rows;
|
|
92
113
|
const last = items.at(-1);
|
|
93
114
|
return {
|
|
94
|
-
actors: items,
|
|
115
|
+
actors: items.map(({ lastCommand, lastCommittedAtMs, ...actor }) => ({
|
|
116
|
+
...actor,
|
|
117
|
+
lastCommand: lastCommand === null || lastCommittedAtMs === null
|
|
118
|
+
? null
|
|
119
|
+
: { command: lastCommand, committedAtMs: lastCommittedAtMs },
|
|
120
|
+
})),
|
|
95
121
|
next: more && last !== undefined ? { actorType: last.actorType, actorId: last.actorId } : null,
|
|
96
122
|
};
|
|
97
123
|
});
|
|
@@ -99,11 +125,11 @@ const findActor = ({ tenant, actorType, actorId }) => Effect.gen(function* () {
|
|
|
99
125
|
const sql = yield* SqlClient.SqlClient;
|
|
100
126
|
const [row] = yield* sql `
|
|
101
127
|
SELECT actor_type AS "actorType", actor_id AS "actorId", routing_key::text AS "routingKey",
|
|
102
|
-
|
|
128
|
+
generation::float8 AS generation, created, placement,
|
|
103
129
|
last_event_sequence::float8 AS "lastEventSequence"
|
|
104
130
|
FROM durable.actors
|
|
105
131
|
WHERE tenant_id = ${tenant} AND actor_type = ${actorType} AND actor_id = ${actorId}`;
|
|
106
|
-
return Option.
|
|
132
|
+
return Option.fromUndefinedOr(row);
|
|
107
133
|
});
|
|
108
134
|
const outboxOf = (row) => ({
|
|
109
135
|
...row,
|
|
@@ -179,6 +205,7 @@ export const actor = (page) => Effect.gen(function* () {
|
|
|
179
205
|
const receipts = yield* sql `
|
|
180
206
|
SELECT r.command_id AS "commandId", r.command, r.caller_key AS "callerKey",
|
|
181
207
|
r.outcome_tag AS "outcomeTag", r.outcome, r.expires_at_ms::float8 AS "expiresAtMs",
|
|
208
|
+
r.started_at_ms::float8 AS "startedAtMs", r.committed_at_ms::float8 AS "committedAtMs",
|
|
182
209
|
(SELECT string_agg(e.sequence::text, ',' ORDER BY e.sequence) FROM durable.events e
|
|
183
210
|
WHERE e.routing_key = r.routing_key AND e.tenant_id = r.tenant_id
|
|
184
211
|
AND e.actor_type = r.actor_type AND e.actor_id = r.actor_id
|
|
@@ -387,6 +414,7 @@ export const receipts = (page) => Effect.gen(function* () {
|
|
|
387
414
|
SELECT r.actor_type AS "actorType", r.actor_id AS "actorId",
|
|
388
415
|
r.command_id AS "commandId", r.command, r.caller_key AS "callerKey",
|
|
389
416
|
r.outcome_tag AS "outcomeTag", r.outcome, r.expires_at_ms::float8 AS "expiresAtMs",
|
|
417
|
+
r.started_at_ms::float8 AS "startedAtMs", r.committed_at_ms::float8 AS "committedAtMs",
|
|
390
418
|
(SELECT string_agg(e.sequence::text, ',' ORDER BY e.sequence) FROM durable.events e
|
|
391
419
|
WHERE e.routing_key = r.routing_key AND e.tenant_id = r.tenant_id
|
|
392
420
|
AND e.actor_type = r.actor_type AND e.actor_id = r.actor_id
|
|
@@ -492,3 +520,44 @@ export const timeline = (page) => Effect.gen(function* () {
|
|
|
492
520
|
: null,
|
|
493
521
|
});
|
|
494
522
|
});
|
|
523
|
+
/** The newest receipts of a type a schedule's last run is looked for among. */
|
|
524
|
+
export const SCHEDULE_RECEIPTS = 10_000;
|
|
525
|
+
/**
|
|
526
|
+
* The tenant's state of each declared cron entry: its pending ticks and the
|
|
527
|
+
* soonest one's due time, and its newest committed tick (a receipt of the
|
|
528
|
+
* entry's command whose caller is the cron source) among the type's newest
|
|
529
|
+
* `SCHEDULE_RECEIPTS` receipts by expiry, which the expiry index reads in
|
|
530
|
+
* order, so a type with more receipts than that answers no last run rather
|
|
531
|
+
* than scan them all.
|
|
532
|
+
*/
|
|
533
|
+
export const schedules = ({ tenant, declared, }) => Effect.forEach(declared, (entry) => Effect.gen(function* () {
|
|
534
|
+
const sql = yield* SqlClient.SqlClient;
|
|
535
|
+
const [timers] = yield* sql `
|
|
536
|
+
SELECT count(*)::int AS pending, min(due_at_ms)::float8 AS "dueAtMs" FROM durable.timers
|
|
537
|
+
WHERE tenant_id = ${tenant} AND actor_type = ${entry.actorType}
|
|
538
|
+
AND timer_key = ${entry.key}`;
|
|
539
|
+
const [last] = yield* sql `
|
|
540
|
+
SELECT command_id AS "commandId", committed_at_ms::float8 AS "committedAtMs",
|
|
541
|
+
duration_ms::float8 AS "durationMs", outcome_tag AS "outcomeTag"
|
|
542
|
+
FROM (
|
|
543
|
+
SELECT command_id, command, caller_key, committed_at_ms, duration_ms, outcome_tag
|
|
544
|
+
FROM durable.receipts
|
|
545
|
+
WHERE tenant_id = ${tenant} AND actor_type = ${entry.actorType}
|
|
546
|
+
ORDER BY expires_at_ms DESC
|
|
547
|
+
LIMIT ${SCHEDULE_RECEIPTS}) r
|
|
548
|
+
WHERE command = ${entry.command} AND starts_with(caller_key, '["System","cron",')
|
|
549
|
+
AND committed_at_ms IS NOT NULL
|
|
550
|
+
ORDER BY committed_at_ms DESC, command_id COLLATE "C" DESC
|
|
551
|
+
LIMIT 1`;
|
|
552
|
+
return {
|
|
553
|
+
actorType: entry.actorType,
|
|
554
|
+
key: entry.key,
|
|
555
|
+
expression: entry.key.startsWith(CRON_PREFIX)
|
|
556
|
+
? entry.key.slice(CRON_PREFIX.length)
|
|
557
|
+
: entry.key,
|
|
558
|
+
command: entry.command,
|
|
559
|
+
pending: timers?.pending ?? 0,
|
|
560
|
+
nextDueAtMs: timers?.dueAtMs ?? null,
|
|
561
|
+
lastRun: last ?? null,
|
|
562
|
+
};
|
|
563
|
+
})).pipe(Effect.map((found) => ({ schedules: found })));
|
package/dist/runtime/layer.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface Options {
|
|
|
81
81
|
};
|
|
82
82
|
/** Telemetry this runner keeps beside the spans and metrics it reports. */
|
|
83
83
|
readonly observability?: {
|
|
84
|
-
/** Defect spans the runner keeps for `
|
|
84
|
+
/** Defect spans the runner keeps for `akter defects list`, newest last. Default 1,000. */
|
|
85
85
|
readonly defects?: number;
|
|
86
86
|
/**
|
|
87
87
|
* How often one runner of the deployment samples the database gauges
|
|
@@ -94,7 +94,7 @@ export interface Options {
|
|
|
94
94
|
* How long this runtime keeps writing event and effect payload versions
|
|
95
95
|
* without refreshing its writer rows; it refreshes every half window and
|
|
96
96
|
* refuses new turns once a window passes without a refresh, so
|
|
97
|
-
* `
|
|
97
|
+
* `akter payloads clear` can tell when no turn still writes an old
|
|
98
98
|
* version. Default 2 minutes, at least 1 second.
|
|
99
99
|
*/
|
|
100
100
|
readonly payloadWriterWindow?: Duration.Input;
|
|
@@ -159,7 +159,7 @@ export interface Options {
|
|
|
159
159
|
};
|
|
160
160
|
/**
|
|
161
161
|
* The fleet views this runtime maintains and serves. They need Postgres with
|
|
162
|
-
* `wal_level=logical`, a login with `REPLICATION`, and `
|
|
162
|
+
* `wal_level=logical`, a login with `REPLICATION`, and `akter fleet setup`;
|
|
163
163
|
* startup refuses otherwise, and PGlite refuses any view. One runner at a
|
|
164
164
|
* time maintains them, off the turn path.
|
|
165
165
|
*/
|
|
@@ -174,7 +174,10 @@ export interface Options {
|
|
|
174
174
|
* Constraints the wiring keeps:
|
|
175
175
|
* - Turns run through the drain gate, so a drain refuses new turns and
|
|
176
176
|
* interrupts the rest. The runtime scope, not the caller, owns an in-flight
|
|
177
|
-
* turn: interrupting a waiter never cancels it.
|
|
177
|
+
* turn: interrupting a waiter never cancels it. Closing that scope does, and
|
|
178
|
+
* a command it cuts off, or one sent after it starts closing, fails with a
|
|
179
|
+
* retryable `ActorUnavailable` instead of an interrupt or a wait that never
|
|
180
|
+
* ends; delivery stops retrying once the scope is closed.
|
|
178
181
|
* - Commands are direct, so Cluster keeps no messages and the receipt is the
|
|
179
182
|
* only admission record; a restarted activation or lost runner drops the
|
|
180
183
|
* uncommitted attempt, the handle retries with the same command id, and the
|
|
@@ -219,42 +222,48 @@ export interface Options {
|
|
|
219
222
|
* retry like any delivery failure; it is not a defect.
|
|
220
223
|
*/
|
|
221
224
|
export declare const layer: (options?: Options) => Layer.Layer<Actors | ContentStore | DefectLog | InternalActors | OperatorRuntime | RuntimeControl | TelemetrySampler, import("effect/sql/Migrator").MigrationError | SqlError.SqlError, Crypto.Crypto | SqlClient.SqlClient>;
|
|
225
|
+
type PostgresOptions = Omit<PgClient.PgPoolConfig, "types"> & {
|
|
226
|
+
readonly offTurnConnections?: number;
|
|
227
|
+
readonly queryConnections?: number;
|
|
228
|
+
readonly replica?: Omit<PgClient.PgPoolConfig, "types"> | undefined;
|
|
229
|
+
/** An unsharded primary shared by every runner; owns coordination rows and Cluster and fleet locks. */
|
|
230
|
+
readonly coordination?: Omit<PgClient.PgPoolConfig, "types"> | undefined;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* A runner holds three primary pools, plus optional replica and coordination
|
|
234
|
+
* pools. Turns lease sessions from the turn pool, `maxConnections` (default
|
|
235
|
+
* 50): a command holds one session until its transaction ends, so a pool
|
|
236
|
+
* smaller than the commands in flight queues callers behind
|
|
237
|
+
* it. Queries read from the query pool, `queryConnections` (default 10),
|
|
238
|
+
* except those of types with owned tables or blobs. Command admission,
|
|
239
|
+
* receipt replays, the relay, migrations, and cluster storage use the
|
|
240
|
+
* off-turn pool, `offTurnConnections` (default 10), unless `coordination`
|
|
241
|
+
* moves Cluster storage and deployment locks to an independent unsharded
|
|
242
|
+
* primary. All runners must designate the same authority. Every pool opens
|
|
243
|
+
* connections only as load needs them, refuses checkouts past its
|
|
244
|
+
* connections plus a bounded waiter allowance, and hands its connections out
|
|
245
|
+
* first come, first served. Keep the sum across runners below the server's
|
|
246
|
+
* `max_connections`.
|
|
247
|
+
*
|
|
248
|
+
* `replica` is this runner's nearest streaming replica of the same primary.
|
|
249
|
+
* Queries read there once it has replayed the commit version their caller
|
|
250
|
+
* last saw, and read the primary when it is behind or fails. Its pool
|
|
251
|
+
* (`maxConnections` default 10) opens connections only as queries need them.
|
|
252
|
+
* Every pool requests server TCP keepalives at 5 seconds idle, 2 seconds
|
|
253
|
+
* between probes, and 3 probes. `startupParameters` overrides these defaults
|
|
254
|
+
* independently on the primary and replica configurations.
|
|
255
|
+
*
|
|
256
|
+
* Registers a `regclass` codec because the pinned driver lacks one and the
|
|
257
|
+
* migrator needs it on restart; remove once Effect #8309 lands.
|
|
258
|
+
*/
|
|
259
|
+
declare function postgres(options: PostgresOptions): Layer.Layer<PgClient.PgClient | SqlClient.SqlClient | TurnConnections, SqlError.SqlError>;
|
|
222
260
|
/** Database layers for `Actors.layer`: `postgres` for real deployments, `pglite` for embedded and test use. */
|
|
223
261
|
export declare const Database: {
|
|
224
|
-
|
|
225
|
-
* A runner holds three primary pools, plus optional replica and coordination
|
|
226
|
-
* pools. Turns lease sessions from the turn pool, `maxConnections` (default
|
|
227
|
-
* 50): a command holds one session until its transaction ends, so a pool
|
|
228
|
-
* smaller than the commands in flight queues callers behind
|
|
229
|
-
* it. Queries read from the query pool, `queryConnections` (default 10),
|
|
230
|
-
* except those of types with owned tables or blobs. Command admission,
|
|
231
|
-
* receipt replays, the relay, migrations, and cluster storage use the
|
|
232
|
-
* off-turn pool, `offTurnConnections` (default 10), unless `coordination`
|
|
233
|
-
* moves Cluster storage and deployment locks to an independent unsharded
|
|
234
|
-
* primary. All runners must designate the same authority. Every pool opens
|
|
235
|
-
* connections only as load needs them, refuses checkouts past its
|
|
236
|
-
* connections plus a bounded waiter allowance, and hands its connections out
|
|
237
|
-
* first come, first served. Keep the sum across runners below the server's
|
|
238
|
-
* `max_connections`.
|
|
239
|
-
*
|
|
240
|
-
* `replica` is this runner's nearest streaming replica of the same primary.
|
|
241
|
-
* Queries read there once it has replayed the commit version their caller
|
|
242
|
-
* last saw, and read the primary when it is behind or fails. Its pool
|
|
243
|
-
* (`maxConnections` default 10) opens connections only as queries need them.
|
|
244
|
-
* Every pool requests server TCP keepalives at 5 seconds idle, 2 seconds
|
|
245
|
-
* between probes, and 3 probes. `startupParameters` overrides these defaults
|
|
246
|
-
* independently on the primary and replica configurations.
|
|
247
|
-
*
|
|
248
|
-
* Registers a `regclass` codec because the pinned driver lacks one and the
|
|
249
|
-
* migrator needs it on restart; remove once Effect #8309 lands.
|
|
250
|
-
*/
|
|
251
|
-
postgres: (options: Omit<PgClient.PgPoolConfig, "types"> & {
|
|
252
|
-
readonly offTurnConnections?: number;
|
|
253
|
-
readonly queryConnections?: number;
|
|
254
|
-
readonly neki?: boolean;
|
|
255
|
-
readonly replica?: Omit<PgClient.PgPoolConfig, "types"> | undefined;
|
|
256
|
-
/** An unsharded primary shared by every runner; owns coordination rows and Cluster and fleet locks. */
|
|
257
|
-
readonly coordination?: Omit<PgClient.PgPoolConfig, "types"> | undefined;
|
|
258
|
-
}) => Layer.Layer<PgClient.PgClient | SqlClient.SqlClient | TurnConnections, SqlError.SqlError, never>;
|
|
262
|
+
postgres: typeof postgres;
|
|
259
263
|
pglite: typeof pglite;
|
|
264
|
+
schemaChange: {
|
|
265
|
+
(lock: number): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient>;
|
|
266
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>, lock: number): Effect.Effect<A, E | SqlError.SqlError, R | SqlClient.SqlClient>;
|
|
267
|
+
};
|
|
260
268
|
};
|
|
269
|
+
export {};
|