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>
128 lines
3.4 KiB
Go
128 lines
3.4 KiB
Go
package export
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ErrorPolicy defines how export operations handle errors
|
|
type ErrorPolicy string
|
|
|
|
const (
|
|
// PolicyStrict fails fast on any error (default for user-initiated exports)
|
|
PolicyStrict ErrorPolicy = "strict"
|
|
|
|
// PolicyBestEffort skips failed operations with warnings (good for auto-export)
|
|
PolicyBestEffort ErrorPolicy = "best-effort"
|
|
|
|
// PolicyPartial retries transient failures, skips persistent ones with manifest
|
|
PolicyPartial ErrorPolicy = "partial"
|
|
|
|
// PolicyRequiredCore fails on core data (issues/deps), skips enrichments (labels/comments)
|
|
PolicyRequiredCore ErrorPolicy = "required-core"
|
|
)
|
|
|
|
// Config keys for export error handling
|
|
const (
|
|
ConfigKeyErrorPolicy = "export.error_policy"
|
|
ConfigKeyRetryAttempts = "export.retry_attempts"
|
|
ConfigKeyRetryBackoffMS = "export.retry_backoff_ms"
|
|
ConfigKeySkipEncodingErrors = "export.skip_encoding_errors"
|
|
ConfigKeyWriteManifest = "export.write_manifest"
|
|
ConfigKeyAutoExportPolicy = "auto_export.error_policy"
|
|
)
|
|
|
|
// Default values
|
|
const (
|
|
DefaultErrorPolicy = PolicyStrict
|
|
DefaultRetryAttempts = 3
|
|
DefaultRetryBackoffMS = 100
|
|
DefaultSkipEncodingErrors = false
|
|
DefaultWriteManifest = false
|
|
DefaultAutoExportPolicy = PolicyBestEffort
|
|
)
|
|
|
|
// Config holds export error handling configuration
|
|
type Config struct {
|
|
Policy ErrorPolicy
|
|
RetryAttempts int
|
|
RetryBackoffMS int
|
|
SkipEncodingErrors bool
|
|
WriteManifest bool
|
|
IsAutoExport bool // If true, may use different policy
|
|
}
|
|
|
|
// Manifest tracks export completeness and failures
|
|
type Manifest struct {
|
|
ExportedCount int `json:"exported_count"`
|
|
FailedIssues []FailedIssue `json:"failed_issues,omitempty"`
|
|
PartialData []string `json:"partial_data,omitempty"` // e.g., ["labels", "comments"]
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Complete bool `json:"complete"`
|
|
ExportedAt time.Time `json:"exported_at"`
|
|
ErrorPolicy string `json:"error_policy"`
|
|
}
|
|
|
|
// FailedIssue tracks a single issue that failed to export
|
|
type FailedIssue struct {
|
|
IssueID string `json:"issue_id"`
|
|
Reason string `json:"reason"`
|
|
MissingData []string `json:"missing_data,omitempty"` // e.g., ["labels", "comments"]
|
|
}
|
|
|
|
// RetryWithBackoff wraps a function with retry logic
|
|
func RetryWithBackoff(ctx context.Context, attempts int, initialBackoffMS int, desc string, fn func() error) error {
|
|
if attempts < 1 {
|
|
attempts = 1
|
|
}
|
|
|
|
var lastErr error
|
|
backoff := time.Duration(initialBackoffMS) * time.Millisecond
|
|
|
|
for attempt := 1; attempt <= attempts; attempt++ {
|
|
err := fn()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
|
|
// Don't retry on context cancellation
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Don't wait after last attempt
|
|
if attempt == attempts {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(backoff):
|
|
backoff *= 2 // Exponential backoff
|
|
}
|
|
}
|
|
|
|
if attempts > 1 {
|
|
return fmt.Errorf("%s failed after %d attempts: %w", desc, attempts, lastErr)
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// IsValid checks if the policy is a valid value
|
|
func (p ErrorPolicy) IsValid() bool {
|
|
switch p {
|
|
case PolicyStrict, PolicyBestEffort, PolicyPartial, PolicyRequiredCore:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// String implements fmt.Stringer
|
|
func (p ErrorPolicy) String() string {
|
|
return string(p)
|
|
}
|