Last Updated: February 25, 2016
·
817
· bashir

Reserve and timeouts in Ruote

Ruote is a workflow orchestration framework with a lot of features including concurrences and sequences to create pretty complex workflows.

One problem I was grappling with lately was to be able to reserve a workitem for certain amount of time before actually doing the work. If the time elapsed and you had not finished, the workitem would be unassigned and ready for someone else to pickup.

Challenges

There were three challenges
1. preventing the storage participant from doing the work before it's owner has been set through 'reserve'
2. having a dynamic timeout value
3. resetting ownership if time out happens

DSL workflow

Here's how it was done:

cursor do
  once '${r:(wi.h.owner = nil) == nil}'
  once '${r:(wi.h.owned_until = nil) == nil}'
  participant 'reader', :task => read_book  #unassigned
  once '${r:wi.h.owner != nil}'
  participant 'reader', :task => read_book, :timeout => "${r: wi.h.owned_until }", :on_timeout => 'rewind'
end

the implicit assignment to nil in the comparison below is a hack to ruote dsl to reset the owner field as it it a workitem field and not accessible in the dsl

once '${r:(wi.h.owner = nil) == nil}'

similar technique is used to reset the dynamic owned_until field (also added to workitem)
once will basically block until the condition is true. since there is an implicit setting to nil, the condition immediately is true so the workflow will flow until it gets to:

participant 'reader', :task => read_book  #unassigned

at this point the read_book task still cannot be performed but is here so that the task structure is created and workflow halted until someone calls the 'reserve' method (see below) to reserve the book. The reserve code not only reserves the workitem but it also sets the duration and very importantly, proceeds the workflow. This will bring us to the following expression:

once '${r:wi.h.owner != nil}'

which blocks until an owner is set, in our case an owner has been set so we flow to the next statement:

participant 'reader', :task => read_book, :timeout => "${r: wi.h.owned_until }", :on_timeout => 'rewind'

at this point the participant can be reading the book and they have until the dynamic duration (owneduntil) to read it and if they don't read(i.e. proceed the workflow) in that time, the time out will hit and the :ontimeout action will be called which will rewind the workflow to the beginning of the cursor. Remember that rewinds do preserve the state of the workitem so any notes the user may have taken on the book will still persist. If you want the workitem to reset as it goes back to the cursor, use 'redo'

Reservation code

def reserve(workitem, owner, duration)
  updated_workitem = RuoteKit.engine.storage_participant.reserve(workitem, owner)
  updated_workitem.h['owned_until'] = duration
  RuoteKit.engine.storage_participant.do_update(updated_workitem)
  RuoteKit.engine.storage_participant.proceed(workitem)
end

If you are into interesting engineering problems, you can join me (@GrandRoundsHealth)[https://grandroundshealth.com/]