naf 2.0.4 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.rdoc +28 -4
  2. data/RELEASE_NOTES.rdoc +8 -0
  3. data/app/assets/javascripts/dataTablesTemplates/application_schedules.js +72 -0
  4. data/app/assets/javascripts/dataTablesTemplates/applications.js +12 -21
  5. data/app/controllers/naf/application_schedule_affinity_tabs_controller.rb +5 -8
  6. data/app/controllers/naf/application_schedules_controller.rb +98 -0
  7. data/app/controllers/naf/applications_controller.rb +6 -98
  8. data/app/controllers/naf/historical_jobs_controller.rb +18 -8
  9. data/app/helpers/naf/application_helper.rb +47 -22
  10. data/app/models/logical/naf/application.rb +9 -126
  11. data/app/models/logical/naf/application_schedule.rb +186 -0
  12. data/app/models/logical/naf/construction_zone/boss.rb +4 -5
  13. data/app/models/logical/naf/construction_zone/foreman.rb +34 -13
  14. data/app/models/logical/naf/construction_zone/proletariat.rb +1 -0
  15. data/app/models/logical/naf/construction_zone/work_order.rb +11 -4
  16. data/app/models/logical/naf/job_fetcher.rb +1 -0
  17. data/app/models/naf/application.rb +5 -16
  18. data/app/models/naf/application_schedule.rb +117 -43
  19. data/app/models/naf/historical_job.rb +6 -3
  20. data/app/models/naf/queued_job.rb +3 -0
  21. data/app/models/naf/run_interval_style.rb +20 -0
  22. data/app/models/naf/running_job.rb +3 -0
  23. data/app/models/process/naf/data_migration/backfill_application_schedule_run_interval.rb +85 -0
  24. data/app/models/process/naf/log_archiver.rb +1 -1
  25. data/app/models/process/naf/log_reader.rb +141 -0
  26. data/app/models/process/naf/runner.rb +6 -49
  27. data/app/views/naf/application_schedule_affinity_tabs/_form.html.erb +1 -1
  28. data/app/views/naf/{applications → application_schedules}/_application_schedule_prerequisites.html.erb +1 -1
  29. data/app/views/naf/application_schedules/_form.html.erb +120 -0
  30. data/app/views/naf/application_schedules/edit.html.erb +11 -0
  31. data/app/views/naf/application_schedules/index.html.erb +47 -0
  32. data/app/views/naf/application_schedules/index.json.erb +8 -0
  33. data/app/views/naf/application_schedules/new.html.erb +11 -0
  34. data/app/views/naf/application_schedules/show.html.erb +130 -0
  35. data/app/views/naf/applications/_form.html.erb +44 -106
  36. data/app/views/naf/applications/_search_container.html.erb +29 -29
  37. data/app/views/naf/applications/index.html.erb +1 -10
  38. data/app/views/naf/applications/index.json.erb +22 -6
  39. data/app/views/naf/applications/show.html.erb +47 -97
  40. data/app/views/naf/logger_styles/_form.html.erb +0 -3
  41. data/app/views/naf/machines/_form.html.erb +3 -4
  42. data/config/routes.rb +3 -4
  43. data/db/migrate/20131219195439_add_run_interval_styles_table.rb +49 -0
  44. data/db/migrate/20140113183243_drop_run_start_minute_from_application_schedules.rb +18 -0
  45. data/lib/naf/version.rb +1 -1
  46. data/naf.gemspec +1 -1
  47. data/spec/controllers/naf/application_schedule_affinity_tabs_controller_spec.rb +34 -27
  48. data/spec/controllers/naf/applications_controller_spec.rb +0 -48
  49. data/spec/factories/naf.rb +14 -8
  50. data/spec/models/logical/naf/application_spec.rb +9 -37
  51. data/spec/models/logical/naf/machine_spec.rb +1 -1
  52. data/spec/models/naf/application_schedule_spec.rb +38 -50
  53. data/spec/models/naf/application_spec.rb +3 -3
  54. data/spec/models/naf/historical_job_spec.rb +4 -2
  55. data/spec/models/naf/queued_job_spec.rb +2 -0
  56. data/spec/models/naf/run_interval_style_spec.rb +28 -0
  57. data/spec/models/naf/running_job_spec.rb +2 -0
  58. metadata +19 -7
  59. data/app/models/logical/naf/job_creator.rb +0 -151
  60. data/app/views/naf/applications/_application_schedule.html.erb +0 -80
  61. data/spec/models/logical/naf/job_creator_spec.rb +0 -102
@@ -80,12 +80,11 @@ module Logical::Naf::ConstructionZone
80
80
  def enqueue_n_commands_on_machines(parameters, number_of_jobs = :from_limit, machines = [])
81
81
  logger.detail "enqueuing #{parameters[:command]} #{number_of_jobs} time(s) on #{machines.length} machine(s)"
82
82
  machines.each do |machine|
83
- number_of_jobs = (parameters[:application_run_group_limit] || 1) if number_of_jobs == :from_limit
83
+ number_of_jobs = (parameters[:application_run_group_quantum] || 1) if number_of_jobs == :from_limit
84
84
  logger.info "enqueuing #{parameters[:command]} #{number_of_jobs} time(s) on #{machine}"
85
85
  (1..number_of_jobs).each do
86
86
  machine_parameters = {
87
- :application_run_group_limit => number_of_jobs,
88
- :application_run_group_restriction => ::Naf::ApplicationRunGroupRestriction.limited_per_machine
87
+ application_run_group_restriction: ::Naf::ApplicationRunGroupRestriction.limited_per_machine
89
88
  }.merge(parameters)
90
89
  machine_parameters[:affinities] =
91
90
  [machine.affinity] + if machine_parameters[:affinities].nil?
@@ -102,10 +101,10 @@ module Logical::Naf::ConstructionZone
102
101
  end
103
102
 
104
103
  def enqueue_n_commands(parameters, number_of_jobs = :from_limit)
105
- number_of_jobs = (parameters[:application_run_group_limit] || 1) if number_of_jobs == :from_limit
104
+ number_of_jobs = (parameters[:application_run_group_quantum] || 1) if number_of_jobs == :from_limit
106
105
  logger.info "enqueuing #{parameters[:command]} #{number_of_jobs} time(s) on #{machine}"
107
106
  (1..number_of_jobs).each do
108
- work_order = AdHocWorkOrder.new({:application_run_group_limit => number_of_jobs}.merge(parameters))
107
+ work_order = AdHocWorkOrder.new(parameters)
109
108
  @foreman.enqueue(work_order)
110
109
  end
111
110
  end
@@ -11,7 +11,8 @@ module Logical::Naf::ConstructionZone
11
11
  unless work_order.enqueue_backlogs
12
12
  if limited_by_run_group?(work_order.application_run_group_restriction,
13
13
  work_order.application_run_group_name,
14
- work_order.application_run_group_limit)
14
+ work_order.application_run_group_limit,
15
+ work_order.historical_job_affinity_tab_parameters)
15
16
  logger.warn "work order limited by run queue limits #{work_order.inspect}"
16
17
  return nil
17
18
  end
@@ -21,26 +22,46 @@ module Logical::Naf::ConstructionZone
21
22
  work_order.historical_job_prerequisite_historical_jobs)
22
23
  end
23
24
 
24
- def limited_by_run_group?(application_run_group_restriction, application_run_group_name, application_run_group_limit)
25
+ def limited_by_run_group?(application_run_group_restriction, application_run_group_name, application_run_group_limit, affinities)
25
26
  if (application_run_group_restriction.id == ::Naf::ApplicationRunGroupRestriction.no_limit.id ||
26
27
  application_run_group_limit.nil? ||
27
28
  application_run_group_name.nil?)
28
29
  false
29
30
  elsif application_run_group_restriction.id == ::Naf::ApplicationRunGroupRestriction.limited_per_machine.id
30
- # XXX this is difficult to figure out, so we punt for now
31
- # XXX we should check if there is any machine affinity (must pass that in) and
32
- # XXX if so check if that machine has this application group running on it.
33
- # XXX but this code is only used as a heuristic for queues
31
+ # Retrieve the affinity associated to the machine
32
+ machine_affinity = nil
33
+ affinities.each do |affinity|
34
+ machine_affinity = ::Naf::Affinity.find_by_id(affinity[:affinity_id])
35
+ if machine_affinity.affinity_classification_name == 'machine'
36
+ break
37
+ end
38
+ end
34
39
 
35
- #(::Naf::QueuedJob.where(:application_run_group_name => application_run_group_name).count +
36
- #::Naf::RunningJob.where(:application_run_group_name => application_run_group_name,
37
- #:started_on_machine_id => @machine.id).count) >= application_run_group_limit
40
+ # If affinity is present, check if the sum of jobs running on the machine
41
+ # and queued for the machine is less the application_run_group_limit.
42
+ # If affinity is not present, send a log warning the user that application schedule
43
+ # should have affinity associated to the machine in order to behave correctly, and
44
+ # queue the application.
45
+ if machine_affinity.present?
46
+ queued_jobs = ::Naf::QueuedJob.
47
+ joins(:historical_job).
48
+ joins("INNER JOIN #{Naf.schema_name}.historical_job_affinity_tabs AS hjat
49
+ ON hjat.historical_job_id = #{Naf.schema_name}.historical_jobs.id").
50
+ where("#{Naf.schema_name}.historical_jobs.application_run_group_name = ? AND hjat.affinity_id = ?",
51
+ application_run_group_name, machine_affinity.id).count
52
+ running_jobs = ::Naf::RunningJob.where(
53
+ application_run_group_name: application_run_group_name,
54
+ started_on_machine_id: machine_affinity.affinity_name
55
+ ).count
38
56
 
39
- # XXX just returning false
40
- false
57
+ queued_jobs + running_jobs >= application_run_group_limit
58
+ else
59
+ logger.warn "application schedule does not have affinity associated with a machine"
60
+ false
61
+ end
41
62
  elsif application_run_group_restriction.id == ::Naf::ApplicationRunGroupRestriction.limited_per_all_machines.id
42
- (::Naf::QueuedJob.where(:application_run_group_name => application_run_group_name).count +
43
- ::Naf::RunningJob.where(:application_run_group_name => application_run_group_name).count) >= application_run_group_limit
63
+ (::Naf::QueuedJob.where(application_run_group_name: application_run_group_name).count +
64
+ ::Naf::RunningJob.where(application_run_group_name: application_run_group_name).count) >= application_run_group_limit
44
65
  else
45
66
  logger.warn "not limited by run group restriction but don't know why: #{application_run_group_restriction.inspect}"
46
67
  true
@@ -27,6 +27,7 @@ module Logical::Naf::ConstructionZone
27
27
 
28
28
  def create_queued_job(historical_job)
29
29
  queued_job = ::Naf::QueuedJob.new(application_id: historical_job.application_id,
30
+ application_schedule_id: historical_job.application_schedule_id,
30
31
  application_type_id: historical_job.application_type_id,
31
32
  command: historical_job.command,
32
33
  application_run_group_restriction_id: historical_job.application_run_group_restriction_id,
@@ -1,8 +1,14 @@
1
1
  module Logical::Naf::ConstructionZone
2
2
  class WorkOrder
3
- attr_reader :command, :application_type, :application_run_group_restriction, :application_run_group_name
4
- attr_reader :application_run_group_limit, :priority, :enqueue_backlogs
5
- attr_reader :application, :application_schedule
3
+ attr_reader :command,
4
+ :application_type,
5
+ :application_run_group_restriction,
6
+ :application_run_group_name,
7
+ :application_run_group_limit,
8
+ :priority,
9
+ :enqueue_backlogs,
10
+ :application,
11
+ :application_schedule
6
12
 
7
13
  def initialize(command,
8
14
  application_type = ::Naf::ApplicationType.rails,
@@ -42,7 +48,8 @@ module Logical::Naf::ConstructionZone
42
48
  application_run_group_name: application_run_group_name,
43
49
  application_run_group_limit: application_run_group_limit,
44
50
  priority: priority,
45
- application_id: application.try(:id)
51
+ application_id: application.try(:id),
52
+ application_schedule_id: application_schedule.try(:id)
46
53
  }
47
54
  end
48
55
 
@@ -117,6 +117,7 @@ module Logical
117
117
  if historical_job.present?
118
118
  ::Naf::QueuedJob.delete(historical_job.id)
119
119
  running_job = ::Naf::RunningJob.new(application_id: historical_job.application_id,
120
+ application_schedule_id: historical_job.application_schedule_id,
120
121
  application_type_id: historical_job.application_type_id,
121
122
  command: historical_job.command,
122
123
  application_run_group_restriction_id: historical_job.application_run_group_restriction_id,
@@ -7,8 +7,8 @@ module Naf
7
7
  :log_level,
8
8
  :short_name,
9
9
  :deleted,
10
- :application_schedule,
11
- :application_schedule_attributes
10
+ :application_schedules,
11
+ :application_schedules_attributes
12
12
 
13
13
  #---------------------
14
14
  # *** Associations ***
@@ -16,7 +16,7 @@ module Naf
16
16
 
17
17
  belongs_to :application_type,
18
18
  class_name: '::Naf::ApplicationType'
19
- has_one :application_schedule,
19
+ has_many :application_schedules,
20
20
  class_name: '::Naf::ApplicationSchedule',
21
21
  dependent: :destroy
22
22
  has_many :historical_jobs,
@@ -35,12 +35,11 @@ module Naf
35
35
  allow_nil: true,
36
36
  format: {
37
37
  with: /^[a-zA-Z_][a-zA-Z0-9_]*$/,
38
- message: "letters should be first"
38
+ message: "short name consists of only letters/numbers/underscores, and it needs to start with a letter/underscore"
39
39
  }
40
40
 
41
- validate :check_references_with_application_schedule_prerequisites
42
41
  before_save :check_blank_values
43
- accepts_nested_attributes_for :application_schedule, allow_destroy: true
42
+ accepts_nested_attributes_for :application_schedules, allow_destroy: true
44
43
 
45
44
  #--------------------
46
45
  # *** Delegations ***
@@ -85,16 +84,6 @@ module Naf
85
84
  self.log_level = nil if self.log_level.blank?
86
85
  end
87
86
 
88
- def check_references_with_application_schedule_prerequisites
89
- if application_schedule.try(:marked_for_destruction?)
90
- prerequisites = Naf::ApplicationSchedulePrerequisite.
91
- where(prerequisite_application_schedule_id: application_schedule.id).all
92
- unless prerequisites.blank?
93
- errors.add(:base, "Cannot delete scheduler, because the following applications are referenced to it: " +
94
- "#{prerequisites.map{ |pre| pre.application_schedule.title }.join(', ') }")
95
- end
96
- end
97
- end
98
87
 
99
88
  end
100
89
  end
@@ -10,10 +10,14 @@ module Naf
10
10
  :priority,
11
11
  :visible,
12
12
  :enabled,
13
- :run_start_minute,
14
13
  :application_run_group_limit,
14
+ :application_run_group_quantum,
15
15
  :application_schedule_prerequisites_attributes,
16
- :enqueue_backlogs
16
+ :enqueue_backlogs,
17
+ :run_interval_style_id,
18
+ :application,
19
+ :run_interval_style,
20
+ :application_run_group_restriction
17
21
 
18
22
  SCHEDULES_LOCK_ID = 0
19
23
 
@@ -25,6 +29,8 @@ module Naf
25
29
  class_name: '::Naf::Application'
26
30
  belongs_to :application_run_group_restriction,
27
31
  class_name: '::Naf::ApplicationRunGroupRestriction'
32
+ belongs_to :run_interval_style,
33
+ class_name: '::Naf::RunIntervalStyle'
28
34
  has_many :application_schedule_affinity_tabs,
29
35
  class_name: '::Naf::ApplicationScheduleAffinityTab',
30
36
  dependent: :destroy
@@ -44,7 +50,10 @@ module Naf
44
50
  # *** Validations ***
45
51
  #++++++++++++++++++++
46
52
 
47
- validates :application_run_group_restriction_id, presence: true
53
+ validates :application_run_group_restriction_id,
54
+ :run_interval_style_id,
55
+ :application_id,
56
+ :priority, presence: true
48
57
  validates :priority, numericality: {
49
58
  only_integer: true,
50
59
  greater_than: -2147483648,
@@ -56,12 +65,6 @@ module Naf
56
65
  less_than: 2147483647,
57
66
  allow_blank: true
58
67
  }
59
- validates :run_start_minute, numericality: {
60
- only_integer: true,
61
- greater_than_or_equal_to: 0,
62
- less_than: 24*60,
63
- allow_blank: true
64
- }
65
68
  validates :run_interval, numericality: {
66
69
  only_integer: true,
67
70
  greater_than_or_equal_to: 0,
@@ -71,9 +74,8 @@ module Naf
71
74
 
72
75
  before_save :check_blank_values
73
76
  validate :visible_enabled_check
74
- validate :run_interval_at_time_check
75
- validate :enabled_application_id_unique
76
77
  validate :prerequisite_application_schedule_id_uniqueness
78
+ validate :run_interval_check
77
79
 
78
80
  #--------------------
79
81
  # *** Delegations ***
@@ -94,17 +96,102 @@ module Naf
94
96
  unlock_record(SCHEDULES_LOCK_ID)
95
97
  end
96
98
 
97
- def self.exact_schedules
98
- where('run_start_minute is not null')
99
+ # find the exact based schedules that should be queued
100
+ # select anything that
101
+ # isn't currently running (or queued) AND
102
+ # hasn't run since run_interval AND
103
+ # should have been run by now
104
+ def self.exact_schedules(time, not_finished_applications, application_last_runs)
105
+ custom_current_time = time.to_date + time.strftime('%H').to_i.hours + time.strftime('%M').to_i.minutes
106
+ schedules = ::Naf::ApplicationSchedule.
107
+ joins(:run_interval_style).
108
+ where("#{Naf.schema_name}.run_interval_styles.name IN (?)", ['at beginning of day', 'at beginning of hour']).
109
+ enabled.select do |schedule|
110
+
111
+ interval_time = time.to_date
112
+ if schedule.run_interval_style.name == 'at beginning of day'
113
+ interval_time += schedule.run_interval.minutes
114
+ elsif schedule.run_interval_style.name == 'at beginning of hour'
115
+ interval_time += time.strftime('%H').to_i.hours + schedule.run_interval.minutes
116
+ end
117
+
118
+ (not_finished_applications[schedule.id].nil? &&
119
+ (application_last_runs[schedule.id].nil? ||
120
+ (interval_time >= application_last_runs[schedule.id].finished_at)
121
+ ) &&
122
+ (custom_current_time - interval_time) == 0.seconds
123
+ )
124
+ end
125
+
126
+ schedules
99
127
  end
100
128
 
101
- def self.relative_schedules
102
- where('run_interval >= 0')
129
+ # find the interval based schedules that should be queued
130
+ # select anything that isn't currently running and completed
131
+ # running more than run_interval minutes ago
132
+ def self.relative_schedules(time, not_finished_applications, application_last_runs)
133
+ schedules = ::Naf::ApplicationSchedule.
134
+ joins(:run_interval_style).
135
+ where("#{Naf.schema_name}.run_interval_styles.name = ?", 'after previous run').
136
+ enabled.select do |schedule|
137
+
138
+ (not_finished_applications[schedule.id].nil? &&
139
+ (application_last_runs[schedule.id].nil? ||
140
+ (time - application_last_runs[schedule.id].finished_at) > schedule.run_interval.minutes
141
+ )
142
+ )
143
+ end
144
+
145
+ schedules
146
+ end
147
+
148
+ def self.constant_schedules
149
+ ::Naf::ApplicationSchedule.
150
+ joins(:run_interval_style).
151
+ where("#{Naf.schema_name}.run_interval_styles.name = ?", 'keep running').
152
+ enabled
153
+ end
154
+
155
+ def self.enabled
156
+ where(enabled: true)
157
+ end
158
+
159
+ def self.should_be_queued
160
+ current_time = Time.zone.now
161
+ # Applications that are still running
162
+ not_finished_applications = ::Naf::HistoricalJob.
163
+ queued_between(current_time - Naf::HistoricalJob::JOB_STALE_TIME, current_time).
164
+ where("finished_at IS NULL AND request_to_terminate = false").
165
+ find_all{ |job| job.application_schedule_id.present? }.
166
+ index_by{ |job| job.application_schedule_id }
167
+
168
+ # Last ran job for each application
169
+ application_last_runs = ::Naf::HistoricalJob.application_last_runs.
170
+ index_by{ |job| job.application_schedule_id }
171
+
172
+ relative_schedules = ::Naf::ApplicationSchedule.
173
+ relative_schedules(current_time, not_finished_applications, application_last_runs)
174
+ exact_schedules = ::Naf::ApplicationSchedule.
175
+ exact_schedules(current_time, not_finished_applications, application_last_runs)
176
+ constant_schedules = ::Naf::ApplicationSchedule.constant_schedules
177
+
178
+ foreman = ::Logical::Naf::ConstructionZone::Foreman.new
179
+ return (relative_schedules + exact_schedules + constant_schedules).select do |schedule|
180
+ affinities = []
181
+ schedule.affinities.each do |affinity|
182
+ affinities << { affinity_id: affinity.id }
183
+ end
184
+
185
+ schedule.enqueue_backlogs || !foreman.limited_by_run_group?(schedule.application_run_group_restriction,
186
+ schedule.application_run_group_name,
187
+ schedule.application_run_group_limit,
188
+ affinities)
189
+ end
103
190
  end
104
191
 
105
- def self.pickables
192
+ def self.pickleables
106
193
  # check the application is deleted
107
- self.where(:deleted => false)
194
+ self.where(deleted: false)
108
195
  end
109
196
 
110
197
  #-------------------------
@@ -124,11 +211,7 @@ module Naf
124
211
  end
125
212
  components << "id: #{id}"
126
213
  components << "\"#{application.title}\""
127
- if run_start_minute
128
- components << "start at: #{"%02d" % (run_start_minute/60)}:#{"%02d" % (run_start_minute%60)}"
129
- else
130
- components << "start every: #{run_interval} minutes"
131
- end
214
+ components << ::Logical::Naf::ApplicationSchedule.new(self).display
132
215
 
133
216
  return "::Naf::ApplicationSchedule<#{components.join(', ')}>"
134
217
  end
@@ -140,31 +223,22 @@ module Naf
140
223
  end
141
224
  end
142
225
 
143
- def enabled_application_id_unique
144
- return unless enabled
145
-
146
- if id
147
- conditions = ["id <> ? AND application_id = ? AND enabled = ?", id, application_id, true]
226
+ # When rolling back from Naf v2.1 to v2.0, check whether run_interval
227
+ # or run_start_minute is nil. Otherwise, just check the presence of
228
+ # run_interval.
229
+ def run_interval_check
230
+ if self.attributes.keys.include?('run_start_minute')
231
+ if !run_start_minute.present? && !run_interval.present?
232
+ errors.add(:run_interval, "or run_start_minute must be nil")
233
+ errors.add(:run_start_minute, "or run_interval must be nil")
234
+ end
148
235
  else
149
- conditions = ["application_id = ? AND enabled = ?", application_id, true]
150
- end
151
-
152
- num_collisions = self.class.count(conditions: conditions)
153
- errors.add(:application_id, "is enabled and has already been taken") if num_collisions > 0
154
- end
155
-
156
- def run_interval_at_time_check
157
- unless (run_start_minute.blank? || run_interval.blank?)
158
- errors.add(:run_interval, "or Run start minute must be nil")
159
- errors.add(:run_start_minute, "or Run interval must be nil")
236
+ if !run_interval.present?
237
+ errors.add(:run_interval, "must be present")
238
+ end
160
239
  end
161
240
  end
162
241
 
163
- def self.pickleables(pickler)
164
- self.joins(:application).
165
- where("#{::Naf.schema_name}.applications.deleted IS FALSE")
166
- end
167
-
168
242
  private
169
243
 
170
244
  def prerequisite_application_schedule_id_uniqueness
@@ -15,6 +15,7 @@ module Naf
15
15
 
16
16
  # Protect from mass-assignment issue
17
17
  attr_accessible :application_id,
18
+ :application_schedule_id,
18
19
  :application_type_id,
19
20
  :command,
20
21
  :application_run_group_restriction_id,
@@ -44,6 +45,8 @@ module Naf
44
45
  # *** Associations ***
45
46
  #+++++++++++++++++++++
46
47
 
48
+ belongs_to :application_schedule,
49
+ class_name: '::Naf::ApplicationSchedule'
47
50
  belongs_to :application_type,
48
51
  class_name: '::Naf::ApplicationType'
49
52
  belongs_to :started_on_machine,
@@ -175,9 +178,9 @@ module Naf
175
178
  end
176
179
 
177
180
  def self.application_last_runs
178
- where("application_id IS NOT NULL").
179
- group("application_id").
180
- select("application_id, MAX(finished_at) AS finished_at").
181
+ where("application_schedule_id IS NOT NULL").
182
+ group("application_schedule_id").
183
+ select("application_schedule_id, MAX(finished_at) AS finished_at").
181
184
  reject{ |job| job.finished_at.nil? }
182
185
  end
183
186