Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Handler ¶
type Handler struct {
DB *database.Database
DBType string
Archive *jsonarchive.Generator
ArchiveFrequency jsonarchive.Frequency
Limiter *RateLimiter
Logf func(string, ...any)
Cache *ResponseCache
TrackInfo *TrackInfoCache // Cache expensive COUNT(DISTINCT) metadata so /api stays fast even on huge datasets.
}
Handler wires together the database and archive generator so HTTP routes can stay small and focused on translating query parameters into the asynchronous building blocks behind the scenes.
func NewHandler ¶
func NewHandler(db *database.Database, dbType string, archive *jsonarchive.Generator, limiter *RateLimiter, logf func(string, ...any), freq jsonarchive.Frequency) *Handler
NewHandler constructs a Handler with sane defaults. Logf is optional; pass nil if logging is not required.
type Permit ¶
type Permit struct {
WaitNotice bool
WaitDuration time.Duration
// contains filtered or unexported fields
}
Permit represents an acquired slot for a particular request. Call Release when the handler finished processing so the next queued request can proceed.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter coordinates per-IP request sequencing without relying on mutexes. Each IP gets its own goroutine so the design follows "Do not communicate by sharing memory; share memory by communicating".
func NewRateLimiter ¶
func NewRateLimiter(heavyCooldown time.Duration) *RateLimiter
NewRateLimiter constructs a limiter with the provided cooldown for heavy endpoints. The limiter immediately starts its coordination goroutine so the caller can use it without additional plumbing.
func (*RateLimiter) Acquire ¶
func (l *RateLimiter) Acquire(ctx context.Context, ip string, kind RequestKind) (*Permit, error)
Acquire reserves a slot for the given IP and request kind. The returned Permit must be released once the handler is done. If the context is cancelled before the permit becomes available an error is returned.
type RequestKind ¶
type RequestKind int
RequestKind distinguishes between lightweight metadata calls and heavy responses that stream larger payloads. This keeps the limiter expressive while staying simple to reason about.
const ( // RequestGeneral marks inexpensive metadata lookups that still benefit // from the per-IP queue so clients cannot overwhelm the server with // concurrent requests. RequestGeneral RequestKind = iota // RequestHeavy marks endpoints that stream large responses. We enforce a // cooldown after each heavy call to prevent repeated downloads from a // single IP. RequestHeavy )
type ResponseCache ¶
type ResponseCache struct {
// contains filtered or unexported fields
}
ResponseCache keeps expensive API responses in memory so identical requests within the TTL avoid hitting the database. We implement it with a dedicated goroutine and channels to honour the constraint of coordinating state without mutexes.
func NewResponseCache ¶
func NewResponseCache(ttl time.Duration) *ResponseCache
NewResponseCache starts the caching goroutine immediately. Callers may pass nil to disable caching entirely. The clock is injectable for tests; in production we default to time.Now.
func (*ResponseCache) Close ¶
func (c *ResponseCache) Close()
Close stops the cache goroutine. The method is safe to call multiple times; subsequent calls have no effect.
func (*ResponseCache) Get ¶
func (c *ResponseCache) Get(ctx context.Context, key string, loader func(context.Context) ([]byte, error)) ([]byte, error)
Get returns cached bytes for the provided key or invokes loader to produce them. We copy the stored slice before returning so callers can safely modify the result without affecting future hits.
type TrackInfoCache ¶
type TrackInfoCache struct {
// contains filtered or unexported fields
}
TrackInfoCache shields handlers from heavy COUNT(DISTINCT) queries by caching the result and refreshing it in the background. Following the proverb "Don't communicate by sharing memory; share memory by communicating", we coordinate exclusively via channels.
func NewTrackInfoCache ¶
func NewTrackInfoCache(db *database.Database, dbType string, ttl, timeout, retry time.Duration, logf func(string, ...any)) *TrackInfoCache
NewTrackInfoCache starts the cache goroutine. ttl controls how long results stay fresh, timeout bounds database calls and retry decides how quickly we attempt to recover after failures. Passing nil db disables the cache so handlers can fall back to direct queries.
func (*TrackInfoCache) Close ¶
func (c *TrackInfoCache) Close()
Close stops the goroutine. The method is idempotent so shutdown paths stay simple.