8000 A new way to restart the bot under v20 · Issue #3718 · python-telegram-bot/python-telegram-bot · GitHub
[go: up one dir, main page]

Skip to content

A new way to restart the bot under v20 #3718

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

Closed
harshil21 opened this issue May 24, 2023 · 11 comments
Closed

A new way to restart the bot under v20 #3718

harshil21 opened this issue May 24, 2023 · 11 comments
Labels
📋 help-wanted work status: help-wanted

Comments

@harshil21
Copy link
Member

In v13, we had a simple code snippet we could use to restart the bot (https://github.com/python-telegram-bot/v13.x-wiki/wiki/Code-snippets#simple-way-of-restarting-the-bot), which restarted the whole script passing in any arguments. However simply adapting that to v20 this doesn't work since:

  1. threading and asyncio don't play well together
  2. the application methods (stop, shutdown, app.updater.shutdown) all need to be awaited instead of simply called
  3. something more I didn't see at first glance

If anyone has figured out a simple way to restart the whole script (e.g. by sending a command to a bot), feel free to propose your solution below!

@Trifase
Copy link
Contributor
Trifase commented Jun 3, 2023

The solution I'm implementing in my v20 bot is fairly primitive but it works well.
It uses another python scripts that launches the main.py file, and if for some reasons this process closes, the script restart it again.
This is the launching script, launch_bot.py that uses subprocess.call to start main.py.

# launch_bot.py
import subprocess
import time
 
while True:
    subprocess.call(["python", "main.py"])
    time.sleep(0.1)

and this is the bot itself, main.py. When I need to restart it, I raise SystemExit(), it gracefully exits and launch_bot.py restarts it again.

# main.py
from telegram import Update
from telegram.ext import ApplicationBuilder, CallbackContext, CommandHandler, ContextTypes
 
BOT_TOKEN = 'my_token'
 
async def restart(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text('See you soon!')
    raise SystemExit()
 
def main():
    builder = ApplicationBuilder()
    builder.token(BOT_TOKEN)
    app = builder.build()
    app.add_handler(CommandHandler('restart', restart))
    app.run_polling()
 
if __name__ == "__main__":
    main()

EDIT, or maybe an afterthought:
Reading today the python documentation, one should use subprocess.run instead of subprocess.call - but it's my understanding that run() calls call() under the hood, so there should be no real difference.

@lemontree210
Copy link
Member

My solution would be to just recommend running the bot as a systemd service in production. It can be set up so as to restart the service every time it fails. I kind of see it as a general problem and a general solution, unrelated to the specifics of PTB, and the solution by @Trifase would also work with any script, not just our bot.

@Bibo-Joshi
Copy link
Member

@lemontree210 auto-restarting the bot if it fails is one thing. Restarting it from within the code is more complictade though …

@Trifase can you paste the output of running python -m telegram on the setup that works for you? It.s not working for me.

In fact, I also know why it's not working: Raising SystemExit currently prevents application.update_queue.task_done() from being called. Now, application.stop() waits for await application.update_queue.join(), which never happens because the interrupted update processing makes the queue never be empty. A rather simple try-finally does help a bit, but doesn't quite cut it, because the exception also makes the background task crash that fetches the updates. This is a problem, becaues application.stop also inserts a special stop signal into the update_queue that usually tells the background task to shut down. This ofc doesn't happen, which again prevents await application.update_queue.join() from finishing.

I guess some research is in order on how (Base)Exceptions are propagated from within asyncio.Tasks to the main loop.
Apart from raising a BaseException, there may also be other ways of providing the user with a nice way of shutting the application down. The main issue is this kind of recursion, i.e. application.stop() waiting for the callbacks to finish, while we want to stop the application from within the callbacks.

@lemontree210
Copy link
Member

@lemontree210 auto-restarting the bot if it fails is one thing. Restarting it from within the code is more complictade though …

How about using something like psystemd to restart the application that is running as a systemd service?

@Bibo-Joshi
Copy link
Member

Does that allow you to trigger a graceful shutdown from within a callback and then restart the program?

@lemontree210
Copy link
Member

Does that allow you to trigger a graceful shutdown from within a callback and then restart the program?

pystemd docs say that the library allows executing commands equivalent to sudo systemctl restart <service>, which I'm pretty sure handles things correctly as it's used to restart things like nginx and gunicorn. But of course, this has to be tested to be sure. Maybe I have to create a simple example and then, say, try to logger.info() some message in a method that is supposed to be called at application shutdown?

@Trifase
Copy link
Contributor
Trifase commented Jun 5, 2023

@Trifase can you paste the output of running python -m telegram on the setup that works for you? It.s not working for me.

Sure!

❯ python -m telegram
python-telegram-bot 20.3
Bot API 6.7
Python 3.11.0 (main, Nov 23 2022, 10:42:54) [GCC 10.2.1 20210110]

Unless I messed up setting up the MWE, that's all I do. It's working for me on raspbian or whatever it's called the raspberry OS nowadays, but it's not working on windows.

This is the debug log (formatted, sorry) of the successfully shutdown after the SystemExit():

[2023-06-05 23:45:07] [telegram.ext.Updater] Stopping Updater
[2023-06-05 23:45:07] [telegram.ext.Updater] Waiting background polling task to finish up.
[2023-06-05 23:45:07] [telegram.ext.Updater] Network loop retry getting Updates was cancelled
[2023-06-05 23:45:07] [telegram.ext.Updater] Updater.stop() is complete
[2023-06-05 23:45:07] [telegram.ext.Application] Application is stopping. This might take a moment.
[2023-06-05 23:45:07] [telegram.ext.Application] Waiting for update_queue to join
[2023-06-05 23:45:07] [telegram.ext.Application] Dropping pending updates
[2023-06-05 23:45:07] [telegram.ext.Application] Application stopped fetching of updates.
[2023-06-05 23:45:07] [telegram.ext.Application] Waiting for running jobs to finish
[2023-06-05 23:45:07] [apscheduler.scheduler] Scheduler has been shut down
[2023-06-05 23:45:07] [telegram.ext.Application] JobQueue stopped
[2023-06-05 23:45:07] [telegram.ext.Application] Waiting for `create_task` calls to be processed
[2023-06-05 23:45:07] [telegram.ext.Application] Waiting for persistence loop to finish
[2023-06-05 23:45:07] [telegram.ext.Application] Application.stop() complete
[2023-06-05 23:45:07] [telegram.ext.ExtBot] This Bot is already shut down. Returning.
[2023-06-05 23:45:07] [telegram.ext.Updater] Shut down of Updater complete
[2023-06-05 23:45:07] [telegram.ext.Application] Updating & flushing persistence before shutdown
[2023-06-05 23:45:07] [telegram.ext.Application] Starting next run of updating the persistence.
[2023-06-05 23:45:07] [telegram.ext.Application] Finished updating persistence.
[2023-06-05 23:45:07] [telegram.ext.Application] Updated and flushed persistence

[2023-06-05 23:45:09] This is the supervisor speaking. I am commanding you to RAISE AGAIN! 

[2023-06-05 23:45:22] [asyncio] Using selector: EpollSelector
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Entering: get_me
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Passing request through rate limiter of type <class 'telegram.ext._aioratelimiter.AIORateLimiter'> with rate_limit_args None
[2023-06-05 23:45:25] [telegram.ext.ExtBot] User([...])
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Exiting: get_me
[2023-06-05 23:45:25] [telegram.ext.ExtBot] This Bot is already initialized.
[2023-06-05 23:45:25] [telegram.ext.Updater] Updater started (polling)
[2023-06-05 23:45:25] [telegram.ext.Updater] Start network loop retry bootstrap del webhook
[2023-06-05 23:45:25] [telegram.ext.Updater] Deleting webhook
[2023-06-05 23:45:25] [telegram.ext.Updater] Dropping pending updates from Telegram server
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Entering: delete_webhook
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Passing request through rate limiter of type <class 'telegram.ext._aioratelimiter.AIORateLimiter'> with rate_limit_args None
[2023-06-05 23:45:25] [telegram.ext.ExtBot] True
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Exiting: delete_webhook
[2023-06-05 23:45:25] [telegram.ext.Updater] Bootstrap done
[2023-06-05 23:45:25] [telegram.ext.Updater] Waiting for polling to start
[2023-06-05 23:45:25] [telegram.ext.Updater] Polling updates from Telegram started
[2023-06-05 23:45:25] [telegram.ext.Updater] Start network loop retry getting Updates
[2023-06-05 23:45:25] [telegram.ext.ExtBot] Entering: get_updates
[2023-06-05 23:45:25] [telegram.ext.Application] Loop for updating persistence started
[2023-06-05 23:45:25] [apscheduler.scheduler] Scheduler started
[2023-06-05 23:45:25] [telegram.ext.Application] JobQueue started
[2023-06-05 23:45:25] [telegram.ext.Application] Application started
[2023-06-05 23:45:25] [apscheduler.scheduler] Looking for jobs to run
[2023-06-05 23:45:25] [apscheduler.scheduler] Next wakeup is due at 2023-06-06 00:00:00+02:00 (in 874.029575 seconds)
[2023-06-05 23:45:36] [telegram.ext.ExtBot] No new updates found.

The original repo in which I'm using the code I'm providing is public, but really undocumented: this is the file that relaunch the bot and this is the restart function - repo is a bit noisy but that's the gist of it

@DDF95
Copy link
DDF95 commented Jun 6, 2023

I use this:

async def restart_bot(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("Restarting...")
    args = sys.argv[:]
    args.insert(0, sys.executable)
    os.chdir(os.getcwd())
    os.execv(sys.executable, args)

It's not optimal, definitely don't do this. On a simple bot it is good enough though.

@Bibo-Joshi
Copy link
Member

Finally got around to have a closer look at my findings from #3718 (comment). By having a look at the source of run_*

loop.run_until_complete(self.initialize())
if self.post_init:
loop.run_until_complete(self.post_init(self))
loop.run_until_complete(updater_coroutine) # one of updater.start_webhook/polling
loop.run_until_complete(self.start())
loop.run_forever()

and then cross-checking with the python docs

loop.run_forever()
Run the event loop until stop() is called.

If stop() is called before run_forever() is called, the loop will poll the I/O selector once with a timeout of zero, run all callbacks scheduled in response to I/O events (and those that were already scheduled), and then exit.

If stop() is called while run_forever() is running, the loop will run the current batch of callbacks and then exit. Note that new callbacks scheduled by callbacks will not run in this case; instead, they will run the next time run_forever() or run_until_complete() is called.

this becomes a bit of a case of RTFM 😄 . I proposed a method of stopping the application from within callbacks in #3804. Once that is possible, restarting the script after it has finished can be done by generic (i.e. non-ptb-specific) means like systemd or os.execv.

@Trifase
Copy link
Contributor
Trifase commented Jul 25, 2023

I still don't get why it works for me.

@Bibo-Joshi
Copy link
Member

v20.5 introduced Application.stop_running and I just added a code snippet based on that. Closing.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
📋 help-wanted work status: help-wanted
Projects
None yet
Development

No branches or pull requests

5 participants
0