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:
117
internal/export/config.go
Normal file
117
internal/export/config.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
)
|
||||
|
||||
// ConfigStore defines the minimal storage interface needed for config
|
||||
type ConfigStore interface {
|
||||
GetConfig(ctx context.Context, key string) (string, error)
|
||||
SetConfig(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
// LoadConfig reads export configuration from storage
|
||||
func LoadConfig(ctx context.Context, store ConfigStore, isAutoExport bool) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Policy: DefaultErrorPolicy,
|
||||
RetryAttempts: DefaultRetryAttempts,
|
||||
RetryBackoffMS: DefaultRetryBackoffMS,
|
||||
SkipEncodingErrors: DefaultSkipEncodingErrors,
|
||||
WriteManifest: DefaultWriteManifest,
|
||||
IsAutoExport: isAutoExport,
|
||||
}
|
||||
|
||||
// Load error policy
|
||||
if isAutoExport {
|
||||
// Check auto-export specific policy first
|
||||
if val, err := store.GetConfig(ctx, ConfigKeyAutoExportPolicy); err == nil && val != "" {
|
||||
policy := ErrorPolicy(val)
|
||||
if policy.IsValid() {
|
||||
cfg.Policy = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to general export policy if not set or not auto-export
|
||||
if cfg.Policy == DefaultErrorPolicy {
|
||||
if val, err := store.GetConfig(ctx, ConfigKeyErrorPolicy); err == nil && val != "" {
|
||||
policy := ErrorPolicy(val)
|
||||
if policy.IsValid() {
|
||||
cfg.Policy = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load retry attempts
|
||||
if val, err := store.GetConfig(ctx, ConfigKeyRetryAttempts); err == nil && val != "" {
|
||||
if attempts, err := strconv.Atoi(val); err == nil && attempts >= 0 {
|
||||
cfg.RetryAttempts = attempts
|
||||
}
|
||||
}
|
||||
|
||||
// Load retry backoff
|
||||
if val, err := store.GetConfig(ctx, ConfigKeyRetryBackoffMS); err == nil && val != "" {
|
||||
if backoff, err := strconv.Atoi(val); err == nil && backoff > 0 {
|
||||
cfg.RetryBackoffMS = backoff
|
||||
}
|
||||
}
|
||||
|
||||
// Load skip encoding errors flag
|
||||
if val, err := store.GetConfig(ctx, ConfigKeySkipEncodingErrors); err == nil && val != "" {
|
||||
if skip, err := strconv.ParseBool(val); err == nil {
|
||||
cfg.SkipEncodingErrors = skip
|
||||
}
|
||||
}
|
||||
|
||||
// Load write manifest flag
|
||||
if val, err := store.GetConfig(ctx, ConfigKeyWriteManifest); err == nil && val != "" {
|
||||
if write, err := strconv.ParseBool(val); err == nil {
|
||||
cfg.WriteManifest = write
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SetPolicy sets the error policy for exports
|
||||
func SetPolicy(ctx context.Context, store storage.Storage, policy ErrorPolicy, autoExport bool) error {
|
||||
if !policy.IsValid() {
|
||||
return fmt.Errorf("invalid error policy: %s (valid: strict, best-effort, partial, required-core)", policy)
|
||||
}
|
||||
|
||||
key := ConfigKeyErrorPolicy
|
||||
if autoExport {
|
||||
key = ConfigKeyAutoExportPolicy
|
||||
}
|
||||
|
||||
return store.SetConfig(ctx, key, string(policy))
|
||||
}
|
||||
|
||||
// SetRetryAttempts sets the number of retry attempts
|
||||
func SetRetryAttempts(ctx context.Context, store storage.Storage, attempts int) error {
|
||||
if attempts < 0 {
|
||||
return fmt.Errorf("retry attempts must be non-negative")
|
||||
}
|
||||
return store.SetConfig(ctx, ConfigKeyRetryAttempts, strconv.Itoa(attempts))
|
||||
}
|
||||
|
||||
// SetRetryBackoff sets the initial retry backoff in milliseconds
|
||||
func SetRetryBackoff(ctx context.Context, store storage.Storage, backoffMS int) error {
|
||||
if backoffMS <= 0 {
|
||||
return fmt.Errorf("retry backoff must be positive")
|
||||
}
|
||||
return store.SetConfig(ctx, ConfigKeyRetryBackoffMS, strconv.Itoa(backoffMS))
|
||||
}
|
||||
|
||||
// SetSkipEncodingErrors sets whether to skip issues with encoding errors
|
||||
func SetSkipEncodingErrors(ctx context.Context, store storage.Storage, skip bool) error {
|
||||
return store.SetConfig(ctx, ConfigKeySkipEncodingErrors, strconv.FormatBool(skip))
|
||||
}
|
||||
|
||||
// SetWriteManifest sets whether to write export manifests
|
||||
func SetWriteManifest(ctx context.Context, store storage.Storage, write bool) error {
|
||||
return store.SetConfig(ctx, ConfigKeyWriteManifest, strconv.FormatBool(write))
|
||||
}
|
||||
Reference in New Issue
Block a user