perf(backend): optimize ban lookups and prune upload group map

- Optimize the ban matching middleware by using a read-only transaction (`db.View`) for the initial scan, avoiding the single bbolt write lock on every request when no ban matches.
- Implement periodic pruning of stale entries in the upload grouper map to prevent unbounded memory growth over time.
- Avoid redundant parsing of the `max_days` form value in the upload handler.
This commit is contained in:
2026-06-01 00:12:43 +03:00
parent 01996c0445
commit 71d9b9db7e
3 changed files with 64 additions and 15 deletions

View File

@@ -80,7 +80,8 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
// Unlimited expiry: admins, or users whose effective MaxDays is negative.
unlimitedExpiry := isAdminUpload || effectivePolicy.MaxDays < 0
maxDays := parseInt(r.FormValue("max_days"))
rawMaxDays := parseInt(r.FormValue("max_days"))
maxDays := rawMaxDays
if maxDays <= 0 {
maxDays = 7
if effectivePolicy.MaxDays > 0 && effectivePolicy.MaxDays < maxDays {
@@ -96,7 +97,7 @@ func (a *App) Upload(w http.ResponseWriter, r *http.Request) {
expiresMinutes := parseInt(r.FormValue("expires_minutes"))
// A negative expires_minutes (or max_days) is the "never expires" request.
// Only honour it for unlimited uploaders; otherwise it's an invalid value.
if expiresMinutes < 0 || parseInt(r.FormValue("max_days")) < 0 {
if expiresMinutes < 0 || rawMaxDays < 0 {
if !unlimitedExpiry {
a.logger.Warn("upload rejected unlimited expiration", "source", "user-upload", "severity", "warn", "code", 4133, "ip", uploadClientIP(r), "user_id", user.ID)
helpers.WriteJSONError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("expiration cannot exceed %d days", effectivePolicy.MaxDays))

View File

@@ -16,13 +16,18 @@ const uploadGroupWindow = 20 * time.Second
// value (per account/IP) within uploadGroupWindow are grouped into one box.
const uploadBatchHeader = "X-Warpbox-Batch"
// uploadGroupPruneInterval is how often entryFor drops stale entries so the map
// can't grow without bound (one key per account/IP + batch value otherwise).
const uploadGroupPruneInterval = 5 * time.Minute
// uploadGrouper tracks the most recent box per batch key so opt-in batched
// uploads land in a single box. Each key has its own lock, which also serialises
// that key's concurrent uploads so they append to the same box instead of racing
// to create several.
type uploadGrouper struct {
mu sync.Mutex
entries map[string]*uploadGroupEntry
mu sync.Mutex
entries map[string]*uploadGroupEntry
lastPrune time.Time
}
type uploadGroupEntry struct {
@@ -40,6 +45,7 @@ func newUploadGrouper() *uploadGrouper {
func (g *uploadGrouper) entryFor(key string) *uploadGroupEntry {
g.mu.Lock()
defer g.mu.Unlock()
g.pruneLocked(time.Now())
entry, ok := g.entries[key]
if !ok {
entry = &uploadGroupEntry{}
@@ -47,3 +53,24 @@ func (g *uploadGrouper) entryFor(key string) *uploadGroupEntry {
}
return entry
}
// pruneLocked drops entries whose last use is well past the grouping window so
// the map stays bounded to recently-active keys. Callers must hold g.mu. Entries
// currently in use, or freshly created but not yet used (zero timestamp), are
// kept to avoid removing one a request is about to populate.
func (g *uploadGrouper) pruneLocked(now time.Time) {
if now.Sub(g.lastPrune) < uploadGroupPruneInterval {
return
}
g.lastPrune = now
for key, entry := range g.entries {
if !entry.mu.TryLock() {
continue
}
stale := !entry.at.IsZero() && now.Sub(entry.at) > 2*uploadGroupWindow
entry.mu.Unlock()
if stale {
delete(g.entries, key)
}
}
}