1//! The in-memory reference adapter: every port, held in a `Mutex` and nothing else. It is what the 2//! conformance suite is first proven against (the suite must pass on the simplest honest 3//! implementation before it is allowed to judge another), what the service's tests run on, and a 4//! readable statement of what each port means. 5//! 6//! The outbound ports (`Judge`, `Embedder`, `Voice`, `Thinker`) are scripted: a test says what they 7//! answer and reads how often they were asked. 8//! 9//! Handles are cheap clones of shared state, so "the process restarted" is a clone of the handle: the 10//! state survives, which is exactly what a restart of a durable adapter must look like from outside. 11 12use std::collections::BTreeMap; 13use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 14use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; 15 16use ::log::{debug, trace}; 17use whiskers_core::{Age, ChatState, Direction, Household, IconId, MemoryDoc, MemorySnapshot, TokenLimit, Verdict}; 18use whiskers_ports::{ 19 Allowance, Appended, Audio, CreditsError, DeviceCursor, DeviceName, Diagnostic, EmbedBatch, EmbedError, Embedder, EntryLine, 20 Hub, Journal, Judge, JudgeError, Lookup, LogCursor, ManualClock, Merged, ModelName, Page, PictureShelf, Put, ReserveError, Reply, 21 SafePictureId, SecretName, Secrets, Settlement, SpeakError, SpeechLine, StoreError, StoredLine, ThinkError, ThinkRequest, Thinker, 22 ThinkingLedger, ThinkingRoom, ThinkingUsage, TimedAudio, TimingError, AudioMs, LineTiming, Tokens, Voice, VoiceCredits, VoiceDay, VoiceReservation, VoiceSpend, 23 Clock, HasAllowance, HasChat, HasClock, HasEmbedder, HasHousehold, HasJournal, HasJudge, HasMemory, HasPictures, HasThinker, HasVoice, Millis, 24}; 25 26fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> { 27 m.lock().unwrap_or_else(PoisonError::into_inner) 28} 29 30/// The memory hub. 31#[derive(Clone, Default)] 32pub struct MemMemoryHub(Arc<Mutex<MemoryDoc>>); 33 34impl Hub for MemMemoryHub { 35 type Document = MemorySnapshot; 36 37 async fn merge(&self, theirs: MemorySnapshot) -> Result<Merged<MemorySnapshot>, StoreError> { 38 let mut doc = lock(&self.0); 39 let changed = doc.merge(theirs); 40 Ok(Merged { document: doc.snapshot(), changed }) 41 } 42 43 async fn current(&self) -> Result<MemorySnapshot, StoreError> { 44 Ok(lock(&self.0).snapshot()) 45 } 46} 47 48/// The household hub. 49#[derive(Clone, Default)] 50pub struct MemHouseholdHub(Arc<Mutex<Household>>); 51 52impl Hub for MemHouseholdHub { 53 type Document = Household; 54 55 async fn merge(&self, theirs: Household) -> Result<Merged<Household>, StoreError> { 56 let mut doc = lock(&self.0); 57 let changed = doc.merge(&theirs); 58 Ok(Merged { document: doc.clone(), changed: usize::from(changed) }) 59 } 60 61 async fn current(&self) -> Result<Household, StoreError> { 62 Ok(lock(&self.0).clone()) 63 } 64} 65 66/// The chat hub. Its storage can be told to refuse writes, to see what a failed save does. 67#[derive(Clone, Default)] 68pub struct MemChatHub { 69 doc: Arc<Mutex<ChatState>>, 70 refuse_writes: Arc<AtomicBool>, 71} 72 73impl MemChatHub { 74 /// From now on (until told otherwise) a copy that would have to be saved is refused. 75 pub fn refuse_writes(&self, refuse: bool) { 76 self.refuse_writes.store(refuse, Ordering::SeqCst); 77 } 78} 79 80impl Hub for MemChatHub { 81 type Document = ChatState; 82 83 async fn merge(&self, theirs: ChatState) -> Result<Merged<ChatState>, StoreError> { 84 let mut doc = lock(&self.doc); 85 if !theirs.is_newer_than(&doc) { 86 return Ok(Merged { document: doc.clone(), changed: 0 }); 87 } 88 if self.refuse_writes.load(Ordering::SeqCst) { 89 debug!("mem chat hub: refusing to keep v{}", theirs.version); 90 return Err(StoreError::Unavailable); 91 } 92 let adopted = doc.merge(theirs); 93 Ok(Merged { document: doc.clone(), changed: usize::from(adopted) }) 94 } 95 96 async fn current(&self) -> Result<ChatState, StoreError> { 97 Ok(lock(&self.doc).clone()) 98 } 99} 100 101/// The journal. 102#[derive(Clone, Default)] 103pub struct MemJournal(Arc<Mutex<Vec<(DeviceName, StoredLine)>>>); 104 105impl MemJournal { 106 fn held_in(log: &[(DeviceName, StoredLine)], device: &DeviceName) -> DeviceCursor { 107 DeviceCursor::new(log.iter().filter(|(d, _)| d == device).count() as u64) 108 } 109} 110 111impl Journal for MemJournal { 112 async fn held(&self, device: &DeviceName) -> Result<DeviceCursor, StoreError> { 113 Ok(Self::held_in(&lock(&self.0), device)) 114 } 115 116 async fn append(&self, device: &DeviceName, at: DeviceCursor, lines: &[EntryLine]) -> Result<Appended, StoreError> { 117 let mut log = lock(&self.0); 118 let held = Self::held_in(&log, device); 119 if lines.is_empty() { 120 return Ok(Appended::Accepted { held }); 121 } 122 if at != held { 123 debug!("mem journal: {} asked to append at {} but holds {}", device.as_str(), at.get(), held.get()); 124 return Ok(Appended::Misaligned { held }); 125 } 126 for l in lines { 127 log.push((device.clone(), StoredLine::of(device, l))); 128 } 129 Ok(Appended::Accepted { held: held.after(lines.len()) }) 130 } 131 132 async fn pull(&self, since: LogCursor) -> Result<Page, StoreError> { 133 let all: Vec<StoredLine> = lock(&self.0).iter().map(|(_, l)| l.clone()).collect(); 134 Ok(Page::of(&all, since)) 135 } 136} 137 138/// The shelf of pictures. 139#[derive(Clone, Default)] 140pub struct MemPictures(Arc<Mutex<BTreeMap<String, Vec<u8>>>>); 141 142impl PictureShelf for MemPictures { 143 async fn has(&self, id: &SafePictureId) -> Result<bool, StoreError> { 144 Ok(lock(&self.0).contains_key(id.as_str())) 145 } 146 147 async fn put(&self, id: &SafePictureId, bytes: &[u8]) -> Result<Put, StoreError> { 148 let mut shelf = lock(&self.0); 149 if shelf.contains_key(id.as_str()) { 150 trace!("mem pictures: {} already kept", id.as_str()); 151 return Ok(Put::AlreadyKept); 152 } 153 shelf.insert(id.as_str().to_owned(), bytes.to_vec()); 154 Ok(Put::Stored) 155 } 156 157 async fn get(&self, id: &SafePictureId) -> Result<Option<Vec<u8>>, StoreError> { 158 Ok(lock(&self.0).get(id.as_str()).cloned()) 159 } 160} 161 162#[derive(Default)] 163struct AllowanceState { 164 thinking: ThinkingLedger, 165 voice: VoiceDay, 166 last_failure: Option<SpeakError>, 167} 168 169/// The allowance. 170#[derive(Clone, Default)] 171pub struct MemAllowance(Arc<Mutex<AllowanceState>>); 172 173impl Allowance for MemAllowance { 174 async fn thinking_room(&self, now: Millis, limit: &TokenLimit) -> Result<ThinkingRoom, StoreError> { 175 Ok(lock(&self.0).thinking.room(now, limit)) 176 } 177 178 async fn charge_thinking(&self, now: Millis, cost: Tokens) -> Result<(), StoreError> { 179 lock(&self.0).thinking.charge(now, cost); 180 Ok(()) 181 } 182 183 async fn thinking_usage(&self, now: Millis, limit: &TokenLimit) -> Result<ThinkingUsage, StoreError> { 184 Ok(lock(&self.0).thinking.usage(now, limit)) 185 } 186 187 async fn reserve_voice(&self, now: Millis, chars: u32, cap: u32) -> Result<VoiceReservation, ReserveError> { 188 lock(&self.0).voice.reserve(now, chars, cap) 189 } 190 191 async fn settle_voice(&self, reservation: VoiceReservation, how: Settlement) -> Result<(), StoreError> { 192 let mut state = lock(&self.0); 193 match how { 194 Settlement::Spoken => state.last_failure = None, 195 Settlement::NotSpoken(why) => { 196 state.voice.release(reservation); 197 state.last_failure = Some(why); 198 } 199 } 200 Ok(()) 201 } 202 203 async fn voice_spend(&self, now: Millis) -> Result<VoiceSpend, StoreError> { 204 let state = lock(&self.0); 205 Ok(VoiceSpend { spent_today: state.voice.spent_on(whiskers_ports::Day::containing(now)), last_failure: state.last_failure.clone() }) 206 } 207} 208 209/// Secrets, as raw text exactly as a store would hold it. 210#[derive(Clone, Default)] 211pub struct MemSecrets(Arc<Mutex<BTreeMap<String, String>>>); 212 213impl MemSecrets { 214 pub fn with(entries: &[(SecretName, &str)]) -> Self { 215 let s = Self::default(); 216 for (n, v) in entries { 217 s.set(*n, v); 218 } 219 s 220 } 221 222 pub fn set(&self, name: SecretName, raw: &str) { 223 lock(&self.0).insert(name.operator_name().to_owned(), raw.to_owned()); 224 } 225} 226 227impl Secrets for MemSecrets { 228 async fn get(&self, name: SecretName) -> Result<Lookup, StoreError> { 229 Ok(Lookup::of(lock(&self.0).get(name.operator_name()).map(String::as_str))) 230 } 231} 232 233/// A Jev that answers what it is told to, and counts how often it was asked. 234pub struct ScriptedJudge { 235 pub verdict: Mutex<Result<Verdict, JudgeError>>, 236 pub ranking: Mutex<Result<Vec<f32>, JudgeError>>, 237 /// What `choose_icon` answers (the options it was last given are in `icon_options`). 238 pub icon: Mutex<Result<Option<IconId>, JudgeError>>, 239 pub icon_options: Mutex<Vec<IconId>>, 240 pub asked: AtomicUsize, 241} 242 243impl Default for ScriptedJudge { 244 fn default() -> Self { 245 Self { 246 verdict: Mutex::new(Ok(Verdict::Allow)), 247 ranking: Mutex::new(Ok(Vec::new())), 248 icon: Mutex::new(Ok(None)), 249 icon_options: Mutex::new(Vec::new()), 250 asked: AtomicUsize::new(0), 251 } 252 } 253} 254 255impl Judge for ScriptedJudge { 256 async fn check(&self, _direction: Direction, _age: Age, _text: &str) -> Result<Verdict, JudgeError> { 257 self.asked.fetch_add(1, Ordering::SeqCst); 258 lock(&self.verdict).clone() 259 } 260 261 async fn rerank(&self, _query: &str, candidates: &[String]) -> Result<Vec<f32>, JudgeError> { 262 self.asked.fetch_add(1, Ordering::SeqCst); 263 match &*lock(&self.ranking) { 264 Ok(p) if p.is_empty() => Ok(vec![0.5; candidates.len()]), 265 other => other.clone(), 266 } 267 } 268 269 async fn choose_icon(&self, _memory: &str, options: &[IconId]) -> Result<Option<IconId>, JudgeError> { 270 self.asked.fetch_add(1, Ordering::SeqCst); 271 *lock(&self.icon_options) = options.to_vec(); 272 lock(&self.icon).clone() 273 } 274} 275 276/// How a scripted embedder turns the texts of a batch into vectors, when a test wants it to answer by what it was 277/// asked rather than with one fixed answer. 278pub type EmbedWith = std::sync::Arc<dyn Fn(&[String]) -> Vec<Vec<f32>> + Send + Sync>; 279 280/// An embedding model that answers what it is told to. 281pub struct ScriptedEmbedder { 282 pub answer: Mutex<Result<Vec<Vec<f32>>, EmbedError>>, 283 /// If set, used instead of `answer`. 284 pub with: Mutex<Option<EmbedWith>>, 285 pub asked: AtomicUsize, 286} 287 288impl Default for ScriptedEmbedder { 289 fn default() -> Self { 290 Self { 291 answer: Mutex::new(Err(EmbedError::Unreachable(Diagnostic::new("embedding server: not scripted")))), 292 with: Mutex::new(None), 293 asked: AtomicUsize::new(0), 294 } 295 } 296} 297 298impl Embedder for ScriptedEmbedder { 299 async fn embed(&self, batch: &EmbedBatch) -> Result<Vec<Vec<f32>>, EmbedError> { 300 self.asked.fetch_add(1, Ordering::SeqCst); 301 if let Some(f) = lock(&self.with).clone() { 302 return Ok(f(batch.texts())); 303 } 304 lock(&self.answer).clone() 305 } 306} 307 308/// A voice that answers what it is told to. 309pub struct ScriptedVoice { 310 pub configured: Mutex<bool>, 311 pub speech: Mutex<Result<Vec<u8>, SpeakError>>, 312 pub credits: Mutex<Result<VoiceCredits, CreditsError>>, 313 pub asked: AtomicUsize, 314} 315 316impl Default for ScriptedVoice { 317 fn default() -> Self { 318 Self { 319 configured: Mutex::new(true), 320 speech: Mutex::new(Ok(vec![0xFF, 0xFB, 1, 2])), 321 credits: Mutex::new(Ok(VoiceCredits { tier: Some("free".into()), used: Some(10), limit: Some(10_000), resets_at_unix: Some(1_800_000_000) })), 322 asked: AtomicUsize::new(0), 323 } 324 } 325} 326 327impl Voice for ScriptedVoice { 328 async fn configured(&self) -> bool { 329 *lock(&self.configured) 330 } 331 332 async fn speak(&self, _line: &SpeechLine) -> Result<Audio, SpeakError> { 333 self.asked.fetch_add(1, Ordering::SeqCst); 334 if !*lock(&self.configured) { 335 return Err(SpeakError::NotConfigured); 336 } 337 lock(&self.speech).clone().map(Audio) 338 } 339 340 /// The scripted audio, and every character said for 100 ms in turn. 341 async fn speak_timed(&self, line: &SpeechLine) -> Result<TimedAudio, SpeakError> { 342 self.asked.fetch_add(1, Ordering::SeqCst); 343 if !*lock(&self.configured) { 344 return Err(SpeakError::NotConfigured); 345 } 346 let audio = Audio(lock(&self.speech).clone()?); 347 let spans = (0..line.chars()).map(|i| (AudioMs::new(i * 100), AudioMs::new(i * 100 + 100))).collect(); 348 let unreadable = |e: TimingError| SpeakError::Unreadable(Diagnostic::new(e.to_string())); 349 let timing = LineTiming::new(line.as_str().to_owned(), spans).map_err(unreadable)?; 350 TimedAudio::new(audio, timing, line).map_err(unreadable) 351 } 352 353 async fn credits(&self) -> Result<VoiceCredits, CreditsError> { 354 lock(&self.credits).clone() 355 } 356} 357 358/// A model that answers what it is told to. 359pub struct ScriptedThinker { 360 pub model: ModelName, 361 pub answer: Mutex<Result<Reply, ThinkError>>, 362 pub asked: AtomicUsize, 363} 364 365impl Default for ScriptedThinker { 366 fn default() -> Self { 367 Self { 368 model: ModelName::new("m").expect("a model name"), 369 answer: Mutex::new(Ok(Reply { status: 200, body: r#"{"content":[],"usage":{"input_tokens":70,"output_tokens":30}}"#.into() })), 370 asked: AtomicUsize::new(0), 371 } 372 } 373} 374 375impl Thinker for ScriptedThinker { 376 fn serves(&self) -> &ModelName { 377 &self.model 378 } 379 380 async fn think(&self, _request: &ThinkRequest) -> Result<Reply, ThinkError> { 381 self.asked.fetch_add(1, Ordering::SeqCst); 382 lock(&self.answer).clone() 383 } 384} 385 386/// Every port, in memory. The fields are public so a test can script the outbound ports and read the 387/// state the service left behind. 388#[derive(Default)] 389pub struct MemBackend { 390 pub memory: MemMemoryHub, 391 pub household: MemHouseholdHub, 392 pub chat: MemChatHub, 393 pub journal: MemJournal, 394 pub pictures: MemPictures, 395 pub allowance: MemAllowance, 396 pub judge: ScriptedJudge, 397 pub embedder: ScriptedEmbedder, 398 pub icons: whiskers_ports::IconVectors, 399 pub voice: ScriptedVoice, 400 pub thinker: ScriptedThinker, 401 pub clock: ManualClock, 402} 403 404// Every capability, so a `MemBackend` is a complete `Backend`. 405impl HasMemory for MemBackend { 406 type Memory = MemMemoryHub; 407 fn memory(&self) -> &MemMemoryHub { 408 &self.memory 409 } 410} 411impl HasHousehold for MemBackend { 412 type Household = MemHouseholdHub; 413 fn household(&self) -> &MemHouseholdHub { 414 &self.household 415 } 416} 417impl HasChat for MemBackend { 418 type Chat = MemChatHub; 419 fn chat(&self) -> &MemChatHub { 420 &self.chat 421 } 422} 423impl HasJournal for MemBackend { 424 type Journal = MemJournal; 425 fn journal(&self) -> &MemJournal { 426 &self.journal 427 } 428} 429impl HasPictures for MemBackend { 430 type Pictures = MemPictures; 431 fn pictures(&self) -> &MemPictures { 432 &self.pictures 433 } 434} 435impl HasAllowance for MemBackend { 436 type Allowance = MemAllowance; 437 fn allowance(&self) -> &MemAllowance { 438 &self.allowance 439 } 440} 441impl HasJudge for MemBackend { 442 type Judge = ScriptedJudge; 443 fn judge(&self) -> &ScriptedJudge { 444 &self.judge 445 } 446} 447impl whiskers_ports::HasIcons for MemBackend { 448 fn icon_vectors(&self) -> &whiskers_ports::IconVectors { 449 &self.icons 450 } 451} 452impl HasEmbedder for MemBackend { 453 type Embedder = ScriptedEmbedder; 454 fn embedder(&self) -> &ScriptedEmbedder { 455 &self.embedder 456 } 457} 458impl HasVoice for MemBackend { 459 type Voice = ScriptedVoice; 460 fn voice(&self) -> &ScriptedVoice { 461 &self.voice 462 } 463} 464impl HasThinker for MemBackend { 465 type Thinker = ScriptedThinker; 466 fn thinker(&self) -> &ScriptedThinker { 467 &self.thinker 468 } 469} 470impl HasClock for MemBackend { 471 type Clock = ManualClock; 472 fn clock(&self) -> &ManualClock { 473 &self.clock 474 } 475} 476 477impl MemBackend { 478 /// A backend whose clock reads `ms`. 479 pub fn at(ms: u64) -> Self { 480 let b = Self::default(); 481 b.clock.set(Millis::new(ms)); 482 b 483 } 484 485 pub fn now(&self) -> Millis { 486 self.clock.now() 487 } 488} 489 490/// The harness of the in-memory adapter: a fixture for every capability, so the whole suite runs on it 491/// (`Suite::complete(MemFixtures)`). A restart is the identity: what is in memory stays in memory, which is 492/// the most a process that keeps nothing on disk can promise. 493#[derive(Clone, Copy, Default)] 494pub struct MemFixtures; 495 496macro_rules! mem_stateful { 497 ($fixture:ident, $port:ident) => { 498 impl crate::suite::fixture::$fixture for MemFixtures { 499 type Port = $port; 500 fn fresh(&self) -> $port { 501 $port::default() 502 } 503 fn restart(&self, port: $port) -> $port { 504 port 505 } 506 } 507 }; 508} 509 510mem_stateful!(MemoryFixture, MemMemoryHub); 511mem_stateful!(HouseholdFixture, MemHouseholdHub); 512mem_stateful!(ChatFixture, MemChatHub); 513mem_stateful!(JournalFixture, MemJournal); 514mem_stateful!(PicturesFixture, MemPictures); 515mem_stateful!(AllowanceFixture, MemAllowance); 516 517impl crate::suite::fixture::ChatFaultFixture for MemFixtures { 518 type Port = MemChatHub; 519 fn fresh_refusing(&self) -> (MemChatHub, crate::suite::hubs::WriteSwitch) { 520 let hub = MemChatHub::default(); 521 let switch = hub.clone(); 522 (hub, Box::new(move |refuse| switch.refuse_writes(refuse))) 523 } 524 fn restart_refusing(&self, port: MemChatHub) -> MemChatHub { 525 port 526 } 527} 528 529impl crate::suite::fixture::JudgeFixture for MemFixtures { 530 type Port = ScriptedJudge; 531 fn without_credentials(&self) -> ScriptedJudge { 532 let judge = ScriptedJudge::default(); 533 *lock(&judge.verdict) = Err(JudgeError::NotConfigured(SecretName::JevKey)); 534 *lock(&judge.ranking) = Err(JudgeError::NotConfigured(SecretName::JevKey)); 535 *lock(&judge.icon) = Err(JudgeError::NotConfigured(SecretName::JevKey)); 536 judge 537 } 538} 539 540impl crate::suite::fixture::VoiceFixture for MemFixtures { 541 type Port = ScriptedVoice; 542 fn without_credentials(&self) -> ScriptedVoice { 543 let voice = ScriptedVoice::default(); 544 *lock(&voice.configured) = false; 545 voice 546 } 547} 548 549impl crate::suite::fixture::ClockFixture for MemFixtures { 550 type Port = ManualClock; 551 fn clock(&self) -> ManualClock { 552 // A manual clock that reads a plausible time (the law asks for one after 2020). 553 ManualClock::at(1_800_000_000_000) 554 } 555}