use serde_json::Value;
use crate::connector::ConnectorConfig;
use crate::errors::OrionError;
use crate::model::admission::{
AdmissionDeps, AdmissionJob, AdmissionOutcome, AdmissionState, admission_json, admit,
run_worker,
};
use crate::model::{ArtifactRef, Manifest};
use crate::runtime::{Criticality, TaskRegistry};
use crate::server::state::AppState;
use crate::storage::models::Model;
pub const TASK_NAME: &str = "model_admission";
pub fn job_for(row: &Model) -> Result<AdmissionJob, OrionError> {
let artifact: ArtifactRef =
serde_json::from_str(&row.artifact_json).map_err(|e| OrionError::Internal {
context: format!(
"model '{}' version {}: artifact_json does not parse: {e}",
row.model_id, row.version
),
source: None,
})?;
let manifest: Manifest =
serde_json::from_str(&row.manifest_json).map_err(|e| OrionError::Internal {
context: format!(
"model '{}' version {}: manifest_json does not parse: {e}",
row.model_id, row.version
),
source: None,
})?;
Ok(AdmissionJob {
model_id: row.model_id.clone(),
version: row.version,
artifact,
signature: row.signature.clone(),
manifest,
})
}
pub async fn admit_now(
state: &AppState,
job: AdmissionJob,
) -> Result<AdmissionOutcome, OrionError> {
let Some(models) = &state.models else {
return Err(OrionError::validation(
"models are disabled on this node (models.enabled = false), so nothing can be \
admitted here",
));
};
let storage = match state.connector_registry.get(&job.artifact.connector).await {
Some(config) => match config.as_ref() {
ConnectorConfig::Storage(storage) => Some(storage.clone()),
other => {
return record(
state,
&models.node,
gate_failure(
&job,
format!(
"connector '{}' is a {} connector, and a model artifact is read \
through a storage connector",
job.artifact.connector,
other.connector_type().as_str()
),
),
)
.await;
}
},
None => None,
};
let Some(storage) = storage else {
return record(
state,
&models.node,
gate_failure(
&job,
format!(
"connector '{}' is not loaded on this node — it does not exist, is \
disabled, or failed to load (see /health)",
job.artifact.connector
),
),
)
.await;
};
let deps = AdmissionDeps {
store: &models.store,
storage: &storage,
client: &state.http_client,
config: &state.config.models,
node: &models.node,
runtimes: &models.runtimes,
};
let outcome = admit(&deps, &job).await;
record(state, &models.node, outcome).await
}
fn gate_failure(job: &AdmissionJob, reason: String) -> AdmissionOutcome {
crate::metrics::record_model_admission("failed", Some("gate"), 0.0);
AdmissionOutcome {
model_id: job.model_id.clone(),
version: job.version,
state: AdmissionState::Failed {
stage: "gate",
reason,
},
artifact_path: None,
elapsed: std::time::Duration::ZERO,
}
}
async fn record(
state: &AppState,
node: &str,
outcome: AdmissionOutcome,
) -> Result<AdmissionOutcome, OrionError> {
let verdict = admission_json(&outcome, node, chrono::Utc::now());
state
.repos
.models
.set_admission(&outcome.model_id, outcome.version, &verdict.to_string())
.await?;
let stats: Option<Value> = match &outcome.state {
AdmissionState::Passed { stats } => Some(serde_json::to_value(stats)?),
AdmissionState::Failed { .. } => None,
};
state
.repos
.models
.set_stats(
&outcome.model_id,
outcome.version,
stats.map(|s| s.to_string()).as_deref(),
)
.await?;
match &outcome.state {
AdmissionState::Passed { .. } => tracing::info!(
model = %outcome.model_id,
version = outcome.version,
elapsed_ms = outcome.elapsed.as_millis() as u64,
"Model admitted"
),
AdmissionState::Failed { stage, reason } => tracing::warn!(
model = %outcome.model_id,
version = outcome.version,
stage,
reason,
"Model admission failed"
),
}
Ok(outcome)
}
pub fn start(tasks: &TaskRegistry, state: AppState) {
let Some(models) = state.models.clone() else {
return;
};
tasks.supervise(TASK_NAME, Criticality::Required, move |mut shutdown| {
let state = state.clone();
let models = models.clone();
async move {
let Some(receiver) = models.take_receiver() else {
tracing::error!(
task = TASK_NAME,
"the admission queue's receiver is gone; the worker cannot restart"
);
return;
};
let worker = run_worker(receiver, move |job| {
let state = state.clone();
async move {
let model = job.model_id.clone();
let version = job.version;
let run = tokio::spawn(async move {
if let Err(e) = admit_now(&state, job).await {
tracing::error!(
model = %model,
version,
error = %e,
"Model admission could not be recorded"
);
}
});
if let Err(e) = run.await {
tracing::error!(error = %e, "Model admission task died");
crate::metrics::record_error("model_admission");
}
}
});
tokio::select! {
_ = worker => {}
_ = shutdown.signalled() => {}
}
}
});
}