js-waku/packages/sdk/src/light_push/retry_manager.spec.ts
Arseniy Klempner 16253026c6
feat: implement lp-v3 error codes with backwards compatibility (#2501)
* feat: implement LightPush v3 protocol support

Add comprehensive LightPush v3 protocol implementation with:

Core Features:
- LightPush v3 protocol codec and multicodec detection
- Status code-based error handling and validation
- Protocol version inference and compatibility layers
- Enhanced error types with detailed failure information

Protocol Support:
- Automatic v3/v2 protocol negotiation and fallback
- Status code mapping to LightPush error types
- Protocol version tracking in SDK results
- Mixed protocol environment support

Testing Infrastructure:
- Comprehensive v3 error code handling tests
- Mock functions for v3/v2 response scenarios
- Protocol version detection and validation tests
- Backward compatibility verification

Implementation Details:
- Clean separation between v2 and v3 response handling
- Type-safe status code validation with isSuccess helper
- Enhanced failure reporting with protocol version context
- Proper error propagation through SDK layers

This implementation maintains full backward compatibility with v2
while providing enhanced functionality for v3 protocol features.

* feat: handle both light push protocols

* fix: unsubscribe test

* feat: consolidate lpv2/v3 types

* feat(tests): bump nwaku to 0.36.0

* fix: remove extraneous exports

* fix: add delay to tests

* fix: remove protocol result types

* feat: consolidate light push codec branching

* fix: revert nwaku image

* fix: remove multicodec

* fix: remove protocolversion

* feat: simplify v2/v3 branching logic to use two stream managers

* fix: remove unused utils

* fix: remove comments

* fix: revert store test

* fix: cleanup lightpush sdk

* fix: remove unused util

* fix: remove unused exports

* fix: rename file from public to protocol_handler

* fix: use proper type for sdk result

* fix: update return types in filter

* fix: rebase against latest master

* fix: use both lightpush codecs when waiting for peer

* fix: handle both lp codecs

* fix: remove unused code

* feat: use array for multicodec fields

* fix: add timestamp if missing in v3 rpc

* fix: resolve on either lp codec when waiting for peer

* fix: remove unused util

* fix: remove unnecessary abstraction

* feat: accept nwaku docker image as arg, test lp backwards compat

* fix: revert filter error

* feat: add legacy flag to enable lightpushv2 only

* Revert "feat: accept nwaku docker image as arg, test lp backwards compat"

This reverts commit 857e12cbc73305e5c51abd057665bd34708b2737.

* fix: remove unused test

* feat: improve lp3 (#2597)

* improve light push core

* move back to singualar multicodec property, enable array prop only for light push

* implement v2/v3 interop e2e test, re-add useLegacy flag, ensure e2e runs for v2 and v3

* fix v2 v3 condition

* generate message package earlier

* add log, fix condition

---------

Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
Co-authored-by: Sasha <oleksandr@status.im>
2025-09-05 00:52:37 +02:00

227 lines
6.3 KiB
TypeScript

import type { PeerId } from "@libp2p/interface";
import {
type LightPushCoreResult,
LightPushError,
ProtocolError,
Protocols
} from "@waku/interfaces";
import { createRoutingInfo } from "@waku/utils";
import { expect } from "chai";
import sinon from "sinon";
import { PeerManager } from "../peer_manager/index.js";
import { RetryManager, ScheduledTask } from "./retry_manager.js";
const TestRoutingInfo = createRoutingInfo(
{ clusterId: 0 },
{ pubsubTopic: "/waku/2/rs/0/0" }
);
describe("RetryManager", () => {
let retryManager: RetryManager;
let peerManager: PeerManager;
let mockPeerId: PeerId;
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers();
mockPeerId = { toString: () => "test-peer-id" } as PeerId;
peerManager = {
getPeers: () => [mockPeerId],
renewPeer: sinon.spy(),
start: sinon.spy(),
stop: sinon.spy()
} as unknown as PeerManager;
retryManager = new RetryManager({
peerManager,
retryIntervalMs: 100
});
});
afterEach(() => {
clock.restore();
retryManager.stop();
sinon.restore();
});
it("should start and stop interval correctly", () => {
const setIntervalSpy = sinon.spy(global, "setInterval");
const clearIntervalSpy = sinon.spy(global, "clearInterval");
retryManager.start();
expect(setIntervalSpy.calledOnce).to.be.true;
retryManager.stop();
expect(clearIntervalSpy.calledOnce).to.be.true;
});
it("should process tasks in queue", async () => {
const successCallback = sinon.spy(
async (peerId: PeerId): Promise<LightPushCoreResult> => ({
success: peerId,
failure: null
})
);
retryManager.push(successCallback, 3, TestRoutingInfo);
retryManager.start();
await clock.tickAsync(200);
retryManager.stop();
expect(successCallback.calledOnce, "called").to.be.true;
expect(successCallback.calledWith(mockPeerId), "called with peer").to.be
.true;
});
it("should requeue task if no peer is available", async () => {
(peerManager as any).getPeers = () => [];
const callback = sinon.spy();
retryManager.push(callback, 2, TestRoutingInfo);
retryManager.start();
const queue = (retryManager as any)["queue"] as ScheduledTask[];
expect(queue.length).to.equal(1);
await clock.tickAsync(200);
retryManager.stop();
expect(callback.called).to.be.false;
expect(queue.length).to.equal(1);
expect(queue[0].maxAttempts).to.equal(1);
});
it("should not requeue if maxAttempts is exhausted and no peer is available", async () => {
(peerManager as any).getPeers = () => [];
const callback = sinon.spy();
retryManager.push(callback, 1, TestRoutingInfo);
retryManager.start();
const queue = (retryManager as any)["queue"] as ScheduledTask[];
expect(queue.length).to.equal(1);
await clock.tickAsync(500);
retryManager.stop();
expect(callback.called).to.be.false;
expect(queue.length).to.equal(0);
});
it("should retry failed tasks", async () => {
const failingCallback = sinon.spy(
async (): Promise<LightPushCoreResult> => ({
success: null,
failure: { error: LightPushError.GENERIC_FAIL }
})
);
const queue = (retryManager as any)["queue"] as ScheduledTask[];
const task = {
callback: failingCallback,
maxAttempts: 2,
routingInfo: TestRoutingInfo
};
await (retryManager as any)["taskExecutor"](task);
expect(failingCallback.calledOnce, "executed callback").to.be.true;
expect(
queue.some((t) => t.maxAttempts === 1),
"task attempt decreased"
).to.be.true;
});
it("should request peer renewal on specific errors", async () => {
const errorCallback = sinon.spy(async (): Promise<LightPushCoreResult> => {
throw new Error(ProtocolError.NO_PEER_AVAILABLE);
});
await (retryManager as RetryManager)["taskExecutor"]({
callback: errorCallback,
maxAttempts: 1,
routingInfo: TestRoutingInfo
});
expect((peerManager.renewPeer as sinon.SinonSpy).calledOnce).to.be.true;
expect(
(peerManager.renewPeer as sinon.SinonSpy).calledWith(mockPeerId, {
protocol: Protocols.LightPush,
pubsubTopic: TestRoutingInfo.pubsubTopic
})
).to.be.true;
});
it("should handle task timeouts", async () => {
const slowCallback = sinon.spy(async (): Promise<LightPushCoreResult> => {
await new Promise((resolve) => setTimeout(resolve, 15000));
return { success: mockPeerId, failure: null };
});
const task = {
callback: slowCallback,
maxAttempts: 1,
routingInfo: TestRoutingInfo
};
const executionPromise = (retryManager as any)["taskExecutor"](task);
await clock.tickAsync(11000);
await executionPromise;
expect(slowCallback.calledOnce).to.be.true;
});
it("should not execute task if max attempts is 0", async () => {
const failingCallback = sinon.spy(
async (): Promise<LightPushCoreResult> => {
throw new Error("test error" as any);
}
);
const task = {
callback: failingCallback,
maxAttempts: 0,
routingInfo: TestRoutingInfo
};
await (retryManager as any)["taskExecutor"](task);
expect(failingCallback.called).to.be.false;
});
it("should not retry if at least one success", async () => {
let called = 0;
(peerManager as any).getPeers = () => [mockPeerId];
const successCallback = sinon.stub().callsFake(() => {
called++;
if (called === 1) retryManager.stop();
return Promise.resolve({ success: mockPeerId, failure: null });
});
retryManager.push(successCallback, 2, TestRoutingInfo);
retryManager.start();
await clock.tickAsync(500);
expect(called).to.equal(1);
});
it("should retry if all attempts fail", async () => {
let called = 0;
(peerManager as any).getPeers = () => [mockPeerId];
const failCallback = sinon.stub().callsFake(() => {
called++;
return Promise.resolve({
success: null,
failure: { error: LightPushError.GENERIC_FAIL }
});
});
retryManager.push(failCallback, 2, TestRoutingInfo);
retryManager.start();
await clock.tickAsync(1000);
retryManager.stop();
expect(called).to.be.greaterThan(1);
const queue = (retryManager as any)["queue"] as ScheduledTask[];
expect(queue.length).to.equal(0);
});
});