docs(console): clarify API and persistence boundaries

This commit is contained in:
Maofeng
2026-07-30 08:13:33 +08:00
parent aea42ba0a6
commit 223ead37c8
7 changed files with 265 additions and 4 deletions
+36 -1
View File
@@ -1,4 +1,14 @@
//! Axum management and public gateway protocol adapters.
//!
//! The management router accepts JSON and multipart packages and exposes
//! lifecycle operations. The gateway router is intentionally smaller: it
//! accepts raw `application/octet-stream` input and returns raw bytes plus
//! `x-wasmeld-revision` and `x-wasmeld-latency-ms` response headers.
//!
//! Neither router installs authentication or authorization middleware. A
//! production deployment must place the appropriate identity, authorization,
//! TLS, rate-limit, and request-size policy in front of these routers. CORS is
//! a browser access policy and must not be treated as authentication.
use super::*;
use axum::{
@@ -21,6 +31,11 @@ use wasmeld_package::wit_package::WitPackageError;
///
/// The returned router exposes Runtime control, component registration,
/// invocation, events, and WIT Registry endpoints.
///
/// `allowed_origins` only affects browser CORS responses. Passing an empty
/// vector does not make the API private; it merely omits allowed origins.
/// Multipart bodies receive one MiB of framing headroom above the larger
/// configured artifact limit.
pub fn app(console: Arc<Console>, allowed_origins: Vec<HeaderValue>) -> Router {
let max_body_bytes = console
.max_artifact_bytes()
@@ -89,7 +104,18 @@ pub fn app(console: Arc<Console>, allowed_origins: Vec<HeaderValue>) -> Router {
/// Builds the public data-plane API around the same resident Runtime.
///
/// This router intentionally contains no registration, lifecycle, Registry,
/// or deployment management routes.
/// or deployment management routes. Callers must send
/// `Content-Type: application/octet-stream`; the stable service ID is resolved
/// to its active immutable revision at the start of each request.
///
/// # Wire example
///
/// ```text
/// POST /v1/services/image-resize/invoke
/// Content-Type: application/octet-stream
///
/// <component-specific bytes>
/// ```
pub fn gateway_app(console: Arc<Console>) -> Router {
let max_input_bytes = console.max_gateway_input_bytes();
Router::new()
@@ -126,6 +152,9 @@ async fn gateway_invoke(
let input = body.map_err(|_| GatewayError::PayloadTooLarge {
limit: console.max_gateway_input_bytes(),
})?;
// Deployment resolution and Actor enqueue happen in the same blocking
// closure. The returned revision header therefore identifies the exact
// revision that received this request, even if deployment changes later.
let invocation = run_blocking(console, move |console| {
console.gateway_invoke(&service_id, input.to_vec())
})
@@ -262,6 +291,9 @@ async fn publish_wit_package(
State(console): State<Arc<Console>>,
mut multipart: Multipart,
) -> Result<(StatusCode, Json<WitPackageMetadata>), ApiError> {
// Axum materializes this one field in memory. DefaultBodyLimit above is
// only a transport guard; WitRegistry repeats the exact artifact limit and
// validates the package identity before committing it to disk.
let mut package = None;
while let Some(field) = multipart
.next_field()
@@ -453,6 +485,9 @@ where
// an operation may have changed state before returning its error.
let operation_console = Arc::clone(&console);
let result = run_blocking(operation_console, operation).await;
// The flush marker orders previously accepted metric deltas before the
// full snapshot transaction. It means "attempted", not "durably written";
// the telemetry worker logs write failures independently.
console.flush_invocation_telemetry().await;
let persisted = console.persist_snapshot().await;
@@ -1,4 +1,10 @@
//! Bounded, asynchronous persistence for data-plane invocation telemetry.
//!
//! Invocation responses must not wait for libSQL, so producers use
//! [`tokio::sync::mpsc::Sender::try_send`] and one task writes ordered batches.
//! When the queue is full or closed, the update is deliberately dropped and a
//! warning is emitted. These counters and events are operational telemetry;
//! they are not suitable as a billing ledger or audit log.
use std::sync::Arc;
@@ -10,6 +16,10 @@ const QUEUE_CAPACITY: usize = 1024;
const MAX_BATCH_SIZE: usize = 64;
/// Non-blocking producer used by Wasmtime invocation threads.
///
/// All producers share one FIFO queue. Queue order is preserved for accepted
/// records, but a process crash can lose records that have not yet reached
/// libSQL.
pub(crate) struct InvocationPersistence {
sender: mpsc::Sender<Command>,
}
@@ -21,6 +31,10 @@ enum Command {
impl InvocationPersistence {
/// Starts the single ordered writer on the current Tokio runtime.
///
/// `sync` is shared with full Console snapshot persistence. Taking it
/// around each batch prevents a snapshot transaction from racing a metric
/// delta transaction.
pub(crate) fn start(persistence: Persistence, sync: Arc<Mutex<()>>) -> Self {
let (sender, mut receiver) = mpsc::channel(QUEUE_CAPACITY);
tokio::spawn(async move {
@@ -45,6 +59,10 @@ impl InvocationPersistence {
tracing::error!(%error, "failed to persist invocation telemetry");
}
}
// Flush is an ordering barrier, not a durability receipt. A
// database error above is logged rather than returned, and a
// flush may wait for records dequeued after the marker when
// those records fit in the same batch.
for flush in flushes {
let _ = flush.send(());
}
@@ -54,6 +72,9 @@ impl InvocationPersistence {
}
/// Enqueues telemetry without extending API response latency.
///
/// Saturation drops this update instead of applying backpressure to the
/// Component invocation path.
pub(crate) fn record(&self, update: InvocationUpdate) {
if let Err(error) = self.sender.try_send(Command::Record(update)) {
tracing::warn!(%error, "invocation telemetry queue is unavailable; update dropped");
@@ -61,6 +82,10 @@ impl InvocationPersistence {
}
/// Waits until all telemetry queued before this call has been attempted.
///
/// This method is used before a control-plane snapshot so the snapshot
/// reads the newest in-memory counters after older delta writes. It does
/// not report persistence failures; those are visible through tracing.
pub(crate) async fn flush(&self) {
let (sender, receiver) = oneshot::channel();
if self.sender.send(Command::Flush(sender)).await.is_ok() {
+32
View File
@@ -1,4 +1,18 @@
//! Bounded bridge between synchronous Component Host calls and async Toasty.
//!
//! Wasmtime calls [`wasmeld_runtime::KvBackend`] synchronously on an Actor
//! thread, while Toasty and libSQL are asynchronous. This adapter moves every
//! database operation onto one dedicated current-thread Tokio runtime. The
//! bounded channel prevents a slow database from accumulating unbounded work
//! or blocking every Actor while waiting for queue capacity.
//!
//! # Timeout semantics
//!
//! A timeout only stops the caller from waiting. It does not remove a command
//! that is already queued or cancel a libSQL operation that has started.
//! Consequently, `set` and `delete` may commit after the Component observes
//! `KvBackendError::Timeout`. Components should make retries idempotent and
//! must not interpret a timeout as proof that no mutation occurred.
use std::{
fmt, io,
@@ -15,6 +29,9 @@ use wasmeld_runtime::{KvBackend, KvBackendError};
use crate::persistence::Persistence;
const COMMAND_CAPACITY: usize = 64;
// Keep Host calls responsive even when a Component requests a larger timeout.
// The Component-facing deadline is an upper bound, not permission to occupy an
// Actor thread indefinitely.
const MAX_RESPONSE_TIMEOUT: Duration = Duration::from_millis(400);
pub(crate) struct ToastyKvBackend {
@@ -47,6 +64,11 @@ enum KvCommand {
}
impl ToastyKvBackend {
/// Starts the database worker and waits until its Tokio runtime is ready.
///
/// Waiting for the ready signal makes construction fail deterministically
/// instead of returning a backend whose first operation discovers that
/// the worker could not initialize.
pub(crate) fn start(persistence: Persistence) -> io::Result<Self> {
let (sender, receiver) = mpsc::sync_channel(COMMAND_CAPACITY);
let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
@@ -78,6 +100,8 @@ impl ToastyKvBackend {
build: impl FnOnce(SyncSender<Result<T, KvBackendError>>) -> KvCommand,
) -> Result<T, KvBackendError> {
let (response, receiver) = mpsc::sync_channel(1);
// `try_send` is intentional: waiting for queue capacity here would
// stall the Actor and defeat the Runtime's invocation deadline.
match self.inner.sender.try_send(build(response)) {
Ok(()) => {}
Err(TrySendError::Full(_)) => return Err(KvBackendError::Busy),
@@ -149,6 +173,9 @@ impl KvBackend for ToastyKvBackend {
impl Drop for BackendInner {
fn drop(&mut self) {
// Only the final Arc owner reaches this Drop. Joining guarantees the
// worker no longer holds the Persistence handle when Console shuts
// down. If commands precede Shutdown, the FIFO channel drains them.
let _ = self.sender.send(KvCommand::Shutdown);
if let Ok(worker) = self.worker.get_mut()
&& let Some(worker) = worker.take()
@@ -163,6 +190,8 @@ fn run_worker(
receiver: Receiver<KvCommand>,
ready: SyncSender<io::Result<()>>,
) {
// Toasty is driven from one thread so synchronous Host calls never need to
// enter or block the Console server's multi-thread Tokio runtime.
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@@ -175,6 +204,9 @@ fn run_worker(
};
let _ = ready.send(Ok(()));
// A single consumer also defines the ordering of mutations submitted to
// this backend. Ordering across different Wasmeld processes is delegated
// to libSQL and is not guaranteed by this queue.
while let Ok(command) = receiver.recv() {
match command {
KvCommand::Get {
+102 -2
View File
@@ -5,6 +5,32 @@
//! to libSQL through Toasty. It also hosts an immutable filesystem-backed WIT
//! package Registry. Actor memory remains process-local and is never presented
//! as durable state.
//!
//! # Ownership model
//!
//! * [`Console`] owns the in-process Runtime and serializes control-plane
//! mutations with a lifecycle mutex.
//! * libSQL stores desired service/deployment state and bounded telemetry.
//! * Component and WIT bytes are immutable filesystem artifacts addressed by
//! identity and digest.
//! * On restart, Actors are recreated from persisted manifests; their linear
//! memory is intentionally reset.
//!
//! # Example
//!
//! ```no_run
//! use wasmeld_console::{Console, ConsoleConfig};
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let console = Console::new(ConsoleConfig::default()).await?;
//! for service in console.services()? {
//! println!("{}@{}: {:?}", service.id, service.revision, service.status);
//! }
//! # Ok(())
//! # }
//! ```
#![warn(missing_docs)]
mod api;
mod invocation_persistence;
@@ -82,6 +108,10 @@ impl Default for ConsoleConfig {
/// registered service metadata, artifacts, metrics, and events remain managed
/// by the Console.
pub struct Console {
// Lock order for lifecycle mutations is `lifecycle` -> `runtime` ->
// `state`. Invocation does not take `lifecycle`; it briefly reads Runtime
// and then updates state after the Actor call. Keep new code on this order
// to avoid deadlocks between HTTP control operations.
runtime: RwLock<Option<Runtime>>,
runtime_config: RuntimeConfig,
artifact_dir: PathBuf,
@@ -124,26 +154,37 @@ struct DeploymentRecord {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ServiceStatus {
/// Actor is accepting invocations for this revision.
Running,
/// Revision is registered but has no active Actor.
Stopped,
/// Its most recent Actor trapped and must be started again.
Faulted,
}
/// API projection of one public service deployment.
#[derive(Clone, Debug, Serialize)]
pub struct DeploymentView {
/// Stable public service ID used by the Gateway route.
pub service_id: String,
/// Immutable revision currently selected for the service.
pub active_revision: String,
/// Current lifecycle status of the selected revision.
pub status: ServiceStatus,
/// Unix timestamp of the most recent deployment change.
pub updated_at_ms: u64,
}
/// Exact versioned host interface imported by a registered Component.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CapabilityView {
/// Fully qualified versioned WIT interface.
pub interface: String,
/// WIT package identity without the version suffix.
pub package: String,
/// Interface name inside the package.
pub name: String,
/// Exact semantic version linked by the Runtime.
pub version: String,
}
@@ -161,16 +202,27 @@ impl From<CapabilityDescriptor> for CapabilityView {
/// API projection of a registered service and its current metrics.
#[derive(Clone, Debug, Serialize)]
pub struct ServiceView {
/// Stable service ID.
pub id: String,
/// Immutable Component revision.
pub revision: String,
/// Canonical path of the stored Component artifact.
pub artifact: String,
/// Fully versioned world declared by the package.
pub world: String,
/// Exact Host interfaces discovered from Component imports.
pub capabilities: Vec<CapabilityView>,
/// Current Actor status for this revision.
pub status: ServiceStatus,
/// Unix timestamp of the most recent lifecycle change.
pub updated_at_ms: u64,
/// Platform limits injected during registration.
pub limits: ResourceLimits,
/// Persisted number of completed invocation attempts.
pub calls: u64,
/// Persisted number of failed invocation attempts.
pub errors: u64,
/// Latency of the most recently persisted invocation.
pub last_latency_ms: Option<u64>,
}
@@ -195,11 +247,17 @@ impl ServiceRecord {
/// One persisted Console lifecycle or invocation event.
#[derive(Clone, Debug, Serialize)]
pub struct EventView {
/// Process-independent monotonically increasing event identity.
pub id: u64,
/// Event creation time as Unix milliseconds.
pub timestamp_ms: u64,
/// Stable category used by API consumers.
pub kind: EventKind,
/// Related service ID, or `None` for Runtime-wide events.
pub service_id: Option<String>,
/// Related immutable revision when the event targets one version.
pub revision: Option<String>,
/// Human-readable operational detail; not a machine protocol.
pub message: String,
}
@@ -207,12 +265,19 @@ pub struct EventView {
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
/// A Component package was registered.
Registered,
/// An inactive revision and its artifacts were removed.
Unregistered,
/// A public deployment switched to a revision.
Deployed,
/// Runtime or Actor startup completed.
Started,
/// Runtime or Actor shutdown completed.
Stopped,
/// One data-plane or management invocation completed.
Invoked,
/// A lifecycle or invocation operation failed.
Failed,
}
@@ -348,55 +413,76 @@ struct GatewayInvocation {
/// Errors raised by Console state, persistence, Registry, or Runtime operations.
#[derive(Debug, Error)]
pub enum ConsoleError {
/// API input failed semantic validation.
#[error("{0}")]
InvalidRequest(String),
/// The requested registered service revision does not exist.
#[error("service revision {0} is not managed by Wasmeld")]
ServiceNotFound(ServiceKey),
/// The public service has no selected revision.
#[error("service {0} has no active deployment")]
DeploymentNotFound(String),
/// Unregister was attempted on the revision currently serving traffic.
#[error("service revision {0} is the active deployment")]
ActiveDeployment(ServiceKey),
/// An operation requiring Wasmtime was attempted while Runtime is stopped.
#[error("Wasmeld Runtime is not running")]
RuntimeNotRunning,
/// Uploaded package bytes exceed the configured control-plane limit.
#[error("artifact exceeds the {limit}-byte limit")]
ArtifactTooLarge { limit: usize },
ArtifactTooLarge {
/// Maximum accepted upload bytes.
limit: usize,
},
/// Component package decoding or integrity validation failed.
#[error(transparent)]
Package(#[from] PackageError),
/// WIT Registry storage or integrity validation failed.
#[error(transparent)]
WitRegistry(#[from] WitRegistryError),
/// Component artifact or manifest filesystem access failed.
#[error("failed to access {path}: {source}")]
Storage {
/// Path involved in the failed operation.
path: PathBuf,
/// Underlying filesystem failure.
#[source]
source: io::Error,
},
/// A generated Runtime manifest could not be encoded.
#[error("failed to serialize manifest: {0}")]
ManifestSerialize(#[from] toml::ser::Error),
/// Toasty or libSQL control-state persistence failed.
#[error("database operation failed: {0}")]
Database(#[from] toasty::Error),
/// The synchronous-to-async KV worker thread could not start.
#[error("failed to start persistent KV backend: {0}")]
KvBackendStart(#[source] io::Error),
/// Persisted rows contain an unknown enum, invalid key, or invalid revision.
#[error("database contains invalid console data: {0}")]
InvalidDatabaseData(String),
/// Component compilation, Actor lifecycle, or invocation failed.
#[error(transparent)]
Runtime(#[from] RuntimeError),
/// A blocking Runtime operation panicked or was cancelled.
#[error("blocking runtime task failed: {0}")]
Join(#[from] tokio::task::JoinError),
/// An internal lifecycle, state, or Runtime lock was poisoned.
#[error("console state lock was poisoned")]
LockPoisoned,
}
@@ -793,6 +879,11 @@ impl Console {
};
manifest.validate()?;
// Artifacts are committed before Runtime registration so
// `register_from_file` can recover the same bytes after restart.
// Every later fallible step removes files it created until the Runtime
// owns the registration. The HTTP wrapper persists the state snapshot
// after this method returns.
write_atomic(&component_path, &component_bytes)?;
let manifest_toml = toml::to_string_pretty(&manifest)?;
if let Err(error) = write_atomic(&manifest_path, manifest_toml.as_bytes()) {
@@ -950,6 +1041,10 @@ impl Console {
let result = runtime.invoke(key, input);
let latency_ms = duration_ms(started_at.elapsed());
// Update counters synchronously so management reads immediately
// observe this call. Durable telemetry is queued below and is
// intentionally best-effort: queue saturation or a process crash can
// lose recent counters/events. Do not use these values for billing.
let mut state = self.state()?;
let record = state
.services
@@ -995,6 +1090,9 @@ impl Console {
service_id: &str,
input: Vec<u8>,
) -> Result<GatewayInvocation, ConsoleError> {
// Clone the selected revision while holding state, then release the
// lock before entering Runtime. An activation racing this call affects
// only subsequent resolutions; this call remains pinned to `revision`.
let revision = {
let state = self.state()?;
state
@@ -1139,7 +1237,9 @@ impl Console {
async fn persist_snapshot(&self) -> Result<(), ConsoleError> {
// Serialize snapshots so a slower earlier write cannot overwrite a
// newer state captured by a concurrent HTTP operation.
// newer state captured by a concurrent HTTP operation. This mutex is
// also shared with the invocation delta writer, which prevents the two
// transaction styles from interleaving.
let _sync = self.persistence_sync.lock().await;
let (services, events, deployments) = self.persistence_snapshot()?;
self.persistence.sync(services, events, deployments).await?;
+26
View File
@@ -1,4 +1,25 @@
//! Standalone Wasmeld control-plane and data-plane server entry point.
//!
//! Configuration is supplied through environment variables:
//!
//! - `WASMELD_ADDR` binds the management API (default `127.0.0.1:8080`).
//! - `WASMELD_GATEWAY_ADDR` binds the public byte API (default `0.0.0.0:8081`).
//! - `WASMELD_ARTIFACT_DIR`, `WASMELD_WIT_REGISTRY_DIR`, and
//! `WASMELD_DATABASE_PATH` select durable local storage.
//! - `WASMELD_ALLOWED_ORIGINS` is a comma-separated management UI CORS list.
//! - `RUST_LOG` configures tracing.
//!
//! For example:
//!
//! ```text
//! WASMELD_ADDR=127.0.0.1:8080 \
//! WASMELD_GATEWAY_ADDR=0.0.0.0:8081 \
//! RUST_LOG=wasmeld_console=debug \
//! cargo run -p wasmeld-console
//! ```
//!
//! The defaults expose only the gateway on non-loopback interfaces.
//! Authentication and TLS must be supplied by the deployment environment.
use std::{env, net::SocketAddr, path::PathBuf, sync::Arc};
@@ -45,6 +66,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gateway = gateway_app(Arc::clone(&console));
let management_listener = tokio::net::TcpListener::bind(management_address).await?;
let gateway_listener = tokio::net::TcpListener::bind(gateway_address).await?;
// Both servers share one shutdown edge so neither listener remains alive
// after Ctrl+C has begun process termination.
let (shutdown_sender, shutdown_receiver) = tokio::sync::watch::channel(false);
let signal_task = tokio::spawn(async move {
shutdown_signal().await;
@@ -61,6 +84,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
);
signal_task.abort();
result?;
// Axum has stopped accepting requests at this point. Flush accepted
// telemetry before dropping Console; persistence failures are traced by
// the worker because telemetry is an operational, best-effort stream.
console.flush_invocation_telemetry().await;
Ok(())
}
+31
View File
@@ -3,6 +3,15 @@
//! Component bytes and WIT packages remain filesystem artifacts. This module
//! persists service manifests, deployments, metrics, the bounded event
//! history, and service-scoped Host KV entries.
//!
//! There are two write paths:
//!
//! - [`Persistence::sync`] stores a complete control-plane snapshot.
//! - [`Persistence::record_invocations`] applies ordered metric deltas.
//!
//! Callers serialize these paths with the Console persistence mutex. Existing
//! service counters are intentionally owned by the delta path; a snapshot
//! updates their manifest and timestamp without replacing those counters.
use std::{
collections::BTreeSet,
@@ -55,6 +64,10 @@ pub(crate) struct StoredKvEntry {
}
/// One invocation's durable metric delta and event.
///
/// The caller assigns the event ID before enqueueing the update. A missing
/// service row is tolerated because unregister may race queued telemetry; the
/// event is still retained for diagnostics.
pub(crate) struct InvocationUpdate {
pub service_key: String,
pub failed: bool,
@@ -64,6 +77,10 @@ pub(crate) struct InvocationUpdate {
}
/// Serialized access to the Toasty database connection.
///
/// This mutex protects a single Toasty [`Db`] handle. It is separate from the
/// higher-level Console persistence mutex, which coordinates whole snapshot
/// and invocation-delta operations before they acquire this connection.
#[derive(Clone)]
pub(crate) struct Persistence {
db: Arc<Mutex<Db>>,
@@ -131,6 +148,11 @@ impl Persistence {
}
/// Atomically upserts a Console snapshot and prunes expired events.
///
/// The input is authoritative for services and deployments: rows omitted
/// from those collections are removed. Events older than the oldest
/// supplied retained event are pruned. Existing invocation counters are
/// preserved because [`Self::record_invocations`] owns counter changes.
pub async fn sync(
&self,
services: Vec<StoredService>,
@@ -178,6 +200,8 @@ impl Persistence {
}
}
// Console stores events newest-first, so the last element is the
// oldest ID that should survive snapshot pruning.
if let Some(oldest_event) = events.last() {
StoredEvent::filter(StoredEvent::fields().id().lt(oldest_event.id))
.delete()
@@ -217,6 +241,9 @@ impl Persistence {
}
/// Atomically records invocation deltas and their bounded event history.
///
/// All supplied updates commit together. Metrics use saturating arithmetic
/// so corrupted or extremely old databases cannot wrap counters.
pub(crate) async fn record_invocations(
&self,
updates: Vec<InvocationUpdate>,
@@ -268,6 +295,8 @@ impl Persistence {
service_id: &str,
entry_key: &str,
) -> toasty::Result<Option<Vec<u8>>> {
// The namespace is the stable service ID rather than its revision.
// Deploying a new revision therefore preserves application state.
let mut db = self.db.lock().await;
Ok(
StoredKvEntry::filter_by_service_id_and_entry_key(service_id, entry_key)
@@ -284,6 +313,8 @@ impl Persistence {
entry_key: String,
value: Vec<u8>,
) -> toasty::Result<()> {
// Upsert makes a retry after an ambiguous Host timeout idempotent for
// the same `(service_id, entry_key, value)`.
let mut db = self.db.lock().await;
StoredKvEntry::upsert_by_service_id_and_entry_key(service_id, entry_key)
.value(value)
+13 -1
View File
@@ -37,28 +37,40 @@ struct StoredWitPackage {
/// Errors produced by WIT Registry storage and integrity checks.
#[derive(Debug, Error)]
pub enum WitRegistryError {
/// Uploaded binary WIT exceeds the configured byte limit.
#[error("WIT package exceeds the {limit}-byte limit")]
TooLarge { limit: usize },
TooLarge {
/// Maximum accepted artifact bytes.
limit: usize,
},
/// The immutable `name@version` already exists.
#[error("WIT package {0} is already published and cannot be overwritten")]
AlreadyExists(String),
/// No indexed artifact matches the requested identity.
#[error("WIT package {0} was not found")]
NotFound(String),
/// Uploaded or persisted bytes are not a valid versioned binary WIT package.
#[error(transparent)]
InvalidPackage(#[from] WitPackageError),
/// Directory layout and decoded artifact identity disagree.
#[error("invalid WIT registry data: {0}")]
InvalidStorage(String),
/// Registry directory or artifact filesystem access failed.
#[error("failed to access {path}: {source}")]
Storage {
/// Path involved in the failed operation.
path: PathBuf,
/// Underlying filesystem failure.
#[source]
source: io::Error,
},
/// The process-local Registry index lock was poisoned.
#[error("WIT registry lock was poisoned")]
LockPoisoned,
}