127 lines
3.7 KiB
Go
127 lines
3.7 KiB
Go
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
|
|
}
|
|
}
|