1use temporalio_client::tonic::Request;
13use temporalio_common::protos::temporal::api::{
14 common::v1::{Payload as ProtoPayload, Payloads, WorkflowExecution, WorkflowType},
15 enums::v1::UpdateWorkflowExecutionLifecycleStage,
16 schedule::v1::{
17 BackfillRequest, Schedule, ScheduleAction, SchedulePatch, ScheduleSpec,
18 TriggerImmediatelyRequest, schedule_action::Action as ScheduleActionKind,
19 },
20 taskqueue::v1::TaskQueue,
21 update::v1::{Input as UpdateInput, Meta as UpdateMeta, Request as UpdateRequest, WaitPolicy},
22 workflow::v1::NewWorkflowExecutionInfo,
23 workflowservice::v1::{
24 CreateScheduleRequest, DeleteScheduleRequest, DeleteWorkflowExecutionRequest,
25 PatchScheduleRequest, RequestCancelWorkflowExecutionRequest, ResetWorkflowExecutionRequest,
26 SignalWorkflowExecutionRequest, TerminateWorkflowExecutionRequest,
27 UpdateWorkflowExecutionRequest,
28 },
29};
30use tmprl_core::mutation::Mutation;
31
32use super::OpError;
33use crate::Conn;
34
35fn identity() -> String {
37 format!(
38 "tmprl@{}",
39 std::env::var("USER").unwrap_or_else(|_| "unknown".into())
40 )
41}
42
43fn request_id() -> String {
49 uuid::Uuid::new_v4().to_string()
50}
51
52fn timestamp(ms: i64) -> prost_wkt_types::Timestamp {
54 prost_wkt_types::Timestamp {
55 seconds: ms.div_euclid(1_000),
56 nanos: (ms.rem_euclid(1_000) * 1_000_000) as i32,
57 }
58}
59
60fn execution(workflow_id: &str, run_id: &str) -> Option<WorkflowExecution> {
61 Some(WorkflowExecution {
62 workflow_id: workflow_id.to_string(),
63 run_id: run_id.to_string(),
64 })
65}
66
67impl Conn {
68 pub async fn mutate(&self, m: &Mutation) -> Result<(), OpError> {
73 match m {
74 Mutation::Cancel {
75 namespace,
76 workflow_id,
77 run_id,
78 } => {
79 self.wf()
80 .request_cancel_workflow_execution(Request::new(
81 RequestCancelWorkflowExecutionRequest {
82 namespace: namespace.clone(),
83 workflow_execution: execution(workflow_id, run_id),
84 identity: identity(),
85 request_id: request_id(),
86 ..Default::default()
87 },
88 ))
89 .await
90 .map_err(|s| OpError::rpc("RequestCancelWorkflowExecution", s))?;
91 }
92
93 Mutation::Terminate {
94 namespace,
95 workflow_id,
96 run_id,
97 reason,
98 } => {
99 self.wf()
100 .terminate_workflow_execution(Request::new(TerminateWorkflowExecutionRequest {
101 namespace: namespace.clone(),
102 workflow_execution: execution(workflow_id, run_id),
103 reason: reason.clone(),
104 identity: identity(),
105 ..Default::default()
106 }))
107 .await
108 .map_err(|s| OpError::rpc("TerminateWorkflowExecution", s))?;
109 }
110
111 Mutation::Signal {
112 namespace,
113 workflow_id,
114 run_id,
115 name,
116 input,
117 } => {
118 self.wf()
119 .signal_workflow_execution(Request::new(SignalWorkflowExecutionRequest {
120 namespace: namespace.clone(),
121 workflow_execution: execution(workflow_id, run_id),
122 signal_name: name.clone(),
123 input: input.as_deref().map(json_payload),
124 identity: identity(),
125 request_id: request_id(),
126 ..Default::default()
127 }))
128 .await
129 .map_err(|s| OpError::rpc("SignalWorkflowExecution", s))?;
130 }
131
132 Mutation::Delete {
133 namespace,
134 workflow_id,
135 run_id,
136 } => {
137 self.wf()
138 .delete_workflow_execution(Request::new(DeleteWorkflowExecutionRequest {
139 namespace: namespace.clone(),
140 workflow_execution: execution(workflow_id, run_id),
141 }))
142 .await
143 .map_err(|s| OpError::rpc("DeleteWorkflowExecution", s))?;
144 }
145
146 Mutation::Reset {
147 namespace,
148 workflow_id,
149 run_id,
150 event_id,
151 reason,
152 } => {
153 self.wf()
154 .reset_workflow_execution(Request::new(ResetWorkflowExecutionRequest {
155 namespace: namespace.clone(),
156 workflow_execution: execution(workflow_id, run_id),
157 reason: reason.clone(),
158 workflow_task_finish_event_id: *event_id,
161 reset_reapply_exclude_types: Vec::new(),
164 identity: identity(),
165 request_id: request_id(),
166 ..Default::default()
167 }))
168 .await
169 .map_err(|s| OpError::rpc("ResetWorkflowExecution", s))?;
170 }
171
172 Mutation::Update {
173 namespace,
174 workflow_id,
175 run_id,
176 name,
177 input,
178 } => {
179 let resp = self
180 .wf()
181 .update_workflow_execution(Request::new(UpdateWorkflowExecutionRequest {
182 namespace: namespace.clone(),
183 workflow_execution: execution(workflow_id, run_id),
184 wait_policy: Some(WaitPolicy {
187 lifecycle_stage: UpdateWorkflowExecutionLifecycleStage::Completed
188 as i32,
189 }),
190 request: Some(UpdateRequest {
191 request_id: request_id(),
192 meta: Some(UpdateMeta {
193 update_id: request_id(),
194 identity: identity(),
195 }),
196 input: Some(UpdateInput {
197 name: name.clone(),
198 args: input.as_deref().map(json_payload),
199 ..Default::default()
200 }),
201 completion_callbacks: Vec::new(),
202 links: Vec::new(),
203 }),
204 ..Default::default()
205 }))
206 .await
207 .map_err(|s| OpError::rpc("UpdateWorkflowExecution", s))?
208 .into_inner();
209
210 if let Some(outcome) = resp.outcome
214 && let Some(
215 temporalio_common::protos::temporal::api::update::v1::outcome::Value::Failure(f),
216 ) = outcome.value
217 {
218 return Err(OpError::Rpc {
219 operation: "UpdateWorkflowExecution",
220 code: "Rejected".into(),
221 message: f.message,
222 });
223 }
224 }
225
226 Mutation::PauseSchedule {
227 namespace,
228 schedule_id,
229 paused,
230 } => {
231 let note = format!("{} from tmprl", if *paused { "paused" } else { "resumed" });
234 self.wf()
235 .patch_schedule(Request::new(PatchScheduleRequest {
236 namespace: namespace.clone(),
237 schedule_id: schedule_id.clone(),
238 patch: Some(SchedulePatch {
239 pause: if *paused { note.clone() } else { String::new() },
240 unpause: if *paused { String::new() } else { note },
241 ..Default::default()
242 }),
243 identity: identity(),
244 request_id: request_id(),
245 }))
246 .await
247 .map_err(|s| OpError::rpc("PatchSchedule", s))?;
248 }
249
250 Mutation::TriggerSchedule {
251 namespace,
252 schedule_id,
253 } => {
254 self.wf()
255 .patch_schedule(Request::new(PatchScheduleRequest {
256 namespace: namespace.clone(),
257 schedule_id: schedule_id.clone(),
258 patch: Some(SchedulePatch {
259 trigger_immediately: Some(TriggerImmediatelyRequest {
260 overlap_policy: 0,
263 scheduled_time: None,
265 }),
266 ..Default::default()
267 }),
268 identity: identity(),
269 request_id: request_id(),
270 }))
271 .await
272 .map_err(|s| OpError::rpc("PatchSchedule", s))?;
273 }
274
275 Mutation::CreateSchedule {
276 namespace,
277 schedule_id,
278 workflow_id,
279 workflow_type,
280 task_queue,
281 spec,
282 input,
283 } => {
284 self.wf()
285 .create_schedule(Request::new(CreateScheduleRequest {
286 namespace: namespace.clone(),
287 schedule_id: schedule_id.clone(),
288 schedule: Some(Schedule {
289 spec: Some(ScheduleSpec {
293 cron_string: vec![spec.clone()],
294 ..Default::default()
295 }),
296 action: Some(ScheduleAction {
297 action: Some(ScheduleActionKind::StartWorkflow(
298 NewWorkflowExecutionInfo {
299 workflow_id: workflow_id.clone(),
300 workflow_type: Some(WorkflowType {
301 name: workflow_type.clone(),
302 }),
303 task_queue: Some(TaskQueue {
304 name: task_queue.clone(),
305 ..Default::default()
306 }),
307 input: input.as_deref().map(json_payload),
308 ..Default::default()
309 },
310 )),
311 }),
312 ..Default::default()
313 }),
314 identity: identity(),
315 request_id: request_id(),
316 ..Default::default()
317 }))
318 .await
319 .map_err(|s| OpError::rpc("CreateSchedule", s))?;
320 }
321
322 Mutation::BackfillSchedule {
323 namespace,
324 schedule_id,
325 range,
326 overlap,
327 } => {
328 self.wf()
329 .patch_schedule(Request::new(PatchScheduleRequest {
330 namespace: namespace.clone(),
331 schedule_id: schedule_id.clone(),
332 patch: Some(SchedulePatch {
333 backfill_request: vec![BackfillRequest {
334 start_time: Some(timestamp(range.start_ms)),
335 end_time: Some(timestamp(range.end_ms)),
336 overlap_policy: overlap.code(),
337 }],
338 ..Default::default()
339 }),
340 identity: identity(),
341 request_id: request_id(),
342 }))
343 .await
344 .map_err(|s| OpError::rpc("PatchSchedule", s))?;
345 }
346
347 Mutation::DeleteSchedule {
348 namespace,
349 schedule_id,
350 } => {
351 self.wf()
352 .delete_schedule(Request::new(DeleteScheduleRequest {
353 namespace: namespace.clone(),
354 schedule_id: schedule_id.clone(),
355 identity: identity(),
356 }))
357 .await
358 .map_err(|s| OpError::rpc("DeleteSchedule", s))?;
359 }
360 }
361 Ok(())
362 }
363}
364
365fn json_payload(input: &str) -> Payloads {
370 Payloads {
371 payloads: vec![ProtoPayload {
372 metadata: [("encoding".to_string(), b"json/plain".to_vec())]
373 .into_iter()
374 .collect(),
375 data: input.as_bytes().to_vec(),
376 external_payloads: Vec::new(),
377 }],
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn a_signal_argument_is_sent_as_json_plain() {
387 let p = json_payload(r#"{"a":1}"#);
388 assert_eq!(p.payloads.len(), 1);
389 assert_eq!(p.payloads[0].data, br#"{"a":1}"#);
390 assert_eq!(
391 p.payloads[0].metadata.get("encoding").map(|v| v.as_slice()),
392 Some(&b"json/plain"[..])
393 );
394 }
395
396 #[test]
397 fn tmprl_names_itself_on_what_it_causes() {
398 assert!(identity().starts_with("tmprl@"));
401 }
402
403 #[test]
404 fn an_execution_carries_both_ids() {
405 let e = execution("w", "r").unwrap();
406 assert_eq!(e.workflow_id, "w");
407 assert_eq!(e.run_id, "r");
408 }
409}