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:
co-authored by
Claude Opus 4.7
parent
b03350ffc1
commit
8a70ba7440
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user