@temporalio/core-bridge 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/releases/aarch64-apple-darwin/index.node +0 -0
- package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
- package/releases/x86_64-apple-darwin/index.node +0 -0
- package/releases/x86_64-pc-windows-msvc/index.node +0 -0
- package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
- package/sdk-core/core/src/core_tests/replay_flag.rs +73 -3
- package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +2 -2
- package/sdk-core/core/src/worker/workflow/machines/patch_state_machine.rs +7 -0
- package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +75 -47
- package/sdk-core/sdk/src/workflow_context.rs +2 -2
- package/sdk-core/tests/integ_tests/workflow_tests/patches.rs +35 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@temporalio/core-bridge",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "Temporal.io SDK Core<>Node bridge",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@opentelemetry/api": "^1.4.1",
|
|
26
|
-
"@temporalio/common": "1.8.
|
|
26
|
+
"@temporalio/common": "1.8.4",
|
|
27
27
|
"arg": "^5.0.2",
|
|
28
28
|
"cargo-cp-artifact": "^0.1.6",
|
|
29
29
|
"which": "^2.0.2"
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "7e65cf816b1deef72973dc64ccbf2c93916a3eb1"
|
|
57
57
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
use crate::{
|
|
1
|
+
use crate::{
|
|
2
|
+
test_help::{
|
|
3
|
+
build_mock_pollers, canned_histories, hist_to_poll_resp, mock_worker, MockPollCfg,
|
|
4
|
+
},
|
|
5
|
+
worker::{client::mocks::mock_workflow_client, ManagedWFFunc, LEGACY_QUERY_ID},
|
|
6
|
+
};
|
|
2
7
|
use rstest::{fixture, rstest};
|
|
3
|
-
use std::time::Duration;
|
|
8
|
+
use std::{collections::VecDeque, time::Duration};
|
|
4
9
|
use temporal_sdk::{WfContext, WorkflowFunction};
|
|
5
|
-
use
|
|
10
|
+
use temporal_sdk_core_api::Worker;
|
|
11
|
+
use temporal_sdk_core_protos::{
|
|
12
|
+
coresdk::{
|
|
13
|
+
workflow_commands::{workflow_command::Variant::RespondToQuery, QueryResult, QuerySuccess},
|
|
14
|
+
workflow_completion::WorkflowActivationCompletion,
|
|
15
|
+
},
|
|
16
|
+
temporal::api::{enums::v1::CommandType, query::v1::WorkflowQuery},
|
|
17
|
+
};
|
|
18
|
+
use temporal_sdk_core_test_utils::start_timer_cmd;
|
|
6
19
|
|
|
7
20
|
fn timers_wf(num_timers: u32) -> WorkflowFunction {
|
|
8
21
|
WorkflowFunction::new(move |command_sink: WfContext| async move {
|
|
@@ -63,3 +76,60 @@ async fn replay_flag_is_correct_partial_history() {
|
|
|
63
76
|
assert_eq!(commands[0].command_type, CommandType::StartTimer as i32);
|
|
64
77
|
wfm.shutdown().await.unwrap();
|
|
65
78
|
}
|
|
79
|
+
|
|
80
|
+
#[tokio::test]
|
|
81
|
+
async fn replay_flag_correct_with_query() {
|
|
82
|
+
let wfid = "fake_wf_id";
|
|
83
|
+
let t = canned_histories::single_timer("1");
|
|
84
|
+
let tasks = VecDeque::from(vec![
|
|
85
|
+
{
|
|
86
|
+
let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), 2.into());
|
|
87
|
+
// Server can issue queries that contain the WFT completion and the subsequent
|
|
88
|
+
// commands, but not the consequences yet.
|
|
89
|
+
pr.query = Some(WorkflowQuery {
|
|
90
|
+
query_type: "query-type".to_string(),
|
|
91
|
+
query_args: Some(b"hi".into()),
|
|
92
|
+
header: None,
|
|
93
|
+
});
|
|
94
|
+
let h = pr.history.as_mut().unwrap();
|
|
95
|
+
h.events.truncate(5);
|
|
96
|
+
pr.started_event_id = 3;
|
|
97
|
+
dbg!(&pr.resp);
|
|
98
|
+
pr
|
|
99
|
+
},
|
|
100
|
+
hist_to_poll_resp(&t, wfid.to_owned(), 2.into()),
|
|
101
|
+
]);
|
|
102
|
+
let mut mock = MockPollCfg::from_resp_batches(wfid, t, tasks, mock_workflow_client());
|
|
103
|
+
mock.num_expected_legacy_query_resps = 1;
|
|
104
|
+
let mut mock = build_mock_pollers(mock);
|
|
105
|
+
mock.worker_cfg(|wc| wc.max_cached_workflows = 10);
|
|
106
|
+
let core = mock_worker(mock);
|
|
107
|
+
|
|
108
|
+
let task = core.poll_workflow_activation().await.unwrap();
|
|
109
|
+
core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
|
|
110
|
+
task.run_id,
|
|
111
|
+
start_timer_cmd(1, Duration::from_secs(1)),
|
|
112
|
+
))
|
|
113
|
+
.await
|
|
114
|
+
.unwrap();
|
|
115
|
+
|
|
116
|
+
let task = core.poll_workflow_activation().await.unwrap();
|
|
117
|
+
assert!(task.is_replaying);
|
|
118
|
+
core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
|
|
119
|
+
task.run_id,
|
|
120
|
+
RespondToQuery(QueryResult {
|
|
121
|
+
query_id: LEGACY_QUERY_ID.to_string(),
|
|
122
|
+
variant: Some(
|
|
123
|
+
QuerySuccess {
|
|
124
|
+
response: Some("hi".into()),
|
|
125
|
+
}
|
|
126
|
+
.into(),
|
|
127
|
+
),
|
|
128
|
+
}),
|
|
129
|
+
))
|
|
130
|
+
.await
|
|
131
|
+
.unwrap();
|
|
132
|
+
|
|
133
|
+
let task = core.poll_workflow_activation().await.unwrap();
|
|
134
|
+
assert!(!task.is_replaying);
|
|
135
|
+
}
|
|
@@ -235,14 +235,14 @@ impl StartCommandCreated {
|
|
|
235
235
|
if event_dat.wf_id != state.workflow_id {
|
|
236
236
|
return TransitionResult::Err(WFMachinesError::Nondeterminism(format!(
|
|
237
237
|
"Child workflow id of scheduled event '{}' does not \
|
|
238
|
-
match child workflow id of
|
|
238
|
+
match child workflow id of command '{}'",
|
|
239
239
|
event_dat.wf_id, state.workflow_id
|
|
240
240
|
)));
|
|
241
241
|
}
|
|
242
242
|
if event_dat.wf_type != state.workflow_type {
|
|
243
243
|
return TransitionResult::Err(WFMachinesError::Nondeterminism(format!(
|
|
244
244
|
"Child workflow type of scheduled event '{}' does not \
|
|
245
|
-
match child workflow type of
|
|
245
|
+
match child workflow type of command '{}'",
|
|
246
246
|
event_dat.wf_type, state.workflow_type
|
|
247
247
|
)));
|
|
248
248
|
}
|
|
@@ -270,6 +270,13 @@ impl TryFrom<HistEventData> for PatchMachineEvents {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
impl PatchMachine {
|
|
274
|
+
/// Returns true if this patch machine has the same id as the one provided
|
|
275
|
+
pub(crate) fn matches_patch(&self, id: &str) -> bool {
|
|
276
|
+
self.shared_state.patch_id == id
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
273
280
|
#[cfg(test)]
|
|
274
281
|
mod tests {
|
|
275
282
|
use crate::{
|
|
@@ -138,7 +138,7 @@ pub(crate) struct WorkflowMachines {
|
|
|
138
138
|
current_wf_task_commands: VecDeque<CommandAndMachine>,
|
|
139
139
|
|
|
140
140
|
/// Information about patch markers we have already seen while replaying history
|
|
141
|
-
|
|
141
|
+
encountered_patch_markers: HashMap<String, ChangeInfo>,
|
|
142
142
|
|
|
143
143
|
/// Contains extra local-activity related data
|
|
144
144
|
local_activity_data: LocalActivityData,
|
|
@@ -255,7 +255,7 @@ impl WorkflowMachines {
|
|
|
255
255
|
id_to_machine: Default::default(),
|
|
256
256
|
commands: Default::default(),
|
|
257
257
|
current_wf_task_commands: Default::default(),
|
|
258
|
-
|
|
258
|
+
encountered_patch_markers: Default::default(),
|
|
259
259
|
local_activity_data: LocalActivityData::default(),
|
|
260
260
|
have_seen_terminal_event: false,
|
|
261
261
|
}
|
|
@@ -367,9 +367,17 @@ impl WorkflowMachines {
|
|
|
367
367
|
/// "no work" situation. Possibly, it may know about some work the machines don't, like queries.
|
|
368
368
|
pub(crate) fn get_wf_activation(&mut self) -> WorkflowActivation {
|
|
369
369
|
let jobs = self.drive_me.drain_jobs();
|
|
370
|
+
// Even though technically we may have satisfied all the criteria to be done with replay,
|
|
371
|
+
// query only activations are always "replaying" to keep things sane.
|
|
372
|
+
let all_query = jobs.iter().all(|j| {
|
|
373
|
+
matches!(
|
|
374
|
+
j.variant,
|
|
375
|
+
Some(workflow_activation_job::Variant::QueryWorkflow(_))
|
|
376
|
+
)
|
|
377
|
+
});
|
|
370
378
|
WorkflowActivation {
|
|
371
379
|
timestamp: self.current_wf_time.map(Into::into),
|
|
372
|
-
is_replaying: self.replaying,
|
|
380
|
+
is_replaying: self.replaying || all_query,
|
|
373
381
|
run_id: self.run_id.clone(),
|
|
374
382
|
history_length: self.last_processed_event as u32,
|
|
375
383
|
jobs,
|
|
@@ -488,7 +496,6 @@ impl WorkflowMachines {
|
|
|
488
496
|
}
|
|
489
497
|
}
|
|
490
498
|
|
|
491
|
-
let mut saw_completed = false;
|
|
492
499
|
let mut do_handle_event = true;
|
|
493
500
|
let mut history = events.into_iter().peekable();
|
|
494
501
|
while let Some(event) = history.next() {
|
|
@@ -504,17 +511,21 @@ impl WorkflowMachines {
|
|
|
504
511
|
// This definition of replaying here is that we are no longer replaying as soon as we
|
|
505
512
|
// see new events that have never been seen or produced by the SDK.
|
|
506
513
|
//
|
|
507
|
-
// Specifically, replay ends once we have seen
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
514
|
+
// Specifically, replay ends once we have seen any non-command event (IE: events that
|
|
515
|
+
// aren't a result of something we produced in the SDK) on a WFT which has the final
|
|
516
|
+
// event in history (meaning we are processing the most recent WFT and there are no
|
|
517
|
+
// more subsequent WFTs). WFT Completed in this case does not count as a non-command
|
|
518
|
+
// event, because that will typically show up as the first event in an incremental
|
|
519
|
+
// history, and we want to ignore it and its associated commands since we "produced"
|
|
520
|
+
// them.
|
|
521
|
+
if self.replaying
|
|
522
|
+
&& has_final_event
|
|
523
|
+
&& event.event_type() != EventType::WorkflowTaskCompleted
|
|
524
|
+
&& !event.is_command_event()
|
|
525
|
+
{
|
|
512
526
|
// Replay is finished
|
|
513
527
|
self.replaying = false;
|
|
514
528
|
}
|
|
515
|
-
if event.event_type() == EventType::WorkflowTaskCompleted {
|
|
516
|
-
saw_completed = true;
|
|
517
|
-
}
|
|
518
529
|
|
|
519
530
|
if do_handle_event {
|
|
520
531
|
let eho = self.handle_event(
|
|
@@ -547,7 +558,7 @@ impl WorkflowMachines {
|
|
|
547
558
|
.peek_next_wft_sequence(last_handled_wft_started_id)
|
|
548
559
|
{
|
|
549
560
|
if let Some((patch_id, _)) = e.get_patch_marker_details() {
|
|
550
|
-
self.
|
|
561
|
+
self.encountered_patch_markers.insert(
|
|
551
562
|
patch_id.clone(),
|
|
552
563
|
ChangeInfo {
|
|
553
564
|
created_command: false,
|
|
@@ -718,7 +729,7 @@ impl WorkflowMachines {
|
|
|
718
729
|
let consumed_cmd = loop {
|
|
719
730
|
if let Some(peek_machine) = self.commands.front() {
|
|
720
731
|
let mach = self.machine(peek_machine.machine);
|
|
721
|
-
match
|
|
732
|
+
match patch_marker_handling(event, mach, next_event)? {
|
|
722
733
|
EventHandlingOutcome::SkipCommand => {
|
|
723
734
|
self.commands.pop_front();
|
|
724
735
|
continue;
|
|
@@ -1138,7 +1149,7 @@ impl WorkflowMachines {
|
|
|
1138
1149
|
WFCommand::SetPatchMarker(attrs) => {
|
|
1139
1150
|
// Do not create commands for change IDs that we have already created commands
|
|
1140
1151
|
// for.
|
|
1141
|
-
let encountered_entry = self.
|
|
1152
|
+
let encountered_entry = self.encountered_patch_markers.get(&attrs.patch_id);
|
|
1142
1153
|
if !matches!(encountered_entry,
|
|
1143
1154
|
Some(ChangeInfo {created_command}) if *created_command)
|
|
1144
1155
|
{
|
|
@@ -1147,17 +1158,17 @@ impl WorkflowMachines {
|
|
|
1147
1158
|
self.replaying,
|
|
1148
1159
|
attrs.deprecated,
|
|
1149
1160
|
encountered_entry.is_some(),
|
|
1150
|
-
self.
|
|
1161
|
+
self.encountered_patch_markers.keys().map(|s| s.as_str()),
|
|
1151
1162
|
self.observed_internal_flags.clone(),
|
|
1152
1163
|
)?;
|
|
1153
1164
|
let mkey =
|
|
1154
1165
|
self.add_cmd_to_wf_task(patch_machine, CommandIdKind::NeverResolves);
|
|
1155
1166
|
self.process_machine_responses(mkey, other_cmds)?;
|
|
1156
1167
|
|
|
1157
|
-
if let Some(ci) = self.
|
|
1168
|
+
if let Some(ci) = self.encountered_patch_markers.get_mut(&attrs.patch_id) {
|
|
1158
1169
|
ci.created_command = true;
|
|
1159
1170
|
} else {
|
|
1160
|
-
self.
|
|
1171
|
+
self.encountered_patch_markers.insert(
|
|
1161
1172
|
attrs.patch_id,
|
|
1162
1173
|
ChangeInfo {
|
|
1163
1174
|
created_command: true,
|
|
@@ -1360,45 +1371,62 @@ enum EventHandlingOutcome {
|
|
|
1360
1371
|
|
|
1361
1372
|
/// Special handling for patch markers, when handling command events as in
|
|
1362
1373
|
/// [WorkflowMachines::handle_command_event]
|
|
1363
|
-
fn
|
|
1374
|
+
fn patch_marker_handling(
|
|
1364
1375
|
event: &HistoryEvent,
|
|
1365
1376
|
mach: &Machines,
|
|
1366
1377
|
next_event: Option<&HistoryEvent>,
|
|
1367
1378
|
) -> Result<EventHandlingOutcome> {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1379
|
+
let patch_machine = match mach {
|
|
1380
|
+
Machines::PatchMachine(pm) => Some(pm),
|
|
1381
|
+
_ => None,
|
|
1382
|
+
};
|
|
1383
|
+
let patch_details = event.get_patch_marker_details();
|
|
1384
|
+
fn skip_one_or_two_events(next_event: Option<&HistoryEvent>) -> Result<EventHandlingOutcome> {
|
|
1385
|
+
// Also ignore the subsequent upsert event if present
|
|
1386
|
+
let mut skip_next_event = false;
|
|
1387
|
+
if let Some(Attributes::UpsertWorkflowSearchAttributesEventAttributes(atts)) =
|
|
1388
|
+
next_event.and_then(|ne| ne.attributes.as_ref())
|
|
1389
|
+
{
|
|
1390
|
+
if let Some(ref sa) = atts.search_attributes {
|
|
1391
|
+
skip_next_event = sa.indexed_fields.contains_key(VERSION_SEARCH_ATTR_KEY);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
Ok(EventHandlingOutcome::SkipEvent { skip_next_event })
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if let Some((patch_name, deprecated)) = patch_details {
|
|
1399
|
+
if let Some(pm) = patch_machine {
|
|
1400
|
+
// If the next machine *is* a patch machine, but this marker is deprecated, it may
|
|
1401
|
+
// either apply to this machine (the `deprecate_patch` call is still in workflow code) -
|
|
1402
|
+
// or it could be another `patched` or `deprecate_patch` call for a *different* patch,
|
|
1403
|
+
// which we should also permit. In the latter case, we should skip this event.
|
|
1404
|
+
if !pm.matches_patch(&patch_name) && deprecated {
|
|
1405
|
+
skip_one_or_two_events(next_event)
|
|
1406
|
+
} else {
|
|
1407
|
+
Ok(EventHandlingOutcome::Normal)
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
// Version markers can be skipped in the event they are deprecated
|
|
1371
1411
|
// Is deprecated. We can simply ignore this event, as deprecated change
|
|
1372
1412
|
// markers are allowed without matching changed calls.
|
|
1373
1413
|
if deprecated {
|
|
1374
|
-
debug!("Deprecated patch marker tried against
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if let Some(ref sa) = atts.search_attributes {
|
|
1382
|
-
skip_next_event = sa.indexed_fields.contains_key(VERSION_SEARCH_ATTR_KEY);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
return Ok(EventHandlingOutcome::SkipEvent { skip_next_event });
|
|
1414
|
+
debug!("Deprecated patch marker tried against non-patch machine, skipping.");
|
|
1415
|
+
skip_one_or_two_events(next_event)
|
|
1416
|
+
} else {
|
|
1417
|
+
Err(WFMachinesError::Nondeterminism(format!(
|
|
1418
|
+
"Non-deprecated patch marker encountered for change {patch_name}, but there is \
|
|
1419
|
+
no corresponding change command!"
|
|
1420
|
+
)))
|
|
1387
1421
|
}
|
|
1388
|
-
return Err(WFMachinesError::Nondeterminism(format!(
|
|
1389
|
-
"Non-deprecated patch marker encountered for change {patch_name}, \
|
|
1390
|
-
but there is no corresponding change command!"
|
|
1391
|
-
)));
|
|
1392
|
-
}
|
|
1393
|
-
// Patch machines themselves may also not *have* matching markers, where non-deprecated
|
|
1394
|
-
// calls take the old path, and deprecated calls assume history is produced by a new-code
|
|
1395
|
-
// worker.
|
|
1396
|
-
if matches!(mach, Machines::PatchMachine(_)) {
|
|
1397
|
-
debug!("Skipping non-matching event against patch machine");
|
|
1398
|
-
return Ok(EventHandlingOutcome::SkipCommand);
|
|
1399
1422
|
}
|
|
1423
|
+
} else if patch_machine.is_some() {
|
|
1424
|
+
debug!("Skipping non-matching event against patch machine");
|
|
1425
|
+
Ok(EventHandlingOutcome::SkipCommand)
|
|
1426
|
+
} else {
|
|
1427
|
+
// Not a patch machine or a patch event
|
|
1428
|
+
Ok(EventHandlingOutcome::Normal)
|
|
1400
1429
|
}
|
|
1401
|
-
Ok(EventHandlingOutcome::Normal)
|
|
1402
1430
|
}
|
|
1403
1431
|
|
|
1404
1432
|
#[derive(derive_more::From)]
|
|
@@ -249,8 +249,8 @@ impl WfContext {
|
|
|
249
249
|
|
|
250
250
|
/// Record that this workflow history was created with the provided patch, and it is being
|
|
251
251
|
/// phased out.
|
|
252
|
-
pub fn deprecate_patch(&self, patch_id: &str) {
|
|
253
|
-
self.patch_impl(patch_id, true)
|
|
252
|
+
pub fn deprecate_patch(&self, patch_id: &str) -> bool {
|
|
253
|
+
self.patch_impl(patch_id, true)
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
fn patch_impl(&self, patch_id: &str, deprecated: bool) -> bool {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
use std::{
|
|
2
|
-
sync::
|
|
2
|
+
sync::{
|
|
3
|
+
atomic::{AtomicBool, Ordering},
|
|
4
|
+
Arc,
|
|
5
|
+
},
|
|
3
6
|
time::Duration,
|
|
4
7
|
};
|
|
5
8
|
|
|
@@ -117,3 +120,34 @@ async fn patched_on_second_workflow_task_is_deterministic() {
|
|
|
117
120
|
starter.start_with_worker(wf_name, &mut worker).await;
|
|
118
121
|
worker.run_until_done().await.unwrap();
|
|
119
122
|
}
|
|
123
|
+
|
|
124
|
+
#[tokio::test]
|
|
125
|
+
async fn can_remove_deprecated_patch_near_other_patch() {
|
|
126
|
+
let wf_name = "can_add_change_markers";
|
|
127
|
+
let mut starter = CoreWfStarter::new(wf_name);
|
|
128
|
+
starter.no_remote_activities();
|
|
129
|
+
let mut worker = starter.worker().await;
|
|
130
|
+
let did_die = Arc::new(AtomicBool::new(false));
|
|
131
|
+
worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| {
|
|
132
|
+
let did_die = did_die.clone();
|
|
133
|
+
async move {
|
|
134
|
+
ctx.timer(Duration::from_millis(200)).await;
|
|
135
|
+
if !did_die.load(Ordering::Acquire) {
|
|
136
|
+
assert!(ctx.deprecate_patch("getting-deprecated"));
|
|
137
|
+
assert!(ctx.patched("staying"));
|
|
138
|
+
} else {
|
|
139
|
+
assert!(ctx.patched("staying"));
|
|
140
|
+
}
|
|
141
|
+
ctx.timer(Duration::from_millis(200)).await;
|
|
142
|
+
|
|
143
|
+
if !did_die.load(Ordering::Acquire) {
|
|
144
|
+
did_die.store(true, Ordering::Release);
|
|
145
|
+
ctx.force_task_fail(anyhow::anyhow!("i'm ded"));
|
|
146
|
+
}
|
|
147
|
+
Ok(().into())
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
starter.start_with_worker(wf_name, &mut worker).await;
|
|
152
|
+
worker.run_until_done().await.unwrap();
|
|
153
|
+
}
|