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



30
31
32
# File 'app/models/job.rb', line 30

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



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

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



26
27
28
# File 'app/models/job.rb', line 26

def self.NOT_STARTED
  -1
end

.params_to_time(p) ⇒ Object



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

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



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

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



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'app/models/job.rb', line 340

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)


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

def active?
  pc >= 0
end

#active_predecessorsObject



388
389
390
# File 'app/models/job.rb', line 388

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

#append_step(step) ⇒ Object



190
191
192
193
194
195
# File 'app/models/job.rb', line 190

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

#append_steps(steps) ⇒ Object



183
184
185
186
187
188
# File 'app/models/job.rb', line 183

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

#argumentsObject



221
222
223
224
225
226
227
228
229
# File 'app/models/job.rb', line 221

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



179
180
181
# File 'app/models/job.rb', line 179

def backtrace
  job_state
end

#cancel(user) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
# File 'app/models/job.rb', line 289

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



212
213
214
215
216
217
218
219
# File 'app/models/job.rb', line 212

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

#done?Boolean

NOTES

job.pc

0: active / runnning (CANNOT GET PC > 0, IS THAT POSSIBLE?) 0: active / runnning -1: not started -2: completed (could be error/abort/cancel)

job.status checks if >=0 or -1 calculates value if -2

UX flow job created => user_id = nil, pc = -1 job started => user_id = , pc = 0 job completed => user_id = , pc = -2 pc cannot be > 0 (the finish_show method in lib/krill/base.rb has a line that increments jobs.pc but it is commented out)

TODO: WE SHOULD CREATE A JOB_STATUSES TABLE IN THE DB TO TRACK STATUSES THE TABLE SHOULD HAVE , , FIELDS, WHERE = ACTUAL STATUS AND = A DONE FLAG

  ACTUAL STATUSES
    - pending
    - running
    - completed
    - errored
    - aborted
    - canceled

   DONE FLAG
     0 = not done
    -1 = done with error
     1 = done without error

Returns:

  • (Boolean)


145
146
147
# File 'app/models/job.rb', line 145

def done?
  pc == Job.COMPLETED # -2
end

#error?Boolean

Returns:

  • (Boolean)


317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/job.rb', line 317

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



336
337
338
# File 'app/models/job.rb', line 336

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

#error_messageObject



332
333
334
# File 'app/models/job.rb', line 332

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

#exportObject



360
361
362
363
364
365
366
367
368
369
# File 'app/models/job.rb', line 360

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



402
403
404
405
406
407
# File 'app/models/job.rb', line 402

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)


301
302
303
304
305
306
307
# File 'app/models/job.rb', line 301

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

#nameObject



384
385
386
# File 'app/models/job.rb', line 384

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

#not_started?Boolean

Returns:

  • (Boolean)


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

def not_started?
  pc == Job.NOT_STARTED # -1
end

#num_postsObject



356
357
358
# File 'app/models/job.rb', line 356

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



411
412
413
# File 'app/models/job.rb', line 411

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



22
23
24
# File 'app/models/job.rb', line 22

def operations
  job_associations.collect(&:operation)
end

#pending?Boolean

Returns:

  • (Boolean)


153
154
155
# File 'app/models/job.rb', line 153

def pending?
  not_started?
end

#plankton?Boolean

Returns:

  • (Boolean)


309
310
311
312
313
314
315
# File 'app/models/job.rb', line 309

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

#remove_types(p) ⇒ Object



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

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



392
393
394
395
396
397
398
399
# File 'app/models/job.rb', line 392

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



274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'app/models/job.rb', line 274

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



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

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


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'app/models/job.rb', line 231

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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/models/job.rb', line 161

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



371
372
373
374
375
376
377
378
379
380
381
382
# File 'app/models/job.rb', line 371

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



200
201
202
203
204
205
206
207
# File 'app/models/job.rb', line 200

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