<html>
<head>
  <title>
    Simuation Design - Workflow Extensions
  </title>
</head>
<body bgcolor="white">

<h1>Simuation Design - Workflow Extensions</h1>

By <a href="https://www.linkedin.com/in/truecalvin">Calvin Correli, former known as Lars Pind</a>

<hr>


<h2>Timers</h2>

<h3>Requirements</h3>

<p>
  Use cases:
</p>

<ul>
  <li>
    A student has one week to send a document to another role. If
    he/she fails to do so, a default action executes.
  </li>
  <li>
    An OpenACS OCT member has one week to vote on a TIP. If he/she does not
    vote within that week, a default "Abstain" action is executed.
  </li>
</ul>

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

<h3>Design</h3>

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

<h4>Extending workflow_actions</h4>

<pre>
create table workflow_actions(
    ...
    -- The number of seconds after having become enabled the action
    -- will automatically execute
    timeout                 interval
    ...
);
</pre>

<p>
  DESIGN NOTE: The 'interval' datatype is not supported in
  Oracle.
</p>

<h4>The Enabled Actions Table</h4>

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

<h4>The Logic</h4>

<p>
  After executing an action, <code>workflow::case::action::execute</code> will:
</p>

<ol>
  <li>
    Delete all actions from <code>worklfow_case_enabled_actions</code> which are no longer enabled.
  </li>
  <li>
    If the timeout is zero, execute immediately.
  </li>
  <li>
    Insert a row for all enabled actions with timeouts which are not
    already in <code>workflow_case_enabled_actions</code>, with
    <code>fire_timestamp = current_timestamp + workflow_actions.timeout_seconds</code> .
  </li>
</ol>

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

<h4>The Sweeper</h4>

<p>
  The sweeper will find rows in
  <code>workflow_case_enabled_actions</code> with <code>fire_timetsamp
  < current_timestamp</code>, ordered by fire_timstamp, and execute
  them.
</p>

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

<h4>The Optimization</h4>

<p>
  Every time the sweeper runs, at least one DB query will be made,
  even if there are no timed actions to be executed.
</p>

<p>
  Possible optimizations:
</p>

<ul>
  <li>
    We keep an NSV with the timestamp (in [clock seconds] format)
    and (case_id, action_id) of the first action to fire. That way,
    the sweeper need not hit the DB at all most of the time.  When a
    new timed action is inserted, we compare with the NSV, and update
    if the new action fires before the old action. When the timed
    action referred to in the NSV is either deleted because it gets
    un-enabled, or executed, we'll clear the NSV, causing the next hit
    to the sweeper to execute the query to find the (case_id,
    action_id, fire_timestamp) of the first action to fire. Finally,
    we would need an NSV value to represent the fact that there are no
    rows in this table, so we don't keep executing the query in that
    case.
  </li>
</ul>

<h2>Hierarchical Workflows</h2>

<h3>Requirements</h3>

<p>
  Use cases:
</p>

<ul>
  <li>
    Leiden: We have several occurrences of the simple AskInfo-GiveInfo
    question/response pair. Defining simulation templates would be
    simplified if that was a reusable component.
  </li>
  <li>
    TIP Voting: There's a master workflow case for the TIP
    itself. When voting, there'll be a sub-workflow case for each TIP
    member to vote on the issue, with timeouts so if they don't vote
    within a week, their vote is automatically 'Abstained'.
  </li>
</ul>

<h3>Design</h3>

<ul>
  <li>
    Actions will no longer be atomic. An action can be "in progress"
    for a long time, while the child workflow(s) completes.
  </li>
  <li>
    We will introduce an uber-state of a case, which can be 'active',
    'completed', 'canceled', or 'suspended'.
  </li>
  <li>
    When the action gets enabled, a callback will create child cases
    linked to this particular enabled action.
  </li>
  <li>
    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'.
  </li>
  <li>
    If the action should ever get un-enabled, a callback will cancel
    all remaining 'active' child cases.
  </li>
  <li>
    If the action becomes enabled again, we will create new child
    cases.
  </li>
  <li>
    A case which is a child of another case cannot leave the
    'completed' or 'canceled' state, unless its parent enabled action
    is still enabled.
  </li>
</ul>

<h4>Data Model</h4>

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

</pre>

<h4>Callback Types</h4>

<ul>
 <li>
   <b>Action.CanEnableP -> (CanEnabledP)</b>: Gets called when an
   action is about to be enabled, and can be used to prevent the
   action from actually being enabled. 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
   the case's state changes. If the callback refuses, the
   <code>enabled_state</code> of the row in
   <code>workflow_case_enabled_actions</code> will be set to
   'refused'.
 </li>
 <li>
   <b>Action.OnEnable -> (output)</b>: Gets called when an
   action is enabled. Output can be used to determine the new state of
   the case (see below), in particular for an in-progress state. 
 </li>
 <li>
   <b>Action.OnUnEnable</b>: Gets called when an action that used to be enabled
   is no longer enabled. Is not called when the action fired and thus
   caused it to no longer be enabled.
 </li>
 <li>
   <b>Action.OnChildCaseStateChange -> (output, CompleteP)</b>: Called
   when a child changes its case state
   (active/completed/canceled/suspended). Returns whether the parent
   action has now completed. Output can be used to determine
   the new state of the case (see below).
 </li>
 <li>
   <b>Action.OnFire -> (output)</b>: Executed when the action fires. Output
   can be used to determine the new state of the case (see below).
 </li>
 <li>
   <b>Action.SideEffect</b>: Unchanged from current implementation. The
   difference between this and OnFire is that we can have multiple
   side-effects, but they cannot determine the new state of the case.
 </li>
</ul>

<h4>Callback Output</h4>

<p>
  The callbacks returning 'output' above must enumerate all the values
  they can possible output (similar construct to GetObjectType
  operation on other current workflow service contracts), and the
  callback itself must return one of those possible values.
</p>

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

<h4>Enabled Action Logic</h3>

<p>
  Executed when an action which was previously not enabled becomes enabled.
</p>

<ol>
  <li>
    If the action has a timeout of 0, then execute the action and quit.
  </li>
  <li>
    Insert a row into workflow_case_enabled_actions.
  </li>
  <li>
    If the action has non-null timeout > 0, then the row will have a
    execution_time of current_timestamp + timeout.
  </li>
  <li>
    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?)
  </li>
  <li>
    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.
  </li>
</ol>

<h4>Un-Enabled Action Logic</h4>

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

<ol>
  <li>
    If the action has any child cases, these will be marked canceled.
  </li>
</ol>

<h4>Child Case State Changed Logic</h4>

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

<p>
  We provide a default implementation, which simply checks if the
  child cases are in the 'complete' state, and if so, fires.
</p>

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

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

<h4>On Fire Logic</h4>

<p>
  When the action finally fires.
</p>

<p>
  If there's any OnFire callback defined, we execute this.
</p>

<p>
  If the callback has output values defined, we use the mappings in
  <code>workflow_action_fsm_output_map</code> to determine which state to 
  move to.
</p>

<p>
  After firing, we execute the SideEffect callbacks and send off
  notifications.
</p>

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

<h4>Case State</h4>

<p>
  Cases can be active, complete, suspended, or canceled.
</p>

<p>
  They start out as active. For FSMs, when they hit a state with
  <code>complete_p = t</code>, the case is moved to 'complete'.
</p>

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

<p>
  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'.
</p>

<p>
  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!).
</p>





<h3>Appendix: Resolution Code</h3>

<ul>
  <li>
    I found another dynamic-workflow product (TrackStudio) on the web,
    and they have resolution be a first-class citizen. If it helps we
    could do something similar, which would make it possible to
    custom-define the resolution codes and simplify something like
    bug-tracker somewhat. Resolution is just a way of saying "these
    states are all the same wrt the workflow, but they have another
    significance". Bug-tracker "duplicate" resolution code could be
    solved by just having a generic mechanism for relating bugs, and
    ignore any UI connection with resolution code "duplicate". Also,
    resolution  code "postponed" could be eliminated, and replaced
    with the case-state of "suspended" above. We could also add an
    "suspended_until" date, to keep track of when the case should
    resurface (if at a fixed date).
  </li>
</ul>



<h3>Appendix: TIP Voting Process</h3>

<pre>
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
</pre>

<h3>Appendix: Leiden's Example Workflow</h3>

<pre>
Leiden Master Workflow
  Model = Dependency
  Roles
    Client 1
    Lawyer 1
    Partner 1
    Secretary 1
    Client 2
    Lawyer 2
    Partner 2
    Secretary 2
    Library
  Actions
    L1 Info Request From C2
    L1 Info Request From L2
      Dependent on = L1 Info Request From C2
    L1 Info Request From Library
    
    L1 Draft With S1
      Dependent on = L1 Info Request From L2, L1 Info Request From Library
    L1 Send Document
      Dependent on = L1 Draft With S1

    P1 Intervenes With L1

    L2 Info Request From C1
    L2 Info Request From L1
      Dependent on = L2 Info Request From C1
    L2 Info Request From Library
    
    L2 Draft With S2
      Dependent on = L2 Info Request From L1, L2 Info Request From Library
    L2 Send Document
      Dependent on = L2 Draft With S2

    P2 Intervenes With L2

    Done
      Dependent on = L1 Send Document, L2 Send Document

AskInfo-GiveInfo Loop Workflow
  Roles
    Informer
    Recipient
  States
    Asked
    Given
  Actions
    Ask Info
      Initial action
      New State = Asked
    Give Info
      Enabled in state = Asked
      Role = Informer
      New state = Given
</pre>



<hr>

</body>
</html>