Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 BarFetcher task obtains bars for each leader instrument.
  • A SignalEngine task evaluates strategies for each instrument.
  • One SignalRouter task owns routing policy and its state cache.
  • One AccountExecutor task 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