Synchronize Handler shared state (H-10) #107
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat!107
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/handler-state-race"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Workstream item #12. Audit finding H-10 (High) — data race on shared mutable Handler state.
reindexRunning,reindexProgress,latestTagsCache,latestTagsCacheTimelived as bare fields onHandlerand were accessed concurrently from HTTP goroutines and from the worker goroutine started inhandleAdminReindex. Real data race in prod, not hypothetical.Changes
internal/handler/state.gowith two typed structs, each holding its ownsync.Mutex:reindexState—tryStart(atomic check-and-set, closes the race where two admins could each seerunning=falseand spawn parallel workers),setProgress,finish,snapshot(returns both fields under one lock so the rendered page never showsrunning=false, progress="3/10 ...").latestTagsCache—get(now),set(now, entries),invalidate. TTL constant moved next to the type.Handlerfields replaced by two embedded structs.search.gogetter + reindex handler,admin.goreindex render path, the sixinvalidateLatestTagsCachecallers continue working through the forwarder method).state_test.go) deliberately hammer both state types from many goroutines sogo test -racecatches any future refactor that drops the lock.Test plan
go test ./...passesgo test -race ./...clean across the whole suite (211s for the handler package)-racetryStartreturns false)🤖 Generated with Claude Code
Audit finding H-10. reindexRunning, reindexProgress, latestTagsCache, and latestTagsCacheTime lived as bare fields on Handler. They were read and written from concurrent HTTP goroutines and from the worker goroutine started in handleAdminReindex — a real data race in production, not a hypothetical one. Replace the four fields with two typed structs (internal/handler/state.go): reindexState — sync.Mutex + running + progress methods: tryStart, setProgress, finish, snapshot latestTagsCache — sync.Mutex + entries map + stored time methods: get, set, invalidate Update every read/write site: - handleAdminReindex now claims the slot via reindex.tryStart (atomically check-and-set) instead of read-then-write, closing the race where two admins could each see running=false and spawn parallel workers. The worker's defer calls reindex.finish. - handleAdminProjects reads both reindex fields atomically via snapshot, so the rendered page never shows a torn state like (running=false, progress="3/10 ..."). - getLatestVersionTags / invalidateLatestTagsCache go through the cache's get/set/invalidate methods. Two new concurrency tests (state_test.go) deliberately hammer both state types from many goroutines so `go test -race` catches any future refactor that drops the lock — the existing tests don't exercise this shape and so wouldn't have flagged H-10 either. `go test -race ./...` clean across the whole suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>