diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-01-23 15:55:48 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-01-23 15:55:48 -1000 |
| commit | 54f091e1b920943967c6aebc9c1f3122ce52e267 (patch) | |
| tree | ea60ba6e86aa9247e73a30094a1be6f39bff0b56 /internal | |
| parent | bc4149d7c9fe7a698cf07895b504ab8f2b26f649 (diff) | |
Fix critical resilience issues from code review
- DB connection pool: Allow 5 connections instead of 1 for better concurrency
- JSON unmarshal: Add error handling to prevent nil slice issues
- Context cancellation: Check ctx.Done() in aggregateData goroutines
- Migration path: Make configurable via MIGRATION_DIR env var
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 2 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 25 | ||||
| -rw-r--r-- | internal/handlers/handlers_test.go | 2 | ||||
| -rw-r--r-- | internal/handlers/heuristic_test.go | 2 | ||||
| -rw-r--r-- | internal/handlers/tab_state_test.go | 2 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 24 |
6 files changed, 46 insertions, 11 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index ba2719d..62e733a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type Config struct { // Paths DatabasePath string + MigrationDir string TemplateDir string StaticDir string @@ -44,6 +45,7 @@ func Load() (*Config, error) { // Paths DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"), + MigrationDir: getEnvWithDefault("MIGRATION_DIR", "migrations"), TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"), StaticDir: getEnvWithDefault("STATIC_DIR", "web/static"), diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index c44e771..b8fc574 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -207,6 +207,11 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Add(1) go func() { defer wg.Done() + select { + case <-ctx.Done(): + return + default: + } boards, err := h.fetchBoards(ctx, forceRefresh) mu.Lock() defer mu.Unlock() @@ -221,6 +226,11 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Add(1) go func() { defer wg.Done() + select { + case <-ctx.Done(): + return + default: + } tasks, err := h.fetchTasks(ctx, forceRefresh) mu.Lock() defer mu.Unlock() @@ -255,6 +265,11 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Add(1) go func() { defer wg.Done() + select { + case <-ctx.Done(): + return + default: + } projects, err := h.todoistClient.GetProjects(ctx) mu.Lock() defer mu.Unlock() @@ -270,6 +285,11 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Add(1) go func() { defer wg.Done() + select { + case <-ctx.Done(): + return + default: + } meals, err := h.fetchMeals(ctx, forceRefresh) mu.Lock() defer mu.Unlock() @@ -286,6 +306,11 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Add(1) go func() { defer wg.Done() + select { + case <-ctx.Done(): + return + default: + } events, err := h.googleCalendarClient.GetUpcomingEvents(ctx, 10) mu.Lock() defer mu.Unlock() diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index e4a9f05..5628237 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -39,7 +39,7 @@ func setupTestDB(t *testing.T) (*store.Store, func()) { } // Initialize store (this runs migrations) - db, err := store.New(tmpFile.Name()) + db, err := store.New(tmpFile.Name(), "migrations") if err != nil { os.Chdir(originalDir) os.Remove(tmpFile.Name()) diff --git a/internal/handlers/heuristic_test.go b/internal/handlers/heuristic_test.go index 2b70218..b03b664 100644 --- a/internal/handlers/heuristic_test.go +++ b/internal/handlers/heuristic_test.go @@ -33,7 +33,7 @@ func TestHandleTasks_Heuristic(t *testing.T) { defer os.Chdir(originalDir) // Initialize store (this runs migrations) - db, err := store.New(tmpFile.Name()) + db, err := store.New(tmpFile.Name(), "migrations") if err != nil { t.Fatalf("Failed to initialize store: %v", err) } diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go index d7bb8dd..60f1340 100644 --- a/internal/handlers/tab_state_test.go +++ b/internal/handlers/tab_state_test.go @@ -13,7 +13,7 @@ import ( func TestHandleDashboard_TabState(t *testing.T) { // Create a temporary database for testing - db, err := store.New(":memory:") + db, err := store.New(":memory:", "../../migrations") if err != nil { t.Fatalf("Failed to create test database: %v", err) } diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index 069313a..a4d01a2 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -22,11 +22,12 @@ const ( ) type Store struct { - db *sql.DB + db *sql.DB + migrationDir string } // New creates a new Store instance and runs migrations -func New(dbPath string) (*Store, error) { +func New(dbPath, migrationDir string) (*Store, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) @@ -42,10 +43,13 @@ func New(dbPath string) (*Store, error) { return nil, fmt.Errorf("failed to enable WAL mode: %w", err) } - // Serialize writes to prevent "database is locked" errors - db.SetMaxOpenConns(1) + // Configure connection pool for SQLite with WAL mode + // WAL allows concurrent reads, but writes still need serialization + db.SetMaxOpenConns(5) + db.SetMaxIdleConns(2) + db.SetConnMaxLifetime(time.Hour) - store := &Store{db: db} + store := &Store{db: db, migrationDir: migrationDir} // Run migrations if err := store.runMigrations(); err != nil { @@ -67,8 +71,9 @@ func (s *Store) DB() *sql.DB { // runMigrations executes all migration files in order func (s *Store) runMigrations() error { - // Get migration files - migrationFiles, err := filepath.Glob("migrations/*.sql") + // Get migration files from configured directory + pattern := filepath.Join(s.migrationDir, "*.sql") + migrationFiles, err := filepath.Glob(pattern) if err != nil { return fmt.Errorf("failed to read migration files: %w", err) } @@ -178,7 +183,10 @@ func (s *Store) GetTasks() ([]models.Task, error) { task.DueDate = &dueDate.Time } - json.Unmarshal([]byte(labelsJSON), &task.Labels) + if err := json.Unmarshal([]byte(labelsJSON), &task.Labels); err != nil { + log.Printf("Warning: failed to unmarshal labels for task %s: %v", task.ID, err) + task.Labels = []string{} + } tasks = append(tasks, task) } |
