pgbus 0.17.1 → 0.17.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 +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +83 -13
- data/Rakefile +26 -0
- data/app/assets/javascripts/pgbus/stream_source_element.js +34 -2
- data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
- data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
- data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
- data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
- data/app/controllers/pgbus/application_controller.rb +5 -2
- data/app/controllers/pgbus/batches_controller.rb +4 -1
- data/app/controllers/pgbus/dashboard_controller.rb +20 -6
- data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
- data/app/controllers/pgbus/events_controller.rb +52 -3
- data/app/controllers/pgbus/frontends_controller.rb +25 -11
- data/app/controllers/pgbus/insights_controller.rb +19 -1
- data/app/controllers/pgbus/jobs_controller.rb +45 -26
- data/app/controllers/pgbus/locks_controller.rb +7 -2
- data/app/controllers/pgbus/outbox_controller.rb +4 -1
- data/app/controllers/pgbus/processes_controller.rb +3 -1
- data/app/controllers/pgbus/queues_controller.rb +16 -4
- data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
- data/app/frontend/pgbus/application.min.js +2 -0
- data/app/frontend/pgbus/application.min.js.map +1 -0
- data/app/frontend/pgbus/modules/charts.js +137 -77
- data/app/frontend/pgbus/modules/charts.min.js +2 -0
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
- data/app/frontend/pgbus/style.css +1 -1
- data/app/frontend/pgbus/tailwind.css +18 -0
- data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
- data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
- data/app/helpers/pgbus/application_helper.rb +155 -113
- data/app/helpers/pgbus/button_helper.rb +88 -0
- data/app/helpers/pgbus/dashboard_helper.rb +157 -0
- data/app/helpers/pgbus/events_helper.rb +40 -0
- data/app/helpers/pgbus/insights_helper.rb +39 -0
- data/app/helpers/pgbus/processes_helper.rb +79 -0
- data/app/views/layouts/pgbus/application.html.erb +0 -1
- data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
- data/app/views/pgbus/batches/show.html.erb +3 -4
- data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
- data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
- data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
- data/app/views/pgbus/dashboard/show.html.erb +1 -1
- data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
- data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
- data/app/views/pgbus/dead_letter/index.html.erb +6 -9
- data/app/views/pgbus/dead_letter/show.html.erb +37 -21
- data/app/views/pgbus/events/_list.html.erb +55 -0
- data/app/views/pgbus/events/_processed_table.html.erb +45 -0
- data/app/views/pgbus/events/_replay.html.erb +10 -0
- data/app/views/pgbus/events/_row.html.erb +162 -0
- data/app/views/pgbus/events/_tabs.html.erb +13 -0
- data/app/views/pgbus/events/index.html.erb +27 -48
- data/app/views/pgbus/events/show.html.erb +25 -7
- data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
- data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
- data/app/views/pgbus/insights/_status_table.html.erb +19 -0
- data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
- data/app/views/pgbus/insights/show.html.erb +83 -100
- data/app/views/pgbus/jobs/_list.html.erb +64 -0
- data/app/views/pgbus/jobs/_row.html.erb +107 -0
- data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
- data/app/views/pgbus/jobs/index.html.erb +10 -10
- data/app/views/pgbus/jobs/show.html.erb +18 -20
- data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
- data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
- data/app/views/pgbus/outbox/index.html.erb +30 -27
- data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
- data/app/views/pgbus/processes/_row.html.erb +48 -0
- data/app/views/pgbus/processes/_status.html.erb +4 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
- data/app/views/pgbus/queues/show.html.erb +69 -118
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
- data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
- data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
- data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
- data/app/views/pgbus/shared/_pager.html.erb +34 -0
- data/config/i18n-tasks.yml +11 -2
- data/config/locales/da.yml +405 -113
- data/config/locales/de.yml +405 -113
- data/config/locales/en.yml +405 -113
- data/config/locales/es.yml +404 -112
- data/config/locales/fi.yml +404 -112
- data/config/locales/fr.yml +406 -114
- data/config/locales/it.yml +405 -113
- data/config/locales/ja.yml +405 -113
- data/config/locales/nb.yml +405 -113
- data/config/locales/nl.yml +405 -113
- data/config/locales/pt.yml +406 -114
- data/config/locales/sv.yml +404 -112
- data/config/routes.rb +1 -1
- data/lib/generators/pgbus/add_presence_generator.rb +2 -1
- data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
- data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
- data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
- data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
- data/lib/pgbus/active_job/executor.rb +16 -6
- data/lib/pgbus/cli/dlq.rb +32 -5
- data/lib/pgbus/client.rb +4 -2
- data/lib/pgbus/configuration/presence_settings.rb +103 -0
- data/lib/pgbus/configuration.rb +51 -2
- data/lib/pgbus/dead_letter_header.rb +129 -0
- data/lib/pgbus/doctor.rb +8 -4
- data/lib/pgbus/engine.rb +13 -7
- data/lib/pgbus/event_bus/handler.rb +18 -11
- data/lib/pgbus/event_bus/registry.rb +6 -0
- data/lib/pgbus/failed_event_recorder.rb +34 -0
- data/lib/pgbus/generators/migration_detector.rb +9 -1
- data/lib/pgbus/instrumentation.rb +2 -1
- data/lib/pgbus/mcp/base_tool.rb +25 -0
- data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
- data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
- data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
- data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
- data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
- data/lib/pgbus/outbox/poller.rb +1 -1
- data/lib/pgbus/process/consumer.rb +53 -3
- data/lib/pgbus/process/dispatcher.rb +1 -1
- data/lib/pgbus/process/supervisor.rb +32 -11
- data/lib/pgbus/process/worker.rb +45 -8
- data/lib/pgbus/recurring/scheduler.rb +2 -1
- data/lib/pgbus/streams/presence.rb +317 -81
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
- data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
- data/lib/pgbus/web/data_source/event_list.rb +161 -0
- data/lib/pgbus/web/data_source/job_list.rb +323 -0
- data/lib/pgbus/web/data_source/list_counts.rb +80 -0
- data/lib/pgbus/web/data_source/process_list.rb +69 -0
- data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
- data/lib/pgbus/web/data_source.rb +132 -254
- data/lib/pgbus/web/dead_letter_reason.rb +81 -0
- data/lib/pgbus/web/event_state.rb +71 -0
- data/lib/pgbus/web/health_app.rb +7 -3
- data/lib/pgbus/web/health_verdict.rb +78 -0
- data/lib/pgbus/web/insights_charts.rb +84 -0
- data/lib/pgbus/web/job_state.rb +111 -0
- data/lib/pgbus/web/process_activity.rb +97 -0
- data/lib/pgbus/web/process_state.rb +187 -0
- data/lib/pgbus/web/queue_summary.rb +104 -0
- data/lib/pgbus/web/stream_app.rb +18 -6
- data/lib/pgbus/web/streamer/connection.rb +7 -1
- data/lib/pgbus/web/streamer/falcon_connection.rb +7 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
- data/lib/pgbus/web/streamer/instance.rb +2 -1
- data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +8 -37
- data/lib/pgbus/web/time_format.rb +128 -0
- metadata +55 -4
- data/app/views/pgbus/events/_pending_table.html.erb +0 -169
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
- data/app/views/pgbus/jobs/_failed_table.html.erb +0 -76
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
class DataSource
|
|
6
|
+
# The Jobs page's unified list (issue #489): every job the dashboard can
|
|
7
|
+
# see, one row each, whatever table it lives in, with its state derived
|
|
8
|
+
# in SQL so tabs, counts and paging all cut on the same value.
|
|
9
|
+
#
|
|
10
|
+
# Sources, one UNION ALL fragment each:
|
|
11
|
+
# - every non-DLQ queue table, LEFT JOINed to its pgbus_failed_events row
|
|
12
|
+
# (failed_events.queue_name is the LOGICAL name; the physical one is
|
|
13
|
+
# matched too for rows older code wrote)
|
|
14
|
+
# - failed rows whose message is no longer in its queue ("orphans")
|
|
15
|
+
# - pgbus_blocked_executions (jobs parked by limits_concurrency)
|
|
16
|
+
#
|
|
17
|
+
# Queue names reach SQL only through sanitize_name; the state filter and
|
|
18
|
+
# paging are bound parameters. Each fragment carries its own ORDER BY +
|
|
19
|
+
# LIMIT so it stays index-backed and bounded; msg_ids from different
|
|
20
|
+
# queues are not comparable, so the outer sort is by time.
|
|
21
|
+
#
|
|
22
|
+
# queues: cuts the list to those physical queues (the Events page passes
|
|
23
|
+
# the EventBus handler queues); exclude: leaves physical queues out (the
|
|
24
|
+
# Jobs page leaves the handler queues to the Events page). Both are
|
|
25
|
+
# optional, and without them the SQL is unchanged (issue #494).
|
|
26
|
+
module JobList
|
|
27
|
+
# A failure recorded after the read that ran the attempt means the job
|
|
28
|
+
# is backing off; a later read (last_read_at past failed_at) means the
|
|
29
|
+
# retry attempt is running now.
|
|
30
|
+
STATE_CASE = <<~SQL.gsub(/\s+/, " ").strip.freeze
|
|
31
|
+
CASE
|
|
32
|
+
WHEN m.read_ct = 0 AND m.vt > now() THEN 'scheduled'
|
|
33
|
+
WHEN m.read_ct = 0 THEN 'ready'
|
|
34
|
+
WHEN f.failed_at IS NOT NULL AND f.failed_at >= m.last_read_at THEN 'retrying'
|
|
35
|
+
WHEN m.vt > now() THEN 'running'
|
|
36
|
+
ELSE 'ready'
|
|
37
|
+
END
|
|
38
|
+
SQL
|
|
39
|
+
|
|
40
|
+
# Per-fragment ceiling for the tab counts, so a million-row queue never
|
|
41
|
+
# forces a full scan on every auto-refresh. A fragment that reaches it
|
|
42
|
+
# makes its states' counts lower bounds.
|
|
43
|
+
COUNT_CAP = 10_000
|
|
44
|
+
|
|
45
|
+
QUEUE_STATES = %w[ready scheduled running retrying].freeze
|
|
46
|
+
|
|
47
|
+
# A predicate on m that each state branch of STATE_CASE already implies.
|
|
48
|
+
# Without it a filtered tab walks the whole pkey and probes the LATERAL for
|
|
49
|
+
# every row before LIMIT bites (300k-row queue, no matches: ~350 ms); with it
|
|
50
|
+
# only candidates reach the CASE. The exact `q.state = $1` still applies.
|
|
51
|
+
STATE_PREFILTER = {
|
|
52
|
+
"scheduled" => "m.read_ct = 0 AND m.vt > now()",
|
|
53
|
+
"ready" => "m.vt <= now()",
|
|
54
|
+
"running" => "m.read_ct > 0 AND m.vt > now()",
|
|
55
|
+
"retrying" => "m.read_ct > 0"
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
INTEGER_COLUMNS = %i[id msg_id read_ct failed_event_id slots_held slots_max].freeze
|
|
59
|
+
|
|
60
|
+
StateCounts = Data.define(:counts, :capped) do
|
|
61
|
+
def [](state) = counts.fetch(state.to_s, 0)
|
|
62
|
+
def capped?(state) = capped.include?(state.to_s)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def job_rows(state: nil, queue_name: nil, queues: nil, exclude: nil, page: 1, per_page: 25)
|
|
66
|
+
scope = job_list_scope(queue_name, queues: queues, exclude: exclude)
|
|
67
|
+
return [] unless scope
|
|
68
|
+
|
|
69
|
+
binds = []
|
|
70
|
+
state_param = nil
|
|
71
|
+
if state && state != "blocked" && scope[:queues].any?
|
|
72
|
+
binds << state
|
|
73
|
+
state_param = "$1"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
fetch = (page * per_page).to_i
|
|
77
|
+
fragments = job_row_fragments(scope, state, state_param, fetch)
|
|
78
|
+
return [] if fragments.empty?
|
|
79
|
+
|
|
80
|
+
sql = <<~SQL
|
|
81
|
+
SELECT * FROM (#{fragments.join("\nUNION ALL\n")}) AS jobs
|
|
82
|
+
ORDER BY sort_at DESC NULLS LAST, id DESC
|
|
83
|
+
LIMIT $#{binds.size + 1} OFFSET $#{binds.size + 2}
|
|
84
|
+
SQL
|
|
85
|
+
binds.push(per_page, (page - 1) * per_page)
|
|
86
|
+
|
|
87
|
+
connection.select_all(sql, "Pgbus Job List", binds).to_a.map { |row| format_job_row(row) }
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Pgbus.logger.error { "[Pgbus::Web] Error reading the job list: #{e.class}: #{e.message}" }
|
|
90
|
+
[]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def job_state_counts(queue_name: nil, queues: nil, exclude: nil)
|
|
94
|
+
scope = job_list_scope(queue_name, queues: queues, exclude: exclude)
|
|
95
|
+
return empty_state_counts unless scope
|
|
96
|
+
|
|
97
|
+
kinds = []
|
|
98
|
+
fragments = []
|
|
99
|
+
count_fragments(scope).each do |kind, sql|
|
|
100
|
+
fragments << "(SELECT #{kinds.size} AS frag, state FROM (#{sql}) c LIMIT #{COUNT_CAP + 1})"
|
|
101
|
+
kinds << kind
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
rows = connection.select_all(<<~SQL, "Pgbus Job State Counts").to_a
|
|
105
|
+
SELECT frag, state, COUNT(*) AS n FROM (#{fragments.join("\nUNION ALL\n")}) AS counted
|
|
106
|
+
GROUP BY frag, state
|
|
107
|
+
SQL
|
|
108
|
+
tally_state_counts(rows, kinds)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
Pgbus.logger.error { "[Pgbus::Web] Error counting job states: #{e.class}: #{e.message}" }
|
|
111
|
+
empty_state_counts
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# { [physical_queue, msg_id] => jobs_ahead } for the ready queue rows
|
|
115
|
+
# on one page: one bounded COUNT per distinct queue, never per row.
|
|
116
|
+
def jobs_ahead(rows)
|
|
117
|
+
ready = rows.select { |r| r[:source] == "queue" && r[:state] == "ready" }
|
|
118
|
+
ready.group_by { |r| r[:queue_name] }.each_with_object({}) do |(queue, queue_rows), out|
|
|
119
|
+
out.merge!(jobs_ahead_in(queue, queue_rows.map { |r| r[:msg_id].to_i }.sort))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def job_list_context(now: Time.now)
|
|
124
|
+
alive = ->(kind) { processes.any? { |p| p[:kind].to_s == kind && p[:healthy] } }
|
|
125
|
+
JobState::Context.new(now: now, max_retries: Pgbus.configuration.max_retries,
|
|
126
|
+
paused: paused_queue_names.to_set, drained: drained_queue_names,
|
|
127
|
+
workers_alive: alive.call("worker"), handler_queues: handler_queue_physical_names.to_set,
|
|
128
|
+
consumers_alive: alive.call("consumer"))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# The physical non-DLQ queues in view, grouped under their logical
|
|
134
|
+
# name, or nil when the requested queue (or every listed one) does not
|
|
135
|
+
# exist. Excluded queues stay known, so their failed rows never show up
|
|
136
|
+
# as orphans of an unknown queue.
|
|
137
|
+
def job_list_scope(queue_name, queues: nil, exclude: nil)
|
|
138
|
+
names = queues_with_metrics.map { |q| q[:name] }.reject { |n| n.end_with?(Pgbus::DEAD_LETTER_SUFFIX) }
|
|
139
|
+
names &= queues if queues
|
|
140
|
+
return nil if queues && names.empty?
|
|
141
|
+
|
|
142
|
+
excluded = exclude ? names & exclude : []
|
|
143
|
+
names -= excluded
|
|
144
|
+
if queue_name
|
|
145
|
+
return nil unless names.include?(queue_name)
|
|
146
|
+
|
|
147
|
+
names = [queue_name]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
pairs = ->(list) { list.map { |n| [sanitize_name(n), logical_queue_name(n)] } }
|
|
151
|
+
scoped = pairs.call(names)
|
|
152
|
+
{ queues: scoped, by_logical: scoped.group_by(&:last), excluded: pairs.call(excluded),
|
|
153
|
+
filtered: !queue_name.nil? || !queues.nil? }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def job_row_fragments(scope, state, state_param, fetch)
|
|
157
|
+
fragments = []
|
|
158
|
+
unless state == "blocked"
|
|
159
|
+
fragments.concat(scope[:queues].map { |(qtable, logical)| queue_row_fragment(qtable, logical, state, state_param, fetch) })
|
|
160
|
+
end
|
|
161
|
+
fragments.concat(orphan_fragments(scope, fetch)) if state.nil? || state == "retrying"
|
|
162
|
+
fragments << blocked_fragment(scope, fetch) if state.nil? || state == "blocked"
|
|
163
|
+
fragments
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def queue_row_fragment(qtable, logical, state, state_param, fetch)
|
|
167
|
+
where = state_param ? "WHERE q.state = #{state_param}" : ""
|
|
168
|
+
prefilter = state_param && STATE_PREFILTER[state] ? "WHERE #{STATE_PREFILTER[state]}" : ""
|
|
169
|
+
<<~SQL.strip
|
|
170
|
+
(SELECT * FROM (
|
|
171
|
+
SELECT 'queue'::text AS source, m.msg_id AS id, '#{qtable}'::text AS queue_name,
|
|
172
|
+
'#{logical}'::text AS logical_queue, m.msg_id, m.message->>'job_class' AS job_class,
|
|
173
|
+
m.read_ct, m.enqueued_at, m.last_read_at, m.vt, #{STATE_CASE} AS state,
|
|
174
|
+
f.error_class, f.error_message, f.id AS failed_event_id,
|
|
175
|
+
NULL::text AS concurrency_key, NULL::integer AS slots_held, NULL::integer AS slots_max,
|
|
176
|
+
m.message::text AS payload, m.headers::text AS headers, m.enqueued_at AS sort_at
|
|
177
|
+
FROM pgmq.q_#{qtable} m
|
|
178
|
+
#{failed_join(qtable, logical)}
|
|
179
|
+
#{prefilter}
|
|
180
|
+
) q #{where} ORDER BY q.msg_id DESC LIMIT #{fetch})
|
|
181
|
+
SQL
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def failed_join(qtable, logical)
|
|
185
|
+
<<~SQL.strip
|
|
186
|
+
LEFT JOIN LATERAL (
|
|
187
|
+
SELECT f.id, f.error_class::text AS error_class, f.error_message,
|
|
188
|
+
f.failed_at::timestamptz AS failed_at
|
|
189
|
+
FROM pgbus_failed_events f
|
|
190
|
+
WHERE f.queue_name IN ('#{logical}', '#{qtable}') AND f.msg_id = m.msg_id
|
|
191
|
+
ORDER BY f.failed_at DESC LIMIT 1
|
|
192
|
+
) f ON true
|
|
193
|
+
SQL
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Failed rows whose message left its queue: one fragment per logical
|
|
197
|
+
# queue, plus (unfiltered) one for rows whose queue no longer exists.
|
|
198
|
+
def orphan_fragments(scope, fetch)
|
|
199
|
+
fragments = scope[:by_logical].map do |logical, tables|
|
|
200
|
+
missing = tables.map { |(qtable, _)| "NOT EXISTS (SELECT 1 FROM pgmq.q_#{qtable} m WHERE m.msg_id = f.msg_id)" }
|
|
201
|
+
names = quoted_list([logical, *tables.map(&:first)])
|
|
202
|
+
orphan_fragment("'#{logical}'::text", "f.queue_name IN (#{names}) AND #{missing.join(" AND ")}", fetch)
|
|
203
|
+
end
|
|
204
|
+
return fragments if scope[:filtered]
|
|
205
|
+
|
|
206
|
+
known = scope[:by_logical].flat_map { |logical, tables| [logical, *tables.map(&:first)] }
|
|
207
|
+
known += scope[:excluded].flat_map { |(qtable, logical)| [logical, qtable] }
|
|
208
|
+
where = known.empty? ? "true" : "NOT (f.queue_name = ANY(ARRAY[#{quoted_list(known)}]))"
|
|
209
|
+
fragments << orphan_fragment("f.queue_name::text", where, fetch)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def orphan_fragment(logical_sql, where, fetch)
|
|
213
|
+
<<~SQL.strip
|
|
214
|
+
(SELECT 'failed'::text AS source, f.id, f.queue_name::text AS queue_name, #{logical_sql} AS logical_queue,
|
|
215
|
+
f.msg_id, f.payload->>'job_class' AS job_class, (f.retry_count + 1) AS read_ct,
|
|
216
|
+
NULL::timestamptz AS enqueued_at, NULL::timestamptz AS last_read_at, NULL::timestamptz AS vt,
|
|
217
|
+
'retrying'::text AS state, f.error_class::text AS error_class, f.error_message,
|
|
218
|
+
f.id AS failed_event_id, NULL::text AS concurrency_key, NULL::integer AS slots_held,
|
|
219
|
+
NULL::integer AS slots_max, f.payload::text AS payload, f.headers::text AS headers,
|
|
220
|
+
f.failed_at::timestamptz AS sort_at
|
|
221
|
+
FROM pgbus_failed_events f WHERE #{where} ORDER BY f.id DESC LIMIT #{fetch})
|
|
222
|
+
SQL
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def blocked_fragment(scope, fetch)
|
|
226
|
+
<<~SQL.strip
|
|
227
|
+
(SELECT 'blocked'::text AS source, b.id, b.queue_name::text AS queue_name,
|
|
228
|
+
b.queue_name::text AS logical_queue, NULL::bigint AS msg_id, b.payload->>'job_class' AS job_class,
|
|
229
|
+
NULL::integer AS read_ct, b.created_at::timestamptz AS enqueued_at,
|
|
230
|
+
NULL::timestamptz AS last_read_at, NULL::timestamptz AS vt, 'blocked'::text AS state,
|
|
231
|
+
NULL::text AS error_class, NULL::text AS error_message, NULL::bigint AS failed_event_id,
|
|
232
|
+
b.concurrency_key::text AS concurrency_key, s.value AS slots_held, s.max_value AS slots_max,
|
|
233
|
+
b.payload::text AS payload, NULL::text AS headers, b.created_at::timestamptz AS sort_at
|
|
234
|
+
FROM pgbus_blocked_executions b
|
|
235
|
+
LEFT JOIN pgbus_semaphores s ON s.key = b.concurrency_key
|
|
236
|
+
#{blocked_where(scope)} ORDER BY b.created_at DESC LIMIT #{fetch})
|
|
237
|
+
SQL
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def blocked_where(scope)
|
|
241
|
+
if scope[:filtered]
|
|
242
|
+
return "WHERE false" if scope[:queues].empty?
|
|
243
|
+
|
|
244
|
+
"WHERE b.queue_name IN (#{quoted_list(scope[:queues].flat_map { |(qtable, logical)| [logical, qtable] })})"
|
|
245
|
+
elsif scope[:excluded].any?
|
|
246
|
+
"WHERE b.queue_name NOT IN (#{quoted_list(scope[:excluded].flat_map { |(qtable, logical)| [logical, qtable] })})"
|
|
247
|
+
else
|
|
248
|
+
""
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# [kind, sql selecting a `state` column] per fragment, uncapped.
|
|
253
|
+
def count_fragments(scope)
|
|
254
|
+
queue = scope[:queues].map do |(qtable, logical)|
|
|
255
|
+
[:queue, "SELECT #{STATE_CASE} AS state FROM pgmq.q_#{qtable} m #{failed_join(qtable, logical)}"]
|
|
256
|
+
end
|
|
257
|
+
orphans = orphan_fragments(scope, COUNT_CAP + 1).map { |sql| [:orphan, "SELECT state FROM #{sql} o"] }
|
|
258
|
+
queue + orphans + [[:blocked, "SELECT state FROM #{blocked_fragment(scope, COUNT_CAP + 1)} b2"]]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def tally_state_counts(rows, kinds)
|
|
262
|
+
counts = Hash.new(0)
|
|
263
|
+
per_fragment = Hash.new(0)
|
|
264
|
+
rows.each do |row|
|
|
265
|
+
n = row["n"].to_i
|
|
266
|
+
counts[row["state"]] += n
|
|
267
|
+
per_fragment[row["frag"].to_i] += n
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
capped = Set.new
|
|
271
|
+
per_fragment.each do |frag, total|
|
|
272
|
+
next if total <= COUNT_CAP
|
|
273
|
+
|
|
274
|
+
capped.merge(capped_states_for(kinds[frag]))
|
|
275
|
+
end
|
|
276
|
+
counts["all"] = counts.values.sum
|
|
277
|
+
StateCounts.new(counts: counts.to_h, capped: capped)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def capped_states_for(kind)
|
|
281
|
+
states = { queue: QUEUE_STATES, orphan: %w[retrying], blocked: %w[blocked] }.fetch(kind)
|
|
282
|
+
[*states, "all"]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def empty_state_counts
|
|
286
|
+
StateCounts.new(counts: {}, capped: Set.new)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def jobs_ahead_in(queue, msg_ids)
|
|
290
|
+
@jobs_ahead_memo ||= {}
|
|
291
|
+
@jobs_ahead_memo[[queue, msg_ids]] ||= begin
|
|
292
|
+
rows = connection.select_all(<<~SQL, "Pgbus Jobs Ahead", ["{#{msg_ids.join(",")}}"])
|
|
293
|
+
SELECT x.msg_id, (
|
|
294
|
+
SELECT COUNT(*) FROM (
|
|
295
|
+
SELECT 1 FROM pgmq.q_#{sanitize_name(queue)} m
|
|
296
|
+
WHERE m.vt <= now() AND m.msg_id < x.msg_id LIMIT #{JobState::AHEAD_CAP}
|
|
297
|
+
) s
|
|
298
|
+
) AS ahead
|
|
299
|
+
FROM unnest($1::bigint[]) AS x(msg_id)
|
|
300
|
+
SQL
|
|
301
|
+
rows.to_a.to_h { |r| [[queue, r["msg_id"].to_i], r["ahead"].to_i] }
|
|
302
|
+
end
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
Pgbus.logger.error { "[Pgbus::Web] Error counting jobs ahead in #{queue}: #{e.class}: #{e.message}" }
|
|
305
|
+
{}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Values are sanitized queue names (word characters only), so quoting
|
|
309
|
+
# them as literals is safe.
|
|
310
|
+
def quoted_list(names)
|
|
311
|
+
names.uniq.map { |n| "'#{sanitize_name(n)}'" }.join(", ")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def format_job_row(row)
|
|
315
|
+
row.to_h.each_with_object({}) do |(key, value), out|
|
|
316
|
+
sym = key.to_sym
|
|
317
|
+
out[sym] = INTEGER_COLUMNS.include?(sym) && !value.nil? ? value.to_i : value
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
class DataSource
|
|
6
|
+
# Row totals for the dashboard's paged lists (issue #496). list_count
|
|
7
|
+
# stops at COUNT_CAP + 1 rows, so a table with millions of rows costs no
|
|
8
|
+
# more than one with ten thousand; past the cap the pager shows "10,000+".
|
|
9
|
+
# The same bound JobList uses for its state tabs. batches_count and
|
|
10
|
+
# recurring_tasks_count are exact (summary_stats' badge reads the latter);
|
|
11
|
+
# they predate the pagers and live here to keep data_source.rb small.
|
|
12
|
+
module ListCounts
|
|
13
|
+
COUNT_CAP = 10_000
|
|
14
|
+
|
|
15
|
+
Count = Data.define(:total, :capped) do
|
|
16
|
+
def capped? = capped
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
EMPTY_COUNT = Count.new(total: 0, capped: false)
|
|
20
|
+
|
|
21
|
+
# Semaphores and parked jobs, FULL OUTER JOINed: a key can have parked
|
|
22
|
+
# rows and no semaphore (its holder died and the sweep removed the row)
|
|
23
|
+
# or a semaphore and nothing parked. Shared with concurrency_keys.
|
|
24
|
+
CONCURRENCY_KEYS_FROM = <<~SQL
|
|
25
|
+
FROM pgbus_semaphores s
|
|
26
|
+
FULL OUTER JOIN (
|
|
27
|
+
SELECT concurrency_key, COUNT(*) AS parked_count, MIN(created_at) AS oldest_parked_at
|
|
28
|
+
FROM pgbus_blocked_executions
|
|
29
|
+
GROUP BY concurrency_key
|
|
30
|
+
) b ON s.key = b.concurrency_key
|
|
31
|
+
SQL
|
|
32
|
+
|
|
33
|
+
LIST_MODELS = {
|
|
34
|
+
batches: -> { BatchEntry },
|
|
35
|
+
job_locks: -> { UniquenessKey },
|
|
36
|
+
outbox: -> { OutboxEntry },
|
|
37
|
+
processed_events: -> { ProcessedEvent },
|
|
38
|
+
recurring_tasks: -> { RecurringTask }
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# list – :batches, :job_locks, :concurrency_keys, :outbox, :processed_events or :recurring_tasks.
|
|
42
|
+
def list_count(list)
|
|
43
|
+
raise ArgumentError, "unknown list: #{list.inspect}" unless list == :concurrency_keys || LIST_MODELS.key?(list)
|
|
44
|
+
|
|
45
|
+
bounded_count(list)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def batches_count
|
|
49
|
+
BatchEntry.count
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting batches: #{e.message}" }
|
|
52
|
+
0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def recurring_tasks_count
|
|
56
|
+
RecurringTask.count
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting recurring tasks: #{e.message}" }
|
|
59
|
+
0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def bounded_count(list)
|
|
65
|
+
rows = list == :concurrency_keys ? concurrency_keys_probe : LIST_MODELS.fetch(list).call.limit(COUNT_CAP + 1).count
|
|
66
|
+
Count.new(total: [rows.to_i, COUNT_CAP].min, capped: rows.to_i > COUNT_CAP)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error counting #{list}: #{e.message}" }
|
|
69
|
+
EMPTY_COUNT
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def concurrency_keys_probe
|
|
73
|
+
connection.select_value(<<~SQL, "Pgbus Concurrency Keys Count")
|
|
74
|
+
SELECT COUNT(*) FROM (SELECT 1 #{CONCURRENCY_KEYS_FROM} LIMIT #{COUNT_CAP + 1}) c
|
|
75
|
+
SQL
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
class DataSource
|
|
6
|
+
# The Processes page's rows and the context ProcessState needs to put
|
|
7
|
+
# them in words (issue #493). Every status in one result set is derived
|
|
8
|
+
# against one ProcessState::Thresholds, so one now.
|
|
9
|
+
module ProcessList
|
|
10
|
+
# thresholds: pass the ones the caller puts the rows in words with, so
|
|
11
|
+
# badge and sentence share one now.
|
|
12
|
+
def processes(thresholds: process_thresholds)
|
|
13
|
+
process_rows.map { |r| format_process(r, thresholds) }
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching processes: #{e.message}" }
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The dashboard host's config: the stall limits the page states are
|
|
20
|
+
# this host's stall_threshold, as the badge always was.
|
|
21
|
+
def process_thresholds(now: Time.now)
|
|
22
|
+
config = Pgbus.configuration
|
|
23
|
+
ProcessState::Thresholds.new(
|
|
24
|
+
now: now, alive_threshold: Process::Heartbeat::ALIVE_THRESHOLD, stall_threshold: config.stall_threshold,
|
|
25
|
+
dispatch_interval: config.dispatch_interval, recurring_schedule_interval: config.recurring_schedule_interval,
|
|
26
|
+
polling_interval: config.polling_interval, notify_fallback_poll: Process::Consumer::NOTIFY_FALLBACK_POLL_SECONDS,
|
|
27
|
+
notify_wakeup: config.worker_notify_wakeup?, reap_interval: Process::Dispatcher::REAP_INTERVAL
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_context(rows:, thresholds: process_thresholds)
|
|
32
|
+
ProcessState.context_for(rows, thresholds)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# The raw rows, memoized per instance like queues_with_metrics (cleared
|
|
38
|
+
# by reset_cache!): the home page reads them for the stats, the signals,
|
|
39
|
+
# the health verdict, the queue lines and the panel (issue #492). Only
|
|
40
|
+
# the rows: each caller derives statuses against its own thresholds.
|
|
41
|
+
def process_rows
|
|
42
|
+
@process_rows ||= connection.select_all("SELECT * FROM pgbus_processes ORDER BY kind, created_at").to_a
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_process(row, thresholds)
|
|
46
|
+
heartbeat = JobState.time(row["last_heartbeat_at"])
|
|
47
|
+
metadata = row["metadata"].is_a?(String) ? JSON.parse(row["metadata"]) : row["metadata"]
|
|
48
|
+
status = ProcessState.derive(kind: row["kind"], metadata: metadata, heartbeat_at: heartbeat,
|
|
49
|
+
thresholds: thresholds)
|
|
50
|
+
started_at = metadata.is_a?(Hash) && metadata["started_at"]
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
id: row["id"].to_i,
|
|
54
|
+
kind: row["kind"],
|
|
55
|
+
hostname: row["hostname"],
|
|
56
|
+
pid: row["pid"].to_i,
|
|
57
|
+
metadata: metadata,
|
|
58
|
+
last_heartbeat_at: heartbeat,
|
|
59
|
+
healthy: status == :healthy,
|
|
60
|
+
status: status,
|
|
61
|
+
created_at: row["created_at"],
|
|
62
|
+
supervisor_pid: metadata.is_a?(Hash) ? metadata["supervisor_pid"] : nil,
|
|
63
|
+
started_at: started_at ? Time.at(started_at.to_f) : nil
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
class DataSource
|
|
6
|
+
# The facts behind a queue page's summary in words (issue #491), turned
|
|
7
|
+
# into sentences by Pgbus::Web::QueueSummary. Both methods take the
|
|
8
|
+
# physical queue name the page is for.
|
|
9
|
+
module QueueSummary
|
|
10
|
+
RUNNING = { paused: false, reason: nil, paused_at: nil, resumes_at: nil, trip_count: 0 }.freeze
|
|
11
|
+
|
|
12
|
+
# Pause state is kept per logical queue, so every priority level and
|
|
13
|
+
# the bare table share it.
|
|
14
|
+
def queue_pause_state(name)
|
|
15
|
+
pause_state_of(QueueState.find_by(queue_name: logical_queue_name(name)))
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
Pgbus.logger.error { "[Pgbus::Web] Error reading pause state for #{name}: #{e.class}: #{e.message}" }
|
|
18
|
+
RUNNING
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# { physical name => pause state } for many queues in one query (the
|
|
22
|
+
# home page's queues table, issue #492). Raises: the caller degrades.
|
|
23
|
+
def queue_pause_states(names)
|
|
24
|
+
states = QueueState.where(queue_name: names.map { |n| logical_queue_name(n) }.uniq).index_by(&:queue_name)
|
|
25
|
+
names.to_h { |name| [name, pause_state_of(states[logical_queue_name(name)])] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# healthy: and streams: let a caller describing many queues load the
|
|
29
|
+
# process rows and the stream registry once (issue #492).
|
|
30
|
+
def queue_drainers(name, healthy: processes.select { |p| p[:healthy] }, streams: stream_queue_names)
|
|
31
|
+
table = name.to_s.delete_suffix(Pgbus::DEAD_LETTER_SUFFIX)
|
|
32
|
+
level = priority_level_of(table)
|
|
33
|
+
logical = level ? logical_queue_name(table) : table.delete_prefix("#{Pgbus.configuration.queue_prefix}_")
|
|
34
|
+
capsules, wildcard = draining_capsules(name)
|
|
35
|
+
{
|
|
36
|
+
logical: logical, capsules: capsules, wildcard: wildcard,
|
|
37
|
+
handler: handler_queue_physical_names.include?(name), stream: streams.include?(name),
|
|
38
|
+
live_workers: healthy.count { |p| p[:kind].to_s == "worker" && worker_drains?(p, logical) },
|
|
39
|
+
live_consumers: healthy.count { |p| p[:kind].to_s == "consumer" && consumer_drains?(p, name) },
|
|
40
|
+
siblings: level ? priority_siblings(name, logical) : [], priority_level: level
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def pause_state_of(state)
|
|
47
|
+
return RUNNING unless state&.paused
|
|
48
|
+
|
|
49
|
+
{ paused: true, reason: state.paused_reason, paused_at: state.paused_at,
|
|
50
|
+
resumes_at: state.circuit_breaker_resume_at, trip_count: state.circuit_breaker_trip_count.to_i }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The level of a priority sub-table, or nil. `_pN` is legal in an
|
|
54
|
+
# ordinary queue name, so only a table the queue strategy creates for
|
|
55
|
+
# the stripped logical name counts.
|
|
56
|
+
def priority_level_of(table)
|
|
57
|
+
level = table[Pgbus::Web::QueueSummary::PRIORITY_LEVEL, 1]
|
|
58
|
+
return unless level && @client.physical_queue_names(logical_queue_name(table)).include?(table)
|
|
59
|
+
|
|
60
|
+
level.to_i
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error resolving priority level of #{table}: #{e.class}: #{e.message}" }
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# [names of the capsules whose queues expand to this table, wildcard?].
|
|
67
|
+
# An anonymous capsule goes by its first queue. Fails open to a
|
|
68
|
+
# wildcard, like drained_queue_names: never a false "nothing drains it".
|
|
69
|
+
def draining_capsules(name)
|
|
70
|
+
capsules = Array(Pgbus.configuration.workers)
|
|
71
|
+
wildcard = capsules.any? { |c| capsule_queue_list(c).include?("*") }
|
|
72
|
+
names = capsules.filter_map do |c|
|
|
73
|
+
queues = capsule_queue_list(c) - ["*"]
|
|
74
|
+
next unless queues.any? { |q| @client.physical_queue_names(q).include?(name) }
|
|
75
|
+
|
|
76
|
+
(c[:name] || c["name"] || queues.first).to_s
|
|
77
|
+
end
|
|
78
|
+
[names, wildcard]
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
Pgbus.logger.error { "[Pgbus::Web] Error resolving capsules for #{name}: #{e.class}: #{e.message}" }
|
|
81
|
+
[[], true]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def capsule_queue_list(capsule)
|
|
85
|
+
Array(capsule[:queues] || capsule["queues"]).map(&:to_s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A heartbeat lists queue names as configured (Array or comma-separated
|
|
89
|
+
# String); tables are named after their normalized form ("bulk-imports"
|
|
90
|
+
# → pgbus_bulk_imports), so compare that.
|
|
91
|
+
def worker_drains?(process, logical)
|
|
92
|
+
queues = Array((process[:metadata] || {})["queues"]).flat_map { |q| q.to_s.split(",") }.map(&:strip)
|
|
93
|
+
queues.include?("*") || queues.any? { |q| normalized_queue(q) == logical }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A consumer drains a handler queue when one of its topic filters
|
|
97
|
+
# overlaps a subscription routed to it (the rule the consumer itself
|
|
98
|
+
# uses to pick queues). A heartbeat without topics counts.
|
|
99
|
+
def consumer_drains?(process, name)
|
|
100
|
+
topics = Array((process[:metadata] || {})["topics"])
|
|
101
|
+
return true if topics.empty?
|
|
102
|
+
|
|
103
|
+
patterns = registered_subscribers.select { |s| s[:physical_queue_name] == name }.map { |s| s[:pattern] }
|
|
104
|
+
registry = EventBus::Registry.instance
|
|
105
|
+
topics.any? { |t| patterns.any? { |pattern| registry.pattern_overlaps?(t.to_s, pattern) } }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def normalized_queue(queue)
|
|
109
|
+
Pgbus.configuration.queue_name(queue).delete_prefix("#{Pgbus.configuration.queue_prefix}_")
|
|
110
|
+
rescue ArgumentError
|
|
111
|
+
queue
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def priority_siblings(name, logical)
|
|
115
|
+
queues_with_metrics.map { |q| q[:name] }.select do |n|
|
|
116
|
+
n != name && n.match?(Pgbus::Web::QueueSummary::PRIORITY_LEVEL) && logical_queue_name(n) == logical
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|