naf 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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