How to get media generated from background task to the client side

I am using a background process to generate some pdf reports as the server call times out. I don’t want to save it in the database if possible. Is there a way I can pass the pdf media object generated in the background task to the client code calling it, e.g. as a link url?

I tried passing it as a task state, but it is not supported. Is there another (possibly simpler) way to go about this?

server code

@anvil.server.background_task
def create_trxn_pdf_background():
  media_object = anvil.pdf.render_form('myform')
  anvil.server.task_state['url']=media_object

client code

while not task.is_completed():
        time.sleep(15)        
media_object = task.get_state()['url']

error

anvil.server.SerializationError: Illegal value in a anvil.server.task_state. Cannot use BlobMedia objects in task state.

1 Like

Let me start by saying I have not used background tasks, so I read the documentation and I think you just need to layer one more step by creating a callable function that returns a task object that is created by running your decorated ‘@anvil.server.background_task’ function. The result of this background task function should return the media object you want.

When task.is_completed() becomes True, the value of task.get_return_value() will be the media object.

2 Likes

So it seems like the .get_state() of the running task is really for communicating information about the task and not returning the final value. (You could use it to give you access inside the running task for example to see how much has been completed, or what file in a list of files it was currently processing, etc.)

Rewriting your code a bit yields:

Server code

@anvil.server.background_task
def create_trxn_pdf_background_task_object(form_object):
  return anvil.pdf.render_form( form_object )


@anvil.server.callable
def create_trxn_pdf_background(form_from_client):
  task_obj = anvil.server.launch_background_task('create_trxn_pdf_background_task_object', form_from_client)
  
  return task_obj

Client Code

task = anvil.server.call('create_trxn_pdf_background' form_to_become_pdf )
while not task.is_completed():
        time.sleep(15)        
#below should be a form self.media_object so that the object is stored in browser memory on the client side, otherwise it will be destroyed as soon as the one request for a url is completed (so it should probably be self.media_object to make it available to the entire form)
media_object = task.get_return_value() 
media_object_url = media_object.url

also this will not work with the media_object.url property since you do not want the media object stored in a data_table, the media_object.url property will point to the clients local machine and so is None. You will have to pass the media object directly into a link. like the documentation here:
(essentially you just set the url of the link to the media_object variable instead of a url string)

https://anvil.works/docs/client/components/basic#link

If a Link’s url property is set to a Media object, it will open or download that media in a new tab.

m = anvil.BlobMedia('text/plain', b'Hello, world!', name='hello.txt')
c = Link(text='Open text document', url=m)

Also also also, I would not use a while loop to delay gathering the status of task.is_completed() I strongly suggest you create a download button or link and make it not visible, then use a timer to check the status of .is_completed() , then if it is, set the link property to download and make the link visible.
You can even hide the download button or link once the user clicks on it.

Edit: I suppose you could also create a button that when clicked just calls

anvil.media.download( self.media_object )

And this would just cause it to download into the users browser without a link. :cake:

1 Like

Thanks for the detailed reply. I tried this out, but this runs into the serialization issue, it cannot return a media object back. So, I don’t have a media object to assign to a Link. As the media object is temporary, I cannot get a url to it to pass that instead.

:thinking:

Is there some kind of security related reason why you do not want anything stored in a data table?

If it is just a space issue, I have written code that uses a small requests table to store media objects, but then cleans up old requests in the table the next time it is called (or really when the app login page is run at all it cleans up anything ‘old’)

Using a table means you can just use the url property for the user to download, or even going through a server module function to retrieve the object from the table, so at the same time you could mark the item as downloaded in the table so it could be cleaned up later.

Thanks again for the detailed replies. You are right, I did end up using the data table finally as that looks like the cleanest way. I was not sure if there was a simple was to avoid this and I just didn’t know how to do it. I simply rewrite requests for the same pdf report from the same user on top of the previous one in the data table, so that I don’t have to write a cleanup procedure.

1 Like

Great, as long as you don’t go over the row limit for the free plan with users or any other limits if you are on the paid plans then it should work forever. (Most use cases will not see 50000 or 150000+ different users but who knows)

If someone else is reading this later and is interested in how to clean up the table (delete old rows) here is some code:

from datetime import datetime, timedelta
import anvil.tz

when creating a new request for your media object and inserting it into a new row for a user have a ‘date and time’ column and add something like this to the row insert:

last_update_time=datetime.now(anvil.tz.tzutc())

For example mine looks like this:

app_tables.requests_handler.add_row(
                                    pending=True,
                                    request_id=unix_time_microseconds_int,
                                    user_email=anvil.users.get_user()['email'],
                                    media_file=my_blobmedia_object,
                                    last_update_time=datetime.now(anvil.tz.tzutc())
                                      )

then make a callable function in the server module like:

@anvil.server.callable
def cleanup_unused_files():
  number_of_hours_before_cleanup = 24

  rows = app_tables.requests_handler.search(last_update_time=q.less_than_or_equal_to(datetime.now(anvil.tz.tzutc() ) - timedelta(hours=number_of_hours_before_cleanup) )  )

  if len(rows) > 0:
    for row in rows:
      row.delete()
      
  return 

Then include a call to this function whenever someone requests your media file.

You could also do without the @anvil.server.callable if you included this in whatever server module function created or stored the media object, since they would be running in the same place.

I’d call this a workaround for the actual serialization problem as pointed out by @ananth.krishnamoorth.
Of course in some applications using a data table would be fine.

I’ve created a feature request: Let background task return a Media object

I always thought it was a feature, not a bug. :man_shrugging: