feat: add SQLite persistence for sidecar task results#45
Merged
Conversation
Replace the in-memory 10-entry ring buffer with a SQLite-backed
ResultStore so task results survive pod restarts. Uses modernc.org/sqlite
(pure Go, no CGo) — zero changes to the build pipeline, Dockerfile, or
GoReleaser config.
Key decisions:
- DB file at {homeDir}/sidecar.db on the existing PVC
- WAL mode, busy_timeout=5000, synchronous=NORMAL, MaxOpenConns=1
- Schema migrations via PRAGMA user_version at startup
- Marker files (.sei-sidecar-*) left untouched
- Cloud-agnostic: identical deployment on AWS/GCP/bare metal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bdchatham
commented
Mar 30, 2026
| } | ||
|
|
||
| func scanInto(s scanner) (*TaskResult, error) { | ||
| var ( |
Contributor
Author
There was a problem hiding this comment.
Let's create a type and not make this anonymous.
- Add doc comment explaining :memory: DSN on NewMemoryStore - Enforce single file-backed store per process via mutex guard - Move migrations to sqlite_migrations.go - Promote scanner to named RowScanner type in store.go - Reorganize: store.go (types+interface), sqlite_store.go (impl), sqlite_migrations.go (schema) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bdchatham
commented
Mar 30, 2026
bdchatham
commented
Mar 30, 2026
bdchatham
commented
Mar 30, 2026
bdchatham
commented
Mar 30, 2026
Address remaining PR comments: - Store is now the single source of truth for all task lifecycle states. Tasks are saved with status=running on submit and updated on completion. - Remove active and scheduled in-memory maps. Only cancel funcs (non-serializable) and the scheduled-task overlap guard remain in memory. - Remove maxResults cap — RecentResults now returns up to 100 from store. The artificial ring buffer limit no longer applies. - Add ListScheduled(now) to ResultStore for efficient due-task queries. EvalSchedules queries the store directly instead of scanning an in-memory map. - Add partial index on (next_run_at) WHERE schedule IS NOT NULL. - GetResult and RecentResults are now simple store queries with no lock contention, which addresses the lock change question. - Add TestStoreListScheduled covering due/not-due/one-shot filtering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three end-to-end tests using real file-backed SQLite databases in t.TempDir (auto-cleaned, including WAL/SHM files): - TestE2E_TaskLifecycle: Full lifecycle across submit, fail, dedup, mark-ready, scheduled tasks, removal, and simulated process restart (close store, reopen same DB, verify persistence and dedup). - TestE2E_ScheduledTaskExecution: Scheduled task fires via EvalSchedules, verifies completion status and NextRunAt advancement in the store. - TestE2E_ConcurrentSubmit: 10 concurrent task submissions against a file-backed store, verifying no races or data loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
bdchatham
added a commit
that referenced
this pull request
Mar 30, 2026
## Summary Builds on #45 and addresses all findings from the Kubernetes and Platform specialist reviews. ### Must-fix (4 items) - **Goroutine drain on shutdown**: `Engine.Wait()` + `sync.WaitGroup` ensures all in-flight task goroutines complete before `store.Close()`. Wired into `serve.go` between server stop and DB close. - **Stale task recovery**: `RecoverStaleTasks()` marks one-shot tasks left as `running` from a previous crash as `failed` on startup. Scheduled tasks are preserved for re-evaluation. - **Transactional migrations**: v1 migration DDL + `PRAGMA user_version` wrapped in `BEGIN`/`COMMIT` for atomicity. - **UTC timestamps**: All `time.Now()` and format calls normalized to `.UTC()` to prevent incorrect string-based comparisons in SQLite. ### Should-fix (3 items) - **RemoveResult race fix**: Completion goroutine checks if its cancel func is still active before saving. Skips save if `RemoveResult` already cleaned it up. - **Singleton guard removed**: Dropped the `storeMu`/`storeCreated` guard on `NewSQLiteStore`. Rely on `serve.go` wiring. - **PVC constraint documented**: `NewSQLiteStore` doc now warns that WAL mode requires block-device-backed storage (not NFS). ### Test improvements - `TestE2E_StaleTaskRecovery` — verifies crash recovery behavior - `TestE2E_ShutdownDrainsGoroutines` — verifies `Wait()` drains before close - `TestE2E_ConcurrentSubmit` — replaced `time.Sleep` with `sync.WaitGroup` ## Test plan - [x] `go test ./sidecar/...` — all tests pass - [x] `CGO_ENABLED=0 go build .` — binary builds - [x] `gofmt -s` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ResultStoreso task results survive pod restartsmodernc.org/sqlite(pure Go, no CGo) — zero changes to the Dockerfile, GoReleaser, or CI pipelines{homeDir}/sidecar.dbon the existing PVC — no new Kubernetes volumes neededDetails
ResultStoreinterface (Save,Get,List,Delete,Close) insidecar/engine/store.goSQLiteStoreimplementation with WAL mode,busy_timeout=5000,synchronous=NORMAL,MaxOpenConns=1PRAGMA user_versionat startup — no migration framework.sei-sidecar-*) intentionally left untouched — separate failure domain:memory:SQLite (no disk artifacts)Test plan
CGO_ENABLED=0 go build .— confirms pure-Go SQLite works without CGogo test ./sidecar/...— all existing + new tests passgofmt -s— no formatting issuesdocker build— verify distroless image builds cleanly in CIseictl serve, POST a task, kill process, restart, GET task by ID — result persists