Skip to content

Commit 4049123

Browse files
authored
feat!: lighten retry logic for LightPush (#2182)
* feat: lighten retry logic for LightPush * update tests * remove base protocol sdk from light push, add unit tests for light push * remove replaced test * ensure numPeersToUse is respected * skip tests
1 parent 19a5d29 commit 4049123

File tree

12 files changed

+250
-220
lines changed

12 files changed

+250
-220
lines changed

packages/interfaces/src/light_push.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { IBaseProtocolCore, IBaseProtocolSDK } from "./protocols.js";
1+
import { IBaseProtocolCore } from "./protocols.js";
22
import type { ISender } from "./sender.js";
33

4-
export type ILightPush = ISender &
5-
IBaseProtocolSDK & { protocol: IBaseProtocolCore };
4+
export type ILightPush = ISender & { protocol: IBaseProtocolCore };

packages/interfaces/src/sender.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import type { IEncoder, IMessage } from "./message.js";
2-
import { ProtocolUseOptions, SDKProtocolResult } from "./protocols.js";
2+
import { SDKProtocolResult } from "./protocols.js";
3+
4+
export type ISenderOptions = {
5+
/**
6+
* Enables retry of a message that was failed to be sent.
7+
* @default false
8+
*/
9+
autoRetry?: boolean;
10+
/**
11+
* Sets number of attempts if `autoRetry` is enabled.
12+
* @default 3
13+
*/
14+
maxAttempts?: number;
15+
};
316

417
export interface ISender {
518
send: (
619
encoder: IEncoder,
720
message: IMessage,
8-
sendOptions?: ProtocolUseOptions
21+
sendOptions?: ISenderOptions
922
) => Promise<SDKProtocolResult>;
1023
}

packages/sdk/src/protocols/base_protocol.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface Options {
1111
maintainPeersInterval?: number;
1212
}
1313

14-
const DEFAULT_NUM_PEERS_TO_USE = 2;
14+
export const DEFAULT_NUM_PEERS_TO_USE = 2;
1515
const DEFAULT_MAINTAIN_PEERS_INTERVAL = 30_000;
1616

1717
export class BaseProtocolSDK implements IBaseProtocolSDK {
@@ -29,20 +29,20 @@ export class BaseProtocolSDK implements IBaseProtocolSDK {
2929
) {
3030
this.log = new Logger(`sdk:${core.multicodec}`);
3131

32-
this.peerManager = new PeerManager(connectionManager, core, this.log);
33-
3432
this.numPeersToUse = options?.numPeersToUse ?? DEFAULT_NUM_PEERS_TO_USE;
3533
const maintainPeersInterval =
3634
options?.maintainPeersInterval ?? DEFAULT_MAINTAIN_PEERS_INTERVAL;
3735

36+
this.peerManager = new PeerManager(connectionManager, core, this.log);
37+
3838
this.log.info(
3939
`Initializing BaseProtocolSDK with numPeersToUse: ${this.numPeersToUse}, maintainPeersInterval: ${maintainPeersInterval}ms`
4040
);
4141
void this.startMaintainPeersInterval(maintainPeersInterval);
4242
}
4343

4444
public get connectedPeers(): Peer[] {
45-
return this.peerManager.getPeers();
45+
return this.peerManager.getPeers().slice(0, this.numPeersToUse);
4646
}
4747

4848
/**
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Peer } from "@libp2p/interface";
2+
import {
3+
ConnectionManager,
4+
createEncoder,
5+
Encoder,
6+
LightPushCodec
7+
} from "@waku/core";
8+
import { Libp2p, ProtocolError } from "@waku/interfaces";
9+
import { utf8ToBytes } from "@waku/utils/bytes";
10+
import { expect } from "chai";
11+
import sinon from "sinon";
12+
13+
import { LightPush } from "./light_push.js";
14+
15+
const PUBSUB_TOPIC = "/waku/2/rs/1/4";
16+
const CONTENT_TOPIC = "/test/1/waku-light-push/utf8";
17+
18+
describe("LightPush SDK", () => {
19+
let libp2p: Libp2p;
20+
let encoder: Encoder;
21+
let lightPush: LightPush;
22+
23+
beforeEach(() => {
24+
libp2p = mockLibp2p();
25+
encoder = createEncoder({ contentTopic: CONTENT_TOPIC });
26+
lightPush = mockLightPush({ libp2p });
27+
});
28+
29+
it("should fail to send if pubsub topics are misconfigured", async () => {
30+
lightPush = mockLightPush({ libp2p, pubsubTopics: ["/wrong"] });
31+
32+
const result = await lightPush.send(encoder, {
33+
payload: utf8ToBytes("test")
34+
});
35+
const failures = result.failures ?? [];
36+
37+
expect(failures.length).to.be.eq(1);
38+
expect(failures.some((v) => v.error === ProtocolError.TOPIC_NOT_CONFIGURED))
39+
.to.be.true;
40+
});
41+
42+
it("should fail to send if no connected peers found", async () => {
43+
const result = await lightPush.send(encoder, {
44+
payload: utf8ToBytes("test")
45+
});
46+
const failures = result.failures ?? [];
47+
48+
expect(failures.length).to.be.eq(1);
49+
expect(failures.some((v) => v.error === ProtocolError.NO_PEER_AVAILABLE)).to
50+
.be.true;
51+
});
52+
53+
it("should send to specified number of peers of used peers", async () => {
54+
libp2p = mockLibp2p({
55+
peers: [mockPeer("1"), mockPeer("2"), mockPeer("3"), mockPeer("4")]
56+
});
57+
58+
// check default value that should be 2
59+
lightPush = mockLightPush({ libp2p });
60+
let sendSpy = sinon.spy(
61+
(_encoder: any, _message: any, peer: Peer) =>
62+
({ success: peer.id }) as any
63+
);
64+
lightPush.protocol.send = sendSpy;
65+
66+
let result = await lightPush.send(encoder, {
67+
payload: utf8ToBytes("test")
68+
});
69+
70+
expect(sendSpy.calledTwice).to.be.true;
71+
expect(result.successes?.length).to.be.eq(2);
72+
73+
// check if setting another value works
74+
lightPush = mockLightPush({ libp2p, numPeersToUse: 3 });
75+
sendSpy = sinon.spy(
76+
(_encoder: any, _message: any, peer: Peer) =>
77+
({ success: peer.id }) as any
78+
);
79+
lightPush.protocol.send = sendSpy;
80+
81+
result = await lightPush.send(encoder, { payload: utf8ToBytes("test") });
82+
83+
expect(sendSpy.calledThrice).to.be.true;
84+
expect(result.successes?.length).to.be.eq(3);
85+
});
86+
87+
it("should retry on failure if specified", async () => {
88+
libp2p = mockLibp2p({
89+
peers: [mockPeer("1"), mockPeer("2")]
90+
});
91+
92+
lightPush = mockLightPush({ libp2p });
93+
let sendSpy = sinon.spy((_encoder: any, _message: any, peer: Peer) => {
94+
if (peer.id.toString() === "1") {
95+
return { success: peer.id };
96+
}
97+
98+
return { failure: { error: "problem" } };
99+
});
100+
lightPush.protocol.send = sendSpy as any;
101+
const attemptRetriesSpy = sinon.spy(lightPush["attemptRetries"]);
102+
lightPush["attemptRetries"] = attemptRetriesSpy;
103+
104+
const result = await lightPush.send(
105+
encoder,
106+
{ payload: utf8ToBytes("test") },
107+
{ autoRetry: true }
108+
);
109+
110+
expect(attemptRetriesSpy.calledOnce).to.be.true;
111+
expect(result.successes?.length).to.be.eq(1);
112+
expect(result.failures?.length).to.be.eq(1);
113+
114+
sendSpy = sinon.spy(() => ({ failure: { error: "problem" } })) as any;
115+
await lightPush["attemptRetries"](sendSpy as any);
116+
117+
expect(sendSpy.callCount).to.be.eq(3);
118+
119+
sendSpy = sinon.spy(() => ({ failure: { error: "problem" } })) as any;
120+
await lightPush["attemptRetries"](sendSpy as any, 2);
121+
122+
expect(sendSpy.callCount).to.be.eq(2);
123+
});
124+
});
125+
126+
type MockLibp2pOptions = {
127+
peers?: Peer[];
128+
};
129+
130+
function mockLibp2p(options?: MockLibp2pOptions): Libp2p {
131+
const peers = options?.peers || [];
132+
const peerStore = {
133+
get: (id: any) => Promise.resolve(peers.find((p) => p.id === id))
134+
};
135+
136+
return {
137+
peerStore,
138+
getPeers: () => peers.map((p) => p.id),
139+
components: {
140+
events: new EventTarget(),
141+
connectionManager: {
142+
getConnections: () => []
143+
} as any,
144+
peerStore
145+
}
146+
} as unknown as Libp2p;
147+
}
148+
149+
type MockLightPushOptions = {
150+
libp2p: Libp2p;
151+
pubsubTopics?: string[];
152+
numPeersToUse?: number;
153+
};
154+
155+
function mockLightPush(options: MockLightPushOptions): LightPush {
156+
return new LightPush(
157+
{
158+
configuredPubsubTopics: options.pubsubTopics || [PUBSUB_TOPIC]
159+
} as ConnectionManager,
160+
options.libp2p,
161+
{ numPeersToUse: options.numPeersToUse }
162+
);
163+
}
164+
165+
function mockPeer(id: string): Peer {
166+
return {
167+
id,
168+
protocols: [LightPushCodec]
169+
} as unknown as Peer;
170+
}

packages/sdk/src/protocols/light_push/light_push.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,51 @@ import {
66
LightPushCore
77
} from "@waku/core";
88
import {
9+
type CoreProtocolResult,
910
Failure,
1011
type IEncoder,
1112
ILightPush,
1213
type IMessage,
14+
type ISenderOptions,
1315
type Libp2p,
1416
type ProtocolCreateOptions,
1517
ProtocolError,
16-
ProtocolUseOptions,
1718
SDKProtocolResult
1819
} from "@waku/interfaces";
1920
import { ensurePubsubTopicIsConfigured, Logger } from "@waku/utils";
2021

21-
import { ReliabilityMonitorManager } from "../../reliability_monitor/index.js";
22-
import { SenderReliabilityMonitor } from "../../reliability_monitor/sender.js";
23-
import { BaseProtocolSDK } from "../base_protocol.js";
22+
import { DEFAULT_NUM_PEERS_TO_USE } from "../base_protocol.js";
2423

2524
const log = new Logger("sdk:light-push");
2625

27-
class LightPush extends BaseProtocolSDK implements ILightPush {
28-
public readonly protocol: LightPushCore;
26+
const DEFAULT_MAX_ATTEMPTS = 3;
27+
const DEFAULT_SEND_OPTIONS: ISenderOptions = {
28+
autoRetry: false,
29+
maxAttempts: DEFAULT_MAX_ATTEMPTS
30+
};
31+
32+
type RetryCallback = (peer: Peer) => Promise<CoreProtocolResult>;
2933

30-
private readonly reliabilityMonitor: SenderReliabilityMonitor;
34+
export class LightPush implements ILightPush {
35+
private numPeersToUse: number = DEFAULT_NUM_PEERS_TO_USE;
36+
public readonly protocol: LightPushCore;
3137

3238
public constructor(
3339
connectionManager: ConnectionManager,
3440
private libp2p: Libp2p,
3541
options?: ProtocolCreateOptions
3642
) {
37-
super(
38-
new LightPushCore(connectionManager.configuredPubsubTopics, libp2p),
39-
connectionManager,
40-
{
41-
numPeersToUse: options?.numPeersToUse
42-
}
43+
this.numPeersToUse = options?.numPeersToUse ?? DEFAULT_NUM_PEERS_TO_USE;
44+
this.protocol = new LightPushCore(
45+
connectionManager.configuredPubsubTopics,
46+
libp2p
4347
);
44-
45-
this.reliabilityMonitor = ReliabilityMonitorManager.createSenderMonitor(
46-
this.renewPeer.bind(this)
47-
);
48-
49-
this.protocol = this.core as LightPushCore;
5048
}
5149

5250
public async send(
5351
encoder: IEncoder,
5452
message: IMessage,
55-
_options?: ProtocolUseOptions
53+
options: ISenderOptions = DEFAULT_SEND_OPTIONS
5654
): Promise<SDKProtocolResult> {
5755
const successes: PeerId[] = [];
5856
const failures: Failure[] = [];
@@ -105,14 +103,10 @@ class LightPush extends BaseProtocolSDK implements ILightPush {
105103
if (failure) {
106104
failures.push(failure);
107105

108-
const connectedPeer = this.connectedPeers.find((connectedPeer) =>
109-
connectedPeer.id.equals(failure.peerId)
110-
);
111-
112-
if (connectedPeer) {
113-
void this.reliabilityMonitor.attemptRetriesOrRenew(
114-
connectedPeer.id,
115-
() => this.protocol.send(encoder, message, connectedPeer)
106+
if (options?.autoRetry) {
107+
void this.attemptRetries(
108+
(peer: Peer) => this.protocol.send(encoder, message, peer),
109+
options.maxAttempts
116110
);
117111
}
118112
}
@@ -129,6 +123,32 @@ class LightPush extends BaseProtocolSDK implements ILightPush {
129123
};
130124
}
131125

126+
private async attemptRetries(
127+
fn: RetryCallback,
128+
maxAttempts?: number
129+
): Promise<void> {
130+
maxAttempts = maxAttempts || DEFAULT_MAX_ATTEMPTS;
131+
const connectedPeers = await this.getConnectedPeers();
132+
133+
if (connectedPeers.length === 0) {
134+
log.warn("Cannot retry with no connected peers.");
135+
return;
136+
}
137+
138+
for (let i = 0; i < maxAttempts; i++) {
139+
const peer = connectedPeers[i % connectedPeers.length]; // always present as we checked for the length already
140+
const response = await fn(peer);
141+
142+
if (response.success) {
143+
return;
144+
}
145+
146+
log.info(
147+
`Attempted retry for peer:${peer.id} failed with:${response?.failure?.error}`
148+
);
149+
}
150+
}
151+
132152
private async getConnectedPeers(): Promise<Peer[]> {
133153
const peerIDs = this.libp2p.getPeers();
134154

0 commit comments

Comments
 (0)