@alchemy.run/effect-cluster 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +101 -4
- package/lib/AWS/Database.d.ts +54 -0
- package/lib/AWS/Database.d.ts.map +1 -0
- package/lib/AWS/Database.js +96 -0
- package/lib/AWS/Database.js.map +1 -0
- package/lib/AWS/EcsPool.d.ts +56 -0
- package/lib/AWS/EcsPool.d.ts.map +1 -0
- package/lib/AWS/EcsPool.js +87 -0
- package/lib/AWS/EcsPool.js.map +1 -0
- package/lib/AWS/index.d.ts +7 -0
- package/lib/AWS/index.d.ts.map +1 -0
- package/lib/AWS/index.js +7 -0
- package/lib/AWS/index.js.map +1 -0
- package/lib/AWS/internal/Gateway.d.ts +23 -0
- package/lib/AWS/internal/Gateway.d.ts.map +1 -0
- package/lib/AWS/internal/Gateway.js +70 -0
- package/lib/AWS/internal/Gateway.js.map +1 -0
- package/lib/Cloudflare/DurableObjectPool.d.ts +42 -0
- package/lib/Cloudflare/DurableObjectPool.d.ts.map +1 -0
- package/lib/Cloudflare/DurableObjectPool.js +136 -0
- package/lib/Cloudflare/DurableObjectPool.js.map +1 -0
- package/lib/Cloudflare/PoolWorker.d.ts +186 -0
- package/lib/Cloudflare/PoolWorker.d.ts.map +1 -0
- package/lib/Cloudflare/PoolWorker.js +75 -0
- package/lib/Cloudflare/PoolWorker.js.map +1 -0
- package/lib/Cloudflare/index.d.ts +7 -0
- package/lib/Cloudflare/index.d.ts.map +1 -0
- package/lib/Cloudflare/index.js +7 -0
- package/lib/Cloudflare/index.js.map +1 -0
- package/lib/Cloudflare/internal/Objects.d.ts +38 -0
- package/lib/Cloudflare/internal/Objects.d.ts.map +1 -0
- package/lib/Cloudflare/internal/Objects.js +102 -0
- package/lib/Cloudflare/internal/Objects.js.map +1 -0
- package/lib/Cron.d.ts +143 -0
- package/lib/Cron.d.ts.map +1 -0
- package/lib/Cron.js +83 -0
- package/lib/Cron.js.map +1 -0
- package/lib/Database.d.ts +95 -0
- package/lib/Database.d.ts.map +1 -0
- package/lib/Database.js +38 -0
- package/lib/Database.js.map +1 -0
- package/lib/Docker/Database.d.ts +42 -0
- package/lib/Docker/Database.d.ts.map +1 -0
- package/lib/Docker/Database.js +53 -0
- package/lib/Docker/Database.js.map +1 -0
- package/lib/Docker/SwarmPool.d.ts +44 -0
- package/lib/Docker/SwarmPool.d.ts.map +1 -0
- package/lib/Docker/SwarmPool.js +56 -0
- package/lib/Docker/SwarmPool.js.map +1 -0
- package/lib/Docker/index.d.ts +7 -0
- package/lib/Docker/index.d.ts.map +1 -0
- package/lib/Docker/index.js +7 -0
- package/lib/Docker/index.js.map +1 -0
- package/lib/Entity.d.ts +348 -0
- package/lib/Entity.d.ts.map +1 -0
- package/lib/Entity.js +308 -0
- package/lib/Entity.js.map +1 -0
- package/lib/Fly/Database.d.ts +50 -0
- package/lib/Fly/Database.d.ts.map +1 -0
- package/lib/Fly/Database.js +56 -0
- package/lib/Fly/Database.js.map +1 -0
- package/lib/Fly/MachinePool.d.ts +39 -0
- package/lib/Fly/MachinePool.d.ts.map +1 -0
- package/lib/Fly/MachinePool.js +51 -0
- package/lib/Fly/MachinePool.js.map +1 -0
- package/lib/Fly/index.d.ts +7 -0
- package/lib/Fly/index.d.ts.map +1 -0
- package/lib/Fly/index.js +7 -0
- package/lib/Fly/index.js.map +1 -0
- package/lib/GCP/ComputeEnginePool.d.ts +41 -0
- package/lib/GCP/ComputeEnginePool.d.ts.map +1 -0
- package/lib/GCP/ComputeEnginePool.js +172 -0
- package/lib/GCP/ComputeEnginePool.js.map +1 -0
- package/lib/GCP/Database.d.ts +48 -0
- package/lib/GCP/Database.d.ts.map +1 -0
- package/lib/GCP/Database.js +115 -0
- package/lib/GCP/Database.js.map +1 -0
- package/lib/GCP/index.d.ts +7 -0
- package/lib/GCP/index.d.ts.map +1 -0
- package/lib/GCP/index.js +7 -0
- package/lib/GCP/index.js.map +1 -0
- package/lib/Hetzner/Database.d.ts +48 -0
- package/lib/Hetzner/Database.d.ts.map +1 -0
- package/lib/Hetzner/Database.js +91 -0
- package/lib/Hetzner/Database.js.map +1 -0
- package/lib/Hetzner/ServerPool.d.ts +35 -0
- package/lib/Hetzner/ServerPool.d.ts.map +1 -0
- package/lib/Hetzner/ServerPool.js +98 -0
- package/lib/Hetzner/ServerPool.js.map +1 -0
- package/lib/Hetzner/index.d.ts +7 -0
- package/lib/Hetzner/index.d.ts.map +1 -0
- package/lib/Hetzner/index.js +7 -0
- package/lib/Hetzner/index.js.map +1 -0
- package/lib/Host.d.ts +60 -0
- package/lib/Host.d.ts.map +1 -0
- package/lib/Host.js +31 -0
- package/lib/Host.js.map +1 -0
- package/lib/Kubernetes/Database.d.ts +40 -0
- package/lib/Kubernetes/Database.d.ts.map +1 -0
- package/lib/Kubernetes/Database.js +56 -0
- package/lib/Kubernetes/Database.js.map +1 -0
- package/lib/Kubernetes/DeploymentPool.d.ts +54 -0
- package/lib/Kubernetes/DeploymentPool.d.ts.map +1 -0
- package/lib/Kubernetes/DeploymentPool.js +49 -0
- package/lib/Kubernetes/DeploymentPool.js.map +1 -0
- package/lib/Kubernetes/index.d.ts +7 -0
- package/lib/Kubernetes/index.d.ts.map +1 -0
- package/lib/Kubernetes/index.js +7 -0
- package/lib/Kubernetes/index.js.map +1 -0
- package/lib/Pool.d.ts +66 -0
- package/lib/Pool.d.ts.map +1 -0
- package/lib/Pool.js +47 -0
- package/lib/Pool.js.map +1 -0
- package/lib/Providers.d.ts +19 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +31 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ShardGroups.d.ts +24 -0
- package/lib/ShardGroups.d.ts.map +1 -0
- package/lib/ShardGroups.js +19 -0
- package/lib/ShardGroups.js.map +1 -0
- package/lib/Singleton.d.ts +110 -0
- package/lib/Singleton.d.ts.map +1 -0
- package/lib/Singleton.js +60 -0
- package/lib/Singleton.js.map +1 -0
- package/lib/Workflow.d.ts +251 -0
- package/lib/Workflow.d.ts.map +1 -0
- package/lib/Workflow.js +288 -0
- package/lib/Workflow.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Bootstrap.d.ts +28 -0
- package/lib/internal/Bootstrap.d.ts.map +1 -0
- package/lib/internal/Bootstrap.js +257 -0
- package/lib/internal/Bootstrap.js.map +1 -0
- package/lib/internal/EntityRuntime.d.ts +40 -0
- package/lib/internal/EntityRuntime.d.ts.map +1 -0
- package/lib/internal/EntityRuntime.js +698 -0
- package/lib/internal/EntityRuntime.js.map +1 -0
- package/lib/internal/Gateway.d.ts +82 -0
- package/lib/internal/Gateway.d.ts.map +1 -0
- package/lib/internal/Gateway.js +219 -0
- package/lib/internal/Gateway.js.map +1 -0
- package/lib/internal/HostedPlatform.d.ts +36 -0
- package/lib/internal/HostedPlatform.d.ts.map +1 -0
- package/lib/internal/HostedPlatform.js +36 -0
- package/lib/internal/HostedPlatform.js.map +1 -0
- package/lib/internal/HostedRuntime.d.ts +58 -0
- package/lib/internal/HostedRuntime.d.ts.map +1 -0
- package/lib/internal/HostedRuntime.js +72 -0
- package/lib/internal/HostedRuntime.js.map +1 -0
- package/lib/internal/Naming.d.ts +12 -0
- package/lib/internal/Naming.d.ts.map +1 -0
- package/lib/internal/Naming.js +40 -0
- package/lib/internal/Naming.js.map +1 -0
- package/lib/internal/ObjectSharding.d.ts +141 -0
- package/lib/internal/ObjectSharding.d.ts.map +1 -0
- package/lib/internal/ObjectSharding.js +337 -0
- package/lib/internal/ObjectSharding.js.map +1 -0
- package/lib/internal/PoolHost.d.ts +80 -0
- package/lib/internal/PoolHost.d.ts.map +1 -0
- package/lib/internal/PoolHost.js +203 -0
- package/lib/internal/PoolHost.js.map +1 -0
- package/lib/internal/PoolHostService.d.ts +25 -0
- package/lib/internal/PoolHostService.d.ts.map +1 -0
- package/lib/internal/PoolHostService.js +10 -0
- package/lib/internal/PoolHostService.js.map +1 -0
- package/lib/internal/Wire.d.ts +60 -0
- package/lib/internal/Wire.d.ts.map +1 -0
- package/lib/internal/Wire.js +36 -0
- package/lib/internal/Wire.js.map +1 -0
- package/lib/internal/WorkflowGateway.d.ts +36 -0
- package/lib/internal/WorkflowGateway.d.ts.map +1 -0
- package/lib/internal/WorkflowGateway.js +124 -0
- package/lib/internal/WorkflowGateway.js.map +1 -0
- package/lib/internal/external-runner.d.ts +2 -0
- package/lib/internal/external-runner.d.ts.map +1 -0
- package/lib/internal/external-runner.js +7 -0
- package/lib/internal/external-runner.js.map +1 -0
- package/lib/internal/runner.d.ts +2 -0
- package/lib/internal/runner.d.ts.map +1 -0
- package/lib/internal/runner.js +5 -0
- package/lib/internal/runner.js.map +1 -0
- package/package.json +128 -3
- package/src/AWS/Database.ts +139 -0
- package/src/AWS/EcsPool.ts +118 -0
- package/src/AWS/index.ts +6 -0
- package/src/AWS/internal/Gateway.ts +82 -0
- package/src/Cloudflare/DurableObjectPool.ts +198 -0
- package/src/Cloudflare/PoolWorker.ts +124 -0
- package/src/Cloudflare/index.ts +6 -0
- package/src/Cloudflare/internal/Objects.ts +160 -0
- package/src/Cron.ts +202 -0
- package/src/Database.ts +116 -0
- package/src/Docker/Database.ts +94 -0
- package/src/Docker/SwarmPool.ts +83 -0
- package/src/Docker/index.ts +6 -0
- package/src/Entity.ts +621 -0
- package/src/Fly/Database.ts +102 -0
- package/src/Fly/MachinePool.ts +72 -0
- package/src/Fly/index.ts +6 -0
- package/src/GCP/ComputeEnginePool.ts +203 -0
- package/src/GCP/Database.ts +168 -0
- package/src/GCP/index.ts +6 -0
- package/src/Hetzner/Database.ts +142 -0
- package/src/Hetzner/ServerPool.ts +116 -0
- package/src/Hetzner/index.ts +6 -0
- package/src/Host.ts +68 -0
- package/src/Kubernetes/Database.ts +89 -0
- package/src/Kubernetes/DeploymentPool.ts +77 -0
- package/src/Kubernetes/index.ts +6 -0
- package/src/Pool.ts +104 -0
- package/src/Providers.ts +49 -0
- package/src/ShardGroups.ts +41 -0
- package/src/Singleton.ts +153 -0
- package/src/Workflow.ts +556 -0
- package/src/index.ts +11 -0
- package/src/internal/Bootstrap.ts +368 -0
- package/src/internal/EntityRuntime.ts +996 -0
- package/src/internal/Gateway.ts +367 -0
- package/src/internal/HostedPlatform.ts +83 -0
- package/src/internal/HostedRuntime.ts +118 -0
- package/src/internal/Naming.ts +44 -0
- package/src/internal/ObjectSharding.ts +578 -0
- package/src/internal/PoolHost.ts +330 -0
- package/src/internal/PoolHostService.ts +25 -0
- package/src/internal/Wire.ts +83 -0
- package/src/internal/WorkflowGateway.ts +188 -0
- package/src/internal/external-runner.ts +10 -0
- package/src/internal/runner.ts +5 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A runner's program: build each entity class's Construction phase, then
|
|
3
|
+
* run every class's instances on Effect Cluster's HTTP runner, with runner
|
|
4
|
+
* registration, shard locks and persisted messages in the cluster's
|
|
5
|
+
* database, and serve the gateway callers reach the entities through.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { lookup } from "node:dns/promises";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { networkInterfaces } from "node:os";
|
|
13
|
+
import { NodeClusterHttp, NodeHttpServer, NodeServices } from "@effect/platform-node";
|
|
14
|
+
import { PgClient } from "@effect/sql-pg";
|
|
15
|
+
import { reifyBoundConfigProvider } from "alchemy/Runtime";
|
|
16
|
+
import {
|
|
17
|
+
entrypointLayer,
|
|
18
|
+
entrypointTag,
|
|
19
|
+
runProcess,
|
|
20
|
+
stackFromEnv,
|
|
21
|
+
} from "alchemy/Runtime/Bootstrap";
|
|
22
|
+
import {
|
|
23
|
+
ClusterCron,
|
|
24
|
+
ClusterSchema,
|
|
25
|
+
ClusterWorkflowEngine,
|
|
26
|
+
Entity as EffectEntity,
|
|
27
|
+
RunnerAddress,
|
|
28
|
+
Singleton,
|
|
29
|
+
} from "effect/cluster";
|
|
30
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
31
|
+
import * as Context from "effect/Context";
|
|
32
|
+
import * as Data from "effect/Data";
|
|
33
|
+
import * as Effect from "effect/Effect";
|
|
34
|
+
import * as FileSystem from "effect/FileSystem";
|
|
35
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
36
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
37
|
+
import * as Layer from "effect/Layer";
|
|
38
|
+
import * as Logger from "effect/Logger";
|
|
39
|
+
import * as Option from "effect/Option";
|
|
40
|
+
import * as Path from "effect/Path";
|
|
41
|
+
import * as Redacted from "effect/Redacted";
|
|
42
|
+
import * as Ref from "effect/Ref";
|
|
43
|
+
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
44
|
+
import type { Workflow as EffectWorkflow } from "effect/workflow";
|
|
45
|
+
import type { HostedCron } from "../Cron.ts";
|
|
46
|
+
import { PlatformDeclaration, type HostedEntity } from "../Entity.ts";
|
|
47
|
+
import { RunnerHost } from "../Host.ts";
|
|
48
|
+
import type { HostedSingleton } from "../Singleton.ts";
|
|
49
|
+
import type { HostedWorkflow } from "../Workflow.ts";
|
|
50
|
+
import { GATEWAY_PORT, RUNNER_PORT, gatewayLayer } from "./Gateway.ts";
|
|
51
|
+
import { HasNoPool, type HostedRuntimeContext } from "./HostedRuntime.ts";
|
|
52
|
+
|
|
53
|
+
const env = (name: string) => process.env[name];
|
|
54
|
+
|
|
55
|
+
/** Whether an IPv4 address is in a CIDR range (`10.46.0.0/16`). */
|
|
56
|
+
const inIpv4Range = (address: string, range: string) => {
|
|
57
|
+
const [base, bits] = range.split("/");
|
|
58
|
+
const toNumber = (ip: string) =>
|
|
59
|
+
ip.split(".").reduce((total, part) => total * 256 + Number(part), 0);
|
|
60
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(address) || base === undefined) return false;
|
|
61
|
+
const size = 2 ** (32 - Number(bits ?? 32));
|
|
62
|
+
return Math.floor(toNumber(address) / size) === Math.floor(toNumber(base) / size);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The address other runners reach this one at: an explicit
|
|
67
|
+
* `CLUSTER_RUNNER_HOST`, else the local address that
|
|
68
|
+
* `CLUSTER_PEERS` (a DNS name every runner resolves to) lists,
|
|
69
|
+
* else the first non-internal IPv4 address.
|
|
70
|
+
*/
|
|
71
|
+
const runnerHost = Effect.gen(function* () {
|
|
72
|
+
const explicit = env("CLUSTER_RUNNER_HOST");
|
|
73
|
+
if (explicit) return explicit;
|
|
74
|
+
const local = Object.values(networkInterfaces())
|
|
75
|
+
.flat()
|
|
76
|
+
.filter((iface) => iface !== undefined && !iface.internal)
|
|
77
|
+
.map((iface) => iface!.address)
|
|
78
|
+
// Link-local addresses (an agent's, a metadata endpoint's) aren't reachable.
|
|
79
|
+
.filter((address) => !address.startsWith("169.254.") && !address.startsWith("fe80:"));
|
|
80
|
+
// On a private network with a known range, the address in it.
|
|
81
|
+
const range = env("CLUSTER_RUNNER_NETWORK");
|
|
82
|
+
if (range) {
|
|
83
|
+
const inRange = local.find((address) => inIpv4Range(address, range));
|
|
84
|
+
if (inRange) return inRange;
|
|
85
|
+
}
|
|
86
|
+
// On Fly, the Machine's name on the App's private network.
|
|
87
|
+
const flyMachine = env("FLY_MACHINE_ID");
|
|
88
|
+
const flyApp = env("FLY_APP_NAME");
|
|
89
|
+
if (flyMachine && flyApp) return `${flyMachine}.vm.${flyApp}.internal`;
|
|
90
|
+
// On ECS, the task's own address in its VPC.
|
|
91
|
+
const ecsMetadata = env("ECS_CONTAINER_METADATA_URI_V4");
|
|
92
|
+
if (ecsMetadata) {
|
|
93
|
+
const address = yield* HttpClient.get(ecsMetadata).pipe(
|
|
94
|
+
Effect.flatMap((response) => response.json),
|
|
95
|
+
Effect.map(
|
|
96
|
+
(metadata) =>
|
|
97
|
+
(metadata as { Networks?: Array<{ IPv4Addresses?: string[] }> }).Networks?.[0]
|
|
98
|
+
?.IPv4Addresses?.[0],
|
|
99
|
+
),
|
|
100
|
+
Effect.orElseSucceed(() => undefined),
|
|
101
|
+
);
|
|
102
|
+
if (address) return address;
|
|
103
|
+
}
|
|
104
|
+
const peers = env("CLUSTER_PEERS");
|
|
105
|
+
if (peers) {
|
|
106
|
+
const resolved = yield* Effect.tryPromise(() => lookup(peers, { all: true })).pipe(
|
|
107
|
+
Effect.map((addresses) => addresses.map((address) => address.address)),
|
|
108
|
+
Effect.orElseSucceed((): string[] => []),
|
|
109
|
+
);
|
|
110
|
+
const mine = resolved.find((address) => local.includes(address));
|
|
111
|
+
if (mine) return mine;
|
|
112
|
+
}
|
|
113
|
+
return local.find((address) => !address.includes(":")) ?? "127.0.0.1";
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run an existing Effect Cluster program as it is, as its own process, with
|
|
118
|
+
* the gateway beside it. The program reads its settings the way Effect
|
|
119
|
+
* Cluster programs do, from its environment, when it loads: `HOST` and
|
|
120
|
+
* `PORT` for the runner's address, `ASSIGNED_SHARD_GROUPS` and
|
|
121
|
+
* `AVAILABLE_SHARD_GROUPS` (`ShardingConfig.layerFromEnv`), and
|
|
122
|
+
* `DATABASE_URL`. The gateway joins the cluster as a client and routes
|
|
123
|
+
* callers to the program's entities.
|
|
124
|
+
*/
|
|
125
|
+
export const runExternal = (
|
|
126
|
+
definitions: ReadonlyArray<{ readonly name: string; readonly value: unknown }>,
|
|
127
|
+
programSource: string,
|
|
128
|
+
): Promise<void> =>
|
|
129
|
+
runProcess(
|
|
130
|
+
"Effect Cluster runner",
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const host = yield* runnerHost;
|
|
133
|
+
const fs = yield* FileSystem.FileSystem;
|
|
134
|
+
const path = yield* Path.Path;
|
|
135
|
+
// The program ships inside the runner; write it out to run it.
|
|
136
|
+
const programDir = yield* fs.makeTempDirectoryScoped({ prefix: "cluster-program-" });
|
|
137
|
+
const programFile = path.join(programDir, "program.mjs");
|
|
138
|
+
yield* fs.writeFileString(programFile, programSource);
|
|
139
|
+
const programEnv = {
|
|
140
|
+
...process.env,
|
|
141
|
+
HOST: host,
|
|
142
|
+
PORT: String(RUNNER_PORT),
|
|
143
|
+
LISTEN_HOST: env("CLUSTER_LISTEN_HOST") ?? "0.0.0.0",
|
|
144
|
+
DATABASE_URL: env("DATABASE_URL") ?? env("CLUSTER_DATABASE_URL"),
|
|
145
|
+
ASSIGNED_SHARD_GROUPS: env("CLUSTER_SHARD_GROUPS"),
|
|
146
|
+
AVAILABLE_SHARD_GROUPS: env("CLUSTER_AVAILABLE_SHARD_GROUPS"),
|
|
147
|
+
};
|
|
148
|
+
yield* Effect.logInfo(`Effect Cluster runner ${host}:${RUNNER_PORT}: external program`);
|
|
149
|
+
// The program owns this runner: if it stops, so does the runner.
|
|
150
|
+
const child = yield* Effect.sync(() =>
|
|
151
|
+
spawn(process.execPath, [programFile], { env: programEnv, stdio: "inherit" }),
|
|
152
|
+
);
|
|
153
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => child.kill("SIGTERM")));
|
|
154
|
+
const exited = Effect.callback<never, ExternalProgramExited>((resume) => {
|
|
155
|
+
child.once("exit", (code) => resume(Effect.fail(new ExternalProgramExited({ code }))));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Each definition is the entity's schema (an `RpcGroup`) or an Effect
|
|
159
|
+
// Cluster entity, as the external declaration's `main` exports it.
|
|
160
|
+
const entities = definitions.map(({ name, value }) =>
|
|
161
|
+
EffectEntity.isEntity(value)
|
|
162
|
+
? (value as HostedEntity["entity"])
|
|
163
|
+
: EffectEntity.fromRpcGroup(name, value as RpcGroup.RpcGroup<Rpc.Any>),
|
|
164
|
+
);
|
|
165
|
+
const cluster = NodeClusterHttp.layer({
|
|
166
|
+
transport: "http",
|
|
167
|
+
storage: "sql",
|
|
168
|
+
clientOnly: true,
|
|
169
|
+
shardingConfig: shardGroupsConfig(),
|
|
170
|
+
}).pipe(Layer.provide(PgClient.layer({ url: Redacted.make(env("CLUSTER_DATABASE_URL")!) })));
|
|
171
|
+
const gateway = gatewayLayer(entities, env("CLUSTER_GATEWAY_TOKEN") ?? "").pipe(
|
|
172
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: GATEWAY_PORT })),
|
|
173
|
+
);
|
|
174
|
+
return yield* Effect.raceFirst(Layer.launch(gateway.pipe(Layer.provide(cluster))), exited);
|
|
175
|
+
}).pipe(
|
|
176
|
+
Effect.scoped,
|
|
177
|
+
Effect.provide(Layer.merge(NodeServices.layer, FetchHttpClient.layer)),
|
|
178
|
+
) as Effect.Effect<never, unknown, never>,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
/** The external program a runner runs stopped, so the runner stops too. */
|
|
182
|
+
export class ExternalProgramExited extends Data.TaggedError("Cluster.ExternalProgramExited")<{
|
|
183
|
+
readonly code: number | null;
|
|
184
|
+
}> {}
|
|
185
|
+
|
|
186
|
+
/** The shard groups this pool serves, and every group in the cluster. */
|
|
187
|
+
const shardGroupsConfig = () => {
|
|
188
|
+
const list = (name: string) =>
|
|
189
|
+
(env(name) ?? "")
|
|
190
|
+
.split(",")
|
|
191
|
+
.map((group) => group.trim())
|
|
192
|
+
.filter((group) => group.length > 0);
|
|
193
|
+
const assigned = list("CLUSTER_SHARD_GROUPS");
|
|
194
|
+
const available = list("CLUSTER_AVAILABLE_SHARD_GROUPS");
|
|
195
|
+
return {
|
|
196
|
+
...(assigned.length > 0 ? { assignedShardGroups: assigned } : {}),
|
|
197
|
+
...(available.length > 0 ? { availableShardGroups: available } : {}),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Build each class's Construction phase, then run the runner until it is stopped. */
|
|
202
|
+
export const bootstrap = (
|
|
203
|
+
definitions: ReadonlyArray<{ readonly name: string; readonly value: unknown }>,
|
|
204
|
+
): Promise<void> => {
|
|
205
|
+
const entrypoints = definitions.map((definition) => definition.value);
|
|
206
|
+
const platform = Layer.mergeAll(
|
|
207
|
+
NodeServices.layer,
|
|
208
|
+
FetchHttpClient.layer,
|
|
209
|
+
Logger.layer([Logger.consolePretty()]),
|
|
210
|
+
).pipe(
|
|
211
|
+
Layer.provideMerge(stackFromEnv),
|
|
212
|
+
Layer.provideMerge(
|
|
213
|
+
Layer.succeed(
|
|
214
|
+
ConfigProvider.ConfigProvider,
|
|
215
|
+
reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const program = Effect.gen(function* () {
|
|
221
|
+
type Hosted = HostedEntity | HostedWorkflow | HostedSingleton | HostedCron;
|
|
222
|
+
// Every class this runner hosts, and the Layers to launch beside them:
|
|
223
|
+
// its exported classes, and the entities Layers host themselves.
|
|
224
|
+
const hostedRef = yield* Ref.make<ReadonlyArray<Hosted>>([]);
|
|
225
|
+
const launchedRef = yield* Ref.make<ReadonlyArray<Layer.Layer<never, never, any>>>([]);
|
|
226
|
+
const add = (built: Hosted) =>
|
|
227
|
+
Ref.update(hostedRef, (all) =>
|
|
228
|
+
built.kind === "entity" &&
|
|
229
|
+
all.some((other) => other.kind === "entity" && other.entity.type === built.entity.type)
|
|
230
|
+
? all
|
|
231
|
+
: [...all, built],
|
|
232
|
+
);
|
|
233
|
+
const runner = RunnerHost.of({
|
|
234
|
+
host: (entity, layer) =>
|
|
235
|
+
Effect.andThen(
|
|
236
|
+
add(entity),
|
|
237
|
+
Ref.update(launchedRef, (all) => [...all, layer]),
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
for (const entrypoint of entrypoints) {
|
|
241
|
+
const declaration =
|
|
242
|
+
(entrypoint as { [PlatformDeclaration]?: unknown } | undefined)?.[PlatformDeclaration] ??
|
|
243
|
+
entrypoint;
|
|
244
|
+
const context = yield* Layer.build(entrypointLayer(declaration)).pipe(
|
|
245
|
+
Effect.provideService(RunnerHost, runner),
|
|
246
|
+
);
|
|
247
|
+
// A Layer that hosts its entities itself (a Domain model's) provides
|
|
248
|
+
// none: it handed them to `RunnerHost`.
|
|
249
|
+
const self = Context.getOption(context, entrypointTag);
|
|
250
|
+
if (Option.isNone(self)) continue;
|
|
251
|
+
const runtime = self.value.RuntimeContext as HostedRuntimeContext<Hosted>;
|
|
252
|
+
const built = runtime.hosted();
|
|
253
|
+
if (built === undefined) {
|
|
254
|
+
return yield* Effect.die(
|
|
255
|
+
new HasNoPool({
|
|
256
|
+
message: `${runtime.Type} '${runtime.id}' did not finish its Construction phase.`,
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
yield* add(built);
|
|
261
|
+
}
|
|
262
|
+
const hosted = yield* Ref.get(hostedRef);
|
|
263
|
+
const launched = yield* Ref.get(launchedRef);
|
|
264
|
+
|
|
265
|
+
const host = yield* runnerHost;
|
|
266
|
+
const group = env("CLUSTER_SHARD_GROUPS") ?? "default";
|
|
267
|
+
// Every hosted entity lives in this pool's own shard group.
|
|
268
|
+
const placed = hosted
|
|
269
|
+
.filter((entry): entry is HostedEntity => entry.kind === "entity")
|
|
270
|
+
.map((entry) => ({
|
|
271
|
+
...entry,
|
|
272
|
+
entity: entry.entity.annotate(ClusterSchema.ShardGroup, () => group),
|
|
273
|
+
}));
|
|
274
|
+
const workflows = hosted.filter((entry): entry is HostedWorkflow => entry.kind === "workflow");
|
|
275
|
+
// Each workflow runs its handler with the services its Construction phase had.
|
|
276
|
+
const workflowLayers = workflows.map((entry) =>
|
|
277
|
+
(entry.workflow as EffectWorkflow.Workflow<string, any, any, any>).toLayer(
|
|
278
|
+
(payload, executionId) =>
|
|
279
|
+
Effect.provide(entry.execute(payload, executionId), entry.context) as Effect.Effect<
|
|
280
|
+
any,
|
|
281
|
+
any,
|
|
282
|
+
never
|
|
283
|
+
>,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
const entityLayers = placed.map((entry) =>
|
|
287
|
+
entry.entity
|
|
288
|
+
.toLayer(
|
|
289
|
+
// The instance phase runs with the services its Construction phase had.
|
|
290
|
+
Effect.provide(entry.instance, entry.context) as Effect.Effect<any, never, any>,
|
|
291
|
+
{
|
|
292
|
+
maxIdleTime: entry.options.maxIdleTime,
|
|
293
|
+
concurrency: entry.options.concurrency,
|
|
294
|
+
mailboxCapacity: entry.options.mailboxCapacity,
|
|
295
|
+
disableFatalDefects: entry.options.disableFatalDefects,
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
// So does the entity itself: its RPCs' middleware is provided to its Layer.
|
|
299
|
+
.pipe(Layer.provide(Layer.succeedContext(entry.context))),
|
|
300
|
+
);
|
|
301
|
+
const cluster = NodeClusterHttp.layer({
|
|
302
|
+
transport: "http",
|
|
303
|
+
storage: "sql",
|
|
304
|
+
shardingConfig: {
|
|
305
|
+
runnerAddress: Option.some(RunnerAddress.make(host, RUNNER_PORT)),
|
|
306
|
+
// Fly's private network is IPv6 only.
|
|
307
|
+
runnerListenAddress: Option.some(
|
|
308
|
+
RunnerAddress.make(env("CLUSTER_LISTEN_HOST") ?? "0.0.0.0", RUNNER_PORT),
|
|
309
|
+
),
|
|
310
|
+
...shardGroupsConfig(),
|
|
311
|
+
},
|
|
312
|
+
}).pipe(Layer.provide(PgClient.layer({ url: Redacted.make(env("CLUSTER_DATABASE_URL")!) })));
|
|
313
|
+
// Singletons and crons run in this pool's shard group too, each with
|
|
314
|
+
// the services its Construction phase had.
|
|
315
|
+
const scheduled = hosted.flatMap((entry) =>
|
|
316
|
+
entry.kind === "singleton"
|
|
317
|
+
? [
|
|
318
|
+
Singleton.make(entry.name, Effect.provide(entry.run, entry.context), {
|
|
319
|
+
shardGroup: group,
|
|
320
|
+
}),
|
|
321
|
+
]
|
|
322
|
+
: entry.kind === "cron"
|
|
323
|
+
? [
|
|
324
|
+
ClusterCron.make({
|
|
325
|
+
name: entry.name,
|
|
326
|
+
cron: entry.cron,
|
|
327
|
+
execute: Effect.provide(entry.run, entry.context),
|
|
328
|
+
shardGroup: group,
|
|
329
|
+
...entry.options,
|
|
330
|
+
}),
|
|
331
|
+
]
|
|
332
|
+
: [],
|
|
333
|
+
);
|
|
334
|
+
const gateway = gatewayLayer(
|
|
335
|
+
placed.map((entry) => entry.entity),
|
|
336
|
+
env("CLUSTER_GATEWAY_TOKEN") ?? "",
|
|
337
|
+
new Map(workflows.map((entry) => [entry.workflow._tag, entry.workflow])),
|
|
338
|
+
).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port: GATEWAY_PORT })));
|
|
339
|
+
|
|
340
|
+
yield* Effect.logInfo(
|
|
341
|
+
`Effect Cluster runner ${host}:${RUNNER_PORT}: ${hosted
|
|
342
|
+
.map((entry) =>
|
|
343
|
+
entry.kind === "entity"
|
|
344
|
+
? entry.entity.type
|
|
345
|
+
: entry.kind === "workflow"
|
|
346
|
+
? entry.workflow._tag
|
|
347
|
+
: `${entry.kind} ${entry.name}`,
|
|
348
|
+
)
|
|
349
|
+
.join(", ")}`,
|
|
350
|
+
);
|
|
351
|
+
return yield* Layer.launch(
|
|
352
|
+
Layer.mergeAll(
|
|
353
|
+
...(entityLayers as [Layer.Layer<never>]),
|
|
354
|
+
...(workflowLayers as Layer.Layer<never, never, any>[]),
|
|
355
|
+
...(scheduled as Layer.Layer<never, never, any>[]),
|
|
356
|
+
// What the classes' Construction phases handed the runner.
|
|
357
|
+
...launched,
|
|
358
|
+
gateway,
|
|
359
|
+
).pipe(
|
|
360
|
+
// Workflows run on Effect Cluster's own engine, over the cluster.
|
|
361
|
+
Layer.provide(ClusterWorkflowEngine.layer),
|
|
362
|
+
Layer.provide(cluster),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}).pipe(Effect.provide(platform), Effect.scoped);
|
|
366
|
+
|
|
367
|
+
return runProcess("Effect Cluster runner", program as Effect.Effect<never, unknown, never>);
|
|
368
|
+
};
|