Index: openacs-4/packages/workflow/www/doc/fall-2003-extensions.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/fall-2003-extensions.html,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/www/doc/fall-2003-extensions.html 20 Nov 2003 12:52:49 -0000 1.1 +++ openacs-4/packages/workflow/www/doc/fall-2003-extensions.html 9 Dec 2003 09:59:45 -0000 1.2 @@ -32,156 +32,6 @@ -
- 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: -
- --create table workflow_actions( - ... +create table workflow_action_children( + child_id integer + constraint ... + primary key, + action_id integer + constraint ... + not null + constraint ... + references workflow_actions(action_id) + on delete cascade, child_workflow integer constraint wf_action_child_wf_fk - references workflows(workflow_id), - ... + 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 @@ -289,154 +126,139 @@ ('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) + 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 + enabled_action_id integer constraint wf_case_enbl_act_case_id_pk primary key, - case_id integer + 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 + 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) + 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 + check (enabled_state in ('enabled','running','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) + 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 enabled_state of rows in workflow_case_enabled_actions can be in one of the following:
-- 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. + When an action with child workflows is enabled, we start the child + cases defined by the parent workflow, executing the initial action + on each of them.
-+ We create one case per role in workflow_action_children times one + case per member/user for roles with a mapping_type of + 'per_member'/'per_user'. If more than one role has a mapping_type other + than 'per_role', we will create cases for the cartesian product of + members/users of those roles in the parent workflow. +
-+ The action can be triggered by a timeout, by the user, by child + cases reaching a certain state, or by all child cases being + completed. +
+ ++ An example of "child cases reaching a certain state" would be the + TIP voting process, where 2/3rd Approved votes is enough to + determine the outcome, and we don't need the rest to vote anymore. +
+ ++ When triggered, all child cases with a case_state of 'active' are + put into the 'canceled' state. All child cases have their 'locked_p' + flag set to true, so they cannot be reopened. +
+ + + + + + + ++ If any change to any child workflow of a case attempts to trigger + the parent action, the trigger condition would tell us whether to + allow the trigger to go through. +
+ ++ The trigger condition could check to see if all child cases are + completed, or it could check if there's enough to determine the + outcome, e.g. a 2/3 approval. +
+ + +XXXXXXXXXXXXXXX- 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? -
++create table workflow_cases( + ... + state char(40) + constraint workflow_cases_state_ck + check (state in ('active', 'completed', + 'canceled', 'suspended')) + default 'active', + locked_p boolean default 'f', + suspended_until timestamptz, + ... +); ++ +
Cases can be active, complete, suspended, or canceled.
@@ -521,6 +362,104 @@ + + + + ++create table workflow_action_fsm_output_map( + action_id integer + not null + references workflow_actions(action_id) + on delete cascade, + output_short_name varchar(100), + new_state integer + references workflow_fsm_states, + constraint ... + primary key (action_id, output_value) +); ++ +
+ Callback: Action.OnFire -> (output): Executed when the action + fires. Output can be used to determine the new state of the case + (see below). +
+ ++ The callback must enumerate all the values it 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. +
+ ++workflow.Action_OnFire: + OnFire -> string + GetObjectType -> string + GetOutputs -> [string] ++ +
+ GetOutputs returns a list of short_names and pretty_names (possibly + localizable, with #...# notation) of possible outputs. +
+ ++ The above table could be merged with the current + workflow_fsm_actions table, which only contains one possible new + state, with a null output_short_name. +
+ + + + + + ++create table workflow_outcomes( + outcome_id integer + constraint ... + primary key, + workflow_id integer + constraint wf_outcomes_wf_fk + references workflows(workflow_id), + short_name varchar(100) + constraint wf_outcomes_short_name_nn + not null, + pretty_name varchar(200) + constraint wf_outcomes_pretty_name_nn + not null +); + +create table workflow_fsm_states( + ... + -- If this is non-null, it implies that the case has completed with + -- the given output, for use in determining the parent workflow's + -- new state + outcome integer + constraint + references workflow_outcomes(outcome_id), + ... +); + ++ +
+ Also, an action can at most be executed a certain number of times. +
+@@ -557,34 +500,68 @@ 'completed'. ++ The second part, about maximum number of times an action can be + executed, this could be solved with a row in the above table with + the action being dependent upon it self with the given max_n value. +
-Maximum Number of Iterations
+ +Enable Condition Callback
+ ++ Action.CanEnableP -> (CanEnabledP): Gets called when an + action is about to be enabled, and can be used to prevent the + action from actually being enabled. +
+ ++ Is called after all database-driven enable preconditions have been + met, i.e. FSM enabled-in-state, and "gated on"-conditions. +
+ ++ This will only get called once per case state change, so if the + callback refuses to let the action become enabled, it will not be + asked again until the next time an action is executed. +
+ ++ If the callback returns false, the
+ + +enabled_state
of the + row inworkflow_case_enabled_actions
will be set to + 'refused' (NOTE: Or the row will be deleted?). +Non-User Triggered Actions
+Requirements
- An action can at most be executed a certain number of times. + Some actions, for example those will child workflows, may not want + to allow users to trigger them.
Design
create table workflow_actions( ... - max_n integer + user_trigger_p boolean default 't', ... );- 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'. + If user_trigger_p is false, we do not show the action on any user's + task list.
+Resolution Codes
Requirements
@@ -660,76 +637,420 @@ +Assignment Notifications
+Requirements
-Appendix: TIP Voting Process
++ When someone is assigned to an action, we want the notification + email to say "You are now assigned to these tasks". +
+Design
+ ++ We'd need to postpone the notifications until we have fully updated + the workflow state to reflect the changed state, to determine who + should get the normal notifications, and who should get personalized + ones. +
+ ++ Notifications doesn't support personalized notifications, but we + could use acs-mail/acs-mail-lite to send them out instead, and + exclude them from the normal notifications if they have instant + notifications set up. +
+ + + +Assignment Reminders
+ +Requirements
+ ++ We want to periodically send out email reminders with a list of + actions the user is assigned to, asking them to come do something + about it. There should be a link to a web page showing all these + actions. +
+ ++ For each action we will list the action pretty-name, the name of the + case object, the date it was enabled, the deadline, and a link to + the action page, where they can do something about it. +
+ + + + + + +Trying to Sum Up
+ +Logic to Determine if Action is Enabled
+ ++ Executed when any action in the workflow has been executed, to + determine which actions are now enabled. +
+ +
If the action is enabled:
+ +If the action is not enabled.
+ ++ 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. +
+ ++ Executed when an enabled action is triggered. +
+ ++ 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? +
+ + + + ++ We need to update the new_from_spec and generate_spec procedures to + output and parse all the new properties from this spec which get + implemented. +
+ + + + ++ 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. +
+ +-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 +create table workflow_actions( + ... + -- The number of seconds after having become enabled the action + -- will automatically execute + timeout interval + ... +); +-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 +
+ 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: +
+ +