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:
@@ -64,10 +64,10 @@ Set `OP_TYPES=5,6,7,9,11,18,24` for a more inclusive count.
|
|||||||
- **Template drift.** The harder part. Gitea's `profile.tmpl` does change
|
- **Template drift.** The harder part. Gitea's `profile.tmpl` does change
|
||||||
between versions — diff the new upstream against your override on each
|
between versions — diff the new upstream against your override on each
|
||||||
upgrade and re-merge the snippet.
|
upgrade and re-merge the snippet.
|
||||||
- **Cache.** 1-hour `Cache-Control` header keeps load trivial. Drop to 5
|
- **Cache.** Results are cached in memory for 1 hour (matching the
|
||||||
minutes for snappier updates if needed; the underlying query is cheap
|
`Cache-Control: max-age=3600` response header), so the DB is only hit
|
||||||
thanks to the existing index on `(user_id, act_user_id, created_unix)`
|
once per hour per user regardless of traffic. Restart the service to
|
||||||
added in Gitea 1.24.
|
clear the cache immediately.
|
||||||
|
|
||||||
## Local dev
|
## Local dev
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -83,12 +84,43 @@ func getEnv(key, fallback string) string {
|
|||||||
return fallback
|
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 --------------------------------------------------------------
|
// ---- handler --------------------------------------------------------------
|
||||||
|
|
||||||
type heatmapHandler struct {
|
type heatmapHandler struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
cfg *config
|
cfg *config
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
|
cache *resultCache
|
||||||
}
|
}
|
||||||
|
|
||||||
type dayCount struct {
|
type dayCount struct {
|
||||||
@@ -118,17 +150,22 @@ func (h *heatmapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
counts, ok := h.cache.get(username)
|
||||||
defer cancel()
|
if !ok {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
counts, err := h.queryHeatmap(ctx, username)
|
var err error
|
||||||
if err != nil {
|
counts, err = h.queryHeatmap(ctx, username)
|
||||||
h.log.Error("heatmap query failed", "user", username, "err", err)
|
if err != nil {
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
h.log.Error("heatmap query failed", "user", username, "err", err)
|
||||||
return
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
}
|
return
|
||||||
if counts == nil {
|
}
|
||||||
counts = []dayCount{}
|
if counts == nil {
|
||||||
|
counts = []dayCount{}
|
||||||
|
}
|
||||||
|
h.cache.set(username, counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -216,7 +253,12 @@ func main() {
|
|||||||
os.Exit(1)
|
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 := http.NewServeMux()
|
||||||
mux.Handle("/heatmap/", h)
|
mux.Handle("/heatmap/", h)
|
||||||
|
|||||||
Reference in New Issue
Block a user