I have the following simple python web server application. When I trigger /sleep call - until the time for sleep ends and the response returns - all other calls on /quick are blocked. I am not sure what it wrong with this code. Can someone provide some clarity?
from aiohttp import web
import asyncio
import time
async def handle(request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)
async def sleephandle(request):
name = request.match_info.get('name', "Anonymous")
time.sleep(12) // trivializing here; actual code has a transition from async to sync
text = "Hello, " + name
return web.Response(text=text)
async def init(loop):
app = web.Application(loop=loop)
app.router.add_get('/quick', handle)
app.router.add_get('/sleep', sleephandle)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('server started')
return srv
def create_server():
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
create_server()
The key idea of my solution is to use loop.run_in_executor with correct for your case Pool. You can solve the problem the following way:
from aiohttp import web
import asyncio
import time
import logging
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
def blocking_code():
"""Some long running code"""
time.sleep(12)
return "!!!!"
async def blocking_code_task(loop: asyncio.BaseEventLoop, request: web.Request):
"""Wrapper to be used in asyncio Task"""
r = await loop.run_in_executor(executor=request.app["workers_pool"], func=blocking_code)
logging.info(f"{datetime.now()}: {r}")
async def handle(request: web.Request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)
async def sleephandle(request: web.Request):
"""We wait fore results here, then send response"""
name = request.match_info.get('name', "Anonymous")
loop = asyncio.get_event_loop()
# if you want to wait for result
r = await loop.run_in_executor(executor=request.app["workers_pool"], func=blocking_code)
text = "Hello, " + name + r
return web.Response(text=text)
async def fast_sleep_answer(request: web.Request):
"""We send response as fast as possible and do all work in another asyncio Task"""
name = request.match_info.get('name', "Anonymous")
loop = asyncio.get_event_loop()
# if you do not want to want for result
asyncio.create_task(blocking_code_task(loop, request))
text = "Fast answer" + name
return web.Response(text=text)
async def on_shutdown(app):
"""Do not forget to correctly close ThreadPool"""
app["workers_pool"].shutdown()
logging.info(f"{datetime.now()}: Pool is closed")
async def init(args=None):
"""Changed your code for newer aiohttp"""
pool = ThreadPoolExecutor(8)
app = web.Application()
app.router.add_get('/quick', handle)
app.router.add_get('/sleep', sleephandle)
app.router.add_get('/fast', fast_sleep_answer)
app["workers_pool"] = pool # can be ThreadPool or ProcessPool
app.on_shutdown.append(on_shutdown) # close the pool when app closes
return app
# the better way to tun app
# name of file is x.py
# in Linux command will be python3
# python -m aiohttp.web -H 0.0.0.0 -P 8080 x:init
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
web.run_app(init(), host="0.0.0.0", port=8080)
All blocking IN/OUT ops are made in ThreadPoolExecutor. If your tasks are CPU bound go with ProcessPoolExecutor. I showed two cases: 1) when you can't answer as fast as possible and need to wait for results 2) when you can just answer and then make all work in background.
Related
Summarize the problem
I have a flask server (with endpoints and socket events) and a discord bot, both work independently, I want to run them on parallel so I can trigger functions of the bot from a flask endpoint.
Describe what you have tried
For context, this is an example of the endpoint:
#app.route("/submit", methods=["POST"])
async def submit():
data = request.json
userid = int(os.getenv("USER_ID"))
message = f"```Title: {data['title']}\nMessage: {data['message']}```"
await send_dm(userid, message)
return data
Where send_dm in its own package looks like this
# notice that this is not a method of a function
# nor a decorated function with properties from the discord library
# it just uses an intance of the commands.Bot class
async def send_dm(userid: int, message: str):
user = await bot.fetch_user(userid)
channel = await user.create_dm()
await channel.send(message)
So to run them on parallel and be able to communicate them with each other I tried:
Attempt 1: multiprocessing module
def main():
executor = ProcessPoolExecutor(2)
loop = asyncio.new_event_loop()
loop.run_in_executor(executor, start_bot)
loop.run_in_executor(executor, start_server)
loop.run_forever()
if __name__ == "__main__":
run()
When the function on the endpoint mentioned executes I get the following error AttributeError: '_MissingSentinel' object has no attribute 'is_set' on concurrent tasks
Attempt 2: threading module
# make them aware of each other
bot.flask_app = app
app.bot = bot
async def main():
task = threading.Thread(target=start_bot)
task.start()
start_server()
if __name__ == "__main__":
asyncio.run(main())
This approach brings two issues:
First is that to run start_bot I use .start() method instead of .run() because according to the this example .run() created its own event pool which would make it unreachable by other processes, and .start() is an async function, so when running this I get the error: RuntimeError: You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.
Second is that even using the run.() function then the same issue arises when executing mentioned endpoint.
Attempt 3: asyncio module
def main():
executor = ProcessPoolExecutor(2)
loop = asyncio.new_event_loop()
boo = loop.run_in_executor(executor, start_bot)
baa = loop.run_in_executor(executor, start_server)
loop.run_forever()
if __name__ == "__main__":
main()
This time I actually get the execute both processes but still cannot call the function I want from the flask endpoint.
I also tried
await asyncio.gather([start_server(), start_bot()])
But same issue as Attempt 2, and I already upgraded the flask[async] module so that is not the issue anymore.
Show some code
To reproduce what I have right now you can either check the full repo here that has only 4 files or this sample should be enough to reproduce.
from server import socketio, app
from bot import bot
from dotenv import load_dotenv
import os
import asyncio
import threading
env_path = os.path.dirname(__file__) + "/.env"
load_dotenv(env_path)
def start_server():
socketio.run(app)
def start_bot():
token = os.getenv("BOT_TOKEN")
bot.run(token)
async def main():
# this doesn't achieve what I want and is the main part of the problem
start_server()
start_bot()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Program exited")
Did I miss something?, point it out in the comments and I will add it in the edit.
I have the next code:
from fastapi import FastAPI, WebSocket, BackgroundTasks
import uvicorn
import time
app = FastAPI()
def run_model():
...
## code of the model
answer = [1, 2, 3]
...
results = {"message": "the model has been excuted succesfully!!", "results": answer}
return results
#app.post("/execute-model")
async def ping(background_tasks: BackgroundTasks):
background_tasks.add_task(run_model)
return {"message": "the model is executing"}
#app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
## Here I wnat the results of run_model
await websocket.send_text(1)
if __name__ == "__main__":
uvicorn.run(app, host="localhost", port=8001)
I need to make a post fecth to /execute-model. This endpoint will excute a run_model function as a background task. I need return the answer to the front when run_model() finish and I thought in use websockets but I don't know how do it. Help please.
I had something similar. Here is how I did it (not saying it's the best or even a good solution, but it's working so far):
The route endpoint:
# client makes a post request, gets saved model immeditely, while a background task is started to process the image
#app.post("/analyse", response_model=schemas.ImageAnalysis , tags=["Image Analysis"])
async def create_image_analysis(
img: schemas.ImageAnalysisCreate,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
):
saved = crud.create_analysis(db=db, img=img)
background_tasks.add_task(analyse_image,db=db, img=img)
#model includes a ws_token (some random string) that the client can connect to right away
return saved
The websocket endpoint:
#app.websocket("/ws/{ws_token}")
async def websocket_endpoint(websocket: WebSocket, ws_token: str):
#add the websocket to the connections dict (by ws_token)
await socket_connections.connect(websocket,ws_token=ws_token)
try:
while True:
print(socket_connections)
await websocket.receive_text() #not really necessary
except WebSocketDisconnect:
socket_connections.disconnect(websocket,ws_token=ws_token)
The analyse_image function:
#notice - the function is not async, as it does not work with background tasks otherwise!!
def analyse_image (db: Session, img: ImageAnalysis):
print('analyse_image started')
for index, round in enumerate(img.rounds):
# some heavy workload etc
# send update to user
socket_connections.send_message({
"status":EstimationStatus.RUNNING,
"current_step":index+1,
"total_steps":len(img.rounds)
}, ws_token=img.ws_token)
print("analysis finished")
The connection Manager:
import asyncio
from typing import Dict, List
from fastapi import WebSocket
#notice: active_connections is changed to a dict (key= ws_token), so we know which user listens to which model
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, List[WebSocket]] = {}
async def connect(self, websocket: WebSocket, ws_token: str):
await websocket.accept()
if ws_token in self.active_connections:
self.active_connections.get(ws_token).append(websocket)
else:
self.active_connections.update({ws_token: [websocket]})
def disconnect(self, websocket: WebSocket, ws_token: str):
self.active_connections.get(ws_token).remove(websocket)
if(len(self.active_connections.get(ws_token))==0):
self.active_connections.pop(ws_token)
# notice: changed from async to sync as background tasks messes up with async functions
def send_message(self, data: dict,ws_token: str):
sockets = self.active_connections.get(ws_token)
if sockets:
#notice: socket send is originally async. We have to change it to syncronous code -
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
for socket in sockets:
socket.send_text
loop.run_until_complete(socket.send_json(data))
socket_connections = ConnectionManager()
I'm quite new to all things async in python. I have a certain code I would like to run while an async slack RTM client is listening on messages with a dedicated callback, like this:
RTM_CLIENT.start()
while True:
...
except Exception as e:
...
finally:
RTM_CLIENT.stop()
the callback function:
#slack.RTMClient.run_on(event='message')
def listen(**payload):
...
The RTM_CLIENT.start() function returns a future object.
I'm not getting any message events though. Am I doing something wrong?
This solves it(thread sync):
import re
import slack
import time
import asyncio
import concurrent
from datetime import datetime
#slack.RTMClient.run_on(event='message')
async def say_hello(**payload):
data = payload['data']
print(data.get('text'))
def sync_loop():
while True:
print("Hi there: ", datetime.now())
time.sleep(5)
async def slack_main():
loop = asyncio.get_event_loop()
rtm_client = slack.RTMClient(token='x', run_async=True, loop=loop)
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
await asyncio.gather(
loop.run_in_executor(executor, sync_loop),
rtm_client.start()
)
if __name__ == "__main__":
asyncio.run(slack_main())
I have a manager to cache some user settings. I want to cleanup it every hour for inactive users (10 seconds in my example). I try to use aiojobs for this. I spawn the same job inside the job coroutine.
from aiohttp import web
from aiojobs.aiohttp import setup, get_scheduler
import asyncio
async def cleanup(scheduler):
await asyncio.sleep(10)
print('do cleanup')
await scheduler.spawn(cleanup(scheduler))
async def handler(request):
if not request.app['init']:
scheduler = get_scheduler(request)
await scheduler.spawn(cleanup(scheduler))
request.app['init'] = True
return web.Response(text = 'ok')
def main():
app = web.Application()
app.router.add_get('/', handler)
setup(app)
app['init'] = False
web.run_app(app, host='127.0.0.1', port = 8000)
main()
Is it a good solution? Should I create my own scheduler because my job does not relate to a request?
I want some background tasks to run in the same loop as the aiohttp web server, even before any http requests arrive. It looks like aiojobs doesn't help me so I'm using something that looks like this. I'm using a janus queue because my real application makes blocking calls from another thread. I don't know aiosync well, so this may be the blind leading the blind.
import asyncio
from aiohttp import web
from aiojobs.aiohttp import setup
import janus
async def ticket_maker(q: janus.Queue):
counter = 1
while True:
print(f'Made ticket {counter}')
await q.async_q.put(counter)
await asyncio.sleep(1)
counter += 1
async def handler(request):
q: janus.Queue = request.app.get('Q')
ticket = await q.async_q.get()
return web.Response(text=f'You got ticket {ticket}')
def main():
q = janus.Queue()
app = web.Application()
asyncio.get_event_loop().create_task(ticket_maker(q))
app.router.add_get('/', handler)
app['Q'] = q
setup(app)
web.run_app(app, port=8080)
if __name__ == '__main__':
main()
from aiohttp import web
from aiohttp import ClientSession
# this would go in a different file but keep it simple for now
class Generate:
# Get a person object from my website
async def get_person(self):
async with ClientSession() as session:
async with session.get('http://surveycodebot.com/person/generate') as response:
resp = await response.json()
# this prints the person
print(resp)
return resp
# loops `get_person` to get more than 1 person
async def get_people(self):
# array for gathering all responses
for _ in range(0,10):
resp = await self.get_person()
return resp
# class to handle '/'
class HomePage(web.View):
async def get(self):
# initiate the Generate class and call get_people
await Generate().get_people()
return web.Response(text="Hello, world")
if __name__ == "__main__":
app = web.Application()
app.router.add_get('/', HomePage)
web.run_app(app)
Code works and everything is fine. I was wondering why the HomePage takes a while to load. I think I should be using yield on line 28, but it barfs when I do that. Thanks.
You can optimize by sharing the session between several client requests via the aiohttp on_startup signal.
Something like the following will do:
import asyncio
from aiohttp import web
from aiohttp import ClientSession
class Generate:
def __init__(self, session):
self.session = session
# Get a person object from my website
async def get_person(self):
async with self.session.get('http://surveycodebot.com/person/generate') as response:
resp = await response.json()
# this prints the person
print(resp)
return resp
# loops `get_person` to get more than 1 person
async def get_people(self):
# array for gathering all responses
for _ in range(0,10):
resp = await self.get_person()
return resp
# class to handle '/'
class HomePage(web.View):
async def get(self):
# initiate the Generate class and call get_people
await app['generate'].get_people()
return web.Response(text="Hello, world")
async def on_startup(app):
session = ClientSession()
app['generate'] = Generate(session)
if __name__ == "__main__":
app = web.Application()
app.router.add_get('/', HomePage)
app.on_startup.append(on_startup)
web.run_app(app)