good_job 3.16.3 → 3.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +41 -15
  4. data/app/frontend/good_job/application.js +2 -0
  5. data/app/frontend/good_job/modules/theme_controller.js +44 -0
  6. data/app/frontend/good_job/vendor/bootstrap/bootstrap.bundle.min.js +3 -3
  7. data/app/frontend/good_job/vendor/bootstrap/bootstrap.min.css +3 -4
  8. data/app/models/concerns/good_job/{lockable.rb → advisory_lockable.rb} +1 -1
  9. data/app/models/good_job/base_execution.rb +24 -1
  10. data/app/models/good_job/batch.rb +2 -2
  11. data/app/models/good_job/batch_record.rb +1 -1
  12. data/app/models/good_job/execution.rb +0 -21
  13. data/app/models/good_job/process.rb +5 -1
  14. data/app/views/good_job/batches/_jobs.erb +3 -3
  15. data/app/views/good_job/batches/_table.erb +1 -1
  16. data/app/views/good_job/batches/show.html.erb +1 -1
  17. data/app/views/good_job/cron_entries/index.html.erb +2 -2
  18. data/app/views/good_job/jobs/_executions.erb +2 -2
  19. data/app/views/good_job/jobs/_table.erb +10 -9
  20. data/app/views/good_job/jobs/show.html.erb +2 -2
  21. data/app/views/good_job/processes/index.html.erb +3 -3
  22. data/app/views/good_job/shared/_footer.erb +1 -1
  23. data/app/views/good_job/shared/_navbar.erb +50 -7
  24. data/app/views/good_job/shared/icons/_circle_half.html.erb +4 -0
  25. data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +5 -0
  26. data/app/views/good_job/shared/icons/_sun_fill.html.erb +4 -0
  27. data/app/views/layouts/good_job/application.html.erb +9 -1
  28. data/config/locales/de.yml +5 -0
  29. data/config/locales/en.yml +5 -0
  30. data/config/locales/es.yml +5 -0
  31. data/config/locales/fr.yml +5 -0
  32. data/config/locales/ja.yml +5 -0
  33. data/config/locales/nl.yml +5 -0
  34. data/config/locales/ru.yml +5 -0
  35. data/config/locales/tr.yml +5 -0
  36. data/config/locales/uk.yml +5 -0
  37. data/lib/good_job/capsule.rb +5 -4
  38. data/lib/good_job/cli.rb +6 -2
  39. data/lib/good_job/configuration.rb +2 -5
  40. data/lib/good_job/cron_manager.rb +3 -2
  41. data/lib/good_job/http_server.rb +75 -0
  42. data/lib/good_job/log_subscriber.rb +18 -0
  43. data/lib/good_job/notifier.rb +59 -44
  44. data/lib/good_job/probe_server.rb +4 -8
  45. data/lib/good_job/sd_notify.rb +157 -0
  46. data/lib/good_job/shared_executor.rb +69 -0
  47. data/lib/good_job/systemd_service.rb +69 -0
  48. data/lib/good_job/version.rb +1 -1
  49. data/lib/good_job.rb +6 -0
  50. metadata +11 -17
@@ -16,7 +16,7 @@ module GoodJob
16
16
  # end
17
17
  # end
18
18
  #
19
- module Lockable
19
+ module AdvisoryLockable
20
20
  extend ActiveSupport::Concern
21
21
 
22
22
  # Indicates an advisory lock is already held on a record by another
@@ -4,9 +4,9 @@ module GoodJob
4
4
  # ActiveRecord model to share behavior between {Job} and {Execution} models
5
5
  # which both read out of the same table.
6
6
  class BaseExecution < BaseRecord
7
+ include AdvisoryLockable
7
8
  include ErrorEvents
8
9
  include Filterable
9
- include Lockable
10
10
  include Reportable
11
11
 
12
12
  self.table_name = 'good_jobs'
@@ -60,5 +60,28 @@ module GoodJob
60
60
  def discrete?
61
61
  self.class.discrete_support? && is_discrete?
62
62
  end
63
+
64
+ # Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
65
+ #
66
+ # @param ignore_deserialization_errors [Boolean]
67
+ # Whether to ignore ActiveJob::DeserializationError when deserializing the arguments.
68
+ # This is most useful if you aren't planning to use the arguments directly.
69
+ def active_job(ignore_deserialization_errors: false)
70
+ ActiveJob::Base.deserialize(active_job_data).tap do |aj|
71
+ aj.send(:deserialize_arguments_if_needed)
72
+ rescue ActiveJob::DeserializationError
73
+ raise unless ignore_deserialization_errors
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def active_job_data
80
+ serialized_params.deep_dup
81
+ .tap do |job_data|
82
+ job_data["provider_job_id"] = id
83
+ job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
84
+ end
85
+ end
63
86
  end
64
87
  end
@@ -124,11 +124,11 @@ module GoodJob
124
124
  end
125
125
 
126
126
  def active_jobs
127
- record.jobs.map(&:head_execution).map(&:active_job)
127
+ record.jobs.map(&:active_job)
128
128
  end
129
129
 
130
130
  def callback_active_jobs
131
- record.callback_jobs.map(&:head_execution).map(&:active_job)
131
+ record.callback_jobs.map(&:active_job)
132
132
  end
133
133
 
134
134
  def assign_properties(properties)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  class BatchRecord < BaseRecord
5
- include Lockable
5
+ include AdvisoryLockable
6
6
 
7
7
  self.table_name = 'good_job_batches'
8
8
 
@@ -508,19 +508,6 @@ module GoodJob
508
508
  self.scheduled_at ||= current_time
509
509
  end
510
510
 
511
- # Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
512
- #
513
- # @param ignore_deserialization_errors [Boolean]
514
- # Whether to ignore ActiveJob::DeserializationError when deserializing the arguments.
515
- # This is most useful if you aren't planning to use the arguments directly.
516
- def active_job(ignore_deserialization_errors: false)
517
- ActiveJob::Base.deserialize(active_job_data).tap do |aj|
518
- aj.send(:deserialize_arguments_if_needed)
519
- rescue ActiveJob::DeserializationError
520
- raise unless ignore_deserialization_errors
521
- end
522
- end
523
-
524
511
  # Return formatted serialized_params for display in the dashboard
525
512
  # @return [Hash]
526
513
  def display_serialized_params
@@ -565,14 +552,6 @@ module GoodJob
565
552
 
566
553
  private
567
554
 
568
- def active_job_data
569
- serialized_params.deep_dup
570
- .tap do |job_data|
571
- job_data["provider_job_id"] = id
572
- job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
573
- end
574
- end
575
-
576
555
  def reset_batch_values(&block)
577
556
  GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
578
557
  end
@@ -5,8 +5,8 @@ require 'socket'
5
5
  module GoodJob # :nodoc:
6
6
  # ActiveRecord model that represents an GoodJob process (either async or CLI).
7
7
  class Process < BaseRecord
8
+ include AdvisoryLockable
8
9
  include AssignableConnection
9
- include Lockable
10
10
 
11
11
  # Interval until the process record being updated
12
12
  STALE_INTERVAL = 30.seconds
@@ -63,6 +63,10 @@ module GoodJob # :nodoc:
63
63
  cron_enabled: GoodJob.configuration.enable_cron?,
64
64
  total_succeeded_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:succeeded_executions_count) },
65
65
  total_errored_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:errored_executions_count) },
66
+ database_connection_pool: {
67
+ size: connection_pool.size,
68
+ active: connection_pool.connections.count(&:in_use?),
69
+ },
66
70
  }
67
71
  end
68
72
 
@@ -1,6 +1,6 @@
1
1
  <div class="my-3 card" data-gj-poll-replace id="jobs-table">
2
2
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
3
- <header class="list-group-item bg-light">
3
+ <header class="list-group-item bg-body-tertiary">
4
4
  <div class="row small text-muted text-uppercase align-items-center">
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>
@@ -27,7 +27,7 @@
27
27
  </div>
28
28
  <div class="col-4 col-lg-1 text-lg-center">
29
29
  <div class="d-lg-none small text-muted mt-1"><%=t "good_job.models.job.queue" %></div>
30
- <span class="badge bg-primary bg-opacity-25 text-dark font-monospace"><%= job.queue_name %></span>
30
+ <span class="badge bg-primary text-dark font-monospace"><%= job.queue_name %></span>
31
31
  </div>
32
32
  <div class="col-4 col-lg-1 text-lg-end">
33
33
  <div class="d-lg-none small text-muted mt-1"><%=t "good_job.models.job.priority" %>Priority</div>
@@ -43,7 +43,7 @@
43
43
  bs_content: job.recent_error
44
44
  } %>
45
45
  <% else %>
46
- <span class="badge bg-secondary bg-opacity-50 rounded-pill"><%= job.executions_count %></span>
46
+ <span class="badge bg-secondary rounded-pill"><%= job.executions_count %></span>
47
47
  <% end %>
48
48
  </div>
49
49
  <div class="mt-3 mt-lg-0 col d-flex gap-3 align-items-center justify-content-end">
@@ -1,6 +1,6 @@
1
1
  <div class="my-3 card" data-gj-poll-replace id="batches-table">
2
2
  <div class="list-group list-group-flush text-nowrap table-batches" role="table">
3
- <header class="list-group-item bg-light">
3
+ <header class="list-group-item bg-body-tertiary">
4
4
  <div class="row small text-muted text-uppercase align-items-center">
5
5
  <div class="col-4"><%=t "good_job.models.batch.name" %></div>
6
6
  <div class="col-lg-1 d-none d-lg-block"><%=t "good_job.models.batch.created" %></div>
@@ -16,7 +16,7 @@
16
16
 
17
17
  <div class="my-4">
18
18
  <h5><%= t ".attributes" %></h5>
19
- <div class="bg-dark text-light p-3 rounded">
19
+ <div class="bg-dark text-secondary p-3 rounded">
20
20
  <%= tag.pre JSON.pretty_generate @batch.display_attributes, class: 'text-wrap text-break' %>
21
21
  </div>
22
22
  </div>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="card my-3">
6
6
  <div class="list-group list-group-flush text-nowrap" role="table">
7
- <header class="list-group-item bg-light">
7
+ <header class="list-group-item body-secondary">
8
8
  <div class="row small text-muted text-uppercase align-items-center">
9
9
  <div class="col-12 col-lg-2"></div>
10
10
  <div class="col-6 col-lg-2 d-none d-lg-block"><%= t "good_job.models.cron.class" %></div>
@@ -68,7 +68,7 @@
68
68
  </div>
69
69
  </div>
70
70
  </div>
71
- <%= tag.div id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties list-group-item collapse small bg-dark text-light" do %>
71
+ <%= tag.div id: dom_id(cron_entry, 'properties'), class: "collapse cron-entry-properties list-group-item collapse small bg-dark text-secondary" do %>
72
72
  <%= tag.pre JSON.pretty_generate(cron_entry.display_properties) %>
73
73
  <% end %>
74
74
  <% end %>
@@ -5,7 +5,7 @@
5
5
  <%= tag.div id: dom_id(execution), class: "list-group-item py-3" do %>
6
6
  <div class="row align-items-center text-nowrap">
7
7
  <div class="col-md-5 d-flex gap-2">
8
- <%= tag.span execution.number, class: "badge bg-secondary bg-opacity-50 rounded-pill" %>
8
+ <%= tag.span execution.number, class: "badge bg-secondary rounded-pill" %>
9
9
  <%= tag.code link_to(execution.id, "##{dom_id(execution)}", class: "text-muted text-decoration-none small") %>
10
10
  </div>
11
11
  <div class="col-md-2 small">
@@ -36,7 +36,7 @@
36
36
  <% if execution.error %>
37
37
  <div class="mt-3 small">
38
38
  <strong class="small"><%=t "good_job.shared.error" %>:</strong>
39
- <code class="text-wrap text-break m-0 text-black"><%= execution.error %></code>
39
+ <code class="text-wrap text-break m-0 text-secondary-emphasis"><%= execution.error %></code>
40
40
  </div>
41
41
  <% end %>
42
42
  <% end %>
@@ -1,12 +1,13 @@
1
1
  <%= form_with(url: mass_update_jobs_path(filter.to_params), method: :put, local: true, data: { "checkbox-toggle": "job_ids" }) do |form| %>
2
2
  <div class="my-3 card" data-gj-poll-replace id="jobs-table">
3
3
  <div class="list-group list-group-flush text-nowrap table-jobs" role="table">
4
- <header class="list-group-item bg-light">
4
+ <header class="list-group-item bg-body-tertiary">
5
5
  <div class="row small text-muted text-uppercase align-items-center">
6
6
  <div class="col-lg-4 d-flex gap-2 flex-wrap">
7
- <%= label_tag('toggle_job_ids', t(".toggle_all_jobs"), class: "visually-hidden") %>
8
- <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
9
-
7
+ <div class="form-check d-flex flex-row px-0 mb-0">
8
+ <%= check_box_tag('toggle_job_ids', "1", false, data: { "checkbox-toggle-all": "job_ids" }) %>
9
+ <%= label_tag('toggle_job_ids', t(".toggle_all_jobs"), class: "visually-hidden") %>
10
+ </div>
10
11
  <%= form.button type: 'submit', name: 'mass_action', value: 'reschedule', class: 'ms-1 btn btn-sm btn-outline-secondary', title: t(".actions.reschedule_all"), data: { confirm: t(".actions.confirm_reschedule_all"), disable: true } do %>
11
12
  <span class="me-1"><%= render_icon "skip_forward" %></span> <%=t "good_job.actions.reschedule" %>
12
13
  <% end %>
@@ -22,13 +23,13 @@
22
23
  <button id="destroy-dropdown-toggle" type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
23
24
  <span class="visually-hidden"><%=t ".toggle_actions" %></span>
24
25
  </button>
25
- <div class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
26
+ <ul class="dropdown-menu" aria-labelledby="destroy-dropdown-toggle">
26
27
  <li>
27
28
  <%= form.button type: 'submit', name: 'mass_action', value: 'destroy', class: 'btn', title: t(".actions.destroy_all"), data: { confirm: t(".actions.confirm_destroy_all"), disable: true } do %>
28
29
  <span class="me-1"><%= render_icon "trash" %></span> <%=t "good_job.actions.destroy" %>
29
30
  <% end %>
30
31
  </li>
31
- </div>
32
+ </ul>
32
33
  </div>
33
34
 
34
35
  </div>
@@ -68,14 +69,14 @@
68
69
  <% if job.error %>
69
70
  <div class="mt-1 small">
70
71
  <strong class="small"><%=t "good_job.shared.error" %>:</strong>
71
- <code class="text-wrap text-break m-0 text-black"><%= job.error %></code>
72
+ <code class="text-wrap text-break m-0 text-secondary-emphasis"><%= job.error %></code>
72
73
  </div>
73
74
  <% end %>
74
75
  </div>
75
76
  </div>
76
77
  <div class="col-4 col-lg-1 text-lg-center">
77
78
  <div class="d-lg-none small text-muted mt-1"><%=t "good_job.models.job.queue" %></div>
78
- <span class="badge bg-primary bg-opacity-25 text-dark font-monospace"><%= job.queue_name %></span>
79
+ <span class="badge bg-primary font-monospace"><%= job.queue_name %></span>
79
80
  </div>
80
81
  <div class="col-4 col-lg-1 text-lg-end">
81
82
  <div class="d-lg-none small text-muted mt-1"><%=t "good_job.models.job.priority" %></div>
@@ -91,7 +92,7 @@
91
92
  bs_content: job.display_error,
92
93
  } %>
93
94
  <% else %>
94
- <span class="badge bg-secondary bg-opacity-50 rounded-pill"><%= job.executions_count %></span>
95
+ <span class="badge bg-secondary rounded-pill"><%= job.executions_count %></span>
95
96
  <% end %>
96
97
  </div>
97
98
  <div class="mt-3 mt-lg-0 col">
@@ -6,7 +6,7 @@
6
6
  <li class="breadcrumb-item active" aria-current="page">
7
7
  <%= tag.code @job.id, class: "text-muted" %>
8
8
  <% if @job.discrete? %>
9
- <span class="badge bg-info text-dark">Discrete</span>
9
+ <span class="badge bg-info">Discrete</span>
10
10
  <% end %>
11
11
  </li>
12
12
  </ol>
@@ -17,7 +17,7 @@
17
17
  </div>
18
18
  <div class="col-6 col-md-2">
19
19
  <div class="small text-muted text-uppercase"><%= t "good_job.models.job.queue" %></div>
20
- <div class="badge bg-primary bg-opacity-25 text-dark font-monospace my-2">
20
+ <div class="badge bg-primary font-monospace my-2">
21
21
  <%= tag.strong @job.queue_name %>
22
22
  </div>
23
23
  </div>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="card my-3" data-live-poll-region="processes">
6
6
  <div class="list-group list-group-flush text-nowrap" role="table">
7
- <header class="list-group-item bg-light">
7
+ <header class="list-group-item bg-body-tertiary">
8
8
  <div class="row small text-muted text-uppercase align-items-center">
9
9
  <div class="col"><%= t ".process" %></div>
10
10
  <div class="col"><%= t ".schedulers" %></div>
@@ -34,9 +34,9 @@
34
34
  <% end %>
35
35
  <div>
36
36
  <span class="text-muted small">PID</span>
37
- <span class="badge rounded-pill bg-light text-dark"><%= process.state["pid"] %></span>
37
+ <span class="badge rounded-pill bg-body-secondary text-secondary"><%= process.state["pid"] %></span>
38
38
  <span class="text-muted small">@</span>
39
- <span class="badge rounded-pill bg-light text-dark"><%= process.state["hostname"] %></span>
39
+ <span class="badge rounded-pill bg-body-secondary text-secondary"><%= process.state["hostname"] %></span>
40
40
  </div>
41
41
  </div>
42
42
  <div class="col">
@@ -1,4 +1,4 @@
1
- <footer class="footer mt-auto py-3 bg-light border-top text-muted small" id="footer" data-live-poll-region="footer">
1
+ <footer class="footer mt-auto py-3 bg-body-tertiary border-top text-muted small" id="footer" data-live-poll-region="footer">
2
2
  <div class="container-fluid">
3
3
  <div class="row">
4
4
  <div class="col-6">
@@ -1,4 +1,4 @@
1
- <nav class="navbar navbar-expand-lg navbar-light border-bottom bg-white sticky-top shadow-sm">
1
+ <nav class="navbar navbar-expand-lg border-bottom bg-body sticky-top shadow-sm">
2
2
  <div class="container-fluid">
3
3
  <%= link_to t(".name"), root_path, class: "navbar-brand mb-0 h1" %>
4
4
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
@@ -36,13 +36,24 @@
36
36
  <% end %>
37
37
  </li>
38
38
  </ul>
39
- <div class="nav-item pe-2">
40
- <label>
41
- <%= check_box_tag "live_poll", params.fetch("poll", 30), params[:poll].present? %>
42
- <%= t(".live_poll") %>
43
- </label>
44
- </div>
39
+
40
+ <hr class="d-lg-none text-secondary">
41
+
45
42
  <ul class="navbar-nav">
43
+ <li class="nav-item d-flex flex-column justify-content-center">
44
+ <div class="form-check form-switch m-0">
45
+ <%= check_box_tag "live_poll", params.fetch("poll", 30), params[:poll].present?, role: "switch", class: "form-check-input" %>
46
+ <label class="form-check-label navbar-text p-0" for="live_poll">
47
+ <%= t(".live_poll") %>
48
+ </label>
49
+ </div>
50
+ </li>
51
+
52
+ <li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
53
+ <div class="vr d-none d-lg-flex h-100 mx-lg-2 text-secondary"></div>
54
+ <hr class="d-lg-none my-2 text-secondary">
55
+ </li>
56
+
46
57
  <li class="nav-item dropdown">
47
58
  <a href="#" class="nav-link dropdown-toggle" type="button" id="localeOptions" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
48
59
  <%= I18n.locale %>
@@ -55,6 +66,38 @@
55
66
  <% end %>
56
67
  </ul>
57
68
  </li>
69
+
70
+ <li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
71
+ <div class="vr d-none d-lg-flex h-100 mx-lg-2 text-secondary"></div>
72
+ <hr class="d-lg-none my-2 text-secondary">
73
+ </li>
74
+
75
+ <li class="nav-item dropdown" data-controller="theme">
76
+ <button class="nav-link dropdown-toggle" data-theme-target="dropdown" type="button" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme">
77
+ <%= render_icon "circle_half" %>
78
+ <span class="visually-hidden"><%= t(".theme.theme") %></span>
79
+ </button>
80
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
81
+ <li>
82
+ <button type="button" class="dropdown-item" data-theme-target="button" data-theme-value-param="light" data-action="theme#change" aria-pressed="true">
83
+ <%= render_icon "sun_fill" %>
84
+ <%= t(".theme.light") %>
85
+ </button>
86
+ </li>
87
+ <li>
88
+ <button type="button" class="dropdown-item" data-theme-target="button" data-theme-value-param="dark" data-action="theme#change" aria-pressed="false">
89
+ <%= render_icon "moon_stars_fill" %>
90
+ <%= t(".theme.dark") %>
91
+ </button>
92
+ </li>
93
+ <li>
94
+ <button type="button" class="dropdown-item btn btn-link" data-theme-target="button" data-theme-value-param="auto" data-action="theme#change" aria-pressed="false">
95
+ <%= render_icon "circle_half" %>
96
+ <%= t(".theme.auto") %>
97
+ </button>
98
+ </li>
99
+ </ul>
100
+ </li>
58
101
  </ul>
59
102
  </div>
60
103
  </div>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/circle-half/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle-half" viewBox="0 0 16 16">
3
+ <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
4
+ </svg>
@@ -0,0 +1,5 @@
1
+ <!-- https://icons.getbootstrap.com/icons/moon-stars-fill/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-stars-fill" viewBox="0 0 16 16">
3
+ <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
4
+ <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" />
5
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/sun-fill/ -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sun-fill" viewBox="0 0 16 16">
3
+ <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
4
+ </svg>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="<%= I18n.locale %>">
2
+ <html lang="<%= I18n.locale %>" data-bs-theme="auto">
3
3
  <head>
4
4
  <title>Good Job Dashboard</title>
5
5
  <meta charset="utf-8">
@@ -19,6 +19,14 @@
19
19
  <% importmaps = GoodJob::FrontendsController.js_modules.keys.index_with { |module_name| frontend_module_path(module_name, format: :js, locale: nil, v: GoodJob::VERSION) } %>
20
20
  <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
21
21
  <%= tag.script "", type: "module", nonce: content_security_policy_nonce do %> import "application"; <% end %>
22
+ <%= tag.script "", nonce: content_security_policy_nonce do %>
23
+ // Ensure theme is updated before dom loads to avoid flash of style
24
+ let theme = localStorage.getItem('good_job-theme');
25
+ if (!["light", "dark"].includes(theme)) {
26
+ theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
27
+ }
28
+ document.documentElement.setAttribute('data-bs-theme', theme);
29
+ <% end %>
22
30
  </head>
23
31
  <body>
24
32
  <div class="d-flex flex-column min-vh-100">
@@ -218,6 +218,11 @@ de:
218
218
  live_poll: Live Poll
219
219
  name: "GoodJob 👍"
220
220
  processes: Prozesse
221
+ theme:
222
+ auto: Auto
223
+ dark: Dunkel
224
+ light: Licht
225
+ theme: Thema
221
226
  status:
222
227
  discarded: Ausrangiert
223
228
  queued: In der Warteschlange
@@ -218,6 +218,11 @@ en:
218
218
  live_poll: Live Poll
219
219
  name: "GoodJob 👍"
220
220
  processes: Processes
221
+ theme:
222
+ auto: Auto
223
+ dark: Dark
224
+ light: Light
225
+ theme: Theme
221
226
  status:
222
227
  discarded: Discarded
223
228
  queued: Queued
@@ -218,6 +218,11 @@ es:
218
218
  live_poll: En vivo
219
219
  name: "GoodJob 👍"
220
220
  processes: Procesos
221
+ theme:
222
+ auto: Auto
223
+ dark: Oscuro
224
+ light: Luz
225
+ theme: Tema
221
226
  status:
222
227
  discarded:
223
228
  one: Descartada
@@ -218,6 +218,11 @@ fr:
218
218
  live_poll: En direct
219
219
  name: "GoodJob 👍"
220
220
  processes: Processus
221
+ theme:
222
+ auto: Auto
223
+ dark: Sombre
224
+ light: Lumière
225
+ theme: Thème
221
226
  status:
222
227
  discarded: Mis au rebut
223
228
  queued: À la file
@@ -218,6 +218,11 @@ ja:
218
218
  live_poll: リアルタイム更新
219
219
  name: "GoodJob 👍"
220
220
  processes: プロセス
221
+ theme:
222
+ auto: 自動
223
+ dark: 暗い
224
+ light: ライト
225
+ theme: テーマ
221
226
  status:
222
227
  discarded: 破棄済み
223
228
  queued: 処理待ち
@@ -218,6 +218,11 @@ nl:
218
218
  live_poll: Live Poll
219
219
  name: "GoodJob 👍"
220
220
  processes: Processen
221
+ theme:
222
+ auto: Auto
223
+ dark: Donker
224
+ light: Licht
225
+ theme: Thema
221
226
  status:
222
227
  discarded: weggegooid
223
228
  queued: In de wachtrij
@@ -244,6 +244,11 @@ ru:
244
244
  live_poll: Живой Опрос
245
245
  name: "GoodJob 👍"
246
246
  processes: Процессы
247
+ theme:
248
+ auto: Авто
249
+ dark: Темный
250
+ light: Свет
251
+ theme: Тема
247
252
  status:
248
253
  discarded: Отброшено
249
254
  queued: В очереди
@@ -218,6 +218,11 @@ tr:
218
218
  live_poll: Anlık Güncelleme
219
219
  name: "GoodJob 👍"
220
220
  processes: Süreçler
221
+ theme:
222
+ auto: Oto
223
+ dark: Karanlık
224
+ light: Işık
225
+ theme: Tema
221
226
  status:
222
227
  discarded: İptal Edildi
223
228
  queued: Sırada
@@ -244,6 +244,11 @@ uk:
244
244
  live_poll: Живе Опитування
245
245
  name: "GoodJob 👍"
246
246
  processes: Процеси
247
+ theme:
248
+ auto: Авто
249
+ dark: Темний
250
+ light: світло
251
+ theme: Тема
247
252
  status:
248
253
  discarded: Відхилено
249
254
  queued: У черзі
@@ -29,13 +29,14 @@ module GoodJob
29
29
  @mutex.synchronize do
30
30
  return unless startable?(force: force)
31
31
 
32
- @notifier = GoodJob::Notifier.new(enable_listening: @configuration.enable_listen_notify)
32
+ @shared_executor = GoodJob::SharedExecutor.new
33
+ @notifier = GoodJob::Notifier.new(enable_listening: @configuration.enable_listen_notify, executor: @shared_executor.executor)
33
34
  @poller = GoodJob::Poller.new(poll_interval: @configuration.poll_interval)
34
35
  @scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: true)
35
36
  @notifier.recipients << [@scheduler, :create_thread]
36
37
  @poller.recipients << [@scheduler, :create_thread]
37
38
 
38
- @cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true) if @configuration.enable_cron?
39
+ @cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true, executor: @shared_executor.executor) if @configuration.enable_cron?
39
40
 
40
41
  @startable = false
41
42
  @running = true
@@ -51,7 +52,7 @@ module GoodJob
51
52
  # @return [void]
52
53
  def shutdown(timeout: :default)
53
54
  timeout = @configuration.shutdown_timeout if timeout == :default
54
- GoodJob._shutdown_all([@notifier, @poller, @scheduler, @cron_manager].compact, timeout: timeout)
55
+ GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @scheduler, @cron_manager].compact, timeout: timeout)
55
56
  @startable = false
56
57
  @running = false
57
58
  end
@@ -73,7 +74,7 @@ module GoodJob
73
74
 
74
75
  # @return [Boolean] Whether the capsule has been shutdown.
75
76
  def shutdown?
76
- [@notifier, @poller, @scheduler, @cron_manager].compact.all?(&:shutdown?)
77
+ [@shared_executor, @notifier, @poller, @scheduler, @cron_manager].compact.all?(&:shutdown?)
77
78
  end
78
79
 
79
80
  # Creates an execution thread(s) with the given attributes.
data/lib/good_job/cli.rb CHANGED
@@ -94,10 +94,12 @@ module GoodJob
94
94
  GoodJob.configuration.options.merge!(options.symbolize_keys)
95
95
  configuration = GoodJob.configuration
96
96
  capsule = GoodJob.capsule
97
+ systemd = GoodJob::SystemdService.new
97
98
 
98
99
  Daemon.new(pidfile: configuration.pidfile).daemonize if configuration.daemonize?
99
100
 
100
101
  capsule.start
102
+ systemd.start
101
103
 
102
104
  if configuration.probe_port
103
105
  probe_server = GoodJob::ProbeServer.new(port: configuration.probe_port)
@@ -114,8 +116,10 @@ module GoodJob
114
116
  break if @stop_good_job_executable || capsule.shutdown?
115
117
  end
116
118
 
117
- capsule.shutdown(timeout: configuration.shutdown_timeout)
118
- probe_server&.stop
119
+ systemd.stop do
120
+ capsule.shutdown(timeout: configuration.shutdown_timeout)
121
+ probe_server&.stop
122
+ end
119
123
  end
120
124
 
121
125
  default_task :start
@@ -51,13 +51,10 @@ module GoodJob
51
51
  # @param warn [Boolean] whether to print a warning when over the limit
52
52
  # @return [Integer]
53
53
  def self.total_estimated_threads(warn: false)
54
- configuration = new({})
55
-
56
- cron_threads = configuration.enable_cron? ? 2 : 0
57
- notifier_threads = 1
54
+ utility_threads = GoodJob::SharedExecutor::MAX_THREADS
58
55
  scheduler_threads = GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats[:max_threads] }
59
56
 
60
- good_job_threads = cron_threads + notifier_threads + scheduler_threads
57
+ good_job_threads = utility_threads + scheduler_threads
61
58
  puma_threads = (Puma::Server.current&.max_threads if defined?(Puma::Server)) || 0
62
59
 
63
60
  total_threads = good_job_threads + puma_threads