@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.
Files changed (233) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +105 -2
  3. package/lib/AWS/Database.d.ts +54 -0
  4. package/lib/AWS/Database.d.ts.map +1 -0
  5. package/lib/AWS/Database.js +96 -0
  6. package/lib/AWS/Database.js.map +1 -0
  7. package/lib/AWS/EcsPool.d.ts +56 -0
  8. package/lib/AWS/EcsPool.d.ts.map +1 -0
  9. package/lib/AWS/EcsPool.js +87 -0
  10. package/lib/AWS/EcsPool.js.map +1 -0
  11. package/lib/AWS/index.d.ts +7 -0
  12. package/lib/AWS/index.d.ts.map +1 -0
  13. package/lib/AWS/index.js +7 -0
  14. package/lib/AWS/index.js.map +1 -0
  15. package/lib/AWS/internal/Gateway.d.ts +23 -0
  16. package/lib/AWS/internal/Gateway.d.ts.map +1 -0
  17. package/lib/AWS/internal/Gateway.js +70 -0
  18. package/lib/AWS/internal/Gateway.js.map +1 -0
  19. package/lib/Cloudflare/DurableObjectPool.d.ts +42 -0
  20. package/lib/Cloudflare/DurableObjectPool.d.ts.map +1 -0
  21. package/lib/Cloudflare/DurableObjectPool.js +136 -0
  22. package/lib/Cloudflare/DurableObjectPool.js.map +1 -0
  23. package/lib/Cloudflare/PoolWorker.d.ts +186 -0
  24. package/lib/Cloudflare/PoolWorker.d.ts.map +1 -0
  25. package/lib/Cloudflare/PoolWorker.js +75 -0
  26. package/lib/Cloudflare/PoolWorker.js.map +1 -0
  27. package/lib/Cloudflare/index.d.ts +7 -0
  28. package/lib/Cloudflare/index.d.ts.map +1 -0
  29. package/lib/Cloudflare/index.js +7 -0
  30. package/lib/Cloudflare/index.js.map +1 -0
  31. package/lib/Cloudflare/internal/Objects.d.ts +38 -0
  32. package/lib/Cloudflare/internal/Objects.d.ts.map +1 -0
  33. package/lib/Cloudflare/internal/Objects.js +102 -0
  34. package/lib/Cloudflare/internal/Objects.js.map +1 -0
  35. package/lib/Cron.d.ts +143 -0
  36. package/lib/Cron.d.ts.map +1 -0
  37. package/lib/Cron.js +83 -0
  38. package/lib/Cron.js.map +1 -0
  39. package/lib/Database.d.ts +95 -0
  40. package/lib/Database.d.ts.map +1 -0
  41. package/lib/Database.js +38 -0
  42. package/lib/Database.js.map +1 -0
  43. package/lib/Docker/Database.d.ts +42 -0
  44. package/lib/Docker/Database.d.ts.map +1 -0
  45. package/lib/Docker/Database.js +53 -0
  46. package/lib/Docker/Database.js.map +1 -0
  47. package/lib/Docker/SwarmPool.d.ts +44 -0
  48. package/lib/Docker/SwarmPool.d.ts.map +1 -0
  49. package/lib/Docker/SwarmPool.js +56 -0
  50. package/lib/Docker/SwarmPool.js.map +1 -0
  51. package/lib/Docker/index.d.ts +7 -0
  52. package/lib/Docker/index.d.ts.map +1 -0
  53. package/lib/Docker/index.js +7 -0
  54. package/lib/Docker/index.js.map +1 -0
  55. package/lib/Entity.d.ts +348 -0
  56. package/lib/Entity.d.ts.map +1 -0
  57. package/lib/Entity.js +308 -0
  58. package/lib/Entity.js.map +1 -0
  59. package/lib/Fly/Database.d.ts +50 -0
  60. package/lib/Fly/Database.d.ts.map +1 -0
  61. package/lib/Fly/Database.js +56 -0
  62. package/lib/Fly/Database.js.map +1 -0
  63. package/lib/Fly/MachinePool.d.ts +39 -0
  64. package/lib/Fly/MachinePool.d.ts.map +1 -0
  65. package/lib/Fly/MachinePool.js +51 -0
  66. package/lib/Fly/MachinePool.js.map +1 -0
  67. package/lib/Fly/index.d.ts +7 -0
  68. package/lib/Fly/index.d.ts.map +1 -0
  69. package/lib/Fly/index.js +7 -0
  70. package/lib/Fly/index.js.map +1 -0
  71. package/lib/GCP/ComputeEnginePool.d.ts +41 -0
  72. package/lib/GCP/ComputeEnginePool.d.ts.map +1 -0
  73. package/lib/GCP/ComputeEnginePool.js +172 -0
  74. package/lib/GCP/ComputeEnginePool.js.map +1 -0
  75. package/lib/GCP/Database.d.ts +48 -0
  76. package/lib/GCP/Database.d.ts.map +1 -0
  77. package/lib/GCP/Database.js +115 -0
  78. package/lib/GCP/Database.js.map +1 -0
  79. package/lib/GCP/index.d.ts +7 -0
  80. package/lib/GCP/index.d.ts.map +1 -0
  81. package/lib/GCP/index.js +7 -0
  82. package/lib/GCP/index.js.map +1 -0
  83. package/lib/Hetzner/Database.d.ts +48 -0
  84. package/lib/Hetzner/Database.d.ts.map +1 -0
  85. package/lib/Hetzner/Database.js +91 -0
  86. package/lib/Hetzner/Database.js.map +1 -0
  87. package/lib/Hetzner/ServerPool.d.ts +35 -0
  88. package/lib/Hetzner/ServerPool.d.ts.map +1 -0
  89. package/lib/Hetzner/ServerPool.js +98 -0
  90. package/lib/Hetzner/ServerPool.js.map +1 -0
  91. package/lib/Hetzner/index.d.ts +7 -0
  92. package/lib/Hetzner/index.d.ts.map +1 -0
  93. package/lib/Hetzner/index.js +7 -0
  94. package/lib/Hetzner/index.js.map +1 -0
  95. package/lib/Host.d.ts +60 -0
  96. package/lib/Host.d.ts.map +1 -0
  97. package/lib/Host.js +31 -0
  98. package/lib/Host.js.map +1 -0
  99. package/lib/Kubernetes/Database.d.ts +40 -0
  100. package/lib/Kubernetes/Database.d.ts.map +1 -0
  101. package/lib/Kubernetes/Database.js +56 -0
  102. package/lib/Kubernetes/Database.js.map +1 -0
  103. package/lib/Kubernetes/DeploymentPool.d.ts +54 -0
  104. package/lib/Kubernetes/DeploymentPool.d.ts.map +1 -0
  105. package/lib/Kubernetes/DeploymentPool.js +49 -0
  106. package/lib/Kubernetes/DeploymentPool.js.map +1 -0
  107. package/lib/Kubernetes/index.d.ts +7 -0
  108. package/lib/Kubernetes/index.d.ts.map +1 -0
  109. package/lib/Kubernetes/index.js +7 -0
  110. package/lib/Kubernetes/index.js.map +1 -0
  111. package/lib/Pool.d.ts +66 -0
  112. package/lib/Pool.d.ts.map +1 -0
  113. package/lib/Pool.js +47 -0
  114. package/lib/Pool.js.map +1 -0
  115. package/lib/Providers.d.ts +19 -0
  116. package/lib/Providers.d.ts.map +1 -0
  117. package/lib/Providers.js +31 -0
  118. package/lib/Providers.js.map +1 -0
  119. package/lib/ShardGroups.d.ts +24 -0
  120. package/lib/ShardGroups.d.ts.map +1 -0
  121. package/lib/ShardGroups.js +19 -0
  122. package/lib/ShardGroups.js.map +1 -0
  123. package/lib/Singleton.d.ts +110 -0
  124. package/lib/Singleton.d.ts.map +1 -0
  125. package/lib/Singleton.js +60 -0
  126. package/lib/Singleton.js.map +1 -0
  127. package/lib/Workflow.d.ts +251 -0
  128. package/lib/Workflow.d.ts.map +1 -0
  129. package/lib/Workflow.js +288 -0
  130. package/lib/Workflow.js.map +1 -0
  131. package/lib/index.d.ts +12 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +12 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/Bootstrap.d.ts +28 -0
  136. package/lib/internal/Bootstrap.d.ts.map +1 -0
  137. package/lib/internal/Bootstrap.js +257 -0
  138. package/lib/internal/Bootstrap.js.map +1 -0
  139. package/lib/internal/EntityRuntime.d.ts +40 -0
  140. package/lib/internal/EntityRuntime.d.ts.map +1 -0
  141. package/lib/internal/EntityRuntime.js +698 -0
  142. package/lib/internal/EntityRuntime.js.map +1 -0
  143. package/lib/internal/Gateway.d.ts +82 -0
  144. package/lib/internal/Gateway.d.ts.map +1 -0
  145. package/lib/internal/Gateway.js +218 -0
  146. package/lib/internal/Gateway.js.map +1 -0
  147. package/lib/internal/HostedPlatform.d.ts +36 -0
  148. package/lib/internal/HostedPlatform.d.ts.map +1 -0
  149. package/lib/internal/HostedPlatform.js +36 -0
  150. package/lib/internal/HostedPlatform.js.map +1 -0
  151. package/lib/internal/HostedRuntime.d.ts +58 -0
  152. package/lib/internal/HostedRuntime.d.ts.map +1 -0
  153. package/lib/internal/HostedRuntime.js +72 -0
  154. package/lib/internal/HostedRuntime.js.map +1 -0
  155. package/lib/internal/Naming.d.ts +12 -0
  156. package/lib/internal/Naming.d.ts.map +1 -0
  157. package/lib/internal/Naming.js +40 -0
  158. package/lib/internal/Naming.js.map +1 -0
  159. package/lib/internal/ObjectSharding.d.ts +141 -0
  160. package/lib/internal/ObjectSharding.d.ts.map +1 -0
  161. package/lib/internal/ObjectSharding.js +337 -0
  162. package/lib/internal/ObjectSharding.js.map +1 -0
  163. package/lib/internal/PoolHost.d.ts +80 -0
  164. package/lib/internal/PoolHost.d.ts.map +1 -0
  165. package/lib/internal/PoolHost.js +203 -0
  166. package/lib/internal/PoolHost.js.map +1 -0
  167. package/lib/internal/PoolHostService.d.ts +25 -0
  168. package/lib/internal/PoolHostService.d.ts.map +1 -0
  169. package/lib/internal/PoolHostService.js +10 -0
  170. package/lib/internal/PoolHostService.js.map +1 -0
  171. package/lib/internal/Wire.d.ts +60 -0
  172. package/lib/internal/Wire.d.ts.map +1 -0
  173. package/lib/internal/Wire.js +36 -0
  174. package/lib/internal/Wire.js.map +1 -0
  175. package/lib/internal/WorkflowGateway.d.ts +36 -0
  176. package/lib/internal/WorkflowGateway.d.ts.map +1 -0
  177. package/lib/internal/WorkflowGateway.js +124 -0
  178. package/lib/internal/WorkflowGateway.js.map +1 -0
  179. package/lib/internal/external-runner.d.ts +2 -0
  180. package/lib/internal/external-runner.d.ts.map +1 -0
  181. package/lib/internal/external-runner.js +7 -0
  182. package/lib/internal/external-runner.js.map +1 -0
  183. package/lib/internal/runner.d.ts +2 -0
  184. package/lib/internal/runner.d.ts.map +1 -0
  185. package/lib/internal/runner.js +5 -0
  186. package/lib/internal/runner.js.map +1 -0
  187. package/package.json +143 -3
  188. package/src/AWS/Database.ts +139 -0
  189. package/src/AWS/EcsPool.ts +118 -0
  190. package/src/AWS/index.ts +6 -0
  191. package/src/AWS/internal/Gateway.ts +82 -0
  192. package/src/Cloudflare/DurableObjectPool.ts +198 -0
  193. package/src/Cloudflare/PoolWorker.ts +124 -0
  194. package/src/Cloudflare/index.ts +6 -0
  195. package/src/Cloudflare/internal/Objects.ts +160 -0
  196. package/src/Cron.ts +202 -0
  197. package/src/Database.ts +116 -0
  198. package/src/Docker/Database.ts +94 -0
  199. package/src/Docker/SwarmPool.ts +83 -0
  200. package/src/Docker/index.ts +6 -0
  201. package/src/Entity.ts +621 -0
  202. package/src/Fly/Database.ts +102 -0
  203. package/src/Fly/MachinePool.ts +72 -0
  204. package/src/Fly/index.ts +6 -0
  205. package/src/GCP/ComputeEnginePool.ts +203 -0
  206. package/src/GCP/Database.ts +168 -0
  207. package/src/GCP/index.ts +6 -0
  208. package/src/Hetzner/Database.ts +142 -0
  209. package/src/Hetzner/ServerPool.ts +116 -0
  210. package/src/Hetzner/index.ts +6 -0
  211. package/src/Host.ts +68 -0
  212. package/src/Kubernetes/Database.ts +89 -0
  213. package/src/Kubernetes/DeploymentPool.ts +77 -0
  214. package/src/Kubernetes/index.ts +6 -0
  215. package/src/Pool.ts +104 -0
  216. package/src/Providers.ts +49 -0
  217. package/src/ShardGroups.ts +41 -0
  218. package/src/Singleton.ts +153 -0
  219. package/src/Workflow.ts +556 -0
  220. package/src/index.ts +11 -0
  221. package/src/internal/Bootstrap.ts +368 -0
  222. package/src/internal/EntityRuntime.ts +996 -0
  223. package/src/internal/Gateway.ts +366 -0
  224. package/src/internal/HostedPlatform.ts +83 -0
  225. package/src/internal/HostedRuntime.ts +118 -0
  226. package/src/internal/Naming.ts +44 -0
  227. package/src/internal/ObjectSharding.ts +578 -0
  228. package/src/internal/PoolHost.ts +330 -0
  229. package/src/internal/PoolHostService.ts +25 -0
  230. package/src/internal/Wire.ts +83 -0
  231. package/src/internal/WorkflowGateway.ts +188 -0
  232. package/src/internal/external-runner.ts +10 -0
  233. package/src/internal/runner.ts +5 -0
@@ -0,0 +1,172 @@
1
+ import * as Compute from "@alchemy.run/gcp/Compute";
2
+ import * as IAM from "@alchemy.run/gcp/IAM";
3
+ import * as Storage from "@alchemy.run/gcp/Storage";
4
+ import * as Output from "alchemy/Output";
5
+ import * as Effect from "effect/Effect";
6
+ import { GATEWAY_PORT, RUNNER_PORT } from "../internal/Gateway.js";
7
+ import { makePoolLayer } from "../internal/PoolHost.js";
8
+ import { COS_IMAGE } from "./Database.js";
9
+ const NODE_IMAGE = "node:26-slim";
10
+ const APP_PATH = "/var/lib/cluster-runner";
11
+ const ENV_KEY = "cluster-runner-env";
12
+ const HASH_KEY = "cluster-runner-hash";
13
+ /**
14
+ * Fetch the runner program from the bucket and run it on the host network.
15
+ * When the instance's metadata changes (a new program, new environment), run
16
+ * this script again, so every runner picks the change up.
17
+ */
18
+ const startupScript = (options) => `#!/bin/bash
19
+ set -uo pipefail
20
+ MD=http://metadata.google.internal/computeMetadata/v1
21
+ mkdir -p ${APP_PATH}
22
+ # Container-Optimized OS drops inbound traffic except SSH: open the runner's ports.
23
+ for PORT in ${RUNNER_PORT} ${GATEWAY_PORT}; do
24
+ iptables -C INPUT -p tcp --dport $PORT -j ACCEPT 2>/dev/null || iptables -A INPUT -p tcp --dport $PORT -j ACCEPT
25
+ done
26
+ fetch() {
27
+ TOKEN="$(curl -sf -H 'Metadata-Flavor: Google' "$MD/instance/service-accounts/default/token" | sed -E 's/.*"access_token":"([^"]+)".*/\\1/')"
28
+ curl -sf -H "Authorization: Bearer $TOKEN" -o ${APP_PATH}/$1.next \\
29
+ "https://storage.googleapis.com/storage/v1/b/${options.bucket}/o/$1?alt=media" \\
30
+ && mv ${APP_PATH}/$1.next ${APP_PATH}/$1
31
+ }
32
+ start() {
33
+ fetch runner.mjs
34
+ curl -sf -H 'Metadata-Flavor: Google' "$MD/instance/attributes/${ENV_KEY}" > ${APP_PATH}/env.list
35
+ docker rm -f cluster-runner >/dev/null 2>&1 || true
36
+ docker run -d --name cluster-runner --restart always --network host \\
37
+ --env-file ${APP_PATH}/env.list -v ${APP_PATH}:/app:ro -w /app ${NODE_IMAGE} node /app/runner.mjs
38
+ }
39
+ etag() {
40
+ curl -sf -H 'Metadata-Flavor: Google' -D - -o /dev/null "$MD/instance/attributes/?recursive=true$1" \\
41
+ | grep -i '^etag:' | awk '{print $2}' | tr -d '\\r'
42
+ }
43
+ start
44
+ (
45
+ ETAG="$(etag "")"
46
+ while true; do
47
+ NEXT="$(etag "&wait_for_change=true&timeout_sec=3600&last_etag=$ETAG")"
48
+ if [ -n "$NEXT" ] && [ "$NEXT" != "$ETAG" ]; then
49
+ curl -sf -H 'Metadata-Flavor: Google' "$MD/instance/attributes/startup-script" > ${APP_PATH}/startup.sh \\
50
+ && exec setsid bash ${APP_PATH}/startup.sh
51
+ fi
52
+ sleep 5
53
+ done
54
+ ) >/dev/null 2>&1 < /dev/null &
55
+ # The runner's logs on the serial console (\`gcloud compute instances get-serial-port-output\`).
56
+ pgrep -f "docker logs -f cluster-runner" >/dev/null || setsid sh -c 'while true; do docker logs -f cluster-runner 2>&1 | sed -u "s/^/cluster-runner: /"; sleep 2; done > /dev/ttyS0' >/dev/null 2>&1 < /dev/null &
57
+ `;
58
+ /**
59
+ * Run a pool of Effect Cluster runners on Compute Engine: instances in the
60
+ * database's VPC, spread across zones, each running every class given these
61
+ * runners from a Cloud Storage bucket, behind a regional load balancer on a
62
+ * static address that serves their gateway to callers anywhere. Runners
63
+ * reach each other and the database through the cluster's network tag.
64
+ * Requires a database on a GCP network — `ClusterGCP.Database`.
65
+ *
66
+ * Cloud Run can't host runners: runners take connections from each other.
67
+ *
68
+ * ### Running on Compute Engine
69
+ * **Example:** Two runners
70
+ * ```typescript
71
+ * export const Database = ClusterGCP.Database("Cluster");
72
+ * export const Default = ClusterGCP.ComputeEnginePool("Default", { replicas: 2 }).pipe(
73
+ * Layer.provide(Database),
74
+ * );
75
+ * ```
76
+ *
77
+ * @primitive
78
+ */
79
+ export const ComputeEnginePool = (id, options = {}) => makePoolLayer({
80
+ label: "ClusterGCP.ComputeEnginePool",
81
+ id,
82
+ settings: options,
83
+ expected: "a GCP network (ClusterGCP.Database)",
84
+ network: (database) => (database.network?.kind === "gcp" ? database.network : undefined),
85
+ // A static address, before the instances, so the URL never depends on it.
86
+ gateway: (_name, network) => Effect.gen(function* () {
87
+ const address = yield* Compute.Address(`${id}GatewayAddress`, { region: network.region });
88
+ return {
89
+ url: Output.interpolate `http://${address.address}:${GATEWAY_PORT}`,
90
+ address: address.address,
91
+ };
92
+ }),
93
+ nodes: ({ network, gateway, program, environment }) => Effect.gen(function* () {
94
+ const zones = options.zones ?? [
95
+ `${network.region}-a`,
96
+ `${network.region}-b`,
97
+ `${network.region}-c`,
98
+ ];
99
+ // The gateway checks every call's token; the passthrough load
100
+ // balancer keeps the caller's address.
101
+ yield* Compute.Firewall(`${id}GatewayFirewall`, {
102
+ network: network.network,
103
+ direction: "INGRESS",
104
+ allowed: [{ protocol: "tcp", ports: [String(GATEWAY_PORT)] }],
105
+ sourceRanges: ["0.0.0.0/0"],
106
+ targetTags: [network.tag],
107
+ });
108
+ const bucket = yield* Storage.Bucket(`${id}Program`, {
109
+ location: network.region,
110
+ forceDestroy: true,
111
+ uniformBucketLevelAccess: true,
112
+ });
113
+ const runner = yield* Storage.Object(`${id}RunnerObject`, {
114
+ bucketName: bucket.bucketName,
115
+ key: "runner.mjs",
116
+ file: program.pipe(Output.map((p) => `${p.dir}/runner.mjs`)),
117
+ contentType: "text/javascript",
118
+ });
119
+ const account = yield* IAM.ServiceAccount(`${id}RunnerAccount`, {
120
+ displayName: "Effect Cluster runners",
121
+ });
122
+ const grant = yield* IAM.Member(`${id}ProgramGrant`, {
123
+ kind: "storage.bucket",
124
+ name: bucket.bucketName,
125
+ role: "roles/storage.objectViewer",
126
+ member: Output.interpolate `serviceAccount:${account.email}`,
127
+ });
128
+ // A Docker env file: one `NAME=value` per line. Each runner finds its
129
+ // own address in the subnetwork's range.
130
+ const envFile = environment.pipe(Output.map((env) => Object.entries({ ...env, CLUSTER_RUNNER_NETWORK: network.cidr })
131
+ .map(([name, value]) => `${name}=${String(value).replaceAll("\n", " ")}`)
132
+ .join("\n")));
133
+ const metadata = Output.all(bucket.bucketName, runner.md5Hash, envFile, grant.role).pipe(Output.map(([bucketName, hash, env]) => ({
134
+ "startup-script": startupScript({ bucket: bucketName }),
135
+ [HASH_KEY]: hash ?? "",
136
+ [ENV_KEY]: env,
137
+ })));
138
+ const instances = [];
139
+ for (let i = 0; i < (options.replicas ?? 2); i++) {
140
+ instances.push(yield* Compute.Instance(`${id}Runner${i}`, {
141
+ zone: zones[i % zones.length],
142
+ machineType: options.machineType ?? "e2-small",
143
+ sourceImage: COS_IMAGE,
144
+ network: Output.interpolate `global/networks/${network.network}`,
145
+ subnetwork: Output.interpolate `regions/${network.region}/subnetworks/${network.subnetwork}`,
146
+ associatePublicIp: true,
147
+ tags: [network.tag],
148
+ serviceAccount: account.email,
149
+ oauthScopes: ["https://www.googleapis.com/auth/cloud-platform"],
150
+ metadata: metadata,
151
+ }));
152
+ }
153
+ const health = yield* Compute.HttpHealthCheck(`${id}GatewayHealth`, {
154
+ port: GATEWAY_PORT,
155
+ requestPath: "/health",
156
+ });
157
+ const pool = yield* Compute.TargetPool(`${id}Gateway`, {
158
+ region: network.region,
159
+ healthChecks: [health.selfLink],
160
+ instances: instances.map((instance) => instance.selfLink),
161
+ });
162
+ yield* Compute.ForwardingRule(`${id}GatewayRule`, {
163
+ region: network.region,
164
+ ipAddress: gateway.address,
165
+ ipProtocol: "TCP",
166
+ portRange: String(GATEWAY_PORT),
167
+ target: pool.selfLink,
168
+ loadBalancingScheme: "EXTERNAL",
169
+ });
170
+ }),
171
+ });
172
+ //# sourceMappingURL=ComputeEnginePool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComputeEnginePool.js","sources":["../../src/GCP/ComputeEnginePool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { GcpEnvironment } from "@alchemy.run/gcp/Environment";
2
+ import * as Layer from "effect/Layer";
3
+ import { Database as ClusterDatabase } from "../Database.ts";
4
+ import type { Providers } from "../Providers.ts";
5
+ export declare const COS_IMAGE = "projects/cos-cloud/global/images/family/cos-stable";
6
+ export interface DatabaseProps {
7
+ /**
8
+ * Zone of the database instance.
9
+ * @default the region's `-a` zone
10
+ */
11
+ zone?: string;
12
+ /**
13
+ * Machine type of the database instance.
14
+ * @default "e2-small"
15
+ */
16
+ machineType?: string;
17
+ /**
18
+ * Size of the database's disk, in GB.
19
+ * @default 20
20
+ */
21
+ diskSize?: number;
22
+ /**
23
+ * Postgres image.
24
+ * @default "postgres:17-alpine"
25
+ */
26
+ image?: string;
27
+ /**
28
+ * IPv4 range of the cluster's subnetwork.
29
+ * @default "10.47.0.0/20"
30
+ */
31
+ cidrBlock?: string;
32
+ }
33
+ /**
34
+ * The database an Effect Cluster coordinates through, on Compute Engine,
35
+ * as a `Cluster.Database` Layer: Postgres on an instance with a persistent
36
+ * disk, in a VPC of its own that the pool's runners join. Only the runners reach
37
+ * it; runners reach each other on the runner port.
38
+ *
39
+ * ### Running the database on GCP
40
+ * **Example:** Postgres on Compute Engine
41
+ * ```typescript
42
+ * export const Database = ClusterGCP.Database("Cluster");
43
+ * ```
44
+ *
45
+ * @primitive
46
+ */
47
+ export declare const Database: (id: string, props?: DatabaseProps) => Layer.Layer<ClusterDatabase, never, GcpEnvironment | Providers | import("@alchemy.run/gcp/Providers").Providers | import("alchemy").Stack>;
48
+ //# sourceMappingURL=Database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Database.d.ts","sources":["../../src/GCP/Database.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAsB,QAAQ,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,eAAO,MAAM,SAAS,uDAAuD,CAAC;AAI9E,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,OAAQ,MAAM,UAAS,aAAa,+IAyFtD,CAAC"}
@@ -0,0 +1,115 @@
1
+ import * as Compute from "@alchemy.run/gcp/Compute";
2
+ import { GcpEnvironment } from "@alchemy.run/gcp/Environment";
3
+ import * as Output from "alchemy/Output";
4
+ import { Random } from "alchemy/Random";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Layer from "effect/Layer";
7
+ import * as Redacted from "effect/Redacted";
8
+ import { clusterShardGroups, Database as ClusterDatabase } from "../Database.js";
9
+ import { RUNNER_PORT } from "../internal/Gateway.js";
10
+ import { stackDnsLabel } from "../internal/Naming.js";
11
+ export const COS_IMAGE = "projects/cos-cloud/global/images/family/cos-stable";
12
+ const DISK_DEVICE = "cluster-data";
13
+ const MOUNT_PATH = "/mnt/disks/postgres";
14
+ const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
15
+ /** Mount the data disk, then run Postgres on the host network. */
16
+ const startupScript = (options) => `#!/bin/bash
17
+ set -euo pipefail
18
+ DEVICE=/dev/disk/by-id/google-${DISK_DEVICE}
19
+ blkid "$DEVICE" >/dev/null || mkfs.ext4 -F "$DEVICE"
20
+ mkdir -p ${MOUNT_PATH}
21
+ mountpoint -q ${MOUNT_PATH} || mount "$DEVICE" ${MOUNT_PATH}
22
+ # Container-Optimized OS drops inbound traffic except SSH: open Postgres.
23
+ iptables -C INPUT -p tcp --dport 5432 -j ACCEPT 2>/dev/null || iptables -A INPUT -p tcp --dport 5432 -j ACCEPT
24
+ docker rm -f postgres >/dev/null 2>&1 || true
25
+ docker run -d --name postgres --restart always --network host \\
26
+ -v ${MOUNT_PATH}:/var/lib/postgresql/data \\
27
+ -e PGDATA=/var/lib/postgresql/data/pgdata \\
28
+ -e POSTGRES_DB=cluster \\
29
+ -e POSTGRES_PASSWORD=${shellQuote(options.password)} \\
30
+ ${shellQuote(options.image)}
31
+ `;
32
+ /**
33
+ * The database an Effect Cluster coordinates through, on Compute Engine,
34
+ * as a `Cluster.Database` Layer: Postgres on an instance with a persistent
35
+ * disk, in a VPC of its own that the pool's runners join. Only the runners reach
36
+ * it; runners reach each other on the runner port.
37
+ *
38
+ * ### Running the database on GCP
39
+ * **Example:** Postgres on Compute Engine
40
+ * ```typescript
41
+ * export const Database = ClusterGCP.Database("Cluster");
42
+ * ```
43
+ *
44
+ * @primitive
45
+ */
46
+ export const Database = (id, props = {}) => Layer.effect(ClusterDatabase, Effect.gen(function* () {
47
+ if (globalThis.__ALCHEMY_RUNTIME__) {
48
+ // Runners read their database from their own environment at runtime.
49
+ return { url: Redacted.make(""), shardGroups: undefined };
50
+ }
51
+ const region = yield* GcpEnvironment.current.pipe(Effect.map((env) => env.region), Effect.orDie);
52
+ const zone = props.zone ?? `${region}-a`;
53
+ const cidr = props.cidrBlock ?? "10.47.0.0/20";
54
+ const poolTag = yield* stackDnsLabel(`${id}-pool`, 63);
55
+ const databaseTag = yield* stackDnsLabel(`${id}-db`, 63);
56
+ const network = yield* Compute.Network(`${id}Network`, { autoCreateSubnetworks: false });
57
+ const subnet = yield* Compute.Subnetwork(`${id}Subnet`, {
58
+ network: network.networkName,
59
+ region,
60
+ ipCidrRange: cidr,
61
+ });
62
+ yield* Compute.Firewall(`${id}DatabaseFirewall`, {
63
+ network: network.networkName,
64
+ direction: "INGRESS",
65
+ allowed: [{ protocol: "tcp", ports: ["5432"] }],
66
+ sourceTags: [poolTag],
67
+ targetTags: [databaseTag],
68
+ });
69
+ // Runners send each other messages for the shards they own.
70
+ yield* Compute.Firewall(`${id}RunnerFirewall`, {
71
+ network: network.networkName,
72
+ direction: "INGRESS",
73
+ allowed: [{ protocol: "tcp", ports: [String(RUNNER_PORT)] }],
74
+ sourceTags: [poolTag],
75
+ targetTags: [poolTag],
76
+ });
77
+ const password = yield* Random(`${id}Password`, { bytes: 24 });
78
+ const disk = yield* Compute.Disk(`${id}Disk`, {
79
+ zone,
80
+ type: "pd-balanced",
81
+ sizeGb: props.diskSize ?? 20,
82
+ });
83
+ const instance = yield* Compute.Instance(`${id}Postgres`, {
84
+ zone,
85
+ machineType: props.machineType ?? "e2-small",
86
+ sourceImage: COS_IMAGE,
87
+ network: Output.interpolate `global/networks/${network.networkName}`,
88
+ subnetwork: Output.interpolate `regions/${region}/subnetworks/${subnet.subnetworkName}`,
89
+ // Pulls the Postgres image.
90
+ associatePublicIp: true,
91
+ tags: [databaseTag],
92
+ attachedDisks: [
93
+ { source: disk.selfLink, deviceName: DISK_DEVICE },
94
+ ],
95
+ metadata: {
96
+ "startup-script": password.text.pipe(Output.map((text) => startupScript({
97
+ image: props.image ?? "postgres:17-alpine",
98
+ password: Redacted.value(text),
99
+ }))),
100
+ },
101
+ });
102
+ return {
103
+ url: Output.all(instance.networkIP, password.text).pipe(Output.map(([ip, text]) => Redacted.make(`postgres://postgres:${encodeURIComponent(text.pipe(Redacted.value))}@${ip}:5432/cluster`))),
104
+ network: {
105
+ kind: "gcp",
106
+ network: network.networkName,
107
+ subnetwork: subnet.subnetworkName,
108
+ region,
109
+ cidr,
110
+ tag: poolTag,
111
+ },
112
+ shardGroups: yield* clusterShardGroups(id),
113
+ };
114
+ }));
115
+ //# sourceMappingURL=Database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Database.js","sources":["../../src/GCP/Database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @alchemy.run/effect-cluster/GCP — run an Effect Cluster's database and runners
3
+ * on Compute Engine. Requires `@alchemy.run/gcp`.
4
+ */
5
+ export * from "./ComputeEnginePool.ts";
6
+ export * from "./Database.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sources":["../../src/GCP/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @alchemy.run/effect-cluster/GCP — run an Effect Cluster's database and runners
3
+ * on Compute Engine. Requires `@alchemy.run/gcp`.
4
+ */
5
+ export * from "./ComputeEnginePool.js";
6
+ export * from "./Database.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/GCP/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,48 @@
1
+ import * as Hetzner from "@alchemy.run/hetzner";
2
+ import * as Layer from "effect/Layer";
3
+ import { Database as ClusterDatabase } from "../Database.ts";
4
+ import type { Providers } from "../Providers.ts";
5
+ export interface DatabaseProps {
6
+ /**
7
+ * Hetzner location of the database and the cluster's Network.
8
+ * @default "nbg1"
9
+ */
10
+ location?: string;
11
+ /**
12
+ * Server type of the database.
13
+ * @default "cx23"
14
+ */
15
+ serverType?: string;
16
+ /**
17
+ * Size of the database's Volume, in GB (at least 10).
18
+ * @default 10
19
+ */
20
+ diskSize?: number;
21
+ /**
22
+ * Postgres image.
23
+ * @default "postgres:17-alpine"
24
+ */
25
+ image?: string;
26
+ /**
27
+ * IPv4 range of the cluster's private Network.
28
+ * @default "10.46.0.0/16"
29
+ */
30
+ ipRange?: string;
31
+ }
32
+ /**
33
+ * The database an Effect Cluster coordinates through, on Hetzner Cloud, as
34
+ * a `Cluster.Database` Layer: Postgres on a Server with a Volume, on a
35
+ * private Network that the pool's Servers attach to. A firewall drops every
36
+ * inbound connection on the Server's public address; runners reach it
37
+ * over the Network.
38
+ *
39
+ * ### Running the database on Hetzner
40
+ * **Example:** Postgres in Nuremberg
41
+ * ```typescript
42
+ * export const Database = ClusterHetzner.Database("Cluster");
43
+ * ```
44
+ *
45
+ * @primitive
46
+ */
47
+ export declare const Database: (id: string, props?: DatabaseProps) => Layer.Layer<ClusterDatabase, never, Providers | Hetzner.Providers>;
48
+ //# sourceMappingURL=Database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Database.d.ts","sources":["../../src/Hetzner/Database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,sBAAsB,CAAC;AAIhD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAsB,QAAQ,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiCD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,OAAQ,MAAM,UAAS,aAAa,uEA0DtD,CAAC"}
@@ -0,0 +1,91 @@
1
+ import * as Hetzner from "@alchemy.run/hetzner";
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.js";
8
+ /** The network zone a location belongs to. */
9
+ const networkZoneOf = (location) => location.startsWith("ash")
10
+ ? "us-east"
11
+ : location.startsWith("hil")
12
+ ? "us-west"
13
+ : location.startsWith("sin")
14
+ ? "ap-southeast"
15
+ : "eu-central";
16
+ const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
17
+ /** Mount the Volume and run Postgres on it, on the Server's own network. */
18
+ const userData = (options) => `#!/bin/bash
19
+ set -euo pipefail
20
+ DEVICE=/dev/disk/by-id/scsi-0HC_Volume_${options.volumeId}
21
+ for _ in $(seq 1 60); do [ -e "$DEVICE" ] && break; sleep 2; done
22
+ blkid "$DEVICE" >/dev/null || mkfs.ext4 -F "$DEVICE"
23
+ mkdir -p /mnt/postgres
24
+ grep -q " /mnt/postgres " /etc/fstab || echo "$DEVICE /mnt/postgres ext4 discard,nofail,defaults 0 0" >> /etc/fstab
25
+ mountpoint -q /mnt/postgres || mount /mnt/postgres
26
+ until docker info >/dev/null 2>&1; do sleep 2; done
27
+ docker rm -f postgres >/dev/null 2>&1 || true
28
+ docker run -d --name postgres --restart always --network host \\
29
+ -v /mnt/postgres:/var/lib/postgresql/data \\
30
+ -e PGDATA=/var/lib/postgresql/data/pgdata \\
31
+ -e POSTGRES_DB=cluster \\
32
+ -e POSTGRES_PASSWORD=${shellQuote(options.password)} \\
33
+ ${shellQuote(options.image)}
34
+ `;
35
+ /**
36
+ * The database an Effect Cluster coordinates through, on Hetzner Cloud, as
37
+ * a `Cluster.Database` Layer: Postgres on a Server with a Volume, on a
38
+ * private Network that the pool's Servers attach to. A firewall drops every
39
+ * inbound connection on the Server's public address; runners reach it
40
+ * over the Network.
41
+ *
42
+ * ### Running the database on Hetzner
43
+ * **Example:** Postgres in Nuremberg
44
+ * ```typescript
45
+ * export const Database = ClusterHetzner.Database("Cluster");
46
+ * ```
47
+ *
48
+ * @primitive
49
+ */
50
+ export const Database = (id, props = {}) => Layer.effect(ClusterDatabase, Effect.gen(function* () {
51
+ if (globalThis.__ALCHEMY_RUNTIME__) {
52
+ // Runners read their database from their own environment at runtime.
53
+ return { url: Redacted.make(""), shardGroups: undefined };
54
+ }
55
+ const location = props.location ?? "nbg1";
56
+ const ipRange = props.ipRange ?? "10.46.0.0/16";
57
+ const password = yield* Random(`${id}Password`, { bytes: 24 });
58
+ const network = yield* Hetzner.Network(`${id}Network`, {
59
+ ipRange,
60
+ subnets: [{ type: "cloud", networkZone: networkZoneOf(location) }],
61
+ });
62
+ // No inbound rules: only the Network reaches Postgres.
63
+ const firewall = yield* Hetzner.Firewall(`${id}Firewall`, {});
64
+ const volume = yield* Hetzner.Volume(`${id}Data`, {
65
+ size: props.diskSize ?? 10,
66
+ format: "ext4",
67
+ location,
68
+ });
69
+ const server = yield* Hetzner.Server(`${id}Postgres`, {
70
+ serverType: props.serverType ?? "cx23",
71
+ // Hetzner's Ubuntu image with Docker preinstalled.
72
+ image: "docker-ce",
73
+ location,
74
+ // IPv4 pulls the Postgres image; IPv6 would only spend an address.
75
+ enableIpv6: false,
76
+ networks: [network],
77
+ firewalls: [firewall],
78
+ volumes: [volume],
79
+ userData: Output.all(volume.id, password.text).pipe(Output.map(([volumeId, text]) => userData({
80
+ volumeId,
81
+ image: props.image ?? "postgres:17-alpine",
82
+ password: Redacted.value(text),
83
+ }))),
84
+ });
85
+ return {
86
+ url: Output.all(server.privateIps, password.text).pipe(Output.map(([ips, text]) => Redacted.make(`postgres://postgres:${encodeURIComponent(text.pipe(Redacted.value))}@${ips[0]}:5432/cluster`))),
87
+ network: { kind: "hetzner", networkId: network.networkId, ipRange, location },
88
+ shardGroups: yield* clusterShardGroups(id),
89
+ };
90
+ }));
91
+ //# sourceMappingURL=Database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Database.js","sources":["../../src/Hetzner/Database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import * as Hetzner from "@alchemy.run/hetzner";
2
+ import { type PoolSettings } from "../internal/PoolHost.ts";
3
+ export interface ServerPoolOptions extends PoolSettings {
4
+ /**
5
+ * Number of runners, one Server each.
6
+ * @default 2
7
+ */
8
+ replicas?: number;
9
+ /**
10
+ * Server type of each runner.
11
+ * @default "cx23"
12
+ */
13
+ serverType?: string;
14
+ }
15
+ /**
16
+ * Run a pool of Effect Cluster runners on Hetzner Cloud: Servers on the
17
+ * database's private Network, each running every class given these
18
+ * runners as a `Hetzner.Service`, behind a Load Balancer that serves their
19
+ * gateway to callers anywhere. Runners talk to each other and the database
20
+ * over the Network; their firewall only lets SSH in, which deploys them.
21
+ * Requires a database on a Hetzner Network — `ClusterHetzner.Database`.
22
+ *
23
+ * ### Running on Hetzner
24
+ * **Example:** Two runners
25
+ * ```typescript
26
+ * export const Database = ClusterHetzner.Database("Cluster");
27
+ * export const Default = ClusterHetzner.ServerPool("Default", { replicas: 2 }).pipe(
28
+ * Layer.provide(Database),
29
+ * );
30
+ * ```
31
+ *
32
+ * @primitive
33
+ */
34
+ export declare const ServerPool: (id: string, options?: ServerPoolOptions) => import("effect/Layer").Layer<import("../internal/PoolHostService.ts").PoolHost, never, import("../Database.ts").Database | import("../Providers.ts").Providers | Hetzner.Providers | import("alchemy").Stack>;
35
+ //# sourceMappingURL=ServerPool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServerPool.d.ts","sources":["../../src/Hetzner/ServerPool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,sBAAsB,CAAC;AAIhD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE3E,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,OAAQ,MAAM,YAAW,iBAAiB,kNA0E7D,CAAC"}