good_job 3.99.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/app/charts/good_job/scheduled_by_queue_chart.rb +2 -2
  4. data/app/controllers/good_job/cron_entries_controller.rb +0 -8
  5. data/app/controllers/good_job/metrics_controller.rb +1 -1
  6. data/app/controllers/good_job/performances_controller.rb +5 -9
  7. data/app/models/concerns/good_job/filterable.rb +1 -1
  8. data/app/models/good_job/base_execution.rb +104 -201
  9. data/app/models/good_job/cron_entry.rb +0 -2
  10. data/app/models/good_job/discrete_execution.rb +1 -31
  11. data/app/models/good_job/execution.rb +3 -7
  12. data/app/models/good_job/job.rb +37 -116
  13. data/app/models/good_job/process.rb +7 -20
  14. data/app/views/good_job/batches/index.html.erb +11 -15
  15. data/app/views/good_job/jobs/_executions.erb +1 -1
  16. data/app/views/good_job/jobs/_table.erb +1 -1
  17. data/app/views/good_job/jobs/show.html.erb +1 -8
  18. data/app/views/good_job/performances/show.html.erb +33 -40
  19. data/config/locales/de.yml +1 -4
  20. data/config/locales/en.yml +1 -4
  21. data/config/locales/es.yml +1 -4
  22. data/config/locales/fr.yml +1 -4
  23. data/config/locales/it.yml +1 -4
  24. data/config/locales/ja.yml +1 -4
  25. data/config/locales/ko.yml +1 -4
  26. data/config/locales/nl.yml +1 -4
  27. data/config/locales/pt-BR.yml +1 -4
  28. data/config/locales/ru.yml +1 -4
  29. data/config/locales/tr.yml +1 -4
  30. data/config/locales/uk.yml +1 -4
  31. data/lib/generators/good_job/templates/update/migrations/01_create_good_jobs.rb.erb +65 -3
  32. data/lib/good_job/active_job_extensions/batches.rb +1 -1
  33. data/lib/good_job/active_job_extensions/concurrency.rb +10 -10
  34. data/lib/good_job/adapter.rb +13 -24
  35. data/lib/good_job/current_thread.rb +6 -6
  36. data/lib/good_job/job_performer.rb +2 -2
  37. data/lib/good_job/log_subscriber.rb +2 -10
  38. data/lib/good_job/notifier.rb +3 -3
  39. data/lib/good_job/version.rb +1 -1
  40. data/lib/good_job.rb +16 -21
  41. metadata +15 -29
  42. data/lib/generators/good_job/templates/update/migrations/02_create_good_job_settings.rb.erb +0 -20
  43. data/lib/generators/good_job/templates/update/migrations/03_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb.erb +0 -19
  44. data/lib/generators/good_job/templates/update/migrations/04_create_good_job_batches.rb.erb +0 -35
  45. data/lib/generators/good_job/templates/update/migrations/05_create_good_job_executions.rb.erb +0 -33
  46. data/lib/generators/good_job/templates/update/migrations/06_create_good_jobs_error_event.rb.erb +0 -16
  47. data/lib/generators/good_job/templates/update/migrations/07_recreate_good_job_cron_indexes_with_conditional.rb.erb +0 -45
  48. data/lib/generators/good_job/templates/update/migrations/08_create_good_job_labels.rb.erb +0 -15
  49. data/lib/generators/good_job/templates/update/migrations/09_create_good_job_labels_index.rb.erb +0 -22
  50. data/lib/generators/good_job/templates/update/migrations/10_remove_good_job_active_id_index.rb.erb +0 -21
  51. data/lib/generators/good_job/templates/update/migrations/11_create_index_good_job_jobs_for_candidate_lookup.rb.erb +0 -19
  52. data/lib/generators/good_job/templates/update/migrations/12_create_good_job_execution_error_backtrace.rb.erb +0 -15
  53. data/lib/generators/good_job/templates/update/migrations/13_create_good_job_process_lock_ids.rb.erb +0 -18
  54. data/lib/generators/good_job/templates/update/migrations/14_create_good_job_process_lock_indexes.rb.erb +0 -38
  55. data/lib/generators/good_job/templates/update/migrations/15_create_good_job_execution_duration.rb.erb +0 -15
@@ -2,10 +2,6 @@
2
2
 
3
3
  module GoodJob
4
4
  # Active Record model that represents an +ActiveJob+ job.
5
- # There is not a table in the database whose discrete rows represents "Jobs".
6
- # The +good_jobs+ table is a table of individual {GoodJob::Execution}s that share the same +active_job_id+.
7
- # A single row from the +good_jobs+ table of executions is fetched to represent a Job.
8
- #
9
5
  class Job < BaseExecution
10
6
  # Raised when an inappropriate action is applied to a Job based on its state.
11
7
  ActionForStateMismatchError = Class.new(StandardError)
@@ -16,29 +12,17 @@ module GoodJob
16
12
  # Raised when Active Job data cannot be deserialized
17
13
  ActiveJobDeserializationError = Class.new(StandardError)
18
14
 
19
- class << self
20
- delegate :table_name, to: GoodJob::Execution
21
-
22
- def table_name=(_value)
23
- raise NotImplementedError, 'Assign GoodJob::Execution.table_name directly'
24
- end
25
- end
26
-
27
- self.primary_key = 'active_job_id'
28
- self.advisory_lockable_column = 'active_job_id'
15
+ self.table_name = 'good_jobs'
16
+ self.advisory_lockable_column = 'id'
29
17
  self.implicit_order_column = 'created_at'
30
18
 
31
19
  belongs_to :batch, class_name: 'GoodJob::BatchRecord', inverse_of: :jobs, optional: true
32
20
  belongs_to :locked_by_process, class_name: "GoodJob::Process", foreign_key: :locked_by_id, inverse_of: :locked_jobs, optional: true
33
- has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
34
- has_many :discrete_executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::DiscreteExecution', foreign_key: 'active_job_id', primary_key: :active_job_id, inverse_of: :job # rubocop:disable Rails/HasManyOrHasOneDependent
21
+ has_many :executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: :active_job_id, inverse_of: :job, dependent: :delete_all
22
+ # TODO: rename callers of discrete_execution to executions, but after v4 has some time to bake for cleaner diffs/patches
23
+ has_many :discrete_executions, -> { order(created_at: :asc) }, class_name: 'GoodJob::Execution', foreign_key: 'active_job_id', primary_key: :active_job_id, inverse_of: :job, dependent: :delete_all
35
24
 
36
- after_destroy lambda {
37
- GoodJob::DiscreteExecution.where(active_job_id: active_job_id).delete_all if discrete? # TODO: move into association `dependent: :delete_all` after v4
38
- }
39
-
40
- # Only the most-recent unretried execution represents a "Job"
41
- default_scope { where(retried_good_job_id: nil) }
25
+ before_create -> { self.id = active_job_id }, if: -> { active_job_id.present? }
42
26
 
43
27
  # Get Jobs finished before the given timestamp.
44
28
  # @!method finished_before(timestamp)
@@ -64,65 +48,11 @@ module GoodJob
64
48
 
65
49
  scope :unfinished_undiscrete, -> { where(finished_at: nil, retried_good_job_id: nil, is_discrete: [nil, false]) }
66
50
 
67
- # The job's Active Job UUID
68
- # @return [String]
69
- def id
70
- active_job_id
71
- end
72
-
73
- # Override #reload to add a custom scope to ensure the reloaded record is the head execution
74
- # @return [Job]
75
- def reload(options = nil)
76
- self.class.connection.clear_query_cache
77
-
78
- # override with the `where(retried_good_job_id: nil)` scope
79
- override_query = self.class.where(retried_good_job_id: nil)
80
- fresh_object =
81
- if options && options[:lock]
82
- self.class.unscoped { override_query.lock(options[:lock]).find(id) }
83
- else
84
- self.class.unscoped { override_query.find(id) }
85
- end
86
-
87
- @attributes = fresh_object.instance_variable_get(:@attributes)
88
- @new_record = false
89
- @previously_new_record = false
90
- self
91
- end
92
-
93
- # This job's most recent {Execution}
94
- # @param reload [Booelan] whether to reload executions
95
- # @return [Execution]
96
- def head_execution(reload: false)
97
- executions.reload if reload
98
- executions.load # memoize the results
99
- executions.last
100
- end
101
-
102
- # This job's initial/oldest {Execution}
103
- # @return [Execution]
104
- def tail_execution
105
- executions.first
106
- end
107
-
108
- # The number of times this job has been executed, according to Active Job's serialized state.
109
- # @return [Numeric]
110
- def executions_count
111
- aj_count = serialized_params.fetch('executions', 0)
112
- # The execution count within serialized_params is not updated
113
- # once the underlying execution has been executed.
114
- if status.in? [:discarded, :succeeded, :running]
115
- aj_count + 1
116
- else
117
- aj_count
118
- end
119
- end
120
-
121
- # The number of times this job has been executed, according to the number of GoodJob {Execution} records.
122
- # @return [Numeric]
123
- def preserved_executions_count
124
- executions.size
125
- end
51
+ # TODO: it would be nice to enforce these values at the model
52
+ # validates :active_job_id, presence: true
53
+ # validates :scheduled_at, presence: true
54
+ # validates :job_class, presence: true
55
+ # validates :error_event, presence: true, if: -> { error.present? }
126
56
 
127
57
  # The most recent error message.
128
58
  # If the job has been retried, the error will be fetched from the previous {Execution} record.
@@ -155,6 +85,10 @@ module GoodJob
155
85
  job_class
156
86
  end
157
87
 
88
+ def executions_count
89
+ super || 0
90
+ end
91
+
158
92
  # Tests whether the job is being executed right now.
159
93
  # @return [Boolean]
160
94
  def running?
@@ -189,33 +123,33 @@ module GoodJob
189
123
  # @return [ActiveJob::Base]
190
124
  def retry_job
191
125
  with_advisory_lock do
192
- execution = head_execution(reload: true)
193
- active_job = execution.active_job(ignore_deserialization_errors: true)
126
+ reload
127
+ active_job = self.active_job(ignore_deserialization_errors: true)
194
128
 
195
129
  raise ActiveJobDeserializationError if active_job.nil?
196
130
  raise AdapterNotGoodJobError unless active_job.class.queue_adapter.is_a? GoodJob::Adapter
197
- raise ActionForStateMismatchError if execution.finished_at.blank? || execution.error.blank?
131
+ raise ActionForStateMismatchError if finished_at.blank? || error.blank?
198
132
 
199
133
  # Update the executions count because the previous execution will not have been preserved
200
134
  # Do not update `exception_executions` because that comes from rescue_from's arguments
201
135
  active_job.executions = (active_job.executions || 0) + 1
202
136
 
203
137
  begin
204
- error_class, error_message = execution.error.split(ERROR_MESSAGE_SEPARATOR).map(&:strip)
138
+ error_class, error_message = error.split(ERROR_MESSAGE_SEPARATOR).map(&:strip)
205
139
  error = error_class.constantize.new(error_message)
206
140
  rescue StandardError
207
- error = StandardError.new(execution.error)
141
+ error = StandardError.new(error)
208
142
  end
209
143
 
210
144
  new_active_job = nil
211
145
  GoodJob::CurrentThread.within do |current_thread|
212
- current_thread.execution = execution
146
+ current_thread.job = self
213
147
  current_thread.retry_now = true
214
148
 
215
- execution.class.transaction(joinable: false, requires_new: true) do
149
+ self.class.transaction(joinable: false, requires_new: true) do
216
150
  new_active_job = active_job.retry_job(wait: 0, error: error)
217
- execution.error_event = ERROR_EVENT_RETRIED if execution.error && execution.class.error_event_migrated?
218
- execution.save!
151
+ self.error_event = ERROR_EVENT_RETRIED if error
152
+ save!
219
153
  end
220
154
  end
221
155
 
@@ -244,11 +178,10 @@ module GoodJob
244
178
  # @return [void]
245
179
  def reschedule_job(scheduled_at = Time.current)
246
180
  with_advisory_lock do
247
- execution = head_execution(reload: true)
248
-
249
- raise ActionForStateMismatchError if execution.finished_at.present?
181
+ reload
182
+ raise ActionForStateMismatchError if finished_at.present?
250
183
 
251
- execution.update(scheduled_at: scheduled_at)
184
+ update(scheduled_at: scheduled_at)
252
185
  end
253
186
  end
254
187
 
@@ -256,9 +189,7 @@ module GoodJob
256
189
  # @return [void]
257
190
  def destroy_job
258
191
  with_advisory_lock do
259
- execution = head_execution(reload: true)
260
-
261
- raise ActionForStateMismatchError if execution.finished_at.blank?
192
+ raise ActionForStateMismatchError if finished_at.blank?
262
193
 
263
194
  destroy
264
195
  end
@@ -270,37 +201,27 @@ module GoodJob
270
201
  attributes['id']
271
202
  end
272
203
 
273
- # Utility method to test whether this job's underlying attributes represents its most recent execution.
274
- # @return [Boolean]
275
- def _head?
276
- _execution_id == head_execution(reload: true).id
277
- end
278
-
279
204
  private
280
205
 
281
206
  def _discard_job(message)
282
- execution = head_execution(reload: true)
283
- active_job = execution.active_job(ignore_deserialization_errors: true)
207
+ active_job = self.active_job(ignore_deserialization_errors: true)
284
208
 
285
- raise ActionForStateMismatchError if execution.finished_at.present?
209
+ raise ActionForStateMismatchError if finished_at.present?
286
210
 
287
211
  job_error = GoodJob::Job::DiscardJobError.new(message)
288
212
 
289
- update_execution = proc do
290
- execution.update(
291
- {
292
- finished_at: Time.current,
293
- error: GoodJob::Execution.format_error(job_error),
294
- }.tap do |attrs|
295
- attrs[:error_event] = ERROR_EVENT_DISCARDED if self.class.error_event_migrated?
296
- end
213
+ update_record = proc do
214
+ update(
215
+ finished_at: Time.current,
216
+ error: self.class.format_error(job_error),
217
+ error_event: ERROR_EVENT_DISCARDED
297
218
  )
298
219
  end
299
220
 
300
221
  if active_job.respond_to?(:instrument)
301
- active_job.send :instrument, :discard, error: job_error, &update_execution
222
+ active_job.send :instrument, :discard, error: job_error, &update_record
302
223
  else
303
- update_execution.call
224
+ update_record.call
304
225
  end
305
226
  end
306
227
  end
@@ -33,13 +33,9 @@ module GoodJob # :nodoc:
33
33
  # @!scope class
34
34
  # @return [ActiveRecord::Relation]
35
35
  scope :active, (lambda do
36
- if lock_type_migrated?
37
- query = joins_advisory_locks
38
- query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_locked
39
- .or(query.where(lock_type: nil).where(arel_table[:updated_at].gt(EXPIRED_INTERVAL.ago)))
40
- else
41
- advisory_locked
42
- end
36
+ query = joins_advisory_locks
37
+ query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_locked
38
+ .or(query.where(lock_type: nil).where(arel_table[:updated_at].gt(EXPIRED_INTERVAL.ago)))
43
39
  end)
44
40
 
45
41
  # Processes that are inactive and unlocked (e.g. SIGKILLed)
@@ -47,13 +43,9 @@ module GoodJob # :nodoc:
47
43
  # @!scope class
48
44
  # @return [ActiveRecord::Relation]
49
45
  scope :inactive, (lambda do
50
- if lock_type_migrated?
51
- query = joins_advisory_locks
52
- query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_unlocked
53
- .or(query.where(lock_type: nil).where(arel_table[:updated_at].lt(EXPIRED_INTERVAL.ago)))
54
- else
55
- advisory_unlocked
56
- end
46
+ query = joins_advisory_locks
47
+ query.where(lock_type: LOCK_TYPE_ENUMS[LOCK_TYPE_ADVISORY]).advisory_unlocked
48
+ .or(query.where(lock_type: nil).where(arel_table[:updated_at].lt(EXPIRED_INTERVAL.ago)))
57
49
  end)
58
50
 
59
51
  # Deletes all inactive process records.
@@ -64,11 +56,6 @@ module GoodJob # :nodoc:
64
56
  end
65
57
  end
66
58
 
67
- # @return [Boolean]
68
- def self.lock_type_migrated?
69
- columns_hash["lock_type"].present?
70
- end
71
-
72
59
  def self.create_record(id:, with_advisory_lock: false)
73
60
  attributes = {
74
61
  id: id,
@@ -76,7 +63,7 @@ module GoodJob # :nodoc:
76
63
  }
77
64
  if with_advisory_lock
78
65
  attributes[:create_with_advisory_lock] = true
79
- attributes[:lock_type] = LOCK_TYPE_ADVISORY if lock_type_migrated?
66
+ attributes[:lock_type] = LOCK_TYPE_ADVISORY
80
67
  end
81
68
  create!(attributes)
82
69
  end
@@ -2,19 +2,15 @@
2
2
  <h2 class="pt-3 pb-2"><%= t ".title" %></h2>
3
3
  </div>
4
4
 
5
- <% if GoodJob::BatchRecord.migrated? %>
6
- <%= render 'good_job/batches/table', batches: @filter.records, filter: @filter %>
7
- <% if @filter.records.present? %>
8
- <nav aria-label="Batch pagination" class="mt-3">
9
- <ul class="pagination">
10
- <li class="page-item">
11
- <%= link_to(@filter.to_params(after_created_at: @filter.last.created_at, after_id: @filter.last.id), class: "page-link") do %>
12
- <%= t ".older_batches" %> <span aria-hidden="true">&raquo;</span>
13
- <% end %>
14
- </li>
15
- </ul>
16
- </nav>
17
- <% end %>
18
- <% else %>
19
- <h3 class="text-center my-5"><%= t ".pending_migrations" %></h3>
5
+ <%= render 'good_job/batches/table', batches: @filter.records, filter: @filter %>
6
+ <% if @filter.records.present? %>
7
+ <nav aria-label="Batch pagination" class="mt-3">
8
+ <ul class="pagination">
9
+ <li class="page-item">
10
+ <%= link_to(@filter.to_params(after_created_at: @filter.last.created_at, after_id: @filter.last.id), class: "page-link") do %>
11
+ <%= t ".older_batches" %> <span aria-hidden="true">&raquo;</span>
12
+ <% end %>
13
+ </li>
14
+ </ul>
15
+ </nav>
20
16
  <% end %>
@@ -38,7 +38,7 @@
38
38
  <strong class="small text-danger"><%= t "good_job.shared.error" %>:</strong>
39
39
  <code class="text-wrap text-break m-0 text-secondary-emphasis"><%= execution.error %></code>
40
40
  </div>
41
- <% if GoodJob::DiscreteExecution.backtrace_migrated? && execution.error_backtrace&.any? %>
41
+ <% if execution.error_backtrace&.any? %>
42
42
  <%= tag.ul class: "nav nav-tabs small w-fit-content", id: dom_id(execution, :tab), role: "tablist" do %>
43
43
  <li class="nav-item" role="presentation">
44
44
  <%= tag.button t(".application_trace"), class: "nav-link active p-1", id: dom_id(execution, :application), data: { bs_toggle: "tab", bs_target: dom_id(execution, :"#application_pane") }, type: "button", role: "tab", aria: { controls: dom_id(execution, :application_pane), selected: true } %>
@@ -102,7 +102,7 @@
102
102
  <%= tag.span relative_time(job.last_status_at), class: "small mt-1" %>
103
103
  <div>
104
104
  <%= status_badge job.status %>
105
- <% if job.status == :discarded && job.class.error_event_migrated? && job.error_event %>
105
+ <% if job.status == :discarded && job.error_event %>
106
106
  <div class="text-black text-center">
107
107
  <small><%= t(job.error_event, scope: 'good_job.error_event') %></small>
108
108
  </div>
@@ -5,9 +5,6 @@
5
5
  <li class="breadcrumb-item"><%= link_to t(".jobs"), jobs_path %></li>
6
6
  <li class="breadcrumb-item active" aria-current="page">
7
7
  <%= tag.code @job.id, class: "text-muted" %>
8
- <% if @job.discrete? %>
9
- <span class="badge bg-info">Discrete</span>
10
- <% end %>
11
8
  </li>
12
9
  </ol>
13
10
  </nav>
@@ -87,8 +84,4 @@
87
84
  <%= tag.pre JSON.pretty_generate(@job.display_serialized_params) %>
88
85
  <% end %>
89
86
 
90
- <% if @job.discrete? %>
91
- <%= render 'executions', executions: @job.discrete_executions.reverse %>
92
- <% else %>
93
- <%= render 'executions', executions: @job.executions.includes_advisory_locks.reverse %>
94
- <% end %>
87
+ <%= render 'executions', executions: @job.executions.reverse %>
@@ -2,49 +2,42 @@
2
2
  <h2 class="pt-3 pb-2"><%= t ".title" %></h2>
3
3
  </div>
4
4
 
5
- <% if @needs_upgrade %>
6
- <div class="alert alert-warning">
7
- <%= t "shared.needs_migration" %>
8
- </div>
9
- <% else %>
10
-
11
- <div class="my-3 card">
12
- <div class="list-group list-group-flush text-nowrap" role="table">
13
- <header class="list-group-item bg-body-tertiary">
14
- <div class="row small text-muted text-uppercase align-items-center">
15
- <div class="col-12 col-lg-4"><%= t ".job_class" %></div>
16
- <div class="col-lg-2 d-none d-lg-block"><%= t ".executions" %></div>
5
+ <div class="my-3 card">
6
+ <div class="list-group list-group-flush text-nowrap" role="table">
7
+ <header class="list-group-item bg-body-tertiary">
8
+ <div class="row small text-muted text-uppercase align-items-center">
9
+ <div class="col-12 col-lg-4"><%= t ".job_class" %></div>
10
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".executions" %></div>
17
11
 
18
- <div class="col-lg-2 d-none d-lg-block"><%= t ".average_duration" %></div>
19
- <div class="col-lg-2 d-none d-lg-block"><%= t ".minimum_duration" %></div>
20
- <div class="col-lg-2 d-none d-lg-block"><%= t ".maximum_duration" %></div>
21
- </div>
22
- </header>
12
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".average_duration" %></div>
13
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".minimum_duration" %></div>
14
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".maximum_duration" %></div>
15
+ </div>
16
+ </header>
23
17
 
24
- <% @performances.each do |performance| %>
25
- <div role="row" class="list-group-item py-3">
26
- <div class="row align-items-center">
27
- <div class="col-12 col-lg-4"><%= performance.job_class %></div>
28
- <div class="col-6 col-lg-2 text-wrap">
29
- <div class="d-lg-none small text-muted mt-1"><%= t ".executions" %></div>
30
- <%= performance.executions_count %>
31
- </div>
18
+ <% @performances.each do |performance| %>
19
+ <div role="row" class="list-group-item py-3">
20
+ <div class="row align-items-center">
21
+ <div class="col-12 col-lg-4"><%= performance.job_class %></div>
22
+ <div class="col-6 col-lg-2 text-wrap">
23
+ <div class="d-lg-none small text-muted mt-1"><%= t ".executions" %></div>
24
+ <%= performance.executions_count %>
25
+ </div>
32
26
 
33
- <div class="col-6 col-lg-2 text-wrap">
34
- <div class="d-lg-none small text-muted mt-1"><%= t ".average_duration" %></div>
35
- <%= format_duration performance.avg_duration %>
36
- </div>
37
- <div class="col-6 col-lg-2 text-wrap">
38
- <div class="d-lg-none small text-muted mt-1"><%= t ".minimum_duration" %></div>
39
- <%= format_duration performance.min_duration %>
40
- </div>
41
- <div class="col-6 col-lg-2 text-wrap">
42
- <div class="d-lg-none small text-muted mt-1"><%= t ".maximum_duration" %></div>
43
- <%= format_duration performance.max_duration %>
44
- </div>
27
+ <div class="col-6 col-lg-2 text-wrap">
28
+ <div class="d-lg-none small text-muted mt-1"><%= t ".average_duration" %></div>
29
+ <%= format_duration performance.avg_duration %>
30
+ </div>
31
+ <div class="col-6 col-lg-2 text-wrap">
32
+ <div class="d-lg-none small text-muted mt-1"><%= t ".minimum_duration" %></div>
33
+ <%= format_duration performance.min_duration %>
34
+ </div>
35
+ <div class="col-6 col-lg-2 text-wrap">
36
+ <div class="d-lg-none small text-muted mt-1"><%= t ".maximum_duration" %></div>
37
+ <%= format_duration performance.max_duration %>
45
38
  </div>
46
39
  </div>
47
- <% end %>
48
- </div>
40
+ </div>
41
+ <% end %>
49
42
  </div>
50
- <% end %>
43
+ </div>
@@ -11,7 +11,6 @@ de:
11
11
  batches:
12
12
  index:
13
13
  older_batches: Ältere Chargen
14
- pending_migrations: GoodJob hat ausstehende Datenbankmigrationen.
15
14
  title: Chargen
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ de:
48
47
  index:
49
48
  no_cron_schedules_found: Keine Cron-Zeitpläne gefunden.
50
49
  title: Cron-Zeitpläne
51
- pending_migrations: Erfordert ausstehende GoodJob-Datenbankmigration.
52
50
  show:
53
51
  cron_entry_key: Cron-Eingabetaste
54
52
  datetime:
@@ -240,6 +238,7 @@ de:
240
238
  dark: Dunkel
241
239
  light: Licht
242
240
  theme: Thema
241
+ pending_migrations: GoodJob hat ausstehende Datenbankmigrationen.
243
242
  secondary_navbar:
244
243
  inspiration: Denk daran, auch du machst einen guten Job!
245
244
  last_updated: Zuletzt aktualisiert
@@ -250,5 +249,3 @@ de:
250
249
  running: Laufend
251
250
  scheduled: Geplant
252
251
  succeeded: Erfolgreich
253
- shared:
254
- needs_migration: Bitte führen Sie GoodJob-Migrationen aus.
@@ -11,7 +11,6 @@ en:
11
11
  batches:
12
12
  index:
13
13
  older_batches: Older batches
14
- pending_migrations: GoodJob has pending database migrations.
15
14
  title: Batches
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ en:
48
47
  index:
49
48
  no_cron_schedules_found: No cron schedules found.
50
49
  title: Cron Schedules
51
- pending_migrations: Requires pending GoodJob database migration.
52
50
  show:
53
51
  cron_entry_key: Cron Entry Key
54
52
  datetime:
@@ -240,6 +238,7 @@ en:
240
238
  dark: Dark
241
239
  light: Light
242
240
  theme: Theme
241
+ pending_migrations: GoodJob has pending database migrations.
243
242
  secondary_navbar:
244
243
  inspiration: Remember, you're doing a Good Job too!
245
244
  last_updated: Last updated
@@ -250,5 +249,3 @@ en:
250
249
  running: Running
251
250
  scheduled: Scheduled
252
251
  succeeded: Succeeded
253
- shared:
254
- needs_migration: Please run GoodJob migrations.
@@ -11,7 +11,6 @@ es:
11
11
  batches:
12
12
  index:
13
13
  older_batches: Batches anteriores
14
- pending_migrations: GoodJob tiene migraciones pendientes.
15
14
  title: Batches
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ es:
48
47
  index:
49
48
  no_cron_schedules_found: No hay tareas programadas.
50
49
  title: Tareas Programadas
51
- pending_migrations: Require las migraciones de GoodJob pendientes.
52
50
  show:
53
51
  cron_entry_key: Cron Entry Key
54
52
  datetime:
@@ -240,6 +238,7 @@ es:
240
238
  dark: Oscuro
241
239
  light: Luz
242
240
  theme: Tema
241
+ pending_migrations: GoodJob tiene migraciones pendientes.
243
242
  secondary_navbar:
244
243
  inspiration: "¡Recuerda, también tú estás haciendo un buen trabajo!"
245
244
  last_updated: Última actualización
@@ -250,5 +249,3 @@ es:
250
249
  running: Ejecutando
251
250
  scheduled: Programado
252
251
  succeeded: Exitoso
253
- shared:
254
- needs_migration: Ejecute las migraciones de GoodJob.
@@ -11,7 +11,6 @@ fr:
11
11
  batches:
12
12
  index:
13
13
  older_batches: Lots plus anciens
14
- pending_migrations: GoodJob a des migrations de bases de données en attente.
15
14
  title: Lots
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ fr:
48
47
  index:
49
48
  no_cron_schedules_found: Aucune planification cron trouvée.
50
49
  title: Entrées Cron
51
- pending_migrations: Nécessite une migration de la base de données GoodJob en attente.
52
50
  show:
53
51
  cron_entry_key: Clé d'entrée Cron
54
52
  datetime:
@@ -240,6 +238,7 @@ fr:
240
238
  dark: Sombre
241
239
  light: Lumière
242
240
  theme: Thème
241
+ pending_migrations: GoodJob a des migrations de bases de données en attente.
243
242
  secondary_navbar:
244
243
  inspiration: N'oublie pas, toi aussi tu fais du bon boulot !
245
244
  last_updated: Dernière mise à jour
@@ -250,5 +249,3 @@ fr:
250
249
  running: En cours
251
250
  scheduled: Planifiés
252
251
  succeeded: Réussis
253
- shared:
254
- needs_migration: Veuillez exécuter des migrations GoodJob.
@@ -11,7 +11,6 @@ it:
11
11
  batches:
12
12
  index:
13
13
  older_batches: Batch più vecchi
14
- pending_migrations: GoodJob ha migrazioni del database in sospeso.
15
14
  title: Batch
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ it:
48
47
  index:
49
48
  no_cron_schedules_found: Nessuna pianificazione cron trovata.
50
49
  title: Pianificazioni cron
51
- pending_migrations: Richiede migrazione del database GoodJob in sospeso.
52
50
  show:
53
51
  cron_entry_key: Chiave voce cron
54
52
  datetime:
@@ -240,6 +238,7 @@ it:
240
238
  dark: Scuro
241
239
  light: Chiaro
242
240
  theme: Tema
241
+ pending_migrations: GoodJob ha migrazioni del database in sospeso.
243
242
  secondary_navbar:
244
243
  inspiration: Ricorda, stai facendo anche tu un Buon Lavoro!
245
244
  last_updated: Ultimo aggiornamento
@@ -250,5 +249,3 @@ it:
250
249
  running: In esecuzione
251
250
  scheduled: Pianificato
252
251
  succeeded: Riuscito
253
- shared:
254
- needs_migration: Esegui le migrazioni GoodJob.
@@ -11,7 +11,6 @@ ja:
11
11
  batches:
12
12
  index:
13
13
  older_batches: より古いバッチ
14
- pending_migrations: GoodJobに保留中のデータベースマイグレーションがあります。
15
14
  title: バッチ
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ ja:
48
47
  index:
49
48
  no_cron_schedules_found: cronスケジュールが見つかりませんでした。
50
49
  title: cronスケジュール
51
- pending_migrations: GoodJobの保留中のデータベースマイグレーションが必要です。
52
50
  show:
53
51
  cron_entry_key: cronエントリキー
54
52
  datetime:
@@ -240,6 +238,7 @@ ja:
240
238
  dark: 暗い
241
239
  light: ライト
242
240
  theme: テーマ
241
+ pending_migrations: GoodJobに保留中のデータベースマイグレーションがあります。
243
242
  secondary_navbar:
244
243
  inspiration: 覚えておいてください、あなたも良い仕事をしています!
245
244
  last_updated: 最終更新
@@ -250,5 +249,3 @@ ja:
250
249
  running: 実行中
251
250
  scheduled: スケジュール待ち
252
251
  succeeded: 成功済み
253
- shared:
254
- needs_migration: GoodJob 移行を実行してください。
@@ -11,7 +11,6 @@ ko:
11
11
  batches:
12
12
  index:
13
13
  older_batches: 더 오래된 배치
14
- pending_migrations: GoodJob에 보류 중인 데이터베이스 마이그레이션 작업이 있습니다.
15
14
  title: 배치
16
15
  jobs:
17
16
  actions:
@@ -48,7 +47,6 @@ ko:
48
47
  index:
49
48
  no_cron_schedules_found: cron 스케줄을 찾을 수 없습니다.
50
49
  title: cron 스케줄
51
- pending_migrations: 보류 중인 GoodJob 데이터베이스 마이그레이션이 필요합니다.
52
50
  show:
53
51
  cron_entry_key: cron 엔트리 키
54
52
  datetime:
@@ -240,6 +238,7 @@ ko:
240
238
  dark: 어두운
241
239
  light: 밝은
242
240
  theme: 테마
241
+ pending_migrations: GoodJob에 보류 중인 데이터베이스 마이그레이션 작업이 있습니다.
243
242
  secondary_navbar:
244
243
  inspiration: 기억하세요, 당신도 좋은 일을 하고 있습니다!
245
244
  last_updated: 최종 업데이트
@@ -250,5 +249,3 @@ ko:
250
249
  running: 실행 중
251
250
  scheduled: 예정됨
252
251
  succeeded: 성공함
253
- shared:
254
- needs_migration: GoodJob 마이그레이션을 실행하세요.