mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-08-25 15:31:16 +00:00
feat: active filter subscription management for edge nodes (#3773)
feat: active filter subscription management for edge nodes ## Subscription Manager * edgeFilterSubLoop reconciles desired vs actual filter subscriptions * edgeFilterHealthLoop pings filter peers, evicts stale ones * EdgeFilterSubState per-shard tracking of confirmed peers and health * best-effort unsubscribe on peer removal * RequestEdgeShardHealth and RequestEdgeFilterPeerCount broker providers ## WakuNode * Remove old edge health loop (loopEdgeHealth, edgeHealthEvent, calculateEdgeTopicHealth) * Register MessageSeenEvent push handler on filter client during start * startDeliveryService now returns `Result[void, string]` and propagates errors ## Health Monitor * getFilterClientHealth queries RequestEdgeFilterPeerCount via broker * Shard/content health providers fall back to RequestEdgeShardHealth when relay inactive * Listen to EventShardTopicHealthChange for health recalculation * Add missing return p.notReady() on failed edge filter peer count request * HealthyThreshold constant moved to `connection_status.nim` ## Broker types * RequestEdgeShardHealth, RequestEdgeFilterPeerCount request types * EventShardTopicHealthChange event type ## Filter Client * Add timeout parameter to ping proc ## Tests * Health monitor event tests with per-node lockNewGlobalBrokerContext * Edge (light client) health update test * Edge health driven by confirmed filter subscriptions test * API subscription tests: sub/receive, failover, peer replacement Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Co-authored by Zoltan Nagy
This commit is contained in:
@@ -12,12 +12,18 @@ import
|
||||
node/health_monitor/health_status,
|
||||
node/health_monitor/connection_status,
|
||||
node/health_monitor/protocol_health,
|
||||
node/health_monitor/topic_health,
|
||||
node/health_monitor/node_health_monitor,
|
||||
node/delivery_service/delivery_service,
|
||||
node/delivery_service/subscription_manager,
|
||||
node/kernel_api/relay,
|
||||
node/kernel_api/store,
|
||||
node/kernel_api/lightpush,
|
||||
node/kernel_api/filter,
|
||||
events/health_events,
|
||||
events/peer_events,
|
||||
waku_archive,
|
||||
common/broker/broker_context,
|
||||
]
|
||||
|
||||
import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils
|
||||
@@ -129,13 +135,12 @@ suite "Health Monitor - health state calculation":
|
||||
|
||||
suite "Health Monitor - events":
|
||||
asyncTest "Core (relay) health update":
|
||||
let
|
||||
nodeAKey = generateSecp256k1Key()
|
||||
var nodeA: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeAKey = generateSecp256k1Key()
|
||||
nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
|
||||
(await nodeA.mountRelay()).expect("Node A failed to mount Relay")
|
||||
|
||||
await nodeA.start()
|
||||
(await nodeA.mountRelay()).expect("Node A failed to mount Relay")
|
||||
await nodeA.start()
|
||||
|
||||
let monitorA = NodeHealthMonitor.new(nodeA)
|
||||
|
||||
@@ -151,17 +156,15 @@ suite "Health Monitor - events":
|
||||
|
||||
monitorA.startHealthMonitor().expect("Health monitor failed to start")
|
||||
|
||||
let
|
||||
nodeBKey = generateSecp256k1Key()
|
||||
var nodeB: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeBKey = generateSecp256k1Key()
|
||||
nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
|
||||
let driver = newSqliteArchiveDriver()
|
||||
nodeB.mountArchive(driver).expect("Node B failed to mount archive")
|
||||
|
||||
(await nodeB.mountRelay()).expect("Node B failed to mount relay")
|
||||
await nodeB.mountStore()
|
||||
|
||||
await nodeB.start()
|
||||
let driver = newSqliteArchiveDriver()
|
||||
nodeB.mountArchive(driver).expect("Node B failed to mount archive")
|
||||
(await nodeB.mountRelay()).expect("Node B failed to mount relay")
|
||||
await nodeB.mountStore()
|
||||
await nodeB.start()
|
||||
|
||||
await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()])
|
||||
|
||||
@@ -214,15 +217,20 @@ suite "Health Monitor - events":
|
||||
await nodeA.stop()
|
||||
|
||||
asyncTest "Edge (light client) health update":
|
||||
let
|
||||
nodeAKey = generateSecp256k1Key()
|
||||
var nodeA: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeAKey = generateSecp256k1Key()
|
||||
nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
nodeA.mountLightpushClient()
|
||||
await nodeA.mountFilterClient()
|
||||
nodeA.mountStoreClient()
|
||||
require nodeA.mountAutoSharding(1, 8).isOk
|
||||
nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata")
|
||||
await nodeA.start()
|
||||
|
||||
nodeA.mountLightpushClient()
|
||||
await nodeA.mountFilterClient()
|
||||
nodeA.mountStoreClient()
|
||||
|
||||
await nodeA.start()
|
||||
let ds =
|
||||
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService")
|
||||
ds.startDeliveryService().expect("Failed to start DeliveryService")
|
||||
|
||||
let monitorA = NodeHealthMonitor.new(nodeA)
|
||||
|
||||
@@ -238,23 +246,40 @@ suite "Health Monitor - events":
|
||||
|
||||
monitorA.startHealthMonitor().expect("Health monitor failed to start")
|
||||
|
||||
let
|
||||
nodeBKey = generateSecp256k1Key()
|
||||
var nodeB: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeBKey = generateSecp256k1Key()
|
||||
nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
let driver = newSqliteArchiveDriver()
|
||||
nodeB.mountArchive(driver).expect("Node B failed to mount archive")
|
||||
(await nodeB.mountRelay()).expect("Node B failed to mount relay")
|
||||
(await nodeB.mountLightpush()).expect("Node B failed to mount lightpush")
|
||||
await nodeB.mountFilter()
|
||||
await nodeB.mountStore()
|
||||
require nodeB.mountAutoSharding(1, 8).isOk
|
||||
nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect(
|
||||
"Node B failed to mount metadata"
|
||||
)
|
||||
await nodeB.start()
|
||||
|
||||
let driver = newSqliteArchiveDriver()
|
||||
nodeB.mountArchive(driver).expect("Node B failed to mount archive")
|
||||
|
||||
(await nodeB.mountRelay()).expect("Node B failed to mount relay")
|
||||
|
||||
(await nodeB.mountLightpush()).expect("Node B failed to mount lightpush")
|
||||
await nodeB.mountFilter()
|
||||
await nodeB.mountStore()
|
||||
|
||||
await nodeB.start()
|
||||
var metadataFut = newFuture[void]("waitForMetadata")
|
||||
let metadataLis = WakuPeerEvent
|
||||
.listen(
|
||||
nodeA.brokerCtx,
|
||||
proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} =
|
||||
if not metadataFut.finished and
|
||||
evt.kind == WakuPeerEventKind.EventMetadataUpdated:
|
||||
metadataFut.complete()
|
||||
,
|
||||
)
|
||||
.expect("Failed to listen for metadata")
|
||||
|
||||
await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()])
|
||||
|
||||
let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit)
|
||||
WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
|
||||
require metadataOk
|
||||
|
||||
let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit
|
||||
var gotConnected = false
|
||||
|
||||
@@ -292,4 +317,118 @@ suite "Health Monitor - events":
|
||||
lastStatus == ConnectionStatus.Disconnected
|
||||
|
||||
await monitorA.stopHealthMonitor()
|
||||
await ds.stopDeliveryService()
|
||||
await nodeA.stop()
|
||||
|
||||
asyncTest "Edge health driven by confirmed filter subscriptions":
|
||||
var nodeA: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeAKey = generateSecp256k1Key()
|
||||
nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
await nodeA.mountFilterClient()
|
||||
nodeA.mountLightpushClient()
|
||||
nodeA.mountStoreClient()
|
||||
require nodeA.mountAutoSharding(1, 8).isOk
|
||||
nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata")
|
||||
await nodeA.start()
|
||||
|
||||
let ds =
|
||||
DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService")
|
||||
ds.startDeliveryService().expect("Failed to start DeliveryService")
|
||||
let subMgr = ds.subscriptionManager
|
||||
|
||||
var nodeB: WakuNode
|
||||
lockNewGlobalBrokerContext:
|
||||
let nodeBKey = generateSecp256k1Key()
|
||||
nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0))
|
||||
let driver = newSqliteArchiveDriver()
|
||||
nodeB.mountArchive(driver).expect("Node B failed to mount archive")
|
||||
(await nodeB.mountRelay()).expect("Node B failed to mount relay")
|
||||
(await nodeB.mountLightpush()).expect("Node B failed to mount lightpush")
|
||||
await nodeB.mountFilter()
|
||||
await nodeB.mountStore()
|
||||
require nodeB.mountAutoSharding(1, 8).isOk
|
||||
nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect(
|
||||
"Node B failed to mount metadata"
|
||||
)
|
||||
await nodeB.start()
|
||||
|
||||
let monitorA = NodeHealthMonitor.new(nodeA)
|
||||
|
||||
var
|
||||
lastStatus = ConnectionStatus.Disconnected
|
||||
healthSignal = newAsyncEvent()
|
||||
|
||||
monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} =
|
||||
lastStatus = status
|
||||
healthSignal.fire()
|
||||
|
||||
monitorA.startHealthMonitor().expect("Health monitor failed to start")
|
||||
|
||||
var metadataFut = newFuture[void]("waitForMetadata")
|
||||
let metadataLis = WakuPeerEvent
|
||||
.listen(
|
||||
nodeA.brokerCtx,
|
||||
proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} =
|
||||
if not metadataFut.finished and
|
||||
evt.kind == WakuPeerEventKind.EventMetadataUpdated:
|
||||
metadataFut.complete()
|
||||
,
|
||||
)
|
||||
.expect("Failed to listen for metadata")
|
||||
|
||||
await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()])
|
||||
|
||||
let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit)
|
||||
WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis)
|
||||
require metadataOk
|
||||
|
||||
var deadline = Moment.now() + TestConnectivityTimeLimit
|
||||
while Moment.now() < deadline:
|
||||
if lastStatus == ConnectionStatus.PartiallyConnected:
|
||||
break
|
||||
if await healthSignal.wait().withTimeout(deadline - Moment.now()):
|
||||
healthSignal.clear()
|
||||
|
||||
check lastStatus == ConnectionStatus.PartiallyConnected
|
||||
|
||||
var shardHealthFut = newFuture[EventShardTopicHealthChange]("waitForShardHealth")
|
||||
|
||||
let shardHealthLis = EventShardTopicHealthChange
|
||||
.listen(
|
||||
nodeA.brokerCtx,
|
||||
proc(
|
||||
evt: EventShardTopicHealthChange
|
||||
): Future[void] {.async: (raises: []), gcsafe.} =
|
||||
if not shardHealthFut.finished and (
|
||||
evt.health == TopicHealth.MINIMALLY_HEALTHY or
|
||||
evt.health == TopicHealth.SUFFICIENTLY_HEALTHY
|
||||
):
|
||||
shardHealthFut.complete(evt)
|
||||
,
|
||||
)
|
||||
.expect("Failed to listen for shard health")
|
||||
|
||||
let contentTopic = ContentTopic("/waku/2/default-content/proto")
|
||||
subMgr.subscribe(contentTopic).expect("Failed to subscribe")
|
||||
|
||||
let shardHealthOk = await shardHealthFut.withTimeout(TestConnectivityTimeLimit)
|
||||
EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis)
|
||||
|
||||
check shardHealthOk == true
|
||||
check subMgr.edgeFilterSubStates.len > 0
|
||||
|
||||
healthSignal.clear()
|
||||
deadline = Moment.now() + TestConnectivityTimeLimit
|
||||
while Moment.now() < deadline:
|
||||
if lastStatus == ConnectionStatus.PartiallyConnected:
|
||||
break
|
||||
if await healthSignal.wait().withTimeout(deadline - Moment.now()):
|
||||
healthSignal.clear()
|
||||
|
||||
check lastStatus == ConnectionStatus.PartiallyConnected
|
||||
|
||||
await ds.stopDeliveryService()
|
||||
await monitorA.stopHealthMonitor()
|
||||
await nodeB.stop()
|
||||
await nodeA.stop()
|
||||
|
||||
Reference in New Issue
Block a user