Skip to content

Commit 866b895

Browse files
author
Pietro Albini
committed
Properly handle multiple instances of the same bot
Telegram doesn't allow multiple instances of the same bot to live together, and previous this commit that behavior resulted in a bunch of "Error fetching the updates" messages spammed to the bot logs. This commit properly handles the situation, providing only a nice message when the thing happens and waiting for the other instances to shut down in the background. Then, after botogram is reasonably sure the other instances are down, a message is displayed to the user and the bot starts working again. This definitely improves the situation in those cases.
1 parent 3a46327 commit 866b895

File tree

3 files changed

+86
-11
lines changed

3 files changed

+86
-11
lines changed

botogram/runner/processes.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,28 @@ def setup(self, bot, commands):
168168

169169
self.fetcher = updates_module.UpdatesFetcher(bot)
170170

171-
def loop(self):
172-
# This allows to control the process
171+
def should_stop(self):
172+
"""Check if the process should stop"""
173173
try:
174174
command = self.commands.get(False)
175-
176-
# The None command will stop the process
177-
if command == "stop":
178-
self.stop = True
179-
return
180175
except queue.Empty:
181-
pass
176+
val = False
177+
else:
178+
val = command == "stop"
179+
180+
self.stop = val
181+
return val
182+
183+
def loop(self):
184+
# This allows to control the process
185+
if self.should_stop():
186+
return
182187

183188
try:
184189
updates, backlog = self.fetcher.fetch()
190+
except updates_module.AnotherInstanceRunningError:
191+
self.handle_another_instance()
192+
return
185193
except api.APIError as e:
186194
self.logger.error("An error occured while fetching updates!")
187195
self.logger.debug("Exception type: %s" % e.__class__.__name__)
@@ -209,6 +217,22 @@ def loop(self):
209217

210218
self.ipc.command("jobs.bulk_put", result)
211219

220+
def handle_another_instance(self):
221+
"""Code run when another instance of the bot is running"""
222+
# Tell the user what's happening
223+
self.logger.error("Another instance of this bot is running!")
224+
self.logger.error("Please close any other instance of the bot, and "
225+
"this one will start working again")
226+
self.logger.error("If you can't find other instances just revoke the "
227+
"API token")
228+
229+
# Wait until the other instances are closed
230+
result = self.fetcher.block_until_alone(when_stop=self.should_stop)
231+
232+
if result:
233+
self.logger.info("This instance is now the only one. The bot is "
234+
"working again")
235+
212236

213237
def _ignore_signal(*__):
214238
pass

botogram/updates.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ class FetchError(api.APIError):
1717
pass
1818

1919

20+
class AnotherInstanceRunningError(FetchError):
21+
"""Another instance of your bot is running somewhere else"""
22+
23+
def __init__(self):
24+
Exception.__init__(self, "Request terminated because of another long "
25+
"pooling or webhook active")
26+
27+
2028
class UpdatesFetcher:
2129
"""Logic for fetching updates"""
2230

@@ -30,16 +38,25 @@ def __init__(self, bot):
3038
if bot.process_backlog:
3139
self._backlog_processed = True
3240

33-
def fetch(self, timeout=1):
34-
"""Fetch the latest updates"""
41+
def _fetch_updates(self, timeout):
42+
"""Low level function to just fetch updates from Telegram"""
3543
try:
36-
updates = self._bot.api.call("getUpdates", {
44+
return self._bot.api.call("getUpdates", {
3745
"offset": self._last_id + 1,
3846
"timeout": timeout,
3947
}, expect=objects.Updates)
48+
except api.APIError as e:
49+
# Raise a specific exception if another instance is running
50+
if e.error_code == 409 and "conflict" in e.description.lower():
51+
raise AnotherInstanceRunningError()
52+
raise
4053
except ValueError:
4154
raise FetchError("Got an invalid response from Telegram!")
4255

56+
def fetch(self, timeout=1):
57+
"""Fetch the latest updates"""
58+
updates = self._fetch_updates(timeout)
59+
4360
# If there are no updates just ignore this block
4461
try:
4562
self._last_id = updates[-1].update_id
@@ -100,6 +117,39 @@ def fetch(self, timeout=1):
100117
# The first is the updates to process, the second the backlog
101118
return updates[to_check:], updates[:to_check]
102119

120+
def block_until_alone(self, treshold=4, check_timeout=1, when_stop=None):
121+
"""Returns when this one is the only instance of the bot"""
122+
checks_count = 0
123+
124+
while checks_count < treshold:
125+
# This provides an artificial end to the blocking
126+
if when_stop():
127+
return False
128+
129+
try:
130+
updates = self._fetch_updates(check_timeout)
131+
except AnotherInstanceRunningError:
132+
# Reset the count
133+
checks_count = 0
134+
continue
135+
136+
# Update the last_id
137+
try:
138+
self._last_id = updates[-1].update_id
139+
except IndexError:
140+
pass
141+
142+
# Don't count requests with new updates, since they don't tell if
143+
# another instance is running, they only make noise
144+
if updates:
145+
continue
146+
147+
# Increment the count every time a request succedes, so the whole
148+
# function exits when checks_needed is reached
149+
checks_count += 1
150+
151+
return True
152+
103153
@property
104154
def backlog_processed(self):
105155
return self._backlog_processed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ New features
4444
* Added new attribute :py:attr:`botogram.Sticker.emoji`
4545
* Every method which sends something to a chat now returns the sent
4646
:py:class:`~botogram.Message`
47+
* Multiple instances of the same bot are now properly handled (as errors)
4748

4849
Changes
4950
-------

0 commit comments

Comments
 (0)