Azure / azure-functions-python-worker

Python worker for Azure Functions.
http://aka.ms/azurefunctions
MIT License
331 stars 100 forks source link

python asyncio.run not working at module level #1336

Closed PabloRuizCuevas closed 7 months ago

PabloRuizCuevas commented 8 months ago

Check for a solution in the Azure portal

For issues in production, please check for a solution to common issues in the Azure portal before opening a bug. In the Azure portal, navigate to your function app, select Diagnose and solve problems from the left, and view relevant dashboards before opening your issue.

Investigative information

Please provide the following:

Repro steps

... main.py
from file import my_test 
... test.py
async test():
    """some async function"""
    await asyncio.sleep(1)
    return "test"

import asyncio
my_test = asyncio.run(test()) 

Notice that test is called at module level, But in an azure function app this simple code it doesn't work.

Expected behavior

It works.

Actual behavior

It doesn't work:

raise RuntimeError('This event loop is already running')

Thi probably means that the function app is already running his own event loop so you can't run it twice.

Known workarounds

None known

many tried like making a run function that gets the loop of the function app

def my_run(coro: Coroutine[Any, Any, T], *args, **kwargs) -> T:
    """ Azure uses already asyncio.run and it cannot be nested, so if run in azure we take their event loop"""
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro,  *args, **kwargs)
    else:
        return loop.run_until_complete(coro(*args, **kwargs))
my_test = my_run(test())  # far from working

but it doesn't work

Related information

Provide any related information

YunchuWang commented 7 months ago

@PabloRuizCuevas thanks for reporting! i am able to run the async code at module level as shown below:

import azure.functions as func
import logging
import asyncio

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

async def test():
    """some async function"""
    await asyncio.sleep(1)
    logging.info('Asyncio function executed successfully.')
    return "test"

@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    asyncio.run(test())

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )
import logging
import asyncio

async def test():
    """some async function"""
    await asyncio.sleep(1)
    logging.info('Asyncio function executed successfully.')
    return "test"

image

by default asyncio apis shall reuse the current event loop in the function execution context which is the worker loop unless explicitly passing your own loop.

Please refer to this link https://learn.microsoft.com/en-us/azure/azure-functions/python-scale-performance-reference#managing-event-loop if you want to manage worker loop yourself. but we dont have native support for customer managed loops which may generate unexpected behaviors

PabloRuizCuevas commented 7 months ago

You are creating a function http_trigger and then calling inside there, but I'm just calling it in the imports as a dependency so it gets executed at module level. Unfortunately i didn't find the way of reusing the event loop, but is what my snipet tries,

YunchuWang commented 7 months ago

by design python function worker manages and run the event loop at runtime. there is a way to add task to worker managed event loop as shown in this example:

import asyncio
import json
import logging

import azure.functions as func
from time import time
from requests import get, Response

async def invoke_get_request(eventloop: asyncio.AbstractEventLoop) -> Response:
    # Wrap requests.get function into a coroutine
    single_result = await eventloop.run_in_executor(
        None,  # using the default executor
        get,  # each task call invoke_get_request
        'SAMPLE_URL'  # the url to be passed into the requests.get function
    )
    return single_result

async def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    eventloop = asyncio.get_event_loop()

    # Create 10 tasks for requests.get synchronous call
    tasks = [
        asyncio.create_task(
            invoke_get_request(eventloop)
        ) for _ in range(10)
    ]

    done_tasks, _ = await asyncio.wait(tasks)
    status_codes = [d.result().status_code for d in done_tasks]

    return func.HttpResponse(body=json.dumps(status_codes),
                             mimetype='application/json')

customer can put the operations need to be handled by the worker loop into tasks and have them submitted to worker loop. can you give this a try?

however run_until_complete can be called once per loop only.

image

also python function worker does not manage or execute code execution outside the function natively(ex. code from module A when importing the top level module A in function_app.py are executed by python simply but worker execute functions in its managed way by attaching function context, invocation id, etc) and encourages running the business logic within function.

microsoft-github-policy-service[bot] commented 7 months ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment.