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 includereCAP_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
- Creazione del socket: l’engine crea una coppia di socket Unix (o un canale IPC equivalente).
- Assegnazione delle estremità: un’estremità va al RelaySlave (nel processo del CartridgeHostRuntime), l’altra al RelaySwitch (nel processo dell’engine).
- 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 parametroinitial_notify. - 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).
- Task di lettura: dopo la verifica, entrambi i lati avviano i task di lettura per l’inoltro continuo dei frame.
- Flusso bidirezionale: il relay è ora attivo. I frame ordinari fluiscono in entrambe le direzioni. I frame RelayNotify e RelayState vengono scambiati secondo necessità.
- 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.