@waku/sdk
Version:
A unified SDK for easy creation and management of js-waku nodes.
167 lines (132 loc) • 3.91 kB
text/typescript
import type { PeerId } from "@libp2p/interface";
import {
type CoreProtocolResult,
type IRoutingInfo,
Protocols
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import type { PeerManager } from "../peer_manager/index.js";
import { shouldPeerBeChanged, timeout } from "./utils.js";
type RetryManagerConfig = {
retryIntervalMs: number;
peerManager: PeerManager;
};
type AttemptCallback = (peerId: PeerId) => Promise<CoreProtocolResult>;
export type ScheduledTask = {
maxAttempts: number;
routingInfo: IRoutingInfo;
callback: AttemptCallback;
};
const MAX_CONCURRENT_TASKS = 5;
const TASK_TIMEOUT_MS = 10_000;
const log = new Logger("sdk:retry-manager");
export class RetryManager {
private intervalID: number | null = null;
private readonly retryIntervalMs: number;
private inProgress: number = 0;
private readonly queue: ScheduledTask[] = [];
private readonly peerManager: PeerManager;
public constructor(config: RetryManagerConfig) {
this.peerManager = config.peerManager;
this.retryIntervalMs = config.retryIntervalMs || 1000;
}
public start(): void {
this.intervalID = setInterval(() => {
this.processQueue();
}, this.retryIntervalMs) as unknown as number;
}
public stop(): void {
if (this.intervalID) {
clearInterval(this.intervalID);
this.intervalID = null;
}
}
public push(
callback: AttemptCallback,
maxAttempts: number,
routingInfo: IRoutingInfo
): void {
this.queue.push({
maxAttempts,
callback,
routingInfo
});
}
private processQueue(): void {
if (this.queue.length === 0) {
return;
}
while (this.queue.length && this.inProgress < MAX_CONCURRENT_TASKS) {
const task = this.queue.shift();
if (task) {
this.scheduleTask(task);
}
}
}
private scheduleTask(task: ScheduledTask): void {
const delayedTask = async (): Promise<void> => {
return this.taskExecutor(task);
};
// schedule execution ASAP
// need to use setTimeout to avoid blocking main execution
setTimeout(delayedTask as () => void, 100);
}
private async taskExecutor(task: ScheduledTask): Promise<void> {
if (task.maxAttempts <= 0) {
log.warn("scheduleTask: max attempts has reached, removing from queue");
return;
}
const peerId = (
await this.peerManager.getPeers({
protocol: Protocols.LightPush,
pubsubTopic: task.routingInfo.pubsubTopic
})
)[0];
if (!peerId) {
log.warn("scheduleTask: no peers, putting back to queue");
this.queue.push({
...task,
maxAttempts: task.maxAttempts - 1
});
return;
}
try {
this.inProgress += 1;
const response = await Promise.race([
timeout(TASK_TIMEOUT_MS),
task.callback(peerId)
]);
if (response?.failure) {
throw Error(response.failure.error);
}
log.info("scheduleTask: executed successfully");
if (task.maxAttempts === 0) {
log.warn("scheduleTask: discarded a task due to limit of max attempts");
return;
}
this.queue.push({
...task,
maxAttempts: task.maxAttempts - 1
});
} catch (_err) {
const error = _err as unknown as { message: string };
log.error("scheduleTask: task execution failed with error:", error);
if (shouldPeerBeChanged(error.message)) {
await this.peerManager.renewPeer(peerId, {
protocol: Protocols.LightPush,
pubsubTopic: task.routingInfo.pubsubTopic
});
}
if (task.maxAttempts === 0) {
log.warn("scheduleTask: discarded a task due to limit of max attempts");
return;
}
this.queue.push({
...task,
maxAttempts: task.maxAttempts - 1
});
} finally {
this.inProgress -= 1;
}
}
}