Files
godump/backup/manager.go
T
2026-06-04 18:25:13 +00:00

339 lines
7.9 KiB
Go

package backup
import (
"os"
"path/filepath"
"sort"
"sync"
"time"
"godump/config"
"godump/logger"
"godump/notify"
"github.com/robfig/cron/v3"
)
type DBStatus struct {
Name string `json:"name"`
FirstDiscovered time.Time `json:"first_discovered"`
LastBackupTime time.Time `json:"last_backup_time"`
LastBackupSize int64 `json:"last_backup_size"`
LastBackupResult string `json:"last_backup_result"` // success, skipped, failed
}
type InstanceStatus struct {
Config config.InstanceConfig
LastRunTime time.Time
NextRunTime time.Time
OverallResult string // success, partial, failed, running
Databases map[string]*DBStatus
IsRunning bool
CronEntryID cron.EntryID
mu sync.RWMutex
}
type DBStatusSnapshot struct {
Name string `json:"name"`
FirstDiscovered time.Time `json:"first_discovered"`
LastBackupTime time.Time `json:"last_backup_time"`
LastBackupSize int64 `json:"last_backup_size"`
LastBackupResult string `json:"last_backup_result"`
}
type InstanceSnapshot struct {
Name string
Host string
LastRunTime time.Time
NextRunTime time.Time
OverallResult string
IsRunning bool
Databases []DBStatusSnapshot
}
func (s *InstanceStatus) Snapshot() InstanceSnapshot {
s.mu.RLock()
defer s.mu.RUnlock()
snap := InstanceSnapshot{
Name: s.Config.Name,
Host: s.Config.Host,
LastRunTime: s.LastRunTime,
NextRunTime: s.NextRunTime,
OverallResult: s.OverallResult,
IsRunning: s.IsRunning,
Databases: make([]DBStatusSnapshot, 0, len(s.Databases)),
}
for _, db := range s.Databases {
snap.Databases = append(snap.Databases, DBStatusSnapshot{
Name: db.Name,
FirstDiscovered: db.FirstDiscovered,
LastBackupTime: db.LastBackupTime,
LastBackupSize: db.LastBackupSize,
LastBackupResult: db.LastBackupResult,
})
}
sort.Slice(snap.Databases, func(i, j int) bool {
return snap.Databases[i].Name < snap.Databases[j].Name
})
return snap
}
type Manager struct {
cfg *config.Config
instances map[string]*InstanceStatus
cron *cron.Cron
mu sync.RWMutex
}
func NewManager(cfg *config.Config) *Manager {
c := cron.New()
c.Start()
m := &Manager{
cfg: cfg,
instances: make(map[string]*InstanceStatus),
cron: c,
}
for _, instCfg := range cfg.Instances {
status := &InstanceStatus{
Config: instCfg,
Databases: make(map[string]*DBStatus),
}
m.instances[instCfg.Name] = status
// Schedule cron job
if instCfg.Schedule != "" {
var id cron.EntryID
id, err := c.AddFunc(instCfg.Schedule, func(name string) func() {
return func() {
m.RunInstance(name)
}
}(instCfg.Name))
if err != nil {
logger.Error(instCfg.Name, "Failed to schedule cron job: %v", err)
} else {
status.CronEntryID = id
status.NextRunTime = c.Entry(id).Next
logger.Info(instCfg.Name, "Scheduled backups, next run at %v", status.NextRunTime)
}
}
}
return m
}
func (m *Manager) GetInstances() []*InstanceStatus {
m.mu.RLock()
defer m.mu.RUnlock()
var result []*InstanceStatus
for _, instCfg := range m.cfg.Instances {
if status, exists := m.instances[instCfg.Name]; exists {
result = append(result, status)
}
}
return result
}
func (m *Manager) GetInstance(name string) *InstanceStatus {
m.mu.RLock()
defer m.mu.RUnlock()
return m.instances[name]
}
func (m *Manager) DiscoverInitial() {
m.mu.RLock()
defer m.mu.RUnlock()
for name, inst := range m.instances {
dbs, err := discoverDatabases(inst.Config)
if err != nil {
logger.Error(name, "Initial database discovery failed: %v", err)
inst.mu.Lock()
inst.OverallResult = "failed"
inst.mu.Unlock()
continue
}
inst.mu.Lock()
var latestInstanceTime time.Time
var hasAnyBackup bool
for _, db := range dbs {
if _, exists := inst.Databases[db]; !exists {
var lastTime time.Time
var lastSize int64
var lastResult string
dbPath := filepath.Join(inst.Config.BackupDir, db)
if entries, err := os.ReadDir(dbPath); err == nil {
for _, e := range entries {
if e.IsDir() {
continue
}
if info, err := e.Info(); err == nil {
if info.ModTime().After(lastTime) {
lastTime = info.ModTime()
lastSize = info.Size()
lastResult = "success"
}
}
}
}
inst.Databases[db] = &DBStatus{
Name: db,
FirstDiscovered: time.Now(),
LastBackupTime: lastTime,
LastBackupSize: lastSize,
LastBackupResult: lastResult,
}
if lastTime.After(latestInstanceTime) {
latestInstanceTime = lastTime
}
if lastResult != "" {
hasAnyBackup = true
}
logger.Info(name, "Discovered initial database: %s", db)
}
}
if latestInstanceTime.After(inst.LastRunTime) {
inst.LastRunTime = latestInstanceTime
}
if hasAnyBackup && inst.OverallResult == "" {
inst.OverallResult = "success"
}
inst.mu.Unlock()
}
}
func (m *Manager) RunAll() {
m.mu.RLock()
defer m.mu.RUnlock()
for name := range m.instances {
go m.RunInstance(name)
}
}
func (m *Manager) RunInstance(name string) {
inst := m.GetInstance(name)
if inst == nil {
return
}
inst.mu.Lock()
if inst.IsRunning {
inst.mu.Unlock()
logger.Warn(name, "Backup job already running, skipping")
return
}
inst.IsRunning = true
inst.OverallResult = "running"
inst.mu.Unlock()
defer func() {
inst.mu.Lock()
inst.IsRunning = false
inst.LastRunTime = time.Now()
if inst.CronEntryID != 0 {
inst.NextRunTime = m.cron.Entry(inst.CronEntryID).Next
}
inst.mu.Unlock()
}()
logger.Info(name, "Starting backup job")
// 1. Discovery
dbs, err := discoverDatabases(inst.Config)
if err != nil {
logger.Error(name, "Database discovery failed: %v", err)
inst.mu.Lock()
inst.OverallResult = "failed"
inst.mu.Unlock()
return
}
sort.Strings(dbs)
inst.mu.Lock()
for _, db := range dbs {
if _, exists := inst.Databases[db]; !exists {
inst.Databases[db] = &DBStatus{
Name: db,
FirstDiscovered: time.Now(),
}
logger.Info(name, "Discovered new database: %s", db)
}
}
inst.mu.Unlock()
// 2. Backup Execution
successCount := 0
failedCount := 0
// We only backup the discovered databases. If a DB disappeared, it will not be in `dbs`.
for _, db := range dbs {
logger.Info(name, "Starting backup for database %s", db)
start := time.Now()
size, err := backupDatabase(inst.Config, db)
duration := time.Since(start)
inst.mu.Lock()
dbStatus := inst.Databases[db]
dbStatus.LastBackupTime = time.Now()
if err != nil {
logger.Error(name, "Failed backup for database %s: %v", db, err)
dbStatus.LastBackupResult = "failed"
failedCount++
} else {
logger.Info(name, "Completed backup for database %s in %v, size %d bytes", db, duration, size)
dbStatus.LastBackupResult = "success"
dbStatus.LastBackupSize = size
successCount++
}
inst.mu.Unlock()
}
// 3. Retention Enforcement
logger.Info(name, "Running retention policy cleanup (keep %d days)", inst.Config.RetentionDays)
deleted, err := enforceRetention(inst.Config)
if err != nil {
logger.Error(name, "Retention cleanup encountered errors: %v", err)
} else {
logger.Info(name, "Retention cleanup finished, deleted %d files", deleted)
}
inst.mu.Lock()
if failedCount == 0 {
inst.OverallResult = "success"
} else if successCount == 0 {
inst.OverallResult = "failed"
} else {
inst.OverallResult = "partial"
}
payload := notify.Payload{
InstanceName: name,
OverallResult: inst.OverallResult,
Time: time.Now(),
}
for _, db := range dbs {
dbStat := inst.Databases[db]
payload.Databases = append(payload.Databases, notify.DBResult{
Name: dbStat.Name,
Size: dbStat.LastBackupSize,
Result: dbStat.LastBackupResult,
})
}
inst.mu.Unlock()
logger.Info(name, "Backup job completed. Result: %s", inst.OverallResult)
notify.Send(m.cfg.Notifications, payload)
}