Index: openacs-4/packages/workflow/www/doc/developer-guide.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/developer-guide.html,v diff -u -r1.2 -r1.3 --- openacs-4/packages/workflow/www/doc/developer-guide.html 28 Aug 2003 09:41:59 -0000 1.2 +++ openacs-4/packages/workflow/www/doc/developer-guide.html 20 Nov 2003 12:52:49 -0000 1.3 @@ -40,7 +40,7 @@
+ By Lars Pind +
+ ++ This requirements and design document is primarily motivated by: +
+ ++ Use cases: +
+ ++ The timer will always be of the form "This action will automatically + execute x amount of time after it becomes enabled". If it is later + un-enabled (disabled) because another action (e.g. a vote action in + the second use casae above) was executed, then the timer will be + reset. If the action later becomes enabled, the timer will start + anew. +
+ ++ We currently do not have any information on which actions are + enabled, and when they're enabled. We will probably need a table, + perhaps one just for timed actions, in which a row is created when a + timed action is enabled, and the row is deleted again when the state + changes. +
+ ++create table workflow_actions( + ... + -- The number of seconds after having become enabled the action + -- will automatically execute + timeout interval + ... +); ++ +
+ DESIGN NOTE: The 'interval' datatype is not supported in + Oracle. +
+ ++create table workflow_case_enabled_actions( + case_id integer + constraint wf_case_enbl_act_case_id_nn + not null + constraint wf_case_enbl_act_case_id_fk + references workflow_cases(case_id) + on delete cascade, + action_id integer + constraint wf_case_enbl_act_action_id_nn + not null + constraint wf_case_enbl_act_action_id_fk + references workflow_actions(action_id) + on delete cascade, + -- the timestamp when this action will fires + execution_time timestamptz + constraint wf_case_enbl_act_timeout_nn + not null, + constraint workflow_case_enabled_actions_pk + primary key (case_id, action_id) +); ++ +
+ After executing an action, workflow::case::action::execute
will:
+
worklfow_case_enabled_actions
which are no longer enabled.
+ workflow_case_enabled_actions
, with
+ fire_timestamp = current_timestamp + workflow_actions.timeout_seconds
.
+ + NOTE: We need to keep running, so if another automatic action + becomes enabled after this action fires, they'll fire as well. +
+ +
+ The sweeper will find rows in
+ workflow_case_enabled_actions
with fire_timetsamp
+ < current_timestamp
, ordered by fire_timstamp, and execute
+ them.
+
+ It should do a query to find the action to fire first, then release + the db-handle and execute it. Then do a fresh query to find the + next, etc. That way we will handle the situation correctly where the + first action firing causes the second action to no longer be + enabled. +
+ ++ Every time the sweeper runs, at least one DB query will be made, + even if there are no timed actions to be executed. +
+ ++ Possible optimizations: +
+ ++ Use cases: +
+ ++create table workflow_actions( + ... + child_workflow integer + constraint wf_action_child_wf_fk + references workflows(workflow_id), + ... +); + +create table workflow_fsm_states( + ... + -- does this state imply that the case is completed? + complete_p boolean, + ... +); + +create table workflow_action_fsm_output_map( + action_id integer + not null + references workflow_actions(action_id) + on delete cascade, + acs_sc_impl_id integer + not null + references acs_sc_impls(impl_id) + on delete cascade, + output_value varchar(4000), + new_state integer + references workflow_fsm_states +); + +create table workflow_action_child_role_map( + parent_action_id integer + constraint wf_act_chid_rl_map_prnt_act_fk + references workflow_actions(action_id), + parent_role integer + constraint wf_act_chid_rl_map_prnt_rl_fk + references workflow_roles(role_id), + child_role integer + constraint wf_act_chid_rl_map_chld_rl_fk + references workflow_roles(role_id), + mapping_type char(40) + constraint wf_act_chid_rl_map_type_ck + check (mapping_type in + ('per_role','per_member','per_user')) +); + + +create table workflow_cases( + ... + state char(40) + constraint workflow_cases_state_ck + check (state in ('active', 'completed', + 'closed', 'canceled', 'suspended')) + default 'active', + suspended_until timestamptz, + parent_enabled_action_id integer + constraint workflow_cases_parent_fk + references workflow_case_enabled_actions(enabled_action_id) +); + +create table workflow_case_enabled_actions( + enabled_action_id integer + constraint wf_case_enbl_act_case_id_pk + primary key, + case_id integer + constraint wf_case_enbl_act_case_id_nn + not null + constraint wf_case_enbl_act_case_id_fk + references workflow_cases(case_id) + on delete cascade, + action_id integer + constraint wf_case_enbl_act_action_id_nn + not null + constraint wf_case_enbl_act_action_id_fk + references workflow_actions(action_id) + on delete cascade, + enabled_state char(40) + constraint wf_case_enbl_act_state_ck + check (enabled_state in ('enabled','completed','canceled','refused')), + -- the timestamp when this action automatically fires + fire_timestamp timestamp + constraint wf_case_enbl_act_timeout_nn + not null, + constraint wf_case_ena_act_case_act_un + primary key (case_id, action_id) +); + ++ +
enabled_state
of the row in
+ workflow_case_enabled_actions
will be set to
+ 'refused'.
+ + The callbacks returning 'output' above must enumerate all the values + they can possible output (similar contruct to GetObjectType + operation on other current workflow service contracts), and the + callback itself must return one of those possible values. +
+ ++ The workflow engine will then allow the workflow designer to map + these possible output values of the callback to new states, in the + case of an FSM, or similar relevant state changes for other models. +
+ ++ Executed when an action which was previously not enabled becomes enabled. +
+ ++ Executed when an action which was previously enabled is no longer + enabled, because the workflow's state was changed by some other + action. +
+ ++ We execute the OnChildCaseStateChange callback, if any. This gets to + determine whether the parent action is now complete and should fire. +
+ ++ We provide a default implementation, which simply checks if the + child cases are in the 'complete' state, and if so, fires. +
+ ++ NOTE: What do we do if any of the child cases are canceled? Consider + the complete and move on with the parent workflow? Cancel the parent + workflow? +
+ ++ NOTE: Should we provide this as internal workflow logic or as a + default callback implementation? If we leave this as a service + contract with a default implementation, then applications can + customize. But would that ever be relevant? Maybe this callback is + never needed. +
+ ++ When the action finally fires. +
+ ++ If there's any OnFire callback defined, we execute this. +
+ +
+ If the callback has output values defined, we use the mappings in
+ workflow_action_fsm_output_map
to determine which state to
+ move to.
+
+ After firing, we execute the SideEffect callbacks and send off + notifications. +
+ ++ DESIGN QUESTION: How do we handle notifications for child cases? We + should consider the child case part of the parent in terms of + notifications, so when a child action executes, we notify those who + have requested notifications on the parent. And when the last child + case completes, which will also complete the parent action, we + should avoid sending out duplicate notifications. How? +
+ ++ Cases can be active, complete, suspended, or canceled. +
+ +
+ They start out as active. For FSMs, when they hit a state with
+ complete_p = t
, the case is moved to 'complete'.
+
+ Users can choose to cancel or suspend a case. When suspending, they + can type in a date, on which the case will spring back to 'active' + life. +
+ ++ When a parent worfklow completes an action with a sub-workflow, the + child cases that are 'completed' are marked 'closed', and the child + cases that are 'active' are marked 'canceled'. +
+ ++ The difference between 'completed' and 'closed' is that completed + does not prevent the workflow from continuing (e.g. bug-tracker + 'closed' state doesn't mean that it cannot be reopened), whereas a + closed case cannot be reactivarted (terminology confusion alert!). +
+ + ++ An action does not become avilable until a given list of other + actions have completed. The advanced version is that you can also + specify for each of these other tasks how many times they must've + been executed. +
+ ++create table workflow_action_dependencies( + action_id integer + constraint wf_action_dep_action_fk + references workflow_actions(action_id), + dependent_on_action integer + constraint wf_action_dep_dep_action_fk + references workflow_actions(action_id), + min_n integer default 1, + max_n integer, + constraint wf_action_dep_act_dep_pk + primary key (action_id, dependent_on_action) +); ++ +
+ When an action is about to be enabled, and before calling the + CanEnableP callback, we check the workflow_case_enabled_actions table + to see that the required actions have the required number of rows in + the workflow_case_enabled_actions table with enabled_state + 'completed'. +
+ + + ++ An action can at most be executed a certain number of times. +
+ ++create table workflow_actions( + ... + max_n integer + ... +); ++ +
+ When an action is about to be enabled, and before calling the + CanEnableP callback, we check the workflow_case_enabled_actions table + to see how many rows exist with enabled_state 'completed'. +
+ + + ++ The bug-tracker has resolution codes under the "Resolve" action. It + would be useful if these could be customized. +
+ ++ In addition, I saw one other dynamic-workflow product (TrackStudio) + on the web, and they have the concept of resolution codes + included. That made me realize that this is generally useful. +
+ ++ In general, a resolution code is a way of distinguishing different + states, even though those states are identical in terms of the + workflow process. +
+ ++ Currently, the code to make these happen is fairly clumsy, what with + the "FormatLogTitle" callback which we invented. +
+ ++create sequence ... + +create table workflow_action_resolutions( + resolution_id integer + constraint wf_act_res_pk + primary key, + action_id integer + constraint wf_act_res_action_fk + references workflow_actions(action_id) + on delete cascade, + sort_order integer + constraint wf_act_res_sort_order_nn + not null, + short_name varchar(100) + constraint wf_act_res_short_name_nn + not null, + pretty_name varchar(200) + constraint wf_act_res_pretty_name_nn + not null +); + +create index workflow_act_res_act_idx on workflow_action_resolutions(action_id); + +create table workflow_action_res_output_map( + action_id integer + not null + references workflow_actions(action_id) + on delete cascade, + acs_sc_impl_id integer + not null + references acs_sc_impls(impl_id) + on delete cascade, + output_value varchar(4000), + resolution_id integer + not null + references workflow_action_resolutions(resolution_id) + on delete cascade, +); + +-- FK index on action_id +-- FK index on acs_sc_impl_id +-- FK index on resolution ++ + + + + +
+TIP Master Workflow + Model = FSM + Roles + Submitter + Voter + States + Proposed + Voting + Withdrawn + Approved + Rejected + Actions + Propose + Initial action + New state = Proposed + Vote + Enabled in state = Proposed + Role = Voter + Sub-workflow = Individual Vote + In progress state = Voting + Sub-role Voter = pparent role Voter + One sub-case per user in the Voter role + New state = Approved | Rejected + Logic = + 0 Rejects + > 0 Approvals = Approved + 2/3rds Approvals => Approved + Otherwise => Rejected + Withdraw + Enabled in state = Proposed, Voting + Role = Submitter + New state = Withdrawn + +TIP Individual Vote Workflow + Model = FSM + Roles + Voter + States + Open + Approved + Rejected + Abstained + Actions + Open + Initial action + New state = Open + Approve + Enabled in state = Open + Role = Voter + New state = Approved + Reject + Enabled in state = Open + Role = Voter + New state = Rejected + Abstain + Enabled in state = Open + Role = Voter + New state = Abstained + No Vote + Enabled in state = Open + Timeout = 7 days + New state = Abstained ++ + +