Timed Tasks - department-of-veterans-affairs/caseflow GitHub Wiki

What are timed tasks?

Tasks (models which count Task amongst their superclasses) indicate that an action needs to be performed by somebody or some automated process. A member of the quality review team may need to review a decision (QualityReviewTask) or the automatic case distribution process may need to distribute a case to a judge (DistributionTask).

In addition to these proactive task actions, sometimes an appeal just needs to wait for some time to pass, so we have extended the task model to account for these waiting periods.

Why do we need timed tasks?

The VLJ support staff may send correspondence to a Veteran's attorney requesting a response within 30 days. In order to record this waiting period and remove the case from that employees active task queue we create a task to indicate that the appeal is "on hold". Caseflow users place tasks on hold, specifying some number of days for the hold, and after that many days have elapsed the task that was placed on hold is returned to their active queue.

How do timed tasks work?

In order to acheive this behaviour the application uses three similarly-named entities: TimedHoldTask, TimeableTask, and TaskTimer. TimedHoldTask is the task in the task tree representing this waiting period. TimeableTask is the mix-in that encapsulates the behaviour that allows for actions to be taken automatically on the TimedHoldTask. And the TaskTimer is the component of the system that keeps track of when the action should happen.

TimedHoldTask

The TimedHoldTask task will always be a part of the task tree which might look like the following:

-----------------------------------------------------------------------------------------------

                                         RootTask

---------+----------------------+----------------------+-------------------------+-------------
         |                      |                      |                         |
+--------+---------+   +--------+---------+   +--------+--------+   +------------+------------+
|                  |   |                  |   |                 |   |                         |
| TrackVeteranTask |   | DistributionTask |   | JudgeAssignTask |   | JudgeDecisionReviewTask |
|                  |   |                  |   |                 |   |                         |
+------------------+   +--------+---------+   +-----------------+   +------------+------------+
                                |                                                |
                       +--------+---------+                         +------------+------------+
                       |                  |                         |                         |
                       | InformalHearing- |                         |      AttorneyTask       |
                       | PresentationTask |                         |                         |
                       |                  |                         +------------+------------+
                       +------------------+                                      |
                                                                    +------------+------------+
                                                                    |                         |
                                                                    |  ExtensionColocatedTask |
                                                                    |                         |
                                                                    +------------+------------+
                                                                                 |
                                                                    +------------+------------+
                                                                    |                         |
                                                                    |  ExtensionColocatedTask |
                                                                    |                         |
                                                                    +------------+------------+
                                                                                 |
                                                                    +------------+------------+
                                                                    |                         |
                                                                    |      TimedHoldTask      |
                                                                    |                         |
                                                                    +-------------------------+

TimeableTask

TimeableTask will never be instantiated directly, rather it is a mix-in that requires the implementing class define two methods: when_timer_ends describing the behaviour of the application when the timer ends, and timer_ends_at indicating when the timer for an instance of the implementing class will end. Continuing to use TimedHoldTask as our example, TimedHoldTask.when_timer_ends updates the status of the task to completed, and TimedHoldTask.timer_ends_at simply adds the desired number of days for the task to be "on hold" to the timestamp of the task's creation.

TaskTimer

In order to trigger the behaviour defined by the when_timer_ends method, the application creates a TaskTimer object associated with a task instance that keeps track of the timer. We expect every instance of a class that mixes in TimeableTask to have exactly 1 associated TaskTimer. We can confirm this expectation with the following SQL query which should return no rows:

select task_id, count(*)
from task_timers
group by task_id
having count(*) > 1;

These task timers are automatically processed and completed by our TaskTimerJob.

There are several timestamps associated with task timers that determine how they are processed.

  • created_at : when the timer was created, automatically set by rails.
  • updated_at : the last time any attribute of the timer was updated, automatically set by rails.
  • last_submitted_at : when the timer should be completed
  • submitted_at : when the timer first became eligible to be completed
  • attempted_at : when the job last attempted to complete the timer
  • processed_at : when the job completed the timer
  • cancelled_at : when the timer was abandoned and not completed

Currently there is nothing stopping a timer's last_submitted_at from being in the past. This is often seen in EvidenceSubmissionWindowTasks as the start time of its 90 day window could be calculated to be more than 90 days ago. To handle timers that should have been completed outside of the 4 day window in TaskTimerJob, we automatically reset last_submitted_at so the timer can be picked up the next time the job is run.

TaskTimerJob

We have a TaskTimerJob that runs every hour checking for TaskTimers that have reached their expiration and calls when_timer_ends on the associated task, completing the timed task process.

TaskTimerJob looks for all task timers that

  1. should have been completed before today (last_sumbitted_at < Time.zone.now)
  2. have not been processed (processed_at_ == nil)
  3. have not been cancelled (cancelled_at_ == nil)
  4. have not had an attempt at processing within the last 3 hours (attempted_at < 3.hours.ago)
  5. OR never been attempted (attempted_at == nil ) AND should have been completed before 3 hours ago (last_submitted_at < 3.hours.ago)
  6. should have been completed within the last 4 days (last_submitted_at > 4.days.ago)

It then

  1. sets the timer's attempted_at to now
  2. processes the timer as defined by the task's when_timer_ends method
  3. closes the task
  4. creates any IHP tasks if needed
  5. clears out any errors
  6. sets the timer's processed_at to now

Why did we implement timed tasks this way?

At the time of this writing there are only two task classes that include TimeableTask: TimedHoldTask and EvidenceSubmissionWindowTask. We anticipate more types of tasks needed the ability to have some action happen after a defined amount of time. For example, it may be desirable for the window for a VSO to submit an IHP to be closed after 90 days which could be accomplished by InformalHearingPresentationTask including TimeableTask.

Can a TimeableTask be cancelled?

Yes. Tasks that implement TimeableTask (only TimedHoldTask and EvidenceSubmissionWindowTask) are cancelled in the same manner as all other tasks, using the "Cancel Task" action. Cancellation of a task that implements TimeableTask will cause associated TaskTimers to be closed as well.

While TimedHoldTasks do not have task actions available directly on the task itself, the parent task (the one that is "on hold") has the option to cancel the associated TimedHoldTask.