1use crate::types::ConversationId;
2use laser_wire::agent::{
3 AgentEnvelope, AgentId, AgentKind, RecordId, SessionPauseRequest, TaskState,
4};
5use laser_wire::dispatch::{
6 OPERATION_SESSION_CANCEL, OPERATION_SESSION_PAUSE, OPERATION_SESSION_RESUME,
7};
8use std::collections::HashMap;
9use std::sync::Mutex;
10use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub struct PendingControl {
18 pub pause_requested: bool,
20 pub cancel_requested: bool,
22 pub control_conflict: bool,
26}
27
28impl PendingControl {
29 fn apply(&mut self, action: ControlAction) {
30 match action {
31 ControlAction::Pause => self.pause_requested = true,
32 ControlAction::Resume => self.pause_requested = false,
33 ControlAction::Cancel => self.cancel_requested = true,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) enum ControlAction {
41 Pause,
42 Resume,
43 Cancel,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub(crate) struct ControlRequest {
49 pub(crate) action: ControlAction,
50 pub(crate) named: bool,
53 pub(crate) record: Option<RecordId>,
55}
56
57pub(crate) fn control_request(
62 envelope: &AgentEnvelope,
63 me: Option<&AgentId>,
64) -> Option<ControlRequest> {
65 if let (Some(target), Some(me)) = (&envelope.target, me)
66 && target != me
67 {
68 return None;
69 }
70 let action = match envelope.kind {
71 AgentKind::Command => match envelope.operation.as_deref()? {
72 OPERATION_SESSION_PAUSE => ControlAction::Pause,
73 OPERATION_SESSION_RESUME => ControlAction::Resume,
74 OPERATION_SESSION_CANCEL => ControlAction::Cancel,
75 _ => return None,
76 },
77 AgentKind::Status if envelope.task_state == Some(TaskState::Canceled) => {
78 ControlAction::Cancel
79 }
80 _ => return None,
81 };
82 let addressed = envelope.target.is_some() && me.is_some();
83 let listed = action == ControlAction::Pause
84 && me.is_some_and(|me| {
85 serde_json::from_slice::<SessionPauseRequest>(&envelope.body)
86 .is_ok_and(|request| request.participants.contains(me))
87 });
88 Some(ControlRequest {
89 action,
90 named: addressed || listed,
91 record: envelope.record,
92 })
93}
94
95pub(crate) type ControlPosition = (u32, u64);
97
98pub(crate) fn control_partition(conversation: ConversationId, partitions: u32) -> Option<u32> {
103 (partitions > 0).then(|| xxh32(conversation.to_string().as_bytes()) % partitions)
104}
105
106const PRIME_1: u32 = 0x9E37_79B1;
107const PRIME_2: u32 = 0x85EB_CA77;
108const PRIME_3: u32 = 0xC2B2_AE3D;
109const PRIME_4: u32 = 0x27D4_EB2F;
110const PRIME_5: u32 = 0x1656_67B1;
111
112fn xxh32(input: &[u8]) -> u32 {
114 let lane =
115 |at: usize| u32::from_le_bytes([input[at], input[at + 1], input[at + 2], input[at + 3]]);
116 let round = |acc: u32, value: u32| {
117 acc.wrapping_add(value.wrapping_mul(PRIME_2))
118 .rotate_left(13)
119 .wrapping_mul(PRIME_1)
120 };
121 let mut at = 0;
122 let mut hash = if input.len() >= 16 {
123 let mut acc = [
124 PRIME_1.wrapping_add(PRIME_2),
125 PRIME_2,
126 0,
127 0u32.wrapping_sub(PRIME_1),
128 ];
129 while at + 16 <= input.len() {
130 for (index, slot) in acc.iter_mut().enumerate() {
131 *slot = round(*slot, lane(at + index * 4));
132 }
133 at += 16;
134 }
135 acc[0]
136 .rotate_left(1)
137 .wrapping_add(acc[1].rotate_left(7))
138 .wrapping_add(acc[2].rotate_left(12))
139 .wrapping_add(acc[3].rotate_left(18))
140 } else {
141 PRIME_5
142 };
143 hash = hash.wrapping_add(input.len() as u32);
144 while at + 4 <= input.len() {
145 hash = hash
146 .wrapping_add(lane(at).wrapping_mul(PRIME_3))
147 .rotate_left(17)
148 .wrapping_mul(PRIME_4);
149 at += 4;
150 }
151 while at < input.len() {
152 hash = hash
153 .wrapping_add(u32::from(input[at]).wrapping_mul(PRIME_5))
154 .rotate_left(11)
155 .wrapping_mul(PRIME_1);
156 at += 1;
157 }
158 hash ^= hash >> 15;
159 hash = hash.wrapping_mul(PRIME_2);
160 hash ^= hash >> 13;
161 hash = hash.wrapping_mul(PRIME_3);
162 hash ^ (hash >> 16)
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub(crate) struct PlacedRequest {
168 pub(crate) at: ControlPosition,
169 pub(crate) named: bool,
170 pub(crate) record: Option<RecordId>,
171}
172
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub(crate) struct ControlState {
177 pub(crate) flags: PendingControl,
178 pub(crate) pause: Option<PlacedRequest>,
180 pub(crate) resume: Option<PlacedRequest>,
182}
183
184impl ControlState {
185 fn apply(&mut self, at: ControlPosition, request: ControlRequest) {
186 self.flags.apply(request.action);
187 let placed = PlacedRequest {
188 at,
189 named: request.named,
190 record: request.record,
191 };
192 match request.action {
193 ControlAction::Pause => self.pause = Some(placed),
194 ControlAction::Resume => {
195 self.pause = None;
196 self.resume = Some(placed);
197 }
198 ControlAction::Cancel => {}
199 }
200 }
201
202 pub(crate) fn paused(&self) -> Option<PlacedRequest> {
204 self.pause.filter(|_| self.flags.pause_requested)
205 }
206}
207
208#[derive(Default)]
215pub(crate) struct ControlBook {
216 entries: Mutex<HashMap<ConversationId, Entry>>,
217 live: AtomicBool,
220 loaded: AtomicBool,
223 partitions: AtomicU32,
225}
226
227#[derive(Default)]
228struct Entry {
229 state: ControlState,
230 last: Option<ControlPosition>,
231 folded: bool,
232 pending: Vec<(ControlPosition, ControlRequest)>,
233 conflict: bool,
235}
236
237impl Entry {
238 fn with_conflict(&self) -> ControlState {
239 let mut state = self.state;
240 state.flags.control_conflict |= self.conflict;
241 state
242 }
243
244 fn apply(&mut self, at: ControlPosition, request: ControlRequest) {
245 if self.last.is_some_and(|last| !newer(at, last)) {
246 return;
247 }
248 self.state.apply(at, request);
249 self.last = Some(at);
250 }
251}
252
253fn newer(at: ControlPosition, last: ControlPosition) -> bool {
256 at.0 != last.0 || at.1 > last.1
257}
258
259impl ControlBook {
260 pub(crate) fn set_partitions(&self, partitions: u32) {
263 self.partitions.store(partitions, Ordering::Release);
264 }
265
266 fn on_route(&self, conversation: ConversationId, partition: u32) -> bool {
269 control_partition(conversation, self.partitions.load(Ordering::Acquire))
270 .is_none_or(|route| route == partition)
271 }
272
273 pub(crate) fn reset(&self) {
276 self.lock().clear();
277 self.loaded.store(false, Ordering::Release);
278 }
279
280 pub(crate) fn set_live(&self, live: bool) {
282 self.live.store(live, Ordering::Release);
283 }
284
285 pub(crate) fn is_live(&self) -> bool {
287 self.live.load(Ordering::Acquire)
288 }
289
290 pub(crate) fn load(
294 &self,
295 records: impl IntoIterator<Item = (ConversationId, ControlPosition, ControlRequest)>,
296 ) {
297 let mut entries = self.lock();
298 for (conversation, at, request) in records {
299 let on_route = self.on_route(conversation, at.0);
300 let entry = entries.entry(conversation).or_default();
301 entry.folded = true;
302 if on_route {
303 entry.apply(at, request);
304 } else {
305 entry.conflict = true;
306 }
307 }
308 self.loaded.store(true, Ordering::Release);
309 }
310
311 pub(crate) fn observe(
313 &self,
314 conversation: ConversationId,
315 at: ControlPosition,
316 request: ControlRequest,
317 ) {
318 let loaded = self.loaded.load(Ordering::Acquire);
319 let on_route = self.on_route(conversation, at.0);
320 let mut entries = self.lock();
321 let entry = entries.entry(conversation).or_default();
322 if !on_route {
323 entry.conflict = true;
324 } else if entry.folded || loaded {
325 entry.folded = true;
326 entry.apply(at, request);
327 } else {
328 entry.pending.push((at, request));
329 }
330 }
331
332 pub(crate) fn state(&self, conversation: ConversationId) -> Option<ControlState> {
335 if !self.is_live() {
336 return None;
337 }
338 let loaded = self.loaded.load(Ordering::Acquire);
339 match self.lock().get(&conversation) {
340 Some(entry) if entry.folded => Some(entry.with_conflict()),
341 Some(_) => None,
342 None => loaded.then(ControlState::default),
343 }
344 }
345
346 pub(crate) fn has_pause_history(&self) -> bool {
349 self.lock()
350 .values()
351 .any(|entry| entry.state.pause.is_some() || entry.state.resume.is_some())
352 }
353
354 pub(crate) fn paused_or_resumed(&self) -> Vec<ConversationId> {
356 self.lock()
357 .iter()
358 .filter(|(_, entry)| entry.state.pause.is_some() || entry.state.resume.is_some())
359 .map(|(conversation, _)| *conversation)
360 .collect()
361 }
362
363 pub(crate) fn install(
367 &self,
368 conversation: ConversationId,
369 state: ControlState,
370 last: Option<ControlPosition>,
371 ) -> ControlState {
372 let live = self.is_live();
373 let mut entries = self.lock();
374 let entry = entries.entry(conversation).or_default();
375 if entry.folded && live {
376 return entry.with_conflict();
377 }
378 let pending = std::mem::take(&mut entry.pending);
379 entry.state = state;
380 entry.last = last;
381 entry.folded = true;
382 for (at, request) in pending {
383 entry.apply(at, request);
384 }
385 entry.with_conflict()
386 }
387
388 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<ConversationId, Entry>> {
389 self.entries
390 .lock()
391 .unwrap_or_else(std::sync::PoisonError::into_inner)
392 }
393}
394
395pub(crate) fn fold(
399 records: impl IntoIterator<Item = (ControlPosition, ControlRequest)>,
400 route: Option<u32>,
401) -> (ControlState, Option<ControlPosition>) {
402 let mut entry = Entry::default();
403 for (at, request) in records {
404 if route.is_none_or(|route| route == at.0) {
405 entry.apply(at, request);
406 } else {
407 entry.conflict = true;
408 }
409 }
410 (entry.with_conflict(), entry.last)
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn given_known_inputs_when_hashed_then_should_match_the_xxh32_reference() {
419 assert_eq!(xxh32(b""), 0x02CC_5D05);
420 assert_eq!(xxh32(b"abc"), 0x32D1_53FF);
421 assert_eq!(
422 xxh32(b"Nobody inspects the spammish repetition"),
423 0xE229_3B2F
424 );
425 }
426
427 #[test]
428 fn given_a_request_off_the_control_route_when_recorded_then_should_flag_and_never_apply() {
429 let session: ConversationId = "01KWM3K3XEP3NP5TN850J17YBP".parse().unwrap();
430 let route = control_partition(session, 2).expect("a partition count");
431 let book = ControlBook::default();
432 book.set_partitions(2);
433 book.load([
434 (session, (route, 1), request(ControlAction::Pause)),
435 (session, (1 - route, 2), request(ControlAction::Cancel)),
436 ]);
437 book.set_live(true);
438 book.observe(session, (1 - route, 3), request(ControlAction::Resume));
439 let state = book.state(session).expect("folded");
440 assert!(state.flags.pause_requested, "the routed pause applies");
441 assert!(
442 !state.flags.cancel_requested,
443 "the misrouted cancel never applies"
444 );
445 assert!(state.flags.control_conflict);
446
447 let (folded, _) = fold(
448 [
449 ((1 - route, 5), request(ControlAction::Cancel)),
450 ((route, 6), request(ControlAction::Pause)),
451 ],
452 Some(route),
453 );
454 assert_eq!(
455 folded.flags, state.flags,
456 "the log fold agrees with the book"
457 );
458
459 book.reset();
460 assert_eq!(
461 book.state(session),
462 None,
463 "a reset book folds again from the log"
464 );
465 }
466 use laser_wire::agent::CorrelationId;
467
468 fn command(operation: &str, target: Option<&str>) -> AgentEnvelope {
469 let envelope = AgentEnvelope::command(
470 RecordId::from_u128(1),
471 laser_wire::agent::ConversationId::from_u128(2),
472 "operator".parse().expect("operator parses"),
473 CorrelationId::from_u128(3),
474 b"{}".to_vec(),
475 )
476 .with_operation(operation);
477 match target {
478 Some(target) => envelope.with_target(target.parse().expect("target parses")),
479 None => envelope,
480 }
481 }
482
483 fn flags(book: &ControlBook, session: ConversationId) -> Option<PendingControl> {
484 book.state(session).map(|state| state.flags)
485 }
486
487 fn request(action: ControlAction) -> ControlRequest {
488 ControlRequest {
489 action,
490 named: false,
491 record: None,
492 }
493 }
494
495 #[test]
496 fn given_control_commands_when_classified_then_should_map_each_request_and_skip_other_agents() {
497 let me: AgentId = "worker".parse().expect("worker parses");
498 let action = |envelope: &AgentEnvelope, me: Option<&AgentId>| {
499 control_request(envelope, me).map(|request| request.action)
500 };
501 assert_eq!(
502 action(&command(OPERATION_SESSION_PAUSE, Some("worker")), Some(&me)),
503 Some(ControlAction::Pause)
504 );
505 assert_eq!(
506 action(&command(OPERATION_SESSION_RESUME, None), Some(&me)),
507 Some(ControlAction::Resume)
508 );
509 assert_eq!(
510 action(&command(OPERATION_SESSION_CANCEL, None), None),
511 Some(ControlAction::Cancel)
512 );
513 assert_eq!(
514 action(
515 &command(OPERATION_SESSION_CANCEL, Some("critic")),
516 Some(&me)
517 ),
518 None
519 );
520 assert_eq!(action(&command("chat", None), Some(&me)), None);
521 }
522
523 #[test]
524 fn given_a_pause_with_participants_when_classified_then_should_name_only_listed_agents() {
525 let me: AgentId = "worker".parse().expect("worker parses");
526 let other: AgentId = "critic".parse().expect("critic parses");
527 let mut pause = command(OPERATION_SESSION_PAUSE, None);
528 pause.body = br#"{"participants":["worker"]}"#.to_vec();
529 let named = control_request(&pause, Some(&me)).expect("the pause applies");
530 assert!(named.named);
531 assert_eq!(named.record, Some(RecordId::from_u128(1)));
532 let unnamed = control_request(&pause, Some(&other)).expect("the pause still applies");
533 assert!(!unnamed.named, "an unlisted agent still pauses");
534 let addressed =
535 control_request(&command(OPERATION_SESSION_PAUSE, Some("worker")), Some(&me))
536 .expect("the pause applies");
537 assert!(addressed.named);
538 let broadcast = control_request(&command(OPERATION_SESSION_PAUSE, None), Some(&me))
539 .expect("the pause applies");
540 assert!(!broadcast.named);
541 }
542
543 #[test]
544 fn given_pause_resume_and_cancel_when_folded_then_should_keep_only_the_latest_pause_state() {
545 let (state, last) = fold(
546 [
547 ((0, 1), request(ControlAction::Pause)),
548 ((0, 2), request(ControlAction::Resume)),
549 ((0, 3), request(ControlAction::Cancel)),
550 ],
551 None,
552 );
553 assert_eq!(
554 state.flags,
555 PendingControl {
556 pause_requested: false,
557 cancel_requested: true,
558 control_conflict: false,
559 }
560 );
561 assert_eq!(state.paused(), None);
562 assert_eq!(state.resume.map(|resume| resume.at), Some((0, 2)));
563 assert_eq!(last, Some((0, 3)));
564 }
565
566 #[test]
567 fn given_live_requests_before_the_fold_when_installed_then_should_replay_only_newer_ones() {
568 let book = ControlBook::default();
569 book.set_live(true);
570 let session = ConversationId::new();
571 book.observe(session, (0, 4), request(ControlAction::Pause));
572 book.observe(session, (0, 6), request(ControlAction::Cancel));
573 assert_eq!(flags(&book, session), None, "unfolded until the first read");
574 let (state, last) = fold(
575 [
576 ((0, 4), request(ControlAction::Pause)),
577 ((0, 5), request(ControlAction::Resume)),
578 ],
579 None,
580 );
581 let installed = book.install(session, state, last);
582 assert_eq!(
583 installed.flags,
584 PendingControl {
585 pause_requested: false,
586 cancel_requested: true,
587 control_conflict: false,
588 }
589 );
590 book.observe(session, (0, 7), request(ControlAction::Pause));
591 assert_eq!(
592 flags(&book, session),
593 Some(PendingControl {
594 pause_requested: true,
595 cancel_requested: true,
596 control_conflict: false,
597 })
598 );
599 assert_eq!(
600 book.state(session)
601 .and_then(|state| state.paused())
602 .map(|pause| pause.at),
603 Some((0, 7))
604 );
605 book.set_live(false);
606 assert_eq!(
607 flags(&book, session),
608 None,
609 "a stopped follower forces a fold"
610 );
611 let refolded = book.install(session, ControlState::default(), None);
612 assert_eq!(
613 refolded,
614 ControlState::default(),
615 "a stopped follower trusts the fold"
616 );
617 }
618
619 #[test]
620 fn given_a_loaded_book_when_reading_an_unseen_session_then_should_report_no_requests() {
621 let book = ControlBook::default();
622 let paused = ConversationId::new();
623 book.load([(paused, (1, 3), request(ControlAction::Pause))]);
624 book.set_live(true);
625 assert!(book.has_pause_history());
626 assert_eq!(book.paused_or_resumed(), vec![paused]);
627 assert_eq!(
628 flags(&book, ConversationId::new()),
629 Some(PendingControl::default())
630 );
631 let later = ConversationId::new();
632 book.observe(later, (2, 9), request(ControlAction::Cancel));
633 assert_eq!(
634 flags(&book, later),
635 Some(PendingControl {
636 pause_requested: false,
637 cancel_requested: true,
638 control_conflict: false,
639 })
640 );
641 }
642}