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 } }