Skip to content

Commit 7a1b6c9

Browse files
committed
Progress.
1 parent c547568 commit 7a1b6c9

File tree

11 files changed

+728
-250
lines changed

11 files changed

+728
-250
lines changed

Cargo.lock

Lines changed: 457 additions & 145 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
anyhow = "1"
8-
ctrlc-async = "3"
8+
ctrlc2 = { version = "3", features = ["termination", "tokio"] }
99
derive_builder = "0.20"
1010
envy = "0.4"
1111
futures-util = "0.3"
@@ -17,9 +17,14 @@ serde = { version = "1", features = ["derive"] }
1717
serde_json = "1"
1818
thiserror = "1"
1919
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread", "sync"] }
20-
tonic = { version = "0.12", features = ["tls"] }
20+
tonic = { version = "0.12", features = ["tls", "tls-native-roots"] }
2121
tracing = "0.1"
2222

2323
[build-dependencies]
2424
tonic-build = "0.12"
2525
tempfile = "3"
26+
27+
[dev-dependencies]
28+
dotenv = "0.15"
29+
rstest = "0.23"
30+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

build.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ fn prepend_package_name_and_build(
1414
writeln!(temp_file, "package {package_name};")?;
1515

1616
let target_path = temp_file.into_temp_path();
17-
tonic_build::compile_protos(target_path)?;
17+
tonic_build::configure()
18+
.disable_package_emission()
19+
.compile_protos(
20+
&[&target_path],
21+
&[target_path.parent().expect("must succeed")],
22+
)?;
1823
Ok(())
1924
}
2025

examples/fibonacci.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use hatchet_sdk::{Client, StepBuilder, WorkflowBuilder};
2+
3+
fn fibonacci(n: u32) -> u32 {
4+
(1..=n)
5+
.fold((0, 1), |(last, current), _| (current, last + current))
6+
.0
7+
}
8+
9+
#[cfg(test)]
10+
mod tests {
11+
use rstest::rstest;
12+
13+
#[rstest]
14+
#[case(0, 0)]
15+
#[case(1, 1)]
16+
#[case(2, 1)]
17+
#[case(3, 2)]
18+
#[case(4, 3)]
19+
#[case(5, 5)]
20+
#[case(6, 8)]
21+
fn fibonacci_test(#[case] input: u32, #[case] expected: u32) {
22+
assert_eq!(expected, super::fibonacci(input))
23+
}
24+
}
25+
26+
#[derive(serde::Deserialize)]
27+
struct Input {
28+
n: u32,
29+
}
30+
31+
#[derive(serde::Serialize)]
32+
struct Output {
33+
result: u32,
34+
}
35+
36+
async fn execute(Input { n }: Input) -> anyhow::Result<Output> {
37+
Ok(Output {
38+
result: fibonacci(n),
39+
})
40+
}
41+
42+
#[tokio::main]
43+
async fn main() -> anyhow::Result<()> {
44+
dotenv::dotenv().ok();
45+
tracing_subscriber::fmt()
46+
.with_target(false)
47+
.with_env_filter(
48+
tracing_subscriber::EnvFilter::from_default_env()
49+
.add_directive("hatchet_sdk=debug".parse()?),
50+
)
51+
.init();
52+
53+
let client = Client::new()?;
54+
let mut worker = client.worker("example").build();
55+
worker.register_workflow(
56+
WorkflowBuilder::default()
57+
.name("fibonacci")
58+
.step(
59+
StepBuilder::default()
60+
.name("compute")
61+
.function(&execute)
62+
.build()?,
63+
)
64+
.build()?,
65+
);
66+
worker.start().await?;
67+
Ok(())
68+
}

hatchet

Submodule hatchet updated 421 files

src/client.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ use crate::worker::WorkerBuilder;
66
pub(crate) struct Environment {
77
pub(crate) token: SecretString,
88
pub(crate) host_port: Option<String>,
9-
#[serde(default)]
10-
pub(crate) listener_v2_timeout: u64,
9+
pub(crate) listener_v2_timeout: Option<u64>,
1110
#[serde(default)]
1211
pub(crate) tls_strategy: crate::ClientTlStrategy,
1312
pub(crate) tls_cert_file: Option<String>,

src/error.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@
22
pub enum Error {
33
#[error("failed to load configuration from the environment: {0}")]
44
Environment(#[from] envy::Error),
5-
#[error("transport error: {0}")]
6-
TonicTransport(#[from] tonic::transport::Error),
7-
#[error("status: {0}")]
8-
TonicStatus(#[from] tonic::Status),
5+
#[error("worker registration request: {0}")]
6+
CouldNotRegisterWorker(tonic::Status),
7+
#[error("workflow registration request:: {0}")]
8+
CouldNotPutWorkflow(tonic::Status),
9+
#[error("dispatcher listen error: {0}")]
10+
CouldNotListenToDispatcher(tonic::Status),
11+
#[error("step status send error: {0}")]
12+
CouldNotSendStepStatus(tonic::Status),
13+
#[error("heartbeat error: {0}")]
14+
CouldNotSendHeartbeat(tonic::Status),
15+
#[error("dispatcher connection error: {0}")]
16+
CouldNotConnectToDispatcher(tonic::transport::Error),
17+
#[error("workflow service connection error: {0:?}")]
18+
CouldNotConnectToWorkflowService(tonic::transport::Error),
919
#[error("could not read file under `{1}`: {0}")]
1020
CouldNotReadFile(std::io::Error, String),
1121
#[error("environment variables {0} and {1} cannot be set simultaneously")]
1222
CantSetBothEnvironmentVariables(&'static str, &'static str),
1323
#[error("could not subscribe to actions after {0} retries")]
1424
CouldNotSubscribeToActions(usize),
15-
#[error("could not decode the provided token to retrieve the host/port pair")]
25+
#[error("could not decode the provided token to retrieve the host/port pair: {0}")]
1626
CouldNotDecodeToken(jsonwebtoken::errors::Error),
1727
#[error("could not decode action payload: {0}")]
1828
CouldNotDecodeActionPayload(serde_json::Error),

src/worker/heartbeat.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ where
2323
heartbeat_at: Some(std::time::SystemTime::now().into()),
2424
worker_id: worker_id.clone(),
2525
})
26-
.await?;
26+
.await
27+
.map_err(crate::Error::CouldNotSendHeartbeat)?;
2728

2829
tokio::select! {
2930
_ = interval.tick() => {

src/worker/listener.rs

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use futures_util::FutureExt;
2+
use tokio::{task::LocalSet, task_local};
13
use tonic::IntoRequest;
2-
use tracing::{error, warn};
4+
use tracing::{debug, error, info, warn};
35

46
use crate::{
57
worker::{grpc::ActionType, DEFAULT_ACTION_TIMEOUT},
@@ -37,10 +39,16 @@ fn step_action_event(
3739
}
3840
}
3941

42+
#[derive(serde::Deserialize)]
43+
struct ActionInput<T> {
44+
input: T,
45+
}
46+
4047
async fn handle_start_step_run<F>(
4148
dispatcher: &mut DispatcherClient<
4249
tonic::service::interceptor::InterceptedService<tonic::transport::Channel, F>,
4350
>,
51+
local_set: &tokio::task::LocalSet,
4452
namespace: &str,
4553
worker_id: &str,
4654
workflows: &[Workflow],
@@ -59,45 +67,53 @@ where
5967
return Ok(());
6068
};
6169

70+
debug!("Received a new action: {action:?}.");
71+
6272
dispatcher
6373
.send_step_action_event(step_action_event(
6474
worker_id,
6575
&action,
6676
StepActionEventType::StepEventTypeStarted,
6777
Default::default(),
6878
))
69-
.await?
79+
.await
80+
.map_err(crate::Error::CouldNotSendStepStatus)?
7081
.into_inner();
7182

72-
let input = serde_json::from_str(&action.action_payload)
83+
let input: ActionInput<serde_json::Value> = serde_json::from_str(&action.action_payload)
7384
.map_err(crate::Error::CouldNotDecodeActionPayload)?;
7485

7586
// FIXME: Obviously, run this asynchronously rather than blocking the main listening loop.
76-
let action_event =
77-
match tokio::task::spawn_local(async move { action_callable(input).await }).await {
78-
Ok(Ok(output_value)) => step_action_event(
79-
worker_id,
80-
&action,
81-
StepActionEventType::StepEventTypeCompleted,
82-
serde_json::to_string(&output_value).expect("must succeed"),
83-
),
84-
Ok(Err(error)) => step_action_event(
85-
worker_id,
86-
&action,
87-
StepActionEventType::StepEventTypeFailed,
88-
error.to_string(),
89-
),
90-
Err(join_error) => step_action_event(
91-
worker_id,
92-
&action,
93-
StepActionEventType::StepEventTypeFailed,
94-
join_error.to_string(),
95-
),
96-
};
87+
let action_event = match local_set
88+
.run_until(async move {
89+
tokio::task::spawn_local(async move { action_callable(input.input).await }).await
90+
})
91+
.await
92+
{
93+
Ok(Ok(output_value)) => step_action_event(
94+
worker_id,
95+
&action,
96+
StepActionEventType::StepEventTypeCompleted,
97+
serde_json::to_string(&output_value).expect("must succeed"),
98+
),
99+
Ok(Err(error)) => step_action_event(
100+
worker_id,
101+
&action,
102+
StepActionEventType::StepEventTypeFailed,
103+
error.to_string(),
104+
),
105+
Err(join_error) => step_action_event(
106+
worker_id,
107+
&action,
108+
StepActionEventType::StepEventTypeFailed,
109+
join_error.to_string(),
110+
),
111+
};
97112

98113
dispatcher
99114
.send_step_action_event(action_event)
100-
.await?
115+
.await
116+
.map_err(crate::Error::CouldNotSendStepStatus)?
101117
.into_inner();
102118

103119
Ok(())
@@ -110,7 +126,7 @@ pub(crate) async fn run<F>(
110126
namespace: &str,
111127
worker_id: &str,
112128
workflows: Vec<Workflow>,
113-
listener_v2_timeout: u64,
129+
listener_v2_timeout: Option<u64>,
114130
mut interrupt_receiver: tokio::sync::mpsc::Receiver<()>,
115131
_heartbeat_interrupt_sender: tokio::sync::mpsc::Sender<()>,
116132
) -> crate::Result<()>
@@ -125,6 +141,8 @@ where
125141
let connection_attempt = tokio::time::Instant::now();
126142

127143
'main_loop: loop {
144+
info!("Listening…");
145+
128146
if connection_attempt.elapsed() > DEFAULT_ACTION_LISTENER_RETRY_INTERVAL {
129147
retries = 0;
130148
}
@@ -134,25 +152,46 @@ where
134152
));
135153
}
136154

137-
let mut stream = match listen_strategy {
155+
let response = match listen_strategy {
138156
ListenStrategy::V1 => {
157+
info!("Using strategy v1");
158+
139159
let mut request = WorkerListenRequest {
140160
worker_id: worker_id.to_owned(),
141161
}
142162
.into_request();
143163
request.set_timeout(DEFAULT_ACTION_TIMEOUT);
144-
dispatcher.listen(request).await?.into_inner()
164+
Box::new(dispatcher.listen(request)).boxed()
145165
}
146166
ListenStrategy::V2 => {
167+
info!("Using strategy v2");
168+
147169
let mut request = WorkerListenRequest {
148170
worker_id: worker_id.to_owned(),
149171
}
150172
.into_request();
151-
request.set_timeout(std::time::Duration::from_millis(listener_v2_timeout));
152-
dispatcher.listen_v2(request).await?.into_inner()
173+
if let Some(listener_v2_timeout) = listener_v2_timeout {
174+
request.set_timeout(std::time::Duration::from_millis(listener_v2_timeout));
175+
}
176+
dispatcher.listen_v2(request).boxed()
153177
}
154178
};
155179

180+
let mut stream = tokio::select! {
181+
response = response => {
182+
response
183+
.map_err(crate::Error::CouldNotListenToDispatcher)?
184+
.into_inner()
185+
}
186+
result = interrupt_receiver.recv() => {
187+
assert!(result.is_some());
188+
warn!("Interrupt received.");
189+
break 'main_loop;
190+
}
191+
};
192+
193+
let local_set = LocalSet::new();
194+
156195
loop {
157196
tokio::select! {
158197
element = stream.next() => {
@@ -190,7 +229,7 @@ where
190229

191230
match action_type {
192231
ActionType::StartStepRun => {
193-
handle_start_step_run(&mut dispatcher, namespace, worker_id, &workflows, action).await?;
232+
handle_start_step_run(&mut dispatcher, &local_set, namespace, worker_id, &workflows, action).await?;
194233
}
195234
ActionType::CancelStepRun => {
196235
todo!()
@@ -200,7 +239,9 @@ where
200239
}
201240
}
202241
}
203-
_ = interrupt_receiver.recv() => {
242+
result = interrupt_receiver.recv() => {
243+
assert!(result.is_some());
244+
warn!("Interrupt received.");
204245
break 'main_loop;
205246
}
206247
}

0 commit comments

Comments
 (0)