In questa sezione

RelaySlave, RelayMaster e come le catene di relay collegano i componenti.

Coppia di relay

Una coppia RelaySlave/RelayMaster forma un ponte trasparente di frame su un socket Unix. La coppia collega un CartridgeHostRuntime (che gestisce i processi delle cartucce) al RelaySwitch (che instrada le invocazioni delle cap).

graph LR
    SW["RelaySwitch"] <-->|socket Unix| RM["RelayMaster"]
    RM <-->|socket Unix| RS["RelaySlave"]
    RS <--> PH["CartridgeHostRuntime"]

    class RM,RS accent

Tutti i frame ordinari passano invariati. Solo due tipi di frame vengono intercettati, RelayNotify e RelayState, che trasportano informazioni di controllo tra lo switch e l’host.

Sorgente: capdag-rs/src/bifaci/relay.rs.

RelaySlave

RelaySlave si trova all’estremità del socket Unix dal lato del CartridgeHostRuntime. Esegue due task concorrenti:

Task 1 (socket → locale): legge i frame dal socket (inviati dal RelaySwitch tramite il master). Intercetta i frame RelayState e ne memorizza il payload. Inoltra tutti gli altri frame al CartridgeHostRuntime locale. Applica un ReorderBuffer per garantire che i frame arrivino in ordine di sequenza.

Task 2 (locale → socket): legge i frame dal CartridgeHostRuntime. Scarta qualsiasi frame RelayState (le cartucce non devono originarli). Inoltra tutto il resto al socket.

Lo slave può anche iniettare su richiesta frame RelayNotify nello stream del socket, cosa che avviene quando cambia l’insieme di capacità del CartridgeHostRuntime.

pub struct RelaySlave<R: AsyncRead + Unpin, W: AsyncWrite + Unpin> {
    local_reader: FrameReader<R>,
    local_writer: FrameWriter<W>,
    resource_state: Arc<Mutex<Vec<u8>>>,
}

Il resource_state è condiviso con il CartridgeHostRuntime tramite resource_state_handle(), e dà all’host accesso all’ultimo payload RelayState (percorsi dei modelli, informazioni sulla GPU, ecc.).

Sorgente: relay.rs (RelaySlave).

RelayMaster

RelayMaster si trova all’estremità del socket Unix dal lato del RelaySwitch. Durante l’inizializzazione, il RelaySwitch legge il primo frame dal socket, un RelayNotify dello slave che annuncia le sue capacità e i suoi limiti.

Dopo l’inizializzazione, il task di lettura del RelaySwitch legge i frame direttamente dal socket del master. Il master memorizza gli ultimi manifest e limiti, che lo switch interroga per le decisioni di instradamento.

Il master applica anche un ReorderBuffer ed espone metodi per inviare frame (send_state per RelayState) e leggere frame (read_frame, che intercetta i frame di controllo e consegna solo i frame di dati).

Sorgente: relay.rs.

Intercettazione dei frame

sequenceDiagram
    participant PH as CartridgeHostRuntime
    participant RS as RelaySlave
    participant RM as RelayMaster
    participant SW as RelaySwitch

    Note over PH,SW: RelayNotify (slave → master)
    PH->>RS: Cap cambiate
    RS->>RM: RelayNotify (manifest, limiti)
    Note over RM: Memorizza il manifest, mai inoltrato
    RM--xSW: (intercettato)

    Note over PH,SW: RelayState (master → slave)
    SW->>RM: RelayState (informazioni sulle risorse)
    RM->>RS: RelayState (payload)
    Note over RS: Memorizza in resource_state, mai inoltrato
    RS--xPH: (intercettato)

    Note over PH,SW: Frame ordinari
    PH->>RS: REQ / CHUNK / END / ecc.
    RS->>RM: (passa invariato)
    RM->>SW: (passa invariato)

RelayNotify (slave → master)

I frame RelayNotify trasportano gli annunci di capacità dall’host dello slave allo switch. Contengono:

  • manifest (byte): manifest aggregato, codificato in JSON, di tutte le cartucce presenti sull’host. Il payload ha la struttura:
    {
      "caps": ["cap:effect=none", "cap:in=...;out=...;generate", ...],
      "installed_cartridges": []
    }
    

    caps è un elenco piatto di tutte le stringhe di URN di capacità di tutte le cartucce sane presenti sull’host. Ogni host deve includere CAP_IDENTITY (cap:effect=none); il RelaySwitch rifiuta le connessioni che non la annunciano.

  • max_frame, max_chunk, max_reorder_buffer: limiti del protocollo.

Quando un CartridgeHostRuntime ricostruisce il suo insieme di capacità (perché una cartuccia è stata aggiunta, è morta o è stata riavviata), invia un RelayNotify attraverso il RelaySlave. Lo slave lo inoltra al socket. Lo switch lo legge e aggiorna il manifest memorizzato dal master e la tabella aggregata delle cap.

I frame RelayNotify non trapelano mai attraverso il relay verso il lato opposto. Il Task 1 dello slave ignora i RelayNotify in arrivo (dal socket), e lo switch non li vede mai nel suo normale instradamento dei frame; vengono consumati durante l’inizializzazione e dal task di lettura.

RelayState (master → slave)

I frame RelayState trasportano informazioni sulle risorse dell’host dall’engine al processo host. Il payload è opaco (CBOR o JSON) e può contenere:

  • Security bookmark di macOS per l’accesso ai file.
  • Disponibilità di GPU/acceleratori.
  • Impostazioni di configurazione.
  • Richieste di capacità (ciò di cui l’engine ha bisogno da questo host).

L’engine invia RelayState attraverso lo switch al socket di un master specifico. Lo slave lo intercetta nel Task 1 (socket → locale) e ne memorizza il payload in resource_state. Il CartridgeHostRuntime legge questo stato condiviso e passa le informazioni pertinenti alle cartucce.

I frame RelayState non raggiungono mai il CartridgeHostRuntime come frame; vengono intercettati e memorizzati.

Policy (switch → host, passa attraverso)

Una Policy non viene intercettata. Lo switch la invia a un master come primo frame dopo il RelayNotify di quel master, e di nuovo a ogni cambiamento. Il master e lo slave la inoltrano invariata al CartridgeHostRuntime, che la tiene e la dà alle sue cartucce. Una Policy che arriva dal lato dell’host è una violazione del protocollo, e il relay termina: il Task 2 dello slave rifiuta quella inviata verso lo switch, e un master rifiuta quella letta dal suo slave.

Ciclo di vita della connessione

sequenceDiagram
    participant E as Engine
    participant RM as RelayMaster
    participant RS as RelaySlave
    participant PH as CartridgeHostRuntime

    Note over E,PH: 1. Creazione del socket
    E->>RM: Crea una coppia di socket Unix
    E->>RS: Altro capo del socket

    Note over E,PH: 2. Annuncio iniziale
    RS->>RM: RelayNotify (cap, limiti)

    Note over E,PH: 3. Verifica dell'identità
    RM->>RS: REQ(CAP_IDENTITY)
    RS->>PH: (inoltro)
    PH-->>RS: eco del nonce
    RS-->>RM: (inoltro)

    Note over E,PH: 4. Avvio dei task di lettura
    Note over E,PH: 5. Flusso bidirezionale — il relay è attivo
  1. Creazione del socket: l’engine crea una coppia di socket Unix (o un canale IPC equivalente).
  2. Assegnazione delle estremità: un’estremità va al RelaySlave (nel processo del CartridgeHostRuntime), l’altra al RelaySwitch (nel processo dell’engine).
  3. Annuncio iniziale: il RelaySlave invia un RelayNotify all’avvio, annunciando le capacità e i limiti del suo host. Questo viene attivato da run() con un parametro initial_notify.
  4. Verifica dell’identità: il RelaySwitch legge il RelayNotify, poi esegue la verifica dell’identità attraverso la catena di relay (la REQ per CAP_IDENTITY passa attraverso lo slave, al CartridgeHostRuntime, a una cartuccia, e la risposta di eco torna indietro).
  5. Task di lettura: dopo la verifica, entrambi i lati avviano i task di lettura per l’inoltro continuo dei frame.
  6. Flusso bidirezionale: il relay è ora attivo. I frame ordinari fluiscono in entrambe le direzioni. I frame RelayNotify e RelayState vengono scambiati secondo necessità.
  7. Arresto: quando uno dei due lati chiude il socket, il task di lettura dell’altro lato rileva l’EOF e si arresta.

Sorgente: relay.rs, relay_switch.rs.

Più master

Un RelaySwitch gestisce un array di master, ciascuno collegato a un host diverso:

graph TD
    SW["RelaySwitch"]
    SW --> RM0["RM_0"]
    SW --> RM1["RM_1"]
    SW --> RM2["RM_2"]

    RM0 --> RS0["RS_0"] --> PH0["PH_0"]
    RM1 --> RS1["RS_1"] --> PH1["PH_1"]
    RM2 --> RS2["RS_2"] --> PH2["PH_2"]

    PH0 --> P0["P0"]
    PH0 --> P1["P1"]
    PH0 --> P2["P2"]
    PH1 --> P3["P3"]
    PH2 --> P4["P4"]
    PH2 --> P5["P5"]

Ogni master fornisce un insieme diverso di capacità (anche se sono possibili sovrapposizioni). Lo switch aggrega tutte le capacità in un’unica tabella delle cap e instrada ogni REQ al master le cui cartucce possono gestire meglio la cap richiesta, usando la corrispondenza is_dispatchable e il ranking per specificità su tutti i master.

I master sono indipendenti. Un guasto in uno non influisce sugli altri. Lo switch contrassegna il master guasto come non sano e continua a instradare verso i master sani rimanenti.

Sorgente: relay_switch.rs.

Equivalente Swift

Relay.swift e RelaySwitch.swift in capdag-objc/Sources/Bifaci/ forniscono la stessa topologia dei relay per gli host basati su Swift. Le regole di intercettazione dei frame (RelayNotify e RelayState) e il comportamento di inoltro trasparente sono identici.