Class: Job

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/job.rb

Overview

Defines a batch of executable Operations of the same type that can be run together. Jobs are executed with the protocol of the OperationType. Protocols must handle being able to run Jobs with varying amounts of Operations.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.COMPLETEDObject



28
29
30
# File 'app/models/job.rb', line 28

def self.COMPLETED
  -2
end

.create_from(operations:, user:, group:) ⇒ Object

Creates a Job with the list of operations for the user and group.

Parameters:

  • operations (OperationsList)

    the list of operations

  • user (User)

    the user scheduling the job

  • group (Group)

    the group for the user



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'app/models/job.rb', line 48

def self.create_from(operations:, user:, group:)
  job = Job.new
  job.path = 'operation.rb'
  job.pc = Job.NOT_STARTED
  operation_type_id = operations.first.operation_type.id
  job.set_arguments(operation_type_id: operation_type_id)
  job.group_id = group.id
  job. = user.id
  job.desired_start_time = Time.now
  job.latest_start_time = Time.now + 1.hour
  job.save

  operations.each do |operation|
    JobAssociation.create(job_id: job.id, operation_id: operation.id)
    operation.save
  end

  job
end

.NOT_STARTEDObject



24
25
26
# File 'app/models/job.rb', line 24

def self.NOT_STARTED
  -1
end

.params_to_time(p) ⇒ Object



100
101
102
103
104
105
106
107
# File 'app/models/job.rb', line 100

def self.params_to_time(p)
  DateTime.civil_from_format(:local,
                             p['dt(1i)'].to_i,
                             p['dt(2i)'].to_i,
                             p['dt(3i)'].to_i,
                             p['dt(4i)'].to_i,
                             p['dt(5i)'].to_i).to_time
end

.schedule(operations:, user:, group: Group.technicians) ⇒ Job

Creates a Job from the list of operations. Defers an operation if it has a primed predecessor.

Parameters:

  • operations (OperationsList)

    the operations

  • user (User)

    the user scheduling the Job

  • group (Group) (defaults to: Group.technicians)

    the group of the user

Returns:

  • (Job)

    the job of scheduled operations



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'app/models/job.rb', line 75

def self.schedule(operations:, user:, group: Group.technicians)
  ops_to_schedule = []
  ops_to_defer = []

  operations.each do |op|
    pps = op.primed_predecessors
    if pps.empty?
      ops_to_schedule << op
    else
      ops_to_schedule += pps
      ops_to_defer << op
    end
  end

  unless ops_to_defer.empty?
    Job.create_from(operations: ops_to_defer, user: user, group: group)
    ops_to_defer.each(&:defer)
  end

  job = Job.create_from(operations: ops_to_schedule, user: user, group: group)
  ops_to_schedule.each(&:schedule)

  job
end

Instance Method Details

#abort_krillObject



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'app/models/job.rb', line 304

def abort_krill
  self.pc = Job.COMPLETED

  state = job_state
  if state.length.odd? # backtrace ends with a 'next'
    append_step operation: 'display', content: [
      { title: 'Interrupted' },
      { note: "This step was being prepared by the protocol when the 'abort' signal was received." }
    ]
  end

  # add next and final
  append_step operation: 'next', time: Time.now, inputs: {}
  append_step operation: 'aborted', rval: {}
end

#active?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'app/models/job.rb', line 121

def active?
  pc >= 0
end

#active_predecessorsObject



352
353
354
# File 'app/models/job.rb', line 352

def active_predecessors
  predecessors.reject(&:done?)
end

#append_step(step) ⇒ Object



154
155
156
157
158
159
# File 'app/models/job.rb', line 154

def append_step(step)
  bt = backtrace
  bt.push step
  self.state = Oj.dump(bt, mode: :compat)
  save
end

#append_steps(steps) ⇒ Object



147
148
149
150
151
152
# File 'app/models/job.rb', line 147

def append_steps(steps)
  bt = backtrace
  bt.concat steps
  self.state = Oj.dump(bt, mode: :compat)
  save
end

#argumentsObject



185
186
187
188
189
190
191
192
193
# File 'app/models/job.rb', line 185

def arguments
  if /\.rb$/.match?(path)
    JSON.parse(state).first['arguments']
  else
    JSON.parse(state)['stack'].first.reject { |k, _v| k == 'user_id' }
  end
rescue JSON::ParserError
  { error: 'unable to parse arguments' }
end

#backtraceObject



143
144
145
# File 'app/models/job.rb', line 143

def backtrace
  job_state
end

#cancel(user) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
# File 'app/models/job.rb', line 253

def cancel(user)
  return if done?

  self.pc = Job.COMPLETED
  self.user_id = user.id
  if /\.rb$/.match?(path)
    Krill::Client.new.abort(id)
    abort_krill
  end
  save
end

#doerString

Gets the login of the user who performed this Job.

Returns:

  • (String)

    user login



176
177
178
179
180
181
182
183
# File 'app/models/job.rb', line 176

def doer
  u = User.find_by(id: user_id.to_i)
  if u
    u.
  else
    '?'
  end
end

#done?Boolean

Returns:

  • (Boolean)


109
110
111
# File 'app/models/job.rb', line 109

def done?
  pc == Job.COMPLETED
end

#error?Boolean

Returns:

  • (Boolean)


281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'app/models/job.rb', line 281

def error?
  if krill?
    begin
      done? && backtrace.last[:operation] != 'complete'
    rescue StandardError
      true
    end
  elsif plankton?
    entries = logs.reject { |l| l.entry_type != 'CANCEL' && l.entry_type != 'ERROR' && l.entry_type != 'ABORT' }
    !entries.empty?
  else
    false
  end
end

#error_backtraceObject



300
301
302
# File 'app/models/job.rb', line 300

def error_backtrace
  backtrace[-3][:backtrace]
end

#error_messageObject



296
297
298
# File 'app/models/job.rb', line 296

def error_message
  backtrace[-3][:message]
end

#exportObject



324
325
326
327
328
329
330
331
332
333
# File 'app/models/job.rb', line 324

def export
  a = attributes
  begin
    a['backtrace'] = JSON.parse a['state'], symbolize_names: true
  rescue StandardError
    a['backtrace'] = { error: 'Could not parse backtrace.' }
  end
  a.delete 'state'
  a
end

#job_stateObject

hides the fact that state is stored as JSON



366
367
368
369
370
371
# File 'app/models/job.rb', line 366

def job_state
  JSON.parse(state, symbolize_names: true)
rescue JSON::ParserError
  # TODO: make this an exception object
  raise "Error: parse error reading state of job #{id}"
end

#krill?Boolean

Returns:

  • (Boolean)


265
266
267
268
269
270
271
# File 'app/models/job.rb', line 265

def krill?
  if /\.rb$/.match?(path)
    true
  else
    false
  end
end

#nameObject



348
349
350
# File 'app/models/job.rb', line 348

def name
  path.split('/').last.split('.').first
end

#not_started?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'app/models/job.rb', line 113

def not_started?
  pc == Job.NOT_STARTED
end

#num_postsObject



320
321
322
# File 'app/models/job.rb', line 320

def num_posts
  post_associations.count
end

#operation_typeObject

get the operation type for the operations of this job TODO: seems like this should be a delegate, but not clear if can do it



375
376
377
# File 'app/models/job.rb', line 375

def operation_type
  operations.first.operation_type
end

#operationsArray<Operation>

A list of all Operations in this Job.

Returns:

  • (Array<Operation>)

    operations in this Job



20
21
22
# File 'app/models/job.rb', line 20

def operations
  job_associations.collect(&:operation)
end

#pending?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'app/models/job.rb', line 117

def pending?
  not_started?
end

#plankton?Boolean

Returns:

  • (Boolean)


273
274
275
276
277
278
279
# File 'app/models/job.rb', line 273

def plankton?
  if /\.pl$/.match?(path)
    true
  else
    false
  end
end

#remove_types(p) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/job.rb', line 212

def remove_types(p)
  case p
  when String, Integer, Float, TrueClass, FalseClass
    p
  when Hash
    h = {}
    p.each_key do |key|
      h[key.to_s.split(' ')[0].to_sym] = remove_types(p[key])
    end
    h
  when Array
    p.collect do |a|
      remove_types a
    end
  end
end

#resetObject



356
357
358
359
360
361
362
363
# File 'app/models/job.rb', line 356

def reset
  puts Krill::Client.new.abort(id)
  self.state = [{ 'operation' => 'initialize', 'arguments' => {}, 'time' => '2017-06-02T11:40:20-07:00' }].to_json
  self.pc = 0
  save
  puts Krill::Client.new.start(id)
  reload
end

#return_valueHash

Get the value returned by the last line of the main method in the protocol which ran this Job.

Returns:

  • (Hash)

    JSON parsed object which was returned by the Job



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'app/models/job.rb', line 238

def return_value
  if /\.rb$/.match?(path)
    begin
      @rval = job_state.last[:rval] || {}
    rescue StandardError
      @rval = { error: 'Could not find return value.' }
    end
  else
    entries = logs.select { |l| l.entry_type == 'return' }
    return nil if entries.empty?

    JSON.parse(entries.first.data, symbolize_names: true)
  end
end

#set_arguments(a) ⇒ Object



229
230
231
232
233
# File 'app/models/job.rb', line 229

def set_arguments(a)
  raise 'Could not set arguments of non-krill protocol' unless /\.rb$/.match?(path)

  self.state = [{ operation: 'initialize', arguments: (remove_types a), time: Time.now }].to_json
end


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/models/job.rb', line 195

def start_link(el, opts = {})
  options = { confirm: false }.merge opts
  confirm = options[:confirm] ? "class='confirm'" : ''

  if /\.rb$/.match?(path)
    if not_started?
      "<a #{confirm} target=_top href='/krill/start?job=#{id}'>#{el}</a>".html_safe
    else
      "<a #{confirm} target=_top href='/krill/ui?job=#{id}'>#{el}</a>".html_safe
    end
  elsif not_started?
    "<a #{confirm} target=_top href='/interpreter/advance?job=#{id}'>#{el}</a>".html_safe
  elsif pc != Job.COMPLETED
    "<a #{confirm} target=_top href='/interpreter/current?job=#{id}'>#{el}</a>".html_safe
  end
end

#statusObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'app/models/job.rb', line 125

def status
  if active?
    status = 'ACTIVE'
  elsif not_started?
    status = 'PENDING'
  else
    entries = (logs.reject do |log|
      log.entry_type != 'ERROR' && log.entry_type != 'ABORT' && log.entry_type != 'CANCEL'
    end).collect(&:entry_type)
    status = if !entries.empty?
               entries[0] == 'ERROR' ? entries[0] : entries[0] + 'ED'
             else
               'COMPLETED'
             end
  end
  status
end

#step_workflowObject



335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/models/job.rb', line 335

def step_workflow
  return unless workflow_process

  begin
    wp = WorkflowProcess.find(workflow_process.id)
    wp.record_result_of self
    wp.step
  rescue StandardError => e
    # TODO: not clear if this should rescue ActiveRecord::RecordNotFound
    Rails.logger.info 'Error trying to step workflow process ' + e.to_s
  end
end

#submitterString

Gets the login of the user who submitted this Job.

Returns:

  • (String)

    user login



164
165
166
167
168
169
170
171
# File 'app/models/job.rb', line 164

def submitter
  u = User.find_by(id: )
  if u
    u.
  else
    '?'
  end
end