Files
OwnCord/Server/ws/event_pruner.go
T
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).

SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:

  - service.Store   (service/datastore.go, renamed from store/store.go)
  - ws.EventStore   (ws/eventstore.go)
  - plugin.PluginStore (plugin/pluginstore.go)

The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.

Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.

Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:33:58 +00:00

75 lines
2.1 KiB
Go

// Phase B Step 7 — Event Persistence Layer (pruner).
//
// StartEventPruner runs a background goroutine that deletes events older than
// the configured retention window. It is the bounded-storage half of the
// event persistence design: the persister appends, the pruner trims.
package ws
import (
"context"
"log/slog"
"time"
)
// maxStartupDelay caps how long StartEventPruner waits before its first
// prune pass. We want a short delay so a freshly started server with a
// tiny dataset doesn't keep stale rows around for a full interval, but we
// don't want the delay to exceed the interval itself (otherwise a server
// running with interval=5s would wait longer than its own tick).
const maxStartupDelay = time.Minute
// StartEventPruner launches a goroutine that wakes every interval and deletes
// events older than retention. The goroutine exits when ctx is cancelled.
func StartEventPruner(ctx context.Context, s EventStore, retention, interval time.Duration) {
if s == nil {
return
}
if retention <= 0 {
retention = 24 * time.Hour
}
if interval <= 0 {
interval = time.Hour
}
// Bound the startup delay by the interval so short test intervals
// (e.g. 100ms in event_pruner_test.go) don't wait a full minute.
startupDelayDuration := maxStartupDelay
if interval < startupDelayDuration {
startupDelayDuration = interval
}
go func() {
// Run once shortly after startup so a tiny dataset stays small.
startupDelay := time.NewTimer(startupDelayDuration)
defer startupDelay.Stop()
select {
case <-ctx.Done():
return
case <-startupDelay.C:
}
runPrune(ctx, s, retention)
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
runPrune(ctx, s, retention)
}
}
}()
}
func runPrune(ctx context.Context, s EventStore, retention time.Duration) {
cutoff := time.Now().Add(-retention)
deleted, err := s.PruneEventsOlderThan(ctx, cutoff)
if err != nil {
slog.Warn("event pruner: PruneEventsOlderThan failed", "err", err)
return
}
if deleted > 0 {
slog.Info("event pruner: pruned old events", "deleted", deleted, "cutoff", cutoff.Format(time.RFC3339))
}
}