P0 Critical: - Implement PostgreSQL native restore with COPY FROM support - Implement MySQL native restore with DELIMITER handling P1 High Priority: - Fix deprecated strings.Title usage in mysql.go - Fix unused variable in man.go - Simplify TrimSuffix patterns in schedule.go - Remove unused functions and commands Dashboard: - Add PITR section with 6 new panels - Integrate PITR and dedup metrics into exporter All checks pass: go build, staticcheck, go test -race
192 lines
5.3 KiB
Go
192 lines
5.3 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ProgressTracker tracks backup/restore progress and sends periodic updates
|
|
type ProgressTracker struct {
|
|
manager *Manager
|
|
database string
|
|
operation string
|
|
startTime time.Time
|
|
ticker *time.Ticker
|
|
stopCh chan struct{}
|
|
mu sync.RWMutex
|
|
bytesTotal int64
|
|
bytesProcessed int64
|
|
tablesTotal int
|
|
tablesProcessed int
|
|
currentPhase string
|
|
enabled bool
|
|
}
|
|
|
|
// NewProgressTracker creates a new progress tracker
|
|
func NewProgressTracker(manager *Manager, database, operation string) *ProgressTracker {
|
|
return &ProgressTracker{
|
|
manager: manager,
|
|
database: database,
|
|
operation: operation,
|
|
startTime: time.Now(),
|
|
stopCh: make(chan struct{}),
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
// Start begins sending periodic progress updates
|
|
func (pt *ProgressTracker) Start(interval time.Duration) {
|
|
if !pt.enabled || pt.manager == nil || !pt.manager.HasEnabledNotifiers() {
|
|
return
|
|
}
|
|
|
|
pt.ticker = time.NewTicker(interval)
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-pt.ticker.C:
|
|
pt.sendProgressUpdate()
|
|
case <-pt.stopCh:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Stop stops sending progress updates
|
|
func (pt *ProgressTracker) Stop() {
|
|
if pt.ticker != nil {
|
|
pt.ticker.Stop()
|
|
}
|
|
close(pt.stopCh)
|
|
}
|
|
|
|
// SetTotals sets the expected totals for tracking
|
|
func (pt *ProgressTracker) SetTotals(bytes int64, tables int) {
|
|
pt.mu.Lock()
|
|
defer pt.mu.Unlock()
|
|
pt.bytesTotal = bytes
|
|
pt.tablesTotal = tables
|
|
}
|
|
|
|
// UpdateBytes updates the number of bytes processed
|
|
func (pt *ProgressTracker) UpdateBytes(bytes int64) {
|
|
pt.mu.Lock()
|
|
defer pt.mu.Unlock()
|
|
pt.bytesProcessed = bytes
|
|
}
|
|
|
|
// UpdateTables updates the number of tables processed
|
|
func (pt *ProgressTracker) UpdateTables(tables int) {
|
|
pt.mu.Lock()
|
|
defer pt.mu.Unlock()
|
|
pt.tablesProcessed = tables
|
|
}
|
|
|
|
// SetPhase sets the current operation phase
|
|
func (pt *ProgressTracker) SetPhase(phase string) {
|
|
pt.mu.Lock()
|
|
defer pt.mu.Unlock()
|
|
pt.currentPhase = phase
|
|
}
|
|
|
|
// GetProgress returns current progress information
|
|
func (pt *ProgressTracker) GetProgress() ProgressInfo {
|
|
pt.mu.RLock()
|
|
defer pt.mu.RUnlock()
|
|
|
|
elapsed := time.Since(pt.startTime)
|
|
|
|
var percentBytes, percentTables float64
|
|
if pt.bytesTotal > 0 {
|
|
percentBytes = float64(pt.bytesProcessed) / float64(pt.bytesTotal) * 100
|
|
}
|
|
if pt.tablesTotal > 0 {
|
|
percentTables = float64(pt.tablesProcessed) / float64(pt.tablesTotal) * 100
|
|
}
|
|
|
|
// Estimate remaining time based on bytes processed
|
|
var estimatedRemaining time.Duration
|
|
if pt.bytesProcessed > 0 && pt.bytesTotal > 0 {
|
|
rate := float64(pt.bytesProcessed) / elapsed.Seconds()
|
|
remaining := pt.bytesTotal - pt.bytesProcessed
|
|
estimatedRemaining = time.Duration(float64(remaining) / rate * float64(time.Second))
|
|
}
|
|
|
|
return ProgressInfo{
|
|
Database: pt.database,
|
|
Operation: pt.operation,
|
|
Phase: pt.currentPhase,
|
|
BytesProcessed: pt.bytesProcessed,
|
|
BytesTotal: pt.bytesTotal,
|
|
TablesProcessed: pt.tablesProcessed,
|
|
TablesTotal: pt.tablesTotal,
|
|
PercentBytes: percentBytes,
|
|
PercentTables: percentTables,
|
|
ElapsedTime: elapsed,
|
|
EstimatedRemaining: estimatedRemaining,
|
|
StartTime: pt.startTime,
|
|
}
|
|
}
|
|
|
|
// sendProgressUpdate sends a progress notification
|
|
func (pt *ProgressTracker) sendProgressUpdate() {
|
|
progress := pt.GetProgress()
|
|
|
|
message := fmt.Sprintf("%s of database '%s' in progress: %s",
|
|
pt.operation, pt.database, progress.FormatSummary())
|
|
|
|
event := NewEvent(EventType(pt.operation+"_progress"), SeverityInfo, message).
|
|
WithDatabase(pt.database).
|
|
WithDetail("operation", pt.operation).
|
|
WithDetail("phase", progress.Phase).
|
|
WithDetail("bytes_processed", formatBytes(progress.BytesProcessed)).
|
|
WithDetail("bytes_total", formatBytes(progress.BytesTotal)).
|
|
WithDetail("percent_bytes", fmt.Sprintf("%.1f%%", progress.PercentBytes)).
|
|
WithDetail("tables_processed", fmt.Sprintf("%d", progress.TablesProcessed)).
|
|
WithDetail("tables_total", fmt.Sprintf("%d", progress.TablesTotal)).
|
|
WithDetail("percent_tables", fmt.Sprintf("%.1f%%", progress.PercentTables)).
|
|
WithDetail("elapsed_time", progress.ElapsedTime.String()).
|
|
WithDetail("estimated_remaining", progress.EstimatedRemaining.String())
|
|
|
|
// Send asynchronously
|
|
go pt.manager.NotifySync(context.Background(), event)
|
|
}
|
|
|
|
// ProgressInfo contains snapshot of current progress
|
|
type ProgressInfo struct {
|
|
Database string
|
|
Operation string
|
|
Phase string
|
|
BytesProcessed int64
|
|
BytesTotal int64
|
|
TablesProcessed int
|
|
TablesTotal int
|
|
PercentBytes float64
|
|
PercentTables float64
|
|
ElapsedTime time.Duration
|
|
EstimatedRemaining time.Duration
|
|
StartTime time.Time
|
|
}
|
|
|
|
// FormatSummary returns a human-readable progress summary
|
|
func (pi *ProgressInfo) FormatSummary() string {
|
|
if pi.TablesTotal > 0 {
|
|
return fmt.Sprintf("%d/%d tables (%.1f%%), %s elapsed",
|
|
pi.TablesProcessed, pi.TablesTotal, pi.PercentTables,
|
|
formatDuration(pi.ElapsedTime))
|
|
}
|
|
|
|
if pi.BytesTotal > 0 {
|
|
return fmt.Sprintf("%s/%s (%.1f%%), %s elapsed, %s remaining",
|
|
formatBytes(pi.BytesProcessed), formatBytes(pi.BytesTotal),
|
|
pi.PercentBytes, formatDuration(pi.ElapsedTime),
|
|
formatDuration(pi.EstimatedRemaining))
|
|
}
|
|
|
|
return fmt.Sprintf("%s elapsed", formatDuration(pi.ElapsedTime))
|
|
}
|