Add in-memory result cache with 1-hour TTL
DB is now queried at most once per hour per user; subsequent requests within the window are served from memory. TTL matches the existing Cache-Control header. Restart clears the cache immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -83,12 +84,43 @@ func getEnv(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// ---- cache ----------------------------------------------------------------
|
||||
|
||||
const cacheTTL = time.Hour
|
||||
|
||||
type cacheEntry struct {
|
||||
data []dayCount
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type resultCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]cacheEntry
|
||||
}
|
||||
|
||||
func (c *resultCache) get(username string) ([]dayCount, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
e, ok := c.entries[username]
|
||||
if !ok || time.Now().After(e.expires) {
|
||||
return nil, false
|
||||
}
|
||||
return e.data, true
|
||||
}
|
||||
|
||||
func (c *resultCache) set(username string, data []dayCount) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries[username] = cacheEntry{data: data, expires: time.Now().Add(cacheTTL)}
|
||||
}
|
||||
|
||||
// ---- handler --------------------------------------------------------------
|
||||
|
||||
type heatmapHandler struct {
|
||||
db *sql.DB
|
||||
cfg *config
|
||||
log *slog.Logger
|
||||
db *sql.DB
|
||||
cfg *config
|
||||
log *slog.Logger
|
||||
cache *resultCache
|
||||
}
|
||||
|
||||
type dayCount struct {
|
||||
@@ -118,17 +150,22 @@ func (h *heatmapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
counts, ok := h.cache.get(username)
|
||||
if !ok {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
counts, err := h.queryHeatmap(ctx, username)
|
||||
if err != nil {
|
||||
h.log.Error("heatmap query failed", "user", username, "err", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if counts == nil {
|
||||
counts = []dayCount{}
|
||||
var err error
|
||||
counts, err = h.queryHeatmap(ctx, username)
|
||||
if err != nil {
|
||||
h.log.Error("heatmap query failed", "user", username, "err", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if counts == nil {
|
||||
counts = []dayCount{}
|
||||
}
|
||||
h.cache.set(username, counts)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -216,7 +253,12 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
h := &heatmapHandler{db: db, cfg: cfg, log: logger}
|
||||
h := &heatmapHandler{
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
log: logger,
|
||||
cache: &resultCache{entries: make(map[string]cacheEntry)},
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/heatmap/", h)
|
||||
|
||||
Reference in New Issue
Block a user