Skip to content

Commit e4e015c

Browse files
committed
feat: taurus will react to nats subject test_execution
1 parent 45cc1d1 commit e4e015c

1 file changed

Lines changed: 232 additions & 22 deletions

File tree

crates/taurus/src/app/worker.rs

Lines changed: 232 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use std::time::Instant;
2+
use std::time::{SystemTime, UNIX_EPOCH};
23

34
use futures_lite::StreamExt;
45
use prost::Message;
56
use taurus_core::runtime::engine::{EmitType, ExecutionEngine, ExecutionId, RespondEmitter};
7+
use taurus_core::types::errors::runtime_error::RuntimeError;
8+
use taurus_core::types::signal::Signal;
69
use taurus_provider::providers::emitter::nats_emitter::NATSRespondEmitter;
710
use taurus_provider::providers::remote::nats_remote_runtime::NATSRemoteRuntime;
811
use tokio::task::JoinHandle;
9-
use tucana::shared::{ExecutionFlow, RuntimeUsage, Value};
12+
use tucana::shared::execution_result;
13+
use tucana::shared::{ExecutionFlow, ExecutionResult, RuntimeUsage, Value};
1014

1115
use crate::client::runtime_usage::TaurusRuntimeUsageService;
1216

@@ -18,7 +22,7 @@ pub fn spawn_worker(
1822
runtime_usage_service: Option<TaurusRuntimeUsageService>,
1923
) -> JoinHandle<()> {
2024
tokio::spawn(async move {
21-
let mut subscription = match client
25+
let mut execution_subscription = match client
2226
.queue_subscribe(String::from("execution.*"), "taurus".into())
2327
.await
2428
{
@@ -32,30 +36,75 @@ pub fn spawn_worker(
3236
}
3337
};
3438

35-
while let Some(message) = subscription.next().await {
36-
process_message(
37-
message,
38-
&engine,
39-
&nats_remote,
40-
&runtime_emitter,
41-
runtime_usage_service.as_ref(),
42-
)
43-
.await;
39+
let mut test_execution_subscription = match client
40+
.queue_subscribe(String::from("test_executions.*"), "taurus".into())
41+
.await
42+
{
43+
Ok(subscription) => {
44+
log::info!("Subscribed to 'test_executions.*'");
45+
subscription
46+
}
47+
Err(err) => {
48+
log::error!("Failed to subscribe to 'test_executions.*': {:?}", err);
49+
return;
50+
}
51+
};
52+
53+
let mut execution_closed = false;
54+
let mut test_execution_closed = false;
55+
56+
while !(execution_closed && test_execution_closed) {
57+
tokio::select! {
58+
message = execution_subscription.next(), if !execution_closed => {
59+
match message {
60+
Some(message) => {
61+
process_execution_message(
62+
message,
63+
&engine,
64+
&nats_remote,
65+
&runtime_emitter,
66+
runtime_usage_service.as_ref(),
67+
).await;
68+
}
69+
None => {
70+
execution_closed = true;
71+
log::warn!("Subscription 'execution.*' ended");
72+
}
73+
}
74+
}
75+
message = test_execution_subscription.next(), if !test_execution_closed => {
76+
match message {
77+
Some(message) => {
78+
process_test_execution_message(
79+
&client,
80+
message,
81+
&engine,
82+
&nats_remote,
83+
runtime_usage_service.as_ref(),
84+
).await;
85+
}
86+
None => {
87+
test_execution_closed = true;
88+
log::warn!("Subscription 'test_executions.*' ended");
89+
}
90+
}
91+
}
92+
}
4493
}
4594

4695
log::info!("NATS worker loop ended");
4796
})
4897
}
4998

50-
async fn process_message(
99+
async fn process_execution_message(
51100
message: async_nats::Message,
52101
engine: &ExecutionEngine,
53102
nats_remote: &NATSRemoteRuntime,
54103
runtime_emitter: &NATSRespondEmitter,
55104
runtime_usage_service: Option<&TaurusRuntimeUsageService>,
56105
) {
57-
let requested_execution_id =
58-
parse_execution_id_from_subject(&message.subject).unwrap_or_else(|| {
106+
let requested_execution_id = parse_execution_id_from_subject(&message.subject, "execution")
107+
.unwrap_or_else(|| {
59108
let generated = ExecutionId::new_v4();
60109
log::warn!(
61110
"Expected subject format 'execution.<uuid>', got '{}'; generated execution id {}",
@@ -83,7 +132,7 @@ async fn process_message(
83132
let respond_emitter = |execution_id, emit_type: EmitType, value: Value| {
84133
runtime_emitter.emit(execution_id, emit_type, value);
85134
};
86-
let runtime_usage = execute_flow(
135+
let run_result = execute_flow(
87136
requested_execution_id,
88137
flow,
89138
engine,
@@ -96,39 +145,200 @@ async fn process_message(
96145
);
97146

98147
if let Some(usage_service) = runtime_usage_service {
99-
usage_service.update_runtime_usage(runtime_usage).await;
148+
usage_service
149+
.update_runtime_usage(run_result.runtime_usage)
150+
.await;
100151
}
101152
}
102153

154+
async fn process_test_execution_message(
155+
client: &async_nats::Client,
156+
message: async_nats::Message,
157+
engine: &ExecutionEngine,
158+
nats_remote: &NATSRemoteRuntime,
159+
runtime_usage_service: Option<&TaurusRuntimeUsageService>,
160+
) {
161+
let requested_execution_id =
162+
match parse_execution_id_from_subject(&message.subject, "test_executions") {
163+
Some(res) => res,
164+
None => {
165+
log::error!("Failed to extract execution uuid from {}", &message.subject);
166+
return;
167+
}
168+
};
169+
170+
let flow: ExecutionFlow = match ExecutionFlow::decode(&*message.payload) {
171+
Ok(flow) => flow,
172+
Err(err) => {
173+
log::error!(
174+
"Failed to deserialize test execution flow: {:?}, payload: {:?}",
175+
err,
176+
&message.payload
177+
);
178+
let result = build_decode_error_result(requested_execution_id);
179+
respond_to_test_execution_request(client, &message, result).await;
180+
return;
181+
}
182+
};
183+
184+
let run_result = execute_flow(requested_execution_id, flow, engine, nats_remote, None);
185+
186+
if let Some(usage_service) = runtime_usage_service {
187+
usage_service
188+
.update_runtime_usage(run_result.runtime_usage.clone())
189+
.await;
190+
}
191+
192+
let execution_result = build_execution_result(
193+
run_result.execution_id,
194+
run_result.flow_id,
195+
run_result.started_at,
196+
run_result.finished_at,
197+
run_result.input,
198+
run_result.signal,
199+
);
200+
respond_to_test_execution_request(client, &message, execution_result).await;
201+
}
202+
203+
#[derive(Clone)]
204+
struct FlowRunResult {
205+
execution_id: ExecutionId,
206+
flow_id: i64,
207+
started_at: i64,
208+
finished_at: i64,
209+
input: Option<Value>,
210+
signal: Signal,
211+
runtime_usage: RuntimeUsage,
212+
}
213+
103214
fn execute_flow(
104215
execution_id: ExecutionId,
105216
flow: ExecutionFlow,
106217
engine: &ExecutionEngine,
107218
nats_remote: &NATSRemoteRuntime,
108219
respond_emitter: Option<&dyn RespondEmitter>,
109-
) -> RuntimeUsage {
220+
) -> FlowRunResult {
221+
let started_at = now_unix_ms();
110222
let start = Instant::now();
111223
let flow_id = flow.flow_id;
112-
let (_signal, _reason) = engine.execute_flow_with_execution_id(
224+
let input = flow.input_value.clone();
225+
let (signal, _reason) = engine.execute_flow_with_execution_id(
113226
execution_id,
114227
flow,
115228
Some(nats_remote),
116229
respond_emitter,
117230
true,
118231
);
232+
let finished_at = now_unix_ms();
119233
let duration_millis = start.elapsed().as_millis() as i64;
120234

121-
RuntimeUsage {
235+
FlowRunResult {
236+
execution_id,
122237
flow_id,
123-
duration: duration_millis,
238+
started_at,
239+
finished_at,
240+
input,
241+
signal,
242+
runtime_usage: RuntimeUsage {
243+
flow_id,
244+
duration: duration_millis,
245+
},
124246
}
125247
}
126248

127-
fn parse_execution_id_from_subject(subject: &async_nats::Subject) -> Option<ExecutionId> {
249+
fn parse_execution_id_from_subject(
250+
subject: &async_nats::Subject,
251+
prefix: &str,
252+
) -> Option<ExecutionId> {
128253
let raw = subject.as_str();
129254
let mut parts = raw.split('.');
130255
match (parts.next(), parts.next(), parts.next()) {
131-
(Some("execution"), Some(uuid), None) => ExecutionId::parse_str(uuid).ok(),
256+
(Some(found_prefix), Some(uuid), None) if found_prefix == prefix => {
257+
ExecutionId::parse_str(uuid).ok()
258+
}
132259
_ => None,
133260
}
134261
}
262+
263+
fn build_execution_result(
264+
execution_id: ExecutionId,
265+
flow_id: i64,
266+
started_at: i64,
267+
finished_at: i64,
268+
input: Option<Value>,
269+
signal: Signal,
270+
) -> ExecutionResult {
271+
let result = match signal {
272+
Signal::Success(value) | Signal::Return(value) | Signal::Respond(value) => {
273+
Some(execution_result::Result::Success(value))
274+
}
275+
Signal::Failure(err) => Some(execution_result::Result::Error(err.as_tucana_error())),
276+
Signal::Stop => Some(execution_result::Result::Success(Value {
277+
kind: Some(tucana::shared::value::Kind::NullValue(0)),
278+
})),
279+
};
280+
281+
ExecutionResult {
282+
execution_identifier: execution_id.to_string(),
283+
flow_id,
284+
started_at,
285+
finished_at,
286+
input,
287+
node_execution_results: Vec::new(),
288+
result,
289+
}
290+
}
291+
292+
fn build_decode_error_result(execution_id: ExecutionId) -> ExecutionResult {
293+
let now = now_unix_ms();
294+
let runtime_error = RuntimeError::new(
295+
"T-TAURUS-000001",
296+
"ExecutionFlowDecodeError",
297+
"Failed to decode test execution flow payload",
298+
);
299+
300+
ExecutionResult {
301+
execution_identifier: execution_id.to_string(),
302+
flow_id: 0,
303+
started_at: now,
304+
finished_at: now,
305+
input: None,
306+
node_execution_results: Vec::new(),
307+
result: Some(execution_result::Result::Error(
308+
runtime_error.as_tucana_error(),
309+
)),
310+
}
311+
}
312+
313+
async fn respond_to_test_execution_request(
314+
client: &async_nats::Client,
315+
message: &async_nats::Message,
316+
result: ExecutionResult,
317+
) {
318+
let Some(reply_subject) = message.reply.as_ref() else {
319+
log::warn!(
320+
"Received test execution request without reply subject on '{}'; cannot return ExecutionResult",
321+
message.subject
322+
);
323+
return;
324+
};
325+
326+
if let Err(err) = client
327+
.publish(reply_subject.clone(), result.encode_to_vec().into())
328+
.await
329+
{
330+
log::error!(
331+
"Failed to publish test execution response on '{}': {:?}",
332+
reply_subject,
333+
err
334+
);
335+
}
336+
}
337+
338+
fn now_unix_ms() -> i64 {
339+
SystemTime::now()
340+
.duration_since(UNIX_EPOCH)
341+
.map(|it| it.as_millis() as i64)
342+
.unwrap_or(0)
343+
}
344+

0 commit comments

Comments
 (0)