@@ 0,0 1,131 @@
+package msecrets
+
+import (
+ "log"
+ "sync"
+ "time"
+)
+
+
+// A hot-swap value is one that can be reloaded, but requires spinning up
+// (connecting) and spinning down (closing the connection) around the times where
+// that happens. It’s intended to be used as a secrets applier, converting values
+// read from the text file into stored entities.
+type HotSwap[T any] struct {
+
+ // The “name” of this value that gets printed out during logging, to
+ // distinguish values from other ones.
+ name string
+
+ // A pointer to the current value (if any), having been connected.
+ currentValue *T
+
+ // The function that gets called with the line read from the secrets text
+ // file, and “connects” to the value given in the string. This should be
+ // called from within the watcher goroutine, so this function does not need
+ // to worry about thread-safety itself.
+ connector func(string) (*T, error)
+
+ // The function that gets called to close down an entity after a new one
+ // comes in.
+ closer func(*T)
+
+ // The last-good string that was used to successfully connect. This is used
+ // to determine whether we need to cycle the entity because the input
+ // changed, or whether we can keep it because it stayed the same.
+ lastString string
+
+ // A queue of input strings that gets passed to the watcher goroutine.
+ queue chan string
+
+ // Whether we have successfully connected with the current value. This is
+ // used to determine whether, even if the lastString is the same, we should
+ // attempt to re-connect anyway.
+ ready bool
+
+ // A mutex that synchronises access to the mutable fields (currentValue,
+ // lastString, and ready).
+ lock sync.RWMutex
+}
+
+// HotSwappable creates a new hot-swappable value with the given name and pair of
+// up/down functions. It does not _call_ any of those functions, so is safe to
+// assign to a top-level variable without weird stuff happening.
+func HotSwappable[T any](name string, connector func(string) (*T, error), closer func(*T)) HotSwap[T] {
+ return HotSwap[T]{
+ name: name,
+ currentValue: nil,
+ connector: connector,
+ closer: closer,
+ lastString: "",
+ ready: false,
+ queue: make(chan string),
+ lock: sync.RWMutex{},
+ }
+}
+
+// Roll starts the process to connect using the given string, resulting in a new
+// entity if successful. Hook this function up as a secrets applier.
+func (hs *HotSwap[T]) Roll(newStr string) {
+ hs.queue <- newStr
+}
+
+// Watch should be called with ‘go’, as it loops forever. It receives values
+// added by Roll, tests whether the value has been updated, and runs the
+// connector if so.
+func (hs *HotSwap[T]) Watch() {
+ for newStr := range hs.queue {
+ hs.lock.RLock()
+ updated := hs.lastString != newStr || ! hs.ready
+ hs.lock.RUnlock()
+
+ if updated {
+ log.Println("Updated value for hot-swap", hs.name)
+ hs.update(newStr)
+ } else {
+ log.Println("No change in value for hot-swap", hs.name)
+ }
+ }
+}
+
+// update runs the connector function to create a new entity using the secret
+// string we have just read from the queue. If it succeeds, updates currentValue;
+// in any case, it updates ready and lastString. If the old value is to be
+// closed, closes it in a separate goroutine.
+func (hs *HotSwap[T]) update(newStr string) {
+ newValue, err := hs.connector(newStr)
+ if err != nil {
+ log.Printf("Failed to run connector for hot-swap %s: %v", hs.name, err)
+
+ hs.lock.Lock()
+ hs.ready = false
+ hs.lastString = ""
+ hs.lock.Unlock()
+
+ return
+ }
+
+ log.Printf("Connected for %s", hs.name)
+ oldValue := hs.currentValue
+
+ hs.lock.Lock()
+ hs.currentValue = newValue
+ hs.lastString = newStr
+ hs.ready = true
+ hs.lock.Unlock()
+
+ if oldValue != nil {
+ started := time.Now()
+ log.Printf("Closing old connection for hot-swap %s...", hs.name)
+ hs.closer(oldValue)
+ log.Printf("Closed old connection for hot-swap %s (took %v)", hs.name, time.Since(started))
+ }
+}
+
+// Get returns a pointer to the current value in the HotSwap. This will be nil if
+// there is no active one.
+func (hs *HotSwap[T]) Get() *T {
+ hs.lock.RLock()
+ defer hs.lock.RUnlock()
+ return hs.currentValue
+}