Tasks#

This guide shows how to define tasks for your own services and execute these tasks from another context. Tasks are pieces of work that are executed on a remote OrangeQS Juice service, which makes OrangeQS Juice tasks a form of RPCs. By defining your own task types and handlers you can let others execute actions on your service.

This guide is split into the following sections:

  • Task flow explains how tasks are routed through the task manager.

  • Defining Custom Tasks shows how to define a task schema and register a handler on a service.

For executing and scheduling tasks, see the Running Tasks tutorial.

Task durability

Tasks are held in an in-memory, so they do not survive a task manager restart. All task states will be lost after the restart.

Task flow#

Tasks are routed through the task manager, the OrangeQS Juice service responsible for collecting and dispatching tasks. The task manager is responsible for:

  • Collecting tasks from clients.

  • Routing tasks to the correct service.

  • Communicating the result back to the client.

Tasks are executed using a request and reply model. Clients submit tasks to the task manager. The task manager dispatches the submitted task to the corresponding OrangeQS Juice service. The OrangeQS Juice service executes the task by calling the registered handler. A task handler is a function that is registered to execute all tasks of a type. The OrangeQS Juice service sends the reply back to the task manager. The task manager sends the reply back to the client.

The full flow — both the immediate request and reply and the scheduled path — is shown in the diagram below. Inside the task manager the router and the scheduler cooperate.

        %%{init: {"theme":"base","themeVariables":{"background":"transparent","primaryColor":"#1e293b","primaryTextColor":"#f1f5f9","primaryBorderColor":"#f97316","lineColor":"#64748b","actorBkg":"#1e293b","actorBorder":"#f97316","actorTextColor":"#f1f5f9","actorLineColor":"#64748b","signalColor":"#64748b","signalTextColor":"#64748b","labelBoxBkgColor":"#334155","labelBoxBorderColor":"#f97316","labelTextColor":"#f1f5f9","loopTextColor":"#64748b","noteBkgColor":"#f97316","noteTextColor":"#0f172a","noteBorderColor":"#ea580c","activationBkgColor":"#334155","activationBorderColor":"#f97316","sequenceNumberColor":"#0f172a"}}}%%
sequenceDiagram
    actor C as Client
    participant R as Router
    participant SL as Serial line
    participant SC as Scheduler
    participant S as Target service

    alt immediate — execute(task)
        C->>R: execute request (target, payload)
        R-->>C: task id
        opt non-parallel
            R->>SL: wait for target's slot
        end
        R->>S: payload
        S->>S: call registered handler
        S-->>R: reply
        R-->>C: reply
    else scheduled — execute(task, run_at / recurrence / priority)
        C->>R: ScheduleTask request
        R->>SC: submit(task)
        SC-->>R: task id
        R-->>C: TaskHandle
        Note over SC: each tick — dispatch eligible Tasks
        opt non-parallel
            SC->>SL: acquire target's slot
        end
        SC->>R: dispatch occurrence
        R->>S: payload
        S-->>R: reply
        R-->>SC: reply
        SC->>SC: emit Event, reschedule if recurring
    end
    

For an immediate execute(task) with no scheduling arg the flow is:

  1. A client sends an execute request with a target service and payload to the task manager.

  2. The task manager creates a unique task identifier and sends this back to the client.

  3. The task manager checks whether the task is parallel. A task is parallel if its type sets parallel = True.

    • A parallel task is dispatched to the target service immediately.

  4. The target service receives the payload and calls the configured handler. A task handler is a function that is registered to execute all tasks of a type.

  5. The target service returns the result of the handler as reply to the task manager.

  6. The task manager returns the reply to the client.

Defining Custom Tasks#

A task definition consists of the task schema and the task handler. The task schema contains the task payload (arguments) and whether the task is parallel or synchronous. The task handler is the function that executes the task on the target service. The task payload will be passed to the handler on the target service.

We start by defining the task schema.

from typing import ClassVar
from orangeqs.juice.task import Task

# The task schema
class Greeting(Task):
    # The task payload
    greeting: str = "Hello"
    name: str

    # Whether the task is allowed be executed in parallel
    parallel: ClassVar[bool] = True

Secondly, we configure the handler on the service instance.

from orangeqs.juice.service import Service

# Option 1: Define the handler as a function
def _handle_greeting(payload: Greeting):
    return f"{payload.greeting} {payload.name}"

class MyService(Service):

    # Option 2: Define the handler as a method
    def _handle_greeting(self, payload: Greeting):
        return f"{payload.greeting} {payload.name} from {self.service_name}"

    def __init__(self, service_name: str) -> None:
        super().__init__(service_name)

        self.register_handler(Greeting, _handle_greeting)  # Option 1
        self.register_handler(Greeting, self._handle_greeting)  # Option 2