@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/core-bridge",
3
- "version": "1.8.2",
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.2",
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": "d85bf54da757741b438f8d39a0e7265b80d4f0d6"
56
+ "gitHead": "7e65cf816b1deef72973dc64ccbf2c93916a3eb1"
57
57
  }
@@ -1,8 +1,21 @@
1
- use crate::{test_help::canned_histories, worker::ManagedWFFunc};
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 temporal_sdk_core_protos::temporal::api::enums::v1::CommandType;
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 activity command '{}'",
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 activity command '{}'",
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
- encountered_change_markers: HashMap<String, ChangeInfo>,
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
- encountered_change_markers: Default::default(),
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 the last command-event which was produced
508
- // as a result of the last completed WFT. Thus, replay would be false for things like
509
- // signals which were received and after the last completion, and thus generated the
510
- // current WFT being handled.
511
- if self.replaying && has_final_event && saw_completed && !event.is_command_event() {
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.encountered_change_markers.insert(
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 change_marker_handling(event, mach, next_event)? {
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.encountered_change_markers.get(&attrs.patch_id);
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.encountered_change_markers.keys().map(|s| s.as_str()),
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.encountered_change_markers.get_mut(&attrs.patch_id) {
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.encountered_change_markers.insert(
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 change_marker_handling(
1374
+ fn patch_marker_handling(
1364
1375
  event: &HistoryEvent,
1365
1376
  mach: &Machines,
1366
1377
  next_event: Option<&HistoryEvent>,
1367
1378
  ) -> Result<EventHandlingOutcome> {
1368
- if !mach.matches_event(event) {
1369
- // Version markers can be skipped in the event they are deprecated
1370
- if let Some((patch_name, deprecated)) = event.get_patch_marker_details() {
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 wrong machine, skipping.");
1375
-
1376
- // Also ignore the subsequent upsert event if present
1377
- let mut skip_next_event = false;
1378
- if let Some(Attributes::UpsertWorkflowSearchAttributesEventAttributes(atts)) =
1379
- next_event.and_then(|ne| ne.attributes.as_ref())
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::atomic::{AtomicBool, Ordering},
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
+ }