Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b305d1342e | |||
| 5456da7183 | |||
| f9ff45cf2a | |||
| 72c06ba5c2 |
@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
|
|||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.50
|
- **Version**: 3.42.50
|
||||||
- **Build Time**: 2026-01-18_11:39:42_UTC
|
- **Build Time**: 2026-01-18_17:52:44_UTC
|
||||||
- **Git Commit**: 59a717a
|
- **Git Commit**: f9ff45c
|
||||||
|
|
||||||
## Recent Updates (v1.1.0)
|
## Recent Updates (v1.1.0)
|
||||||
- ✅ Fixed TUI progress display with line-by-line output
|
- ✅ Fixed TUI progress display with line-by-line output
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ import (
|
|||||||
"dbbackup/internal/config"
|
"dbbackup/internal/config"
|
||||||
"dbbackup/internal/database"
|
"dbbackup/internal/database"
|
||||||
"dbbackup/internal/logger"
|
"dbbackup/internal/logger"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Backup phase constants for consistency
|
||||||
|
const (
|
||||||
|
backupPhaseGlobals = 1
|
||||||
|
backupPhaseDatabases = 2
|
||||||
|
backupPhaseCompressing = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackupExecutionModel handles backup execution with progress
|
// BackupExecutionModel handles backup execution with progress
|
||||||
@@ -31,7 +39,10 @@ type BackupExecutionModel struct {
|
|||||||
cancelling bool // True when user has requested cancellation
|
cancelling bool // True when user has requested cancellation
|
||||||
err error
|
err error
|
||||||
result string
|
result string
|
||||||
|
archivePath string // Path to created archive (for summary)
|
||||||
|
archiveSize int64 // Size of created archive (for summary)
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
elapsed time.Duration // Final elapsed time
|
||||||
details []string
|
details []string
|
||||||
spinnerFrame int
|
spinnerFrame int
|
||||||
|
|
||||||
@@ -41,6 +52,9 @@ type BackupExecutionModel struct {
|
|||||||
dbName string // Current database being backed up
|
dbName string // Current database being backed up
|
||||||
overallPhase int // 1=globals, 2=databases, 3=compressing
|
overallPhase int // 1=globals, 2=databases, 3=compressing
|
||||||
phaseDesc string // Description of current phase
|
phaseDesc string // Description of current phase
|
||||||
|
phase2StartTime time.Time // When phase 2 (databases) started (for realtime ETA)
|
||||||
|
dbPhaseElapsed time.Duration // Elapsed time since database backup phase started
|
||||||
|
dbAvgPerDB time.Duration // Average time per database backup
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedBackupProgressState holds progress state that can be safely accessed from callbacks
|
// sharedBackupProgressState holds progress state that can be safely accessed from callbacks
|
||||||
@@ -52,6 +66,9 @@ type sharedBackupProgressState struct {
|
|||||||
overallPhase int // 1=globals, 2=databases, 3=compressing
|
overallPhase int // 1=globals, 2=databases, 3=compressing
|
||||||
phaseDesc string // Description of current phase
|
phaseDesc string // Description of current phase
|
||||||
hasUpdate bool
|
hasUpdate bool
|
||||||
|
phase2StartTime time.Time // When phase 2 started (for realtime ETA calculation)
|
||||||
|
dbPhaseElapsed time.Duration // Elapsed time since database backup phase started
|
||||||
|
dbAvgPerDB time.Duration // Average time per database backup
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package-level shared progress state for backup operations
|
// Package-level shared progress state for backup operations
|
||||||
@@ -72,12 +89,12 @@ func clearCurrentBackupProgress() {
|
|||||||
currentBackupProgressState = nil
|
currentBackupProgressState = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhase int, phaseDesc string, hasUpdate bool) {
|
func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhase int, phaseDesc string, hasUpdate bool, dbPhaseElapsed, dbAvgPerDB time.Duration, phase2StartTime time.Time) {
|
||||||
currentBackupProgressMu.Lock()
|
currentBackupProgressMu.Lock()
|
||||||
defer currentBackupProgressMu.Unlock()
|
defer currentBackupProgressMu.Unlock()
|
||||||
|
|
||||||
if currentBackupProgressState == nil {
|
if currentBackupProgressState == nil {
|
||||||
return 0, 0, "", 0, "", false
|
return 0, 0, "", 0, "", false, 0, 0, time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentBackupProgressState.mu.Lock()
|
currentBackupProgressState.mu.Lock()
|
||||||
@@ -86,9 +103,17 @@ func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, overallPhas
|
|||||||
hasUpdate = currentBackupProgressState.hasUpdate
|
hasUpdate = currentBackupProgressState.hasUpdate
|
||||||
currentBackupProgressState.hasUpdate = false
|
currentBackupProgressState.hasUpdate = false
|
||||||
|
|
||||||
|
// Calculate realtime phase elapsed if we have a phase 2 start time
|
||||||
|
dbPhaseElapsed = currentBackupProgressState.dbPhaseElapsed
|
||||||
|
if !currentBackupProgressState.phase2StartTime.IsZero() {
|
||||||
|
dbPhaseElapsed = time.Since(currentBackupProgressState.phase2StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
return currentBackupProgressState.dbTotal, currentBackupProgressState.dbDone,
|
return currentBackupProgressState.dbTotal, currentBackupProgressState.dbDone,
|
||||||
currentBackupProgressState.dbName, currentBackupProgressState.overallPhase,
|
currentBackupProgressState.dbName, currentBackupProgressState.overallPhase,
|
||||||
currentBackupProgressState.phaseDesc, hasUpdate
|
currentBackupProgressState.phaseDesc, hasUpdate,
|
||||||
|
dbPhaseElapsed, currentBackupProgressState.dbAvgPerDB,
|
||||||
|
currentBackupProgressState.phase2StartTime
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
|
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
|
||||||
@@ -134,6 +159,9 @@ type backupProgressMsg struct {
|
|||||||
type backupCompleteMsg struct {
|
type backupCompleteMsg struct {
|
||||||
result string
|
result string
|
||||||
err error
|
err error
|
||||||
|
archivePath string
|
||||||
|
archiveSize int64
|
||||||
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int) tea.Cmd {
|
func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int) tea.Cmd {
|
||||||
@@ -176,9 +204,13 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
|
|||||||
progressState.dbDone = done
|
progressState.dbDone = done
|
||||||
progressState.dbTotal = total
|
progressState.dbTotal = total
|
||||||
progressState.dbName = currentDB
|
progressState.dbName = currentDB
|
||||||
progressState.overallPhase = 2 // Phase 2: Backing up databases
|
progressState.overallPhase = backupPhaseDatabases
|
||||||
progressState.phaseDesc = fmt.Sprintf("Phase 2/3: Databases (%d/%d)", done, total)
|
progressState.phaseDesc = fmt.Sprintf("Phase 2/3: Backing up Databases (%d/%d)", done, total)
|
||||||
progressState.hasUpdate = true
|
progressState.hasUpdate = true
|
||||||
|
// Set phase 2 start time on first callback (for realtime ETA calculation)
|
||||||
|
if progressState.phase2StartTime.IsZero() {
|
||||||
|
progressState.phase2StartTime = time.Now()
|
||||||
|
}
|
||||||
progressState.mu.Unlock()
|
progressState.mu.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -218,6 +250,7 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
|
|||||||
return backupCompleteMsg{
|
return backupCompleteMsg{
|
||||||
result: result,
|
result: result,
|
||||||
err: nil,
|
err: nil,
|
||||||
|
elapsed: elapsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,13 +263,15 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
|
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
|
||||||
|
|
||||||
// Poll for database progress updates from callbacks
|
// Poll for database progress updates from callbacks
|
||||||
dbTotal, dbDone, dbName, overallPhase, phaseDesc, hasUpdate := getCurrentBackupProgress()
|
dbTotal, dbDone, dbName, overallPhase, phaseDesc, hasUpdate, dbPhaseElapsed, dbAvgPerDB, _ := getCurrentBackupProgress()
|
||||||
if hasUpdate {
|
if hasUpdate {
|
||||||
m.dbTotal = dbTotal
|
m.dbTotal = dbTotal
|
||||||
m.dbDone = dbDone
|
m.dbDone = dbDone
|
||||||
m.dbName = dbName
|
m.dbName = dbName
|
||||||
m.overallPhase = overallPhase
|
m.overallPhase = overallPhase
|
||||||
m.phaseDesc = phaseDesc
|
m.phaseDesc = phaseDesc
|
||||||
|
m.dbPhaseElapsed = dbPhaseElapsed
|
||||||
|
m.dbAvgPerDB = dbAvgPerDB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status based on progress and elapsed time
|
// Update status based on progress and elapsed time
|
||||||
@@ -284,6 +319,7 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.done = true
|
m.done = true
|
||||||
m.err = msg.err
|
m.err = msg.err
|
||||||
m.result = msg.result
|
m.result = msg.result
|
||||||
|
m.elapsed = msg.elapsed
|
||||||
if m.err == nil {
|
if m.err == nil {
|
||||||
m.status = "[OK] Backup completed successfully!"
|
m.status = "[OK] Backup completed successfully!"
|
||||||
} else {
|
} else {
|
||||||
@@ -361,14 +397,52 @@ func renderBackupDatabaseProgressBar(done, total int, dbName string, width int)
|
|||||||
return fmt.Sprintf(" Database: [%s] %d/%d", bar, done, total)
|
return fmt.Sprintf(" Database: [%s] %d/%d", bar, done, total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderBackupDatabaseProgressBarWithTiming renders database backup progress with ETA
|
||||||
|
func renderBackupDatabaseProgressBarWithTiming(done, total int, dbPhaseElapsed, dbAvgPerDB time.Duration) string {
|
||||||
|
if total == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate progress percentage
|
||||||
|
percent := float64(done) / float64(total)
|
||||||
|
if percent > 1.0 {
|
||||||
|
percent = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build progress bar
|
||||||
|
barWidth := 50
|
||||||
|
filled := int(float64(barWidth) * percent)
|
||||||
|
if filled > barWidth {
|
||||||
|
filled = barWidth
|
||||||
|
}
|
||||||
|
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
|
||||||
|
|
||||||
|
// Calculate ETA similar to restore
|
||||||
|
var etaStr string
|
||||||
|
if done > 0 && done < total {
|
||||||
|
avgPerDB := dbPhaseElapsed / time.Duration(done)
|
||||||
|
remaining := total - done
|
||||||
|
eta := avgPerDB * time.Duration(remaining)
|
||||||
|
etaStr = fmt.Sprintf(" | ETA: %s", formatDuration(eta))
|
||||||
|
} else if done == total {
|
||||||
|
etaStr = " | Complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(" Databases: [%s] %d/%d | Elapsed: %s%s\n",
|
||||||
|
bar, done, total, formatDuration(dbPhaseElapsed), etaStr)
|
||||||
|
}
|
||||||
|
|
||||||
func (m BackupExecutionModel) View() string {
|
func (m BackupExecutionModel) View() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.Grow(512) // Pre-allocate estimated capacity for better performance
|
s.Grow(512) // Pre-allocate estimated capacity for better performance
|
||||||
|
|
||||||
// Clear screen with newlines and render header
|
// Clear screen with newlines and render header
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
header := titleStyle.Render("[EXEC] Backup Execution")
|
header := "[EXEC] Backing up Database"
|
||||||
s.WriteString(header)
|
if m.backupType == "cluster" {
|
||||||
|
header = "[EXEC] Cluster Backup"
|
||||||
|
}
|
||||||
|
s.WriteString(titleStyle.Render(header))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Backup details - properly aligned
|
// Backup details - properly aligned
|
||||||
@@ -379,7 +453,6 @@ func (m BackupExecutionModel) View() string {
|
|||||||
if m.ratio > 0 {
|
if m.ratio > 0 {
|
||||||
s.WriteString(fmt.Sprintf(" %-10s %d\n", "Sample:", m.ratio))
|
s.WriteString(fmt.Sprintf(" %-10s %d\n", "Sample:", m.ratio))
|
||||||
}
|
}
|
||||||
s.WriteString(fmt.Sprintf(" %-10s %s\n", "Duration:", time.Since(m.startTime).Round(time.Second)))
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Status display
|
// Status display
|
||||||
@@ -395,11 +468,15 @@ func (m BackupExecutionModel) View() string {
|
|||||||
|
|
||||||
elapsedSec := int(time.Since(m.startTime).Seconds())
|
elapsedSec := int(time.Since(m.startTime).Seconds())
|
||||||
|
|
||||||
if m.overallPhase == 2 && m.dbTotal > 0 {
|
if m.overallPhase == backupPhaseDatabases && m.dbTotal > 0 {
|
||||||
// Phase 2: Database backups - contributes 15-90%
|
// Phase 2: Database backups - contributes 15-90%
|
||||||
dbPct := int((int64(m.dbDone) * 100) / int64(m.dbTotal))
|
dbPct := int((int64(m.dbDone) * 100) / int64(m.dbTotal))
|
||||||
overallProgress = 15 + (dbPct * 75 / 100)
|
overallProgress = 15 + (dbPct * 75 / 100)
|
||||||
phaseLabel = m.phaseDesc
|
phaseLabel = m.phaseDesc
|
||||||
|
} else if m.overallPhase == backupPhaseCompressing {
|
||||||
|
// Phase 3: Compressing archive
|
||||||
|
overallProgress = 92
|
||||||
|
phaseLabel = "Phase 3/3: Compressing Archive"
|
||||||
} else if elapsedSec < 5 {
|
} else if elapsedSec < 5 {
|
||||||
// Initial setup
|
// Initial setup
|
||||||
overallProgress = 2
|
overallProgress = 2
|
||||||
@@ -430,9 +507,9 @@ func (m BackupExecutionModel) View() string {
|
|||||||
}
|
}
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Database progress bar
|
// Database progress bar with timing
|
||||||
progressBar := renderBackupDatabaseProgressBar(m.dbDone, m.dbTotal, m.dbName, 50)
|
s.WriteString(renderBackupDatabaseProgressBarWithTiming(m.dbDone, m.dbTotal, m.dbPhaseElapsed, m.dbAvgPerDB))
|
||||||
s.WriteString(progressBar + "\n")
|
s.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
// Intermediate phase (globals)
|
// Intermediate phase (globals)
|
||||||
spinner := spinnerFrames[m.spinnerFrame]
|
spinner := spinnerFrames[m.spinnerFrame]
|
||||||
@@ -449,7 +526,10 @@ func (m BackupExecutionModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !m.cancelling {
|
if !m.cancelling {
|
||||||
s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n")
|
// Elapsed time
|
||||||
|
s.WriteString(fmt.Sprintf("Elapsed: %s\n", formatDuration(time.Since(m.startTime))))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(infoStyle.Render("[KEYS] Press Ctrl+C or ESC to cancel"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show completion summary with detailed stats
|
// Show completion summary with detailed stats
|
||||||
@@ -474,6 +554,14 @@ func (m BackupExecutionModel) View() string {
|
|||||||
s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────"))
|
s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Archive info (if available)
|
||||||
|
if m.archivePath != "" {
|
||||||
|
s.WriteString(fmt.Sprintf(" Archive: %s\n", filepath.Base(m.archivePath)))
|
||||||
|
}
|
||||||
|
if m.archiveSize > 0 {
|
||||||
|
s.WriteString(fmt.Sprintf(" Archive Size: %s\n", FormatBytes(m.archiveSize)))
|
||||||
|
}
|
||||||
|
|
||||||
// Backup type specific info
|
// Backup type specific info
|
||||||
switch m.backupType {
|
switch m.backupType {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
@@ -497,12 +585,21 @@ func (m BackupExecutionModel) View() string {
|
|||||||
s.WriteString(infoStyle.Render(" ─── Timing ────────────────────────────────────────────────"))
|
s.WriteString(infoStyle.Render(" ─── Timing ────────────────────────────────────────────────"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
elapsed := time.Since(m.startTime)
|
elapsed := m.elapsed
|
||||||
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatBackupDuration(elapsed)))
|
if elapsed == 0 {
|
||||||
|
elapsed = time.Since(m.startTime)
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatDuration(elapsed)))
|
||||||
|
|
||||||
|
// Calculate and show throughput if we have size info
|
||||||
|
if m.archiveSize > 0 && elapsed.Seconds() > 0 {
|
||||||
|
throughput := float64(m.archiveSize) / elapsed.Seconds()
|
||||||
|
s.WriteString(fmt.Sprintf(" Throughput: %s/s (average)\n", FormatBytes(int64(throughput))))
|
||||||
|
}
|
||||||
|
|
||||||
if m.backupType == "cluster" && m.dbTotal > 0 && m.err == nil {
|
if m.backupType == "cluster" && m.dbTotal > 0 && m.err == nil {
|
||||||
avgPerDB := elapsed / time.Duration(m.dbTotal)
|
avgPerDB := elapsed / time.Duration(m.dbTotal)
|
||||||
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB)))
|
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatDuration(avgPerDB)))
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
@@ -513,18 +610,3 @@ func (m BackupExecutionModel) View() string {
|
|||||||
|
|
||||||
return s.String()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatBackupDuration formats duration in human readable format
|
|
||||||
func formatBackupDuration(d time.Duration) string {
|
|
||||||
if d < time.Minute {
|
|
||||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
||||||
}
|
|
||||||
if d < time.Hour {
|
|
||||||
minutes := int(d.Minutes())
|
|
||||||
seconds := int(d.Seconds()) % 60
|
|
||||||
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
|
||||||
}
|
|
||||||
hours := int(d.Hours())
|
|
||||||
minutes := int(d.Minutes()) % 60
|
|
||||||
return fmt.Sprintf("%dh %dm", hours, minutes)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ type sharedProgressState struct {
|
|||||||
// Timing info for database restore phase
|
// Timing info for database restore phase
|
||||||
dbPhaseElapsed time.Duration // Elapsed time since restore phase started
|
dbPhaseElapsed time.Duration // Elapsed time since restore phase started
|
||||||
dbAvgPerDB time.Duration // Average time per database restore
|
dbAvgPerDB time.Duration // Average time per database restore
|
||||||
|
phase3StartTime time.Time // When phase 3 started (for realtime ETA calculation)
|
||||||
|
|
||||||
// Overall phase tracking (1=Extract, 2=Globals, 3=Databases)
|
// Overall phase tracking (1=Extract, 2=Globals, 3=Databases)
|
||||||
overallPhase int
|
overallPhase int
|
||||||
@@ -190,12 +191,12 @@ func clearCurrentRestoreProgress() {
|
|||||||
currentRestoreProgressState = nil
|
currentRestoreProgressState = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration, currentDB string, overallPhase int, extractionDone bool, dbBytesTotal, dbBytesDone int64) {
|
func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description string, hasUpdate bool, dbTotal, dbDone int, speed float64, dbPhaseElapsed, dbAvgPerDB time.Duration, currentDB string, overallPhase int, extractionDone bool, dbBytesTotal, dbBytesDone int64, phase3StartTime time.Time) {
|
||||||
currentRestoreProgressMu.Lock()
|
currentRestoreProgressMu.Lock()
|
||||||
defer currentRestoreProgressMu.Unlock()
|
defer currentRestoreProgressMu.Unlock()
|
||||||
|
|
||||||
if currentRestoreProgressState == nil {
|
if currentRestoreProgressState == nil {
|
||||||
return 0, 0, "", false, 0, 0, 0, 0, 0, "", 0, false, 0, 0
|
return 0, 0, "", false, 0, 0, 0, 0, 0, "", 0, false, 0, 0, time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRestoreProgressState.mu.Lock()
|
currentRestoreProgressState.mu.Lock()
|
||||||
@@ -204,13 +205,20 @@ func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description strin
|
|||||||
// Calculate rolling window speed
|
// Calculate rolling window speed
|
||||||
speed = calculateRollingSpeed(currentRestoreProgressState.speedSamples)
|
speed = calculateRollingSpeed(currentRestoreProgressState.speedSamples)
|
||||||
|
|
||||||
|
// Calculate realtime phase elapsed if we have a phase 3 start time
|
||||||
|
dbPhaseElapsed = currentRestoreProgressState.dbPhaseElapsed
|
||||||
|
if !currentRestoreProgressState.phase3StartTime.IsZero() {
|
||||||
|
dbPhaseElapsed = time.Since(currentRestoreProgressState.phase3StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
return currentRestoreProgressState.bytesTotal, currentRestoreProgressState.bytesDone,
|
return currentRestoreProgressState.bytesTotal, currentRestoreProgressState.bytesDone,
|
||||||
currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate,
|
currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate,
|
||||||
currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed,
|
currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed,
|
||||||
currentRestoreProgressState.dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB,
|
dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB,
|
||||||
currentRestoreProgressState.currentDB, currentRestoreProgressState.overallPhase,
|
currentRestoreProgressState.currentDB, currentRestoreProgressState.overallPhase,
|
||||||
currentRestoreProgressState.extractionDone,
|
currentRestoreProgressState.extractionDone,
|
||||||
currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone
|
currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone,
|
||||||
|
currentRestoreProgressState.phase3StartTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateRollingSpeed calculates speed from recent samples (last 5 seconds)
|
// calculateRollingSpeed calculates speed from recent samples (last 5 seconds)
|
||||||
@@ -357,6 +365,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
|
|||||||
progressState.overallPhase = 3
|
progressState.overallPhase = 3
|
||||||
progressState.extractionDone = true
|
progressState.extractionDone = true
|
||||||
progressState.hasUpdate = true
|
progressState.hasUpdate = true
|
||||||
|
// Set phase 3 start time on first callback (for realtime ETA calculation)
|
||||||
|
if progressState.phase3StartTime.IsZero() {
|
||||||
|
progressState.phase3StartTime = time.Now()
|
||||||
|
}
|
||||||
// Clear byte progress when switching to db progress
|
// Clear byte progress when switching to db progress
|
||||||
progressState.bytesTotal = 0
|
progressState.bytesTotal = 0
|
||||||
progressState.bytesDone = 0
|
progressState.bytesDone = 0
|
||||||
@@ -375,6 +387,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
|
|||||||
progressState.dbPhaseElapsed = phaseElapsed
|
progressState.dbPhaseElapsed = phaseElapsed
|
||||||
progressState.dbAvgPerDB = avgPerDB
|
progressState.dbAvgPerDB = avgPerDB
|
||||||
progressState.hasUpdate = true
|
progressState.hasUpdate = true
|
||||||
|
// Set phase 3 start time on first callback (for realtime ETA calculation)
|
||||||
|
if progressState.phase3StartTime.IsZero() {
|
||||||
|
progressState.phase3StartTime = time.Now()
|
||||||
|
}
|
||||||
// Clear byte progress when switching to db progress
|
// Clear byte progress when switching to db progress
|
||||||
progressState.bytesTotal = 0
|
progressState.bytesTotal = 0
|
||||||
progressState.bytesDone = 0
|
progressState.bytesDone = 0
|
||||||
@@ -392,6 +408,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
|
|||||||
progressState.overallPhase = 3
|
progressState.overallPhase = 3
|
||||||
progressState.extractionDone = true
|
progressState.extractionDone = true
|
||||||
progressState.hasUpdate = true
|
progressState.hasUpdate = true
|
||||||
|
// Set phase 3 start time on first callback (for realtime ETA calculation)
|
||||||
|
if progressState.phase3StartTime.IsZero() {
|
||||||
|
progressState.phase3StartTime = time.Now()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Store progress state in a package-level variable for the ticker to access
|
// Store progress state in a package-level variable for the ticker to access
|
||||||
@@ -447,7 +467,8 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.elapsed = time.Since(m.startTime)
|
m.elapsed = time.Since(m.startTime)
|
||||||
|
|
||||||
// Poll shared progress state for real-time updates
|
// Poll shared progress state for real-time updates
|
||||||
bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB, currentDB, overallPhase, extractionDone, dbBytesTotal, dbBytesDone := getCurrentRestoreProgress()
|
// Note: dbPhaseElapsed is now calculated in realtime inside getCurrentRestoreProgress()
|
||||||
|
bytesTotal, bytesDone, description, hasUpdate, dbTotal, dbDone, speed, dbPhaseElapsed, dbAvgPerDB, currentDB, overallPhase, extractionDone, dbBytesTotal, dbBytesDone, _ := getCurrentRestoreProgress()
|
||||||
if hasUpdate && bytesTotal > 0 && !extractionDone {
|
if hasUpdate && bytesTotal > 0 && !extractionDone {
|
||||||
// Phase 1: Extraction
|
// Phase 1: Extraction
|
||||||
m.bytesTotal = bytesTotal
|
m.bytesTotal = bytesTotal
|
||||||
@@ -1150,12 +1171,12 @@ func formatRestoreError(errStr string) string {
|
|||||||
|
|
||||||
// Provide specific recommendations based on error
|
// Provide specific recommendations based on error
|
||||||
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
|
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
|
||||||
s.WriteString(errorStyle.Render(" • Cannot access file: stat : no such file or directory\n"))
|
s.WriteString(errorStyle.Render(" • PostgreSQL lock table exhausted\n"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
|
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
s.WriteString(" Lock table exhausted. Total capacity = max_locks_per_transaction\n")
|
s.WriteString(" Lock capacity = max_locks_per_transaction\n")
|
||||||
s.WriteString(" × (max_connections + max_prepared_transactions).\n\n")
|
s.WriteString(" × (max_connections + max_prepared_transactions)\n\n")
|
||||||
s.WriteString(" If you reduced VM size or max_connections, you need higher\n")
|
s.WriteString(" If you reduced VM size or max_connections, you need higher\n")
|
||||||
s.WriteString(" max_locks_per_transaction to compensate.\n\n")
|
s.WriteString(" max_locks_per_transaction to compensate.\n\n")
|
||||||
s.WriteString(successStyle.Render(" FIX OPTIONS:\n"))
|
s.WriteString(successStyle.Render(" FIX OPTIONS:\n"))
|
||||||
|
|||||||
Reference in New Issue
Block a user