Skip to main content
Last Updated: March 17, 2025

Managing Long-Running Tasks on SaladCloud with Google Cloud Pub/Sub

Managing long running tasks, such as molecular simulations, LoRA training, and LLM finetuning, presents unique challenges on SaladCloud, due primarily to the interruptible nature of nodes. At the core of all solutions to this problem are a job queue, and progress checkpoints. The job queue is responsible for distributing tasks to workers, and detecting when a worker has been interrupted. Workloads should save checkpoints of their progress and upload it to cloud storage, so that they can be resumed from the last checkpoint in the event of an interruption. Workers should also upload completed artifacts to cloud storage.
Basic Architecture

Basic architecture for long-running tasks on SaladCloud

We will be using Google Cloud Pub/Sub as our job queue, and Cloudflare R2, an S3-compatible object storage service, as our cloud storage. We prefer R2 to AWS S3 for many SaladCloud workloads, because R2 does not charge for egress data, which helps reduce costs when distributed workers fetch inputs and upload results. Instrumenting your code to use S3-compatible storage will make it easier to switch storage providers in the future if you choose to do so. For this guide, we will build an application that slowly calculates a sum for n steps, sleeping for 30 seconds between steps to simulate work. We will set up a job queue and related resources, a storage bucket, a checkpoint saving system, and review a simple auto-scaling mechanism. You will need a Google Cloud account, and a Cloudflare account to follow this guide.

The Job Queue: Google Cloud Pub/Sub

Google Cloud Pub/Sub is a messaging service that allows you to send and receive messages between independent applications. We will use it to distribute tasks to workers. You can create a new Pub/Sub topic and subscription using the Google Cloud Console.

Relevant Limitations

  • Pub/Sub messages can be at most 10MB in size. This means that you should not send large payloads in a single message. Instead, you should send a reference to the payload in cloud storage.
  • Pub/Sub messages are not guaranteed to be delivered in order. This means that you should not rely on the order of messages in the queue to determine the order of tasks.
  • Pub/Sub is billed primarily based on the amount of throughput you use, in kB. This means there are significant cost savings to only including references to large assets, as opposed to encoding them in the message itself.
  • Similar to other hyperscaler clouds, permission management can be complex and painful in Google Cloud. Make sure you understand the IAM roles and permissions you are granting to your Pub/Sub resources.

Creating a Topic and Subscription

Navigate to the GCP Pub/Sub Console and create a new schema called job-schema. This will enable automatic message validation for your topic, which can be useful for ensuring that your workers are receiving the valid jobs. Use the following JSON schema, defining a Job record with a job_id and steps field:
With this created, navigate to the “Topics” tab, and click “Create Topic”. We’ll call our topic Jobs, and we won’t use the default subscription. Go ahead and select the schema we just created, and use the default google-managed encryption key.
Creating a new topic

Creating a new topic

Create a second topic called deadletter that we will use for failed jobs. For this one, we do not want the schema, and we do want to enable message retention. Next, navigate to the “Subscriptions” tab, and click “Create Subscription”. We’ll call our subscription job-workers, and assign it to the jobs topic. We’ll use the “Pull” delivery type, set message retention to the maximum of 31 days, and set it to “Never expire”. We’ll also set the “Acknowledgement deadline” to 60 seconds, which means that if a worker doesn’t acknowledge the message, or extend the deadline within 60 seconds, the message will be handed out to a different worker.
Creating a new subscription

Creating a new subscription

We will enable exactly once delivery, to ensure that our long-running, presumably expensive tasks do not get run more than once. We will also enable dead-lettering, and set the dead-letter topic to the deadletter topic we created earlier. Maximum delivery attempts can be the minimum value of 5. We want failed jobs to be immediately retried.
Creating a new subscription

Creating a new subscription

Once this is created, go ahead and create another subscription called deadletter, and attach it to the deadletter topic. This is where you could attach something to process failed messages, although we will not be covering that in this guide. Finally, you will need to use the Service Accounts console to create an IAM principal with the Pub/Sub Subscriber permission set. Create a set of JSON service account keys for the new principal, and save the file as keys.json. Add this file to your .gitignore to avoid committing it to your repository.

Cloud Storage: R2

R2 is a cloud storage service from Cloudflare that is compatible with the S3 API. It is a great choice for SaladCloud workloads because it does not charge egress fees, which helps reduce costs when distributed workers fetch inputs and upload results. From the R2 console, navigate to “R2 Object Storage”, and click “Create Bucket”.
The R2 Object Storage Console

The R2 Object Storage Console

Give your bucket a meaningful name, and select an appropriate location. We are going to use the standard storage class, and automatic location.
Creating a new bucket

Creating a new bucket

Once your bucket is created, you will need to create an access key and secret key. Select “Manage API tokens” from the ”{ } API” menu, and click “Create Token”.
Navigate to manage api tokens

You still need an API token to access your bucket

Create a token with “Object Read & Write” permissions, and only grant it access to the bucket we’ve just created. Since secret rotation is outside the scope of this guide, we’re going to use the “forever” TTL. However, it is best practice to user shorter-lived secrets and to have easy automatic mechanisms in place to rotate secrets as needed. Once created you will be given an access key and secret key. Save these somewhere safe, as you will not be able to retrieve them again. The application code will get these keys from environment variables, so you will need to set them in your environment. Also on that page will be the S3 endpoint URL for your bucket. Save this as well, as it will be needed in the application code.

Instrumenting Our Application

We’re going to use the boto3 library to interact with R2, and the google-cloud-pubsub library to interact with Google Cloud Pub/Sub. You can install these libraries with pip install boto3 google-cloud-pubsub. First, we need to set up our environment variables. All of the following environment variables will be needed by the application code. There are several ways to do this, but what I’ve done for my development environment is create a file called worker.env in the root of my project, and add the following lines:
To get the GCP_KEY, you can run the following command:
It is important to use the -w 0 flag to ensure that the base64 encoded string is on a single line. Then, to source this into my environment when I run my code, I run the following command:
Make sure *.env is in your .gitignore. You don’t want to commit your secrets to your repository. Now, create a file called main.py in the root of your project, and add the following code:
First, let’s look at our simulated workload:
Next, we want some helper functions to interact with R2:
Next, we want a function that will periodically extend the message deadline:
We also will use some helper functions to interact with Pub/Sub:
Now, we need a function that ties everything together:
Finally, we need a function to start the worker:

Completed Example

Submitting Jobs To The Queue

Now we need to populate the queue with jobs. First, we’ll define some environment variables in a new file submitter.env.
I’ve saved mine in a file called submitter.env, and I’m going to source them into my environment with the following command:
Suppose we have a csv with our 10,000 jobs, and we want to submit them all. Our CSV (data.csv) looks like this, with 10,000 rows.
We can submit all of these jobs with the following code:

Running the Job Submitter

To run the job submitter, you can use the following command:

Containerize the Worker Application

Now that we have our worker application and our job submitter, we can package our worker in a docker container, and run it on a SaladCloud Container Group. First, let’s make sure our dependencies are documented in requirements.txt.
Now, we’re going to use create a launch script called launch.sh that will decode our base64 encoded GCP service account key, and then run our worker application.
Now, create a new file called Dockerfile. Our application is simple, so a basic python base image should be fine.
Now, build the docker image, and use a tag that makes sense for you.
Now, we can test it locally to make sure it works, before we deploy it to SaladCloud.
You should see it start up and begin processing a job. Once this is working, you can go ahead and terminate the container with Ctrl+C. Now, we can push the image to Docker Hub.

Deploying the Worker to SaladCloud

To deploy our worker to SaladCloud, we need to create a new Container Group. This can be done via the API, SDKs, or the Portal. We’re going to use the Portal. We’re going to create a new Container Group, and we’re going to use the image we just pushed to Docker Hub. We’re going to request 100 replicas (the max via the portal), and we’re going to set all of our environment variables from worker.env.
Creating a new container group

Creating a new container group

Our application is extremely simple, so we’re going to only request 1 vCPU, 1 GB of RAM, and no GPU. Your hardware requirements are likely significantly higher than this.
Setting the hardware requirements

Setting the hardware requirements

All CPU-only jobs are prioritized as “Batch” (the lowest tier), and we don’t need any additional storage for this particular application.
Setting the job priority and storage requirements

Setting the job priority and storage requirements

We do not need the container gateway, as our application pulls its work from a queue. We also do not need health probes, as those are primarily for services accessed via Container Gateway. Go ahead and hit deploy, and you’ll be taken to the container group page, where you can see its status.
Deploying the container group

Deploying the container group

After a few minutes, instances should start to become “Running”.
Container group running

Container group running

Validating That It Works

Now that our cluster is up and running, we can go to the Subscription console, and look at the “metrics” tab to verify there is activity on the subscription.
Active messages in the subscription

Active messages in the subscription

From the R2 console, we can see that our bucket is being filled with checkpoints and results.
Checkpoints and results in the R2 bucket

Checkpoints and results in the R2 bucket

Autoscaling

Now that we have our worker running, we can set up some simple autoscaling to automatically scale the number of replicas up and down based on the number of messages in the queue. There are many ways to implement autoscaling, but for simplicity, we recommend creating a scheduled task that runs every 5 minutes, and sets the number of replicas to be equal to the number of messages in the queue, limited to 250 replicas (the maximum in the API). To implement this, you can use a serverless function, such as Cloudflare workers, or GCP Cloud Functions Here is the outline of a python implementation that you can modify to suit your needs.
From the Salad API, you will need the following endpoints:

Conclusion

In this guide, we’ve shown you how to set up a simple long-running task worker on SaladCloud using Google Cloud Pub/Sub as a job queue, and R2 as a storage backend. We’ve also shown you how to deploy the worker to SaladCloud, and how to implement autoscaling based on the number of messages in the queue.