Asyncio compatible with anvil?

Hi,

I am trying to send data via the Uplink server function to multiple LCD1602 displays simultaneously. In the following code snippet i tried the asyncio function but it does not work. The REPL gives me the following error: Error: Call from invalid context.

Can anybody help me to solve this problem - any help would be greatly appreciated

Code Sample:

import anvil.google.drive
import anvil.google.mail
from anvil.google.drive import app_files
import anvil.users
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.server
import asyncio
from concurrent.futures import ThreadPoolExecutor

@anvil.server.background_task
def send_till_lcd():
   def get_user_data():
       # Search for the user by email in the articles table
       users = app_tables.articles.search()
       
       billigstetidspunkter_list = []
       billigstepriser_list = []
       device_id_list = []
       userscopy_id_list = []
       
       for user in users:
           billigstetidspunkter_list.append(user['Starttime'])
           billigstepriser_list.append(user['lowest_prices'])
           device_id_list.append(user['device_id'])
           userscopy_id_list.append(user['userscopy'])
       
       return billigstetidspunkter_list, billigstepriser_list, device_id_list, userscopy_id_list

   def send_data_to_lcd_sync(device_id, billigstetidspunkter, billigstepriser):
       try:
           anvil.server.call(f"pushtolcd_{device_id}", billigstepriser, billigstetidspunkter)
           print(f"Data sent to device ID: {device_id}")
       except Exception as e:
           print(f"Failed to send data to device ID: {device_id}. Error: {str(e)}")
       #time.sleep(2)  # Delay of 2 seconds

   async def send_data_to_lcd(device_id, billigstetidspunkter, billigstepriser):
       loop = asyncio.get_running_loop()
       with ThreadPoolExecutor() as pool:
           await loop.run_in_executor(
               pool, send_data_to_lcd_sync, device_id, billigstetidspunkter, billigstepriser
           )

   async def main():
       billigstetidspunkter_list, billigstepriser_list, device_id_list, userscopy_id_list = get_user_data()

       tasks = [
           send_data_to_lcd(device_id, billigstetidspunkter, billigstepriser)
           for device_id, billigstetidspunkter, billigstepriser in zip(device_id_list, billigstetidspunkter_list, billigstepriser_list)
       ]

       await asyncio.gather(*tasks)

   asyncio.run(main())

# Uncomment the line below if you want to execute the function
send_till_lcd()

The @anvil.server.background_task decorator works with anvil.server.wait_forever(), not with asyncio.run(main()).

Said that, I don’t understand what you are trying to do.

If you want an app to call an uplink function, then you need that function to be decorated as callable, not as background_task.

Otherwise… please explain what you are trying to do.
… and at what line you see the error.

1 Like

Hi Stefano,

Every minute i run the background task send_till_lcd(). This will push electricity prices and corresponding times/dates to pico units connected via uplink. each pico has its own unique id so I am using the same uplink for several picos. The picos are connected to an LCD display that will show the different electricity prices through the day.
My problem is that my current code run without the asyncio function resulting in the picos showing the prices one at a time and in sequence. I want to push data to the LCD displays so that they will all show the prices at the same time.
There is no line indication where the error occur and i assume that my approach to this problem is generally wrong as you suggest.

Thanks for the clarification, I thought the script I was looking at was the uplink.

Now I understand that’s a server module, with a background task executed by the scheduler every minute.

I would try something like this, where the scheduler launches send_till_lcd every minute and send_till_lcd launches a send_data_to_lcd per device:

@anvil.server.background_task
def send_till_lcd():
    users_data = [
        {
            'billigstetidspunkter': user['Starttime'],
            'billigstepriser':      user['lowest_prices'],
            'device_id':            user['device_id'],
            'userscopy_id':         user['userscopy'],
        }
        for user in app_tables.articles.search()
    ]
    for user_data in users_data:
        anvil.server.launch_background_task('send_data_to_lcd', user_data)


@anvil.server.background_task
def send_data_to_lcd(user_data):
    device_id = user_data['device_id']
    try:
        anvil.server.call(f"pushtolcd_{device_id}", user_data['billigstepriser'], user_data['billigstetidspunkter'])
        print(f"Data sent to device ID: {device_id}")
    except Exception as e:
        print(f"Failed to send data to device ID: {device_id}. Error: {str(e)}")
  • send_till_lcd is a background task so the scheduler can run it
  • send_data_to_lcd is a background task so send_till_lcd can execute many instances concurrently

Launching concurrent background tasks is similar to starting multiple threads, but with some additional overhead required to manage the background tasks in the Anvil infrastructure. Your call from invalid context error makes me think that your script was trying to access Anvil resources that were not correctly initialized. By launching new background tasks you will have a little overhead, but you should solve that problem.

Be careful: this is not hugely scalable!
Five or ten concurrent background tasks should work just fine, hundreds or thousands may choke the server.


I typed all of the above quickly, without any fact checking. These are my feelings, that’s what I would try first, but I also might be wrong on all accounts.

1 Like

I’m not familiar enough with asyncio to say what your specific issue is, but wanted to post to let you know that Anvil is compatible with asyncio. A long-running background task can use asyncio to run multiple coroutines at once, to avoid the memory scaling issues that come with multiple background tasks.

1 Like

This is good to know, a great architecture would be a background task, started by a scheduler every minute that checks to see if the same process is already running, then exits if it finds one.

It could then check a data table for queued work and spin off coroutines to do the work and write back results to the table, looping with a short timer to continue to check for new work.

This way you would only need one background task environment running, as long as you managed threading correctly (or used thread safe code) to avoid memory conflicts.

1 Like

Thanks a lot for all your input.
@stefano.menci i tried the code snippet you provided but unfortunately the result is the same. The data is shown on the LCD displays one at a time and not in a concurrent manner.
I would like to go for the asyncio solution but it seems that Anvil is somewhat limited to run asyncio processes ?

I wonder if the problem is the background tasks being to slow to launch, or the server calls not happening quickly enough.

Have you tried creating an uplink in your computer that sends the message to one single display, make multiple copies of the script, one per display, then run them all at once?

You could even add some simple syncing across all the scripts, for example they all could connect to the server, then wait for a file to exist before sending the text to their display.

At that point you create the file and the calls to the displays will all happen exactly at the same time, and you will see if the texts appear at the same time. If they don’t, then you can stop trying with async. If they do, then async as @jshaffstall says should work, and it makes sense to keep investigating.