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  }