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