server bootstrap and state

This commit is contained in:
2025-09-06 18:55:21 +03:00
parent cdc8c366e1
commit 714f0a8f52
2 changed files with 764 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use rusqlite::Connection;
use tokio::sync::{Mutex, RwLock, Semaphore};
use uuid::Uuid;
use cruciverb_core::clue::{ClueDatabase, CrypticClueDb};
use crate::activitypub::ApState;
use cruciverb_core::dict::Dictionary;
use cruciverb_core::puzzle::Puzzle;
const STALENESS_WINDOW: u64 = 50;
const PUZZLE_CACHE_MAX: usize = 500;
const PUZZLE_CACHE_EXPIRY_SECS: u64 = 3600;
pub struct AppStateInner {
pub db: Connection,
}
pub struct AppState {
pub dict: Arc<Dictionary>,
pub clue_db: Arc<ClueDatabase>,
pub cryptic_db: Option<Arc<CrypticClueDb>>,
pub word_diffs: Arc<HashMap<String, f64>>,
pub puzzles: RwLock<HashMap<Uuid, (Puzzle, Instant)>>,
pub db: Mutex<AppStateInner>,
pub db_path: String,
pub word_quality: RwLock<HashMap<String, f64>>,
pub clue_quality: RwLock<HashMap<(String, String), f64>>,
pub rp_id: String,
pub rp_origin: String,
// maps challenge -> (session_token, timestamp)
pub reg_challenges: Mutex<HashMap<String, (String, i64)>>,
// maps challenge -> timestamp (no session needed for auth)
pub auth_challenges: Mutex<HashMap<String, i64>>,
pub generation_semaphore: Semaphore,
pub ap: Option<Arc<ApState>>,
pub daily_secret: String,
pub rooms: crate::rooms::Rooms,
pub blocklist: std::sync::RwLock<cruciverb_core::dict::blocklist::Blocklist>,
pub definitions: Arc<HashMap<String, String>>,
pub wotd_words: Arc<Vec<String>>,
/// Per-(date, preset) locks so the pre-generator and the API cannot both
/// generate the same daily puzzle at once.
pub daily_gen_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
/// Tracks the generation number when each word was last used in a puzzle.
pub word_staleness: RwLock<HashMap<String, u64>>,
/// Monotonic counter incremented after each puzzle generation.
pub generation_counter: AtomicU64,
}
impl AppState {
pub async fn cache_puzzle(&self, id: Uuid, puzzle: Puzzle) {
let mut cache = self.puzzles.write().await;
cache.insert(id, (puzzle, Instant::now()));
if cache.len() > PUZZLE_CACHE_MAX {
let cutoff = Instant::now() - std::time::Duration::from_secs(PUZZLE_CACHE_EXPIRY_SECS);
cache.retain(|_, (_, ts)| *ts > cutoff);
}
if cache.len() > PUZZLE_CACHE_MAX {
let mut entries: Vec<(Uuid, Instant)> = cache.iter().map(|(k, (_, ts))| (*k, *ts)).collect();
entries.sort_by_key(|(_, ts)| *ts);
for (k, _) in entries.iter().take(100) {
cache.remove(k);
}
}
}
/// Build a staleness penalty map for the solver.
/// Returns word -> penalty where penalty is 0.3 (just used) to 1.0 (old/unused).
pub async fn build_staleness_penalties(&self) -> Option<Arc<HashMap<String, f64>>> {
let gen = self.generation_counter.load(Ordering::Relaxed);
let staleness = self.word_staleness.read().await;
if staleness.is_empty() {
return None;
}
let mut penalties: HashMap<String, f64> = HashMap::new();
for (word, &last_gen) in staleness.iter() {
let age = gen.saturating_sub(last_gen);
if age < STALENESS_WINDOW {
// 0.3 for just-used words, linearly decaying to 1.0
let decay = age as f64 / STALENESS_WINDOW as f64;
penalties.insert(word.clone(), 0.3 + 0.7 * decay);
}
}
if penalties.is_empty() { None } else { Some(Arc::new(penalties)) }
}
/// Record words from a generated puzzle so future puzzles deprioritize them.
pub async fn record_puzzle_words(&self, words: &[String]) {
let gen = self.generation_counter.fetch_add(1, Ordering::Relaxed);
let mut staleness = self.word_staleness.write().await;
for word in words {
staleness.insert(word.clone(), gen);
}
// prune old entries periodically
if staleness.len() > 10_000 {
let cutoff = gen.saturating_sub(STALENESS_WINDOW);
staleness.retain(|_, v| *v >= cutoff);
}
}
/// Returns a per-(date, preset) async lock that serializes daily
/// generation between the pre-generator and on-demand API requests.
pub async fn daily_gen_lock(&self, date: &str, preset: &str) -> Arc<tokio::sync::Mutex<()>> {
let mut locks = self.daily_gen_locks.lock().await;
locks
.entry(format!("{}:{}", date, preset))
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
pub fn new(
dict: Dictionary,
clue_db: ClueDatabase,
cryptic_db: Option<CrypticClueDb>,
word_diffs: HashMap<String, f64>,
db: Connection,
db_path: String,
rp_id: String,
rp_origin: String,
ap: Option<ApState>,
daily_secret: String,
blocklist: cruciverb_core::dict::blocklist::Blocklist,
definitions: HashMap<String, String>,
wotd_words: Vec<String>,
) -> Arc<Self> {
Arc::new(Self {
dict: Arc::new(dict),
clue_db: Arc::new(clue_db),
cryptic_db: cryptic_db.map(Arc::new),
word_diffs: Arc::new(word_diffs),
puzzles: RwLock::new(HashMap::new()),
db: Mutex::new(AppStateInner { db }),
db_path,
word_quality: RwLock::new(HashMap::new()),
clue_quality: RwLock::new(HashMap::new()),
rp_id,
rp_origin,
reg_challenges: Mutex::new(HashMap::new()),
auth_challenges: Mutex::new(HashMap::new()),
generation_semaphore: Semaphore::new(3),
ap: ap.map(Arc::new),
daily_secret,
rooms: crate::rooms::new_rooms(),
blocklist: std::sync::RwLock::new(blocklist),
definitions: Arc::new(definitions),
wotd_words: Arc::new(wotd_words),
daily_gen_locks: Mutex::new(HashMap::new()),
word_staleness: RwLock::new(HashMap::new()),
generation_counter: AtomicU64::new(0),
})
}
}