0

Ok so, i have a Discord bot, declared like such in a main.py:

 import discord
 import asyncio
 #
 import settingsDB
 import logger
 #
 import module_one
 import module_two
 import module_three
 import module_four
 [...]
 
 client = discord.Client()
 botToken = settingsDB.getBotSetting("DiscordToken")
 
 # on_ready event stuff
 @client.event
 async def on_ready():
      # stuff
      # [...]
 
 # on_message event stuff
 @client.event
 async def on_message(message):
      # stuff
      # [...]

 # Tasks functions
 async def taskOne():
      while True:
           cycle = settingsDB.getBotSetting("taskOne_Cycle")
           # calling for stuff in module_one.py
           # stuff
           # [...]
           await asyncio.sleep(int(cycle))

 async def taskTwo():
      while True:
           cycle = settingsDB.getBotSetting("taskTwo_Cycle")
           # calling for stuff in module_two.py
           # stuff
           # [...]
           await asyncio.sleep(int(cycle))

 async def taskThree():
      while True:
           cycle = settingsDB.getBotSetting("taskThree_Cycle")
           # calling for stuff in module_three.py
           # stuff
           # [...]
           await asyncio.sleep(int(cycle))

 async def taskFour():
      while True:
           cycle = settingsDB.getBotSetting("taskFour_Cycle")
           # calling for stuff in module_four.py
           # stuff
           # [...]
           await asyncio.sleep(int(cycle))


 client.run(botToken)

settingsDB refers to settingsDB.py where all the function querying my DB are stored and can change depending on user inputs updated via another source not related with Discord bot (via website PHP).

logger refers to logger.py where i'm wrting into a txt file stuff that i want as logs.

Under on_ready event i wrote:

 @client.event
 async def on_ready():
      print('Logged in as {0.user}'.format(client))

      try:
           asyncio.ensure_future(taskOne())
           asyncio.ensure_future(taskTwo())
           asyncio.ensure_future(taskThree())
           asyncio.ensure_future(taskFour())
      except BaseException as err:
           logger.logger('Unexpected error > "' + str(err) + '" / "' + str(type(err)) + '"')
           raise
      

I've changed this part multiple times over the previous days because i wanted to have real asynchronous behaviours, but couldn't get it.

But whatever i've wrote here, i've noticed that after a couple of hours, the 4 Tasks, and even the bot itself was being launched randomly and not according to the await asyncio.sleep(int(cycle)) that each task###() has at their end.
Wierdest part is that the bot itself was firing the print line therefore telling me that it logged in again (???)

I have to mention, that taskOne() can vary quiet a lot depending on the stuff it processes, can go from 1 to near 20 minutes long.

Any ideas why it behaves like that ?
Please tell me if you need more details.


After @PythonPro recommendations i've changed on_ready function:

 G_hasLaunched = False

      @client.event
      async def on_ready():
           global G_hasLaunched
           print('Logged in as {0.user}'.format(client))
 
           if G_hasLaunched == False:
                G_hasLaunched = True
                try:
                     print("creating tasks")
                     asyncio.ensure_future(taskOne())
                     asyncio.ensure_future(taskTwo())
                     asyncio.ensure_future(taskThree())
                     asyncio.ensure_future(taskFour())
                except BaseException as err:
                     logger.logger('Unexpected error > "' + str(err) + '" / "' + str(type(err)) + '"')
                     raise

Still trying to figure out if it fixed the whole mess

Bo. Ga.
  • 138
  • 1
  • 4
  • 12
  • I would like provide some context on when the `on_ready()` is executed, it gets executed when the bot is first started or there is a network outage and the bot tried to re-connect. So, does it happen after a network outage then. – kite Dec 09 '21 at 13:32
  • How about using `asyncio.create_task` instead of `asyncio.ensure_future`? – Ratery Dec 09 '21 at 13:33
  • Also your fetches from database are synchronous. They can block an event loop. – Ratery Dec 09 '21 at 13:34
  • @Ratery at first i used `client.loop.created_task` then i tried `asyncio.create_task`, same behaviour :( – Bo. Ga. Dec 09 '21 at 15:31

2 Answers2

1

If a task is blocking (non-async), and it takes too long, the bot's connection to Discord times out and from the user's viewpoint, the bot has gone offline. When control is returned to the bot's connection logic, it reconnects to Discord. Once it has reconnected, it will fire the on_ready event again.

If you need to execute blocking logic, you can refer to this StackOverflow answer. To deal with on_ready firing every time you reconnect, you should have a variable that is set to False at the beginning of the script. Then, in on_ready, check if the variable is False. If it is, set it to True and start the tasks in your modules. The key point here is that the tasks are only started once.

PythonPro
  • 291
  • 2
  • 11
  • So, i've added a `G_hasLaunched` boolean, it seemed to have worked properly for 7hours straight. Until an error occured not related to this. I forced quit script with `CTRL+C`, waited for the bot to go to offline state in Discord application. Then launched `mail.py` again, but then, `print('Logged in as [...])` appeared 2 times in less than 10mn... I don't know why.... I have checked `ps aux | grep .py` none showed up apart my own `grep` command, `/usr/bin/python3 /usr/bin/fail2ban-server -xf start` and `/usr/bin/python2 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf`. – Bo. Ga. Dec 10 '21 at 14:20
  • So far, last test lasted 21hours long, until i had to force quit for an edit in my app. It seems to work for now. i'll wait a couple of days and accept your response as the solving one. – Bo. Ga. Dec 11 '21 at 11:04
  • Your bot may have reconnected due to a network hiccup. It's fairly normal, and the solution I suggested should ensure that only one set of tasks are started, no matter how many times on_ready is fired. – PythonPro Dec 13 '21 at 03:22
0

discord module has tasks which can be used for purposes exactly like this. It is not recommended that you used while loop for this.

Instead use tasks.loop() available in the discord module that you are using. Do note this has to manually imported from discord.ext So it'll look something like this :

from discord.ext import tasks

@bot.event
async def on_ready():
    if not taskOne.is_running():
        taskOne.start()
    # Do this for other tasks as well

@tasks.loop(seconds=0)
async def taskOne():
    # Do your things here and write other tasks that you want run in background
    cycle = settingsDB.getBotSetting("taskOne_Cycle")
    # calling for stuff in module_one.py
    # stuff
    # [...]
    await asyncio.sleep(int(cycle))

Now we have created a non-blocking loop, this loop will be started when bot goes online.

Do note :

We first if the task is already running and if it's not only then do we start it, this is to prevent the task from creating multiple instances of itself because on_ready can potentially be called multiple times.

Achxy_
  • 1,171
  • 1
  • 5
  • 25
  • At very first version, i've used tasks, but i changed this, because i couldn't change the cycle with a variable. There's something i don't understand in your explanation > the `@tasks.loop(seconds=0)` and `await asyncio.sleep(int(cycle))`. Each tasks can have a different `cycle` than the others. – Bo. Ga. Dec 10 '21 at 16:19