Class: Maestro::MaestroWorker

Inherits:
Object
  • Object
show all
Defined in:
lib/maestro_plugin/maestro_worker.rb

Overview

Helper Class for Maestro Plugins written in Ruby. The lifecycle of the plugin starts with a call to the main entry point perform called by the Maestro agent, all the other methods are helpers that can be used to deal with parsing, errors, etc. The lifecycle ends with a call to run_callbacks which can be customized by specifying an on_complete_handler_method, or on_complete_handler_block.

Constant Summary collapse

CONTEXT_OUTPUTS_META =

Workitem constants

'__context_outputs__'
OUTPUT_META =
'__output__'
PREVIOUS_CONTEXT_OUTPUTS_META =
'__previous_context_outputs__'
STREAMING_META =
'__streaming__'
ERROR_META =
'__error__'
WAITING_META =
'__waiting__'
CANCEL_META =
'__cancel__'
NOT_NEEDED =
'__not_needed__'
'__links__'
PERSIST_META =
'__persist__'
MODEL_META =
'__model__'
RECORD_ID_META =
'__record_id__'
RECORD_FIELD_META =
'__record_field__'
RECORD_VALUE_META =
'__record_value__'
RECORD_FIELDS_META =
'__record_fields__'
RECORD_VALUES_META =
'__record_values__'
FILTER_META =
'__filter__'
NAME_META =
'__name__'
CREATE_META =
'__create__'
UPDATE_META =
'__update__'
DELETE_META =
'__delete__'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.exception_handler_blockObject (readonly)

Returns the value of attribute exception_handler_block.



51
52
53
# File 'lib/maestro_plugin/maestro_worker.rb', line 51

def exception_handler_block
  @exception_handler_block
end

.exception_handler_methodObject (readonly)

Returns the value of attribute exception_handler_method.



51
52
53
# File 'lib/maestro_plugin/maestro_worker.rb', line 51

def exception_handler_method
  @exception_handler_method
end

.on_complete_handler_blockObject (readonly)

Returns the value of attribute on_complete_handler_block.



51
52
53
# File 'lib/maestro_plugin/maestro_worker.rb', line 51

def on_complete_handler_block
  @on_complete_handler_block
end

.on_complete_handler_methodObject (readonly)

Returns the value of attribute on_complete_handler_method.



51
52
53
# File 'lib/maestro_plugin/maestro_worker.rb', line 51

def on_complete_handler_method
  @on_complete_handler_method
end

Instance Attribute Details

#actionObject

Returns the value of attribute action.



87
88
89
# File 'lib/maestro_plugin/maestro_worker.rb', line 87

def action
  @action
end

#workitemObject

Returns the value of attribute workitem.



87
88
89
# File 'lib/maestro_plugin/maestro_worker.rb', line 87

def workitem
  @workitem
end

Class Method Details

.mock!Object

Call this to mock calls to outbound systems.



72
73
74
# File 'lib/maestro_plugin/maestro_worker.rb', line 72

def mock!
  @mock = true
end

.mock?Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/maestro_plugin/maestro_worker.rb', line 81

def mock?
  @mock
end

.on_complete(handler = nil, &block) ⇒ Object

Register a callback method or block that gets called when the action was successfully completed. Block callbacks get the workitem as parameter.



66
67
68
69
# File 'lib/maestro_plugin/maestro_worker.rb', line 66

def on_complete(handler = nil, &block)
  @on_complete_handler_method = handler
  @on_complete_handler_block = block
end

.on_exception(handler = nil, &block) ⇒ Object

Register a callback method or block that gets called when an exception occurs during the processing of an action. handler can be a symbol or string with a method name, or a block. Both will get the exception as the first parameter, and the block handler will receive the participant instance as the second parameter



58
59
60
61
# File 'lib/maestro_plugin/maestro_worker.rb', line 58

def on_exception(handler = nil, &block)
  @exception_handler_method = handler
  @exception_handler_block = block
end

.unmock!Object

Call this to unmock calls to outbound systems.



77
78
79
# File 'lib/maestro_plugin/maestro_worker.rb', line 77

def unmock!
  @mock = false
end

Instance Method Details

Adds a link to be displayed in the Maestro UI.



377
378
379
380
# File 'lib/maestro_plugin/maestro_worker.rb', line 377

def add_link(name, url)
  set_field(LINKS_META, []) if fields[LINKS_META].nil?
  fields[LINKS_META] << {'name' => name, 'url' => url}
end

#as_boolean(value) ⇒ Object

Return boolean version of a value



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/maestro_plugin/maestro_worker.rb', line 400

def as_boolean(value)
  res = false

  if value
    if value.is_a?(TrueClass) || value.is_a?(FalseClass)
      res = value
    elsif value.is_a?(Fixnum)
      res = value != 0
    elsif value.respond_to?(:to_s)
      value = value.to_s.downcase

      res = (value == 't' || value == 'true')
    end
  end

  res
end

#as_int(value, default = 0) ⇒ Object

Return numeric version of value



385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/maestro_plugin/maestro_worker.rb', line 385

def as_int(value, default = 0)
  res = default

  if value
    if value.is_a?(Fixnum)
      res = value
    elsif value.respond_to?(:to_i)
      res = value.to_i
    end
  end

  res
end

#cancelObject

Send the “cancel” message to the server



271
272
273
274
275
276
277
278
# File 'lib/maestro_plugin/maestro_worker.rb', line 271

def cancel
  workitem[CANCEL_META] = true
  send_workitem_message
rescue Exception => e
  Maestro.log.warn "Failed To Send Cancel Message To Server #{e.class} #{e}: #{e.backtrace.join("\n")}"
ensure
  workitem.delete(CANCEL_META)
end

#create_record_with_fields(model, record_fields, record_values = nil) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/maestro_plugin/maestro_worker.rb', line 309

def create_record_with_fields(model, record_fields, record_values = nil)
  workitem[PERSIST_META] = true
  workitem[CREATE_META] = true
  workitem[MODEL_META] = model
  unless record_fields.is_a? Hash
    Maestro.log.warn 'deprecation: create_record_with_fields should be called with a Hash'
    record_fields = record_fields.join(',') if record_fields.respond_to? 'join'
    record_values = record_values.join(',') if record_values.respond_to? 'join'
  end

  workitem[RECORD_FIELDS_META] = record_fields
  workitem[RECORD_VALUES_META] = record_values
  send_workitem_message

  workitem.delete(PERSIST_META)
  workitem.delete(CREATE_META)
end

#delete_record(model, filter) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/maestro_plugin/maestro_worker.rb', line 328

def delete_record(model, filter)
  workitem[PERSIST_META] = true
  workitem[DELETE_META] = true
  workitem[MODEL_META] = model

  if filter.is_a? Hash
    workitem[FILTER_META] = filter
  else
    Maestro.log.warn 'deprecation: delete_record should be called with a Hash'
    workitem[NAME_META] = filter.to_s
  end

  send_workitem_message

  workitem.delete(PERSIST_META)
  workitem.delete(DELETE_META)
end

#errorObject



246
247
248
# File 'lib/maestro_plugin/maestro_worker.rb', line 246

def error
  fields[ERROR_META]
end

#error?Boolean

Returns:

  • (Boolean)


250
251
252
# File 'lib/maestro_plugin/maestro_worker.rb', line 250

def error?
  !(error.nil? or error.empty?)
end

#fieldsObject Also known as: get_fields



365
366
367
# File 'lib/maestro_plugin/maestro_worker.rb', line 365

def fields
  workitem['fields']
end

#get_boolean_field(field) ⇒ Object

Helper that renders a field as a boolean



361
362
363
# File 'lib/maestro_plugin/maestro_worker.rb', line 361

def get_boolean_field(field)
  as_boolean(get_field(field))
end

#get_field(field, default = nil) ⇒ Object

Get a field from workitem, supporting default value



349
350
351
352
353
# File 'lib/maestro_plugin/maestro_worker.rb', line 349

def get_field(field, default = nil)
  value = fields[field]
  value = default if !default.nil? && (value.nil? || (value.respond_to?(:empty?) && value.empty?))
  value
end

#get_int_field(field, default = 0) ⇒ Object

Helper that renders a field as an int



356
357
358
# File 'lib/maestro_plugin/maestro_worker.rb', line 356

def get_int_field(field, default = 0)
  as_int(get_field(field), default)
end

#handle_exception(e) ⇒ Object

Fire supplied exception handlers if supplied, otherwise do nothing



126
127
128
129
130
131
132
# File 'lib/maestro_plugin/maestro_worker.rb', line 126

def handle_exception(e)
  if self.class.exception_handler_method
    send(self.class.exception_handler_method, e)
  elsif self.class.exception_handler_block
    self.class.exception_handler_block.call(e, self)
  end
end

#is_json?(string) ⇒ Boolean Also known as: is_json

Returns:

  • (Boolean)


89
90
91
92
93
94
# File 'lib/maestro_plugin/maestro_worker.rb', line 89

def is_json?(string)
  JSON.parse string
  true
rescue Exception
  false
end

#not_neededObject

Send the “not needed” message to the server.



282
283
284
285
286
287
288
289
# File 'lib/maestro_plugin/maestro_worker.rb', line 282

def not_needed
  workitem[NOT_NEEDED] = true
  send_workitem_message
rescue Exception => e
  Maestro.log.warn "Failed To Send Not Needed Message To Server #{e.class} #{e}: #{e.backtrace.join("\n")}"
ensure
  workitem.delete(NOT_NEEDED)
end

#outputObject



237
238
239
240
241
242
243
244
# File 'lib/maestro_plugin/maestro_worker.rb', line 237

def output
  if MaestroWorker.mock?
    workitem[OUTPUT_META]
  else
    Maestro.log.warn "Output is only accessible when mock is enabled in tests. Otherwise is directly sent to Maestro"
    nil
  end
end

#perform(action, workitem) ⇒ Object

Perform the specified action with the provided workitem. Invokes the method specified by the action parameter.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/maestro_plugin/maestro_worker.rb', line 100

def perform(action, workitem)
  @action, @workitem = action, workitem
  send(action)
  write_output('') # Triggers any remaining buffered output to be sent
  run_callbacks
rescue MaestroDev::Plugin::PluginError => e
  write_output('') # Triggers any remaining buffered output to be sent
  set_error(e.message)
rescue Exception => e
  write_output('') # Triggers any remaining buffered output to be sent
  lowerstack = e.backtrace.find_index(caller[0])
  stack = lowerstack ? e.backtrace[0..lowerstack - 1] : e.backtrace
  msg = "Unexpected error executing task: #{e.class} #{e} at\n" + stack.join("\n")
  Maestro.log.warn("#{msg}\nFull stack:\n" + e.backtrace.join("\n"))

  # Let user-supplied exception handler do its thing
  handle_exception(e)
  set_error(msg)
ensure
  # Older agents expected this method to *maybe* return something
  # .. something that no longer exists, but if we return anything
  # it will be *wrong* :P
  return nil
end

#read_output_value(name) ⇒ Object

Read a value from the context output



151
152
153
154
155
156
157
158
# File 'lib/maestro_plugin/maestro_worker.rb', line 151

def read_output_value(name)
  if get_field(PREVIOUS_CONTEXT_OUTPUTS_META).nil?
    set_field(CONTEXT_OUTPUTS_META, {}) if get_field(CONTEXT_OUTPUTS_META).nil?
    get_field(CONTEXT_OUTPUTS_META)[name]
  else
    get_field(PREVIOUS_CONTEXT_OUTPUTS_META)[name]
  end
end

#reset_buffered_outputObject



232
233
234
235
# File 'lib/maestro_plugin/maestro_worker.rb', line 232

def reset_buffered_output
  @buffered_output = ''
  @last_write_output = Time.now
end

#run_callbacksObject



134
135
136
137
138
139
140
141
142
# File 'lib/maestro_plugin/maestro_worker.rb', line 134

def run_callbacks
  return if self.class.on_complete_handler_block.nil? && self.class.on_complete_handler_method.nil?

  if self.class.on_complete_handler_method
    send(self.class.on_complete_handler_method)
  else
    self.class.on_complete_handler_block.call(workitem)
  end
end

#save_output_value(name, value) ⇒ Object

Set a value in the context output



145
146
147
148
# File 'lib/maestro_plugin/maestro_worker.rb', line 145

def save_output_value(name, value)
  set_field(CONTEXT_OUTPUTS_META, {}) if get_field(CONTEXT_OUTPUTS_META).nil?
  get_field(CONTEXT_OUTPUTS_META)[name] = value
end

#set_error(error) ⇒ Object



254
255
256
# File 'lib/maestro_plugin/maestro_worker.rb', line 254

def set_error(error)
  set_field(ERROR_META, error)
end

#set_field(field, value) ⇒ Object



372
373
374
# File 'lib/maestro_plugin/maestro_worker.rb', line 372

def set_field(field, value)
  fields[field] = value
end

#set_waiting(should_wait) ⇒ Object

Sets the current task as waiting



261
262
263
264
265
266
267
268
# File 'lib/maestro_plugin/maestro_worker.rb', line 261

def set_waiting(should_wait)
  workitem[WAITING_META] = should_wait
  send_workitem_message
rescue Exception => e
  Maestro.log.warn "Failed To Send Waiting Message To Server #{e.class} #{e}: #{e.backtrace.join("\n")}"
ensure
  workitem.delete(WAITING_META) unless should_wait
end

#update_fields_in_record(model, name_or_id, record_field, record_value) ⇒ Object

persistence



294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/maestro_plugin/maestro_worker.rb', line 294

def update_fields_in_record(model, name_or_id, record_field, record_value)
  workitem[PERSIST_META] = true
  workitem[UPDATE_META] = true
  workitem[MODEL_META] = model
  workitem[RECORD_ID_META] = name_or_id.to_s
  workitem[RECORD_FIELD_META] = record_field
  workitem[RECORD_VALUE_META] = record_value

  send_workitem_message

  workitem.delete(PERSIST_META)
  workitem.delete(UPDATE_META)
end

#write_output(output, options = {}) ⇒ Object

Sends the specified ouput string to the server for persistence If called with :buffer as an option, the output will be queued up until a number of writes has occurred, or a reasonable period since the last sent occurred. Any call without the :buffer option will cause any buffered output to be sent immediately.

Example:

write_output("I am Sam\n")                      <-- send immediately
write_output("Sam I am\n", :buffer => true)     <-- buffer for later
write_output("I like Ham\n")                    <-- sends 'Sam I am\nI like Ham\n'


169
170
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
# File 'lib/maestro_plugin/maestro_worker.rb', line 169

def write_output(output, options = {})
  # First time thru?  We need to do some setup!
  reset_buffered_output if @buffered_output.nil?

  @buffered_output += output

  # If a) we have data to write, and
  #    b) its been > 2 seconds since we last sent
  #
  # The 2 second factor is there to allow slowly accumulating data to be sent out more regularly.
  if !@buffered_output.empty? && (!options[:buffer] || Time.now - @last_write_output > 2)
    # Ensure the output is json-able.
    # It seems some code doesn't wholly respect encoding rules.  We've found some http responses that
    # don't have the correct encoding, despite the response headers stating 'utf-8', etc.  Same goes
    # for shell output streams, that don't seem to respect the apps encoding.
    # What this code does is to try to json encode the @buffered_output.  First a direct conversion,
    # if that fails, try to force-encoding to utf-8, if that fails, try to remove all chars with
    # code > 127.  If that fails - we gave it a good shot, and maybe just insert a 'redacted' string
    # so at least the task doesn't fail :)
    begin
      @buffered_output.to_json
    rescue Exception => e1
      Maestro.log.warn("Unable to 'to_json' output [#{e1}]: #{@buffered_output}")
      begin
        test = @buffered_output
        test.force_encoding('UTF-8')
        test.to_json
        # If forcing encoding worked, updated buffered_output
        Maestro.log.warn("Had to force encoding to utf-8 for workitem stream")
        @buffered_output = test
      rescue Exception
        begin
          test = @buffered_output.gsub(/[^\x00-\x7f]/, '?')
          test.to_json
          # If worked, updated buffered_output
          Maestro.log.warn("Had to strip top-bit-set chars for workitem stream")
          @buffered_output = test
        rescue Exception
          Maestro.log.warn("Had to redact block of output, unable to 'to_json' it for workitem stream")
          @buffered_output = '?_?'
        end
      end
    end

    if !MaestroWorker.mock?
      workitem[OUTPUT_META] = @buffered_output
    else
      # Test mode, we want to retain output - normal operation clears out
      # data after it is sent
      workitem[OUTPUT_META] = '' if !workitem[OUTPUT_META]
      workitem[OUTPUT_META] = workitem[OUTPUT_META] + @buffered_output
    end

    workitem[STREAMING_META] = true
    send_workitem_message
    reset_buffered_output
  end
rescue Exception => e
  Maestro.log.warn "Unable To Write Output To Server #{e.class} #{e}: #{e.backtrace.join("\n")}"
ensure
  workitem.delete(STREAMING_META)
end