mirror of
https://github.com/dpup/meshstream.git
synced 2026-07-06 01:41:14 +02:00
239 lines
6.7 KiB
Go
239 lines
6.7 KiB
Go
package mqtt
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
meshtreampb "meshstream/generated/meshstream"
|
|
|
|
"github.com/dpup/prefab/logging"
|
|
)
|
|
|
|
// CircularBuffer implements a fixed-size circular buffer for caching packets
|
|
type CircularBuffer struct {
|
|
buffer []*meshtreampb.Packet // Fixed size buffer to store packets
|
|
size int // Size of the buffer
|
|
next int // Index where the next packet will be stored
|
|
count int // Number of packets currently in the buffer
|
|
mutex sync.RWMutex // Lock for thread-safe access
|
|
}
|
|
|
|
// NewCircularBuffer creates a new circular buffer with the given size
|
|
func NewCircularBuffer(size int) *CircularBuffer {
|
|
return &CircularBuffer{
|
|
buffer: make([]*meshtreampb.Packet, size),
|
|
size: size,
|
|
next: 0,
|
|
count: 0,
|
|
}
|
|
}
|
|
|
|
// Add adds a packet to the circular buffer
|
|
func (cb *CircularBuffer) Add(packet *meshtreampb.Packet) {
|
|
cb.mutex.Lock()
|
|
defer cb.mutex.Unlock()
|
|
|
|
cb.buffer[cb.next] = packet
|
|
cb.next = (cb.next + 1) % cb.size
|
|
|
|
// Update count if we haven't filled the buffer yet
|
|
if cb.count < cb.size {
|
|
cb.count++
|
|
}
|
|
}
|
|
|
|
// GetAll returns all packets in the buffer in chronological order
|
|
func (cb *CircularBuffer) GetAll() []*meshtreampb.Packet {
|
|
cb.mutex.RLock()
|
|
defer cb.mutex.RUnlock()
|
|
|
|
if cb.count == 0 {
|
|
return []*meshtreampb.Packet{}
|
|
}
|
|
|
|
result := make([]*meshtreampb.Packet, cb.count)
|
|
|
|
// If buffer isn't full yet, just copy from start to next
|
|
if cb.count < cb.size {
|
|
copy(result, cb.buffer[:cb.count])
|
|
return result
|
|
}
|
|
|
|
// If buffer is full, we need to handle the wrap-around
|
|
// First copy from next to end
|
|
firstPartLength := cb.size - cb.next
|
|
if firstPartLength > 0 {
|
|
copy(result, cb.buffer[cb.next:])
|
|
}
|
|
|
|
// Then copy from start to next
|
|
if cb.next > 0 {
|
|
copy(result[firstPartLength:], cb.buffer[:cb.next])
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Broker distributes messages from a source channel to multiple subscriber channels
|
|
type Broker struct {
|
|
sourceChan <-chan *meshtreampb.Packet // Source of packets (e.g., from MQTT client)
|
|
subscribers map[chan *meshtreampb.Packet]struct{} // Active subscribers
|
|
subscriberMutex sync.RWMutex // Lock for modifying the subscribers map
|
|
done chan struct{} // Signal to stop the dispatch loop
|
|
wg sync.WaitGroup // Wait group to ensure clean shutdown
|
|
logger logging.Logger // Logger for broker operations
|
|
cache *CircularBuffer // Circular buffer for caching packets
|
|
}
|
|
|
|
// NewBroker creates a new broker that distributes messages from sourceChannel to subscribers
|
|
func NewBroker(sourceChannel <-chan *meshtreampb.Packet, cacheSize int, logger logging.Logger) *Broker {
|
|
broker := &Broker{
|
|
sourceChan: sourceChannel,
|
|
subscribers: make(map[chan *meshtreampb.Packet]struct{}),
|
|
done: make(chan struct{}),
|
|
logger: logger.Named("mqtt.broker"),
|
|
cache: NewCircularBuffer(cacheSize),
|
|
}
|
|
|
|
// Start the dispatch loop
|
|
broker.wg.Add(1)
|
|
go broker.dispatchLoop()
|
|
|
|
return broker
|
|
}
|
|
|
|
// Subscribe creates and returns a new subscriber channel
|
|
// The bufferSize parameter controls how many messages can be buffered in the channel
|
|
func (b *Broker) Subscribe(bufferSize int) <-chan *meshtreampb.Packet {
|
|
// Create a new channel for this subscriber
|
|
subscriberChan := make(chan *meshtreampb.Packet, bufferSize)
|
|
|
|
// Register the new subscriber
|
|
b.subscriberMutex.Lock()
|
|
b.subscribers[subscriberChan] = struct{}{}
|
|
b.subscriberMutex.Unlock()
|
|
|
|
// Send cached packets to the new subscriber
|
|
cachedPackets := b.cache.GetAll()
|
|
if len(cachedPackets) > 0 {
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// This can happen if the channel was closed while we were sending
|
|
b.logger.Warn("Recovered from panic when sending cached packets, channel likely closed")
|
|
}
|
|
}()
|
|
|
|
for _, packet := range cachedPackets {
|
|
select {
|
|
case subscriberChan <- packet:
|
|
// Successfully sent packet
|
|
case <-time.After(1500 * time.Millisecond):
|
|
// Give up after waiting some time to avoid blocking indefinitely
|
|
b.logger.Warn("Timeout when sending cached packet to new subscriber")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Return the channel
|
|
return subscriberChan
|
|
}
|
|
|
|
// Unsubscribe removes a subscriber and closes its channel
|
|
func (b *Broker) Unsubscribe(ch <-chan *meshtreampb.Packet) {
|
|
b.subscriberMutex.Lock()
|
|
defer b.subscriberMutex.Unlock()
|
|
|
|
// Find the channel in our subscribers map
|
|
for subCh := range b.subscribers {
|
|
if subCh == ch {
|
|
delete(b.subscribers, subCh)
|
|
close(subCh)
|
|
return
|
|
}
|
|
}
|
|
|
|
// If we get here, the channel wasn't found
|
|
b.logger.Warn("Subscriber channel not found - cannot unsubscribe")
|
|
}
|
|
|
|
// Close shuts down the broker and closes all subscriber channels
|
|
func (b *Broker) Close() {
|
|
// Signal the dispatch loop to stop
|
|
close(b.done)
|
|
|
|
// Wait for the dispatch loop to exit
|
|
b.wg.Wait()
|
|
|
|
// Close all subscriber channels
|
|
b.subscriberMutex.Lock()
|
|
defer b.subscriberMutex.Unlock()
|
|
|
|
for ch := range b.subscribers {
|
|
close(ch)
|
|
}
|
|
b.subscribers = make(map[chan *meshtreampb.Packet]struct{})
|
|
}
|
|
|
|
// dispatchLoop continuously reads from the source channel and distributes to subscribers
|
|
func (b *Broker) dispatchLoop() {
|
|
defer b.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-b.done:
|
|
// Broker is shutting down
|
|
return
|
|
|
|
case packet, ok := <-b.sourceChan:
|
|
if !ok {
|
|
// Source channel has been closed, shut down the broker
|
|
b.logger.Info("Source channel closed, shutting down broker")
|
|
b.Close()
|
|
return
|
|
}
|
|
|
|
// Add packet to the cache
|
|
b.cache.Add(packet)
|
|
|
|
// Distribute the packet to all subscribers
|
|
b.broadcast(packet)
|
|
}
|
|
}
|
|
}
|
|
|
|
// broadcast sends a packet to all active subscribers without blocking
|
|
func (b *Broker) broadcast(packet *meshtreampb.Packet) {
|
|
// Take a read lock to get a snapshot of the subscribers
|
|
b.subscriberMutex.RLock()
|
|
subscribers := make([]chan *meshtreampb.Packet, 0, len(b.subscribers))
|
|
for ch := range b.subscribers {
|
|
subscribers = append(subscribers, ch)
|
|
}
|
|
b.subscriberMutex.RUnlock()
|
|
|
|
// Distribute to all subscribers
|
|
for _, ch := range subscribers {
|
|
// Use a goroutine and recover to ensure sending to a closed channel doesn't panic
|
|
go func(ch chan *meshtreampb.Packet) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// This can happen if the channel was closed after we took a snapshot
|
|
b.logger.Warn("Recovered from panic in broadcast, channel likely closed")
|
|
}
|
|
}()
|
|
|
|
// Try to send without blocking
|
|
select {
|
|
case ch <- packet:
|
|
// Message delivered successfully
|
|
default:
|
|
// Channel buffer is full, log warning and drop the message
|
|
b.logger.Warn("Subscriber buffer full, dropping message")
|
|
}
|
|
}(ch)
|
|
}
|
|
}
|