Remove the MinIO/S3 storage backend entirely. The filesystem backend is fully implemented, already used in production, and a migrate-storage tool exists for any remaining MinIO deployments to migrate beforehand. Changes: - Delete MinIO client implementation (internal/storage/storage.go) - Delete migrate-storage tool (cmd/migrate-storage, scripts/migrate-storage.sh) - Remove MinIO service, volumes, and env vars from all Docker Compose files - Simplify StorageConfig: remove Endpoint, AccessKey, SecretKey, Bucket, UseSSL, Region fields; add SILO_STORAGE_ROOT_DIR env override - Change all SQL COALESCE defaults from 'minio' to 'filesystem' - Add migration 020 to update column defaults to 'filesystem' - Remove minio-go/v7 dependency (go mod tidy) - Update all config examples, setup scripts, docs, and tests
137 lines
3.2 KiB
Go
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 storage 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
|
|
}
|