fix UI 503s under checkpoint load

- Store: skip identical CT checkpoint writes (CertSpotter calls StoreLogState
  per batch across 64 logs; hashing the state avoids redundant writes).
- webui: cache /api/stats for 10s and raise API query timeouts; UI polls, so
  repeated aggregate queries no longer queue behind checkpoint bursts.
- reword discovery detail (no 'name enumeration' for S3/GCS; only Azure uses a
  container list).
This commit is contained in:
StasonJatham committed 2026-09-22 17:45:18 +02:00
1 parent 32035d0b9d
commit 1ce5700cec
6 files changed
+50 -14

No files matched your search

+30 -9
View File
@@ -12,6 +12,7 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -73,14 +74,16 @@ func New(s Store, cfg Config) http.Handler {
Limit: limit,
Offset: offset,
}
assets, err := s.ListAssets(r.Context(), f)
ctx, cancel := context.WithTimeout(r.Context(), 9*time.Second)
defer cancel()
assets, err := s.ListAssets(ctx, f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
total, err := s.CountAssets(r.Context(), f)
total, err := s.CountAssets(ctx, f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
writeJSON(w, map[string]any{"assets": assets, "total": total, "limit": limit, "offset": offset})
@@ -89,9 +92,11 @@ func New(s Store, cfg Config) http.Handler {
q := r.URL.Query()
limit, _ := strconv.Atoi(q.Get("limit"))
offset, _ := strconv.Atoi(q.Get("offset"))
entries, total, err := s.SearchEntries(r.Context(), strings.TrimSpace(q.Get("q")), limit, offset)
ctx, cancel := context.WithTimeout(r.Context(), 9*time.Second)
defer cancel()
entries, total, err := s.SearchEntries(ctx, strings.TrimSpace(q.Get("q")), limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
writeJSON(w, map[string]any{"entries": entries, "total": total, "limit": limit, "offset": offset})
@@ -143,12 +148,28 @@ func New(s Store, cfg Config) http.Handler {
writeJSON(w, map[string]any{"started": true, "exposure": exposure, "limit": limit})
})
}
var (
statsMu sync.Mutex
statsVal store.Stats
statsExp time.Time
)
mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
st, err := s.Stats(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// Cached for a few seconds: the UI polls, and each computation is several
// aggregate queries that can queue behind CertSpotter checkpoint bursts.
statsMu.Lock()
defer statsMu.Unlock()
if time.Now().Before(statsExp) {
writeJSON(w, statsVal)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 9*time.Second)
defer cancel()
st, err := s.Stats(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
statsVal, statsExp = st, time.Now().Add(10*time.Second)
writeJSON(w, st)
})