Wednesday, April 24, 2024

#5570 (Jane doesn’t send email notifications) is not trivial. It was (of course) caused by my changes in the background task runner. I am really still too naive regarding asynchronous programming!

Should we add test coverage for asynchronous operations. A unit test case in noi1r that creates a log directory (in order to activate the socket logger), launches a pm linod process and some django-admin command and then checks the content of the lino.log file…

Here is an excerpt from lino_xl.lib.invoicing.models:

class Task(Runnable, UserAuthored):

    target_journal = dd.ForeignKey(
        'ledger.Journal',
        verbose_name=_("Target journal"),
        related_name="invoicing_task_targets")

    def __str__(self):
        return _("Make {}").format(self.target_journal)

class Runnable(Sequenced, RecurrenceSet):

    async def start_task(self, ar):
        ...

Arguments: (Unprintable Task(pk=1,error=SynchronousOnlyOperation(‘You cannot call this from an async context - use a thread or sync_to_async.’),)

Do we really need the asynchronous versions of ar.debug, ar.info and ar.warning?

Edit 20240426: Yes we do. Because these methods potentially lead to I/O operations. When calling them from an async context, code execution would potentially continue before they have done their work. I tried to reproduce the problem but without success. Maybe the problem exists only with Django?

"""
Trying to show why the logging module needs an async interface.
"""

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()

import asyncio
from time import sleep

from asgiref.sync import sync_to_async


info = logger.info
ainfo = sync_to_async(logger.info)

async def task1():
    info("- Run task1")
    sleep(1)

async def task2(name):
    info("- Run %s", name)
    sleep(1)


async def main():
    # await ainfo("Start task runner ")
    info("Start task runner ")
    count = 0
    while count < 2:
        count += 1
        # await ainfo("Start loop %s.", count)
        info("Start loop %s.", count)
        await task1()
        await task2("foo")
        await task2("bar")

    # await ainfo("Done task runner ")
    info("Done task runner ")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

“If you want to call a part of Django that is still synchronous, you will need to wrap it in a sync_to_async() call. If you accidentally try to call a part of Django that is synchronous-only from an async view, you will trigger Django’s asynchronous safety protection to protect your data from corruption.”

“Transactions do not yet work in async mode. If you have a piece of code that needs transactions behavior, we recommend you write that piece as a single synchronous function and call it using sync_to_async().”