From 3b974d13e6980910e698011a5e2b1005e906c628 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 13:55:38 -0800 Subject: [PATCH 01/59] Pretty far with impl, but just ran into big generic wall. --- src/machines/workflow_machines.rs | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/machines/workflow_machines.rs diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs new file mode 100644 index 000000000..d2dd29e2e --- /dev/null +++ b/src/machines/workflow_machines.rs @@ -0,0 +1,69 @@ +use crate::machines::TemporalStateMachine; +use crate::protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}; +use std::collections::HashMap; + +struct WorkflowMachines { + /// The event id of the last event in the history which is expected to be startedEventId unless + /// it is replay from a JSON file. + workflow_task_started_event_id: u64, + /// EventId of the last handled WorkflowTaskStartedEvent + current_started_event_id: u64, + /// The event id of the started event of the last successfully executed workflow task + previous_started_event_id: u64, + /// True if the workflow is replaying from history + replaying: bool, + + machines_by_id: HashMap>, +} + +impl WorkflowMachines { + /// Handle a single event from the workflow history. `has_next_event` should be false if `event` + /// is the last event in the history. + /// + /// TODO: Describe what actually happens in here + fn handle_event(&mut self, event: HistoryEvent, has_next_event: bool) { + if event.is_command_event() { + self.handle_command_event(event); + return; + } + + if self.replaying + && self.current_started_event_id >= self.previous_started_event_id + && event.event_type != EventType::WorkflowTaskCompleted as i32 + { + // Replay is finished + self.replaying = false; + } + + if let Some(initial_command_event_id) = event.get_initial_command_event_id() { + // EntityStateMachine c = stateMachines.get(initialCommandEventId); + // c.handle_event(event, hasNextEvent); + // if (c.is_final_state()) { + // stateMachines.remove(initialCommandEventId); + // } + unimplemented!() + } else { + self.handle_non_stateful_event(event, has_next_event) + } + } + + fn handle_command_event(&self, event: HistoryEvent) { + unimplemented!() + } + + fn handle_non_stateful_event(&self, event: HistoryEvent, has_next_event: bool) { + unimplemented!() + } + + /// Given an event id (possibly zero) of the last successfully executed workflow task and an + /// id of the last event, sets the ids internally and appropriately sets the replaying flag. + fn set_started_ids( + &mut self, + previous_started_event_id: u64, + workflow_task_started_event_id: u64, + ) { + self.previous_started_event_id = previous_started_event_id; + self.workflow_task_started_event_id = workflow_task_started_event_id; + self.replaying = previous_started_event_id > 0; + } +} From 0ee244b0adaabc7789b84bd04969fa4545fa2fce Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 16:35:57 -0800 Subject: [PATCH 02/59] Test helper translation mostly done. Need to dedupe. --- Cargo.toml | 2 + README.md | 9 ++ fsm/state_machine_procmacro/src/lib.rs | 29 ++++ fsm/state_machine_trait/src/lib.rs | 5 + src/lib.rs | 3 + src/machines/mod.rs | 47 ++++++- src/machines/test_help.rs | 187 ++++++++++++++++++++++--- src/machines/timer_state_machine.rs | 5 +- src/machines/workflow_machines.rs | 67 +++++---- src/protos/mod.rs | 91 ++++++++++++ 10 files changed, 399 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4835310f5..fddb25f7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2018" [lib] [dependencies] +anyhow = "1.0" async-trait = "0.1" derive_more = "0.99" +log = "0.4" prost = "0.6" prost-types = "0.6" thiserror = "1.0" diff --git a/README.md b/README.md index fff1f3818..cc352b7ba 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,12 @@ To format all code run: We are using [clippy](https://github.com/rust-lang/rust-clippy) for linting. You can run it using: `cargo clippy --all -- -D warnings` + +## Style Guidelines + +### Error handling +Any error which is returned from a public interface should be well-typed, and we use +[thiserror](https://github.com/dtolnay/thiserror) for that purpose. + +Errors returned from things only used in testing are free to use +[anyhow](https://github.com/dtolnay/anyhow) for less verbosity. diff --git a/fsm/state_machine_procmacro/src/lib.rs b/fsm/state_machine_procmacro/src/lib.rs index 7dc3284b8..1081015d5 100644 --- a/fsm/state_machine_procmacro/src/lib.rs +++ b/fsm/state_machine_procmacro/src/lib.rs @@ -183,6 +183,13 @@ struct StateMachineDefinition { transitions: HashSet, } +impl StateMachineDefinition { + fn is_final_state(&self, state: &Ident) -> bool { + // If no transitions go from this state, it's a final state. + self.transitions.iter().find(|t| t.from == *state).is_none() + } +} + impl Parse for StateMachineDefinition { // TODO: Pub keyword fn parse(input: ParseStream) -> Result { @@ -347,6 +354,23 @@ impl StateMachineDefinition { #(#state_variants),* } }; + let state_is_final_match_arms = states.iter().map(|s| { + let val = if self.is_final_state(s) { + quote! { true } + } else { + quote! { false } + }; + quote! { #state_enum_name::#s(_) => #val } + }); + let states_enum_impl = quote! { + impl #state_enum_name { + fn is_final(&self) -> bool { + match self { + #(#state_is_final_match_arms),* + } + } + } + }; // Build the events enum let events: HashSet = self.transitions.iter().map(|t| t.event.clone()).collect(); @@ -469,6 +493,10 @@ impl StateMachineDefinition { &self.shared_state } + fn on_final_state(&self) -> bool { + self.state.is_final() + } + fn from_parts(shared: Self::SharedState, state: Self::State) -> Self { Self { shared_state: shared, state } } @@ -484,6 +512,7 @@ impl StateMachineDefinition { #transition_type_alias #machine_struct #states_enum + #states_enum_impl #events_enum #trait_impl }; diff --git a/fsm/state_machine_trait/src/lib.rs b/fsm/state_machine_trait/src/lib.rs index a205ce3d1..77760c2d6 100644 --- a/fsm/state_machine_trait/src/lib.rs +++ b/fsm/state_machine_trait/src/lib.rs @@ -56,11 +56,16 @@ pub trait StateMachine: Sized { } } + /// Returns the current state of the machine fn state(&self) -> &Self::State; fn set_state(&mut self, new_state: Self::State); + /// Returns the current shared state of the machine fn shared_state(&self) -> &Self::SharedState; + /// Returns true if the machine's current state is a final one + fn on_final_state(&self) -> bool; + /// Given the shared data and new state, create a new instance. fn from_parts(shared: Self::SharedState, state: Self::State) -> Self; } diff --git a/src/lib.rs b/src/lib.rs index 829db137a..bb17a4f7b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate log; + mod machines; pub mod protos; diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 18392bee1..b1ca1fff9 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -1,4 +1,4 @@ -use crate::protos::temporal::api::command::v1::Command; +mod workflow_machines; #[allow(unused)] mod activity_state_machine; @@ -34,6 +34,49 @@ mod workflow_task_state_machine; #[cfg(test)] mod test_help; +use crate::protos::temporal::api::{ + command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, +}; +use rustfsm::StateMachine; +use workflow_machines::WorkflowMachines; + +/// Status returned by [EntityStateMachine::handle_event] +enum HandleEventStatus { + // TODO: Feels like we can put more information in these? + /// Event handled successfully + Ok, + /// The event is inapplicable to the current state + NonMatchingEvent, +} + +/// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. +/// +/// Formerly known as `EntityStateMachine` in Java. +trait TemporalStateMachine: CheckStateMachineInFinal { + fn handle_command(&self, command_type: CommandType); + fn handle_event(&self, event: &HistoryEvent, has_next_event: bool) -> HandleEventStatus; + + // TODO: This is a weird one that only applies to version state machine. Introduce only if + // needed. Ideally handle differently. + // fn handle_workflow_task_started(); +} + +/// Exists purely to allow generic implementation of `is_final_state` for all [StateMachine] +/// implementors +trait CheckStateMachineInFinal { + /// Returns true if the state machine is in a final state + fn is_final_state(&self) -> bool; +} + +impl CheckStateMachineInFinal for SM +where + SM: StateMachine, +{ + fn is_final_state(&self) -> bool { + self.on_final_state() + } +} + /// A command which can be cancelled #[derive(Debug, Clone)] pub struct CancellableCommand { @@ -42,7 +85,7 @@ pub struct CancellableCommand { } impl CancellableCommand { - pub(crate) fn cancel(&mut self) { + pub(super) fn cancel(&mut self) { self.command = None; } } diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index f3970db65..9973dcc71 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -1,14 +1,20 @@ -use crate::protos::temporal::api::{ - enums::v1::EventType, - history::v1::{ - history_event::Attributes, HistoryEvent, WorkflowTaskCompletedEventAttributes, - WorkflowTaskStartedEventAttributes, +use crate::{ + machines::workflow_machines::WorkflowMachines, + protos::temporal::api::{ + enums::v1::EventType, + history::v1::{ + history_event::Attributes, HistoryEvent, WorkflowTaskCompletedEventAttributes, + WorkflowTaskStartedEventAttributes, + }, }, }; +use anyhow::bail; use std::time::SystemTime; +type Result = std::result::Result; + #[derive(Default, Debug)] -pub(crate) struct TestHistoryBuilder { +pub(super) struct TestHistoryBuilder { events: Vec, /// Is incremented every time a new event is added, and that *new* value is used as that event's /// id @@ -20,17 +26,17 @@ pub(crate) struct TestHistoryBuilder { impl TestHistoryBuilder { /// Add an event by type with attributes. Bundles both into a [HistoryEvent] with an id that is /// incremented on each call to add. - pub(crate) fn add(&mut self, event_type: EventType, attribs: Attributes) { + pub(super) fn add(&mut self, event_type: EventType, attribs: Attributes) { self.build_and_push_event(event_type, Some(attribs)); } /// Adds an event to the history by type, without attributes - pub(crate) fn add_by_type(&mut self, event_type: EventType) { + pub(super) fn add_by_type(&mut self, event_type: EventType) { self.build_and_push_event(event_type.clone(), None); } /// Adds an event, returning the ID that was assigned to it - pub(crate) fn add_get_event_id( + pub(super) fn add_get_event_id( &mut self, event_type: EventType, attrs: Option, @@ -45,24 +51,24 @@ impl TestHistoryBuilder { /// EVENT_TYPE_WORKFLOW_TASK_STARTED /// EVENT_TYPE_WORKFLOW_TASK_COMPLETED /// ``` - pub(crate) fn add_workflow_task(&mut self) { + pub(super) fn add_workflow_task(&mut self) { self.add_workflow_task_scheduled_and_started(); self.add_workflow_task_completed(); } - pub(crate) fn add_workflow_task_scheduled_and_started(&mut self) { + pub(super) fn add_workflow_task_scheduled_and_started(&mut self) { self.add_workflow_task_scheduled(); self.add_workflow_task_started(); } - pub(crate) fn add_workflow_task_scheduled(&mut self) { + pub(super) fn add_workflow_task_scheduled(&mut self) { // WFStarted always immediately follows WFScheduled self.previous_started_event_id = self.workflow_task_scheduled_event_id + 1; self.workflow_task_scheduled_event_id = self.add_get_event_id(EventType::WorkflowTaskScheduled, None); } - pub(crate) fn add_workflow_task_started(&mut self) { + pub(super) fn add_workflow_task_started(&mut self) { let attrs = WorkflowTaskStartedEventAttributes { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() @@ -73,7 +79,7 @@ impl TestHistoryBuilder { ); } - pub(crate) fn add_workflow_task_completed(&mut self) { + pub(super) fn add_workflow_task_completed(&mut self) { let attrs = WorkflowTaskCompletedEventAttributes { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() @@ -87,10 +93,18 @@ impl TestHistoryBuilder { /// Counts the number of whole workflow tasks. Looks for WFTaskStarted followed by /// WFTaskCompleted, adding one to the count for every match. It will additionally count /// a WFTaskStarted at the end of the event list. - pub(crate) fn get_workflow_task_count(&self) -> usize { + /// + /// If `up_to_event_id` is provided, the count will be returned as soon as processing advances + /// past that id. + pub(super) fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { let mut last_wf_started_id = 0; let mut count = 0; for (i, event) in self.events.iter().enumerate() { + if let Some(upto) = up_to_event_id { + if event.event_id > upto { + return Ok(count); + } + } let at_last_item = i == self.events.len() - 1; let next_item_is_wftc = self .events @@ -106,12 +120,143 @@ impl TestHistoryBuilder { if at_last_item { // No more events if last_wf_started_id != event.event_id { - panic!("Last item in history wasn't WorkflowTaskStarted") + bail!("Last item in history wasn't WorkflowTaskStarted") } - return count; + return Ok(count); } } - count + Ok(count) + } + + /// Handle a workflow task using the provided [WorkflowMachines] + /// + /// # Panics + /// * Can panic if the passed in machines have been manipulated outside of this builder + pub(super) fn handle_workflow_task( + &self, + wf_machines: &mut WorkflowMachines, + to_task_index: usize, + ) -> Result<()> { + let (_, events) = self + .events + .split_at(wf_machines.get_last_started_event_id() as usize); + let mut history = events.iter().peekable(); + + let hist_info = self.get_history_info(to_task_index)?; + wf_machines.set_started_ids( + hist_info.previous_started_event_id, + hist_info.workflow_task_started_event_id, + ); + let mut started_id = hist_info.previous_started_event_id; + let mut count = if wf_machines.get_last_started_event_id() > 0 { + self.get_workflow_task_count(history.peek().map(|e| e.event_id - 1))? + } else { + 0 + }; + + while let Some(e) = history.next() { + let next_event = history.peek(); + if next_event.is_none() { + if e.is_final_wf_execution_event() { + return Ok(()); + } + if started_id != e.event_id { + // TODO: I think this message is a lie + bail!("The last event in the history isn't WF task started"); + } + unreachable!() + } + + if e.event_type == EventType::WorkflowTaskStarted as i32 { + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); + let next_is_failed_or_timeout = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskFailed as i32 + || ne.event_type == EventType::WorkflowTaskTimedOut as i32 + }); + + if next_event.is_none() || next_is_completed { + started_id = e.event_id; + count += 1; + if count == toTaskIndex || next_event.is_none() { + wf_machines.handleEvent(e, false); + return Ok(()); + } + } else if next_event.is_some() && !next_is_failed_or_timeout { + bail!( + "Invalid history! Event {:?} should be WF task completed, \ + failed, or timed out.", + &event + ); + } + } + + wf_machines.handle_event(e, next_event.is_some()); + } + + Ok(()) + } + + /// Iterates over the events in this builder to return a [HistoryInfo] + fn get_history_info(&self, to_task_index: usize) -> Result { + let mut lastest_wf_started_id = 0; + let mut previous_wf_started_id = 0; + let mut count = 0; + for (i, event) in self.events.iter().enumerate() { + let at_last_item = i == self.events.len() - 1; + + if event.event_type == EventType::WorkflowTaskStarted as i32 { + let next_item_is_wftc = self + .events + .get(i + 1) + .map(|e| e.event_type == EventType::WorkflowTaskCompleted as i32) + .unwrap_or(false); + let next_item_is_wf_tf_or_to = self + .events + .get(i + 1) + .map(|e| { + e.event_type == EventType::WorkflowTaskCompleted as i32 + || e.event_type == EventType::WorkflowExecutionTimedOut as i32 + }) + .unwrap_or(false); + + if at_last_item || next_item_is_wftc { + previous_wf_started_id = lastest_wf_started_id; + lastest_wf_started_id = event.event_id; + if lastest_wf_started_id == previous_wf_started_id { + bail!("Latest wf started id and previvous one are equal!") + } + count += 1; + if count == to_task_index || at_last_item { + return Ok(HistoryInfo::new( + previous_wf_started_id, + lastest_wf_started_id, + )); + } + } else if !at_last_item && !next_item_is_wf_tf_or_to { + bail!( + "Invalid history! Event {:?} should be WF task completed, \ + failed, or timed out.", + &event + ); + } + } + + if at_last_item { + if event.is_final_wf_execution_event() { + return Ok(HistoryInfo::new( + previous_wf_started_id, + lastest_wf_started_id, + )); + } + // No more events + if lastest_wf_started_id != event.event_id { + bail!("Last item in history wasn't WorkflowTaskStarted") + } + } + } + unreachable!() } fn build_and_push_event(&mut self, event_type: EventType, attribs: Option) { @@ -126,3 +271,9 @@ impl TestHistoryBuilder { self.events.push(evt); } } + +#[derive(Clone, Debug, derive_more::Constructor, Eq, PartialEq, Hash)] +pub(super) struct HistoryInfo { + pub(super) previous_started_event_id: i64, + pub(super) workflow_task_started_event_id: i64, +} diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 5beeb6be9..73d087026 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -20,7 +20,8 @@ fsm! { shared_state SharedState; CancelTimerCommandCreated --(Cancel) --> CancelTimerCommandCreated; - CancelTimerCommandCreated --(CommandCancelTimer, shared on_command_cancel_timer) --> CancelTimerCommandSent; + CancelTimerCommandCreated + --(CommandCancelTimer, shared on_command_cancel_timer) --> CancelTimerCommandSent; CancelTimerCommandSent --(TimerCanceled) --> Canceled; @@ -187,6 +188,6 @@ mod test { }), ); t.add_workflow_task_scheduled_and_started(); - assert_eq!(2, t.get_workflow_task_count()); + assert_eq!(2, t.get_workflow_task_count(None).unwrap()); } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index d2dd29e2e..050884fdc 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,27 +1,36 @@ -use crate::machines::TemporalStateMachine; -use crate::protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}; -use std::collections::HashMap; +use crate::{ + machines::TemporalStateMachine, + protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}, +}; +use std::collections::{hash_map::Entry, HashMap}; -struct WorkflowMachines { +pub(super) struct WorkflowMachines { /// The event id of the last event in the history which is expected to be startedEventId unless /// it is replay from a JSON file. - workflow_task_started_event_id: u64, - /// EventId of the last handled WorkflowTaskStartedEvent - current_started_event_id: u64, + workflow_task_started_event_id: i64, + /// EventId of the last handled WorkflowTaskStarted event + current_started_event_id: i64, /// The event id of the started event of the last successfully executed workflow task - previous_started_event_id: u64, + previous_started_event_id: i64, /// True if the workflow is replaying from history replaying: bool, - machines_by_id: HashMap>, + /// A mapping for accessing all the machines, where the key is the id of the initiating event + /// for that machine. + machines_by_id: HashMap>, } impl WorkflowMachines { + /// Returns the id of the last seen WorkflowTaskStarted event + pub(super) fn get_last_started_event_id(&self) -> i64 { + self.current_started_event_id + } + /// Handle a single event from the workflow history. `has_next_event` should be false if `event` /// is the last event in the history. /// /// TODO: Describe what actually happens in here - fn handle_event(&mut self, event: HistoryEvent, has_next_event: bool) { + pub(super) fn handle_event(&mut self, event: &HistoryEvent, has_next_event: bool) { if event.is_command_event() { self.handle_command_event(event); return; @@ -35,32 +44,42 @@ impl WorkflowMachines { self.replaying = false; } - if let Some(initial_command_event_id) = event.get_initial_command_event_id() { - // EntityStateMachine c = stateMachines.get(initialCommandEventId); - // c.handle_event(event, hasNextEvent); - // if (c.is_final_state()) { - // stateMachines.remove(initialCommandEventId); - // } - unimplemented!() - } else { - self.handle_non_stateful_event(event, has_next_event) + match event + .get_initial_command_event_id() + .map(|id| self.machines_by_id.entry(id)) + { + Some(Entry::Occupied(sme)) => { + let sm = sme.get(); + sm.handle_event(event, has_next_event); + if sm.is_final_state() { + sme.remove(); + } + } + Some(Entry::Vacant(_)) => { + error!( + "During event handling, this event had an initial command ID but \ + we could not find a matching state machine! Event: {:?}", + event + ); + } + _ => self.handle_non_stateful_event(event, has_next_event), } } - fn handle_command_event(&self, event: HistoryEvent) { + fn handle_command_event(&self, event: &HistoryEvent) { unimplemented!() } - fn handle_non_stateful_event(&self, event: HistoryEvent, has_next_event: bool) { + fn handle_non_stateful_event(&self, event: &HistoryEvent, has_next_event: bool) { unimplemented!() } /// Given an event id (possibly zero) of the last successfully executed workflow task and an /// id of the last event, sets the ids internally and appropriately sets the replaying flag. - fn set_started_ids( + pub(super) fn set_started_ids( &mut self, - previous_started_event_id: u64, - workflow_task_started_event_id: u64, + previous_started_event_id: i64, + workflow_task_started_event_id: i64, ) { self.previous_started_event_id = previous_started_event_id; self.workflow_task_started_event_id = workflow_task_started_event_id; diff --git a/src/protos/mod.rs b/src/protos/mod.rs index d52a71031..17ef05f81 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -36,39 +36,130 @@ pub mod temporal { } pub mod history { pub mod v1 { + use crate::protos::temporal::api::{ + enums::v1::EventType, history::v1::history_event::Attributes, + }; include!("temporal.api.history.v1.rs"); + + impl HistoryEvent { + /// Returns true if this is an event created to mirror a command + pub fn is_command_event(&self) -> bool { + if let Some(et) = EventType::from_i32(self.event_type) { + match et { + EventType::ActivityTaskScheduled + | EventType::StartChildWorkflowExecutionInitiated + | EventType::TimerStarted + | EventType::WorkflowExecutionCompleted + | EventType::WorkflowExecutionFailed + | EventType::WorkflowExecutionCanceled + | EventType::WorkflowExecutionContinuedAsNew + | EventType::ActivityTaskCancelRequested + | EventType::TimerCanceled + | EventType::RequestCancelExternalWorkflowExecutionInitiated + | EventType::MarkerRecorded + | EventType::SignalExternalWorkflowExecutionInitiated + | EventType::UpsertWorkflowSearchAttributes => true, + _ => false, + } + } else { + debug!( + "Could not determine type of event with enum index {}", + self.event_type + ); + false + } + } + + /// Returns the command's initiating event id, if present. This is the id of the + /// event which "started" the command. Usually, the "scheduled" event for the + /// command. + pub fn get_initial_command_event_id(&self) -> Option { + self.attributes.as_ref().and_then(|a| { + // Fun! Not really any way to make this better w/o incompatibly changing + // protos. + match a { + Attributes::ActivityTaskStartedEventAttributes(a) => + Some(a.scheduled_event_id), + Attributes::ActivityTaskCompletedEventAttributes(a) => + Some(a.scheduled_event_id), + Attributes::ActivityTaskFailedEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::ActivityTaskTimedOutEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::ActivityTaskCancelRequestedEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::ActivityTaskCanceledEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::TimerFiredEventAttributes(a) => Some(a.started_event_id), + Attributes::TimerCanceledEventAttributes(a) => Some(a.started_event_id), + Attributes::RequestCancelExternalWorkflowExecutionFailedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ExternalWorkflowExecutionCancelRequestedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::StartChildWorkflowExecutionFailedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionStartedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionCompletedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionFailedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionCanceledEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionTimedOutEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ChildWorkflowExecutionTerminatedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::SignalExternalWorkflowExecutionFailedEventAttributes(a) => Some(a.initiated_event_id), + Attributes::ExternalWorkflowExecutionSignaledEventAttributes(a) => Some(a.initiated_event_id), + Attributes::WorkflowTaskStartedEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::WorkflowTaskCompletedEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::WorkflowTaskTimedOutEventAttributes(a) => Some(a.scheduled_event_id), + Attributes::WorkflowTaskFailedEventAttributes(a) => Some(a.scheduled_event_id), + _ => None + } + }) + } + + /// Returns true if the event is one which would end a workflow + pub fn is_final_wf_execution_event(&self) -> bool { + match EventType::from_i32(self.event_type) { + Some(EventType::WorkflowExecutionCompleted) => true, + Some(EventType::WorkflowExecutionCanceled) => true, + Some(EventType::WorkflowExecutionFailed) => true, + Some(EventType::WorkflowExecutionTimedOut) => true, + Some(EventType::WorkflowExecutionContinuedAsNew) => true, + Some(EventType::WorkflowExecutionTerminated) => true, + _ => false, + } + } + } } } + pub mod namespace { pub mod v1 { include!("temporal.api.namespace.v1.rs"); } } + pub mod query { pub mod v1 { include!("temporal.api.query.v1.rs"); } } + pub mod replication { pub mod v1 { include!("temporal.api.replication.v1.rs"); } } + pub mod taskqueue { pub mod v1 { include!("temporal.api.taskqueue.v1.rs"); } } + pub mod version { pub mod v1 { include!("temporal.api.version.v1.rs"); } } + pub mod workflow { pub mod v1 { include!("temporal.api.workflow.v1.rs"); } } + pub mod workflowservice { pub mod v1 { include!("temporal.api.workflowservice.v1.rs"); From dd5f544dbe1a5be52d369ef6f2e39c98f471a1a1 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 16:52:51 -0800 Subject: [PATCH 03/59] Cleanup to make things cleaner with peeking. Dedupe not worth it. --- src/machines/test_help.rs | 71 ++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 9973dcc71..34ec4a328 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -99,26 +99,25 @@ impl TestHistoryBuilder { pub(super) fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { let mut last_wf_started_id = 0; let mut count = 0; - for (i, event) in self.events.iter().enumerate() { + let mut history = self.events.iter().peekable(); + while let Some(event) = history.next() { + let next_event = history.peek(); if let Some(upto) = up_to_event_id { if event.event_id > upto { return Ok(count); } } - let at_last_item = i == self.events.len() - 1; - let next_item_is_wftc = self - .events - .get(i + 1) - .map(|e| e.event_type == EventType::WorkflowTaskCompleted as i32) - .unwrap_or(false); + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); if event.event_type == EventType::WorkflowTaskStarted as i32 - && (at_last_item || next_item_is_wftc) + && (next_event.is_none() || next_is_completed) { last_wf_started_id = event.event_id; count += 1; } - if at_last_item { - // No more events + + if next_event.is_none() { if last_wf_started_id != event.event_id { bail!("Last item in history wasn't WorkflowTaskStarted") } @@ -154,20 +153,20 @@ impl TestHistoryBuilder { 0 }; - while let Some(e) = history.next() { + while let Some(event) = history.next() { let next_event = history.peek(); if next_event.is_none() { - if e.is_final_wf_execution_event() { + if event.is_final_wf_execution_event() { return Ok(()); } - if started_id != e.event_id { + if started_id != event.event_id { // TODO: I think this message is a lie bail!("The last event in the history isn't WF task started"); } unreachable!() } - if e.event_type == EventType::WorkflowTaskStarted as i32 { + if event.event_type == EventType::WorkflowTaskStarted as i32 { let next_is_completed = next_event.map_or(false, |ne| { ne.event_type == EventType::WorkflowTaskCompleted as i32 }); @@ -177,10 +176,10 @@ impl TestHistoryBuilder { }); if next_event.is_none() || next_is_completed { - started_id = e.event_id; + started_id = event.event_id; count += 1; - if count == toTaskIndex || next_event.is_none() { - wf_machines.handleEvent(e, false); + if count == to_task_index || next_event.is_none() { + wf_machines.handle_event(event, false); return Ok(()); } } else if next_event.is_some() && !next_is_failed_or_timeout { @@ -192,7 +191,7 @@ impl TestHistoryBuilder { } } - wf_machines.handle_event(e, next_event.is_some()); + wf_machines.handle_event(event, next_event.is_some()); } Ok(()) @@ -203,38 +202,34 @@ impl TestHistoryBuilder { let mut lastest_wf_started_id = 0; let mut previous_wf_started_id = 0; let mut count = 0; - for (i, event) in self.events.iter().enumerate() { - let at_last_item = i == self.events.len() - 1; + let mut history = self.events.iter().peekable(); + + while let Some(event) = history.next() { + let next_event = history.peek(); if event.event_type == EventType::WorkflowTaskStarted as i32 { - let next_item_is_wftc = self - .events - .get(i + 1) - .map(|e| e.event_type == EventType::WorkflowTaskCompleted as i32) - .unwrap_or(false); - let next_item_is_wf_tf_or_to = self - .events - .get(i + 1) - .map(|e| { - e.event_type == EventType::WorkflowTaskCompleted as i32 - || e.event_type == EventType::WorkflowExecutionTimedOut as i32 - }) - .unwrap_or(false); + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); + let next_is_failed_or_timeout = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskFailed as i32 + || ne.event_type == EventType::WorkflowTaskTimedOut as i32 + }); - if at_last_item || next_item_is_wftc { + if next_event.is_none() || next_is_completed { previous_wf_started_id = lastest_wf_started_id; lastest_wf_started_id = event.event_id; if lastest_wf_started_id == previous_wf_started_id { - bail!("Latest wf started id and previvous one are equal!") + bail!("Latest wf started id and previous one are equal!") } count += 1; - if count == to_task_index || at_last_item { + if count == to_task_index || next_event.is_none() { return Ok(HistoryInfo::new( previous_wf_started_id, lastest_wf_started_id, )); } - } else if !at_last_item && !next_item_is_wf_tf_or_to { + } else if next_event.is_some() && !next_is_failed_or_timeout { bail!( "Invalid history! Event {:?} should be WF task completed, \ failed, or timed out.", @@ -243,7 +238,7 @@ impl TestHistoryBuilder { } } - if at_last_item { + if next_event.is_none() { if event.is_final_wf_execution_event() { return Ok(HistoryInfo::new( previous_wf_started_id, From bf60b3424bf2ad34025408e6393fc75848cf9b65 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 17:27:16 -0800 Subject: [PATCH 04/59] Need to implement non_stateful_event --- src/machines/mod.rs | 6 ++++-- src/machines/test_help.rs | 13 ++++++++++++- src/machines/timer_state_machine.rs | 5 +++++ src/machines/workflow_machines.rs | 22 ++++++++++++++++++---- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index b1ca1fff9..b6f84f843 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -38,7 +38,9 @@ use crate::protos::temporal::api::{ command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, }; use rustfsm::StateMachine; -use workflow_machines::WorkflowMachines; + +// TODO: May need to be our SDKWFCommand type +pub(crate) type MachineCommand = Command; /// Status returned by [EntityStateMachine::handle_event] enum HandleEventStatus { @@ -81,7 +83,7 @@ where #[derive(Debug, Clone)] pub struct CancellableCommand { /// The inner protobuf command, if None, command has been cancelled - command: Option, + command: Option, } impl CancellableCommand { diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 34ec4a328..84238d4d5 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -1,3 +1,4 @@ +use crate::machines::MachineCommand; use crate::{ machines::workflow_machines::WorkflowMachines, protos::temporal::api::{ @@ -127,6 +128,15 @@ impl TestHistoryBuilder { Ok(count) } + pub(super) fn handle_workflow_task_take_cmds( + &self, + wf_machines: &mut WorkflowMachines, + to_task_index: Option, + ) -> Result> { + self.handle_workflow_task(wf_machines, to_task_index)?; + Ok(wf_machines.take_commands()) + } + /// Handle a workflow task using the provided [WorkflowMachines] /// /// # Panics @@ -134,8 +144,9 @@ impl TestHistoryBuilder { pub(super) fn handle_workflow_task( &self, wf_machines: &mut WorkflowMachines, - to_task_index: usize, + to_task_index: Option, ) -> Result<()> { + let to_task_index = to_task_index.unwrap_or(usize::MAX); let (_, events) = self .events .split_at(wf_machines.get_last_started_event_id() as usize); diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 73d087026..f0d4ab27f 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -153,6 +153,7 @@ impl StartCommandRecorded { #[cfg(test)] mod test { + use crate::machines::workflow_machines::WorkflowMachines; use crate::{ machines::test_help::TestHistoryBuilder, protos::temporal::api::{ @@ -176,6 +177,8 @@ mod test { 8: EVENT_TYPE_WORKFLOW_TASK_STARTED */ let mut t = TestHistoryBuilder::default(); + let mut state_machines = WorkflowMachines::new(); + t.add_by_type(EventType::WorkflowExecutionStarted); t.add_workflow_task(); let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); @@ -189,5 +192,7 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); assert_eq!(2, t.get_workflow_task_count(None).unwrap()); + let commands = t.handle_workflow_task_take_cmds(&mut state_machines, Some(1)); + dbg!(commands); } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 050884fdc..520f6c7e0 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,9 +1,10 @@ use crate::{ - machines::TemporalStateMachine, + machines::{CancellableCommand, MachineCommand, TemporalStateMachine}, protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}, }; -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::{hash_map::Entry, HashMap, VecDeque}; +#[derive(Default)] pub(super) struct WorkflowMachines { /// The event id of the last event in the history which is expected to be startedEventId unless /// it is replay from a JSON file. @@ -18,9 +19,16 @@ pub(super) struct WorkflowMachines { /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. machines_by_id: HashMap>, + + /// Queued commands which have been produced by machines and await processing + commands: VecDeque, } impl WorkflowMachines { + pub(crate) fn new() -> Self { + Self::default() + } + /// Returns the id of the last seen WorkflowTaskStarted event pub(super) fn get_last_started_event_id(&self) -> i64 { self.current_started_event_id @@ -66,14 +74,20 @@ impl WorkflowMachines { } } - fn handle_command_event(&self, event: &HistoryEvent) { + fn handle_command_event(&self, _event: &HistoryEvent) { unimplemented!() } - fn handle_non_stateful_event(&self, event: &HistoryEvent, has_next_event: bool) { + fn handle_non_stateful_event(&self, _event: &HistoryEvent, _has_next_event: bool) { unimplemented!() } + /// Fetches commands ready for processing from the state machines, removing them from the + /// internal command queue. + pub(super) fn take_commands(&mut self) -> Vec { + self.commands.drain(0..).flat_map(|c| c.command).collect() + } + /// Given an event id (possibly zero) of the last successfully executed workflow task and an /// id of the last event, sets the ids internally and appropriately sets the replaying flag. pub(super) fn set_started_ids( From 495ba2b1cfcb406fe4855da0b7ec2ab71fa358a6 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 18:12:53 -0800 Subject: [PATCH 05/59] More progress. In the middle of implementing non stateful event handler. --- build.rs | 5 ++ src/machines/mod.rs | 22 ++++----- src/machines/test_help.rs | 47 +++++++++++------- src/machines/workflow_machines.rs | 80 ++++++++++++++++++++++++++++--- 4 files changed, 119 insertions(+), 35 deletions(-) diff --git a/build.rs b/build.rs index 750b9c8f5..a71be0df2 100644 --- a/build.rs +++ b/build.rs @@ -5,6 +5,11 @@ fn main() -> Result<(), Box> { .build_server(false) .build_client(false) .out_dir("src/protos") + // Make conversions easier for some types + .type_attribute( + "temporal.api.history.v1.HistoryEvent.attributes", + "#[derive(::derive_more::From)]", + ) .compile( &["protos/local/core_interface.proto"], &["protos/api_upstream", "protos/local"], diff --git a/src/machines/mod.rs b/src/machines/mod.rs index b6f84f843..a32d3b990 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -34,29 +34,27 @@ mod workflow_task_state_machine; #[cfg(test)] mod test_help; -use crate::protos::temporal::api::{ - command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, +use crate::{ + machines::workflow_machines::WFMachinesError, + protos::temporal::api::{ + command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, + }, }; use rustfsm::StateMachine; // TODO: May need to be our SDKWFCommand type pub(crate) type MachineCommand = Command; -/// Status returned by [EntityStateMachine::handle_event] -enum HandleEventStatus { - // TODO: Feels like we can put more information in these? - /// Event handled successfully - Ok, - /// The event is inapplicable to the current state - NonMatchingEvent, -} - /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. trait TemporalStateMachine: CheckStateMachineInFinal { fn handle_command(&self, command_type: CommandType); - fn handle_event(&self, event: &HistoryEvent, has_next_event: bool) -> HandleEventStatus; + fn handle_event( + &self, + event: &HistoryEvent, + has_next_event: bool, + ) -> Result<(), WFMachinesError>; // TODO: This is a weird one that only applies to version state machine. Introduce only if // needed. Ideally handle differently. diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 84238d4d5..83aeccdb9 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -1,4 +1,8 @@ use crate::machines::MachineCommand; +use crate::protos::temporal::api::history::v1::{ + TimerStartedEventAttributes, WorkflowExecutionStartedEventAttributes, + WorkflowTaskScheduledEventAttributes, +}; use crate::{ machines::workflow_machines::WorkflowMachines, protos::temporal::api::{ @@ -28,12 +32,14 @@ impl TestHistoryBuilder { /// Add an event by type with attributes. Bundles both into a [HistoryEvent] with an id that is /// incremented on each call to add. pub(super) fn add(&mut self, event_type: EventType, attribs: Attributes) { - self.build_and_push_event(event_type, Some(attribs)); + self.build_and_push_event(event_type, attribs); } - /// Adds an event to the history by type, without attributes + /// Adds an event to the history by type, with default attributes. pub(super) fn add_by_type(&mut self, event_type: EventType) { - self.build_and_push_event(event_type.clone(), None); + let attribs = + default_attribs(event_type).expect("Couldn't make default attributes in test builder"); + self.build_and_push_event(event_type.clone(), attribs); } /// Adds an event, returning the ID that was assigned to it @@ -42,7 +48,11 @@ impl TestHistoryBuilder { event_type: EventType, attrs: Option, ) -> i64 { - self.build_and_push_event(event_type, attrs); + if let Some(a) = attrs { + self.build_and_push_event(event_type, a); + } else { + self.add_by_type(event_type); + } self.current_event_id } @@ -74,10 +84,7 @@ impl TestHistoryBuilder { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() }; - self.build_and_push_event( - EventType::WorkflowTaskStarted, - Some(Attributes::WorkflowTaskStartedEventAttributes(attrs)), - ); + self.build_and_push_event(EventType::WorkflowTaskStarted, attrs.into()); } pub(super) fn add_workflow_task_completed(&mut self) { @@ -85,10 +92,7 @@ impl TestHistoryBuilder { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() }; - self.build_and_push_event( - EventType::WorkflowTaskCompleted, - Some(Attributes::WorkflowTaskCompletedEventAttributes(attrs)), - ); + self.build_and_push_event(EventType::WorkflowTaskCompleted, attrs.into()); } /// Counts the number of whole workflow tasks. Looks for WFTaskStarted followed by @@ -190,7 +194,7 @@ impl TestHistoryBuilder { started_id = event.event_id; count += 1; if count == to_task_index || next_event.is_none() { - wf_machines.handle_event(event, false); + wf_machines.handle_event(event, false)?; return Ok(()); } } else if next_event.is_some() && !next_is_failed_or_timeout { @@ -202,7 +206,7 @@ impl TestHistoryBuilder { } } - wf_machines.handle_event(event, next_event.is_some()); + wf_machines.handle_event(event, next_event.is_some())?; } Ok(()) @@ -265,19 +269,30 @@ impl TestHistoryBuilder { unreachable!() } - fn build_and_push_event(&mut self, event_type: EventType, attribs: Option) { + fn build_and_push_event(&mut self, event_type: EventType, attribs: Attributes) { self.current_event_id += 1; let evt = HistoryEvent { event_type: event_type as i32, event_id: self.current_event_id, event_time: Some(SystemTime::now().into()), - attributes: attribs, + attributes: Some(attribs), ..Default::default() }; self.events.push(evt); } } +fn default_attribs(et: EventType) -> Result { + Ok(match et { + EventType::WorkflowExecutionStarted => { + WorkflowExecutionStartedEventAttributes::default().into() + } + EventType::WorkflowTaskScheduled => WorkflowTaskScheduledEventAttributes::default().into(), + EventType::TimerStarted => TimerStartedEventAttributes::default().into(), + _ => bail!("Don't know how to construct default attrs for {:?}", et), + }) +} + #[derive(Clone, Debug, derive_more::Constructor, Eq, PartialEq, Hash)] pub(super) struct HistoryInfo { pub(super) previous_started_event_id: i64, diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 520f6c7e0..bbb620a6f 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,9 +1,12 @@ +use crate::protos::temporal::api::history::v1::history_event; use crate::{ machines::{CancellableCommand, MachineCommand, TemporalStateMachine}, protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}, }; use std::collections::{hash_map::Entry, HashMap, VecDeque}; +type Result = std::result::Result; + #[derive(Default)] pub(super) struct WorkflowMachines { /// The event id of the last event in the history which is expected to be startedEventId unless @@ -15,6 +18,8 @@ pub(super) struct WorkflowMachines { previous_started_event_id: i64, /// True if the workflow is replaying from history replaying: bool, + /// Identifies the current run and is used as a seed for faux-randomness. + current_run_id: String, /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. @@ -24,6 +29,15 @@ pub(super) struct WorkflowMachines { commands: VecDeque, } +#[derive(thiserror::Error, Debug)] +pub(crate) enum WFMachinesError { + // TODO: more context + #[error("Event {0:?} was not expected")] + UnexpectedEvent(HistoryEvent), + #[error("Event {0:?} was malformed: {1}")] + MalformedEvent(HistoryEvent, String), +} + impl WorkflowMachines { pub(crate) fn new() -> Self { Self::default() @@ -38,10 +52,14 @@ impl WorkflowMachines { /// is the last event in the history. /// /// TODO: Describe what actually happens in here - pub(super) fn handle_event(&mut self, event: &HistoryEvent, has_next_event: bool) { + pub(super) fn handle_event( + &mut self, + event: &HistoryEvent, + has_next_event: bool, + ) -> Result<()> { if event.is_command_event() { self.handle_command_event(event); - return; + return Ok(()); } if self.replaying @@ -58,7 +76,7 @@ impl WorkflowMachines { { Some(Entry::Occupied(sme)) => { let sm = sme.get(); - sm.handle_event(event, has_next_event); + sm.handle_event(event, has_next_event)?; if sm.is_final_state() { sme.remove(); } @@ -70,16 +88,64 @@ impl WorkflowMachines { event ); } - _ => self.handle_non_stateful_event(event, has_next_event), + _ => self.handle_non_stateful_event(event, has_next_event)?, } + + Ok(()) } - fn handle_command_event(&self, _event: &HistoryEvent) { + fn handle_command_event(&mut self, _event: &HistoryEvent) { unimplemented!() } - fn handle_non_stateful_event(&self, _event: &HistoryEvent, _has_next_event: bool) { - unimplemented!() + fn handle_non_stateful_event( + &mut self, + event: &HistoryEvent, + _has_next_event: bool, + ) -> Result<()> { + // switch (event.getEventType()) { + // case EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: + // this.currentRunId = + // event.getWorkflowExecutionStartedEventAttributes().getOriginalExecutionRunId(); + // callbacks.start(event); + // break; + // case EVENT_TYPE_WORKFLOW_TASK_SCHEDULED: + // WorkflowTaskStateMachine c = + // WorkflowTaskStateMachine.newInstance( + // workflowTaskStartedEventId, new WorkflowTaskCommandsListener()); + // c.handleEvent(event, hasNextEvent); + // stateMachines.put(event.getEventId(), c); + // break; + // case EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED: + // callbacks.signal(event); + // break; + // case EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED: + // callbacks.cancel(event); + // break; + // case UNRECOGNIZED: + // break; + // default: + // throw new IllegalArgumentException("Unexpected event:" + event); + // } + match EventType::from_i32(event.event_type) { + Some(EventType::WorkflowExecutionStarted) => { + if let Some(history_event::Attributes::WorkflowExecutionStartedEventAttributes( + attrs, + )) = &event.attributes + { + self.current_run_id = attrs.original_execution_run_id.clone(); + // TODO: callbacks.start(event) -- but without the callbacks ;) + } else { + return Err(WFMachinesError::MalformedEvent( + event.clone(), + "WorkflowExecutionStarted event did not have appropriate attribues" + .to_string(), + )); + } + } + _ => return Err(WFMachinesError::UnexpectedEvent(event.clone())), + } + Ok(()) } /// Fetches commands ready for processing from the state machines, removing them from the From bc0dd18bae3890fb73136cbd347dcab475c2b6a2 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 19 Jan 2021 23:51:16 -0800 Subject: [PATCH 06/59] Implement event dispatch pattern & add visibility to fsm! --- fsm/src/lib.rs | 2 +- fsm/state_machine_procmacro/src/lib.rs | 13 ++-- src/machines/activity_state_machine.rs | 66 +++++++++---------- src/machines/cancel_external_state_machine.rs | 24 +++---- src/machines/cancel_workflow_state_machine.rs | 16 ++--- src/machines/child_workflow_state_machine.rs | 48 +++++++------- .../complete_workflow_state_machine.rs | 16 ++--- .../continue_as_new_workflow_state_machine.rs | 18 ++--- src/machines/fail_workflow_state_machine.rs | 16 ++--- src/machines/local_activity_state_machine.rs | 38 +++++------ src/machines/mod.rs | 47 ++++++++++++- .../mutable_side_effect_state_machine.rs | 42 ++++++------ src/machines/side_effect_state_machine.rs | 26 ++++---- src/machines/signal_external_state_machine.rs | 30 +++++---- src/machines/test_help.rs | 12 ++-- src/machines/timer_state_machine.rs | 64 ++++++++++++------ .../upsert_search_attributes_state_machine.rs | 18 ++--- src/machines/version_state_machine.rs | 42 ++++++------ src/machines/workflow_machines.rs | 22 ++++--- src/machines/workflow_task_state_machine.rs | 30 ++++----- 20 files changed, 335 insertions(+), 255 deletions(-) diff --git a/fsm/src/lib.rs b/fsm/src/lib.rs index 9f1f43fa8..a41113ac5 100644 --- a/fsm/src/lib.rs +++ b/fsm/src/lib.rs @@ -1,2 +1,2 @@ pub use state_machine_procmacro::fsm; -pub use state_machine_trait::{StateMachine, TransitionResult}; +pub use state_machine_trait::{MachineError, StateMachine, TransitionResult}; diff --git a/fsm/state_machine_procmacro/src/lib.rs b/fsm/state_machine_procmacro/src/lib.rs index 1081015d5..72d967b2a 100644 --- a/fsm/state_machine_procmacro/src/lib.rs +++ b/fsm/state_machine_procmacro/src/lib.rs @@ -9,7 +9,7 @@ use syn::{ parse_macro_input, punctuated::Punctuated, spanned::Spanned, - Error, Fields, Ident, Token, Type, Variant, + Error, Fields, Ident, Token, Type, Variant, Visibility, }; /// Parses a DSL for defining finite state machines, and produces code implementing the @@ -176,6 +176,7 @@ mod kw { } struct StateMachineDefinition { + visibility: Visibility, name: Ident, shared_state_type: Option, command_type: Ident, @@ -193,7 +194,9 @@ impl StateMachineDefinition { impl Parse for StateMachineDefinition { // TODO: Pub keyword fn parse(input: ParseStream) -> Result { - // First parse the state machine name, command type, and error type + // Parse visibility if present + let visibility = input.parse()?; + // parse the state machine name, command type, and error type let (name, command_type, error_type, shared_state_type) = parse_machine_types(&input).map_err(|mut e| { e.combine(Error::new( e.span(), @@ -207,6 +210,7 @@ impl Parse for StateMachineDefinition { input.parse_terminated(Transition::parse)?; let transitions = transitions.into_iter().collect(); Ok(Self { + visibility, name, shared_state_type, transitions, @@ -323,6 +327,7 @@ impl Parse for Transition { impl StateMachineDefinition { fn codegen(&self) -> TokenStream { + let visibility = self.visibility.clone(); // First extract all of the states into a set, and build the enum's insides let states: HashSet<_> = self .transitions @@ -343,14 +348,14 @@ impl StateMachineDefinition { .unwrap_or_else(|| syn::parse_str("()").unwrap()); let machine_struct = quote! { #[derive(Clone)] - pub struct #name { + #visibility struct #name { state: #state_enum_name, shared_state: #shared_state_type } }; let states_enum = quote! { #[derive(::derive_more::From, Clone)] - pub enum #state_enum_name { + #visibility enum #state_enum_name { #(#state_variants),* } }; diff --git a/src/machines/activity_state_machine.rs b/src/machines/activity_state_machine.rs index 66ac7aabd..23efd857d 100644 --- a/src/machines/activity_state_machine.rs +++ b/src/machines/activity_state_machine.rs @@ -3,7 +3,7 @@ use rustfsm::{fsm, TransitionResult}; // Schedule / cancel are "explicit events" (imperative rather than past events?) fsm! { - name ActivityMachine; command ActivityCommand; error ActivityMachineError; + pub(super) name ActivityMachine; command ActivityCommand; error ActivityMachineError; Created --(Schedule, on_schedule)--> ScheduleCommandCreated; @@ -50,94 +50,94 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum ActivityMachineError {} +pub(super) enum ActivityMachineError {} -pub enum ActivityCommand {} +pub(super) enum ActivityCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> ActivityMachineTransition { + pub(super) fn on_schedule(self) -> ActivityMachineTransition { // would add command here ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct ScheduleCommandCreated {} +pub(super) struct ScheduleCommandCreated {} impl ScheduleCommandCreated { - pub fn on_activity_task_scheduled(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_scheduled(self) -> ActivityMachineTransition { // set initial command event id // this.initialCommandEventId = currentEvent.getEventId(); ActivityMachineTransition::default::() } - pub fn on_canceled(self) -> ActivityMachineTransition { + pub(super) fn on_canceled(self) -> ActivityMachineTransition { // cancelCommandNotifyCanceled ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct ScheduledEventRecorded {} +pub(super) struct ScheduledEventRecorded {} impl ScheduledEventRecorded { - pub fn on_task_started(self) -> ActivityMachineTransition { + pub(super) fn on_task_started(self) -> ActivityMachineTransition { // setStartedCommandEventId ActivityMachineTransition::default::() } - pub fn on_task_timed_out(self) -> ActivityMachineTransition { + pub(super) fn on_task_timed_out(self) -> ActivityMachineTransition { // notify_timed_out ActivityMachineTransition::default::() } - pub fn on_canceled(self) -> ActivityMachineTransition { + pub(super) fn on_canceled(self) -> ActivityMachineTransition { // createRequestCancelActivityTaskCommand ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct Started {} +pub(super) struct Started {} impl Started { - pub fn on_activity_task_completed(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_completed(self) -> ActivityMachineTransition { // notify_completed ActivityMachineTransition::default::() } - pub fn on_activity_task_failed(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_failed(self) -> ActivityMachineTransition { // notify_failed ActivityMachineTransition::default::() } - pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_timed_out(self) -> ActivityMachineTransition { // notify_timed_out ActivityMachineTransition::default::() } - pub fn on_canceled(self) -> ActivityMachineTransition { + pub(super) fn on_canceled(self) -> ActivityMachineTransition { // createRequestCancelActivityTaskCommand ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct ScheduledActivityCancelCommandCreated {} +pub(super) struct ScheduledActivityCancelCommandCreated {} impl ScheduledActivityCancelCommandCreated { - pub fn on_command_request_cancel_activity_task(self) -> ActivityMachineTransition { + pub(super) fn on_command_request_cancel_activity_task(self) -> ActivityMachineTransition { // notifyCanceledIfTryCancel ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct ScheduledActivityCancelEventRecorded {} +pub(super) struct ScheduledActivityCancelEventRecorded {} impl ScheduledActivityCancelEventRecorded { - pub fn on_activity_task_canceled(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_canceled(self) -> ActivityMachineTransition { // notify_canceled ActivityMachineTransition::default::() } - pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_timed_out(self) -> ActivityMachineTransition { // notify_timed_out ActivityMachineTransition::default::() } @@ -150,32 +150,32 @@ impl From for ScheduledActivityCancelEven } #[derive(Default, Clone)] -pub struct StartedActivityCancelCommandCreated {} +pub(super) struct StartedActivityCancelCommandCreated {} impl StartedActivityCancelCommandCreated { - pub fn on_activity_task_cancel_requested(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_cancel_requested(self) -> ActivityMachineTransition { // notifyCanceledIfTryCancel ActivityMachineTransition::default::() } } #[derive(Default, Clone)] -pub struct StartedActivityCancelEventRecorded {} +pub(super) struct StartedActivityCancelEventRecorded {} impl StartedActivityCancelEventRecorded { - pub fn on_activity_task_completed(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_completed(self) -> ActivityMachineTransition { // notify_completed ActivityMachineTransition::default::() } - pub fn on_activity_task_failed(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_failed(self) -> ActivityMachineTransition { // notify_failed ActivityMachineTransition::default::() } - pub fn on_activity_task_timed_out(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_timed_out(self) -> ActivityMachineTransition { // notify_timed_out ActivityMachineTransition::default::() } - pub fn on_activity_task_canceled(self) -> ActivityMachineTransition { + pub(super) fn on_activity_task_canceled(self) -> ActivityMachineTransition { // notifyCancellationFromEvent ActivityMachineTransition::default::() } @@ -188,16 +188,16 @@ impl From for StartedActivityCancelEventRe } #[derive(Default, Clone)] -pub struct Completed {} +pub(super) struct Completed {} #[derive(Default, Clone)] -pub struct Failed {} +pub(super) struct Failed {} #[derive(Default, Clone)] -pub struct TimedOut {} +pub(super) struct TimedOut {} #[derive(Default, Clone)] -pub struct Canceled {} +pub(super) struct Canceled {} #[cfg(test)] mod activity_machine_tests { diff --git a/src/machines/cancel_external_state_machine.rs b/src/machines/cancel_external_state_machine.rs index 45cfc48f6..8c12ccfae 100644 --- a/src/machines/cancel_external_state_machine.rs +++ b/src/machines/cancel_external_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name CancelExternalMachine; command CancelExternalCommand; error CancelExternalMachineError; + pub(super) name CancelExternalMachine; command CancelExternalCommand; error CancelExternalMachineError; Created --(Schedule, on_schedule) --> RequestCancelExternalCommandCreated; @@ -13,27 +13,27 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum CancelExternalMachineError {} +pub(super) enum CancelExternalMachineError {} -pub enum CancelExternalCommand {} +pub(super) enum CancelExternalCommand {} #[derive(Default, Clone)] -pub struct CancelRequested {} +pub(super) struct CancelRequested {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> CancelExternalMachineTransition { + pub(super) fn on_schedule(self) -> CancelExternalMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct RequestCancelExternalCommandCreated {} +pub(super) struct RequestCancelExternalCommandCreated {} impl RequestCancelExternalCommandCreated { - pub fn on_request_cancel_external_workflow_execution_initiated( + pub(super) fn on_request_cancel_external_workflow_execution_initiated( self, ) -> CancelExternalMachineTransition { unimplemented!() @@ -41,15 +41,15 @@ impl RequestCancelExternalCommandCreated { } #[derive(Default, Clone)] -pub struct RequestCancelExternalCommandRecorded {} +pub(super) struct RequestCancelExternalCommandRecorded {} impl RequestCancelExternalCommandRecorded { - pub fn on_external_workflow_execution_cancel_requested( + pub(super) fn on_external_workflow_execution_cancel_requested( self, ) -> CancelExternalMachineTransition { unimplemented!() } - pub fn on_request_cancel_external_workflow_execution_failed( + pub(super) fn on_request_cancel_external_workflow_execution_failed( self, ) -> CancelExternalMachineTransition { unimplemented!() @@ -57,4 +57,4 @@ impl RequestCancelExternalCommandRecorded { } #[derive(Default, Clone)] -pub struct RequestCancelFailed {} +pub(super) struct RequestCancelFailed {} diff --git a/src/machines/cancel_workflow_state_machine.rs b/src/machines/cancel_workflow_state_machine.rs index e60175615..d28a90429 100644 --- a/src/machines/cancel_workflow_state_machine.rs +++ b/src/machines/cancel_workflow_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name CancelWorkflowMachine; command CancelWorkflowCommand; error CancelWorkflowMachineError; + pub(super) name CancelWorkflowMachine; command CancelWorkflowCommand; error CancelWorkflowMachineError; CancelWorkflowCommandCreated --(CommandCancelWorkflowExecution) --> CancelWorkflowCommandCreated; CancelWorkflowCommandCreated --(WorkflowExecutionCanceled, on_workflow_execution_canceled) --> CancelWorkflowCommandRecorded; @@ -10,27 +10,27 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum CancelWorkflowMachineError {} +pub(super) enum CancelWorkflowMachineError {} -pub enum CancelWorkflowCommand {} +pub(super) enum CancelWorkflowCommand {} #[derive(Default, Clone)] -pub struct CancelWorkflowCommandCreated {} +pub(super) struct CancelWorkflowCommandCreated {} impl CancelWorkflowCommandCreated { - pub fn on_workflow_execution_canceled(self) -> CancelWorkflowMachineTransition { + pub(super) fn on_workflow_execution_canceled(self) -> CancelWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct CancelWorkflowCommandRecorded {} +pub(super) struct CancelWorkflowCommandRecorded {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> CancelWorkflowMachineTransition { + pub(super) fn on_schedule(self) -> CancelWorkflowMachineTransition { unimplemented!() } } diff --git a/src/machines/child_workflow_state_machine.rs b/src/machines/child_workflow_state_machine.rs index fc5e30ad6..026820e96 100644 --- a/src/machines/child_workflow_state_machine.rs +++ b/src/machines/child_workflow_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name ChildWorkflowMachine; command ChildWorkflowCommand; error ChildWorkflowMachineError; + pub(super) name ChildWorkflowMachine; command ChildWorkflowCommand; error ChildWorkflowMachineError; Created --(Schedule, on_schedule) --> StartCommandCreated; @@ -20,78 +20,80 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum ChildWorkflowMachineError {} +pub(super) enum ChildWorkflowMachineError {} -pub enum ChildWorkflowCommand {} +pub(super) enum ChildWorkflowCommand {} #[derive(Default, Clone)] -pub struct Canceled {} +pub(super) struct Canceled {} #[derive(Default, Clone)] -pub struct Completed {} +pub(super) struct Completed {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_schedule(self) -> ChildWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Failed {} +pub(super) struct Failed {} #[derive(Default, Clone)] -pub struct StartCommandCreated {} +pub(super) struct StartCommandCreated {} impl StartCommandCreated { - pub fn on_start_child_workflow_execution_initiated(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_start_child_workflow_execution_initiated( + self, + ) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_cancel(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_cancel(self) -> ChildWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct StartEventRecorded {} +pub(super) struct StartEventRecorded {} impl StartEventRecorded { - pub fn on_child_workflow_execution_started(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_started(self) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_start_child_workflow_execution_failed(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_start_child_workflow_execution_failed(self) -> ChildWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct StartFailed {} +pub(super) struct StartFailed {} #[derive(Default, Clone)] -pub struct Started {} +pub(super) struct Started {} impl Started { - pub fn on_child_workflow_execution_completed(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_completed(self) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_child_workflow_execution_failed(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_failed(self) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_child_workflow_execution_timed_out(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_timed_out(self) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_child_workflow_execution_canceled(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_canceled(self) -> ChildWorkflowMachineTransition { unimplemented!() } - pub fn on_child_workflow_execution_terminated(self) -> ChildWorkflowMachineTransition { + pub(super) fn on_child_workflow_execution_terminated(self) -> ChildWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Terminated {} +pub(super) struct Terminated {} #[derive(Default, Clone)] -pub struct TimedOut {} +pub(super) struct TimedOut {} diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index d1cbe455a..3bc6b5901 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name CompleteWorkflowMachine; command CompleteWorkflowCommand; error CompleteWorkflowMachineError; + pub(super) name CompleteWorkflowMachine; command CompleteWorkflowCommand; error CompleteWorkflowMachineError; CompleteWorkflowCommandCreated --(CommandCompleteWorkflowExecution) --> CompleteWorkflowCommandCreated; CompleteWorkflowCommandCreated --(WorkflowExecutionCompleted, on_workflow_execution_completed) --> CompleteWorkflowCommandRecorded; @@ -10,27 +10,27 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum CompleteWorkflowMachineError {} +pub(super) enum CompleteWorkflowMachineError {} -pub enum CompleteWorkflowCommand {} +pub(super) enum CompleteWorkflowCommand {} #[derive(Default, Clone)] -pub struct CompleteWorkflowCommandCreated {} +pub(super) struct CompleteWorkflowCommandCreated {} impl CompleteWorkflowCommandCreated { - pub fn on_workflow_execution_completed(self) -> CompleteWorkflowMachineTransition { + pub(super) fn on_workflow_execution_completed(self) -> CompleteWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct CompleteWorkflowCommandRecorded {} +pub(super) struct CompleteWorkflowCommandRecorded {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> CompleteWorkflowMachineTransition { + pub(super) fn on_schedule(self) -> CompleteWorkflowMachineTransition { unimplemented!() } } diff --git a/src/machines/continue_as_new_workflow_state_machine.rs b/src/machines/continue_as_new_workflow_state_machine.rs index 4a45d15b2..ea23d8a57 100644 --- a/src/machines/continue_as_new_workflow_state_machine.rs +++ b/src/machines/continue_as_new_workflow_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name ContinueAsNewWorkflowMachine; command ContinueAsNewWorkflowCommand; error ContinueAsNewWorkflowMachineError; + pub(super) name ContinueAsNewWorkflowMachine; command ContinueAsNewWorkflowCommand; error ContinueAsNewWorkflowMachineError; ContinueAsNewWorkflowCommandCreated --(CommandContinueAsNewWorkflowExecution) --> ContinueAsNewWorkflowCommandCreated; ContinueAsNewWorkflowCommandCreated --(WorkflowExecutionContinuedAsNew, on_workflow_execution_continued_as_new) --> ContinueAsNewWorkflowCommandRecorded; @@ -10,27 +10,29 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum ContinueAsNewWorkflowMachineError {} +pub(super) enum ContinueAsNewWorkflowMachineError {} -pub enum ContinueAsNewWorkflowCommand {} +pub(super) enum ContinueAsNewWorkflowCommand {} #[derive(Default, Clone)] -pub struct ContinueAsNewWorkflowCommandCreated {} +pub(super) struct ContinueAsNewWorkflowCommandCreated {} impl ContinueAsNewWorkflowCommandCreated { - pub fn on_workflow_execution_continued_as_new(self) -> ContinueAsNewWorkflowMachineTransition { + pub(super) fn on_workflow_execution_continued_as_new( + self, + ) -> ContinueAsNewWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ContinueAsNewWorkflowCommandRecorded {} +pub(super) struct ContinueAsNewWorkflowCommandRecorded {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> ContinueAsNewWorkflowMachineTransition { + pub(super) fn on_schedule(self) -> ContinueAsNewWorkflowMachineTransition { unimplemented!() } } diff --git a/src/machines/fail_workflow_state_machine.rs b/src/machines/fail_workflow_state_machine.rs index 872d9cacd..194a88cb8 100644 --- a/src/machines/fail_workflow_state_machine.rs +++ b/src/machines/fail_workflow_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name FailWorkflowMachine; command FailWorkflowCommand; error FailWorkflowMachineError; + pub(super) name FailWorkflowMachine; command FailWorkflowCommand; error FailWorkflowMachineError; Created --(Schedule, on_schedule) --> FailWorkflowCommandCreated; @@ -10,27 +10,27 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum FailWorkflowMachineError {} +pub(super) enum FailWorkflowMachineError {} -pub enum FailWorkflowCommand {} +pub(super) enum FailWorkflowCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> FailWorkflowMachineTransition { + pub(super) fn on_schedule(self) -> FailWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct FailWorkflowCommandCreated {} +pub(super) struct FailWorkflowCommandCreated {} impl FailWorkflowCommandCreated { - pub fn on_workflow_execution_failed(self) -> FailWorkflowMachineTransition { + pub(super) fn on_workflow_execution_failed(self) -> FailWorkflowMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct FailWorkflowCommandRecorded {} +pub(super) struct FailWorkflowCommandRecorded {} diff --git a/src/machines/local_activity_state_machine.rs b/src/machines/local_activity_state_machine.rs index d275ab49f..738de64fd 100644 --- a/src/machines/local_activity_state_machine.rs +++ b/src/machines/local_activity_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name LocalActivityMachine; command LocalActivityCommand; error LocalActivityMachineError; + pub(super) name LocalActivityMachine; command LocalActivityCommand; error LocalActivityMachineError; Created --(CheckExecutionState, on_check_execution_state) --> Replaying; Created --(CheckExecutionState, on_check_execution_state) --> Executing; @@ -24,51 +24,51 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum LocalActivityMachineError {} +pub(super) enum LocalActivityMachineError {} -pub enum LocalActivityCommand {} +pub(super) enum LocalActivityCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_check_execution_state(self) -> LocalActivityMachineTransition { + pub(super) fn on_check_execution_state(self) -> LocalActivityMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Executing {} +pub(super) struct Executing {} impl Executing { - pub fn on_schedule(self) -> LocalActivityMachineTransition { + pub(super) fn on_schedule(self) -> LocalActivityMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreated {} +pub(super) struct MarkerCommandCreated {} impl MarkerCommandCreated { - pub fn on_command_record_marker(self) -> LocalActivityMachineTransition { + pub(super) fn on_command_record_marker(self) -> LocalActivityMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandRecorded {} +pub(super) struct MarkerCommandRecorded {} #[derive(Default, Clone)] -pub struct Replaying {} +pub(super) struct Replaying {} #[derive(Default, Clone)] -pub struct RequestPrepared {} +pub(super) struct RequestPrepared {} #[derive(Default, Clone)] -pub struct RequestSent {} +pub(super) struct RequestSent {} impl RequestSent { - pub fn on_handle_result(self) -> LocalActivityMachineTransition { + pub(super) fn on_handle_result(self) -> LocalActivityMachineTransition { unimplemented!() } } @@ -80,22 +80,22 @@ impl From for RequestSent { } #[derive(Default, Clone)] -pub struct ResultNotified {} +pub(super) struct ResultNotified {} impl ResultNotified { - pub fn on_marker_recorded(self) -> LocalActivityMachineTransition { + pub(super) fn on_marker_recorded(self) -> LocalActivityMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct WaitingMarkerEvent {} +pub(super) struct WaitingMarkerEvent {} impl WaitingMarkerEvent { - pub fn on_marker_recorded(self) -> LocalActivityMachineTransition { + pub(super) fn on_marker_recorded(self) -> LocalActivityMachineTransition { unimplemented!() } - pub fn on_non_replay_workflow_task_started(self) -> LocalActivityMachineTransition { + pub(super) fn on_non_replay_workflow_task_started(self) -> LocalActivityMachineTransition { unimplemented!() } } diff --git a/src/machines/mod.rs b/src/machines/mod.rs index a32d3b990..10a0d6629 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -40,7 +40,8 @@ use crate::{ command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, }, }; -use rustfsm::StateMachine; +use rustfsm::{MachineError, StateMachine}; +use std::convert::{TryFrom, TryInto}; // TODO: May need to be our SDKWFCommand type pub(crate) type MachineCommand = Command; @@ -49,9 +50,9 @@ pub(crate) type MachineCommand = Command; /// /// Formerly known as `EntityStateMachine` in Java. trait TemporalStateMachine: CheckStateMachineInFinal { - fn handle_command(&self, command_type: CommandType); + fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError>; fn handle_event( - &self, + &mut self, event: &HistoryEvent, has_next_event: bool, ) -> Result<(), WFMachinesError>; @@ -61,6 +62,46 @@ trait TemporalStateMachine: CheckStateMachineInFinal { // fn handle_workflow_task_started(); } +impl TemporalStateMachine for SM +where + SM: StateMachine + CheckStateMachineInFinal + Clone, + ::Event: TryFrom, + ::Event: TryFrom, + ::Error: Into, +{ + fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError> { + if let Ok(converted_command) = command_type.try_into() { + match self.on_event_mut(converted_command) { + Ok(_) => Ok(()), + Err(MachineError::InvalidTransition) => { + Err(WFMachinesError::UnexpectedCommand(command_type)) + } + Err(MachineError::Underlying(_)) => Err(WFMachinesError::Underlying), + } + } else { + Err(WFMachinesError::UnexpectedCommand(command_type)) + } + } + + fn handle_event( + &mut self, + event: &HistoryEvent, + _has_next_event: bool, + ) -> Result<(), WFMachinesError> { + if let Ok(converted_event) = event.clone().try_into() { + match self.on_event_mut(converted_event) { + Ok(_) => Ok(()), + Err(MachineError::InvalidTransition) => { + Err(WFMachinesError::UnexpectedEvent(event.clone())) + } + Err(MachineError::Underlying(_)) => Err(WFMachinesError::Underlying), + } + } else { + Err(WFMachinesError::UnexpectedEvent(event.clone())) + } + } +} + /// Exists purely to allow generic implementation of `is_final_state` for all [StateMachine] /// implementors trait CheckStateMachineInFinal { diff --git a/src/machines/mutable_side_effect_state_machine.rs b/src/machines/mutable_side_effect_state_machine.rs index c75d88f06..de635b3c1 100644 --- a/src/machines/mutable_side_effect_state_machine.rs +++ b/src/machines/mutable_side_effect_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name MutableSideEffectMachine; command MutableSideEffectCommand; error MutableSideEffectMachineError; + pub(super) name MutableSideEffectMachine; command MutableSideEffectCommand; error MutableSideEffectMachineError; Created --(CheckExecutionState, on_check_execution_state) --> Replaying; Created --(CheckExecutionState, on_check_execution_state) --> Executing; @@ -25,69 +25,69 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum MutableSideEffectMachineError {} +pub(super) enum MutableSideEffectMachineError {} -pub enum MutableSideEffectCommand {} +pub(super) enum MutableSideEffectCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_check_execution_state(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_check_execution_state(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Executing {} +pub(super) struct Executing {} impl Executing { - pub fn on_schedule(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_schedule(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreated {} +pub(super) struct MarkerCommandCreated {} impl MarkerCommandCreated { - pub fn on_command_record_marker(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_command_record_marker(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreatedReplaying {} +pub(super) struct MarkerCommandCreatedReplaying {} #[derive(Default, Clone)] -pub struct MarkerCommandRecorded {} +pub(super) struct MarkerCommandRecorded {} #[derive(Default, Clone)] -pub struct Replaying {} +pub(super) struct Replaying {} impl Replaying { - pub fn on_schedule(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_schedule(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ResultNotified {} +pub(super) struct ResultNotified {} impl ResultNotified { - pub fn on_marker_recorded(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_marker_recorded(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ResultNotifiedReplaying {} +pub(super) struct ResultNotifiedReplaying {} impl ResultNotifiedReplaying { - pub fn on_non_matching_event(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_non_matching_event(self) -> MutableSideEffectMachineTransition { unimplemented!() } - pub fn on_marker_recorded(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_marker_recorded(self) -> MutableSideEffectMachineTransition { unimplemented!() } } @@ -99,13 +99,13 @@ impl From for ResultNotifiedReplaying { } #[derive(Default, Clone)] -pub struct Skipped {} +pub(super) struct Skipped {} impl Skipped { - pub fn on_command_record_marker(self) -> MutableSideEffectMachineTransition { + pub(super) fn on_command_record_marker(self) -> MutableSideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct SkippedNotified {} +pub(super) struct SkippedNotified {} diff --git a/src/machines/side_effect_state_machine.rs b/src/machines/side_effect_state_machine.rs index 29aed6606..8860d1dc4 100644 --- a/src/machines/side_effect_state_machine.rs +++ b/src/machines/side_effect_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name SideEffectMachine; command SideEffectCommand; error SideEffectMachineError; + pub(super) name SideEffectMachine; command SideEffectCommand; error SideEffectMachineError; Created --(Schedule, on_schedule) --> MarkerCommandCreated; Created --(Schedule, on_schedule) --> MarkerCommandCreatedReplaying; @@ -16,48 +16,48 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum SideEffectMachineError {} +pub(super) enum SideEffectMachineError {} -pub enum SideEffectCommand {} +pub(super) enum SideEffectCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> SideEffectMachineTransition { + pub(super) fn on_schedule(self) -> SideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreated {} +pub(super) struct MarkerCommandCreated {} impl MarkerCommandCreated { - pub fn on_command_record_marker(self) -> SideEffectMachineTransition { + pub(super) fn on_command_record_marker(self) -> SideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreatedReplaying {} +pub(super) struct MarkerCommandCreatedReplaying {} #[derive(Default, Clone)] -pub struct MarkerCommandRecorded {} +pub(super) struct MarkerCommandRecorded {} #[derive(Default, Clone)] -pub struct ResultNotified {} +pub(super) struct ResultNotified {} impl ResultNotified { - pub fn on_marker_recorded(self) -> SideEffectMachineTransition { + pub(super) fn on_marker_recorded(self) -> SideEffectMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ResultNotifiedReplaying {} +pub(super) struct ResultNotifiedReplaying {} impl ResultNotifiedReplaying { - pub fn on_marker_recorded(self) -> SideEffectMachineTransition { + pub(super) fn on_marker_recorded(self) -> SideEffectMachineTransition { unimplemented!() } } diff --git a/src/machines/signal_external_state_machine.rs b/src/machines/signal_external_state_machine.rs index a50b57a01..88bf1f6cb 100644 --- a/src/machines/signal_external_state_machine.rs +++ b/src/machines/signal_external_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name SignalExternalMachine; command SignalExternalCommand; error SignalExternalMachineError; + pub(super) name SignalExternalMachine; command SignalExternalCommand; error SignalExternalMachineError; Created --(Schedule, on_schedule) --> SignalExternalCommandCreated; @@ -15,33 +15,33 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum SignalExternalMachineError {} +pub(super) enum SignalExternalMachineError {} -pub enum SignalExternalCommand {} +pub(super) enum SignalExternalCommand {} #[derive(Default, Clone)] -pub struct Canceled {} +pub(super) struct Canceled {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> SignalExternalMachineTransition { + pub(super) fn on_schedule(self) -> SignalExternalMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Failed {} +pub(super) struct Failed {} #[derive(Default, Clone)] -pub struct SignalExternalCommandCreated {} +pub(super) struct SignalExternalCommandCreated {} impl SignalExternalCommandCreated { - pub fn on_cancel(self) -> SignalExternalMachineTransition { + pub(super) fn on_cancel(self) -> SignalExternalMachineTransition { unimplemented!() } - pub fn on_signal_external_workflow_execution_initiated( + pub(super) fn on_signal_external_workflow_execution_initiated( self, ) -> SignalExternalMachineTransition { unimplemented!() @@ -49,16 +49,18 @@ impl SignalExternalCommandCreated { } #[derive(Default, Clone)] -pub struct SignalExternalCommandRecorded {} +pub(super) struct SignalExternalCommandRecorded {} impl SignalExternalCommandRecorded { - pub fn on_external_workflow_execution_signaled(self) -> SignalExternalMachineTransition { + pub(super) fn on_external_workflow_execution_signaled(self) -> SignalExternalMachineTransition { unimplemented!() } - pub fn on_signal_external_workflow_execution_failed(self) -> SignalExternalMachineTransition { + pub(super) fn on_signal_external_workflow_execution_failed( + self, + ) -> SignalExternalMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Signaled {} +pub(super) struct Signaled {} diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 83aeccdb9..8f5036ad7 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -1,15 +1,11 @@ -use crate::machines::MachineCommand; -use crate::protos::temporal::api::history::v1::{ - TimerStartedEventAttributes, WorkflowExecutionStartedEventAttributes, - WorkflowTaskScheduledEventAttributes, -}; use crate::{ - machines::workflow_machines::WorkflowMachines, + machines::{workflow_machines::WorkflowMachines, MachineCommand}, protos::temporal::api::{ enums::v1::EventType, history::v1::{ - history_event::Attributes, HistoryEvent, WorkflowTaskCompletedEventAttributes, - WorkflowTaskStartedEventAttributes, + history_event::Attributes, HistoryEvent, TimerStartedEventAttributes, + WorkflowExecutionStartedEventAttributes, WorkflowTaskCompletedEventAttributes, + WorkflowTaskScheduledEventAttributes, WorkflowTaskStartedEventAttributes, }, }, }; diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index f0d4ab27f..0e9ef05db 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,5 +1,6 @@ #![allow(clippy::large_enum_variant)] +use crate::machines::workflow_machines::WFMachinesError; use crate::{ machines::CancellableCommand, protos::{ @@ -12,11 +13,12 @@ use crate::{ }, }; use rustfsm::{fsm, TransitionResult}; +use std::convert::TryFrom; fsm! { - name TimerMachine; + pub(super) name TimerMachine; command TimerCommand; - error TimerMachineError; + error WFMachinesError; shared_state SharedState; CancelTimerCommandCreated --(Cancel) --> CancelTimerCommandCreated; @@ -35,8 +37,33 @@ fsm! { StartCommandRecorded --(Cancel, on_cancel) --> CancelTimerCommandCreated; } +impl TryFrom for TimerMachineEvents { + type Error = (); + + fn try_from(e: HistoryEvent) -> Result { + Ok(match EventType::from_i32(e.event_type) { + Some(EventType::TimerStarted) => Self::TimerStarted(e.event_id), + Some(EventType::TimerCanceled) => Self::TimerCanceled, + Some(EventType::TimerFired) => Self::TimerFired(e), + _ => return Err(()), + }) + } +} + +impl TryFrom for TimerMachineEvents { + type Error = (); + + fn try_from(c: CommandType) -> Result { + Ok(match c { + CommandType::StartTimer => Self::CommandStartTimer, + CommandType::CancelTimer => Self::CommandCancelTimer, + _ => return Err(()), + }) + } +} + #[derive(Default, Clone)] -pub struct SharedState { +pub(super) struct SharedState { timer_attributes: StartTimerCommandAttributes, } @@ -58,19 +85,16 @@ impl SharedState { } } -#[derive(thiserror::Error, Debug)] -pub enum TimerMachineError {} - -pub enum TimerCommand { +pub(super) enum TimerCommand { StartTimer(CancellableCommand), CancelTimer(/* TODO: Command attribs */), ProduceHistoryEvent(HistoryEvent), } #[derive(Default, Clone)] -pub struct CancelTimerCommandCreated {} +pub(super) struct CancelTimerCommandCreated {} impl CancelTimerCommandCreated { - pub fn on_command_cancel_timer(self, dat: SharedState) -> TimerMachineTransition { + pub(super) fn on_command_cancel_timer(self, dat: SharedState) -> TimerMachineTransition { TimerMachineTransition::ok( vec![dat.into_timer_canceled_event_command()], Canceled::default(), @@ -79,10 +103,10 @@ impl CancelTimerCommandCreated { } #[derive(Default, Clone)] -pub struct CancelTimerCommandSent {} +pub(super) struct CancelTimerCommandSent {} #[derive(Default, Clone)] -pub struct Canceled {} +pub(super) struct Canceled {} impl From for Canceled { fn from(_: CancelTimerCommandSent) -> Self { Self::default() @@ -90,10 +114,10 @@ impl From for Canceled { } #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self, dat: SharedState) -> TimerMachineTransition { + pub(super) fn on_schedule(self, dat: SharedState) -> TimerMachineTransition { let cmd = Command { command_type: CommandType::StartTimer as i32, attributes: Some(Attributes::StartTimerCommandAttributes( @@ -110,19 +134,19 @@ impl Created { } #[derive(Default, Clone)] -pub struct Fired {} +pub(super) struct Fired {} #[derive(Clone)] -pub struct StartCommandCreated { +pub(super) struct StartCommandCreated { cancellable_command: CancellableCommand, } impl StartCommandCreated { - pub fn on_timer_started(self, id: HistoryEventId) -> TimerMachineTransition { + pub(super) fn on_timer_started(self, id: HistoryEventId) -> TimerMachineTransition { // Java recorded an initial event ID, but it seemingly was never used. TimerMachineTransition::default::() } - pub fn on_cancel(mut self, dat: SharedState) -> TimerMachineTransition { + pub(super) fn on_cancel(mut self, dat: SharedState) -> TimerMachineTransition { // Cancel the initial command - which just sets a "canceled" flag in a wrapper of a // proto command. TODO: Does this make any sense? let _canceled_cmd = self.cancellable_command.cancel(); @@ -134,16 +158,16 @@ impl StartCommandCreated { } #[derive(Default, Clone)] -pub struct StartCommandRecorded {} +pub(super) struct StartCommandRecorded {} impl StartCommandRecorded { - pub fn on_timer_fired(self, event: HistoryEvent) -> TimerMachineTransition { + pub(super) fn on_timer_fired(self, event: HistoryEvent) -> TimerMachineTransition { TimerMachineTransition::ok( vec![TimerCommand::ProduceHistoryEvent(event)], Fired::default(), ) } - pub fn on_cancel(self) -> TimerMachineTransition { + pub(super) fn on_cancel(self) -> TimerMachineTransition { TimerMachineTransition::ok( vec![TimerCommand::CancelTimer()], CancelTimerCommandCreated::default(), diff --git a/src/machines/upsert_search_attributes_state_machine.rs b/src/machines/upsert_search_attributes_state_machine.rs index 874689b6d..7424f1de3 100644 --- a/src/machines/upsert_search_attributes_state_machine.rs +++ b/src/machines/upsert_search_attributes_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name UpsertSearchAttributesMachine; command UpsertSearchAttributesCommand; error UpsertSearchAttributesMachineError; + pub(super) name UpsertSearchAttributesMachine; command UpsertSearchAttributesCommand; error UpsertSearchAttributesMachineError; Created --(Schedule, on_schedule) --> UpsertCommandCreated; @@ -10,27 +10,29 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum UpsertSearchAttributesMachineError {} +pub(super) enum UpsertSearchAttributesMachineError {} -pub enum UpsertSearchAttributesCommand {} +pub(super) enum UpsertSearchAttributesCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_schedule(self) -> UpsertSearchAttributesMachineTransition { + pub(super) fn on_schedule(self) -> UpsertSearchAttributesMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct UpsertCommandCreated {} +pub(super) struct UpsertCommandCreated {} impl UpsertCommandCreated { - pub fn on_upsert_workflow_search_attributes(self) -> UpsertSearchAttributesMachineTransition { + pub(super) fn on_upsert_workflow_search_attributes( + self, + ) -> UpsertSearchAttributesMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct UpsertCommandRecorded {} +pub(super) struct UpsertCommandRecorded {} diff --git a/src/machines/version_state_machine.rs b/src/machines/version_state_machine.rs index fc6846dbd..55cbb38d7 100644 --- a/src/machines/version_state_machine.rs +++ b/src/machines/version_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name VersionMachine; command VersionCommand; error VersionMachineError; + pub(super) name VersionMachine; command VersionCommand; error VersionMachineError; Created --(CheckExecutionState, on_check_execution_state) --> Replaying; Created --(CheckExecutionState, on_check_execution_state) --> Executing; @@ -25,69 +25,69 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum VersionMachineError {} +pub(super) enum VersionMachineError {} -pub enum VersionCommand {} +pub(super) enum VersionCommand {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_check_execution_state(self) -> VersionMachineTransition { + pub(super) fn on_check_execution_state(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Executing {} +pub(super) struct Executing {} impl Executing { - pub fn on_schedule(self) -> VersionMachineTransition { + pub(super) fn on_schedule(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreated {} +pub(super) struct MarkerCommandCreated {} impl MarkerCommandCreated { - pub fn on_command_record_marker(self) -> VersionMachineTransition { + pub(super) fn on_command_record_marker(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct MarkerCommandCreatedReplaying {} +pub(super) struct MarkerCommandCreatedReplaying {} #[derive(Default, Clone)] -pub struct MarkerCommandRecorded {} +pub(super) struct MarkerCommandRecorded {} #[derive(Default, Clone)] -pub struct Replaying {} +pub(super) struct Replaying {} impl Replaying { - pub fn on_schedule(self) -> VersionMachineTransition { + pub(super) fn on_schedule(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ResultNotified {} +pub(super) struct ResultNotified {} impl ResultNotified { - pub fn on_marker_recorded(self) -> VersionMachineTransition { + pub(super) fn on_marker_recorded(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct ResultNotifiedReplaying {} +pub(super) struct ResultNotifiedReplaying {} impl ResultNotifiedReplaying { - pub fn on_non_matching_event(self) -> VersionMachineTransition { + pub(super) fn on_non_matching_event(self) -> VersionMachineTransition { unimplemented!() } - pub fn on_marker_recorded(self) -> VersionMachineTransition { + pub(super) fn on_marker_recorded(self) -> VersionMachineTransition { unimplemented!() } } @@ -99,13 +99,13 @@ impl From for ResultNotifiedReplaying { } #[derive(Default, Clone)] -pub struct Skipped {} +pub(super) struct Skipped {} impl Skipped { - pub fn on_command_record_marker(self) -> VersionMachineTransition { + pub(super) fn on_command_record_marker(self) -> VersionMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct SkippedNotified {} +pub(super) struct SkippedNotified {} diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index bbb620a6f..06d0f47a7 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,14 +1,16 @@ -use crate::protos::temporal::api::history::v1::history_event; use crate::{ machines::{CancellableCommand, MachineCommand, TemporalStateMachine}, - protos::temporal::api::{enums::v1::EventType, history::v1::HistoryEvent}, + protos::temporal::api::{ + enums::v1::{CommandType, EventType}, + history::v1::{history_event, HistoryEvent}, + }, }; use std::collections::{hash_map::Entry, HashMap, VecDeque}; type Result = std::result::Result; #[derive(Default)] -pub(super) struct WorkflowMachines { +pub(crate) struct WorkflowMachines { /// The event id of the last event in the history which is expected to be startedEventId unless /// it is replay from a JSON file. workflow_task_started_event_id: i64, @@ -31,11 +33,15 @@ pub(super) struct WorkflowMachines { #[derive(thiserror::Error, Debug)] pub(crate) enum WFMachinesError { - // TODO: more context #[error("Event {0:?} was not expected")] UnexpectedEvent(HistoryEvent), #[error("Event {0:?} was malformed: {1}")] MalformedEvent(HistoryEvent, String), + #[error("Command type {0:?} was not expected")] + UnexpectedCommand(CommandType), + // TODO: Pretty sure can remove this if no machines need some specific error + #[error("Underlying machine error")] + Underlying, } impl WorkflowMachines { @@ -52,7 +58,7 @@ impl WorkflowMachines { /// is the last event in the history. /// /// TODO: Describe what actually happens in here - pub(super) fn handle_event( + pub(crate) fn handle_event( &mut self, event: &HistoryEvent, has_next_event: bool, @@ -74,8 +80,8 @@ impl WorkflowMachines { .get_initial_command_event_id() .map(|id| self.machines_by_id.entry(id)) { - Some(Entry::Occupied(sme)) => { - let sm = sme.get(); + Some(Entry::Occupied(mut sme)) => { + let sm = sme.get_mut(); sm.handle_event(event, has_next_event)?; if sm.is_final_state() { sme.remove(); @@ -150,7 +156,7 @@ impl WorkflowMachines { /// Fetches commands ready for processing from the state machines, removing them from the /// internal command queue. - pub(super) fn take_commands(&mut self) -> Vec { + pub(crate) fn take_commands(&mut self) -> Vec { self.commands.drain(0..).flat_map(|c| c.command).collect() } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 15a0a8292..ca7a23bdd 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,7 +1,7 @@ use rustfsm::{fsm, TransitionResult}; fsm! { - name WorkflowTaskMachine; command WorkflowTaskCommand; error WorkflowTaskMachineError; + pub(super) name WorkflowTaskMachine; command WorkflowTaskCommand; error WorkflowTaskMachineError; Created --(WorkflowTaskScheduled, on_workflow_task_scheduled) --> Scheduled; @@ -14,51 +14,51 @@ fsm! { } #[derive(thiserror::Error, Debug)] -pub enum WorkflowTaskMachineError {} +pub(super) enum WorkflowTaskMachineError {} -pub enum WorkflowTaskCommand {} +pub(super) enum WorkflowTaskCommand {} #[derive(Default, Clone)] -pub struct Completed {} +pub(super) struct Completed {} #[derive(Default, Clone)] -pub struct Created {} +pub(super) struct Created {} impl Created { - pub fn on_workflow_task_scheduled(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_scheduled(self) -> WorkflowTaskMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Failed {} +pub(super) struct Failed {} #[derive(Default, Clone)] -pub struct Scheduled {} +pub(super) struct Scheduled {} impl Scheduled { - pub fn on_workflow_task_started(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_started(self) -> WorkflowTaskMachineTransition { unimplemented!() } - pub fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct Started {} +pub(super) struct Started {} impl Started { - pub fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { unimplemented!() } - pub fn on_workflow_task_failed(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_failed(self) -> WorkflowTaskMachineTransition { unimplemented!() } - pub fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { + pub(super) fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { unimplemented!() } } #[derive(Default, Clone)] -pub struct TimedOut {} +pub(super) struct TimedOut {} From a23441d08d5d86f6b4f6bff7c7215dcf450ef8e9 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 10:32:13 -0800 Subject: [PATCH 07/59] Workflow task state machine partially implemented --- Cargo.toml | 8 +- fsm/state_machine_procmacro/src/lib.rs | 2 +- src/machines/mod.rs | 8 +- src/machines/workflow_machines.rs | 26 +++- src/machines/workflow_task_state_machine.rs | 124 ++++++++++++++++---- 5 files changed, 133 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fddb25f7e..1cccbabdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,16 +11,16 @@ anyhow = "1.0" async-trait = "0.1" derive_more = "0.99" log = "0.4" -prost = "0.6" -prost-types = "0.6" +prost = "0.7" +prost-types = "0.7" thiserror = "1.0" -tonic = "0.3" +tonic = "0.4" [dependencies.rustfsm] path = "fsm" [build-dependencies] -tonic-build = "0.3" +tonic-build = "0.4" [workspace] members = [".", "fsm"] diff --git a/fsm/state_machine_procmacro/src/lib.rs b/fsm/state_machine_procmacro/src/lib.rs index 72d967b2a..25eac2a2f 100644 --- a/fsm/state_machine_procmacro/src/lib.rs +++ b/fsm/state_machine_procmacro/src/lib.rs @@ -382,7 +382,7 @@ impl StateMachineDefinition { let events_enum_name = Ident::new(&format!("{}Events", name), name.span()); let events: Vec<_> = events.into_iter().collect(); let events_enum = quote! { - pub enum #events_enum_name { + #visibility enum #events_enum_name { #(#events),* } }; diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 10a0d6629..f3557d7b3 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -67,7 +67,7 @@ where SM: StateMachine + CheckStateMachineInFinal + Clone, ::Event: TryFrom, ::Event: TryFrom, - ::Error: Into, + ::Error: Into + 'static + Send + Sync, { fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError> { if let Ok(converted_command) = command_type.try_into() { @@ -76,7 +76,7 @@ where Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedCommand(command_type)) } - Err(MachineError::Underlying(_)) => Err(WFMachinesError::Underlying), + Err(MachineError::Underlying(e)) => Err(WFMachinesError::Underlying(Box::new(e))), } } else { Err(WFMachinesError::UnexpectedCommand(command_type)) @@ -88,13 +88,15 @@ where event: &HistoryEvent, _has_next_event: bool, ) -> Result<(), WFMachinesError> { + // TODO: `has_next_event` is *only* used by WorkflowTaskStateMachine. Figure out how + // to deal with it. if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { Ok(_) => Ok(()), Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedEvent(event.clone())) } - Err(MachineError::Underlying(_)) => Err(WFMachinesError::Underlying), + Err(MachineError::Underlying(e)) => Err(WFMachinesError::Underlying(Box::new(e))), } } else { Err(WFMachinesError::UnexpectedEvent(event.clone())) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 06d0f47a7..3b2127642 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,11 +1,15 @@ use crate::{ - machines::{CancellableCommand, MachineCommand, TemporalStateMachine}, + machines::{ + workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, MachineCommand, + TemporalStateMachine, + }, protos::temporal::api::{ enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent}, }, }; use std::collections::{hash_map::Entry, HashMap, VecDeque}; +use std::error::Error; type Result = std::result::Result; @@ -40,8 +44,8 @@ pub(crate) enum WFMachinesError { #[error("Command type {0:?} was not expected")] UnexpectedCommand(CommandType), // TODO: Pretty sure can remove this if no machines need some specific error - #[error("Underlying machine error")] - Underlying, + #[error("Underlying machine error {0:?}")] + Underlying(#[from] Box), } impl WorkflowMachines { @@ -107,7 +111,7 @@ impl WorkflowMachines { fn handle_non_stateful_event( &mut self, event: &HistoryEvent, - _has_next_event: bool, + has_next_event: bool, ) -> Result<()> { // switch (event.getEventType()) { // case EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: @@ -144,11 +148,23 @@ impl WorkflowMachines { } else { return Err(WFMachinesError::MalformedEvent( event.clone(), - "WorkflowExecutionStarted event did not have appropriate attribues" + "WorkflowExecutionStarted event did not have appropriate attributes" .to_string(), )); } } + Some(EventType::WorkflowTaskScheduled) => { + let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); + wf_task_sm.handle_event(event, has_next_event)?; + self.machines_by_id + .insert(event.event_id, Box::new(wf_task_sm)); + } + Some(EventType::WorkflowExecutionSignaled) => { + // TODO: Signal callbacks + } + Some(EventType::WorkflowExecutionCancelRequested) => { + // TODO: Cancel callbacks + } _ => return Err(WFMachinesError::UnexpectedEvent(event.clone())), } Ok(()) diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index ca7a23bdd..7641be219 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,20 +1,75 @@ +use crate::{ + machines::workflow_machines::WFMachinesError, + protos::temporal::api::{ + enums::v1::{CommandType, EventType}, + history::v1::HistoryEvent, + }, +}; use rustfsm::{fsm, TransitionResult}; +use std::{convert::TryFrom, time::SystemTime}; fsm! { - pub(super) name WorkflowTaskMachine; command WorkflowTaskCommand; error WorkflowTaskMachineError; + pub(super) name WorkflowTaskMachine; + command WorkflowTaskCommand; + error WFMachinesError; + shared_state SharedState; - Created --(WorkflowTaskScheduled, on_workflow_task_scheduled) --> Scheduled; + Created --(WorkflowTaskScheduled) --> Scheduled; - Scheduled --(WorkflowTaskStarted, on_workflow_task_started) --> Started; - Scheduled --(WorkflowTaskTimedOut, on_workflow_task_timed_out) --> TimedOut; + Scheduled --(WorkflowTaskStarted(WFTStartedDat), on_workflow_task_started) --> Started; + Scheduled --(WorkflowTaskTimedOut) --> TimedOut; Started --(WorkflowTaskCompleted, on_workflow_task_completed) --> Completed; Started --(WorkflowTaskFailed, on_workflow_task_failed) --> Failed; - Started --(WorkflowTaskTimedOut, on_workflow_task_timed_out) --> TimedOut; + Started --(WorkflowTaskTimedOut) --> TimedOut; } -#[derive(thiserror::Error, Debug)] -pub(super) enum WorkflowTaskMachineError {} +impl WorkflowTaskMachine { + pub(super) fn new(wf_task_started_event_id: i64) -> Self { + Self { + state: Created {}.into(), + shared_state: SharedState { + wf_task_started_event_id, + }, + } + } +} + +impl TryFrom for WorkflowTaskMachineEvents { + type Error = WFMachinesError; + + fn try_from(e: HistoryEvent) -> Result { + Ok(match EventType::from_i32(e.event_type) { + Some(EventType::WorkflowTaskScheduled) => Self::WorkflowTaskScheduled, + Some(EventType::WorkflowTaskStarted) => Self::WorkflowTaskStarted(WFTStartedDat { + started_event_id: e.event_id, + current_time_millis: e.event_time.clone().map(|ts| ts.into()).ok_or_else(|| { + WFMachinesError::MalformedEvent( + e, + "Workflow task started event must contain timestamp".to_string(), + ) + })?, + }), + Some(EventType::WorkflowTaskTimedOut) => Self::WorkflowTaskTimedOut, + Some(EventType::WorkflowTaskCompleted) => Self::WorkflowTaskCompleted, + Some(EventType::WorkflowTaskFailed) => Self::WorkflowTaskFailed, + _ => return Err(WFMachinesError::UnexpectedEvent(e)), + }) + } +} + +impl TryFrom for WorkflowTaskMachineEvents { + type Error = (); + + fn try_from(_: CommandType) -> Result { + Err(()) + } +} + +#[derive(Debug, Clone)] +pub(super) struct SharedState { + wf_task_started_event_id: i64, +} pub(super) enum WorkflowTaskCommand {} @@ -24,29 +79,47 @@ pub(super) struct Completed {} #[derive(Default, Clone)] pub(super) struct Created {} -impl Created { - pub(super) fn on_workflow_task_scheduled(self) -> WorkflowTaskMachineTransition { - unimplemented!() - } -} - #[derive(Default, Clone)] pub(super) struct Failed {} #[derive(Default, Clone)] pub(super) struct Scheduled {} +pub(super) struct WFTStartedDat { + current_time_millis: SystemTime, + started_event_id: i64, +} impl Scheduled { - pub(super) fn on_workflow_task_started(self) -> WorkflowTaskMachineTransition { - unimplemented!() + pub(super) fn on_workflow_task_started( + self, + WFTStartedDat { + current_time_millis, + started_event_id, + }: WFTStartedDat, + ) -> WorkflowTaskMachineTransition { + WorkflowTaskMachineTransition::ok( + vec![], + Started { + current_time_millis, + started_event_id, + }, + ) } - pub(super) fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { - unimplemented!() +} + +impl From for Scheduled { + fn from(_: Created) -> Self { + Self::default() } } -#[derive(Default, Clone)] -pub(super) struct Started {} +#[derive(Clone)] +pub(super) struct Started { + /// Started event's timestamp + current_time_millis: SystemTime, + /// Started event's id + started_event_id: i64, +} impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { @@ -55,10 +128,17 @@ impl Started { pub(super) fn on_workflow_task_failed(self) -> WorkflowTaskMachineTransition { unimplemented!() } - pub(super) fn on_workflow_task_timed_out(self) -> WorkflowTaskMachineTransition { - unimplemented!() - } } #[derive(Default, Clone)] pub(super) struct TimedOut {} +impl From for TimedOut { + fn from(_: Scheduled) -> Self { + Self::default() + } +} +impl From for TimedOut { + fn from(_: Started) -> Self { + Self::default() + } +} From 2120430424f45e3d9ac5093c5d618d689de3e83f Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 17:57:07 -0800 Subject: [PATCH 08/59] Saving progress. Halfway through detangling workflow task machine callbacks. --- fsm/state_machine_procmacro/src/lib.rs | 5 + fsm/state_machine_trait/src/lib.rs | 14 +++ src/machines/mod.rs | 34 ++++-- src/machines/timer_state_machine.rs | 53 +++++++-- src/machines/workflow_machines.rs | 112 ++++++++++++++------ src/machines/workflow_task_state_machine.rs | 21 +++- 6 files changed, 190 insertions(+), 49 deletions(-) diff --git a/fsm/state_machine_procmacro/src/lib.rs b/fsm/state_machine_procmacro/src/lib.rs index 25eac2a2f..6a5056bb5 100644 --- a/fsm/state_machine_procmacro/src/lib.rs +++ b/fsm/state_machine_procmacro/src/lib.rs @@ -340,6 +340,7 @@ impl StateMachineDefinition { } }); let name = &self.name; + let name_str = &self.name.to_string(); let state_enum_name = Ident::new(&format!("{}State", name), name.span()); // If user has not defined any shared state, use the unit type. let shared_state_type = self @@ -480,6 +481,10 @@ impl StateMachineDefinition { type Event = #events_enum_name; type Command = #cmd_type; + fn name(&self) -> &str { + #name_str + } + fn on_event(self, event: #events_enum_name) -> ::rustfsm::TransitionResult { match self.state { diff --git a/fsm/state_machine_trait/src/lib.rs b/fsm/state_machine_trait/src/lib.rs index 77760c2d6..364f7e5c9 100644 --- a/fsm/state_machine_trait/src/lib.rs +++ b/fsm/state_machine_trait/src/lib.rs @@ -56,6 +56,8 @@ pub trait StateMachine: Sized { } } + fn name(&self) -> &str; + /// Returns the current state of the machine fn state(&self) -> &Self::State; fn set_state(&mut self, new_state: Self::State); @@ -165,6 +167,18 @@ where } } + /// Produce a transition with commands relying on [Default] for the destination state's value + pub fn commands(commands: CI) -> Self + where + CI: IntoIterator, + DestState: Into + Default, + { + Self::OkNoShare { + commands: commands.into_iter().collect(), + new_state: DestState::default().into(), + } + } + /// Produce a transition with no commands relying on [Default] for the destination state's /// value pub fn default() -> Self diff --git a/src/machines/mod.rs b/src/machines/mod.rs index f3557d7b3..a4c6bc024 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -41,7 +41,10 @@ use crate::{ }, }; use rustfsm::{MachineError, StateMachine}; -use std::convert::{TryFrom, TryInto}; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, +}; // TODO: May need to be our SDKWFCommand type pub(crate) type MachineCommand = Command; @@ -49,7 +52,8 @@ pub(crate) type MachineCommand = Command; /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. -trait TemporalStateMachine: CheckStateMachineInFinal { +trait TemporalStateMachine: CheckStateMachineInFinal + IsWfTaskMachine { + fn name(&self) -> &str; fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError>; fn handle_event( &mut self, @@ -64,15 +68,24 @@ trait TemporalStateMachine: CheckStateMachineInFinal { impl TemporalStateMachine for SM where - SM: StateMachine + CheckStateMachineInFinal + Clone, + SM: StateMachine + CheckStateMachineInFinal + IsWfTaskMachine + Clone, ::Event: TryFrom, ::Event: TryFrom, + ::Command: Debug, ::Error: Into + 'static + Send + Sync, { + fn name(&self) -> &str { + ::name(self) + } + fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError> { + dbg!(self.name(), "handling command", command_type); if let Ok(converted_command) = command_type.try_into() { match self.on_event_mut(converted_command) { - Ok(_) => Ok(()), + Ok(c) => { + dbg!(c); + Ok(()) + } Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedCommand(command_type)) } @@ -88,8 +101,8 @@ where event: &HistoryEvent, _has_next_event: bool, ) -> Result<(), WFMachinesError> { - // TODO: `has_next_event` is *only* used by WorkflowTaskStateMachine. Figure out how - // to deal with it. + // TODO: Real tracing + dbg!(self.name(), "handling event", &event); if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { Ok(_) => Ok(()), @@ -120,6 +133,15 @@ where } } +/// Only should be implemented (with `true`) for [WorkflowTaskMachine] +// This is a poor but effective substitute for specialization. Remove when it's finally +// stabilized: https://github.com/rust-lang/rust/issues/31844 +trait IsWfTaskMachine { + fn is_wf_task_machine(&self) -> bool { + false + } +} + /// A command which can be cancelled #[derive(Debug, Clone)] pub struct CancellableCommand { diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 0e9ef05db..23cf3fd94 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -12,7 +12,7 @@ use crate::{ }, }, }; -use rustfsm::{fsm, TransitionResult}; +use rustfsm::{fsm, StateMachine, TransitionResult}; use std::convert::TryFrom; fsm! { @@ -37,6 +37,23 @@ fsm! { StartCommandRecorded --(Cancel, on_cancel) --> CancelTimerCommandCreated; } +impl TimerMachine { + pub(crate) fn new(attribs: StartTimerCommandAttributes) -> Self { + Self { + state: Created {}.into(), + shared_state: SharedState { + timer_attributes: attribs, + }, + } + } + + /// Should generally be called immediately after constructing a timer + pub(crate) fn schedule(&mut self) -> Vec { + self.on_event_mut(TimerMachineEvents::Schedule) + .expect("Scheduling timers doesn't fail") + } +} + impl TryFrom for TimerMachineEvents { type Error = (); @@ -85,12 +102,22 @@ impl SharedState { } } +#[derive(Debug)] pub(super) enum TimerCommand { StartTimer(CancellableCommand), CancelTimer(/* TODO: Command attribs */), ProduceHistoryEvent(HistoryEvent), } +impl From for CancellableCommand { + fn from(t: TimerCommand) -> Self { + match t { + TimerCommand::StartTimer(c) => c, + _ => unimplemented!(), + } + } +} + #[derive(Default, Clone)] pub(super) struct CancelTimerCommandCreated {} impl CancelTimerCommandCreated { @@ -177,14 +204,12 @@ impl StartCommandRecorded { #[cfg(test)] mod test { - use crate::machines::workflow_machines::WorkflowMachines; + use super::*; use crate::{ - machines::test_help::TestHistoryBuilder, - protos::temporal::api::{ - enums::v1::EventType, - history::{v1::history_event::Attributes, v1::TimerFiredEventAttributes}, - }, + machines::{test_help::TestHistoryBuilder, workflow_machines::WorkflowMachines}, + protos::temporal::api::history::v1::TimerFiredEventAttributes, }; + use std::time::Duration; #[test] fn test_fire_happy_path() { @@ -202,13 +227,18 @@ mod test { */ let mut t = TestHistoryBuilder::default(); let mut state_machines = WorkflowMachines::new(); + state_machines.new_timer(StartTimerCommandAttributes { + timer_id: "Sometimer".to_string(), + start_to_fire_timeout: Some(Duration::from_secs(5).into()), + ..Default::default() + }); t.add_by_type(EventType::WorkflowExecutionStarted); t.add_workflow_task(); let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); t.add( EventType::TimerFired, - Attributes::TimerFiredEventAttributes(TimerFiredEventAttributes { + history_event::Attributes::TimerFiredEventAttributes(TimerFiredEventAttributes { started_event_id: timer_started_event_id, timer_id: "timer1".to_string(), ..Default::default() @@ -216,7 +246,10 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); assert_eq!(2, t.get_workflow_task_count(None).unwrap()); - let commands = t.handle_workflow_task_take_cmds(&mut state_machines, Some(1)); - dbg!(commands); + let commands = t + .handle_workflow_task_take_cmds(&mut state_machines, Some(1)) + .unwrap(); + dbg!(&commands); + assert_eq!(commands.len(), 1); } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 3b2127642..08aac392b 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,22 +1,26 @@ use crate::{ machines::{ - workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, MachineCommand, - TemporalStateMachine, + timer_state_machine::TimerMachine, workflow_task_state_machine::WorkflowTaskMachine, + CancellableCommand, MachineCommand, TemporalStateMachine, }, protos::temporal::api::{ + command::v1::StartTimerCommandAttributes, enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent}, }, }; -use std::collections::{hash_map::Entry, HashMap, VecDeque}; -use std::error::Error; +use std::{ + collections::{hash_map::Entry, HashMap, VecDeque}, + convert::TryInto, + error::Error, +}; type Result = std::result::Result; #[derive(Default)] pub(crate) struct WorkflowMachines { - /// The event id of the last event in the history which is expected to be startedEventId unless - /// it is replay from a JSON file. + /// The event id of the last wf task started event in the history which is expected to be + /// [current_started_event_id] except during replay. workflow_task_started_event_id: i64, /// EventId of the last handled WorkflowTaskStarted event current_started_event_id: i64, @@ -33,6 +37,9 @@ pub(crate) struct WorkflowMachines { /// Queued commands which have been produced by machines and await processing commands: VecDeque, + /// Commands generated by the currently processed workflow task. It is a queue as commands can + /// be added (due to marker based commands) while iterating over already added commands. + current_wf_task_commands: VecDeque, } #[derive(thiserror::Error, Debug)] @@ -53,6 +60,15 @@ impl WorkflowMachines { Self::default() } + /// Create a new timer + // TODO: Return cancellation callback? + pub(crate) fn new_timer(&mut self, attribs: StartTimerCommandAttributes) { + let mut timer = TimerMachine::new(attribs); + let commands = timer.schedule(); + self.current_wf_task_commands + .extend(commands.into_iter().map(Into::into)); + } + /// Returns the id of the last seen WorkflowTaskStarted event pub(super) fn get_last_started_event_id(&self) -> i64 { self.current_started_event_id @@ -87,6 +103,13 @@ impl WorkflowMachines { Some(Entry::Occupied(mut sme)) => { let sm = sme.get_mut(); sm.handle_event(event, has_next_event)?; + + // The workflow task machine has a little bit of special handling + if sm.is_wf_task_machine() { + // TODO: Make me work + //self.wf_task_machine_special_handling(sm, event, has_next_event)?; + } + if sm.is_final_state() { sme.remove(); } @@ -113,30 +136,6 @@ impl WorkflowMachines { event: &HistoryEvent, has_next_event: bool, ) -> Result<()> { - // switch (event.getEventType()) { - // case EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: - // this.currentRunId = - // event.getWorkflowExecutionStartedEventAttributes().getOriginalExecutionRunId(); - // callbacks.start(event); - // break; - // case EVENT_TYPE_WORKFLOW_TASK_SCHEDULED: - // WorkflowTaskStateMachine c = - // WorkflowTaskStateMachine.newInstance( - // workflowTaskStartedEventId, new WorkflowTaskCommandsListener()); - // c.handleEvent(event, hasNextEvent); - // stateMachines.put(event.getEventId(), c); - // break; - // case EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED: - // callbacks.signal(event); - // break; - // case EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED: - // callbacks.cancel(event); - // break; - // case UNRECOGNIZED: - // break; - // default: - // throw new IllegalArgumentException("Unexpected event:" + event); - // } match EventType::from_i32(event.event_type) { Some(EventType::WorkflowExecutionStarted) => { if let Some(history_event::Attributes::WorkflowExecutionStartedEventAttributes( @@ -154,6 +153,7 @@ impl WorkflowMachines { } } Some(EventType::WorkflowTaskScheduled) => { + // This normally takes a listener which drives event loop on wf task started let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); wf_task_sm.handle_event(event, has_next_event)?; self.machines_by_id @@ -187,4 +187,56 @@ impl WorkflowMachines { self.workflow_task_started_event_id = workflow_task_started_event_id; self.replaying = previous_started_event_id > 0; } + + /// Workflow task machines require a bit of special handling + fn wf_task_machine_special_handling( + &mut self, + machine: &mut Box, + event: &HistoryEvent, + has_next_event: bool, + ) -> Result<(), WFMachinesError> { + match EventType::from_i32(event.event_type) { + Some(EventType::WorkflowTaskStarted) => { + // Workflow task machines self-complete if this is the last event + // TODO: Does first clause from java matter? + // if (currentEvent.getEventId() >= workflowTaskStartedEventId && !hasNextEvent) + // It does - it will be false during replay + if !has_next_event { + let mut completion = HistoryEvent::default(); + completion.event_type = EventType::WorkflowTaskCompleted as i32; + machine.handle_event( + &completion.try_into().ok().expect("Manually constructed"), + has_next_event, + ) + } else { + Ok(()) + } + } + Some(EventType::WorkflowTaskCompleted) => { + // // If some new commands are pending and there are no more command events. + // for (CancellableCommand cancellableCommand : commands) { + // if (cancellableCommand == null) { + // break; + // } + // cancellableCommand.handleWorkflowTaskStarted(); + // } + // // Give local activities a chance to recreate their requests if they were lost due + // // to the last workflow task failure. The loss could happen only the last workflow task + // // was forcibly created by setting forceCreate on RespondWorkflowTaskCompletedRequest. + // if (nonProcessedWorkflowTask) { + // for (LocalActivityStateMachine value : localActivityMap.values()) { + // value.nonReplayWorkflowTaskStarted(); + // } + // } + // WorkflowStateMachines.this.currentStartedEventId = startedEventId; + // setCurrentTimeMillis(currentTimeMillis); + // eventLoop(); + + // TODO: We do in fact need to bubble this up -- this all runs in WorkflowMachines + + Ok(()) + } + _ => Ok(()), + } + } } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 7641be219..ff74820c4 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,5 +1,5 @@ use crate::{ - machines::workflow_machines::WFMachinesError, + machines::{workflow_machines::WFMachinesError, IsWfTaskMachine}, protos::temporal::api::{ enums::v1::{CommandType, EventType}, history::v1::HistoryEvent, @@ -68,10 +68,14 @@ impl TryFrom for WorkflowTaskMachineEvents { #[derive(Debug, Clone)] pub(super) struct SharedState { + // TODO: This can be removed I think since all the things that need it got pushed up one layer wf_task_started_event_id: i64, } -pub(super) enum WorkflowTaskCommand {} +#[derive(Debug)] +pub(super) enum WorkflowTaskCommand { + WFTaskStarted { event_id: i64, time: SystemTime }, +} #[derive(Default, Clone)] pub(super) struct Completed {} @@ -123,7 +127,12 @@ pub(super) struct Started { impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { - unimplemented!() + WorkflowTaskMachineTransition::commands::<_, Completed>(vec![ + WorkflowTaskCommand::WFTaskStarted { + event_id: self.started_event_id, + time: self.current_time_millis, + }, + ]) } pub(super) fn on_workflow_task_failed(self) -> WorkflowTaskMachineTransition { unimplemented!() @@ -142,3 +151,9 @@ impl From for TimedOut { Self::default() } } + +impl IsWfTaskMachine for WorkflowTaskMachine { + fn is_wf_task_machine(&self) -> bool { + true + } +} From 911db1980679e6679703c0a6e629c3ec24a887c4 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 19:46:17 -0800 Subject: [PATCH 09/59] Annoying function factoring --- src/machines/workflow_machines.rs | 61 +++++++++++++++++-------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 08aac392b..d34afda73 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -10,7 +10,7 @@ use crate::{ }, }; use std::{ - collections::{hash_map::Entry, HashMap, VecDeque}, + collections::{HashMap, VecDeque}, convert::TryInto, error::Error, }; @@ -96,32 +96,28 @@ impl WorkflowMachines { self.replaying = false; } - match event - .get_initial_command_event_id() - .map(|id| self.machines_by_id.entry(id)) - { - Some(Entry::Occupied(mut sme)) => { - let sm = sme.get_mut(); - sm.handle_event(event, has_next_event)?; - - // The workflow task machine has a little bit of special handling - if sm.is_wf_task_machine() { - // TODO: Make me work - //self.wf_task_machine_special_handling(sm, event, has_next_event)?; - } + match event.get_initial_command_event_id() { + Some(initial_cmd_id) => { + if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { + sm.handle_event(event, has_next_event)?; - if sm.is_final_state() { - sme.remove(); - } - } - Some(Entry::Vacant(_)) => { - error!( - "During event handling, this event had an initial command ID but \ + // The workflow task machine has a little bit of special handling + if sm.is_wf_task_machine() { + Self::wf_task_machine_special_handling(sm, event, has_next_event)?; + } + + if sm.is_final_state() { + self.machines_by_id.remove(&initial_cmd_id); + } + } else { + error!( + "During event handling, this event had an initial command ID but \ we could not find a matching state machine! Event: {:?}", - event - ); + event + ); + } } - _ => self.handle_non_stateful_event(event, has_next_event)?, + None => self.handle_non_stateful_event(event, has_next_event)?, } Ok(()) @@ -190,7 +186,6 @@ impl WorkflowMachines { /// Workflow task machines require a bit of special handling fn wf_task_machine_special_handling( - &mut self, machine: &mut Box, event: &HistoryEvent, has_next_event: bool, @@ -201,7 +196,18 @@ impl WorkflowMachines { // TODO: Does first clause from java matter? // if (currentEvent.getEventId() >= workflowTaskStartedEventId && !hasNextEvent) // It does - it will be false during replay - if !has_next_event { + let wf_task_started_id = match &event.attributes { + Some(history_event::Attributes::WorkflowTaskStartedEventAttributes(a)) => { + a.scheduled_event_id + } + _ => { + return Err(WFMachinesError::MalformedEvent( + event.clone(), + "".to_string(), + )) + } + }; + if !has_next_event && event.event_id >= wf_task_started_id { let mut completion = HistoryEvent::default(); completion.event_type = EventType::WorkflowTaskCompleted as i32; machine.handle_event( @@ -213,6 +219,7 @@ impl WorkflowMachines { } } Some(EventType::WorkflowTaskCompleted) => { + // TODO: This stuff will need to be passed in independently of self // // If some new commands are pending and there are no more command events. // for (CancellableCommand cancellableCommand : commands) { // if (cancellableCommand == null) { @@ -232,8 +239,6 @@ impl WorkflowMachines { // setCurrentTimeMillis(currentTimeMillis); // eventLoop(); - // TODO: We do in fact need to bubble this up -- this all runs in WorkflowMachines - Ok(()) } _ => Ok(()), From 2de4a50eed1a2be174a42cfd92910f3c270bed46 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 22:53:16 -0800 Subject: [PATCH 10/59] Cleanup, got to timer started command --- src/machines/mod.rs | 23 +++- src/machines/workflow_machines.rs | 132 ++++++++++---------- src/machines/workflow_task_state_machine.rs | 26 ++-- 3 files changed, 100 insertions(+), 81 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index a4c6bc024..ba753a67c 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -44,11 +44,22 @@ use rustfsm::{MachineError, StateMachine}; use std::{ convert::{TryFrom, TryInto}, fmt::Debug, + time::SystemTime, }; // TODO: May need to be our SDKWFCommand type pub(crate) type MachineCommand = Command; +#[derive(Debug)] +pub(crate) enum TSMCommand { + /// Issed by the [WorkflowTaskMachine] to trigger the event loop + WFTaskStartedTrigger { + event_id: i64, + time: SystemTime, + only_if_last_event: bool, + }, +} + /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. @@ -59,7 +70,7 @@ trait TemporalStateMachine: CheckStateMachineInFinal + IsWfTaskMachine { &mut self, event: &HistoryEvent, has_next_event: bool, - ) -> Result<(), WFMachinesError>; + ) -> Result, WFMachinesError>; // TODO: This is a weird one that only applies to version state machine. Introduce only if // needed. Ideally handle differently. @@ -72,6 +83,8 @@ where ::Event: TryFrom, ::Event: TryFrom, ::Command: Debug, + // TODO: Do we really need this bound? Check back and see how many fsms really issue them this way + ::Command: Into, ::Error: Into + 'static + Send + Sync, { fn name(&self) -> &str { @@ -100,12 +113,15 @@ where &mut self, event: &HistoryEvent, _has_next_event: bool, - ) -> Result<(), WFMachinesError> { + ) -> Result, WFMachinesError> { // TODO: Real tracing dbg!(self.name(), "handling event", &event); if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { - Ok(_) => Ok(()), + Ok(c) => { + dbg!(&c); + Ok(c.into_iter().map(Into::into).collect()) + } Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedEvent(event.clone())) } @@ -133,6 +149,7 @@ where } } +//TODO: Remove now? /// Only should be implemented (with `true`) for [WorkflowTaskMachine] // This is a poor but effective substitute for specialization. Remove when it's finally // stabilized: https://github.com/rust-lang/rust/issues/31844 diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index d34afda73..47420d6ba 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,3 +1,4 @@ +use crate::machines::TSMCommand; use crate::{ machines::{ timer_state_machine::TimerMachine, workflow_task_state_machine::WorkflowTaskMachine, @@ -11,8 +12,8 @@ use crate::{ }; use std::{ collections::{HashMap, VecDeque}, - convert::TryInto, error::Error, + time::SystemTime, }; type Result = std::result::Result; @@ -30,6 +31,8 @@ pub(crate) struct WorkflowMachines { replaying: bool, /// Identifies the current run and is used as a seed for faux-randomness. current_run_id: String, + /// The current workflow time if it has been established + current_wf_time: Option, /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. @@ -99,15 +102,40 @@ impl WorkflowMachines { match event.get_initial_command_event_id() { Some(initial_cmd_id) => { if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { - sm.handle_event(event, has_next_event)?; - - // The workflow task machine has a little bit of special handling - if sm.is_wf_task_machine() { - Self::wf_task_machine_special_handling(sm, event, has_next_event)?; - } - - if sm.is_final_state() { - self.machines_by_id.remove(&initial_cmd_id); + let commands = sm.handle_event(event, has_next_event)?.into_iter(); + for cmd in commands { + match cmd { + TSMCommand::WFTaskStartedTrigger { + event_id, + time, + only_if_last_event, + } if !only_if_last_event || !has_next_event => { + dbg!("In task started trigger"); + // TODO: Seems to only matter for version machine. Figure out then. + // // If some new commands are pending and there are no more command events. + // for (CancellableCommand cancellableCommand : commands) { + // if (cancellableCommand == null) { + // break; + // } + // cancellableCommand.handleWorkflowTaskStarted(); + // } + + // TODO: Local activity machines + // // Give local activities a chance to recreate their requests if they were lost due + // // to the last workflow task failure. The loss could happen only the last workflow task + // // was forcibly created by setting forceCreate on RespondWorkflowTaskCompletedRequest. + // if (nonProcessedWorkflowTask) { + // for (LocalActivityStateMachine value : localActivityMap.values()) { + // value.nonReplayWorkflowTaskStarted(); + // } + // } + + self.current_started_event_id = event_id; + self.set_current_time(time); + self.event_loop(); + } + _ => (), + } } } else { error!( @@ -116,6 +144,13 @@ impl WorkflowMachines { event ); } + + // Have to fetch machine again here to avoid borrowing self mutably twice + if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { + if sm.is_final_state() { + self.machines_by_id.remove(&initial_cmd_id); + } + } } None => self.handle_non_stateful_event(event, has_next_event)?, } @@ -184,64 +219,25 @@ impl WorkflowMachines { self.replaying = previous_started_event_id > 0; } - /// Workflow task machines require a bit of special handling - fn wf_task_machine_special_handling( - machine: &mut Box, - event: &HistoryEvent, - has_next_event: bool, - ) -> Result<(), WFMachinesError> { - match EventType::from_i32(event.event_type) { - Some(EventType::WorkflowTaskStarted) => { - // Workflow task machines self-complete if this is the last event - // TODO: Does first clause from java matter? - // if (currentEvent.getEventId() >= workflowTaskStartedEventId && !hasNextEvent) - // It does - it will be false during replay - let wf_task_started_id = match &event.attributes { - Some(history_event::Attributes::WorkflowTaskStartedEventAttributes(a)) => { - a.scheduled_event_id - } - _ => { - return Err(WFMachinesError::MalformedEvent( - event.clone(), - "".to_string(), - )) - } - }; - if !has_next_event && event.event_id >= wf_task_started_id { - let mut completion = HistoryEvent::default(); - completion.event_type = EventType::WorkflowTaskCompleted as i32; - machine.handle_event( - &completion.try_into().ok().expect("Manually constructed"), - has_next_event, - ) - } else { - Ok(()) - } - } - Some(EventType::WorkflowTaskCompleted) => { - // TODO: This stuff will need to be passed in independently of self - // // If some new commands are pending and there are no more command events. - // for (CancellableCommand cancellableCommand : commands) { - // if (cancellableCommand == null) { - // break; - // } - // cancellableCommand.handleWorkflowTaskStarted(); - // } - // // Give local activities a chance to recreate their requests if they were lost due - // // to the last workflow task failure. The loss could happen only the last workflow task - // // was forcibly created by setting forceCreate on RespondWorkflowTaskCompletedRequest. - // if (nonProcessedWorkflowTask) { - // for (LocalActivityStateMachine value : localActivityMap.values()) { - // value.nonReplayWorkflowTaskStarted(); - // } - // } - // WorkflowStateMachines.this.currentStartedEventId = startedEventId; - // setCurrentTimeMillis(currentTimeMillis); - // eventLoop(); - - Ok(()) - } - _ => Ok(()), + fn set_current_time(&mut self, time: SystemTime) -> SystemTime { + if self.current_wf_time.map(|t| t < time).unwrap_or(true) { + self.current_wf_time = Some(time); + } + self.current_wf_time + .expect("We have just ensured this is populated") + } + + fn event_loop(&mut self) { + // todo: callbacks.eventLoop() + self.prepare_commands() + } + + fn prepare_commands(&mut self) { + while let Some(c) = self.current_wf_task_commands.pop_front() { + // TODO - some special case stuff that can maybe be managed differently? + // handleCommand should be called even on canceled ones to support mutableSideEffect + // command.handleCommand(command.getCommandType()); + self.commands.push_back(c); } } } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index ff74820c4..2860c53e3 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,3 +1,4 @@ +use crate::machines::TSMCommand; use crate::{ machines::{workflow_machines::WFMachinesError, IsWfTaskMachine}, protos::temporal::api::{ @@ -10,13 +11,13 @@ use std::{convert::TryFrom, time::SystemTime}; fsm! { pub(super) name WorkflowTaskMachine; - command WorkflowTaskCommand; + command TSMCommand; error WFMachinesError; shared_state SharedState; Created --(WorkflowTaskScheduled) --> Scheduled; - Scheduled --(WorkflowTaskStarted(WFTStartedDat), on_workflow_task_started) --> Started; + Scheduled --(WorkflowTaskStarted(WFTStartedDat), shared on_workflow_task_started) --> Started; Scheduled --(WorkflowTaskTimedOut) --> TimedOut; Started --(WorkflowTaskCompleted, on_workflow_task_completed) --> Completed; @@ -68,15 +69,9 @@ impl TryFrom for WorkflowTaskMachineEvents { #[derive(Debug, Clone)] pub(super) struct SharedState { - // TODO: This can be removed I think since all the things that need it got pushed up one layer wf_task_started_event_id: i64, } -#[derive(Debug)] -pub(super) enum WorkflowTaskCommand { - WFTaskStarted { event_id: i64, time: SystemTime }, -} - #[derive(Default, Clone)] pub(super) struct Completed {} @@ -96,13 +91,23 @@ pub(super) struct WFTStartedDat { impl Scheduled { pub(super) fn on_workflow_task_started( self, + shared: SharedState, WFTStartedDat { current_time_millis, started_event_id, }: WFTStartedDat, ) -> WorkflowTaskMachineTransition { + let cmds = if started_event_id >= shared.wf_task_started_event_id { + vec![TSMCommand::WFTaskStartedTrigger { + event_id: started_event_id, + time: current_time_millis, + only_if_last_event: true, + }] + } else { + vec![] + }; WorkflowTaskMachineTransition::ok( - vec![], + cmds, Started { current_time_millis, started_event_id, @@ -128,9 +133,10 @@ pub(super) struct Started { impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { WorkflowTaskMachineTransition::commands::<_, Completed>(vec![ - WorkflowTaskCommand::WFTaskStarted { + TSMCommand::WFTaskStartedTrigger { event_id: self.started_event_id, time: self.current_time_millis, + only_if_last_event: false, }, ]) } From eeb723a223c88268f97c6f51d0eee6288792118f Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 22:54:31 -0800 Subject: [PATCH 11/59] Remove unneded trait --- src/machines/mod.rs | 14 ++------------ src/machines/workflow_task_state_machine.rs | 9 +-------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index ba753a67c..9628b7992 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -63,7 +63,7 @@ pub(crate) enum TSMCommand { /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. -trait TemporalStateMachine: CheckStateMachineInFinal + IsWfTaskMachine { +trait TemporalStateMachine: CheckStateMachineInFinal { fn name(&self) -> &str; fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError>; fn handle_event( @@ -79,7 +79,7 @@ trait TemporalStateMachine: CheckStateMachineInFinal + IsWfTaskMachine { impl TemporalStateMachine for SM where - SM: StateMachine + CheckStateMachineInFinal + IsWfTaskMachine + Clone, + SM: StateMachine + CheckStateMachineInFinal + Clone, ::Event: TryFrom, ::Event: TryFrom, ::Command: Debug, @@ -149,16 +149,6 @@ where } } -//TODO: Remove now? -/// Only should be implemented (with `true`) for [WorkflowTaskMachine] -// This is a poor but effective substitute for specialization. Remove when it's finally -// stabilized: https://github.com/rust-lang/rust/issues/31844 -trait IsWfTaskMachine { - fn is_wf_task_machine(&self) -> bool { - false - } -} - /// A command which can be cancelled #[derive(Debug, Clone)] pub struct CancellableCommand { diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 2860c53e3..66a3e7626 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,6 +1,5 @@ -use crate::machines::TSMCommand; use crate::{ - machines::{workflow_machines::WFMachinesError, IsWfTaskMachine}, + machines::{workflow_machines::WFMachinesError, TSMCommand}, protos::temporal::api::{ enums::v1::{CommandType, EventType}, history::v1::HistoryEvent, @@ -157,9 +156,3 @@ impl From for TimedOut { Self::default() } } - -impl IsWfTaskMachine for WorkflowTaskMachine { - fn is_wf_task_machine(&self) -> bool { - true - } -} From 5b46c78d99dfe161ddee39deae9d8746c8ef6c78 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 20 Jan 2021 22:58:47 -0800 Subject: [PATCH 12/59] Now need to implement handle command event! brr brr brr --- src/machines/timer_state_machine.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 23cf3fd94..0f062a13f 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -251,5 +251,11 @@ mod test { .unwrap(); dbg!(&commands); assert_eq!(commands.len(), 1); + assert_eq!(commands[0].command_type, CommandType::StartTimer as i32); + let commands = t + .handle_workflow_task_take_cmds(&mut state_machines, Some(2)) + .unwrap(); + dbg!(&commands); + assert_eq!(commands.len(), 1); } } From 66dc25929bfb458bedca813b28e63da4eac47a20 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 08:57:58 -0800 Subject: [PATCH 13/59] Moving to deskstop --- src/machines/workflow_machines.rs | 35 +++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 47420d6ba..009185ca6 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -56,6 +56,8 @@ pub(crate) enum WFMachinesError { // TODO: Pretty sure can remove this if no machines need some specific error #[error("Underlying machine error {0:?}")] Underlying(#[from] Box), + #[error("No command was scheduled for event {0:?}")] + NoCommandScheduledForEvent(HistoryEvent), } impl WorkflowMachines { @@ -158,8 +160,37 @@ impl WorkflowMachines { Ok(()) } - fn handle_command_event(&mut self, _event: &HistoryEvent) { - unimplemented!() + /// A command event is an event which is generated from a command emitted by a past decision. + /// Each command has a correspondent event. For example ScheduleActivityTaskCommand + /// is recorded to the history as ActivityTaskScheduledEvent. + /// + /// Command events always follow WorkflowTaskCompletedEvent. + /// + /// The handling consists from verifying that the next command in the commands queue matches the + /// event, command state machine is notified about the event and the command is removed from the + /// commands queue. + fn handle_command_event(&mut self, event: &HistoryEvent) -> Result<()> { + // TODO: + // if (handleLocalActivityMarker(event)) { + // return; + // } + let mut maybe_command = self.commands.get(0); + if maybe_command.is_none() { + return Err(WFMachinesError::NoCommandScheduledForEvent(event.clone())); + } + while let Some(c) = maybe_command { + // TODO: More special handling for version machine + // handleVersionMarker can skip a marker event if the getVersion call was removed. + // In this case we don't want to consume a command. + // That's why get is used instead of pop_front + + // Consume command + self.commands.pop_front(); + if c.command.is_some() { + break; + } + } + Ok(()) } fn handle_non_stateful_event( From aefb9b9fc7379cb0af0da76c2b53449d948e4878 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 10:32:11 -0800 Subject: [PATCH 14/59] Managed to get past gnarly borrow checker problems --- build.rs | 4 ++ src/machines/mod.rs | 35 ++++++++---- src/machines/timer_state_machine.rs | 84 ++++++++++++++--------------- src/machines/workflow_machines.rs | 56 +++++++++++++------ 4 files changed, 109 insertions(+), 70 deletions(-) diff --git a/build.rs b/build.rs index 313de9e77..282ce147a 100644 --- a/build.rs +++ b/build.rs @@ -10,6 +10,10 @@ fn main() -> Result<(), Box> { "temporal.api.history.v1.HistoryEvent.attributes", "#[derive(::derive_more::From)]", ) + .type_attribute( + "temporal.api.command.v1.Command.attributes", + "#[derive(::derive_more::From)]", + ) .compile( &[ "protos/local/core_interface.proto", diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 9628b7992..4fbeb067b 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -40,10 +40,12 @@ use crate::{ command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, }, }; +use prost::alloc::fmt::Formatter; use rustfsm::{MachineError, StateMachine}; use std::{ convert::{TryFrom, TryInto}, fmt::Debug, + rc::Rc, time::SystemTime, }; @@ -58,6 +60,17 @@ pub(crate) enum TSMCommand { time: SystemTime, only_if_last_event: bool, }, + /// Issued by state machines to create commands + AddCommand(AddCommand), + // TODO: This is not quite right. Should be more like "notify completion". Need to investigate + // more examples + ProduceHistoryEvent(HistoryEvent), +} + +#[derive(Debug, ::derive_more::From)] +pub(crate) struct AddCommand { + /// The protobuf command + pub(crate) command: Command, } /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. @@ -149,23 +162,25 @@ where } } -/// A command which can be cancelled +/// A command which can be cancelled, associated with some state machine that produced it #[derive(Debug, Clone)] -pub struct CancellableCommand { - /// The inner protobuf command, if None, command has been cancelled - command: Option, +enum CancellableCommand { + Cancelled, + Active { + /// The inner protobuf command, if None, command has been cancelled + command: MachineCommand, + machine: Rc, + }, } impl CancellableCommand { pub(super) fn cancel(&mut self) { - self.command = None; + *self = CancellableCommand::Cancelled; } } -impl From for CancellableCommand { - fn from(command: Command) -> Self { - Self { - command: Some(command), - } +impl Debug for dyn TemporalStateMachine { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) } } diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 0f062a13f..e70e5a8e2 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,12 +1,14 @@ #![allow(clippy::large_enum_variant)] -use crate::machines::workflow_machines::WFMachinesError; use crate::{ - machines::CancellableCommand, + machines::{workflow_machines::WFMachinesError, AddCommand, TSMCommand}, protos::{ coresdk::HistoryEventId, temporal::api::{ - command::v1::{command::Attributes, Command, StartTimerCommandAttributes}, + command::v1::{ + command::Attributes, CancelTimerCommandAttributes, Command, + StartTimerCommandAttributes, + }, enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent, TimerCanceledEventAttributes}, }, @@ -17,7 +19,7 @@ use std::convert::TryFrom; fsm! { pub(super) name TimerMachine; - command TimerCommand; + command TSMCommand; error WFMachinesError; shared_state SharedState; @@ -34,7 +36,7 @@ fsm! { StartCommandCreated --(Cancel, shared on_cancel) --> Canceled; StartCommandRecorded --(TimerFired(HistoryEvent), on_timer_fired) --> Fired; - StartCommandRecorded --(Cancel, on_cancel) --> CancelTimerCommandCreated; + StartCommandRecorded --(Cancel, shared on_cancel) --> CancelTimerCommandCreated; } impl TimerMachine { @@ -47,10 +49,18 @@ impl TimerMachine { } } - /// Should generally be called immediately after constructing a timer - pub(crate) fn schedule(&mut self) -> Vec { - self.on_event_mut(TimerMachineEvents::Schedule) + /// Create a new timer and immediately schedule it + pub(crate) fn new_scheduled(attribs: StartTimerCommandAttributes) -> (Self, AddCommand) { + let mut s = Self::new(attribs); + let cmd = match s + .on_event_mut(TimerMachineEvents::Schedule) .expect("Scheduling timers doesn't fail") + .pop() + { + Some(TSMCommand::AddCommand(c)) => c, + _ => panic!("Timer on_schedule must produce command"), + }; + (s, cmd) } } @@ -85,7 +95,7 @@ pub(super) struct SharedState { } impl SharedState { - fn into_timer_canceled_event_command(self) -> TimerCommand { + fn into_timer_canceled_event_command(self) -> TSMCommand { let attrs = TimerCanceledEventAttributes { identity: "workflow".to_string(), timer_id: self.timer_attributes.timer_id, @@ -98,23 +108,7 @@ impl SharedState { )), ..Default::default() }; - TimerCommand::ProduceHistoryEvent(event) - } -} - -#[derive(Debug)] -pub(super) enum TimerCommand { - StartTimer(CancellableCommand), - CancelTimer(/* TODO: Command attribs */), - ProduceHistoryEvent(HistoryEvent), -} - -impl From for CancellableCommand { - fn from(t: TimerCommand) -> Self { - match t { - TimerCommand::StartTimer(c) => c, - _ => unimplemented!(), - } + TSMCommand::ProduceHistoryEvent(event) } } @@ -147,26 +141,19 @@ impl Created { pub(super) fn on_schedule(self, dat: SharedState) -> TimerMachineTransition { let cmd = Command { command_type: CommandType::StartTimer as i32, - attributes: Some(Attributes::StartTimerCommandAttributes( - dat.timer_attributes, - )), + attributes: Some(dat.timer_attributes.into()), }; - TimerMachineTransition::ok( - vec![TimerCommand::StartTimer(cmd.clone().into())], - StartCommandCreated { - cancellable_command: CancellableCommand::from(cmd), - }, - ) + TimerMachineTransition::commands::<_, StartCommandCreated>(vec![TSMCommand::AddCommand( + cmd.into(), + )]) } } #[derive(Default, Clone)] pub(super) struct Fired {} -#[derive(Clone)] -pub(super) struct StartCommandCreated { - cancellable_command: CancellableCommand, -} +#[derive(Default, Clone)] +pub(super) struct StartCommandCreated {} impl StartCommandCreated { pub(super) fn on_timer_started(self, id: HistoryEventId) -> TimerMachineTransition { @@ -175,8 +162,7 @@ impl StartCommandCreated { } pub(super) fn on_cancel(mut self, dat: SharedState) -> TimerMachineTransition { // Cancel the initial command - which just sets a "canceled" flag in a wrapper of a - // proto command. TODO: Does this make any sense? - let _canceled_cmd = self.cancellable_command.cancel(); + // proto command. TODO: Does this make any sense? - no - propagate up TimerMachineTransition::ok( vec![dat.into_timer_canceled_event_command()], Canceled::default(), @@ -190,13 +176,23 @@ pub(super) struct StartCommandRecorded {} impl StartCommandRecorded { pub(super) fn on_timer_fired(self, event: HistoryEvent) -> TimerMachineTransition { TimerMachineTransition::ok( - vec![TimerCommand::ProduceHistoryEvent(event)], + vec![TSMCommand::ProduceHistoryEvent(event)], Fired::default(), ) } - pub(super) fn on_cancel(self) -> TimerMachineTransition { + pub(super) fn on_cancel(self, dat: SharedState) -> TimerMachineTransition { + let cmd = Command { + command_type: CommandType::CancelTimer as i32, + attributes: Some( + CancelTimerCommandAttributes { + timer_id: dat.timer_attributes.timer_id, + ..Default::default() + } + .into(), + ), + }; TimerMachineTransition::ok( - vec![TimerCommand::CancelTimer()], + vec![TSMCommand::AddCommand(cmd.into())], CancelTimerCommandCreated::default(), ) } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 009185ca6..72ecc5c4b 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -13,6 +13,7 @@ use crate::{ use std::{ collections::{HashMap, VecDeque}, error::Error, + rc::Rc, time::SystemTime, }; @@ -68,10 +69,12 @@ impl WorkflowMachines { /// Create a new timer // TODO: Return cancellation callback? pub(crate) fn new_timer(&mut self, attribs: StartTimerCommandAttributes) { - let mut timer = TimerMachine::new(attribs); - let commands = timer.schedule(); - self.current_wf_task_commands - .extend(commands.into_iter().map(Into::into)); + let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); + let cc = CancellableCommand::Active { + command: add_cmd.command, + machine: Rc::new(timer), + }; + self.current_wf_task_commands.push_back(cc); } /// Returns the id of the last seen WorkflowTaskStarted event @@ -89,7 +92,7 @@ impl WorkflowMachines { has_next_event: bool, ) -> Result<()> { if event.is_command_event() { - self.handle_command_event(event); + self.handle_command_event(event)?; return Ok(()); } @@ -174,21 +177,33 @@ impl WorkflowMachines { // if (handleLocalActivityMarker(event)) { // return; // } - let mut maybe_command = self.commands.get(0); - if maybe_command.is_none() { - return Err(WFMachinesError::NoCommandScheduledForEvent(event.clone())); - } - while let Some(c) = maybe_command { - // TODO: More special handling for version machine + + let mut maybe_command; + loop { // handleVersionMarker can skip a marker event if the getVersion call was removed. // In this case we don't want to consume a command. - // That's why get is used instead of pop_front + // That's why front is used instead of pop_front + maybe_command = self.commands.front_mut(); + let command = if let Some(c) = maybe_command { + c + } else { + return Err(WFMachinesError::NoCommandScheduledForEvent(event.clone())); + }; - // Consume command - self.commands.pop_front(); - if c.command.is_some() { + // TODO: More special handling for version machine - see java + + // Feed the machine the event + if let CancellableCommand::Active { + ref mut machine, .. + } = command + { + // TODO: Remove unwrap + Rc::get_mut(machine).unwrap().handle_event(event, true)?; break; } + + // Consume command + self.commands.pop_front(); } Ok(()) } @@ -235,7 +250,16 @@ impl WorkflowMachines { /// Fetches commands ready for processing from the state machines, removing them from the /// internal command queue. pub(crate) fn take_commands(&mut self) -> Vec { - self.commands.drain(0..).flat_map(|c| c.command).collect() + self.commands + .drain(0..) + .filter_map(|c| { + if let CancellableCommand::Active { command, .. } = c { + Some(command) + } else { + None + } + }) + .collect() } /// Given an event id (possibly zero) of the last successfully executed workflow task and an From fb2671eebe8c62fef22eac9784e3556414259c12 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 11:34:36 -0800 Subject: [PATCH 15/59] So close! Just producing wrong cmd at end --- src/machines/test_help.rs | 29 ++++++++++++++++++----------- src/machines/timer_state_machine.rs | 4 ++++ src/machines/workflow_machines.rs | 9 ++++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 8f5036ad7..ed531e2c6 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -134,7 +134,7 @@ impl TestHistoryBuilder { to_task_index: Option, ) -> Result> { self.handle_workflow_task(wf_machines, to_task_index)?; - Ok(wf_machines.take_commands()) + Ok(wf_machines.get_commands()) } /// Handle a workflow task using the provided [WorkflowMachines] @@ -150,6 +150,7 @@ impl TestHistoryBuilder { let (_, events) = self .events .split_at(wf_machines.get_last_started_event_id() as usize); + dbg!(&events); let mut history = events.iter().peekable(); let hist_info = self.get_history_info(to_task_index)?; @@ -166,16 +167,6 @@ impl TestHistoryBuilder { while let Some(event) = history.next() { let next_event = history.peek(); - if next_event.is_none() { - if event.is_final_wf_execution_event() { - return Ok(()); - } - if started_id != event.event_id { - // TODO: I think this message is a lie - bail!("The last event in the history isn't WF task started"); - } - unreachable!() - } if event.event_type == EventType::WorkflowTaskStarted as i32 { let next_is_completed = next_event.map_or(false, |ne| { @@ -203,6 +194,22 @@ impl TestHistoryBuilder { } wf_machines.handle_event(event, next_event.is_some())?; + + if next_event.is_none() { + if event.is_final_wf_execution_event() { + return Ok(()); + } + if started_id != event.event_id { + // TODO: I think this message is a lie? + bail!( + "The last event in the history (id {}) isn't the last WF task \ + started (id {})", + event.event_id, + started_id + ); + } + unreachable!() + } } Ok(()) diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index e70e5a8e2..9485367a4 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -253,5 +253,9 @@ mod test { .unwrap(); dbg!(&commands); assert_eq!(commands.len(), 1); + assert_eq!( + commands[0].command_type, + CommandType::CompleteWorkflowExecution as i32 + ); } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 72ecc5c4b..6d97f9500 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -247,14 +247,13 @@ impl WorkflowMachines { Ok(()) } - /// Fetches commands ready for processing from the state machines, removing them from the - /// internal command queue. - pub(crate) fn take_commands(&mut self) -> Vec { + /// Fetches commands ready for processing from the state machines + pub(crate) fn get_commands(&mut self) -> Vec { self.commands - .drain(0..) + .iter() .filter_map(|c| { if let CancellableCommand::Active { command, .. } = c { - Some(command) + Some(command.clone()) } else { None } From 20eec95af420eb1c52b493ad61d4466b8e256807 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 12:15:09 -0800 Subject: [PATCH 16/59] Erroneous command is gone. Just need to trigger WF completion --- Cargo.toml | 1 + src/machines/test_help.rs | 2 -- src/machines/timer_state_machine.rs | 2 ++ src/machines/workflow_machines.rs | 33 +++++++++++++++++++++++------ src/protos/mod.rs | 16 +++++++------- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1cccbabdb..e50a7d5fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" anyhow = "1.0" async-trait = "0.1" derive_more = "0.99" +env_logger = "0.8" log = "0.4" prost = "0.7" prost-types = "0.7" diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index ed531e2c6..1b2e7c494 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -150,7 +150,6 @@ impl TestHistoryBuilder { let (_, events) = self .events .split_at(wf_machines.get_last_started_event_id() as usize); - dbg!(&events); let mut history = events.iter().peekable(); let hist_info = self.get_history_info(to_task_index)?; @@ -200,7 +199,6 @@ impl TestHistoryBuilder { return Ok(()); } if started_id != event.event_id { - // TODO: I think this message is a lie? bail!( "The last event in the history (id {}) isn't the last WF task \ started (id {})", diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 9485367a4..8d4f032e8 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -209,6 +209,7 @@ mod test { #[test] fn test_fire_happy_path() { + env_logger::init(); // We don't actually have a way to author workflows in rust yet, but the workflow that would // match up with this is just a wf with one timer in it that fires normally. /* @@ -257,5 +258,6 @@ mod test { commands[0].command_type, CommandType::CompleteWorkflowExecution as i32 ); + // TODO: Timer fired event not ever handled for some reason } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 6d97f9500..5062b4f3a 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -37,7 +37,7 @@ pub(crate) struct WorkflowMachines { /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. - machines_by_id: HashMap>, + machines_by_id: HashMap>, /// Queued commands which have been produced by machines and await processing commands: VecDeque, @@ -107,9 +107,14 @@ impl WorkflowMachines { match event.get_initial_command_event_id() { Some(initial_cmd_id) => { if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { - let commands = sm.handle_event(event, has_next_event)?.into_iter(); + // TODO: Remove unwrap + let commands = Rc::get_mut(sm) + .unwrap() + .handle_event(event, has_next_event)? + .into_iter(); for cmd in commands { match cmd { + // TODO: Does add command need to be handled here? TSMCommand::WFTaskStartedTrigger { event_id, time, @@ -153,6 +158,7 @@ impl WorkflowMachines { // Have to fetch machine again here to avoid borrowing self mutably twice if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { if sm.is_final_state() { + dbg!(sm.name(), "final state"); self.machines_by_id.remove(&initial_cmd_id); } } @@ -179,7 +185,7 @@ impl WorkflowMachines { // } let mut maybe_command; - loop { + let consumed_cmd = loop { // handleVersionMarker can skip a marker event if the getVersion call was removed. // In this case we don't want to consume a command. // That's why front is used instead of pop_front @@ -193,18 +199,31 @@ impl WorkflowMachines { // TODO: More special handling for version machine - see java // Feed the machine the event + let mut break_later = false; if let CancellableCommand::Active { ref mut machine, .. } = command { // TODO: Remove unwrap - Rc::get_mut(machine).unwrap().handle_event(event, true)?; - break; + let out_commands = Rc::get_mut(machine).unwrap().handle_event(event, true)?; + dbg!(&out_commands); + break_later = true; } // Consume command - self.commands.pop_front(); + let cmd = self.commands.pop_front(); + if break_later { + // Unwrap OK since we would have already exited if not present. TODO: Cleanup + break cmd.unwrap(); + } + }; + + // TODO: validate command + + if let CancellableCommand::Active { machine, .. } = consumed_cmd { + self.machines_by_id.insert(event.event_id, machine); } + Ok(()) } @@ -234,7 +253,7 @@ impl WorkflowMachines { let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); wf_task_sm.handle_event(event, has_next_event)?; self.machines_by_id - .insert(event.event_id, Box::new(wf_task_sm)); + .insert(event.event_id, Rc::new(wf_task_sm)); } Some(EventType::WorkflowExecutionSignaled) => { // TODO: Signal callbacks diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 17ef05f81..a0c8dd25b 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -47,18 +47,18 @@ pub mod temporal { if let Some(et) = EventType::from_i32(self.event_type) { match et { EventType::ActivityTaskScheduled + | EventType::ActivityTaskCancelRequested + | EventType::MarkerRecorded + | EventType::RequestCancelExternalWorkflowExecutionInitiated + | EventType::SignalExternalWorkflowExecutionInitiated | EventType::StartChildWorkflowExecutionInitiated + | EventType::TimerCanceled | EventType::TimerStarted - | EventType::WorkflowExecutionCompleted - | EventType::WorkflowExecutionFailed + | EventType::UpsertWorkflowSearchAttributes | EventType::WorkflowExecutionCanceled + | EventType::WorkflowExecutionCompleted | EventType::WorkflowExecutionContinuedAsNew - | EventType::ActivityTaskCancelRequested - | EventType::TimerCanceled - | EventType::RequestCancelExternalWorkflowExecutionInitiated - | EventType::MarkerRecorded - | EventType::SignalExternalWorkflowExecutionInitiated - | EventType::UpsertWorkflowSearchAttributes => true, + | EventType::WorkflowExecutionFailed => true, _ => false, } } else { From 576bfcf4e1e1bcec64fe27f0e35f4611fa432627 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 17:42:59 -0800 Subject: [PATCH 17/59] Things are coming together! Just need to add a complete workflow machine --- src/machines/mod.rs | 57 +++++++++++++++++-- src/machines/timer_state_machine.rs | 88 +++++++++++++++++++++++++---- src/machines/workflow_machines.rs | 69 +++++++++++++++------- 3 files changed, 177 insertions(+), 37 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 4fbeb067b..3d146e7c8 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -37,7 +37,12 @@ mod test_help; use crate::{ machines::workflow_machines::WFMachinesError, protos::temporal::api::{ - command::v1::Command, enums::v1::CommandType, history::v1::HistoryEvent, + command::v1::Command, + enums::v1::CommandType, + history::v1::{ + HistoryEvent, WorkflowExecutionCanceledEventAttributes, + WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, + }, }, }; use prost::alloc::fmt::Formatter; @@ -50,8 +55,39 @@ use std::{ }; // TODO: May need to be our SDKWFCommand type -pub(crate) type MachineCommand = Command; +type MachineCommand = Command; + +/// Implementors of this trait represent something that can (eventually) call into a workflow to +/// drive it, start it, signal it, cancel it, etc. +trait DrivenWorkflow { + /// Start the workflow + fn start( + &self, + attribs: WorkflowExecutionStartedEventAttributes, + ) -> Result, anyhow::Error>; + + /// Iterate the workflow. The workflow driver should execute workflow code until there is + /// nothing left to do. EX: Awaiting an activity/timer, workflow completion. + fn iterate_wf(&self) -> Result, anyhow::Error>; + + /// Signal the workflow + fn signal( + &self, + attribs: WorkflowExecutionSignaledEventAttributes, + ) -> Result<(), anyhow::Error>; + + /// Cancel the workflow + fn cancel( + &self, + attribs: WorkflowExecutionCanceledEventAttributes, + ) -> Result<(), anyhow::Error>; +} +/// These commands are issued by state machines to inform their driver ([WorkflowMachines]) that +/// it may need to take some action. +/// +/// In java this functionality was largely handled via callbacks passed into the state machines +/// which was difficult to follow #[derive(Debug)] pub(crate) enum TSMCommand { /// Issed by the [WorkflowTaskMachine] to trigger the event loop @@ -67,12 +103,23 @@ pub(crate) enum TSMCommand { ProduceHistoryEvent(HistoryEvent), } -#[derive(Debug, ::derive_more::From)] +/// The struct for [WFCommand::AddCommand] +#[derive(Debug, derive_more::From)] pub(crate) struct AddCommand { /// The protobuf command pub(crate) command: Command, } +/// [DrivenWorkflow]s respond with these when called, to indicate what they want to do next. +/// EX: Create a new timer, complete the workflow, etc. +/// +/// TODO: Maybe this and TSMCommand are really the same thing? +#[derive(Debug, derive_more::From)] +enum WFCommand { + /// Add a new entity/command + Add(CancellableCommand), +} + /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. @@ -115,7 +162,7 @@ where Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedCommand(command_type)) } - Err(MachineError::Underlying(e)) => Err(WFMachinesError::Underlying(Box::new(e))), + Err(MachineError::Underlying(e)) => Err(e.into()), } } else { Err(WFMachinesError::UnexpectedCommand(command_type)) @@ -138,7 +185,7 @@ where Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedEvent(event.clone())) } - Err(MachineError::Underlying(e)) => Err(WFMachinesError::Underlying(Box::new(e))), + Err(MachineError::Underlying(e)) => Err(e.into()), } } else { Err(WFMachinesError::UnexpectedEvent(event.clone())) diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 8d4f032e8..f986ed393 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -201,17 +201,87 @@ impl StartCommandRecorded { #[cfg(test)] mod test { use super::*; + use crate::machines::WFCommand; use crate::{ - machines::{test_help::TestHistoryBuilder, workflow_machines::WorkflowMachines}, - protos::temporal::api::history::v1::TimerFiredEventAttributes, + machines::{ + test_help::TestHistoryBuilder, workflow_machines::WorkflowMachines, DrivenWorkflow, + }, + protos::temporal::api::history::v1::{ + TimerFiredEventAttributes, WorkflowExecutionCanceledEventAttributes, + WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, + }, }; - use std::time::Duration; + use std::sync::mpsc::channel; + use std::{error::Error, sync::mpsc::Receiver, time::Duration}; + + // TODO: This will need to be broken out into it's own place and evolved / made more generic as + // we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to + // follow. + struct TestWorkflowDriver { + /// A queue of things to return upon calls to [DrivenWorkflow::iterate_wf]. This gives us + /// more manual control than actually running the workflow for real would, for example + /// allowing us to simulate nondeterminism. + iteration_results: Receiver, + } + + impl TestWorkflowDriver { + pub fn new(iteration_results: I) -> Self + where + I: IntoIterator, + { + let (sender, receiver) = channel(); + for r in iteration_results.into_iter() { + sender.send(r); + } + Self { + iteration_results: receiver, + } + } + } + + impl DrivenWorkflow for TestWorkflowDriver { + fn start( + &self, + attribs: WorkflowExecutionStartedEventAttributes, + ) -> Result, anyhow::Error> { + self.iterate_wf() + } + + fn iterate_wf(&self) -> Result, anyhow::Error> { + // Timeout exists just to make blocking obvious. We should never block. + let cmd = self + .iteration_results + .recv_timeout(Duration::from_millis(10))?; + Ok(vec![cmd]) + } + + fn signal( + &self, + attribs: WorkflowExecutionSignaledEventAttributes, + ) -> Result<(), anyhow::Error> { + Ok(()) + } + + fn cancel( + &self, + attribs: WorkflowExecutionCanceledEventAttributes, + ) -> Result<(), anyhow::Error> { + Ok(()) + } + } #[test] fn test_fire_happy_path() { env_logger::init(); - // We don't actually have a way to author workflows in rust yet, but the workflow that would - // match up with this is just a wf with one timer in it that fires normally. + let twd = TestWorkflowDriver::new(vec![ + WorkflowMachines::new_timer(StartTimerCommandAttributes { + timer_id: "Sometimer".to_string(), + start_to_fire_timeout: Some(Duration::from_secs(5).into()), + ..Default::default() + }), + // Complete wf task + ]); + /* 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED @@ -223,12 +293,7 @@ mod test { 8: EVENT_TYPE_WORKFLOW_TASK_STARTED */ let mut t = TestHistoryBuilder::default(); - let mut state_machines = WorkflowMachines::new(); - state_machines.new_timer(StartTimerCommandAttributes { - timer_id: "Sometimer".to_string(), - start_to_fire_timeout: Some(Duration::from_secs(5).into()), - ..Default::default() - }); + let mut state_machines = WorkflowMachines::new(Box::new(twd)); t.add_by_type(EventType::WorkflowExecutionStarted); t.add_workflow_task(); @@ -258,6 +323,5 @@ mod test { commands[0].command_type, CommandType::CompleteWorkflowExecution as i32 ); - // TODO: Timer fired event not ever handled for some reason } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 5062b4f3a..998ee2359 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,8 +1,8 @@ -use crate::machines::TSMCommand; +use crate::machines::WFCommand; use crate::{ machines::{ timer_state_machine::TimerMachine, workflow_task_state_machine::WorkflowTaskMachine, - CancellableCommand, MachineCommand, TemporalStateMachine, + CancellableCommand, DrivenWorkflow, MachineCommand, TSMCommand, TemporalStateMachine, }, protos::temporal::api::{ command::v1::StartTimerCommandAttributes, @@ -12,14 +12,12 @@ use crate::{ }; use std::{ collections::{HashMap, VecDeque}, - error::Error, rc::Rc, time::SystemTime, }; type Result = std::result::Result; -#[derive(Default)] pub(crate) struct WorkflowMachines { /// The event id of the last wf task started event in the history which is expected to be /// [current_started_event_id] except during replay. @@ -31,7 +29,7 @@ pub(crate) struct WorkflowMachines { /// True if the workflow is replaying from history replaying: bool, /// Identifies the current run and is used as a seed for faux-randomness. - current_run_id: String, + current_run_id: Option, /// The current workflow time if it has been established current_wf_time: Option, @@ -44,6 +42,9 @@ pub(crate) struct WorkflowMachines { /// Commands generated by the currently processed workflow task. It is a queue as commands can /// be added (due to marker based commands) while iterating over already added commands. current_wf_task_commands: VecDeque, + + /// The workflow that is being driven by this instance of the machines + drive_me: Box, } #[derive(thiserror::Error, Debug)] @@ -54,27 +55,40 @@ pub(crate) enum WFMachinesError { MalformedEvent(HistoryEvent, String), #[error("Command type {0:?} was not expected")] UnexpectedCommand(CommandType), - // TODO: Pretty sure can remove this if no machines need some specific error - #[error("Underlying machine error {0:?}")] - Underlying(#[from] Box), #[error("No command was scheduled for event {0:?}")] NoCommandScheduledForEvent(HistoryEvent), + + // TODO: This may not be the best thing to do here, tbd. + #[error("Underlying error {0:?}")] + Underlying(#[from] anyhow::Error), } impl WorkflowMachines { - pub(crate) fn new() -> Self { - Self::default() + pub(super) fn new(driven_wf: Box) -> Self { + Self { + drive_me: driven_wf, + // In an ideal world one could say ..Default::default() here and it'd still work. + workflow_task_started_event_id: 0, + current_started_event_id: 0, + previous_started_event_id: 0, + replaying: false, + current_run_id: None, + current_wf_time: None, + machines_by_id: Default::default(), + commands: Default::default(), + current_wf_task_commands: Default::default(), + } } /// Create a new timer - // TODO: Return cancellation callback? - pub(crate) fn new_timer(&mut self, attribs: StartTimerCommandAttributes) { + // TODO: Seems like this doesn't really belong here now. + pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> WFCommand { let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); - let cc = CancellableCommand::Active { + CancellableCommand::Active { command: add_cmd.command, machine: Rc::new(timer), - }; - self.current_wf_task_commands.push_back(cc); + } + .into() } /// Returns the id of the last seen WorkflowTaskStarted event @@ -114,7 +128,6 @@ impl WorkflowMachines { .into_iter(); for cmd in commands { match cmd { - // TODO: Does add command need to be handled here? TSMCommand::WFTaskStartedTrigger { event_id, time, @@ -144,7 +157,14 @@ impl WorkflowMachines { self.set_current_time(time); self.event_loop(); } - _ => (), + TSMCommand::ProduceHistoryEvent(e) => { + // TODO: Make this go. During replay, at least, we need to ensure + // produced event matches expected. + dbg!("History event produced", e); + } + // TODO: We shouldn't need this. Make a different type if this branch + // really never ends up being hit. + _ => unreachable!("If this is hit, we're missing something"), } } } else { @@ -238,8 +258,9 @@ impl WorkflowMachines { attrs, )) = &event.attributes { - self.current_run_id = attrs.original_execution_run_id.clone(); - // TODO: callbacks.start(event) -- but without the callbacks ;) + self.current_run_id = Some(attrs.original_execution_run_id.clone()); + let results = self.drive_me.start(attrs.clone())?; + self.handle_driven_results(results); } else { return Err(WFMachinesError::MalformedEvent( event.clone(), @@ -249,7 +270,6 @@ impl WorkflowMachines { } } Some(EventType::WorkflowTaskScheduled) => { - // This normally takes a listener which drives event loop on wf task started let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); wf_task_sm.handle_event(event, has_next_event)?; self.machines_by_id @@ -302,6 +322,8 @@ impl WorkflowMachines { fn event_loop(&mut self) { // todo: callbacks.eventLoop() + // This is where the `EntityManagerListener` would normally deal with the callback + self.prepare_commands() } @@ -313,4 +335,11 @@ impl WorkflowMachines { self.commands.push_back(c); } } + fn handle_driven_results(&mut self, results: Vec) { + for cmd in results { + match cmd { + WFCommand::Add(cc) => self.current_wf_task_commands.push_back(cc), + } + } + } } From fef3c3212facfe93061cd28e000b4419ce38efd4 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 18:16:39 -0800 Subject: [PATCH 18/59] Woohooo! This is pretty much working. Spitting out an extra command, need to fix that. --- .../complete_workflow_state_machine.rs | 110 +++++++++++++++--- src/machines/timer_state_machine.rs | 98 +++++++++------- src/machines/workflow_machines.rs | 27 ++--- 3 files changed, 157 insertions(+), 78 deletions(-) diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index 3bc6b5901..f2e1c1d22 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -1,36 +1,112 @@ -use rustfsm::{fsm, TransitionResult}; +use crate::protos::temporal::api::command::v1::Command; +use crate::{ + machines::{AddCommand, CancellableCommand, TSMCommand, WFCommand, WFMachinesError}, + protos::temporal::api::{ + command::v1::CompleteWorkflowExecutionCommandAttributes, + enums::v1::{CommandType, EventType}, + history::v1::HistoryEvent, + }, +}; +use rustfsm::{fsm, StateMachine, TransitionResult}; +use std::{convert::TryFrom, rc::Rc}; fsm! { - pub(super) name CompleteWorkflowMachine; command CompleteWorkflowCommand; error CompleteWorkflowMachineError; + pub(super) + name CompleteWorkflowMachine; + command TSMCommand; + error WFMachinesError; + shared_state CompleteWorkflowExecutionCommandAttributes; - CompleteWorkflowCommandCreated --(CommandCompleteWorkflowExecution) --> CompleteWorkflowCommandCreated; - CompleteWorkflowCommandCreated --(WorkflowExecutionCompleted, on_workflow_execution_completed) --> CompleteWorkflowCommandRecorded; + Created --(Schedule, shared on_schedule) --> CompleteWorkflowCommandCreated; - Created --(Schedule, on_schedule) --> CompleteWorkflowCommandCreated; + CompleteWorkflowCommandCreated --(CommandCompleteWorkflowExecution) + --> CompleteWorkflowCommandCreated; + CompleteWorkflowCommandCreated --(WorkflowExecutionCompleted) + --> CompleteWorkflowCommandRecorded; } -#[derive(thiserror::Error, Debug)] -pub(super) enum CompleteWorkflowMachineError {} +/// Complete a workflow +pub(super) fn complete_workflow(attribs: CompleteWorkflowExecutionCommandAttributes) -> WFCommand { + let (machine, add_cmd) = CompleteWorkflowMachine::new_scheduled(attribs); + CancellableCommand::Active { + command: add_cmd.command, + machine: Rc::new(machine), + } + .into() +} -pub(super) enum CompleteWorkflowCommand {} +impl CompleteWorkflowMachine { + /// Create a new WF machine and schedule it + pub(crate) fn new_scheduled( + attribs: CompleteWorkflowExecutionCommandAttributes, + ) -> (Self, AddCommand) { + let mut s = Self { + state: Created {}.into(), + shared_state: attribs, + }; + let cmd = match s + .on_event_mut(CompleteWorkflowMachineEvents::Schedule) + .expect("Scheduling timers doesn't fail") + .pop() + { + Some(TSMCommand::AddCommand(c)) => c, + _ => panic!("Timer on_schedule must produce command"), + }; + (s, cmd) + } +} -#[derive(Default, Clone)] -pub(super) struct CompleteWorkflowCommandCreated {} +impl TryFrom for CompleteWorkflowMachineEvents { + type Error = (); -impl CompleteWorkflowCommandCreated { - pub(super) fn on_workflow_execution_completed(self) -> CompleteWorkflowMachineTransition { - unimplemented!() + fn try_from(e: HistoryEvent) -> Result { + Ok(match EventType::from_i32(e.event_type) { + Some(EventType::WorkflowExecutionCompleted) => Self::WorkflowExecutionCompleted, + _ => return Err(()), + }) } } -#[derive(Default, Clone)] -pub(super) struct CompleteWorkflowCommandRecorded {} +impl TryFrom for CompleteWorkflowMachineEvents { + type Error = (); + + fn try_from(c: CommandType) -> Result { + Ok(match c { + CommandType::CompleteWorkflowExecution => Self::CommandCompleteWorkflowExecution, + _ => return Err(()), + }) + } +} #[derive(Default, Clone)] pub(super) struct Created {} impl Created { - pub(super) fn on_schedule(self) -> CompleteWorkflowMachineTransition { - unimplemented!() + pub(super) fn on_schedule( + self, + dat: CompleteWorkflowExecutionCommandAttributes, + ) -> CompleteWorkflowMachineTransition { + let cmd = Command { + command_type: CommandType::CompleteWorkflowExecution as i32, + attributes: Some(dat.into()), + }; + TransitionResult::commands::<_, CompleteWorkflowCommandCreated>(vec![ + TSMCommand::AddCommand(cmd.into()), + ]) + } +} + +#[derive(thiserror::Error, Debug)] +pub(super) enum CompleteWorkflowMachineError {} + +#[derive(Default, Clone)] +pub(super) struct CompleteWorkflowCommandCreated {} + +#[derive(Default, Clone)] +pub(super) struct CompleteWorkflowCommandRecorded {} + +impl From for CompleteWorkflowCommandRecorded { + fn from(_: CompleteWorkflowCommandCreated) -> Self { + Default::default() } } diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index f986ed393..85645e0d0 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,7 +1,9 @@ #![allow(clippy::large_enum_variant)] use crate::{ - machines::{workflow_machines::WFMachinesError, AddCommand, TSMCommand}, + machines::{ + workflow_machines::WFMachinesError, AddCommand, CancellableCommand, TSMCommand, WFCommand, + }, protos::{ coresdk::HistoryEventId, temporal::api::{ @@ -15,7 +17,7 @@ use crate::{ }, }; use rustfsm::{fsm, StateMachine, TransitionResult}; -use std::convert::TryFrom; +use std::{convert::TryFrom, rc::Rc}; fsm! { pub(super) name TimerMachine; @@ -23,12 +25,6 @@ fsm! { error WFMachinesError; shared_state SharedState; - CancelTimerCommandCreated --(Cancel) --> CancelTimerCommandCreated; - CancelTimerCommandCreated - --(CommandCancelTimer, shared on_command_cancel_timer) --> CancelTimerCommandSent; - - CancelTimerCommandSent --(TimerCanceled) --> Canceled; - Created --(Schedule, shared on_schedule) --> StartCommandCreated; StartCommandCreated --(CommandStartTimer) --> StartCommandCreated; @@ -37,18 +33,25 @@ fsm! { StartCommandRecorded --(TimerFired(HistoryEvent), on_timer_fired) --> Fired; StartCommandRecorded --(Cancel, shared on_cancel) --> CancelTimerCommandCreated; + + CancelTimerCommandCreated --(Cancel) --> CancelTimerCommandCreated; + CancelTimerCommandCreated + --(CommandCancelTimer, shared on_command_cancel_timer) --> CancelTimerCommandSent; + + CancelTimerCommandSent --(TimerCanceled) --> Canceled; } -impl TimerMachine { - pub(crate) fn new(attribs: StartTimerCommandAttributes) -> Self { - Self { - state: Created {}.into(), - shared_state: SharedState { - timer_attributes: attribs, - }, - } +/// Create a new timer +pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> WFCommand { + let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); + CancellableCommand::Active { + command: add_cmd.command, + machine: Rc::new(timer), } + .into() +} +impl TimerMachine { /// Create a new timer and immediately schedule it pub(crate) fn new_scheduled(attribs: StartTimerCommandAttributes) -> (Self, AddCommand) { let mut s = Self::new(attribs); @@ -62,6 +65,15 @@ impl TimerMachine { }; (s, cmd) } + + fn new(attribs: StartTimerCommandAttributes) -> Self { + Self { + state: Created {}.into(), + shared_state: SharedState { + timer_attributes: attribs, + }, + } + } } impl TryFrom for TimerMachineEvents { @@ -112,6 +124,21 @@ impl SharedState { } } +#[derive(Default, Clone)] +pub(super) struct Created {} + +impl Created { + pub(super) fn on_schedule(self, dat: SharedState) -> TimerMachineTransition { + let cmd = Command { + command_type: CommandType::StartTimer as i32, + attributes: Some(dat.timer_attributes.into()), + }; + TimerMachineTransition::commands::<_, StartCommandCreated>(vec![TSMCommand::AddCommand( + cmd.into(), + )]) + } +} + #[derive(Default, Clone)] pub(super) struct CancelTimerCommandCreated {} impl CancelTimerCommandCreated { @@ -134,21 +161,6 @@ impl From for Canceled { } } -#[derive(Default, Clone)] -pub(super) struct Created {} - -impl Created { - pub(super) fn on_schedule(self, dat: SharedState) -> TimerMachineTransition { - let cmd = Command { - command_type: CommandType::StartTimer as i32, - attributes: Some(dat.timer_attributes.into()), - }; - TimerMachineTransition::commands::<_, StartCommandCreated>(vec![TSMCommand::AddCommand( - cmd.into(), - )]) - } -} - #[derive(Default, Clone)] pub(super) struct Fired {} @@ -156,8 +168,8 @@ pub(super) struct Fired {} pub(super) struct StartCommandCreated {} impl StartCommandCreated { - pub(super) fn on_timer_started(self, id: HistoryEventId) -> TimerMachineTransition { - // Java recorded an initial event ID, but it seemingly was never used. + pub(super) fn on_timer_started(self, _id: HistoryEventId) -> TimerMachineTransition { + // TODO: Java recorded an initial event ID, but it seemingly was never used. TimerMachineTransition::default::() } pub(super) fn on_cancel(mut self, dat: SharedState) -> TimerMachineTransition { @@ -201,18 +213,20 @@ impl StartCommandRecorded { #[cfg(test)] mod test { use super::*; - use crate::machines::WFCommand; use crate::{ machines::{ - test_help::TestHistoryBuilder, workflow_machines::WorkflowMachines, DrivenWorkflow, + complete_workflow_state_machine::complete_workflow, test_help::TestHistoryBuilder, + workflow_machines::WorkflowMachines, DrivenWorkflow, WFCommand, }, - protos::temporal::api::history::v1::{ - TimerFiredEventAttributes, WorkflowExecutionCanceledEventAttributes, - WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, + protos::temporal::api::{ + command::v1::CompleteWorkflowExecutionCommandAttributes, + history::v1::{ + TimerFiredEventAttributes, WorkflowExecutionCanceledEventAttributes, + WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, + }, }, }; - use std::sync::mpsc::channel; - use std::{error::Error, sync::mpsc::Receiver, time::Duration}; + use std::{error::Error, sync::mpsc::channel, sync::mpsc::Receiver, time::Duration}; // TODO: This will need to be broken out into it's own place and evolved / made more generic as // we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to @@ -274,12 +288,12 @@ mod test { fn test_fire_happy_path() { env_logger::init(); let twd = TestWorkflowDriver::new(vec![ - WorkflowMachines::new_timer(StartTimerCommandAttributes { + new_timer(StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() }), - // Complete wf task + complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()), ]); /* diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 998ee2359..61d8f88cd 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,11 +1,10 @@ use crate::machines::WFCommand; use crate::{ machines::{ - timer_state_machine::TimerMachine, workflow_task_state_machine::WorkflowTaskMachine, - CancellableCommand, DrivenWorkflow, MachineCommand, TSMCommand, TemporalStateMachine, + workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, + MachineCommand, TSMCommand, TemporalStateMachine, }, protos::temporal::api::{ - command::v1::StartTimerCommandAttributes, enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent}, }, @@ -80,17 +79,6 @@ impl WorkflowMachines { } } - /// Create a new timer - // TODO: Seems like this doesn't really belong here now. - pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> WFCommand { - let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); - CancellableCommand::Active { - command: add_cmd.command, - machine: Rc::new(timer), - } - .into() - } - /// Returns the id of the last seen WorkflowTaskStarted event pub(super) fn get_last_started_event_id(&self) -> i64 { self.current_started_event_id @@ -155,7 +143,7 @@ impl WorkflowMachines { self.current_started_event_id = event_id; self.set_current_time(time); - self.event_loop(); + self.event_loop()?; } TSMCommand::ProduceHistoryEvent(e) => { // TODO: Make this go. During replay, at least, we need to ensure @@ -320,11 +308,12 @@ impl WorkflowMachines { .expect("We have just ensured this is populated") } - fn event_loop(&mut self) { - // todo: callbacks.eventLoop() - // This is where the `EntityManagerListener` would normally deal with the callback + fn event_loop(&mut self) -> Result<()> { + let results = self.drive_me.iterate_wf()?; + self.handle_driven_results(results); - self.prepare_commands() + self.prepare_commands(); + Ok(()) } fn prepare_commands(&mut self) { From 9f09a0a21e4d3d8827d5dc23105fcb36a6a5465c Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 21 Jan 2021 23:26:15 -0800 Subject: [PATCH 19/59] Clean up clippy warnings for tomorrow me --- src/machines/mod.rs | 15 +++++++++ src/machines/timer_state_machine.rs | 37 ++++++++++++++++++--- src/machines/workflow_machines.rs | 32 +++++++++++------- src/machines/workflow_task_state_machine.rs | 2 ++ 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 3d146e7c8..5be4a8027 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -1,3 +1,4 @@ +#[allow(unused)] mod workflow_machines; #[allow(unused)] @@ -89,6 +90,7 @@ trait DrivenWorkflow { /// In java this functionality was largely handled via callbacks passed into the state machines /// which was difficult to follow #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub(crate) enum TSMCommand { /// Issed by the [WorkflowTaskMachine] to trigger the event loop WFTaskStartedTrigger { @@ -211,7 +213,10 @@ where /// A command which can be cancelled, associated with some state machine that produced it #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum CancellableCommand { + // TODO: You'll be used soon, friend. + #[allow(dead_code)] Cancelled, Active { /// The inner protobuf command, if None, command has been cancelled @@ -221,9 +226,19 @@ enum CancellableCommand { } impl CancellableCommand { + #[allow(dead_code)] pub(super) fn cancel(&mut self) { *self = CancellableCommand::Cancelled; } + + #[cfg(test)] + fn unwrap_machine(&self) -> Rc { + if let CancellableCommand::Active { machine, .. } = self { + machine.clone() + } else { + panic!("No machine in command, already canceled") + } + } } impl Debug for dyn TemporalStateMachine { diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 85645e0d0..cd89ba0a3 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -41,14 +41,13 @@ fsm! { CancelTimerCommandSent --(TimerCanceled) --> Canceled; } -/// Create a new timer -pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> WFCommand { +/// Creates a new, scheduled, timer as a [CancellableCommand] +pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> CancellableCommand { let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); CancellableCommand::Active { command: add_cmd.command, machine: Rc::new(timer), } - .into() } impl TimerMachine { @@ -198,7 +197,6 @@ impl StartCommandRecorded { attributes: Some( CancelTimerCommandAttributes { timer_id: dat.timer_attributes.timer_id, - ..Default::default() } .into(), ), @@ -292,7 +290,8 @@ mod test { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() - }), + }) + .into(), complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()), ]); @@ -338,4 +337,32 @@ mod test { CommandType::CompleteWorkflowExecution as i32 ); } + + #[test] + fn test_timer_cancel() { + // TODO: Incomplete + + let timer_cmd = new_timer(StartTimerCommandAttributes { + timer_id: "Sometimer".to_string(), + start_to_fire_timeout: Some(Duration::from_secs(5).into()), + ..Default::default() + }); + let timer_machine = timer_cmd.unwrap_machine(); + let twd = TestWorkflowDriver::new(vec![ + timer_cmd.into(), + complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()), + ]); + + let mut t = TestHistoryBuilder::default(); + let mut state_machines = WorkflowMachines::new(Box::new(twd)); + + t.add_by_type(EventType::WorkflowExecutionStarted); + t.add_workflow_task(); + let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); + t.add_workflow_task_scheduled_and_started(); + assert_eq!(2, t.get_workflow_task_count(None).unwrap()); + let commands = t + .handle_workflow_task_take_cmds(&mut state_machines, Some(1)) + .unwrap(); + } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 61d8f88cd..334c4dfb4 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -145,14 +145,18 @@ impl WorkflowMachines { self.set_current_time(time); self.event_loop()?; } + TSMCommand::WFTaskStartedTrigger { .. } => {} TSMCommand::ProduceHistoryEvent(e) => { // TODO: Make this go. During replay, at least, we need to ensure // produced event matches expected. dbg!("History event produced", e); } - // TODO: We shouldn't need this. Make a different type if this branch - // really never ends up being hit. - _ => unreachable!("If this is hit, we're missing something"), + TSMCommand::AddCommand(_) => { + // TODO: This is where we could handle machines saying they need + // to cancel themselves (since they don't have a ref to their own + // command, could go with that too) -- but also + unimplemented!() + } } } } else { @@ -178,16 +182,16 @@ impl WorkflowMachines { } /// A command event is an event which is generated from a command emitted by a past decision. - /// Each command has a correspondent event. For example ScheduleActivityTaskCommand - /// is recorded to the history as ActivityTaskScheduledEvent. + /// Each command has a correspondent event. For example ScheduleActivityTaskCommand is recorded + /// to the history as ActivityTaskScheduledEvent. /// /// Command events always follow WorkflowTaskCompletedEvent. /// - /// The handling consists from verifying that the next command in the commands queue matches the - /// event, command state machine is notified about the event and the command is removed from the - /// commands queue. + /// The handling consists of verifying that the next command in the commands queue is associated + /// with a state machine, which is then notified about the event and the command is removed from + /// the commands queue. fn handle_command_event(&mut self, event: &HistoryEvent) -> Result<()> { - // TODO: + // TODO: Local activity handling stuff // if (handleLocalActivityMarker(event)) { // return; // } @@ -204,8 +208,6 @@ impl WorkflowMachines { return Err(WFMachinesError::NoCommandScheduledForEvent(event.clone())); }; - // TODO: More special handling for version machine - see java - // Feed the machine the event let mut break_later = false; if let CancellableCommand::Active { @@ -214,6 +216,10 @@ impl WorkflowMachines { { // TODO: Remove unwrap let out_commands = Rc::get_mut(machine).unwrap().handle_event(event, true)?; + // TODO: Handle invalid event errors + // * More special handling for version machine - see java + // * Command/machine supposed to have cancelled itself + dbg!(&out_commands); break_later = true; } @@ -229,7 +235,9 @@ impl WorkflowMachines { // TODO: validate command if let CancellableCommand::Active { machine, .. } = consumed_cmd { - self.machines_by_id.insert(event.event_id, machine); + if !machine.is_final_state() { + self.machines_by_id.insert(event.event_id, machine); + } } Ok(()) diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 66a3e7626..11ca876dc 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,3 +1,5 @@ +#![allow(clippy::enum_variant_names)] + use crate::{ machines::{workflow_machines::WFMachinesError, TSMCommand}, protos::temporal::api::{ From 157a9abc31f5e2c6418b2ff98e59bde0f765b8fc Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Sat, 23 Jan 2021 10:27:36 -0800 Subject: [PATCH 20/59] Checkpointing before I use tracing to make this more understandable --- Cargo.toml | 3 + .../complete_workflow_state_machine.rs | 3 +- src/machines/mod.rs | 26 ++--- src/machines/test_help.rs | 27 +++--- src/machines/timer_state_machine.rs | 96 +++++++++++++------ src/machines/workflow_machines.rs | 72 +++++++------- src/machines/workflow_task_state_machine.rs | 17 ++-- 7 files changed, 145 insertions(+), 99 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e50a7d5fb..4e3aedccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ tonic = "0.4" [dependencies.rustfsm] path = "fsm" +[dev-dependencies] +rstest = "0.6" + [build-dependencies] tonic-build = "0.4" diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index f2e1c1d22..c80da6e0d 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -8,6 +8,7 @@ use crate::{ }, }; use rustfsm::{fsm, StateMachine, TransitionResult}; +use std::cell::RefCell; use std::{convert::TryFrom, rc::Rc}; fsm! { @@ -30,7 +31,7 @@ pub(super) fn complete_workflow(attribs: CompleteWorkflowExecutionCommandAttribu let (machine, add_cmd) = CompleteWorkflowMachine::new_scheduled(attribs); CancellableCommand::Active { command: add_cmd.command, - machine: Rc::new(machine), + machine: Rc::new(RefCell::new(machine)), } .into() } diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 5be4a8027..8bde972c2 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -49,6 +49,7 @@ use crate::{ use prost::alloc::fmt::Formatter; use rustfsm::{MachineError, StateMachine}; use std::{ + cell::RefCell, convert::{TryFrom, TryInto}, fmt::Debug, rc::Rc, @@ -92,11 +93,10 @@ trait DrivenWorkflow { #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub(crate) enum TSMCommand { - /// Issed by the [WorkflowTaskMachine] to trigger the event loop + /// Issued by the [WorkflowTaskMachine] to (possibly) trigger the event loop WFTaskStartedTrigger { - event_id: i64, + task_started_event_id: i64, time: SystemTime, - only_if_last_event: bool, }, /// Issued by state machines to create commands AddCommand(AddCommand), @@ -116,7 +116,7 @@ pub(crate) struct AddCommand { /// EX: Create a new timer, complete the workflow, etc. /// /// TODO: Maybe this and TSMCommand are really the same thing? -#[derive(Debug, derive_more::From)] +#[derive(Debug, derive_more::From, Clone)] enum WFCommand { /// Add a new entity/command Add(CancellableCommand), @@ -145,7 +145,7 @@ where ::Event: TryFrom, ::Event: TryFrom, ::Command: Debug, - // TODO: Do we really need this bound? Check back and see how many fsms really issue them this way + // TODO: Do we really need this bound? Check back and see if we can just use TSMCommand directly ::Command: Into, ::Error: Into + 'static + Send + Sync, { @@ -154,11 +154,11 @@ where } fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError> { - dbg!(self.name(), "handling command", command_type); + // dbg!(self.name(), "handling command", command_type); if let Ok(converted_command) = command_type.try_into() { match self.on_event_mut(converted_command) { - Ok(c) => { - dbg!(c); + Ok(_c) => { + // dbg!(c); Ok(()) } Err(MachineError::InvalidTransition) => { @@ -177,11 +177,11 @@ where _has_next_event: bool, ) -> Result, WFMachinesError> { // TODO: Real tracing - dbg!(self.name(), "handling event", &event); + // dbg!(self.name(), "handling event", &event); if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { Ok(c) => { - dbg!(&c); + // dbg!(&c); Ok(c.into_iter().map(Into::into).collect()) } Err(MachineError::InvalidTransition) => { @@ -221,7 +221,7 @@ enum CancellableCommand { Active { /// The inner protobuf command, if None, command has been cancelled command: MachineCommand, - machine: Rc, + machine: Rc>, }, } @@ -232,9 +232,9 @@ impl CancellableCommand { } #[cfg(test)] - fn unwrap_machine(&self) -> Rc { + fn unwrap_machine(&mut self) -> &mut dyn TemporalStateMachine { if let CancellableCommand::Active { machine, .. } = self { - machine.clone() + RefCell::get_mut(Rc::get_mut(machine).unwrap()) } else { panic!("No machine in command, already canceled") } diff --git a/src/machines/test_help.rs b/src/machines/test_help.rs index 1b2e7c494..41cfcb48b 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help.rs @@ -128,37 +128,38 @@ impl TestHistoryBuilder { Ok(count) } + /// Handle workflow task(s) using the provided [WorkflowMachines]. Will process as many workflow + /// tasks as the provided `to_wf_task_num` parameter.. + /// + /// # Panics + /// * Can panic if the passed in machines have been manipulated outside of this builder pub(super) fn handle_workflow_task_take_cmds( &self, wf_machines: &mut WorkflowMachines, - to_task_index: Option, + to_wf_task_num: Option, ) -> Result> { - self.handle_workflow_task(wf_machines, to_task_index)?; + self.handle_workflow_task(wf_machines, to_wf_task_num)?; Ok(wf_machines.get_commands()) } - /// Handle a workflow task using the provided [WorkflowMachines] - /// - /// # Panics - /// * Can panic if the passed in machines have been manipulated outside of this builder - pub(super) fn handle_workflow_task( + fn handle_workflow_task( &self, wf_machines: &mut WorkflowMachines, - to_task_index: Option, + to_wf_task_num: Option, ) -> Result<()> { - let to_task_index = to_task_index.unwrap_or(usize::MAX); + let to_wf_task_num = to_wf_task_num.unwrap_or(usize::MAX); let (_, events) = self .events .split_at(wf_machines.get_last_started_event_id() as usize); let mut history = events.iter().peekable(); - let hist_info = self.get_history_info(to_task_index)?; + let hist_info = self.get_history_info(to_wf_task_num)?; wf_machines.set_started_ids( hist_info.previous_started_event_id, hist_info.workflow_task_started_event_id, ); let mut started_id = hist_info.previous_started_event_id; - let mut count = if wf_machines.get_last_started_event_id() > 0 { + let mut num_seen_wf_tasks = if wf_machines.get_last_started_event_id() > 0 { self.get_workflow_task_count(history.peek().map(|e| e.event_id - 1))? } else { 0 @@ -178,8 +179,8 @@ impl TestHistoryBuilder { if next_event.is_none() || next_is_completed { started_id = event.event_id; - count += 1; - if count == to_task_index || next_event.is_none() { + num_seen_wf_tasks += 1; + if num_seen_wf_tasks == to_wf_task_num || next_event.is_none() { wf_machines.handle_event(event, false)?; return Ok(()); } diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index cd89ba0a3..79db7a5ed 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -17,6 +17,7 @@ use crate::{ }, }; use rustfsm::{fsm, StateMachine, TransitionResult}; +use std::cell::RefCell; use std::{convert::TryFrom, rc::Rc}; fsm! { @@ -46,7 +47,7 @@ pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> CancellableComm let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); CancellableCommand::Active { command: add_cmd.command, - machine: Rc::new(timer), + machine: Rc::new(RefCell::new(timer)), } } @@ -224,29 +225,34 @@ mod test { }, }, }; + use rstest::{fixture, rstest}; + use std::sync::mpsc::Sender; use std::{error::Error, sync::mpsc::channel, sync::mpsc::Receiver, time::Duration}; // TODO: This will need to be broken out into it's own place and evolved / made more generic as // we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to // follow. struct TestWorkflowDriver { - /// A queue of things to return upon calls to [DrivenWorkflow::iterate_wf]. This gives us - /// more manual control than actually running the workflow for real would, for example - /// allowing us to simulate nondeterminism. - iteration_results: Receiver, + /// A queue of command lists to return upon calls to [DrivenWorkflow::iterate_wf]. This + /// gives us more manual control than actually running the workflow for real would, for + /// example allowing us to simulate nondeterminism. + iteration_results: Receiver>, + iteration_sender: Sender>, + + /// The entire history passed in when we were constructed + full_history: Vec>, } impl TestWorkflowDriver { pub fn new(iteration_results: I) -> Self where - I: IntoIterator, + I: IntoIterator>, { let (sender, receiver) = channel(); - for r in iteration_results.into_iter() { - sender.send(r); - } Self { iteration_results: receiver, + iteration_sender: sender, + full_history: iteration_results.into_iter().collect(), } } } @@ -256,15 +262,20 @@ mod test { &self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, anyhow::Error> { - self.iterate_wf() + dbg!("T start"); + self.full_history + .iter() + .for_each(|cmds| self.iteration_sender.send(cmds.to_vec()).unwrap()); + Ok(vec![]) } fn iterate_wf(&self) -> Result, anyhow::Error> { + dbg!("T iterate"); // Timeout exists just to make blocking obvious. We should never block. let cmd = self .iteration_results .recv_timeout(Duration::from_millis(10))?; - Ok(vec![cmd]) + dbg!(Ok(cmd)) } fn signal( @@ -282,19 +293,8 @@ mod test { } } - #[test] - fn test_fire_happy_path() { - env_logger::init(); - let twd = TestWorkflowDriver::new(vec![ - new_timer(StartTimerCommandAttributes { - timer_id: "Sometimer".to_string(), - start_to_fire_timeout: Some(Duration::from_secs(5).into()), - ..Default::default() - }) - .into(), - complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()), - ]); - + #[fixture] + fn fire_happy_hist() -> (TestHistoryBuilder, WorkflowMachines) { /* 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED @@ -304,7 +304,24 @@ mod test { 6: EVENT_TYPE_TIMER_FIRED 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + + Should iterate once, produce started command, iterate again, producing no commands + (timer complete), third iteration completes workflow. */ + let twd = TestWorkflowDriver::new(vec![ + vec![new_timer(StartTimerCommandAttributes { + timer_id: "Sometimer".to_string(), + start_to_fire_timeout: Some(Duration::from_secs(5).into()), + ..Default::default() + }) + .into()], + // TODO: Needs to be here in incremental one, but not full :/ + // vec![], // timer complete, no new commands + vec![complete_workflow( + CompleteWorkflowExecutionCommandAttributes::default(), + )], + ]); + let mut t = TestHistoryBuilder::default(); let mut state_machines = WorkflowMachines::new(Box::new(twd)); @@ -321,16 +338,35 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); assert_eq!(2, t.get_workflow_task_count(None).unwrap()); + (t, state_machines) + } + + #[rstest] + fn test_fire_happy_path_inc(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { + env_logger::init(); + let (t, mut state_machines) = fire_happy_hist; + let commands = t .handle_workflow_task_take_cmds(&mut state_machines, Some(1)) .unwrap(); - dbg!(&commands); assert_eq!(commands.len(), 1); assert_eq!(commands[0].command_type, CommandType::StartTimer as i32); let commands = t .handle_workflow_task_take_cmds(&mut state_machines, Some(2)) .unwrap(); - dbg!(&commands); + assert_eq!(commands.len(), 1); + assert_eq!( + commands[0].command_type, + CommandType::CompleteWorkflowExecution as i32 + ); + } + + #[rstest] + fn test_fire_happy_path_full(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { + let (t, mut state_machines) = fire_happy_hist; + let commands = t + .handle_workflow_task_take_cmds(&mut state_machines, Some(2)) + .unwrap(); assert_eq!(commands.len(), 1); assert_eq!( commands[0].command_type, @@ -342,15 +378,17 @@ mod test { fn test_timer_cancel() { // TODO: Incomplete - let timer_cmd = new_timer(StartTimerCommandAttributes { + let mut timer_cmd = new_timer(StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() }); let timer_machine = timer_cmd.unwrap_machine(); let twd = TestWorkflowDriver::new(vec![ - timer_cmd.into(), - complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()), + vec![timer_cmd.into()], + vec![complete_workflow( + CompleteWorkflowExecutionCommandAttributes::default(), + )], ]); let mut t = TestHistoryBuilder::default(); diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 334c4dfb4..734195935 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,8 +1,7 @@ -use crate::machines::WFCommand; use crate::{ machines::{ workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, - MachineCommand, TSMCommand, TemporalStateMachine, + MachineCommand, TSMCommand, TemporalStateMachine, WFCommand, }, protos::temporal::api::{ enums::v1::{CommandType, EventType}, @@ -10,6 +9,8 @@ use crate::{ }, }; use std::{ + borrow::BorrowMut, + cell::RefCell, collections::{HashMap, VecDeque}, rc::Rc, time::SystemTime, @@ -34,7 +35,8 @@ pub(crate) struct WorkflowMachines { /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. - machines_by_id: HashMap>, + // TODO: Maybe value here should just be `CancellableCommand` + machines_by_id: HashMap>>, /// Queued commands which have been produced by machines and await processing commands: VecDeque, @@ -97,10 +99,12 @@ impl WorkflowMachines { self.handle_command_event(event)?; return Ok(()); } + let event_type = EventType::from_i32(event.event_type) + .ok_or_else(|| WFMachinesError::UnexpectedEvent(event.clone()))?; if self.replaying && self.current_started_event_id >= self.previous_started_event_id - && event.event_type != EventType::WorkflowTaskCompleted as i32 + && event_type != EventType::WorkflowTaskCompleted { // Replay is finished self.replaying = false; @@ -108,20 +112,29 @@ impl WorkflowMachines { match event.get_initial_command_event_id() { Some(initial_cmd_id) => { - if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { - // TODO: Remove unwrap - let commands = Rc::get_mut(sm) - .unwrap() + if let Some(sm) = self.machines_by_id.get(&initial_cmd_id) { + let commands = (*sm.clone()) + .borrow_mut() .handle_event(event, has_next_event)? .into_iter(); + dbg!(&commands); for cmd in commands { match cmd { TSMCommand::WFTaskStartedTrigger { - event_id, + task_started_event_id, time, - only_if_last_event, - } if !only_if_last_event || !has_next_event => { + } => { + let cur_event_past_or_at_start = + event.event_id >= task_started_event_id; + if event_type == EventType::WorkflowTaskStarted + && !(cur_event_past_or_at_start && !has_next_event) + { + // Last event in history is a task started event, so we don't + // want to iterate. + continue; + } dbg!("In task started trigger"); + // TODO: Seems to only matter for version machine. Figure out then. // // If some new commands are pending and there are no more command events. // for (CancellableCommand cancellableCommand : commands) { @@ -141,14 +154,13 @@ impl WorkflowMachines { // } // } - self.current_started_event_id = event_id; + self.current_started_event_id = task_started_event_id; self.set_current_time(time); self.event_loop()?; } - TSMCommand::WFTaskStartedTrigger { .. } => {} TSMCommand::ProduceHistoryEvent(e) => { // TODO: Make this go. During replay, at least, we need to ensure - // produced event matches expected. + // produced event matches expected? dbg!("History event produced", e); } TSMCommand::AddCommand(_) => { @@ -169,8 +181,7 @@ impl WorkflowMachines { // Have to fetch machine again here to avoid borrowing self mutably twice if let Some(sm) = self.machines_by_id.get_mut(&initial_cmd_id) { - if sm.is_final_state() { - dbg!(sm.name(), "final state"); + if sm.borrow().is_final_state() { self.machines_by_id.remove(&initial_cmd_id); } } @@ -201,7 +212,7 @@ impl WorkflowMachines { // handleVersionMarker can skip a marker event if the getVersion call was removed. // In this case we don't want to consume a command. // That's why front is used instead of pop_front - maybe_command = self.commands.front_mut(); + maybe_command = self.commands.front(); let command = if let Some(c) = maybe_command { c } else { @@ -210,12 +221,8 @@ impl WorkflowMachines { // Feed the machine the event let mut break_later = false; - if let CancellableCommand::Active { - ref mut machine, .. - } = command - { - // TODO: Remove unwrap - let out_commands = Rc::get_mut(machine).unwrap().handle_event(event, true)?; + if let CancellableCommand::Active { mut machine, .. } = command.clone() { + let out_commands = (*machine).borrow_mut().handle_event(event, true)?; // TODO: Handle invalid event errors // * More special handling for version machine - see java // * Command/machine supposed to have cancelled itself @@ -235,7 +242,7 @@ impl WorkflowMachines { // TODO: validate command if let CancellableCommand::Active { machine, .. } = consumed_cmd { - if !machine.is_final_state() { + if !machine.borrow().is_final_state() { self.machines_by_id.insert(event.event_id, machine); } } @@ -269,7 +276,7 @@ impl WorkflowMachines { let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); wf_task_sm.handle_event(event, has_next_event)?; self.machines_by_id - .insert(event.event_id, Rc::new(wf_task_sm)); + .insert(event.event_id, Rc::new(RefCell::new(wf_task_sm))); } Some(EventType::WorkflowExecutionSignaled) => { // TODO: Signal callbacks @@ -324,6 +331,14 @@ impl WorkflowMachines { Ok(()) } + fn handle_driven_results(&mut self, results: Vec) { + for cmd in results { + match cmd { + WFCommand::Add(cc) => self.current_wf_task_commands.push_back(cc), + } + } + } + fn prepare_commands(&mut self) { while let Some(c) = self.current_wf_task_commands.pop_front() { // TODO - some special case stuff that can maybe be managed differently? @@ -332,11 +347,4 @@ impl WorkflowMachines { self.commands.push_back(c); } } - fn handle_driven_results(&mut self, results: Vec) { - for cmd in results { - match cmd { - WFCommand::Add(cc) => self.current_wf_task_commands.push_back(cc), - } - } - } } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 11ca876dc..51b8d1b6e 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -98,17 +98,12 @@ impl Scheduled { started_event_id, }: WFTStartedDat, ) -> WorkflowTaskMachineTransition { - let cmds = if started_event_id >= shared.wf_task_started_event_id { + dbg!("wtsm started trans"); + WorkflowTaskMachineTransition::ok( vec![TSMCommand::WFTaskStartedTrigger { - event_id: started_event_id, + task_started_event_id: shared.wf_task_started_event_id, time: current_time_millis, - only_if_last_event: true, - }] - } else { - vec![] - }; - WorkflowTaskMachineTransition::ok( - cmds, + }], Started { current_time_millis, started_event_id, @@ -133,11 +128,11 @@ pub(super) struct Started { impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { + dbg!("wtsm completed trans"); WorkflowTaskMachineTransition::commands::<_, Completed>(vec![ TSMCommand::WFTaskStartedTrigger { - event_id: self.started_event_id, + task_started_event_id: self.started_event_id, time: self.current_time_millis, - only_if_last_event: false, }, ]) } From 1186d24cc1ad6154fd602a19d49899915330c0b3 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Sun, 24 Jan 2021 13:10:02 -0800 Subject: [PATCH 21/59] Tracing makes debug easier --- src/machines/timer_state_machine.rs | 41 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 79db7a5ed..b3b5719ef 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -17,8 +17,8 @@ use crate::{ }, }; use rustfsm::{fsm, StateMachine, TransitionResult}; -use std::cell::RefCell; -use std::{convert::TryFrom, rc::Rc}; +use std::{cell::RefCell, convert::TryFrom, rc::Rc}; +use tracing::Level; fsm! { pub(super) name TimerMachine; @@ -228,10 +228,13 @@ mod test { use rstest::{fixture, rstest}; use std::sync::mpsc::Sender; use std::{error::Error, sync::mpsc::channel, sync::mpsc::Receiver, time::Duration}; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; // TODO: This will need to be broken out into it's own place and evolved / made more generic as // we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to // follow. + #[derive(Debug)] struct TestWorkflowDriver { /// A queue of command lists to return upon calls to [DrivenWorkflow::iterate_wf]. This /// gives us more manual control than actually running the workflow for real would, for @@ -258,24 +261,25 @@ mod test { } impl DrivenWorkflow for TestWorkflowDriver { + #[instrument] fn start( &self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, anyhow::Error> { - dbg!("T start"); self.full_history .iter() .for_each(|cmds| self.iteration_sender.send(cmds.to_vec()).unwrap()); Ok(vec![]) } + #[instrument] fn iterate_wf(&self) -> Result, anyhow::Error> { - dbg!("T iterate"); // Timeout exists just to make blocking obvious. We should never block. let cmd = self .iteration_results .recv_timeout(Duration::from_millis(10))?; - dbg!(Ok(cmd)) + event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmd); + Ok(cmd) } fn signal( @@ -343,7 +347,19 @@ mod test { #[rstest] fn test_fire_happy_path_inc(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { - env_logger::init(); + let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() + .with_service_name("report_example") + .install() + .unwrap(); + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init() + .unwrap(); + + let s = span!(Level::DEBUG, "Test start", t = "inc"); + let _enter = s.enter(); + let (t, mut state_machines) = fire_happy_hist; let commands = t @@ -363,6 +379,19 @@ mod test { #[rstest] fn test_fire_happy_path_full(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { + let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() + .with_service_name("report_example") + .install() + .unwrap(); + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init() + .unwrap(); + + let s = span!(Level::DEBUG, "Test start", t = "full"); + let _enter = s.enter(); + let (t, mut state_machines) = fire_happy_hist; let commands = t .handle_workflow_task_take_cmds(&mut state_machines, Some(2)) From a5d0a04fb2596b4fe7e084f4693e3850ca9efbb4 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Sun, 24 Jan 2021 13:22:03 -0800 Subject: [PATCH 22/59] IDE just didn't commit a bunch of stuff, cool. --- Cargo.toml | 5 ++++ src/lib.rs | 2 ++ src/machines/mod.rs | 26 ++++++++++++--------- src/machines/workflow_machines.rs | 24 ++++++++++++------- src/machines/workflow_task_state_machine.rs | 3 +-- src/protos/mod.rs | 14 +++++++++++ 6 files changed, 53 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e3aedccd..f6319576c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ prost = "0.7" prost-types = "0.7" thiserror = "1.0" tonic = "0.4" +tracing = { version = "0.1", features = ["log"] } +# TODO: Feature flag this stuff +tracing-opentelemetry = "0.10" +tracing-subscriber = "0.2" +opentelemetry-jaeger = "0.10" [dependencies.rustfsm] path = "fsm" diff --git a/src/lib.rs b/src/lib.rs index 4d778e430..86901e986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #[macro_use] extern crate log; +#[macro_use] +extern crate tracing; mod machines; mod pollers; diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 8bde972c2..8bbbfa9da 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -55,6 +55,7 @@ use std::{ rc::Rc, time::SystemTime, }; +use tracing::Level; // TODO: May need to be our SDKWFCommand type type MachineCommand = Command; @@ -154,13 +155,15 @@ where } fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError> { - // dbg!(self.name(), "handling command", command_type); + event!( + Level::DEBUG, + msg = "handling command", + ?command_type, + machine_name = %self.name() + ); if let Ok(converted_command) = command_type.try_into() { match self.on_event_mut(converted_command) { - Ok(_c) => { - // dbg!(c); - Ok(()) - } + Ok(_c) => Ok(()), Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedCommand(command_type)) } @@ -176,14 +179,15 @@ where event: &HistoryEvent, _has_next_event: bool, ) -> Result, WFMachinesError> { - // TODO: Real tracing - // dbg!(self.name(), "handling event", &event); + event!( + Level::DEBUG, + msg = "handling event", + %event, + machine_name = %self.name() + ); if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { - Ok(c) => { - // dbg!(&c); - Ok(c.into_iter().map(Into::into).collect()) - } + Ok(c) => Ok(c.into_iter().map(Into::into).collect()), Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedEvent(event.clone())) } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 734195935..cead82a31 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -15,6 +15,7 @@ use std::{ rc::Rc, time::SystemTime, }; +use tracing::Level; type Result = std::result::Result; @@ -90,6 +91,7 @@ impl WorkflowMachines { /// is the last event in the history. /// /// TODO: Describe what actually happens in here + #[instrument(skip(self))] pub(crate) fn handle_event( &mut self, event: &HistoryEvent, @@ -117,7 +119,7 @@ impl WorkflowMachines { .borrow_mut() .handle_event(event, has_next_event)? .into_iter(); - dbg!(&commands); + event!(Level::DEBUG, msg = "Machine produced commands", ?commands); for cmd in commands { match cmd { TSMCommand::WFTaskStartedTrigger { @@ -133,7 +135,8 @@ impl WorkflowMachines { // want to iterate. continue; } - dbg!("In task started trigger"); + let s = span!(Level::DEBUG, "Task started trigger"); + let _enter = s.enter(); // TODO: Seems to only matter for version machine. Figure out then. // // If some new commands are pending and there are no more command events. @@ -158,10 +161,10 @@ impl WorkflowMachines { self.set_current_time(time); self.event_loop()?; } - TSMCommand::ProduceHistoryEvent(e) => { + TSMCommand::ProduceHistoryEvent(event) => { // TODO: Make this go. During replay, at least, we need to ensure // produced event matches expected? - dbg!("History event produced", e); + event!(Level::DEBUG, msg = "History event produced", %event); } TSMCommand::AddCommand(_) => { // TODO: This is where we could handle machines saying they need @@ -172,10 +175,11 @@ impl WorkflowMachines { } } } else { - error!( - "During event handling, this event had an initial command ID but \ + event!( + Level::ERROR, + msg = "During event handling, this event had an initial command ID but \ we could not find a matching state machine! Event: {:?}", - event + ?event ); } @@ -227,7 +231,11 @@ impl WorkflowMachines { // * More special handling for version machine - see java // * Command/machine supposed to have cancelled itself - dbg!(&out_commands); + event!( + Level::DEBUG, + msg = "Machine produced commands", + ?out_commands + ); break_later = true; } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 51b8d1b6e..f546736d4 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -9,6 +9,7 @@ use crate::{ }; use rustfsm::{fsm, TransitionResult}; use std::{convert::TryFrom, time::SystemTime}; +use tracing::Level; fsm! { pub(super) name WorkflowTaskMachine; @@ -98,7 +99,6 @@ impl Scheduled { started_event_id, }: WFTStartedDat, ) -> WorkflowTaskMachineTransition { - dbg!("wtsm started trans"); WorkflowTaskMachineTransition::ok( vec![TSMCommand::WFTaskStartedTrigger { task_started_event_id: shared.wf_task_started_event_id, @@ -128,7 +128,6 @@ pub(super) struct Started { impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { - dbg!("wtsm completed trans"); WorkflowTaskMachineTransition::commands::<_, Completed>(vec![ TSMCommand::WFTaskStartedTrigger { task_started_event_id: self.started_event_id, diff --git a/src/protos/mod.rs b/src/protos/mod.rs index a0c8dd25b..0d7816532 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -39,6 +39,9 @@ pub mod temporal { use crate::protos::temporal::api::{ enums::v1::EventType, history::v1::history_event::Attributes, }; + use prost::alloc::fmt::Formatter; + use std::fmt::Display; + include!("temporal.api.history.v1.rs"); impl HistoryEvent { @@ -121,6 +124,17 @@ pub mod temporal { } } } + + impl Display for HistoryEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "HistoryEvent(id: {}, {:?})", + self.event_id, + EventType::from_i32(self.event_type) + ) + } + } } } From d0f53430ad0c2836be8e9f1279aee60ebf24f0c7 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Sun, 24 Jan 2021 13:36:25 -0800 Subject: [PATCH 23/59] Test helpers to own modules before rework --- src/lib.rs | 2 - .../history_builder.rs} | 35 ++++---- src/machines/test_help/mod.rs | 7 ++ src/machines/test_help/workflow_driver.rs | 79 +++++++++++++++++++ src/machines/timer_state_machine.rs | 79 ++----------------- 5 files changed, 109 insertions(+), 93 deletions(-) rename src/machines/{test_help.rs => test_help/history_builder.rs} (91%) create mode 100644 src/machines/test_help/mod.rs create mode 100644 src/machines/test_help/workflow_driver.rs diff --git a/src/lib.rs b/src/lib.rs index 86901e986..4267ccf6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,4 @@ #[macro_use] -extern crate log; -#[macro_use] extern crate tracing; mod machines; diff --git a/src/machines/test_help.rs b/src/machines/test_help/history_builder.rs similarity index 91% rename from src/machines/test_help.rs rename to src/machines/test_help/history_builder.rs index 41cfcb48b..42b94e927 100644 --- a/src/machines/test_help.rs +++ b/src/machines/test_help/history_builder.rs @@ -1,3 +1,4 @@ +use super::Result; use crate::{ machines::{workflow_machines::WorkflowMachines, MachineCommand}, protos::temporal::api::{ @@ -12,10 +13,8 @@ use crate::{ use anyhow::bail; use std::time::SystemTime; -type Result = std::result::Result; - #[derive(Default, Debug)] -pub(super) struct TestHistoryBuilder { +pub struct TestHistoryBuilder { events: Vec, /// Is incremented every time a new event is added, and that *new* value is used as that event's /// id @@ -27,23 +26,19 @@ pub(super) struct TestHistoryBuilder { impl TestHistoryBuilder { /// Add an event by type with attributes. Bundles both into a [HistoryEvent] with an id that is /// incremented on each call to add. - pub(super) fn add(&mut self, event_type: EventType, attribs: Attributes) { + pub fn add(&mut self, event_type: EventType, attribs: Attributes) { self.build_and_push_event(event_type, attribs); } /// Adds an event to the history by type, with default attributes. - pub(super) fn add_by_type(&mut self, event_type: EventType) { + pub fn add_by_type(&mut self, event_type: EventType) { let attribs = default_attribs(event_type).expect("Couldn't make default attributes in test builder"); self.build_and_push_event(event_type.clone(), attribs); } /// Adds an event, returning the ID that was assigned to it - pub(super) fn add_get_event_id( - &mut self, - event_type: EventType, - attrs: Option, - ) -> i64 { + pub fn add_get_event_id(&mut self, event_type: EventType, attrs: Option) -> i64 { if let Some(a) = attrs { self.build_and_push_event(event_type, a); } else { @@ -58,24 +53,24 @@ impl TestHistoryBuilder { /// EVENT_TYPE_WORKFLOW_TASK_STARTED /// EVENT_TYPE_WORKFLOW_TASK_COMPLETED /// ``` - pub(super) fn add_workflow_task(&mut self) { + pub fn add_workflow_task(&mut self) { self.add_workflow_task_scheduled_and_started(); self.add_workflow_task_completed(); } - pub(super) fn add_workflow_task_scheduled_and_started(&mut self) { + pub fn add_workflow_task_scheduled_and_started(&mut self) { self.add_workflow_task_scheduled(); self.add_workflow_task_started(); } - pub(super) fn add_workflow_task_scheduled(&mut self) { + pub fn add_workflow_task_scheduled(&mut self) { // WFStarted always immediately follows WFScheduled self.previous_started_event_id = self.workflow_task_scheduled_event_id + 1; self.workflow_task_scheduled_event_id = self.add_get_event_id(EventType::WorkflowTaskScheduled, None); } - pub(super) fn add_workflow_task_started(&mut self) { + pub fn add_workflow_task_started(&mut self) { let attrs = WorkflowTaskStartedEventAttributes { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() @@ -83,7 +78,7 @@ impl TestHistoryBuilder { self.build_and_push_event(EventType::WorkflowTaskStarted, attrs.into()); } - pub(super) fn add_workflow_task_completed(&mut self) { + pub fn add_workflow_task_completed(&mut self) { let attrs = WorkflowTaskCompletedEventAttributes { scheduled_event_id: self.workflow_task_scheduled_event_id, ..Default::default() @@ -97,7 +92,7 @@ impl TestHistoryBuilder { /// /// If `up_to_event_id` is provided, the count will be returned as soon as processing advances /// past that id. - pub(super) fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { + pub fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { let mut last_wf_started_id = 0; let mut count = 0; let mut history = self.events.iter().peekable(); @@ -133,7 +128,7 @@ impl TestHistoryBuilder { /// /// # Panics /// * Can panic if the passed in machines have been manipulated outside of this builder - pub(super) fn handle_workflow_task_take_cmds( + pub(crate) fn handle_workflow_task_take_cmds( &self, wf_machines: &mut WorkflowMachines, to_wf_task_num: Option, @@ -296,7 +291,7 @@ fn default_attribs(et: EventType) -> Result { } #[derive(Clone, Debug, derive_more::Constructor, Eq, PartialEq, Hash)] -pub(super) struct HistoryInfo { - pub(super) previous_started_event_id: i64, - pub(super) workflow_task_started_event_id: i64, +pub struct HistoryInfo { + pub previous_started_event_id: i64, + pub workflow_task_started_event_id: i64, } diff --git a/src/machines/test_help/mod.rs b/src/machines/test_help/mod.rs new file mode 100644 index 000000000..c7aa4037f --- /dev/null +++ b/src/machines/test_help/mod.rs @@ -0,0 +1,7 @@ +type Result = std::result::Result; + +mod history_builder; +mod workflow_driver; + +pub(super) use history_builder::TestHistoryBuilder; +pub(super) use workflow_driver::TestWorkflowDriver; diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs new file mode 100644 index 000000000..fed684a4f --- /dev/null +++ b/src/machines/test_help/workflow_driver.rs @@ -0,0 +1,79 @@ +use super::Result; +use crate::{ + machines::{DrivenWorkflow, WFCommand}, + protos::temporal::api::history::v1::{ + WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, + WorkflowExecutionStartedEventAttributes, + }, +}; +use std::{ + sync::mpsc::{channel, Receiver, Sender}, + time::Duration, +}; +use tracing::Level; + +// TODO: This will need to be broken out into it's own place and evolved / made more generic as +// we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to +// follow. +#[derive(Debug)] +pub(crate) struct TestWorkflowDriver { + /// A queue of command lists to return upon calls to [DrivenWorkflow::iterate_wf]. This + /// gives us more manual control than actually running the workflow for real would, for + /// example allowing us to simulate nondeterminism. + iteration_results: Receiver>, + iteration_sender: Sender>, + + /// The entire history passed in when we were constructed + full_history: Vec>, +} + +impl TestWorkflowDriver { + pub(in crate::machines) fn new(iteration_results: I) -> Self + where + I: IntoIterator>, + { + let (sender, receiver) = channel(); + Self { + iteration_results: receiver, + iteration_sender: sender, + full_history: iteration_results.into_iter().collect(), + } + } +} + +impl DrivenWorkflow for TestWorkflowDriver { + #[instrument] + fn start( + &self, + attribs: WorkflowExecutionStartedEventAttributes, + ) -> Result, anyhow::Error> { + self.full_history + .iter() + .for_each(|cmds| self.iteration_sender.send(cmds.to_vec()).unwrap()); + Ok(vec![]) + } + + #[instrument] + fn iterate_wf(&self) -> Result, anyhow::Error> { + // Timeout exists just to make blocking obvious. We should never block. + let cmd = self + .iteration_results + .recv_timeout(Duration::from_millis(10))?; + event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmd); + Ok(cmd) + } + + fn signal( + &self, + _attribs: WorkflowExecutionSignaledEventAttributes, + ) -> Result<(), anyhow::Error> { + Ok(()) + } + + fn cancel( + &self, + _attribs: WorkflowExecutionCanceledEventAttributes, + ) -> Result<(), anyhow::Error> { + Ok(()) + } +} diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index b3b5719ef..1b6064774 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -212,9 +212,10 @@ impl StartCommandRecorded { #[cfg(test)] mod test { use super::*; + use crate::machines::test_help::{TestHistoryBuilder, TestWorkflowDriver}; use crate::{ machines::{ - complete_workflow_state_machine::complete_workflow, test_help::TestHistoryBuilder, + complete_workflow_state_machine::complete_workflow, workflow_machines::WorkflowMachines, DrivenWorkflow, WFCommand, }, protos::temporal::api::{ @@ -226,76 +227,12 @@ mod test { }, }; use rstest::{fixture, rstest}; - use std::sync::mpsc::Sender; - use std::{error::Error, sync::mpsc::channel, sync::mpsc::Receiver, time::Duration}; - use tracing_subscriber::layer::SubscriberExt; - use tracing_subscriber::util::SubscriberInitExt; - - // TODO: This will need to be broken out into it's own place and evolved / made more generic as - // we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to - // follow. - #[derive(Debug)] - struct TestWorkflowDriver { - /// A queue of command lists to return upon calls to [DrivenWorkflow::iterate_wf]. This - /// gives us more manual control than actually running the workflow for real would, for - /// example allowing us to simulate nondeterminism. - iteration_results: Receiver>, - iteration_sender: Sender>, - - /// The entire history passed in when we were constructed - full_history: Vec>, - } - - impl TestWorkflowDriver { - pub fn new(iteration_results: I) -> Self - where - I: IntoIterator>, - { - let (sender, receiver) = channel(); - Self { - iteration_results: receiver, - iteration_sender: sender, - full_history: iteration_results.into_iter().collect(), - } - } - } - - impl DrivenWorkflow for TestWorkflowDriver { - #[instrument] - fn start( - &self, - attribs: WorkflowExecutionStartedEventAttributes, - ) -> Result, anyhow::Error> { - self.full_history - .iter() - .for_each(|cmds| self.iteration_sender.send(cmds.to_vec()).unwrap()); - Ok(vec![]) - } - - #[instrument] - fn iterate_wf(&self) -> Result, anyhow::Error> { - // Timeout exists just to make blocking obvious. We should never block. - let cmd = self - .iteration_results - .recv_timeout(Duration::from_millis(10))?; - event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmd); - Ok(cmd) - } - - fn signal( - &self, - attribs: WorkflowExecutionSignaledEventAttributes, - ) -> Result<(), anyhow::Error> { - Ok(()) - } - - fn cancel( - &self, - attribs: WorkflowExecutionCanceledEventAttributes, - ) -> Result<(), anyhow::Error> { - Ok(()) - } - } + use std::{ + error::Error, + sync::mpsc::{channel, Receiver, Sender}, + time::Duration, + }; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[fixture] fn fire_happy_hist() -> (TestHistoryBuilder, WorkflowMachines) { From 54246bd2da91a68a0002002562f81d1e1023f74b Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Sun, 24 Jan 2021 15:07:33 +0200 Subject: [PATCH 24/59] Modify interface --- Cargo.toml | 2 +- protos/local/core_interface.proto | 72 ++++++++++++++++++++++--------- src/lib.rs | 26 ++++++++--- 3 files changed, 74 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e50a7d5fb..e81c446b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "sdk-core" +name = "temporal-sdk-core" version = "0.1.0" authors = ["Spencer Judge ", "Vitaly Arbuzov "] edition = "2018" diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index eca909199..2a4c4b2ff 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -6,22 +6,38 @@ package coresdk; // the include paths. You can make it work by going to the "Protobuf Support" settings section // and adding the "api_upstream" subdir as an include path. +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; +import "dependencies/gogoproto/gogo.proto"; import "temporal/api/workflowservice/v1/request_response.proto"; import "temporal/api/taskqueue/v1/message.proto"; import "temporal/api/enums/v1/failed_cause.proto"; import "temporal/api/failure/v1/message.proto"; import "temporal/api/common/v1/message.proto"; +import "temporal/api/command/v1/message.proto"; -// TODO: SDK prefix in front of everything is maybe pointless given it's all within this package +// TODO: Use the SDK prefix? +message WorkflowIdentifier { + string namespace = 1; + string name = 2; +} -service CoreSDKService { - rpc PollSDKTask (PollSDKTaskReq) returns (PollSDKTaskResp) {} - rpc CompleteSDKTask (CompleteSDKTaskReq) returns (CompleteSDKTaskResp) {} +message ActivityIdentifier { + string namespace = 1; + string name = 2; } -message PollSDKTaskReq { - // Maybe? Not sure it makes sense to support multiple workers in the same core sdk instance - repeated temporal.api.taskqueue.v1.TaskQueue task_queues = 1; +message RegistrationReq { + repeated WorkflowIdentifier workflows = 1; + repeated ActivityIdentifier activities = 2; +} + +// TODO: SDK prefix in front of everything is maybe pointless given it's all within this package + +service CoreSDKService { + rpc PollSDKTask (google.protobuf.Empty) returns (PollSDKTaskResp) {} + rpc CompleteSDKTask (CompleteSDKTaskReq) returns (google.protobuf.Empty) {} + rpc RegisterImplementations (RegistrationReq) returns (google.protobuf.Empty) {} } message PollSDKTaskResp { @@ -32,9 +48,34 @@ message PollSDKTaskResp { } } +message StartWorkflowTaskAttributes { + string namespace = 1; + string name = 2; + temporal.api.common.v1.Payloads arguments = 3; +} + +message CompleteTimerTaskAttributes { + int32 timer_id = 1; +} + +message CancelTimerTaskAttributes { + int32 timer_id = 1; +} + +enum WFTaskType { + START_WORKFLOW = 0; + COMPLETE_TIMER = 1; + CANCEL_TIMER = 2; +} + message SDKWFTask { - // Original task from temporal service - temporal.api.workflowservice.v1.PollWorkflowTaskQueueResponse original = 1; + WFTaskType type = 1; + google.protobuf.Timestamp timestamp = 2 [(gogoproto.stdtime) = true]; + oneof attributes { + StartWorkflowTaskAttributes start_workflow_task_attributes = 3; + CompleteTimerTaskAttributes complete_timer_task_attributes = 4; + CancelTimerTaskAttributes cancel_timer_task_attributes = 5; + } } message SDKActivityTask { @@ -51,8 +92,6 @@ message CompleteSDKTaskReq { } } -message CompleteSDKTaskResp {} - message SDKWFTaskCompletion { oneof status { SDKWFTaskSuccess successful = 1; @@ -68,7 +107,7 @@ message SDKActivityTaskCompletion { } message SDKWFTaskSuccess { - repeated SDKWFCommand commands = 1; + repeated temporal.api.command.v1.Command commands = 1; // Other bits from RespondWorkflowTaskCompletedRequest as needed } @@ -82,15 +121,8 @@ message SDKActivityTaskSuccess { temporal.api.common.v1.Payloads result = 1; // Other bits from RespondActivityTaskCompletedRequest as needed } + message SDKActivityTaskFailure { temporal.api.failure.v1.Failure failure = 1; // Other bits from RespondActivityTaskFailedRequest as needed } - -message SDKWFCommand { - oneof command { - // Commands go here. Should we reuse and add on top of the originals? - // https://github.com/temporalio/api/blob/master/temporal/api/command/v1/message.proto#L174 - bool nothing = 1; - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4d778e430..c42ef852b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,20 +5,36 @@ mod machines; mod pollers; pub mod protos; -use protos::coresdk::{CompleteSdkTaskReq, CompleteSdkTaskResp, PollSdkTaskReq, PollSdkTaskResp}; +use protos::coresdk::{CompleteSdkTaskReq, PollSdkTaskResp, RegistrationReq}; -type Result = std::result::Result; +pub type Result = std::result::Result; // TODO: Should probably enforce Send + Sync #[async_trait::async_trait] pub trait CoreSDKService { - async fn poll_sdk_task(&self, req: PollSdkTaskReq) -> Result; - async fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result; + async fn poll_sdk_task(&self) -> Result; + async fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; + async fn register_implementations(&self, req: RegistrationReq) -> Result<()>; +} + +pub struct CoreSDKInitOptions { + queue_name: String, + max_concurrent_workflow_executions: u32, + max_concurrent_activity_executions: u32, +} + +unsafe impl Send for CoreSDKInitOptions {} +unsafe impl Sync for CoreSDKInitOptions {} + +pub fn init_sdk(opts: CoreSDKInitOptions) -> Result> { + Err(SDKServiceError::Unknown {}) } #[derive(thiserror::Error, Debug)] pub enum SDKServiceError { - // tbd + #[error("Unknown service error")] + Unknown, + // more errors TBD } #[cfg(test)] From af6c682f17863e97bd0441d6fe7e24e7df3f6fa2 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 09:27:08 -0800 Subject: [PATCH 25/59] Moving back to desktop --- src/machines/test_help/history_builder.rs | 2 +- src/machines/timer_state_machine.rs | 26 ++++++----------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/machines/test_help/history_builder.rs b/src/machines/test_help/history_builder.rs index 42b94e927..19d1bf94b 100644 --- a/src/machines/test_help/history_builder.rs +++ b/src/machines/test_help/history_builder.rs @@ -210,7 +210,7 @@ impl TestHistoryBuilder { } /// Iterates over the events in this builder to return a [HistoryInfo] - fn get_history_info(&self, to_task_index: usize) -> Result { + pub(crate) fn get_history_info(&self, to_task_index: usize) -> Result { let mut lastest_wf_started_id = 0; let mut previous_wf_started_id = 0; let mut count = 0; diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 1b6064774..82f94480b 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -249,19 +249,15 @@ mod test { Should iterate once, produce started command, iterate again, producing no commands (timer complete), third iteration completes workflow. */ - let twd = TestWorkflowDriver::new(vec![ - vec![new_timer(StartTimerCommandAttributes { + let twd = TestWorkflowDriver::new(async { + let timer = new_timer(StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() - }) - .into()], - // TODO: Needs to be here in incremental one, but not full :/ - // vec![], // timer complete, no new commands - vec![complete_workflow( - CompleteWorkflowExecutionCommandAttributes::default(), - )], - ]); + }); + timer.await; + complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); + }); let mut t = TestHistoryBuilder::default(); let mut state_machines = WorkflowMachines::new(Box::new(twd)); @@ -284,16 +280,6 @@ mod test { #[rstest] fn test_fire_happy_path_inc(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { - let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() - .with_service_name("report_example") - .install() - .unwrap(); - let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); - tracing_subscriber::registry() - .with(opentelemetry) - .try_init() - .unwrap(); - let s = span!(Level::DEBUG, "Test start", t = "inc"); let _enter = s.enter(); From 05768de3d48420a02673af4f79dc42899b731f0a Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 10:59:24 -0800 Subject: [PATCH 26/59] Nice! Refactoring to allow handling *just* machine commands. Remove TSMCommand --- Cargo.toml | 1 + .../complete_workflow_state_machine.rs | 30 ++++-- src/machines/mod.rs | 56 +++++----- src/machines/timer_state_machine.rs | 80 ++++++++++---- src/machines/workflow_machines.rs | 102 +++++++----------- src/machines/workflow_task_state_machine.rs | 49 ++++++++- 6 files changed, 195 insertions(+), 123 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f6319576c..02884cec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ anyhow = "1.0" async-trait = "0.1" derive_more = "0.99" env_logger = "0.8" +futures = "0.3" log = "0.4" prost = "0.7" prost-types = "0.7" diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index c80da6e0d..6c46e32b2 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -1,8 +1,10 @@ -use crate::protos::temporal::api::command::v1::Command; use crate::{ - machines::{AddCommand, CancellableCommand, TSMCommand, WFCommand, WFMachinesError}, + machines::{ + workflow_machines::WorkflowMachines, AddCommand, CancellableCommand, WFCommand, + WFMachinesAdapter, WFMachinesError, + }, protos::temporal::api::{ - command::v1::CompleteWorkflowExecutionCommandAttributes, + command::v1::{Command, CompleteWorkflowExecutionCommandAttributes}, enums::v1::{CommandType, EventType}, history::v1::HistoryEvent, }, @@ -14,7 +16,7 @@ use std::{convert::TryFrom, rc::Rc}; fsm! { pub(super) name CompleteWorkflowMachine; - command TSMCommand; + command CompleteWFCommand; error WFMachinesError; shared_state CompleteWorkflowExecutionCommandAttributes; @@ -26,6 +28,11 @@ fsm! { --> CompleteWorkflowCommandRecorded; } +#[derive(Debug)] +pub(super) enum CompleteWFCommand { + AddCommand(AddCommand), +} + /// Complete a workflow pub(super) fn complete_workflow(attribs: CompleteWorkflowExecutionCommandAttributes) -> WFCommand { let (machine, add_cmd) = CompleteWorkflowMachine::new_scheduled(attribs); @@ -50,7 +57,7 @@ impl CompleteWorkflowMachine { .expect("Scheduling timers doesn't fail") .pop() { - Some(TSMCommand::AddCommand(c)) => c, + Some(CompleteWFCommand::AddCommand(c)) => c, _ => panic!("Timer on_schedule must produce command"), }; (s, cmd) @@ -92,7 +99,7 @@ impl Created { attributes: Some(dat.into()), }; TransitionResult::commands::<_, CompleteWorkflowCommandCreated>(vec![ - TSMCommand::AddCommand(cmd.into()), + CompleteWFCommand::AddCommand(cmd.into()), ]) } } @@ -111,3 +118,14 @@ impl From for CompleteWorkflowCommandRecorded { Default::default() } } + +impl WFMachinesAdapter for CompleteWorkflowMachine { + fn adapt_response( + _wf_machines: &mut WorkflowMachines, + _event: &HistoryEvent, + _has_next_event: bool, + _my_command: CompleteWFCommand, + ) -> Result<(), WFMachinesError> { + Ok(()) + } +} diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 8bbbfa9da..ab8ffb5b6 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -36,7 +36,7 @@ mod workflow_task_state_machine; mod test_help; use crate::{ - machines::workflow_machines::WFMachinesError, + machines::workflow_machines::{WFMachinesError, WorkflowMachines}, protos::temporal::api::{ command::v1::Command, enums::v1::CommandType, @@ -53,7 +53,6 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, rc::Rc, - time::SystemTime, }; use tracing::Level; @@ -86,26 +85,6 @@ trait DrivenWorkflow { ) -> Result<(), anyhow::Error>; } -/// These commands are issued by state machines to inform their driver ([WorkflowMachines]) that -/// it may need to take some action. -/// -/// In java this functionality was largely handled via callbacks passed into the state machines -/// which was difficult to follow -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub(crate) enum TSMCommand { - /// Issued by the [WorkflowTaskMachine] to (possibly) trigger the event loop - WFTaskStartedTrigger { - task_started_event_id: i64, - time: SystemTime, - }, - /// Issued by state machines to create commands - AddCommand(AddCommand), - // TODO: This is not quite right. Should be more like "notify completion". Need to investigate - // more examples - ProduceHistoryEvent(HistoryEvent), -} - /// The struct for [WFCommand::AddCommand] #[derive(Debug, derive_more::From)] pub(crate) struct AddCommand { @@ -129,11 +108,16 @@ enum WFCommand { trait TemporalStateMachine: CheckStateMachineInFinal { fn name(&self) -> &str; fn handle_command(&mut self, command_type: CommandType) -> Result<(), WFMachinesError>; + + /// Tell the state machine to handle some event. The [WorkflowMachines] instance calling this + /// function passes a reference to itself in so that the [WFMachinesAdapter] trait can use + /// the machines' specific type information while also manipulating [WorkflowMachines] fn handle_event( &mut self, event: &HistoryEvent, has_next_event: bool, - ) -> Result, WFMachinesError>; + wf_machines: &mut WorkflowMachines, + ) -> Result<(), WFMachinesError>; // TODO: This is a weird one that only applies to version state machine. Introduce only if // needed. Ideally handle differently. @@ -142,12 +126,10 @@ trait TemporalStateMachine: CheckStateMachineInFinal { impl TemporalStateMachine for SM where - SM: StateMachine + CheckStateMachineInFinal + Clone, + SM: StateMachine + CheckStateMachineInFinal + WFMachinesAdapter + Clone, ::Event: TryFrom, ::Event: TryFrom, ::Command: Debug, - // TODO: Do we really need this bound? Check back and see if we can just use TSMCommand directly - ::Command: Into, ::Error: Into + 'static + Send + Sync, { fn name(&self) -> &str { @@ -177,8 +159,9 @@ where fn handle_event( &mut self, event: &HistoryEvent, - _has_next_event: bool, - ) -> Result, WFMachinesError> { + has_next_event: bool, + wf_machines: &mut WorkflowMachines, + ) -> Result<(), WFMachinesError> { event!( Level::DEBUG, msg = "handling event", @@ -187,7 +170,13 @@ where ); if let Ok(converted_event) = event.clone().try_into() { match self.on_event_mut(converted_event) { - Ok(c) => Ok(c.into_iter().map(Into::into).collect()), + Ok(c) => { + event!(Level::DEBUG, msg = "Machine produced commands", ?c); + for cmd in c { + Self::adapt_response(wf_machines, event, has_next_event, cmd)?; + } + Ok(()) + } Err(MachineError::InvalidTransition) => { Err(WFMachinesError::UnexpectedEvent(event.clone())) } @@ -215,6 +204,15 @@ where } } +pub(super) trait WFMachinesAdapter: StateMachine { + fn adapt_response( + _wf_machines: &mut WorkflowMachines, + _event: &HistoryEvent, + _has_next_event: bool, + _my_command: Self::Command, + ) -> Result<(), WFMachinesError>; +} + /// A command which can be cancelled, associated with some state machine that produced it #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 82f94480b..36d6eeef0 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,9 +1,9 @@ #![allow(clippy::large_enum_variant)] +use crate::machines::workflow_machines::WorkflowMachines; +use crate::machines::WFMachinesAdapter; use crate::{ - machines::{ - workflow_machines::WFMachinesError, AddCommand, CancellableCommand, TSMCommand, WFCommand, - }, + machines::{workflow_machines::WFMachinesError, AddCommand, CancellableCommand, WFCommand}, protos::{ coresdk::HistoryEventId, temporal::api::{ @@ -22,7 +22,7 @@ use tracing::Level; fsm! { pub(super) name TimerMachine; - command TSMCommand; + command TimerMachineCommand; error WFMachinesError; shared_state SharedState; @@ -42,6 +42,12 @@ fsm! { CancelTimerCommandSent --(TimerCanceled) --> Canceled; } +#[derive(Debug)] +pub(super) enum TimerMachineCommand { + AddCommand(AddCommand), + Complete(HistoryEvent), +} + /// Creates a new, scheduled, timer as a [CancellableCommand] pub(super) fn new_timer(attribs: StartTimerCommandAttributes) -> CancellableCommand { let (timer, add_cmd) = TimerMachine::new_scheduled(attribs); @@ -60,7 +66,7 @@ impl TimerMachine { .expect("Scheduling timers doesn't fail") .pop() { - Some(TSMCommand::AddCommand(c)) => c, + Some(TimerMachineCommand::AddCommand(c)) => c, _ => panic!("Timer on_schedule must produce command"), }; (s, cmd) @@ -107,7 +113,7 @@ pub(super) struct SharedState { } impl SharedState { - fn into_timer_canceled_event_command(self) -> TSMCommand { + fn into_timer_canceled_event_command(self) -> TimerMachineCommand { let attrs = TimerCanceledEventAttributes { identity: "workflow".to_string(), timer_id: self.timer_attributes.timer_id, @@ -120,7 +126,7 @@ impl SharedState { )), ..Default::default() }; - TSMCommand::ProduceHistoryEvent(event) + TimerMachineCommand::Complete(event) } } @@ -133,9 +139,9 @@ impl Created { command_type: CommandType::StartTimer as i32, attributes: Some(dat.timer_attributes.into()), }; - TimerMachineTransition::commands::<_, StartCommandCreated>(vec![TSMCommand::AddCommand( - cmd.into(), - )]) + TimerMachineTransition::commands::<_, StartCommandCreated>(vec![ + TimerMachineCommand::AddCommand(cmd.into()), + ]) } } @@ -187,10 +193,7 @@ pub(super) struct StartCommandRecorded {} impl StartCommandRecorded { pub(super) fn on_timer_fired(self, event: HistoryEvent) -> TimerMachineTransition { - TimerMachineTransition::ok( - vec![TSMCommand::ProduceHistoryEvent(event)], - Fired::default(), - ) + TimerMachineTransition::ok(vec![TimerMachineCommand::Complete(event)], Fired::default()) } pub(super) fn on_cancel(self, dat: SharedState) -> TimerMachineTransition { let cmd = Command { @@ -203,20 +206,38 @@ impl StartCommandRecorded { ), }; TimerMachineTransition::ok( - vec![TSMCommand::AddCommand(cmd.into())], + vec![TimerMachineCommand::AddCommand(cmd.into())], CancelTimerCommandCreated::default(), ) } } +impl WFMachinesAdapter for TimerMachine { + fn adapt_response( + _wf_machines: &mut WorkflowMachines, + _event: &HistoryEvent, + _has_next_event: bool, + my_command: TimerMachineCommand, + ) -> Result<(), WFMachinesError> { + match my_command { + TimerMachineCommand::AddCommand(_) => { + unreachable!() + } + TimerMachineCommand::Complete(_event) => {} + } + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; - use crate::machines::test_help::{TestHistoryBuilder, TestWorkflowDriver}; use crate::{ machines::{ complete_workflow_state_machine::complete_workflow, - workflow_machines::WorkflowMachines, DrivenWorkflow, WFCommand, + test_help::{TestHistoryBuilder, TestWorkflowDriver}, + workflow_machines::WorkflowMachines, + DrivenWorkflow, WFCommand, }, protos::temporal::api::{ command::v1::CompleteWorkflowExecutionCommandAttributes, @@ -249,15 +270,28 @@ mod test { Should iterate once, produce started command, iterate again, producing no commands (timer complete), third iteration completes workflow. */ - let twd = TestWorkflowDriver::new(async { - let timer = new_timer(StartTimerCommandAttributes { + // let twd = TestWorkflowDriver::new(async { + // let timer = new_timer(StartTimerCommandAttributes { + // timer_id: "Sometimer".to_string(), + // start_to_fire_timeout: Some(Duration::from_secs(5).into()), + // ..Default::default() + // }); + // //timer.await; + // complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); + // }); + let twd = TestWorkflowDriver::new(vec![ + vec![new_timer(StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() - }); - timer.await; - complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); - }); + }) + .into()], + // TODO: Needs to be here in incremental one, but not full :/ + // vec![], // timer complete, no new commands + vec![complete_workflow( + CompleteWorkflowExecutionCommandAttributes::default(), + )], + ]); let mut t = TestHistoryBuilder::default(); let mut state_machines = WorkflowMachines::new(Box::new(twd)); diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index cead82a31..046431f7a 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,13 +1,14 @@ use crate::{ machines::{ workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, - MachineCommand, TSMCommand, TemporalStateMachine, WFCommand, + MachineCommand, TemporalStateMachine, WFCommand, }, protos::temporal::api::{ enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent}, }, }; +use rustfsm::StateMachine; use std::{ borrow::BorrowMut, cell::RefCell, @@ -115,65 +116,9 @@ impl WorkflowMachines { match event.get_initial_command_event_id() { Some(initial_cmd_id) => { if let Some(sm) = self.machines_by_id.get(&initial_cmd_id) { - let commands = (*sm.clone()) + (*sm.clone()) .borrow_mut() - .handle_event(event, has_next_event)? - .into_iter(); - event!(Level::DEBUG, msg = "Machine produced commands", ?commands); - for cmd in commands { - match cmd { - TSMCommand::WFTaskStartedTrigger { - task_started_event_id, - time, - } => { - let cur_event_past_or_at_start = - event.event_id >= task_started_event_id; - if event_type == EventType::WorkflowTaskStarted - && !(cur_event_past_or_at_start && !has_next_event) - { - // Last event in history is a task started event, so we don't - // want to iterate. - continue; - } - let s = span!(Level::DEBUG, "Task started trigger"); - let _enter = s.enter(); - - // TODO: Seems to only matter for version machine. Figure out then. - // // If some new commands are pending and there are no more command events. - // for (CancellableCommand cancellableCommand : commands) { - // if (cancellableCommand == null) { - // break; - // } - // cancellableCommand.handleWorkflowTaskStarted(); - // } - - // TODO: Local activity machines - // // Give local activities a chance to recreate their requests if they were lost due - // // to the last workflow task failure. The loss could happen only the last workflow task - // // was forcibly created by setting forceCreate on RespondWorkflowTaskCompletedRequest. - // if (nonProcessedWorkflowTask) { - // for (LocalActivityStateMachine value : localActivityMap.values()) { - // value.nonReplayWorkflowTaskStarted(); - // } - // } - - self.current_started_event_id = task_started_event_id; - self.set_current_time(time); - self.event_loop()?; - } - TSMCommand::ProduceHistoryEvent(event) => { - // TODO: Make this go. During replay, at least, we need to ensure - // produced event matches expected? - event!(Level::DEBUG, msg = "History event produced", %event); - } - TSMCommand::AddCommand(_) => { - // TODO: This is where we could handle machines saying they need - // to cancel themselves (since they don't have a ref to their own - // command, could go with that too) -- but also - unimplemented!() - } - } - } + .handle_event(event, has_next_event, self)?; } else { event!( Level::ERROR, @@ -196,6 +141,41 @@ impl WorkflowMachines { Ok(()) } + /// Called when we want to run the event loop because a workflow task started event has + /// triggered + pub(super) fn task_started( + &mut self, + task_started_event_id: i64, + time: SystemTime, + ) -> Result<()> { + let s = span!(Level::DEBUG, "Task started trigger"); + let _enter = s.enter(); + + // TODO: Seems to only matter for version machine. Figure out then. + // // If some new commands are pending and there are no more command events. + // for (CancellableCommand cancellableCommand : commands) { + // if (cancellableCommand == null) { + // break; + // } + // cancellableCommand.handleWorkflowTaskStarted(); + // } + + // TODO: Local activity machines + // // Give local activities a chance to recreate their requests if they were lost due + // // to the last workflow task failure. The loss could happen only the last workflow task + // // was forcibly created by setting forceCreate on RespondWorkflowTaskCompletedRequest. + // if (nonProcessedWorkflowTask) { + // for (LocalActivityStateMachine value : localActivityMap.values()) { + // value.nonReplayWorkflowTaskStarted(); + // } + // } + + self.current_started_event_id = task_started_event_id; + self.set_current_time(time); + self.event_loop()?; + Ok(()) + } + /// A command event is an event which is generated from a command emitted by a past decision. /// Each command has a correspondent event. For example ScheduleActivityTaskCommand is recorded /// to the history as ActivityTaskScheduledEvent. @@ -226,7 +206,7 @@ impl WorkflowMachines { // Feed the machine the event let mut break_later = false; if let CancellableCommand::Active { mut machine, .. } = command.clone() { - let out_commands = (*machine).borrow_mut().handle_event(event, true)?; + let out_commands = (*machine).borrow_mut().handle_event(event, true, self)?; // TODO: Handle invalid event errors // * More special handling for version machine - see java // * Command/machine supposed to have cancelled itself @@ -282,7 +262,7 @@ impl WorkflowMachines { } Some(EventType::WorkflowTaskScheduled) => { let mut wf_task_sm = WorkflowTaskMachine::new(self.workflow_task_started_event_id); - wf_task_sm.handle_event(event, has_next_event)?; + wf_task_sm.handle_event(event, has_next_event, self)?; self.machines_by_id .insert(event.event_id, Rc::new(RefCell::new(wf_task_sm))); } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index f546736d4..59c544bd2 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -1,7 +1,10 @@ #![allow(clippy::enum_variant_names)] use crate::{ - machines::{workflow_machines::WFMachinesError, TSMCommand}, + machines::{ + workflow_machines::{WFMachinesError, WorkflowMachines}, + WFMachinesAdapter, + }, protos::temporal::api::{ enums::v1::{CommandType, EventType}, history::v1::HistoryEvent, @@ -13,7 +16,7 @@ use tracing::Level; fsm! { pub(super) name WorkflowTaskMachine; - command TSMCommand; + command WFTaskMachineCommand; error WFMachinesError; shared_state SharedState; @@ -38,6 +41,44 @@ impl WorkflowTaskMachine { } } +#[derive(Debug)] +pub(super) enum WFTaskMachineCommand { + /// Issued to (possibly) trigger the event loop + WFTaskStartedTrigger { + task_started_event_id: i64, + time: SystemTime, + }, +} + +impl WFMachinesAdapter for WorkflowTaskMachine { + fn adapt_response( + wf_machines: &mut WorkflowMachines, + event: &HistoryEvent, + has_next_event: bool, + my_command: WFTaskMachineCommand, + ) -> Result<(), WFMachinesError> { + match my_command { + WFTaskMachineCommand::WFTaskStartedTrigger { + task_started_event_id, + time, + } => { + let event_type = EventType::from_i32(event.event_type) + .ok_or_else(|| WFMachinesError::UnexpectedEvent(event.clone()))?; + let cur_event_past_or_at_start = event.event_id >= task_started_event_id; + if event_type == EventType::WorkflowTaskStarted + && !(cur_event_past_or_at_start && !has_next_event) + { + // Last event in history is a task started event, so we don't + // want to iterate. + return Ok(()); + } + wf_machines.task_started(task_started_event_id, time)?; + } + } + Ok(()) + } +} + impl TryFrom for WorkflowTaskMachineEvents { type Error = WFMachinesError; @@ -100,7 +141,7 @@ impl Scheduled { }: WFTStartedDat, ) -> WorkflowTaskMachineTransition { WorkflowTaskMachineTransition::ok( - vec![TSMCommand::WFTaskStartedTrigger { + vec![WFTaskMachineCommand::WFTaskStartedTrigger { task_started_event_id: shared.wf_task_started_event_id, time: current_time_millis, }], @@ -129,7 +170,7 @@ pub(super) struct Started { impl Started { pub(super) fn on_workflow_task_completed(self) -> WorkflowTaskMachineTransition { WorkflowTaskMachineTransition::commands::<_, Completed>(vec![ - TSMCommand::WFTaskStartedTrigger { + WFTaskMachineCommand::WFTaskStartedTrigger { task_started_event_id: self.started_event_id, time: self.current_time_millis, }, From 84b717ae5484f83b33bb1196582e68026d62b2e3 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 11:49:12 -0800 Subject: [PATCH 27/59] Add new way to create the timers with completion future --- .../complete_workflow_state_machine.rs | 1 + src/machines/mod.rs | 8 ++++++- src/machines/timer_state_machine.rs | 12 ++++++++-- src/machines/workflow_machines.rs | 22 +++++++++++++++++++ src/machines/workflow_task_state_machine.rs | 1 + 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index 6c46e32b2..6e78885d1 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -121,6 +121,7 @@ impl From for CompleteWorkflowCommandRecorded { impl WFMachinesAdapter for CompleteWorkflowMachine { fn adapt_response( + &self, _wf_machines: &mut WorkflowMachines, _event: &HistoryEvent, _has_next_event: bool, diff --git a/src/machines/mod.rs b/src/machines/mod.rs index ab8ffb5b6..920202a9e 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -173,7 +173,7 @@ where Ok(c) => { event!(Level::DEBUG, msg = "Machine produced commands", ?c); for cmd in c { - Self::adapt_response(wf_machines, event, has_next_event, cmd)?; + self.adapt_response(wf_machines, event, has_next_event, cmd)?; } Ok(()) } @@ -204,8 +204,14 @@ where } } +/// This trait exists to bridge [StateMachine]s and the [WorkflowMachines] instance. It has access +/// to the machine's concrete types while hiding those details from [WorkflowMachines] pub(super) trait WFMachinesAdapter: StateMachine { + /// Given a reference to [WorkflowMachines], the event being processed, and a command that this + /// [StateMachine] instance just produced, perform any handling that needs access to the + /// [WorkflowMachines] instance in response to that command. fn adapt_response( + &self, _wf_machines: &mut WorkflowMachines, _event: &HistoryEvent, _has_next_event: bool, diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 36d6eeef0..afbdc9eec 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -149,6 +149,7 @@ impl Created { pub(super) struct CancelTimerCommandCreated {} impl CancelTimerCommandCreated { pub(super) fn on_command_cancel_timer(self, dat: SharedState) -> TimerMachineTransition { + // TODO: Think we need to produce a completion command here TimerMachineTransition::ok( vec![dat.into_timer_canceled_event_command()], Canceled::default(), @@ -214,7 +215,8 @@ impl StartCommandRecorded { impl WFMachinesAdapter for TimerMachine { fn adapt_response( - _wf_machines: &mut WorkflowMachines, + &self, + wf_machines: &mut WorkflowMachines, _event: &HistoryEvent, _has_next_event: bool, my_command: TimerMachineCommand, @@ -223,7 +225,13 @@ impl WFMachinesAdapter for TimerMachine { TimerMachineCommand::AddCommand(_) => { unreachable!() } - TimerMachineCommand::Complete(_event) => {} + // Fire the completion + TimerMachineCommand::Complete(_event) => { + wf_machines + .timer_futures + .remove(&self.shared_state.timer_attributes.timer_id) + .map(|c| c.send(true)); + } } Ok(()) } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 046431f7a..c0962f65d 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,3 +1,5 @@ +use crate::machines::timer_state_machine::new_timer; +use crate::protos::temporal::api::command::v1::StartTimerCommandAttributes; use crate::{ machines::{ workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, @@ -8,6 +10,8 @@ use crate::{ history::v1::{history_event, HistoryEvent}, }, }; +use futures::channel::oneshot; +use futures::Future; use rustfsm::StateMachine; use std::{ borrow::BorrowMut, @@ -48,6 +52,9 @@ pub(crate) struct WorkflowMachines { /// The workflow that is being driven by this instance of the machines drive_me: Box, + + /// Holds channels for completing timers. Key is the ID of the timer. + pub(super) timer_futures: HashMap>, } #[derive(thiserror::Error, Debug)] @@ -80,9 +87,24 @@ impl WorkflowMachines { machines_by_id: Default::default(), commands: Default::default(), current_wf_task_commands: Default::default(), + timer_futures: Default::default(), } } + /// Create a new timer for this workflow with the provided attributes + /// + /// Returns the command and a future that will resolve when the timer completes + pub(super) fn new_timer( + &mut self, + attribs: StartTimerCommandAttributes, + ) -> (CancellableCommand, impl Future) { + let timer_id = attribs.timer_id.clone(); + let timer = new_timer(attribs); + let (tx, rx) = oneshot::channel(); + self.timer_futures.insert(timer_id, tx); + (timer, rx) + } + /// Returns the id of the last seen WorkflowTaskStarted event pub(super) fn get_last_started_event_id(&self) -> i64 { self.current_started_event_id diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 59c544bd2..9ccd5d925 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -52,6 +52,7 @@ pub(super) enum WFTaskMachineCommand { impl WFMachinesAdapter for WorkflowTaskMachine { fn adapt_response( + &self, wf_machines: &mut WorkflowMachines, event: &HistoryEvent, has_next_event: bool, From b499fa9312999379cc75701545b37f51c4f5a498 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 15:21:11 -0800 Subject: [PATCH 28/59] Getting real close. Just need to wrap the command sink to be able to return futures to await timer completion --- src/machines/mod.rs | 11 +-- src/machines/test_help/workflow_driver.rs | 81 +++++++++++---------- src/machines/timer_state_machine.rs | 67 ++++------------- src/machines/workflow_machines.rs | 9 +-- src/machines/workflow_task_state_machine.rs | 1 + 5 files changed, 63 insertions(+), 106 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 920202a9e..43a520b93 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -234,19 +234,10 @@ enum CancellableCommand { } impl CancellableCommand { - #[allow(dead_code)] + #[allow(dead_code)] // TODO: Use pub(super) fn cancel(&mut self) { *self = CancellableCommand::Cancelled; } - - #[cfg(test)] - fn unwrap_machine(&mut self) -> &mut dyn TemporalStateMachine { - if let CancellableCommand::Active { machine, .. } = self { - RefCell::get_mut(Rc::get_mut(machine).unwrap()) - } else { - panic!("No machine in command, already canceled") - } - } } impl Debug for dyn TemporalStateMachine { diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index fed684a4f..91bca923b 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -6,61 +6,68 @@ use crate::{ WorkflowExecutionStartedEventAttributes, }, }; -use std::{ - sync::mpsc::{channel, Receiver, Sender}, - time::Duration, +use futures::{ + channel::mpsc::{channel, Sender}, + executor::block_on, + Future, StreamExt, }; use tracing::Level; -// TODO: This will need to be broken out into it's own place and evolved / made more generic as -// we learn more. It replaces "TestEnitityTestListenerBase" in java which is pretty hard to -// follow. +/// This is a test only implementation of a [DrivenWorkflow] which has finer-grained control +/// over when commands are returned than a normal workflow would. +/// +/// It replaces "TestEnitityTestListenerBase" in java which is pretty hard to follow. #[derive(Debug)] -pub(crate) struct TestWorkflowDriver { - /// A queue of command lists to return upon calls to [DrivenWorkflow::iterate_wf]. This - /// gives us more manual control than actually running the workflow for real would, for - /// example allowing us to simulate nondeterminism. - iteration_results: Receiver>, - iteration_sender: Sender>, - - /// The entire history passed in when we were constructed - full_history: Vec>, +pub(in crate::machines) struct TestWorkflowDriver { + wf_function: F, } -impl TestWorkflowDriver { - pub(in crate::machines) fn new(iteration_results: I) -> Self - where - I: IntoIterator>, - { - let (sender, receiver) = channel(); +impl TestWorkflowDriver +where + F: Fn(Sender) -> Fut, + Fut: Future, +{ + pub(in crate::machines) fn new(workflow_fn: F) -> Self { Self { - iteration_results: receiver, - iteration_sender: sender, - full_history: iteration_results.into_iter().collect(), + wf_function: workflow_fn, } } + // TODO: Need some way to make the await return immediately for timer future even when it + // hasn't completed, when it should produce the "thunk" for waiting + // pub(crate) fn new_timer( + // &self, + // attribs: StartTimerCommandAttributes, + // ) -> (CancellableCommand, impl Future) { + // } } -impl DrivenWorkflow for TestWorkflowDriver { - #[instrument] +impl DrivenWorkflow for TestWorkflowDriver +where + F: Fn(Sender) -> Fut, + Fut: Future, +{ + #[instrument(skip(self))] fn start( &self, - attribs: WorkflowExecutionStartedEventAttributes, + _attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, anyhow::Error> { - self.full_history - .iter() - .for_each(|cmds| self.iteration_sender.send(cmds.to_vec()).unwrap()); Ok(vec![]) } - #[instrument] + #[instrument(skip(self))] fn iterate_wf(&self) -> Result, anyhow::Error> { - // Timeout exists just to make blocking obvious. We should never block. - let cmd = self - .iteration_results - .recv_timeout(Duration::from_millis(10))?; - event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmd); - Ok(cmd) + dbg!("iterate"); + let (sender, receiver) = channel(1_000); + // Call the closure that produces the workflow future + let wf_future = (self.wf_function)(sender); + + // TODO: This block_on might cause issues later + block_on(wf_future); + let cmds: Vec<_> = block_on(async { receiver.collect().await }); + event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmds); + + // Return only the last command + Ok(vec![cmds.into_iter().last().unwrap()]) } fn signal( diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index afbdc9eec..48e64eaea 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -255,12 +255,10 @@ mod test { }, }, }; + use futures::channel::mpsc::Sender; + use futures::{FutureExt, SinkExt}; use rstest::{fixture, rstest}; - use std::{ - error::Error, - sync::mpsc::{channel, Receiver, Sender}, - time::Duration, - }; + use std::{error::Error, time::Duration}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[fixture] @@ -275,31 +273,21 @@ mod test { 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + // TODO: Update this with deets Should iterate once, produce started command, iterate again, producing no commands (timer complete), third iteration completes workflow. */ - // let twd = TestWorkflowDriver::new(async { - // let timer = new_timer(StartTimerCommandAttributes { - // timer_id: "Sometimer".to_string(), - // start_to_fire_timeout: Some(Duration::from_secs(5).into()), - // ..Default::default() - // }); - // //timer.await; - // complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); - // }); - let twd = TestWorkflowDriver::new(vec![ - vec![new_timer(StartTimerCommandAttributes { + let twd = TestWorkflowDriver::new(|mut command_sink: Sender| async move { + let timer = new_timer(StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() - }) - .into()], - // TODO: Needs to be here in incremental one, but not full :/ - // vec![], // timer complete, no new commands - vec![complete_workflow( - CompleteWorkflowExecutionCommandAttributes::default(), - )], - ]); + }); + command_sink.send(timer.into()).await; + + let complete = complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); + command_sink.send(complete).await; + }); let mut t = TestHistoryBuilder::default(); let mut state_machines = WorkflowMachines::new(Box::new(twd)); @@ -361,40 +349,11 @@ mod test { let commands = t .handle_workflow_task_take_cmds(&mut state_machines, Some(2)) .unwrap(); + dbg!(&commands); assert_eq!(commands.len(), 1); assert_eq!( commands[0].command_type, CommandType::CompleteWorkflowExecution as i32 ); } - - #[test] - fn test_timer_cancel() { - // TODO: Incomplete - - let mut timer_cmd = new_timer(StartTimerCommandAttributes { - timer_id: "Sometimer".to_string(), - start_to_fire_timeout: Some(Duration::from_secs(5).into()), - ..Default::default() - }); - let timer_machine = timer_cmd.unwrap_machine(); - let twd = TestWorkflowDriver::new(vec![ - vec![timer_cmd.into()], - vec![complete_workflow( - CompleteWorkflowExecutionCommandAttributes::default(), - )], - ]); - - let mut t = TestHistoryBuilder::default(); - let mut state_machines = WorkflowMachines::new(Box::new(twd)); - - t.add_by_type(EventType::WorkflowExecutionStarted); - t.add_workflow_task(); - let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); - t.add_workflow_task_scheduled_and_started(); - assert_eq!(2, t.get_workflow_task_count(None).unwrap()); - let commands = t - .handle_workflow_task_take_cmds(&mut state_machines, Some(1)) - .unwrap(); - } } diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index c0962f65d..7484c09ae 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,17 +1,16 @@ -use crate::machines::timer_state_machine::new_timer; -use crate::protos::temporal::api::command::v1::StartTimerCommandAttributes; use crate::{ machines::{ + test_help::TestWorkflowDriver, timer_state_machine::new_timer, workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, MachineCommand, TemporalStateMachine, WFCommand, }, protos::temporal::api::{ + command::v1::StartTimerCommandAttributes, enums::v1::{CommandType, EventType}, history::v1::{history_event, HistoryEvent}, }, }; -use futures::channel::oneshot; -use futures::Future; +use futures::{channel::mpsc::Sender, channel::oneshot, Future}; use rustfsm::StateMachine; use std::{ borrow::BorrowMut, @@ -51,7 +50,7 @@ pub(crate) struct WorkflowMachines { current_wf_task_commands: VecDeque, /// The workflow that is being driven by this instance of the machines - drive_me: Box, + drive_me: Box, /// Holds channels for completing timers. Key is the ID of the timer. pub(super) timer_futures: HashMap>, diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 9ccd5d925..04def12f0 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -73,6 +73,7 @@ impl WFMachinesAdapter for WorkflowTaskMachine { // want to iterate. return Ok(()); } + dbg!(&event); wf_machines.task_started(task_started_event_id, time)?; } } From f0f95625ebe5fd22bcf2fcb3003531aa860b3200 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 17:39:39 -0800 Subject: [PATCH 29/59] Remarkably close. Saving this before trying slot type approach --- .../complete_workflow_state_machine.rs | 5 +- src/machines/mod.rs | 15 ++- src/machines/test_help/mod.rs | 2 +- src/machines/test_help/workflow_driver.rs | 123 +++++++++++++++--- src/machines/timer_state_machine.rs | 15 ++- src/machines/workflow_machines.rs | 24 +++- src/machines/workflow_task_state_machine.rs | 1 - 7 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/machines/complete_workflow_state_machine.rs b/src/machines/complete_workflow_state_machine.rs index 6e78885d1..e6f50f51b 100644 --- a/src/machines/complete_workflow_state_machine.rs +++ b/src/machines/complete_workflow_state_machine.rs @@ -34,13 +34,14 @@ pub(super) enum CompleteWFCommand { } /// Complete a workflow -pub(super) fn complete_workflow(attribs: CompleteWorkflowExecutionCommandAttributes) -> WFCommand { +pub(super) fn complete_workflow( + attribs: CompleteWorkflowExecutionCommandAttributes, +) -> CancellableCommand { let (machine, add_cmd) = CompleteWorkflowMachine::new_scheduled(attribs); CancellableCommand::Active { command: add_cmd.command, machine: Rc::new(RefCell::new(machine)), } - .into() } impl CompleteWorkflowMachine { diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 43a520b93..5033735fe 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -38,7 +38,9 @@ mod test_help; use crate::{ machines::workflow_machines::{WFMachinesError, WorkflowMachines}, protos::temporal::api::{ - command::v1::Command, + command::v1::{ + Command, CompleteWorkflowExecutionCommandAttributes, StartTimerCommandAttributes, + }, enums::v1::CommandType, history::v1::{ HistoryEvent, WorkflowExecutionCanceledEventAttributes, @@ -46,6 +48,7 @@ use crate::{ }, }, }; +use futures::channel::oneshot; use prost::alloc::fmt::Formatter; use rustfsm::{MachineError, StateMachine}; use std::{ @@ -94,12 +97,10 @@ pub(crate) struct AddCommand { /// [DrivenWorkflow]s respond with these when called, to indicate what they want to do next. /// EX: Create a new timer, complete the workflow, etc. -/// -/// TODO: Maybe this and TSMCommand are really the same thing? -#[derive(Debug, derive_more::From, Clone)] -enum WFCommand { - /// Add a new entity/command - Add(CancellableCommand), +#[derive(Debug, derive_more::From)] +pub enum WFCommand { + AddTimer(StartTimerCommandAttributes, oneshot::Sender), + CompleteWorkflow(CompleteWorkflowExecutionCommandAttributes), } /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. diff --git a/src/machines/test_help/mod.rs b/src/machines/test_help/mod.rs index c7aa4037f..427f0876f 100644 --- a/src/machines/test_help/mod.rs +++ b/src/machines/test_help/mod.rs @@ -4,4 +4,4 @@ mod history_builder; mod workflow_driver; pub(super) use history_builder::TestHistoryBuilder; -pub(super) use workflow_driver::TestWorkflowDriver; +pub(super) use workflow_driver::{CommandSender, TestWFCommand, TestWorkflowDriver}; diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 91bca923b..5f9d96a85 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -1,18 +1,28 @@ use super::Result; use crate::{ machines::{DrivenWorkflow, WFCommand}, - protos::temporal::api::history::v1::{ - WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, - WorkflowExecutionStartedEventAttributes, + protos::temporal::api::{ + command::v1::StartTimerCommandAttributes, + history::v1::{ + WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, + WorkflowExecutionStartedEventAttributes, + }, }, }; -use futures::{ - channel::mpsc::{channel, Sender}, - executor::block_on, - Future, StreamExt, +use futures::{channel::oneshot, executor::block_on, lock::Mutex, Future, FutureExt}; +use std::sync::Arc; +use std::{ + collections::{hash_map, HashMap}, + sync::{ + mpsc::{self, Receiver, Sender}, + RwLock, + }, }; use tracing::Level; +// TODO: Mutex is not necessary +type TimerMap = HashMap>>>; + /// This is a test only implementation of a [DrivenWorkflow] which has finer-grained control /// over when commands are returned than a normal workflow would. /// @@ -20,30 +30,29 @@ use tracing::Level; #[derive(Debug)] pub(in crate::machines) struct TestWorkflowDriver { wf_function: F, + + /// Holds completion channels for timers so we don't recreate them by accident. Key is timer id. + /// + /// I don't love that this is pretty duplicative of workflow machines + timers: RwLock, } impl TestWorkflowDriver where - F: Fn(Sender) -> Fut, + F: Fn(CommandSender<'_>) -> Fut, Fut: Future, { pub(in crate::machines) fn new(workflow_fn: F) -> Self { Self { wf_function: workflow_fn, + timers: RwLock::new(HashMap::default()), } } - // TODO: Need some way to make the await return immediately for timer future even when it - // hasn't completed, when it should produce the "thunk" for waiting - // pub(crate) fn new_timer( - // &self, - // attribs: StartTimerCommandAttributes, - // ) -> (CancellableCommand, impl Future) { - // } } impl DrivenWorkflow for TestWorkflowDriver where - F: Fn(Sender) -> Fut, + F: Fn(CommandSender<'_>) -> Fut, Fut: Future, { #[instrument(skip(self))] @@ -57,17 +66,32 @@ where #[instrument(skip(self))] fn iterate_wf(&self) -> Result, anyhow::Error> { dbg!("iterate"); - let (sender, receiver) = channel(1_000); + let (sender, receiver) = CommandSender::new(&self.timers); // Call the closure that produces the workflow future let wf_future = (self.wf_function)(sender); // TODO: This block_on might cause issues later block_on(wf_future); - let cmds: Vec<_> = block_on(async { receiver.collect().await }); + let cmds = receiver.into_iter(); event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmds); - // Return only the last command - Ok(vec![cmds.into_iter().last().unwrap()]) + let mut last_cmd = None; + for cmd in cmds { + match cmd { + TestWFCommand::WFCommand(c) => last_cmd = Some(c), + TestWFCommand::Waiting => { + // Ignore further commands since we're waiting on something + break; + } + } + } + + // Return only the last command. TODO: Later this will need to account for the wf + Ok(if let Some(c) = dbg!(last_cmd) { + vec![c] + } else { + vec![] + }) } fn signal( @@ -84,3 +108,62 @@ where Ok(()) } } + +#[derive(Debug, derive_more::From)] +pub enum TestWFCommand { + WFCommand(WFCommand), + /// When a test workflow wants to await something like a timer or an activity, we will + /// ignore all commands produced after the wait, since they couldn't have actually happened + /// in a real workflow, since you'd be stuck waiting + Waiting, +} + +pub struct CommandSender<'a> { + chan: Sender, + timer_map: &'a RwLock, +} + +impl<'a> CommandSender<'a> { + fn new(timer_map: &'a RwLock) -> (Self, Receiver) { + let (chan, rx) = mpsc::channel(); + (Self { chan, timer_map }, rx) + } + + /// Request to create a timer, returning future which resolves when the timer completes + pub fn timer(&mut self, a: StartTimerCommandAttributes) -> impl Future + '_ { + dbg!("Timer call", &a.timer_id); + let mut rx = match self.timer_map.write().unwrap().entry(a.timer_id.clone()) { + hash_map::Entry::Occupied(existing) => { + dbg!("occupied"); + existing.get().clone() + } + hash_map::Entry::Vacant(v) => { + dbg!("vacant"); + let (tx, rx) = oneshot::channel(); + let c = WFCommand::AddTimer(a, tx); + // In theory we should send this in both branches + self.chan.send(c.into()).unwrap(); + v.insert(Arc::new(Mutex::new(rx))).clone() + } + }; + let chan = &mut self.chan; + async move { + // let mut lock = rx.lock().await.fuse(); + futures::select! { + // If the timer is done, nothing to do. + _ = &(*rx.lock().await) => { + dbg!("He done"); + } + // Timer is not done. Send waiting command + default => { + dbg!("He waitin"); + chan.send(TestWFCommand::Waiting).unwrap(); + } + } + } + } + + pub fn send(&mut self, c: WFCommand) { + self.chan.send(c.into()).unwrap(); + } +} diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 48e64eaea..94fc78e98 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -240,10 +240,11 @@ impl WFMachinesAdapter for TimerMachine { #[cfg(test)] mod test { use super::*; + use crate::machines::test_help::CommandSender; use crate::{ machines::{ complete_workflow_state_machine::complete_workflow, - test_help::{TestHistoryBuilder, TestWorkflowDriver}, + test_help::{TestHistoryBuilder, TestWFCommand, TestWorkflowDriver}, workflow_machines::WorkflowMachines, DrivenWorkflow, WFCommand, }, @@ -277,16 +278,16 @@ mod test { Should iterate once, produce started command, iterate again, producing no commands (timer complete), third iteration completes workflow. */ - let twd = TestWorkflowDriver::new(|mut command_sink: Sender| async move { - let timer = new_timer(StartTimerCommandAttributes { + let twd = TestWorkflowDriver::new(|mut command_sink: CommandSender| async move { + let timer = StartTimerCommandAttributes { timer_id: "Sometimer".to_string(), start_to_fire_timeout: Some(Duration::from_secs(5).into()), ..Default::default() - }); - command_sink.send(timer.into()).await; + }; + command_sink.timer(timer).await; - let complete = complete_workflow(CompleteWorkflowExecutionCommandAttributes::default()); - command_sink.send(complete).await; + let complete = CompleteWorkflowExecutionCommandAttributes::default(); + command_sink.send(complete.into()); }); let mut t = TestHistoryBuilder::default(); diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 7484c09ae..547ff134d 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,3 +1,4 @@ +use crate::machines::complete_workflow_state_machine::complete_workflow; use crate::{ machines::{ test_help::TestWorkflowDriver, timer_state_machine::new_timer, @@ -10,7 +11,7 @@ use crate::{ history::v1::{history_event, HistoryEvent}, }, }; -use futures::{channel::mpsc::Sender, channel::oneshot, Future}; +use futures::{channel::oneshot, Future}; use rustfsm::StateMachine; use std::{ borrow::BorrowMut, @@ -90,18 +91,19 @@ impl WorkflowMachines { } } - /// Create a new timer for this workflow with the provided attributes + /// Create a new timer for this workflow with the provided attributes and sender. The sender + /// is sent `true` when the timer completes. /// /// Returns the command and a future that will resolve when the timer completes pub(super) fn new_timer( &mut self, attribs: StartTimerCommandAttributes, - ) -> (CancellableCommand, impl Future) { + completion_channel: oneshot::Sender, + ) -> CancellableCommand { let timer_id = attribs.timer_id.clone(); let timer = new_timer(attribs); - let (tx, rx) = oneshot::channel(); - self.timer_futures.insert(timer_id, tx); - (timer, rx) + self.timer_futures.insert(timer_id, completion_channel); + timer } /// Returns the id of the last seen WorkflowTaskStarted event @@ -342,8 +344,16 @@ impl WorkflowMachines { fn handle_driven_results(&mut self, results: Vec) { for cmd in results { + // I don't love how boilerplatey this is match cmd { - WFCommand::Add(cc) => self.current_wf_task_commands.push_back(cc), + WFCommand::AddTimer(attrs, completion_channel) => { + let timer = self.new_timer(attrs, completion_channel); + self.current_wf_task_commands.push_back(timer); + } + WFCommand::CompleteWorkflow(attrs) => { + self.current_wf_task_commands + .push_back(complete_workflow(attrs)); + } } } } diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 04def12f0..9ccd5d925 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -73,7 +73,6 @@ impl WFMachinesAdapter for WorkflowTaskMachine { // want to iterate. return Ok(()); } - dbg!(&event); wf_machines.task_started(task_started_event_id, time)?; } } From d42c5494d622e230bd5eee3bf1c1f73265a45ddf Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 18:33:15 -0800 Subject: [PATCH 30/59] Done! YAY! --- Cargo.toml | 5 +- src/machines/mod.rs | 4 +- src/machines/test_help/workflow_driver.rs | 76 ++++++++++------------- src/machines/timer_state_machine.rs | 11 ++-- src/machines/workflow_machines.rs | 18 +++--- 5 files changed, 52 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 02884cec5..f3bcacd8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [lib] +# TODO: Feature flag opentelem stuff [dependencies] anyhow = "1.0" async-trait = "0.1" @@ -13,15 +14,15 @@ derive_more = "0.99" env_logger = "0.8" futures = "0.3" log = "0.4" +opentelemetry-jaeger = "0.10" prost = "0.7" prost-types = "0.7" thiserror = "1.0" +tokio = { version = "1.1", features = ["rt", "rt-multi-thread"] } tonic = "0.4" tracing = { version = "0.1", features = ["log"] } -# TODO: Feature flag this stuff tracing-opentelemetry = "0.10" tracing-subscriber = "0.2" -opentelemetry-jaeger = "0.10" [dependencies.rustfsm] path = "fsm" diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 5033735fe..c110b1de1 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -48,7 +48,6 @@ use crate::{ }, }, }; -use futures::channel::oneshot; use prost::alloc::fmt::Formatter; use rustfsm::{MachineError, StateMachine}; use std::{ @@ -56,6 +55,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, rc::Rc, + sync::{atomic::AtomicBool, Arc}, }; use tracing::Level; @@ -99,7 +99,7 @@ pub(crate) struct AddCommand { /// EX: Create a new timer, complete the workflow, etc. #[derive(Debug, derive_more::From)] pub enum WFCommand { - AddTimer(StartTimerCommandAttributes, oneshot::Sender), + AddTimer(StartTimerCommandAttributes, Arc), CompleteWorkflow(CompleteWorkflowExecutionCommandAttributes), } diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 5f9d96a85..509d84246 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -9,19 +9,18 @@ use crate::{ }, }, }; -use futures::{channel::oneshot, executor::block_on, lock::Mutex, Future, FutureExt}; -use std::sync::Arc; +use futures::Future; use std::{ collections::{hash_map, HashMap}, sync::{ + atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, Sender}, - RwLock, + Arc, RwLock, }, }; use tracing::Level; -// TODO: Mutex is not necessary -type TimerMap = HashMap>>>; +type TimerMap = HashMap>; /// This is a test only implementation of a [DrivenWorkflow] which has finer-grained control /// over when commands are returned than a normal workflow would. @@ -31,28 +30,30 @@ type TimerMap = HashMap pub(in crate::machines) struct TestWorkflowDriver { wf_function: F, - /// Holds completion channels for timers so we don't recreate them by accident. Key is timer id. + /// Holds status for timers so we don't recreate them by accident. Key is timer id, value is + /// true if it is complete. /// - /// I don't love that this is pretty duplicative of workflow machines - timers: RwLock, + /// I don't love that this is pretty duplicative of workflow machines, nor the nasty sync + /// involved. Would be good to eliminate. + timers: Arc>, } impl TestWorkflowDriver where - F: Fn(CommandSender<'_>) -> Fut, + F: Fn(CommandSender) -> Fut, Fut: Future, { pub(in crate::machines) fn new(workflow_fn: F) -> Self { Self { wf_function: workflow_fn, - timers: RwLock::new(HashMap::default()), + timers: Arc::new(RwLock::new(HashMap::default())), } } } impl DrivenWorkflow for TestWorkflowDriver where - F: Fn(CommandSender<'_>) -> Fut, + F: Fn(CommandSender) -> Fut, Fut: Future, { #[instrument(skip(self))] @@ -65,13 +66,14 @@ where #[instrument(skip(self))] fn iterate_wf(&self) -> Result, anyhow::Error> { - dbg!("iterate"); - let (sender, receiver) = CommandSender::new(&self.timers); + let (sender, receiver) = CommandSender::new(self.timers.clone()); // Call the closure that produces the workflow future let wf_future = (self.wf_function)(sender); - // TODO: This block_on might cause issues later - block_on(wf_future); + // Create a tokio runtime to block on the future + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(wf_future); + let cmds = receiver.into_iter(); event!(Level::DEBUG, msg = "Test wf driver emitting", ?cmds); @@ -87,7 +89,7 @@ where } // Return only the last command. TODO: Later this will need to account for the wf - Ok(if let Some(c) = dbg!(last_cmd) { + Ok(if let Some(c) = last_cmd { vec![c] } else { vec![] @@ -118,49 +120,35 @@ pub enum TestWFCommand { Waiting, } -pub struct CommandSender<'a> { +pub struct CommandSender { chan: Sender, - timer_map: &'a RwLock, + timer_map: Arc>, } -impl<'a> CommandSender<'a> { - fn new(timer_map: &'a RwLock) -> (Self, Receiver) { +impl<'a> CommandSender { + fn new(timer_map: Arc>) -> (Self, Receiver) { let (chan, rx) = mpsc::channel(); (Self { chan, timer_map }, rx) } /// Request to create a timer, returning future which resolves when the timer completes pub fn timer(&mut self, a: StartTimerCommandAttributes) -> impl Future + '_ { - dbg!("Timer call", &a.timer_id); - let mut rx = match self.timer_map.write().unwrap().entry(a.timer_id.clone()) { - hash_map::Entry::Occupied(existing) => { - dbg!("occupied"); - existing.get().clone() - } + let finished = match self.timer_map.write().unwrap().entry(a.timer_id.clone()) { + hash_map::Entry::Occupied(existing) => existing.get().load(Ordering::SeqCst), hash_map::Entry::Vacant(v) => { - dbg!("vacant"); - let (tx, rx) = oneshot::channel(); - let c = WFCommand::AddTimer(a, tx); + let atomic = Arc::new(AtomicBool::new(false)); + + let c = WFCommand::AddTimer(a, atomic.clone()); // In theory we should send this in both branches self.chan.send(c.into()).unwrap(); - v.insert(Arc::new(Mutex::new(rx))).clone() + v.insert(atomic); + false } }; - let chan = &mut self.chan; - async move { - // let mut lock = rx.lock().await.fuse(); - futures::select! { - // If the timer is done, nothing to do. - _ = &(*rx.lock().await) => { - dbg!("He done"); - } - // Timer is not done. Send waiting command - default => { - dbg!("He waitin"); - chan.send(TestWFCommand::Waiting).unwrap(); - } - } + if !finished { + self.chan.send(TestWFCommand::Waiting).unwrap(); } + futures::future::ready(()) } pub fn send(&mut self, c: WFCommand) { diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 94fc78e98..c2dd5834c 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,9 +1,10 @@ #![allow(clippy::large_enum_variant)] -use crate::machines::workflow_machines::WorkflowMachines; -use crate::machines::WFMachinesAdapter; use crate::{ - machines::{workflow_machines::WFMachinesError, AddCommand, CancellableCommand, WFCommand}, + machines::{ + workflow_machines::WFMachinesError, workflow_machines::WorkflowMachines, AddCommand, + CancellableCommand, WFCommand, WFMachinesAdapter, + }, protos::{ coresdk::HistoryEventId, temporal::api::{ @@ -17,7 +18,7 @@ use crate::{ }, }; use rustfsm::{fsm, StateMachine, TransitionResult}; -use std::{cell::RefCell, convert::TryFrom, rc::Rc}; +use std::{cell::RefCell, convert::TryFrom, rc::Rc, sync::atomic::Ordering}; use tracing::Level; fsm! { @@ -230,7 +231,7 @@ impl WFMachinesAdapter for TimerMachine { wf_machines .timer_futures .remove(&self.shared_state.timer_attributes.timer_id) - .map(|c| c.send(true)); + .map(|a| a.store(true, Ordering::SeqCst)); } } Ok(()) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 547ff134d..ae4d413e1 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,7 +1,6 @@ -use crate::machines::complete_workflow_state_machine::complete_workflow; use crate::{ machines::{ - test_help::TestWorkflowDriver, timer_state_machine::new_timer, + complete_workflow_state_machine::complete_workflow, timer_state_machine::new_timer, workflow_task_state_machine::WorkflowTaskMachine, CancellableCommand, DrivenWorkflow, MachineCommand, TemporalStateMachine, WFCommand, }, @@ -11,13 +10,14 @@ use crate::{ history::v1::{history_event, HistoryEvent}, }, }; -use futures::{channel::oneshot, Future}; +use futures::Future; use rustfsm::StateMachine; use std::{ borrow::BorrowMut, cell::RefCell, collections::{HashMap, VecDeque}, rc::Rc, + sync::{atomic::AtomicBool, Arc}, time::SystemTime, }; use tracing::Level; @@ -53,8 +53,8 @@ pub(crate) struct WorkflowMachines { /// The workflow that is being driven by this instance of the machines drive_me: Box, - /// Holds channels for completing timers. Key is the ID of the timer. - pub(super) timer_futures: HashMap>, + /// Holds atomics for completing timers. Key is the ID of the timer. + pub(super) timer_futures: HashMap>, } #[derive(thiserror::Error, Debug)] @@ -98,11 +98,11 @@ impl WorkflowMachines { pub(super) fn new_timer( &mut self, attribs: StartTimerCommandAttributes, - completion_channel: oneshot::Sender, + completion_flag: Arc, ) -> CancellableCommand { let timer_id = attribs.timer_id.clone(); let timer = new_timer(attribs); - self.timer_futures.insert(timer_id, completion_channel); + self.timer_futures.insert(timer_id, completion_flag); timer } @@ -346,8 +346,8 @@ impl WorkflowMachines { for cmd in results { // I don't love how boilerplatey this is match cmd { - WFCommand::AddTimer(attrs, completion_channel) => { - let timer = self.new_timer(attrs, completion_channel); + WFCommand::AddTimer(attrs, completion_flag) => { + let timer = self.new_timer(attrs, completion_flag); self.current_wf_task_commands.push_back(timer); } WFCommand::CompleteWorkflow(attrs) => { From a45a1cc558c420eebe259a9cc14c21916b6c5b40 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 19:46:00 -0800 Subject: [PATCH 31/59] Clippy fixes --- src/machines/test_help/workflow_driver.rs | 5 +++++ src/machines/timer_state_machine.rs | 6 ++++-- src/machines/workflow_task_state_machine.rs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 509d84246..6dfdcc08a 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -43,6 +43,11 @@ where F: Fn(CommandSender) -> Fut, Fut: Future, { + /// Create a new test workflow driver from a workflow "function" which is really a closure + /// that returns an async block. + /// + /// In an ideal world, the workflow fn would actually be a generator which can yield commands, + /// and we may well end up doing something like that later. pub(in crate::machines) fn new(workflow_fn: F) -> Self { Self { wf_function: workflow_fn, diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index c2dd5834c..0295aa26b 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -228,10 +228,12 @@ impl WFMachinesAdapter for TimerMachine { } // Fire the completion TimerMachineCommand::Complete(_event) => { - wf_machines + if let Some(a) = wf_machines .timer_futures .remove(&self.shared_state.timer_attributes.timer_id) - .map(|a| a.store(true, Ordering::SeqCst)); + { + a.store(true, Ordering::SeqCst) + }; } } Ok(()) diff --git a/src/machines/workflow_task_state_machine.rs b/src/machines/workflow_task_state_machine.rs index 9ccd5d925..333a74796 100644 --- a/src/machines/workflow_task_state_machine.rs +++ b/src/machines/workflow_task_state_machine.rs @@ -67,7 +67,7 @@ impl WFMachinesAdapter for WorkflowTaskMachine { .ok_or_else(|| WFMachinesError::UnexpectedEvent(event.clone()))?; let cur_event_past_or_at_start = event.event_id >= task_started_event_id; if event_type == EventType::WorkflowTaskStarted - && !(cur_event_past_or_at_start && !has_next_event) + && (!cur_event_past_or_at_start || has_next_event) { // Last event in history is a task started event, so we don't // want to iterate. From 1a44f34c2904d49eec1e4d38392128ef7d5ae6fd Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 23:21:39 -0800 Subject: [PATCH 32/59] Remove/fix some todos --- src/machines/mod.rs | 4 ---- src/machines/test_help/workflow_driver.rs | 2 +- src/machines/workflow_machines.rs | 13 ++++--------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/machines/mod.rs b/src/machines/mod.rs index c110b1de1..bd2a2f6fd 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -119,10 +119,6 @@ trait TemporalStateMachine: CheckStateMachineInFinal { has_next_event: bool, wf_machines: &mut WorkflowMachines, ) -> Result<(), WFMachinesError>; - - // TODO: This is a weird one that only applies to version state machine. Introduce only if - // needed. Ideally handle differently. - // fn handle_workflow_task_started(); } impl TemporalStateMachine for SM diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 6dfdcc08a..366f84e83 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -93,7 +93,7 @@ where } } - // Return only the last command. TODO: Later this will need to account for the wf + // Return only the last command, since that's what would've been yielded in a real wf Ok(if let Some(c) = last_cmd { vec![c] } else { diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index ae4d413e1..9f0b36ef7 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -41,7 +41,6 @@ pub(crate) struct WorkflowMachines { /// A mapping for accessing all the machines, where the key is the id of the initiating event /// for that machine. - // TODO: Maybe value here should just be `CancellableCommand` machines_by_id: HashMap>>, /// Queued commands which have been produced by machines and await processing @@ -214,12 +213,11 @@ impl WorkflowMachines { // return; // } - let mut maybe_command; let consumed_cmd = loop { // handleVersionMarker can skip a marker event if the getVersion call was removed. - // In this case we don't want to consume a command. - // That's why front is used instead of pop_front - maybe_command = self.commands.front(); + // In this case we don't want to consume a command. -- we will need to replace it back + // to the front when implementing, or something better + let maybe_command = self.commands.pop_front(); let command = if let Some(c) = maybe_command { c } else { @@ -242,11 +240,8 @@ impl WorkflowMachines { break_later = true; } - // Consume command - let cmd = self.commands.pop_front(); if break_later { - // Unwrap OK since we would have already exited if not present. TODO: Cleanup - break cmd.unwrap(); + command } }; From 3d0d4ebd8e5c14099223b46ad32415e67e8e6d18 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 25 Jan 2021 23:22:42 -0800 Subject: [PATCH 33/59] Missed a word --- src/machines/workflow_machines.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index 9f0b36ef7..909524980 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -241,7 +241,7 @@ impl WorkflowMachines { } if break_later { - command + break command; } }; From df99d7e4f2c3702bc957127a1cee3b60a1bb6a34 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Tue, 26 Jan 2021 11:26:25 +0200 Subject: [PATCH 34/59] Remove Sync+Send impl for CoreSDKInitOptions --- src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c42ef852b..8f39aa83d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,9 +23,6 @@ pub struct CoreSDKInitOptions { max_concurrent_activity_executions: u32, } -unsafe impl Send for CoreSDKInitOptions {} -unsafe impl Sync for CoreSDKInitOptions {} - pub fn init_sdk(opts: CoreSDKInitOptions) -> Result> { Err(SDKServiceError::Unknown {}) } From 8ef8bba8ecf98962c06113a0fa2fb55e27fa6b76 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Tue, 26 Jan 2021 16:38:19 +0200 Subject: [PATCH 35/59] Add workflow_id to SDKWFTask --- protos/local/core_interface.proto | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 2a4c4b2ff..575e35aeb 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -71,10 +71,11 @@ enum WFTaskType { message SDKWFTask { WFTaskType type = 1; google.protobuf.Timestamp timestamp = 2 [(gogoproto.stdtime) = true]; + string workflow_id = 3; oneof attributes { - StartWorkflowTaskAttributes start_workflow_task_attributes = 3; - CompleteTimerTaskAttributes complete_timer_task_attributes = 4; - CancelTimerTaskAttributes cancel_timer_task_attributes = 5; + StartWorkflowTaskAttributes start_workflow_task_attributes = 4; + CompleteTimerTaskAttributes complete_timer_task_attributes = 5; + CancelTimerTaskAttributes cancel_timer_task_attributes = 6; } } From aaf3c655be4fe4449bc04cd54741b20c65d25161 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 26 Jan 2021 14:10:24 -0800 Subject: [PATCH 36/59] Simple rename / comment upgrade --- src/machines/test_help/workflow_driver.rs | 6 +++++- src/machines/timer_state_machine.rs | 13 +++++++++---- src/machines/workflow_machines.rs | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 509d84246..5caafc9cb 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -35,6 +35,10 @@ pub(in crate::machines) struct TestWorkflowDriver { /// /// I don't love that this is pretty duplicative of workflow machines, nor the nasty sync /// involved. Would be good to eliminate. + /// + /// It can and should be eliminated by not recreating CommandSender on every iteration, which + /// means keeping the workflow suspended in a future somewhere. I tried this and it was hard, + /// but ultimately it's how real workflows will need to work. timers: Arc>, } @@ -88,7 +92,7 @@ where } } - // Return only the last command. TODO: Later this will need to account for the wf + // Return only the last command Ok(if let Some(c) = last_cmd { vec![c] } else { diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index c2dd5834c..19bbf195b 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -229,7 +229,7 @@ impl WFMachinesAdapter for TimerMachine { // Fire the completion TimerMachineCommand::Complete(_event) => { wf_machines - .timer_futures + .timer_notifiers .remove(&self.shared_state.timer_attributes.timer_id) .map(|a| a.store(true, Ordering::SeqCst)); } @@ -275,9 +275,14 @@ mod test { 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED 8: EVENT_TYPE_WORKFLOW_TASK_STARTED - // TODO: Update this with deets - Should iterate once, produce started command, iterate again, producing no commands - (timer complete), third iteration completes workflow. + We have two versions of this test, one which processes the history in two calls, + and one which replays all of it in one go. The former will run the event loop three + times total, and the latter two. + + There are two workflow tasks, so it seems we should only loop two times, but the reason + for the extra iteration in the incremental version is that we need to "wait" for the + timer to fire. In the all-in-one-go test, the timer is created and resolved in the same + task, hence no extra loop. */ let twd = TestWorkflowDriver::new(|mut command_sink: CommandSender| async move { let timer = StartTimerCommandAttributes { diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index ae4d413e1..cb227fd2b 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -54,7 +54,7 @@ pub(crate) struct WorkflowMachines { drive_me: Box, /// Holds atomics for completing timers. Key is the ID of the timer. - pub(super) timer_futures: HashMap>, + pub(super) timer_notifiers: HashMap>, } #[derive(thiserror::Error, Debug)] @@ -87,7 +87,7 @@ impl WorkflowMachines { machines_by_id: Default::default(), commands: Default::default(), current_wf_task_commands: Default::default(), - timer_futures: Default::default(), + timer_notifiers: Default::default(), } } @@ -102,7 +102,7 @@ impl WorkflowMachines { ) -> CancellableCommand { let timer_id = attribs.timer_id.clone(); let timer = new_timer(attribs); - self.timer_futures.insert(timer_id, completion_flag); + self.timer_notifiers.insert(timer_id, completion_flag); timer } From 9d5fec091c4e2b06056fd3ead8b2e4ac20f3b6b0 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 26 Jan 2021 14:33:58 -0800 Subject: [PATCH 37/59] Fix up clippy lints and remove async from sdk trait --- src/lib.rs | 16 +++++++--------- src/protos/mod.rs | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 641d2072a..cb6100f85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,21 +9,19 @@ use protos::coresdk::{CompleteSdkTaskReq, PollSdkTaskResp, RegistrationReq}; pub type Result = std::result::Result; -// TODO: Should probably enforce Send + Sync -#[async_trait::async_trait] pub trait CoreSDKService { - async fn poll_sdk_task(&self) -> Result; - async fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; - async fn register_implementations(&self, req: RegistrationReq) -> Result<()>; + fn poll_sdk_task(&self) -> Result; + fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; + fn register_implementations(&self, req: RegistrationReq) -> Result<()>; } pub struct CoreSDKInitOptions { - queue_name: String, - max_concurrent_workflow_executions: u32, - max_concurrent_activity_executions: u32, + _queue_name: String, + _max_concurrent_workflow_executions: u32, + _max_concurrent_activity_executions: u32, } -pub fn init_sdk(opts: CoreSDKInitOptions) -> Result> { +pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { Err(SDKServiceError::Unknown {}) } diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 0d7816532..141b3d5cf 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -1,3 +1,4 @@ +#[allow(clippy::large_enum_variant)] pub mod coresdk { include!("coresdk.rs"); From 38e8e8931047b8c89280ee9e2f282c8c1600bc0b Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 26 Jan 2021 14:41:58 -0800 Subject: [PATCH 38/59] Add autoconversion to make life easier for lang side proto construction --- build.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.rs b/build.rs index 282ce147a..a5c23ea12 100644 --- a/build.rs +++ b/build.rs @@ -14,6 +14,10 @@ fn main() -> Result<(), Box> { "temporal.api.command.v1.Command.attributes", "#[derive(::derive_more::From)]", ) + .type_attribute( + "coresdk.SDKWFTask.attributes", + "#[derive(::derive_more::From)]", + ) .compile( &[ "protos/local/core_interface.proto", From fc2523387ce2f5a67a870893796971a291c0135e Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 26 Jan 2021 18:34:19 -0800 Subject: [PATCH 39/59] Some prototyping of how we might glue FSMs to CoreService * Brain fried. May not make sense. :) * So fried I managed to blow away these changes and had to recover them --- Cargo.toml | 1 + build.rs | 4 + fsm/state_machine_procmacro/tests/progress.rs | 41 ------- protos/local/core_interface.proto | 1 + src/lib.rs | 114 +++++++++++++++++- src/machines/mod.rs | 10 +- src/machines/test_help/workflow_driver.rs | 8 +- 7 files changed, 126 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2818ac2b7..b15d99ec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" [dependencies] anyhow = "1.0" async-trait = "0.1" +dashmap = "4.0" derive_more = "0.99" env_logger = "0.8" futures = "0.3" diff --git a/build.rs b/build.rs index a5c23ea12..f6a653646 100644 --- a/build.rs +++ b/build.rs @@ -18,6 +18,10 @@ fn main() -> Result<(), Box> { "coresdk.SDKWFTask.attributes", "#[derive(::derive_more::From)]", ) + .type_attribute( + "coresdk.PollSDKTaskResp.task", + "#[derive(::derive_more::From)]", + ) .compile( &[ "protos/local/core_interface.proto", diff --git a/fsm/state_machine_procmacro/tests/progress.rs b/fsm/state_machine_procmacro/tests/progress.rs index 5a3e4a4fc..0621c0d7d 100644 --- a/fsm/state_machine_procmacro/tests/progress.rs +++ b/fsm/state_machine_procmacro/tests/progress.rs @@ -1,49 +1,8 @@ extern crate state_machine_trait as rustfsm; -use state_machine_trait::TransitionResult; -use std::convert::Infallible; - #[test] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/trybuild/*_pass.rs"); t.compile_fail("tests/trybuild/*_fail.rs"); } - -// Kept here to inspect manual expansion -state_machine_procmacro::fsm! { - name SimpleMachine; command SimpleMachineCommand; error Infallible; - - One --(A(String), foo)--> Two; - One --(B)--> Two; - Two --(B)--> One; - Two --(C, baz)--> One -} - -#[derive(Default, Clone)] -pub struct One {} -impl One { - fn foo(self, _: String) -> SimpleMachineTransition { - TransitionResult::default::() - } -} -impl From for One { - fn from(_: Two) -> Self { - One {} - } -} - -#[derive(Default, Clone)] -pub struct Two {} -impl Two { - fn baz(self) -> SimpleMachineTransition { - TransitionResult::default::() - } -} -impl From for Two { - fn from(_: One) -> Self { - Two {} - } -} - -pub enum SimpleMachineCommand {} diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 575e35aeb..4b0fdb90a 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -62,6 +62,7 @@ message CancelTimerTaskAttributes { int32 timer_id = 1; } +// TODO: Remove this - type info exists in attributes oneof enum WFTaskType { START_WORKFLOW = 0; COMPLETE_TIMER = 1; diff --git a/src/lib.rs b/src/lib.rs index cb6100f85..1cebd0a6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,26 @@ mod machines; mod pollers; pub mod protos; -use protos::coresdk::{CompleteSdkTaskReq, PollSdkTaskResp, RegistrationReq}; +use crate::{ + machines::{DrivenWorkflow, WFCommand}, + protos::{ + coresdk::{ + complete_sdk_task_req::Completion, sdkwf_task_completion::Status, CompleteSdkTaskReq, + PollSdkTaskResp, RegistrationReq, SdkwfTask, SdkwfTaskCompletion, + }, + temporal::api::history::v1::{ + WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, + WorkflowExecutionStartedEventAttributes, + }, + }, +}; +use anyhow::Error; +use dashmap::DashMap; +use prost::alloc::collections::VecDeque; pub type Result = std::result::Result; -pub trait CoreSDKService { +pub trait CoreSDKService: Send + Sync { fn poll_sdk_task(&self) -> Result; fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; fn register_implementations(&self, req: RegistrationReq) -> Result<()>; @@ -25,11 +40,104 @@ pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { Err(SDKServiceError::Unknown {}) } +pub struct CoreSDK { + /// Key is workflow id + workflows: DashMap, +} + +impl CoreSDKService for CoreSDK { + fn poll_sdk_task(&self) -> Result { + for mut wfb in self.workflows.iter_mut() { + if let Some(task) = wfb.value_mut().get_next_task() { + return Ok(PollSdkTaskResp { + task_token: b"TODO: Something real!".to_vec(), + task: Some(task.into()), + }); + } + } + // Block thread instead? Or return optional task? + Err(SDKServiceError::NoWork) + } + + fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<(), SDKServiceError> { + match &req.completion { + Some(Completion::Workflow(SdkwfTaskCompletion { + status: Some(wfstatus), + })) => { + match wfstatus { + Status::Successful(success) => { + let cmds = success.commands.iter().map(|_c| { + // TODO: Make it go + vec![] + }); + // TODO: Need to use task token or wfid etc + self.workflows + .get_mut("ID") + .unwrap() + .incoming_commands + .extend(cmds); + } + Status::Failed(_) => {} + } + Ok(()) + } + Some(Completion::Activity(_)) => { + unimplemented!() + } + _ => Err(SDKServiceError::MalformedCompletion(req)), + } + } + + fn register_implementations(&self, _req: RegistrationReq) -> Result<(), SDKServiceError> { + unimplemented!() + } +} + +/// The [DrivenWorkflow] trait expects to be called to make progress, but the [CoreSDKService] +/// expects to be polled by the lang sdk. This struct acts as the bridge between the two, buffering +/// output from calls to [DrivenWorkflow] and offering them to [CoreSDKService] +pub struct WorkflowBridge { + started_attrs: WorkflowExecutionStartedEventAttributes, + outgoing_tasks: VecDeque, + incoming_commands: VecDeque>, +} + +impl WorkflowBridge { + pub fn get_next_task(&mut self) -> Option { + self.outgoing_tasks.pop_front() + } +} + +impl DrivenWorkflow for WorkflowBridge { + fn start( + &mut self, + attribs: WorkflowExecutionStartedEventAttributes, + ) -> Result, Error> { + self.started_attrs = attribs; + Ok(vec![]) + } + + fn iterate_wf(&mut self) -> Result, Error> { + Ok(self.incoming_commands.pop_front().unwrap_or_default()) + } + + fn signal(&mut self, _attribs: WorkflowExecutionSignaledEventAttributes) -> Result<(), Error> { + unimplemented!() + } + + fn cancel(&mut self, _attribs: WorkflowExecutionCanceledEventAttributes) -> Result<(), Error> { + unimplemented!() + } +} + #[derive(thiserror::Error, Debug)] pub enum SDKServiceError { #[error("Unknown service error")] Unknown, - // more errors TBD + #[error("No tasks to perform for now")] + NoWork, + #[error("Lang SDK sent us a malformed completion: {0:?}")] + MalformedCompletion(CompleteSdkTaskReq), } #[cfg(test)] diff --git a/src/machines/mod.rs b/src/machines/mod.rs index bd2a2f6fd..f729c6b00 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -64,26 +64,26 @@ type MachineCommand = Command; /// Implementors of this trait represent something that can (eventually) call into a workflow to /// drive it, start it, signal it, cancel it, etc. -trait DrivenWorkflow { +pub(crate) trait DrivenWorkflow { /// Start the workflow fn start( - &self, + &mut self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, anyhow::Error>; /// Iterate the workflow. The workflow driver should execute workflow code until there is /// nothing left to do. EX: Awaiting an activity/timer, workflow completion. - fn iterate_wf(&self) -> Result, anyhow::Error>; + fn iterate_wf(&mut self) -> Result, anyhow::Error>; /// Signal the workflow fn signal( - &self, + &mut self, attribs: WorkflowExecutionSignaledEventAttributes, ) -> Result<(), anyhow::Error>; /// Cancel the workflow fn cancel( - &self, + &mut self, attribs: WorkflowExecutionCanceledEventAttributes, ) -> Result<(), anyhow::Error>; } diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 1db3dd5ef..427aa5c6e 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -67,14 +67,14 @@ where { #[instrument(skip(self))] fn start( - &self, + &mut self, _attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, anyhow::Error> { Ok(vec![]) } #[instrument(skip(self))] - fn iterate_wf(&self) -> Result, anyhow::Error> { + fn iterate_wf(&mut self) -> Result, anyhow::Error> { let (sender, receiver) = CommandSender::new(self.timers.clone()); // Call the closure that produces the workflow future let wf_future = (self.wf_function)(sender); @@ -106,14 +106,14 @@ where } fn signal( - &self, + &mut self, _attribs: WorkflowExecutionSignaledEventAttributes, ) -> Result<(), anyhow::Error> { Ok(()) } fn cancel( - &self, + &mut self, _attribs: WorkflowExecutionCanceledEventAttributes, ) -> Result<(), anyhow::Error> { Ok(()) From 7d26c3cd24b84ae6f078984dc1acf2132de71277 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Wed, 27 Jan 2021 17:31:19 +0200 Subject: [PATCH 40/59] Define an SDK Command type --- protos/local/core_interface.proto | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 4b0fdb90a..282b11154 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -108,8 +108,19 @@ message SDKActivityTaskCompletion { } } +message SDKCommand { + // Reserved for SDK specific commands +} + +message Command { + oneof variant { + temporal.api.command.v1.Command api = 1; + SDKCommand sdk = 2; + } +} + message SDKWFTaskSuccess { - repeated temporal.api.command.v1.Command commands = 1; + repeated Command commands = 1; // Other bits from RespondWorkflowTaskCompletedRequest as needed } From ac756403f7354d458cb6d4124ec0844779a27ee0 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 27 Jan 2021 13:59:07 -0800 Subject: [PATCH 41/59] Made some reasonable progress. Trying to figure out how to push commands into the bridge from lang (or test) --- Cargo.toml | 1 + protos/local/core_interface.proto | 4 +- src/lib.rs | 150 ++++++++++++++++++---- src/machines/mod.rs | 27 ++-- src/machines/test_help/mod.rs | 2 +- src/machines/test_help/workflow_driver.rs | 2 +- src/machines/workflow_machines.rs | 2 +- src/protos/mod.rs | 9 ++ 8 files changed, 153 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b15d99ec2..d7f93e8ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = "0.2" path = "fsm" [dev-dependencies] +mockall = "0.9" rstest = "0.6" [build-dependencies] diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 4b0fdb90a..ba95eb6bb 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -55,11 +55,11 @@ message StartWorkflowTaskAttributes { } message CompleteTimerTaskAttributes { - int32 timer_id = 1; + string timer_id = 1; } message CancelTimerTaskAttributes { - int32 timer_id = 1; + string timer_id = 1; } // TODO: Remove this - type info exists in attributes oneof diff --git a/src/lib.rs b/src/lib.rs index 1cebd0a6f..0cbcae52f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ mod machines; mod pollers; pub mod protos; +use crate::machines::WorkflowMachines; +use crate::protos::coresdk::poll_sdk_task_resp; +use crate::protos::coresdk::poll_sdk_task_resp::Task; +use crate::protos::temporal::api::history::v1::HistoryEvent; use crate::{ machines::{DrivenWorkflow, WFCommand}, protos::{ @@ -20,11 +24,16 @@ use crate::{ }; use anyhow::Error; use dashmap::DashMap; -use prost::alloc::collections::VecDeque; +use std::collections::VecDeque; pub type Result = std::result::Result; -pub trait CoreSDKService: Send + Sync { +// TODO: Do we actually need this to be send+sync? Probably, but there's also no reason to have +// any given WorfklowMachines instance accessed on more than one thread. Ideally this trait can +// be accessed from many threads, but each workflow is pinned to one thread (possibly with many +// sharing the same thread). IE: WorkflowMachines should be Send but not Sync, and this should +// be both, ideally. +pub trait CoreSDKService { fn poll_sdk_task(&self) -> Result; fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; fn register_implementations(&self, req: RegistrationReq) -> Result<()>; @@ -40,23 +49,39 @@ pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { Err(SDKServiceError::Unknown {}) } -pub struct CoreSDK { +pub struct CoreSDK { + work_provider: WP, /// Key is workflow id - workflows: DashMap, + workflows: DashMap, } -impl CoreSDKService for CoreSDK { +// impl CoreSDK where WP: WorkProvider { +// fn apply_hist_event(event: &HistoryEvent, has_next_event) +// } + +/// Implementors can provide new work to the SDK. The connection to the server is the real +/// implementor, which adapts workflow tasks from the server to sdk workflow tasks. +#[cfg_attr(test, mockall::automock)] +pub trait WorkProvider { + // TODO: Should actually likely return server tasks directly, so coresdk can do things like + // apply history events that don't need workflow to actually do anything, like schedule. + /// Fetch new work. Should block indefinitely if there is no work. + fn get_work(&self, task_queue: &str) -> Result; +} + +impl CoreSDKService for CoreSDK +where + WP: WorkProvider, +{ fn poll_sdk_task(&self) -> Result { - for mut wfb in self.workflows.iter_mut() { - if let Some(task) = wfb.value_mut().get_next_task() { - return Ok(PollSdkTaskResp { - task_token: b"TODO: Something real!".to_vec(), - task: Some(task.into()), - }); + // This will block forever in the event there is no work from the server + let work = self.work_provider.get_work("TODO: Real task queue")?; + match work { + Task::WfTask(t) => return Ok(PollSdkTaskResp::from_wf_task(b"token".to_vec(), t)), + Task::ActivityTask(_) => { + unimplemented!() } } - // Block thread instead? Or return optional task? - Err(SDKServiceError::NoWork) } fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<(), SDKServiceError> { @@ -66,16 +91,16 @@ impl CoreSDKService for CoreSDK { })) => { match wfstatus { Status::Successful(success) => { - let cmds = success.commands.iter().map(|_c| { - // TODO: Make it go - vec![] - }); + // let cmds = success.commands.iter().map(|_c| { + // // TODO: Make it go + // vec![] + // }); // TODO: Need to use task token or wfid etc - self.workflows - .get_mut("ID") - .unwrap() - .incoming_commands - .extend(cmds); + // self.workflows + // .get_mut("ID") + // .unwrap() + // .incoming_commands + // .extend(cmds); } Status::Failed(_) => {} } @@ -96,8 +121,10 @@ impl CoreSDKService for CoreSDK { /// The [DrivenWorkflow] trait expects to be called to make progress, but the [CoreSDKService] /// expects to be polled by the lang sdk. This struct acts as the bridge between the two, buffering /// output from calls to [DrivenWorkflow] and offering them to [CoreSDKService] +#[derive(Debug, Default)] pub struct WorkflowBridge { - started_attrs: WorkflowExecutionStartedEventAttributes, + // does wf id belong in here? + started_attrs: Option, outgoing_tasks: VecDeque, incoming_commands: VecDeque>, } @@ -113,7 +140,7 @@ impl DrivenWorkflow for WorkflowBridge { &mut self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, Error> { - self.started_attrs = attribs; + self.started_attrs = Some(attribs); Ok(vec![]) } @@ -142,6 +169,79 @@ pub enum SDKServiceError { #[cfg(test)] mod test { + use super::*; + use crate::{ + machines::{test_help::TestHistoryBuilder, WorkflowMachines}, + protos::{ + coresdk::{CompleteTimerTaskAttributes, StartWorkflowTaskAttributes, WfTaskType}, + temporal::api::{ + enums::v1::EventType, + history::v1::{history_event, TimerFiredEventAttributes}, + }, + }, + }; + #[test] - fn foo() {} + fn workflow_bridge() { + let wfid = "fake_wf_id"; + let mut tasks = VecDeque::from(vec![ + SdkwfTask { + r#type: WfTaskType::StartWorkflow as i32, + timestamp: None, + workflow_id: wfid.to_string(), + attributes: Some( + StartWorkflowTaskAttributes { + namespace: "namespace".to_string(), + name: "wf name?".to_string(), + arguments: None, + } + .into(), + ), + } + .into(), + SdkwfTask { + r#type: WfTaskType::CompleteTimer as i32, + timestamp: None, + workflow_id: wfid.to_string(), + attributes: Some( + CompleteTimerTaskAttributes { + timer_id: "timer".to_string(), + } + .into(), + ), + } + .into(), + ]); + + let mut mock_provider = MockWorkProvider::new(); + mock_provider + .expect_get_work() + .returning(move |_| Ok(tasks.pop_front().unwrap())); + + let mut wfb = WorkflowBridge::default(); + let state_machines = WorkflowMachines::new(Box::new(wfb)); + let core = CoreSDK { + workflows: DashMap::new(), + work_provider: mock_provider, + }; + core.workflows.insert(wfid.to_string(), state_machines); + + let mut t = TestHistoryBuilder::default(); + + t.add_by_type(EventType::WorkflowExecutionStarted); + t.add_workflow_task(); + let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); + t.add( + EventType::TimerFired, + history_event::Attributes::TimerFiredEventAttributes(TimerFiredEventAttributes { + started_event_id: timer_started_event_id, + timer_id: "timer1".to_string(), + ..Default::default() + }), + ); + t.add_workflow_task_scheduled_and_started(); + + let res = core.poll_sdk_task(); + dbg!(res); + } } diff --git a/src/machines/mod.rs b/src/machines/mod.rs index f729c6b00..b7e4fdfb8 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -33,19 +33,18 @@ mod version_state_machine; mod workflow_task_state_machine; #[cfg(test)] -mod test_help; - -use crate::{ - machines::workflow_machines::{WFMachinesError, WorkflowMachines}, - protos::temporal::api::{ - command::v1::{ - Command, CompleteWorkflowExecutionCommandAttributes, StartTimerCommandAttributes, - }, - enums::v1::CommandType, - history::v1::{ - HistoryEvent, WorkflowExecutionCanceledEventAttributes, - WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, - }, +pub(crate) mod test_help; + +pub(crate) use workflow_machines::{WFMachinesError, WorkflowMachines}; + +use crate::protos::temporal::api::{ + command::v1::{ + Command, CompleteWorkflowExecutionCommandAttributes, StartTimerCommandAttributes, + }, + enums::v1::CommandType, + history::v1::{ + HistoryEvent, WorkflowExecutionCanceledEventAttributes, + WorkflowExecutionSignaledEventAttributes, WorkflowExecutionStartedEventAttributes, }, }; use prost::alloc::fmt::Formatter; @@ -64,7 +63,7 @@ type MachineCommand = Command; /// Implementors of this trait represent something that can (eventually) call into a workflow to /// drive it, start it, signal it, cancel it, etc. -pub(crate) trait DrivenWorkflow { +pub(crate) trait DrivenWorkflow: Send + Sync { /// Start the workflow fn start( &mut self, diff --git a/src/machines/test_help/mod.rs b/src/machines/test_help/mod.rs index 427f0876f..b7ab7509b 100644 --- a/src/machines/test_help/mod.rs +++ b/src/machines/test_help/mod.rs @@ -3,5 +3,5 @@ type Result = std::result::Result; mod history_builder; mod workflow_driver; -pub(super) use history_builder::TestHistoryBuilder; +pub(crate) use history_builder::TestHistoryBuilder; pub(super) use workflow_driver::{CommandSender, TestWFCommand, TestWorkflowDriver}; diff --git a/src/machines/test_help/workflow_driver.rs b/src/machines/test_help/workflow_driver.rs index 427aa5c6e..aa89b8562 100644 --- a/src/machines/test_help/workflow_driver.rs +++ b/src/machines/test_help/workflow_driver.rs @@ -62,7 +62,7 @@ where impl DrivenWorkflow for TestWorkflowDriver where - F: Fn(CommandSender) -> Fut, + F: Fn(CommandSender) -> Fut + Send + Sync, Fut: Future, { #[instrument(skip(self))] diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index ed9b50ac4..cb647bfd8 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -73,7 +73,7 @@ pub(crate) enum WFMachinesError { } impl WorkflowMachines { - pub(super) fn new(driven_wf: Box) -> Self { + pub(crate) fn new(driven_wf: Box) -> Self { Self { drive_me: driven_wf, // In an ideal world one could say ..Default::default() here and it'd still work. diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 141b3d5cf..b0bebdb56 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -3,6 +3,15 @@ pub mod coresdk { include!("coresdk.rs"); pub type HistoryEventId = i64; + + impl PollSdkTaskResp { + pub fn from_wf_task(task_token: Vec, t: SdkwfTask) -> Self { + PollSdkTaskResp { + task_token, + task: Some(t.into()), + } + } + } } // No need to lint these From 1eed2cb39328d19712dedd56987669f0ae88cc79 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 27 Jan 2021 16:10:20 -0800 Subject: [PATCH 42/59] Pushing broken stuff so can pair w/ Vitaly --- build.rs | 5 +- protos/local/core_interface.proto | 25 ++----- src/lib.rs | 114 ++++++++++++++++++++---------- src/machines/mod.rs | 34 ++++++++- 4 files changed, 117 insertions(+), 61 deletions(-) diff --git a/build.rs b/build.rs index f6a653646..2126a93a9 100644 --- a/build.rs +++ b/build.rs @@ -14,10 +14,7 @@ fn main() -> Result<(), Box> { "temporal.api.command.v1.Command.attributes", "#[derive(::derive_more::From)]", ) - .type_attribute( - "coresdk.SDKWFTask.attributes", - "#[derive(::derive_more::From)]", - ) + .type_attribute("coresdk.SDKWFTask.task", "#[derive(::derive_more::From)]") .type_attribute( "coresdk.PollSDKTaskResp.task", "#[derive(::derive_more::From)]", diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index aa67cc445..3b68fa1b2 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -54,29 +54,16 @@ message StartWorkflowTaskAttributes { temporal.api.common.v1.Payloads arguments = 3; } -message CompleteTimerTaskAttributes { +message UnblockTimerTaskAttibutes { string timer_id = 1; } -message CancelTimerTaskAttributes { - string timer_id = 1; -} - -// TODO: Remove this - type info exists in attributes oneof -enum WFTaskType { - START_WORKFLOW = 0; - COMPLETE_TIMER = 1; - CANCEL_TIMER = 2; -} - message SDKWFTask { - WFTaskType type = 1; - google.protobuf.Timestamp timestamp = 2 [(gogoproto.stdtime) = true]; - string workflow_id = 3; - oneof attributes { - StartWorkflowTaskAttributes start_workflow_task_attributes = 4; - CompleteTimerTaskAttributes complete_timer_task_attributes = 5; - CancelTimerTaskAttributes cancel_timer_task_attributes = 6; + google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; + string workflow_id = 2; + oneof task { + StartWorkflowTaskAttributes start_workflow = 3; + UnblockTimerTaskAttibutes unblock_timer = 4; } } diff --git a/src/lib.rs b/src/lib.rs index 0cbcae52f..56c8f8c7b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,10 +5,9 @@ mod machines; mod pollers; pub mod protos; -use crate::machines::WorkflowMachines; +use crate::machines::{InconvertibleCommandError, WorkflowMachines}; use crate::protos::coresdk::poll_sdk_task_resp; use crate::protos::coresdk::poll_sdk_task_resp::Task; -use crate::protos::temporal::api::history::v1::HistoryEvent; use crate::{ machines::{DrivenWorkflow, WFCommand}, protos::{ @@ -24,7 +23,9 @@ use crate::{ }; use anyhow::Error; use dashmap::DashMap; -use std::collections::VecDeque; +use std::convert::TryInto; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, SendError, Sender}; pub type Result = std::result::Result; @@ -51,13 +52,21 @@ pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { pub struct CoreSDK { work_provider: WP, - /// Key is workflow id - workflows: DashMap, + /// Key is workflow id -- but perhaps ought to be task token + workflow_machines: DashMap>)>, } -// impl CoreSDK where WP: WorkProvider { -// fn apply_hist_event(event: &HistoryEvent, has_next_event) -// } +impl CoreSDK +where + WP: WorkProvider, +{ + fn new_workflow(&self, id: String) { + let (wfb, cmd_sink) = WorkflowBridge::new(); + let state_machines = WorkflowMachines::new(Box::new(wfb)); + self.workflow_machines + .insert(id, (state_machines, cmd_sink)); + } +} /// Implementors can provide new work to the SDK. The connection to the server is the real /// implementor, which adapts workflow tasks from the server to sdk workflow tasks. @@ -91,16 +100,18 @@ where })) => { match wfstatus { Status::Successful(success) => { - // let cmds = success.commands.iter().map(|_c| { - // // TODO: Make it go - // vec![] - // }); + // Convert to wf commands + let cmds = success + .commands + .into_iter() + .map(|c| c.try_into().map_err(Into::into)) + .collect::>>()?; // TODO: Need to use task token or wfid etc - // self.workflows - // .get_mut("ID") - // .unwrap() - // .incoming_commands - // .extend(cmds); + self.workflow_machines + .get_mut("fake_wf_id") + .unwrap() + .1 + .send(cmds)?; } Status::Failed(_) => {} } @@ -121,17 +132,24 @@ where /// The [DrivenWorkflow] trait expects to be called to make progress, but the [CoreSDKService] /// expects to be polled by the lang sdk. This struct acts as the bridge between the two, buffering /// output from calls to [DrivenWorkflow] and offering them to [CoreSDKService] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct WorkflowBridge { // does wf id belong in here? started_attrs: Option, - outgoing_tasks: VecDeque, - incoming_commands: VecDeque>, + incoming_commands: Receiver>, } impl WorkflowBridge { - pub fn get_next_task(&mut self) -> Option { - self.outgoing_tasks.pop_front() + /// Create a new bridge, returning it and the sink used to send commands to it. + pub(crate) fn new() -> (Self, Sender>) { + let (tx, rx) = mpsc::channel(); + ( + Self { + started_attrs: None, + incoming_commands: rx, + }, + tx, + ) } } @@ -145,7 +163,7 @@ impl DrivenWorkflow for WorkflowBridge { } fn iterate_wf(&mut self) -> Result, Error> { - Ok(self.incoming_commands.pop_front().unwrap_or_default()) + Ok(self.incoming_commands.try_recv().unwrap_or_default()) } fn signal(&mut self, _attribs: WorkflowExecutionSignaledEventAttributes) -> Result<(), Error> { @@ -165,31 +183,40 @@ pub enum SDKServiceError { NoWork, #[error("Lang SDK sent us a malformed completion: {0:?}")] MalformedCompletion(CompleteSdkTaskReq), + #[error("Error buffering commands")] + CantSendCommands(#[from] SendError>), + #[error("Couldn't interpret command from ")] + UninterprableCommand(#[from] InconvertibleCommandError), } #[cfg(test)] mod test { use super::*; + use crate::protos::coresdk::{SdkwfTaskSuccess, UnblockTimerTaskAttibutes}; + use crate::protos::temporal::api::command::v1::StartTimerCommandAttributes; use crate::{ - machines::{test_help::TestHistoryBuilder, WorkflowMachines}, + machines::test_help::TestHistoryBuilder, protos::{ - coresdk::{CompleteTimerTaskAttributes, StartWorkflowTaskAttributes, WfTaskType}, + coresdk::StartWorkflowTaskAttributes, temporal::api::{ enums::v1::EventType, history::v1::{history_event, TimerFiredEventAttributes}, }, }, }; + use std::collections::VecDeque; + use std::sync::atomic::AtomicBool; + use std::sync::Arc; #[test] fn workflow_bridge() { let wfid = "fake_wf_id"; + let timer_id = "fake_timer"; let mut tasks = VecDeque::from(vec![ SdkwfTask { - r#type: WfTaskType::StartWorkflow as i32, timestamp: None, workflow_id: wfid.to_string(), - attributes: Some( + task: Some( StartWorkflowTaskAttributes { namespace: "namespace".to_string(), name: "wf name?".to_string(), @@ -200,12 +227,11 @@ mod test { } .into(), SdkwfTask { - r#type: WfTaskType::CompleteTimer as i32, timestamp: None, workflow_id: wfid.to_string(), - attributes: Some( - CompleteTimerTaskAttributes { - timer_id: "timer".to_string(), + task: Some( + UnblockTimerTaskAttibutes { + timer_id: timer_id.to_string(), } .into(), ), @@ -218,13 +244,11 @@ mod test { .expect_get_work() .returning(move |_| Ok(tasks.pop_front().unwrap())); - let mut wfb = WorkflowBridge::default(); - let state_machines = WorkflowMachines::new(Box::new(wfb)); let core = CoreSDK { - workflows: DashMap::new(), + workflow_machines: DashMap::new(), work_provider: mock_provider, }; - core.workflows.insert(wfid.to_string(), state_machines); + core.new_workflow(wfid.to_string()); let mut t = TestHistoryBuilder::default(); @@ -241,7 +265,23 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); - let res = core.poll_sdk_task(); - dbg!(res); + let res = core.poll_sdk_task().unwrap(); + let task_tok = res.task_token; + let timer_atom = Arc::new(AtomicBool::new(false)); + core.complete_sdk_task(CompleteSdkTaskReq { + task_token: task_tok, + completion: Some( + SdkwfTaskSuccess { + commands: vec![StartTimerCommandAttributes { + timer_id: timer_id.to_string(), + ..Default::default() + }], + } + .into(), + ), + }) + .unwrap(); + + // SDK commands should be StartTimer followed by Complete WE } } diff --git a/src/machines/mod.rs b/src/machines/mod.rs index b7e4fdfb8..8174d2729 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -37,6 +37,9 @@ pub(crate) mod test_help; pub(crate) use workflow_machines::{WFMachinesError, WorkflowMachines}; +use crate::protos::coresdk; +use crate::protos::coresdk::command::Variant; +use crate::protos::temporal::api::command::v1::command::Attributes; use crate::protos::temporal::api::{ command::v1::{ Command, CompleteWorkflowExecutionCommandAttributes, StartTimerCommandAttributes, @@ -63,7 +66,7 @@ type MachineCommand = Command; /// Implementors of this trait represent something that can (eventually) call into a workflow to /// drive it, start it, signal it, cancel it, etc. -pub(crate) trait DrivenWorkflow: Send + Sync { +pub(crate) trait DrivenWorkflow: Send { /// Start the workflow fn start( &mut self, @@ -102,6 +105,35 @@ pub enum WFCommand { CompleteWorkflow(CompleteWorkflowExecutionCommandAttributes), } +#[derive(Debug)] +pub struct InconvertibleCommandError(pub coresdk::Command); + +impl TryFrom for WFCommand { + type Error = InconvertibleCommandError; + + fn try_from(c: coresdk::Command) -> Result { + // TODO: Return error without cloning + match c.variant.clone() { + Some(a) => match a { + Variant::Api(Command { + attributes: Some(attrs), + .. + }) => match attrs { + Attributes::StartTimerCommandAttributes(s) => { + Ok(WFCommand::AddTimer(s, Arc::new(AtomicBool::new(false)))) + } + Attributes::CompleteWorkflowExecutionCommandAttributes(c) => { + Ok(WFCommand::CompleteWorkflow(c)) + } + _ => unimplemented!(), + }, + _ => Err(c), + }, + None => Err(c), + } + } +} + /// Extends [rustfsm::StateMachine] with some functionality specific to the temporal SDK. /// /// Formerly known as `EntityStateMachine` in Java. From 9a61adb1b989e53d35e48ab1234fb38ad9b35902 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 27 Jan 2021 17:10:41 -0800 Subject: [PATCH 43/59] Make stuff compile --- build.rs | 1 + src/lib.rs | 31 ++++++++++++++++++++----------- src/machines/mod.rs | 7 ++++--- src/protos/mod.rs | 13 +++++++++++++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/build.rs b/build.rs index 2126a93a9..b473d3cfa 100644 --- a/build.rs +++ b/build.rs @@ -14,6 +14,7 @@ fn main() -> Result<(), Box> { "temporal.api.command.v1.Command.attributes", "#[derive(::derive_more::From)]", ) + .type_attribute("coresdk.Command.variant", "#[derive(::derive_more::From)]") .type_attribute("coresdk.SDKWFTask.task", "#[derive(::derive_more::From)]") .type_attribute( "coresdk.PollSDKTaskResp.task", diff --git a/src/lib.rs b/src/lib.rs index 56c8f8c7b..697e2144b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,7 @@ where } fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<(), SDKServiceError> { - match &req.completion { + match req.completion { Some(Completion::Workflow(SdkwfTaskCompletion { status: Some(wfstatus), })) => { @@ -192,8 +192,12 @@ pub enum SDKServiceError { #[cfg(test)] mod test { use super::*; + use crate::protos::coresdk; + use crate::protos::coresdk::command::Variant; use crate::protos::coresdk::{SdkwfTaskSuccess, UnblockTimerTaskAttibutes}; - use crate::protos::temporal::api::command::v1::StartTimerCommandAttributes; + use crate::protos::temporal::api::command::v1::{ + command, Command, StartTimerCommandAttributes, + }; use crate::{ machines::test_help::TestHistoryBuilder, protos::{ @@ -268,17 +272,22 @@ mod test { let res = core.poll_sdk_task().unwrap(); let task_tok = res.task_token; let timer_atom = Arc::new(AtomicBool::new(false)); + let cmd: command::Attributes = StartTimerCommandAttributes { + timer_id: timer_id.to_string(), + ..Default::default() + } + .into(); + let cmd: Command = cmd.into(); + let success = SdkwfTaskSuccess { + commands: vec![coresdk::Command { + variant: Some(Variant::Api(cmd)), + }], + }; core.complete_sdk_task(CompleteSdkTaskReq { task_token: task_tok, - completion: Some( - SdkwfTaskSuccess { - commands: vec![StartTimerCommandAttributes { - timer_id: timer_id.to_string(), - ..Default::default() - }], - } - .into(), - ), + completion: Some(Completion::Workflow(SdkwfTaskCompletion { + status: Some(Status::Successful(success)), + })), }) .unwrap(); diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 8174d2729..36596cab9 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -105,7 +105,8 @@ pub enum WFCommand { CompleteWorkflow(CompleteWorkflowExecutionCommandAttributes), } -#[derive(Debug)] +#[derive(thiserror::Error, Debug, derive_more::From)] +#[error("Couldn't convert command")] pub struct InconvertibleCommandError(pub coresdk::Command); impl TryFrom for WFCommand { @@ -127,9 +128,9 @@ impl TryFrom for WFCommand { } _ => unimplemented!(), }, - _ => Err(c), + _ => Err(c.into()), }, - None => Err(c), + None => Err(c.into()), } } } diff --git a/src/protos/mod.rs b/src/protos/mod.rs index b0bebdb56..33c6a0e13 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -21,7 +21,20 @@ pub mod temporal { pub mod api { pub mod command { pub mod v1 { + use crate::protos::temporal::api::command::v1::command::Attributes; + use crate::protos::temporal::api::enums::v1::CommandType; include!("temporal.api.command.v1.rs"); + impl From for Command { + fn from(c: command::Attributes) -> Self { + match c { + a @ Attributes::StartTimerCommandAttributes(_) => Self { + command_type: CommandType::StartTimer as i32, + attributes: Some(a), + }, + _ => unimplemented!(), + } + } + } } } pub mod enums { From 5cb642ffe7fb43f66bf751d56b4dfa5186825ae5 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Wed, 27 Jan 2021 23:24:13 -0800 Subject: [PATCH 44/59] Set us up for tomorrow --- src/lib.rs | 141 ++++++++++++++++-------------- src/pollers/workflow_poll_task.rs | 28 +++--- 2 files changed, 87 insertions(+), 82 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 697e2144b..b9db375c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,27 +5,29 @@ mod machines; mod pollers; pub mod protos; -use crate::machines::{InconvertibleCommandError, WorkflowMachines}; -use crate::protos::coresdk::poll_sdk_task_resp; -use crate::protos::coresdk::poll_sdk_task_resp::Task; use crate::{ - machines::{DrivenWorkflow, WFCommand}, + machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ coresdk::{ complete_sdk_task_req::Completion, sdkwf_task_completion::Status, CompleteSdkTaskReq, - PollSdkTaskResp, RegistrationReq, SdkwfTask, SdkwfTaskCompletion, + PollSdkTaskResp, RegistrationReq, SdkwfTaskCompletion, }, - temporal::api::history::v1::{ - WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, - WorkflowExecutionStartedEventAttributes, + temporal::api::{ + common::v1::WorkflowExecution, + history::v1::{ + WorkflowExecutionCanceledEventAttributes, WorkflowExecutionSignaledEventAttributes, + WorkflowExecutionStartedEventAttributes, + }, + workflowservice::v1::PollWorkflowTaskQueueResponse, }, }, }; use anyhow::Error; use dashmap::DashMap; -use std::convert::TryInto; -use std::sync::mpsc; -use std::sync::mpsc::{Receiver, SendError, Sender}; +use std::{ + convert::TryInto, + sync::mpsc::{self, Receiver, SendError, Sender}, +}; pub type Result = std::result::Result; @@ -52,7 +54,7 @@ pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { pub struct CoreSDK { work_provider: WP, - /// Key is workflow id -- but perhaps ought to be task token + /// Key is workflow id workflow_machines: DashMap>)>, } @@ -69,13 +71,11 @@ where } /// Implementors can provide new work to the SDK. The connection to the server is the real -/// implementor, which adapts workflow tasks from the server to sdk workflow tasks. +/// implementor. #[cfg_attr(test, mockall::automock)] pub trait WorkProvider { - // TODO: Should actually likely return server tasks directly, so coresdk can do things like - // apply history events that don't need workflow to actually do anything, like schedule. /// Fetch new work. Should block indefinitely if there is no work. - fn get_work(&self, task_queue: &str) -> Result; + fn get_work(&self, task_queue: &str) -> Result; } impl CoreSDKService for CoreSDK @@ -85,12 +85,19 @@ where fn poll_sdk_task(&self) -> Result { // This will block forever in the event there is no work from the server let work = self.work_provider.get_work("TODO: Real task queue")?; - match work { - Task::WfTask(t) => return Ok(PollSdkTaskResp::from_wf_task(b"token".to_vec(), t)), - Task::ActivityTask(_) => { - unimplemented!() + match &work.workflow_execution { + Some(WorkflowExecution { workflow_id, .. }) => { + // TODO: Only do if it doesn't exist yet + self.new_workflow(workflow_id.to_string()); } + // TODO: Appropriate error + None => return Err(SDKServiceError::Unknown), } + // TODO: Apply history to machines, get commands out, convert them to task + Ok(PollSdkTaskResp { + task_token: work.task_token, + task: None, + }) } fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<(), SDKServiceError> { @@ -176,6 +183,7 @@ impl DrivenWorkflow for WorkflowBridge { } #[derive(thiserror::Error, Debug)] +#[allow(clippy::large_enum_variant)] pub enum SDKServiceError { #[error("Unknown service error")] Unknown, @@ -192,70 +200,28 @@ pub enum SDKServiceError { #[cfg(test)] mod test { use super::*; - use crate::protos::coresdk; - use crate::protos::coresdk::command::Variant; - use crate::protos::coresdk::{SdkwfTaskSuccess, UnblockTimerTaskAttibutes}; - use crate::protos::temporal::api::command::v1::{ - command, Command, StartTimerCommandAttributes, - }; use crate::{ machines::test_help::TestHistoryBuilder, protos::{ - coresdk::StartWorkflowTaskAttributes, + coresdk::{self, command::Variant, SdkwfTaskSuccess}, temporal::api::{ + command::v1::{command, Command, StartTimerCommandAttributes}, enums::v1::EventType, history::v1::{history_event, TimerFiredEventAttributes}, }, }, }; - use std::collections::VecDeque; - use std::sync::atomic::AtomicBool; - use std::sync::Arc; + use std::{ + collections::VecDeque, + sync::{atomic::AtomicBool, Arc}, + }; #[test] fn workflow_bridge() { let wfid = "fake_wf_id"; let timer_id = "fake_timer"; - let mut tasks = VecDeque::from(vec![ - SdkwfTask { - timestamp: None, - workflow_id: wfid.to_string(), - task: Some( - StartWorkflowTaskAttributes { - namespace: "namespace".to_string(), - name: "wf name?".to_string(), - arguments: None, - } - .into(), - ), - } - .into(), - SdkwfTask { - timestamp: None, - workflow_id: wfid.to_string(), - task: Some( - UnblockTimerTaskAttibutes { - timer_id: timer_id.to_string(), - } - .into(), - ), - } - .into(), - ]); - - let mut mock_provider = MockWorkProvider::new(); - mock_provider - .expect_get_work() - .returning(move |_| Ok(tasks.pop_front().unwrap())); - - let core = CoreSDK { - workflow_machines: DashMap::new(), - work_provider: mock_provider, - }; - core.new_workflow(wfid.to_string()); let mut t = TestHistoryBuilder::default(); - t.add_by_type(EventType::WorkflowExecutionStarted); t.add_workflow_task(); let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); @@ -269,6 +235,45 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); + // TODO: Use test history builder to issue appropriate histories + let mut tasks = VecDeque::from(vec![]); + let mut mock_provider = MockWorkProvider::new(); + mock_provider + .expect_get_work() + .returning(move |_| Ok(tasks.pop_front().unwrap())); + + let core = CoreSDK { + workflow_machines: DashMap::new(), + work_provider: mock_provider, + }; + core.new_workflow(wfid.to_string()); + + // TODO: These are what poll_sdk_task should end up returning + // SdkwfTask { + // timestamp: None, + // workflow_id: wfid.to_string(), + // task: Some( + // StartWorkflowTaskAttributes { + // namespace: "namespace".to_string(), + // name: "wf name?".to_string(), + // arguments: None, + // } + // .into(), + // ), + // } + // .into(), + // SdkwfTask { + // timestamp: None, + // workflow_id: wfid.to_string(), + // task: Some( + // UnblockTimerTaskAttibutes { + // timer_id: timer_id.to_string(), + // } + // .into(), + // ), + // } + // .into(), + let res = core.poll_sdk_task().unwrap(); let task_tok = res.task_token; let timer_atom = Arc::new(AtomicBool::new(false)); diff --git a/src/pollers/workflow_poll_task.rs b/src/pollers/workflow_poll_task.rs index 8a8495ba6..1745b03ac 100644 --- a/src/pollers/workflow_poll_task.rs +++ b/src/pollers/workflow_poll_task.rs @@ -1,14 +1,14 @@ -use tonic::codegen::Future; -use tonic::{Response, Status}; - -use crate::pollers::poll_task::{PollTask, Result}; -use crate::protos::temporal::api::enums::v1 as enums; -use crate::protos::temporal::api::taskqueue::v1 as tq; -use crate::protos::temporal::api::workflowservice::v1 as temporal; -use crate::protos::temporal::api::workflowservice::v1::{ - PollWorkflowTaskQueueRequest, PollWorkflowTaskQueueResponse, +use crate::{ + pollers::poll_task::PollTask, + pollers::poll_task::Result, + protos::temporal::api::{ + enums::v1::TaskQueueKind, taskqueue::v1::TaskQueue, + workflowservice::v1::workflow_service_client::WorkflowServiceClient, + workflowservice::v1::PollWorkflowTaskQueueRequest, + workflowservice::v1::PollWorkflowTaskQueueResponse, + }, }; -use temporal::workflow_service_client::WorkflowServiceClient; +use tonic::{codegen::Future, Response, Status}; struct WorkflowPollTask<'a> { service: &'a mut WorkflowServiceClient, @@ -19,13 +19,13 @@ struct WorkflowPollTask<'a> { } #[async_trait::async_trait] -impl PollTask for WorkflowPollTask<'_> { +impl PollTask for WorkflowPollTask<'_> { async fn poll(&mut self) -> Result { - let request = tonic::Request::new(temporal::PollWorkflowTaskQueueRequest { + let request = tonic::Request::new(PollWorkflowTaskQueueRequest { namespace: self.namespace.clone(), - task_queue: Some(tq::TaskQueue { + task_queue: Some(TaskQueue { name: self.task_queue.clone(), - kind: enums::TaskQueueKind::Unspecified as i32, + kind: TaskQueueKind::Unspecified as i32, }), identity: self.identity.clone(), binary_checksum: self.binary_checksum.clone(), From f102bb2f164acdce4e0aa55d2104fe1931f2605e Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Thu, 28 Jan 2021 10:22:11 +0200 Subject: [PATCH 45/59] Remove SDK prefix and refine interface a bit --- build.rs | 4 +-- protos/local/core_interface.proto | 59 +++++++++++++++---------------- src/lib.rs | 52 +++++++++++++-------------- src/protos/mod.rs | 8 ++--- 4 files changed, 60 insertions(+), 63 deletions(-) diff --git a/build.rs b/build.rs index b473d3cfa..05d28921b 100644 --- a/build.rs +++ b/build.rs @@ -15,11 +15,11 @@ fn main() -> Result<(), Box> { "#[derive(::derive_more::From)]", ) .type_attribute("coresdk.Command.variant", "#[derive(::derive_more::From)]") - .type_attribute("coresdk.SDKWFTask.task", "#[derive(::derive_more::From)]") .type_attribute( - "coresdk.PollSDKTaskResp.task", + "coresdk.WorkflowTask.attributes", "#[derive(::derive_more::From)]", ) + .type_attribute("coresdk.Task.variant", "#[derive(::derive_more::From)]") .compile( &[ "protos/local/core_interface.proto", diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 3b68fa1b2..6548ec66f 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -16,7 +16,6 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/command/v1/message.proto"; -// TODO: Use the SDK prefix? message WorkflowIdentifier { string namespace = 1; string name = 2; @@ -32,19 +31,17 @@ message RegistrationReq { repeated ActivityIdentifier activities = 2; } -// TODO: SDK prefix in front of everything is maybe pointless given it's all within this package - -service CoreSDKService { - rpc PollSDKTask (google.protobuf.Empty) returns (PollSDKTaskResp) {} - rpc CompleteSDKTask (CompleteSDKTaskReq) returns (google.protobuf.Empty) {} +service Core { + rpc PollTask (google.protobuf.Empty) returns (Task) {} + rpc CompleteTask (CompleteTaskReq) returns (google.protobuf.Empty) {} rpc RegisterImplementations (RegistrationReq) returns (google.protobuf.Empty) {} } -message PollSDKTaskResp { +message Task { bytes task_token = 1; - oneof task { - SDKWFTask wf_task = 2; - SDKActivityTask activity_task = 3; + oneof variant { + WorkflowTask workflow = 2; + ActivityTask activity = 3; } } @@ -54,75 +51,75 @@ message StartWorkflowTaskAttributes { temporal.api.common.v1.Payloads arguments = 3; } -message UnblockTimerTaskAttibutes { +message TriggerTimerTaskAttributes { string timer_id = 1; } -message SDKWFTask { +message WorkflowTask { google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; string workflow_id = 2; - oneof task { + oneof attributes { StartWorkflowTaskAttributes start_workflow = 3; - UnblockTimerTaskAttibutes unblock_timer = 4; + TriggerTimerTaskAttributes trigger_timer = 4; } } -message SDKActivityTask { +message ActivityTask { // Original task from temporal service temporal.api.workflowservice.v1.PollActivityTaskQueueResponse original = 1; } -message CompleteSDKTaskReq { +message CompleteTaskReq { bytes task_token = 1; oneof completion { - SDKWFTaskCompletion workflow = 2; - SDKActivityTaskCompletion activity = 3; + WorkflowTaskCompletion workflow = 2; + ActivityTaskCompletion activity = 3; } } -message SDKWFTaskCompletion { +message WorkflowTaskCompletion { oneof status { - SDKWFTaskSuccess successful = 1; - SDKWFTaskFailure failed = 2; + WorkflowTaskSuccess successful = 1; + WorkflowTaskFailure failed = 2; } } -message SDKActivityTaskCompletion { +message ActivityTaskCompletion { oneof status { - SDKActivityTaskSuccess successful = 1; - SDKActivityTaskFailure failed = 2; + ActivityTaskSuccess successful = 1; + ActivityTaskFailure failed = 2; } } -message SDKCommand { - // Reserved for SDK specific commands +message CoreCommand { + // Reserved for specific commands } message Command { oneof variant { temporal.api.command.v1.Command api = 1; - SDKCommand sdk = 2; + CoreCommand core = 2; } } -message SDKWFTaskSuccess { +message WorkflowTaskSuccess { repeated Command commands = 1; // Other bits from RespondWorkflowTaskCompletedRequest as needed } -message SDKWFTaskFailure { +message WorkflowTaskFailure { temporal.api.enums.v1.WorkflowTaskFailedCause cause = 1; temporal.api.failure.v1.Failure failure = 2; // Other bits from RespondWorkflowTaskFailedRequest as needed } -message SDKActivityTaskSuccess { +message ActivityTaskSuccess { temporal.api.common.v1.Payloads result = 1; // Other bits from RespondActivityTaskCompletedRequest as needed } -message SDKActivityTaskFailure { +message ActivityTaskFailure { temporal.api.failure.v1.Failure failure = 1; // Other bits from RespondActivityTaskFailedRequest as needed } diff --git a/src/lib.rs b/src/lib.rs index b9db375c1..53726438c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ coresdk::{ - complete_sdk_task_req::Completion, sdkwf_task_completion::Status, CompleteSdkTaskReq, - PollSdkTaskResp, RegistrationReq, SdkwfTaskCompletion, + complete_task_req::Completion, workflow_task_completion::Status, CompleteTaskReq, + RegistrationReq, Task, WorkflowTaskCompletion, }, temporal::api::{ common::v1::WorkflowExecution, @@ -29,27 +29,27 @@ use std::{ sync::mpsc::{self, Receiver, SendError, Sender}, }; -pub type Result = std::result::Result; +pub type Result = std::result::Result; // TODO: Do we actually need this to be send+sync? Probably, but there's also no reason to have // any given WorfklowMachines instance accessed on more than one thread. Ideally this trait can // be accessed from many threads, but each workflow is pinned to one thread (possibly with many // sharing the same thread). IE: WorkflowMachines should be Send but not Sync, and this should // be both, ideally. -pub trait CoreSDKService { - fn poll_sdk_task(&self) -> Result; - fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<()>; +pub trait Core { + fn poll_task(&self) -> Result; + fn complete_task(&self, req: CompleteTaskReq) -> Result<()>; fn register_implementations(&self, req: RegistrationReq) -> Result<()>; } -pub struct CoreSDKInitOptions { +pub struct CoreInitOptions { _queue_name: String, _max_concurrent_workflow_executions: u32, _max_concurrent_activity_executions: u32, } -pub fn init_sdk(_opts: CoreSDKInitOptions) -> Result> { - Err(SDKServiceError::Unknown {}) +pub fn init(_opts: CoreInitOptions) -> Result> { + Err(CoreError::Unknown {}) } pub struct CoreSDK { @@ -78,11 +78,11 @@ pub trait WorkProvider { fn get_work(&self, task_queue: &str) -> Result; } -impl CoreSDKService for CoreSDK +impl Core for CoreSDK where WP: WorkProvider, { - fn poll_sdk_task(&self) -> Result { + fn poll_task(&self) -> Result { // This will block forever in the event there is no work from the server let work = self.work_provider.get_work("TODO: Real task queue")?; match &work.workflow_execution { @@ -91,18 +91,18 @@ where self.new_workflow(workflow_id.to_string()); } // TODO: Appropriate error - None => return Err(SDKServiceError::Unknown), + None => return Err(CoreError::Unknown), } // TODO: Apply history to machines, get commands out, convert them to task - Ok(PollSdkTaskResp { + Ok(Task { task_token: work.task_token, - task: None, + variant: None, }) } - fn complete_sdk_task(&self, req: CompleteSdkTaskReq) -> Result<(), SDKServiceError> { + fn complete_task(&self, req: CompleteTaskReq) -> Result<(), CoreError> { match req.completion { - Some(Completion::Workflow(SdkwfTaskCompletion { + Some(Completion::Workflow(WorkflowTaskCompletion { status: Some(wfstatus), })) => { match wfstatus { @@ -127,11 +127,11 @@ where Some(Completion::Activity(_)) => { unimplemented!() } - _ => Err(SDKServiceError::MalformedCompletion(req)), + _ => Err(CoreError::MalformedCompletion(req)), } } - fn register_implementations(&self, _req: RegistrationReq) -> Result<(), SDKServiceError> { + fn register_implementations(&self, _req: RegistrationReq) -> Result<(), CoreError> { unimplemented!() } } @@ -184,13 +184,13 @@ impl DrivenWorkflow for WorkflowBridge { #[derive(thiserror::Error, Debug)] #[allow(clippy::large_enum_variant)] -pub enum SDKServiceError { +pub enum CoreError { #[error("Unknown service error")] Unknown, #[error("No tasks to perform for now")] NoWork, #[error("Lang SDK sent us a malformed completion: {0:?}")] - MalformedCompletion(CompleteSdkTaskReq), + MalformedCompletion(CompleteTaskReq), #[error("Error buffering commands")] CantSendCommands(#[from] SendError>), #[error("Couldn't interpret command from ")] @@ -203,7 +203,7 @@ mod test { use crate::{ machines::test_help::TestHistoryBuilder, protos::{ - coresdk::{self, command::Variant, SdkwfTaskSuccess}, + coresdk::{self, command::Variant, WorkflowTaskSuccess}, temporal::api::{ command::v1::{command, Command, StartTimerCommandAttributes}, enums::v1::EventType, @@ -248,7 +248,7 @@ mod test { }; core.new_workflow(wfid.to_string()); - // TODO: These are what poll_sdk_task should end up returning + // TODO: These are what poll_task should end up returning // SdkwfTask { // timestamp: None, // workflow_id: wfid.to_string(), @@ -274,7 +274,7 @@ mod test { // } // .into(), - let res = core.poll_sdk_task().unwrap(); + let res = core.poll_task().unwrap(); let task_tok = res.task_token; let timer_atom = Arc::new(AtomicBool::new(false)); let cmd: command::Attributes = StartTimerCommandAttributes { @@ -283,14 +283,14 @@ mod test { } .into(); let cmd: Command = cmd.into(); - let success = SdkwfTaskSuccess { + let success = WorkflowTaskSuccess { commands: vec![coresdk::Command { variant: Some(Variant::Api(cmd)), }], }; - core.complete_sdk_task(CompleteSdkTaskReq { + core.complete_task(CompleteTaskReq { task_token: task_tok, - completion: Some(Completion::Workflow(SdkwfTaskCompletion { + completion: Some(Completion::Workflow(WorkflowTaskCompletion { status: Some(Status::Successful(success)), })), }) diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 33c6a0e13..60fec832f 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -4,11 +4,11 @@ pub mod coresdk { pub type HistoryEventId = i64; - impl PollSdkTaskResp { - pub fn from_wf_task(task_token: Vec, t: SdkwfTask) -> Self { - PollSdkTaskResp { + impl Task { + pub fn from_wf_task(task_token: Vec, t: WorkflowTask) -> Self { + Task { task_token, - task: Some(t.into()), + variant: Some(t.into()), } } } From f03824c6e451feb345d6de3ffc2e3cf4a8ce86ad Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 28 Jan 2021 09:54:51 -0800 Subject: [PATCH 46/59] Made enough meaningful progress --- src/lib.rs | 41 ++++++++++++++++++++--- src/machines/test_help/history_builder.rs | 13 ++++--- src/machines/timer_state_machine.rs | 2 ++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 53726438c..ca51e0d4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,7 @@ pub enum CoreError { #[cfg(test)] mod test { use super::*; + use crate::protos::temporal::api::history::v1::History; use crate::{ machines::test_help::TestHistoryBuilder, protos::{ @@ -234,9 +235,41 @@ mod test { }), ); t.add_workflow_task_scheduled_and_started(); + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + --- + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_TIMER_STARTED + 6: EVENT_TYPE_TIMER_FIRED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + --- + */ + let events_first_batch = t.get_history_info(1).unwrap().events; + let wf = Some(WorkflowExecution { + workflow_id: wfid.to_string(), + run_id: "".to_string(), + }); + let first_response = PollWorkflowTaskQueueResponse { + history: Some(History { + events: events_first_batch, + }), + workflow_execution: wf.clone(), + ..Default::default() + }; + let events_second_batch = t.get_history_info(2).unwrap().events; + let second_response = PollWorkflowTaskQueueResponse { + history: Some(History { + events: events_second_batch, + }), + workflow_execution: wf.clone(), + ..Default::default() + }; + let responses = vec![first_response, second_response]; - // TODO: Use test history builder to issue appropriate histories - let mut tasks = VecDeque::from(vec![]); + let mut tasks = VecDeque::from(responses); let mut mock_provider = MockWorkProvider::new(); mock_provider .expect_get_work() @@ -246,7 +279,6 @@ mod test { workflow_machines: DashMap::new(), work_provider: mock_provider, }; - core.new_workflow(wfid.to_string()); // TODO: These are what poll_task should end up returning // SdkwfTask { @@ -274,7 +306,8 @@ mod test { // } // .into(), - let res = core.poll_task().unwrap(); + let res = dbg!(core.poll_task().unwrap()); + assert!(core.workflow_machines.get(wfid).is_some()); let task_tok = res.task_token; let timer_atom = Arc::new(AtomicBool::new(false)); let cmd: command::Attributes = StartTimerCommandAttributes { diff --git a/src/machines/test_help/history_builder.rs b/src/machines/test_help/history_builder.rs index 19d1bf94b..0df161e9f 100644 --- a/src/machines/test_help/history_builder.rs +++ b/src/machines/test_help/history_builder.rs @@ -209,14 +209,16 @@ impl TestHistoryBuilder { Ok(()) } - /// Iterates over the events in this builder to return a [HistoryInfo] - pub(crate) fn get_history_info(&self, to_task_index: usize) -> Result { + /// Iterates over the events in this builder to return a [HistoryInfo] of the n-th workflow task. + pub(crate) fn get_history_info(&self, to_wf_task_num: usize) -> Result { let mut lastest_wf_started_id = 0; let mut previous_wf_started_id = 0; let mut count = 0; let mut history = self.events.iter().peekable(); + let mut events = vec![]; while let Some(event) = history.next() { + events.push(event.clone()); let next_event = history.peek(); if event.event_type == EventType::WorkflowTaskStarted as i32 { @@ -235,10 +237,11 @@ impl TestHistoryBuilder { bail!("Latest wf started id and previous one are equal!") } count += 1; - if count == to_task_index || next_event.is_none() { + if count == to_wf_task_num || next_event.is_none() { return Ok(HistoryInfo::new( previous_wf_started_id, lastest_wf_started_id, + events, )); } } else if next_event.is_some() && !next_is_failed_or_timeout { @@ -255,6 +258,7 @@ impl TestHistoryBuilder { return Ok(HistoryInfo::new( previous_wf_started_id, lastest_wf_started_id, + events, )); } // No more events @@ -290,8 +294,9 @@ fn default_attribs(et: EventType) -> Result { }) } -#[derive(Clone, Debug, derive_more::Constructor, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, derive_more::Constructor, PartialEq)] pub struct HistoryInfo { pub previous_started_event_id: i64, pub workflow_task_started_event_id: i64, + pub events: Vec, } diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index e38d31f2d..d1ec9146e 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -314,6 +314,8 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); assert_eq!(2, t.get_workflow_task_count(None).unwrap()); + assert_eq!(3, t.get_history_info(1).unwrap().events.len()); + assert_eq!(8, t.get_history_info(2).unwrap().events.len()); (t, state_machines) } From cb888070856c340795f3dfe9c66399724d6f5e36 Mon Sep 17 00:00:00 2001 From: Roey Berman Date: Thu, 28 Jan 2021 21:07:04 +0200 Subject: [PATCH 47/59] Amend the core interface with meeting conclusions --- protos/local/core_interface.proto | 30 ++++++++++-------------------- src/lib.rs | 7 ------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 6548ec66f..d4e23c09d 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -16,25 +16,14 @@ import "temporal/api/failure/v1/message.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/command/v1/message.proto"; -message WorkflowIdentifier { - string namespace = 1; - string name = 2; -} - -message ActivityIdentifier { - string namespace = 1; - string name = 2; -} - -message RegistrationReq { - repeated WorkflowIdentifier workflows = 1; - repeated ActivityIdentifier activities = 2; -} - service Core { - rpc PollTask (google.protobuf.Empty) returns (Task) {} + rpc PollTask (PollTaskReq) returns (Task) {} rpc CompleteTask (CompleteTaskReq) returns (google.protobuf.Empty) {} - rpc RegisterImplementations (RegistrationReq) returns (google.protobuf.Empty) {} +} + +message PollTaskReq { + bool workflows = 1; + bool activities = 1; } message Task { @@ -51,16 +40,17 @@ message StartWorkflowTaskAttributes { temporal.api.common.v1.Payloads arguments = 3; } -message TriggerTimerTaskAttributes { +message UnblockTimerTaskAttributes { string timer_id = 1; } message WorkflowTask { google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; string workflow_id = 2; + string run_id = 3; oneof attributes { - StartWorkflowTaskAttributes start_workflow = 3; - TriggerTimerTaskAttributes trigger_timer = 4; + StartWorkflowTaskAttributes start_workflow = 4; + UnblockTimerTaskAttributes unblock_timer = 5; } } diff --git a/src/lib.rs b/src/lib.rs index ca51e0d4b..937d411ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,13 +39,10 @@ pub type Result = std::result::Result; pub trait Core { fn poll_task(&self) -> Result; fn complete_task(&self, req: CompleteTaskReq) -> Result<()>; - fn register_implementations(&self, req: RegistrationReq) -> Result<()>; } pub struct CoreInitOptions { _queue_name: String, - _max_concurrent_workflow_executions: u32, - _max_concurrent_activity_executions: u32, } pub fn init(_opts: CoreInitOptions) -> Result> { @@ -130,10 +127,6 @@ where _ => Err(CoreError::MalformedCompletion(req)), } } - - fn register_implementations(&self, _req: RegistrationReq) -> Result<(), CoreError> { - unimplemented!() - } } /// The [DrivenWorkflow] trait expects to be called to make progress, but the [CoreSDKService] From 90c84684e9102b11a0897f46ec91f323c673daef Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 28 Jan 2021 13:35:51 -0800 Subject: [PATCH 48/59] Compile fix --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 937d411ff..9c2bf551f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ coresdk::{ - complete_task_req::Completion, workflow_task_completion::Status, CompleteTaskReq, - RegistrationReq, Task, WorkflowTaskCompletion, + complete_task_req::Completion, workflow_task_completion::Status, CompleteTaskReq, Task, + WorkflowTaskCompletion, }, temporal::api::{ common::v1::WorkflowExecution, From 55d26dbea9894d9e2fb4f0565d9536d3db094b31 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 28 Jan 2021 14:43:03 -0800 Subject: [PATCH 49/59] Clean up one of the more obnoxious proto conversion chains --- protos/local/core_interface.proto | 2 +- src/lib.rs | 43 ++++++++++--------------------- src/protos/mod.rs | 39 ++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index d4e23c09d..53b9bc58d 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -23,7 +23,7 @@ service Core { message PollTaskReq { bool workflows = 1; - bool activities = 1; + bool activities = 2; } message Task { diff --git a/src/lib.rs b/src/lib.rs index 9c2bf551f..36f278f7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,22 +193,15 @@ pub enum CoreError { #[cfg(test)] mod test { use super::*; - use crate::protos::temporal::api::history::v1::History; use crate::{ machines::test_help::TestHistoryBuilder, - protos::{ - coresdk::{self, command::Variant, WorkflowTaskSuccess}, - temporal::api::{ - command::v1::{command, Command, StartTimerCommandAttributes}, - enums::v1::EventType, - history::v1::{history_event, TimerFiredEventAttributes}, - }, + protos::temporal::api::{ + command::v1::StartTimerCommandAttributes, + enums::v1::EventType, + history::v1::{history_event, History, TimerFiredEventAttributes}, }, }; - use std::{ - collections::VecDeque, - sync::{atomic::AtomicBool, Arc}, - }; + use std::collections::VecDeque; #[test] fn workflow_bridge() { @@ -302,24 +295,14 @@ mod test { let res = dbg!(core.poll_task().unwrap()); assert!(core.workflow_machines.get(wfid).is_some()); let task_tok = res.task_token; - let timer_atom = Arc::new(AtomicBool::new(false)); - let cmd: command::Attributes = StartTimerCommandAttributes { - timer_id: timer_id.to_string(), - ..Default::default() - } - .into(); - let cmd: Command = cmd.into(); - let success = WorkflowTaskSuccess { - commands: vec![coresdk::Command { - variant: Some(Variant::Api(cmd)), - }], - }; - core.complete_task(CompleteTaskReq { - task_token: task_tok, - completion: Some(Completion::Workflow(WorkflowTaskCompletion { - status: Some(Status::Successful(success)), - })), - }) + core.complete_task(CompleteTaskReq::ok_from_api_attrs( + StartTimerCommandAttributes { + timer_id: timer_id.to_string(), + ..Default::default() + } + .into(), + task_tok, + )) .unwrap(); // SDK commands should be StartTimer followed by Complete WE diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 60fec832f..80c873d44 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -1,6 +1,10 @@ #[allow(clippy::large_enum_variant)] pub mod coresdk { include!("coresdk.rs"); + use super::temporal::api::command::v1 as api_command; + use super::temporal::api::command::v1::Command as ApiCommand; + use crate::protos::coresdk::complete_task_req::Completion; + use command::Variant; pub type HistoryEventId = i64; @@ -12,6 +16,36 @@ pub mod coresdk { } } } + + impl From> for WorkflowTaskSuccess { + fn from(v: Vec) -> Self { + WorkflowTaskSuccess { + commands: v + .into_iter() + .map(|cmd| Command { + variant: Some(Variant::Api(cmd)), + }) + .collect(), + } + } + } + + impl CompleteTaskReq { + /// Build a successful completion from some api command attributes and a task token + pub fn ok_from_api_attrs( + cmd: api_command::command::Attributes, + task_token: Vec, + ) -> Self { + let cmd: ApiCommand = cmd.into(); + let success: WorkflowTaskSuccess = vec![cmd].into(); + CompleteTaskReq { + task_token, + completion: Some(Completion::Workflow(WorkflowTaskCompletion { + status: Some(workflow_task_completion::Status::Successful(success)), + })), + } + } + } } // No need to lint these @@ -21,9 +55,10 @@ pub mod temporal { pub mod api { pub mod command { pub mod v1 { - use crate::protos::temporal::api::command::v1::command::Attributes; - use crate::protos::temporal::api::enums::v1::CommandType; include!("temporal.api.command.v1.rs"); + use crate::protos::temporal::api::enums::v1::CommandType; + use command::Attributes; + impl From for Command { fn from(c: command::Attributes) -> Self { match c { From a4723a317b21774b18f72bdc56cd87d70fc2aae9 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 28 Jan 2021 13:59:21 -0800 Subject: [PATCH 50/59] Dumping all the stuff --- src/machines/test_help/history_builder.rs | 68 +---------------------- src/machines/timer_state_machine.rs | 2 - src/protosext/history_info.rs | 0 src/protosext/mod.rs | 0 4 files changed, 3 insertions(+), 67 deletions(-) create mode 100644 src/protosext/history_info.rs create mode 100644 src/protosext/mod.rs diff --git a/src/machines/test_help/history_builder.rs b/src/machines/test_help/history_builder.rs index 0df161e9f..c7dc85ed2 100644 --- a/src/machines/test_help/history_builder.rs +++ b/src/machines/test_help/history_builder.rs @@ -1,4 +1,5 @@ use super::Result; +use crate::protosext::HistoryInfo; use crate::{ machines::{workflow_machines::WorkflowMachines, MachineCommand}, protos::temporal::api::{ @@ -210,64 +211,8 @@ impl TestHistoryBuilder { } /// Iterates over the events in this builder to return a [HistoryInfo] of the n-th workflow task. - pub(crate) fn get_history_info(&self, to_wf_task_num: usize) -> Result { - let mut lastest_wf_started_id = 0; - let mut previous_wf_started_id = 0; - let mut count = 0; - let mut history = self.events.iter().peekable(); - let mut events = vec![]; - - while let Some(event) = history.next() { - events.push(event.clone()); - let next_event = history.peek(); - - if event.event_type == EventType::WorkflowTaskStarted as i32 { - let next_is_completed = next_event.map_or(false, |ne| { - ne.event_type == EventType::WorkflowTaskCompleted as i32 - }); - let next_is_failed_or_timeout = next_event.map_or(false, |ne| { - ne.event_type == EventType::WorkflowTaskFailed as i32 - || ne.event_type == EventType::WorkflowTaskTimedOut as i32 - }); - - if next_event.is_none() || next_is_completed { - previous_wf_started_id = lastest_wf_started_id; - lastest_wf_started_id = event.event_id; - if lastest_wf_started_id == previous_wf_started_id { - bail!("Latest wf started id and previous one are equal!") - } - count += 1; - if count == to_wf_task_num || next_event.is_none() { - return Ok(HistoryInfo::new( - previous_wf_started_id, - lastest_wf_started_id, - events, - )); - } - } else if next_event.is_some() && !next_is_failed_or_timeout { - bail!( - "Invalid history! Event {:?} should be WF task completed, \ - failed, or timed out.", - &event - ); - } - } - - if next_event.is_none() { - if event.is_final_wf_execution_event() { - return Ok(HistoryInfo::new( - previous_wf_started_id, - lastest_wf_started_id, - events, - )); - } - // No more events - if lastest_wf_started_id != event.event_id { - bail!("Last item in history wasn't WorkflowTaskStarted") - } - } - } - unreachable!() + pub(crate) fn get_history_info(&self, to_wf_task_num: usize) -> HistoryInfo { + HistoryInfo::new_from_events(self.events.clone(), to_wf_task_num) } fn build_and_push_event(&mut self, event_type: EventType, attribs: Attributes) { @@ -293,10 +238,3 @@ fn default_attribs(et: EventType) -> Result { _ => bail!("Don't know how to construct default attrs for {:?}", et), }) } - -#[derive(Clone, Debug, derive_more::Constructor, PartialEq)] -pub struct HistoryInfo { - pub previous_started_event_id: i64, - pub workflow_task_started_event_id: i64, - pub events: Vec, -} diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index d1ec9146e..e38d31f2d 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -314,8 +314,6 @@ mod test { ); t.add_workflow_task_scheduled_and_started(); assert_eq!(2, t.get_workflow_task_count(None).unwrap()); - assert_eq!(3, t.get_history_info(1).unwrap().events.len()); - assert_eq!(8, t.get_history_info(2).unwrap().events.len()); (t, state_machines) } diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/protosext/mod.rs b/src/protosext/mod.rs new file mode 100644 index 000000000..e69de29bb From a136972badc63a3c53c4932617a2a12eaff7b249 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 28 Jan 2021 16:37:47 -0800 Subject: [PATCH 51/59] Merge --- src/lib.rs | 2 + src/protosext/history_info.rs | 126 ++++++++++++++++++++++++++++++++++ src/protosext/mod.rs | 2 + 3 files changed, 130 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 36f278f7c..b730d48e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ extern crate tracing; mod machines; mod pollers; pub mod protos; +mod protosext; +pub use protosext::HistoryInfo; use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index e69de29bb..935848941 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -0,0 +1,126 @@ +use crate::protos::temporal::api::enums::v1::EventType; +use crate::protos::temporal::api::history::v1::{History, HistoryEvent}; +use crate::protosext::history_info::HistoryInfoError::UnexpectedEventId; + +#[derive(Clone, Debug, derive_more::Constructor, PartialEq)] +pub struct HistoryInfo { + pub previous_started_event_id: i64, + pub workflow_task_started_event_id: i64, + pub events: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum HistoryInfoError { + #[error("Latest wf started id and previous one are equal! ${previous_started_event_id:?}")] + UnexpectedEventId { + previous_started_event_id: i64, + workflow_task_started_event_id: i64, + }, + #[error("Invalid history! Event {0:?} should be WF task completed, failed, or timed out")] + FailedOrTimeout(HistoryEvent), + #[error("Last item in history wasn't WorkflowTaskStarted")] + HistoryEndsUnexpectedly, +} +impl HistoryInfo { + pub fn new_from_events( + events: Vec, + to_wf_task_num: usize, + ) -> Result { + let mut workflow_task_started_event_id = 0; + let mut previous_started_event_id = 0; + let mut count = 0; + let mut history = events.iter().peekable(); + let mut events = vec![]; + + while let Some(event) = history.next() { + events.push(event.clone()); + let next_event = history.peek(); + + if event.event_type == EventType::WorkflowTaskStarted as i32 { + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); + let next_is_failed_or_timeout = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskFailed as i32 + || ne.event_type == EventType::WorkflowTaskTimedOut as i32 + }); + + if next_event.is_none() || next_is_completed { + previous_started_event_id = workflow_task_started_event_id; + workflow_task_started_event_id = event.event_id; + if workflow_task_started_event_id == previous_started_event_id { + return Err(HistoryInfoError::UnexpectedEventId { + previous_started_event_id, + workflow_task_started_event_id, + }); + } + count += 1; + if count == to_wf_task_num || next_event.is_none() { + return Ok(Self { + previous_started_event_id, + workflow_task_started_event_id, + events, + }); + } + } else if next_event.is_some() && !next_is_failed_or_timeout { + Err(HistoryInfoError::FailedOrTimeout(event.clone())); + } + } + + if next_event.is_none() { + if event.is_final_wf_execution_event() { + return Ok(Self { + previous_started_event_id, + workflow_task_started_event_id, + events, + }); + } + // No more events + if workflow_task_started_event_id != event.event_id { + return Err(HistoryInfoError::HistoryEndsUnexpectedly); + } + } + } + unreachable!() + } + + pub fn new(h: History, to_wf_task_num: usize) -> Self { + Self::new_from_events(h.events, to_wf_task_num) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::machines::test_help::TestHistoryBuilder; + use crate::protos::temporal::api::history::v1::{history_event, TimerFiredEventAttributes}; + + #[test] + fn blah() { + /* + 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED + 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 3: EVENT_TYPE_WORKFLOW_TASK_STARTED + 4: EVENT_TYPE_WORKFLOW_TASK_COMPLETED + 5: EVENT_TYPE_TIMER_STARTED + 6: EVENT_TYPE_TIMER_FIRED + 7: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED + 8: EVENT_TYPE_WORKFLOW_TASK_STARTED + */ + let mut t = TestHistoryBuilder::default(); + + t.add_by_type(EventType::WorkflowExecutionStarted); + t.add_workflow_task(); + let timer_started_event_id = t.add_get_event_id(EventType::TimerStarted, None); + t.add( + EventType::TimerFired, + history_event::Attributes::TimerFiredEventAttributes(TimerFiredEventAttributes { + started_event_id: timer_started_event_id, + timer_id: "timer1".to_string(), + ..Default::default() + }), + ); + t.add_workflow_task_scheduled_and_started(); + t.get + } +} diff --git a/src/protosext/mod.rs b/src/protosext/mod.rs index e69de29bb..eadc7aa13 100644 --- a/src/protosext/mod.rs +++ b/src/protosext/mod.rs @@ -0,0 +1,2 @@ +mod history_info; +pub use history_info::HistoryInfo; From 9cac8b6b088db5e698deae81cf39622afabbcd2d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 28 Jan 2021 16:52:45 -0800 Subject: [PATCH 52/59] compiling --- src/lib.rs | 2 +- src/machines/test_help/history_builder.rs | 22 +++++++++++----------- src/protosext/history_info.rs | 5 ++--- src/protosext/mod.rs | 1 + 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b730d48e7..59ea8e1ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ mod machines; mod pollers; pub mod protos; mod protosext; -pub use protosext::HistoryInfo; +pub use protosext::{HistoryInfo, HistoryInfoError}; use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, diff --git a/src/machines/test_help/history_builder.rs b/src/machines/test_help/history_builder.rs index c7dc85ed2..ba9ad993e 100644 --- a/src/machines/test_help/history_builder.rs +++ b/src/machines/test_help/history_builder.rs @@ -1,16 +1,13 @@ use super::Result; use crate::protosext::HistoryInfo; -use crate::{ - machines::{workflow_machines::WorkflowMachines, MachineCommand}, - protos::temporal::api::{ - enums::v1::EventType, - history::v1::{ - history_event::Attributes, HistoryEvent, TimerStartedEventAttributes, - WorkflowExecutionStartedEventAttributes, WorkflowTaskCompletedEventAttributes, - WorkflowTaskScheduledEventAttributes, WorkflowTaskStartedEventAttributes, - }, +use crate::{machines::{workflow_machines::WorkflowMachines, MachineCommand}, protos::temporal::api::{ + enums::v1::EventType, + history::v1::{ + history_event::Attributes, HistoryEvent, TimerStartedEventAttributes, + WorkflowExecutionStartedEventAttributes, WorkflowTaskCompletedEventAttributes, + WorkflowTaskScheduledEventAttributes, WorkflowTaskStartedEventAttributes, }, -}; +}, HistoryInfoError}; use anyhow::bail; use std::time::SystemTime; @@ -211,7 +208,10 @@ impl TestHistoryBuilder { } /// Iterates over the events in this builder to return a [HistoryInfo] of the n-th workflow task. - pub(crate) fn get_history_info(&self, to_wf_task_num: usize) -> HistoryInfo { + pub(crate) fn get_history_info( + &self, + to_wf_task_num: usize, + ) -> Result { HistoryInfo::new_from_events(self.events.clone(), to_wf_task_num) } diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index 935848941..c5e9d2998 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -63,7 +63,7 @@ impl HistoryInfo { }); } } else if next_event.is_some() && !next_is_failed_or_timeout { - Err(HistoryInfoError::FailedOrTimeout(event.clone())); + return Err(HistoryInfoError::FailedOrTimeout(event.clone())); } } @@ -84,7 +84,7 @@ impl HistoryInfo { unreachable!() } - pub fn new(h: History, to_wf_task_num: usize) -> Self { + pub fn new_from_history(h: History, to_wf_task_num: usize) -> Result { Self::new_from_events(h.events, to_wf_task_num) } } @@ -121,6 +121,5 @@ mod tests { }), ); t.add_workflow_task_scheduled_and_started(); - t.get } } diff --git a/src/protosext/mod.rs b/src/protosext/mod.rs index eadc7aa13..dd4f27a90 100644 --- a/src/protosext/mod.rs +++ b/src/protosext/mod.rs @@ -1,2 +1,3 @@ mod history_info; pub use history_info::HistoryInfo; +pub use history_info::HistoryInfoError; From 4e15a15bf8f80bdb1370632acea8e0a5a043625f Mon Sep 17 00:00:00 2001 From: Vitaly Date: Thu, 28 Jan 2021 17:04:49 -0800 Subject: [PATCH 53/59] add assertion --- src/protosext/history_info.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index c5e9d2998..f2a60d134 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -121,5 +121,7 @@ mod tests { }), ); t.add_workflow_task_scheduled_and_started(); + let history_info_result = t.get_history_info(1); + assert!(history_info_result.is_ok()); } } From 8f541fc8a53abf7df6c5851f4cd46ee0cc6394c3 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 08:57:07 -0800 Subject: [PATCH 54/59] Upgrade histinfo test a bit --- src/protosext/history_info.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index f2a60d134..7fa797877 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -1,6 +1,5 @@ use crate::protos::temporal::api::enums::v1::EventType; use crate::protos::temporal::api::history::v1::{History, HistoryEvent}; -use crate::protosext::history_info::HistoryInfoError::UnexpectedEventId; #[derive(Clone, Debug, derive_more::Constructor, PartialEq)] pub struct HistoryInfo { @@ -92,11 +91,13 @@ impl HistoryInfo { #[cfg(test)] mod tests { use super::*; - use crate::machines::test_help::TestHistoryBuilder; - use crate::protos::temporal::api::history::v1::{history_event, TimerFiredEventAttributes}; + use crate::{ + machines::test_help::TestHistoryBuilder, + protos::temporal::api::history::v1::{history_event, TimerFiredEventAttributes}, + }; #[test] - fn blah() { + fn history_info_constructs_properly() { /* 1: EVENT_TYPE_WORKFLOW_EXECUTION_STARTED 2: EVENT_TYPE_WORKFLOW_TASK_SCHEDULED @@ -121,7 +122,9 @@ mod tests { }), ); t.add_workflow_task_scheduled_and_started(); - let history_info_result = t.get_history_info(1); - assert!(history_info_result.is_ok()); + let history_info = t.get_history_info(1).unwrap(); + assert_eq!(3, history_info.events.len()); + let history_info = t.get_history_info(2).unwrap(); + assert_eq!(8, history_info.events.len()); } } From b2838c92865ecf02fdce7998464075d9d0bcabb9 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 09:10:06 -0800 Subject: [PATCH 55/59] Add comments etc from our meet with Max --- protos/local/core_interface.proto | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index 53b9bc58d..b939eaf81 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -36,21 +36,25 @@ message Task { message StartWorkflowTaskAttributes { string namespace = 1; - string name = 2; - temporal.api.common.v1.Payloads arguments = 3; + string workflow_id = 3; + string name = 4; + temporal.api.common.v1.Payloads arguments = 5; + // will be others - workflow exe started attrs, etc } +// maybe we just go back to timer fired to keep consistent message UnblockTimerTaskAttributes { string timer_id = 1; } +// TODO: Rename - Activation, iteration, wakeup? message WorkflowTask { google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; - string workflow_id = 2; - string run_id = 3; + string run_id = 2; oneof attributes { - StartWorkflowTaskAttributes start_workflow = 4; - UnblockTimerTaskAttributes unblock_timer = 5; + // could literally be attributes from events -- maybe we don't need our own types + StartWorkflowTaskAttributes start_workflow = 3; + UnblockTimerTaskAttributes unblock_timer = 4; } } From 8e9016c92838b8736ebeb778a1c51966d8c2ef2a Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 10:38:21 -0800 Subject: [PATCH 56/59] Woo! Test is going. Need to do some cleanup and need to pump tasks *out* to lang side --- src/lib.rs | 91 ++++++++++++---- src/machines/mod.rs | 4 +- src/machines/test_help/history_builder.rs | 21 ++-- src/machines/timer_state_machine.rs | 1 + src/machines/workflow_machines.rs | 8 +- src/protos/mod.rs | 4 + src/protosext/history_info.rs | 124 ++++++++++++++++++++-- src/protosext/mod.rs | 2 +- 8 files changed, 212 insertions(+), 43 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 59ea8e1ce..577a6cf66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,11 @@ mod machines; mod pollers; pub mod protos; mod protosext; -pub use protosext::{HistoryInfo, HistoryInfoError}; +pub use protosext::HistoryInfo; + +use crate::protos::coresdk::WorkflowTaskSuccess; +use crate::protosext::HistoryInfoError; use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ @@ -53,7 +56,7 @@ pub fn init(_opts: CoreInitOptions) -> Result> { pub struct CoreSDK { work_provider: WP, - /// Key is workflow id + /// Key is workflow id TODO: Make it run ID workflow_machines: DashMap>)>, } @@ -61,12 +64,32 @@ impl CoreSDK where WP: WorkProvider, { - fn new_workflow(&self, id: String) { + fn instantiate_workflow_if_needed(&self, id: String) { + if self.workflow_machines.contains_key(&id) { + return; + } let (wfb, cmd_sink) = WorkflowBridge::new(); let state_machines = WorkflowMachines::new(Box::new(wfb)); self.workflow_machines .insert(id, (state_machines, cmd_sink)); } + + /// Feed commands from the lang sdk into the appropriate workflow bridge + fn push_lang_commands(&self, success: WorkflowTaskSuccess) -> Result<(), CoreError> { + // Convert to wf commands + let cmds = success + .commands + .into_iter() + .map(|c| c.try_into().map_err(Into::into)) + .collect::>>()?; + // TODO: Need to use run id here + self.workflow_machines + .get_mut("fake_wf_id") + .unwrap() + .1 + .send(cmds)?; + Ok(()) + } } /// Implementors can provide new work to the SDK. The connection to the server is the real @@ -84,15 +107,30 @@ where fn poll_task(&self) -> Result { // This will block forever in the event there is no work from the server let work = self.work_provider.get_work("TODO: Real task queue")?; - match &work.workflow_execution { + let workflow_id = match &work.workflow_execution { Some(WorkflowExecution { workflow_id, .. }) => { - // TODO: Only do if it doesn't exist yet - self.new_workflow(workflow_id.to_string()); + // TODO: Use run id + self.instantiate_workflow_if_needed(workflow_id.to_string()); + workflow_id } // TODO: Appropriate error None => return Err(CoreError::Unknown), - } + }; + let history = if let Some(hist) = work.history { + hist + } else { + return Err(CoreError::BadDataFromWorkProvider(work)); + }; // TODO: Apply history to machines, get commands out, convert them to task + // TODO: How to get appropriate wf task number? + let hist_info = HistoryInfo::new_from_history(&history, None)?; + if let Some(mut machines) = self.workflow_machines.get_mut(workflow_id) { + let cmds = hist_info.apply_history_take_cmds(&mut machines.value_mut().0)?; + dbg!(cmds); + } else { + //err + } + Ok(Task { task_token: work.task_token, variant: None, @@ -105,20 +143,7 @@ where status: Some(wfstatus), })) => { match wfstatus { - Status::Successful(success) => { - // Convert to wf commands - let cmds = success - .commands - .into_iter() - .map(|c| c.try_into().map_err(Into::into)) - .collect::>>()?; - // TODO: Need to use task token or wfid etc - self.workflow_machines - .get_mut("fake_wf_id") - .unwrap() - .1 - .send(cmds)?; - } + Status::Successful(success) => self.push_lang_commands(success)?, Status::Failed(_) => {} } Ok(()) @@ -160,12 +185,18 @@ impl DrivenWorkflow for WorkflowBridge { &mut self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, Error> { + dbg!("wb start"); self.started_attrs = Some(attribs); + // TODO: Need to actually tell the workflow to... start, that's what outgoing was for lol Ok(vec![]) } fn iterate_wf(&mut self) -> Result, Error> { - Ok(self.incoming_commands.try_recv().unwrap_or_default()) + dbg!("wb iter"); + Ok(self + .incoming_commands + .try_recv() + .unwrap_or(vec![WFCommand::NoCommandsFromLang])) } fn signal(&mut self, _attribs: WorkflowExecutionSignaledEventAttributes) -> Result<(), Error> { @@ -184,17 +215,22 @@ pub enum CoreError { Unknown, #[error("No tasks to perform for now")] NoWork, + #[error("Poll response from server was malformed: {0:?}")] + BadDataFromWorkProvider(PollWorkflowTaskQueueResponse), #[error("Lang SDK sent us a malformed completion: {0:?}")] MalformedCompletion(CompleteTaskReq), #[error("Error buffering commands")] CantSendCommands(#[from] SendError>), #[error("Couldn't interpret command from ")] UninterprableCommand(#[from] InconvertibleCommandError), + #[error("Underlying error in history processing")] + UnderlyingHistError(#[from] HistoryInfoError), } #[cfg(test)] mod test { use super::*; + use crate::protos::temporal::api::command::v1::CompleteWorkflowExecutionCommandAttributes; use crate::{ machines::test_help::TestHistoryBuilder, protos::temporal::api::{ @@ -296,6 +332,7 @@ mod test { let res = dbg!(core.poll_task().unwrap()); assert!(core.workflow_machines.get(wfid).is_some()); + let task_tok = res.task_token; core.complete_task(CompleteTaskReq::ok_from_api_attrs( StartTimerCommandAttributes { @@ -306,7 +343,15 @@ mod test { task_tok, )) .unwrap(); + dbg!("sent completion w/ start timer"); - // SDK commands should be StartTimer followed by Complete WE + let res = dbg!(core.poll_task().unwrap()); + let task_tok = res.task_token; + core.complete_task(CompleteTaskReq::ok_from_api_attrs( + CompleteWorkflowExecutionCommandAttributes { result: None }.into(), + task_tok, + )) + .unwrap(); + dbg!("sent workflow done"); } } diff --git a/src/machines/mod.rs b/src/machines/mod.rs index 36596cab9..1022a1761 100644 --- a/src/machines/mod.rs +++ b/src/machines/mod.rs @@ -62,7 +62,7 @@ use std::{ use tracing::Level; // TODO: May need to be our SDKWFCommand type -type MachineCommand = Command; +pub(crate) type MachineCommand = Command; /// Implementors of this trait represent something that can (eventually) call into a workflow to /// drive it, start it, signal it, cancel it, etc. @@ -101,6 +101,8 @@ pub(crate) struct AddCommand { /// EX: Create a new timer, complete the workflow, etc. #[derive(Debug, derive_more::From)] pub enum WFCommand { + /// Returned when we need to wait for the lang sdk to send us something + NoCommandsFromLang, AddTimer(StartTimerCommandAttributes, Arc), CompleteWorkflow(CompleteWorkflowExecutionCommandAttributes), } diff --git a/src/machines/test_help/history_builder.rs b/src/machines/test_help/history_builder.rs index ba9ad993e..523ac41a6 100644 --- a/src/machines/test_help/history_builder.rs +++ b/src/machines/test_help/history_builder.rs @@ -1,13 +1,16 @@ use super::Result; -use crate::protosext::HistoryInfo; -use crate::{machines::{workflow_machines::WorkflowMachines, MachineCommand}, protos::temporal::api::{ - enums::v1::EventType, - history::v1::{ - history_event::Attributes, HistoryEvent, TimerStartedEventAttributes, - WorkflowExecutionStartedEventAttributes, WorkflowTaskCompletedEventAttributes, - WorkflowTaskScheduledEventAttributes, WorkflowTaskStartedEventAttributes, +use crate::{ + machines::{workflow_machines::WorkflowMachines, MachineCommand}, + protos::temporal::api::{ + enums::v1::EventType, + history::v1::{ + history_event::Attributes, HistoryEvent, TimerStartedEventAttributes, + WorkflowExecutionStartedEventAttributes, WorkflowTaskCompletedEventAttributes, + WorkflowTaskScheduledEventAttributes, WorkflowTaskStartedEventAttributes, + }, }, -}, HistoryInfoError}; + protosext::{HistoryInfo, HistoryInfoError}, +}; use anyhow::bail; use std::time::SystemTime; @@ -212,7 +215,7 @@ impl TestHistoryBuilder { &self, to_wf_task_num: usize, ) -> Result { - HistoryInfo::new_from_events(self.events.clone(), to_wf_task_num) + HistoryInfo::new_from_events(&self.events, Some(to_wf_task_num)) } fn build_and_push_event(&mut self, event_type: EventType, attribs: Attributes) { diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index e38d31f2d..aa250d27b 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -327,6 +327,7 @@ mod test { let commands = t .handle_workflow_task_take_cmds(&mut state_machines, Some(1)) .unwrap(); + dbg!(&commands); assert_eq!(commands.len(), 1); assert_eq!(commands[0].command_type, CommandType::StartTimer as i32); let commands = t diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index cb647bfd8..a6887073a 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -57,7 +57,7 @@ pub(crate) struct WorkflowMachines { } #[derive(thiserror::Error, Debug)] -pub(crate) enum WFMachinesError { +pub enum WFMachinesError { #[error("Event {0:?} was not expected")] UnexpectedEvent(HistoryEvent), #[error("Event {0:?} was malformed: {1}")] @@ -106,7 +106,7 @@ impl WorkflowMachines { } /// Returns the id of the last seen WorkflowTaskStarted event - pub(super) fn get_last_started_event_id(&self) -> i64 { + pub(crate) fn get_last_started_event_id(&self) -> i64 { self.current_started_event_id } @@ -120,6 +120,7 @@ impl WorkflowMachines { event: &HistoryEvent, has_next_event: bool, ) -> Result<()> { + dbg!(&event); if event.is_command_event() { self.handle_command_event(event)?; return Ok(()); @@ -311,7 +312,7 @@ impl WorkflowMachines { /// Given an event id (possibly zero) of the last successfully executed workflow task and an /// id of the last event, sets the ids internally and appropriately sets the replaying flag. - pub(super) fn set_started_ids( + pub(crate) fn set_started_ids( &mut self, previous_started_event_id: i64, workflow_task_started_event_id: i64, @@ -349,6 +350,7 @@ impl WorkflowMachines { self.current_wf_task_commands .push_back(complete_workflow(attrs)); } + WFCommand::NoCommandsFromLang => (), } } } diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 80c873d44..2916b7b4e 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -66,6 +66,10 @@ pub mod temporal { command_type: CommandType::StartTimer as i32, attributes: Some(a), }, + a @ Attributes::CompleteWorkflowExecutionCommandAttributes(_) => Self { + command_type: CommandType::CompleteWorkflowExecution as i32, + attributes: Some(a), + }, _ => unimplemented!(), } } diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index 7fa797877..5804ad822 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -1,3 +1,4 @@ +use crate::machines::{MachineCommand, WFMachinesError, WorkflowMachines}; use crate::protos::temporal::api::enums::v1::EventType; use crate::protos::temporal::api::history::v1::{History, HistoryEvent}; @@ -8,6 +9,8 @@ pub struct HistoryInfo { pub events: Vec, } +type Result = std::result::Result; + #[derive(thiserror::Error, Debug)] pub enum HistoryInfoError { #[error("Latest wf started id and previous one are equal! ${previous_started_event_id:?}")] @@ -19,12 +22,17 @@ pub enum HistoryInfoError { FailedOrTimeout(HistoryEvent), #[error("Last item in history wasn't WorkflowTaskStarted")] HistoryEndsUnexpectedly, + #[error("Underlying error in workflow machine")] + UnderlyingMachineError(#[from] WFMachinesError), } impl HistoryInfo { - pub fn new_from_events( - events: Vec, - to_wf_task_num: usize, - ) -> Result { + /// Constructs a new instance, retaining only enough events to reach the provided workflow + /// task number. If not provided, all events are retained. + pub(crate) fn new_from_events( + events: &[HistoryEvent], + to_wf_task_num: Option, + ) -> Result { + let to_wf_task_num = to_wf_task_num.unwrap_or(usize::MAX); let mut workflow_task_started_event_id = 0; let mut previous_started_event_id = 0; let mut count = 0; @@ -83,8 +91,112 @@ impl HistoryInfo { unreachable!() } - pub fn new_from_history(h: History, to_wf_task_num: usize) -> Result { - Self::new_from_events(h.events, to_wf_task_num) + pub(crate) fn new_from_history(h: &History, to_wf_task_num: Option) -> Result { + Self::new_from_events(&h.events, to_wf_task_num) + } + + /// Handle workflow task(s) using the provided [WorkflowMachines]. Will process as many workflow + /// tasks as you provided in the `to_wf_task_num` parameter to the constructor. + pub(crate) fn apply_history_take_cmds( + &self, + wf_machines: &mut WorkflowMachines, + ) -> Result> { + self.apply_history_events(wf_machines)?; + Ok(wf_machines.get_commands()) + } + + /// Apply events from history to workflow machines. Remember that only the events that exist + /// in this instance will be applied, which is determined by `to_wf_task_num` passed into the + /// constructor. + pub(crate) fn apply_history_events(&self, wf_machines: &mut WorkflowMachines) -> Result<()> { + let (_, events) = self + .events + .split_at(wf_machines.get_last_started_event_id() as usize); + let mut history = events.iter().peekable(); + + wf_machines.set_started_ids( + self.previous_started_event_id, + self.workflow_task_started_event_id, + ); + let mut started_id = self.previous_started_event_id; + + while let Some(event) = history.next() { + let next_event = history.peek(); + + if event.event_type == EventType::WorkflowTaskStarted as i32 { + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); + let next_is_failed_or_timeout = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskFailed as i32 + || ne.event_type == EventType::WorkflowTaskTimedOut as i32 + }); + + if next_event.is_none() || next_is_completed { + started_id = event.event_id; + if next_event.is_none() { + wf_machines.handle_event(event, false)?; + return Ok(()); + } + } else if next_event.is_some() && !next_is_failed_or_timeout { + return Err(HistoryInfoError::FailedOrTimeout(event.clone())); + } + } + + wf_machines.handle_event(event, next_event.is_some())?; + + if next_event.is_none() { + if event.is_final_wf_execution_event() { + return Ok(()); + } + if started_id != event.event_id { + return Err(HistoryInfoError::UnexpectedEventId { + previous_started_event_id: started_id, + workflow_task_started_event_id: event.event_id, + }); + } + unreachable!() + } + } + + Ok(()) + } + + /// Counts the number of whole workflow tasks in this history info. Looks for WFTaskStarted + /// followed by WFTaskCompleted, adding one to the count for every match. It will additionally + /// count a WFTaskStarted at the end of the event list. + /// + /// If `up_to_event_id` is provided, the count will be returned as soon as processing advances + /// past that id. + pub(crate) fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { + let mut last_wf_started_id = 0; + let mut count = 0; + let mut history = self.events.iter().peekable(); + while let Some(event) = history.next() { + let next_event = history.peek(); + if let Some(upto) = up_to_event_id { + if event.event_id > upto { + return Ok(count); + } + } + let next_is_completed = next_event.map_or(false, |ne| { + ne.event_type == EventType::WorkflowTaskCompleted as i32 + }); + if event.event_type == EventType::WorkflowTaskStarted as i32 + && (next_event.is_none() || next_is_completed) + { + last_wf_started_id = event.event_id; + count += 1; + } + + if next_event.is_none() { + if last_wf_started_id != event.event_id { + return Err(HistoryInfoError::HistoryEndsUnexpectedly); + } + return Ok(count); + } + } + Ok(count) } } diff --git a/src/protosext/mod.rs b/src/protosext/mod.rs index dd4f27a90..f55576e31 100644 --- a/src/protosext/mod.rs +++ b/src/protosext/mod.rs @@ -1,3 +1,3 @@ mod history_info; pub use history_info::HistoryInfo; -pub use history_info::HistoryInfoError; +pub(crate) use history_info::HistoryInfoError; From b24aa33df2f28490ff8cde175cfc5e3c6b5bf67b Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 12:01:21 -0800 Subject: [PATCH 57/59] :tada: The test works! The activations go out! Commands come in! Cleanup, then merge! --- Cargo.toml | 1 + build.rs | 4 ++ protos/local/core_interface.proto | 17 ++++---- src/lib.rs | 63 +++++++++++------------------ src/machines/timer_state_machine.rs | 8 ++++ src/machines/workflow_machines.rs | 34 +++++++++++++++- src/protos/mod.rs | 12 +++--- 7 files changed, 83 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7f93e8ef..c6245c47e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = "0.2" path = "fsm" [dev-dependencies] +assert_matches = "1.4" mockall = "0.9" rstest = "0.6" diff --git a/build.rs b/build.rs index 05d28921b..cb6500556 100644 --- a/build.rs +++ b/build.rs @@ -15,6 +15,10 @@ fn main() -> Result<(), Box> { "#[derive(::derive_more::From)]", ) .type_attribute("coresdk.Command.variant", "#[derive(::derive_more::From)]") + .type_attribute( + "coresdk.WFActivation.attributes", + "#[derive(::derive_more::From)]", + ) .type_attribute( "coresdk.WorkflowTask.attributes", "#[derive(::derive_more::From)]", diff --git a/protos/local/core_interface.proto b/protos/local/core_interface.proto index b939eaf81..0019c2696 100644 --- a/protos/local/core_interface.proto +++ b/protos/local/core_interface.proto @@ -29,7 +29,7 @@ message PollTaskReq { message Task { bytes task_token = 1; oneof variant { - WorkflowTask workflow = 2; + WFActivation workflow = 2; ActivityTask activity = 3; } } @@ -47,8 +47,7 @@ message UnblockTimerTaskAttributes { string timer_id = 1; } -// TODO: Rename - Activation, iteration, wakeup? -message WorkflowTask { +message WFActivation { google.protobuf.Timestamp timestamp = 1 [(gogoproto.stdtime) = true]; string run_id = 2; oneof attributes { @@ -67,15 +66,15 @@ message ActivityTask { message CompleteTaskReq { bytes task_token = 1; oneof completion { - WorkflowTaskCompletion workflow = 2; + WFActivationCompletion workflow = 2; ActivityTaskCompletion activity = 3; } } -message WorkflowTaskCompletion { +message WFActivationCompletion { oneof status { - WorkflowTaskSuccess successful = 1; - WorkflowTaskFailure failed = 2; + WFActivationSuccess successful = 1; + WFActivationFailure failed = 2; } } @@ -97,12 +96,12 @@ message Command { } } -message WorkflowTaskSuccess { +message WFActivationSuccess { repeated Command commands = 1; // Other bits from RespondWorkflowTaskCompletedRequest as needed } -message WorkflowTaskFailure { +message WFActivationFailure { temporal.api.enums.v1.WorkflowTaskFailedCause cause = 1; temporal.api.failure.v1.Failure failure = 2; // Other bits from RespondWorkflowTaskFailedRequest as needed diff --git a/src/lib.rs b/src/lib.rs index 577a6cf66..109ac926b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #[macro_use] extern crate tracing; +#[macro_use] +extern crate assert_matches; mod machines; mod pollers; @@ -8,14 +10,13 @@ mod protosext; pub use protosext::HistoryInfo; -use crate::protos::coresdk::WorkflowTaskSuccess; use crate::protosext::HistoryInfoError; use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ coresdk::{ - complete_task_req::Completion, workflow_task_completion::Status, CompleteTaskReq, Task, - WorkflowTaskCompletion, + complete_task_req::Completion, wf_activation_completion::Status, CompleteTaskReq, Task, + WfActivationCompletion, WfActivationSuccess, }, temporal::api::{ common::v1::WorkflowExecution, @@ -75,7 +76,7 @@ where } /// Feed commands from the lang sdk into the appropriate workflow bridge - fn push_lang_commands(&self, success: WorkflowTaskSuccess) -> Result<(), CoreError> { + fn push_lang_commands(&self, success: WfActivationSuccess) -> Result<(), CoreError> { // Convert to wf commands let cmds = success .commands @@ -121,25 +122,27 @@ where } else { return Err(CoreError::BadDataFromWorkProvider(work)); }; - // TODO: Apply history to machines, get commands out, convert them to task - // TODO: How to get appropriate wf task number? + // We pass none since we want to apply all the history we just got. + // Will need to change a bit once we impl caching. let hist_info = HistoryInfo::new_from_history(&history, None)?; - if let Some(mut machines) = self.workflow_machines.get_mut(workflow_id) { - let cmds = hist_info.apply_history_take_cmds(&mut machines.value_mut().0)?; - dbg!(cmds); + let activation = if let Some(mut machines) = self.workflow_machines.get_mut(workflow_id) { + // TODO -- Here, we want to get workflow activations back out + hist_info.apply_history_events(&mut machines.value_mut().0)?; + machines.0.get_wf_activation() } else { //err - } + unimplemented!() + }; Ok(Task { task_token: work.task_token, - variant: None, + variant: activation.map(Into::into), }) } fn complete_task(&self, req: CompleteTaskReq) -> Result<(), CoreError> { match req.completion { - Some(Completion::Workflow(WorkflowTaskCompletion { + Some(Completion::Workflow(WfActivationCompletion { status: Some(wfstatus), })) => { match wfstatus { @@ -153,6 +156,7 @@ where } _ => Err(CoreError::MalformedCompletion(req)), } + // TODO: Get fsm commands and send them to server } } @@ -196,7 +200,7 @@ impl DrivenWorkflow for WorkflowBridge { Ok(self .incoming_commands .try_recv() - .unwrap_or(vec![WFCommand::NoCommandsFromLang])) + .unwrap_or_else(|_| vec![WFCommand::NoCommandsFromLang])) } fn signal(&mut self, _attribs: WorkflowExecutionSignaledEventAttributes) -> Result<(), Error> { @@ -230,6 +234,7 @@ pub enum CoreError { #[cfg(test)] mod test { use super::*; + use crate::protos::coresdk::{task, wf_activation, WfActivation}; use crate::protos::temporal::api::command::v1::CompleteWorkflowExecutionCommandAttributes; use crate::{ machines::test_help::TestHistoryBuilder, @@ -304,33 +309,10 @@ mod test { work_provider: mock_provider, }; - // TODO: These are what poll_task should end up returning - // SdkwfTask { - // timestamp: None, - // workflow_id: wfid.to_string(), - // task: Some( - // StartWorkflowTaskAttributes { - // namespace: "namespace".to_string(), - // name: "wf name?".to_string(), - // arguments: None, - // } - // .into(), - // ), - // } - // .into(), - // SdkwfTask { - // timestamp: None, - // workflow_id: wfid.to_string(), - // task: Some( - // UnblockTimerTaskAttibutes { - // timer_id: timer_id.to_string(), - // } - // .into(), - // ), - // } - // .into(), - let res = dbg!(core.poll_task().unwrap()); + // TODO: uggo + assert_matches!(res, Task {variant: Some(task::Variant::Workflow(WfActivation { + attributes: Some(wf_activation::Attributes::StartWorkflow(_)), ..})), ..}); assert!(core.workflow_machines.get(wfid).is_some()); let task_tok = res.task_token; @@ -346,6 +328,9 @@ mod test { dbg!("sent completion w/ start timer"); let res = dbg!(core.poll_task().unwrap()); + // TODO: uggo + assert_matches!(res, Task {variant: Some(task::Variant::Workflow(WfActivation { + attributes: Some(wf_activation::Attributes::UnblockTimer(_)), ..})), ..}); let task_tok = res.task_token; core.complete_task(CompleteTaskReq::ok_from_api_attrs( CompleteWorkflowExecutionCommandAttributes { result: None }.into(), diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index aa250d27b..198393c07 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -1,5 +1,6 @@ #![allow(clippy::large_enum_variant)] +use crate::protos::coresdk::{wf_activation, UnblockTimerTaskAttributes, WfActivation}; use crate::{ machines::{ workflow_machines::WFMachinesError, workflow_machines::WorkflowMachines, AddCommand, @@ -228,12 +229,19 @@ impl WFMachinesAdapter for TimerMachine { } // Fire the completion TimerMachineCommand::Complete(_event) => { + // TODO: Remove atomic bool nonsense -- kept for now to keep test here passing if let Some(a) = wf_machines .timer_notifiers .remove(&self.shared_state.timer_attributes.timer_id) { a.store(true, Ordering::SeqCst) }; + wf_machines.outgoing_wf_actications.push_back( + UnblockTimerTaskAttributes { + timer_id: self.shared_state.timer_attributes.timer_id.clone(), + } + .into(), + ); } } Ok(()) diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index a6887073a..f1944315f 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -1,3 +1,4 @@ +use crate::protos::coresdk::{wf_activation, StartWorkflowTaskAttributes, WfActivation}; use crate::{ machines::{ complete_workflow_state_machine::complete_workflow, timer_state_machine::new_timer, @@ -45,9 +46,13 @@ pub(crate) struct WorkflowMachines { /// Queued commands which have been produced by machines and await processing commands: VecDeque, - /// Commands generated by the currently processed workflow task. It is a queue as commands can - /// be added (due to marker based commands) while iterating over already added commands. + /// Commands generated by the currently processing workflow task. + /// + /// Old note: It is a queue as commands can be added (due to marker based commands) while + /// iterating over already added commands. current_wf_task_commands: VecDeque, + /// Outgoing activations that need to be sent to the lang sdk + pub(super) outgoing_wf_actications: VecDeque, /// The workflow that is being driven by this instance of the machines drive_me: Box, @@ -87,6 +92,7 @@ impl WorkflowMachines { commands: Default::default(), current_wf_task_commands: Default::default(), timer_notifiers: Default::default(), + outgoing_wf_actications: Default::default(), } } @@ -269,6 +275,16 @@ impl WorkflowMachines { )) = &event.attributes { self.current_run_id = Some(attrs.original_execution_run_id.clone()); + // TODO: Actual values -- not entirely sure this is the right spot + self.outgoing_wf_actications.push_back( + StartWorkflowTaskAttributes { + namespace: "".to_string(), + workflow_id: "".to_string(), + name: "".to_string(), + arguments: None, + } + .into(), + ); let results = self.drive_me.start(attrs.clone())?; self.handle_driven_results(results); } else { @@ -310,6 +326,20 @@ impl WorkflowMachines { .collect() } + /// Returns the next activation that needs to be performed by the lang sdk. Things like unblock + /// timer, etc. + pub(crate) fn get_wf_activation(&mut self) -> Option { + self.outgoing_wf_actications + .pop_front() + .map(|attrs| WfActivation { + // todo wat ? + timestamp: None, + // TODO: fix unwrap + run_id: self.current_run_id.clone().unwrap(), + attributes: attrs.into(), + }) + } + /// Given an event id (possibly zero) of the last successfully executed workflow task and an /// id of the last event, sets the ids internally and appropriately sets the replaying flag. pub(crate) fn set_started_ids( diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 2916b7b4e..f7e1231e5 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -9,7 +9,7 @@ pub mod coresdk { pub type HistoryEventId = i64; impl Task { - pub fn from_wf_task(task_token: Vec, t: WorkflowTask) -> Self { + pub fn from_wf_task(task_token: Vec, t: WfActivation) -> Self { Task { task_token, variant: Some(t.into()), @@ -17,9 +17,9 @@ pub mod coresdk { } } - impl From> for WorkflowTaskSuccess { + impl From> for WfActivationSuccess { fn from(v: Vec) -> Self { - WorkflowTaskSuccess { + WfActivationSuccess { commands: v .into_iter() .map(|cmd| Command { @@ -37,11 +37,11 @@ pub mod coresdk { task_token: Vec, ) -> Self { let cmd: ApiCommand = cmd.into(); - let success: WorkflowTaskSuccess = vec![cmd].into(); + let success: WfActivationSuccess = vec![cmd].into(); CompleteTaskReq { task_token, - completion: Some(Completion::Workflow(WorkflowTaskCompletion { - status: Some(workflow_task_completion::Status::Successful(success)), + completion: Some(Completion::Workflow(WfActivationCompletion { + status: Some(wf_activation_completion::Status::Successful(success)), })), } } From 51f75d9a86d8fe46cd7d5690a36124a4a0080632 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 13:25:11 -0800 Subject: [PATCH 58/59] Just a smidge of cleanup --- src/lib.rs | 38 ++++++++++++++++++------ src/machines/workflow_machines.rs | 1 - src/protosext/history_info.rs | 49 +------------------------------ 3 files changed, 30 insertions(+), 58 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 109ac926b..c1f8378f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,7 @@ impl Core for CoreSDK where WP: WorkProvider, { + #[instrument(skip(self))] fn poll_task(&self) -> Result { // This will block forever in the event there is no work from the server let work = self.work_provider.get_work("TODO: Real task queue")?; @@ -126,7 +127,6 @@ where // Will need to change a bit once we impl caching. let hist_info = HistoryInfo::new_from_history(&history, None)?; let activation = if let Some(mut machines) = self.workflow_machines.get_mut(workflow_id) { - // TODO -- Here, we want to get workflow activations back out hist_info.apply_history_events(&mut machines.value_mut().0)?; machines.0.get_wf_activation() } else { @@ -140,6 +140,7 @@ where }) } + #[instrument(skip(self))] fn complete_task(&self, req: CompleteTaskReq) -> Result<(), CoreError> { match req.completion { Some(Completion::Workflow(WfActivationCompletion { @@ -185,18 +186,18 @@ impl WorkflowBridge { } impl DrivenWorkflow for WorkflowBridge { + #[instrument] fn start( &mut self, attribs: WorkflowExecutionStartedEventAttributes, ) -> Result, Error> { - dbg!("wb start"); self.started_attrs = Some(attribs); // TODO: Need to actually tell the workflow to... start, that's what outgoing was for lol Ok(vec![]) } + #[instrument] fn iterate_wf(&mut self) -> Result, Error> { - dbg!("wb iter"); Ok(self .incoming_commands .try_recv() @@ -234,20 +235,39 @@ pub enum CoreError { #[cfg(test)] mod test { use super::*; - use crate::protos::coresdk::{task, wf_activation, WfActivation}; - use crate::protos::temporal::api::command::v1::CompleteWorkflowExecutionCommandAttributes; use crate::{ machines::test_help::TestHistoryBuilder, - protos::temporal::api::{ - command::v1::StartTimerCommandAttributes, - enums::v1::EventType, - history::v1::{history_event, History, TimerFiredEventAttributes}, + protos::{ + coresdk::{task, wf_activation, WfActivation}, + temporal::api::{ + command::v1::{ + CompleteWorkflowExecutionCommandAttributes, StartTimerCommandAttributes, + }, + enums::v1::EventType, + history::v1::{history_event, History, TimerFiredEventAttributes}, + }, }, }; use std::collections::VecDeque; + use tracing::Level; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; #[test] fn workflow_bridge() { + let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() + .with_service_name("report_example") + .install() + .unwrap(); + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init() + .unwrap(); + + let s = span!(Level::DEBUG, "Test start"); + let _enter = s.enter(); + let wfid = "fake_wf_id"; let timer_id = "fake_timer"; diff --git a/src/machines/workflow_machines.rs b/src/machines/workflow_machines.rs index f1944315f..81aaf12e6 100644 --- a/src/machines/workflow_machines.rs +++ b/src/machines/workflow_machines.rs @@ -126,7 +126,6 @@ impl WorkflowMachines { event: &HistoryEvent, has_next_event: bool, ) -> Result<()> { - dbg!(&event); if event.is_command_event() { self.handle_command_event(event)?; return Ok(()); diff --git a/src/protosext/history_info.rs b/src/protosext/history_info.rs index 5804ad822..9db67a641 100644 --- a/src/protosext/history_info.rs +++ b/src/protosext/history_info.rs @@ -1,4 +1,4 @@ -use crate::machines::{MachineCommand, WFMachinesError, WorkflowMachines}; +use crate::machines::{WFMachinesError, WorkflowMachines}; use crate::protos::temporal::api::enums::v1::EventType; use crate::protos::temporal::api::history::v1::{History, HistoryEvent}; @@ -95,16 +95,6 @@ impl HistoryInfo { Self::new_from_events(&h.events, to_wf_task_num) } - /// Handle workflow task(s) using the provided [WorkflowMachines]. Will process as many workflow - /// tasks as you provided in the `to_wf_task_num` parameter to the constructor. - pub(crate) fn apply_history_take_cmds( - &self, - wf_machines: &mut WorkflowMachines, - ) -> Result> { - self.apply_history_events(wf_machines)?; - Ok(wf_machines.get_commands()) - } - /// Apply events from history to workflow machines. Remember that only the events that exist /// in this instance will be applied, which is determined by `to_wf_task_num` passed into the /// constructor. @@ -161,43 +151,6 @@ impl HistoryInfo { Ok(()) } - - /// Counts the number of whole workflow tasks in this history info. Looks for WFTaskStarted - /// followed by WFTaskCompleted, adding one to the count for every match. It will additionally - /// count a WFTaskStarted at the end of the event list. - /// - /// If `up_to_event_id` is provided, the count will be returned as soon as processing advances - /// past that id. - pub(crate) fn get_workflow_task_count(&self, up_to_event_id: Option) -> Result { - let mut last_wf_started_id = 0; - let mut count = 0; - let mut history = self.events.iter().peekable(); - while let Some(event) = history.next() { - let next_event = history.peek(); - if let Some(upto) = up_to_event_id { - if event.event_id > upto { - return Ok(count); - } - } - let next_is_completed = next_event.map_or(false, |ne| { - ne.event_type == EventType::WorkflowTaskCompleted as i32 - }); - if event.event_type == EventType::WorkflowTaskStarted as i32 - && (next_event.is_none() || next_is_completed) - { - last_wf_started_id = event.event_id; - count += 1; - } - - if next_event.is_none() { - if last_wf_started_id != event.event_id { - return Err(HistoryInfoError::HistoryEndsUnexpectedly); - } - return Ok(count); - } - } - Ok(count) - } } #[cfg(test)] From e6a24363cd89e505bc0eb77ac6cbd795d6e2aa88 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 29 Jan 2021 16:14:15 -0800 Subject: [PATCH 59/59] Use run id instead of wfid --- src/lib.rs | 177 ++++++++++++++++------------ src/machines/timer_state_machine.rs | 10 -- 2 files changed, 104 insertions(+), 83 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c1f8378f7..1c3519274 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ #[macro_use] extern crate tracing; +#[cfg(test)] #[macro_use] extern crate assert_matches; @@ -10,7 +11,6 @@ mod protosext; pub use protosext::HistoryInfo; -use crate::protosext::HistoryInfoError; use crate::{ machines::{DrivenWorkflow, InconvertibleCommandError, WFCommand, WorkflowMachines}, protos::{ @@ -27,6 +27,7 @@ use crate::{ workflowservice::v1::PollWorkflowTaskQueueResponse, }, }, + protosext::HistoryInfoError, }; use anyhow::Error; use dashmap::DashMap; @@ -56,49 +57,12 @@ pub fn init(_opts: CoreInitOptions) -> Result> { } pub struct CoreSDK { + /// Provides work in the form of responses the server would send from polling task Qs work_provider: WP, - /// Key is workflow id TODO: Make it run ID + /// Key is run id workflow_machines: DashMap>)>, -} - -impl CoreSDK -where - WP: WorkProvider, -{ - fn instantiate_workflow_if_needed(&self, id: String) { - if self.workflow_machines.contains_key(&id) { - return; - } - let (wfb, cmd_sink) = WorkflowBridge::new(); - let state_machines = WorkflowMachines::new(Box::new(wfb)); - self.workflow_machines - .insert(id, (state_machines, cmd_sink)); - } - - /// Feed commands from the lang sdk into the appropriate workflow bridge - fn push_lang_commands(&self, success: WfActivationSuccess) -> Result<(), CoreError> { - // Convert to wf commands - let cmds = success - .commands - .into_iter() - .map(|c| c.try_into().map_err(Into::into)) - .collect::>>()?; - // TODO: Need to use run id here - self.workflow_machines - .get_mut("fake_wf_id") - .unwrap() - .1 - .send(cmds)?; - Ok(()) - } -} - -/// Implementors can provide new work to the SDK. The connection to the server is the real -/// implementor. -#[cfg_attr(test, mockall::automock)] -pub trait WorkProvider { - /// Fetch new work. Should block indefinitely if there is no work. - fn get_work(&self, task_queue: &str) -> Result; + /// Maps task tokens to workflow run ids + workflow_task_tokens: DashMap, String>, } impl Core for CoreSDK @@ -109,11 +73,10 @@ where fn poll_task(&self) -> Result { // This will block forever in the event there is no work from the server let work = self.work_provider.get_work("TODO: Real task queue")?; - let workflow_id = match &work.workflow_execution { - Some(WorkflowExecution { workflow_id, .. }) => { - // TODO: Use run id - self.instantiate_workflow_if_needed(workflow_id.to_string()); - workflow_id + let run_id = match &work.workflow_execution { + Some(WorkflowExecution { run_id, .. }) => { + self.instantiate_workflow_if_needed(run_id.to_string()); + run_id } // TODO: Appropriate error None => return Err(CoreError::Unknown), @@ -123,10 +86,15 @@ where } else { return Err(CoreError::BadDataFromWorkProvider(work)); }; + + // Correlate task token w/ run ID + self.workflow_task_tokens + .insert(work.task_token.clone(), run_id.clone()); + // We pass none since we want to apply all the history we just got. // Will need to change a bit once we impl caching. let hist_info = HistoryInfo::new_from_history(&history, None)?; - let activation = if let Some(mut machines) = self.workflow_machines.get_mut(workflow_id) { + let activation = if let Some(mut machines) = self.workflow_machines.get_mut(run_id) { hist_info.apply_history_events(&mut machines.value_mut().0)?; machines.0.get_wf_activation() } else { @@ -142,17 +110,29 @@ where #[instrument(skip(self))] fn complete_task(&self, req: CompleteTaskReq) -> Result<(), CoreError> { - match req.completion { - Some(Completion::Workflow(WfActivationCompletion { - status: Some(wfstatus), - })) => { + match req { + CompleteTaskReq { + task_token, + completion: + Some(Completion::Workflow(WfActivationCompletion { + status: Some(wfstatus), + })), + } => { + let wf_run_id = self + .workflow_task_tokens + .get(&task_token) + .map(|x| x.value().clone()) + .ok_or(CoreError::NothingFoundForTaskToken(task_token))?; match wfstatus { - Status::Successful(success) => self.push_lang_commands(success)?, + Status::Successful(success) => self.push_lang_commands(&wf_run_id, success)?, Status::Failed(_) => {} } Ok(()) } - Some(Completion::Activity(_)) => { + CompleteTaskReq { + completion: Some(Completion::Activity(_)), + .. + } => { unimplemented!() } _ => Err(CoreError::MalformedCompletion(req)), @@ -161,6 +141,49 @@ where } } +impl CoreSDK +where + WP: WorkProvider, +{ + fn instantiate_workflow_if_needed(&self, run_id: String) { + if self.workflow_machines.contains_key(&run_id) { + return; + } + let (wfb, cmd_sink) = WorkflowBridge::new(); + let state_machines = WorkflowMachines::new(Box::new(wfb)); + self.workflow_machines + .insert(run_id, (state_machines, cmd_sink)); + } + + /// Feed commands from the lang sdk into the appropriate workflow bridge + fn push_lang_commands( + &self, + run_id: &str, + success: WfActivationSuccess, + ) -> Result<(), CoreError> { + // Convert to wf commands + let cmds = success + .commands + .into_iter() + .map(|c| c.try_into().map_err(Into::into)) + .collect::>>()?; + self.workflow_machines + .get_mut(run_id) + .unwrap() + .1 + .send(cmds)?; + Ok(()) + } +} + +/// Implementors can provide new work to the SDK. The connection to the server is the real +/// implementor. +#[cfg_attr(test, mockall::automock)] +pub trait WorkProvider { + /// Fetch new work. Should block indefinitely if there is no work. + fn get_work(&self, task_queue: &str) -> Result; +} + /// The [DrivenWorkflow] trait expects to be called to make progress, but the [CoreSDKService] /// expects to be polled by the lang sdk. This struct acts as the bridge between the two, buffering /// output from calls to [DrivenWorkflow] and offering them to [CoreSDKService] @@ -230,6 +253,8 @@ pub enum CoreError { UninterprableCommand(#[from] InconvertibleCommandError), #[error("Underlying error in history processing")] UnderlyingHistError(#[from] HistoryInfoError), + #[error("Task token had nothing associated with it: {0:?}")] + NothingFoundForTaskToken(Vec), } #[cfg(test)] @@ -250,25 +275,14 @@ mod test { }; use std::collections::VecDeque; use tracing::Level; - use tracing_subscriber::layer::SubscriberExt; - use tracing_subscriber::util::SubscriberInitExt; #[test] fn workflow_bridge() { - let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() - .with_service_name("report_example") - .install() - .unwrap(); - let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); - tracing_subscriber::registry() - .with(opentelemetry) - .try_init() - .unwrap(); - let s = span!(Level::DEBUG, "Test start"); let _enter = s.enter(); let wfid = "fake_wf_id"; + let run_id = "fake_run_id"; let timer_id = "fake_timer"; let mut t = TestHistoryBuilder::default(); @@ -299,7 +313,7 @@ mod test { let events_first_batch = t.get_history_info(1).unwrap().events; let wf = Some(WorkflowExecution { workflow_id: wfid.to_string(), - run_id: "".to_string(), + run_id: run_id.to_string(), }); let first_response = PollWorkflowTaskQueueResponse { history: Some(History { @@ -325,15 +339,24 @@ mod test { .returning(move |_| Ok(tasks.pop_front().unwrap())); let core = CoreSDK { - workflow_machines: DashMap::new(), work_provider: mock_provider, + workflow_machines: DashMap::new(), + workflow_task_tokens: DashMap::new(), }; let res = dbg!(core.poll_task().unwrap()); // TODO: uggo - assert_matches!(res, Task {variant: Some(task::Variant::Workflow(WfActivation { - attributes: Some(wf_activation::Attributes::StartWorkflow(_)), ..})), ..}); - assert!(core.workflow_machines.get(wfid).is_some()); + assert_matches!( + res, + Task { + variant: Some(task::Variant::Workflow(WfActivation { + attributes: Some(wf_activation::Attributes::StartWorkflow(_)), + .. + })), + .. + } + ); + assert!(core.workflow_machines.get(run_id).is_some()); let task_tok = res.task_token; core.complete_task(CompleteTaskReq::ok_from_api_attrs( @@ -349,8 +372,16 @@ mod test { let res = dbg!(core.poll_task().unwrap()); // TODO: uggo - assert_matches!(res, Task {variant: Some(task::Variant::Workflow(WfActivation { - attributes: Some(wf_activation::Attributes::UnblockTimer(_)), ..})), ..}); + assert_matches!( + res, + Task { + variant: Some(task::Variant::Workflow(WfActivation { + attributes: Some(wf_activation::Attributes::UnblockTimer(_)), + .. + })), + .. + } + ); let task_tok = res.task_token; core.complete_task(CompleteTaskReq::ok_from_api_attrs( CompleteWorkflowExecutionCommandAttributes { result: None }.into(), diff --git a/src/machines/timer_state_machine.rs b/src/machines/timer_state_machine.rs index 198393c07..ffa73c862 100644 --- a/src/machines/timer_state_machine.rs +++ b/src/machines/timer_state_machine.rs @@ -350,16 +350,6 @@ mod test { #[rstest] fn test_fire_happy_path_full(fire_happy_hist: (TestHistoryBuilder, WorkflowMachines)) { - let (tracer, _uninstall) = opentelemetry_jaeger::new_pipeline() - .with_service_name("report_example") - .install() - .unwrap(); - let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); - tracing_subscriber::registry() - .with(opentelemetry) - .try_init() - .unwrap(); - let s = span!(Level::DEBUG, "Test start", t = "full"); let _enter = s.enter();