Files
gemini-banlancer/internal/syncer/syncer.go

159 lines
3.3 KiB
Go

package syncer
import (
"context"
"fmt"
"gemini-balancer/internal/store"
"sync"
"time"
"github.com/sirupsen/logrus"
)
const (
ReconnectDelay = 5 * time.Second
ReloadTimeout = 30 * time.Second
)
type LoaderFunc[T any] func() (T, error)
type CacheSyncer[T any] struct {
mu sync.RWMutex
cache T
loader LoaderFunc[T]
store store.Store
channelName string
logger *logrus.Entry
stopChan chan struct{}
wg sync.WaitGroup
}
func NewCacheSyncer[T any](
loader LoaderFunc[T],
store store.Store,
channelName string,
logger *logrus.Logger,
) (*CacheSyncer[T], error) {
s := &CacheSyncer[T]{
loader: loader,
store: store,
channelName: channelName,
logger: logger.WithField("component", fmt.Sprintf("CacheSyncer[%s]", channelName)),
stopChan: make(chan struct{}),
}
if err := s.reload(); err != nil {
return nil, fmt.Errorf("initial load failed: %w", err)
}
s.wg.Add(1)
go s.listenForUpdates()
return s, nil
}
func (s *CacheSyncer[T]) Get() T {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cache
}
func (s *CacheSyncer[T]) Invalidate() error {
s.logger.Info("Publishing invalidation notification")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.store.Publish(ctx, s.channelName, []byte("reload")); err != nil {
s.logger.WithError(err).Error("Failed to publish invalidation")
return err
}
return nil
}
func (s *CacheSyncer[T]) Stop() {
close(s.stopChan)
s.wg.Wait()
s.logger.Info("CacheSyncer stopped")
}
func (s *CacheSyncer[T]) reload() error {
s.logger.Info("Reloading cache...")
ctx, cancel := context.WithTimeout(context.Background(), ReloadTimeout)
defer cancel()
type result struct {
data T
err error
}
resultChan := make(chan result, 1)
go func() {
data, err := s.loader()
resultChan <- result{data, err}
}()
select {
case res := <-resultChan:
if res.err != nil {
s.logger.WithError(res.err).Error("Failed to reload cache")
return res.err
}
s.mu.Lock()
s.cache = res.data
s.mu.Unlock()
s.logger.Info("Cache reloaded successfully")
return nil
case <-ctx.Done():
s.logger.Error("Cache reload timeout")
return fmt.Errorf("reload timeout after %v", ReloadTimeout)
}
}
func (s *CacheSyncer[T]) listenForUpdates() {
defer s.wg.Done()
for {
select {
case <-s.stopChan:
return
default:
}
if err := s.subscribeAndListen(); err != nil {
s.logger.WithError(err).Warnf("Subscription error, retrying in %v", ReconnectDelay)
select {
case <-time.After(ReconnectDelay):
case <-s.stopChan:
return
}
}
}
}
func (s *CacheSyncer[T]) subscribeAndListen() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
subscription, err := s.store.Subscribe(ctx, s.channelName)
if err != nil {
return fmt.Errorf("failed to subscribe: %w", err)
}
defer subscription.Close()
s.logger.Info("Subscribed to channel")
for {
select {
case msg, ok := <-subscription.Channel():
if !ok {
return fmt.Errorf("subscription channel closed")
}
s.logger.WithField("message", string(msg.Payload)).Info("Received invalidation notification")
if err := s.reload(); err != nil {
s.logger.WithError(err).Error("Failed to reload after notification")
}
case <-s.stopChan:
return nil
}
}
}