Sanic: Long running task in async sanic method

Created on 6 Jun 2019  路  3Comments  路  Source: sanic-org/sanic

I'm trying to run a task that takes some time to return without it blocking other web requests to the server. In the example below lib.getAlarmState() is a ctypes C function that takes a few seconds to return. Its blocking all other processes while waiting, I've done enough research to know that I need to also run the lib.getAlarmState() method asynchronously (via asyncio?) but I'm having troubles getting that to work.

async def test():  
    return await lib.getAlarmState()

@app.route('/processjson')
async def processjson(request):

    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(test())
    loop.close()

    return response.json({"alarm:" : results})
question

Most helpful comment

I responded to your question on Stackoverflow.

You cannot do this inside Sanic:

    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(test())
    loop.close()

Because there already is a loop running.

Sanic does provide a mechanism for adding async tasks using app.add_task() See docs. However, that is for simply creating a task, similar to asyncio.create_task.

What you want is to gather() the task and return its result. However, even this is not really going to help. It would allow the loop to take over and check if there are any incoming processes. But once it starts, it will block.

Here's a little exercise to help understand what is going on:

import asyncio

SECONDS = 3


async def async_sleeper(sleeper_id):
    print(f"{sleeper_id}: start sleeping")
    await asyncio.sleep(SECONDS)
    print(f"{sleeper_id}: done sleeping")


async def main():
    await async_sleeper(1)
    await async_sleeper(2)


if __name__ == "__main__":
    asyncio.run(main())
1: start sleeping
1: done sleeping
2: start sleeping
2: done sleeping
[Finished in 6.1s]

Now, when we change main()

async def main():
    asyncio.create_task(async_sleeper(1))
    asyncio.create_task(async_sleeper(2))
    await asyncio.sleep(SECONDS)

Different results, half the time.

1: start sleeping
2: start sleeping
1: done sleeping
2: done sleeping
[Finished in 3.1s]

What if we change that to its blocking version? Even if it is called inside a task?

import time
import asyncio

SECONDS = 3


def sync_sleeper(sleeper_id):
    print(f"{sleeper_id}: start sleeping")
    time.sleep(SECONDS)
    print(f"{sleeper_id}: done sleeping")


async def sync_sleeper_wrapper(sleeper_id):
    sync_sleeper(sleeper_id)


async def main():
    asyncio.create_task(sync_sleeper_wrapper(1))
    asyncio.create_task(sync_sleeper_wrapper(2))
    await asyncio.sleep(SECONDS)


if __name__ == "__main__":
    asyncio.run(main())

The result is still 6 seconds:

1: start sleeping
1: done sleeping
2: start sleeping
2: done sleeping
[Finished in 6.1s]

Maybe you need to put lib.getAlarmState() into a thread? Seems like you might need some other concurrency needs besides just asyncio.

All 3 comments

I responded to your question on Stackoverflow.

You cannot do this inside Sanic:

    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(test())
    loop.close()

Because there already is a loop running.

Sanic does provide a mechanism for adding async tasks using app.add_task() See docs. However, that is for simply creating a task, similar to asyncio.create_task.

What you want is to gather() the task and return its result. However, even this is not really going to help. It would allow the loop to take over and check if there are any incoming processes. But once it starts, it will block.

Here's a little exercise to help understand what is going on:

import asyncio

SECONDS = 3


async def async_sleeper(sleeper_id):
    print(f"{sleeper_id}: start sleeping")
    await asyncio.sleep(SECONDS)
    print(f"{sleeper_id}: done sleeping")


async def main():
    await async_sleeper(1)
    await async_sleeper(2)


if __name__ == "__main__":
    asyncio.run(main())
1: start sleeping
1: done sleeping
2: start sleeping
2: done sleeping
[Finished in 6.1s]

Now, when we change main()

async def main():
    asyncio.create_task(async_sleeper(1))
    asyncio.create_task(async_sleeper(2))
    await asyncio.sleep(SECONDS)

Different results, half the time.

1: start sleeping
2: start sleeping
1: done sleeping
2: done sleeping
[Finished in 3.1s]

What if we change that to its blocking version? Even if it is called inside a task?

import time
import asyncio

SECONDS = 3


def sync_sleeper(sleeper_id):
    print(f"{sleeper_id}: start sleeping")
    time.sleep(SECONDS)
    print(f"{sleeper_id}: done sleeping")


async def sync_sleeper_wrapper(sleeper_id):
    sync_sleeper(sleeper_id)


async def main():
    asyncio.create_task(sync_sleeper_wrapper(1))
    asyncio.create_task(sync_sleeper_wrapper(2))
    await asyncio.sleep(SECONDS)


if __name__ == "__main__":
    asyncio.run(main())

The result is still 6 seconds:

1: start sleeping
1: done sleeping
2: start sleeping
2: done sleeping
[Finished in 6.1s]

Maybe you need to put lib.getAlarmState() into a thread? Seems like you might need some other concurrency needs besides just asyncio.

I am closing this as it is not a Sanic issue.

Thanks for the feedback, I recognize this is simply a synchronicity issue with my other code causing blocking not with some Sanic functionality. I guess its back to the drawing bored.

Was this page helpful?
0 / 5 - 0 ratings