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 -N -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 @@ -

Timers

- -

Requirements

- -

- 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. -

- -

Design

- -

- 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. -

- -

Extending workflow_actions

- -
-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. -

- -

The Enabled Actions Table

- -
-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)
-);
-
- -

The Logic

- -

- After executing an action, workflow::case::action::execute will: -

- -
    -
  1. - Delete all actions from worklfow_case_enabled_actions which are no longer enabled. -
  2. -
  3. - If the timeout is zero, execute immediately. -
  4. -
  5. - Insert a row for all enabled actions with timeouts which are not - already in workflow_case_enabled_actions, with - fire_timestamp = current_timestamp + workflow_actions.timeout_seconds . -
  6. -
- -

- NOTE: We need to keep running, so if another automatic action - becomes enabled after this action fires, they'll fire as well. -

- -

The Sweeper

- -

- 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. -

- -

The Optimization

- -

- 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: -

- - -

Hierarchical Workflows

Requirements

@@ -219,12 +69,13 @@ When the action gets enabled, a callback will create child cases linked to this particular enabled action. +
  • Whenever a child case changes its case_state, a callback on the parent action is invoked, which examines the state of all of its child cases and determines whether the parent action is complete - and ready to fire or not. If the parent action is completed, all - any 'active' child cases will be made 'canceled'. + and ready to fire or not. If the parent action is completed, any + remaining 'active' child cases will be marked 'canceled'.
  • If the action should ever get un-enabled, a callback will cancel @@ -244,35 +95,21 @@

    Data Model

    -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)
     );
    -
     
    -

    Callback Types

    +

    Enabled States Explained

    - - -

    Callback Output

    -

    - 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. -

    - -

    Enabled Action Logic

    - -

    - Executed when an action which was previously not enabled becomes enabled. -

    - -
      +
    + -

    Un-Enabled Action Logic

    +

    When 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.

    -
      -
    1. - If the action has any child cases, these will be marked canceled. -
    2. -
    +

    + 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. +

    -

    Child Case State Changed Logic

    +

    When Triggered

    + 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. +

    + + + + + + + +

    Trigger Conditions

    + +

    Requirements

    + +

    + 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

    Child Case State Changed Logic

    > We execute the OnChildCaseStateChange callback, if any. This gets to determine whether the parent action is now complete and should fire.

    @@ -460,38 +282,57 @@ never needed.

    -

    On Fire Logic

    -

    - 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? -

    +

    Case State

    -

    Case State

    +

    Requirements

    + + +

    Design

    + +
    +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 @@

    + + + + +

    Conditional Transformation For Atomic Actions

    + +
    +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. +

    + +

    Service Contract

    + +
    +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. +

    + +

    Note

    + +

    + 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. +

    + + + + + + +

    Conditional Transformation Based on Child Workflows

    + +
    +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),
    +  ...
    +);
    +
    +
    + +

    Gated Actions

    Requirements

    @@ -532,6 +471,10 @@ been executed.

    +

    + Also, an action can at most be executed a certain number of times. +

    +

    Design

    @@ -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 in workflow_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.

    + + + +

    Enabled Action Logic

    + +

    + Executed when an action which was previously not enabled becomes enabled. +

    + +
      +
    1. + Insert a row into workflow_case_enabled_actions with enabled_state + = 'enabled', with the proper fire_timestamp: timeout = null -> + fire_timestamp = nul; timeout = 0 -> fire_timestamp = + current_timestamp; timeout > 0 -> fire_timestamp = + current_timestamp + timeout. +
    2. +
    3. + If the action has a timeout of 0, then call + workflow::case::action::execute and quit. +
    4. + + + +

      Un-Enabled Action Logic

      + +

      + Executed when an action which was previously enabled is no longer + enabled, because the workflow's state was changed by some other + action. +

      + +
        +
      1. + If the action has any child cases, these will be marked canceled. +
      2. +
      + + +

      Action Execute Logic

      + +

      + Executed when an enabled action is triggered. +

      + +
        +XXXXXXXXXXXXXXXXXXXXXXX + +
      • + If the action has non-null child_workflow, create child cases. For + each role which has a mapping_type of 'per_member' or 'per_user', + create one case per member/user of that role. If more roles have + per_member/per_user setting, then the cartesian product of child + cases are created (DESIGN QUESTION: Would this ever be relevant?) +
      • +
      • + If there is any ActionEnabled callback, execute that (only the + first, if multiple exists), and use the workflow_fsm_output_map to + determine which new state to bump the workflow to, if any. +
      • +
    + + + +

    Child Case State Changed Logic

    + +

    + 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. +

    + +

    On Fire Logic

    + +

    + 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? +

    + + + + +

    Callback Types

    + + + + + +

    NOTE: Cloning

    + +

    + 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. +

    + + + + +

    Implemented: Timers

    + +

    Requirements

    + +

    + 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. +

    + +

    Design

    + +

    + 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. +

    + +

    Extending workflow_actions

    +
    -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. +

    + +

    The Enabled Actions Table

    + +
    +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)
    +);
     
    +

    The Logic

    +

    + After executing an action, workflow::case::action::execute will: +

    + +
      +
    1. + Delete all actions from worklfow_case_enabled_actions which are no longer enabled. +
    2. +
    3. + If the timeout is zero, execute immediately. +
    4. +
    5. + Insert a row for all enabled actions with timeouts which are not + already in workflow_case_enabled_actions, with + fire_timestamp = current_timestamp + workflow_actions.timeout_seconds . +
    6. +
    + +

    + NOTE: We need to keep running, so if another automatic action + becomes enabled after this action fires, they'll fire as well. +

    + +

    The Sweeper

    + +

    + 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. +

    + +

    The Optimization

    + +

    + 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: +

    + + + + +
    +