Monday, September 4, 2023

Define and Call Background Task in Quart

def write2sqliteonecol(instance, stringonecol): # synchronous function
    try:
        instance.createandwritelocaldb((stringonecol, ))
    except Exception as error:
        print('Error writing DWDS to db!', error)
 
async def asyncwrite2sqliteonecol(instance, stringonecol): # asynchronous function
...
    instance.createandwritelocaldb((stringonecol, ))
...

Async case 1 - asyncio.get_event_loop().create_task:
asyncio.get_event_loop().create_task(asyncwrite2sqliteonecol(instance, stringonecol))
 
Async Case 2 - app.add_background_task with another function passing arguments:
async def task():  
    await asyncwrite2sqliteonecol(instance, stringonecol) 
app.add_background_task(task)
 
Sync case 3 - arguments after comma, not in brackets:
app.add_background_task(write2sqliteonecol, instance, stringonecol) #
 
Sync case 4 - arguments in brackets + lambda function:
app.add_background_task(lambda: write2sqliteonecol(instance, stringonecol))
 
Sync case 5 - needs import:
from functools import partial
app.add_background_task(partial(write2sqliteonecol, instance, stringonecol))
 
*****
Both asyncio.get_event_loop().create_task() and app.add_background_task() are methods to run tasks concurrently in Quart, but they serve slightly different purposes.

asyncio.get_event_loop().create_task() is a lower-level asyncio function that schedules a coroutine to be run on the event loop. It's a general-purpose way to run tasks concurrently in any asyncio-based application.

app.add_background_task() is a Quart-specific method that adds a task to be run in the background when the Quart app is running. One key feature of app.add_background_task() is that it ensures the task finishes during the shutdown of the Quart app, unless the server times out and cancels it. This is particularly useful in a production environment where you want to ensure that all tasks are completed before the app shuts down.

In terms of performance, there should not be a significant difference between the two methods. Both methods will run the tasks concurrently, which can lead to a significant performance improvement over running the tasks sequentially, especially for IO-bound tasks.

However, if you're writing to multiple SQLite databases, you should be aware that SQLite is not designed for high-level concurrency. It uses a file-based locking system for transactions which can become a bottleneck when many tasks are writing to the database concurrently. If high-level concurrency is required, you might want to consider using a different database system that is designed for concurrent access, such as PostgreSQL or MySQL.

The reason is that app.add_background_task is designed to ensure that the tasks finish during the shutdown of the application (unless the server times out and cancels). This is particularly useful in a production environment where you'd want all tasks to complete before the application shuts down.

On the other hand, asyncio.get_event_loop().create_task does not provide this guarantee. If the application shuts down before the task completes, the task may be cancelled and not finish its work. So, for writing to a database, which is a critical operation that should not be interrupted, app.add_background_task would be a better choice.

*******

functools.partial
is a function in Python's standard library that allows you to fix a certain number of arguments of a function and generate a new function. This can be useful when you want to "freeze" some parameters of a function.

Here is a simple example:

from functools import partial  
  
def multiply(x, y):  
    return x * y  
  
# Create a new function that multiplies by 2  
double = partial(multiply, 2)  
  
print(double(4))  # Outputs: 8  

In the context of the Quart code you've been working with, functools.partial could be used in scenarios where you want to predefine some arguments of a function that you want to run as a background task. For instance, if you have a function that writes to a SQLite database and takes the database connection and some data as arguments, you could use

functools.partial
to create a new function that has the database connection already set:

from functools import partial  
  
def write_to_db(connection, data):  
    # Code to write data to the database  
  
# Create a new function that writes to a specific database  
write_to_specific_db = partial(write_to_db, specific_connection)  
  
# Now you can use write_to_specific_db in your code and only pass the data  
app.add_background_task(write_to_specific_db, data)  
In this example,
write_to_specific_db
is a new function that takes only one argument (
data
), and uses
specific_connection
as the database connection. Source: AI from https://www.getonboard.dev

****

Quart has startup and shutdown methods that allow something to be started before the server starts serving and stopped when the server finishes serving. If your background task is mostly IO bound I'd recommend just using a coroutine function rather than a thread,

async def background_task():
    while True:
        ...

@app.before_serving
async def startup():
    app.background_task = asyncio.ensure_future(background_task())

@app.after_serving
async def shutdown():
    app.background_task.cancel()  # Or use a variable in the while loop

Or you can do the same with your Service,

@app.before_serving
async def startup():
    service.start()

@app.after_serving
async def shutdown():
    service.stop()

Note to check:
app.add_background_task(background_task) in "before" and app.background_tasks.pop().cancel() in "after".
 https://stackoverflow.com