module StatusWorkflow
Constants
- LOCK_ACQUISITION_TIMEOUT
- LOCK_CHECK_RATE
- LOCK_EXPIRY
- VERSION
Public Class Methods
included(klass)
click to toggle source
# File lib/status_workflow.rb, line 9 def self.included(klass) klass.extend ClassMethods end
redis()
click to toggle source
# File lib/status_workflow.rb, line 17 def self.redis @redis or raise("please set StatusWorkflow.redis=") end
redis=(redis)
click to toggle source
# File lib/status_workflow.rb, line 13 def self.redis=(redis) @redis = redis end
Public Instance Methods
status_transition!(intermediate_to_status, final_to_status, prefix = nil) { || ... }
click to toggle source
# File lib/status_workflow.rb, line 25 def status_transition!(intermediate_to_status, final_to_status, prefix = nil) result = nil # what the block yields, return to the user before_status_transition = self.class.instance_variable_get(:@before_status_transition) intermediate_to_status = intermediate_to_status&.to_s final_to_status = final_to_status&.to_s prefix_ = prefix ? "#{prefix}_" : nil status_column = "#{prefix_}status" status_changed_at_column = "#{status_column}_changed_at" error_column = "#{status_column}_error" lock_obtained_at = nil lock_key = "status_workflow/#{self.class.name}/#{id}/#{status_column}" # Give ourselves 8 seconds to get the lock, checking every 0.2 seconds Timeout.timeout(LOCK_ACQUISITION_TIMEOUT, nil, "#{lock_key} timeout waiting for lock") do until StatusWorkflow.redis.set(lock_key, true, nx: true, ex: LOCK_EXPIRY) sleep LOCK_CHECK_RATE end lock_obtained_at = Time.now end heartbeat = nil initial_to_status = intermediate_to_status || final_to_status begin # depend on #can_enter_X to reload send "#{prefix_}can_enter_#{initial_to_status}?", true if intermediate_to_status update_columns status_column => intermediate_to_status, status_changed_at_column => Time.now end # If a block was given, start a heartbeat thread if block_given? begin heartbeat = Thread.new do loop do StatusWorkflow.redis.expire lock_key, LOCK_EXPIRY lock_obtained_at = Time.now sleep LOCK_EXPIRY/2.0 end end result = yield before_status_transition&.call rescue # If the block errors, set status to error and record the backtrace error = (["#{$!.class} #{$!.message}"] + $!.backtrace).join("\n") before_status_transition&.call status = read_attribute status_column update_columns status_column => "#{status}_error", status_changed_at_column => Time.now, error_column => error raise end end # Success! if intermediate_to_status send "#{prefix_}can_enter_#{final_to_status}?", true end update_columns status_column => final_to_status, status_changed_at_column => Time.now ensure raise TooSlow, "#{lock_key} lost lock" if Time.now - lock_obtained_at > LOCK_EXPIRY StatusWorkflow.redis.del lock_key heartbeat.kill if heartbeat end result end