-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnotifier.go
More file actions
215 lines (202 loc) · 6.79 KB
/
Copy pathnotifier.go
File metadata and controls
215 lines (202 loc) · 6.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package nexus
// Notifier carries the "something changed" signal between nexus's
// mutating subsystems (registry, cron scheduler, rate-limit store)
// and any code that wants to observe state mutations.
//
// Why not a channel directly: callers that mutate state can't
// block on a slow consumer, and would have to pre-allocate the
// right buffer size. A non-blocking Notify with select-default
// drop is the right shape for "tell whoever's listening; never
// stall the request path".
//
// Relocated from live/notifier.go into the root package in the
// same change that removed the .nlt template engine; the Notifier
// is generic pub-sub and the original live/ home no longer made
// sense without the template-engine consumers.
import "sync"
// Notifier is the shared signal hub. Multiple subsystems call
// Notify(); multiple consumers (typically just streamLive, but the
// API supports more for future widgets) Subscribe to receive a
// nudge channel.
//
// Topic-aware: NotifyTopic("post:42") wakes only subscribers
// who joined that topic via SubscribeTopic; the un-topic'd
// Notify() and Subscribe() pair preserves the v0 broadcast
// behavior for callers that don't care about routing. The two
// surfaces overlap deliberately — Notify() also wakes every
// topic subscriber so a global "everything changed" still
// reaches everyone, while NotifyTopic stays scoped.
//
// Notify is cheap (single mutex acquire + N non-blocking sends) and
// safe from any goroutine. The zero value is unusable — callers
// must go through New so the maps are initialized.
type Notifier struct {
mu sync.Mutex
listeners []chan struct{} // broadcast subscribers (Subscribe)
topics map[string][]chan struct{} // topic → subscribers (SubscribeTopic)
bus Bus // optional cross-process fan-out (AttachBus)
}
// NewNotifier returns a fresh Notifier. Typically created once at
// app boot and threaded into each mutating subsystem via
// SetChangeHook (or the equivalent setter on each package).
//
// Production apps don't need to call this directly — nexus.Run
// wires a singleton into the fx graph; constructors that need a
// notifier just take a *Notifier param.
func NewNotifier() *Notifier {
return &Notifier{
topics: make(map[string][]chan struct{}),
}
}
// Notify wakes every current subscriber whose channel has buffer
// room. Includes both broadcast subscribers (Subscribe) and every
// topic subscriber (SubscribeTopic), so "global change" callers
// don't need to know what topics exist. Subscribers whose channel
// already has a pending nudge are left alone — coalescing N rapid
// mutations into one wake-up is the whole point. Never blocks.
//
// When a Bus is attached, also Publish("") so peer nodes wake
// their local subscribers too — that's the horizontal-scale
// path. Publish errors are swallowed: the wake is best-effort,
// and a flaky bus shouldn't take down the mutator that called us.
func (n *Notifier) Notify() {
if n == nil {
return
}
n.notifyLocal()
n.mu.Lock()
bus := n.bus
n.mu.Unlock()
if bus != nil {
_ = bus.Publish("")
}
}
// NotifyTopic wakes only subscribers of the given topic.
// Broadcast subscribers (Subscribe with no topic) are NOT woken —
// they're for "tell me about everything" and would defeat the
// scoping if every topic notify also woke them. Use Notify() for
// the global case.
//
// Topic strings are arbitrary; common patterns are entity-scoped
// keys like "post:42" or "user:alice/inbox". An empty topic is a
// no-op (NotifyTopic("") matches no one — SubscribeTopic rejects
// empty strings).
//
// When a Bus is attached, also publishes the topic so peer nodes
// fan it out to their local subscribers. See Notify for the
// rationale on swallowed Publish errors.
func (n *Notifier) NotifyTopic(topic string) {
if n == nil || topic == "" {
return
}
n.notifyLocalTopic(topic)
n.mu.Lock()
bus := n.bus
n.mu.Unlock()
if bus != nil {
_ = bus.Publish(topic)
}
}
// notifyLocal wakes only this node's subscribers — no bus
// publish. Called by Notify (which then publishes) and by the
// Bus-attached forwarding goroutine (which received the message
// from a peer and shouldn't re-publish it).
func (n *Notifier) notifyLocal() {
n.mu.Lock()
defer n.mu.Unlock()
for _, ch := range n.listeners {
nudge(ch)
}
for _, subs := range n.topics {
for _, ch := range subs {
nudge(ch)
}
}
}
// notifyLocalTopic wakes only this node's subscribers for one
// topic. Same role as notifyLocal but topic-scoped — see comment
// there for why local-only.
func (n *Notifier) notifyLocalTopic(topic string) {
n.mu.Lock()
defer n.mu.Unlock()
for _, ch := range n.topics[topic] {
nudge(ch)
}
}
// Subscribe registers a new broadcast listener and returns its
// nudge channel plus a cancel func. The channel has buffer 1 so a
// single pending notify is held even when the subscriber is busy.
// Cancel removes the listener and closes the channel; subsequent
// Notify calls skip it.
//
// Cancel is safe to call multiple times.
func (n *Notifier) Subscribe() (<-chan struct{}, func()) {
ch := make(chan struct{}, 1)
n.mu.Lock()
n.listeners = append(n.listeners, ch)
n.mu.Unlock()
var once sync.Once
cancel := func() {
once.Do(func() {
n.mu.Lock()
for i, c := range n.listeners {
if c == ch {
n.listeners = append(n.listeners[:i], n.listeners[i+1:]...)
break
}
}
n.mu.Unlock()
close(ch)
})
}
return ch, cancel
}
// SubscribeTopic registers a listener scoped to one topic. Same
// channel + cancel contract as Subscribe; the difference is that
// only NotifyTopic(topic) and the global Notify() wake the
// channel — NotifyTopic for other topics leaves it alone.
//
// Empty topic returns a closed channel and a no-op cancel — the
// caller usually has a programming error in that case (constructed
// a topic key from missing input), and a never-firing channel
// makes the symptom visible without panicking.
func (n *Notifier) SubscribeTopic(topic string) (<-chan struct{}, func()) {
if topic == "" {
ch := make(chan struct{})
close(ch)
return ch, func() {}
}
ch := make(chan struct{}, 1)
n.mu.Lock()
n.topics[topic] = append(n.topics[topic], ch)
n.mu.Unlock()
var once sync.Once
cancel := func() {
once.Do(func() {
n.mu.Lock()
subs := n.topics[topic]
for i, c := range subs {
if c == ch {
n.topics[topic] = append(subs[:i], subs[i+1:]...)
break
}
}
if len(n.topics[topic]) == 0 {
delete(n.topics, topic)
}
n.mu.Unlock()
close(ch)
})
}
return ch, cancel
}
// nudge does the non-blocking send dance used by Notify and
// NotifyTopic. Extracted so the two callers don't drift.
func nudge(ch chan struct{}) {
select {
case ch <- struct{}{}:
default:
// Already pending — coalesce. The subscriber sees one
// wake-up covering all the notifies it missed.
}
}