Skip to content

Request limiter - prevent hitting Telegram's API limits #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 20, 2017

Conversation

jacklul
Copy link
Collaborator

@jacklul jacklul commented Jan 21, 2017

#157

So, I've been using this implementation for a while now and I don't see any issues so far, neither any exceptions about Telegram returning response that I've hit the limit.

By default disabled because it can be heavy on the database.
Will delay requests using sleep(1);.

As mentioned in https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this

  • No more than 1 message / second (chat)
  • No more than 30 messages / second (globally)
  • No more than 20 messages / minute (groups and channels)

This also applies to message editing! Does not apply to methods like getChat or sendChatAction.

Currently I also included 'retry' mode, sometimes if the bot is heavily used 60 seconds wait might not be enough... should I remove this and simply give it only 60 seconds, no more? Or leave it as is?

Will appreciate anyone to take a look at this!
Suggestions are welcome!

Copy link
Member

@noplanman noplanman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a silly question, but shouldn't we add a request limiter as default behaviour?

Does this slow the bot down a lot?

src/Request.php Outdated
'editMessageReplyMarkup',
];

if ((isset($data['chat_id']) || isset($data['inline_message_id'])) && in_array($action, $limited_methods)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to put some extra parenthesis here to clearly define what is being tested, making it easier to read the code.

src/Request.php Outdated

$requests = DB::getTelegramRequestCount((isset($data['chat_id']) ? $data['chat_id'] : null), (isset($data['inline_message_id']) ? $data['inline_message_id'] : null));

if ($requests['LIMIT_PER_SEC'] <= 0 && ((isset($data['inline_message_id']) && $requests['LIMIT_PER_SEC_ALL'] < 30) || (isset($data['chat_id']) && $data['chat_id'] > 0 && $requests['LIMIT_PER_SEC_ALL'] < 30) || (isset($data['chat_id']) && $data['chat_id'] < 0 && $requests['LIMIT_PER_MINUTE'] < 20))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this one is quite difficult to understand.

Maybe there's a better way of presenting/evaluating this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could transform it into multi-line statement with comments, should be fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this it would make sense to add variables for $data['chat_id'] and $data['inline_message_id'] as they're also used further up.

So instead of all the isset() && ... you could just use the variables directly.

Will think about this exact spot and how to make it more readable 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$chat_id = isset($data['chat_id']) ? $data['chat_id'] : null;
$inline_message_id = isset($data['inline_message_id']) ? $data['inline_message_id'] : null;

if (($chat_id || $inline_message_id) && in_array($action, $limited_methods)) {
	$timeout = 60;

	while (true) {
		if ($timeout <= 0) {
			throw new TelegramException('Timed out while waiting for a request spot!');
		}

		$requests = DB::getTelegramRequestCount($chat_id, $inline_message_id);

		if (
			$requests['LIMIT_PER_SEC'] == 0 &&  // No more than one message per second inside a particular chat
			(
				(($chat_id > 0 || $inline_message_id) && $requests['LIMIT_PER_SEC_ALL'] < 30) ||  // No more than 30 messages per second globally
				($chat_id < 0 && $requests['LIMIT_PER_MINUTE'] < 20)  // No more than 20 messages per minute allowed for group chats
			)
		) {
			break;
		}

		$timeout--;
		sleep(1);
	}

	DB::insertTelegramRequest($action, $data);
}

Seems to look a bit better, what you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, looks a lot better, nice 👌

src/Request.php Outdated
$timeout = 60;

while (true) {
if ($timeout <= 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also do $timeout-- here instead of $timeout = $timeout - 1; further down.

@nesttle
Copy link

nesttle commented Feb 7, 2017

Looks awesome, IMHO this should be implemented by default on next releases.

src/Request.php Outdated
@@ -1029,21 +1029,30 @@ private static function limitTelegramRequests($action, array $data = [])
'editMessageReplyMarkup',
];

if ((isset($data['chat_id']) || isset($data['inline_message_id'])) && in_array($action, $limited_methods)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love one-line code instead multiline

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nesttle Me too! (as long as it's readable 😉)


$requests = DB::getTelegramRequestCount($chat_id, $inline_message_id);

if ($requests['LIMIT_PER_SEC'] == 0 // No more than one message per second inside a particular chat
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way I can (realistically) test these limits?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing it alone will be quite hard, you can test the one msg per sec and no more than 20 per minute in groups rules easily, 30 per sec globally will be pretty hard to achieve alone.

Spamming command with callback button editing own message should be good testing ground.

sleep(1);
}

DB::insertTelegramRequest($action, $data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that entries once added, never get deleted from the DB once handled.
Shouldn't they be cleared again after being processed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if I remove them next requests will think they are ok to go.
I could add a query to remove entries older than 1 minute but I don't think it will be good practice to run such query every second (or more under heavy load)...?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm just thinking long term here. As this is a temporary table, it just gets bigger and bigger, in some cases pretty quick, right?
So some sort of cleanup would be good I think.

Maybe something like:

if (mt_rand(0, 999) === 42) {
    do_cleanup();
}

Or maybe I'm going too far here and this isn't really a problem...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, in my case I'm cleaning the table every 5 minutes, set as a task in Mysql.

I have no 'good' idea how to implement cleanup inside this code without adding potential delays or performance losses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then let's leave this as is for the moment and passively think of something 😉

@noplanman
Copy link
Member

Busy trying to test this and get it merged. Hope to get it in soon 😊

@noplanman
Copy link
Member

Didn't find the time to thoroughly test this, but I think it should be ok to merge.
If no one has any additional comments, I'll go ahead and merge soon.

@noplanman noplanman merged commit bc45e04 into php-telegram-bot:develop Feb 20, 2017
@jacklul jacklul deleted the limiter_test branch February 20, 2017 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants