From 7b4ab7631349e79d33d97c9f8da7cfd8012238ce Mon Sep 17 00:00:00 2001 From: Alexander Renz Date: Wed, 14 Jan 2026 15:59:12 +0100 Subject: [PATCH] v3.42.30: Add go-multierror for better error aggregation - Use hashicorp/go-multierror for cluster restore error collection - Shows ALL failed databases with full error context (not just count) - Bullet-pointed output for readability - Thread-safe error aggregation with dedicated mutex - Error wrapping with %w for proper error chain preservation --- CHANGELOG.md | 20 ++++++++++++++++++++ go.mod | 2 ++ go.sum | 4 ++++ internal/restore/engine.go | 31 +++++++++++++++++++++---------- main.go | 2 +- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b9c74..bc42b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to dbbackup will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.42.30] - 2026-01-09 "Better Error Aggregation" + +### Added - go-multierror for Cluster Restore Errors +- **Enhanced error reporting** - Now shows ALL database failures, not just a count +- Uses `hashicorp/go-multierror` for proper error aggregation +- Each failed database error is preserved with full context +- Bullet-pointed error output for readability: + ``` + cluster restore completed with 3 failures: + 3 database(s) failed: + • db1: restore failed: max_locks_per_transaction exceeded + • db2: restore failed: connection refused + • db3: failed to create database: permission denied + ``` + +### Changed +- Replaced string slice error collection with proper `*multierror.Error` +- Thread-safe error aggregation with dedicated mutex +- Improved error wrapping with `%w` for error chain preservation + ## [3.42.10] - 2026-01-08 "Code Quality" ### Fixed - Code Quality Issues diff --git a/go.mod b/go.mod index 0e4fb5b..d814584 100755 --- a/go.mod +++ b/go.mod @@ -75,6 +75,8 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/go.sum b/go.sum index 56b92ee..c2019f1 100755 --- a/go.sum +++ b/go.sum @@ -149,6 +149,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAV github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/internal/restore/engine.go b/internal/restore/engine.go index afcc011..76a7cbf 100755 --- a/internal/restore/engine.go +++ b/internal/restore/engine.go @@ -20,6 +20,7 @@ import ( "dbbackup/internal/progress" "dbbackup/internal/security" + "github.com/hashicorp/go-multierror" _ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver ) @@ -961,7 +962,8 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { }() } - var failedDBs []string + var restoreErrors *multierror.Error + var restoreErrorsMu sync.Mutex totalDBs := 0 // Count total databases @@ -995,7 +997,6 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { } var successCount, failCount int32 - var failedDBsMu sync.Mutex var mu sync.Mutex // Protect shared resources (progress, logger) // Create semaphore to limit concurrency @@ -1050,9 +1051,9 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { // STEP 2: Create fresh database if err := e.ensureDatabaseExists(ctx, dbName); err != nil { e.log.Error("Failed to create database", "name", dbName, "error", err) - failedDBsMu.Lock() - failedDBs = append(failedDBs, fmt.Sprintf("%s: failed to create database: %v", dbName, err)) - failedDBsMu.Unlock() + restoreErrorsMu.Lock() + restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%s: failed to create database: %w", dbName, err)) + restoreErrorsMu.Unlock() atomic.AddInt32(&failCount, 1) return } @@ -1095,10 +1096,10 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { mu.Unlock() } - failedDBsMu.Lock() + restoreErrorsMu.Lock() // Include more context in the error message - failedDBs = append(failedDBs, fmt.Sprintf("%s: restore failed: %v", dbName, restoreErr)) - failedDBsMu.Unlock() + restoreErrors = multierror.Append(restoreErrors, fmt.Errorf("%s: restore failed: %w", dbName, restoreErr)) + restoreErrorsMu.Unlock() atomic.AddInt32(&failCount, 1) return } @@ -1116,7 +1117,17 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { failCountFinal := int(atomic.LoadInt32(&failCount)) if failCountFinal > 0 { - failedList := strings.Join(failedDBs, "\n ") + // Format multi-error with detailed output + restoreErrors.ErrorFormat = func(errs []error) string { + if len(errs) == 1 { + return errs[0].Error() + } + points := make([]string, len(errs)) + for i, err := range errs { + points[i] = fmt.Sprintf(" • %s", err.Error()) + } + return fmt.Sprintf("%d database(s) failed:\n%s", len(errs), strings.Join(points, "\n")) + } // Log summary e.log.Info("Cluster restore completed with failures", @@ -1127,7 +1138,7 @@ func (e *Engine) RestoreCluster(ctx context.Context, archivePath string) error { e.progress.Fail(fmt.Sprintf("Cluster restore: %d succeeded, %d failed out of %d total", successCountFinal, failCountFinal, totalDBs)) operation.Complete(fmt.Sprintf("Partial restore: %d/%d databases succeeded", successCountFinal, totalDBs)) - return fmt.Errorf("cluster restore completed with %d failures:\n %s", failCountFinal, failedList) + return fmt.Errorf("cluster restore completed with %d failures:\n%s", failCountFinal, restoreErrors.Error()) } e.progress.Complete(fmt.Sprintf("Cluster restored successfully: %d databases", successCountFinal)) diff --git a/main.go b/main.go index e0d682a..5ad5254 100755 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( // Build information (set by ldflags) var ( - version = "3.42.10" + version = "3.42.30" buildTime = "unknown" gitCommit = "unknown" )