Add configurable export error handling policies (bd-exug)
Implements flexible error handling for export operations with four policies: - strict: Fail-fast on any error (default for user exports) - best-effort: Skip errors with warnings (default for auto-exports) - partial: Retry then skip with manifest tracking - required-core: Fail on core data, skip enrichments Key features: - Per-project configuration via `bd config set export.error_policy` - Separate policy for auto-exports: `auto_export.error_policy` - Retry with exponential backoff (configurable attempts/delay) - Optional export manifests documenting completeness - Per-issue encoding error handling This allows users to choose the right trade-off between data integrity and system availability for their specific project needs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
176
internal/export/policy_test.go
Normal file
176
internal/export/policy_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRetryWithBackoff(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("succeeds first try", func(t *testing.T) {
|
||||
attempts := 0
|
||||
err := RetryWithBackoff(ctx, 3, 100, "test", func() error {
|
||||
attempts++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("expected 1 attempt, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("succeeds after retries", func(t *testing.T) {
|
||||
attempts := 0
|
||||
err := RetryWithBackoff(ctx, 3, 10, "test", func() error {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return errors.New("transient error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Errorf("expected 3 attempts, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails after max retries", func(t *testing.T) {
|
||||
attempts := 0
|
||||
err := RetryWithBackoff(ctx, 3, 10, "test", func() error {
|
||||
attempts++
|
||||
return errors.New("persistent error")
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Errorf("expected 3 attempts, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("respects context cancellation", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
attempts := 0
|
||||
err := RetryWithBackoff(ctx, 10, 100, "test", func() error {
|
||||
attempts++
|
||||
return errors.New("error")
|
||||
})
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Errorf("expected DeadlineExceeded, got %v", err)
|
||||
}
|
||||
// Should stop before reaching max retries due to timeout
|
||||
if attempts >= 10 {
|
||||
t.Errorf("expected fewer than 10 attempts due to timeout, got %d", attempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy ErrorPolicy
|
||||
valid bool
|
||||
}{
|
||||
{"strict", PolicyStrict, true},
|
||||
{"best-effort", PolicyBestEffort, true},
|
||||
{"partial", PolicyPartial, true},
|
||||
{"required-core", PolicyRequiredCore, true},
|
||||
{"invalid", ErrorPolicy("invalid"), false},
|
||||
{"empty", ErrorPolicy(""), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.policy.IsValid(); got != tt.valid {
|
||||
t.Errorf("IsValid() = %v, want %v", got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchWithPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("strict policy fails fast", func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Policy: PolicyStrict,
|
||||
RetryAttempts: 1,
|
||||
RetryBackoffMS: 10,
|
||||
}
|
||||
result := FetchWithPolicy(ctx, cfg, DataTypeCore, "test", func() error {
|
||||
return errors.New("test error")
|
||||
})
|
||||
if result.Err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("expected Success=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("best-effort policy skips errors", func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Policy: PolicyBestEffort,
|
||||
RetryAttempts: 1,
|
||||
RetryBackoffMS: 10,
|
||||
}
|
||||
result := FetchWithPolicy(ctx, cfg, DataTypeLabels, "test", func() error {
|
||||
return errors.New("test error")
|
||||
})
|
||||
if result.Err != nil {
|
||||
t.Errorf("expected no error in best-effort, got %v", result.Err)
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("expected Success=false")
|
||||
}
|
||||
if len(result.Warnings) == 0 {
|
||||
t.Error("expected warnings")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("required-core fails on core data", func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Policy: PolicyRequiredCore,
|
||||
RetryAttempts: 1,
|
||||
RetryBackoffMS: 10,
|
||||
}
|
||||
result := FetchWithPolicy(ctx, cfg, DataTypeCore, "test", func() error {
|
||||
return errors.New("test error")
|
||||
})
|
||||
if result.Err == nil {
|
||||
t.Error("expected error for core data, got nil")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("expected Success=false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("required-core skips enrichment errors", func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Policy: PolicyRequiredCore,
|
||||
RetryAttempts: 1,
|
||||
RetryBackoffMS: 10,
|
||||
}
|
||||
result := FetchWithPolicy(ctx, cfg, DataTypeLabels, "test", func() error {
|
||||
return errors.New("test error")
|
||||
})
|
||||
if result.Err != nil {
|
||||
t.Errorf("expected no error for enrichment, got %v", result.Err)
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("expected Success=false")
|
||||
}
|
||||
if len(result.Warnings) == 0 {
|
||||
t.Error("expected warnings")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user