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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +69 -14
  4. data/Rakefile +7 -1
  5. data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
  6. data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
  7. data/app/controllers/pgbus/application_controller.rb +5 -2
  8. data/app/controllers/pgbus/batches_controller.rb +4 -1
  9. data/app/controllers/pgbus/dashboard_controller.rb +20 -6
  10. data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
  11. data/app/controllers/pgbus/events_controller.rb +52 -3
  12. data/app/controllers/pgbus/insights_controller.rb +19 -1
  13. data/app/controllers/pgbus/jobs_controller.rb +45 -26
  14. data/app/controllers/pgbus/locks_controller.rb +7 -2
  15. data/app/controllers/pgbus/outbox_controller.rb +4 -1
  16. data/app/controllers/pgbus/processes_controller.rb +3 -1
  17. data/app/controllers/pgbus/queues_controller.rb +16 -4
  18. data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
  19. data/app/frontend/pgbus/modules/charts.js +119 -77
  20. data/app/frontend/pgbus/style.css +1 -1
  21. data/app/frontend/pgbus/tailwind.css +18 -0
  22. data/app/helpers/pgbus/application_helper.rb +155 -113
  23. data/app/helpers/pgbus/button_helper.rb +88 -0
  24. data/app/helpers/pgbus/dashboard_helper.rb +157 -0
  25. data/app/helpers/pgbus/events_helper.rb +40 -0
  26. data/app/helpers/pgbus/insights_helper.rb +39 -0
  27. data/app/helpers/pgbus/processes_helper.rb +79 -0
  28. data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
  29. data/app/views/pgbus/batches/show.html.erb +3 -4
  30. data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
  31. data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
  32. data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
  33. data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
  34. data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
  35. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  36. data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
  37. data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
  38. data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
  39. data/app/views/pgbus/dead_letter/index.html.erb +6 -9
  40. data/app/views/pgbus/dead_letter/show.html.erb +37 -21
  41. data/app/views/pgbus/events/_list.html.erb +55 -0
  42. data/app/views/pgbus/events/_processed_table.html.erb +45 -0
  43. data/app/views/pgbus/events/_replay.html.erb +10 -0
  44. data/app/views/pgbus/events/_row.html.erb +162 -0
  45. data/app/views/pgbus/events/_tabs.html.erb +13 -0
  46. data/app/views/pgbus/events/index.html.erb +27 -48
  47. data/app/views/pgbus/events/show.html.erb +25 -7
  48. data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
  49. data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
  50. data/app/views/pgbus/insights/_status_table.html.erb +19 -0
  51. data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
  52. data/app/views/pgbus/insights/show.html.erb +83 -100
  53. data/app/views/pgbus/jobs/_list.html.erb +64 -0
  54. data/app/views/pgbus/jobs/_row.html.erb +107 -0
  55. data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
  56. data/app/views/pgbus/jobs/index.html.erb +10 -10
  57. data/app/views/pgbus/jobs/show.html.erb +18 -20
  58. data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
  59. data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
  60. data/app/views/pgbus/outbox/index.html.erb +30 -27
  61. data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
  62. data/app/views/pgbus/processes/_row.html.erb +48 -0
  63. data/app/views/pgbus/processes/_status.html.erb +4 -0
  64. data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
  65. data/app/views/pgbus/queues/show.html.erb +69 -118
  66. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
  67. data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
  68. data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
  69. data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
  70. data/app/views/pgbus/shared/_pager.html.erb +34 -0
  71. data/config/i18n-tasks.yml +11 -2
  72. data/config/locales/da.yml +405 -113
  73. data/config/locales/de.yml +405 -113
  74. data/config/locales/en.yml +405 -113
  75. data/config/locales/es.yml +404 -112
  76. data/config/locales/fi.yml +404 -112
  77. data/config/locales/fr.yml +406 -114
  78. data/config/locales/it.yml +405 -113
  79. data/config/locales/ja.yml +405 -113
  80. data/config/locales/nb.yml +405 -113
  81. data/config/locales/nl.yml +405 -113
  82. data/config/locales/pt.yml +406 -114
  83. data/config/locales/sv.yml +404 -112
  84. data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
  85. data/lib/pgbus/active_job/executor.rb +36 -17
  86. data/lib/pgbus/cli/dlq.rb +32 -5
  87. data/lib/pgbus/client.rb +7 -2
  88. data/lib/pgbus/configuration.rb +93 -2
  89. data/lib/pgbus/dead_letter_header.rb +129 -0
  90. data/lib/pgbus/doctor.rb +37 -5
  91. data/lib/pgbus/event_bus/handler.rb +18 -11
  92. data/lib/pgbus/event_bus/registry.rb +6 -0
  93. data/lib/pgbus/failed_event_recorder.rb +51 -0
  94. data/lib/pgbus/instrumentation.rb +2 -1
  95. data/lib/pgbus/mcp/base_tool.rb +25 -0
  96. data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
  97. data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
  98. data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
  99. data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
  100. data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
  101. data/lib/pgbus/outbox/poller.rb +1 -1
  102. data/lib/pgbus/process/claim_buffer.rb +115 -0
  103. data/lib/pgbus/process/consumer.rb +94 -17
  104. data/lib/pgbus/process/dispatcher.rb +1 -1
  105. data/lib/pgbus/process/supervisor.rb +35 -12
  106. data/lib/pgbus/process/worker.rb +91 -25
  107. data/lib/pgbus/recurring/scheduler.rb +2 -1
  108. data/lib/pgbus/ruby_jit.rb +21 -0
  109. data/lib/pgbus/version.rb +1 -1
  110. data/lib/pgbus/visibility_heartbeat.rb +38 -3
  111. data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
  112. data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
  113. data/lib/pgbus/web/data_source/event_list.rb +161 -0
  114. data/lib/pgbus/web/data_source/job_list.rb +323 -0
  115. data/lib/pgbus/web/data_source/list_counts.rb +80 -0
  116. data/lib/pgbus/web/data_source/process_list.rb +69 -0
  117. data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
  118. data/lib/pgbus/web/data_source.rb +132 -254
  119. data/lib/pgbus/web/dead_letter_reason.rb +81 -0
  120. data/lib/pgbus/web/event_state.rb +71 -0
  121. data/lib/pgbus/web/health_app.rb +7 -3
  122. data/lib/pgbus/web/health_verdict.rb +78 -0
  123. data/lib/pgbus/web/insights_charts.rb +84 -0
  124. data/lib/pgbus/web/job_state.rb +111 -0
  125. data/lib/pgbus/web/process_activity.rb +97 -0
  126. data/lib/pgbus/web/process_state.rb +187 -0
  127. data/lib/pgbus/web/queue_summary.rb +104 -0
  128. data/lib/pgbus/web/time_format.rb +128 -0
  129. metadata +45 -4
  130. data/app/views/pgbus/events/_pending_table.html.erb +0 -169
  131. data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
  132. 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.0
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/_pending_table.html.erb
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/_enqueued_table.html.erb
185
- - app/views/pgbus/jobs/_failed_table.html.erb
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>