Files
beads/internal/storage/storage.go
Steve Yegge 92759710de Fix race condition in dirty issue tracking (bd-52, bd-53)
Fix critical TOCTOU bug where concurrent operations could lose dirty
issue tracking, causing data loss in incremental exports. Also fixes
bug where export with filters would incorrectly clear all dirty issues.

The Problem:
1. GetDirtyIssues() returns [bd-1, bd-2]
2. Concurrent CRUD marks bd-3 dirty
3. Export writes bd-1, bd-2
4. ClearDirtyIssues() deletes ALL (including bd-3)
5. Result: bd-3 never gets exported!

The Fix:
- Add ClearDirtyIssuesByID() that only clears specific issue IDs
- Track which issues were actually exported
- Clear only those specific IDs, not all dirty issues
- Fixes both race condition and filter export bug

Changes:
- internal/storage/sqlite/dirty.go:
  * Add ClearDirtyIssuesByID() method
  * Add warning to ClearDirtyIssues() about race condition
- internal/storage/storage.go:
  * Add ClearDirtyIssuesByID to interface
- cmd/bd/main.go:
  * Update auto-flush to use ClearDirtyIssuesByID()
- cmd/bd/export.go:
  * Track exported issue IDs
  * Use ClearDirtyIssuesByID() instead of ClearDirtyIssues()

Testing:
- Created test-1, test-2, test-3 (all dirty)
- Updated test-2 to in_progress
- Exported with --status open filter (exports only test-1, test-3)
- Verified only test-2 remains dirty ✓
- All existing tests pass ✓

Impact:
- Race condition eliminated - concurrent operations are safe
- Export with filters now works correctly
- No data loss from competing writes

Closes bd-52, bd-53

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 00:29:23 -07:00

74 lines
2.7 KiB
Go

// Package storage defines the interface for issue storage backends.
package storage
import (
"context"
"github.com/steveyegge/beads/internal/types"
)
// Storage defines the interface for issue storage backends
type Storage interface {
// Issues
CreateIssue(ctx context.Context, issue *types.Issue, actor string) error
GetIssue(ctx context.Context, id string) (*types.Issue, error)
UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string) error
SearchIssues(ctx context.Context, query string, filter types.IssueFilter) ([]*types.Issue, error)
// Dependencies
AddDependency(ctx context.Context, dep *types.Dependency, actor string) error
RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error
GetDependencies(ctx context.Context, issueID string) ([]*types.Issue, error)
GetDependents(ctx context.Context, issueID string) ([]*types.Issue, error)
GetDependencyRecords(ctx context.Context, issueID string) ([]*types.Dependency, error)
GetAllDependencyRecords(ctx context.Context) (map[string][]*types.Dependency, error)
GetDependencyTree(ctx context.Context, issueID string, maxDepth int) ([]*types.TreeNode, error)
DetectCycles(ctx context.Context) ([][]*types.Issue, error)
// Labels
AddLabel(ctx context.Context, issueID, label, actor string) error
RemoveLabel(ctx context.Context, issueID, label, actor string) error
GetLabels(ctx context.Context, issueID string) ([]string, error)
GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error)
// Ready Work & Blocking
GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error)
GetBlockedIssues(ctx context.Context) ([]*types.BlockedIssue, error)
// Events
AddComment(ctx context.Context, issueID, actor, comment string) error
GetEvents(ctx context.Context, issueID string, limit int) ([]*types.Event, error)
// Statistics
GetStatistics(ctx context.Context) (*types.Statistics, error)
// Dirty tracking (for incremental JSONL export)
GetDirtyIssues(ctx context.Context) ([]string, error)
ClearDirtyIssues(ctx context.Context) error // WARNING: Race condition (bd-52), use ClearDirtyIssuesByID
ClearDirtyIssuesByID(ctx context.Context, issueIDs []string) error
// Config
SetConfig(ctx context.Context, key, value string) error
GetConfig(ctx context.Context, key string) (string, error)
// Lifecycle
Close() error
}
// Config holds database configuration
type Config struct {
Backend string // "sqlite" or "postgres"
// SQLite config
Path string // database file path
// PostgreSQL config
Host string
Port int
Database string
User string
Password string
SSLMode string
}