fix(api): route analytics handlers through read pool

The four /api/analytics/* endpoints (timeline, summary, top-agents,
top-channels) all ran their SELECT queries on the write pool
(MaxOpenConns=1, serialized) and would time out at 125s with
context-canceled whenever a long writer (e.g. dream dispatch) held the
single connection. Summary swallows the error and returns {0,0,0}, so
the dashboard rendered an empty-cluster lie.

Plumb ReadDB through RouterConfig from main, fall back to DB if the
read pool is unset, and pass it to NewAnalyticsHandler. Same shape as
the /readyz fix in b03350f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Algis Dumbris
2026-05-18 15:01:07 +03:00
co-authored by Claude Opus 4.7
parent b03350ffc1
commit 8a70ba7440
3 changed files with 16 additions and 4 deletions
+1
View File
@@ -865,6 +865,7 @@ func runServe(cmd *cobra.Command, args []string) error {
Broadcaster: sseBroadcaster,
SessionMiddleware: sessionMiddleware,
DB: db.DB,
ReadDB: db.QueryDB(),
Version: version,
PushService: pushService,
TrustService: trustService,
+2
View File
@@ -21,6 +21,8 @@ type AnalyticsHandler struct {
}
// NewAnalyticsHandler creates a new analytics handler.
// db should be the read pool — every query in this handler is read-only and
// must not block on the serialized write pool.
func NewAnalyticsHandler(db *sql.DB, agentService *agents.AgentService, channelService *channels.Service) *AnalyticsHandler {
return &AnalyticsHandler{
db: db,
+13 -4
View File
@@ -52,8 +52,12 @@ type RouterConfig struct {
Broadcaster *SSEBroadcaster
SessionMiddleware func(http.Handler) http.Handler
DB *sql.DB
Version string
BaseURL string
// ReadDB is the read-only connection pool (query_only=ON, MaxOpenConns>1).
// Read-only API handlers must use this to avoid blocking on the serialized
// write pool. If nil, handlers fall back to DB.
ReadDB *sql.DB
Version string
BaseURL string
// CoreMemoryStore (feature 020 — US2) wires the per-(owner, agent)
// core memory REST endpoints. Nil → routes not registered.
@@ -324,9 +328,14 @@ func NewRouterWithConfig(cfg RouterConfig) chi.Router {
})
}
// Analytics (authenticated, requires DB)
// Analytics (authenticated, requires DB). Reads must use the read pool so
// dashboard queries don't serialize behind long-running writers.
if cfg.DB != nil {
analyticsHandler := NewAnalyticsHandler(cfg.DB, cfg.AgentService, cfg.ChannelService)
analyticsDB := cfg.ReadDB
if analyticsDB == nil {
analyticsDB = cfg.DB
}
analyticsHandler := NewAnalyticsHandler(analyticsDB, cfg.AgentService, cfg.ChannelService)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)