Search code examples
multithreadingrustmio

How can I combine discord-rs events with other events from Twitter or timers?


I have written a bot for the Discord chat service using the discord-rs library. This library gives me events when they arise in a single thread in a main loop:

fn start() {
    // ...
    loop {
        let event = match connection.recv_event() {
            Ok(event) => event,
            Err(err) => { ... },
        }
    }
}

I want to add some timers and other things which are calculated in their own threads and which must notify me to do something in the main loop's thread. I also want to add Twitter support. So it may look as this:

(Discord's network connection, Twitter network connection, some timer in another thread) -> main loop

This will look something like this:

fn start() {
    // ...
    loop {
        let event = match recv_events() {
            // 1. if Discord - do something with discord
            // 2. if timer - handle timer's notification
            // 3. if Twitter network connection - handle twitter
        }
    }
}

In raw C and C sockets, it could be done by (e)polling them but here I have no idea how to do that in Rust or if it is even possible. I think I want something like poll of few different sources which would provide me objects of different types.

I guess this could be implemented if I provide a wrapper for mio's Evented trait and use mio's poll as described in the Deadline example.

Is there any way to combine these events?


Solution

  • This library gives me events when they arise in a single thread in a main loop

    The "single thread" thing is only true for small bots. As soon as you reach the 2500 guilds limit, Discord will refuse to connect your bot in a normal way. You'll have to use sharding. And I guess you're not going to provision new virtual servers for your bot shards. Chance is, you will spawn new threads instead, one event loop per shard.

    Here is how I do it, BTW:

    fn event_loop(shard_id: u8, total_shards: u8){
        loop {
            let bot = Discord::from_bot_token("...").expect("!from_bot_token");
            let (mut dc, ready_ev) = bot.connect_sharded(shard_id, total_shards).expect("!connect");
            // ...
        }
    }
    
    fn main() {
        let total_shards = 10;
        for shard_id in 0..total_shards {
            sleep(Duration::from_secs(6)); // There must be a five-second pause between connections from one IP.
            ThreadBuilder::new().name (fomat! ("shard " (shard_id)))
              .spawn (move || {
                loop {
                  if let Err (err) = catch_unwind (move || event_loop (shard_id, total_shards)) {
                    log! ("shard " (shard_id) " panic: " (gstuff::any_to_str (&*err) .unwrap_or ("")));
                    sleep (Duration::from_secs (10));
                    continue}  // Panic restarts the shard.
                  break}
              }) .expect ("!spawn");
        }
    }
    

    I want to add some timers and other things which are calculated in their own threads and which must notify me to do something in the main loop's thread

    Option 1. Don't.

    Chance is, you don't really need to come back to the Discord event loop! Let's say you want to post a reply, to update an embed, etc. You do not need the Discord event loop to do that!

    Discord API goes in two parts:
    1) Websocket API, represented by the Connection, is used to get events from Discord.
    2) REST API, represented by the Discord interface, is used to send events out.

    You can send events from pretty much anywhere. From any thread. Maybe even from your timers.

    Discord is Sync. Wrap it in the Arc and share it with your timers and threads.

    Option 2. Seize the opportunity.

    Even though recv_event doesn't have a timeout, Discord will be constantly sending you new events. Users are signing in, signing out, typing, posting messages, starting videogames, editing stuff and what not. Indeed, if the stream of events stops then you have a problem with your Discord connection (for my bot I've implemented a High Availability failover based on that signal).

    You could share a deque with your threads and timers. Once the timer is finished it will post a little something to the deque, then the even loop will check the deque for new things to do once Discord wakes it with a new event.

    Option 3. Bird's-eye view.

    As belst have pointed out, you could start a generic event loop, a loop "to rule them all", then lift Discord events into that loop. This is particularly interesting because with sharding you're going to have multiple event loops.

    So, Discord event loop -> simple event filter -> channel -> main event loop.

    Option 4. Sharded.

    If you want your bot to stay online during code upgrades and restarts, then you should provision for a way to restart each shard separately (or otherwise implement a High Availability failover on the shard level, like I did). Because you can't immediately connect all your shards after a process restart, Discord won't let you.

    If all your shards share the same process, then after that process restarts you have to wait five seconds before attaching a new shard. With 10 shards it's almost a minute of bot downtime.

    One way to separate the shard restarts is to dedicate a process to every shard. Then when you need to upgrade the bot, you'd restart each process separately. That way you still have to wait five to six seconds per shard, but your user's don't.

    Even better is that you now need to restart the Discord event loop processes only for discord-rs upgrades and similar maintance-related tasks. Your main event loop, on the other hand, can be restarted immediately and as often as you like. This should speed up the compile-run-test loop considerably.

    So, Discord event loop, in a separate shard process -> simple event filter -> RPC or database -> main event loop, in a separate process.