Detailed schema design, API changes, test matrix, and migration helper following go-store's SQLite pattern (modernc.org/sqlite, WAL mode). Co-Authored-By: Virgil <virgil@lethean.io>
6.3 KiB
6.3 KiB
TODO.md -- go-ratelimit
Dispatched from core/go orchestration. Pick up tasks in order.
Phase 0: Hardening & Test Coverage
- Expand test coverage --
ratelimit_test.gorewritten with testify. Tests for:CanSend()at exact limits (RPM, TPM, RPD boundaries),RecordUsage()with concurrent goroutines,WaitForCapacity()timeout and immediate-capacity paths,prune()sliding window edge cases, daily reset logic (24h boundary), YAML persistence (save + reload), corrupt/unreadable state file recovery,Reset()single/all/nonexistent,Stats()known/unknown/quota-only models,AllStats()with pruning and daily reset. - Race condition test --
go test -race ./...with 20 goroutines callingCanSend()+RecordUsage()+Stats()concurrently. Additional tests: concurrentReset()+RecordUsage()+AllStats(), concurrent multi-model access (5 models), concurrentPersist()+Load()filesystem race, concurrentAllStats()+RecordUsage(), concurrentWaitForCapacity()+RecordUsage(). All pass clean. - Benchmark -- 7 benchmarks:
BenchmarkCanSend(1000-entry window),BenchmarkRecordUsage,BenchmarkCanSendConcurrent(parallel),BenchmarkCanSendWithPrune(500 old + 500 new),BenchmarkStats(1000 entries),BenchmarkAllStats(5 models x 200 entries),BenchmarkPersist(YAML I/O). Zero allocs on hot paths. go vet ./...clean -- No warnings.- Coverage: 95.1% (up from 77.1%). Remaining uncovered:
CountTokenssuccess path (hardcoded Google URL),yaml.Marshalerror path inPersist(),os.UserHomeDirerror path inNewWithConfig.
Phase 1: Generalise Beyond Gemini
- Provider-agnostic config -- Added
Providertype,ProviderProfile,Configstruct,NewWithConfig()constructor. Quotas are no longer hardcoded inNew(). - Quota profiles --
DefaultProfiles()returns pre-configured profiles for Gemini, OpenAI (gpt-4o, o1, o3-mini), Anthropic (claude-opus-4, claude-sonnet-4, claude-haiku-3.5), and Local (empty, user-configurable). - Configurable defaults --
Configstruct acceptsFilePath,Providerslist, and explicitQuotasmap. Explicit quotas override provider defaults. YAML-serialisable. - Backward compatibility --
New()delegates toNewWithConfig(Config{Providers: []Provider{ProviderGemini}}). Existing API unchanged. TestTestNewBackwardCompatibilityverifies exact parity. - Runtime configuration --
SetQuota()andAddProvider()allow modifying quotas after construction. Both are mutex-protected.
Phase 2: SQLite Persistent State
Current YAML persistence is single-process only. Phase 2 adds multi-process safe SQLite storage following the go-store pattern (modernc.org/sqlite, pure Go, no CGO).
2.1 SQLite Backend
- Add
modernc.org/sqlitedependency —go get modernc.org/sqlite. Pure Go, compiles everywhere. - Create
sqlite.go— Internal SQLite persistence layer:type sqliteStore struct { db *sql.DB }— wraps database/sql connectionfunc newSQLiteStore(dbPath string) (*sqliteStore, error)— Open DB, setPRAGMA journal_mode=WAL,PRAGMA busy_timeout=5000,db.SetMaxOpenConns(1). Create schema:CREATE TABLE IF NOT EXISTS quotas ( model TEXT PRIMARY KEY, max_rpm INTEGER NOT NULL DEFAULT 0, max_tpm INTEGER NOT NULL DEFAULT 0, max_rpd INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS requests ( model TEXT NOT NULL, ts INTEGER NOT NULL -- UnixNano ); CREATE TABLE IF NOT EXISTS tokens ( model TEXT NOT NULL, ts INTEGER NOT NULL, -- UnixNano count INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS daily ( model TEXT PRIMARY KEY, day_start INTEGER NOT NULL, day_count INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_requests_model_ts ON requests(model, ts); CREATE INDEX IF NOT EXISTS idx_tokens_model_ts ON tokens(model, ts);func (s *sqliteStore) saveQuotas(quotas map[string]ModelQuota) error— UPSERT all quotasfunc (s *sqliteStore) loadQuotas() (map[string]ModelQuota, error)— SELECT all quotasfunc (s *sqliteStore) saveState(state map[string]*UsageStats) error— Transaction: DELETE old + INSERT requests/tokens/daily for each modelfunc (s *sqliteStore) loadState() (map[string]*UsageStats, error)— SELECT and reconstruct UsageStats mapfunc (s *sqliteStore) close() error— Close DB connection
2.2 Wire Into RateLimiter
- Add
Backendfield to Config —Backend stringwith values"yaml"(default),"sqlite". Default""maps to"yaml"for backward compat. - Update
Persist()andLoad()— Check internal backend type. If SQLite, usesqliteStore; otherwise use existing YAML. Keep both paths working. - Add
NewWithSQLite(dbPath string) (*RateLimiter, error)— Convenience constructor that creates a SQLite-backed limiter. Sets backend type, initialises DB. - Graceful close — Add
Close() errormethod that closes SQLite DB if open. No-op for YAML backend.
2.3 Tests
- SQLite basic tests — newSQLiteStore, saveQuotas/loadQuotas round-trip, saveState/loadState round-trip, close.
- SQLite integration — NewWithSQLite, RecordUsage → Persist → Load → verify state preserved. Same test matrix as existing YAML tests but with SQLite backend.
- Concurrent SQLite — 10 goroutines × 100 ops (RecordUsage + CanSend + Persist + Load). Race-clean.
- YAML backward compat — Existing tests must pass unchanged (still default to YAML).
- Migration helper —
MigrateYAMLToSQLite(yamlPath, sqlitePath string) error— reads YAML state, writes to SQLite. Test with sample YAML. - Corrupt DB recovery — Truncated DB file → graceful error, fresh start.
Phase 3: Integration
- Wire into go-ml backends for automatic rate limiting on inference calls
- Wire into go-ai facade so all providers share a unified rate limit layer
- Add metrics export (requests/minute, tokens/minute, rejections) for monitoring
Workflow
- Virgil in core/go writes tasks here after research
- This repo's dedicated session picks up tasks in phase order
- Mark
[x]when done, note commit hash