@@ -7,17 +7,54 @@ import (
77 "time"
88)
99
10+ // StateChangeObserver is invoked AFTER a membership mutation
11+ // (Upsert / Mark / Remove) commits, with the membership lock
12+ // already released. Observers MUST NOT call back into the
13+ // Membership they were registered on (deadlock); they SHOULD do
14+ // minimal work (publish to a channel, increment a counter) and
15+ // return promptly, since multiple observers run sequentially on
16+ // the goroutine that performed the mutation.
17+ //
18+ // Phase C SSE: the cache binary registers one observer that
19+ // publishes a `members` event onto the in-process event bus, so
20+ // SSE subscribers see state transitions without re-deriving them
21+ // by polling.
22+ type StateChangeObserver func (id NodeID , state NodeState , version uint64 )
23+
1024// Membership tracks current cluster nodes (static MVP, future: gossip/swim).
1125type Membership struct {
12- mu sync.RWMutex
13- nodes map [NodeID ]* Node
14- ring * Ring
15- ver MembershipVersion
26+ mu sync.RWMutex
27+ nodes map [NodeID ]* Node
28+ ring * Ring
29+ ver MembershipVersion
30+ observers []StateChangeObserver
1631}
1732
1833// NewMembership creates a new membership container bound to a ring.
1934func NewMembership (ring * Ring ) * Membership { return & Membership {nodes : map [NodeID ]* Node {}, ring : ring } }
2035
36+ // OnStateChange registers a callback invoked after every membership
37+ // mutation (Upsert, Mark, Remove). Registration is append-only and
38+ // not safe for concurrent use with mutations — call this once at
39+ // construction before the cache starts driving heartbeats / gossip.
40+ //
41+ // The callback runs OUTSIDE the membership lock so a slow observer
42+ // does not block the SWIM heartbeat loop. Observers fire in
43+ // registration order; one panicking observer would skip every
44+ // observer registered after it, so observer authors must recover
45+ // in their own code (the package itself does not wrap with
46+ // recover() because that would mask programming bugs).
47+ func (m * Membership ) OnStateChange (fn StateChangeObserver ) {
48+ if fn == nil {
49+ return
50+ }
51+
52+ m .mu .Lock ()
53+
54+ m .observers = append (m .observers , fn )
55+ m .mu .Unlock ()
56+ }
57+
2158// Upsert adds or updates a node and rebuilds ring.
2259func (m * Membership ) Upsert (n * Node ) {
2360 m .mu .Lock ()
@@ -32,10 +69,14 @@ func (m *Membership) Upsert(n *Node) {
3269 }
3370 }
3471
35- m .ver .Next ()
72+ version := m .ver .Next ()
73+ observers := m .observers
74+ id := n .ID
75+ state := n .State
3676 m .mu .Unlock ()
3777
3878 m .ring .Build (nodes )
79+ notify (observers , id , state , version )
3980}
4081
4182// List returns current nodes snapshot.
@@ -80,10 +121,15 @@ func (m *Membership) Remove(id NodeID) bool {
80121 }
81122 }
82123
83- m .ver .Next ()
124+ version := m .ver .Next ()
125+ observers := m .observers
84126 m .mu .Unlock ()
85127
86128 m .ring .Build (nodes )
129+ // State NodeDead represents "removed from membership" for
130+ // observer purposes; the node is gone from the map and will
131+ // not appear in any subsequent List() call.
132+ notify (observers , id , NodeDead , version )
87133
88134 return true
89135}
@@ -93,18 +139,34 @@ func (m *Membership) Mark(id NodeID, state NodeState) bool {
93139 m .mu .Lock ()
94140
95141 n , ok := m .nodes [id ]
96- if ok {
97- n .State = state
98- n .Incarnation ++
99-
100- n .LastSeen = time .Now ()
142+ if ! ok {
143+ m .mu .Unlock ()
101144
102- m . ver . Next ()
145+ return false
103146 }
104147
148+ n .State = state
149+ n .Incarnation ++
150+
151+ n .LastSeen = time .Now ()
152+
153+ version := m .ver .Next ()
154+ observers := m .observers
155+
105156 m .mu .Unlock ()
106157
107- return ok
158+ notify (observers , id , state , version )
159+
160+ return true
161+ }
162+
163+ // notify invokes each observer in registration order with the
164+ // resolved state and version. Pulled out so the call sites read
165+ // "mutate, unlock, notify" uniformly without inline loops.
166+ func notify (observers []StateChangeObserver , id NodeID , state NodeState , version uint64 ) {
167+ for _ , fn := range observers {
168+ fn (id , state , version )
169+ }
108170}
109171
110172// Version returns current membership version.
0 commit comments