UniteLabs

Execution Modes

Controlling how the server handles concurrent invocations of the same SiLA command.

By default, a SiLA command can be executed by multiple clients at the same time, and each invocation runs independently. That's the right behavior for most commands, but not all of them - a command that moves a physical axis, writes to a shared resource, or otherwise can't tolerate overlapping executions needs the server to coordinate concurrent calls instead of just running them all at once.

The mode argument on sila.ObservableCommand and sila.UnobservableCommand controls exactly this: what the server does when a client calls a command while a previous invocation of that same command is still running.

src/unitelabs/tutorial/features/execution_modes.py
import asyncio

from unitelabs.cdk import sila


class ExecutionModeTutorialService(sila.Feature):
    @sila.ObservableCommand(mode=sila.ExecutionMode.PARALLEL)
    async def run_parallel(self, duration: float, *, status: sila.Status) -> None:
        """
        Run for `duration` seconds. This is the default execution mode.

        Args:
          Duration: How many seconds this command should run for.
        """
        status.update(progress=0)
        await asyncio.sleep(duration)
        status.update(progress=1)

    @sila.ObservableCommand(mode=sila.ExecutionMode.QUEUED)
    async def run_queued(self, duration: float, *, status: sila.Status) -> None:
        """
        Run for `duration` seconds, queueing concurrent invocations.

        Args:
          Duration: How many seconds this command should run for.
        """
        status.update(progress=0)
        await asyncio.sleep(duration)
        status.update(progress=1)

    @sila.ObservableCommand(mode=sila.ExecutionMode.SINGLE)
    async def run_single(self, duration: float, *, status: sila.Status) -> None:
        """
        Run for `duration` seconds, rejecting concurrent invocations.

        Args:
          Duration: How many seconds this command should run for.
        """
        status.update(progress=0)
        await asyncio.sleep(duration)
        status.update(progress=1)

Listing 1: The same command implementation, run under all three execution modes.

Calling run_parallel, run_queued, and run_single a second time - while the first call is still sleeping - demonstrates the difference between the three modes:

ModeBehavior
sila.ExecutionMode.PARALLEL (default)No coordination. Every invocation runs independently, with no limit on how many can be in flight at once.
sila.ExecutionMode.QUEUEDThe second call is accepted immediately, but its execution waits until the first one finishes. Invocations of the same command run one after another, in the order they were received.
sila.ExecutionMode.SINGLEThe second call is rejected outright while the first is still running - it is never queued.
Execution mode only coordinates repeated calls to the same command. QUEUED or SINGLE on run_queued has no effect on calls to run_parallel or any other command - each command is tracked independently.

Choosing a mode

  • Use PARALLEL for anything stateless or safe to run concurrently, such as reading a sensor value multiple times. This is the default, so most commands don't need to set mode at all.
  • Use QUEUED when overlapping executions are safe to run eventually, but not at the same time - for example, a command that moves a shared stage: every request should still complete, just not simultaneously.
  • Use SINGLE when a second, overlapping request should never happen, and calling it anyway is a client error worth surfacing immediately - a command that isn't idempotent, or hardware that has no way to serialize two in-flight commands safely.

Handling rejected invocations

Under SINGLE mode, an invocation that arrives while another is still running fails with the SiLA framework error CommandExecutionNotAccepted. This is raised by the SiLA framework itself, not by your command implementation, so there's nothing to catch or declare - see Framework Errors for how these are surfaced to the client.

For ObservableCommand, this distinction shows up in when the client finds out: QUEUED confirms the execution immediately and only delays the underlying work, while SINGLE fails the initiating call itself. For UnobservableCommand, both QUEUED and SINGLE affect the single blocking call the client makes - QUEUED simply delays the response, while SINGLE fails it immediately.