Skip to content

Instantly share code, notes, and snippets.

@uWynell
Last active May 24, 2023 04:49
Show Gist options
  • Save uWynell/129ddad2aed0e1f65847c39836774d1a to your computer and use it in GitHub Desktop.
Save uWynell/129ddad2aed0e1f65847c39836774d1a to your computer and use it in GitHub Desktop.
Telegraf parallel handling

Introduction

Telegraf handles all requests sequentially, one after another. I won't get into details about why it might be done like that by default, but sometimes it's not really what you want.

So I had this problem too. While the bot was handling one heavy command, it could not handle other commands at the same time. Even if they were in different chats. That was quite a problem for me.

I didn't dive inside Telegraf, instead I just came up with a guess and it worked. I'm just sharing my experience, you might need to tweak the code for yourself a little.

Solutions

Handle each event concurrently. The solution here is a simple one-liner:

bot.use((ctx, next) => void next())

Why? I thought, if Telegraf awaits promises returned from handler functions for some reason, then I can just replace them with undefined (using middleware), so there will be nothing to await.

I think this is a pretty bad solution, why - read below.

Handle events from different chats concurrently (but consequently within one chat):

I believe this is a better option. In the very bottom of the note I'll explain why.

const stacks = new Map()

export function makeParallel() {
  return (ctx, next) => {
    const key = `${ctx.from?.id ?? 0}-${ctx.chat?.id ?? 0}`
    if (!stacks.has(key)) stacks.set(key, Promise.resolve())
    const stack = stacks.get(key)!
    stacks.set(
      key,
      stack.finally(() => next())
    )
  }
}

bot.use(makeParallel())

The same idea is behind this code, but it also avoids the issue, which I'll explain right below.

The problem with the first solution

Let's say you have a shop bot. The user has balance of 101, and the service costs 100. The user of course should not be able to buy something if he doesn't have enough money. And when you enter the /buy command let's say, the bot has to process some information for 10 seconds before returning some answer to the customer. The customer sends /buy and right after that does it (/buy) one more time. Let's see how it will work in two cases:

If bot processes them consecutively. I think no questions should arise here.

  1. The bot checks the balance: 101 > 100. Pass.
  2. The bot does its work for 10s.
  3. The bot removes 100 from user's balance: 101 -> 1.
  4. The bot returns the result to the customer.
  5. The bot checks the balance. 1 < 100. Fail.

If bot processes them concurrently

  1. The bot checks the balance: 101 > 100. Pass.
  2. The bot checks the balance: 101 > 100. Pass.
  3. The bot does its work for 10s.
  4. The bot does its work for 10s.
  5. The bot returns the result to the customer.
  6. The bot returns the result to the customer.
  7. The bot removes 100 from user's balance: 101 -> 1.
  8. The bot removes 100 from user's balance: 1 -> -99.

So as you can see, the bot did the work twice, even though the user didn't have enough money. He had even gone overdraft.

If you know hot to make this article better (better code, examples, explainations etc.), write me in Telegram: @social_pressure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment