@alchemy.run/effect-cluster 0.0.0-stage → 2.0.0-beta.86
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 +105 -2
- 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 +218 -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 +143 -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 +366 -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
package/src/Database.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Input } from "alchemy/Input";
|
|
2
|
+
import * as Config from "effect/Config";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import type { Providers } from "./Providers.ts";
|
|
8
|
+
import { ShardGroups } from "./ShardGroups.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The private network runners must join to reach the database and each
|
|
12
|
+
* other. A pool Layer matches on `kind` and refuses a network it cannot
|
|
13
|
+
* join.
|
|
14
|
+
*/
|
|
15
|
+
export type ClusterNetwork =
|
|
16
|
+
| {
|
|
17
|
+
/** A Docker network; runner containers join it by name. */
|
|
18
|
+
readonly kind: "docker";
|
|
19
|
+
readonly name: Input<string>;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
/** An AWS VPC; runners run in its subnets, in the pool's security group. */
|
|
23
|
+
readonly kind: "aws";
|
|
24
|
+
readonly vpcId: Input<string>;
|
|
25
|
+
readonly subnetIds: Input<string>[];
|
|
26
|
+
/** The group every runner joins: it reaches the database and the other runners. */
|
|
27
|
+
readonly securityGroupId: Input<string>;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
/** A Fly private network; runners' Apps join it by name. */
|
|
31
|
+
readonly kind: "fly";
|
|
32
|
+
readonly network: Input<string>;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
/** A Hetzner private Network; runners' Servers attach to it. */
|
|
36
|
+
readonly kind: "hetzner";
|
|
37
|
+
readonly networkId: Input<number>;
|
|
38
|
+
/** Its IPv4 range, where each runner finds its own address. */
|
|
39
|
+
readonly ipRange: string;
|
|
40
|
+
readonly location: string;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
/** A GCP VPC; runners' instances join its subnetwork. */
|
|
44
|
+
readonly kind: "gcp";
|
|
45
|
+
readonly network: Input<string>;
|
|
46
|
+
readonly subnetwork: Input<string>;
|
|
47
|
+
readonly region: string;
|
|
48
|
+
/** The subnetwork's IPv4 range, where each runner finds its own address. */
|
|
49
|
+
readonly cidr: string;
|
|
50
|
+
/** The network tag every runner carries: it reaches the database and the other runners. */
|
|
51
|
+
readonly tag: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
/** A Kubernetes cluster; runners run as a Deployment in its namespace. */
|
|
55
|
+
readonly kind: "kubernetes";
|
|
56
|
+
/** The cluster, as `@alchemy.run/kubernetes` takes it. */
|
|
57
|
+
readonly cluster: unknown;
|
|
58
|
+
readonly namespace: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** What every database Layer gives the pool it is provided to. */
|
|
62
|
+
export interface DatabaseShape {
|
|
63
|
+
/**
|
|
64
|
+
* The Postgres connection URL runners use for runner registration,
|
|
65
|
+
* shard locks and persisted messages.
|
|
66
|
+
*/
|
|
67
|
+
readonly url: Input<Redacted.Redacted<string>>;
|
|
68
|
+
/** The network runners join to reach it, when it is private. */
|
|
69
|
+
readonly network?: ClusterNetwork;
|
|
70
|
+
/** Every shard group in the cluster, which each pool registers with. */
|
|
71
|
+
readonly shardGroups: ShardGroups;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The database an Effect Cluster coordinates through: runners register in
|
|
76
|
+
* it, lock their shards in it, and keep persisted messages in it. Each
|
|
77
|
+
* host's `Database` Layer provides it.
|
|
78
|
+
*/
|
|
79
|
+
export class Database extends Context.Service<Database, DatabaseShape>()("Cluster.Database") {}
|
|
80
|
+
|
|
81
|
+
export interface DatabaseProps {
|
|
82
|
+
/** A Postgres connection URL. */
|
|
83
|
+
url: Redacted.Redacted<string> | Config.Config<Redacted.Redacted<string>>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A Postgres database you already run, as an Effect Cluster `Database`
|
|
88
|
+
* Layer: Neon, PlanetScale, RDS, or any Postgres the pool can reach.
|
|
89
|
+
*
|
|
90
|
+
* ### Using an existing database
|
|
91
|
+
* **Example:** A Postgres URL from config
|
|
92
|
+
* ```typescript
|
|
93
|
+
* export const Database = Cluster.Database.fromUrl({
|
|
94
|
+
* url: Config.Redacted("CLUSTER_DATABASE_URL"),
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @primitive
|
|
99
|
+
*/
|
|
100
|
+
export const fromUrl = (
|
|
101
|
+
props: DatabaseProps,
|
|
102
|
+
): Layer.Layer<Database, Config.ConfigError, Providers> =>
|
|
103
|
+
Layer.effect(
|
|
104
|
+
Database,
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
if (globalThis.__ALCHEMY_RUNTIME__) {
|
|
107
|
+
// Runners read their database from their own environment at runtime.
|
|
108
|
+
return { url: Redacted.make(""), shardGroups: undefined as never };
|
|
109
|
+
}
|
|
110
|
+
const url = Config.isConfig(props.url) ? yield* props.url : props.url;
|
|
111
|
+
return { url, shardGroups: yield* clusterShardGroups("Database") };
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/** The shard groups resource every database Layer declares for its cluster. @internal */
|
|
116
|
+
export const clusterShardGroups = (id: string) => ShardGroups(`${id}ShardGroups`, {});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as Docker from "@alchemy.run/docker";
|
|
2
|
+
import * as Output from "alchemy/Output";
|
|
3
|
+
import { Random } from "alchemy/Random";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import { clusterShardGroups, Database as ClusterDatabase } from "../Database.ts";
|
|
8
|
+
import type { Providers } from "../Providers.ts";
|
|
9
|
+
|
|
10
|
+
/** The Postgres release `ClusterDocker.Database` runs by default. */
|
|
11
|
+
export const DEFAULT_POSTGRES_IMAGE = "postgres:17-alpine";
|
|
12
|
+
|
|
13
|
+
export interface DatabaseProps {
|
|
14
|
+
/**
|
|
15
|
+
* Postgres image.
|
|
16
|
+
* @default "postgres:17-alpine"
|
|
17
|
+
*/
|
|
18
|
+
image?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Publish Postgres on this host port, to inspect it from your machine.
|
|
21
|
+
* @default not published
|
|
22
|
+
*/
|
|
23
|
+
port?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The Docker engine to run on: a context name, a `Docker.Context`, or a
|
|
26
|
+
* `Docker.Swarm`.
|
|
27
|
+
* @default the current Docker context
|
|
28
|
+
*/
|
|
29
|
+
context?: Docker.Docker.EngineRef;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The database an Effect Cluster coordinates through, on a Docker swarm, as
|
|
34
|
+
* an `Cluster.Database` Layer: Postgres as a one-replica swarm service
|
|
35
|
+
* with its data on a named volume, on an overlay network the pool's runners join.
|
|
36
|
+
* The Docker engine must be in swarm mode (`docker swarm init`).
|
|
37
|
+
*
|
|
38
|
+
* One Postgres on one disk is meant for development and tests.
|
|
39
|
+
*
|
|
40
|
+
* ### Running a local database
|
|
41
|
+
* **Example:** Postgres for local entities
|
|
42
|
+
* ```typescript
|
|
43
|
+
* export const Database = ClusterDocker.Database("Cluster");
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @primitive
|
|
47
|
+
*/
|
|
48
|
+
export const Database = (
|
|
49
|
+
id: string,
|
|
50
|
+
props: DatabaseProps = {},
|
|
51
|
+
): Layer.Layer<ClusterDatabase, never, Providers | Docker.Providers> =>
|
|
52
|
+
Layer.effect(
|
|
53
|
+
ClusterDatabase,
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
if (globalThis.__ALCHEMY_RUNTIME__) {
|
|
56
|
+
// Runners read their database from their own environment at runtime.
|
|
57
|
+
return { url: Redacted.make(""), shardGroups: undefined as never };
|
|
58
|
+
}
|
|
59
|
+
const context = props.context === undefined ? {} : { context: props.context };
|
|
60
|
+
const password = yield* Random(`${id}Password`, { bytes: 24 }) as unknown as Effect.Effect<
|
|
61
|
+
Random,
|
|
62
|
+
never,
|
|
63
|
+
Providers
|
|
64
|
+
>;
|
|
65
|
+
const network = yield* Docker.Network(`${id}Network`, { driver: "overlay", ...context });
|
|
66
|
+
const volumeContext = Docker.dockerEngineContextName(props.context);
|
|
67
|
+
const data = yield* Docker.Volume(
|
|
68
|
+
`${id}Data`,
|
|
69
|
+
volumeContext === undefined ? {} : { context: volumeContext },
|
|
70
|
+
);
|
|
71
|
+
const alias = "cluster-db";
|
|
72
|
+
const db = yield* Docker.Service(`${id}Postgres`, {
|
|
73
|
+
image: props.image ?? DEFAULT_POSTGRES_IMAGE,
|
|
74
|
+
...context,
|
|
75
|
+
environment: { POSTGRES_PASSWORD: password.text, POSTGRES_DB: "cluster" },
|
|
76
|
+
ports: props.port === undefined ? [] : [{ external: props.port, internal: 5432 }],
|
|
77
|
+
volumes: [{ hostPath: data.name, containerPath: "/var/lib/postgresql/data" }],
|
|
78
|
+
networks: [{ name: network.name, aliases: [alias] }],
|
|
79
|
+
replicas: 1,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
// Ordered after the service: runners connect once it exists.
|
|
83
|
+
url: Output.all(db.name, password.text).pipe(
|
|
84
|
+
Output.map(([, text]) =>
|
|
85
|
+
Redacted.make(
|
|
86
|
+
`postgres://postgres:${encodeURIComponent(text.pipe(Redacted.value))}@${alias}:5432/cluster`,
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
network: { kind: "docker" as const, name: network.name },
|
|
91
|
+
shardGroups: yield* clusterShardGroups(id),
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as Docker from "@alchemy.run/docker";
|
|
2
|
+
import * as Output from "alchemy/Output";
|
|
3
|
+
import type { Stack } from "alchemy/Stack";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import type * as Layer from "effect/Layer";
|
|
6
|
+
import type { Database } from "../Database.ts";
|
|
7
|
+
import { GATEWAY_PORT } from "../internal/Gateway.ts";
|
|
8
|
+
import { makePoolLayer, type PoolSettings } from "../internal/PoolHost.ts";
|
|
9
|
+
import type { PoolHost } from "../internal/PoolHostService.ts";
|
|
10
|
+
import type { Providers } from "../Providers.ts";
|
|
11
|
+
|
|
12
|
+
export interface SwarmPoolOptions extends PoolSettings {
|
|
13
|
+
/**
|
|
14
|
+
* Number of runners.
|
|
15
|
+
* @default 1
|
|
16
|
+
*/
|
|
17
|
+
replicas?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Publish the gateway on this host port, so code outside the swarm (a
|
|
20
|
+
* test, a script) can call the entities at `http://localhost:<port>`.
|
|
21
|
+
* @default not published
|
|
22
|
+
*/
|
|
23
|
+
gatewayPort?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run a pool of Effect Cluster runners on a Docker swarm: a swarm service whose
|
|
28
|
+
* tasks host every `Cluster.Entity` given this pool, on the
|
|
29
|
+
* database's overlay network. Runners find each other through the
|
|
30
|
+
* database and talk on the network; the gateway is served on the same
|
|
31
|
+
* network. Requires an `Cluster.Database` on a Docker network —
|
|
32
|
+
* `ClusterDocker.Database`.
|
|
33
|
+
*
|
|
34
|
+
* Every entity given the same pool shares it: declare the pool once
|
|
35
|
+
* and provide them to each class.
|
|
36
|
+
*
|
|
37
|
+
* ### Running entities on Docker
|
|
38
|
+
* **Example:** Two runners on the local swarm
|
|
39
|
+
* ```typescript
|
|
40
|
+
* export const Database = ClusterDocker.Database("Cluster");
|
|
41
|
+
* export const Default = ClusterDocker.SwarmPool("Default", { replicas: 2 }).pipe(
|
|
42
|
+
* Layer.provide(Database),
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @primitive
|
|
47
|
+
*/
|
|
48
|
+
export const SwarmPool = (
|
|
49
|
+
id: string,
|
|
50
|
+
options: SwarmPoolOptions = {},
|
|
51
|
+
): Layer.Layer<PoolHost, never, Database | Stack | Providers | Docker.Providers> =>
|
|
52
|
+
makePoolLayer({
|
|
53
|
+
label: "ClusterDocker.SwarmPool",
|
|
54
|
+
id,
|
|
55
|
+
settings: options,
|
|
56
|
+
expected: "a Docker network (ClusterDocker.Database)",
|
|
57
|
+
network: (database) => (database.network?.kind === "docker" ? database.network : undefined),
|
|
58
|
+
// Callers on the swarm reach the gateway by the service's name.
|
|
59
|
+
gateway: (name) => Effect.succeed({ url: `http://${name}:${GATEWAY_PORT}` }),
|
|
60
|
+
nodes: ({ network, name, program, environment }) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
// Tagged with the program's hash, so a changed program is a new image.
|
|
63
|
+
const image = yield* Docker.Image(`${id}RunnerImage`, {
|
|
64
|
+
build: { context: program.pipe(Output.map((p) => p.dir)) as unknown as string },
|
|
65
|
+
tag: program.pipe(Output.map((p) => p.hash)) as unknown as string,
|
|
66
|
+
skipPush: true,
|
|
67
|
+
});
|
|
68
|
+
yield* Docker.Service(`${id}Pool`, {
|
|
69
|
+
name,
|
|
70
|
+
image: image.imageRef,
|
|
71
|
+
replicas: options.replicas ?? 1,
|
|
72
|
+
networks: [{ name: network.name }],
|
|
73
|
+
ports:
|
|
74
|
+
options.gatewayPort === undefined
|
|
75
|
+
? []
|
|
76
|
+
: [{ external: options.gatewayPort, internal: GATEWAY_PORT }],
|
|
77
|
+
// Each runner finds its own address among the service's tasks.
|
|
78
|
+
environment: environment.pipe(
|
|
79
|
+
Output.map((env) => ({ ...env, CLUSTER_PEERS: `tasks.${name}` })),
|
|
80
|
+
) as unknown as Record<string, string>,
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
});
|