use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::animate::{arm_timer_at, cancel_timer, timer_fire_now};
use super::scheduler::{request_frame, wake_handle, WakeHandle};
use super::scope::Scope;
use super::signal::Signal;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConnState {
Connecting,
Connected,
Degraded(String),
Reconnecting { attempt: u32, next_in: Duration },
Closed,
}
#[derive(Clone, Debug)]
pub struct Backoff {
base: Duration,
cap: Duration,
attempt: u32,
rng: u64,
}
impl Default for Backoff {
fn default() -> Backoff {
Backoff::new(Duration::from_millis(500), Duration::from_secs(30))
}
}
impl Backoff {
pub fn new(base: Duration, cap: Duration) -> Backoff {
Backoff {
base,
cap,
attempt: 0,
rng: entropy_seed(),
}
}
pub fn seeded(mut self, seed: u64) -> Backoff {
self.rng = seed | 1; self
}
pub fn ceiling(&self) -> Duration {
let mut c = self.base.min(self.cap);
for _ in 0..self.attempt {
if c >= self.cap {
return self.cap;
}
c = c.saturating_mul(2).min(self.cap);
}
c
}
pub fn next_delay(&mut self) -> Duration {
let bound = self.ceiling();
self.attempt = self.attempt.saturating_add(1);
self.draw(bound)
}
pub fn attempt(&self) -> u32 {
self.attempt
}
pub fn reset(&mut self) {
self.attempt = 0;
}
fn draw(&mut self, bound: Duration) -> Duration {
self.rng ^= self.rng << 13;
self.rng ^= self.rng >> 7;
self.rng ^= self.rng << 17;
let nanos = bound.as_nanos().min(u64::MAX as u128) as u64;
if nanos == 0 {
return Duration::ZERO;
}
Duration::from_nanos(self.rng % (nanos + 1))
}
}
fn entropy_seed() -> u64 {
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| (d.as_secs() << 32) ^ u64::from(d.subsec_nanos()))
.unwrap_or(0x9E37_79B9_7F4A_7C15);
let addr = &t as *const _ as u64;
(t ^ addr.rotate_left(32)) | 1
}
struct CoreUi {
backoff: Backoff,
timer: Rc<std::cell::Cell<Option<u64>>>,
dial: Option<Box<dyn FnMut(ConnectionEvents)>>,
}
struct Shared {
wake: WakeHandle,
gen: AtomicU64,
closed: AtomicBool,
stale_reports: AtomicU64,
}
#[derive(Clone)]
pub struct ConnectionEvents {
shared: Arc<Shared>,
gen: u64,
state: Signal<ConnState>,
core: Signal<Rc<RefCell<CoreUi>>>,
}
impl ConnectionEvents {
pub fn connected(&self) {
self.post(Report::Connected);
}
pub fn degraded(&self, reason: impl Into<String>) {
self.post(Report::Degraded(reason.into()));
}
pub fn failed(&self, reason: impl Into<String>) {
self.post(Report::Failed(reason.into()));
}
pub fn closed(&self) {
self.post(Report::Closed);
}
pub fn is_closed(&self) -> bool {
self.shared.closed.load(Ordering::Acquire)
}
pub fn is_current(&self) -> bool {
!self.is_closed() && self.shared.gen.load(Ordering::Acquire) == self.gen
}
pub fn stale_reports(&self) -> u64 {
self.shared.stale_reports.load(Ordering::Relaxed)
}
fn post(&self, report: Report) {
let shared = self.shared.clone();
let gen = self.gen;
let state = self.state;
let core = self.core;
self.shared.wake.post(move || {
if !state.is_alive()
|| shared.closed.load(Ordering::Acquire)
|| shared.gen.load(Ordering::Acquire) != gen
{
shared.stale_reports.fetch_add(1, Ordering::Relaxed);
return;
}
apply_report(state, core, &shared, report);
});
}
}
enum Report {
Connected,
Degraded(String),
Failed(String),
Closed,
}
fn apply_report(
state: Signal<ConnState>,
core: Signal<Rc<RefCell<CoreUi>>>,
shared: &Arc<Shared>,
report: Report,
) {
let core_rc = core.get_untracked();
match report {
Report::Connected => {
core_rc.borrow_mut().backoff.reset();
state.set(ConnState::Connected);
}
Report::Degraded(reason) => {
core_rc.borrow_mut().backoff.reset();
state.set(ConnState::Degraded(reason));
}
Report::Failed(_reason) => {
shared.gen.fetch_add(1, Ordering::AcqRel);
let (attempt, next_in) = {
let mut c = core_rc.borrow_mut();
let next_in = c.backoff.next_delay();
(c.backoff.attempt(), next_in)
};
state.set(ConnState::Reconnecting { attempt, next_in });
arm_retry(state, core, shared.clone(), &core_rc, next_in);
}
Report::Closed => close_now(state, &core_rc, shared),
}
}
fn arm_retry(
state: Signal<ConnState>,
core: Signal<Rc<RefCell<CoreUi>>>,
shared: Arc<Shared>,
core_rc: &Rc<RefCell<CoreUi>>,
next_in: Duration,
) {
let now = timer_fire_now().unwrap_or_else(Instant::now);
let timer_slot = core_rc.borrow().timer.clone();
let id = arm_timer_at(now + next_in, move || {
if shared.closed.load(Ordering::Acquire) || !state.is_alive() {
return; }
dial_now(state, core, &shared);
});
timer_slot.set(Some(id));
request_frame();
}
fn dial_now(state: Signal<ConnState>, core: Signal<Rc<RefCell<CoreUi>>>, shared: &Arc<Shared>) {
let core_rc = core.get_untracked();
core_rc.borrow().timer.set(None);
let _ = state.set_if_changed(ConnState::Connecting);
let events = ConnectionEvents {
shared: shared.clone(),
gen: shared.gen.load(Ordering::Acquire),
state,
core,
};
let taken = core_rc.borrow_mut().dial.take();
if let Some(mut dial) = taken {
dial(events);
if !shared.closed.load(Ordering::Acquire) {
let mut c = core_rc.borrow_mut();
if c.dial.is_none() {
c.dial = Some(dial);
}
}
}
}
fn close_now(state: Signal<ConnState>, core_rc: &Rc<RefCell<CoreUi>>, shared: &Arc<Shared>) {
if shared.closed.swap(true, Ordering::AcqRel) {
return;
}
if let Some(id) = core_rc.borrow().timer.take() {
let _ = cancel_timer(id);
}
core_rc.borrow_mut().dial = None;
if state.is_alive() {
state.set(ConnState::Closed);
}
}
#[derive(Clone)]
pub struct Connection {
state: Signal<ConnState>,
core: Signal<Rc<RefCell<CoreUi>>>,
core_rc: Rc<RefCell<CoreUi>>,
shared: Arc<Shared>,
}
impl Connection {
pub fn state(&self) -> Signal<ConnState> {
self.state
}
pub fn close(&self) {
close_now(self.state, &self.core_rc, &self.shared);
}
pub fn is_closed(&self) -> bool {
self.shared.closed.load(Ordering::Acquire)
}
pub fn retry_now(&self) {
if self.is_closed() || !self.state.is_alive() {
return;
}
if !matches!(self.state.get_untracked(), ConnState::Reconnecting { .. }) {
return;
}
if let Some(id) = self.core_rc.borrow().timer.take() {
let _ = cancel_timer(id);
}
dial_now(self.state, self.core, &self.shared);
}
}
pub fn connection(
cx: Scope,
backoff: Backoff,
dial: impl FnMut(ConnectionEvents) + 'static,
) -> Connection {
let state = cx.signal(ConnState::Connecting);
let core_rc = Rc::new(RefCell::new(CoreUi {
backoff,
timer: Rc::new(std::cell::Cell::new(None)),
dial: Some(Box::new(dial)),
}));
let core = cx.signal(core_rc.clone());
let shared = Arc::new(Shared {
wake: wake_handle(),
gen: AtomicU64::new(1),
closed: AtomicBool::new(false),
stale_reports: AtomicU64::new(0),
});
{
let core_rc = core_rc.clone();
let shared = shared.clone();
cx.on_cleanup(move || close_now(state, &core_rc, &shared));
}
dial_now(state, core, &shared);
Connection {
state,
core,
core_rc,
shared,
}
}
#[cfg(test)]
#[path = "connection_tests.rs"]
mod tests;