From 0b8daea09c90e18ac7b8ff90a11897e7f2fb2d57 Mon Sep 17 00:00:00 2001 From: Tim Vilgot Mikael Fredenberg Date: Wed, 18 Feb 2026 14:50:01 +0100 Subject: [PATCH 1/2] relax exact gateway session resume count match Requring an exact match causes unexpected invalidations. At least 25% more guilds are supported in the worst case. --- src/main.rs | 30 ++++++++++++++++-------------- src/resume.rs | 33 ++++++++++++++++----------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/main.rs b/src/main.rs index 17e2f5b..4d2493e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,19 +36,6 @@ async fn main() -> anyhow::Result<()> { let info = async { anyhow::Ok(http.gateway().authed().await?.model().await?) } .await .context("getting info")?; - async { - http.interaction(app.id) - .set_global_commands(&command::global_commands()) - .await?; - http.interaction(app.id) - .set_guild_commands(ADMIN_GUILD_ID, &command::admin_commands(info.shards)) - .await?; - anyhow::Ok(()) - } - .await - .context("putting commands")?; - let shards = DashMap::new(); - context::init(app.id, http, shards); // The queue defaults are static and may be incorrect for large or newly // restarted bots. @@ -59,9 +46,24 @@ async fn main() -> anyhow::Result<()> { info.session_start_limit.total, ); let config = ConfigBuilder::new(token, INTENTS).queue(queue).build(); - let shards = resume::restore(config, info.shards).await; + async { + http.interaction(app.id) + .set_global_commands(&command::global_commands()) + .await?; + http.interaction(app.id) + .set_guild_commands( + ADMIN_GUILD_ID, + &command::admin_commands(shards.len() as u32), + ) + .await?; + anyhow::Ok(()) + } + .await + .context("putting commands")?; + context::init(app.id, http, DashMap::with_capacity(shards.len())); + let tasks = shards .into_iter() .map(|shard| tokio::spawn(dispatch::run(event_handler, shard, |_shard| ()))) diff --git a/src/resume.rs b/src/resume.rs index 5e2021a..9d49e61 100644 --- a/src/resume.rs +++ b/src/resume.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::iter; use tokio::fs; use twilight_gateway::{Config, ConfigBuilder, Session, Shard, ShardId}; @@ -54,33 +55,25 @@ pub async fn save(info: &[Info]) -> anyhow::Result<()> { } /// Restores shard resumption information from the file system. -pub async fn restore(config: Config, shards: u32) -> Vec { +pub async fn restore(config: Config, recommended_shards: u32) -> Vec { let info = async { let contents = fs::read(INFO_FILE).await?; anyhow::Ok(serde_json::from_slice::>(&contents)?) } .await; - let shard_ids = (0..shards).map(|shard| ShardId::new(shard, shards)); - - // A session may only be successfully resumed if it retains its shard ID, but - // Discord may have recommend a different shard count (producing different shard - // IDs). - let shards: Vec<_> = if let Ok(info) = info - && info.len() == shards as usize + // The recommended shard count targets 1000 guilds per shard (out of a maximum + // of 2500), so it might be different from the previous shard count. + let shards = if let Ok(info) = info + && recommended_shards / 2 <= info.len() as u32 { tracing::info!("resuming previous gateway sessions"); - shard_ids + let configs = iter::repeat_n(config, info.len()) .zip(info) - .map(|(shard_id, info)| { - let builder = ConfigBuilder::from(config.clone()).resume_info(info); - Shard::with_config(shard_id, builder.build()) - }) - .collect() + .map(|(config, info)| ConfigBuilder::from(config).resume_info(info).build()); + shards(configs).collect() } else { - shard_ids - .map(|shard_id| Shard::with_config(shard_id, config.clone())) - .collect() + shards(iter::repeat_n(config, recommended_shards as usize)).collect() }; // Resumed or not, the saved resume info is now stale. @@ -88,3 +81,9 @@ pub async fn restore(config: Config, shards: u32) -> Vec { shards } + +fn shards(iter: impl ExactSizeIterator) -> impl ExactSizeIterator { + let total = iter.len() as u32; + iter.zip((0..total).map(move |id| ShardId::new(id, total))) + .map(|(config, shard_id)| Shard::with_config(shard_id, config)) +} From 288f39e68fa19100e7317c71797fb18bfdc33cbb Mon Sep 17 00:00:00 2001 From: Tim Vilgot Mikael Fredenberg Date: Thu, 19 Feb 2026 19:22:27 +0100 Subject: [PATCH 2/2] move command registration out of main --- src/command.rs | 20 ++++++++++---------- src/main.rs | 16 ++-------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/command.rs b/src/command.rs index 53fe793..48315c2 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,20 +1,20 @@ mod ping; mod restart; +use crate::{ADMIN_GUILD_ID, CTX}; use twilight_model::{ - application::{ - command::Command, - interaction::{InteractionData, InteractionType}, - }, + application::interaction::{InteractionData, InteractionType}, gateway::payload::incoming::InteractionCreate, }; -pub fn admin_commands(shards: u32) -> [Command; 1] { - [restart::command(shards)] -} - -pub fn global_commands() -> [Command; 1] { - [ping::command()] +pub async fn register() -> anyhow::Result<()> { + CTX.interaction() + .set_global_commands(&[ping::command()]) + .await?; + CTX.interaction() + .set_guild_commands(ADMIN_GUILD_ID, &[restart::command(CTX.shards.len() as u32)]) + .await?; + Ok(()) } #[derive(Clone, Copy, Debug)] diff --git a/src/main.rs b/src/main.rs index 4d2493e..27bd63e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,22 +48,10 @@ async fn main() -> anyhow::Result<()> { let config = ConfigBuilder::new(token, INTENTS).queue(queue).build(); let shards = resume::restore(config, info.shards).await; - async { - http.interaction(app.id) - .set_global_commands(&command::global_commands()) - .await?; - http.interaction(app.id) - .set_guild_commands( - ADMIN_GUILD_ID, - &command::admin_commands(shards.len() as u32), - ) - .await?; - anyhow::Ok(()) - } - .await - .context("putting commands")?; context::init(app.id, http, DashMap::with_capacity(shards.len())); + command::register().await.context("registering commands")?; + let tasks = shards .into_iter() .map(|shard| tokio::spawn(dispatch::run(event_handler, shard, |_shard| ())))