mirror of
https://github.com/status-im/js-waku.git
synced 2025-02-23 18:38:11 +00:00
fix: attempt to fix some of the Filter issues (#2183)
* 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 * turn off check for missing messages * fix recurring ping * add useful logs * skip tests * remove comment * feat: check filter subscriptions against lightPush (#2185)
This commit is contained in:
parent
4049123f14
commit
ded994f8ec
@ -20,7 +20,7 @@ export type SubscriptionCallback<T extends IDecodedMessage> = {
|
|||||||
export type SubscribeOptions = {
|
export type SubscribeOptions = {
|
||||||
keepAlive?: number;
|
keepAlive?: number;
|
||||||
pingsBeforePeerRenewed?: number;
|
pingsBeforePeerRenewed?: number;
|
||||||
maxMissedMessagesThreshold?: number;
|
enableLightPushFilterCheck?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ISubscription {
|
export interface ISubscription {
|
||||||
|
@ -2,7 +2,7 @@ import { sha256 } from "@noble/hashes/sha256";
|
|||||||
import type { IDecodedMessage, IProtoMessage } from "@waku/interfaces";
|
import type { IDecodedMessage, IProtoMessage } from "@waku/interfaces";
|
||||||
import { isDefined } from "@waku/utils";
|
import { isDefined } from "@waku/utils";
|
||||||
import {
|
import {
|
||||||
bytesToUtf8,
|
bytesToHex,
|
||||||
concat,
|
concat,
|
||||||
numberToBytes,
|
numberToBytes,
|
||||||
utf8ToBytes
|
utf8ToBytes
|
||||||
@ -56,6 +56,6 @@ export function messageHashStr(
|
|||||||
message: IProtoMessage | IDecodedMessage
|
message: IProtoMessage | IDecodedMessage
|
||||||
): string {
|
): string {
|
||||||
const hash = messageHash(pubsubTopic, message);
|
const hash = messageHash(pubsubTopic, message);
|
||||||
const hashStr = bytesToUtf8(hash);
|
const hashStr = bytesToHex(hash);
|
||||||
return hashStr;
|
return hashStr;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
export const DEFAULT_KEEP_ALIVE = 60_000;
|
export const DEFAULT_KEEP_ALIVE = 60_000;
|
||||||
|
export const DEFAULT_LIGHT_PUSH_FILTER_CHECK = false;
|
||||||
|
export const DEFAULT_LIGHT_PUSH_FILTER_CHECK_INTERVAL = 10_000;
|
||||||
|
|
||||||
export const DEFAULT_SUBSCRIBE_OPTIONS = {
|
export const DEFAULT_SUBSCRIBE_OPTIONS = {
|
||||||
keepAlive: DEFAULT_KEEP_ALIVE
|
keepAlive: DEFAULT_KEEP_ALIVE,
|
||||||
|
enableLightPushFilterCheck: DEFAULT_LIGHT_PUSH_FILTER_CHECK
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
type IDecodedMessage,
|
type IDecodedMessage,
|
||||||
type IDecoder,
|
type IDecoder,
|
||||||
type IFilter,
|
type IFilter,
|
||||||
|
type ILightPush,
|
||||||
type Libp2p,
|
type Libp2p,
|
||||||
NetworkConfig,
|
NetworkConfig,
|
||||||
type ProtocolCreateOptions,
|
type ProtocolCreateOptions,
|
||||||
@ -38,7 +39,8 @@ class Filter extends BaseProtocolSDK implements IFilter {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
connectionManager: ConnectionManager,
|
connectionManager: ConnectionManager,
|
||||||
libp2p: Libp2p,
|
private libp2p: Libp2p,
|
||||||
|
private lightPush?: ILightPush,
|
||||||
options?: ProtocolCreateOptions
|
options?: ProtocolCreateOptions
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
@ -195,7 +197,9 @@ class Filter extends BaseProtocolSDK implements IFilter {
|
|||||||
this.protocol,
|
this.protocol,
|
||||||
this.connectionManager,
|
this.connectionManager,
|
||||||
() => this.connectedPeers,
|
() => this.connectedPeers,
|
||||||
this.renewPeer.bind(this)
|
this.renewPeer.bind(this),
|
||||||
|
this.libp2p,
|
||||||
|
this.lightPush
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -300,7 +304,9 @@ class Filter extends BaseProtocolSDK implements IFilter {
|
|||||||
|
|
||||||
export function wakuFilter(
|
export function wakuFilter(
|
||||||
connectionManager: ConnectionManager,
|
connectionManager: ConnectionManager,
|
||||||
|
lightPush?: ILightPush,
|
||||||
init?: ProtocolCreateOptions
|
init?: ProtocolCreateOptions
|
||||||
): (libp2p: Libp2p) => IFilter {
|
): (libp2p: Libp2p) => IFilter {
|
||||||
return (libp2p: Libp2p) => new Filter(connectionManager, libp2p, init);
|
return (libp2p: Libp2p) =>
|
||||||
|
new Filter(connectionManager, libp2p, lightPush, init);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import type { Peer } from "@libp2p/interface";
|
import type { Peer } from "@libp2p/interface";
|
||||||
import type { PeerId } from "@libp2p/interface";
|
import type { PeerId } from "@libp2p/interface";
|
||||||
import { ConnectionManager, FilterCore } from "@waku/core";
|
import {
|
||||||
|
ConnectionManager,
|
||||||
|
createDecoder,
|
||||||
|
createEncoder,
|
||||||
|
FilterCore,
|
||||||
|
LightPushCore
|
||||||
|
} from "@waku/core";
|
||||||
import {
|
import {
|
||||||
type Callback,
|
type Callback,
|
||||||
type ContentTopic,
|
type ContentTopic,
|
||||||
@ -8,8 +14,10 @@ import {
|
|||||||
EConnectionStateEvents,
|
EConnectionStateEvents,
|
||||||
type IDecodedMessage,
|
type IDecodedMessage,
|
||||||
type IDecoder,
|
type IDecoder,
|
||||||
|
type ILightPush,
|
||||||
type IProtoMessage,
|
type IProtoMessage,
|
||||||
type ISubscription,
|
type ISubscription,
|
||||||
|
type Libp2p,
|
||||||
type PeerIdStr,
|
type PeerIdStr,
|
||||||
ProtocolError,
|
ProtocolError,
|
||||||
type PubsubTopic,
|
type PubsubTopic,
|
||||||
@ -23,14 +31,23 @@ import { groupByContentTopic, Logger } from "@waku/utils";
|
|||||||
import { ReliabilityMonitorManager } from "../../reliability_monitor/index.js";
|
import { ReliabilityMonitorManager } from "../../reliability_monitor/index.js";
|
||||||
import { ReceiverReliabilityMonitor } from "../../reliability_monitor/receiver.js";
|
import { ReceiverReliabilityMonitor } from "../../reliability_monitor/receiver.js";
|
||||||
|
|
||||||
import { DEFAULT_KEEP_ALIVE, DEFAULT_SUBSCRIBE_OPTIONS } from "./constants.js";
|
import {
|
||||||
|
DEFAULT_KEEP_ALIVE,
|
||||||
|
DEFAULT_LIGHT_PUSH_FILTER_CHECK,
|
||||||
|
DEFAULT_LIGHT_PUSH_FILTER_CHECK_INTERVAL,
|
||||||
|
DEFAULT_SUBSCRIBE_OPTIONS
|
||||||
|
} from "./constants.js";
|
||||||
|
|
||||||
const log = new Logger("sdk:filter:subscription_manager");
|
const log = new Logger("sdk:filter:subscription_manager");
|
||||||
|
|
||||||
export class SubscriptionManager implements ISubscription {
|
export class SubscriptionManager implements ISubscription {
|
||||||
private reliabilityMonitor: ReceiverReliabilityMonitor;
|
private reliabilityMonitor: ReceiverReliabilityMonitor;
|
||||||
|
|
||||||
private keepAliveTimer: number | null = null;
|
private keepAliveTimeout: number = DEFAULT_KEEP_ALIVE;
|
||||||
|
private keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
private enableLightPushFilterCheck = DEFAULT_LIGHT_PUSH_FILTER_CHECK;
|
||||||
|
|
||||||
private subscriptionCallbacks: Map<
|
private subscriptionCallbacks: Map<
|
||||||
ContentTopic,
|
ContentTopic,
|
||||||
SubscriptionCallback<IDecodedMessage>
|
SubscriptionCallback<IDecodedMessage>
|
||||||
@ -43,7 +60,9 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
private readonly getPeers: () => Peer[],
|
private readonly getPeers: () => Peer[],
|
||||||
private readonly renewPeer: (
|
private readonly renewPeer: (
|
||||||
peerToDisconnect: PeerId
|
peerToDisconnect: PeerId
|
||||||
) => Promise<Peer | undefined>
|
) => Promise<Peer | undefined>,
|
||||||
|
private readonly libp2p: Libp2p,
|
||||||
|
private readonly lightPush?: ILightPush
|
||||||
) {
|
) {
|
||||||
this.pubsubTopic = pubsubTopic;
|
this.pubsubTopic = pubsubTopic;
|
||||||
this.subscriptionCallbacks = new Map();
|
this.subscriptionCallbacks = new Map();
|
||||||
@ -54,7 +73,8 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
this.renewPeer.bind(this),
|
this.renewPeer.bind(this),
|
||||||
() => Array.from(this.subscriptionCallbacks.keys()),
|
() => Array.from(this.subscriptionCallbacks.keys()),
|
||||||
this.protocol.subscribe.bind(this.protocol),
|
this.protocol.subscribe.bind(this.protocol),
|
||||||
this.protocol.addLibp2pEventListener.bind(this.protocol)
|
this.protocol.addLibp2pEventListener.bind(this.protocol),
|
||||||
|
this.sendLightPushCheckMessage.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,11 +83,10 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
callback: Callback<T>,
|
callback: Callback<T>,
|
||||||
options: SubscribeOptions = DEFAULT_SUBSCRIBE_OPTIONS
|
options: SubscribeOptions = DEFAULT_SUBSCRIBE_OPTIONS
|
||||||
): Promise<SDKProtocolResult> {
|
): Promise<SDKProtocolResult> {
|
||||||
this.reliabilityMonitor.setMaxMissedMessagesThreshold(
|
|
||||||
options.maxMissedMessagesThreshold
|
|
||||||
);
|
|
||||||
this.reliabilityMonitor.setMaxPingFailures(options.pingsBeforePeerRenewed);
|
this.reliabilityMonitor.setMaxPingFailures(options.pingsBeforePeerRenewed);
|
||||||
this.keepAliveTimer = options.keepAlive || DEFAULT_KEEP_ALIVE;
|
this.keepAliveTimeout = options.keepAlive || DEFAULT_KEEP_ALIVE;
|
||||||
|
this.enableLightPushFilterCheck =
|
||||||
|
options?.enableLightPushFilterCheck || DEFAULT_LIGHT_PUSH_FILTER_CHECK;
|
||||||
|
|
||||||
const decodersArray = Array.isArray(decoders) ? decoders : [decoders];
|
const decodersArray = Array.isArray(decoders) ? decoders : [decoders];
|
||||||
|
|
||||||
@ -85,11 +104,20 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.enableLightPushFilterCheck) {
|
||||||
|
decodersArray.push(
|
||||||
|
createDecoder(
|
||||||
|
this.buildLightPushContentTopic(),
|
||||||
|
this.pubsubTopic
|
||||||
|
) as IDecoder<T>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const decodersGroupedByCT = groupByContentTopic(decodersArray);
|
const decodersGroupedByCT = groupByContentTopic(decodersArray);
|
||||||
const contentTopics = Array.from(decodersGroupedByCT.keys());
|
const contentTopics = Array.from(decodersGroupedByCT.keys());
|
||||||
|
|
||||||
const promises = this.getPeers().map(async (peer) =>
|
const promises = this.getPeers().map(async (peer) =>
|
||||||
this.protocol.subscribe(this.pubsubTopic, peer, contentTopics)
|
this.subscribeWithPeerVerification(peer, contentTopics)
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
@ -107,12 +135,17 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
callback
|
callback
|
||||||
} as unknown as SubscriptionCallback<IDecodedMessage>;
|
} as unknown as SubscriptionCallback<IDecodedMessage>;
|
||||||
|
|
||||||
|
// don't handle case of internal content topic
|
||||||
|
if (contentTopic === this.buildLightPushContentTopic()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The callback and decoder may override previous values, this is on
|
// The callback and decoder may override previous values, this is on
|
||||||
// purpose as the user may call `subscribe` to refresh the subscription
|
// purpose as the user may call `subscribe` to refresh the subscription
|
||||||
this.subscriptionCallbacks.set(contentTopic, subscriptionCallback);
|
this.subscriptionCallbacks.set(contentTopic, subscriptionCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.startSubscriptionsMaintenance(this.keepAliveTimer);
|
this.startSubscriptionsMaintenance(this.keepAliveTimeout);
|
||||||
|
|
||||||
return finalResult;
|
return finalResult;
|
||||||
}
|
}
|
||||||
@ -174,10 +207,9 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
message: WakuMessage,
|
message: WakuMessage,
|
||||||
peerIdStr: PeerIdStr
|
peerIdStr: PeerIdStr
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const alreadyReceived = this.reliabilityMonitor.processIncomingMessage(
|
const alreadyReceived = this.reliabilityMonitor.notifyMessageReceived(
|
||||||
message,
|
peerIdStr,
|
||||||
this.pubsubTopic,
|
message as IProtoMessage
|
||||||
peerIdStr
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (alreadyReceived) {
|
if (alreadyReceived) {
|
||||||
@ -200,6 +232,19 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
await pushMessage(subscriptionCallback, this.pubsubTopic, message);
|
await pushMessage(subscriptionCallback, this.pubsubTopic, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async subscribeWithPeerVerification(
|
||||||
|
peer: Peer,
|
||||||
|
contentTopics: string[]
|
||||||
|
): Promise<CoreProtocolResult> {
|
||||||
|
const result = await this.protocol.subscribe(
|
||||||
|
this.pubsubTopic,
|
||||||
|
peer,
|
||||||
|
contentTopics
|
||||||
|
);
|
||||||
|
await this.sendLightPushCheckMessage(peer);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private handleResult(
|
private handleResult(
|
||||||
results: PromiseSettledResult<CoreProtocolResult>[],
|
results: PromiseSettledResult<CoreProtocolResult>[],
|
||||||
type: "ping" | "subscribe" | "unsubscribe" | "unsubscribeAll"
|
type: "ping" | "subscribe" | "unsubscribe" | "unsubscribeAll"
|
||||||
@ -240,23 +285,26 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await this.protocol.ping(peer);
|
result = await this.protocol.ping(peer);
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
result = {
|
||||||
success: null,
|
success: null,
|
||||||
failure: {
|
failure: {
|
||||||
peerId,
|
peerId,
|
||||||
error: ProtocolError.GENERIC_FAIL
|
error: ProtocolError.GENERIC_FAIL
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} finally {
|
|
||||||
void this.reliabilityMonitor.handlePingResult(peerId, result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Received result from filter ping peerId:${peerId.toString()}\tsuccess:${result.success?.toString()}\tfailure:${result.failure?.error}`
|
||||||
|
);
|
||||||
|
await this.reliabilityMonitor.handlePingResult(peerId, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private startSubscriptionsMaintenance(interval: number): void {
|
private startSubscriptionsMaintenance(timeout: number): void {
|
||||||
log.info("Starting subscriptions maintenance");
|
log.info("Starting subscriptions maintenance");
|
||||||
this.startKeepAlivePings(interval);
|
this.startKeepAlivePings(timeout);
|
||||||
this.startConnectionListener();
|
this.startConnectionListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,31 +343,69 @@ export class SubscriptionManager implements ISubscription {
|
|||||||
log.error(`networkStateListener failed to recover: ${err}`);
|
log.error(`networkStateListener failed to recover: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startKeepAlivePings(this.keepAliveTimer || DEFAULT_KEEP_ALIVE);
|
this.startKeepAlivePings(this.keepAliveTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startKeepAlivePings(interval: number): void {
|
private startKeepAlivePings(timeout: number): void {
|
||||||
if (this.keepAliveTimer) {
|
if (this.keepAliveInterval) {
|
||||||
log.info("Recurring pings already set up.");
|
log.info("Recurring pings already set up.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keepAliveTimer = setInterval(() => {
|
this.keepAliveInterval = setInterval(() => {
|
||||||
void this.ping()
|
void this.ping();
|
||||||
.then(() => log.info("Keep-alive ping successful"))
|
}, timeout);
|
||||||
.catch((error) => log.error("Error in keep-alive ping cycle:", error));
|
|
||||||
}, interval) as unknown as number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopKeepAlivePings(): void {
|
private stopKeepAlivePings(): void {
|
||||||
if (!this.keepAliveTimer) {
|
if (!this.keepAliveInterval) {
|
||||||
log.info("Already stopped recurring pings.");
|
log.info("Already stopped recurring pings.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Stopping recurring pings.");
|
log.info("Stopping recurring pings.");
|
||||||
clearInterval(this.keepAliveTimer);
|
clearInterval(this.keepAliveInterval);
|
||||||
this.keepAliveTimer = null;
|
this.keepAliveInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendLightPushCheckMessage(peer: Peer): Promise<void> {
|
||||||
|
if (
|
||||||
|
this.lightPush &&
|
||||||
|
this.libp2p &&
|
||||||
|
this.reliabilityMonitor.shouldVerifyPeer(peer.id)
|
||||||
|
) {
|
||||||
|
const encoder = createEncoder({
|
||||||
|
contentTopic: this.buildLightPushContentTopic(),
|
||||||
|
pubsubTopic: this.pubsubTopic,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = { payload: new Uint8Array(1) };
|
||||||
|
const protoMessage = await encoder.toProtoObj(message);
|
||||||
|
|
||||||
|
// make a delay to be sure message is send when subscription is in place
|
||||||
|
setTimeout(
|
||||||
|
(async () => {
|
||||||
|
const result = await (this.lightPush!.protocol as LightPushCore).send(
|
||||||
|
encoder,
|
||||||
|
message,
|
||||||
|
peer
|
||||||
|
);
|
||||||
|
this.reliabilityMonitor.notifyMessageSent(peer.id, protoMessage);
|
||||||
|
if (result.failure) {
|
||||||
|
log.error(
|
||||||
|
`failed to send lightPush ping message to peer:${peer.id.toString()}\t${result.failure.error}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}) as () => void,
|
||||||
|
DEFAULT_LIGHT_PUSH_FILTER_CHECK_INTERVAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildLightPushContentTopic(): string {
|
||||||
|
return `/js-waku-subscription-ping/1/${this.libp2p.peerId.toString()}/utf8`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@ export class ReliabilityMonitorManager {
|
|||||||
peer: Peer,
|
peer: Peer,
|
||||||
contentTopics: ContentTopic[]
|
contentTopics: ContentTopic[]
|
||||||
) => Promise<CoreProtocolResult>,
|
) => Promise<CoreProtocolResult>,
|
||||||
addLibp2pEventListener: Libp2p["addEventListener"]
|
addLibp2pEventListener: Libp2p["addEventListener"],
|
||||||
|
sendLightPushMessage: (peer: Peer) => Promise<void>
|
||||||
): ReceiverReliabilityMonitor {
|
): ReceiverReliabilityMonitor {
|
||||||
if (ReliabilityMonitorManager.receiverMonitors.has(pubsubTopic)) {
|
if (ReliabilityMonitorManager.receiverMonitors.has(pubsubTopic)) {
|
||||||
return ReliabilityMonitorManager.receiverMonitors.get(pubsubTopic)!;
|
return ReliabilityMonitorManager.receiverMonitors.get(pubsubTopic)!;
|
||||||
@ -36,7 +37,8 @@ export class ReliabilityMonitorManager {
|
|||||||
renewPeer,
|
renewPeer,
|
||||||
getContentTopics,
|
getContentTopics,
|
||||||
protocolSubscribe,
|
protocolSubscribe,
|
||||||
addLibp2pEventListener
|
addLibp2pEventListener,
|
||||||
|
sendLightPushMessage
|
||||||
);
|
);
|
||||||
ReliabilityMonitorManager.receiverMonitors.set(pubsubTopic, monitor);
|
ReliabilityMonitorManager.receiverMonitors.set(pubsubTopic, monitor);
|
||||||
return monitor;
|
return monitor;
|
||||||
@ -50,7 +52,6 @@ export class ReliabilityMonitorManager {
|
|||||||
|
|
||||||
public static stopAll(): void {
|
public static stopAll(): void {
|
||||||
for (const [pubsubTopic, monitor] of this.receiverMonitors) {
|
for (const [pubsubTopic, monitor] of this.receiverMonitors) {
|
||||||
monitor.setMaxMissedMessagesThreshold(undefined);
|
|
||||||
monitor.setMaxPingFailures(undefined);
|
monitor.setMaxPingFailures(undefined);
|
||||||
this.receiverMonitors.delete(pubsubTopic);
|
this.receiverMonitors.delete(pubsubTopic);
|
||||||
}
|
}
|
||||||
|
@ -8,24 +8,20 @@ import {
|
|||||||
PubsubTopic
|
PubsubTopic
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { messageHashStr } from "@waku/message-hash";
|
import { messageHashStr } from "@waku/message-hash";
|
||||||
import { WakuMessage } from "@waku/proto";
|
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
|
import { bytesToUtf8 } from "@waku/utils/bytes";
|
||||||
type ReceivedMessageHashes = {
|
|
||||||
all: Set<string>;
|
|
||||||
nodes: Record<PeerIdStr, Set<string>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_MAX_MISSED_MESSAGES_THRESHOLD = 3;
|
|
||||||
|
|
||||||
const log = new Logger("sdk:receiver:reliability_monitor");
|
const log = new Logger("sdk:receiver:reliability_monitor");
|
||||||
|
|
||||||
const DEFAULT_MAX_PINGS = 3;
|
const DEFAULT_MAX_PINGS = 3;
|
||||||
|
const MESSAGE_VERIFICATION_DELAY = 5_000;
|
||||||
|
|
||||||
export class ReceiverReliabilityMonitor {
|
export class ReceiverReliabilityMonitor {
|
||||||
private receivedMessagesHashes: ReceivedMessageHashes;
|
private receivedMessagesFormPeer = new Set<string>();
|
||||||
private missedMessagesByPeer: Map<string, number> = new Map();
|
private receivedMessages = new Set<string>();
|
||||||
private maxMissedMessagesThreshold = DEFAULT_MAX_MISSED_MESSAGES_THRESHOLD;
|
private scheduledVerification = new Map<string, number>();
|
||||||
|
private verifiedPeers = new Set<string>();
|
||||||
|
|
||||||
private peerFailures: Map<string, number> = new Map();
|
private peerFailures: Map<string, number> = new Map();
|
||||||
private maxPingFailures: number = DEFAULT_MAX_PINGS;
|
private maxPingFailures: number = DEFAULT_MAX_PINGS;
|
||||||
private peerRenewalLocks: Set<PeerIdStr> = new Set();
|
private peerRenewalLocks: Set<PeerIdStr> = new Set();
|
||||||
@ -40,18 +36,9 @@ export class ReceiverReliabilityMonitor {
|
|||||||
peer: Peer,
|
peer: Peer,
|
||||||
contentTopics: ContentTopic[]
|
contentTopics: ContentTopic[]
|
||||||
) => Promise<CoreProtocolResult>,
|
) => Promise<CoreProtocolResult>,
|
||||||
private addLibp2pEventListener: Libp2p["addEventListener"]
|
private addLibp2pEventListener: Libp2p["addEventListener"],
|
||||||
|
private sendLightPushMessage: (peer: Peer) => Promise<void>
|
||||||
) {
|
) {
|
||||||
const allPeerIdStr = this.getPeers().map((p) => p.id.toString());
|
|
||||||
|
|
||||||
this.receivedMessagesHashes = {
|
|
||||||
all: new Set(),
|
|
||||||
nodes: {
|
|
||||||
...Object.fromEntries(allPeerIdStr.map((peerId) => [peerId, new Set()]))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
allPeerIdStr.forEach((peerId) => this.missedMessagesByPeer.set(peerId, 0));
|
|
||||||
|
|
||||||
this.addLibp2pEventListener("peer:disconnect", (evt) => {
|
this.addLibp2pEventListener("peer:disconnect", (evt) => {
|
||||||
const peerId = evt.detail;
|
const peerId = evt.detail;
|
||||||
if (this.getPeers().some((p) => p.id.equals(peerId))) {
|
if (this.getPeers().some((p) => p.id.equals(peerId))) {
|
||||||
@ -60,13 +47,6 @@ export class ReceiverReliabilityMonitor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMaxMissedMessagesThreshold(value: number | undefined): void {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.maxMissedMessagesThreshold = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMaxPingFailures(value: number | undefined): void {
|
public setMaxPingFailures(value: number | undefined): void {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return;
|
return;
|
||||||
@ -88,6 +68,9 @@ export class ReceiverReliabilityMonitor {
|
|||||||
|
|
||||||
if (failures >= this.maxPingFailures) {
|
if (failures >= this.maxPingFailures) {
|
||||||
try {
|
try {
|
||||||
|
log.info(
|
||||||
|
`Attempting to renew ${peerId.toString()} due to ping failures.`
|
||||||
|
);
|
||||||
await this.renewAndSubscribePeer(peerId);
|
await this.renewAndSubscribePeer(peerId);
|
||||||
this.peerFailures.delete(peerId.toString());
|
this.peerFailures.delete(peerId.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -96,77 +79,79 @@ export class ReceiverReliabilityMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public processIncomingMessage(
|
public notifyMessageReceived(
|
||||||
message: WakuMessage,
|
peerIdStr: string,
|
||||||
pubsubTopic: PubsubTopic,
|
message: IProtoMessage
|
||||||
peerIdStr?: string
|
|
||||||
): boolean {
|
): boolean {
|
||||||
const alreadyReceived = this.addMessageToCache(
|
const hash = this.buildMessageHash(message);
|
||||||
message,
|
|
||||||
pubsubTopic,
|
this.verifiedPeers.add(peerIdStr);
|
||||||
peerIdStr
|
this.receivedMessagesFormPeer.add(`${peerIdStr}-${hash}`);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`notifyMessage received debug: ephemeral:${message.ephemeral}\t${bytesToUtf8(message.payload)}`
|
||||||
);
|
);
|
||||||
void this.checkAndRenewPeers();
|
log.info(`notifyMessage received: peer:${peerIdStr}\tmessage:${hash}`);
|
||||||
return alreadyReceived;
|
|
||||||
|
if (this.receivedMessages.has(hash)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.receivedMessages.add(hash);
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addMessageToCache(
|
public notifyMessageSent(peerId: PeerId, message: IProtoMessage): void {
|
||||||
message: WakuMessage,
|
const peerIdStr = peerId.toString();
|
||||||
pubsubTopic: PubsubTopic,
|
const hash = this.buildMessageHash(message);
|
||||||
peerIdStr?: string
|
|
||||||
): boolean {
|
|
||||||
const hashedMessageStr = messageHashStr(
|
|
||||||
pubsubTopic,
|
|
||||||
message as IProtoMessage
|
|
||||||
);
|
|
||||||
|
|
||||||
const alreadyReceived =
|
log.info(`notifyMessage sent debug: ${bytesToUtf8(message.payload)}`);
|
||||||
this.receivedMessagesHashes.all.has(hashedMessageStr);
|
|
||||||
this.receivedMessagesHashes.all.add(hashedMessageStr);
|
|
||||||
|
|
||||||
if (peerIdStr) {
|
if (this.scheduledVerification.has(peerIdStr)) {
|
||||||
const hashesForPeer = this.receivedMessagesHashes.nodes[peerIdStr];
|
log.warn(
|
||||||
if (!hashesForPeer) {
|
`notifyMessage sent: attempting to schedule verification for pending peer:${peerIdStr}\tmessage:${hash}`
|
||||||
log.warn(
|
);
|
||||||
`Peer ${peerIdStr} not initialized in receivedMessagesHashes.nodes, adding it.`
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = window.setTimeout(
|
||||||
|
(async () => {
|
||||||
|
const receivedAnyMessage = this.verifiedPeers.has(peerIdStr);
|
||||||
|
const receivedTestMessage = this.receivedMessagesFormPeer.has(
|
||||||
|
`${peerIdStr}-${hash}`
|
||||||
);
|
);
|
||||||
this.receivedMessagesHashes.nodes[peerIdStr] = new Set();
|
|
||||||
}
|
|
||||||
this.receivedMessagesHashes.nodes[peerIdStr].add(hashedMessageStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return alreadyReceived;
|
if (receivedAnyMessage || receivedTestMessage) {
|
||||||
|
log.info(
|
||||||
|
`notifyMessage sent setTimeout: verified that peer pushes filter messages, peer:${peerIdStr}\tmessage:${hash}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
`notifyMessage sent setTimeout: peer didn't return probe message, attempting renewAndSubscribe, peer:${peerIdStr}\tmessage:${hash}`
|
||||||
|
);
|
||||||
|
this.scheduledVerification.delete(peerIdStr);
|
||||||
|
await this.renewAndSubscribePeer(peerId);
|
||||||
|
}) as () => void,
|
||||||
|
MESSAGE_VERIFICATION_DELAY
|
||||||
|
);
|
||||||
|
|
||||||
|
this.scheduledVerification.set(peerIdStr, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkAndRenewPeers(): Promise<void> {
|
public shouldVerifyPeer(peerId: PeerId): boolean {
|
||||||
for (const hash of this.receivedMessagesHashes.all) {
|
const peerIdStr = peerId.toString();
|
||||||
for (const [peerIdStr, hashes] of Object.entries(
|
|
||||||
this.receivedMessagesHashes.nodes
|
const isPeerVerified = this.verifiedPeers.has(peerIdStr);
|
||||||
)) {
|
const isVerificationPending = this.scheduledVerification.has(peerIdStr);
|
||||||
if (!hashes.has(hash)) {
|
|
||||||
this.incrementMissedMessageCount(peerIdStr);
|
return !(isPeerVerified || isVerificationPending);
|
||||||
if (this.shouldRenewPeer(peerIdStr)) {
|
}
|
||||||
log.info(
|
|
||||||
`Peer ${peerIdStr} has missed too many messages, renewing.`
|
private buildMessageHash(message: IProtoMessage): string {
|
||||||
);
|
return messageHashStr(this.pubsubTopic, message);
|
||||||
const peerId = this.getPeers().find(
|
|
||||||
(p) => p.id.toString() === peerIdStr
|
|
||||||
)?.id;
|
|
||||||
if (!peerId) {
|
|
||||||
log.error(
|
|
||||||
`Unexpected Error: Peer ${peerIdStr} not found in connected peers.`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.renewAndSubscribePeer(peerId);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(`Failed to renew peer ${peerIdStr}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renewAndSubscribePeer(
|
private async renewAndSubscribePeer(
|
||||||
@ -193,12 +178,9 @@ export class ReceiverReliabilityMonitor {
|
|||||||
this.getContentTopics()
|
this.getContentTopics()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.receivedMessagesHashes.nodes[newPeer.id.toString()] = new Set();
|
await this.sendLightPushMessage(newPeer);
|
||||||
this.missedMessagesByPeer.set(newPeer.id.toString(), 0);
|
|
||||||
|
|
||||||
this.peerFailures.delete(peerIdStr);
|
this.peerFailures.delete(peerIdStr);
|
||||||
this.missedMessagesByPeer.delete(peerIdStr);
|
|
||||||
delete this.receivedMessagesHashes.nodes[peerIdStr];
|
|
||||||
|
|
||||||
return newPeer;
|
return newPeer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -208,14 +190,4 @@ export class ReceiverReliabilityMonitor {
|
|||||||
this.peerRenewalLocks.delete(peerIdStr);
|
this.peerRenewalLocks.delete(peerIdStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private incrementMissedMessageCount(peerIdStr: string): void {
|
|
||||||
const currentCount = this.missedMessagesByPeer.get(peerIdStr) || 0;
|
|
||||||
this.missedMessagesByPeer.set(peerIdStr, currentCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldRenewPeer(peerIdStr: string): boolean {
|
|
||||||
const missedMessages = this.missedMessagesByPeer.get(peerIdStr) || 0;
|
|
||||||
return missedMessages > this.maxMissedMessagesThreshold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,11 @@ export class WakuNode implements IWaku {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (protocolsEnabled.filter) {
|
if (protocolsEnabled.filter) {
|
||||||
const filter = wakuFilter(this.connectionManager, options);
|
const filter = wakuFilter(
|
||||||
|
this.connectionManager,
|
||||||
|
this.lightPush,
|
||||||
|
options
|
||||||
|
);
|
||||||
this.filter = filter(libp2p);
|
this.filter = filter(libp2p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user