Skip to content

Latest commit

 

History

History
31 lines (25 loc) · 1.96 KB

File metadata and controls

31 lines (25 loc) · 1.96 KB

ADR-005: SessionBackend Async Trait with Pin<Box>

Status: Accepted Date: 2026-04-13

Context: TerminalSession needs to work with both local PTY and Docker exec backends. The backend must be swappable at runtime (a session is either local or Docker, decided at creation time). The trait needs async read for non-blocking I/O.

Decision: Define SessionBackend as a regular trait (not async_trait) with the read method returning Pin<Box<dyn Future<Output = Result<usize, io::Error>> + Send + 'a>>. Store backends as Box<dyn SessionBackend>.

pub trait SessionBackend: Send + Sync {
    fn read<'a>(&'a self, buf: &'a mut [u8])
        -> Pin<Box<dyn Future<Output = Result<usize, io::Error>> + Send + 'a>>;
    fn write_stdin(&self, data: &[u8]) -> Result<usize, io::Error>;
    fn resize(&self, cols: u16, rows: u16) -> Result<(), io::Error>;
    fn is_alive(&self) -> bool;
    fn kill(&self) -> Result<(), io::Error>;
}

Alternatives considered:

  • async_trait crate — adds the same boxing under the hood but hides it. Explicit Pin<Box> makes the cost visible and avoids a macro dependency.
  • Generics (TerminalSession<B: SessionBackend>) — avoids boxing but makes TerminalSession generic, complicating storage in collections (e.g., DashMap<String, Arc<TerminalSession>>) and requiring monomorphization.
  • Enum dispatchenum Backend { Local(LocalPty), Docker(DockerExec) } avoids trait objects but requires updating the enum for every new backend. Less extensible.

Consequences:

  • Dynamic dispatch via Box<dyn SessionBackend> — one allocation per session, negligible
  • read boxing cost: one heap allocation per PTY read (~8 KiB chunks), not per byte
  • write_stdin, resize, is_alive, kill are synchronous — no boxing needed
  • Send + Sync bounds enable Arc<TerminalSession> sharing across tokio tasks
  • Adding a new backend (e.g., SSH, Kubernetes exec) requires only implementing the trait