Skip to content

Commit 2e329e4

Browse files
committed
refactor(mesh-conn): hoist pollLoop/runPeerLink to *Mesh methods, add ctx shutdown
In-process test (next commit) needs two mesh-conn instances side by side. Package-global `sessions` map collides across instances; same for pollLoop's hardcoded background-context HTTP polling. Move the session registry into a new Mesh struct, hoist the polling + per-peer link functions to methods on it, and thread a ctx through runPeerLink/dialAndPump/pollLoop so a test can stop them cleanly. Add an onLinkUp(peerID) hook fired right after the "link up" log line so the test can observe convergence without grepping logs. No behavior change for the production binary — main() builds one Mesh and calls Run(context.Background()).
1 parent 8aba0a5 commit 2e329e4

1 file changed

Lines changed: 110 additions & 41 deletions

File tree

  • consul-postgres-ha/mesh-conn

consul-postgres-ha/mesh-conn/main.go

Lines changed: 110 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -375,25 +375,56 @@ func mustEnv(k string) string {
375375
func main() {
376376
flag.Parse()
377377
cfg := loadConfig()
378-
self := cfg.peerByID(cfg.SelfID)
378+
m := newMesh(cfg)
379+
m.Run(context.Background())
380+
}
381+
382+
// Mesh is one runtime instance — one mesh-conn process. Owns the
383+
// per-peer session registry, signalling URL, and shutdown plumbing.
384+
// Methods that previously closed over the package-level `sessions`
385+
// map now receive *Mesh; main() builds one and calls Run().
386+
//
387+
// Reason for instance-scoping: the in-process loopback test boots two
388+
// Mesh values side by side; a package-global session map collides
389+
// across instances.
390+
type Mesh struct {
391+
cfg *Config
392+
self *Peer
393+
394+
sessionsMu sync.Mutex
395+
sessions map[string]*peerSession
396+
397+
// onLinkUp is an optional test hook fired right after each
398+
// per-peer "link up" log line. nil in production.
399+
onLinkUp func(peerID string)
400+
}
401+
402+
func newMesh(cfg *Config) *Mesh {
403+
return &Mesh{
404+
cfg: cfg,
405+
self: cfg.peerByID(cfg.SelfID),
406+
sessions: map[string]*peerSession{},
407+
}
408+
}
379409

380-
others := make([]Peer, 0, len(cfg.Peers)-1)
381-
for _, p := range cfg.Peers {
382-
if p.ID != cfg.SelfID {
410+
func (m *Mesh) Run(ctx context.Context) {
411+
others := make([]Peer, 0, len(m.cfg.Peers)-1)
412+
for _, p := range m.cfg.Peers {
413+
if p.ID != m.cfg.SelfID {
383414
others = append(others, p)
384415
}
385416
}
386417
log.Printf("mesh-conn: self=%s vip=%d allowlist=%v other=%d",
387-
cfg.SelfID, self.Vip, allowlistPorts(cfg.Allowlist), len(others))
418+
m.cfg.SelfID, m.self.Vip, allowlistPorts(m.cfg.Allowlist), len(others))
388419

389-
go pollLoop(cfg)
420+
go m.pollLoop(ctx)
390421

391422
var wg sync.WaitGroup
392423
for _, p := range others {
393424
wg.Add(1)
394425
go func(p Peer) {
395426
defer wg.Done()
396-
runPeerLink(cfg, *self, p)
427+
m.runPeerLink(ctx, p)
397428
}(p)
398429
}
399430
wg.Wait()
@@ -404,24 +435,43 @@ func main() {
404435
// per-peer link: ICE conn + bound UDP socket on peer's identity port
405436
// =============================================================================
406437

407-
func runPeerLink(cfg *Config, self, peer Peer) {
438+
func (m *Mesh) runPeerLink(ctx context.Context, peer Peer) {
408439
// One peerSession per peer for the lifetime of mesh-conn. The
409440
// session holds authCh + candidateBuffer + abortAttempt — the
410441
// stable wiring pollLoop dispatches through. The ICE agent itself
411442
// is rebuilt per attempt inside dialICE (pion's Restart() doesn't
412443
// support re-Dial; see peerSession docstring).
413-
sess := setupPeerSession(peer.ID)
444+
sess := m.setupPeerSession(peer.ID)
414445
for {
415-
if err := dialAndPump(cfg, self, peer, sess); err != nil {
446+
if ctx.Err() != nil {
447+
return
448+
}
449+
if err := m.dialAndPump(ctx, peer, sess); err != nil {
416450
log.Printf("[%s] link failed: %v — retrying in 5s", peer.ID, err)
417-
time.Sleep(5 * time.Second)
451+
if !sleepCtx(ctx, 5*time.Second) {
452+
return
453+
}
418454
continue
419455
}
420456
// dialAndPump returns nil only when the conn closed cleanly.
421457
log.Printf("[%s] link closed — reconnecting", peer.ID)
422458
}
423459
}
424460

461+
// sleepCtx sleeps for d or until ctx is cancelled. Returns true if
462+
// the full sleep completed (so the caller should continue), false if
463+
// the caller should bail.
464+
func sleepCtx(ctx context.Context, d time.Duration) bool {
465+
t := time.NewTimer(d)
466+
defer t.Stop()
467+
select {
468+
case <-t.C:
469+
return true
470+
case <-ctx.Done():
471+
return false
472+
}
473+
}
474+
425475
// Stream header layout: 3 bytes per stream open.
426476
// byte 0 = tag (streamUDP or streamTCP)
427477
// bytes 1-2 = receiver-side port (big-endian uint16) — the port number
@@ -449,12 +499,16 @@ func quicConfig() *quic.Config {
449499
}
450500
}
451501

452-
func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
502+
func (m *Mesh) dialAndPump(parentCtx context.Context, peer Peer, sess *peerSession) error {
503+
cfg := m.cfg
504+
self := *m.self
453505
// One attemptCtx for the entire attempt — covers dialICE, QUIC dial,
454506
// and the long-running pump goroutines. pollLoop and the ICE state
455507
// callback both wire abortAttempt to this ctx; cancelling it makes
456508
// the whole pile collapse so runPeerLink's 5s retry can re-enter.
457-
attemptCtx, abort := context.WithCancel(context.Background())
509+
// parentCtx (from Mesh.Run) is the outer shutdown signal; cancelling
510+
// it also collapses the attempt.
511+
attemptCtx, abort := context.WithCancel(parentCtx)
458512
sess.mu.Lock()
459513
sess.consumedAuth = [2]string{}
460514
sess.abortAttempt = abort
@@ -471,7 +525,7 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
471525
}()
472526

473527
// 1. Establish ICE + wrap with a counting conn for byte-level telemetry.
474-
rawConn, err := dialICE(cfg, peer.ID, sess, attemptCtx)
528+
rawConn, err := m.dialICE(peer.ID, sess, attemptCtx)
475529
if err != nil {
476530
return fmt.Errorf("ice: %w", err)
477531
}
@@ -596,6 +650,9 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
596650

597651
log.Printf("[%s] link up — peer vip=127.50.0.%d, allowlist=%v (udp=%d, tcp=%d)",
598652
peer.ID, peer.Vip, allowlistPorts(cfg.Allowlist), udpPortCount, len(tcpListeners))
653+
if m.onLinkUp != nil {
654+
m.onLinkUp(peer.ID)
655+
}
599656

600657
// 5. Start pumps. UDP: one bidirectional pump pair per UDP port.
601658
// TCP: one accept-loop per port (each accepted conn opens an
@@ -959,35 +1016,31 @@ type peerSession struct {
9591016
candidateBuffer []ice.Candidate // remote candidates seen since last peer-auth; replayed into each new agent
9601017
}
9611018

962-
var (
963-
sessionsMu sync.Mutex
964-
sessions = map[string]*peerSession{} // key = remote peer id
965-
)
966-
9671019
// currentSession returns the persistent session for remoteID, or nil
9681020
// if setupPeerSession hasn't installed it yet. Used by pollLoop to
9691021
// dispatch incoming messages to the right agent.
970-
func currentSession(remoteID string) *peerSession {
971-
sessionsMu.Lock()
972-
defer sessionsMu.Unlock()
973-
return sessions[remoteID]
1022+
func (m *Mesh) currentSession(remoteID string) *peerSession {
1023+
m.sessionsMu.Lock()
1024+
defer m.sessionsMu.Unlock()
1025+
return m.sessions[remoteID]
9741026
}
9751027

9761028
// setupPeerSession allocates the long-lived session shell (authCh,
9771029
// candidateBuffer, mutex) for one peer. The ICE agent itself is built
9781030
// per-attempt inside dialICE — see the peerSession docstring for why
9791031
// per-attempt rather than persistent.
980-
func setupPeerSession(remoteID string) *peerSession {
1032+
func (m *Mesh) setupPeerSession(remoteID string) *peerSession {
9811033
sess := &peerSession{
9821034
authCh: make(chan [2]string, 1),
9831035
}
984-
sessionsMu.Lock()
985-
sessions[remoteID] = sess
986-
sessionsMu.Unlock()
1036+
m.sessionsMu.Lock()
1037+
m.sessions[remoteID] = sess
1038+
m.sessionsMu.Unlock()
9871039
return sess
9881040
}
9891041

990-
func dialICE(cfg *Config, remoteID string, sess *peerSession, attemptCtx context.Context) (*ice.Conn, error) {
1042+
func (m *Mesh) dialICE(remoteID string, sess *peerSession, attemptCtx context.Context) (*ice.Conn, error) {
1043+
cfg := m.cfg
9911044
var urls []*stun.URI
9921045
if cfg.TurnHost != "" {
9931046
user, pass := turnCreds(cfg.TurnSecret, time.Hour)
@@ -1181,36 +1234,52 @@ func publish(cfg *Config, to, typ, data string) {
11811234
resp.Body.Close()
11821235
}
11831236

1184-
func pollLoop(cfg *Config) {
1237+
func (m *Mesh) pollLoop(ctx context.Context) {
1238+
cfg := m.cfg
1239+
client := &http.Client{}
11851240
for {
1186-
resp, err := http.Get(cfg.SignalingURL + "/poll?peer=" + url.QueryEscape(cfg.SelfID))
1241+
if ctx.Err() != nil {
1242+
return
1243+
}
1244+
req, _ := http.NewRequestWithContext(ctx,
1245+
http.MethodGet,
1246+
cfg.SignalingURL+"/poll?peer="+url.QueryEscape(cfg.SelfID),
1247+
nil)
1248+
resp, err := client.Do(req)
11871249
if err != nil {
1250+
if ctx.Err() != nil {
1251+
return
1252+
}
11881253
log.Printf("poll err: %v", err)
1189-
time.Sleep(time.Second)
1254+
if !sleepCtx(ctx, time.Second) {
1255+
return
1256+
}
11901257
continue
11911258
}
11921259
var msgs []Message
11931260
if err := json.NewDecoder(resp.Body).Decode(&msgs); err != nil {
11941261
log.Printf("poll decode: %v", err)
11951262
resp.Body.Close()
1196-
time.Sleep(time.Second)
1263+
if !sleepCtx(ctx, time.Second) {
1264+
return
1265+
}
11971266
continue
11981267
}
11991268
resp.Body.Close()
1200-
for _, m := range msgs {
1201-
sess := currentSession(m.From)
1269+
for _, msg := range msgs {
1270+
sess := m.currentSession(msg.From)
12021271
if sess == nil {
12031272
// setupPeerSession hasn't installed this remote yet;
12041273
// drop. runPeerLink installs all peer sessions at
12051274
// startup, so this only fires if we receive a message
12061275
// from a non-peer id (mis-configured cluster).
12071276
continue
12081277
}
1209-
switch m.Type {
1278+
switch msg.Type {
12101279
case "auth":
1211-
parts := strings.SplitN(m.Data, ":", 2)
1280+
parts := strings.SplitN(msg.Data, ":", 2)
12121281
if len(parts) != 2 {
1213-
log.Printf("[%s] bad auth %q", m.From, m.Data)
1282+
log.Printf("[%s] bad auth %q", msg.From, msg.Data)
12141283
continue
12151284
}
12161285
newAuth := [2]string{parts[0], parts[1]}
@@ -1235,7 +1304,7 @@ func pollLoop(cfg *Config) {
12351304
sess.consumedAuth != zero &&
12361305
sess.consumedAuth != newAuth {
12371306
log.Printf("[%s] fresh auth (ufrag=%s) supersedes consumed (ufrag=%s) — aborting attempt",
1238-
m.From, newAuth[0], sess.consumedAuth[0])
1307+
msg.From, newAuth[0], sess.consumedAuth[0])
12391308
sess.abortAttempt()
12401309
}
12411310
sess.mu.Unlock()
@@ -1250,9 +1319,9 @@ func pollLoop(cfg *Config) {
12501319
}
12511320
sess.authCh <- newAuth
12521321
case "candidate":
1253-
cand, err := ice.UnmarshalCandidate(m.Data)
1322+
cand, err := ice.UnmarshalCandidate(msg.Data)
12541323
if err != nil {
1255-
log.Printf("[%s] bad candidate: %v", m.From, err)
1324+
log.Printf("[%s] bad candidate: %v", msg.From, err)
12561325
continue
12571326
}
12581327
// Buffer first, then dispatch to the live agent if any.
@@ -1269,7 +1338,7 @@ func pollLoop(cfg *Config) {
12691338
sess.mu.Unlock()
12701339
if agent != nil {
12711340
if err := agent.AddRemoteCandidate(cand); err != nil {
1272-
log.Printf("[%s] AddRemoteCandidate: %v", m.From, err)
1341+
log.Printf("[%s] AddRemoteCandidate: %v", msg.From, err)
12731342
}
12741343
}
12751344
}

0 commit comments

Comments
 (0)