Task
Xila needs more than an async executor. Other services must be able to answer questions such as which task is making this request?, which user does it run as?, what environment does it inherit?, and where should a signal be delivered?
The Task module gives async work that logical identity and metadata. Registered Embassy spawners still run the futures; Xila's task manager adds the system-level context around them.
Mental model
Think of the task::Manager as the directory that connects a Xila task to two different worlds:
- outward, a stable
task::TaskIdentifierand metadata that other Xila services can reason about; - downward, the executor/spawner that actually polls the future.
The distinction matters because executor-internal task identity is an implementation detail. Xila can expose one logical task model to the VFS, user system, ABI, and application runtimes without making those components depend directly on Embassy internals.
What happens when a task is spawned?
When runtime code asks the manager to spawn work, the manager first creates a logical task record. That record inherits user, group, and environment information from its parent when the parent is known.
The manager then chooses the requested spawner or, when none is specified, the currently least-loaded registered spawner. Embassy receives the future and performs the actual cooperative execution. Once the future begins running, Xila associates Embassy's internal task identity with the logical TaskIdentifier, allowing other code to recover the current Xila task later.
When the future finishes, its join handle is signalled and the manager removes the task metadata. If that task had children, they are reparented to the root task rather than being left with a dead parent.
This is a lifecycle description, not a requirement for callers to manually register and unregister tasks: spawn owns that bookkeeping for normal spawned work.
What the task identity carries
A task record is deliberately broader than an executor handle. It currently tracks:
- a short human-readable name;
- parent/child relationship;
- user and group identifiers;
- inherited environment variables;
- pending signal state;
- the selected spawner and the executor-internal identifier used for reverse lookup.
That is why seemingly unrelated modules depend on Task. The VFS can apply permissions in the context of the calling task, executables can inherit environment state, signal delivery has a stable destination, and runtime boundaries can carry one task identifier instead of learning executor-specific details.
Scheduling is still cooperative
The Task module does not implement a preemptive CPU scheduler or process address-space isolation. It delegates execution to Embassy executors. A future that never awaits or yields can therefore starve other work scheduled on the same executor.
Spawner selection is also intentionally simple: the current heuristic balances by registered task count, not CPU consumption, latency, or IO cost. Registering several spawners makes multi-executor deployments possible, but it does not turn the manager into a workload-aware scheduler.
Under the hood
Each task::Manager owns an async RwLock around three BTreeMaps:
tasks: BTreeMap<TaskIdentifier, Metadata>stores the logical task records;identifiers: BTreeMap<usize, TaskIdentifier>maps executor-internal identity back to Xila identity;spawners: BTreeMap<usize, embassy_executor::Spawner>stores the execution endpoints available to this manager.
Read-heavy metadata queries take the read side of that lock; registration, removal, environment mutation, and signal changes take the write side.
Manager::new() creates those registries without installing a production singleton. The current executor task becomes that manager's logical root when the manager first needs to resolve it. Test/test-harness configurations retain helper accessors, but production integrations are expected to pass the manager explicitly.
Task names are currently stored in an inline SmolStr and truncated by a 23-byte slice during registration (with a UTF-8 boundary caveat described below). Environment variables are cloned from the parent task into the child record. Signals are represented by a SignalAccumulator bitset.
This diagram describes the current implementation, not the public task contract. The map layout, lock granularity, name storage, and spawner-selection heuristic may evolve without changing what a task identifier means to callers.
Failure and resource boundaries
The manager has finite identifier space, with a TooManyTasks registration error. However, the current spawn path panics on a registration error before it can propagate that variant. Asking for an unknown explicit spawner fails with InvalidSpawnerIdentifier, while operations on stale/unknown logical identifiers can return InvalidTaskIdentifier.
Those exact variants are reference-level details; architecturally, the important point is that logical registration and executor dispatch can fail independently. A task is only useful after both the logical record and its execution mapping are established.
The C/ABI thread surface is also not yet complete: several xila_thread_* entry points remain unfinished. That limitation belongs to the ABI boundary rather than changing the internal Task model described here.
Exact task APIs and spawn failure behavior
Manager::new() constructs an independent registry. The executor's current task is registered as that manager's logical root on first use of get_current_task_identifier().await; it is not pre-populated by the constructor. Production code passes an explicitly owned &'static Manager to spawn, while initialize() and get_instance() are only compiled for tests or the test_harness feature.
register_spawner(spawner) registers an Embassy execution endpoint and returns its SpawnerIdentifier. The asynchronous spawn signature accepts a parent identifier, name, optional spawner identifier and closure receiving the new task identifier:
let (join, task_id) = task_manager
.spawn(parent_id, "worker", None, |id| async move {
// Application work using id
})
.await?;The task manager must already have a registered spawner. With None, the current policy chooses the spawner with the fewest registered logical tasks; it is not a CPU-load scheduler.
Failure caveats: spawn currently calls expect if logical registration or the Embassy task-pool spawn fails, rather than returning a typed error at those points. An unavailable spawner can produce NoSpawnerAvailable; an unknown explicit spawner returns InvalidSpawnerIdentifier. Because registration precedes spawner selection and no cleanup occurs on these failure branches, a failed spawn can leave a registered task record. Consumers should not assume every failure rolls back automatically.
The register implementation truncates names by byte offset (&name[..23]), so a non-ASCII name crossing that UTF-8 boundary can panic. This is an implementation limitation, not a recommended naming contract.
Contract and implementation boundary
Code using Task should depend on manager-scoped identity, metadata, spawn/join behavior, environment inheritance, and signal semantics. It should not rely on the BTreeMap layout, internal Embassy identifier, current balancing heuristic, or fixed-width task-name policy.
Explore further
taskcrate — complete host-side Task Rustdoc.task::Manager— manager API and associated methods.task::TaskIdentifier— logical task identifier.subsystems/task/src/manager/lifecycle.rs— spawn, join and task identity.subsystems/task/src/manager/spawner.rs— spawner registration and selection.subsystems/task/src/manager/registration.rs— record allocation and cleanup.- Users — identity information stored on tasks.
- Virtual file system — a major consumer of task context.
- Time — another foundational service used alongside Task.