Execution Modes
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.
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:
| Mode | Behavior |
|---|---|
sila.ExecutionMode.PARALLEL (default) | No coordination. Every invocation runs independently, with no limit on how many can be in flight at once. |
sila.ExecutionMode.QUEUED | The 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.SINGLE | The second call is rejected outright while the first is still running - it is never queued. |
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
PARALLELfor 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 setmodeat all. - Use
QUEUEDwhen 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
SINGLEwhen 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.