This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-events / src / lib.rs
34 kB 1129 lines
1use std::collections::{BTreeSet, HashMap, VecDeque}; 2use std::net::IpAddr; 3use std::sync::{Arc, Mutex}; 4 5use serde::ser::SerializeMap; 6use serde::{Serialize, Serializer}; 7use serde_json::Value; 8use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch}; 9 10use knot_runtime::{Clock, UnixMicros}; 11use knot_types::{ 12 AccountDid, ChangedFiles, Email, LanguageBytes, LanguageName, ObjectFormat, Oid, OwnerDid, 13 PushOptions, RefName, RefTransition, RepoDid, RepoPath, Tid, 14}; 15 16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] 17#[serde(transparent)] 18pub struct EventCursor(i64); 19 20impl EventCursor { 21 pub const START: Self = Self(0); 22 23 pub fn new(nanos: i64) -> Self { 24 Self(nanos) 25 } 26 27 pub fn get(self) -> i64 { 28 self.0 29 } 30 31 fn from_unix_micros(micros: UnixMicros) -> Self { 32 Self((micros.get() as i64).saturating_mul(1_000)) 33 } 34} 35 36pub trait Publish: Serialize { 37 const NSID: &'static str; 38} 39 40#[derive(Debug, Clone, Serialize)] 41pub struct Event { 42 pub rkey: Tid, 43 pub nsid: &'static str, 44 #[serde(rename = "event")] 45 pub payload: Value, 46 pub created: EventCursor, 47} 48 49// `sh.tangled.git.refUpdate` requires ref, oldSha and newSha, 50// so a record about no ref at all sends "" for the three rather than `null`. 51#[derive(Debug, Clone)] 52enum RefChange { 53 Absent, 54 Applied { 55 ref_name: RefName, 56 old_sha: Oid, 57 new_sha: Oid, 58 }, 59} 60 61impl Serialize for RefChange { 62 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { 63 let mut map = serializer.serialize_map(Some(3))?; 64 match self { 65 Self::Absent => { 66 map.serialize_entry("ref", "")?; 67 map.serialize_entry("oldSha", "")?; 68 map.serialize_entry("newSha", "")?; 69 } 70 Self::Applied { 71 ref_name, 72 old_sha, 73 new_sha, 74 } => { 75 map.serialize_entry("ref", ref_name.as_str())?; 76 map.serialize_entry("oldSha", &old_sha.to_hex())?; 77 map.serialize_entry("newSha", &new_sha.to_hex())?; 78 } 79 } 80 map.end() 81 } 82} 83 84#[derive(Debug, Clone, Serialize)] 85pub struct GitRefUpdate { 86 #[serde(rename = "$type")] 87 record_type: &'static str, 88 #[serde(rename = "changedFiles", skip_serializing_if = "Vec::is_empty")] 89 changed_files: Vec<RepoPath>, 90 #[serde(rename = "committerDid")] 91 committer_did: AccountDid, 92 meta: Option<RefUpdateMeta>, 93 #[serde(rename = "ownerDid", skip_serializing_if = "Option::is_none")] 94 owner_did: Option<OwnerDid>, 95 #[serde(rename = "pushOptions", skip_serializing_if = "PushOptions::is_empty")] 96 push_options: PushOptions, 97 #[serde(flatten)] 98 change: RefChange, 99 repo: RepoDid, 100} 101 102impl GitRefUpdate { 103 pub fn new(repo: RepoDid, owner: Option<OwnerDid>, committer: AccountDid) -> Self { 104 Self { 105 record_type: Self::NSID, 106 changed_files: Vec::new(), 107 committer_did: committer, 108 meta: None, 109 owner_did: owner, 110 push_options: PushOptions::default(), 111 change: RefChange::Absent, 112 repo, 113 } 114 } 115 116 pub fn on_ref( 117 mut self, 118 ref_name: RefName, 119 transition: RefTransition, 120 format: ObjectFormat, 121 ) -> Self { 122 self.change = RefChange::Applied { 123 ref_name, 124 old_sha: transition.old_oid().unwrap_or_else(|| format.null_oid()), 125 new_sha: transition.new_oid().unwrap_or_else(|| format.null_oid()), 126 }; 127 self 128 } 129 130 pub fn with_changed_files(mut self, changed: ChangedFiles) -> Self { 131 self.changed_files = changed.into_paths(); 132 self 133 } 134 135 pub fn with_push_options(mut self, options: &PushOptions) -> Self { 136 self.push_options = options.clone(); 137 self 138 } 139 140 pub fn with_meta(mut self, meta: RefUpdateMeta) -> Self { 141 self.meta = Some(meta); 142 self 143 } 144} 145 146impl Publish for GitRefUpdate { 147 const NSID: &'static str = "sh.tangled.git.refUpdate"; 148} 149 150#[derive(Debug, Clone, Serialize)] 151pub struct RefUpdateMeta { 152 #[serde(rename = "isDefaultRef")] 153 is_default_ref: bool, 154 #[serde(rename = "commitCount")] 155 commit_count: CommitCountBreakdown, 156 #[serde(rename = "langBreakdown", skip_serializing_if = "Option::is_none")] 157 lang_breakdown: Option<LangBreakdown>, 158} 159 160impl RefUpdateMeta { 161 pub fn new( 162 is_default_ref: bool, 163 by_email: Vec<EmailCommitCount>, 164 languages: Vec<LanguageSize>, 165 ) -> Self { 166 Self { 167 is_default_ref, 168 commit_count: CommitCountBreakdown { 169 by_email: (!by_email.is_empty()).then_some(by_email), 170 }, 171 lang_breakdown: (!languages.is_empty()).then_some(LangBreakdown { 172 inputs: Some(languages), 173 }), 174 } 175 } 176} 177 178#[derive(Debug, Clone, Serialize)] 179struct CommitCountBreakdown { 180 #[serde(rename = "byEmail", skip_serializing_if = "Option::is_none")] 181 by_email: Option<Vec<EmailCommitCount>>, 182} 183 184#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] 185#[serde(transparent)] 186pub struct CommitCount(u64); 187 188impl CommitCount { 189 pub const fn new(count: u64) -> Self { 190 Self(count) 191 } 192 193 pub const fn get(self) -> u64 { 194 self.0 195 } 196 197 pub const fn succ(self) -> Self { 198 Self(self.0.saturating_add(1)) 199 } 200} 201 202#[derive(Debug, Clone, Serialize)] 203pub struct EmailCommitCount { 204 email: Email, 205 count: CommitCount, 206} 207 208impl EmailCommitCount { 209 pub fn new(email: Email, count: CommitCount) -> Self { 210 Self { email, count } 211 } 212} 213 214#[derive(Debug, Clone, Serialize)] 215struct LangBreakdown { 216 #[serde(skip_serializing_if = "Option::is_none")] 217 inputs: Option<Vec<LanguageSize>>, 218} 219 220#[derive(Debug, Clone, Serialize)] 221pub struct LanguageSize { 222 lang: LanguageName, 223 size: LanguageBytes, 224} 225 226impl LanguageSize { 227 pub fn new(lang: LanguageName, size: LanguageBytes) -> Self { 228 Self { lang, size } 229 } 230} 231 232#[derive(Debug, Clone, Copy, Serialize)] 233#[serde(rename_all = "lowercase")] 234enum AclOp { 235 Add, 236 Remove, 237} 238 239#[derive(Debug, Clone, Serialize)] 240pub struct KnotMemberUpdate { 241 op: AclOp, 242 subject: AccountDid, 243} 244 245impl KnotMemberUpdate { 246 pub fn added(subject: AccountDid) -> Self { 247 Self { 248 op: AclOp::Add, 249 subject, 250 } 251 } 252 253 pub fn removed(subject: AccountDid) -> Self { 254 Self { 255 op: AclOp::Remove, 256 subject, 257 } 258 } 259} 260 261impl Publish for KnotMemberUpdate { 262 const NSID: &'static str = "sh.tangled.knot.memberUpdate"; 263} 264 265#[derive(Debug, Clone, Serialize)] 266pub struct RepoCollaboratorUpdate { 267 op: AclOp, 268 subject: AccountDid, 269 repo: RepoDid, 270} 271 272impl RepoCollaboratorUpdate { 273 pub fn added(subject: AccountDid, repo: RepoDid) -> Self { 274 Self { 275 op: AclOp::Add, 276 subject, 277 repo, 278 } 279 } 280 281 pub fn removed(subject: AccountDid, repo: RepoDid) -> Self { 282 Self { 283 op: AclOp::Remove, 284 subject, 285 repo, 286 } 287 } 288} 289 290impl Publish for RepoCollaboratorUpdate { 291 const NSID: &'static str = "sh.tangled.repo.collaboratorUpdate"; 292} 293 294#[derive(Debug, Clone, Copy, PartialEq, Eq)] 295pub struct ReplayEvents(std::num::NonZeroUsize); 296 297impl ReplayEvents { 298 pub fn new(value: usize) -> Option<Self> { 299 std::num::NonZeroUsize::new(value).map(Self) 300 } 301 302 pub fn get(self) -> usize { 303 self.0.get() 304 } 305} 306 307#[derive(Debug, Clone, Copy, PartialEq, Eq)] 308pub struct ReplayBytes(std::num::NonZeroUsize); 309 310impl ReplayBytes { 311 pub fn new(value: usize) -> Option<Self> { 312 std::num::NonZeroUsize::new(value).map(Self) 313 } 314 315 pub fn get(self) -> usize { 316 self.0.get() 317 } 318} 319 320#[derive(Debug, Clone, Copy, PartialEq, Eq)] 321pub struct ReplayBounds { 322 events: ReplayEvents, 323 bytes: ReplayBytes, 324} 325 326impl ReplayBounds { 327 pub fn new(events: ReplayEvents, bytes: ReplayBytes) -> Self { 328 Self { events, bytes } 329 } 330} 331 332#[derive(Debug, Clone, Copy, PartialEq, Eq)] 333pub enum BatchEnd { 334 CaughtUp, 335 Bounded, 336} 337 338pub struct Replayed { 339 pub events: Vec<Arc<Event>>, 340 pub end: BatchEnd, 341} 342 343struct Entry { 344 event: Arc<Event>, 345 bytes: usize, 346} 347 348struct Ring { 349 entries: VecDeque<Entry>, 350 bytes: usize, 351 last_micros: UnixMicros, 352 pending: BTreeSet<EventCursor>, 353} 354 355struct Inner { 356 bounds: ReplayBounds, 357 ring: Mutex<Ring>, 358 head: watch::Sender<EventCursor>, 359} 360 361impl Inner { 362 fn lock(&self) -> std::sync::MutexGuard<'_, Ring> { 363 self.ring 364 .lock() 365 .unwrap_or_else(|poisoned| poisoned.into_inner()) 366 } 367 368 // leave that guiness be, boy! it needs to 369 fn settle(&self, ring: std::sync::MutexGuard<'_, Ring>) { 370 let head = stable_head(&ring); 371 drop(ring); 372 self.head.send_if_modified(|current| { 373 let changed = *current != head; 374 *current = head; 375 changed 376 }); 377 } 378} 379 380fn stable_head(ring: &Ring) -> EventCursor { 381 let stable = match ring.pending.iter().next().copied() { 382 Some(horizon) => ring 383 .entries 384 .partition_point(|entry| entry.event.created < horizon), 385 None => ring.entries.len(), 386 }; 387 stable 388 .checked_sub(1) 389 .and_then(|index| ring.entries.get(index)) 390 .map(|entry| entry.event.created) 391 .unwrap_or(EventCursor::START) 392} 393 394fn value_bytes(value: &Value) -> usize { 395 let node = std::mem::size_of::<Value>(); 396 match value { 397 Value::Null | Value::Bool(_) | Value::Number(_) => node, 398 Value::String(text) => node + text.len(), 399 Value::Array(items) => node + items.iter().map(value_bytes).sum::<usize>(), 400 Value::Object(fields) => { 401 node + fields 402 .iter() 403 .map(|(key, field)| key.len() + value_bytes(field)) 404 .sum::<usize>() 405 } 406 } 407} 408 409fn insert_sorted(ring: &mut Ring, event: Event, bounds: ReplayBounds) { 410 let bytes = std::mem::size_of::<Event>() + value_bytes(&event.payload); 411 let position = ring 412 .entries 413 .partition_point(|existing| existing.event.created < event.created); 414 ring.entries.insert( 415 position, 416 Entry { 417 event: Arc::new(event), 418 bytes, 419 }, 420 ); 421 ring.bytes += bytes; 422 evict_oldest(ring, bounds); 423} 424 425fn evict_oldest(ring: &mut Ring, bounds: ReplayBounds) { 426 let over = ring.entries.len() > bounds.events.get() 427 || (ring.bytes > bounds.bytes.get() && ring.entries.len() > 1); 428 if let Some(evicted) = over.then(|| ring.entries.pop_front()).flatten() { 429 ring.bytes -= evicted.bytes; 430 evict_oldest(ring, bounds); 431 } 432} 433 434pub struct EventLog<C> { 435 clock: C, 436 inner: Arc<Inner>, 437} 438 439impl<C: Clock> EventLog<C> { 440 pub fn new(clock: C, bounds: ReplayBounds) -> Self { 441 Self { 442 clock, 443 inner: Arc::new(Inner { 444 bounds, 445 ring: Mutex::new(Ring { 446 entries: VecDeque::new(), 447 bytes: 0, 448 last_micros: UnixMicros::new(0), 449 pending: BTreeSet::new(), 450 }), 451 head: watch::Sender::new(EventCursor::START), 452 }), 453 } 454 } 455 456 fn next_cursor(&self, ring: &mut Ring) -> (UnixMicros, EventCursor) { 457 let micros = self.clock.now_unix_micros().max(ring.last_micros.next()); 458 ring.last_micros = micros; 459 (micros, EventCursor::from_unix_micros(micros)) 460 } 461 462 pub fn publish<P: Publish>(&self, payload: &P) -> EventCursor { 463 let payload = serde_json::to_value(payload).expect("event payload serializes to JSON"); 464 let mut ring = self.inner.lock(); 465 let (micros, created) = self.next_cursor(&mut ring); 466 insert_sorted( 467 &mut ring, 468 Event { 469 rkey: Tid::from_time(micros.get(), 0), 470 nsid: P::NSID, 471 payload, 472 created, 473 }, 474 self.inner.bounds, 475 ); 476 self.inner.settle(ring); 477 created 478 } 479 480 pub fn reserve(&self) -> Reservation { 481 let mut ring = self.inner.lock(); 482 let (micros, cursor) = self.next_cursor(&mut ring); 483 ring.pending.insert(cursor); 484 drop(ring); 485 Reservation { 486 inner: Arc::clone(&self.inner), 487 cursor, 488 micros, 489 fulfilled: false, 490 } 491 } 492 493 pub fn replay(&self, after: EventCursor, bounds: ReplayBounds) -> Replayed { 494 let ring = self.inner.lock(); 495 // The corresponding read side guarantee of `reserve`. 496 let horizon = ring.pending.iter().next().copied(); 497 let visible = |entry: &&Entry| { 498 entry.event.created > after 499 && horizon.is_none_or(|horizon| entry.event.created < horizon) 500 }; 501 let events: Vec<Arc<Event>> = ring 502 .entries 503 .iter() 504 .filter(visible) 505 .take(bounds.events.get()) 506 // Why the first event gets to ignore the byte bound? 507 // Imagine a consumer whose next event is by itself wider 508 // than the entire bound, right - 509 // every batch it requests would come back empty, 510 // its cursor would never advance, 511 // it would ask again, repeat. 512 // Sending that one event alone over the bound 513 // is the only way. 514 .scan(0usize, |spent, entry| { 515 let first = *spent == 0; 516 *spent += entry.bytes; 517 (first || *spent <= bounds.bytes.get()).then(|| Arc::clone(&entry.event)) 518 }) 519 .collect(); 520 let end = match ring.entries.iter().filter(visible).nth(events.len()) { 521 Some(_) => BatchEnd::Bounded, 522 None => BatchEnd::CaughtUp, 523 }; 524 Replayed { events, end } 525 } 526 527 pub fn subscribe(&self) -> watch::Receiver<EventCursor> { 528 self.inner.head.subscribe() 529 } 530} 531 532pub struct Reservation { 533 inner: Arc<Inner>, 534 cursor: EventCursor, 535 micros: UnixMicros, 536 fulfilled: bool, 537} 538 539impl Reservation { 540 pub fn cursor(&self) -> EventCursor { 541 self.cursor 542 } 543 544 pub fn fulfill<P: Publish>(mut self, payload: &P) { 545 let payload = serde_json::to_value(payload).expect("event payload serializes to JSON"); 546 let mut ring = self.inner.lock(); 547 insert_sorted( 548 &mut ring, 549 Event { 550 rkey: Tid::from_time(self.micros.get(), 0), 551 nsid: P::NSID, 552 payload, 553 created: self.cursor, 554 }, 555 self.inner.bounds, 556 ); 557 ring.pending.remove(&self.cursor); 558 self.inner.settle(ring); 559 self.fulfilled = true; 560 } 561} 562 563impl Drop for Reservation { 564 fn drop(&mut self) { 565 if self.fulfilled { 566 return; 567 } 568 let mut ring = self.inner.lock(); 569 ring.pending.remove(&self.cursor); 570 self.inner.settle(ring); 571 } 572} 573 574knot_types::scalar_newtype! { 575 pub struct GlobalSubscriberLimit(usize); 576 pub struct PerPeerSubscriberLimit(usize); 577} 578 579pub struct SubscriberGate { 580 global: Arc<Semaphore>, 581 per_peer_max: usize, 582 peers: Mutex<HashMap<IpAddr, usize>>, 583} 584 585impl SubscriberGate { 586 pub fn new(global_max: GlobalSubscriberLimit, per_peer_max: PerPeerSubscriberLimit) -> Self { 587 Self { 588 global: Arc::new(Semaphore::new(global_max.get().max(1))), 589 per_peer_max: per_peer_max.get().max(1), 590 peers: Mutex::new(HashMap::new()), 591 } 592 } 593 594 pub fn try_admit(self: &Arc<Self>, peer: IpAddr) -> Option<SubscriberPermit> { 595 let global = Arc::clone(&self.global).try_acquire_owned().ok()?; 596 let mut peers = self 597 .peers 598 .lock() 599 .unwrap_or_else(|poisoned| poisoned.into_inner()); 600 let current = peers.get(&peer).copied().unwrap_or(0); 601 if current >= self.per_peer_max { 602 return None; 603 } 604 peers.insert(peer, current + 1); 605 Some(SubscriberPermit { 606 _global: global, 607 gate: Arc::clone(self), 608 peer, 609 }) 610 } 611} 612 613pub struct SubscriberPermit { 614 _global: OwnedSemaphorePermit, 615 gate: Arc<SubscriberGate>, 616 peer: IpAddr, 617} 618 619impl Drop for SubscriberPermit { 620 fn drop(&mut self) { 621 let mut peers = self 622 .gate 623 .peers 624 .lock() 625 .unwrap_or_else(|poisoned| poisoned.into_inner()); 626 if let Some(count) = peers.get_mut(&self.peer) { 627 *count -= 1; 628 if *count == 0 { 629 peers.remove(&self.peer); 630 } 631 } 632 } 633} 634 635#[cfg(test)] 636mod tests { 637 use super::*; 638 639 use knot_runtime::{ManualClock, UnixMicros}; 640 641 fn bounds(events: usize, bytes: usize) -> ReplayBounds { 642 ReplayBounds::new( 643 ReplayEvents::new(events).unwrap(), 644 ReplayBytes::new(bytes).unwrap(), 645 ) 646 } 647 648 fn log(capacity: usize) -> EventLog<ManualClock> { 649 EventLog::new( 650 ManualClock::new(UnixMicros::new(1_700_000_000_000_000)), 651 bounds(capacity, 1 << 20), 652 ) 653 } 654 655 fn replay(log: &EventLog<ManualClock>, after: EventCursor, limit: usize) -> Vec<Arc<Event>> { 656 log.replay(after, bounds(limit, 1 << 30)).events 657 } 658 659 fn update() -> GitRefUpdate { 660 GitRefUpdate::new( 661 RepoDid::new("did:plc:limpet").unwrap(), 662 Some(OwnerDid::new("did:web:olaren.dev").unwrap()), 663 AccountDid::new("did:plc:nel").unwrap(), 664 ) 665 } 666 667 fn wire<P: Publish>(log: &EventLog<ManualClock>, payload: &P) -> serde_json::Value { 668 log.publish(payload); 669 serde_json::to_value(&*replay(log, EventCursor::START, 1).remove(0)).unwrap() 670 } 671 672 #[test] 673 fn publish_nsids_are_valid_type_names() { 674 [ 675 GitRefUpdate::NSID, 676 KnotMemberUpdate::NSID, 677 RepoCollaboratorUpdate::NSID, 678 ] 679 .iter() 680 .for_each(|nsid| { 681 assert!(knot_types::TypeName::new(*nsid).is_ok(), "{nsid}"); 682 }); 683 } 684 685 #[test] 686 fn a_frozen_clock_still_yields_strictly_increasing_cursors_and_distinct_rkeys() { 687 let log = log(8); 688 let cursors: Vec<_> = (0..3).map(|_| log.publish(&update())).collect(); 689 assert!(cursors.windows(2).all(|pair| pair[0] < pair[1])); 690 let events = replay(&log, EventCursor::START, 8); 691 let rkeys: std::collections::BTreeSet<_> = events 692 .iter() 693 .map(|event| event.rkey.as_str().to_string()) 694 .collect(); 695 assert_eq!(rkeys.len(), 3); 696 } 697 698 #[test] 699 fn the_ring_evicts_the_oldest_event_past_capacity() { 700 let log = log(2); 701 let first = log.publish(&update()); 702 log.publish(&update()); 703 log.publish(&update()); 704 let replayed = replay(&log, EventCursor::START, 8); 705 assert_eq!(replayed.len(), 2); 706 assert!(replayed.iter().all(|event| event.created > first)); 707 } 708 709 #[test] 710 fn a_wide_event_evicts_by_bytes_long_before_the_ring_fills() { 711 let wide = |count: usize| { 712 update().with_changed_files(fill_changed( 713 (0..count).map(|index| format!("crates/knot-events/src/f{index}.rs")), 714 )) 715 }; 716 let log = EventLog::new( 717 ManualClock::new(UnixMicros::new(1_700_000_000_000_000)), 718 bounds(1_024, 64 * 1_024), 719 ); 720 (0..16).for_each(|_| { 721 log.publish(&wide(512)); 722 }); 723 let replayed = replay(&log, EventCursor::START, 1_024); 724 assert!( 725 (1..16).contains(&replayed.len()), 726 "the byte maximum evicts before the event maximum does: {}", 727 replayed.len() 728 ); 729 730 let one = EventLog::new( 731 ManualClock::new(UnixMicros::new(1_700_000_000_000_000)), 732 bounds(1_024, 1), 733 ); 734 let only = one.publish(&wide(512)); 735 assert_eq!( 736 replay(&one, EventCursor::START, 8) 737 .iter() 738 .map(|event| event.created) 739 .collect::<Vec<EventCursor>>(), 740 vec![only], 741 "the ring keeps the one event wider than the whole byte maximum" 742 ); 743 } 744 745 fn fill_changed(paths: impl Iterator<Item = String>) -> ChangedFiles { 746 let mut budget = knot_types::ChangedFilesBudget::new(); 747 let _ = paths.into_iter().try_for_each(|path| { 748 budget.admit(knot_types::RepoPath::new(path).expect("test path is well-formed")) 749 }); 750 budget.finish() 751 } 752 753 #[test] 754 fn replay_honors_the_cursor_and_the_limit() { 755 let log = log(8); 756 let cursors: Vec<_> = (0..4).map(|_| log.publish(&update())).collect(); 757 let after_second = replay(&log, cursors[1], 8); 758 assert_eq!( 759 after_second 760 .iter() 761 .map(|event| event.created) 762 .collect::<Vec<_>>(), 763 cursors[2..].to_vec() 764 ); 765 assert_eq!(replay(&log, EventCursor::START, 2).len(), 2); 766 assert!(replay(&log, cursors[3], 8).is_empty()); 767 } 768 769 #[test] 770 fn a_replay_batch_stops_at_the_byte_maximum_and_reports_whether_more_remains() { 771 let log = EventLog::new( 772 ManualClock::new(UnixMicros::new(1_700_000_000_000_000)), 773 bounds(1_024, 1 << 20), 774 ); 775 assert_eq!( 776 log.replay(EventCursor::START, bounds(8, 1 << 20)).end, 777 BatchEnd::CaughtUp, 778 "an empty ring has nothing left to send" 779 ); 780 let wide = update().with_changed_files(fill_changed( 781 (0..512).map(|index| format!("crates/knot-events/src/f{index}.rs")), 782 )); 783 let cursors: Vec<EventCursor> = (0..8).map(|_| log.publish(&wide)).collect(); 784 785 let batch = log.replay(EventCursor::START, bounds(1_024, 16 * 1_024)); 786 assert!( 787 (1..8).contains(&batch.events.len()), 788 "the byte maximum stops the batch before the event maximum does: {}", 789 batch.events.len() 790 ); 791 assert_eq!(batch.end, BatchEnd::Bounded); 792 793 let rest = log.replay( 794 batch.events.last().expect("the batch is nonempty").created, 795 bounds(1_024, 1 << 30), 796 ); 797 assert_eq!(rest.end, BatchEnd::CaughtUp); 798 assert_eq!( 799 batch.events.len() + rest.events.len(), 800 cursors.len(), 801 "the two batches together are every event, with none repeated or skipped" 802 ); 803 let head = log.replay(cursors[7], bounds(8, 1 << 20)); 804 assert!(head.events.is_empty() && head.end == BatchEnd::CaughtUp); 805 806 let single = log.replay(EventCursor::START, bounds(1_024, 1)); 807 assert_eq!( 808 single.events.len(), 809 1, 810 "an event wider than the whole batch maximum is sent alone" 811 ); 812 assert_eq!(single.end, BatchEnd::Bounded); 813 } 814 815 #[test] 816 fn a_subscriber_observes_the_head_advance() { 817 let log = log(8); 818 let mut head = log.subscribe(); 819 assert_eq!(*head.borrow_and_update(), EventCursor::START); 820 let created = log.publish(&update()); 821 assert!(head.has_changed().unwrap()); 822 assert_eq!(*head.borrow_and_update(), created); 823 } 824 825 #[test] 826 fn the_wire_event_matches_the_eventstream_shape() { 827 let wire = wire(&log(8), &update()); 828 assert_eq!(wire["nsid"], "sh.tangled.git.refUpdate"); 829 assert_eq!(wire["created"].as_i64().unwrap() % 1_000, 0); 830 assert_eq!(wire["rkey"].as_str().unwrap().len(), 13); 831 let payload = &wire["event"]; 832 assert_eq!(payload["$type"], "sh.tangled.git.refUpdate"); 833 assert_eq!(payload["committerDid"], "did:plc:nel"); 834 assert_eq!(payload["ownerDid"], "did:web:olaren.dev"); 835 assert_eq!(payload["repo"], "did:plc:limpet"); 836 assert_eq!(payload["meta"], serde_json::Value::Null); 837 assert_eq!(payload["ref"], ""); 838 assert_eq!( 839 payload["oldSha"], "", 840 "a record about no ref sends the empty sha" 841 ); 842 assert_eq!(payload["newSha"], ""); 843 } 844 845 #[test] 846 fn an_absent_sha_of_a_transition_is_the_null_oid_of_the_repo_object_format() { 847 let new = Oid::from_hex(&"cd".repeat(32)).unwrap(); 848 let created = &wire( 849 &log(8), 850 &GitRefUpdate::new( 851 RepoDid::new("did:plc:limpet").unwrap(), 852 None, 853 AccountDid::new("did:plc:nel").unwrap(), 854 ) 855 .on_ref( 856 RefName::new("refs/heads/fresh").unwrap(), 857 RefTransition::Create { new }, 858 ObjectFormat::SHA256, 859 ), 860 )["event"]; 861 assert_eq!(created["oldSha"], "0".repeat(64)); 862 assert_eq!(created["newSha"], new.to_hex()); 863 864 let old = Oid::from_hex(&"ab".repeat(20)).unwrap(); 865 let rebuilt = update() 866 .on_ref( 867 RefName::new("refs/heads/fresh").unwrap(), 868 RefTransition::Create { 869 new: Oid::from_hex(&"cd".repeat(20)).unwrap(), 870 }, 871 ObjectFormat::SHA1, 872 ) 873 .on_ref( 874 RefName::new("refs/heads/gone").unwrap(), 875 RefTransition::Delete { old }, 876 ObjectFormat::SHA1, 877 ); 878 let payload = &wire(&log(8), &rebuilt)["event"]; 879 assert_eq!( 880 payload["ref"], "refs/heads/gone", 881 "a later transition replaces the earlier one whole" 882 ); 883 assert_eq!(payload["oldSha"], old.to_hex()); 884 assert_eq!(payload["newSha"], "0".repeat(40)); 885 } 886 887 fn peer(last: u8) -> IpAddr { 888 IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, last)) 889 } 890 891 #[test] 892 fn gate_enforces_limits_and_prunes() { 893 let global = Arc::new(SubscriberGate::new( 894 GlobalSubscriberLimit::new(2), 895 PerPeerSubscriberLimit::new(8), 896 )); 897 let first = global 898 .try_admit(peer(1)) 899 .expect("first subscriber is admitted"); 900 let _second = global 901 .try_admit(peer(2)) 902 .expect("second subscriber is admitted"); 903 assert!( 904 global.try_admit(peer(3)).is_none(), 905 "third subscriber is refused once the global limit is reached" 906 ); 907 drop(first); 908 assert!( 909 global.try_admit(peer(3)).is_some(), 910 "freeing global slot admits waiting subscriber" 911 ); 912 913 let per_peer = Arc::new(SubscriberGate::new( 914 GlobalSubscriberLimit::new(16), 915 PerPeerSubscriberLimit::new(2), 916 )); 917 let _socket = per_peer 918 .try_admit(peer(1)) 919 .expect("first socket is admitted"); 920 let second = per_peer 921 .try_admit(peer(1)) 922 .expect("second socket is admitted"); 923 assert!( 924 per_peer.try_admit(peer(1)).is_none(), 925 "third socket from same peer is refused at the per-peer limit" 926 ); 927 assert!( 928 per_peer.try_admit(peer(2)).is_some(), 929 "different peer keeps its own budget" 930 ); 931 drop(second); 932 assert!( 933 per_peer.try_admit(peer(1)).is_some(), 934 "freed per-peer slot is reusable" 935 ); 936 937 let prune = Arc::new(SubscriberGate::new( 938 GlobalSubscriberLimit::new(16), 939 PerPeerSubscriberLimit::new(2), 940 )); 941 let permit = prune.try_admit(peer(1)).expect("admitted"); 942 drop(permit); 943 assert!( 944 prune 945 .peers 946 .lock() 947 .unwrap_or_else(|poisoned| poisoned.into_inner()) 948 .is_empty(), 949 "peer map prunes peer once its last socket closes" 950 ); 951 } 952 953 #[test] 954 fn a_ref_update_includes_its_computed_meta_on_the_wire() { 955 let wire = wire( 956 &log(8), 957 &update().with_meta(RefUpdateMeta::new( 958 true, 959 vec![EmailCommitCount::new( 960 Email::new("nel@oyster.cafe"), 961 CommitCount::new(3), 962 )], 963 vec![LanguageSize::new( 964 LanguageName::new("Rust"), 965 LanguageBytes::new(1234), 966 )], 967 )), 968 ); 969 let meta = &wire["event"]["meta"]; 970 assert_eq!(meta["isDefaultRef"], true); 971 assert_eq!( 972 meta["commitCount"]["byEmail"][0]["email"], 973 "nel@oyster.cafe" 974 ); 975 assert_eq!(meta["commitCount"]["byEmail"][0]["count"], 3); 976 assert_eq!(meta["langBreakdown"]["inputs"][0]["lang"], "Rust"); 977 assert_eq!(meta["langBreakdown"]["inputs"][0]["size"], 1234); 978 } 979 980 #[test] 981 fn an_empty_breakdown_omits_the_optional_meta_arrays() { 982 let wire = wire( 983 &log(8), 984 &update().with_meta(RefUpdateMeta::new(false, Vec::new(), Vec::new())), 985 ); 986 let meta = &wire["event"]["meta"]; 987 assert_eq!(meta["isDefaultRef"], false); 988 assert!(meta["commitCount"].get("byEmail").is_none()); 989 assert!(meta.get("langBreakdown").is_none()); 990 } 991 992 #[test] 993 fn acl_updates_match_eventstream_shape() { 994 let log = log(8); 995 log.publish(&KnotMemberUpdate::added( 996 AccountDid::new("did:plc:nel").unwrap(), 997 )); 998 log.publish(&KnotMemberUpdate::removed( 999 AccountDid::new("did:plc:olaren").unwrap(), 1000 )); 1001 log.publish(&RepoCollaboratorUpdate::added( 1002 AccountDid::new("did:plc:nel").unwrap(), 1003 RepoDid::new("did:plc:limpet").unwrap(), 1004 )); 1005 log.publish(&RepoCollaboratorUpdate::removed( 1006 AccountDid::new("did:plc:nel").unwrap(), 1007 RepoDid::new("did:plc:limpet").unwrap(), 1008 )); 1009 let events = replay(&log, EventCursor::START, 8); 1010 1011 let member_added = serde_json::to_value(&*events[0]).unwrap(); 1012 assert_eq!(member_added["nsid"], "sh.tangled.knot.memberUpdate"); 1013 assert_eq!(member_added["event"]["op"], "add"); 1014 assert_eq!(member_added["event"]["subject"], "did:plc:nel"); 1015 assert!(member_added["event"].get("$type").is_none()); 1016 let member_removed = serde_json::to_value(&*events[1]).unwrap(); 1017 assert_eq!(member_removed["event"]["op"], "remove"); 1018 assert_eq!(member_removed["event"]["subject"], "did:plc:olaren"); 1019 1020 let collab_added = serde_json::to_value(&*events[2]).unwrap(); 1021 assert_eq!(collab_added["nsid"], "sh.tangled.repo.collaboratorUpdate"); 1022 assert_eq!(collab_added["event"]["op"], "add"); 1023 assert_eq!(collab_added["event"]["subject"], "did:plc:nel"); 1024 assert_eq!(collab_added["event"]["repo"], "did:plc:limpet"); 1025 assert!(collab_added["event"].get("$type").is_none()); 1026 let collab_removed = serde_json::to_value(&*events[3]).unwrap(); 1027 assert_eq!(collab_removed["event"]["op"], "remove"); 1028 assert_eq!(collab_removed["event"]["repo"], "did:plc:limpet"); 1029 } 1030 1031 fn cursors(log: &EventLog<ManualClock>) -> Vec<EventCursor> { 1032 replay(log, EventCursor::START, 64) 1033 .iter() 1034 .map(|event| event.created) 1035 .collect() 1036 } 1037 1038 #[test] 1039 fn a_reservation_holds_back_later_events_until_it_is_fulfilled() { 1040 let log = log(8); 1041 let early = log.publish(&update()); 1042 let reservation = log.reserve(); 1043 let later = log.publish(&update()); 1044 assert!(reservation.cursor() > early && reservation.cursor() < later); 1045 assert_eq!( 1046 cursors(&log), 1047 vec![early], 1048 "event published after reservation waits behind it" 1049 ); 1050 let mid = reservation.cursor(); 1051 reservation.fulfill(&update()); 1052 assert_eq!( 1053 cursors(&log), 1054 vec![early, mid, later], 1055 "fulfilling reservation releases it and event queued behind it, in cursor order" 1056 ); 1057 } 1058 1059 #[test] 1060 fn out_of_order_fulfillment_still_replays_in_cursor_order() { 1061 let log = log(8); 1062 let first = log.reserve(); 1063 let second = log.reserve(); 1064 let (c1, c2) = (first.cursor(), second.cursor()); 1065 assert!(c1 < c2); 1066 second.fulfill(&update()); 1067 assert!( 1068 cursors(&log).is_empty(), 1069 "later reservation stays hidden while earlier one is outstanding" 1070 ); 1071 first.fulfill(&update()); 1072 assert_eq!( 1073 cursors(&log), 1074 vec![c1, c2], 1075 "both surface in cursor order regardless of fulfillment order" 1076 ); 1077 } 1078 1079 #[test] 1080 fn a_dropped_reservation_unblocks_the_horizon_without_an_event() { 1081 let log = log(8); 1082 let reservation = log.reserve(); 1083 let later = log.publish(&update()); 1084 assert!( 1085 cursors(&log).is_empty(), 1086 "later event waits behind unfulfilled reservation" 1087 ); 1088 drop(reservation); 1089 assert_eq!( 1090 cursors(&log), 1091 vec![later], 1092 "dropping reservation surfaces queued event and leaves no gap" 1093 ); 1094 } 1095 1096 #[test] 1097 fn the_head_holds_at_the_last_stable_event_until_a_reservation_is_fulfilled() { 1098 let log = log(8); 1099 let mut head = log.subscribe(); 1100 let early = log.publish(&update()); 1101 assert_eq!(*head.borrow_and_update(), early); 1102 let reservation = log.reserve(); 1103 let later = log.publish(&update()); 1104 assert_eq!( 1105 *head.borrow_and_update(), 1106 early, 1107 "head holds while lower-cursor reservation is pending" 1108 ); 1109 reservation.fulfill(&update()); 1110 assert_eq!( 1111 *head.borrow_and_update(), 1112 later, 1113 "fulfilling reservation advances head past released events" 1114 ); 1115 } 1116 1117 #[test] 1118 fn an_anonymous_owner_is_omitted_from_the_wire() { 1119 let wire = wire( 1120 &log(8), 1121 &GitRefUpdate::new( 1122 RepoDid::new("did:plc:limpet").unwrap(), 1123 None, 1124 AccountDid::new("did:plc:nel").unwrap(), 1125 ), 1126 ); 1127 assert!(wire["event"].get("ownerDid").is_none()); 1128 } 1129}