I have the following async consumer:
class MyAsyncSoncumer(AsyncWebsocketConsumer):
async def send_http_request(self):
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=60) # We have 60 seconds total timeout
) as session:
await session.post('my_url', json={
'key': 'value'
})
async def connect(self):
await self.accept()
await self.send_http_request()
async def receive(self, text_data=None, bytes_data=None):
print(text_data)
Here, on connect method, I first accept the connection, then call a method that issues an http request with aiohttp, which has a 60 second timeout. Lets assume that the url we're sending the request to is inaccessible. My initial understanding is, as all these methods are coroutines, while we are waiting for the response to the request, if we receive a message, receive method would be called and we could process the message, before the request finishes. However, in reality, I only start receiveing messages after the request times out, so it seems like the consumer is waiting for the send_http_request to finish before being able to receive messages.
If I replace
await self.send_http_request()
with
asyncio.create_task(self.send_http_request())
I can reveive messages while the request is being made, as I do not await for it to finish on accept method.
My understanding was that in the first case also, while awaiting for the request, I would be able to receive messages as we are using different coroutines here, but that is not the case. Could it be that the whole consumer instance works as a single coroutine? Can someone clarify what's happenning here?
Consumers in django channels each run thier own runloop (async Task). But this is per consumer not per message, so if you are handling a message and you await something then the entire runloop for that websocket connection is awaiting.
Related
In my django quiz project I work with Channels. My consumer QuizConsumer is synchronous but now I need it to start a timer that runs in parallel to its other functions and sends a message after the timer is over.
class QuizConsumer(WebsocketConsumer):
def configure_round(self, lobby_id):
# Call function to send message to begin the round to all players
async_to_sync(self.channel_layer.group_send)(
self.lobby_group_name,
{
'type':'start_round',
'lobby_id':lobby_id,
}
)
self.show_results(lobby_id, numberVideos, playlength)
def start_round(self, event):
self.send(text_data=json.dumps({
'type':'start_round',
'lobby_id': event['lobby_id']
}))
def show_results(self, lobby_id, numberVideos, playlength):
async_to_sync(self.channel_layer.group_send)(
self.lobby_group_name,
{
'type':'show_results_delay',
'lobby_id':lobby_id,
'numberVideos':numberVideos,
'playlength':playlength,
}
)
def show_results_delay(self, event):
time.sleep(event['numberVideos']*event['playlength']+5)
self.send(text_data=json.dumps({
'type':'show_solution',
'lobby_id': event['lobby_id']
}))
I tried to work with async functions and await, but I did not get it to work as I expected yet. I tried to work with async functions but so far it disconnected from the websocket or it did not send the "start_round" until the timer was finished.
I am implementing server sent events using flask. If I use time.sleep inside my function, the sse doesn't return anything and the request stays as pending in the browser. If I don't use sleep, there would be overload of responses in the browser, so I need to use some delay. Why is time.sleep blocking the request? Is there another way I can add time delay here?
def get_message():
time.sleep(1.0)
s="xyz" #some function here for our business logic
return s
#app.route('/stream')
def stream():
def eventStream():
while True:
yield 'data: {}\n\n'.format(get_message())
return Response(eventStream(), mimetype="text/event-stream")
After moving to channels2 I'm still struggling with python's "new" async/await and asyncio.
First I tried to reproduce Worker and Background Tasks from the docs but then I realised that my task should just run as simple async function.
So, my test function is
async def replay_run(self, event):
print_info("replay_run", event, self.channel_name)
import asyncio
for i in range(10):
await asyncio.sleep(1)
print_info("replay_run-",i, event, self.channel_name)
and both of the following calls inside async def receive_json(self, event)
seem to prevent a subsequent incoming message from being handled right away.
Version 1:
await self.channel_layer.send(
self.channel_name, {
"type": "replay_run", "sessionID": msg["sessionID"]
})
Version 2:
await self.replay_run(msg)
First I thought of version 1 because I thought I needed to register a "new event consumer" like await asyncio.gather...
Any hint on how to do this right is appreciated ...
Found the solution here: django.channels async consumer does not appear to execute asynchronously
Apparently the way would be
Version 3:
asyncio.ensure_future( self.replay_runit(msg) )
... in order to schedule the "long running coroutine" without blocking the channel.
I have added django.channels to a django project in order to support long running processes that notify users of progress via websockets.
Everything appears to work fine except for the fact that the implementation of the long running process doesn't seem to respond asynchronously.
For testing I have created an AsyncConsumer that recognizes two types of messages 'run' and 'isBusy'.
The 'run' message handler sets a 'busy flag' sends back a 'process is running' message, waits asynchronously for 20 seconds resets the 'busy flag' and then sends back a 'process complete message'
The 'isBusy' message returns a message with the status of the busy flag.
My expectation is that if I send a run message I will receive immediately a 'process is running' message back and after 20 seconds I will receive a 'process complete' message.
This works as expected.
I also expect that if I send a 'isBusy' message I will receive immediately a response with the state of the flag.
The observed behaviour is as follows:
a message 'run' is sent (from the client)
a message 'running please wait' is immediately received
a message 'isBusy' is sent (from the client)
the message reaches the web socket listener on the server side
nothing happens until the run handler finishes
a 'finished running' message is received on the client
followed immediately by a 'process isBusy:False' message
Here is the implementation of the Channel listener:
class BackgroundConsoleConsumer(AsyncConsumer):
def __init__(self, scope):
super().__init__(scope)
self.busy = False
async def run(self, message):
print("run got message", message)
self.busy = True
await self.channel_layer.group_send('consoleChannel',{
"type":"consoleResponse",
"text":"running please wait"
})
await asyncio.sleep(20)
self.busy = False
await self.channel_layer.group_send('consoleChannel',{
"type":"consoleResponse",
"text": "finished running"
})
async def isBusy(self,message):
print('isBusy got message', message)
await self.channel_layer.group_send('consoleChannel',{
"type":"consoleResponse",
"text": "process isBusy:{0}".format(self.busy)
})
The channel is set up in the routing file as follows:
application = ProtocolTypeRouter({
"websocket": AuthMiddlewareStack(
URLRouter([
url("^console/$", ConsoleConsumer),
])
),
"channel": ChannelNameRouter({
"background-console":BackgroundConsoleConsumer,
}),
})
I run the channel with one worker (via ./manage.py runworker ).
The experiment was done with the django test server (via runserver).
Any ideas as to why the channel consumer does not appear to work asynchronously would be appreciated.
After a bit of digging around here is the problem and one solution to it.
A channel adds messages sent to it to a asyncio.Queue and processes them sequentially.
It is not enough to release the coroutine control (via a asyncio.sleep() or something similar), one must finish processing the message handler before a new message is received by the consumer.
Here is the fix to the previous example that behaves as expected (i.e. responds to the isBusy messages while processing the run long running task)
Thank you #user4815162342 for your suggestions.
class BackgroundConsoleConsumer(AsyncConsumer):
def __init__(self, scope):
super().__init__(scope)
self.busy = False
async def run(self, message):
loop = asyncio.get_event_loop()
loop.create_task(self.longRunning())
async def longRunning(self):
self.busy = True
await self.channel_layer.group_send('consoleChannel',{
"type":"the.type",
"text": json.dumps({'message': "running please wait", 'author': 'background console process'})
})
print('before sleeping')
await asyncio.sleep(20)
print('after sleeping')
self.busy = False
await self.channel_layer.group_send('consoleChannel',{
"type":"the.type",
"text": json.dumps({'message': "finished running", 'author': 'background console process'})
})
async def isBusy(self,message):
print('isBusy got message', message)
await self.channel_layer.group_send('consoleChannel',{
"type":"the.type",
"text": json.dumps({'message': "process isBusy:{0}".format(self.busy),
'author': 'background console process'})
})
I'll first explain the architecture of my system and then move to the question:
I have a REST API which is used as my API gateway. This server is build using Flask. I also have RabbitMQ cluster, and a client I wrote that listens to a specific queue and executes the tasks its getting.
Until now, all of my requests were asynchronous, so once a request has reached to the API gateway, a callback_uri field with URL to POST the results to provided as part of the request, and the API gateway was just responsible for sending the task to RabbitMQ and the worker processed the task, and at the end POSTed the results back to the callback URL.
My question is:
I want a new endpoint to be synchronous in the sense of, that the processing will be done still by the same worker as before, but I want to get the results back to the API gateway to return to the user and release the connection.
My current solution:
I'm sending a unique callback_uri as part of the request to the worker as before, but now the specific endpoint is implemented by my API gateway and allow both POST and GET methods, so the worker can POST the results once it finished, and my API gateway keeps polling the callback URL until a result is available and then return the result to the client.
Is there any other preferred option other than having a busy-waiting HTTP worker polling its own endpoint to get the results? but still be synchronous so the connection released only when the results become available?
Code for illustration only:
#app.route('/long_task', methods=['POST'])
#sync_request
def long_task():
try:
if request.get_json() is None:
return ERROR_MSG_NO_JSON, 400
create_and_send_request_to_rabbitmq()
return '', 200
except Exception as ex:
return ERROR_MSG_NO_DATA, 400
def sync_request(func):
def call(*args, **kwargs):
create_callback_uri()
result = func(*args, **kwargs)
status_code = result[1]
if status_code == 200:
result = get_callback_result()
return result
return call
def get_callback_result():
callback_uri = request.get_json()['callback_uri']
has_answer = False
headers = {'content-type': 'application/json'}
empty_response = {}
content = json.dumps(empty_response)
try:
with Timeout(seconds=SYNC_REQUEST_TIMEOUT_SECONDS):
while not has_answer:
response = requests.get(callback_uri, headers=headers)
if response.status_code == 200:
has_answer = True
content = response.content
else:
time.sleep(0.2)
except TimeoutException:
log.debug('Timed out on sync request for request %s ' % request)
return content, 200
So if I understand you correctly you want your backend to wait for the response from some worker (via RabbitMQ). You can achieve that by implementing rpc over rabbitmq. The key idea is to use the correlation id.
But definitely the most efficient way would be to run the client over websockets (or raw tcp socket if it is not a browser) and notify him directly when the job is done. That way you don't lock resources (client connection, rabbitmq queues) and you avoid performance hit (rpc).