use super::{
super::{json, stage::Stage},
heartbeat::{Heartbeater, Heartbeats},
};
use leaky_bucket_lite::LeakyBucket;
use serde::ser::Serialize;
use std::{
convert::{TryFrom, TryInto},
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
sync::{
atomic::{AtomicU64, AtomicU8, Ordering},
Arc, Mutex as MutexSync,
},
time::Duration,
};
use tokio::{
sync::{
mpsc::{error::SendError, UnboundedSender},
OnceCell,
},
task::JoinHandle,
};
use tokio_tungstenite::tungstenite::{protocol::CloseFrame, Message as TungsteniteMessage};
use twilight_model::gateway::payload::outgoing::Heartbeat;
const RESET_DURATION_MILLISECONDS: u64 = 60_000;
#[derive(Debug)]
pub struct SessionSendError {
pub(super) source: Option<Box<dyn Error + Send + Sync>>,
pub(super) kind: SessionSendErrorType,
}
impl SessionSendError {
pub const fn kind(&self) -> &SessionSendErrorType {
&self.kind
}
}
impl Display for SessionSendError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
SessionSendErrorType::Serializing => f.write_str("failed to serialize payload as json"),
SessionSendErrorType::Sending => f.write_str("failed to send message over websocket"),
}
}
}
impl Error for SessionSendError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| &**source as &(dyn Error + 'static))
}
}
#[derive(Debug)]
pub enum SessionSendErrorType {
Sending,
Serializing,
}
#[derive(Debug)]
pub struct Session {
pub heartbeater_handle: Arc<MutexSync<Option<JoinHandle<()>>>>,
pub heartbeats: Arc<Heartbeats>,
pub heartbeat_interval: AtomicU64,
pub id: MutexSync<Option<Box<str>>>,
pub seq: Arc<AtomicU64>,
pub stage: AtomicU8,
pub tx: UnboundedSender<TungsteniteMessage>,
pub ratelimit: OnceCell<LeakyBucket>,
}
impl Session {
pub fn new(tx: UnboundedSender<TungsteniteMessage>) -> Self {
Self {
heartbeater_handle: Arc::new(MutexSync::new(None)),
heartbeats: Arc::new(Heartbeats::default()),
heartbeat_interval: AtomicU64::new(0),
id: MutexSync::new(None),
seq: Arc::new(AtomicU64::new(0)),
stage: AtomicU8::new(Stage::default() as u8),
tx,
ratelimit: OnceCell::new(),
}
}
pub fn send(&self, payload: impl Serialize) -> Result<(), SessionSendError> {
let bytes = json::to_vec(&payload).map_err(|source| SessionSendError {
kind: SessionSendErrorType::Serializing,
source: Some(Box::new(source)),
})?;
self.tx
.send(TungsteniteMessage::Binary(bytes))
.map_err(|source| SessionSendError {
kind: SessionSendErrorType::Sending,
source: Some(Box::new(source)),
})?;
Ok(())
}
pub fn close(
&self,
close_frame: Option<CloseFrame<'static>>,
) -> Result<(), SendError<TungsteniteMessage>> {
self.tx.send(TungsteniteMessage::Close(close_frame))
}
pub fn heartbeat_interval(&self) -> u64 {
self.heartbeat_interval.load(Ordering::Relaxed)
}
pub fn set_heartbeat_interval(&self, new_heartbeat_interval: u64) {
const REFILL_INTERVAL: Duration = Duration::from_millis(RESET_DURATION_MILLISECONDS);
self.heartbeat_interval
.store(new_heartbeat_interval, Ordering::Release);
let commands_allotted = f64::from(heartbeats_per_reset(new_heartbeat_interval));
let _result = self.ratelimit.set(
LeakyBucket::builder()
.max(commands_allotted)
.tokens(commands_allotted)
.refill_interval(REFILL_INTERVAL)
.refill_amount(commands_allotted)
.build(),
);
}
pub fn seq(&self) -> u64 {
self.seq.load(Ordering::Relaxed)
}
pub fn set_seq(&self, seq: u64) {
self.seq.store(seq, Ordering::Release);
}
pub fn stage(&self) -> Stage {
Stage::try_from(self.stage.load(Ordering::Relaxed)).unwrap_or_default()
}
pub fn set_stage(&self, stage: Stage) {
self.stage.store(stage as u8, Ordering::Release);
}
pub fn heartbeat(&self) -> Result<(), SessionSendError> {
self.send(Heartbeat::new(self.seq()))
}
pub fn id(&self) -> Option<Box<str>> {
self.id.lock().expect("id poisoned").clone()
}
pub fn set_id(&self, new_id: Box<str>) {
self.id.lock().expect("id poisoned").replace(new_id);
}
pub fn stop_heartbeater(&self) {
if let Some(handle) = self
.heartbeater_handle
.lock()
.expect("heartbeater poisoned")
.take()
{
handle.abort();
}
}
pub fn start_heartbeater(&self) {
let interval = self.heartbeat_interval();
let seq = Arc::clone(&self.seq);
let heartbeats = Arc::clone(&self.heartbeats);
let heartbeater = Heartbeater::new(heartbeats, interval, seq, self.tx.clone()).run();
let handle = tokio::spawn(heartbeater);
if let Some(old) = self
.heartbeater_handle
.lock()
.expect("heartbeater poisoned")
.replace(handle)
{
old.abort();
}
}
}
impl Drop for Session {
fn drop(&mut self) {
self.stop_heartbeater();
}
}
fn heartbeats_per_reset(heartbeat_interval: u64) -> u8 {
const COMMANDS_PER_RESET: u8 = 120;
let mut heartbeats = RESET_DURATION_MILLISECONDS / heartbeat_interval;
let remainder = RESET_DURATION_MILLISECONDS % heartbeat_interval;
if remainder > 0 {
heartbeats += 1;
}
let heartbeats_converted = if let Ok(value) = heartbeats.try_into() {
value
} else {
const ALLOT_ON_FAIL: u8 = COMMANDS_PER_RESET - 10;
#[cfg(feature = "tracing")]
tracing::warn!(
%heartbeats,
"heartbeats > u8 max; defaulting to allotting {}",
ALLOT_ON_FAIL,
);
ALLOT_ON_FAIL
};
COMMANDS_PER_RESET.saturating_sub(heartbeats_converted)
}
#[cfg(test)]
mod tests {
#[test]
fn test_heartbeats_per_reset() {
assert_eq!(119, super::heartbeats_per_reset(60_000));
assert_eq!(118, super::heartbeats_per_reset(42_500));
assert_eq!(118, super::heartbeats_per_reset(30_000));
assert_eq!(117, super::heartbeats_per_reset(29_999));
}
}