Driver: Automate (re)connection logic (#81)
This PR adds several enhancements to Driver connection logic: * Driver (re)connection attempts now have a default timeout of around 10s. * The driver will now attempt to retry full connection attempts using a user-provided strategy: currently, this defaults to 5 attempts under an exponential backoff strategy. * The driver will now fire `DriverDisconnect` events at the end of any session -- this unifies (re)connection failure events with session expiry as seen in #76, which should provide users with enough detail to know *which* voice channel to reconnect to. Users still need to be careful to read the session/channel IDs to ensure that they aren't overwriting another join. This has been tested using `cargo make ready`, and by setting low timeouts to force failures in the voice receive example (with some additional error handlers). Closes #68.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
#[cfg(feature = "driver-core")]
|
#[cfg(feature = "driver-core")]
|
||||||
use super::driver::{CryptoMode, DecodeMode};
|
use super::driver::{retry::Retry, CryptoMode, DecodeMode};
|
||||||
|
|
||||||
#[cfg(feature = "gateway-core")]
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Configuration for drivers and calls.
|
/// Configuration for drivers and calls.
|
||||||
@@ -61,6 +60,20 @@ pub struct Config {
|
|||||||
/// Changes to this field in a running driver will only ever increase
|
/// Changes to this field in a running driver will only ever increase
|
||||||
/// the capacity of the track store.
|
/// the capacity of the track store.
|
||||||
pub preallocated_tracks: usize,
|
pub preallocated_tracks: usize,
|
||||||
|
#[cfg(feature = "driver-core")]
|
||||||
|
/// Connection retry logic for the [`Driver`].
|
||||||
|
///
|
||||||
|
/// This controls how many times the [`Driver`] should retry any connections,
|
||||||
|
/// as well as how long to wait between attempts.
|
||||||
|
///
|
||||||
|
/// [`Driver`]: crate::driver::Driver
|
||||||
|
pub driver_retry: Retry,
|
||||||
|
#[cfg(feature = "driver-core")]
|
||||||
|
/// Configures the maximum amount of time to wait for an attempted voice
|
||||||
|
/// connection to Discord.
|
||||||
|
///
|
||||||
|
/// Defaults to 10 seconds. If set to `None`, connections will never time out.
|
||||||
|
pub driver_timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -74,6 +87,10 @@ impl Default for Config {
|
|||||||
gateway_timeout: Some(Duration::from_secs(10)),
|
gateway_timeout: Some(Duration::from_secs(10)),
|
||||||
#[cfg(feature = "driver-core")]
|
#[cfg(feature = "driver-core")]
|
||||||
preallocated_tracks: 1,
|
preallocated_tracks: 1,
|
||||||
|
#[cfg(feature = "driver-core")]
|
||||||
|
driver_retry: Default::default(),
|
||||||
|
#[cfg(feature = "driver-core")]
|
||||||
|
driver_timeout: Some(Duration::from_secs(10)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +115,18 @@ impl Config {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets this `Config`'s timeout for establishing a voice connection.
|
||||||
|
pub fn driver_timeout(mut self, driver_timeout: Option<Duration>) -> Self {
|
||||||
|
self.driver_timeout = driver_timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets this `Config`'s voice connection retry configuration.
|
||||||
|
pub fn driver_retry(mut self, driver_retry: Retry) -> Self {
|
||||||
|
self.driver_retry = driver_retry;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// This is used to prevent changes which would invalidate the current session.
|
/// This is used to prevent changes which would invalidate the current session.
|
||||||
pub(crate) fn make_safe(&mut self, previous: &Config, connected: bool) {
|
pub(crate) fn make_safe(&mut self, previous: &Config, connected: bool) {
|
||||||
if connected {
|
if connected {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ use crate::{
|
|||||||
use flume::SendError;
|
use flume::SendError;
|
||||||
use serde_json::Error as JsonError;
|
use serde_json::Error as JsonError;
|
||||||
use std::{error::Error as StdError, fmt, io::Error as IoError};
|
use std::{error::Error as StdError, fmt, io::Error as IoError};
|
||||||
|
#[cfg(not(feature = "tokio-02-marker"))]
|
||||||
|
use tokio::time::error::Elapsed;
|
||||||
|
#[cfg(feature = "tokio-02-marker")]
|
||||||
|
use tokio_compat::time::Elapsed;
|
||||||
use xsalsa20poly1305::aead::Error as CryptoError;
|
use xsalsa20poly1305::aead::Error as CryptoError;
|
||||||
|
|
||||||
/// Errors encountered while connecting to a Discord voice server over the driver.
|
/// Errors encountered while connecting to a Discord voice server over the driver.
|
||||||
@@ -38,6 +42,8 @@ pub enum Error {
|
|||||||
InterconnectFailure(Recipient),
|
InterconnectFailure(Recipient),
|
||||||
/// Error communicating with gateway server over WebSocket.
|
/// Error communicating with gateway server over WebSocket.
|
||||||
Ws(WsError),
|
Ws(WsError),
|
||||||
|
/// Connection attempt timed out.
|
||||||
|
TimedOut,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CryptoError> for Error {
|
impl From<CryptoError> for Error {
|
||||||
@@ -82,6 +88,12 @@ impl From<WsError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Elapsed> for Error {
|
||||||
|
fn from(_e: Elapsed) -> Error {
|
||||||
|
Error::TimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "failed to connect to Discord RTP server: ")?;
|
write!(f, "failed to connect to Discord RTP server: ")?;
|
||||||
@@ -99,6 +111,7 @@ impl fmt::Display for Error {
|
|||||||
Json(e) => e.fmt(f),
|
Json(e) => e.fmt(f),
|
||||||
InterconnectFailure(e) => write!(f, "failed to contact other task ({:?})", e),
|
InterconnectFailure(e) => write!(f, "failed to contact other task ({:?})", e),
|
||||||
Ws(e) => write!(f, "websocket issue ({:?}).", e),
|
Ws(e) => write!(f, "websocket issue ({:?}).", e),
|
||||||
|
TimedOut => write!(f, "connection attempt timed out"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +131,7 @@ impl StdError for Error {
|
|||||||
Error::Json(e) => e.source(),
|
Error::Json(e) => e.source(),
|
||||||
Error::InterconnectFailure(_) => None,
|
Error::InterconnectFailure(_) => None,
|
||||||
Error::Ws(_) => None,
|
Error::Ws(_) => None,
|
||||||
|
Error::TimedOut => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ use error::{Error, Result};
|
|||||||
use flume::Sender;
|
use flume::Sender;
|
||||||
use std::{net::IpAddr, str::FromStr, sync::Arc};
|
use std::{net::IpAddr, str::FromStr, sync::Arc};
|
||||||
#[cfg(not(feature = "tokio-02-marker"))]
|
#[cfg(not(feature = "tokio-02-marker"))]
|
||||||
use tokio::{net::UdpSocket, spawn};
|
use tokio::{net::UdpSocket, spawn, time::timeout};
|
||||||
#[cfg(feature = "tokio-02-marker")]
|
#[cfg(feature = "tokio-02-marker")]
|
||||||
use tokio_compat::{net::UdpSocket, spawn};
|
use tokio_compat::{net::UdpSocket, spawn, time::timeout};
|
||||||
use tracing::{debug, info, instrument};
|
use tracing::{debug, info, instrument};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use xsalsa20poly1305::{aead::NewAead, XSalsa20Poly1305 as Cipher};
|
use xsalsa20poly1305::{aead::NewAead, XSalsa20Poly1305 as Cipher};
|
||||||
@@ -42,9 +42,23 @@ pub(crate) struct Connection {
|
|||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
pub(crate) async fn new(
|
pub(crate) async fn new(
|
||||||
|
info: ConnectionInfo,
|
||||||
|
interconnect: &Interconnect,
|
||||||
|
config: &Config,
|
||||||
|
idx: usize,
|
||||||
|
) -> Result<Connection> {
|
||||||
|
if let Some(t) = config.driver_timeout {
|
||||||
|
timeout(t, Connection::new_inner(info, interconnect, config, idx)).await?
|
||||||
|
} else {
|
||||||
|
Connection::new_inner(info, interconnect, config, idx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn new_inner(
|
||||||
mut info: ConnectionInfo,
|
mut info: ConnectionInfo,
|
||||||
interconnect: &Interconnect,
|
interconnect: &Interconnect,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
idx: usize,
|
||||||
) -> Result<Connection> {
|
) -> Result<Connection> {
|
||||||
let url = generate_url(&mut info.endpoint)?;
|
let url = generate_url(&mut info.endpoint)?;
|
||||||
|
|
||||||
@@ -207,6 +221,8 @@ impl Connection {
|
|||||||
client,
|
client,
|
||||||
ssrc,
|
ssrc,
|
||||||
hello.heartbeat_interval,
|
hello.heartbeat_interval,
|
||||||
|
idx,
|
||||||
|
info.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
spawn(udp_rx::runner(
|
spawn(udp_rx::runner(
|
||||||
@@ -226,7 +242,16 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn reconnect(&mut self) -> Result<()> {
|
pub async fn reconnect(&mut self, config: &Config) -> Result<()> {
|
||||||
|
if let Some(t) = config.driver_timeout {
|
||||||
|
timeout(t, self.reconnect_inner()).await?
|
||||||
|
} else {
|
||||||
|
self.reconnect_inner().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn reconnect_inner(&mut self) -> Result<()> {
|
||||||
let url = generate_url(&mut self.info.endpoint)?;
|
let url = generate_url(&mut self.info.endpoint)?;
|
||||||
|
|
||||||
// Thread may have died, we want to send to prompt a clean exit
|
// Thread may have died, we want to send to prompt a clean exit
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod bench_internals;
|
|||||||
pub(crate) mod connection;
|
pub(crate) mod connection;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
mod decode_mode;
|
mod decode_mode;
|
||||||
|
pub mod retry;
|
||||||
pub(crate) mod tasks;
|
pub(crate) mod tasks;
|
||||||
|
|
||||||
use connection::error::{Error, Result};
|
use connection::error::{Error, Result};
|
||||||
|
|||||||
49
src/driver/retry/mod.rs
Normal file
49
src/driver/retry/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Configuration for connection retries.
|
||||||
|
|
||||||
|
mod strategy;
|
||||||
|
|
||||||
|
pub use self::strategy::*;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Configuration to be used for retrying driver connection attempts.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Retry {
|
||||||
|
/// Strategy used to determine how long to wait between retry attempts.
|
||||||
|
///
|
||||||
|
/// *Defaults to an [`ExponentialBackoff`] from 0.25s
|
||||||
|
/// to 10s, with a jitter of `0.1`.*
|
||||||
|
///
|
||||||
|
/// [`ExponentialBackoff`]: Strategy::Backoff
|
||||||
|
pub strategy: Strategy,
|
||||||
|
/// The maximum number of retries to attempt.
|
||||||
|
///
|
||||||
|
/// `None` will attempt an infinite number of retries,
|
||||||
|
/// while `Some(0)` will attempt to connect *once* (no retries).
|
||||||
|
///
|
||||||
|
/// *Defaults to `Some(5)`.*
|
||||||
|
pub retry_limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Retry {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
strategy: Strategy::Backoff(Default::default()),
|
||||||
|
retry_limit: Some(5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Retry {
|
||||||
|
pub(crate) fn retry_in(
|
||||||
|
&self,
|
||||||
|
last_wait: Option<Duration>,
|
||||||
|
attempts: usize,
|
||||||
|
) -> Option<Duration> {
|
||||||
|
if self.retry_limit.map(|a| attempts < a).unwrap_or(true) {
|
||||||
|
Some(self.strategy.retry_in(last_wait))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/driver/retry/strategy.rs
Normal file
84
src/driver/retry/strategy.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use rand::random;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Logic used to determine how long to wait between retry attempts.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Strategy {
|
||||||
|
/// The driver will wait for the same amount of time between each retry.
|
||||||
|
Every(Duration),
|
||||||
|
/// Exponential backoff waiting strategy, where the duration between
|
||||||
|
/// attempts (approximately) doubles each time.
|
||||||
|
Backoff(ExponentialBackoff),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Strategy {
|
||||||
|
pub(crate) fn retry_in(&self, last_wait: Option<Duration>) -> Duration {
|
||||||
|
match self {
|
||||||
|
Self::Every(t) => *t,
|
||||||
|
Self::Backoff(exp) => exp.retry_in(last_wait),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exponential backoff waiting strategy.
|
||||||
|
///
|
||||||
|
/// Each attempt waits for twice the last delay plus/minus a
|
||||||
|
/// random jitter, clamped to a min and max value.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ExponentialBackoff {
|
||||||
|
/// Minimum amount of time to wait between retries.
|
||||||
|
///
|
||||||
|
/// *Defaults to 0.25s.*
|
||||||
|
pub min: Duration,
|
||||||
|
/// Maximum amount of time to wait between retries.
|
||||||
|
///
|
||||||
|
/// This will be clamped to `>=` min.
|
||||||
|
///
|
||||||
|
/// *Defaults to 10s.*
|
||||||
|
pub max: Duration,
|
||||||
|
/// Amount of uniform random jitter to apply to generated wait times.
|
||||||
|
/// I.e., 0.1 will add +/-10% to generated intervals.
|
||||||
|
///
|
||||||
|
/// This is restricted to within +/-100%.
|
||||||
|
///
|
||||||
|
/// *Defaults to `0.1`.*
|
||||||
|
pub jitter: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExponentialBackoff {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
min: Duration::from_millis(250),
|
||||||
|
max: Duration::from_secs(10),
|
||||||
|
jitter: 0.1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExponentialBackoff {
|
||||||
|
pub(crate) fn retry_in(&self, last_wait: Option<Duration>) -> Duration {
|
||||||
|
let attempt = last_wait.map(|t| 2 * t).unwrap_or(self.min);
|
||||||
|
let perturb = (1.0 - (self.jitter * 2.0 * (random::<f32>() - 1.0)))
|
||||||
|
.max(0.0)
|
||||||
|
.min(2.0);
|
||||||
|
let mut target_time = attempt.mul_f32(perturb);
|
||||||
|
|
||||||
|
// Now clamp target time into given range.
|
||||||
|
let safe_max = if self.max < self.min {
|
||||||
|
self.min
|
||||||
|
} else {
|
||||||
|
self.max
|
||||||
|
};
|
||||||
|
|
||||||
|
if target_time > safe_max {
|
||||||
|
target_time = safe_max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if target_time < self.min {
|
||||||
|
target_time = self.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
target_time
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
driver::{connection::error::Error, Bitrate, Config},
|
driver::{connection::error::Error, Bitrate, Config},
|
||||||
events::EventData,
|
events::{context_data::DisconnectReason, EventData},
|
||||||
tracks::Track,
|
tracks::Track,
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
};
|
};
|
||||||
@@ -12,6 +12,8 @@ use flume::Sender;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CoreMessage {
|
pub enum CoreMessage {
|
||||||
ConnectWithResult(ConnectionInfo, Sender<Result<(), Error>>),
|
ConnectWithResult(ConnectionInfo, Sender<Result<(), Error>>),
|
||||||
|
RetryConnect(usize),
|
||||||
|
SignalWsClosure(usize, ConnectionInfo, Option<DisconnectReason>),
|
||||||
Disconnect,
|
Disconnect,
|
||||||
SetTrack(Option<Track>),
|
SetTrack(Option<Track>),
|
||||||
AddTrack(Track),
|
AddTrack(Track),
|
||||||
|
|||||||
@@ -9,18 +9,25 @@ pub(crate) mod udp_rx;
|
|||||||
pub(crate) mod udp_tx;
|
pub(crate) mod udp_tx;
|
||||||
pub(crate) mod ws;
|
pub(crate) mod ws;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::connection::{error::Error as ConnectionError, Connection};
|
use super::connection::{error::Error as ConnectionError, Connection};
|
||||||
use crate::{
|
use crate::{
|
||||||
events::{internal_data::InternalConnect, CoreContext},
|
events::{
|
||||||
|
context_data::{DisconnectKind, DisconnectReason},
|
||||||
|
internal_data::{InternalConnect, InternalDisconnect},
|
||||||
|
CoreContext,
|
||||||
|
},
|
||||||
Config,
|
Config,
|
||||||
|
ConnectionInfo,
|
||||||
};
|
};
|
||||||
use flume::{Receiver, RecvError, Sender};
|
use flume::{Receiver, RecvError, Sender};
|
||||||
use message::*;
|
use message::*;
|
||||||
#[cfg(not(feature = "tokio-02-marker"))]
|
#[cfg(not(feature = "tokio-02-marker"))]
|
||||||
use tokio::{runtime::Handle, spawn};
|
use tokio::{runtime::Handle, spawn, time::sleep as tsleep};
|
||||||
#[cfg(feature = "tokio-02-marker")]
|
#[cfg(feature = "tokio-02-marker")]
|
||||||
use tokio_compat::{runtime::Handle, spawn};
|
use tokio_compat::{runtime::Handle, spawn, time::delay_for as tsleep};
|
||||||
use tracing::{error, instrument, trace};
|
use tracing::{debug, instrument, trace};
|
||||||
|
|
||||||
pub(crate) fn start(config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMessage>) {
|
pub(crate) fn start(config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMessage>) {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
@@ -61,8 +68,10 @@ fn start_internals(core: Sender<CoreMessage>, config: Config) -> Interconnect {
|
|||||||
#[instrument(skip(rx, tx))]
|
#[instrument(skip(rx, tx))]
|
||||||
async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMessage>) {
|
async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMessage>) {
|
||||||
let mut next_config: Option<Config> = None;
|
let mut next_config: Option<Config> = None;
|
||||||
let mut connection = None;
|
let mut connection: Option<Connection> = None;
|
||||||
let mut interconnect = start_internals(tx, config.clone());
|
let mut interconnect = start_internals(tx, config.clone());
|
||||||
|
let mut retrying = None;
|
||||||
|
let mut attempt_idx = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv_async().await {
|
match rx.recv_async().await {
|
||||||
@@ -76,36 +85,69 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
config
|
config
|
||||||
};
|
};
|
||||||
|
|
||||||
connection = match Connection::new(info, &interconnect, &config).await {
|
if connection
|
||||||
Ok(connection) => {
|
.as_ref()
|
||||||
// Other side may not be listening: this is fine.
|
.map(|conn| conn.info != info)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
// Only *actually* reconnect if the conn info changed, or we don't have an
|
||||||
|
// active connection.
|
||||||
|
// This allows the gateway component to keep sending join requests independent
|
||||||
|
// of driver failures.
|
||||||
|
connection = ConnectionRetryData::connect(tx, info, &mut attempt_idx)
|
||||||
|
.attempt(&mut retrying, &interconnect, &config)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
// No reconnection was attempted as there's a valid, identical connection;
|
||||||
|
// tell the outside listener that the operation was a success.
|
||||||
let _ = tx.send(Ok(()));
|
let _ = tx.send(Ok(()));
|
||||||
|
}
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
|
||||||
CoreContext::DriverConnect(InternalConnect {
|
|
||||||
server: connection.info.endpoint.clone(),
|
|
||||||
ssrc: connection.ssrc,
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
Some(connection)
|
|
||||||
},
|
},
|
||||||
Err(why) => {
|
Ok(CoreMessage::RetryConnect(retry_idx)) => {
|
||||||
// See above.
|
debug!("Retrying idx: {} (vs. {})", retry_idx, attempt_idx);
|
||||||
let _ = tx.send(Err(why));
|
if retry_idx == attempt_idx {
|
||||||
|
if let Some(progress) = retrying.take() {
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
connection = progress
|
||||||
CoreContext::DriverConnectFailed,
|
.attempt(&mut retrying, &interconnect, &config)
|
||||||
));
|
.await;
|
||||||
|
}
|
||||||
None
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
Ok(CoreMessage::Disconnect) => {
|
Ok(CoreMessage::Disconnect) => {
|
||||||
|
let last_conn = connection.take();
|
||||||
|
let _ = interconnect.mixer.send(MixerMessage::DropConn);
|
||||||
|
let _ = interconnect.mixer.send(MixerMessage::RebuildEncoder);
|
||||||
|
|
||||||
|
if let Some(conn) = last_conn {
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverDisconnect(InternalDisconnect {
|
||||||
|
kind: DisconnectKind::Runtime,
|
||||||
|
reason: None,
|
||||||
|
info: conn.info.clone(),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(CoreMessage::SignalWsClosure(ws_idx, ws_info, mut reason)) => {
|
||||||
|
// if idx is not a match, quash reason
|
||||||
|
// (i.e., prevent users from mistakenly trying to reconnect for an *old* dead conn).
|
||||||
|
// if it *is* a match, the conn needs to die!
|
||||||
|
// (as the WS channel has truly given up the ghost).
|
||||||
|
if ws_idx != attempt_idx {
|
||||||
|
reason = None;
|
||||||
|
} else {
|
||||||
connection = None;
|
connection = None;
|
||||||
let _ = interconnect.mixer.send(MixerMessage::DropConn);
|
let _ = interconnect.mixer.send(MixerMessage::DropConn);
|
||||||
let _ = interconnect.mixer.send(MixerMessage::RebuildEncoder);
|
let _ = interconnect.mixer.send(MixerMessage::RebuildEncoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverDisconnect(InternalDisconnect {
|
||||||
|
kind: DisconnectKind::Runtime,
|
||||||
|
reason,
|
||||||
|
info: ws_info,
|
||||||
|
}),
|
||||||
|
));
|
||||||
},
|
},
|
||||||
Ok(CoreMessage::SetTrack(s)) => {
|
Ok(CoreMessage::SetTrack(s)) => {
|
||||||
let _ = interconnect.mixer.send(MixerMessage::SetTrack(s));
|
let _ = interconnect.mixer.send(MixerMessage::SetTrack(s));
|
||||||
@@ -138,7 +180,7 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
// if still issue, full connect.
|
// if still issue, full connect.
|
||||||
let info = conn.info.clone();
|
let info = conn.info.clone();
|
||||||
|
|
||||||
let full_connect = match conn.reconnect().await {
|
let full_connect = match conn.reconnect(&config).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
connection = Some(conn);
|
connection = Some(conn);
|
||||||
false
|
false
|
||||||
@@ -146,7 +188,7 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
Err(ConnectionError::InterconnectFailure(_)) => {
|
Err(ConnectionError::InterconnectFailure(_)) => {
|
||||||
interconnect.restart_volatile_internals();
|
interconnect.restart_volatile_internals();
|
||||||
|
|
||||||
match conn.reconnect().await {
|
match conn.reconnect(&config).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
connection = Some(conn);
|
connection = Some(conn);
|
||||||
false
|
false
|
||||||
@@ -158,22 +200,13 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
};
|
};
|
||||||
|
|
||||||
if full_connect {
|
if full_connect {
|
||||||
connection = Connection::new(info, &interconnect, &config)
|
connection = ConnectionRetryData::reconnect(info, &mut attempt_idx)
|
||||||
.await
|
.attempt(&mut retrying, &interconnect, &config)
|
||||||
.map_err(|e| {
|
.await;
|
||||||
error!("Catastrophic connection failure. Stopping. {:?}", e);
|
} else if let Some(ref connection) = &connection {
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
|
||||||
CoreContext::DriverReconnectFailed,
|
|
||||||
));
|
|
||||||
e
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref connection) = &connection {
|
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
CoreContext::DriverReconnect(InternalConnect {
|
CoreContext::DriverReconnect(InternalConnect {
|
||||||
server: connection.info.endpoint.clone(),
|
info: connection.info.clone(),
|
||||||
ssrc: connection.ssrc,
|
ssrc: connection.ssrc,
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
@@ -184,25 +217,9 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
if let Some(conn) = connection.take() {
|
if let Some(conn) = connection.take() {
|
||||||
let info = conn.info.clone();
|
let info = conn.info.clone();
|
||||||
|
|
||||||
connection = Connection::new(info, &interconnect, &config)
|
connection = ConnectionRetryData::reconnect(info, &mut attempt_idx)
|
||||||
.await
|
.attempt(&mut retrying, &interconnect, &config)
|
||||||
.map_err(|e| {
|
.await;
|
||||||
error!("Catastrophic connection failure. Stopping. {:?}", e);
|
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
|
||||||
CoreContext::DriverReconnectFailed,
|
|
||||||
));
|
|
||||||
e
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
if let Some(ref connection) = &connection {
|
|
||||||
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
|
||||||
CoreContext::DriverReconnect(InternalConnect {
|
|
||||||
server: connection.info.endpoint.clone(),
|
|
||||||
ssrc: connection.ssrc,
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Ok(CoreMessage::RebuildInterconnect) => {
|
Ok(CoreMessage::RebuildInterconnect) => {
|
||||||
interconnect.restart_volatile_internals();
|
interconnect.restart_volatile_internals();
|
||||||
@@ -216,3 +233,138 @@ async fn runner(mut config: Config, rx: Receiver<CoreMessage>, tx: Sender<CoreMe
|
|||||||
trace!("Main thread exited");
|
trace!("Main thread exited");
|
||||||
interconnect.poison_all();
|
interconnect.poison_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ConnectionRetryData {
|
||||||
|
flavour: ConnectionFlavour,
|
||||||
|
attempts: usize,
|
||||||
|
last_wait: Option<Duration>,
|
||||||
|
info: ConnectionInfo,
|
||||||
|
idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionRetryData {
|
||||||
|
fn connect(
|
||||||
|
tx: Sender<Result<(), ConnectionError>>,
|
||||||
|
info: ConnectionInfo,
|
||||||
|
idx_src: &mut usize,
|
||||||
|
) -> Self {
|
||||||
|
Self::base(ConnectionFlavour::Connect(tx), info, idx_src)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconnect(info: ConnectionInfo, idx_src: &mut usize) -> Self {
|
||||||
|
Self::base(ConnectionFlavour::Reconnect, info, idx_src)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base(flavour: ConnectionFlavour, info: ConnectionInfo, idx_src: &mut usize) -> Self {
|
||||||
|
*idx_src = idx_src.wrapping_add(1);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
flavour,
|
||||||
|
attempts: 0,
|
||||||
|
last_wait: None,
|
||||||
|
info,
|
||||||
|
idx: *idx_src,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn attempt(
|
||||||
|
mut self,
|
||||||
|
attempt_slot: &mut Option<Self>,
|
||||||
|
interconnect: &Interconnect,
|
||||||
|
config: &Config,
|
||||||
|
) -> Option<Connection> {
|
||||||
|
match Connection::new(self.info.clone(), interconnect, config, self.idx).await {
|
||||||
|
Ok(connection) => {
|
||||||
|
match self.flavour {
|
||||||
|
ConnectionFlavour::Connect(tx) => {
|
||||||
|
// Other side may not be listening: this is fine.
|
||||||
|
let _ = tx.send(Ok(()));
|
||||||
|
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverConnect(InternalConnect {
|
||||||
|
info: connection.info.clone(),
|
||||||
|
ssrc: connection.ssrc,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
ConnectionFlavour::Reconnect => {
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverReconnect(InternalConnect {
|
||||||
|
info: connection.info.clone(),
|
||||||
|
ssrc: connection.ssrc,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(connection)
|
||||||
|
},
|
||||||
|
Err(why) => {
|
||||||
|
debug!("Failed to connect for {:?}: {}", self.info.guild_id, why);
|
||||||
|
if let Some(t) = config.driver_retry.retry_in(self.last_wait, self.attempts) {
|
||||||
|
let remote_ic = interconnect.clone();
|
||||||
|
let idx = self.idx;
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
tsleep(t).await;
|
||||||
|
let _ = remote_ic.core.send(CoreMessage::RetryConnect(idx));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.attempts += 1;
|
||||||
|
self.last_wait = Some(t);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Retrying connection for {:?} in {}s ({}/{:?})",
|
||||||
|
self.info.guild_id,
|
||||||
|
t.as_secs_f32(),
|
||||||
|
self.attempts,
|
||||||
|
config.driver_retry.retry_limit
|
||||||
|
);
|
||||||
|
|
||||||
|
*attempt_slot = Some(self);
|
||||||
|
} else {
|
||||||
|
let reason = Some(DisconnectReason::from(&why));
|
||||||
|
|
||||||
|
match self.flavour {
|
||||||
|
ConnectionFlavour::Connect(tx) => {
|
||||||
|
// See above.
|
||||||
|
let _ = tx.send(Err(why));
|
||||||
|
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverConnectFailed,
|
||||||
|
));
|
||||||
|
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverDisconnect(InternalDisconnect {
|
||||||
|
kind: DisconnectKind::Connect,
|
||||||
|
reason,
|
||||||
|
info: self.info,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
ConnectionFlavour::Reconnect => {
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverReconnectFailed,
|
||||||
|
));
|
||||||
|
|
||||||
|
let _ = interconnect.events.send(EventMessage::FireCoreEvent(
|
||||||
|
CoreContext::DriverDisconnect(InternalDisconnect {
|
||||||
|
kind: DisconnectKind::Reconnect,
|
||||||
|
reason,
|
||||||
|
info: self.info,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectionFlavour {
|
||||||
|
Connect(Sender<Result<(), ConnectionError>>),
|
||||||
|
Reconnect,
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::{
|
|||||||
SpeakingState,
|
SpeakingState,
|
||||||
},
|
},
|
||||||
ws::{Error as WsError, ReceiverExt, SenderExt, WsStream},
|
ws::{Error as WsError, ReceiverExt, SenderExt, WsStream},
|
||||||
|
ConnectionInfo,
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "tokio-02-marker"))]
|
#[cfg(not(feature = "tokio-02-marker"))]
|
||||||
use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||||
@@ -39,6 +40,9 @@ struct AuxNetwork {
|
|||||||
|
|
||||||
speaking: SpeakingState,
|
speaking: SpeakingState,
|
||||||
last_heartbeat_nonce: Option<u64>,
|
last_heartbeat_nonce: Option<u64>,
|
||||||
|
|
||||||
|
attempt_idx: usize,
|
||||||
|
info: ConnectionInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuxNetwork {
|
impl AuxNetwork {
|
||||||
@@ -47,6 +51,8 @@ impl AuxNetwork {
|
|||||||
ws_client: WsStream,
|
ws_client: WsStream,
|
||||||
ssrc: u32,
|
ssrc: u32,
|
||||||
heartbeat_interval: f64,
|
heartbeat_interval: f64,
|
||||||
|
attempt_idx: usize,
|
||||||
|
info: ConnectionInfo,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rx: evt_rx,
|
rx: evt_rx,
|
||||||
@@ -58,6 +64,9 @@ impl AuxNetwork {
|
|||||||
|
|
||||||
speaking: SpeakingState::empty(),
|
speaking: SpeakingState::empty(),
|
||||||
last_heartbeat_nonce: None,
|
last_heartbeat_nonce: None,
|
||||||
|
|
||||||
|
attempt_idx,
|
||||||
|
info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +77,7 @@ impl AuxNetwork {
|
|||||||
loop {
|
loop {
|
||||||
let mut ws_error = false;
|
let mut ws_error = false;
|
||||||
let mut should_reconnect = false;
|
let mut should_reconnect = false;
|
||||||
|
let mut ws_reason = None;
|
||||||
|
|
||||||
let hb = sleep_until(next_heartbeat);
|
let hb = sleep_until(next_heartbeat);
|
||||||
|
|
||||||
@@ -75,7 +85,8 @@ impl AuxNetwork {
|
|||||||
_ = hb => {
|
_ = hb => {
|
||||||
ws_error = match self.send_heartbeat().await {
|
ws_error = match self.send_heartbeat().await {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
should_reconnect = ws_error_is_not_final(e);
|
should_reconnect = ws_error_is_not_final(&e);
|
||||||
|
ws_reason = Some((&e).into());
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -89,7 +100,8 @@ impl AuxNetwork {
|
|||||||
false
|
false
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
should_reconnect = ws_error_is_not_final(e);
|
should_reconnect = ws_error_is_not_final(&e);
|
||||||
|
ws_reason = Some((&e).into());
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
@@ -129,7 +141,8 @@ impl AuxNetwork {
|
|||||||
|
|
||||||
ws_error |= match ssu_status {
|
ws_error |= match ssu_status {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
should_reconnect = ws_error_is_not_final(e);
|
should_reconnect = ws_error_is_not_final(&e);
|
||||||
|
ws_reason = Some((&e).into());
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -149,6 +162,11 @@ impl AuxNetwork {
|
|||||||
if should_reconnect {
|
if should_reconnect {
|
||||||
let _ = interconnect.core.send(CoreMessage::Reconnect);
|
let _ = interconnect.core.send(CoreMessage::Reconnect);
|
||||||
} else {
|
} else {
|
||||||
|
let _ = interconnect.core.send(CoreMessage::SignalWsClosure(
|
||||||
|
self.attempt_idx,
|
||||||
|
self.info.clone(),
|
||||||
|
ws_reason,
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,15 +235,24 @@ pub(crate) async fn runner(
|
|||||||
ws_client: WsStream,
|
ws_client: WsStream,
|
||||||
ssrc: u32,
|
ssrc: u32,
|
||||||
heartbeat_interval: f64,
|
heartbeat_interval: f64,
|
||||||
|
attempt_idx: usize,
|
||||||
|
info: ConnectionInfo,
|
||||||
) {
|
) {
|
||||||
trace!("WS thread started.");
|
trace!("WS thread started.");
|
||||||
let mut aux = AuxNetwork::new(evt_rx, ws_client, ssrc, heartbeat_interval);
|
let mut aux = AuxNetwork::new(
|
||||||
|
evt_rx,
|
||||||
|
ws_client,
|
||||||
|
ssrc,
|
||||||
|
heartbeat_interval,
|
||||||
|
attempt_idx,
|
||||||
|
info,
|
||||||
|
);
|
||||||
|
|
||||||
aux.run(&mut interconnect).await;
|
aux.run(&mut interconnect).await;
|
||||||
trace!("WS thread finished.");
|
trace!("WS thread finished.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ws_error_is_not_final(err: WsError) -> bool {
|
fn ws_error_is_not_final(err: &WsError) -> bool {
|
||||||
match err {
|
match err {
|
||||||
WsError::WsClosed(Some(frame)) => match frame.code {
|
WsError::WsClosed(Some(frame)) => match frame.code {
|
||||||
CloseCode::Library(l) =>
|
CloseCode::Library(l) =>
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
use crate::id::*;
|
||||||
|
|
||||||
/// Voice connection details gathered at setup/reinstantiation.
|
/// Voice connection details gathered at setup/reinstantiation.
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct ConnectData<'a> {
|
pub struct ConnectData<'a> {
|
||||||
|
/// ID of the voice channel being joined, if it is known.
|
||||||
|
///
|
||||||
|
/// If this is available, then this can be used to reconnect/renew
|
||||||
|
/// a voice session via thew gateway.
|
||||||
|
pub channel_id: Option<ChannelId>,
|
||||||
|
/// ID of the target voice channel's parent guild.
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
/// Unique string describing this session for validation/authentication purposes.
|
||||||
|
pub session_id: &'a str,
|
||||||
/// The domain name of Discord's voice/TURN server.
|
/// The domain name of Discord's voice/TURN server.
|
||||||
///
|
///
|
||||||
/// With the introduction of Discord's automatic voice server selection,
|
/// With the introduction of Discord's automatic voice server selection,
|
||||||
|
|||||||
119
src/events/context/data/disconnect.rs
Normal file
119
src/events/context/data/disconnect.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use crate::{
|
||||||
|
error::ConnectionError,
|
||||||
|
id::*,
|
||||||
|
model::{CloseCode as VoiceCloseCode, FromPrimitive},
|
||||||
|
ws::Error as WsError,
|
||||||
|
};
|
||||||
|
#[cfg(not(feature = "tokio-02-marker"))]
|
||||||
|
use async_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||||
|
#[cfg(feature = "tokio-02-marker")]
|
||||||
|
use async_tungstenite_compat::tungstenite::protocol::frame::coding::CloseCode;
|
||||||
|
|
||||||
|
/// Voice connection details gathered at termination or failure.
|
||||||
|
///
|
||||||
|
/// In the event of a failure, this event data is gathered after
|
||||||
|
/// a reconnection strategy has exhausted all of its attempts.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct DisconnectData<'a> {
|
||||||
|
/// The location that a voice connection was terminated.
|
||||||
|
pub kind: DisconnectKind,
|
||||||
|
/// The cause of any connection failure.
|
||||||
|
///
|
||||||
|
/// If `None`, then this disconnect was requested by the user in some way
|
||||||
|
/// (i.e., leaving or changing voice channels).
|
||||||
|
pub reason: Option<DisconnectReason>,
|
||||||
|
/// ID of the voice channel being joined, if it is known.
|
||||||
|
///
|
||||||
|
/// If this is available, then this can be used to reconnect/renew
|
||||||
|
/// a voice session via thew gateway.
|
||||||
|
pub channel_id: Option<ChannelId>,
|
||||||
|
/// ID of the target voice channel's parent guild.
|
||||||
|
pub guild_id: GuildId,
|
||||||
|
/// Unique string describing this session for validation/authentication purposes.
|
||||||
|
pub session_id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The location that a voice connection was terminated.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum DisconnectKind {
|
||||||
|
/// The voice driver failed to connect to the server.
|
||||||
|
///
|
||||||
|
/// This requires explicit handling at the gateway level
|
||||||
|
/// to either reconnect or fully disconnect.
|
||||||
|
Connect,
|
||||||
|
/// The voice driver failed to reconnect to the server.
|
||||||
|
///
|
||||||
|
/// This requires explicit handling at the gateway level
|
||||||
|
/// to either reconnect or fully disconnect.
|
||||||
|
Reconnect,
|
||||||
|
/// The voice connection was terminated mid-session by either
|
||||||
|
/// the user or Discord.
|
||||||
|
///
|
||||||
|
/// If `reason == None`, then this disconnection is either
|
||||||
|
/// a full disconnect or a user-requested channel change.
|
||||||
|
/// Otherwise, this is likely a session expiry (requiring user
|
||||||
|
/// handling to fully disconnect/reconnect).
|
||||||
|
Runtime,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reason that a voice connection failed.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum DisconnectReason {
|
||||||
|
/// This (re)connection attempt was dropped due to another request.
|
||||||
|
AttemptDiscarded,
|
||||||
|
/// Songbird had an internal error.
|
||||||
|
///
|
||||||
|
/// This should never happen; if this is ever seen, raise an issue with logs.
|
||||||
|
Internal,
|
||||||
|
/// A host-specific I/O error caused the fault; this is likely transient, and
|
||||||
|
/// should be retried some time later.
|
||||||
|
Io,
|
||||||
|
/// Songbird and Discord disagreed on the protocol used to establish a
|
||||||
|
/// voice connection.
|
||||||
|
///
|
||||||
|
/// This should never happen; if this is ever seen, raise an issue with logs.
|
||||||
|
ProtocolViolation,
|
||||||
|
/// A voice connection was not established in the specified time.
|
||||||
|
TimedOut,
|
||||||
|
/// The Websocket connection was closed by Discord.
|
||||||
|
///
|
||||||
|
/// This typically indicates that the voice session has expired,
|
||||||
|
/// and a new one needs to be requested via the gateway.
|
||||||
|
WsClosed(Option<VoiceCloseCode>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ConnectionError> for DisconnectReason {
|
||||||
|
fn from(e: &ConnectionError) -> Self {
|
||||||
|
use ConnectionError::*;
|
||||||
|
|
||||||
|
match e {
|
||||||
|
AttemptDiscarded => Self::AttemptDiscarded,
|
||||||
|
CryptoModeInvalid
|
||||||
|
| CryptoModeUnavailable
|
||||||
|
| EndpointUrl
|
||||||
|
| ExpectedHandshake
|
||||||
|
| IllegalDiscoveryResponse
|
||||||
|
| IllegalIp
|
||||||
|
| Json(_) => Self::ProtocolViolation,
|
||||||
|
Io(_) => Self::Io,
|
||||||
|
Crypto(_) | InterconnectFailure(_) => Self::Internal,
|
||||||
|
Ws(ws) => ws.into(),
|
||||||
|
TimedOut => Self::TimedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&WsError> for DisconnectReason {
|
||||||
|
fn from(e: &WsError) -> Self {
|
||||||
|
Self::WsClosed(match e {
|
||||||
|
WsError::WsClosed(Some(frame)) => match frame.code {
|
||||||
|
CloseCode::Library(l) => VoiceCloseCode::from_u16(l),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
//!
|
//!
|
||||||
//! [`EventContext`]: super::EventContext
|
//! [`EventContext`]: super::EventContext
|
||||||
mod connect;
|
mod connect;
|
||||||
|
mod disconnect;
|
||||||
mod rtcp;
|
mod rtcp;
|
||||||
mod speaking;
|
mod speaking;
|
||||||
mod voice;
|
mod voice;
|
||||||
|
|
||||||
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
||||||
|
|
||||||
pub use self::{connect::*, rtcp::*, speaking::*, voice::*};
|
pub use self::{connect::*, disconnect::*, rtcp::*, speaking::*, voice::*};
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
use super::context_data::*;
|
use super::context_data::*;
|
||||||
|
use crate::ConnectionInfo;
|
||||||
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
use discortp::{rtcp::Rtcp, rtp::Rtp};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct InternalConnect {
|
pub struct InternalConnect {
|
||||||
pub server: String,
|
pub info: ConnectionInfo,
|
||||||
pub ssrc: u32,
|
pub ssrc: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InternalDisconnect {
|
||||||
|
pub kind: DisconnectKind,
|
||||||
|
pub reason: Option<DisconnectReason>,
|
||||||
|
pub info: ConnectionInfo,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct InternalSpeakingUpdate {
|
pub struct InternalSpeakingUpdate {
|
||||||
pub ssrc: u32,
|
pub ssrc: u32,
|
||||||
@@ -31,12 +39,27 @@ pub struct InternalRtcpPacket {
|
|||||||
impl<'a> From<&'a InternalConnect> for ConnectData<'a> {
|
impl<'a> From<&'a InternalConnect> for ConnectData<'a> {
|
||||||
fn from(val: &'a InternalConnect) -> Self {
|
fn from(val: &'a InternalConnect) -> Self {
|
||||||
Self {
|
Self {
|
||||||
server: &val.server,
|
channel_id: val.info.channel_id,
|
||||||
|
guild_id: val.info.guild_id,
|
||||||
|
session_id: &val.info.session_id,
|
||||||
|
server: &val.info.endpoint,
|
||||||
ssrc: val.ssrc,
|
ssrc: val.ssrc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a InternalDisconnect> for DisconnectData<'a> {
|
||||||
|
fn from(val: &'a InternalDisconnect) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: val.kind,
|
||||||
|
reason: val.reason,
|
||||||
|
channel_id: val.info.channel_id,
|
||||||
|
guild_id: val.info.guild_id,
|
||||||
|
session_id: &val.info.session_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a InternalSpeakingUpdate> for SpeakingUpdateData {
|
impl<'a> From<&'a InternalSpeakingUpdate> for SpeakingUpdateData {
|
||||||
fn from(val: &'a InternalSpeakingUpdate) -> Self {
|
fn from(val: &'a InternalSpeakingUpdate) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use internal_data::*;
|
|||||||
///
|
///
|
||||||
/// [`Track`]: crate::tracks::Track
|
/// [`Track`]: crate::tracks::Track
|
||||||
/// [`Driver::add_global_event`]: crate::driver::Driver::add_global_event
|
/// [`Driver::add_global_event`]: crate::driver::Driver::add_global_event
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum EventContext<'a> {
|
pub enum EventContext<'a> {
|
||||||
/// Track event context, passed to events created via [`TrackHandle::add_event`],
|
/// Track event context, passed to events created via [`TrackHandle::add_event`],
|
||||||
@@ -47,12 +47,32 @@ pub enum EventContext<'a> {
|
|||||||
DriverConnect(ConnectData<'a>),
|
DriverConnect(ConnectData<'a>),
|
||||||
/// Fires when this driver successfully reconnects after a network error.
|
/// Fires when this driver successfully reconnects after a network error.
|
||||||
DriverReconnect(ConnectData<'a>),
|
DriverReconnect(ConnectData<'a>),
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "Please use the DriverDisconnect event instead."
|
||||||
|
)]
|
||||||
/// Fires when this driver fails to connect to a voice channel.
|
/// Fires when this driver fails to connect to a voice channel.
|
||||||
|
///
|
||||||
|
/// Users will need to manually reconnect on receipt of this error.
|
||||||
|
/// **This event is deprecated in favour of [`DriverDisconnect`].**
|
||||||
|
///
|
||||||
|
/// [`DriverDisconnect`]: Self::DriverDisconnect
|
||||||
|
// TODO: remove in 0.3.x
|
||||||
DriverConnectFailed,
|
DriverConnectFailed,
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "Please use the DriverDisconnect event instead."
|
||||||
|
)]
|
||||||
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
||||||
///
|
///
|
||||||
/// Users will need to manually reconnect on receipt of this error.
|
/// Users will need to manually reconnect on receipt of this error.
|
||||||
|
/// **This event is deprecated in favour of [`DriverDisconnect`].**
|
||||||
|
///
|
||||||
|
/// [`DriverDisconnect`]: Self::DriverDisconnect
|
||||||
|
// TODO: remove in 0.3.x
|
||||||
DriverReconnectFailed,
|
DriverReconnectFailed,
|
||||||
|
/// Fires when this driver fails to connect to, or drops from, a voice channel.
|
||||||
|
DriverDisconnect(DisconnectData<'a>),
|
||||||
#[deprecated(
|
#[deprecated(
|
||||||
since = "0.2.0",
|
since = "0.2.0",
|
||||||
note = "Please use the DriverConnect/Reconnect events instead."
|
note = "Please use the DriverConnect/Reconnect events instead."
|
||||||
@@ -69,7 +89,7 @@ pub enum EventContext<'a> {
|
|||||||
SsrcKnown(u32),
|
SsrcKnown(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CoreContext {
|
pub enum CoreContext {
|
||||||
SpeakingStateUpdate(Speaking),
|
SpeakingStateUpdate(Speaking),
|
||||||
SpeakingUpdate(InternalSpeakingUpdate),
|
SpeakingUpdate(InternalSpeakingUpdate),
|
||||||
@@ -79,6 +99,7 @@ pub enum CoreContext {
|
|||||||
ClientDisconnect(ClientDisconnect),
|
ClientDisconnect(ClientDisconnect),
|
||||||
DriverConnect(InternalConnect),
|
DriverConnect(InternalConnect),
|
||||||
DriverReconnect(InternalConnect),
|
DriverReconnect(InternalConnect),
|
||||||
|
DriverDisconnect(InternalDisconnect),
|
||||||
DriverConnectFailed,
|
DriverConnectFailed,
|
||||||
DriverReconnectFailed,
|
DriverReconnectFailed,
|
||||||
SsrcKnown(u32),
|
SsrcKnown(u32),
|
||||||
@@ -97,7 +118,10 @@ impl<'a> CoreContext {
|
|||||||
ClientDisconnect(evt) => EventContext::ClientDisconnect(*evt),
|
ClientDisconnect(evt) => EventContext::ClientDisconnect(*evt),
|
||||||
DriverConnect(evt) => EventContext::DriverConnect(ConnectData::from(evt)),
|
DriverConnect(evt) => EventContext::DriverConnect(ConnectData::from(evt)),
|
||||||
DriverReconnect(evt) => EventContext::DriverReconnect(ConnectData::from(evt)),
|
DriverReconnect(evt) => EventContext::DriverReconnect(ConnectData::from(evt)),
|
||||||
|
DriverDisconnect(evt) => EventContext::DriverDisconnect(DisconnectData::from(evt)),
|
||||||
|
#[allow(deprecated)]
|
||||||
DriverConnectFailed => EventContext::DriverConnectFailed,
|
DriverConnectFailed => EventContext::DriverConnectFailed,
|
||||||
|
#[allow(deprecated)]
|
||||||
DriverReconnectFailed => EventContext::DriverReconnectFailed,
|
DriverReconnectFailed => EventContext::DriverReconnectFailed,
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
SsrcKnown(s) => EventContext::SsrcKnown(*s),
|
SsrcKnown(s) => EventContext::SsrcKnown(*s),
|
||||||
@@ -112,15 +136,18 @@ impl EventContext<'_> {
|
|||||||
use EventContext::*;
|
use EventContext::*;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
SpeakingStateUpdate { .. } => Some(CoreEvent::SpeakingStateUpdate),
|
SpeakingStateUpdate(_) => Some(CoreEvent::SpeakingStateUpdate),
|
||||||
SpeakingUpdate { .. } => Some(CoreEvent::SpeakingUpdate),
|
SpeakingUpdate(_) => Some(CoreEvent::SpeakingUpdate),
|
||||||
VoicePacket { .. } => Some(CoreEvent::VoicePacket),
|
VoicePacket(_) => Some(CoreEvent::VoicePacket),
|
||||||
RtcpPacket { .. } => Some(CoreEvent::RtcpPacket),
|
RtcpPacket(_) => Some(CoreEvent::RtcpPacket),
|
||||||
ClientConnect { .. } => Some(CoreEvent::ClientConnect),
|
ClientConnect(_) => Some(CoreEvent::ClientConnect),
|
||||||
ClientDisconnect { .. } => Some(CoreEvent::ClientDisconnect),
|
ClientDisconnect(_) => Some(CoreEvent::ClientDisconnect),
|
||||||
DriverConnect(_) => Some(CoreEvent::DriverConnect),
|
DriverConnect(_) => Some(CoreEvent::DriverConnect),
|
||||||
DriverReconnect(_) => Some(CoreEvent::DriverReconnect),
|
DriverReconnect(_) => Some(CoreEvent::DriverReconnect),
|
||||||
|
DriverDisconnect(_) => Some(CoreEvent::DriverDisconnect),
|
||||||
|
#[allow(deprecated)]
|
||||||
DriverConnectFailed => Some(CoreEvent::DriverConnectFailed),
|
DriverConnectFailed => Some(CoreEvent::DriverConnectFailed),
|
||||||
|
#[allow(deprecated)]
|
||||||
DriverReconnectFailed => Some(CoreEvent::DriverReconnectFailed),
|
DriverReconnectFailed => Some(CoreEvent::DriverReconnectFailed),
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
SsrcKnown(_) => Some(CoreEvent::SsrcKnown),
|
SsrcKnown(_) => Some(CoreEvent::SsrcKnown),
|
||||||
|
|||||||
@@ -33,12 +33,22 @@ pub enum CoreEvent {
|
|||||||
DriverConnect,
|
DriverConnect,
|
||||||
/// Fires when this driver successfully reconnects after a network error.
|
/// Fires when this driver successfully reconnects after a network error.
|
||||||
DriverReconnect,
|
DriverReconnect,
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "Please use the DriverDisconnect event instead."
|
||||||
|
)]
|
||||||
/// Fires when this driver fails to connect to a voice channel.
|
/// Fires when this driver fails to connect to a voice channel.
|
||||||
DriverConnectFailed,
|
DriverConnectFailed,
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "Please use the DriverDisconnect event instead."
|
||||||
|
)]
|
||||||
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
/// Fires when this driver fails to reconnect to a voice channel after a network error.
|
||||||
///
|
///
|
||||||
/// Users will need to manually reconnect on receipt of this error.
|
/// Users will need to manually reconnect on receipt of this error.
|
||||||
DriverReconnectFailed,
|
DriverReconnectFailed,
|
||||||
|
/// Fires when this driver fails to connect to, or drops from, a voice channel.
|
||||||
|
DriverDisconnect,
|
||||||
/// Fires whenever the driver is assigned a new [RTP SSRC] by the voice server.
|
/// Fires whenever the driver is assigned a new [RTP SSRC] by the voice server.
|
||||||
///
|
///
|
||||||
/// This typically fires alongside a [DriverConnect], or a full [DriverReconnect].
|
/// This typically fires alongside a [DriverConnect], or a full [DriverReconnect].
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ impl Call {
|
|||||||
let (gw_tx, gw_rx) = flume::unbounded();
|
let (gw_tx, gw_rx) = flume::unbounded();
|
||||||
|
|
||||||
let do_conn = self
|
let do_conn = self
|
||||||
.should_actually_join(|_| Ok(()), &tx, channel_id)
|
.should_actually_join(|_| (), &gw_tx, channel_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if do_conn {
|
if do_conn {
|
||||||
@@ -218,6 +218,14 @@ impl Call {
|
|||||||
.await
|
.await
|
||||||
.map(|_| Join::new(rx.into_recv_async(), gw_rx.into_recv_async(), timeout))
|
.map(|_| Join::new(rx.into_recv_async(), gw_rx.into_recv_async(), timeout))
|
||||||
} else {
|
} else {
|
||||||
|
// Skipping the gateway connection implies that the current connection is complete
|
||||||
|
// AND the channel is a match.
|
||||||
|
//
|
||||||
|
// Send a polite request to the driver, which should only *actually* reconnect
|
||||||
|
// if it had a problem earlier.
|
||||||
|
let info = self.current_connection().unwrap().clone();
|
||||||
|
self.driver.raw_connect(info, tx.clone());
|
||||||
|
|
||||||
Ok(Join::new(
|
Ok(Join::new(
|
||||||
rx.into_recv_async(),
|
rx.into_recv_async(),
|
||||||
gw_rx.into_recv_async(),
|
gw_rx.into_recv_async(),
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ impl ConnectionProgress {
|
|||||||
|
|
||||||
/// Parameters and information needed to start communicating with Discord's voice servers, either
|
/// Parameters and information needed to start communicating with Discord's voice servers, either
|
||||||
/// with the Songbird driver, lavalink, or other system.
|
/// with the Songbird driver, lavalink, or other system.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||||
pub struct ConnectionInfo {
|
pub struct ConnectionInfo {
|
||||||
/// ID of the voice channel being joined, if it is known.
|
/// ID of the voice channel being joined, if it is known.
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user