From 72c06ba5c272414995a0401c995519a3e3ded5a5 Mon Sep 17 00:00:00 2001 From: Alexander Renz Date: Sun, 18 Jan 2026 18:36:48 +0100 Subject: [PATCH] fix(tui): realtime ETA updates during phase 3 cluster restore Previously, the ETA during phase 3 (database restores) would appear to hang because dbPhaseElapsed was only updated when a new database started restoring, not during the restore operation itself. Fixed by: - Added phase3StartTime to track when phase 3 begins - Calculate dbPhaseElapsed in realtime using time.Since(phase3StartTime) - ETA now updates every 100ms tick instead of only on database transitions This ensures the elapsed time and ETA display continuously update during long-running database restores. --- bin/README.md | 4 ++-- internal/tui/restore_exec.go | 31 ++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/bin/README.md b/bin/README.md index 8060a76..edebbb3 100644 --- a/bin/README.md +++ b/bin/README.md @@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult ## Build Information - **Version**: 3.42.50 -- **Build Time**: 2026-01-18_11:39:42_UTC -- **Git Commit**: 59a717a +- **Build Time**: 2026-01-18_17:17:17_UTC +- **Git Commit**: a0a401c ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/internal/tui/restore_exec.go b/internal/tui/restore_exec.go index 342cf8a..a7c3edf 100755 --- a/internal/tui/restore_exec.go +++ b/internal/tui/restore_exec.go @@ -154,6 +154,7 @@ type sharedProgressState struct { // Timing info for database restore phase dbPhaseElapsed time.Duration // Elapsed time since restore phase started 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) overallPhase int @@ -190,12 +191,12 @@ func clearCurrentRestoreProgress() { 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() defer currentRestoreProgressMu.Unlock() 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() @@ -204,13 +205,20 @@ func getCurrentRestoreProgress() (bytesTotal, bytesDone int64, description strin // Calculate rolling window speed 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, currentRestoreProgressState.description, currentRestoreProgressState.hasUpdate, currentRestoreProgressState.dbTotal, currentRestoreProgressState.dbDone, speed, - currentRestoreProgressState.dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB, + dbPhaseElapsed, currentRestoreProgressState.dbAvgPerDB, currentRestoreProgressState.currentDB, currentRestoreProgressState.overallPhase, currentRestoreProgressState.extractionDone, - currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone + currentRestoreProgressState.dbBytesTotal, currentRestoreProgressState.dbBytesDone, + currentRestoreProgressState.phase3StartTime } // 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.extractionDone = 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 progressState.bytesTotal = 0 progressState.bytesDone = 0 @@ -375,6 +387,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config progressState.dbPhaseElapsed = phaseElapsed progressState.dbAvgPerDB = avgPerDB 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 progressState.bytesTotal = 0 progressState.bytesDone = 0 @@ -392,6 +408,10 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config progressState.overallPhase = 3 progressState.extractionDone = 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 @@ -447,7 +467,8 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.elapsed = time.Since(m.startTime) // 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 { // Phase 1: Extraction m.bytesTotal = bytesTotal