pgbus 0.17.0 → 0.17.2
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 +50 -0
- data/README.md +69 -14
- data/Rakefile +7 -1
- 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/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/modules/charts.js +119 -77
- data/app/frontend/pgbus/style.css +1 -1
- data/app/frontend/pgbus/tailwind.css +18 -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/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/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
- data/lib/pgbus/active_job/executor.rb +36 -17
- data/lib/pgbus/cli/dlq.rb +32 -5
- data/lib/pgbus/client.rb +7 -2
- data/lib/pgbus/configuration.rb +93 -2
- data/lib/pgbus/dead_letter_header.rb +129 -0
- data/lib/pgbus/doctor.rb +37 -5
- 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 +51 -0
- 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/claim_buffer.rb +115 -0
- data/lib/pgbus/process/consumer.rb +94 -17
- data/lib/pgbus/process/dispatcher.rb +1 -1
- data/lib/pgbus/process/supervisor.rb +35 -12
- data/lib/pgbus/process/worker.rb +91 -25
- data/lib/pgbus/recurring/scheduler.rb +2 -1
- data/lib/pgbus/ruby_jit.rb +21 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/visibility_heartbeat.rb +38 -3
- 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/time_format.rb +128 -0
- metadata +45 -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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
# Turns one queue's facts into the sentences an operator reads at the top
|
|
6
|
+
# of its page: why it is paused, who drains it, what is waiting, and which
|
|
7
|
+
# priority level it is (issue #491).
|
|
8
|
+
#
|
|
9
|
+
# Pure like JobState: it takes queue_detail, queue_pause_state and
|
|
10
|
+
# queue_drainers as given and does no I/O. Each Line carries a translation
|
|
11
|
+
# key under pgbus.queues.show.summary, raw args (Times under :ago/:time,
|
|
12
|
+
# seconds under :age) and a tone the view maps to colours.
|
|
13
|
+
module QueueSummary
|
|
14
|
+
Line = Data.define(:key, :args, :tone)
|
|
15
|
+
|
|
16
|
+
PRIORITY_LEVEL = /_p(\d+)\z/
|
|
17
|
+
FAILURES = /(\d+) consecutive failures/
|
|
18
|
+
BACKLOG_KEYS = %w[backlog backlog_parked_only empty].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def present(detail, pause_state, drainers, max_retries:)
|
|
23
|
+
name = detail[:name].to_s
|
|
24
|
+
dlq = name.end_with?(Pgbus::DEAD_LETTER_SUFFIX)
|
|
25
|
+
[
|
|
26
|
+
pause_line(pause_state),
|
|
27
|
+
drain_line(dlq, detail, drainers, max_retries),
|
|
28
|
+
(backlog_line(detail) unless dlq || drainers[:stream]),
|
|
29
|
+
priority_line(drainers)
|
|
30
|
+
].compact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pause_line(state)
|
|
34
|
+
return unless state[:paused]
|
|
35
|
+
|
|
36
|
+
if state[:resumes_at]
|
|
37
|
+
failures = state[:reason].to_s[FAILURES, 1].to_i
|
|
38
|
+
return line("paused_circuit_breaker", :yellow, ago: state[:paused_at], failures: failures,
|
|
39
|
+
time: state[:resumes_at], trip: state[:trip_count].to_i)
|
|
40
|
+
end
|
|
41
|
+
return line("paused_operator_no_reason", :yellow, ago: state[:paused_at]) if state[:reason].to_s.strip.empty?
|
|
42
|
+
|
|
43
|
+
line("paused_operator", :yellow, ago: state[:paused_at], reason: state[:reason])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# First match wins, as in JobState#ready_overlay: what the queue is
|
|
47
|
+
# explains everything below it, then whether anything is configured to
|
|
48
|
+
# drain it, and only then whether a worker is alive.
|
|
49
|
+
def drain_line(dlq, detail, drainers, max_retries)
|
|
50
|
+
return line("dlq", :gray, logical: drainers[:logical], max: max_retries) if dlq
|
|
51
|
+
return line("stream", :gray) if drainers[:stream]
|
|
52
|
+
|
|
53
|
+
if drainers[:handler]
|
|
54
|
+
consumers = drainers[:live_consumers].to_i
|
|
55
|
+
return line("handler", :gray, count: consumers) if consumers.positive?
|
|
56
|
+
|
|
57
|
+
return line("no_consumers", :red, count: detail[:queue_visible_length].to_i)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
capsules = Array(drainers[:capsules])
|
|
61
|
+
live = drainers[:live_workers].to_i
|
|
62
|
+
if capsules.empty? && !drainers[:wildcard]
|
|
63
|
+
# Capsules from `pgbus --queues` never reach the web process's
|
|
64
|
+
# configuration: a healthy heartbeat for this queue outranks it.
|
|
65
|
+
return live.zero? ? line("not_drained", :red) : line("drained_live", :gray, count: live)
|
|
66
|
+
end
|
|
67
|
+
return line("no_workers", :red, count: detail[:queue_visible_length].to_i) if live.zero?
|
|
68
|
+
return line("drained_wildcard", :gray, count: live) if capsules.empty?
|
|
69
|
+
|
|
70
|
+
line("drained_by", :gray, capsules: capsules.join(", "), count: live)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def backlog_line(detail)
|
|
74
|
+
visible = detail[:queue_visible_length].to_i
|
|
75
|
+
parked = detail[:parked_length].to_i
|
|
76
|
+
if visible.positive?
|
|
77
|
+
line("backlog", :gray, visible: visible, age: detail[:oldest_claimable_age_sec], parked: parked)
|
|
78
|
+
elsif parked.positive?
|
|
79
|
+
line("backlog_parked_only", :gray, parked: parked)
|
|
80
|
+
else
|
|
81
|
+
line("empty", :gray)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def priority_line(drainers)
|
|
86
|
+
level = drainers[:priority_level]
|
|
87
|
+
return unless level
|
|
88
|
+
|
|
89
|
+
line("priority_level", :gray, level: level, logical: drainers[:logical])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The one line a queue row has room for (the home page's queues table,
|
|
93
|
+
# issue #492): whatever needs attention (a pause, nothing draining it),
|
|
94
|
+
# else what is waiting, else what the queue is.
|
|
95
|
+
def headline(lines)
|
|
96
|
+
lines.find { |l| l.tone != :gray } || lines.find { |l| BACKLOG_KEYS.include?(l.key) } || lines.first
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def line(key, tone, **args)
|
|
100
|
+
Line.new(key: key, args: args, tone: tone)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "active_support/core_ext/time"
|
|
5
|
+
require "active_support/core_ext/date_time"
|
|
6
|
+
|
|
7
|
+
module Pgbus
|
|
8
|
+
module Web
|
|
9
|
+
# Every timestamp, age and duration the dashboard prints (issue #497).
|
|
10
|
+
#
|
|
11
|
+
# Accepts the shapes the data sources hand the views: Time (pg decoders),
|
|
12
|
+
# ActiveSupport::TimeWithZone (AR attributes), DateTime, ISO 8601 or
|
|
13
|
+
# Time#to_s Strings (PGMQ JSON, payload["scheduled_at"]), Numeric epoch
|
|
14
|
+
# seconds (ActiveJob before 7.1), and nil. Every time is shown in
|
|
15
|
+
# Time.zone. Every word comes from pgbus.helpers.time in the locale.
|
|
16
|
+
# Pure: no I/O, no view context; the view helpers wrap it in <time>.
|
|
17
|
+
module TimeFormat
|
|
18
|
+
SCOPE = "pgbus.helpers.time"
|
|
19
|
+
NONE = "—"
|
|
20
|
+
MINUTE = 60
|
|
21
|
+
HOUR = 3600
|
|
22
|
+
DAY = 86_400
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def coerce(value)
|
|
27
|
+
case value
|
|
28
|
+
when nil then nil
|
|
29
|
+
when Time, DateTime, ActiveSupport::TimeWithZone then value.in_time_zone
|
|
30
|
+
when Numeric then epoch(value)
|
|
31
|
+
when String then parse(value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# NaN and Infinity are not moments.
|
|
36
|
+
def epoch(seconds)
|
|
37
|
+
Time.at(seconds).in_time_zone
|
|
38
|
+
rescue RangeError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse(string)
|
|
43
|
+
string = string.strip
|
|
44
|
+
return nil if string.empty?
|
|
45
|
+
|
|
46
|
+
Time.zone ? Time.zone.parse(string) : Time.parse(string)
|
|
47
|
+
rescue ArgumentError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# "5m ago", "in 2h" or "now", measured from now:.
|
|
52
|
+
def relative(value, now: Time.current)
|
|
53
|
+
time = coerce(value)
|
|
54
|
+
return nil unless time
|
|
55
|
+
|
|
56
|
+
delta = time - now
|
|
57
|
+
return I18n.t("#{SCOPE}.now") if delta.abs < 1
|
|
58
|
+
|
|
59
|
+
distance = largest_unit(delta.abs.to_i)
|
|
60
|
+
I18n.t("#{SCOPE}.#{delta.negative? ? "past" : "future"}", distance: distance)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def absolute(value)
|
|
64
|
+
coerce(value)&.strftime(I18n.t("#{SCOPE}.absolute_format"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clock(value)
|
|
68
|
+
coerce(value)&.strftime(I18n.t("#{SCOPE}.clock_format"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def iso(value)
|
|
72
|
+
coerce(value)&.utc&.iso8601
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Two units at most: "45s", "2m 5s", "1h 2m", "1d 1h".
|
|
76
|
+
def duration(seconds)
|
|
77
|
+
return NONE unless seconds
|
|
78
|
+
|
|
79
|
+
# An age is never negative; one is clock skew between hosts.
|
|
80
|
+
seconds = [seconds.to_i, 0].max
|
|
81
|
+
if seconds < MINUTE then unit(:seconds, seconds)
|
|
82
|
+
elsif seconds < HOUR then pair(:minutes, seconds / MINUTE, :seconds, seconds % MINUTE)
|
|
83
|
+
elsif seconds < DAY then pair(:hours, seconds / HOUR, :minutes, (seconds % HOUR) / MINUTE)
|
|
84
|
+
else pair(:days, seconds / DAY, :hours, (seconds % DAY) / HOUR)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ms_duration(millis)
|
|
89
|
+
return NONE unless millis
|
|
90
|
+
|
|
91
|
+
millis = millis.to_i
|
|
92
|
+
if millis < 1000 then unit(:milliseconds, millis)
|
|
93
|
+
elsif millis < 60_000 then unit(:seconds, (millis / 1000.0).round(1))
|
|
94
|
+
else unit(:minutes, (millis / 60_000.0).round(1))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# "15 minutes", "6 hours", "7 days" for an Insights range in minutes.
|
|
99
|
+
def range_label(minutes)
|
|
100
|
+
minutes = [minutes.to_i, 1].max
|
|
101
|
+
|
|
102
|
+
if minutes > 1440 && (minutes % 1440).zero?
|
|
103
|
+
I18n.t("#{SCOPE}.range.days", count: minutes / 1440)
|
|
104
|
+
elsif minutes >= 60 && (minutes % 60).zero?
|
|
105
|
+
I18n.t("#{SCOPE}.range.hours", count: minutes / 60)
|
|
106
|
+
else
|
|
107
|
+
I18n.t("#{SCOPE}.range.minutes", count: minutes)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def largest_unit(seconds)
|
|
112
|
+
if seconds < MINUTE then unit(:seconds, seconds)
|
|
113
|
+
elsif seconds < HOUR then unit(:minutes, seconds / MINUTE)
|
|
114
|
+
elsif seconds < DAY then unit(:hours, seconds / HOUR)
|
|
115
|
+
else unit(:days, seconds / DAY)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def pair(big, big_count, small, small_count)
|
|
120
|
+
"#{unit(big, big_count)} #{unit(small, small_count)}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unit(name, count)
|
|
124
|
+
I18n.t("#{SCOPE}.units.#{name}", count: count)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.17.
|
|
4
|
+
version: 0.17.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -123,6 +123,8 @@ files:
|
|
|
123
123
|
- README.md
|
|
124
124
|
- Rakefile
|
|
125
125
|
- app/assets/javascripts/pgbus/stream_source_element.js
|
|
126
|
+
- app/controllers/concerns/pgbus/job_listing.rb
|
|
127
|
+
- app/controllers/concerns/pgbus/process_listing.rb
|
|
126
128
|
- app/controllers/pgbus/api/insights_controller.rb
|
|
127
129
|
- app/controllers/pgbus/api/metrics_controller.rb
|
|
128
130
|
- app/controllers/pgbus/api/stats_controller.rb
|
|
@@ -147,6 +149,11 @@ files:
|
|
|
147
149
|
- app/frontend/pgbus/vendor/apexcharts.js
|
|
148
150
|
- app/frontend/pgbus/vendor/turbo.js
|
|
149
151
|
- app/helpers/pgbus/application_helper.rb
|
|
152
|
+
- app/helpers/pgbus/button_helper.rb
|
|
153
|
+
- app/helpers/pgbus/dashboard_helper.rb
|
|
154
|
+
- app/helpers/pgbus/events_helper.rb
|
|
155
|
+
- app/helpers/pgbus/insights_helper.rb
|
|
156
|
+
- app/helpers/pgbus/processes_helper.rb
|
|
150
157
|
- app/helpers/pgbus/streams_helper.rb
|
|
151
158
|
- app/models/pgbus/application_record.rb
|
|
152
159
|
- app/models/pgbus/batch_entry.rb
|
|
@@ -174,15 +181,26 @@ files:
|
|
|
174
181
|
- app/views/pgbus/dashboard/_recent_failures.html.erb
|
|
175
182
|
- app/views/pgbus/dashboard/_stats_cards.html.erb
|
|
176
183
|
- app/views/pgbus/dashboard/show.html.erb
|
|
184
|
+
- app/views/pgbus/dead_letter/_error.html.erb
|
|
177
185
|
- app/views/pgbus/dead_letter/_messages_table.html.erb
|
|
186
|
+
- app/views/pgbus/dead_letter/_row.html.erb
|
|
178
187
|
- app/views/pgbus/dead_letter/index.html.erb
|
|
179
188
|
- app/views/pgbus/dead_letter/show.html.erb
|
|
180
|
-
- app/views/pgbus/events/
|
|
189
|
+
- app/views/pgbus/events/_list.html.erb
|
|
190
|
+
- app/views/pgbus/events/_processed_table.html.erb
|
|
191
|
+
- app/views/pgbus/events/_replay.html.erb
|
|
192
|
+
- app/views/pgbus/events/_row.html.erb
|
|
193
|
+
- app/views/pgbus/events/_tabs.html.erb
|
|
181
194
|
- app/views/pgbus/events/index.html.erb
|
|
182
195
|
- app/views/pgbus/events/show.html.erb
|
|
196
|
+
- app/views/pgbus/insights/_chart_card.html.erb
|
|
197
|
+
- app/views/pgbus/insights/_latency_table.html.erb
|
|
198
|
+
- app/views/pgbus/insights/_status_table.html.erb
|
|
199
|
+
- app/views/pgbus/insights/_throughput_table.html.erb
|
|
183
200
|
- app/views/pgbus/insights/show.html.erb
|
|
184
|
-
- app/views/pgbus/jobs/
|
|
185
|
-
- app/views/pgbus/jobs/
|
|
201
|
+
- app/views/pgbus/jobs/_list.html.erb
|
|
202
|
+
- app/views/pgbus/jobs/_row.html.erb
|
|
203
|
+
- app/views/pgbus/jobs/_tabs.html.erb
|
|
186
204
|
- app/views/pgbus/jobs/index.html.erb
|
|
187
205
|
- app/views/pgbus/jobs/show.html.erb
|
|
188
206
|
- app/views/pgbus/locks/_concurrency.html.erb
|
|
@@ -190,6 +208,8 @@ files:
|
|
|
190
208
|
- app/views/pgbus/locks/index.html.erb
|
|
191
209
|
- app/views/pgbus/outbox/index.html.erb
|
|
192
210
|
- app/views/pgbus/processes/_processes_table.html.erb
|
|
211
|
+
- app/views/pgbus/processes/_row.html.erb
|
|
212
|
+
- app/views/pgbus/processes/_status.html.erb
|
|
193
213
|
- app/views/pgbus/processes/index.html.erb
|
|
194
214
|
- app/views/pgbus/queues/_queues_list.html.erb
|
|
195
215
|
- app/views/pgbus/queues/index.html.erb
|
|
@@ -197,6 +217,8 @@ files:
|
|
|
197
217
|
- app/views/pgbus/recurring_tasks/_tasks_table.html.erb
|
|
198
218
|
- app/views/pgbus/recurring_tasks/index.html.erb
|
|
199
219
|
- app/views/pgbus/recurring_tasks/show.html.erb
|
|
220
|
+
- app/views/pgbus/shared/_empty_row.html.erb
|
|
221
|
+
- app/views/pgbus/shared/_pager.html.erb
|
|
200
222
|
- config/i18n-tasks.yml
|
|
201
223
|
- config/locales/da.yml
|
|
202
224
|
- config/locales/de.yml
|
|
@@ -285,6 +307,7 @@ files:
|
|
|
285
307
|
- lib/pgbus/configuration/capsule_dsl.rb
|
|
286
308
|
- lib/pgbus/current_attributes.rb
|
|
287
309
|
- lib/pgbus/database_tasks_guard.rb
|
|
310
|
+
- lib/pgbus/dead_letter_header.rb
|
|
288
311
|
- lib/pgbus/dedicated_connection.rb
|
|
289
312
|
- lib/pgbus/dedup_cache.rb
|
|
290
313
|
- lib/pgbus/doctor.rb
|
|
@@ -348,6 +371,7 @@ files:
|
|
|
348
371
|
- lib/pgbus/pgmq_schema/pgmq_v1.11.1.sql
|
|
349
372
|
- lib/pgbus/pgmq_schema/pgmq_v1.12.0.sql
|
|
350
373
|
- lib/pgbus/pgmq_schema/pgmq_v1.13.0.sql
|
|
374
|
+
- lib/pgbus/process/claim_buffer.rb
|
|
351
375
|
- lib/pgbus/process/consumer.rb
|
|
352
376
|
- lib/pgbus/process/consumer_priority.rb
|
|
353
377
|
- lib/pgbus/process/dispatcher.rb
|
|
@@ -376,6 +400,7 @@ files:
|
|
|
376
400
|
- lib/pgbus/recurring/scheduler.rb
|
|
377
401
|
- lib/pgbus/recurring/task.rb
|
|
378
402
|
- lib/pgbus/retry_backoff.rb
|
|
403
|
+
- lib/pgbus/ruby_jit.rb
|
|
379
404
|
- lib/pgbus/serializer.rb
|
|
380
405
|
- lib/pgbus/stat_buffer.rb
|
|
381
406
|
- lib/pgbus/streams.rb
|
|
@@ -407,11 +432,26 @@ files:
|
|
|
407
432
|
- lib/pgbus/visibility_heartbeat.rb
|
|
408
433
|
- lib/pgbus/web/authentication.rb
|
|
409
434
|
- lib/pgbus/web/data_source.rb
|
|
435
|
+
- lib/pgbus/web/data_source/dashboard_signals.rb
|
|
436
|
+
- lib/pgbus/web/data_source/dead_letter.rb
|
|
437
|
+
- lib/pgbus/web/data_source/event_list.rb
|
|
438
|
+
- lib/pgbus/web/data_source/job_list.rb
|
|
439
|
+
- lib/pgbus/web/data_source/list_counts.rb
|
|
440
|
+
- lib/pgbus/web/data_source/process_list.rb
|
|
441
|
+
- lib/pgbus/web/data_source/queue_summary.rb
|
|
442
|
+
- lib/pgbus/web/dead_letter_reason.rb
|
|
443
|
+
- lib/pgbus/web/event_state.rb
|
|
410
444
|
- lib/pgbus/web/health_app.rb
|
|
411
445
|
- lib/pgbus/web/health_server.rb
|
|
446
|
+
- lib/pgbus/web/health_verdict.rb
|
|
447
|
+
- lib/pgbus/web/insights_charts.rb
|
|
412
448
|
- lib/pgbus/web/job_context.rb
|
|
449
|
+
- lib/pgbus/web/job_state.rb
|
|
413
450
|
- lib/pgbus/web/metrics_serializer.rb
|
|
414
451
|
- lib/pgbus/web/payload_filter.rb
|
|
452
|
+
- lib/pgbus/web/process_activity.rb
|
|
453
|
+
- lib/pgbus/web/process_state.rb
|
|
454
|
+
- lib/pgbus/web/queue_summary.rb
|
|
415
455
|
- lib/pgbus/web/stream_app.rb
|
|
416
456
|
- lib/pgbus/web/streamer.rb
|
|
417
457
|
- lib/pgbus/web/streamer/connection.rb
|
|
@@ -429,6 +469,7 @@ files:
|
|
|
429
469
|
- lib/pgbus/web/streamer/registry.rb
|
|
430
470
|
- lib/pgbus/web/streamer/stream_counter.rb
|
|
431
471
|
- lib/pgbus/web/streamer/stream_event_dispatcher.rb
|
|
472
|
+
- lib/pgbus/web/time_format.rb
|
|
432
473
|
- lib/puma/plugin/pgbus_streams.rb
|
|
433
474
|
- lib/tasks/pgbus_autovacuum.rake
|
|
434
475
|
- lib/tasks/pgbus_doctor.rake
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
<turbo-frame id="pending-events">
|
|
2
|
-
<%# Bulk form outside table to avoid nested form issues %>
|
|
3
|
-
<form id="bulk-discard-events-form" action="<%= pgbus.discard_selected_events_path %>" method="post" data-turbo-frame="_top" class="hidden">
|
|
4
|
-
<%= hidden_field_tag :authenticity_token, form_authenticity_token %>
|
|
5
|
-
</form>
|
|
6
|
-
<div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="events">
|
|
7
|
-
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
8
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
9
|
-
<tr>
|
|
10
|
-
<% if @pending.any? %>
|
|
11
|
-
<th class="w-10 px-4 py-3">
|
|
12
|
-
<input type="checkbox" data-bulk-select-all
|
|
13
|
-
aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
|
|
14
|
-
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
|
15
|
-
</th>
|
|
16
|
-
<% end %>
|
|
17
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.id") %></th>
|
|
18
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.routing_key") %></th>
|
|
19
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.handler_queue") %></th>
|
|
20
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.enqueued") %></th>
|
|
21
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.reads") %></th>
|
|
22
|
-
</tr>
|
|
23
|
-
</thead>
|
|
24
|
-
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
25
|
-
<% @pending.each do |m| %>
|
|
26
|
-
<%# Preserve the raw message so corrupt payloads stay visible in the
|
|
27
|
-
edit textarea and full-JSON panel. pgbus_parse_message swallows
|
|
28
|
-
JSON::ParserError and returns {}, which would otherwise hide
|
|
29
|
-
exactly the data the operator is trying to inspect/repair.
|
|
30
|
-
Also coerce scalar JSON values (numbers, booleans, nil, arrays)
|
|
31
|
-
to {} for hash-style access below — otherwise payload["foo"]
|
|
32
|
-
raises NoMethodError. %>
|
|
33
|
-
<% payload_raw = pgbus_parse_message(m[:message]) %>
|
|
34
|
-
<% payload = payload_raw.is_a?(Hash) ? payload_raw : {} %>
|
|
35
|
-
<% parse_ok = m[:message].blank? || payload_raw != {} || m[:message].to_s.strip == "{}" %>
|
|
36
|
-
<tr>
|
|
37
|
-
<td class="w-10 px-4 py-3 align-top">
|
|
38
|
-
<input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
|
|
39
|
-
aria-label="<%= t("pgbus.helpers.bulk_select_row", id: m[:msg_id]) %>"
|
|
40
|
-
data-queue-name="<%= m[:queue_name] %>" data-msg-id="<%= m[:msg_id] %>"
|
|
41
|
-
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1">
|
|
42
|
-
<input type="hidden" name="messages[][queue_name]" value="<%= m[:queue_name] %>" form="bulk-discard-events-form" disabled>
|
|
43
|
-
<input type="hidden" name="messages[][msg_id]" value="<%= m[:msg_id] %>" form="bulk-discard-events-form" disabled>
|
|
44
|
-
</td>
|
|
45
|
-
<td colspan="5" class="p-0">
|
|
46
|
-
<details class="group">
|
|
47
|
-
<summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
|
|
48
|
-
<span class="w-16 shrink-0 px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= m[:msg_id] %></span>
|
|
49
|
-
<span class="flex-1 px-4 py-3 text-sm font-mono text-indigo-600 truncate"><%= payload["routing_key"] || payload["event_id"] || "—" %></span>
|
|
50
|
-
<span class="w-40 shrink-0 px-4 py-3 text-sm text-gray-700"><%= m[:queue_name] %></span>
|
|
51
|
-
<span class="w-28 shrink-0 px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(m[:enqueued_at]) %></span>
|
|
52
|
-
<span class="w-16 shrink-0 px-4 py-3 text-sm text-gray-500"><%= m[:read_ct] %></span>
|
|
53
|
-
</summary>
|
|
54
|
-
<div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100">
|
|
55
|
-
<div class="flex items-center justify-between mt-3 mb-3">
|
|
56
|
-
<span class="text-xs font-mono text-gray-400"><%= t("pgbus.events.pending_table.event_id") %> <%= payload["event_id"] %></span>
|
|
57
|
-
<div class="flex space-x-2">
|
|
58
|
-
<%# Mark Handled — handler_class is resolved server-side
|
|
59
|
-
from the subscriber registry, so we only send queue_name. %>
|
|
60
|
-
<%= button_to t("pgbus.events.pending_table.mark_handled"),
|
|
61
|
-
pgbus.mark_handled_event_path(m[:msg_id], queue_name: m[:queue_name]),
|
|
62
|
-
method: :post,
|
|
63
|
-
class: "rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-500",
|
|
64
|
-
data: { turbo_confirm: t("pgbus.events.pending_table.mark_handled_confirm"), turbo_frame: "_top" } %>
|
|
65
|
-
<%# Reroute — dropdown with available handlers %>
|
|
66
|
-
<details class="relative inline-block">
|
|
67
|
-
<summary class="rounded-md bg-yellow-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-yellow-500 cursor-pointer list-none">
|
|
68
|
-
<%= t("pgbus.events.pending_table.reroute") %>
|
|
69
|
-
</summary>
|
|
70
|
-
<div class="absolute right-0 z-10 mt-1 w-64 rounded-md bg-white dark:bg-gray-800 shadow-lg ring-1 ring-gray-200 dark:ring-gray-700 p-2">
|
|
71
|
-
<p class="text-xs text-gray-500 mb-2"><%= t("pgbus.events.pending_table.reroute_label") %></p>
|
|
72
|
-
<% @subscribers.each do |s| %>
|
|
73
|
-
<% next if s[:physical_queue_name] == m[:queue_name] %>
|
|
74
|
-
<%= button_to s[:handler_class],
|
|
75
|
-
pgbus.reroute_event_path(m[:msg_id], queue_name: m[:queue_name], target_queue: s[:physical_queue_name]),
|
|
76
|
-
method: :post,
|
|
77
|
-
class: "block w-full text-left rounded px-2 py-1 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
78
|
-
data: { turbo_confirm: t("pgbus.events.pending_table.reroute_confirm"), turbo_frame: "_top" } %>
|
|
79
|
-
<% end %>
|
|
80
|
-
</div>
|
|
81
|
-
</details>
|
|
82
|
-
<%# Discard %>
|
|
83
|
-
<%= button_to t("pgbus.events.pending_table.discard"),
|
|
84
|
-
pgbus.discard_event_path(m[:msg_id], queue_name: m[:queue_name]),
|
|
85
|
-
method: :post,
|
|
86
|
-
class: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-500",
|
|
87
|
-
data: { turbo_confirm: t("pgbus.events.pending_table.discard_confirm"), turbo_frame: "_top" } %>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
<%# Payload display %>
|
|
91
|
-
<div class="grid grid-cols-2 gap-4 mb-3">
|
|
92
|
-
<div>
|
|
93
|
-
<span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.arguments") %></span>
|
|
94
|
-
<pre class="text-xs text-gray-700 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-40"><%= parse_ok ? (JSON.pretty_generate(payload["payload"] || payload_raw) rescue "—") : m[:message] %></pre>
|
|
95
|
-
</div>
|
|
96
|
-
<div>
|
|
97
|
-
<span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.metadata") %></span>
|
|
98
|
-
<div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
|
|
99
|
-
<% if m[:read_ct] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.read_count") %></strong> <%= m[:read_ct] %></p><% end %>
|
|
100
|
-
<% if m[:vt] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.visible_at") %></strong> <%= m[:vt] %></p><% end %>
|
|
101
|
-
<% if m[:last_read_at] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.last_read") %></strong> <%= m[:last_read_at] %></p><% end %>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<%# Persisted Current attributes (issue #431) %>
|
|
106
|
-
<% if (event_context = pgbus_job_context(m[:message])) %>
|
|
107
|
-
<div class="mb-3" data-testid="job-context">
|
|
108
|
-
<span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.context") %></span>
|
|
109
|
-
<div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-2">
|
|
110
|
-
<% event_context.each do |klass_name, attrs| %>
|
|
111
|
-
<div>
|
|
112
|
-
<p class="font-semibold text-gray-700 dark:text-gray-300 font-mono"><%= klass_name %></p>
|
|
113
|
-
<dl class="grid grid-cols-1 gap-x-4 gap-y-0.5 sm:grid-cols-2">
|
|
114
|
-
<% attrs.each do |name, value| %>
|
|
115
|
-
<div class="flex gap-2">
|
|
116
|
-
<dt class="text-gray-500 font-mono"><%= name %></dt>
|
|
117
|
-
<dd class="text-gray-900 dark:text-white font-mono break-all"><%= value %></dd>
|
|
118
|
-
</div>
|
|
119
|
-
<% end %>
|
|
120
|
-
</dl>
|
|
121
|
-
</div>
|
|
122
|
-
<% end %>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
<% end %>
|
|
126
|
-
<%# Edit Payload form %>
|
|
127
|
-
<details class="mt-2">
|
|
128
|
-
<summary class="text-xs font-medium text-indigo-600 cursor-pointer hover:text-indigo-500"><%= t("pgbus.events.pending_table.edit_payload") %></summary>
|
|
129
|
-
<div class="mt-2 p-3 bg-white dark:bg-gray-800 rounded ring-1 ring-gray-200 dark:ring-gray-700">
|
|
130
|
-
<%= form_with url: pgbus.edit_payload_event_path(m[:msg_id]), method: :post, local: true, data: { turbo_frame: "_top" } do |f| %>
|
|
131
|
-
<%= hidden_field_tag :queue_name, m[:queue_name] %>
|
|
132
|
-
<label class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.edit_payload_label") %></label>
|
|
133
|
-
<textarea name="payload" rows="6"
|
|
134
|
-
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-white text-xs font-mono shadow-sm focus:border-indigo-500 focus:ring-indigo-500"><%= parse_ok ? (JSON.pretty_generate(payload_raw) rescue m[:message]) : m[:message] %></textarea>
|
|
135
|
-
<div class="mt-2 flex justify-end">
|
|
136
|
-
<button type="submit"
|
|
137
|
-
class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500"
|
|
138
|
-
data-turbo-confirm="<%= t("pgbus.events.pending_table.edit_payload_confirm") %>">
|
|
139
|
-
<%= t("pgbus.events.pending_table.edit_payload") %>
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
<% end %>
|
|
143
|
-
</div>
|
|
144
|
-
</details>
|
|
145
|
-
<%# Full JSON %>
|
|
146
|
-
<details class="mt-2">
|
|
147
|
-
<summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.events.pending_table.full_json_payload") %></summary>
|
|
148
|
-
<pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= parse_ok ? (JSON.pretty_generate(payload_raw) rescue m[:message]) : m[:message] %></pre>
|
|
149
|
-
</details>
|
|
150
|
-
<% if m[:headers] %>
|
|
151
|
-
<details class="mt-2">
|
|
152
|
-
<summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.events.pending_table.headers_section") %></summary>
|
|
153
|
-
<pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(m[:headers])) rescue m[:headers] %></pre>
|
|
154
|
-
</details>
|
|
155
|
-
<% end %>
|
|
156
|
-
</div>
|
|
157
|
-
</details>
|
|
158
|
-
</td>
|
|
159
|
-
</tr>
|
|
160
|
-
<% end %>
|
|
161
|
-
<% if @pending.empty? %>
|
|
162
|
-
<%# Header only renders the bulk-select column when rows are present,
|
|
163
|
-
so the empty-state colspan must match (5 without, 6 with). %>
|
|
164
|
-
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.events.index.pending_empty") %></td></tr>
|
|
165
|
-
<% end %>
|
|
166
|
-
</tbody>
|
|
167
|
-
</table>
|
|
168
|
-
</div>
|
|
169
|
-
</turbo-frame>
|