use std::collections::VecDeque;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
pub const DEFAULT_CONNECTION_EVENT_INBOX_CAPACITY: usize = 64;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ConnectionEventKind {
Connected,
Disconnected,
Reconnected,
FailedOver,
EndpointAttemptFailed,
AllEndpointsUnreachable,
AuthFailed,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ConnectionEvent {
pub kind: ConnectionEventKind,
pub host: Option<String>,
pub port: Option<String>,
pub previous_host: Option<String>,
pub previous_port: Option<String>,
pub attempt_number: Option<u64>,
pub cause_code: Option<crate::ErrorCode>,
pub cause_msg: Option<String>,
pub timestamp_millis: i64,
}
impl ConnectionEvent {
pub(crate) fn new(kind: ConnectionEventKind) -> Self {
let timestamp_millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
Self {
kind,
host: None,
port: None,
previous_host: None,
previous_port: None,
attempt_number: None,
cause_code: None,
cause_msg: None,
timestamp_millis,
}
}
pub(crate) fn at(mut self, host: &str, port: &str) -> Self {
self.host = Some(host.to_string());
self.port = Some(port.to_string());
self
}
pub(crate) fn previously_at(mut self, host: &str, port: &str) -> Self {
self.previous_host = Some(host.to_string());
self.previous_port = Some(port.to_string());
self
}
pub(crate) fn attempt(mut self, attempt: u64) -> Self {
self.attempt_number = Some(attempt);
self
}
pub(crate) fn caused_by(mut self, err: &crate::Error) -> Self {
self.cause_code = Some(err.code());
self.cause_msg = Some(err.msg().to_string());
self
}
}
pub type ConnectionListener = Arc<dyn Fn(&ConnectionEvent) + Send + Sync>;
struct DispatcherInner<T> {
inbox: Mutex<VecDeque<T>>,
available: Condvar,
capacity: usize,
listener: Arc<dyn Fn(&T) + Send + Sync>,
name: &'static str,
closed: AtomicBool,
dropped: AtomicU64,
delivered: AtomicU64,
}
impl<T> DispatcherInner<T> {
fn lock_inbox(&self) -> std::sync::MutexGuard<'_, VecDeque<T>> {
match self.inbox.lock() {
Ok(inbox) => inbox,
Err(poisoned) => poisoned.into_inner(),
}
}
}
pub struct EventDispatcher<T> {
inner: Arc<DispatcherInner<T>>,
thread: Option<std::thread::JoinHandle<()>>,
}
pub type ConnectionEventDispatcher = EventDispatcher<ConnectionEvent>;
impl<T: std::fmt::Debug + Send + 'static> EventDispatcher<T> {
pub fn new(listener: Arc<dyn Fn(&T) + Send + Sync>, capacity: usize) -> Self {
Self::named("conn-events", listener, capacity)
}
pub(crate) fn named(
name: &'static str,
listener: Arc<dyn Fn(&T) + Send + Sync>,
capacity: usize,
) -> Self {
let capacity = if capacity == 0 {
DEFAULT_CONNECTION_EVENT_INBOX_CAPACITY
} else {
capacity
};
let inner = Arc::new(DispatcherInner {
inbox: Mutex::new(VecDeque::with_capacity(capacity)),
available: Condvar::new(),
capacity,
listener,
name,
closed: AtomicBool::new(false),
dropped: AtomicU64::new(0),
delivered: AtomicU64::new(0),
});
let thread_inner = Arc::clone(&inner);
let thread = match std::thread::Builder::new()
.name(format!("questdb-{name}"))
.spawn(move || dispatch_loop(thread_inner))
{
Ok(handle) => Some(handle),
Err(err) => {
log::warn!("{name} dispatcher thread failed to spawn: {err}");
inner.closed.store(true, Ordering::Release);
None
}
};
Self { inner, thread }
}
pub fn offer(&self, event: T) {
if self.inner.closed.load(Ordering::Acquire) {
self.inner.dropped.fetch_add(1, Ordering::Relaxed);
return;
}
{
let mut inbox = self.inner.lock_inbox();
if inbox.len() >= self.inner.capacity {
inbox.pop_front();
self.inner.dropped.fetch_add(1, Ordering::Relaxed);
}
inbox.push_back(event);
}
self.inner.available.notify_one();
}
pub fn dropped(&self) -> u64 {
self.inner.dropped.load(Ordering::Relaxed)
}
pub fn delivered(&self) -> u64 {
self.inner.delivered.load(Ordering::Relaxed)
}
pub(crate) fn shutdown(self) -> (u64, u64) {
let inner = Arc::clone(&self.inner);
drop(self);
(
inner.delivered.load(Ordering::Relaxed),
inner.dropped.load(Ordering::Relaxed),
)
}
}
impl<T> Drop for EventDispatcher<T> {
fn drop(&mut self) {
self.inner.closed.store(true, Ordering::Release);
self.inner.available.notify_one();
if let Some(handle) = self.thread.take()
&& handle.thread().id() != std::thread::current().id()
{
let _ = handle.join();
}
}
}
fn dispatch_loop<T: std::fmt::Debug>(inner: Arc<DispatcherInner<T>>) {
loop {
let event = {
let mut inbox = inner.lock_inbox();
loop {
if inner.closed.load(Ordering::Acquire) {
let discarded = inbox.len() as u64;
if discarded > 0 {
inbox.clear();
inner.dropped.fetch_add(discarded, Ordering::Relaxed);
}
break None;
}
if let Some(event) = inbox.pop_front() {
break Some(event);
}
inbox = match inner.available.wait(inbox) {
Ok(inbox) => inbox,
Err(poisoned) => poisoned.into_inner(),
};
}
};
let Some(event) = event else {
return;
};
let listener = &inner.listener;
if catch_unwind(AssertUnwindSafe(|| listener(&event))).is_err() {
log::warn!("{} listener panicked; event: {event:?}", inner.name);
}
inner.delivered.fetch_add(1, Ordering::Relaxed);
}
}
impl std::fmt::Debug for ConnectionEventSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectionEventSource")
.field("delivered", &self.delivered())
.field("dropped", &self.dropped())
.finish()
}
}
pub(crate) struct ConnectionEventSource {
dispatcher: Mutex<Option<ConnectionEventDispatcher>>,
attempts: AtomicU64,
last_endpoint: Mutex<Option<(String, String)>>,
failed_since_success: AtomicBool,
}
impl ConnectionEventSource {
pub(crate) fn new(listener: ConnectionListener, inbox_capacity: usize) -> Self {
Self::with_dispatcher(Some(ConnectionEventDispatcher::new(
listener,
inbox_capacity,
)))
}
pub(crate) fn disabled() -> Self {
Self::with_dispatcher(None)
}
fn with_dispatcher(dispatcher: Option<ConnectionEventDispatcher>) -> Self {
Self {
dispatcher: Mutex::new(dispatcher),
attempts: AtomicU64::new(0),
last_endpoint: Mutex::new(None),
failed_since_success: AtomicBool::new(false),
}
}
fn lock_dispatcher(&self) -> std::sync::MutexGuard<'_, Option<ConnectionEventDispatcher>> {
match self.dispatcher.lock() {
Ok(dispatcher) => dispatcher,
Err(poisoned) => poisoned.into_inner(),
}
}
pub(crate) fn close(&self) {
let dispatcher = self.lock_dispatcher().take();
drop(dispatcher);
}
fn offer(&self, event: ConnectionEvent) {
if let Some(dispatcher) = self.lock_dispatcher().as_ref() {
dispatcher.offer(event);
}
}
pub(crate) fn next_attempt(&self) -> u64 {
self.attempts.fetch_add(1, Ordering::Relaxed) + 1
}
pub(crate) fn connect_attempt_failed(
&self,
host: &str,
port: &str,
err: &crate::Error,
attempt: u64,
) {
self.failed_since_success.store(true, Ordering::Relaxed);
self.offer(
ConnectionEvent::new(ConnectionEventKind::EndpointAttemptFailed)
.at(host, port)
.attempt(attempt)
.caused_by(err),
);
}
pub(crate) fn auth_failed(&self, host: &str, port: &str, err: &crate::Error, attempt: u64) {
self.failed_since_success.store(true, Ordering::Relaxed);
self.offer(
ConnectionEvent::new(ConnectionEventKind::AuthFailed)
.at(host, port)
.attempt(attempt)
.caused_by(err),
);
}
pub(crate) fn all_endpoints_unreachable(&self, err: &crate::Error) {
self.failed_since_success.store(true, Ordering::Relaxed);
self.offer(
ConnectionEvent::new(ConnectionEventKind::AllEndpointsUnreachable).caused_by(err),
);
}
pub(crate) fn disconnected(&self, host: &str, port: &str) {
self.failed_since_success.store(true, Ordering::Relaxed);
self.offer(ConnectionEvent::new(ConnectionEventKind::Disconnected).at(host, port));
}
pub(crate) fn connect_succeeded(&self, host: &str, port: &str) {
let mut last = match self.last_endpoint.lock() {
Ok(last) => last,
Err(poisoned) => poisoned.into_inner(),
};
let failed = self.failed_since_success.swap(false, Ordering::Relaxed);
let event = match last.as_ref() {
None => Some(ConnectionEvent::new(ConnectionEventKind::Connected).at(host, port)),
Some((prev_host, prev_port)) if prev_host == host && prev_port == port => {
if failed {
Some(ConnectionEvent::new(ConnectionEventKind::Reconnected).at(host, port))
} else {
None
}
}
Some((prev_host, prev_port)) => Some(
ConnectionEvent::new(ConnectionEventKind::FailedOver)
.at(host, port)
.previously_at(prev_host, prev_port),
),
};
*last = Some((host.to_string(), port.to_string()));
drop(last);
if let Some(event) = event {
self.offer(event);
}
}
pub(crate) fn dropped(&self) -> u64 {
self.lock_dispatcher()
.as_ref()
.map(ConnectionEventDispatcher::dropped)
.unwrap_or(0)
}
pub(crate) fn delivered(&self) -> u64 {
self.lock_dispatcher()
.as_ref()
.map(ConnectionEventDispatcher::delivered)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn wait_for(mut cond: impl FnMut() -> bool) {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !cond() {
assert!(std::time::Instant::now() < deadline, "timed out");
std::thread::sleep(Duration::from_millis(1));
}
}
type SeenKindsAndHosts = Arc<Mutex<Vec<(ConnectionEventKind, Option<String>)>>>;
#[test]
fn delivers_in_order_on_dispatcher_thread() {
let seen: SeenKindsAndHosts = Arc::new(Mutex::new(Vec::new()));
let seen_in_listener = Arc::clone(&seen);
let offering_thread = std::thread::current().id();
let dispatcher = ConnectionEventDispatcher::new(
Arc::new(move |event: &ConnectionEvent| {
assert_ne!(std::thread::current().id(), offering_thread);
seen_in_listener
.lock()
.unwrap()
.push((event.kind, event.host.clone()));
}),
8,
);
dispatcher.offer(ConnectionEvent::new(ConnectionEventKind::Connected).at("a", "1"));
dispatcher.offer(
ConnectionEvent::new(ConnectionEventKind::FailedOver)
.at("b", "2")
.previously_at("a", "1"),
);
wait_for(|| dispatcher.delivered() == 2);
let seen = seen.lock().unwrap();
assert_eq!(
*seen,
vec![
(ConnectionEventKind::Connected, Some("a".to_string())),
(ConnectionEventKind::FailedOver, Some("b".to_string())),
]
);
assert_eq!(dispatcher.dropped(), 0);
}
#[test]
fn drop_oldest_when_full() {
let gate = Arc::new(Mutex::new(()));
let seen: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let seen_in_listener = Arc::clone(&seen);
let gate_in_listener = Arc::clone(&gate);
let dispatcher = ConnectionEventDispatcher::new(
Arc::new(move |event: &ConnectionEvent| {
drop(gate_in_listener.lock().unwrap());
seen_in_listener
.lock()
.unwrap()
.push(event.attempt_number.unwrap());
}),
2,
);
{
let _held = gate.lock().unwrap();
dispatcher
.offer(ConnectionEvent::new(ConnectionEventKind::EndpointAttemptFailed).attempt(0));
wait_for(|| dispatcher.inner.inbox.lock().unwrap().is_empty());
for attempt in 1..=4u64 {
dispatcher.offer(
ConnectionEvent::new(ConnectionEventKind::EndpointAttemptFailed)
.attempt(attempt),
);
}
}
wait_for(|| dispatcher.delivered() == 3);
assert_eq!(*seen.lock().unwrap(), vec![0, 3, 4]);
assert_eq!(dispatcher.dropped(), 2);
}
#[test]
fn listener_panic_does_not_kill_dispatcher() {
let seen: Arc<Mutex<Vec<u64>>> = Arc::new(Mutex::new(Vec::new()));
let seen_in_listener = Arc::clone(&seen);
let dispatcher = ConnectionEventDispatcher::new(
Arc::new(move |event: &ConnectionEvent| {
let attempt = event.attempt_number.unwrap();
if attempt == 0 {
panic!("listener bug");
}
seen_in_listener.lock().unwrap().push(attempt);
}),
8,
);
dispatcher.offer(ConnectionEvent::new(ConnectionEventKind::Disconnected).attempt(0));
dispatcher.offer(ConnectionEvent::new(ConnectionEventKind::Reconnected).attempt(1));
wait_for(|| dispatcher.delivered() == 2);
assert_eq!(*seen.lock().unwrap(), vec![1]);
}
#[test]
fn drop_joins_in_flight_delivery_and_discards_backlog() {
let release = Arc::new(AtomicBool::new(false));
let delivered_after_drop = Arc::new(AtomicBool::new(false));
let dropped_flag = Arc::new(AtomicBool::new(false));
let release_in_listener = Arc::clone(&release);
let delivered_after_drop_in_listener = Arc::clone(&delivered_after_drop);
let dropped_flag_in_listener = Arc::clone(&dropped_flag);
let dispatcher = ConnectionEventDispatcher::new(
Arc::new(move |_: &ConnectionEvent| {
while !release_in_listener.load(Ordering::Acquire) {
std::thread::sleep(Duration::from_millis(1));
}
if dropped_flag_in_listener.load(Ordering::Acquire) {
delivered_after_drop_in_listener.store(true, Ordering::Release);
}
}),
8,
);
dispatcher.offer(ConnectionEvent::new(ConnectionEventKind::Connected).attempt(0));
wait_for(|| dispatcher.inner.inbox.lock().unwrap().is_empty());
for attempt in 1..=3u64 {
dispatcher
.offer(ConnectionEvent::new(ConnectionEventKind::Disconnected).attempt(attempt));
}
let inner = Arc::clone(&dispatcher.inner);
let releaser = {
let release = Arc::clone(&release);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
release.store(true, Ordering::Release);
})
};
drop(dispatcher);
dropped_flag.store(true, Ordering::Release);
releaser.join().unwrap();
std::thread::sleep(Duration::from_millis(50));
assert!(!delivered_after_drop.load(Ordering::Acquire));
assert_eq!(inner.delivered.load(Ordering::Relaxed), 1);
assert_eq!(inner.dropped.load(Ordering::Relaxed), 3);
}
#[test]
fn drop_signals_thread_exit_and_discards_late_offers() {
let dispatcher = ConnectionEventDispatcher::new(Arc::new(|_: &ConnectionEvent| {}), 4);
let inner = Arc::clone(&dispatcher.inner);
drop(dispatcher);
wait_for(|| Arc::strong_count(&inner) == 1);
assert!(inner.closed.load(Ordering::Acquire));
}
#[test]
fn source_close_fences_listener() {
let seen = Arc::new(Mutex::new(Vec::new()));
let seen_in_listener = Arc::clone(&seen);
let source = ConnectionEventSource::new(
Arc::new(move |event: &ConnectionEvent| {
seen_in_listener.lock().unwrap().push(event.kind);
}),
8,
);
source.connect_succeeded("a", "1");
source.disconnected("a", "1");
source.connect_succeeded("a", "1");
wait_for(|| seen.lock().unwrap().len() == 3);
assert_eq!(
*seen.lock().unwrap(),
vec![
ConnectionEventKind::Connected,
ConnectionEventKind::Disconnected,
ConnectionEventKind::Reconnected
]
);
source.close();
source.disconnected("a", "1");
std::thread::sleep(Duration::from_millis(20));
assert_eq!(seen.lock().unwrap().len(), 3);
}
#[test]
fn disabled_source_discards_events() {
let source = ConnectionEventSource::disabled();
source.connect_succeeded("a", "1");
source.disconnected("a", "1");
assert_eq!(source.delivered(), 0);
assert_eq!(source.dropped(), 0);
}
}