Skip to content

Commit

Permalink
fix: 🐛 add retry logic to connecting providers
Browse files Browse the repository at this point in the history
  • Loading branch information
timbrinded committed Nov 13, 2024
1 parent 87bf403 commit 9e6093d
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 142 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
**/dist/
**/.tsup/
test/html/*
*.sqlite
**/*.log
downloads/*
*.wasm
Expand All @@ -22,4 +21,6 @@ artifacts
**/contracts/out/*


.idea
.idea
db.sqlite*
polkadot*
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
moonbeam
polkadot
*.wasm
*.txt
1 change: 0 additions & 1 deletion packages/cli/src/internal/localNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export async function launchNode(cmd: string, args: string[], name: string) {
for (const port of ports) {
try {
await checkWebSocketJSONRPC(port);
// console.log(`Port ${port} supports WebSocket JSON RPC!`);
break probe;
} catch {}
}
Expand Down
14 changes: 13 additions & 1 deletion packages/cli/src/internal/providerFactories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class ProviderFactory {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: async () => {
process.env.DEFAULT_TIMEOUT_MS = "30000";
const options: ApiOptions = {
provider: new WsProvider(this.url),
initWasm: false,
Expand Down Expand Up @@ -226,7 +227,10 @@ export class ProviderInterfaceFactory {
}

private async createPolkadotJs(): Promise<ProviderInterface> {
debug(`🔌 Connecting PolkadotJs provider: ${this.name}`);
const api = (await this.connect()) as ApiPromise;
debug(`✅ PolkadotJs provider ${this.name} connected`);
1;
return {
name: this.name,
api,
Expand Down Expand Up @@ -309,7 +313,15 @@ export class ProviderInterfaceFactory {
type: ProviderType,
connect: () => Promise<ApiPromise> | Wallet | Web3 | Promise<ViemClient> | null
): Promise<ProviderInterface> {
return await new ProviderInterfaceFactory(name, type, connect).create();
debug(`🔄 Populating provider: ${name} of type: ${type}`);
try {
const providerInterface = await new ProviderInterfaceFactory(name, type, connect).create();
debug(`✅ Successfully populated provider: ${name}`);
return providerInterface;
} catch (error: any) {
console.error(`❌ Failed to populate provider: ${name} - ${error.message}`);
throw error;
}
}
}

Expand Down
134 changes: 119 additions & 15 deletions packages/cli/src/lib/globalContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
MoonwallConfig,
MoonwallEnvironment,
MoonwallProvider,
ProviderType,
} from "@moonwall/types";
import type { ApiPromise } from "@polkadot/api";
import zombie, { type Network } from "@zombienet/orchestrator";
Expand Down Expand Up @@ -35,6 +36,7 @@ import {
isOptionSet,
} from "./configReader";
import { type ChildProcess, exec, execSync } from "node:child_process";
import { promisify } from "node:util";
const debugSetup = Debug("global:context");

export class MoonwallContext {
Expand Down Expand Up @@ -369,13 +371,32 @@ export class MoonwallContext {

const promises = nodes.map(async ({ cmd, args, name, launch }) => {
if (launch) {
const { runningNode } = await launchNode(cmd, args, name || "node");
this.nodes.push(runningNode);
try {
const { runningNode } = await launchNode(cmd, args, name || "node");
this.nodes.push(runningNode);
} catch (error: any) {
throw new Error(`Failed to start node '${name || "unnamed"}': ${error.message}`);
}
} else {
return Promise.resolve();
}
});
await Promise.allSettled(promises);

try {
await Promise.race([
Promise.allSettled(promises),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Network startup timed out after 30 seconds")), 30000)
),
]);
} catch (error: any) {
console.error(`Error starting network: ${error.message}`);
console.error(
"Current nodes:",
this.nodes.map((n) => ({ pid: n.pid }))
);
throw error;
}

return ctx;
}
Expand All @@ -395,14 +416,62 @@ export class MoonwallContext {
return MoonwallContext.getContext();
}

const promises = this.environment.providers.map(
async ({ name, type, connect }) =>
new Promise(async (resolve) => {
this.providers.push(await ProviderInterfaceFactory.populate(name, type, connect as any));
resolve("");
})
);
await Promise.all(promises);
const connectWithRetry = async (provider: {
name: string;
type: ProviderType;
connect: any;
}) => {
const maxRetries = 15;
const retryDelay = 1000;
const connectTimeout = 10000;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const connectedProvider = await Promise.race([
ProviderInterfaceFactory.populate(provider.name, provider.type, provider.connect),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Connection attempt timed out")), connectTimeout)
),
]);

this.providers.push(connectedProvider);
debugSetup(`✅ Provider ${provider.name} connected on attempt ${attempt}`);
return;
} catch (error: any) {
console.error(
`Error connecting provider ${provider.name} on attempt ${attempt}: ${error.message}`
);

if (attempt === maxRetries) {
throw new Error(
`Failed to connect provider '${provider.name}' after ${maxRetries} attempts: ${error.message}`
);
}

debugSetup(
`⚠️ Retrying provider ${provider.name} connection, attempt ${attempt + 1}/${maxRetries}`
);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
};

try {
await Promise.race([
Promise.all(this.environment.providers.map((provider) => connectWithRetry(provider))),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Provider connection timed out after 30 seconds")),
30000
)
),
]);
} catch (error: any) {
console.error(`Error connecting to environment: ${error.message}`);
console.error("Current providers:", this.providers.map((p) => p.name).join(", "));
console.error(`Total providers: ${this.environment.providers.map((p) => p.name).join(", ")}`);
throw error;
}

if (this.foundation === "zombie") {
let readStreams: any[] = [];
Expand Down Expand Up @@ -477,13 +546,42 @@ export class MoonwallContext {
throw new Error(`Provider ${providerName} not found`);
}

prov.disconnect();
try {
await prov.disconnect();
debugSetup(`✅ Provider ${providerName} disconnected`);
} catch (error: any) {
console.error(`❌ Error disconnecting provider ${providerName}: ${error.message}`);
}
} else {
await Promise.all(this.providers.map((prov) => prov.disconnect()));
await Promise.all(
this.providers.map(async (prov) => {
try {
await prov.disconnect();
debugSetup(`✅ Provider ${prov.name} disconnected`);
} catch (error: any) {
console.error(`❌ Error disconnecting provider ${prov.name}: ${error.message}`);
}
})
);
this.providers = [];
}
}

// public async disconnect(providerName?: string) {
// if (providerName) {
// const prov = this.providers.find(({ name }) => name === providerName);

// if (!prov) {
// throw new Error(`Provider ${providerName} not found`);
// }

// prov.disconnect();
// } else {
// await Promise.all(this.providers.map((prov) => prov.disconnect()));
// this.providers = [];
// }
// }

public static async getContext(config?: MoonwallConfig, force = false): Promise<MoonwallContext> {
if (!MoonwallContext.instance?.configured || force) {
if (!config) {
Expand Down Expand Up @@ -582,9 +680,15 @@ export interface IGlobalContextFoundation {
foundationType: FoundationType;
}

const execAsync = promisify(exec);

async function isPidRunning(pid: number): Promise<boolean> {
const output = exec(`ps -p ${pid} -o pid=`);
return output.exitCode === 0;
try {
const { stdout } = await execAsync(`ps -p ${pid} -o pid=`);
return stdout.trim() === pid.toString();
} catch (error) {
return false;
}
}

async function waitForPidsToDie(pids: number[]): Promise<void> {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/lib/runnerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export function describeSuite<T extends FoundationType>({
beforeAll(async () => {
const env = getEnvironmentFromConfig();
ctx = await contextCreator();

if (env.foundation.type === "read_only") {
const settings = loadParams(env.foundation.launchSpec);
limiter = new Bottleneck(settings);
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ export type RepoSpec = {
*
* @example
* {
* "name": "hydradx"
* "name": "hydration"
* }
*
* @example
Expand Down
20 changes: 20 additions & 0 deletions test/configs/assethub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
endpoint: wss://polkadot-asset-hub-rpc.polkadot.io
mock-signature-host: true
block: ${env.POLKADOT_ASSET_HUB_BLOCK_NUMBER}
db: ./tmp/db.sqlite

import-storage:
System:
Account:
-
-
- 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
- providers: 1
data:
free: 1000000000000000
Assets:
Account:
- [[1984, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY], { balance: 1000000000 }]
- [[1337, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY], { balance: 1000000000 }]
- [[21, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY], { balance: 1000000000 }]
Asset: [[[21], { supply: 1000000000 }]]
78 changes: 0 additions & 78 deletions test/configs/hydradx.yml

This file was deleted.

Loading

0 comments on commit 9e6093d

Please sign in to comment.