games: a ladder you climb against the clock
This commit is contained in:
126
internal/web/trivia_bank.go
Normal file
126
internal/web/trivia_bank.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"pete/internal/games/trivia"
|
||||
"pete/internal/opentdb"
|
||||
"pete/internal/storage"
|
||||
)
|
||||
|
||||
// Keeping the trivia bank stocked.
|
||||
//
|
||||
// The bank is not consumed by play — a question drawn is still there afterwards
|
||||
// — so this loop is about *variety*, not supply. It fills each difficulty up to
|
||||
// a target and then has nothing to do, which is why a pass that finds the bank
|
||||
// full costs three COUNT queries and no network at all.
|
||||
|
||||
// bankTarget is how many questions of each difficulty we want to hold. Twelve
|
||||
// rungs drawn from four hundred is enough that a regular player doesn't start
|
||||
// recognising them, and it's a size OpenTDB's pool can actually fill.
|
||||
const bankTarget = 400
|
||||
|
||||
// bankMaxFetches bounds one pass. At OpenTDB's politeness interval this is a
|
||||
// couple of minutes of drip, after which the loop goes back to sleep rather than
|
||||
// hammering a free API for an hour to top up the last few questions.
|
||||
const bankMaxFetches = 12
|
||||
|
||||
// bankInterval is how often we go back and look. The bank is a slow-moving
|
||||
// thing: it only grows, and it only needs to grow once.
|
||||
const bankInterval = 12 * time.Hour
|
||||
|
||||
// StartTriviaBank launches the refill loop if the casino is on. Safe to call
|
||||
// unconditionally; a no-op when games are off.
|
||||
func (s *Server) StartTriviaBank(ctx context.Context) {
|
||||
if !s.gamesReady() {
|
||||
return
|
||||
}
|
||||
go s.runTriviaBank(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) runTriviaBank(ctx context.Context) {
|
||||
slog.Info("games: trivia bank refill started", "target", bankTarget, "interval", bankInterval)
|
||||
|
||||
s.refillTriviaBank(ctx)
|
||||
|
||||
ticker := time.NewTicker(bankInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.refillTriviaBank(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refillTriviaBank tops each difficulty up toward the target, politely.
|
||||
//
|
||||
// Every failure here is survivable and none of them stop the loop: OpenTDB is a
|
||||
// free API that is sometimes down, and a thin bank costs a player nothing worse
|
||||
// than a "give it a minute" when they try to start a ladder.
|
||||
func (s *Server) refillTriviaBank(ctx context.Context) {
|
||||
client := opentdb.New()
|
||||
fetches := 0
|
||||
|
||||
for _, t := range trivia.Tiers {
|
||||
for fetches < bankMaxFetches {
|
||||
have, err := storage.CountTrivia(t.Difficulty)
|
||||
if err != nil {
|
||||
slog.Error("games: trivia bank count", "difficulty", t.Difficulty, "err", err)
|
||||
break
|
||||
}
|
||||
if have >= bankTarget {
|
||||
break
|
||||
}
|
||||
|
||||
qs, err := client.Fetch(ctx, t.Difficulty, opentdb.Batch)
|
||||
fetches++
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
slog.Warn("games: trivia bank fetch", "difficulty", t.Difficulty, "err", err)
|
||||
// Whatever went wrong, waiting is the only sensible response: the
|
||||
// likeliest cause is the rate limit, and retrying at once earns another.
|
||||
if !sleepCtx(ctx, opentdb.Politeness) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
added, err := storage.AddTriviaQuestions(t.Difficulty, qs)
|
||||
if err != nil {
|
||||
slog.Error("games: trivia bank store", "difficulty", t.Difficulty, "err", err)
|
||||
break
|
||||
}
|
||||
slog.Info("games: trivia bank filled",
|
||||
"difficulty", t.Difficulty, "fetched", len(qs), "new", added, "have", have+added)
|
||||
|
||||
// The API hands back random batches, so once the bank is deep the
|
||||
// overlap gets heavy and a batch adds almost nothing new. When it adds
|
||||
// nothing at all, this difficulty has given us what it has: stop asking.
|
||||
if added == 0 {
|
||||
break
|
||||
}
|
||||
if !sleepCtx(ctx, opentdb.Politeness) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sleepCtx waits, unless we're being shut down. Reports false if we are.
|
||||
func sleepCtx(ctx context.Context, d time.Duration) bool {
|
||||
t := time.NewTimer(d)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-t.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user