Files
Baradb/src/barabadb/core/gossip.nim
T
dimgigov 71dcffecce fix(gossip): use async UDP socket to avoid blocking the event loop
The gossip listener used synchronous newSocket + recvFrom,
which blocks the async event loop and prevents other async
operations from running while waiting for UDP packets.

Switch to newAsyncSocket + async recvFrom so the loop can
continue processing other tasks between gossip rounds.
2026-05-14 02:22:07 +03:00

341 lines
11 KiB
Nim

## Gossip Protocol — membership and failure detection with UDP transport
import std/tables
import std/random
import std/monotimes
import std/asyncdispatch
import std/net
import std/asyncnet
import std/strutils
import std/streams
import std/os
type
NodeState* = enum
nsAlive
nsSuspect
nsDead
GossipNode* = ref object
id*: string
host*: string
port*: int
state*: NodeState
incarnation*: uint64
lastSeen*: int64
metadata*: Table[string, string]
GossipMessage* = object
senderId*: string
senderIncarnation*: uint64
nodes*: seq[(string, NodeState, uint64, string, int)] # (id, state, incarnation, host, port)
GossipProtocol* = ref object
self*: GossipNode
members*: Table[string, GossipNode]
suspectTimeout*: int64 # nanoseconds
deadTimeout*: int64 # nanoseconds
fanout*: int # number of nodes to gossip to per round
gossipPort*: int
running*: bool
sock*: AsyncSocket
onJoin*: proc(node: GossipNode) {.gcsafe.}
onLeave*: proc(nodeId: string) {.gcsafe.}
onSuspect*: proc(nodeId: string) {.gcsafe.}
onMembershipChanged*: proc() {.gcsafe.}
# ---------------------------------------------------------------------------
# GossipMessage binary serialization (for UDP transport)
# ---------------------------------------------------------------------------
const GossipMagic = "GOSS"
const GossipProtoVersion = 1'u32
proc serialize*(msg: GossipMessage): seq[byte] =
let s = newStringStream()
s.write(GossipMagic)
s.write(GossipProtoVersion)
s.write(uint32(msg.senderId.len))
if msg.senderId.len > 0:
s.writeData(msg.senderId[0].unsafeAddr, msg.senderId.len)
s.write(msg.senderIncarnation)
s.write(uint32(msg.nodes.len))
for (id, state, incarnation, host, port) in msg.nodes:
s.write(uint32(id.len))
if id.len > 0:
s.writeData(id[0].unsafeAddr, id.len)
s.write(uint32(ord(state)))
s.write(incarnation)
s.write(uint32(host.len))
if host.len > 0:
s.writeData(host[0].unsafeAddr, host.len)
s.write(uint32(port))
let strData = s.data
result = newSeq[byte](strData.len)
for i in 0 ..< strData.len:
result[i] = byte(strData[i])
s.close()
proc deserializeGossipMessage*(data: seq[byte]): GossipMessage =
let s = newStringStream(cast[string](data))
let magic = s.readStr(4)
if magic != GossipMagic:
raise newException(ValueError, "Invalid gossip magic")
let version = s.readUint32()
if version != GossipProtoVersion:
raise newException(ValueError, "Unsupported gossip protocol version")
let senderIdLen = int(s.readUint32())
result.senderId = if senderIdLen > 0: s.readStr(senderIdLen) else: ""
result.senderIncarnation = s.readUint64()
let nodeCount = int(s.readUint32())
result.nodes = newSeq[(string, NodeState, uint64, string, int)](nodeCount)
for i in 0 ..< nodeCount:
let idLen = int(s.readUint32())
let id = if idLen > 0: s.readStr(idLen) else: ""
let state = NodeState(s.readUint32())
let incarnation = s.readUint64()
let hostLen = int(s.readUint32())
let host = if hostLen > 0: s.readStr(hostLen) else: ""
let port = int(s.readUint32())
result.nodes[i] = (id, state, incarnation, host, port)
s.close()
# ---------------------------------------------------------------------------
# Core Gossip Protocol
# ---------------------------------------------------------------------------
proc newGossipNode*(id: string, host: string, port: int): GossipNode =
GossipNode(
id: id, host: host, port: port,
state: nsAlive, incarnation: 1,
lastSeen: getMonoTime().ticks(),
metadata: initTable[string, string](),
)
proc newGossipProtocol*(id: string, host: string, port: int,
fanout: int = 3, gossipPort: int = 0): GossipProtocol =
let self = newGossipNode(id, host, port)
GossipProtocol(
self: self,
members: initTable[string, GossipNode](),
suspectTimeout: 5_000_000_000, # 5 seconds
deadTimeout: 15_000_000_000, # 15 seconds
fanout: fanout,
gossipPort: gossipPort,
running: false,
sock: nil,
onJoin: nil,
onLeave: nil,
onSuspect: nil,
onMembershipChanged: nil,
)
proc join*(gp: GossipProtocol, seedNode: GossipNode) =
gp.members[seedNode.id] = seedNode
if gp.onJoin != nil:
gp.onJoin(seedNode)
if gp.onMembershipChanged != nil:
gp.onMembershipChanged()
proc addMember*(gp: GossipProtocol, node: GossipNode) =
if node.id == gp.self.id:
return
let existed = node.id in gp.members
gp.members[node.id] = node
if not existed and gp.onJoin != nil:
gp.onJoin(node)
if gp.onMembershipChanged != nil:
gp.onMembershipChanged()
proc removeMember*(gp: GossipProtocol, nodeId: string) =
if nodeId in gp.members:
gp.members.del(nodeId)
if gp.onLeave != nil:
gp.onLeave(nodeId)
if gp.onMembershipChanged != nil:
gp.onMembershipChanged()
proc suspect*(gp: GossipProtocol, nodeId: string) =
if nodeId in gp.members:
gp.members[nodeId].state = nsSuspect
if gp.onSuspect != nil:
gp.onSuspect(nodeId)
proc declareDead*(gp: GossipProtocol, nodeId: string) =
if nodeId in gp.members:
gp.members[nodeId].state = nsDead
if gp.onLeave != nil:
gp.onLeave(nodeId)
if gp.onMembershipChanged != nil:
gp.onMembershipChanged()
proc createGossipMessage*(gp: GossipProtocol): GossipMessage =
result = GossipMessage(
senderId: gp.self.id,
senderIncarnation: gp.self.incarnation,
nodes: @[],
)
for id, node in gp.members:
result.nodes.add((id, node.state, node.incarnation, node.host, node.port))
proc applyGossipMessage*(gp: GossipProtocol, msg: GossipMessage) =
for (nodeId, state, incarnation, host, port) in msg.nodes:
if nodeId == gp.self.id:
if state == nsSuspect and incarnation >= gp.self.incarnation:
inc gp.self.incarnation
continue
if nodeId in gp.members:
let existing = gp.members[nodeId]
if incarnation > existing.incarnation:
existing.state = state
existing.incarnation = incarnation
existing.lastSeen = getMonoTime().ticks()
if host.len > 0: existing.host = host
if port > 0: existing.port = port
if state == nsDead:
gp.removeMember(nodeId)
elif incarnation == existing.incarnation and state == nsDead:
existing.state = nsDead
gp.removeMember(nodeId)
else:
if state != nsDead:
let newNode = GossipNode(
id: nodeId, host: host, port: port,
state: state, incarnation: incarnation,
lastSeen: getMonoTime().ticks(),
)
gp.addMember(newNode)
if gp.onMembershipChanged != nil:
gp.onMembershipChanged()
proc selectGossipTargets*(gp: GossipProtocol): seq[GossipNode] =
var alive: seq[GossipNode] = @[]
for id, node in gp.members:
if node.state == nsAlive and node.host.len > 0 and node.port > 0:
alive.add(node)
result = @[]
let count = min(gp.fanout, alive.len)
for i in 0..<count:
let idx = rand(alive.len - 1)
result.add(alive[idx])
alive.delete(idx)
proc checkHealth*(gp: GossipProtocol) =
let now = getMonoTime().ticks()
var toRemove: seq[string] = @[]
for id, node in gp.members:
let elapsed = now - node.lastSeen
if node.state == nsAlive and elapsed > gp.suspectTimeout:
node.state = nsSuspect
if gp.onSuspect != nil:
gp.onSuspect(id)
elif node.state == nsSuspect and elapsed > gp.deadTimeout:
toRemove.add(id)
for id in toRemove:
gp.declareDead(id)
proc aliveMembers*(gp: GossipProtocol): seq[GossipNode] =
result = @[]
for id, node in gp.members:
if node.state == nsAlive:
result.add(node)
proc memberCount*(gp: GossipProtocol): int = gp.members.len
proc aliveCount*(gp: GossipProtocol): int = gp.aliveMembers().len
proc memberIds*(gp: GossipProtocol): seq[string] =
result = @[]
for id in gp.members.keys:
result.add(id)
proc isMember*(gp: GossipProtocol, nodeId: string): bool =
return nodeId in gp.members
proc getMember*(gp: GossipProtocol, nodeId: string): GossipNode =
gp.members.getOrDefault(nodeId, nil)
# ---------------------------------------------------------------------------
# UDP transport layer (using blocking sockets for simplicity)
# ---------------------------------------------------------------------------
proc sendGossipUdp(gp: GossipProtocol, target: GossipNode, msg: GossipMessage) =
if target.host.len == 0 or target.port == 0:
return
try:
let sock = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
let data = serialize(msg)
sock.sendTo(target.host, Port(target.port), cast[string](data))
sock.close()
except:
discard
proc broadcastGossip(gp: GossipProtocol) =
let msg = gp.createGossipMessage()
let targets = gp.selectGossipTargets()
for target in targets:
gp.sendGossipUdp(target, msg)
proc handleIncomingGossip(gp: GossipProtocol, data: string, senderAddr: string) =
try:
let msg = deserializeGossipMessage(cast[seq[byte]](data))
if msg.senderId in gp.members:
gp.members[msg.senderId].lastSeen = getMonoTime().ticks()
elif msg.senderId != gp.self.id:
var host = senderAddr
if ':' in host:
host = host.split(":")[0]
let newNode = GossipNode(
id: msg.senderId, host: host, port: gp.gossipPort,
state: nsAlive, incarnation: msg.senderIncarnation,
lastSeen: getMonoTime().ticks(),
)
gp.addMember(newNode)
gp.applyGossipMessage(msg)
except:
discard
proc startHealthCheck*(gp: GossipProtocol, intervalMs: int = 1000) {.async.} =
while gp.running:
await sleepAsync(intervalMs)
gp.checkHealth()
proc startGossipRound*(gp: GossipProtocol, intervalMs: int = 2000) {.async.} =
while gp.running:
await sleepAsync(intervalMs)
gp.broadcastGossip()
proc startGossipListener*(gp: GossipProtocol) {.async.} =
gp.sock = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock.setSockOpt(OptReuseAddr, true)
gp.sock.bindAddr(Port(gp.gossipPort))
gp.running = true
while gp.running:
try:
let (data, senderAddr, senderPort) = await gp.sock.recvFrom(65535)
if data.len > 0:
gp.handleIncomingGossip(data, senderAddr)
except:
# Small sleep on error to avoid spin
gp.sock.close()
# Recreate socket for next iteration
try:
gp.sock = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock.setSockOpt(OptReuseAddr, true)
gp.sock.bindAddr(Port(gp.gossipPort))
except:
break
proc startGossip*(gp: GossipProtocol, healthIntervalMs: int = 1000,
gossipIntervalMs: int = 2000) =
gp.running = true
asyncCheck gp.startGossipListener()
asyncCheck gp.startHealthCheck(healthIntervalMs)
asyncCheck gp.startGossipRound(gossipIntervalMs)
proc stop*(gp: GossipProtocol) =
gp.running = false
if gp.sock != nil:
gp.sock.close()
gp.sock = nil