@nylorun/runtime 0.10.0-beta → 0.12.0-beta
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 +86 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +51 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +645 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +38 -0
- package/dist/tenant/http.js +53 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +40 -0
- package/dist/tenant/sessions.js +163 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
1
|
+
import type { SessionStore } from "../store/types.js";
|
|
2
|
+
import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
|
|
3
3
|
export interface MemoryTenantStoreOptions {
|
|
4
|
+
/** When set, each Tenant's directory under this Host root lives as long as the Tenant. */
|
|
5
|
+
hostRoot?: string;
|
|
4
6
|
openRuntime: OpenTenantRuntime;
|
|
5
7
|
configFor: (tenantId: string) => TenantConfig;
|
|
6
8
|
logger?: Logger;
|
|
7
|
-
/** Schema version recorded for new tenants (default TENANT_SCHEMA_VERSION). */
|
|
8
|
-
schemaVersion?: number;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* so conformance can exercise the same module path as the FS adapter.
|
|
13
|
-
*/
|
|
14
|
-
export declare function createMemoryTenantStore(options: MemoryTenantStoreOptions): TenantStore & {
|
|
15
|
-
/** Test helper: insert a raw record without opening. */
|
|
16
|
-
seed(envelope: TenantEnvelope, bootstrap: BootstrapPrincipal): void;
|
|
17
|
-
/** Test helper: current live ids. */
|
|
10
|
+
export type MemoryTenantStore = TenantStore & {
|
|
11
|
+
/** Test helper: current Tenant ids. */
|
|
18
12
|
ids(): string[];
|
|
13
|
+
/** Test helper: a Session Store on a Tenant's data, as another process would open it. */
|
|
14
|
+
sessionStore(id: string): SessionStore;
|
|
19
15
|
};
|
|
16
|
+
export declare function createMemoryTenantStore(options: MemoryTenantStoreOptions): MemoryTenantStore;
|
|
@@ -1,100 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory Tenant store for tests: each Tenant is a `MemoryStoreData` (its Session
|
|
3
|
+
* Store's data) and an envelope. It behaves like the Postgres store (`store-pg.ts`): the
|
|
4
|
+
* Tenant directory under the Host root, when one is given, is created with the Tenant and
|
|
5
|
+
* removed with it, and `open` hands the Tenant Runtime a fresh Session Store on the Tenant's
|
|
6
|
+
* data. Not a supported profile.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
1
9
|
import { isTenantId } from "@nylorun/core/compatibility";
|
|
2
|
-
import {
|
|
10
|
+
import { MEMORY_SCHEMA_VERSION, MemorySessionStore, MemoryStoreData, } from "../store/memory.js";
|
|
3
11
|
import { quarantine } from "./quarantine.js";
|
|
4
12
|
import { parseEnvelope } from "./envelope.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
13
|
+
import { tenantPaths } from "./paths.js";
|
|
14
|
+
import { bootstrapPrincipal, bootstrapPrincipalMatches } from "./principals.js";
|
|
15
|
+
import { TenantNotFoundError, } from "./types.js";
|
|
9
16
|
export function createMemoryTenantStore(options) {
|
|
10
17
|
const records = new Map();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
function sessionStore(id, data) {
|
|
19
|
+
return new MemorySessionStore({
|
|
20
|
+
tenantId: id,
|
|
21
|
+
onError: (error) => options.logger?.error("post-commit step failed", {
|
|
22
|
+
message: error instanceof Error ? error.message : String(error),
|
|
23
|
+
}),
|
|
24
|
+
}, data);
|
|
25
|
+
}
|
|
26
|
+
function makeDirectory(id) {
|
|
27
|
+
if (!options.hostRoot)
|
|
28
|
+
return;
|
|
29
|
+
const p = tenantPaths(options.hostRoot, id);
|
|
30
|
+
mkdirSync(p.root, { recursive: true, mode: 0o700 });
|
|
31
|
+
for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
function removeDirectory(id) {
|
|
35
|
+
if (options.hostRoot)
|
|
36
|
+
rmSync(tenantPaths(options.hostRoot, id).root, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
force: true,
|
|
19
39
|
});
|
|
20
|
-
|
|
40
|
+
}
|
|
41
|
+
async function withStore(id, data, fn) {
|
|
42
|
+
const store = sessionStore(id, data);
|
|
43
|
+
try {
|
|
44
|
+
return await fn(store);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await store.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
21
51
|
ids() {
|
|
22
|
-
return [...records.keys()]
|
|
52
|
+
return [...records.keys()];
|
|
53
|
+
},
|
|
54
|
+
sessionStore(id) {
|
|
55
|
+
const record = records.get(id);
|
|
56
|
+
if (!record)
|
|
57
|
+
throw new TenantNotFoundError(id);
|
|
58
|
+
return sessionStore(id, record.data);
|
|
23
59
|
},
|
|
24
60
|
async enumerate() {
|
|
25
|
-
return [...records.
|
|
26
|
-
.filter(([, r]) => !r.trashed)
|
|
27
|
-
.map(([id]) => id)
|
|
28
|
-
.filter(isTenantId);
|
|
61
|
+
return [...records.keys()].filter(isTenantId).sort();
|
|
29
62
|
},
|
|
30
63
|
async readEnvelope(id) {
|
|
31
64
|
const record = records.get(id);
|
|
32
|
-
if (!record
|
|
33
|
-
throw
|
|
34
|
-
tenantId: id,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
65
|
+
if (!record)
|
|
66
|
+
throw new TenantNotFoundError(id);
|
|
37
67
|
return parseEnvelope(record.envelope, id);
|
|
38
68
|
},
|
|
39
69
|
async create(envelope, bootstrap) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (existing && !existing.trashed)
|
|
70
|
+
const parsed = parseEnvelope({ ...envelope, schemaVersion: MEMORY_SCHEMA_VERSION }, envelope.id);
|
|
71
|
+
if (records.has(parsed.id))
|
|
43
72
|
return "exists";
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
73
|
+
makeDirectory(parsed.id);
|
|
74
|
+
const data = new MemoryStoreData();
|
|
75
|
+
await withStore(parsed.id, data, (store) => store.tx((t) => bootstrapPrincipal(t, bootstrap)));
|
|
76
|
+
records.set(parsed.id, { envelope: parsed, data });
|
|
49
77
|
return "created";
|
|
50
78
|
},
|
|
51
79
|
async bootstrapMatches(id, bootstrap) {
|
|
52
80
|
const record = records.get(id);
|
|
53
|
-
if (!record
|
|
81
|
+
if (!record)
|
|
54
82
|
return false;
|
|
55
|
-
return (record.
|
|
56
|
-
record.bootstrap.credentialHash === bootstrap.credentialHash &&
|
|
57
|
-
record.bootstrap.idempotencyKey === bootstrap.idempotencyKey);
|
|
83
|
+
return withStore(id, record.data, (store) => store.tx((t) => bootstrapPrincipalMatches(t, bootstrap)));
|
|
58
84
|
},
|
|
59
85
|
async open(id) {
|
|
60
86
|
const record = records.get(id);
|
|
61
|
-
if (!record
|
|
62
|
-
throw
|
|
63
|
-
|
|
64
|
-
});
|
|
87
|
+
if (!record)
|
|
88
|
+
throw new TenantNotFoundError(id);
|
|
89
|
+
if (record.envelope.schemaVersion > MEMORY_SCHEMA_VERSION)
|
|
90
|
+
throw quarantine("schema-too-new", `Tenant schema version ${record.envelope.schemaVersion} is newer than this Runtime's ${MEMORY_SCHEMA_VERSION}`, { tenantId: id });
|
|
91
|
+
makeDirectory(id);
|
|
92
|
+
const store = sessionStore(id, record.data);
|
|
93
|
+
try {
|
|
94
|
+
return await options.openRuntime({ ...options.configFor(id), tenantId: id }, { store, envelope: record.envelope });
|
|
65
95
|
}
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
catch (error) {
|
|
97
|
+
await store.close().catch(() => undefined);
|
|
98
|
+
throw error;
|
|
68
99
|
}
|
|
69
|
-
const config = options.configFor(id);
|
|
70
|
-
const opened = await options.openRuntime({
|
|
71
|
-
...config,
|
|
72
|
-
tenantId: id,
|
|
73
|
-
});
|
|
74
|
-
const handle = {
|
|
75
|
-
...opened,
|
|
76
|
-
envelope: record.envelope,
|
|
77
|
-
handle: opened.handle.bind(opened),
|
|
78
|
-
summary: opened.summary.bind(opened),
|
|
79
|
-
drain: opened.drain.bind(opened),
|
|
80
|
-
close: opened.close.bind(opened),
|
|
81
|
-
};
|
|
82
|
-
record.handle = handle;
|
|
83
|
-
return handle;
|
|
84
100
|
},
|
|
85
|
-
async trash(id
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return;
|
|
89
|
-
if (record.handle) {
|
|
90
|
-
await record.handle.close().catch(() => undefined);
|
|
91
|
-
}
|
|
92
|
-
record.trashed = true;
|
|
93
|
-
record.handle = undefined;
|
|
101
|
+
async trash(id) {
|
|
102
|
+
records.delete(id);
|
|
103
|
+
removeDirectory(id);
|
|
94
104
|
},
|
|
95
105
|
async removePartial(id) {
|
|
96
|
-
records.
|
|
106
|
+
if (records.has(id))
|
|
107
|
+
return;
|
|
108
|
+
removeDirectory(id);
|
|
97
109
|
},
|
|
98
110
|
};
|
|
99
|
-
return store;
|
|
100
111
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
2
|
+
import type { Migration } from "../store/postgres/migrations/index.js";
|
|
3
|
+
import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantStore } from "./types.js";
|
|
4
|
+
export interface PostgresTenantStoreOptions {
|
|
5
|
+
hostRoot: string;
|
|
6
|
+
/** The Host's pool. The store never ends it. */
|
|
7
|
+
sql: PostgresClient;
|
|
8
|
+
/** Opens the Tenant Runtime on a store the catalog opened; the Runtime then owns it. */
|
|
9
|
+
openRuntime: OpenTenantRuntime;
|
|
10
|
+
configFor: (tenantId: string) => TenantConfig;
|
|
11
|
+
logger?: Logger;
|
|
12
|
+
/** Tests only: the migrations this Runtime knows. */
|
|
13
|
+
migrations?: readonly Migration[];
|
|
14
|
+
}
|
|
15
|
+
export declare function createPostgresTenantStore(options: PostgresTenantStoreOptions): TenantStore;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant store over the Postgres Tenant catalog (architecture §8.1, §8.2).
|
|
3
|
+
*
|
|
4
|
+
* A Tenant exists when its schema `tenant_<id>` exists (`store/postgres/tenants.ts`); the
|
|
5
|
+
* envelope is a row in it, so this store keeps no registry. What stays on the Host root is
|
|
6
|
+
* the Tenant directory `tenants/<id>/`: the vault key (`vault-kek`), `plugin-data/`, `logs/`,
|
|
7
|
+
* and the private `home/`, `tmp/` and `sandboxes/`. It is created with the Tenant and removed
|
|
8
|
+
* with it; nothing in it says whether the Tenant exists.
|
|
9
|
+
*
|
|
10
|
+
* - `create` makes the directory, then creates, migrates and bootstraps the schema in one
|
|
11
|
+
* transaction. A schema that already exists is `exists`; the module compares its bootstrap.
|
|
12
|
+
* - `open` migrates an older schema forward and opens the Tenant Runtime on the schema's
|
|
13
|
+
* Session Store. A missing schema is `TenantNotFoundError`; a schema newer than this
|
|
14
|
+
* Runtime, a failed migration, an unreadable envelope or vault ciphertext without its key
|
|
15
|
+
* throws that quarantine. A failure outside the Tenant (Postgres unreachable, its sweep
|
|
16
|
+
* not armed) is `TenantUnavailableError`, which the module does not cache.
|
|
17
|
+
* - `trash` drops the schema and removes the directory. There is no `trash/` copy: the key
|
|
18
|
+
* is useless without the schema.
|
|
19
|
+
*/
|
|
20
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
21
|
+
import { tenantSchemaName } from "../store/postgres/names.js";
|
|
22
|
+
import { createPostgresTenantCatalog } from "../store/postgres/tenants.js";
|
|
23
|
+
import { tenantPaths } from "./paths.js";
|
|
24
|
+
import { QuarantineError, TenantConflictError } from "./quarantine.js";
|
|
25
|
+
import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
|
|
26
|
+
/** Repair instructions for a Postgres Tenant, naming its schema. */
|
|
27
|
+
function repairFor(code, tenantId) {
|
|
28
|
+
const status = `nylo tenant status ${tenantId}`;
|
|
29
|
+
const schema = tenantSchemaName(tenantId);
|
|
30
|
+
switch (code) {
|
|
31
|
+
case "schema-too-new":
|
|
32
|
+
return `${status} — schema ${schema} was migrated by a newer Runtime; run that version or newer`;
|
|
33
|
+
case "migration-failed":
|
|
34
|
+
return `${status} — inspect the Runtime log for the failed migration of ${schema}, fix it, then restart the Runtime`;
|
|
35
|
+
case "envelope-invalid":
|
|
36
|
+
return `${status} — the tenant table of schema ${schema} is missing or invalid; restore the schema from a Postgres backup`;
|
|
37
|
+
case "kek-missing":
|
|
38
|
+
return `${status} — restore vault-kek under the Tenant directory (ciphertext cannot be opened without it)`;
|
|
39
|
+
default:
|
|
40
|
+
return `${status} — inspect the Runtime log and Postgres, then restart the Runtime`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function unavailable(tenantId, cause) {
|
|
44
|
+
return cause instanceof TenantUnavailableError
|
|
45
|
+
? cause
|
|
46
|
+
: new TenantUnavailableError(tenantId, { cause });
|
|
47
|
+
}
|
|
48
|
+
function quarantined(reason, tenantId) {
|
|
49
|
+
return new QuarantineError({ ...reason, repair: repairFor(reason.code, tenantId) });
|
|
50
|
+
}
|
|
51
|
+
export function createPostgresTenantStore(options) {
|
|
52
|
+
const catalog = createPostgresTenantCatalog({
|
|
53
|
+
sql: options.sql,
|
|
54
|
+
...(options.migrations ? { migrations: options.migrations } : {}),
|
|
55
|
+
onError: (error) => options.logger?.error("post-commit step failed", {
|
|
56
|
+
message: error instanceof Error ? error.message : String(error),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
const paths = (id) => tenantPaths(options.hostRoot, id);
|
|
60
|
+
function makeDirectory(id) {
|
|
61
|
+
const p = paths(id);
|
|
62
|
+
mkdirSync(p.root, { recursive: true, mode: 0o700 });
|
|
63
|
+
for (const dir of [p.home, p.tmp, p.sandboxes, p.pluginData, p.logs])
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
async function readEnvelope(id) {
|
|
67
|
+
try {
|
|
68
|
+
return await catalog.readEnvelope(id);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof QuarantineError))
|
|
72
|
+
throw unavailable(id, error);
|
|
73
|
+
// Dropped since it was listed (deleted by another process).
|
|
74
|
+
if (!(await catalog.tenantExists(id).catch(() => true)))
|
|
75
|
+
throw new TenantNotFoundError(id);
|
|
76
|
+
throw quarantined(error.toQuarantine(), id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
enumerate: () => catalog.listTenantIds(),
|
|
81
|
+
readEnvelope,
|
|
82
|
+
async create(envelope, bootstrap) {
|
|
83
|
+
makeDirectory(envelope.id);
|
|
84
|
+
try {
|
|
85
|
+
const result = await catalog.createTenant({ envelope, principals: bootstrap });
|
|
86
|
+
return result.status;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
// An existing Tenant with other bootstrap material; the module reports the conflict.
|
|
90
|
+
if (error instanceof TenantConflictError)
|
|
91
|
+
return "exists";
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
bootstrapMatches: (id, bootstrap) => catalog.bootstrapMatches(id, bootstrap),
|
|
96
|
+
async open(id) {
|
|
97
|
+
const result = await catalog
|
|
98
|
+
.openTenant(id)
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
throw unavailable(id, error);
|
|
101
|
+
});
|
|
102
|
+
if (result.status === "not-found")
|
|
103
|
+
throw new TenantNotFoundError(id);
|
|
104
|
+
if (result.status === "quarantined")
|
|
105
|
+
throw quarantined(result.reason, id);
|
|
106
|
+
if (result.migrated.from !== result.migrated.to)
|
|
107
|
+
options.logger?.info("tenant schema migrated", {
|
|
108
|
+
tenantId: id,
|
|
109
|
+
from: result.migrated.from,
|
|
110
|
+
to: result.migrated.to,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
makeDirectory(id);
|
|
114
|
+
const config = options.configFor(id);
|
|
115
|
+
return await options.openRuntime({ ...config, tenantId: id }, { store: result.store, envelope: result.envelope });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
await result.store.close().catch(() => undefined);
|
|
119
|
+
// The Runtime's own quarantines (`kek-missing`) get this store's repair wording.
|
|
120
|
+
const reason = error?.quarantine;
|
|
121
|
+
if (reason)
|
|
122
|
+
throw quarantined(reason, id);
|
|
123
|
+
if (error instanceof QuarantineError)
|
|
124
|
+
throw error;
|
|
125
|
+
// Anything else failed outside the Tenant (Postgres, arming its sweep in Restate):
|
|
126
|
+
// the next use tries again rather than quarantining it.
|
|
127
|
+
throw unavailable(id, error);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
async trash(id) {
|
|
131
|
+
await catalog.deleteTenant(id);
|
|
132
|
+
rmSync(paths(id).root, { recursive: true, force: true });
|
|
133
|
+
},
|
|
134
|
+
async removePartial(id) {
|
|
135
|
+
if (await catalog.tenantExists(id))
|
|
136
|
+
return;
|
|
137
|
+
rmSync(paths(id).root, { recursive: true, force: true });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { SessionStore } from "../store/types.js";
|
|
2
|
+
import { type Relay } from "../streams/relay.js";
|
|
3
|
+
import { type DurableStreams } from "../streams/types.js";
|
|
4
|
+
import type { TenantContext } from "./context.js";
|
|
5
|
+
export interface WireStreamsOptions {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
store: SessionStore;
|
|
8
|
+
streams: DurableStreams;
|
|
9
|
+
/** Close `streams` with the wiring (streams created for this Tenant alone). */
|
|
10
|
+
ownsStreams?: boolean;
|
|
11
|
+
/** An existing relay to use instead of creating one; the caller closes it. */
|
|
12
|
+
relay?: Relay;
|
|
13
|
+
}
|
|
14
|
+
/** The state of the Tenant's basin as this process last saw it. */
|
|
15
|
+
export interface BasinStatus {
|
|
16
|
+
/** `ensureTenant` succeeded and no append has failed since. */
|
|
17
|
+
ready: boolean;
|
|
18
|
+
/** Failed `ensureTenant` attempts since the basin was last ready. */
|
|
19
|
+
failures: number;
|
|
20
|
+
/** The last failure's message, cleared once the basin is ready. */
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** What one `collectSessionStreams` pass did. */
|
|
24
|
+
export interface CollectResult {
|
|
25
|
+
/** Abandoned session streams deleted. */
|
|
26
|
+
deleted: number;
|
|
27
|
+
/** Session streams of existing sessions, kept. */
|
|
28
|
+
kept: number;
|
|
29
|
+
/** Deletions that failed; the collection stays pending. */
|
|
30
|
+
failed: number;
|
|
31
|
+
}
|
|
32
|
+
/** The handles `wireStreams` returns (also kept on `ctx.live.wiring`). */
|
|
33
|
+
export interface StreamsWiring {
|
|
34
|
+
readonly streams: DurableStreams;
|
|
35
|
+
readonly relay: Relay;
|
|
36
|
+
/** The basin's state (`streamsStatus`). */
|
|
37
|
+
basin(): BasinStatus;
|
|
38
|
+
/** Appends leftover outbox rows, at most `limit`; resolves with the number relayed. */
|
|
39
|
+
drain(limit?: number): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Deletes abandoned session streams when a collection is pending; undefined when none was.
|
|
42
|
+
* One pass at a time.
|
|
43
|
+
*/
|
|
44
|
+
collect(): Promise<CollectResult | undefined>;
|
|
45
|
+
/** Stops the readers, waits for in-flight relays, and closes what `wireStreams` created. */
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** The Tenant setting that holds a pending collection's token ("" when none is pending). */
|
|
49
|
+
export declare const COLLECT_SETTING = "streams.collect";
|
|
50
|
+
/**
|
|
51
|
+
* Wires the Tenant to Durable Streams: checks the basin, creates the relay (unless given),
|
|
52
|
+
* starts the work and control readers, and records the handles on `ctx.live.wiring`. Call it
|
|
53
|
+
* once, after `ctx` is built and before the Tenant serves requests.
|
|
54
|
+
*/
|
|
55
|
+
export declare function wireStreams(ctx: TenantContext, options: WireStreamsOptions): Promise<StreamsWiring>;
|
|
56
|
+
/** The Tenant sweep's outbox recovery: appends leftover rows, at most `limit`. */
|
|
57
|
+
export declare function drainOutbox(ctx: TenantContext, limit?: number): Promise<number>;
|
|
58
|
+
/**
|
|
59
|
+
* Appends `session.cancel` for `sessionId` to `tenant/control`, so the process running its
|
|
60
|
+
* advance aborts it. Call it from `t.afterCommit` after a cancel commits. A lost signal costs
|
|
61
|
+
* latency only (the advance checks the Session Store before every effect), so failures are
|
|
62
|
+
* logged, never thrown.
|
|
63
|
+
*/
|
|
64
|
+
export declare function signalSessionCancel(ctx: TenantContext, sessionId: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Before a reset that deletes sessions: records a pending collection of their streams in the
|
|
67
|
+
* Tenant settings, so it is retried by the sweep even if this process stops before it runs.
|
|
68
|
+
*/
|
|
69
|
+
export declare function requestStreamCollection(ctx: TenantContext): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* After a reset deleted sessions: tells every process with the Tenant open to check its
|
|
72
|
+
* session feeds (`sessions.reset`), and deletes the abandoned streams now, best effort.
|
|
73
|
+
* Failures are logged; the sweep retries the collection.
|
|
74
|
+
*/
|
|
75
|
+
export declare function sessionStreamsAbandoned(ctx: TenantContext): void;
|
|
76
|
+
/**
|
|
77
|
+
* Deletes the Tenant's session streams that no session uses any more: streams of deleted
|
|
78
|
+
* sessions and of earlier incarnations. Runs only while a collection is pending
|
|
79
|
+
* (`requestStreamCollection`), and clears it once every deletion succeeded, unless another
|
|
80
|
+
* reset requested a new one meanwhile.
|
|
81
|
+
*
|
|
82
|
+
* Streams are listed before their sessions are read, so a session created meanwhile keeps
|
|
83
|
+
* its stream: its row committed before the relay created the stream.
|
|
84
|
+
*/
|
|
85
|
+
export declare function collectSessionStreams(ctx: TenantContext, streams: DurableStreams): Promise<CollectResult | undefined>;
|
|
86
|
+
/** Tenant status of the streams seam (for `GET /v1/tenant` and the Admin API). */
|
|
87
|
+
export interface StreamsStatus {
|
|
88
|
+
/** The streams' service answered a probe (`probeStreams`) within the timeout. */
|
|
89
|
+
reachable: boolean;
|
|
90
|
+
basin: BasinStatus;
|
|
91
|
+
outbox: {
|
|
92
|
+
/** Events committed but not yet in their streams. */
|
|
93
|
+
depth: number;
|
|
94
|
+
/** Age of the oldest of them, or null when the outbox is empty. */
|
|
95
|
+
oldestAgeMs: number | null;
|
|
96
|
+
};
|
|
97
|
+
/** How far the relay is behind: the oldest unrelayed event's age, 0 when none is waiting. */
|
|
98
|
+
relayLagMs: number;
|
|
99
|
+
/** Abandoned session streams are waiting to be deleted. */
|
|
100
|
+
collectionPending: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The streams seam's status for an open Tenant: S2 reachability, the basin, outbox depth and
|
|
104
|
+
* relay lag. Reads the Session Store and probes the streams (at most `probeTimeoutMs`).
|
|
105
|
+
*/
|
|
106
|
+
export declare function streamsStatus(ctx: TenantContext, options?: {
|
|
107
|
+
probeTimeoutMs?: number;
|
|
108
|
+
now?: () => number;
|
|
109
|
+
}): Promise<StreamsStatus>;
|
|
110
|
+
/**
|
|
111
|
+
* Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
|
|
112
|
+
* retries transient failures, then throws. A failure need not fail the creation: opening the
|
|
113
|
+
* Tenant repairs a missing basin.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createTenantStreams(streams: DurableStreams, tenantId: string, options?: {
|
|
116
|
+
attempts?: number;
|
|
117
|
+
}): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Deletes a Tenant's basin and every stream in it. Call it when the Tenant is deleted (never
|
|
120
|
+
* on reset). Idempotent; retries transient failures, then throws.
|
|
121
|
+
*/
|
|
122
|
+
export declare function deleteTenantStreams(streams: DurableStreams, tenantId: string, options?: {
|
|
123
|
+
attempts?: number;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
/** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
|
|
126
|
+
export declare function closeStreams(ctx: TenantContext): Promise<void>;
|