Class: Doable::Job

Inherits:
Object
  • Object
show all
Includes:
Helpers::Framework, Helpers::Logging
Defined in:
lib/doable/job.rb

Overview

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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers::Logging

#colorize, #log

Methods included from Helpers::Framework

#skip

Constructor Details

#initialize {|_self| ... } ⇒ Job

Yields itself to allow the syntax seen in the plan class method.

Yields:

  • (_self)

Yield Parameters:

  • _self (Doable::Job)

    the object that the method was called on



24
25
26
27
28
29
30
# File 'lib/doable/job.rb', line 24

def initialize
  @hooks    = {}
  @steps    = []
  @handlers = {}
  @threads  = []
  yield self
end

Instance Attribute Details

#handlersObject (readonly)

Returns the value of attribute handlers.



9
10
11
# File 'lib/doable/job.rb', line 9

def handlers
  @handlers
end

#hooksObject (readonly)

Returns the value of attribute hooks.



9
10
11
# File 'lib/doable/job.rb', line 9

def hooks
  @hooks
end

#stepsObject (readonly)

Returns the value of attribute steps.



9
10
11
# File 'lib/doable/job.rb', line 9

def steps
  @steps
end

#threadsObject (readonly)

Returns the value of attribute threads.



9
10
11
# File 'lib/doable/job.rb', line 9

def threads
  @threads
end

Class Method Details

.plan(&block) ⇒ Object

Allows sequential definition of job steps.

Examples:

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


19
20
21
# File 'lib/doable/job.rb', line 19

def self.plan(&block)
  self.new(&block)
end

Instance Method Details

#after(options = {}, &block) ⇒ Boolean

Registers an action to be performed after normal execution completes

Parameters:

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:

  • (Boolean)


64
65
66
# File 'lib/doable/job.rb', line 64

def after(options = {}, &block)
  on(:after, options, &block)
end

#attempt(options = {}, &block) ⇒ Step

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

Parameters:

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:

  • (Step)

    the step just create by this method



73
74
75
76
77
78
79
80
81
82
83
84
# 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

#background(options = {}, &block) ⇒ Step

Allow running steps in the background

Parameters:

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:



90
91
92
93
94
95
# 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

#before(options = {}, &block) ⇒ Step

Registers an action to be performed before normal step execution

Parameters:

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:



56
57
58
# File 'lib/doable/job.rb', line 56

def before(options = {}, &block)
  on(:before, options, &block)
end

#contextBinding

Returns the binding context of the Job

Returns:

  • (Binding)


115
116
117
# File 'lib/doable/job.rb', line 115

def context
  binding
end

#handle(exception, &block) ⇒ Object

Register a handler for named exception

Parameters:

  • exception (String, StandardError)

    Exception to register handler for

  • block (Proc)


122
123
124
# File 'lib/doable/job.rb', line 122

def handle(exception, &block)
  @handlers[exception] = Step.new(self, &block)
end

#multitasking?Boolean

Check if background steps are running

Returns:

  • (Boolean)


99
100
101
# File 'lib/doable/job.rb', line 99

def multitasking?
  return @threads.collect {|t| t if t.alive? }.compact.empty? ? false : true
end

#on(hook, options = {}, &block) ⇒ Step

Registers a hook action to be performed when the hook is triggered

Parameters:

  • hook (Symbol)

    Name of the hook to register the action with

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:



37
38
39
40
41
# 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

#rollback!Object

Trigger a rollback of the entire Job, based on calls to #rollback!() on each eligible Step

Raises:



104
105
106
107
108
109
110
111
# 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

#runObject

Here we actually trigger the execution of a Job



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# 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

#step(options = {}, &block) ⇒ Step

Adds a step to the queue

Parameters:

  • options (Hash) (defaults to: {})
  • block (Proc)

Returns:



47
48
49
50
# 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

#trigger(hook) ⇒ Boolean

This triggers a block associated with a hook

Parameters:

  • hook (Symbol)

    Hook to trigger

Returns:

  • (Boolean)

    returns true no exceptions are encounter during enumeration of hook steps.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 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