use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::{SystemTime, UNIX_EPOCH};
use gunnar_proto::{event, Event, EventGap, EventKind, RepoPath, StreamOpened, WatchRequest};
use tokio::sync::broadcast;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::Stream;
pub fn now_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis().min(i64::MAX as u128) as i64)
.unwrap_or(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BusStats {
pub subscribers: u64,
pub published: u64,
pub dropped: u64,
pub capacity: u64,
}
#[derive(Debug)]
pub struct EventBus {
tx: broadcast::Sender<Arc<Event>>,
seq: AtomicU64,
published: AtomicU64,
dropped: Arc<AtomicU64>,
capacity: usize,
epoch: Arc<str>,
recent: Mutex<VecDeque<Arc<Event>>>,
}
fn new_epoch() -> Arc<str> {
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let pid = u64::from(std::process::id());
let n = SEQUENCE.fetch_add(1, Ordering::Relaxed);
Arc::from(format!("{nanos:016x}{pid:08x}{n:04x}"))
}
impl EventBus {
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(1);
let (tx, _rx) = broadcast::channel(capacity);
Self {
tx,
seq: AtomicU64::new(0),
published: AtomicU64::new(0),
dropped: Arc::new(AtomicU64::new(0)),
capacity,
epoch: new_epoch(),
recent: Mutex::new(VecDeque::with_capacity(capacity)),
}
}
pub fn publish(&self, payload: event::Payload) -> Arc<Event> {
let mut recent = self.recent.lock().unwrap_or_else(|e| e.into_inner());
let seq = self.seq.fetch_add(1, Ordering::SeqCst) + 1;
let event = Arc::new(Event::new(seq, now_unix_ms(), payload));
self.published.fetch_add(1, Ordering::Relaxed);
if recent.len() == self.capacity {
recent.pop_front();
}
recent.push_back(Arc::clone(&event));
let _ = self.tx.send(Arc::clone(&event));
event
}
pub fn subscribe(&self, filter: EventFilter) -> EventStream {
self.open(filter, None)
}
pub fn subscribe_from(&self, filter: EventFilter, from_seq: u64) -> EventStream {
self.open(filter, Some(from_seq))
}
fn open(&self, filter: EventFilter, resume: Option<u64>) -> EventStream {
let recent = self.recent.lock().unwrap_or_else(|e| e.into_inner());
let rx = self.tx.subscribe();
let last_seq = self.seq.load(Ordering::SeqCst);
let mut prologue = VecDeque::new();
if let Some(from_seq) = resume {
let oldest = recent.front().map(|e| e.seq).unwrap_or(0);
let replay: Vec<Arc<Event>> = if from_seq == 0 {
Vec::new()
} else {
recent
.iter()
.filter(|e| e.seq > from_seq)
.cloned()
.collect()
};
let dropped = if from_seq == 0 {
0
} else if oldest == 0 {
last_seq.saturating_sub(from_seq)
} else {
oldest.saturating_sub(from_seq).saturating_sub(1)
};
prologue.push_back(Event::new(
0,
now_unix_ms(),
event::Payload::StreamOpened(StreamOpened {
epoch: self.epoch.to_string(),
last_seq,
oldest_buffered_seq: oldest,
replaying: replay.len() as u64,
from_seq,
}),
));
if dropped > 0 {
self.dropped.fetch_add(dropped, Ordering::Relaxed);
tracing::warn!(
dropped,
from_seq,
oldest_buffered_seq = oldest,
"a subscriber asked to resume from before the oldest buffered event; the \
bus is a bounded ring and has no log, so it is being told what it lost \
rather than promised a replay it cannot have."
);
prologue.push_back(Event::new(
0,
now_unix_ms(),
event::Payload::Gap(EventGap {
dropped,
last_delivered_seq: from_seq,
resume_seq: replay.first().map(|e| e.seq).unwrap_or(0),
epoch: self.epoch.to_string(),
}),
));
}
for event in replay {
if filter.matches(&event) {
prologue.push_back(filter.shape(&event));
}
}
}
drop(recent);
EventStream {
inner: BroadcastStream::new(rx),
filter,
prologue,
queued: None,
last_seq,
dropped_total: Arc::clone(&self.dropped),
epoch: Arc::clone(&self.epoch),
}
}
pub fn epoch(&self) -> &str {
&self.epoch
}
pub fn oldest_buffered_seq(&self) -> u64 {
self.recent
.lock()
.unwrap_or_else(|e| e.into_inner())
.front()
.map(|e| e.seq)
.unwrap_or(0)
}
pub fn stats(&self) -> BusStats {
BusStats {
subscribers: self.tx.receiver_count() as u64,
published: self.published.load(Ordering::Relaxed),
dropped: self.dropped.load(Ordering::Relaxed),
capacity: self.capacity as u64,
}
}
pub fn last_seq(&self) -> u64 {
self.seq.load(Ordering::SeqCst)
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new(crate::config::DEFAULT_EVENT_BUFFER)
}
}
#[derive(Debug, Clone, Default)]
pub struct EventFilter {
pub namespaces: Vec<String>,
pub repos: Vec<RepoPath>,
pub kinds: Vec<EventKind>,
pub include_attestation_object_ids: bool,
}
impl EventFilter {
pub fn from_request(req: &WatchRequest) -> Self {
Self {
namespaces: req.namespaces.clone(),
repos: req.repos.clone(),
kinds: req
.kinds
.iter()
.filter_map(|k| EventKind::try_from(*k).ok())
.filter(|k| *k != EventKind::Unspecified)
.collect(),
include_attestation_object_ids: req.include_attestation_object_ids,
}
}
pub fn matches(&self, event: &Event) -> bool {
if event.kind == EventKind::Gap as i32 || event.kind == EventKind::StreamOpened as i32 {
return true;
}
if !self.kinds.is_empty() && !self.kinds.iter().any(|k| *k as i32 == event.kind) {
return false;
}
if !self.namespaces.is_empty() {
match event.repo_path() {
Some(path) if self.namespaces.contains(&path.namespace) => {}
_ => return false,
}
}
if !self.repos.is_empty() {
match event.repo_path() {
Some(path)
if self
.repos
.iter()
.any(|r| r.namespace == path.namespace && r.repo == path.repo) => {}
_ => return false,
}
}
true
}
fn shape(&self, event: &Event) -> Event {
let mut event = event.clone();
if !self.include_attestation_object_ids {
if let Some(event::Payload::PushAttestation(att)) = event.payload.as_mut() {
att.new_object_oids.clear();
if att.new_object_oids_unknown.is_empty() {
att.new_object_oids_unknown = "this subscription did not set \
WatchRequest.include_attestation_object_ids, so the object ids were \
stripped from this event; the count and the byte total are complete"
.to_owned();
}
}
}
event
}
}
pub struct EventStream {
inner: BroadcastStream<Arc<Event>>,
filter: EventFilter,
prologue: VecDeque<Event>,
queued: Option<Event>,
last_seq: u64,
dropped_total: Arc<AtomicU64>,
epoch: Arc<str>,
}
impl EventStream {
fn gap(&mut self, dropped: u64, resume_seq: u64) -> Event {
let last_delivered_seq = self.last_seq;
Event::new(
0,
now_unix_ms(),
event::Payload::Gap(EventGap {
dropped,
last_delivered_seq,
resume_seq,
epoch: self.epoch.to_string(),
}),
)
}
}
impl Stream for EventStream {
type Item = Result<Event, tonic::Status>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(event) = this.prologue.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
if let Some(event) = this.queued.take() {
return Poll::Ready(Some(Ok(event)));
}
loop {
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(event))) => {
this.last_seq = event.seq;
if this.filter.matches(&event) {
return Poll::Ready(Some(Ok(this.filter.shape(&event))));
}
continue;
}
Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(dropped)))) => {
this.dropped_total.fetch_add(dropped, Ordering::Relaxed);
tracing::warn!(
dropped,
last_delivered_seq = this.last_seq,
"event subscriber lagged; dropping the oldest events and sending a gap \
marker. Memory is bounded by the bus ring, never by this subscriber."
);
let resume = match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(event))) => Some(event),
_ => None,
};
let resume_seq = resume.as_ref().map(|e| e.seq).unwrap_or(0);
let gap = this.gap(dropped, resume_seq);
if let Some(event) = resume {
this.last_seq = event.seq;
if this.filter.matches(&event) {
this.queued = Some(this.filter.shape(&event));
}
}
return Poll::Ready(Some(Ok(gap)));
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use gunnar_proto::{RepoDeleted, RepoPath};
fn deleted(namespace: &str, repo: &str) -> event::Payload {
event::Payload::RepoDeleted(RepoDeleted {
repo: Some(RepoPath::new(namespace, repo)),
principal_id: String::new(),
})
}
#[test]
fn publish_assigns_monotonic_sequences_starting_at_one() {
let bus = EventBus::new(8);
assert_eq!(bus.last_seq(), 0, "nothing published yet");
assert_eq!(bus.publish(deleted("ns", "a")).seq, 1);
assert_eq!(bus.publish(deleted("ns", "b")).seq, 2);
assert_eq!(bus.last_seq(), 2);
}
#[test]
fn published_event_kind_agrees_with_its_payload() {
let bus = EventBus::new(8);
let event = bus.publish(deleted("ns", "a"));
assert!(event.kind_matches_payload());
assert_eq!(event.kind, EventKind::RepoDeleted as i32);
}
#[test]
fn filter_matches_are_not_vacuous() {
let bus = EventBus::new(8);
let event = bus.publish(deleted("alpha", "one"));
let empty = EventFilter::default();
assert!(empty.matches(&event), "an empty filter watches everything");
let ns_hit = EventFilter {
namespaces: vec!["alpha".into()],
..Default::default()
};
let ns_miss = EventFilter {
namespaces: vec!["beta".into()],
..Default::default()
};
assert!(ns_hit.matches(&event));
assert!(!ns_miss.matches(&event), "namespace filter must exclude");
let repo_hit = EventFilter {
repos: vec![RepoPath::new("alpha", "one")],
..Default::default()
};
let repo_miss = EventFilter {
repos: vec![RepoPath::new("alpha", "two")],
..Default::default()
};
assert!(repo_hit.matches(&event));
assert!(!repo_miss.matches(&event), "repo filter must exclude");
let kind_hit = EventFilter {
kinds: vec![EventKind::RepoDeleted],
..Default::default()
};
let kind_miss = EventFilter {
kinds: vec![EventKind::RefUpdated],
..Default::default()
};
assert!(kind_hit.matches(&event));
assert!(!kind_miss.matches(&event), "kind filter must exclude");
}
#[test]
fn a_gap_survives_every_filter() {
let gap = Event::new(
0,
0,
event::Payload::Gap(EventGap {
dropped: 3,
last_delivered_seq: 1,
resume_seq: 5,
epoch: "an-epoch".to_owned(),
}),
);
let narrow = EventFilter {
namespaces: vec!["nothing-matches-this".into()],
repos: vec![RepoPath::new("no", "such")],
kinds: vec![EventKind::KeyUsed],
include_attestation_object_ids: false,
};
assert!(
narrow.matches(&gap),
"a subscriber must never filter away its own gap marker"
);
let opened = Event::new(
0,
0,
event::Payload::StreamOpened(StreamOpened {
epoch: "an-epoch".to_owned(),
..Default::default()
}),
);
assert!(
narrow.matches(&opened),
"…nor the prologue that tells it which run its sequence numbers belong to"
);
let ordinary = Event::new(
1,
0,
event::Payload::RepoDeleted(RepoDeleted {
repo: Some(RepoPath::new("nordisk", "gunnar")),
principal_id: String::new(),
}),
);
assert!(
!narrow.matches(&ordinary),
"this filter matches no repository and no kind, so an ordinary event must be excluded"
);
}
#[test]
fn an_epoch_is_never_the_same_twice() {
let a = new_epoch();
let b = new_epoch();
assert_ne!(a, b, "two epochs minted in a row must differ");
assert!(!a.is_empty());
}
}