@rikalabs/akter 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +24 -0
- package/dist/actor/descriptor.js +3 -0
- package/dist/actor/handles.d.ts +1 -1
- package/dist/actor/layers.js +2 -0
- package/dist/actor/reads.js +2 -1
- package/dist/contexts/command.d.ts +2 -0
- package/dist/identity/caller.d.ts +4 -4
- package/dist/identity/caller.js +11 -2
- package/dist/identity/mint.d.ts +1 -1
- package/dist/policies/command.d.ts +3 -0
- package/dist/policies/command.js +1 -0
- package/dist/runtime/connections/owner.js +60 -13
- package/dist/runtime/connections/protocol.d.ts +2 -2
- package/dist/runtime/connections/streams.js +2 -1
- package/dist/runtime/database/bounded.js +17 -2
- package/dist/runtime/database/connection.d.ts +3 -2
- package/dist/runtime/database/connection.js +9 -4
- package/dist/runtime/database/coordination.d.ts +6 -3
- package/dist/runtime/database/coordination.js +19 -18
- package/dist/runtime/database/migrations.d.ts +4 -0
- package/dist/runtime/database/migrations.js +72 -1
- package/dist/runtime/entity/mailbox.d.ts +18 -10
- package/dist/runtime/entity/mailbox.js +42 -34
- package/dist/runtime/entity/register.d.ts +3 -3
- package/dist/runtime/entity/register.js +280 -271
- package/dist/runtime/events/append.d.ts +3 -3
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/layer.d.ts +24 -1
- package/dist/runtime/layer.js +64 -9
- package/dist/runtime/members.d.ts +2 -0
- package/dist/runtime/operators/export.d.ts +3 -1
- package/dist/runtime/operators/export.js +42 -11
- package/dist/runtime/queries.d.ts +1 -1
- package/dist/runtime/queries.js +23 -6
- package/dist/runtime/registration.js +27 -1
- package/dist/runtime/request.d.ts +1 -1
- package/dist/runtime/runner.d.ts +18 -1
- package/dist/runtime/runner.js +24 -4
- package/dist/runtime/storage/cold-storage.d.ts +56 -0
- package/dist/runtime/storage/cold-storage.js +138 -0
- package/dist/runtime/storage/cold-tier.d.ts +141 -0
- package/dist/runtime/storage/cold-tier.js +355 -0
- package/dist/runtime/storage/generation.d.ts +14 -0
- package/dist/runtime/storage/generation.js +2 -0
- package/dist/runtime/storage/placements.js +1 -0
- package/dist/runtime/telemetry/metrics.d.ts +4 -0
- package/dist/runtime/telemetry/metrics.js +20 -4
- package/dist/runtime/telemetry/sampler.js +4 -3
- package/dist/runtime/telemetry/spans.d.ts +1 -1
- package/dist/runtime/topology/locks.js +8 -6
- package/dist/runtime/turn/blobs.d.ts +4 -1
- package/dist/runtime/turn/blobs.js +19 -2
- package/dist/runtime/turn/execute.d.ts +3 -1
- package/dist/runtime/turn/execute.js +272 -20
- package/dist/runtime/turn/hooks.d.ts +1 -1
- package/dist/runtime/turn/outbox.d.ts +1 -1
- package/dist/runtime/turn/receipt.d.ts +1 -1
- package/dist/runtime/turn/relay.d.ts +2 -2
- package/dist/runtime/turn/relay.js +53 -7
- package/dist/testing/actor-test.d.ts +1 -0
- package/dist/testing/actor-test.js +4 -1
- package/package.json +1 -1
|
@@ -1213,6 +1213,77 @@ export const migrations = {
|
|
|
1213
1213
|
${[...FIRST_VIEWS, ["views", 1]].map(([view, version]) => `('${view}', ${version})`).join(", ")}
|
|
1214
1214
|
) AS v(view_name, version)`);
|
|
1215
1215
|
}),
|
|
1216
|
+
/** Cold pointers replace state and blob rows atomically; garbage rows never authorize deletion by themselves. */
|
|
1217
|
+
"0034_cold_tier": Effect.gen(function* () {
|
|
1218
|
+
const sql = yield* SqlClient.SqlClient;
|
|
1219
|
+
yield* briefLocks(Effect.gen(function* () {
|
|
1220
|
+
yield* sql `ALTER TABLE actor_generations
|
|
1221
|
+
ADD COLUMN cold_ref text,
|
|
1222
|
+
ADD COLUMN cold_digest text,
|
|
1223
|
+
ADD COLUMN cold_state_version integer,
|
|
1224
|
+
ADD CONSTRAINT actor_generations_cold_check CHECK (
|
|
1225
|
+
(cold_ref IS NULL AND cold_digest IS NULL AND cold_state_version IS NULL)
|
|
1226
|
+
OR (cold_ref IS NOT NULL AND cold_digest IS NOT NULL AND cold_state_version IS NOT NULL
|
|
1227
|
+
AND cold_digest ~ '^[0-9a-f]{64}$' AND cold_state_version >= 0))`;
|
|
1228
|
+
yield* sql `ALTER TABLE actor_outbox DROP CONSTRAINT actor_outbox_kind_check,
|
|
1229
|
+
ADD CONSTRAINT actor_outbox_kind_check CHECK (kind IN ('intent', 'job', 'feed', 'control', 'cold')),
|
|
1230
|
+
ADD CONSTRAINT actor_outbox_cold_check CHECK (
|
|
1231
|
+
(kind = 'cold') = (command = '$cold') AND
|
|
1232
|
+
(kind <> 'cold' OR (timer_key = '$cold' AND target_type = actor_type
|
|
1233
|
+
AND target_id = actor_id AND payload = 'null' AND caller::jsonb->>'source' = 'cold'
|
|
1234
|
+
AND caller::jsonb->>'_tag' = 'System') IS TRUE))`;
|
|
1235
|
+
yield* sql `ALTER TABLE actor_placements ADD COLUMN state_version integer NOT NULL DEFAULT 0`;
|
|
1236
|
+
}));
|
|
1237
|
+
yield* sql `CREATE INDEX actor_generations_cold_version
|
|
1238
|
+
ON actor_generations (actor_type, cold_state_version) WHERE cold_ref IS NOT NULL`;
|
|
1239
|
+
yield* sql `CREATE TABLE actor_cold_garbage (
|
|
1240
|
+
routing_key bigint NOT NULL,
|
|
1241
|
+
tenant_id text NOT NULL,
|
|
1242
|
+
actor_type text NOT NULL,
|
|
1243
|
+
actor_id text NOT NULL,
|
|
1244
|
+
object_key text NOT NULL,
|
|
1245
|
+
unreferenced_at_ms bigint NOT NULL,
|
|
1246
|
+
PRIMARY KEY (routing_key, object_key)
|
|
1247
|
+
)`;
|
|
1248
|
+
yield* sql `CREATE INDEX actor_cold_garbage_due ON actor_cold_garbage (unreferenced_at_ms)`;
|
|
1249
|
+
yield* sql `ALTER TABLE actor_cold_garbage ENABLE ROW LEVEL SECURITY`;
|
|
1250
|
+
yield* sql `CREATE POLICY durable_tenant ON actor_cold_garbage
|
|
1251
|
+
USING (tenant_id = current_setting('durable.tenant', true))
|
|
1252
|
+
WITH CHECK (tenant_id = current_setting('durable.tenant', true))`;
|
|
1253
|
+
yield* sql `CREATE OR REPLACE VIEW durable.actors AS
|
|
1254
|
+
SELECT g.tenant_id, g.actor_type, g.actor_id, g.routing_key, p.placement,
|
|
1255
|
+
g.generation, g.created, g.event_sequence AS last_event_sequence,
|
|
1256
|
+
g.cold_ref IS NOT NULL AS cold
|
|
1257
|
+
FROM actor_generations g
|
|
1258
|
+
LEFT JOIN actor_placements p ON p.actor_type = g.actor_type`;
|
|
1259
|
+
}),
|
|
1260
|
+
/** The transaction's generation lock, not a referential trigger, authorizes state and receipt writes. */
|
|
1261
|
+
"0035_hot_table_foreign_keys": Effect.gen(function* () {
|
|
1262
|
+
const sql = yield* SqlClient.SqlClient;
|
|
1263
|
+
yield* briefLocks(Effect.gen(function* () {
|
|
1264
|
+
for (const table of ["actor_state", "actor_receipts"]) {
|
|
1265
|
+
const constraints = yield* sql `
|
|
1266
|
+
SELECT c.conname AS name FROM pg_constraint c
|
|
1267
|
+
WHERE c.contype = 'f' AND c.conrelid = ${table}::regclass
|
|
1268
|
+
AND c.confrelid = 'actor_generations'::regclass
|
|
1269
|
+
AND c.confupdtype = 'a' AND c.confdeltype = 'a' AND c.confmatchtype = 's'
|
|
1270
|
+
AND c.convalidated AND NOT c.condeferrable AND NOT c.condeferred
|
|
1271
|
+
AND ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY k(n, ord)
|
|
1272
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n
|
|
1273
|
+
ORDER BY k.ord) = ARRAY['routing_key', 'tenant_id', 'actor_type', 'actor_id']
|
|
1274
|
+
AND ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY k(n, ord)
|
|
1275
|
+
JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.n
|
|
1276
|
+
ORDER BY k.ord) = ARRAY['routing_key', 'tenant_id', 'actor_type', 'actor_id']`;
|
|
1277
|
+
if (constraints.length !== 1) {
|
|
1278
|
+
return yield* new Migrator.MigrationError({
|
|
1279
|
+
kind: "BadState",
|
|
1280
|
+
message: `Expected exactly one generation foreign key on ${table}; found ${constraints.length}`,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
yield* sql `ALTER TABLE ${sql(table)} DROP CONSTRAINT ${sql(constraints[0].name)}`;
|
|
1284
|
+
}
|
|
1285
|
+
}));
|
|
1286
|
+
}),
|
|
1216
1287
|
};
|
|
1217
1288
|
/**
|
|
1218
1289
|
* Runs `record` like `Migrator`, but first refuses a database where a
|
|
@@ -1246,7 +1317,7 @@ export const migrator = (record) => {
|
|
|
1246
1317
|
const transactional = refuseSkipped.pipe(Effect.andThen(run), Effect.tap(() => refuseSkipped));
|
|
1247
1318
|
return Effect.gen(function* () {
|
|
1248
1319
|
return yield* Option.isSome(yield* Effect.serviceOption(PgClient.PgClient))
|
|
1249
|
-
? withMigrationCoordination(transactional)
|
|
1320
|
+
? withMigrationCoordination({ effect: transactional, history: "actor_migrations" })
|
|
1250
1321
|
: transactional;
|
|
1251
1322
|
});
|
|
1252
1323
|
};
|
|
@@ -65,22 +65,30 @@ export declare const takeBatch: <W extends Mergeable & {
|
|
|
65
65
|
* front command is takeable, so the worker never sleeps past ready work and a
|
|
66
66
|
* command whose hook is still running holds back every command behind it.
|
|
67
67
|
* `alone` outlives the handler: it holds the ids of a failed batch, each run
|
|
68
|
-
* in a transaction of its own once.
|
|
68
|
+
* in a transaction of its own once. Methods are shared; the waiting commands,
|
|
69
|
+
* wake-up and retry isolation belong to this handler alone.
|
|
69
70
|
*/
|
|
70
71
|
export declare const activationMailbox: <W extends Mergeable & {
|
|
71
72
|
queued: boolean;
|
|
72
|
-
}>(alone: Set<string>) =>
|
|
73
|
-
|
|
73
|
+
}>(alone: Set<string>) => ActivationMailbox<W>;
|
|
74
|
+
declare class ActivationMailbox<W extends Mergeable & {
|
|
75
|
+
queued: boolean;
|
|
76
|
+
}> {
|
|
77
|
+
private readonly alone;
|
|
78
|
+
private readonly waiting;
|
|
79
|
+
private readonly ready;
|
|
80
|
+
constructor(alone: Set<string>);
|
|
81
|
+
offer(entry: W): void;
|
|
74
82
|
/** Marks `entry`'s `queued` hook finished, so a batch may take it. */
|
|
75
|
-
queued
|
|
83
|
+
queued(entry: W): void;
|
|
76
84
|
/** Waits until the front command may be takeable. */
|
|
77
|
-
await: import("effect/Effect").Effect<void, never, never>;
|
|
85
|
+
readonly await: import("effect/Effect").Effect<void, never, never>;
|
|
78
86
|
/** The next batch, possibly empty; the wake-up closes once the front command is not takeable. */
|
|
79
|
-
take
|
|
87
|
+
take(): W[];
|
|
80
88
|
/** Puts taken commands back in front of everything that arrived since. */
|
|
81
|
-
requeue
|
|
89
|
+
requeue(entries: ReadonlyArray<W>): void;
|
|
82
90
|
/** Runs each of `ids` alone once, the next time it is taken. */
|
|
83
|
-
isolate
|
|
84
|
-
size
|
|
85
|
-
}
|
|
91
|
+
isolate(ids: Iterable<string>): void;
|
|
92
|
+
size(): number;
|
|
93
|
+
}
|
|
86
94
|
export {};
|
|
@@ -84,38 +84,46 @@ export const takeBatch = ({ waiting, alone, }) => {
|
|
|
84
84
|
* front command is takeable, so the worker never sleeps past ready work and a
|
|
85
85
|
* command whose hook is still running holds back every command behind it.
|
|
86
86
|
* `alone` outlives the handler: it holds the ids of a failed batch, each run
|
|
87
|
-
* in a transaction of its own once.
|
|
87
|
+
* in a transaction of its own once. Methods are shared; the waiting commands,
|
|
88
|
+
* wake-up and retry isolation belong to this handler alone.
|
|
88
89
|
*/
|
|
89
|
-
export const activationMailbox = (alone) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
90
|
+
export const activationMailbox = (alone) => new ActivationMailbox(alone);
|
|
91
|
+
class ActivationMailbox {
|
|
92
|
+
alone;
|
|
93
|
+
waiting = [];
|
|
94
|
+
ready = Latch.makeUnsafe(false);
|
|
95
|
+
constructor(alone) {
|
|
96
|
+
this.alone = alone;
|
|
97
|
+
}
|
|
98
|
+
offer(entry) {
|
|
99
|
+
this.waiting.push(entry);
|
|
100
|
+
}
|
|
101
|
+
/** Marks `entry`'s `queued` hook finished, so a batch may take it. */
|
|
102
|
+
queued(entry) {
|
|
103
|
+
entry.queued = true;
|
|
104
|
+
this.ready.openUnsafe();
|
|
105
|
+
}
|
|
106
|
+
/** Waits until the front command may be takeable. */
|
|
107
|
+
await = this.ready.await;
|
|
108
|
+
/** The next batch, possibly empty; the wake-up closes once the front command is not takeable. */
|
|
109
|
+
take() {
|
|
110
|
+
const batch = takeBatch({ waiting: this.waiting, alone: this.alone });
|
|
111
|
+
if (this.waiting[0]?.queued !== true)
|
|
112
|
+
this.ready.closeUnsafe();
|
|
113
|
+
return batch;
|
|
114
|
+
}
|
|
115
|
+
/** Puts taken commands back in front of everything that arrived since. */
|
|
116
|
+
requeue(entries) {
|
|
117
|
+
if (entries.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
this.waiting.unshift(...entries);
|
|
120
|
+
this.ready.openUnsafe();
|
|
121
|
+
}
|
|
122
|
+
/** Runs each of `ids` alone once, the next time it is taken. */
|
|
123
|
+
isolate(ids) {
|
|
124
|
+
markAlone({ alone: this.alone, ids });
|
|
125
|
+
}
|
|
126
|
+
size() {
|
|
127
|
+
return this.waiting.length;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -42,7 +42,7 @@ export declare const commandEntity: (name: string) => Entity.Entity<string, Rpc.
|
|
|
42
42
|
readonly caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
|
|
43
43
|
readonly subject: Schema.NonEmptyString;
|
|
44
44
|
}>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
|
|
45
|
-
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
|
|
45
|
+
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
|
|
46
46
|
readonly ref: Schema.optional<Schema.Struct<{
|
|
47
47
|
readonly tenant: Schema.NonEmptyString;
|
|
48
48
|
readonly actor: Schema.NonEmptyString;
|
|
@@ -147,7 +147,7 @@ export declare const connectionEntity: (name: string) => Entity.Entity<`${string
|
|
|
147
147
|
caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
|
|
148
148
|
readonly subject: Schema.NonEmptyString;
|
|
149
149
|
}>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
|
|
150
|
-
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
|
|
150
|
+
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
|
|
151
151
|
readonly ref: Schema.optional<Schema.Struct<{
|
|
152
152
|
readonly tenant: Schema.NonEmptyString;
|
|
153
153
|
readonly actor: Schema.NonEmptyString;
|
|
@@ -224,7 +224,7 @@ export declare const connectionEntity: (name: string) => Entity.Entity<`${string
|
|
|
224
224
|
caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
|
|
225
225
|
readonly subject: Schema.NonEmptyString;
|
|
226
226
|
}>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
|
|
227
|
-
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
|
|
227
|
+
readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
|
|
228
228
|
readonly ref: Schema.optional<Schema.Struct<{
|
|
229
229
|
readonly tenant: Schema.NonEmptyString;
|
|
230
230
|
readonly actor: Schema.NonEmptyString;
|