main: bound startup sweep with context timeout

Sweep() used to run synchronously at startup without any time bound.
On a large uploads directory this can block the server for 30s or more,
keeping /healthz returning 503 while the reaper works. Add a context
timeout so startup continues if the sweep cannot finish within budget.

The timeout is set to 5 seconds: long enough to clear typical leftover
state, short enough that a stuck sweep will not meaningfully delay
health checks or startup readiness.

Change Store.Sweep to accept a context.Context and check ctx.Err() at
the top of each iteration. If the context is cancelled or times out,
Sweep returns the files deleted so far and ctx.Err().

Add TestStore_Sweep_HonorsContextCancel to ensure a cancelled context
causes Sweep to exit early instead of running to completion.

Deliberately unchanged: there is still no periodic reaper. The server
only sweeps once at startup; this change just bounds that single sweep.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-14 10:18:13 +08:00
co-authored by Claude
parent 97f6184f65
commit 157848d87a
3 changed files with 53 additions and 6 deletions
+5 -1
View File
@@ -4,6 +4,7 @@
package uploads
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@@ -160,7 +161,7 @@ func (s *Store) Validate(absPath string) (string, error) {
// Sweep deletes data files (and their sidecars) whose uploaded_at is older
// than ttl, as well as orphan sidecars that have no matching data file. For
// data files whose sidecar is missing, the file's mtime is used as a fallback.
func (s *Store) Sweep(ttl time.Duration) (deleted int, err error) {
func (s *Store) Sweep(ctx context.Context, ttl time.Duration) (deleted int, err error) {
entries, err := os.ReadDir(s.Root)
if err != nil {
return 0, fmt.Errorf("uploads: read root: %w", err)
@@ -197,6 +198,9 @@ func (s *Store) Sweep(ttl time.Duration) (deleted int, err error) {
cutoff := time.Now().Add(-ttl)
for fileID, it := range items {
if err := ctx.Err(); err != nil {
return deleted, err
}
dataPath := filepath.Join(s.Root, fileID)
metaPath := dataPath + ".meta.json"