an http based roguelike rpg about jelqing
| 1 | //! The live runs. The table is only locked long enough to find a run, and each run has its own |
| 2 | //! lock, so one slow turn never holds up anyone else's. Nothing ever waits on a run's lock while |
| 3 | //! holding the table's, which is what keeps the two from deadlocking. |
| 4 | |
| 5 | use std::{ |
| 6 | collections::{HashMap, hash_map::Entry}, |
| 7 | sync::{ |
| 8 | Arc, Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard, |
| 9 | TryLockError, |
| 10 | }, |
| 11 | time::{Duration, Instant}, |
| 12 | }; |
| 13 | |
| 14 | use crate::game::Run; |
| 15 | |
| 16 | pub type Slot = Arc<Mutex<Run>>; |
| 17 | |
| 18 | #[derive(Default)] |
| 19 | pub struct Registry { |
| 20 | runs: RwLock<HashMap<String, Slot>>, |
| 21 | } |
| 22 | |
| 23 | /// A run's lock as it was found. A panic mid-turn poisons it, and that run cannot be trusted. |
| 24 | pub enum Lock<'a> { |
| 25 | Held(MutexGuard<'a, Run>), |
| 26 | Poisoned(MutexGuard<'a, Run>), |
| 27 | } |
| 28 | |
| 29 | impl<'a> Lock<'a> { |
| 30 | pub fn take(slot: &'a Slot) -> Self { |
| 31 | match slot.lock() { |
| 32 | Ok(run) => Self::Held(run), |
| 33 | Err(poisoned) => Self::Poisoned(poisoned.into_inner()), |
| 34 | } |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | fn peek(slot: &Slot) -> Option<MutexGuard<'_, Run>> { |
| 39 | match slot.try_lock() { |
| 40 | Ok(run) => Some(run), |
| 41 | Err(TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()), |
| 42 | Err(TryLockError::WouldBlock) => None, |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | impl Registry { |
| 47 | pub fn get(&self, id: &str) -> Option<Slot> { |
| 48 | self.read().get(id).cloned() |
| 49 | } |
| 50 | |
| 51 | pub fn len(&self) -> usize { |
| 52 | self.read().len() |
| 53 | } |
| 54 | |
| 55 | pub fn contains(&self, id: &str) -> bool { |
| 56 | self.read().contains_key(id) |
| 57 | } |
| 58 | |
| 59 | /// Refuses to replace a live run, so a colliding id can never evict one. |
| 60 | pub fn insert_new(&self, run: Run) -> bool { |
| 61 | match self.write().entry(run.id().to_owned()) { |
| 62 | Entry::Occupied(_) => false, |
| 63 | Entry::Vacant(slot) => { |
| 64 | slot.insert(Arc::new(Mutex::new(run))); |
| 65 | true |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | pub fn remove(&self, id: &str) { |
| 71 | self.write().remove(id); |
| 72 | } |
| 73 | |
| 74 | /// Every live run not mid-turn. A busy run is skipped rather than waited for. |
| 75 | pub fn each<T>(&self, mut visit: impl FnMut(&Run) -> T) -> Vec<T> { |
| 76 | let slots: Vec<Slot> = self.read().values().cloned().collect(); |
| 77 | slots |
| 78 | .iter() |
| 79 | .filter_map(|slot| peek(slot).map(|run| visit(&run))) |
| 80 | .collect() |
| 81 | } |
| 82 | |
| 83 | pub fn evict_idle(&self, now: Instant, ttl: Duration) -> Vec<Slot> { |
| 84 | let idle: Vec<String> = self |
| 85 | .read() |
| 86 | .iter() |
| 87 | .filter(|(_, slot)| { |
| 88 | peek(slot).is_some_and(|run| now.duration_since(run.last_seen()) >= ttl) |
| 89 | }) |
| 90 | .map(|(id, _)| id.clone()) |
| 91 | .collect(); |
| 92 | let mut runs = self.write(); |
| 93 | idle.iter().filter_map(|id| runs.remove(id)).collect() |
| 94 | } |
| 95 | |
| 96 | fn read(&self) -> RwLockReadGuard<'_, HashMap<String, Slot>> { |
| 97 | self.runs.read().unwrap_or_else(PoisonError::into_inner) |
| 98 | } |
| 99 | |
| 100 | fn write(&self) -> RwLockWriteGuard<'_, HashMap<String, Slot>> { |
| 101 | self.runs.write().unwrap_or_else(PoisonError::into_inner) |
| 102 | } |
| 103 | } |