Files
silo/internal/api/servermode.go
forbes-0023 b531617e39 feat(storage): define FileStore interface and refactor to use it
Extract a FileStore interface from the concrete *storage.Storage MinIO
wrapper so the API layer is storage-backend agnostic.

- Define FileStore interface in internal/storage/interface.go
- Add Exists method to MinIO Storage (via StatObject)
- Add compile-time interface satisfaction check
- Change Server.storage and ServerState.storage to FileStore interface
- Update NewServer and NewServerState signatures
- Add Backend and FilesystemConfig fields to StorageConfig
- Add backend selection switch in main.go (minio/filesystem/unknown)
- Update config.example.yaml with backend field

The nil-interface pattern is preserved: when storage is unconfigured,
store remains a true nil FileStore (not a typed nil pointer), so all
existing if s.storage == nil checks continue to work correctly.

Closes #126
2026-02-17 11:49:35 -06:00

137 lines
3.2 KiB
Go

package api
import (
"context"
"sync"
"time"
"github.com/kindredsystems/silo/internal/storage"
"github.com/rs/zerolog"
)
// ServerMode represents the operational mode of the server.
type ServerMode string
const (
ModeNormal ServerMode = "normal"
ModeReadOnly ServerMode = "read-only"
ModeDegraded ServerMode = "degraded"
)
const storageCheckInterval = 30 * time.Second
// ServerState tracks the server's current operational mode.
type ServerState struct {
logger zerolog.Logger
mu sync.RWMutex
readOnly bool
storageOK bool
storage storage.FileStore
broker *Broker
done chan struct{}
}
// NewServerState creates a new server state tracker.
func NewServerState(logger zerolog.Logger, store storage.FileStore, broker *Broker) *ServerState {
return &ServerState{
logger: logger.With().Str("component", "server-state").Logger(),
storageOK: store != nil, // assume healthy if configured
storage: store,
broker: broker,
done: make(chan struct{}),
}
}
// Mode returns the current effective server mode.
// Priority: explicit read-only > storage unhealthy (degraded) > normal.
func (ss *ServerState) Mode() ServerMode {
ss.mu.RLock()
defer ss.mu.RUnlock()
if ss.readOnly {
return ModeReadOnly
}
if ss.storage != nil && !ss.storageOK {
return ModeDegraded
}
return ModeNormal
}
// IsReadOnly returns true if the server should reject writes.
// Only explicit read-only mode blocks writes; degraded is informational.
func (ss *ServerState) IsReadOnly() bool {
ss.mu.RLock()
defer ss.mu.RUnlock()
return ss.readOnly
}
// SetReadOnly sets the explicit read-only flag and broadcasts a state change.
func (ss *ServerState) SetReadOnly(ro bool) {
ss.mu.Lock()
old := ss.mode()
ss.readOnly = ro
new := ss.mode()
ss.mu.Unlock()
if old != new {
ss.logger.Info().Str("mode", string(new)).Msg("server mode changed")
ss.broker.Publish("server.state", mustMarshal(map[string]string{"mode": string(new)}))
}
}
// ToggleReadOnly flips the read-only flag.
func (ss *ServerState) ToggleReadOnly() {
ss.mu.RLock()
current := ss.readOnly
ss.mu.RUnlock()
ss.SetReadOnly(!current)
}
// StartStorageHealthCheck launches a periodic check of MinIO reachability.
// Updates storageOK and broadcasts server.state on transitions.
func (ss *ServerState) StartStorageHealthCheck() {
if ss.storage == nil {
return
}
go func() {
ticker := time.NewTicker(storageCheckInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err := ss.storage.Ping(ctx)
cancel()
ss.mu.Lock()
old := ss.mode()
ss.storageOK = err == nil
new := ss.mode()
ss.mu.Unlock()
if old != new {
ss.logger.Info().Str("mode", string(new)).Err(err).Msg("server mode changed")
ss.broker.Publish("server.state", mustMarshal(map[string]string{"mode": string(new)}))
}
case <-ss.done:
return
}
}
}()
}
// Shutdown stops the health check loop.
func (ss *ServerState) Shutdown() {
close(ss.done)
}
// mode returns the current mode. Must be called with mu held.
func (ss *ServerState) mode() ServerMode {
if ss.readOnly {
return ModeReadOnly
}
if ss.storage != nil && !ss.storageOK {
return ModeDegraded
}
return ModeNormal
}