From 1043b58c164ac48757cdb373967ead2b34aa7388 Mon Sep 17 00:00:00 2001 From: Bernhard Guillon Date: Mon, 6 Jul 2026 21:58:06 +0200 Subject: Add structured error handling with retry logic - errors.go: retry wrapper for transient errors (ENOENT), structured error logging (disk full, permission denied, symlink loops) - sync.go: continue on individual file errors, log clearly, skip broken symlinks instead of failing entire sync - watcher.go: log all errors with context, handle readlink failures - Added 11 tests for error handling scenarios - 50 tests passing --- errors.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 errors.go (limited to 'errors.go') diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..282a08b --- /dev/null +++ b/errors.go @@ -0,0 +1,82 @@ +package main + +import ( + "errors" + "io/fs" + "log" + "os" + "syscall" + "time" +) + +const maxRetries = 3 +const retryDelay = 100 * time.Millisecond + +type SyncError struct { + Path string + Op string + Err error +} + +func (e *SyncError) Error() string { + return e.Op + " " + e.Path + ": " + e.Err.Error() +} + +func (e *SyncError) Unwrap() error { + return e.Err +} + +func retry(name string, fn func() error) error { + var lastErr error + for i := 0; i < maxRetries; i++ { + lastErr = fn() + if lastErr == nil { + return nil + } + if !isRetryable(lastErr) { + return lastErr + } + time.Sleep(retryDelay * time.Duration(i+1)) + } + return lastErr +} + +func isRetryable(err error) bool { + if errors.Is(err, fs.ErrNotExist) { + return true + } + if errors.Is(err, syscall.ENOENT) { + return true + } + return false +} + +func logError(op, relPath string, err error) { + if err == nil { + return + } + if errors.Is(err, fs.ErrNotExist) { + return + } + var pathErr *fs.PathError + if errors.As(err, &pathErr) { + err = pathErr.Err + } + + switch { + case errors.Is(err, syscall.ENOSPC): + log.Printf("DISK FULL: %s %s failed - no space left on device", op, relPath) + case errors.Is(err, syscall.EACCES), errors.Is(err, syscall.EPERM): + log.Printf("PERMISSION DENIED: %s %s - check volume mount permissions", op, relPath) + case errors.Is(err, syscall.ELOOP): + log.Printf("SYMLINK LOOP: %s %s - too many symbolic links", op, relPath) + default: + log.Printf("ERROR: %s %s: %v", op, relPath, err) + } +} + +func copyFileWithRetry(src, dst string, mode os.FileMode) error { + return retry("copy", func() error { + return copyFile(src, dst, mode) + }) +} -- cgit v1.2.3