server bootstrap and state
This commit is contained in:
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user