Files
beads/internal/export/policy.go
Steve Yegge e3e0a04496 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>
2025-11-23 20:21:51 -08:00

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)
}