todo: extract storage interface with in-memory implementation#1960
todo: extract storage interface with in-memory implementation#1960dgageot merged 2 commits intodocker:mainfrom
Conversation
Introduce TodoStorage interface to decouple the todo toolset from its storage layer. Add MemoryTodoStorage as the default in-memory implementation and a WithStorage functional option so callers can provide their own. Assisted-By: cagent
There was a problem hiding this comment.
Review Summary
Assessment: 🟡 NEEDS ATTENTION
This PR introduces a clean abstraction with the TodoStorage interface, successfully decoupling the todo toolset from its storage implementation. However, there are 3 concurrency and safety issues in the newly added code that should be addressed:
- Nil storage validation —
WithStoragecan accept nil, causing panics - ID generation race conditions — Both
createTodoandcreateTodoshave non-atomic read-modify-write sequences that can produce duplicate IDs in concurrent scenarios
These issues are all in the changed code and should be fixed before merging.
Findings: 3 issues
| type TodoOption func(*TodoTool) | ||
|
|
||
| // WithStorage sets a custom storage implementation for the TodoTool. | ||
| func WithStorage(storage TodoStorage) TodoOption { |
There was a problem hiding this comment.
MEDIUM: WithStorage option can set nil storage causing nil pointer dereference
The WithStorage function accepts a TodoStorage parameter and assigns it directly to t.handler.storage without nil validation. If called as WithStorage(nil), subsequent method calls like h.storage.Len() or h.storage.Add() will panic with nil pointer dereferences.
Recommendation: Add validation:
func WithStorage(storage TodoStorage) TodoOption {
return func(t *TodoTool) {
if storage == nil {
panic("storage cannot be nil")
}
t.handler.storage = storage
}
}Or document that nil is invalid and will cause panics.
Address PR review feedback: - WithStorage now panics early if given nil storage - Replace length-based ID generation with atomic counter to prevent duplicate IDs under concurrent access Assisted-By: cagent
|
/review |
There was a problem hiding this comment.
Review Summary
Assessment: 🟢 APPROVE
This PR successfully introduces a clean storage abstraction for the todo toolset. The implementation is well-designed:
✅ Interface Design: TodoStorage provides a clear contract with 6 well-defined methods
✅ Thread Safety: MemoryTodoStorage properly uses concurrent.Slice for concurrency-safe operations
✅ Backward Compatibility: Default storage behavior preserved via functional options pattern
✅ ID Generation: Atomic counter ensures globally unique IDs across concurrent operations
✅ Test Coverage: Comprehensive tests including storage injection and nil-safety
The code was reviewed for potential race conditions and concurrency issues. All operations are properly protected by the underlying concurrent data structures. The atomic ID counter ensures unique IDs even under concurrent load, and the storage abstraction allows for future implementations (e.g., persistent storage) without changing the tool interface.
No blocking issues found. The changed code works correctly as designed.
Follow the same pattern used for todos in PR docker#1960: - Define a TaskStorage interface with Load/Save methods - Extract file I/O into FileTaskStorage implementation - Add TaskOption functional options with WithTaskStorage - Move mutex from TasksTool into FileTaskStorage - Update NewTasksTool to accept variadic options - Add tests for WithTaskStorage and nil panic No in-memory storage implementation is needed for tasks. Assisted-By: cagent
Follow the same pattern used for todos in PR docker#1960: - Define a TaskStorage interface with domain-level methods (All, Get, Put, Delete) that map to what the toolset does - Extract file I/O into FileTaskStorage implementation - Add TaskOption functional options with WithTaskStorage - Move mutex from TasksTool into FileTaskStorage - Refactor all handlers to use the interface directly - Update NewTasksTool to accept variadic options - Add tests for WithTaskStorage and nil panic No in-memory storage implementation is needed for tasks. Assisted-By: cagent
Follow the same pattern used for todos in PR docker#1960, with a domain-level interface whose methods map to what the toolset does: type TaskStorage interface { Create(task Task) error Get(id string) (TaskWithStatus, error) List() ([]TaskWithStatus, error) Update(task Task) error Delete(id string) error } The storage owns all data-integrity concerns: - Create/Update validate that dependencies exist and are acyclic - Delete cascades removal from other tasks' dependency lists - Get/List compute effective status (blocked when a dep is not done) This keeps the tool handlers thin: they deal with argument parsing, description resolution, priority/status validation, and JSON formatting — then delegate to the storage for persistence and graph logic. Other changes: - Add FileTaskStorage (file-backed implementation) - Add TaskOption / WithTaskStorage functional option - Export TaskWithStatus (was taskWithEffective) - Add sentinel errors: ErrTaskNotFound, ErrDependencyNotFound, ErrDependencyCycle, ErrDuplicateDependency - Add FileTaskStorage unit tests for dep validation, cycle detection, cascade delete, and effective status - Update NewTasksTool to accept variadic options No in-memory storage implementation is needed for tasks. Assisted-By: cagent
Follow the same pattern used for todos in PR docker#1960, with a domain-level interface whose methods map to what the toolset does: type TaskStorage interface { Create(task Task) error Get(id string) (TaskWithStatus, error) List() ([]TaskWithStatus, error) Update(task Task) error Delete(id string) error } The storage owns all data-integrity concerns: - Create/Update validate that dependencies exist and are acyclic - Delete cascades removal from other tasks' dependency lists - Get/List compute effective status (blocked when a dep is not done) This keeps the tool handlers thin: they deal with argument parsing, description resolution, priority/status validation, and JSON formatting — then delegate to the storage for persistence and graph logic. Other changes: - Add FileTaskStorage (file-backed implementation) - Add TaskOption / WithTaskStorage functional option (nil is silently ignored, keeping the default storage) - Export TaskWithStatus (was taskWithEffective) - Add sentinel errors: ErrTaskNotFound, ErrDependencyNotFound, ErrDependencyCycle, ErrDuplicateDependency - Add FileTaskStorage unit tests for dep validation, cycle detection, cascade delete, and effective status - Update NewTasksTool to accept variadic options No in-memory storage implementation is needed for tasks. Assisted-By: cagent
Introduce a
TodoStorageinterface to decouple the todo toolset from its storage layer.Changes
TodoStorageinterface — defines the storage contract with 6 methods:Add,All,Len,FindByID,Update, andClear.MemoryTodoStorage— in-memory, concurrency-safe implementation backed byconcurrent.Slice[Todo].WithStoragefunctional option — lets callers inject their own storage implementation intoNewTodoTool.NewTodoToolnow accepts variadicTodoOptions. Defaults toMemoryTodoStoragewhen no option is provided, so all existing call sites remain unchanged.TestTodoTool_WithStoragetest.Assisted-By: cagent