Add core components for request coalescing and service management
- Introduced coalescing logic in `coalescing.go` to handle concurrent identical upstream fetches, including a state machine and response buffering for improved performance. - Implemented a new cache file format in `format.go`, supporting serialization and deserialization of HTTP responses, along with range request handling. - Developed an HTTP handler in `handler.go` to manage requests, including special endpoint handling and metrics reporting. - Added rate limiting functionality in `ratelimit.go` to control per-client and global request rates, enhancing security and performance. - Created service management capabilities in `service.go` to define and manage cacheable services, including user-agent detection. - Updated tests in `steamcache_test.go` to cover new functionalities, ensuring robustness and reliability across the codebase.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// steamcache/coalescing.go
|
||||
// Request coalescing (de-duplicating concurrent identical upstream fetches for the same
|
||||
// cache key). Includes the coalescedRequest state machine + waiter/leader coordination,
|
||||
// response buffering for thundering herd avoidance, and the coalescer wrapper that
|
||||
// owns the in-flight map + mutex (SteamCache methods delegate; no direct map access
|
||||
// in core or handler).
|
||||
package steamcache
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type coalescedRequest struct {
|
||||
waitingCount atomic.Int32
|
||||
done bool
|
||||
mu sync.Mutex
|
||||
// Buffered response data for coalesced clients
|
||||
responseData []byte
|
||||
responseHeaders http.Header
|
||||
statusCode int
|
||||
// Broadcast signal for all waiters (closed by leader in complete)
|
||||
doneCh chan struct{}
|
||||
completionErr error
|
||||
// Active protocol (post-legacy cleanup): waiters wake on doneCh, then read completionErr/response* under mu (or pre-unlock copies in waiter).
|
||||
}
|
||||
|
||||
func newCoalescedRequest() *coalescedRequest {
|
||||
cr := &coalescedRequest{
|
||||
done: false,
|
||||
responseHeaders: make(http.Header),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
cr.waitingCount.Store(1)
|
||||
return cr
|
||||
}
|
||||
|
||||
func (cr *coalescedRequest) addWaiter() {
|
||||
cr.waitingCount.Add(1)
|
||||
}
|
||||
|
||||
func (cr *coalescedRequest) complete(resp *http.Response, err error) {
|
||||
cr.mu.Lock()
|
||||
defer cr.mu.Unlock()
|
||||
if cr.done {
|
||||
return
|
||||
}
|
||||
cr.done = true
|
||||
|
||||
if err != nil {
|
||||
cr.completionErr = err
|
||||
} else {
|
||||
// Store response data for coalesced clients
|
||||
if resp != nil {
|
||||
cr.statusCode = resp.StatusCode
|
||||
// Copy headers (excluding hop-by-hop headers via filter)
|
||||
cr.responseHeaders = filterHopByHopHeaders(resp.Header)
|
||||
}
|
||||
}
|
||||
// Broadcast to *all* waiters (thundering herd fix). Close is safe here because of the done guard above.
|
||||
close(cr.doneCh)
|
||||
}
|
||||
|
||||
// setResponseData stores the buffered response data for coalesced clients
|
||||
func (cr *coalescedRequest) setResponseData(data []byte) {
|
||||
cr.mu.Lock()
|
||||
defer cr.mu.Unlock()
|
||||
cr.responseData = make([]byte, len(data))
|
||||
copy(cr.responseData, data)
|
||||
}
|
||||
|
||||
// coalescer owns the coalesced requests map and mutex. It encapsulates the
|
||||
// in-flight request dedup state so SteamCache no longer directly manipulates
|
||||
// the raw map (Phase 2 extraction). Unexported; same-package access for tests.
|
||||
type coalescer struct {
|
||||
mu sync.Mutex
|
||||
requests map[string]*coalescedRequest
|
||||
}
|
||||
|
||||
// newCoalescer constructs an empty coalescer (called from SteamCache.New).
|
||||
func newCoalescer() *coalescer {
|
||||
return &coalescer{
|
||||
requests: make(map[string]*coalescedRequest),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *coalescer) getOrCreate(cacheKey string) (*coalescedRequest, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if cr, exists := c.requests[cacheKey]; exists {
|
||||
cr.addWaiter()
|
||||
return cr, false
|
||||
}
|
||||
|
||||
cr := newCoalescedRequest()
|
||||
c.requests[cacheKey] = cr
|
||||
return cr, true
|
||||
}
|
||||
|
||||
func (c *coalescer) remove(cacheKey string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.requests, cacheKey)
|
||||
}
|
||||
|
||||
// getOrCreateCoalescedRequest delegates to the owned coalescer (preserves
|
||||
// existing call sites in handler.go and any white-box tests unchanged).
|
||||
func (sc *SteamCache) getOrCreateCoalescedRequest(cacheKey string) (*coalescedRequest, bool) {
|
||||
return sc.coalescer.getOrCreate(cacheKey)
|
||||
}
|
||||
|
||||
// removeCoalescedRequest delegates to the owned coalescer.
|
||||
func (sc *SteamCache) removeCoalescedRequest(cacheKey string) {
|
||||
sc.coalescer.remove(cacheKey)
|
||||
}
|
||||
Reference in New Issue
Block a user