que-view 0.3.1 → 0.3.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d07972f8adf809f3f10f495824376c64724e1526e7afd6d85a05da93632741e0
4
- data.tar.gz: 95d6ce6e4d1340dcb550a1ae4b85560f19a18de0b9c0dec1eea5ce5a44726ddf
3
+ metadata.gz: 062e48206fe33c45a7f007d831b31a5c4566d6eb4af6891cfad6949108f45b56
4
+ data.tar.gz: c3c2fd233567554fc46e7296a9895d7e8c7b650789fcb1fb7f49d2d023cc3670
5
5
  SHA512:
6
- metadata.gz: 9553edb70775fc2fa268ad2ec3e14d3ab51c3858b54363efbb0d6b8aae85f00d450d99e925cd576dc7117f1ff784bbbe664d763305123598acc35bda57e38e8b
7
- data.tar.gz: ce8bc2cbfef7844814f60125bbad375bf676bc2ef14180d5a2bdc2b4a9fd5cc3ac1920f5b5f40b540a2e600ee4107c247c3b89f90d3e7662725de1a6539c2461
6
+ metadata.gz: 3a4b0f0e2bac21033c9e3db5a8ea193b599679e88abb064f7a07b71608893e861e192196d2b688f70c8f9cf5c092d48b40925bfa190d1760172965ac8c319149
7
+ data.tar.gz: 3db157d3fb95fa530c43607ac9c7924b9ca84cbc305f134ae08635048d77c0213e71401770e4e8c30bbffbfc414c4f233d17bd5896ce2b661b12d464f1914c93
data/README.md CHANGED
@@ -4,12 +4,13 @@ SQL queries came from Que::Web, some styling from there too.
4
4
 
5
5
  Benefits for using this one:
6
6
  - no Sinatra (que-web based on Sinatra),
7
- - no Foundation for styles,
8
- - no jQuery,
7
+ - no Foundation for styles (and no external styles at all),
8
+ - no jQuery (and no java scripts at all),
9
9
  - Que::Web was last updated 2 years ago.
10
10
 
11
- <img width="1735" alt="Снимок экрана 2024-02-21 в 15 10 35" src="https://github.com/kortirso/que-view/assets/6195394/cd2812c7-abb0-48d9-92d5-4dbef93bcd9e">
12
- <img width="1735" alt="Снимок экрана 2024-02-21 в 15 11 12" src="https://github.com/kortirso/que-view/assets/6195394/8af01e7f-a002-4ef1-aeff-f96fd27c639f">
11
+ <img width="1731" alt="Снимок экрана 2024-03-03 в 08 28 09" src="https://github.com/kortirso/que-view/assets/6195394/fdaf315d-6c6e-40ee-a60f-cd740cc7ec93">
12
+ <img width="1734" alt="Снимок экрана 2024-03-03 в 08 27 47" src="https://github.com/kortirso/que-view/assets/6195394/e45d334e-7637-41f8-af16-d6a9ac35f263">
13
+ <img width="1730" alt="Снимок экрана 2024-03-03 в 23 09 57" src="https://github.com/kortirso/que-view/assets/6195394/d790066f-3e96-4775-afd6-018750d0afd3">
13
14
 
14
15
 
15
16
  ## Installation
@@ -29,6 +29,10 @@ body {
29
29
  background: #fafafa;
30
30
  }
31
31
 
32
+ h1 {
33
+ margin-top: 0;
34
+ }
35
+
32
36
  .row {
33
37
  display: flex;
34
38
  }
@@ -112,7 +116,7 @@ body {
112
116
  }
113
117
 
114
118
  .pagination {
115
- margin: 1rem auto;
119
+ margin: 0 auto 1rem;
116
120
  }
117
121
 
118
122
  .pagination .pagination-link {
@@ -181,6 +185,10 @@ body {
181
185
  background: #e7e5e4;
182
186
  }
183
187
 
188
+ .search-form {
189
+ margin-bottom: 1rem;
190
+ }
191
+
184
192
  .search-form .form-select {
185
193
  margin-right: 1rem;
186
194
  padding: .5rem 1rem;
@@ -221,6 +229,19 @@ table p {
221
229
  margin: 0;
222
230
  }
223
231
 
232
+ .lockers {
233
+ margin-top: 2rem;
234
+ }
235
+
236
+ .lockers .locker-process {
237
+ display: flex;
238
+ flex-direction: column;
239
+ }
240
+
241
+ .metrics a {
242
+ color: #000;
243
+ }
244
+
224
245
  @media screen and (max-width: 768px) {
225
246
  .dashboard-row {
226
247
  flex-direction: column;
@@ -84,7 +84,7 @@ module Que
84
84
 
85
85
  def find_jobs(params)
86
86
  case params[:status]&.to_sym
87
- when :running then ::Que::View.fetch_running_jobs(params)
87
+ when :running then ::Que::View.fetch_running_jobs
88
88
  when :failing then ::Que::View.fetch_failing_jobs(PER_PAGE, offset, params)
89
89
  when :scheduled then ::Que::View.fetch_scheduled_jobs(PER_PAGE, offset, params)
90
90
  when :finished then ::Que::View.fetch_finished_jobs(PER_PAGE, offset, params)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ module View
5
+ class QueueMetricsController < Que::View::ApplicationController
6
+ before_action :find_queue_metrics, only: %i[index]
7
+ before_action :find_queue_latencies, only: %i[index]
8
+
9
+ def index; end
10
+
11
+ private
12
+
13
+ def find_queue_metrics
14
+ @queue_metrics = ::Que::View.fetch_queue_metrics
15
+ end
16
+
17
+ def find_queue_latencies
18
+ current_time = DateTime.now.to_i
19
+ @queue_latencies =
20
+ ::Que::View
21
+ .fetch_queue_latencies(@queue_metrics.keys)
22
+ .transform_values { |value| current_time - value.to_time.to_i }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,6 +5,7 @@ module Que
5
5
  class WelcomeController < Que::View::ApplicationController
6
6
  def index
7
7
  @dashboard_stats = ::Que::View.fetch_dashboard_stats[0]
8
+ @lockers = ::Que::View.fetch_que_lockers
8
9
  end
9
10
  end
10
11
  end
@@ -26,6 +26,9 @@
26
26
  <li class="<%= 'active' if params[:status] == 'expired' %>">
27
27
  <%= link_to 'Expired', jobs_path(status: 'expired') %>
28
28
  </li>
29
+ <li class="<%= 'active' if current_page?(queue_metrics_path) %>">
30
+ <%= link_to 'Queue metrics', queue_metrics_path %>
31
+ </li>
29
32
  </ul>
30
33
  <ul class="version">
31
34
  <li>Que <%= Que::VERSION %></li>
@@ -1,3 +1,4 @@
1
+ <h1><%= params[:status].capitalize %> jobs</h1>
1
2
  <%= form_with url: jobs_path, method: :get, class: 'search-form' do |form| %>
2
3
  <%= form.hidden_field :status, value: params[:status] %>
3
4
  <div class="row">
@@ -6,77 +7,73 @@
6
7
  <%= form.submit 'Search', class: 'btn-primary' %>
7
8
  </div>
8
9
  <% end %>
9
- <% if @jobs.any? %>
10
- <% if @pagination %>
11
- <div class="row pagination">
12
- <% if @pagination.previous_page? %>
13
- <%= link_to 'First', jobs_path(status: params[:status], page: 1), class: 'pagination-link' %>
14
- <%= link_to '< Previous', jobs_path(status: params[:status], page: @pagination.previous_page), class: 'pagination-link' %>
15
- <% else %>
16
- <p class="pagination-link disabled">First</p>
17
- <p class="pagination-link disabled">&#60; Previous</p>
10
+ <% if @pagination && @pagination.total_pages > 1 %>
11
+ <div class="row pagination">
12
+ <% if @pagination.previous_page? %>
13
+ <%= link_to 'First', jobs_path(status: params[:status], page: 1), class: 'pagination-link' %>
14
+ <%= link_to '< Previous', jobs_path(status: params[:status], page: @pagination.previous_page), class: 'pagination-link' %>
15
+ <% else %>
16
+ <p class="pagination-link disabled">First</p>
17
+ <p class="pagination-link disabled">&#60; Previous</p>
18
+ <% end %>
19
+ <p class="total-pages"><%= "Page #{@pagination.page} of #{@pagination.total_pages}" %></p>
20
+ <% if @pagination.next_page? %>
21
+ <%= link_to 'Next >', jobs_path(status: params[:status], page: @pagination.next_page), class: 'pagination-link' %>
22
+ <%= link_to 'Last', jobs_path(status: params[:status], page: @pagination.total_pages), class: 'pagination-link' %>
23
+ <% else %>
24
+ <p class="pagination-link disabled">Next &#62;</p>
25
+ <p class="pagination-link disabled">Last </p>
26
+ <% end %>
27
+ </div>
28
+ <% end %>
29
+ <table cellspacing="0" class="jobs-list">
30
+ <thead>
31
+ <tr>
32
+ <th>ID</th>
33
+ <th>Job</th>
34
+ <th>Queue</th>
35
+ <th>Run at</th>
36
+ <th>Arguments</th>
37
+ <% if params[:status] == 'failing' %>
38
+ <th>Failures</th>
39
+ <th>Error</th>
18
40
  <% end %>
19
- <p class="total-pages"><%= "Page #{@pagination.page} of #{@pagination.total_pages}" %></p>
20
- <% if @pagination.next_page? %>
21
- <%= link_to 'Next >', jobs_path(status: params[:status], page: @pagination.next_page), class: 'pagination-link' %>
22
- <%= link_to 'Last', jobs_path(status: params[:status], page: @pagination.total_pages), class: 'pagination-link' %>
23
- <% else %>
24
- <p class="pagination-link disabled">Next &#62;</p>
25
- <p class="pagination-link disabled">Last </p>
41
+ <% if %w[failing scheduled].include?(params[:status]) %>
42
+ <th>Actions</th>
26
43
  <% end %>
27
- </div>
28
- <% end %>
29
- <table cellspacing="0" class="jobs-list">
30
- <thead>
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ <% @jobs.each do |job| %>
31
48
  <tr>
32
- <th>ID</th>
33
- <th>Job</th>
34
- <th>Queue</th>
35
- <th>Run at</th>
36
- <th>Arguments</th>
49
+ <td><%= link_to job[:id], job_path(job[:id]) %></td>
50
+ <td><%= humanized_job_class(job) %></td>
51
+ <td><%= job[:queue] %></td>
52
+ <td><%= job[:run_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
53
+ <td>
54
+ <% job.dig(:args, 0, :arguments).each do |argument| %>
55
+ <p><%= argument %></p>
56
+ <% end %>
57
+ </td>
37
58
  <% if params[:status] == 'failing' %>
38
- <th>Failures</th>
39
- <th>Error</th>
59
+ <td><%= job[:error_count] %></td>
60
+ <td><%= format_error(job) %></td>
40
61
  <% end %>
41
62
  <% if %w[failing scheduled].include?(params[:status]) %>
42
- <th>Actions</th>
43
- <% end %>
44
- </tr>
45
- </thead>
46
- <tbody>
47
- <% @jobs.each do |job| %>
48
- <tr>
49
- <td><%= link_to job[:id], job_path(job[:id]) %></td>
50
- <td><%= humanized_job_class(job) %></td>
51
- <td><%= job[:queue] %></td>
52
- <td><%= job[:run_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
53
63
  <td>
54
- <% job.dig(:args, 0, :arguments).each do |argument| %>
55
- <p><%= argument %></p>
56
- <% end %>
64
+ <div class="actions">
65
+ <%= button_to 'Run', job_path(job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
66
+ <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
67
+ </div>
57
68
  </td>
58
- <% if params[:status] == 'failing' %>
59
- <td><%= job[:error_count] %></td>
60
- <td><%= format_error(job) %></td>
61
- <% end %>
62
- <% if %w[failing scheduled].include?(params[:status]) %>
63
- <td>
64
- <div class="actions">
65
- <%= button_to 'Run', job_path(job[:id]), class: 'btn-danger', method: :patch, onclick: "return confirm('Are you sure you wish to reschedule job?')" %>
66
- <%= button_to 'Delete', job_path(job[:id]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete job?')" %>
67
- </div>
68
- </td>
69
- <% end %>
70
- </tr>
71
- <% end %>
72
- </tbody>
73
- </table>
69
+ <% end %>
70
+ </tr>
71
+ <% end %>
72
+ </tbody>
73
+ </table>
74
+ <% if @pagination && %w[failing scheduled].include?(params[:status]) %>
74
75
  <div class="row">
75
76
  <%= button_to 'Run All', reschedule_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :post, onclick: "return confirm('Are you sure you wish to reschedule all jobs?')" %>
76
77
  <%= button_to 'Delete All', destroy_all_jobs_path(status: params[:status]), class: 'btn-danger', method: :delete, onclick: "return confirm('Are you sure you wish to delete all jobs?')" %>
77
78
  </div>
78
- <% else %>
79
- <div class="row">
80
- <p>No jobs found</p>
81
- </div>
82
79
  <% end %>
@@ -18,6 +18,10 @@
18
18
  <th>Priority</th>
19
19
  <td><%= @job[:priority] %></td>
20
20
  </tr>
21
+ <tr>
22
+ <th>Enqueued at</th>
23
+ <td><%= @job.dig(:args, 0, :enqueued_at).to_time.strftime("%Y-%m-%d %H:%M:%S") %></td>
24
+ </tr>
21
25
  <tr>
22
26
  <th>Run at</th>
23
27
  <td><%= @job[:run_at].strftime("%Y-%m-%d %H:%M:%S") %></td>
@@ -0,0 +1,29 @@
1
+ <div class="metrics">
2
+ <h1>Queue Metrics</h1>
3
+ <table cellspacing="0">
4
+ <thead>
5
+ <tr>
6
+ <th>Queue</th>
7
+ <th>Scheduled</th>
8
+ <th>Failing</th>
9
+ <th>Running</th>
10
+ <th>Finished</th>
11
+ <th>Expired</th>
12
+ <th>Latency (seconds)</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% @queue_metrics.each do |queue, values| %>
17
+ <tr>
18
+ <td><%= queue %></td>
19
+ <td><%= link_to values[:scheduled], jobs_path(status: 'scheduled', queue_name: queue) %></td>
20
+ <td><%= link_to values[:failing], jobs_path(status: 'failing', queue_name: queue) %></td>
21
+ <td><%= link_to values[:running], jobs_path(status: 'running', queue_name: queue) %></td>
22
+ <td><%= link_to values[:finished], jobs_path(status: 'finished', queue_name: queue) %></td>
23
+ <td><%= link_to values[:expired], jobs_path(status: 'expired', queue_name: queue) %></td>
24
+ <td><%= @queue_latencies[queue] %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ </div>
@@ -58,3 +58,29 @@
58
58
  <% end %>
59
59
  </div>
60
60
  </div>
61
+ <div class="lockers">
62
+ <h1>Processes</h1>
63
+ <table cellspacing="0">
64
+ <thead>
65
+ <tr>
66
+ <th>ID</th>
67
+ <th>Worker count</th>
68
+ <th>Worker priorities</th>
69
+ <th>Listening</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ <% @lockers.each.with_index do |locker, index| %>
74
+ <tr>
75
+ <td class="locker-process">
76
+ <span>Process <%= index + 1 %></span>
77
+ <span><strong>Queues:</strong> <%= locker[:queues][1..-2].split(',').join(', ') %></span>
78
+ </td>
79
+ <td><%= locker[:worker_count] %></td>
80
+ <td><%= locker[:worker_priorities] %></td>
81
+ <td><%= locker[:listening] %></td>
82
+ </tr>
83
+ <% end %>
84
+ </tbody>
85
+ </table>
86
+ </div>
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ Que::View::Engine.routes.draw do
5
5
  post :reschedule_all, on: :collection
6
6
  delete :destroy_all, on: :collection
7
7
  end
8
+ resources :queue_metrics, only: %i[index]
8
9
 
9
10
  root 'welcome#index'
10
11
  end
data/lib/que/view/dsl.rb CHANGED
@@ -8,6 +8,21 @@ module Que
8
8
  execute(fetch_dashboard_stats_sql)
9
9
  end
10
10
 
11
+ def fetch_que_lockers
12
+ execute(fetch_que_lockers_sql)
13
+ end
14
+
15
+ def fetch_queue_metrics
16
+ execute(fetch_queue_metrics_sql).each_with_object({}) { |element, acc|
17
+ acc[element[:queue_name].to_sym] ||= basis_queue_stats
18
+ acc[element[:queue_name].to_sym][element[:status].to_sym] = element[:count_all]
19
+ }
20
+ end
21
+
22
+ def fetch_queue_latencies(queue_names)
23
+ queue_names.index_with { |queue_name| execute(fetch_queue_oldest_job_sql(queue_name)).dig(0, :enqueued_at) }
24
+ end
25
+
11
26
  def fetch_queue_names
12
27
  execute(fetch_queue_names_sql).map { |queues_data|
13
28
  ["#{queues_data[:queue_name]} (#{queues_data[:count_all]})", queues_data[:queue_name]]
@@ -20,7 +35,7 @@ module Que
20
35
  }
21
36
  end
22
37
 
23
- def fetch_running_jobs(...)
38
+ def fetch_running_jobs
24
39
  Que.job_states
25
40
  end
26
41
 
@@ -70,6 +85,10 @@ module Que
70
85
 
71
86
  private
72
87
 
88
+ def basis_queue_stats
89
+ { scheduled: 0, failing: 0, running: 0, finished: 0, expired: 0 }
90
+ end
91
+
73
92
  # rubocop: disable Metrics/MethodLength
74
93
  def fetch_dashboard_stats_sql
75
94
  <<-SQL.squish
@@ -88,6 +107,52 @@ module Que
88
107
  SQL
89
108
  end
90
109
 
110
+ def fetch_que_lockers_sql
111
+ <<-SQL.squish
112
+ SELECT *
113
+ FROM que_lockers
114
+ SQL
115
+ end
116
+
117
+ def fetch_queue_metrics_sql
118
+ <<-SQL.squish
119
+ SELECT COUNT(*) AS count_all, queue AS queue_name,
120
+ CASE
121
+ WHEN expired_at IS NOT NULL THEN 'expired'
122
+ WHEN finished_at IS NOT NULL THEN 'finished'
123
+ WHEN locks.job_id IS NULL AND error_count > 0 THEN 'failing'
124
+ WHEN locks.job_id IS NULL AND error_count = 0 THEN 'scheduled'
125
+ ELSE 'running'
126
+ END status
127
+ FROM que_jobs
128
+ LEFT JOIN (
129
+ SELECT (classid::bigint << 32) + objid::bigint AS job_id
130
+ FROM pg_locks
131
+ WHERE locktype = 'advisory'
132
+ ) locks ON (que_jobs.id=locks.job_id)
133
+ GROUP BY queue,
134
+ CASE
135
+ WHEN expired_at IS NOT NULL THEN 'expired'
136
+ WHEN finished_at IS NOT NULL THEN 'finished'
137
+ WHEN locks.job_id IS NULL AND error_count > 0 THEN 'failing'
138
+ WHEN locks.job_id IS NULL AND error_count = 0 THEN 'scheduled'
139
+ ELSE 'running'
140
+ END
141
+ SQL
142
+ end
143
+
144
+ def fetch_queue_oldest_job_sql(queue_name)
145
+ <<-SQL.squish
146
+ SELECT args #>> '{0, enqueued_at}' AS enqueued_at
147
+ FROM que_jobs
148
+ WHERE queue = '#{queue_name}'
149
+ AND expired_at IS NULL
150
+ AND finished_at IS NULL
151
+ ORDER BY args #>> '{0, enqueued_at}'
152
+ LIMIT 1
153
+ SQL
154
+ end
155
+
91
156
  def fetch_queue_names_sql
92
157
  <<-SQL.squish
93
158
  SELECT COUNT(*) AS count_all, queue AS queue_name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Que
4
4
  module View
5
- VERSION = '0.3.1'
5
+ VERSION = '0.3.3'
6
6
  end
7
7
  end
data/lib/que/view.rb CHANGED
@@ -34,7 +34,7 @@ module Que
34
34
  end
35
35
 
36
36
  # Public: All the methods delegated to instance. These should match the interface of Que::View::DSL.
37
- def_delegators :instance, :fetch_dashboard_stats,
37
+ def_delegators :instance, :fetch_dashboard_stats, :fetch_que_lockers, :fetch_queue_metrics, :fetch_queue_latencies,
38
38
  :fetch_queue_names, :fetch_job_names,
39
39
  :fetch_running_jobs, :fetch_failing_jobs, :fetch_scheduled_jobs, :fetch_finished_jobs,
40
40
  :fetch_expired_jobs, :fetch_job,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que-view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdanov Anton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: que
@@ -164,11 +164,13 @@ files:
164
164
  - app/assets/stylesheets/que/view/application.css
165
165
  - app/controllers/que/view/application_controller.rb
166
166
  - app/controllers/que/view/jobs_controller.rb
167
+ - app/controllers/que/view/queue_metrics_controller.rb
167
168
  - app/controllers/que/view/welcome_controller.rb
168
169
  - app/helpers/que/view/application_helper.rb
169
170
  - app/views/layouts/que/view/application.html.erb
170
171
  - app/views/que/view/jobs/index.html.erb
171
172
  - app/views/que/view/jobs/show.html.erb
173
+ - app/views/que/view/queue_metrics/index.html.erb
172
174
  - app/views/que/view/welcome/index.html.erb
173
175
  - config/routes.rb
174
176
  - lib/que/view.rb