good_job 4.6.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e6676f189c0e22c751837d8792b84ece28dfcc118941077abfcdfac7e352c18
4
- data.tar.gz: 891e630829ec28fb022d516e50d5d9c61e51dc81531ce3f20c25f2facfee09c6
3
+ metadata.gz: 69c9c36ff0026c4a82ac6db945370e397d218e7976d4fc540aa4144225f75a25
4
+ data.tar.gz: 4db17623ab0fc1e1747a6c1de6ea596efde905c774a4c8193beb9f7a380c5e31
5
5
  SHA512:
6
- metadata.gz: d443d783208de5f01cd003773943199c618f397a46a1ecb5d7e9167a9eaedc0511f9e4b3cec6122c17cd0be328b490230727024041b7622ad1f9f8afcaf817e1
7
- data.tar.gz: fc1171540d6227d0e7757a09904b617791289daeec89c3045251b46e8d82959b9df4b5ecd0b4c812a2804ac7c7626712ebea93d3718984fb08a1592c097db068
6
+ metadata.gz: 4c9972c721472ca50148161ffc9611f59799e501b7c8fb0af5feac5b1a3f0dcdbf906295125bf4376b2e7fda0e72f9a8d27230fdb491fc6ea0c6c78bca8e4655
7
+ data.tar.gz: 3b5f9fc708ae37ebb1e6a27f6d5f453312193caf78334f210b09dfd0a5d53ba591db1017516cb7c681b1afe01dd1dd66115f4b6728a6fa7cb28a4c1383005d04
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.8.0](https://github.com/bensheldon/good_job/tree/v4.8.0) (2025-01-22)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.7.0...v4.8.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add ability to pause jobs by queue, job class, or label \(experimental\) [\#1575](https://github.com/bensheldon/good_job/pull/1575) ([bensheldon](https://github.com/bensheldon))
10
+
11
+ **Closed issues:**
12
+
13
+ - perform\_now enqueues a job when raising exception [\#1570](https://github.com/bensheldon/good_job/issues/1570)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Use Ruby 3.4.1 for development [\#1579](https://github.com/bensheldon/good_job/pull/1579) ([bensheldon](https://github.com/bensheldon))
18
+ - Fix generator tests [\#1577](https://github.com/bensheldon/good_job/pull/1577) ([bensheldon](https://github.com/bensheldon))
19
+ - Add the logger gem as a dependency to fix rails head [\#1576](https://github.com/bensheldon/good_job/pull/1576) ([bensheldon](https://github.com/bensheldon))
20
+ - fix: typo in en.yml when discarding jobs [\#1569](https://github.com/bensheldon/good_job/pull/1569) ([nickstanish](https://github.com/nickstanish))
21
+ - Bump the all group with 60 updates [\#1567](https://github.com/bensheldon/good_job/pull/1567) ([dependabot[bot]](https://github.com/apps/dependabot))
22
+
23
+ ## [v4.7.0](https://github.com/bensheldon/good_job/tree/v4.7.0) (2024-12-31)
24
+
25
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.6.0...v4.7.0)
26
+
27
+ **Implemented enhancements:**
28
+
29
+ - Expose good job labels in dashboard [\#1561](https://github.com/bensheldon/good_job/pull/1561) ([BClark88](https://github.com/BClark88))
30
+
31
+ **Merged pull requests:**
32
+
33
+ - Allow cron configuration `class` value to be a proc [\#1566](https://github.com/bensheldon/good_job/pull/1566) ([bensheldon](https://github.com/bensheldon))
34
+ - Add Ruby 3.4 to CI [\#1565](https://github.com/bensheldon/good_job/pull/1565) ([Earlopain](https://github.com/Earlopain))
35
+
3
36
  ## [v4.6.0](https://github.com/bensheldon/good_job/tree/v4.6.0) (2024-12-12)
4
37
 
5
38
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.5.1...v4.6.0)
data/README.md CHANGED
@@ -73,6 +73,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
73
73
  - [Write tests](#write-tests)
74
74
  - [PgBouncer compatibility](#pgbouncer-compatibility)
75
75
  - [CLI HTTP health check probes](#cli-http-health-check-probes)
76
+ - [Pausing Jobs](#pausing-jobs)
76
77
  - [Doing your best job with GoodJob](#doing-your-best-job-with-goodjob)
77
78
  - [Sizing jobs: mice and elephants](#sizing-jobs-mice-and-elephants)
78
79
  - [Isolating by total latency](#isolating-by-total-latency)
@@ -329,6 +330,8 @@ Available configuration options are:
329
330
  config.good_job.probe_handler = 'webrick'
330
331
  ```
331
332
 
333
+ - `enable_pauses` (boolean) whether job processing can be paused. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_PAUSES`.
334
+
332
335
  By default, GoodJob configures the following execution modes per environment:
333
336
 
334
337
  ```ruby
@@ -1559,9 +1562,35 @@ gem 'webrick'
1559
1562
 
1560
1563
  If WEBrick is configured to be used, but the dependency is not found, GoodJob will log a warning and fallback to the default probe server.
1561
1564
 
1565
+ ### Pausing Jobs
1566
+
1567
+ GoodJob allows for pausing jobs by queue or job class. This feature is currently opt-in because the performance impact of loading and filtering these attributes is not yet known. To enable this feature, add the following to your configuration:
1568
+
1569
+ > ```ruby
1570
+ > config.good_job.enable_pauses = true
1571
+ > ```
1572
+
1573
+ Pausing can be done via the Dashboard's Performance page, or in Ruby
1574
+
1575
+ ```ruby
1576
+ # To pause:
1577
+ GoodJob.pause(queue: "default")
1578
+ GoodJob.pause(job_class: "MyJob")
1579
+
1580
+ # To check status
1581
+ GoodJob.paused # => { queues: ["default"], job_classes: ["MyJob"] }
1582
+ GoodJob.paused?(queue: "default") # => true
1583
+ GoodJob.paused?(job_class: "MyJob") # => true
1584
+ GoodJob.paused? # => true
1585
+
1586
+ # To unpause
1587
+ GoodJob.unpause(queue: "default")
1588
+ GoodJob.unpause(job_class: "MyJob")
1589
+ ````
1590
+
1562
1591
  ## Doing your best job with GoodJob
1563
1592
 
1564
- _This section explains how to use GoodJob the most efficiently and performantly, according to its maintainers. GoodJob is very flexible and you dont necessarily have to use it this way, but the concepts explained here are part of GoodJobs design intent._
1593
+ _This section explains how to use GoodJob the most efficiently and performantly, according to its maintainers. GoodJob is very flexible and you don't necessarily have to use it this way, but the concepts explained here are part of GoodJob's design intent._
1565
1594
 
1566
1595
  Background jobs are hard. There are two extremes:
1567
1596
 
@@ -6,6 +6,7 @@ module GoodJob
6
6
  jobs_count = GoodJob::Job.count
7
7
  batches_count = GoodJob::BatchRecord.all.size
8
8
  cron_entries_count = GoodJob::CronEntry.all.size
9
+ pauses_count = GoodJob::Setting.paused.values.sum(&:count)
9
10
  processes_count = GoodJob::Process.active.count
10
11
  discarded_count = GoodJob::Job.discarded.count
11
12
 
@@ -13,6 +14,7 @@ module GoodJob
13
14
  jobs_count: helpers.number_to_human(jobs_count),
14
15
  batches_count: helpers.number_to_human(batches_count),
15
16
  cron_entries_count: helpers.number_to_human(cron_entries_count),
17
+ pauses_count: helpers.number_to_human(pauses_count),
16
18
  processes_count: helpers.number_to_human(processes_count),
17
19
  discarded_count: helpers.number_to_human(discarded_count),
18
20
  }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GoodJob
4
+ class PausesController < ApplicationController
5
+ before_action :validate_type, only: [:create, :destroy]
6
+ def index
7
+ @paused = GoodJob::Setting.paused
8
+ end
9
+
10
+ def create
11
+ pause_type = params[:type].to_sym
12
+ pause_value = params[:value].to_s
13
+
14
+ GoodJob::Setting.pause(pause_type => pause_value)
15
+ redirect_to({ action: :index }, notice: "Successfully paused #{params[:type]} '#{params[:value]}'")
16
+ end
17
+
18
+ def destroy
19
+ pause_type = params[:type].to_sym
20
+ pause_value = params[:value].to_s
21
+
22
+ GoodJob::Setting.unpause(pause_type => pause_value)
23
+ redirect_to({ action: :index }, notice: "Successfully unpaused #{params[:type]} '#{params[:value]}'")
24
+ end
25
+
26
+ private
27
+
28
+ def validate_type
29
+ return if params[:type].in?(%w[queue job_class label]) && params[:value].to_s.present?
30
+
31
+ raise ActionController::BadRequest, "Invalid type"
32
+ end
33
+ end
34
+ end
@@ -3,17 +3,21 @@
3
3
  module GoodJob
4
4
  class PerformanceController < ApplicationController
5
5
  def index
6
- @performances = GoodJob::Execution
7
- .where.not(job_class: nil)
8
- .group(:job_class)
9
- .select("
10
- job_class,
11
- COUNT(*) AS executions_count,
12
- AVG(duration) AS avg_duration,
13
- MIN(duration) AS min_duration,
14
- MAX(duration) AS max_duration
15
- ")
16
- .order("job_class")
6
+ @performances = GoodJob::Execution.group(:job_class).select("
7
+ job_class,
8
+ COUNT(*) AS executions_count,
9
+ AVG(duration) AS avg_duration,
10
+ MIN(duration) AS min_duration,
11
+ MAX(duration) AS max_duration
12
+ ").order(:job_class)
13
+
14
+ @queue_performances = GoodJob::Execution.group(:queue_name).select("
15
+ queue_name,
16
+ COUNT(*) AS executions_count,
17
+ AVG(duration) AS avg_duration,
18
+ MIN(duration) AS min_duration,
19
+ MAX(duration) AS max_duration
20
+ ").order(:queue_name)
17
21
  end
18
22
 
19
23
  def show
@@ -101,8 +101,12 @@ module GoodJob # :nodoc:
101
101
  current_thread.cron_key = key
102
102
  current_thread.cron_at = cron_at
103
103
 
104
- configured_job = job_class.constantize.set(set_value)
105
104
  I18n.with_locale(I18n.default_locale) do
105
+ job_klass = job_class_value
106
+ job_klass = job_klass.constantize if job_klass.is_a?(String)
107
+ next unless job_klass.is_a?(Class)
108
+
109
+ configured_job = job_klass.set(set_value)
106
110
  kwargs_value.present? ? configured_job.perform_later(*args_value, **kwargs_value) : configured_job.perform_later(*args_value)
107
111
  end
108
112
  end
@@ -118,6 +122,7 @@ module GoodJob # :nodoc:
118
122
  set: display_property(set),
119
123
  description: display_property(description),
120
124
  }.tap do |properties|
125
+ properties[:class] = display_property(job_class) if job_class.present?
121
126
  properties[:args] = display_property(args) if args.present?
122
127
  properties[:kwargs] = display_property(kwargs) if kwargs.present?
123
128
  end
@@ -160,6 +165,11 @@ module GoodJob # :nodoc:
160
165
  @_fugit ||= Fugit.parse(cron)
161
166
  end
162
167
 
168
+ def job_class_value
169
+ value = job_class || nil
170
+ value.respond_to?(:call) ? value.call : value
171
+ end
172
+
163
173
  def set_value
164
174
  value = set || {}
165
175
  value.respond_to?(:call) ? value.call : value
@@ -96,6 +96,33 @@ module GoodJob
96
96
  # @return [ActiveRecord::Relation]
97
97
  scope :only_scheduled, -> { where(arel_table['scheduled_at'].lteq(bind_value('scheduled_at', DateTime.current, ActiveRecord::Type::DateTime))).or(where(scheduled_at: nil)) }
98
98
 
99
+ # Exclude jobs that are paused via queue_name or job_class.
100
+ # Only applies when enable_pauses configuration is true.
101
+ # @!method exclude_paused
102
+ # @!scope class
103
+ # @return [ActiveRecord::Relation]
104
+ scope :exclude_paused, lambda {
105
+ return all unless GoodJob.configuration.enable_pauses
106
+
107
+ paused_query = GoodJob::Setting.where(key: GoodJob::Setting::PAUSES)
108
+ paused_queues_query = paused_query.select("jsonb_array_elements_text(value->'queues')")
109
+ paused_job_classes_query = paused_query.select("jsonb_array_elements_text(value->'job_classes')")
110
+ paused_labels_query = paused_query.select("jsonb_array_elements_text(value->'labels')")
111
+
112
+ where.not(queue_name: paused_queues_query)
113
+ .where.not(job_class: paused_job_classes_query)
114
+ .where(
115
+ Arel::Nodes::Not.new(
116
+ Arel::Nodes::NamedFunction.new(
117
+ "COALESCE", [
118
+ Arel::Nodes::InfixOperation.new('&&', arel_table['labels'], Arel::Nodes::NamedFunction.new('ARRAY', [paused_labels_query.arel])),
119
+ Arel::Nodes::SqlLiteral.new('FALSE'),
120
+ ]
121
+ )
122
+ )
123
+ )
124
+ }
125
+
99
126
  # Order jobs by priority (highest priority first).
100
127
  # @!method priority_ordered
101
128
  # @!scope class
@@ -290,7 +317,7 @@ module GoodJob
290
317
  job = nil
291
318
  result = nil
292
319
 
293
- unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |jobs|
320
+ unfinished.dequeueing_ordered(parsed_queues).only_scheduled.exclude_paused.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |jobs|
294
321
  job = jobs.first
295
322
 
296
323
  if job&.executable?
@@ -4,6 +4,7 @@ module GoodJob
4
4
  class Setting < BaseRecord
5
5
  CRON_KEYS_ENABLED = "cron_keys_enabled"
6
6
  CRON_KEYS_DISABLED = "cron_keys_disabled"
7
+ PAUSES = "pauses"
7
8
 
8
9
  self.table_name = 'good_job_settings'
9
10
  self.implicit_order_column = 'created_at'
@@ -47,5 +48,82 @@ module GoodJob
47
48
  disabled_setting.value << key unless disabled_setting.value.include?(key_string)
48
49
  disabled_setting.save!
49
50
  end
51
+
52
+ def self.pause(queue: nil, job_class: nil, label: nil)
53
+ raise ArgumentError, "Must provide exactly one of queue, job_class, or label" unless [queue, job_class, label].count(&:present?) == 1
54
+
55
+ setting = find_or_initialize_by(key: PAUSES) do |record|
56
+ record.value = { "queues" => [], "job_classes" => [], "labels" => [] }
57
+ end
58
+
59
+ if queue
60
+ setting.value["queues"] ||= []
61
+ setting.value["queues"] << queue.to_s unless setting.value["queues"].include?(queue.to_s)
62
+ elsif job_class
63
+ setting.value["job_classes"] ||= []
64
+ setting.value["job_classes"] << job_class.to_s unless setting.value["job_classes"].include?(job_class.to_s)
65
+ else
66
+ setting.value["labels"] ||= []
67
+ setting.value["labels"] << label.to_s unless setting.value["labels"].include?(label.to_s)
68
+ end
69
+ setting.save!
70
+ end
71
+
72
+ def self.unpause(queue: nil, job_class: nil, label: nil)
73
+ raise ArgumentError, "Must provide exactly one of queue, job_class, or label" unless [queue, job_class, label].count(&:present?) == 1
74
+
75
+ setting = find_by(key: PAUSES)
76
+ return unless setting
77
+
78
+ if queue
79
+ return unless setting.value["queues"]&.include?(queue.to_s)
80
+
81
+ setting.value["queues"].delete(queue.to_s)
82
+ elsif job_class
83
+ return unless setting.value["job_classes"]&.include?(job_class.to_s)
84
+
85
+ setting.value["job_classes"].delete(job_class.to_s)
86
+ else
87
+ return unless setting.value["labels"]&.include?(label.to_s)
88
+
89
+ setting.value["labels"].delete(label.to_s)
90
+ end
91
+ setting.save!
92
+ end
93
+
94
+ def self.paused?(queue: nil, job_class: nil, label: nil)
95
+ raise ArgumentError, "Must provide at most one of queue, job_class, or label" if [queue, job_class, label].count(&:present?) > 1
96
+
97
+ if queue
98
+ queue.in? paused(:queues)
99
+ elsif job_class
100
+ job_class.in? paused(:job_classes)
101
+ elsif label
102
+ label.in? paused(:labels)
103
+ else
104
+ paused.values.any?(&:any?)
105
+ end
106
+ end
107
+
108
+ def self.paused(type = nil)
109
+ setting = find_by(key: PAUSES)
110
+ pauses = setting&.value&.deep_dup || { "queues" => [], "job_classes" => [], "labels" => [] }
111
+ pauses = pauses.with_indifferent_access
112
+
113
+ case type
114
+ when :queues
115
+ pauses["queues"]
116
+ when :job_classes
117
+ pauses["job_classes"]
118
+ when :labels
119
+ pauses["labels"]
120
+ else
121
+ {
122
+ queues: pauses["queues"] || [],
123
+ job_classes: pauses["job_classes"] || [],
124
+ labels: pauses["labels"] || [],
125
+ }
126
+ end
127
+ end
50
128
  end
51
129
  end
@@ -5,6 +5,7 @@
5
5
  <div class="col-4"><%= t("good_job.models.batch.jobs") %></div>
6
6
  <div class="d-none d-lg-block col-lg-1 text-lg-center"><%= t "good_job.models.job.queue" %></div>
7
7
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.priority" %></div>
8
+ <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.labels" %></div>
8
9
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.attempts" %></div>
9
10
  <div class="col text-end">
10
11
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
@@ -33,6 +34,12 @@
33
34
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.priority" %></div>
34
35
  <span class="font-monospace fw-bold"><%= job.priority %></span>
35
36
  </div>
37
+ <div class="col-4 col-lg-1 text-lg-end">
38
+ <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
39
+ <% job.labels&.each do |label| %>
40
+ <span class="badge rounded-pill bg-secondary font-monospace"><%= label %></span>
41
+ <% end %>
42
+ </div>
36
43
  <div class="col-4 col-lg-1 text-lg-end">
37
44
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.attempts" %></div>
38
45
  <% if job.error %>
@@ -35,6 +35,7 @@
35
35
  </div>
36
36
  <div class="d-none d-lg-block col-lg-1 text-lg-center"><%= t "good_job.models.job.queue" %></div>
37
37
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.priority" %></div>
38
+ <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.labels" %></div>
38
39
  <div class="d-none d-lg-block col-lg-1 text-lg-end"><%= t "good_job.models.job.attempts" %></div>
39
40
  <div class="col text-end">
40
41
  <%= tag.button type: "button", class: "btn btn-sm text-muted", role: "button",
@@ -82,6 +83,12 @@
82
83
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.priority" %></div>
83
84
  <%= job.priority %>
84
85
  </div>
86
+ <div class="col-4 col-lg-1 text-wrap text-lg-end">
87
+ <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.labels" %></div>
88
+ <% job.labels&.each do |label| %>
89
+ <span class="badge rounded-pill bg-secondary font-monospace"><%= label %></span>
90
+ <% end %>
91
+ </div>
85
92
  <div class="col-4 col-lg-1 text-lg-end">
86
93
  <div class="d-lg-none small text-muted mt-1"><%= t "good_job.models.job.attempts" %></div>
87
94
  <% if job.error %>
@@ -0,0 +1,28 @@
1
+ <% group = type.to_s.pluralize.to_sym %>
2
+ <% items = paused.fetch(group) { [] } %>
3
+ <% if items&.any? %>
4
+ <div class="my-3 card">
5
+ <div class="list-group list-group-flush text-nowrap" role="table">
6
+ <header class="list-group-item bg-body-tertiary">
7
+ <div class="row small text-muted text-uppercase align-items-center">
8
+ <div class="col-12"><%= t("good_job.pauses.index.#{type}") %></div>
9
+ </div>
10
+ </header>
11
+
12
+ <% items.each do |value| %>
13
+ <li class="list-group-item d-flex justify-content-between align-items-center">
14
+ <%= value %>
15
+ <%= button_to(
16
+ { action: :destroy, type: type, value: value },
17
+ method: :delete,
18
+ class: 'btn btn-sm btn-outline-primary',
19
+ data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) }
20
+ ) do %>
21
+ <%= render_icon "play" %>
22
+ <%= t("good_job.pauses.index.unpause") %>
23
+ <% end %>
24
+ </li>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <li class="list-group-item d-flex justify-content-between align-items-center">
2
+ <%= value %>
3
+ <%= button_to t('good_job.pauses.index.unpause'),
4
+ { action: :destroy, type: type, value: value },
5
+ method: :delete,
6
+ class: 'btn btn-sm btn-outline-primary',
7
+ data: { confirm: t('good_job.pauses.index.confirm_unpause', value: value) } %>
8
+ </li>
@@ -0,0 +1,46 @@
1
+ <%= content_for :page_title do %>
2
+ <%= t('good_job.pauses.index.title') %>
3
+ <% end %>
4
+
5
+ <div class="border-bottom">
6
+ <h1 class="h2 pt-3 pb-2"><%= t('good_job.pauses.index.title') %></h1>
7
+ </div>
8
+
9
+ <% if GoodJob.configuration.enable_pauses %>
10
+ <div class="card mb-4">
11
+ <div class="card-body">
12
+ <%= form_tag({ action: :create }, method: :post, class: 'row g-3 align-items-end') do %>
13
+ <div class="col-3">
14
+ <%= label_tag :type, t('good_job.pauses.index.type'), class: 'form-label' %>
15
+ <%= select_tag :type,
16
+ options_for_select([
17
+ [t('good_job.pauses.index.queue'), 'queue'],
18
+ [t('good_job.pauses.index.job_class'), 'job_class'],
19
+ [t('good_job.pauses.index.label'), 'label'],
20
+ ]),
21
+ class: 'form-select',
22
+ required: true %>
23
+ </div>
24
+ <div class="col-6">
25
+ <%= label_tag :value, t('good_job.pauses.index.value'), class: 'form-label' %>
26
+ <%= text_field_tag :value, nil, class: 'form-control', autocomplete: "off", required: true %>
27
+ </div>
28
+ <div class="col-3">
29
+ <%= button_tag type: "submit", class: 'btn btn-primary' do %>
30
+ <%= render_icon "pause" %>
31
+ <%= t('good_job.pauses.index.pause', value: '') %>
32
+ <% end %>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+
38
+ <%= render 'group', paused: @paused, type: :queue %>
39
+ <%= render 'group', paused: @paused, type: :job_class %>
40
+ <%= render 'group', paused: @paused, type: :label %>
41
+ <% else %>
42
+ <div class="alert alert-warning" role="alert">
43
+ <p><%= t('good_job.pauses.index.disabled') %>:</p>
44
+ <pre class="mb-0">config.good_job.enable_pauses = true</pre>
45
+ </div>
46
+ <% end %>
@@ -10,7 +10,6 @@
10
10
  <div class="row small text-muted text-uppercase align-items-center">
11
11
  <div class="col-12 col-lg-4"><%= t ".job_class" %></div>
12
12
  <div class="col-lg-2 d-none d-lg-block"><%= t ".executions" %></div>
13
-
14
13
  <div class="col-lg-2 d-none d-lg-block"><%= t ".average_duration" %></div>
15
14
  <div class="col-lg-2 d-none d-lg-block"><%= t ".minimum_duration" %></div>
16
15
  <div class="col-lg-2 d-none d-lg-block"><%= t ".maximum_duration" %></div>
@@ -20,12 +19,57 @@
20
19
  <% @performances.each do |performance| %>
21
20
  <div role="row" class="list-group-item py-3">
22
21
  <div class="row align-items-center">
23
- <div class="col-12 col-lg-4"><%= link_to performance.job_class, performance_path(performance.job_class) %></div>
22
+ <div class="col-12 col-lg-4">
23
+ <div class="d-flex gap-3 justify-content-between">
24
+ <%= link_to performance.job_class, performance_path(performance.job_class) %>
25
+ </div>
26
+ </div>
24
27
  <div class="col-6 col-lg-2 text-wrap">
25
28
  <div class="d-lg-none small text-muted mt-1"><%= t ".executions" %></div>
26
29
  <%= performance.executions_count %>
27
30
  </div>
31
+ <div class="col-6 col-lg-2 text-wrap">
32
+ <div class="d-lg-none small text-muted mt-1"><%= t ".average_duration" %></div>
33
+ <%= format_duration performance.avg_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 ".minimum_duration" %></div>
37
+ <%= format_duration performance.min_duration %>
38
+ </div>
39
+ <div class="col-6 col-lg-2 text-wrap">
40
+ <div class="d-lg-none small text-muted mt-1"><%= t ".maximum_duration" %></div>
41
+ <%= format_duration performance.max_duration %>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <% end %>
46
+ </div>
47
+ </div>
28
48
 
49
+ <div class="my-3 card">
50
+ <div class="list-group list-group-flush text-nowrap" role="table">
51
+ <header class="list-group-item bg-body-tertiary">
52
+ <div class="row small text-muted text-uppercase align-items-center">
53
+ <div class="col-12 col-lg-4"><%= t ".queue_name" %></div>
54
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".executions" %></div>
55
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".average_duration" %></div>
56
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".minimum_duration" %></div>
57
+ <div class="col-lg-2 d-none d-lg-block"><%= t ".maximum_duration" %></div>
58
+ </div>
59
+ </header>
60
+
61
+ <% @queue_performances.each do |performance| %>
62
+ <div role="row" class="list-group-item py-3">
63
+ <div class="row align-items-center">
64
+ <div class="col-12 col-lg-4">
65
+ <div class="d-flex gap-3 justify-content-between">
66
+ <%= performance.queue_name %>
67
+ </div>
68
+ </div>
69
+ <div class="col-6 col-lg-2 text-wrap">
70
+ <div class="d-lg-none small text-muted mt-1"><%= t ".executions" %></div>
71
+ <%= performance.executions_count %>
72
+ </div>
29
73
  <div class="col-6 col-lg-2 text-wrap">
30
74
  <div class="d-lg-none small text-muted mt-1"><%= t ".average_duration" %></div>
31
75
  <%= format_duration performance.avg_duration %>
@@ -44,6 +44,12 @@
44
44
  <%= t(".performance") %>
45
45
  <% end %>
46
46
  </li>
47
+ <li class="nav-item">
48
+ <%= link_to pauses_path, class: ["nav-link", ("active" if controller_name == 'pauses')] do %>
49
+ <%= t(".pauses") %>
50
+ <span data-async-values-target="value" data-async-values-key="pauses_count" data-async-values-zero-class="d-none" class="badge bg-warning rounded-pill d-none"></span>
51
+ <% end %>
52
+ </li>
47
53
  <li class="nav-item">
48
54
  <%= link_to cleaner_index_path, class: ["nav-link", ("active" if controller_name == 'cleaner')] do %>
49
55
  <%= t(".cleaner") %>
@@ -194,6 +194,7 @@ de:
194
194
  job:
195
195
  arguments: Argumente
196
196
  attempts: Versuche
197
+ labels: Etiketten
197
198
  priority: Priorität
198
199
  queue: Warteschlange
199
200
  number:
@@ -213,14 +214,28 @@ de:
213
214
  thousand: K
214
215
  trillion: T
215
216
  unit: ''
217
+ pauses:
218
+ index:
219
+ confirm_pause: Sind Sie sicher, dass Sie %{value} pausieren möchten?
220
+ confirm_unpause: Sind Sie sicher, dass Sie %{value} fortsetzen möchten?
221
+ disabled: Die experimentelle Pausenfunktion von GoodJob ist standardmäßig deaktiviert, da sie die Leistung beeinträchtigen kann. Konfigurieren Sie sie zur Aktivierung
222
+ job_class: Job-Klasse
223
+ label: Label
224
+ pause: Pausieren
225
+ queue: Warteschlange
226
+ title: Pausen
227
+ type: Pausentyp
228
+ unpause: Fortsetzen
229
+ value: Wert
216
230
  performance:
217
231
  index:
218
232
  average_duration: Durchschnittliche Dauer
219
- chart_title: Gesamtdauer der Jobs in Sekunden
233
+ chart_title: Gesamtausführungszeit in Sekunden
220
234
  executions: Ausführungen
221
235
  job_class: Job-Klasse
222
236
  maximum_duration: Maximale Dauer
223
- minimum_duration: Mindestdauer
237
+ minimum_duration: Minimale Dauer
238
+ queue_name: Warteschlangenname
224
239
  title: Leistung
225
240
  show:
226
241
  slow: Langsam
@@ -255,6 +270,7 @@ de:
255
270
  jobs: Jobs
256
271
  live_poll: Live Poll
257
272
  name: "GoodJob 👍"
273
+ pauses: Pausen
258
274
  performance: Leistung
259
275
  processes: Prozesse
260
276
  theme: