class Doable::Job
The Job
class is responsible for describing the process of running some set of steps. It utilizes a very specific DSL for defining what steps need executing, along with their order. It can also describe how to recover when things break and provides hooks and triggers to make more flexible scripts for varying environments.
Attributes
Public Class Methods
Yields itself to allow the syntax seen in the plan class method.
# File lib/doable/job.rb, line 24 def initialize @hooks = {} @steps = [] @handlers = {} @threads = [] yield self end
Allows sequential definition of job steps. @example usage
job = Doable::Job.plan do |j| j.before { log "Starting my awesome job" } j.step { # do some stuff here } j.attempt { # try to do some other stuff here } j.after { log "Looks like we're all set" } end
# File lib/doable/job.rb, line 19 def self.plan(&block) self.new(&block) end
Public Instance Methods
Registers an action to be performed after normal execution completes @param options [Hash] @param block [Proc] @return [Boolean]
# File lib/doable/job.rb, line 64 def after(options = {}, &block) on(:after, options, &block) end
Add a step to the queue, but first wrap it in a begin..rescue WARNING! Exception handlers are __not__ used with these steps, as they never actually raise exceptions @param options [Hash] @param block [Proc] @return [Step] the step just create by this method
# File lib/doable/job.rb, line 73 def attempt(options = {}, &block) @steps << Step.new(self, options) do begin self.instance_exec(&block) rescue SkipStep => e raise e # We'll rescue this somewhere higher up the stack rescue => e log "Ignoring Exception in attempted step: #{colorize("#{e.class}: (#{e.message})", :red)}" end end @steps.last # return the last step (the one we just defined) end
Allow running steps in the background @param options [Hash] @param block [Proc] @return [Step]
# File lib/doable/job.rb, line 90 def background(options = {}, &block) @steps << Step.new(self, options) do @threads << Thread.new { self.instance_exec(&block) } end @steps.last # return the last step (the one we just defined) end
Registers an action to be performed before normal step execution @param options [Hash] @param block [Proc] @return [Step]
# File lib/doable/job.rb, line 56 def before(options = {}, &block) on(:before, options, &block) end
Returns the binding context of the Job
@return [Binding]
# File lib/doable/job.rb, line 115 def context binding end
Register a handler for named exception @param exception [String,StandardError] Exception to register handler for @param block [Proc]
# File lib/doable/job.rb, line 122 def handle(exception, &block) @handlers[exception] = Step.new(self, &block) end
Check if background steps are running @return [Boolean]
# File lib/doable/job.rb, line 99 def multitasking? return @threads.collect {|t| t if t.alive? }.compact.empty? ? false : true end
Registers a hook action to be performed when the hook is triggered @param hook [Symbol] Name of the hook to register the action with @param options [Hash] @param block [Proc] @return [Step]
# File lib/doable/job.rb, line 37 def on(hook, options = {}, &block) @hooks[hook] ||= [] @hooks[hook] << Step.new(self, options, &block) @hooks[hook].last # return the last step (the one we just defined) end
Trigger a rollback of the entire Job
, based on calls to rollback!()
on each eligible Step
# File lib/doable/job.rb, line 104 def rollback! log "Rolling Back...", :warn @hooks[:after].reverse.each {|s| s.rollback! if s.rollbackable? } if @hooks.has_key?(:after) @steps.reverse.each {|s| s.rollback! if s.rollbackable? } @hooks[:before].reverse.each {|s| s.rollback! if s.rollbackable? } if @hooks.has_key?(:before) log "Rollback complete!", :warn raise RolledBack end
Here we actually trigger the execution of a Job
# File lib/doable/job.rb, line 171 def run #merge_config FILE_CONFIG #merge_config CLI_CONFIG ## Run our defaults Proc to merge in any default configs #@defaults.call(@@config) # before hooks trigger(:before) # Actual installer steps @steps.each_with_index do |step, index| begin step.call rescue SkipStep => e step.skip log e.message, :warn rescue => e if @handlers[e.message] log "Handling #{e.class}: (#{e.message})", :warn @handlers[e.message].call(e, step) step.handled elsif @handlers[e.class] log "Handling #{e.class}: (#{e.message})", :warn @handlers[e.class].call(e, step) step.handled else # Check the ancestry of the exception to see if any lower level Exception classes are caught e.class.ancestors[1..-4].each do |ancestor| if @handlers[ancestor] log "Handling #{e.class}: (#{e.message}) via handler for #{ancestor}", :warn @handlers[ancestor].call(e, step) step.handled end # if @handlers[ancestor] end unless step.successful? message = "\n\nUnhandled Exception in #{colorize("steps[#{index}]", :yellow)}: #{colorize("#{e.class}: (#{e.message})", :red)}\n\n" #if @config.auto_rollback # log message # rollback! #else raise message #end end # unless end # if @handlers... end # rescue end # @steps.each_with_index # after hooks trigger(:after) # bring together all background threads unless @threads.empty? log "Cleaning up background tasks..." @threads.each do |t| begin t.join rescue => e # We don't really need to do anything here, # we've already handled or died from aborted Threads end end end log "All Job steps completed successfully!", :success # This should only happen if everything goes well end
Adds a step to the queue @param options [Hash] @param block [Proc] @return [Step]
# File lib/doable/job.rb, line 47 def step(options = {}, &block) @steps << Step.new(self, options, &block) @steps.last # return the last step (the one we just defined) end
This triggers a block associated with a hook @param hook [Symbol] Hook to trigger @return [Boolean] returns true no exceptions are encounter during enumeration of hook steps.
# File lib/doable/job.rb, line 129 def trigger(hook) @hooks[hook].each_with_index do |step, index| begin step.call rescue SkipStep => e step.skip log e.message, :warn rescue => e if @handlers[e.message] log "Handling #{e.class}: (#{e.message})", :warn @handlers[e.message].call(e, step) step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped elsif @handlers[e.class] log "Handling #{e.class}: (#{e.message})", :warn @handlers[e.class].call(e, step) step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped else # Check the ancestry of the exception to see if any lower level Exception classes are caught e.class.ancestors[1..-4].each do |ancestor| if @handlers[ancestor] log "Handling #{e.class}: (#{e.message}) via handler for #{ancestor}", :warn @handlers[ancestor].call(e, step) step.handled unless step.status == :skipped # Don't mark the step as "handled" if it was skipped end # if @@handlers[ancestor] end unless step.successful? message = "\n\nUnhandled Exception in #{colorize("hooks##{hook}[#{index}]", :yellow)}: #{colorize("#{e.class}: (#{e.message})", :red)}\n\n" #if @config.auto_rollback # log message # rollback! #else raise message #end end # unless end end # begin() end if @hooks[hook] # each_with_index() return true end