use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
use aion_store::OutboxRow;
use async_trait::async_trait;
use liminal::protocol::WorkerRegistration as WireWorkerRegistration;
use liminal_sdk::{SchemaMetadata, SchemaValidate};
use liminal_server::ServerError as LiminalServerError;
use liminal_server::server::connection::{ConnectionNotifier, ConnectionSupervisor};
use serde::{Deserialize, Serialize};
use super::bridge::OutboxDeliveryCallback;
use super::outbox_dispatcher::OutboxRowDispatch;
use super::registry::{ConnectedWorkerRegistry, WorkerDelivery, WorkerRegistration};
use crate::error::ServerError;
const PUSH_REPLY_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct DispatchRequest {
pub activity_type: String,
pub workflow_id: WorkflowId,
pub ordinal: u64,
pub run_id: Option<RunId>,
pub input: Vec<u8>,
}
impl SchemaValidate for DispatchRequest {
fn schema_metadata() -> SchemaMetadata {
SchemaMetadata::new(
"aion.outbox.dispatch.request",
"1",
br#"{"type":"object"}"#.as_slice(),
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct DispatchResponse {
pub workflow_id: WorkflowId,
pub ordinal: u64,
pub run_id: Option<RunId>,
pub outcome: Result<String, String>,
}
impl SchemaValidate for DispatchResponse {
fn schema_metadata() -> SchemaMetadata {
SchemaMetadata::new(
"aion.outbox.dispatch.response",
"1",
br#"{"type":"object"}"#.as_slice(),
)
}
}
#[must_use]
pub fn request_for_row(row: &OutboxRow) -> DispatchRequest {
DispatchRequest {
activity_type: row.activity_type.clone(),
workflow_id: row.workflow_id.clone(),
ordinal: row.ordinal,
run_id: row.run_id.clone(),
input: row.input.bytes().to_vec(),
}
}
const SEGMENT_SEPARATOR: char = '.';
const SEGMENT_ESCAPE: char = '%';
fn encode_segment(segment: &str) -> String {
if !segment.contains([SEGMENT_SEPARATOR, SEGMENT_ESCAPE]) {
return segment.to_owned();
}
let mut encoded = String::with_capacity(segment.len());
for ch in segment.chars() {
match ch {
SEGMENT_ESCAPE => encoded.push_str("%25"),
SEGMENT_SEPARATOR => encoded.push_str("%2E"),
other => encoded.push(other),
}
}
encoded
}
#[must_use]
pub fn dispatch_channel_name(namespace: &str, task_queue: &str, node: Option<&str>) -> String {
let namespace = encode_segment(namespace);
let task_queue = encode_segment(task_queue);
match node {
Some(node) => {
let node = encode_segment(node);
format!("aion.dispatch.{namespace}.{task_queue}.{node}")
}
None => format!("aion.dispatch.{namespace}.{task_queue}"),
}
}
#[must_use]
pub fn channel_for_row(row: &OutboxRow) -> String {
dispatch_channel_name(&row.namespace, &row.task_queue, row.node.as_deref())
}
fn dispatch_error(channel: &str, reason: String) -> ServerError {
ServerError::WorkerDispatch {
namespace: "liminal".to_owned(),
activity_type: channel.to_owned(),
reason,
}
}
pub struct LiminalCompletionSource {
callback: Arc<dyn OutboxDeliveryCallback>,
}
impl std::fmt::Debug for LiminalCompletionSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiminalCompletionSource")
.finish_non_exhaustive()
}
}
impl LiminalCompletionSource {
#[must_use]
pub fn new(callback: Arc<dyn OutboxDeliveryCallback>) -> Self {
Self { callback }
}
pub fn deliver(&self, response: &DispatchResponse) -> Result<bool, ServerError> {
let activity_id = ActivityId::from_sequence_position(response.ordinal);
match &response.outcome {
Ok(result) => self.callback.deliver_completion(
&response.workflow_id,
&activity_id,
response.run_id.as_ref(),
result.clone(),
),
Err(reason) => self.callback.deliver_failure(
&response.workflow_id,
&activity_id,
response.run_id.as_ref(),
reason.clone(),
),
}
}
}
#[must_use]
pub fn payload_from_request(request: &DispatchRequest) -> Payload {
Payload::new(ContentType::Json, request.input.clone())
}
#[derive(Clone)]
pub struct LiminalWorkerDelivery {
supervisor: ConnectionSupervisor,
pid: u64,
}
impl std::fmt::Debug for LiminalWorkerDelivery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiminalWorkerDelivery")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl LiminalWorkerDelivery {
#[must_use]
pub const fn new(supervisor: ConnectionSupervisor, pid: u64) -> Self {
Self { supervisor, pid }
}
#[must_use]
pub const fn pid(&self) -> u64 {
self.pid
}
pub fn dispatch(&self, request: &DispatchRequest) -> Result<DispatchResponse, ServerError> {
let payload = serde_json::to_vec(request).map_err(|error| {
dispatch_error("liminal-push", format!("request serialize failed: {error}"))
})?;
let awaiter = self
.supervisor
.push_to_connection(self.pid, payload)
.map_err(|error| {
ServerError::worker_connection_lost(
"liminal-push",
format!("push to worker failed: {error}"),
)
})?;
let reply = awaiter.receive(PUSH_REPLY_TIMEOUT).map_err(|error| {
if is_connection_closed_reply_error(&error) {
ServerError::worker_connection_lost(
"liminal-push",
format!("worker connection closed before reply: {error}"),
)
} else {
dispatch_error("liminal-push", format!("worker reply failed: {error}"))
}
})?;
serde_json::from_slice(&reply).map_err(|error| {
dispatch_error(
"liminal-push",
format!("worker reply decode failed: {error}"),
)
})
}
}
fn is_connection_closed_reply_error(error: &LiminalServerError) -> bool {
matches!(error, LiminalServerError::PushReplyDisconnected { .. })
}
pub struct RegistryLiminalDispatch {
registry: ConnectedWorkerRegistry,
completion: LiminalCompletionSource,
}
impl std::fmt::Debug for RegistryLiminalDispatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegistryLiminalDispatch")
.finish_non_exhaustive()
}
}
impl RegistryLiminalDispatch {
#[must_use]
pub fn new(
registry: ConnectedWorkerRegistry,
callback: Arc<dyn OutboxDeliveryCallback>,
) -> Self {
Self {
registry,
completion: LiminalCompletionSource::new(callback),
}
}
}
#[async_trait]
impl OutboxRowDispatch for RegistryLiminalDispatch {
async fn dispatch(&self, row: &OutboxRow) -> Result<(), ServerError> {
let worker = self
.registry
.select_worker(
&row.namespace,
&row.task_queue,
&row.activity_type,
row.node.as_deref(),
)?
.ok_or_else(|| {
dispatch_error(
&channel_for_row(row),
"no liminal worker registered for the row's pool".to_owned(),
)
})?;
let delivery = match worker.delivery() {
WorkerDelivery::Liminal(delivery) => delivery.clone(),
WorkerDelivery::Grpc(_) => {
return Err(dispatch_error(
&channel_for_row(row),
"selected worker is not delivered over liminal".to_owned(),
));
}
};
let request = request_for_row(row);
let response = tokio::task::spawn_blocking(move || delivery.dispatch(&request))
.await
.map_err(|error| {
dispatch_error(
&channel_for_row(row),
format!("dispatch task join failed: {error}"),
)
})??;
self.completion.deliver(&response)?;
Ok(())
}
}
fn normalize_wire_node(node: Option<&str>) -> Option<String> {
node.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
pub struct LiminalConnectionNotifier {
registry: ConnectedWorkerRegistry,
supervisor: OnceLock<ConnectionSupervisor>,
guards: Mutex<HashMap<u64, WorkerRegistration>>,
}
impl std::fmt::Debug for LiminalConnectionNotifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiminalConnectionNotifier")
.field("supervisor_bound", &self.supervisor.get().is_some())
.finish_non_exhaustive()
}
}
impl LiminalConnectionNotifier {
#[must_use]
pub fn new(registry: ConnectedWorkerRegistry) -> Self {
Self {
registry,
supervisor: OnceLock::new(),
guards: Mutex::new(HashMap::new()),
}
}
pub fn bind_supervisor(&self, supervisor: ConnectionSupervisor) -> bool {
self.supervisor.set(supervisor).is_ok()
}
}
impl ConnectionNotifier for LiminalConnectionNotifier {
fn on_worker_registered(
&self,
pid: u64,
registration: &WireWorkerRegistration,
) -> Result<(), LiminalServerError> {
let supervisor =
self.supervisor
.get()
.ok_or_else(|| LiminalServerError::ListenerAccept {
message: format!(
"liminal worker registration for connection {pid} rejected: \
notifier supervisor handle not yet bound"
),
})?;
let delivery = WorkerDelivery::Liminal(LiminalWorkerDelivery::new(supervisor.clone(), pid));
let node = normalize_wire_node(registration.node.as_deref());
let guard = self
.registry
.register_delivery(
registration.namespaces.iter().cloned(),
registration.task_queue.clone(),
node,
registration.activity_types.iter(),
delivery,
)
.map_err(|error| LiminalServerError::ListenerAccept {
message: format!(
"liminal worker registration for connection {pid} rejected: {error}"
),
})?;
let mut guards = self.guards.lock().map_err(|_| {
LiminalServerError::ListenerAccept {
message: format!(
"liminal worker registration for connection {pid} rejected: \
notifier guard map poisoned"
),
}
})?;
guards.insert(pid, guard);
tracing::info!(
connection_pid = pid,
identity = %registration.identity,
task_queue = %registration.task_queue,
"registered liminal worker in-band"
);
Ok(())
}
fn on_worker_unregistered(&self, pid: u64) {
let removed = match self.guards.lock() {
Ok(mut guards) => guards.remove(&pid),
Err(poisoned) => poisoned.into_inner().remove(&pid),
};
if removed.is_some() {
tracing::info!(
connection_pid = pid,
"deregistered liminal worker on disconnect"
);
}
}
}
#[cfg(test)]
mod tests {
use super::{channel_for_row, dispatch_channel_name, normalize_wire_node};
use aion_core::{ContentType, Payload, WorkflowId};
use aion_store::{OutboxRow, OutboxStatus};
use chrono::Utc;
use uuid::Uuid;
#[test]
fn channel_format_is_pinned() {
assert_eq!(
dispatch_channel_name("remote", "gpu", None),
"aion.dispatch.remote.gpu"
);
assert_eq!(
dispatch_channel_name("local", "norn", None),
"aion.dispatch.local.norn"
);
}
#[test]
fn node_pinned_channel_appends_node_subsegment() {
assert_eq!(
dispatch_channel_name("remote", "gpu", Some("box-7")),
"aion.dispatch.remote.gpu.box-7"
);
}
#[test]
fn channel_derivation_is_stable() {
assert_eq!(
dispatch_channel_name("default", "default", None),
dispatch_channel_name("default", "default", None)
);
assert_eq!(
dispatch_channel_name("default", "default", Some("box-1")),
dispatch_channel_name("default", "default", Some("box-1"))
);
}
#[test]
fn distinct_pools_get_distinct_channels() {
assert_ne!(
dispatch_channel_name("remote", "gpu", None),
dispatch_channel_name("local", "norn", None)
);
}
#[test]
fn node_pin_separates_channels() {
let unpinned = dispatch_channel_name("remote", "gpu", None);
let box7 = dispatch_channel_name("remote", "gpu", Some("box-7"));
let box8 = dispatch_channel_name("remote", "gpu", Some("box-8"));
assert_ne!(
unpinned, box7,
"pinned dispatch must not reach unpinned pool"
);
assert_ne!(box7, box8, "distinct nodes must not collide");
}
#[test]
fn dotted_fields_do_not_collide_across_segments() {
assert_ne!(
dispatch_channel_name("a.b", "c", None),
dispatch_channel_name("a", "b.c", None),
"a '.' in a field must not bleed across the segment separator"
);
}
#[test]
fn node_subsegment_does_not_collide_with_dotted_fields() {
assert_ne!(
dispatch_channel_name("a", "b", Some("c")),
dispatch_channel_name("a", "b.c", None),
"a node sub-segment must not collide with a dotted task_queue"
);
assert_ne!(
dispatch_channel_name("a.b", "c", None),
dispatch_channel_name("a", "b", Some("c")),
"a dotted namespace must not collide with a node-pinned channel"
);
}
#[test]
fn reserved_char_shifts_stay_distinct() {
assert_ne!(
dispatch_channel_name("ns.", "tq", None),
dispatch_channel_name("ns", ".tq", None)
);
assert_ne!(
dispatch_channel_name("", "a.b", None),
dispatch_channel_name(".a", "b", None)
);
assert_ne!(
dispatch_channel_name("%2E", "x", None),
dispatch_channel_name(".", "x", None)
);
}
#[test]
fn encoding_is_injective_over_reserved_char_triples() {
let fields = ["a", "a.b", "a.", ".a", ".", "", "%", "%2E", "a%b", "%2."];
let nodes = [
None,
Some("a"),
Some("a.b"),
Some("."),
Some(""),
Some("%2E"),
];
let mut channels = std::collections::HashSet::new();
for ns in fields {
for tq in fields {
for node in nodes {
let channel = dispatch_channel_name(ns, tq, node);
assert!(
channels.insert(channel.clone()),
"collision on ({ns:?}, {tq:?}, {node:?}) -> {channel}"
);
}
}
}
}
fn row(namespace: &str, task_queue: &str) -> OutboxRow {
let workflow_id = WorkflowId::new(Uuid::new_v4());
OutboxRow {
dispatch_key: format!("{workflow_id}:0"),
workflow_id,
ordinal: 0,
run_id: None,
namespace: namespace.to_owned(),
task_queue: task_queue.to_owned(),
node: None,
activity_type: "charge-card".to_owned(),
input: Payload::new(ContentType::Json, Vec::new()),
status: OutboxStatus::Pending,
attempt: 0,
visible_after: Utc::now(),
claimed_at: None,
}
}
#[test]
fn channel_for_row_uses_namespace_and_task_queue_only() {
let remote_gpu = row("remote", "gpu");
let local_norn = row("local", "norn");
assert_eq!(channel_for_row(&remote_gpu), "aion.dispatch.remote.gpu");
assert_eq!(channel_for_row(&local_norn), "aion.dispatch.local.norn");
assert_ne!(channel_for_row(&remote_gpu), channel_for_row(&local_norn));
let mut other_activity = row("remote", "gpu");
other_activity.activity_type = "refund".to_owned();
assert_eq!(
channel_for_row(&remote_gpu),
channel_for_row(&other_activity),
"activity_type must not affect the channel"
);
}
#[test]
fn channel_for_row_derives_node_subchannel_when_pinned() {
let mut pinned = row("remote", "gpu");
pinned.node = Some("box-7".to_owned());
assert_eq!(channel_for_row(&pinned), "aion.dispatch.remote.gpu.box-7");
let unpinned = row("remote", "gpu");
assert_eq!(channel_for_row(&unpinned), "aion.dispatch.remote.gpu");
assert_ne!(channel_for_row(&pinned), channel_for_row(&unpinned));
}
#[test]
fn wire_node_normalizes_empty_to_none() {
assert_eq!(normalize_wire_node(None), None);
assert_eq!(normalize_wire_node(Some("")), None);
assert_eq!(normalize_wire_node(Some("box-7")), Some("box-7".to_owned()));
}
}