Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/devnet-auto-port-allocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@evolution-sdk/devnet": patch
---

`Cluster.make` now auto-allocates host ports for cardano-node, Kupo, and Ogmios when not specified, so parallel test runs no longer collide on the previous fixed defaults (1442/1337/4001/8090). The returned `Cluster` exposes a new `ports` field with the assigned host ports.

Consumers passing explicit `port` values on `kupo`, `ogmios`, or `ports` keep their existing behavior. Consumers that relied on the previous fixed defaults should read the assigned port from `cluster.ports.kupo`, `cluster.ports.ogmios`, `cluster.ports.node`, and `cluster.ports.submit` instead.

Container starts now retry with exponential backoff to absorb transient Docker daemon pressure when many containers come up at once.
44 changes: 40 additions & 4 deletions packages/evolution-devnet/src/Cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NodeStream } from "@effect/platform-node"
import Docker from "dockerode"
import { Data, Effect, Stream } from "effect"
import * as fs from "fs"
import { createServer } from "net"
import * as os from "os"
import * as path from "path"

Expand All @@ -20,10 +21,29 @@ export interface Cluster {
readonly kupo?: Container.Container | undefined
readonly ogmios?: Container.Container | undefined
readonly networkName: string
/** Host ports assigned to each container. */
readonly ports: {
readonly node: number
readonly submit: number
readonly kupo?: number
readonly ogmios?: number
}
/** The Shelley genesis config used by this cluster (needed for slot config) */
readonly shelleyGenesis: Config.ShelleyGenesis
}

const findFreePort = (): Promise<number> =>
new Promise((resolve, reject) => {
const server = createServer()
server.unref()
server.on("error", reject)
server.listen(0, () => {
const addr = server.address()
const port = typeof addr === "object" && addr !== null ? addr.port : 0
server.close(() => resolve(port))
})
})

/**
* Internal utilities for cluster operations.
*
Expand Down Expand Up @@ -93,12 +113,25 @@ const writeConfigFiles = (config: Required<Config.DevNetConfig>) =>
*/
export const makeEffect = (config: Config.DevNetConfig = {}): Effect.Effect<Cluster, ClusterError> =>
Effect.gen(function* () {
const kupoEnabled = config.kupo?.enabled ?? Config.DEFAULT_DEVNET_CONFIG.kupo.enabled
const ogmiosEnabled = config.ogmios?.enabled ?? Config.DEFAULT_DEVNET_CONFIG.ogmios.enabled
const resolvedPorts = {
node: config.ports?.node ?? (yield* Effect.promise(() => findFreePort())),
submit: config.ports?.submit ?? (yield* Effect.promise(() => findFreePort())),
kupo: kupoEnabled
? (config.kupo?.port ?? (yield* Effect.promise(() => findFreePort())))
: undefined,
ogmios: ogmiosEnabled
? (config.ogmios?.port ?? (yield* Effect.promise(() => findFreePort())))
: undefined
}

const fullConfig: Required<Config.DevNetConfig> = {
clusterName: config.clusterName ?? Config.DEFAULT_DEVNET_CONFIG.clusterName,
image: config.image ?? Config.DEFAULT_DEVNET_CONFIG.image,
ports: {
...Config.DEFAULT_DEVNET_CONFIG.ports,
...config.ports
node: resolvedPorts.node,
submit: resolvedPorts.submit
},
networkMagic: config.networkMagic ?? Config.DEFAULT_DEVNET_CONFIG.networkMagic,
nodeConfig: {
Expand Down Expand Up @@ -135,11 +168,13 @@ export const makeEffect = (config: Config.DevNetConfig = {}): Effect.Effect<Clus
},
kupo: {
...Config.DEFAULT_DEVNET_CONFIG.kupo,
...config.kupo
...config.kupo,
...(resolvedPorts.kupo !== undefined ? { port: resolvedPorts.kupo } : {})
},
ogmios: {
...Config.DEFAULT_DEVNET_CONFIG.ogmios,
...config.ogmios
...config.ogmios,
...(resolvedPorts.ogmios !== undefined ? { port: resolvedPorts.ogmios } : {})
}
}

Expand Down Expand Up @@ -291,6 +326,7 @@ export const makeEffect = (config: Config.DevNetConfig = {}): Effect.Effect<Clus
}
: undefined,
networkName,
ports: resolvedPorts,
shelleyGenesis: fullConfig.shelleyGenesis as Config.ShelleyGenesis
}
})
Expand Down
8 changes: 6 additions & 2 deletions packages/evolution-devnet/src/Container.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Docker from "dockerode"
import { Data, Effect } from "effect"
import { Data, Effect, Schedule } from "effect"
import { PassThrough } from "stream"

import * as Config from "./Config.js"
Expand All @@ -16,6 +16,10 @@ export interface Container {
readonly name: string
}

// Docker daemon rejects concurrent container starts under load; retry transient
// failures with exponential backoff so parallel test forks don't fail-fast.
const startRetrySchedule = Schedule.exponential("500 millis").pipe(Schedule.compose(Schedule.recurs(3)))

/**
* Start a specific devnet container.
*
Expand All @@ -31,7 +35,7 @@ export const startEffect = (container: Container): Effect.Effect<void, Container
message: "Check if ports are available and Docker has sufficient resources.",
cause
})
})
}).pipe(Effect.retry(startRetrySchedule))

/**
* Start a specific devnet container, throws on error.
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/Client.Devnet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ describe("Client with Devnet", () => {

const createTestClient = () =>
Client.make(Cluster.getChain(devnetCluster!))
.withKupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })

beforeAll(async () => {
Expand All @@ -41,10 +44,9 @@ describe("Client with Devnet", () => {

devnetCluster = await Cluster.make({
clusterName: "client-kupmios-wallet-test",
ports: { node: 6001, submit: 9002 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1443, logLevel: "Info" },
ogmios: { enabled: true, port: 1338, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
1 change: 0 additions & 1 deletion packages/evolution-devnet/test/Devnet.Genesis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe("Devnet.Genesis", () => {

devnetCluster = await Cluster.make({
clusterName: "genesis-utxo-calculation-test",
ports: { node: 6002, submit: 9003 },
shelleyGenesis: genesisConfig,
kupo: { enabled: false },
ogmios: { enabled: false }
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ describe("TxBuilder addSigner (Devnet Submit)", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand All @@ -47,10 +50,9 @@ describe("TxBuilder addSigner (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "addsigner-test",
ports: { node: 6007, submit: 9008 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1449, logLevel: "Info" },
ogmios: { enabled: true, port: 1344, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Chain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ describe("TxBuilder.chainResult", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1456", ogmiosUrl: "http://localhost:1348" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand All @@ -40,10 +43,9 @@ describe("TxBuilder.chainResult", () => {

devnetCluster = await Cluster.make({
clusterName: "chain-test",
ports: { node: 6013, submit: 9013 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1456, logLevel: "Info" },
ogmios: { enabled: true, port: 1348, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ describe("TxBuilder compose (Devnet Submit)", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1458", ogmiosUrl: "http://localhost:1350" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand All @@ -48,10 +51,9 @@ describe("TxBuilder compose (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "compose-test",
ports: { node: 6015, submit: 9015 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1458, logLevel: "Info" },
ogmios: { enabled: true, port: 1350, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ describe("TxBuilder Governance Operations", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1457", ogmiosUrl: "http://localhost:1349" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand Down Expand Up @@ -80,11 +83,10 @@ describe("TxBuilder Governance Operations", () => {

devnetCluster = await Cluster.make({
clusterName: "governance-ops-test",
ports: { node: 6014, submit: 9014 },
shelleyGenesis: genesisConfig,
conwayGenesis,
kupo: { enabled: true, port: 1457, logLevel: "Info" },
ogmios: { enabled: true, port: 1349, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1450", ogmiosUrl: "http://localhost:1345" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand All @@ -47,10 +50,9 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "metadata-test",
ports: { node: 6008, submit: 9009 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1450, logLevel: "Info" },
ogmios: { enabled: true, port: 1345, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Mint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ describe("TxBuilder Minting (Devnet Submit)", () => {
const createTestClient = () => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })
}

Expand Down Expand Up @@ -64,10 +67,9 @@ describe("TxBuilder Minting (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "client-minting-test",
ports: { node: 6001, submit: 9002 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1443, logLevel: "Info" },
ogmios: { enabled: true, port: 1338, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand All @@ -64,10 +67,9 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "nativescript-test",
ports: { node: 6007, submit: 9008 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1449, logLevel: "Info" },
ogmios: { enabled: true, port: 1344, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => {
const createTestClient = () => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1444", ogmiosUrl: "http://localhost:1339" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster!.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster!.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })
}

Expand All @@ -92,10 +95,9 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => {

devnetCluster = await Cluster.make({
clusterName: "plutus-minting-test",
ports: { node: 6002, submit: 9003 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1444, logLevel: "Info" },
ogmios: { enabled: true, port: 1339, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
10 changes: 6 additions & 4 deletions packages/evolution-devnet/test/TxBuilder.Pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ describe("TxBuilder Pool Operations", () => {
const createTestClient = (accountIndex: number = 0) => {
if (!devnetCluster) throw new Error("Cluster not initialized")
return Client.make(Cluster.getChain(devnetCluster))
.withKupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" })
.withKupmios({
kupoUrl: `http://localhost:${devnetCluster.ports.kupo}`,
ogmiosUrl: `http://localhost:${devnetCluster.ports.ogmios}`
})
.withSeed({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })
}

Expand Down Expand Up @@ -72,10 +75,9 @@ describe("TxBuilder Pool Operations", () => {

devnetCluster = await Cluster.make({
clusterName: "pool-ops-test",
ports: { node: 6006, submit: 9007 },
shelleyGenesis: genesisConfig,
kupo: { enabled: true, port: 1453, logLevel: "Info" },
ogmios: { enabled: true, port: 1343, logLevel: "info" }
kupo: { enabled: true, logLevel: "Info" },
ogmios: { enabled: true, logLevel: "info" }
})

await Cluster.start(devnetCluster)
Expand Down
Loading