Async and Concurrency
Aikido uses Tokio as an application runtime. It does not implement a scheduler; it chooses task and channel boundaries that encode domain ownership.
Task topology
- A
BarFetchertask obtains bars for each leader instrument. - A
SignalEnginetask evaluates strategies for each instrument. - One
SignalRoutertask owns routing policy and its state cache. - One
AccountExecutortask owns each deployment’s broker client. - Monitoring, command, WebSocket, persistence, and alert tasks surround the core path.
Tasks are concurrent. They may run in parallel on Tokio’s worker threads, but the design does not depend on a particular task remaining on a particular OS thread.
Bounded channels are asynchronous queues
The core channels use tokio::sync::mpsc::channel(64). A send that finds a full
queue waits asynchronously. This provides two properties:
- memory cannot grow without bound merely because a consumer is slow;
- overload propagates upstream toward the producer.
This is backpressure, although it is not enough by itself. A queue full of old trading decisions may be bounded yet unsafe, so the executor independently rejects stale exposure-increasing signals.
select! merges independent event sources
The router waits for either engine batches or executor feedback:
tokio::select! {
batch = signal_rx.recv() => { /* route forward */ }
update = state_rx.recv() => { /* fan state backward */ }
}
This is concurrent waiting inside one task, not parallel execution. Keeping
both branches in one router task also gives RouterStateCache a single mutable
owner, avoiding a mutex around routing decisions.
The account executor is an actor-like single writer
An executor owns client: C, HashMap<String, ContractRuntimeState>, command
receiver, reconcile schedule, and shutdown lifecycle. Callers send enum
commands. They cannot concurrently mutate the broker or position maps.
The executor deliberately prioritizes queued runtime-control commands ahead of ordinary signal commands when draining its channel. A plain FIFO queue is not always the correct service policy for safety operations.
Source: account_executor.rs:2290
Avoiding locks across .await
Shared stores and UI snapshots use standard mutexes, but critical sections are kept short: clone or calculate the needed value, drop the guard, then await. The signal engine, for example, finishes strategy evaluation inside a block before awaiting its channel send.
Holding std::sync::MutexGuard across .await would both serialize unrelated
tasks and can make a spawned future fail its Send requirement.
Supervision is not fail-fast
The binary collects task handles in a JoinSet and records task failures while
other live tasks continue. It reports failure only after the task set drains.
That is an explicit availability policy, not Tokio’s default behavior.
Source: runtime.rs:3457