In questa sezione

Parsing della notazione delle macchine, costruzione del DAG e risoluzione delle cap.

Panoramica

L’orchestratore trasforma una macchina in qualcosa che si esegue. Il suo input è la notazione delle macchine (Notazione delle macchine) oppure uno Strand del planner; il suo output è un MachinePlan per ciascuno strand, che execute_plan esegue.

flowchart LR
    MN["Notazione delle macchine"] --> RES["Risoluzione<br/>(modulo machine)"]
    ST["Strand del planner"] --> RES
    RES --> BUILD["MachinePlanBuilder"]
    BUILD --> MP["MachinePlan<br/>(uno per strand)"]
    MP --> EX["execute_plan"]

C’è un solo percorso. La notazione viene risolta in una Machine (ricerca delle cap, binding delle sorgenti agli argomenti, output presi, cardinalità, controllo dei cicli, inclusione delle macchine incluse), e MachinePlanBuilder emette un piano per ciascuno strand e la forma dell’esecuzione (Planner): nodi Cap e un nodo CapOutput per ogni altro output che un passo fornisce, nodi InputSlot e Output al confine, ForEach, Item e Collect dove la cardinalità richiede una mappa o una raccolta, archi Arg per ogni binding che non è l’input principale.

ResolvedNotation è il front end che un host usa: risolve la notazione una volta, dice che cosa la macchina prende e fornisce con i nomi che la notazione ha scritto, e costruisce il piano di uno strand per un’esecuzione. Un’esecuzione dice, per nome dell’input, che cosa lega (RunInputs): quali input sono sequenze, e il tipo di ciò che lega dove questo è più specifico del tipo dichiarato dell’input (un file di un tipo dato a un input che ne accetta diversi). Il piano segue i tipi dati: un passo il cui output è il suo input invariato produce il tipo dell’esecuzione. Un tipo che non è conforme a quello dell’input, o un nome che non è un input, viene rifiutato. Una macchina che ne include altre viene risolta rispetto alla MachineSource dell’host.

Il piano di uno strand per un’esecuzione è uno StrandPlan: lo strand, il piano e il nome che la notazione scrive per ciascun nodo che contiene un valore. È ciò che un’esecuzione è. Ogni risoluzione conia nuovi token di passo, quindi un host a cui è stato mostrato un piano, e che ha indicizzato i valori degli argomenti con i token dei suoi passi, esegue quel piano: lo restituisce, e il piano viene confrontato con quello in cui la notazione si risolve ora per la stessa esecuzione (StrandPlan::check_is_a_resolution_of). I passi vengono accoppiati con i loro nomi scritti (ogni passo che contiene un valore ne ha uno), le mappe con il loro nodo, e un passo che non contiene alcun valore con tutto il resto di ciò che è; con le coppie, i token del piano restituito vengono rinominati e i due devono essere uguali campo per campo, altrimenti il controllo dice dove differiscono. L’accoppiamento serve solo al controllo: il piano che si esegue mantiene i propri token.

Sorgente: capdag-rs/src/orchestrator/machine_plan.rs (ResolvedNotation, RunInputs, StrandPlan, build_plans_from_notation), capdag-rs/src/planner/plan_builder.rs.

Ambiti e segmenti

Un piano ha un ambito per ciascun luogo in cui un passo può essere eseguito: la macchina stessa e il corpo di ciascun ForEach. execute_plan esegue un ambito eseguendone le unità: catene di passi e le mappe direttamente al suo interno. Una catena viene consegnata all’EngineRuntime come segmento: un ResolvedGraph che contiene solo quei passi (Esecuzione).

ResolvedGraph

Un ResolvedGraph è un insieme di passi con le definizioni delle cap che eseguono, collegati dai nodi che leggono e producono:

pub struct ResolvedGraph {
    pub nodes: HashMap<String, String>,   // nome del nodo → il media URN che contiene
    pub steps: Vec<ResolvedStep>,          // invocazioni di cap, in nessun ordine particolare
    pub graph_name: Option<String>,
}

Un nodo che nessun passo del grafo produce è un input del grafo: il suo valore viene fornito quando il grafo viene eseguito.

Sorgente: capdag-rs/src/orchestrator/types.rs.

ResolvedStep

Un passo è un’invocazione di una cap:

pub struct ResolvedStep {
    pub token_id: StepToken,       // l'identità del passo, portata dalla macchina
    pub node: String,              // dove viene memorizzato il suo output PRINCIPALE
    pub cap_urn: String,
    pub cap: Cap,                  // la definizione completa della cap
    pub inputs: Vec<StepInput>,    // gli stream che gli vengono inviati
    pub outputs: Vec<StepOutput>,  // i suoi altri output che qualcosa prende
    pub out_media: String,         // il media URN dell'output principale
    pub policy: StepPolicy,        // che cosa fa quando fallisce
}

pub struct StepInput {
    pub stream_urn: String,        // `in=` per l'input principale, l'URN dell'argomento per ogni altro
    pub source: StepSource,
}

pub enum StepSource {
    Node(String),                  // il valore di un nodo, così com'è
    Gather(Vec<String>),           // i valori di più nodi, in ordine, come una sola sequenza
}

pub struct StepOutput {
    pub output_media_urn: String,  // l'URN dell'output nella definizione
    pub node: String,              // dove viene memorizzato il suo valore
}

Una cartuccia distingue i suoi input tramite stream_urn. Un membro raccolto che contiene un solo valore contribuisce con un elemento e uno che contiene una sequenza contribuisce con i suoi elementi; una raccolta senza membri è una sequenza vuota. Uno stream di risposta per un output dichiarato che nessuno StepOutput prende viene verificato, consumato e scartato. Ogni passo porta la propria Cap, quindi nulla viene risolto di nuovo durante l’esecuzione. I valori che un argomento riceve dal registro degli argomenti dell’esecuzione, e le costanti, qui non sono input: vengono inviati con la richiesta.

Sorgente: types.rs.

Convertitore di piani

steps_to_resolved_graph(plan, step_ids, registry) converte un insieme scelto di nodi Cap di un piano in un ResolvedGraph; plan_to_resolved_graph(plan, registry) converte ogni passo di un piano che non ha mappe.

Tipo di nodo del piano Elemento del grafo
Cap Un passo. Il suo arco entrante Direct è il suo input principale e ciascun arco entrante Arg un altro stream di input.
CapOutput Uno StepOutput del passo a cui appartiene: il nodo che contiene quell’output.
Collect Una sorgente Gather, con i suoi membri nell’ordine dei binding.
InputSlot, Item e ogni nodo prodotto fuori dai passi scelti Un nodo che il grafo legge e non produce.
Output Niente: execute_plan legge il nodo che nomina quando l’esecuzione termina.
ForEach Non convertito. plan_to_resolved_graph rifiuta un piano che ne contiene uno; execute_plan esegue la mappa da sé e converte i passi del suo corpo.

Sorgente: capdag-rs/src/orchestrator/plan_converter.rs.

Validazione

Una macchina viene validata dove viene risolta (Machine::from_string, per strand): cap e nomi sconosciuti, il binding delle sorgenti agli argomenti e la conformità di ogni sorgente legata al tipo che il suo argomento accetta, gli output presi, la politica dei passi, il rilevamento dei cicli con l’algoritmo di Kahn, la coerenza del media URN di ogni nodo tra i wiring che lo toccano, e tutto ciò a cui è tenuta una macchina in posizione di passo. Ogni fallimento è uno degli errori elencati in Notazione delle macchine.

Nulla a valle valida di nuovo. Il costruttore di piani e il convertitore controllano solo gli invarianti del proprio input (MachinePlan::validate, e i controlli propri del convertitore sui passi che riceve), e segnalano una violazione come InvalidGraph: a quel punto è un difetto, non un errore dell’utente.

Tipi di errore

pub enum ParseOrchestrationError {
    MachineSyntaxParseFailed(String),      // la notazione non è stata analizzata o risolta
    CapNotFound { cap_urn: String },        // cap URN non presente nel registro
    NotADag { cycle_nodes: Vec<String> },   // i passi formano un ciclo
    InvalidGraph { message: String },        // il piano non può essere eseguito come richiesto
}

Tutti gli errori sono descrittivi e includono i nomi dei nodi o gli URN coinvolti. Non ci sono varianti generiche di ripiego.

Sorgente: types.rs.

Altre implementazioni

Il resolver esiste nelle librerie Go, Python e JavaScript, che analizzano e risolvono la stessa notazione negli stessi binding. Il costruttore di piani ed execute_plan esistono solo nella libreria Rust: è ciò che eseguono la CLI capdag e il motore.