Deploy MQTT test backend with private viewer and Gitea CI/CD
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Store retains bounded inspection events; the transport selects safe or opt-in raw previews.
|
||||
type Notice struct {
|
||||
Event *Event `json:"event,omitempty"`
|
||||
Gap uint64 `json:"gap,omitempty"`
|
||||
ResetID uint64 `json:"reset_id,omitempty"`
|
||||
}
|
||||
type Subscriber struct {
|
||||
channel chan Notice
|
||||
lost uint64
|
||||
}
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
history []Event
|
||||
capacity int
|
||||
next uint64
|
||||
counts map[Kind]uint64
|
||||
log *slog.Logger
|
||||
nextSub uint64
|
||||
subs map[uint64]*Subscriber
|
||||
}
|
||||
|
||||
func NewStore(capacity int, log *slog.Logger) *Store {
|
||||
return &Store{capacity: capacity, counts: make(map[Kind]uint64), log: log, subs: make(map[uint64]*Subscriber)}
|
||||
}
|
||||
func (s *Store) Add(e Event) {
|
||||
s.mu.Lock()
|
||||
s.next++
|
||||
e.ID = s.next
|
||||
e.ObservedAt = time.Now().UTC()
|
||||
s.counts[e.Kind]++
|
||||
if len(s.history) == s.capacity {
|
||||
copy(s.history, s.history[1:])
|
||||
s.history = s.history[:len(s.history)-1]
|
||||
}
|
||||
s.history = append(s.history, cloneEvent(e))
|
||||
for _, sub := range s.subs {
|
||||
if sub.lost > 0 {
|
||||
select {
|
||||
// Advance the cursor through the current event with the gap notice.
|
||||
// Leaving the cursor before it would cause a queue-of-one viewer to
|
||||
// receive a gap forever, since every notice would fill its queue.
|
||||
case sub.channel <- Notice{Gap: sub.lost + 1, ResetID: e.ID}:
|
||||
sub.lost = 0
|
||||
continue
|
||||
default:
|
||||
sub.lost++
|
||||
continue
|
||||
}
|
||||
}
|
||||
copyEvent := cloneEvent(e)
|
||||
select {
|
||||
case sub.channel <- Notice{Event: ©Event}:
|
||||
default:
|
||||
sub.lost++
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.log.Info(string(e.Kind), "event", e)
|
||||
}
|
||||
func (s *Store) Snapshot() ([]Event, map[Kind]uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
history := make([]Event, len(s.history))
|
||||
for i, event := range s.history {
|
||||
history[i] = cloneEvent(event)
|
||||
}
|
||||
counts := make(map[Kind]uint64, len(s.counts))
|
||||
for k, v := range s.counts {
|
||||
counts[k] = v
|
||||
}
|
||||
return history, counts
|
||||
}
|
||||
|
||||
// Clone pointer fields so snapshot consumers cannot mutate retained history.
|
||||
func cloneEvent(e Event) Event {
|
||||
if e.Scenario != nil {
|
||||
result := *e.Scenario
|
||||
e.Scenario = &result
|
||||
}
|
||||
e.Properties.CorrelationData = append([]byte(nil), e.Properties.CorrelationData...)
|
||||
if e.Properties.MessageExpirySeconds != nil {
|
||||
n := *e.Properties.MessageExpirySeconds
|
||||
e.Properties.MessageExpirySeconds = &n
|
||||
}
|
||||
if e.Duplicate != nil {
|
||||
n := *e.Duplicate
|
||||
e.Duplicate = &n
|
||||
}
|
||||
if e.LatencyMS != nil {
|
||||
n := *e.LatencyMS
|
||||
e.LatencyMS = &n
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Subscribe atomically captures bounded replay and registers a bounded live queue.
|
||||
// A cursor outside retained history creates an explicit reset to the oldest event.
|
||||
func (s *Store) Subscribe(after uint64, queue, maxReplay int) (replay []Event, initialGap *Notice, stream <-chan Notice, unsubscribe func()) {
|
||||
s.mu.Lock()
|
||||
s.nextSub++
|
||||
id := s.nextSub
|
||||
sub := &Subscriber{channel: make(chan Notice, queue)}
|
||||
s.subs[id] = sub
|
||||
if len(s.history) > 0 {
|
||||
oldest := s.history[0].ID
|
||||
newest := s.history[len(s.history)-1].ID
|
||||
if after > newest {
|
||||
initialGap = &Notice{Gap: after - newest, ResetID: newest}
|
||||
after = newest
|
||||
} else if after < oldest-1 {
|
||||
initialGap = &Notice{Gap: oldest - 1 - after, ResetID: oldest - 1}
|
||||
after = oldest - 1
|
||||
}
|
||||
if maxReplay > 0 && newest-after > uint64(maxReplay) {
|
||||
reset := newest - uint64(maxReplay)
|
||||
missed := reset - after
|
||||
if initialGap == nil {
|
||||
initialGap = &Notice{Gap: missed, ResetID: reset}
|
||||
} else {
|
||||
initialGap.Gap += missed
|
||||
initialGap.ResetID = reset
|
||||
}
|
||||
after = reset
|
||||
}
|
||||
for _, event := range s.history {
|
||||
if event.ID > after {
|
||||
replay = append(replay, cloneEvent(event))
|
||||
}
|
||||
}
|
||||
} else if after > 0 {
|
||||
initialGap = &Notice{Gap: after, ResetID: 0}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
var once sync.Once
|
||||
return replay, initialGap, sub.channel, func() { once.Do(func() { s.mu.Lock(); delete(s.subs, id); close(sub.channel); s.mu.Unlock() }) }
|
||||
}
|
||||
Reference in New Issue
Block a user