From 7d9de7c090a630d4988b03f0de196990e129b64f Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 9 Aug 2026 20:22:46 -0500 Subject: [PATCH] admin: resolve handles after limiting top users The dashboard's top-users panel resolved a handle for every user with a quota record, then sorted and truncated to ten. On a hold with ~500 crew that is ~500 serial identity lookups to render ten rows. Each lookup goes through the shared identity directory, whose HTTP client allows 10s per request. One stalled lookup consumed the entire reverse proxy budget, so the panel returned a partial body and the client hung up mid-render: admin/auth.go:161 "Failed to render template" template=partials/top_users.html error="write: broken pipe" "GET /admin/api/top-users?limit=10" - 200 4096B in 10.005s Sort and truncate first, then resolve only the surviving rows, so the count is bounded by the limit rather than by hold size. Resolve those concurrently under a 3s deadline: a slow lookup now degrades to a bare DID instead of taking the whole request down with it. The crew tab has the same underlying problem in a different shape, one lazy-load request per row, and is not addressed here. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/hold/admin/handlers.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pkg/hold/admin/handlers.go b/pkg/hold/admin/handlers.go index dc4eb42..2132fcd 100644 --- a/pkg/hold/admin/handlers.go +++ b/pkg/hold/admin/handlers.go @@ -6,11 +6,19 @@ import ( "net/http" "sort" "strconv" + "sync" + "time" "atcr.io/pkg/atproto" "github.com/go-chi/render" ) +// topUsersResolveTimeout bounds handle resolution on the dashboard's top-users +// panel. The shared identity directory allows each lookup 10s, which is the +// whole reverse proxy budget, so the panel caps its own resolution well short +// of that and renders bare DIDs for anything slower. +const topUsersResolveTimeout = 3 * time.Second + // DashboardStats contains dashboard statistics type DashboardStats struct { TotalCrewMembers int @@ -155,7 +163,6 @@ func (ui *AdminUI) handleTopUsersAPI(w http.ResponseWriter, r *http.Request) { for did, q := range allQuotas { users = append(users, UserUsage{ DID: did, - Handle: resolveHandle(ctx, did), Usage: q.TotalSize, UsageHuman: formatHumanBytes(q.TotalSize), BlobCount: q.UniqueBlobs, @@ -172,6 +179,25 @@ func (ui *AdminUI) handleTopUsersAPI(w http.ResponseWriter, r *http.Request) { users = users[:limit] } + // Resolve handles only for the rows that survived the limit. Doing this + // inside the loop above cost one network lookup per user on the hold to + // display ten of them, which blew past the identity directory's 10s HTTP + // timeout and left the client hanging up mid-render. Resolve concurrently + // under a deadline well inside the reverse proxy's budget so a stalled + // lookup degrades to a bare DID instead of a dead request. + resolveCtx, cancel := context.WithTimeout(ctx, topUsersResolveTimeout) + defer cancel() + + var wg sync.WaitGroup + for i := range users { + wg.Add(1) + go func(u *UserUsage) { + defer wg.Done() + u.Handle = resolveHandle(resolveCtx, u.DID) + }(&users[i]) + } + wg.Wait() + // If HTMX request, return HTML partial if r.Header.Get("HX-Request") == "true" { data := struct {