2
0
mirror of https://github.com/lightninglabs/loop synced 2024-11-11 13:11:12 +00:00
loop/fsm/observer.go
sputn1ck 20db07dccf
fsm: add fsm module
This commit adds a module for a finite state machine. The goal of the
module is to provide a simple, easy to use, and easy to understand
finite state machine. The module is designed to be used in future
loop subsystems. Additionally a state visualizer is provided to
help with understanding the state machine.
2023-09-07 17:41:15 +02:00

135 lines
3.1 KiB
Go

package fsm
import (
"context"
"sync"
"time"
)
// CachedObserver is an observer that caches all states and transitions of
// the observed state machine.
type CachedObserver struct {
lastNotification Notification
cachedNotifications *FixedSizeSlice[Notification]
notificationCond *sync.Cond
notificationMx sync.Mutex
}
// NewCachedObserver creates a new cached observer with the given maximum
// number of cached notifications.
func NewCachedObserver(maxElements int) *CachedObserver {
fixedSizeSlice := NewFixedSizeSlice[Notification](maxElements)
observer := &CachedObserver{
cachedNotifications: fixedSizeSlice,
}
observer.notificationCond = sync.NewCond(&observer.notificationMx)
return observer
}
// Notify implements the Observer interface.
func (c *CachedObserver) Notify(notification Notification) {
c.notificationMx.Lock()
defer c.notificationMx.Unlock()
c.cachedNotifications.Add(notification)
c.lastNotification = notification
c.notificationCond.Broadcast()
}
// GetCachedNotifications returns a copy of the cached notifications.
func (c *CachedObserver) GetCachedNotifications() []Notification {
c.notificationMx.Lock()
defer c.notificationMx.Unlock()
return c.cachedNotifications.Get()
}
// WaitForState waits for the state machine to reach the given state.
func (s *CachedObserver) WaitForState(ctx context.Context,
timeout time.Duration, state StateType) error {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Channel to notify when the desired state is reached
ch := make(chan struct{})
// Goroutine to wait on condition variable
go func() {
s.notificationMx.Lock()
defer s.notificationMx.Unlock()
for {
// Check if the last state is the desired state
if s.lastNotification.NextState == state {
ch <- struct{}{}
return
}
// Otherwise, wait for the next notification
s.notificationCond.Wait()
}
}()
// Wait for either the condition to be met or for a timeout
select {
case <-timeoutCtx.Done():
return NewErrWaitingForStateTimeout(
state, s.lastNotification.NextState,
)
case <-ch:
return nil
}
}
// FixedSizeSlice is a slice with a fixed size.
type FixedSizeSlice[T any] struct {
data []T
maxLen int
sync.Mutex
}
// NewFixedSlice initializes a new FixedSlice with a given maximum length.
func NewFixedSizeSlice[T any](maxLen int) *FixedSizeSlice[T] {
return &FixedSizeSlice[T]{
data: make([]T, 0, maxLen),
maxLen: maxLen,
}
}
// Add appends a new element to the slice. If the slice reaches its maximum
// length, the first element is removed.
func (fs *FixedSizeSlice[T]) Add(element T) {
fs.Lock()
defer fs.Unlock()
if len(fs.data) == fs.maxLen {
// Remove the first element
fs.data = fs.data[1:]
}
// Add the new element
fs.data = append(fs.data, element)
}
// Get returns a copy of the slice.
func (fs *FixedSizeSlice[T]) Get() []T {
fs.Lock()
defer fs.Unlock()
data := make([]T, len(fs.data))
copy(data, fs.data)
return data
}
// GetElement returns the element at the given index.
func (fs *FixedSizeSlice[T]) GetElement(index int) T {
fs.Lock()
defer fs.Unlock()
return fs.data[index]
}