I am attempting to grasp Python's asyncio library, and I'm having a problem with it's timeout exception. I can't figure out why does the "asyncio.TimeoutError" exception in the "async def create" function always execute at the end of my program even though the timeout limit hasn't passed. I would appreciate Your expert advise and opinion :)
Thank you for your time.
import asyncio
import multiprocessing as mp
from enum import Enum
class Sensor(Enum):
GREEN = 0
RED = 1
class State(Enum):
NORMAL = 0
MEDIUM = 1
BURNED = 2
class Toaster:
def __init__(self, min = 20, max = 50, temp = 0, timer = 0, state = State.NORMAL, sensor = Sensor.GREEN):
self.min = min
self.max = max
self.temp = self.min
self.timer = timer
self.state = state
self.sensor = sensor
def display(self):
print("\nTimer state:", self.timer, "seconds")
print("Toast state:", self.state.name)
print("Sensor state:", self.sensor.name)
async def start(self):
while True:
if self.temp <= self.max:
await asyncio.sleep(0.1)
print("Temperature:", self.temp)
self.temp+=1
else:
print("\nMaximum temperature", self.max, "celsius reached")
await self.measure_state()
await self.restart()
break
async def restart(self):
while True:
if self.temp >= self.min:
await asyncio.sleep(0.1)
print("Temperature:", self.temp)
self.temp-=1
else:
self.sensor = Sensor.GREEN
print("\nMinimum temperature", self.min, "celsius reached")
break
async def validateInput(self, message):
valid = False
while not valid:
try:
userInput = int(input(message))
if userInput == 0 or userInput == 1:
valid = True
return userInput
else:
raise ValueError("\nInvalid value", userInput)
except ValueError as v:
print(v)
async def eject(self):
self.display()
message = "\nEject toast - 1(Yes), 0(No):"
try:
return await asyncio.wait_for(self.validateInput(message), timeout=1000)
except asyncio.TimeoutError:
print("Took too long - eject")
async def repeat(self):
message = "\nInject another toast - 1(Yes), 0(No):"
try:
return await asyncio.wait_for(self.validateInput(message), timeout=1000)
except asyncio.TimeoutError:
print("Took too long - repeat")
async def measure_state(self):
while True:
await asyncio.sleep(5)
self.timer+=50
if self.timer == 50:
print("\nToast is in it's", self.state.name, "state")
if await self.eject() == 1:
print("\nToast ejected")
if await self.repeat() == 1:
self.timer = 0
self.state = State.NORMAL
await self.measure_state()
break
elif self.timer == 100:
self.state = State.MEDIUM
self.sensor = Sensor.RED
print("\nToast is in it's", self.state.name, "state")
if await self.eject() == 1:
print("\nToast ejected")
if await self.repeat() == 1:
self.timer = 0
self.state = State.NORMAL
await self.measure_state()
break
elif self.timer >= 150:
self.state = State.BURNED
print("\nToast is in it's", self.state.name, "state, ejecting toast")
break
async def toaster(self):
message = "\nInsert a toast - 1(Yes), 0(No):"
while await self.validateInput(message) != 1:
print("\nPlease insert a toast")
print("\nToast inserted")
await self.start()
async def create(self):
x = loop.create_task(Toaster().toaster())
y = loop.create_task(Toaster().toaster())
z = loop.create_task(Toaster().toaster())
try:
await asyncio.wait([x, y, z], timeout=1000)
raise asyncio.TimeoutError("\nTook too long - create")
except asyncio.TimeoutError as t:
print(t)
x.cancel(), y.cancel(), z.cancel()
def get_process_count():
nproc = mp.cpu_count()
pool = mp.Pool(processes=nproc)
return pool
class Connector(Toaster):
pass
async def main():
connector = Connector()
result = get_process_count()
result.map(await connector.create())
await asyncio.gather(result)
if __name__ == "__main__":
loop = None
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
except Exception as e:
pass
finally:
loop.close()
In create() you raise the exception immediately after waiting for tasks x,y,z to complete. Adding some prints to toaster and create shows that the three tasks finish so execution just resumes with the raise asyncio.TimeoutError... statement.
...
async def toaster(self):
message = "\nInsert a toast - 1(Yes), 0(No):"
while await self.validateInput(message) != 1:
print("\nPlease insert a toast")
print("\nToast inserted")
await self.start()
return 'FINISHED'
async def create(self):
x = loop.create_task(Toaster().toaster())
y = loop.create_task(Toaster().toaster())
z = loop.create_task(Toaster().toaster())
try:
await asyncio.wait([x, y, z], timeout=1000)
for thing in (x,y,z):
print(thing)
raise asyncio.TimeoutError("\nTook too long - create") # <-- you raise the exception Here!
except asyncio.TimeoutError as t:
print(t)
x.cancel(), y.cancel(), z.cancel()
Results in
>>>
...
...
Temperature: 20
Minimum temperature 20 celsius reached
Temperature: 20
Minimum temperature 20 celsius reached
Temperature: 20
Minimum temperature 20 celsius reached
<Task finished coro=<Toaster.toaster() done, defined at tmp.py:129> result='FINISHED'>
<Task finished coro=<Toaster.toaster() done, defined at tmp.py:129> result='FINISHED'>
<Task finished coro=<Toaster.toaster() done, defined at tmp.py:129> result='FINISHED'>
Took too long - create
I inserted 3 toasts and ejected them at first request.
I went through a dozen cycles of injecting and ejecting toast and it didn't
timeout but the exception was raised as soon as a declined to inject
more toast.
...why does the "asyncio.TimeoutError" exception in the "async def create" function always execute at the end of my program ...?
I would say you wrote it to do that - looks like that was your intention.
Related
I have an async method, as shown below.
I pass in lists of 1000 numbers, where the method will pass in each number to a helper function which will return something from a website.
I have a global variable called count, which i surround with locks to make sure it doesnt get changed by anything else
I use add_done_callback with the task to make this method async.
The goal is to keep sending a number in the list of 1000 numbers to the server, and only when the server returns data (can take anywhere from 0.1 to 2 seconds), to pause, write the data to a sql database, and then continue
The code works as expected without locks, or without making the callback function, (which is named 'function' below) asyncrounous. But adding locks gives me an error: RuntimeWarning: coroutine 'function' was never awaited self._context.run(self._callback, *self._args) RuntimeWarning: Enable tracemalloc to get the object allocation traceback
I am super new to async in python so any help/advice is greatly appriciated
My code is shown below. It is just a simple draft:
import time
import random
import asyncio
# from helper import get_message_from_server
async def get(number):
# get_message_from_server(number), which takes somewhere between 0.1 to 2 seconds
await asyncio.sleep(random.uniform(0.1, 2))
s = 'Done with number ' + number
return s
async def function(future, lock):
global count
print(future.result())
# write future.result() to db
acquired = await lock.acquire()
count -= 1 if (count > 1) else 0
lock.release()
async def main(numbers, lock):
global count
count = 0
for i, number in enumerate(numbers):
print('number:', number, 'count:', count)
acquired = await lock.acquire()
count += 1
lock.release()
task = asyncio.create_task(get(number))
task.add_done_callback(
lambda x: function(x, lock)
)
if (count == 50):
print('Reached 50')
await task
acquired = await lock.acquire()
count = 0
lock.release()
if (i == len(numbers) - 1):
await task
def make_numbers():
count = []
for i in range(1001):
count.append(str(i))
return count
if __name__ == '__main__':
numbers = make_numbers()
loop = asyncio.get_event_loop()
lock = asyncio.Lock()
try:
loop.run_until_complete(main(numbers, lock))
except Exception as e:
pass
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.stop()
The above comment helped a lot
This is what the final working code looks like:
import time
import random
import asyncio
from functools import partial
# from helper import get_message_from_server
async def get(number):
# get_message_from_server(number), which takes somewhere between 0.1 to 2 seconds
await asyncio.sleep(random.uniform(0.1, 2))
s = 'Done with number ' + number
return s
def function(result, lock):
print(result.result())
async def count_decrement(lock):
global count
print('in count decrement')
acquired = await lock.acquire()
count -= 1 if (count > 1) else 0
lock.release()
asyncio.create_task(count_decrement(lock))
async def main(numbers, lock):
global count
count = 0
for i, number in enumerate(numbers):
print('number:', number, 'count:', count)
acquired = await lock.acquire()
count += 1
lock.release()
task = asyncio.create_task(get(number))
task.add_done_callback(partial(function, lock = lock))
if (count == 50):
print('Reached 50')
await task
acquired = await lock.acquire()
count = 0
lock.release()
if (i == len(numbers) - 1):
await task
def make_numbers():
count = []
for i in range(1001):
count.append(str(i))
return count
if __name__ == '__main__':
numbers = make_numbers()
loop = asyncio.get_event_loop()
lock = asyncio.Lock()
try:
loop.run_until_complete(main(numbers, lock))
except Exception as e:
pass
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.stop()
Description
First and foremost, I'm fairly new to async/await development, especially with Python. At this moment, I've been developing a Discord bot where players can execute slots <bet>. An embed is displayed with emojis that are edited.
When you first invoke the command, the embed showcases a rotating emoji gif that is then 'edited' to be the real emoji after a few seconds (to give the impression that the slots are 'spinning'):
After it's complete, the player can select the "retry" functionality to repeat the process over again. So, all in all, the flow is this:
Player invokes 'slot' command slots <bet>
3 "Rotating" emojis appear and after 0.5 seconds, 1 after the other are switched to the 'static' verison
The player can click the "retry" button and the process repeats.
Code
utilities.py
import asyncio
from functools import wraps, partial
def sync_to_async(func):
#wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = asyncio.get_event_loop()
pfunc = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, pfunc)
return run
slotmachine.py
import asyncio
import random
from enum import Enum
from typing import Union
from discord import Color, Embed, Message, Reaction, User
from discord.ext.commands import Bot, Context
from utilities import sync_to_async
import arrow
class SlotMachine:
class Reel(Enum):
BANANA = "🍌"
CHERRY = "🍒"
GRAPE = "🍇"
DOLLAR = "💵"
MONEY = "🤑"
symbols = list(Reel)
animated_emoji = "<a:slots:765434893781041183>"
row_of_three_payout = {
Reel.BANANA: 2,
Reel.CHERRY: 4,
Reel.GRAPE: 8,
Reel.DOLLAR: 15,
Reel.MONEY: 20,
}
row_of_two_payout = {
Reel.BANANA: 1,
Reel.CHERRY: 2,
Reel.GRAPE: 4,
Reel.DOLLAR: 0,
Reel.MONEY: 0,
}
def __init__(self, ctx, bot, user, coins, bet):
self.ctx = ctx
self.bot = bot
self.user = user
self.bet = bet
self.profit = 0
self.initial_coins = coins
self.coins = coins
self.message = None
#sync_to_async
def has_row_of_three(self, slots: list) -> bool:
if all(i == slots[0] for i in slots):
return SlotMachine.row_of_three_payout[slots[0]]
#sync_to_async
def has_row_of_two(self, slots: list) -> bool:
if slots[0] == slots[1]:
return True
elif slots[1] == slots[2]:
return True
else:
return False
async def calculate_payout_multiplier(self, slots: list):
has_three, has_two = await asyncio.gather(
self.has_row_of_three(slots), self.has_row_of_two(slots)
)
if has_three:
return SlotMachine.row_of_three_payout[slots[0]]
elif has_two:
return SlotMachine.row_of_two_payout[slots[1]]
else:
return 0
#sync_to_async
def generate_embed(self, embed_color, multiplier, slots):
embed = Embed(title=f"Slots 🎰", color=embed_color)
embed.add_field(
name="Spin",
value=f"{' | '.join(slots)}",
inline=False,
)
multiplier = f"{multiplier}x" if multiplier > 0 else "None"
embed.add_field(name="Multiplier", value=multiplier)
embed.add_field(name="Profit", value=self.profit)
embed.add_field(name="Coins", value=self.coins)
embed.add_field(name="Again?", value="React with 🔁 to play again.")
embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
return embed
async def play(self):
slots = [random.choice(SlotMachine.symbols) for _ in range(3)]
payout_multiplier = await self.calculate_payout_multiplier(slots)
return (self.bet * payout_multiplier, payout_multiplier, slots)
#sync_to_async
def generate_spin_embed(self, display) -> Embed:
embed = Embed(title=f"Slots 🎰", color=Color.from_rgb(0, 0, 0))
embed.add_field(
name="Spin",
value=" | ".join(display),
inline=False,
)
embed.set_footer(text=self.user, icon_url=self.user.avatar_url)
return embed
async def spin_animation(self, slots) -> None:
display = [SlotMachine.animated_emoji] * 3
embed = await self.generate_spin_embed(display)
await self.message.edit(embed=embed)
for i in range(len(slots)):
await asyncio.sleep(0.5)
display[i] = slots[i].value
embed = await self.generate_spin_embed(display)
await self.message.edit(embed=embed)
def is_valid_reaction(self, reaction: Reaction, user: User):
return (
user == self.user
and str(reaction.emoji) in ("🔁")
and reaction.message.id == self.message.id
)
async def game_loop(self):
has_busted = False
is_first = True
self.message = await self.ctx.send(embed=Embed(title="⌛ Loading..."))
try:
while not has_busted:
money_out, multiplier, slots = await self.play()
self.profit = money_out - self.bet
self.coins += self.profit
await self.spin_animation(slots)
if self.coins <= 0:
self.coins = 0
has_busted = True
await self.message.edit(
embed=await self.generate_embed(
Color.red(), multiplier, [s.value for s in slots]
)
)
else:
if self.profit == 0:
embed_color = Color.from_rgb(0, 0, 0)
elif self.profit > 0:
embed_color = Color.green()
else:
embed_color = Color.red()
embed = await self.generate_embed(
embed_color, multiplier, [s.value for s in slots]
)
await asyncio.sleep(0.5)
await self.message.edit(embed=embed)
if is_first:
await self.message.add_reaction("🔁")
is_first = False
else:
await self.message.remove_reaction("🔁", self.user)
if self.bet > self.coins:
has_busted = True
else:
await self.bot.wait_for(
"reaction_add", timeout=30, check=self.is_valid_reaction
)
# They busted at this point
amount = self.bet if self.bet > self.coins else self.initial_coins
await self.bot.api.modify_gambling_profit(
user_id=self.user.id, game="slots", amount=-1 * amount
)
except asyncio.TimeoutError:
if self.initial_coins > self.coins:
amount = -1 * (self.initial_coins - self.coins)
else:
amount = self.coins - self.initial_coins
await self.bot.api.modify_gambling_profit(
user_id=self.user.id, game="slots", amount=amount
)
commands.py
#commands.command(name="slots", aliases=["slot"])
#commands.cooldown(1, 5, commands.BucketType.user)
async def slots_command(self, ctx: Context, bet: int) -> None:
user_id = ctx.author.id
if await self.currently_playing.get(f"gambling.{user_id}"):
await ctx.send("❌ Already in another game.")
return
else:
await self.currently_playing.set(f"gambling.{user_id}", 1)
if not await self.is_valid_bet(bet):
await ctx.send("❌ Bet must be a positive whole number.")
return
stats = await self.bot.api.get_economy_user(user_id=user_id)
coins = stats["coins"]
if await self.has_enough_coins(user_id, coins, bet):
await SlotMachine(ctx, self.bot, ctx.author, coins, bet).game_loop()
else:
await ctx.send("❌ Not enough coins")
await self.currently_playing.delete(f"gambling.{user_id}")
Problem
When I was testing with 1 person, it works fantastic; however, when multiple people are running the same 'slots' command everyone receives a huge lag. It's almost as if one slot 'run' is waiting on the other.
Questions
Does anyone see what is noticeably wrong with the code? Is there something that is blocking the main the thread? I suppose so?
What methods can I use to debug / investigate this issue? I'm currently running debug on asyncio, but it doesn't yield any warnings about the slots command?
At this point, I've become so frustrated that I'm about to switch over to a more 'async' friendly language such as go or javascript. That's a bit of an over-exaggerative / novice thought though ;)
I want to make a timer command.
#commands.command()
async def timer(self, ctx, seconds):
try:
secondint = int(seconds)
if secondint > 300:
await ctx.send("I dont think im allowed to do go above 300 seconds.")
raise BaseException
if secondint < 0 or secondint == 0:
await ctx.send("I dont think im allowed to do negatives")
raise BaseException
message = await ctx.send("Timer: " + seconds)
while True:
secondint = secondint - 1
if secondint == 0:
await message.edit(new_content=("Ended!"))
break
await message.edit(new_content=("Timer: {0}".format(secondint)))
await asyncio.sleep(1)
await ctx.send(ctx.message.author.mention + " Your countdown Has ended!")
except ValueError:
await ctx.send("Must be a number!")
I tried this but this doesn't work , it doesn't edit message like I want it to and no errors.
Alright, so here's a modified version of that script above marked with the green checkmark. I made some changes to make it more user-friendly (unit converter which converts for example "5m" to 300 seconds, and instead of displaying say, "90 seconds" it would display "1 minute 30 seconds" etc.), and easier for the general public to use. I'm not that great at coding, I'm at a beginners level, but I hope this helps!
#commands.command()
async def timer(self, ctx, timeInput):
try:
try:
time = int(timeInput)
except:
convertTimeList = {'s':1, 'm':60, 'h':3600, 'd':86400, 'S':1, 'M':60, 'H':3600, 'D':86400}
time = int(timeInput[:-1]) * convertTimeList[timeInput[-1]]
if time > 86400:
await ctx.send("I can\'t do timers over a day long")
return
if time <= 0:
await ctx.send("Timers don\'t go into negatives :/")
return
if time >= 3600:
message = await ctx.send(f"Timer: {time//3600} hours {time%3600//60} minutes {time%60} seconds")
elif time >= 60:
message = await ctx.send(f"Timer: {time//60} minutes {time%60} seconds")
elif time < 60:
message = await ctx.send(f"Timer: {time} seconds")
while True:
try:
await asyncio.sleep(5)
time -= 5
if time >= 3600:
await message.edit(content=f"Timer: {time//3600} hours {time %3600//60} minutes {time%60} seconds")
elif time >= 60:
await message.edit(content=f"Timer: {time//60} minutes {time%60} seconds")
elif time < 60:
await message.edit(content=f"Timer: {time} seconds")
if time <= 0:
await message.edit(content="Ended!")
await ctx.send(f"{ctx.author.mention} Your countdown Has ended!")
break
except:
break
except:
await ctx.send(f"Alright, first you gotta let me know how I\'m gonna time **{timeInput}**....")
It doesn't edit the message since new_content isn't a Message.edit() method argument.It only has: content / embed / suppress / delete_after / allowed_mentions.
The one you're looking for is content:
#commands.command()
async def timer(self, ctx, seconds):
try:
secondint = int(seconds)
if secondint > 300:
await ctx.send("I dont think im allowed to do go above 300 seconds.")
raise BaseException
if secondint <= 0:
await ctx.send("I dont think im allowed to do negatives")
raise BaseException
message = await ctx.send("Timer: {seconds}")
while True:
secondint -= 1
if secondint == 0:
await message.edit(content="Ended!")
break
await message.edit(content=f"Timer: {secondint}")
await asyncio.sleep(1)
await ctx.send(f"{ctx.author.mention} Your countdown Has ended!")
except ValueError:
await ctx.send("Must be a number!")
The only way it works is if you do something like this
import asyncio
#client.command()
async def count(ctx, number:int):
try:
if number < 0:
await ctx.send('number cant be a negative')
elif number > 300:
await ctx.send('number must be under 300')
else:
message = await ctx.send(number)
while number != 0:
number -= 1
await message.edit(content=number)
await asyncio.sleep(1)
await message.edit(content='Ended!')
except ValueError:
await ctx.send('time was not a number')
I am trying to make an alarm discord bot using python. No errors are happening, but also no output from the bot past the try, except function
Please note: alarm, not timer
#client.command(aliases = ["alarm"])
async def alarm_at(ctx, time):
alarm = False
if alarm == False:
alarm = True
now = datetime.now()
mtimeA = time
mtimeB = mtimeA.split(":")
hr = int(mtimeB[0])
min = int(mtimeB[1])
secsleft = int((timedelta(hours=24) - (now - now.replace(hour=hr, minute=min, second=0, microsecond=0))).total_seconds() % (24 * 3600))
print(secsleft)
await ctx.send(f"OK\Alarm go off at {time}")
def check(message):
return message.author == ctx.author and message.content.lower() == "cancel alarm"
try:
await client.wait_for("message", check=check, timeout=time)
await ctx.send("alarm cancelled")
except:
if secsleft == 0:
await ctx.send(f"{ctx.guild.default_role} alarm finished")
elif alarm == True:
await ctx.send("Please cancel the current alarm to run a new alarm")
If the error is happening in the try ... except, you won't see anything. Try this instead:
try:
await client.wait_for("message", check=check, timeout=time)
await ctx.send("alarm cancelled")
except Exception as e:
print(f"Error: {e}")
if secsleft == 0:
await ctx.send(f"{ctx.guild.default_role} alarm finished")
I am writing a minimalist code based on asyncio and composed of tasks, workers and a queue:
Workers look in the queue for a task and execute it
Tasks are represented as a random asyncio.sleep()
When a task is done it adds two more tasks to the queue
Two restrictions:
10 max worker at a time
100 tasks should be executed in total, the script should end then
To monitor the total number of tasks executed, I use a global variable task_queued updated each time a task is being added to the queue.
I'm sure there's a better, pythonic way, to do that and not using a global variable, but all the solution I came up with are much more complicated.
I'm missing something here, any clue ?
Here's my code:
import asyncio
from random import random
import sys
MAX_WORKERS = 10
MAX_TASKS = 100
task_queued = 0
async def task(queue, id="1"):
global task_queued
sleep_time = 0.5 + random()
print(' Begin task #{}'.format(id))
await asyncio.sleep(sleep_time)
if task_queued < MAX_TASKS:
await queue.put(id + ".1")
task_queued += 1
if task_queued < MAX_TASKS:
await queue.put(id + ".2")
task_queued += 1
print(' End task #{} ({} item(s) in the queue)'.format(id, queue.qsize()))
async def worker(worker_id, queue):
while True:
task_id = await queue.get()
print('Worker #{} takes charge of task {}'.format(worker_id, task_id))
await task(queue, task_id)
queue.task_done()
async def main():
global task_queued
print('Begin main \n')
queue = asyncio.Queue()
await queue.put("1") # We add one task to the queue
task_queued += 1
workers = [asyncio.create_task((worker(worker_id + 1, queue))) for worker_id in range(MAX_WORKERS)]
await queue.join()
print('Queue is empty, {} tasks completed'.format(task_queued))
for w in workers:
w.cancel()
print('\n End main')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
except KeyboardInterrupt:
print('\nBye bye')
sys.exit(0)
Thank to user4815162342 for the answer, here's the code if anyone is interested
import asyncio
from random import random
import sys
class Factory:
"""
Factory
"""
def __init__(self, max_workers, max_tasks):
self.task_queued = 0
self.max_workers = max_workers
self.max_tasks = max_tasks
self.queue = asyncio.Queue()
async def task(self, task_id):
sleep_time = 0.5 + random()
print(' Begin task #{}'.format(task_id))
await asyncio.sleep(sleep_time)
if self.task_queued < self.max_tasks:
await self.queue.put(task_id + ".1")
self.task_queued += 1
if self.task_queued < self.max_tasks:
await self.queue.put(task_id + ".2")
self.task_queued += 1
print(' End task #{} ({} item(s) in the queue)'.format(task_id, self.queue.qsize()))
async def worker(self, worker_id):
while True:
task_id = await self.queue.get()
print('Worker #{} takes charge of task {}'.format(worker_id, task_id))
await self.task(task_id)
self.queue.task_done()
async def organize_work(self):
print('Begin work \n')
await self.queue.put("1") # We add one task to the queue to start
self.task_queued += 1
workers = [asyncio.create_task((self.worker(worker_id + 1))) for worker_id in range(self.max_workers)]
await self.queue.join()
print('Queue is empty, {} tasks completed'.format(self.task_queued))
for w in workers:
w.cancel()
print('\nEnd work')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
factory = Factory(max_workers=3, max_tasks=50)
try:
loop.run_until_complete(factory.organize_work())
except KeyboardInterrupt:
print('\nBye bye')
sys.exit(0)