Running Tasks#
Tasks are pieces of work that run on a remote OrangeQS Juice service. A client submits a task, the task manager routes it to the target service, the service runs it and the result travels back to the client. This makes tasks a form of remote procedure call: you call an action and it executes somewhere else.
This guide walks you through a simple task workflow.
Defining a task to describe the work you want to run.
Executing a task to run it on a service and read the result.
Scheduling a task to run it later instead of right now.
Next steps for accessing more advanced features of tasks.
We will assume you have opened and logged in to the OrangeQS Juice JupyterHub from your browser. This guide targets the shared service, which is available by default in every OrangeQS Juice installation.
Defining a task#
This section explains how to describe the work you want a service to run.
A task is a Python class that carries its payload — the arguments the service needs to do the work.
For this guide we use IPythonTask, a ready-made task type that runs a block of code in the target service’s IPython kernel and returns the result.
By subclassing it, you only have to fill in the code property.
Open up a new notebook from the JupyterHub launcher and define the task:
from pydantic import computed_field
from orangeqs.juice.schemas.tasks import IPythonTask
class HelloServiceTask(IPythonTask):
service_name: str
@computed_field
@property
def code(self) -> str:
return f"print('Hello from {self.service_name}!')"
Note that the order of the decorators matters here: @computed_field must sit above @property.
Executing a task#
This section explains how to run your task on a service and read back the result.
Tasks are executed with a Client.
Create a client and build a task instance with the payload you want to send:
from orangeqs.juice import Client
client = Client()
payload = HelloServiceTask(service_name="shared")
There are two ways to run the task, depending on whether you want to wait for the result:
# Option 1. Schedule the task and do not wait for the result.
request = await client.request("shared", payload)
task_id = request.task_id
result = await request
# Option 2. Schedule the task, wait for it to finish and return the result.
result = await client.execute("shared", payload)
Both target shared, the service the task runs on.
execute() blocks until the task finishes and hands you the result directly, which is the simplest way to start.
request() returns immediately with a handle you can await later, which is useful when you do not want to block.
See the respective docstrings for more detailed information.
Scheduling a task#
This section explains how to run a task later instead of immediately.
By default execute() blocks and returns the task result.
Passing a scheduling argument flips this: the call returns immediately with a
TaskHandle, since a scheduled task cannot block for a result that does not exist yet.
The scheduling argument is run_at: the earliest time the task should fire.
It means “not before this time”, never “exactly at this time”.
from datetime import datetime, timedelta, timezone
# Run once, 10 seconds from now.
handle = await client.execute(
"shared", payload, run_at=datetime.now() + timedelta(seconds=10)
)
print(handle.status) # will be "queued"
# Await the handle to update the status and result when the scheduled Task is finished.
schedule_status = await handle
print(schedule_status.status) # will be one of "done" / "failed" / "cancelled"
print(schedule_status.result) # the result of the Task, if it completed successfully
A scheduled task has a different lifecycle than an immediate task: it is first queued, then running, then done or failed.
(queued / running / done / failed / cancelled).
The handle references that Task — use it to read the last-known status, await the next completion, or cancel the schedule.
Next steps#
This guide has shown you the basics of running tasks. Tasks can do much more than run code blocks in a kernel, refer to the list below to learn about more of their features:
Define your own task types and register handlers on your own services, so others can execute actions you expose. Refer to the Tasks guide for more information.
Mark a task type as parallel so it fires as soon as it is eligible instead of waiting for the target’s slot. Non-parallel tasks are dispatched one at a time per target; parallel tasks bypass the wait.