diff options
| author | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:58:06 +0200 |
|---|---|---|
| committer | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:58:06 +0200 |
| commit | 1043b58c164ac48757cdb373967ead2b34aa7388 (patch) | |
| tree | bc8286a404c17a5eec6a0de9b9adb1aa6201a413 /errors.go | |
| parent | 5a2d70be3ce8524c366f4dd303b950974f05aa7c (diff) | |
| download | sourcewatch-1043b58c164ac48757cdb373967ead2b34aa7388.tar.gz sourcewatch-1043b58c164ac48757cdb373967ead2b34aa7388.zip | |
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
Diffstat (limited to 'errors.go')
| -rw-r--r-- | errors.go | 82 |
1 files changed, 82 insertions, 0 deletions
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) + }) +} |
