github.com/richardwilkes/toolbox@v1.121.0/notifier/notifier.go (about) 1 // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 // 3 // This Source Code Form is subject to the terms of the Mozilla Public 4 // License, version 2.0. If a copy of the MPL was not distributed with 5 // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 // 7 // This Source Code Form is "Incompatible With Secondary Licenses", as 8 // defined by the Mozilla Public License, version 2.0. 9 10 package notifier 11 12 import ( 13 "sort" 14 "strings" 15 "sync" 16 17 "github.com/richardwilkes/toolbox/errs" 18 ) 19 20 // Target defines the method a target of notifications must implement. 21 type Target interface { 22 // HandleNotification is called to deliver a notification. 23 HandleNotification(name string, data, producer any) 24 } 25 26 // BatchTarget defines the methods a target of notifications that wants to be notified when a batch change occurs must 27 // implement. 28 type BatchTarget interface { 29 Target 30 // BatchMode is called both before and after a series of notifications are about to be broadcast. The target is not 31 // guaranteed to have intervening calls to HandleNotification() made to it. 32 BatchMode(start bool) 33 } 34 35 // Notifier tracks targets of notifications and provides methods for notifying them. 36 type Notifier struct { 37 recoveryHandler errs.RecoveryHandler 38 lockedData 39 lock sync.RWMutex 40 } 41 42 type lockedData struct { 43 batchTargets map[BatchTarget]bool 44 productionMap map[string]map[Target]int 45 nameMap map[Target]map[string]bool 46 currentBatch []BatchTarget 47 batchLevel int 48 enabled bool 49 } 50 51 // New creates a new notifier. 52 func New(recoveryHandler errs.RecoveryHandler) *Notifier { 53 return &Notifier{ 54 recoveryHandler: recoveryHandler, 55 lockedData: lockedData{ 56 batchTargets: make(map[BatchTarget]bool), 57 productionMap: make(map[string]map[Target]int), 58 nameMap: make(map[Target]map[string]bool), 59 enabled: true, 60 }, 61 } 62 } 63 64 // Register a target with this notifier. 'priority' is the relative notification priority, with higher values being 65 // delivered first. 'names' are the names the target wishes to consume. Names are hierarchical (separated by a .), so 66 // specifying a name of "foo.bar" will consume not only a produced name of "foo.bar", but all sub-names, such as 67 // "foo.bar.a", but not "foo.barn" or "foo.barn.a". 68 func (n *Notifier) Register(target Target, priority int, names ...string) { 69 var normalizedNames []string 70 for _, name := range names { 71 if name = normalizeName(name); name != "" { 72 normalizedNames = append(normalizedNames, name) 73 } 74 } 75 if len(normalizedNames) > 0 { 76 n.lock.Lock() 77 targetNames, ok := n.nameMap[target] 78 if !ok { 79 targetNames = make(map[string]bool, len(normalizedNames)) 80 n.nameMap[target] = targetNames 81 } 82 if batchTarget, ok2 := target.(BatchTarget); ok2 { 83 n.batchTargets[batchTarget] = true 84 } 85 for _, name := range normalizedNames { 86 set, ok3 := n.productionMap[name] 87 if !ok3 { 88 set = make(map[Target]int) 89 n.productionMap[name] = set 90 } 91 set[target] = priority 92 targetNames[name] = true 93 } 94 n.lock.Unlock() 95 } 96 } 97 98 func normalizeName(name string) string { 99 var buffer strings.Builder 100 for _, one := range strings.Split(name, ".") { 101 if one != "" { 102 if buffer.Len() > 0 { 103 buffer.WriteByte('.') 104 } 105 buffer.WriteString(one) 106 } 107 } 108 return buffer.String() 109 } 110 111 // RegisterFromNotifier adds all registrations from the other notifier into this notifier. 112 func (n *Notifier) RegisterFromNotifier(other *Notifier) { 113 if n == other { 114 return 115 } 116 // To avoid a potential deadlock, we make a copy of the other notifier's data first. 117 other.lock.Lock() 118 batchTargets := make(map[BatchTarget]bool, len(other.batchTargets)) 119 for k, v := range other.batchTargets { 120 batchTargets[k] = v 121 } 122 productionMap := make(map[string]map[Target]int, len(other.productionMap)) 123 for k, v := range other.productionMap { 124 m := make(map[Target]int, len(v)) 125 for k1, v1 := range v { 126 m[k1] = v1 127 } 128 productionMap[k] = m 129 } 130 nameMap := make(map[Target]map[string]bool, len(other.nameMap)) 131 for k, v := range other.nameMap { 132 m := make(map[string]bool, len(v)) 133 for k1, v1 := range v { 134 m[k1] = v1 135 } 136 nameMap[k] = m 137 } 138 other.lock.Unlock() 139 140 n.lock.Lock() 141 for k, v := range batchTargets { 142 n.batchTargets[k] = v 143 } 144 for k, v := range productionMap { 145 if pm, ok := n.productionMap[k]; ok { 146 for k1, v1 := range pm { 147 pm[k1] = v1 148 } 149 n.productionMap[k] = pm 150 } else { 151 n.productionMap[k] = v 152 } 153 } 154 for k, v := range nameMap { 155 if nm, ok := n.nameMap[k]; ok { 156 for k1, v1 := range nm { 157 nm[k1] = v1 158 } 159 n.nameMap[k] = nm 160 } else { 161 n.nameMap[k] = v 162 } 163 } 164 n.lock.Unlock() 165 } 166 167 // Unregister a target. 168 func (n *Notifier) Unregister(target Target) { 169 n.lock.Lock() 170 if nameMap, exists := n.nameMap[target]; exists { 171 if batchTarget, ok := target.(BatchTarget); ok { 172 delete(n.batchTargets, batchTarget) 173 } 174 for name := range nameMap { 175 if set, ok := n.productionMap[name]; ok { 176 delete(set, target) 177 if len(set) == 0 { 178 delete(n.productionMap, name) 179 } 180 } 181 } 182 delete(n.nameMap, target) 183 } 184 n.lock.Unlock() 185 } 186 187 // Enabled returns true if this notifier is currently enabled. 188 func (n *Notifier) Enabled() bool { 189 n.lock.RLock() 190 defer n.lock.RUnlock() 191 return n.enabled 192 } 193 194 // SetEnabled sets whether this notifier is enabled or not. 195 func (n *Notifier) SetEnabled(enabled bool) { 196 n.lock.Lock() 197 n.enabled = enabled 198 n.lock.Unlock() 199 } 200 201 // Notify sends a notification to all interested targets. 202 func (n *Notifier) Notify(name string, producer any) { 203 n.NotifyWithData(name, nil, producer) 204 } 205 206 // NotifyWithData sends a notification to all interested targets. This is a synchronous notification and will not return 207 // until all interested targets handle the notification. 208 func (n *Notifier) NotifyWithData(name string, data, producer any) { 209 if n.Enabled() { 210 if name = normalizeName(name); name != "" { 211 targets := make(map[Target]int) 212 names := strings.Split(name, ".") 213 n.lock.RLock() 214 if n.enabled { 215 var buffer strings.Builder 216 for _, one := range names { 217 buffer.WriteString(one) 218 one = buffer.String() 219 if set, ok := n.productionMap[one]; ok { 220 for k, v := range set { 221 targets[k] = v 222 } 223 } 224 buffer.WriteByte('.') 225 } 226 } 227 n.lock.RUnlock() 228 if len(targets) > 0 { 229 list := make([]Target, 0, len(targets)) 230 for k := range targets { 231 list = append(list, k) 232 } 233 sort.Slice(list, func(i, j int) bool { 234 return targets[list[i]] > targets[list[j]] 235 }) 236 for _, target := range list { 237 n.notifyTarget(target, name, data, producer) 238 } 239 } 240 } 241 } 242 } 243 244 func (n *Notifier) notifyTarget(target Target, name string, data, producer any) { 245 defer errs.Recovery(n.recoveryHandler) 246 target.HandleNotification(name, data, producer) 247 } 248 249 // BatchLevel returns the current batch level. 250 func (n *Notifier) BatchLevel() int { 251 n.lock.RLock() 252 defer n.lock.RUnlock() 253 return n.batchLevel 254 } 255 256 // StartBatch informs all BatchTargets that a batch of notifications will be starting. If a previous call to this method 257 // was made without a call to EndBatch(), then the batch level will be incremented, but no notifications will be made. 258 func (n *Notifier) StartBatch() { 259 var targets []BatchTarget 260 n.lock.Lock() 261 if n.enabled { 262 n.batchLevel++ 263 if n.batchLevel == 1 && len(n.batchTargets) > 0 { 264 n.currentBatch = make([]BatchTarget, 0, len(n.batchTargets)) 265 for k := range n.batchTargets { 266 n.currentBatch = append(n.currentBatch, k) 267 } 268 targets = n.currentBatch 269 } 270 } 271 n.lock.Unlock() 272 for _, target := range targets { 273 n.notifyBatchTarget(target, true) 274 } 275 } 276 277 func (n *Notifier) notifyBatchTarget(target BatchTarget, start bool) { 278 defer errs.Recovery(n.recoveryHandler) 279 target.BatchMode(start) 280 } 281 282 // EndBatch informs all BatchTargets that were present when StartBatch() was called that a batch of notifications just 283 // finished. If batch level is still greater than zero after being decremented, then no notifications will be made. 284 func (n *Notifier) EndBatch() { 285 var targets []BatchTarget 286 n.lock.Lock() 287 if n.enabled && n.batchLevel > 0 { 288 n.batchLevel-- 289 if n.batchLevel == 0 { 290 targets = n.currentBatch 291 n.currentBatch = nil 292 } 293 } 294 n.lock.Unlock() 295 for _, target := range targets { 296 n.notifyBatchTarget(target, false) 297 } 298 } 299 300 // Reset removes all targets. 301 func (n *Notifier) Reset() { 302 n.lock.Lock() 303 n.batchTargets = make(map[BatchTarget]bool) 304 n.productionMap = make(map[string]map[Target]int) 305 n.nameMap = make(map[Target]map[string]bool) 306 n.currentBatch = nil 307 n.batchLevel = 0 308 n.lock.Unlock() 309 }