Skip to content

Commit ec6d7e9

Browse files
committed
feat: added new provider crate for emitters and remote runtime
1 parent 32443b9 commit ec6d7e9

8 files changed

Lines changed: 202 additions & 0 deletions

File tree

crates/taurus-provider/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "taurus-provider"
3+
version.workspace = true
4+
edition.workspace = true
5+
6+
[dependencies]
7+
code0-flow = { workspace = true, features = ["flow_service"] }
8+
tucana = { workspace = true }
9+
tokio = { workspace = true }
10+
log = { workspace = true }
11+
futures-lite = { workspace = true }
12+
rand = { workspace = true }
13+
base64 = { workspace = true }
14+
env_logger = { workspace = true }
15+
async-nats = { workspace = true }
16+
prost = { workspace = true }
17+
tonic-health = { workspace = true }
18+
tonic = { workspace = true }
19+
taurus-core = { workspace = true }

crates/taurus-provider/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod providers;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod nats_emitter;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use async_nats::Client;
2+
use prost::Message;
3+
use std::collections::HashMap;
4+
use taurus_core::runtime::engine::{EmitType, ExecutionId, RespondEmitter};
5+
use tokio::sync::mpsc;
6+
use tucana::shared::value::Kind::{StringValue, StructValue};
7+
use tucana::shared::{Struct, Value};
8+
9+
const DEFAULT_TOPIC_PREFIX: &str = "runtime.emitter";
10+
11+
pub struct NATSRespondEmitter {
12+
tx: mpsc::UnboundedSender<NATSEmitMessage>,
13+
}
14+
15+
struct NATSEmitMessage {
16+
execution_id: ExecutionId,
17+
emit_type: EmitType,
18+
value: Value,
19+
}
20+
21+
impl NATSRespondEmitter {
22+
pub fn new(client: Client) -> Self {
23+
Self::with_topic_prefix(client, DEFAULT_TOPIC_PREFIX)
24+
}
25+
26+
pub fn with_topic_prefix(client: Client, topic_prefix: impl Into<String>) -> Self {
27+
let topic_prefix = topic_prefix.into();
28+
let (tx, mut rx) = mpsc::unbounded_channel::<NATSEmitMessage>();
29+
30+
// Keep the public emitter API synchronous while publishing asynchronously.
31+
// This worker serializes outbound lifecycle events to one NATS topic per execution:
32+
// `<topic_prefix>.<execution_id>`.
33+
// Event type is embedded in the payload so subscribers do not need four topic bindings.
34+
tokio::spawn(async move {
35+
while let Some(message) = rx.recv().await {
36+
let topic = format!("{}.{}", topic_prefix, message.execution_id);
37+
let encoded_payload = encode_emit_message(message.emit_type, message.value);
38+
39+
if let Err(err) = client
40+
.publish(topic.clone(), encoded_payload.encode_to_vec().into())
41+
.await
42+
{
43+
log::error!(
44+
"Failed to publish runtime emit message on '{}': {:?}",
45+
topic,
46+
err
47+
);
48+
}
49+
}
50+
});
51+
52+
Self { tx }
53+
}
54+
}
55+
56+
impl RespondEmitter for NATSRespondEmitter {
57+
fn emit(&self, execution_id: ExecutionId, emit_type: EmitType, value: Value) {
58+
if let Err(err) = self.tx.send(NATSEmitMessage {
59+
execution_id,
60+
emit_type,
61+
value,
62+
}) {
63+
log::debug!(
64+
"Dropped runtime emit message because NATS emitter worker is unavailable: {:?}",
65+
err
66+
);
67+
}
68+
}
69+
}
70+
71+
fn encode_emit_message(emit_type: EmitType, payload: Value) -> Value {
72+
let emit_type_value = Value {
73+
kind: Some(StringValue(emit_type_as_str(emit_type).to_string())),
74+
};
75+
76+
Value {
77+
kind: Some(StructValue(Struct {
78+
fields: HashMap::from([
79+
("emit_type".to_string(), emit_type_value),
80+
("payload".to_string(), payload),
81+
]),
82+
})),
83+
}
84+
}
85+
86+
fn emit_type_as_str(emit_type: EmitType) -> &'static str {
87+
match emit_type {
88+
EmitType::StartingExec => "starting",
89+
EmitType::OngoingExec => "ongoing",
90+
EmitType::FinishedExec => "finished",
91+
EmitType::FailedExec => "failed",
92+
}
93+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// Provider for Remote Runtimes
2+
pub mod remote;
3+
4+
/// Provider for Remote Emitters
5+
pub mod emitter;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod nats_remote_runtime;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use async_nats::Client;
2+
use prost::Message;
3+
use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime};
4+
use taurus_core::types::errors::runtime_error::RuntimeError;
5+
use tonic::async_trait;
6+
use tucana::aquila::ExecutionResult;
7+
use tucana::shared::Value;
8+
9+
pub struct NATSRemoteRuntime {
10+
client: Client,
11+
}
12+
13+
impl NATSRemoteRuntime {
14+
pub fn new(client: Client) -> Self {
15+
NATSRemoteRuntime { client }
16+
}
17+
}
18+
19+
#[async_trait]
20+
impl RemoteRuntime for NATSRemoteRuntime {
21+
async fn execute_remote(&self, execution: RemoteExecution) -> Result<Value, RuntimeError> {
22+
let topic = format!(
23+
"action.{}.{}",
24+
execution.target_service, execution.request.execution_identifier
25+
);
26+
let payload = execution.request.encode_to_vec();
27+
28+
log::info!("Request Remote Runtime Execution with topic: : {}", topic);
29+
let res = self.client.request(topic, payload.into()).await;
30+
let message = match res {
31+
Ok(r) => r,
32+
Err(err) => {
33+
log::error!(
34+
"RemoteRuntimeExeption: failed to handle NATS message: {}",
35+
err
36+
);
37+
return Err(RuntimeError::new(
38+
"T-PROV-000001",
39+
"RemoteRuntimeExeption",
40+
"Failed to receive any response messages from a remote runtime.",
41+
));
42+
}
43+
};
44+
45+
let decode_result = ExecutionResult::decode(message.payload);
46+
let execution_result = match decode_result {
47+
Ok(r) => r,
48+
Err(err) => {
49+
log::error!(
50+
"RemoteRuntimeExeption: failed to decode NATS message: {}",
51+
err
52+
);
53+
return Err(RuntimeError::new(
54+
"T-PROV-000002",
55+
"RemoteRuntimeExeption",
56+
"Failed to read Remote Response",
57+
));
58+
}
59+
};
60+
61+
match execution_result.result {
62+
Some(result) => match result {
63+
tucana::aquila::execution_result::Result::Success(value) => Ok(value),
64+
tucana::aquila::execution_result::Result::Error(err) => {
65+
let code = err.code.to_string();
66+
let description = match err.description {
67+
Some(string) => string,
68+
None => "Unknown Error".to_string(),
69+
};
70+
let error = RuntimeError::new(code, "RemoteExecutionError", description);
71+
Err(error)
72+
}
73+
},
74+
None => Err(RuntimeError::new(
75+
"T-PROV-000003",
76+
"RemoteRuntimeExeption",
77+
"Result of Remote Response was empty.",
78+
)),
79+
}
80+
}
81+
}

crates/taurus/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ prost = { workspace = true }
1717
tonic-health = { workspace = true }
1818
tonic = { workspace = true }
1919
taurus-core = { workspace = true }
20+
taurus-provider = { workspace = true }

0 commit comments

Comments
 (0)