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.