pgbus 0.8.1 → 0.8.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/app/controllers/pgbus/api/insights_controller.rb +1 -0
- data/app/controllers/pgbus/insights_controller.rb +2 -0
- data/app/views/pgbus/insights/show.html.erb +55 -0
- data/config/locales/da.yml +9 -0
- data/config/locales/de.yml +9 -0
- data/config/locales/en.yml +9 -0
- data/config/locales/es.yml +9 -0
- data/config/locales/fi.yml +9 -0
- data/config/locales/fr.yml +9 -0
- data/config/locales/it.yml +9 -0
- data/config/locales/ja.yml +9 -0
- data/config/locales/nb.yml +9 -0
- data/config/locales/nl.yml +9 -0
- data/config/locales/pt.yml +9 -0
- data/config/locales/sv.yml +9 -0
- data/lib/pgbus/client/notify_stream.rb +5 -3
- data/lib/pgbus/engine.rb +5 -0
- data/lib/pgbus/streams/broadcastable_override.rb +135 -0
- data/lib/pgbus/streams/turbo_broadcastable.rb +7 -2
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +13 -0
- data/lib/pgbus/web/streamer/connection.rb +3 -2
- data/lib/pgbus/web/streamer/instance.rb +4 -2
- data/lib/pgbus/web/streamer/registry.rb +12 -5
- data/lib/pgbus/web/streamer/stream_counter.rb +93 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +14 -10
- data/lib/pgbus/web/streamer.rb +4 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 479168f8521b550bb2b786766ac630374cb80de0afb9e2e26afe87a9585b466c
|
|
4
|
+
data.tar.gz: a6c28faae035eebd4eb925d66b2914e50112728b648eaefc79473a5ff8c1513c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c8dc58764091199f12b11befacf8f218038e2f09226350b52c625ea953751a25d39d3c28e74088fb9df19fe0f7b87a055b16cd380a5dae7618198ab35dda430
|
|
7
|
+
data.tar.gz: 5a76fa6870bd3b207da626b4a8e5317d38470e4d2447cfbb20fa01faafe88eb23156d5a3ecb0ef309b758f41a8222ee8c80fe38ec66599f5239c951b4de82325
|
|
@@ -15,6 +15,7 @@ module Pgbus
|
|
|
15
15
|
payload[:latency_trend] = data_source.latency_trend(minutes: minutes)
|
|
16
16
|
payload[:latency_by_queue] = data_source.latency_by_queue(minutes: minutes)
|
|
17
17
|
end
|
|
18
|
+
payload[:live_streams] = data_source.live_stream_metrics
|
|
18
19
|
render json: payload
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -9,6 +9,8 @@ module Pgbus
|
|
|
9
9
|
@latency_by_queue = data_source.latency_by_queue(minutes: @minutes)
|
|
10
10
|
@latency_available = Pgbus::JobStat.latency_columns?
|
|
11
11
|
|
|
12
|
+
@live_stream_metrics = data_source.live_stream_metrics
|
|
13
|
+
|
|
12
14
|
@stream_stats_available = data_source.stream_stats_available?
|
|
13
15
|
return unless @stream_stats_available
|
|
14
16
|
|
|
@@ -159,6 +159,61 @@
|
|
|
159
159
|
</table>
|
|
160
160
|
</div>
|
|
161
161
|
|
|
162
|
+
<% if @live_stream_metrics[:totals][:streams] > 0 %>
|
|
163
|
+
<!-- Live stream counters (in-memory, always-on) -->
|
|
164
|
+
<div class="mt-8">
|
|
165
|
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4"><%= t("pgbus.insights.show.live_streams.title") %></h2>
|
|
166
|
+
|
|
167
|
+
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 mb-6">
|
|
168
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
169
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.broadcasts") %></dt>
|
|
170
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:broadcasts]) %></dd>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
173
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.active_connections") %></dt>
|
|
174
|
+
<dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@live_stream_metrics[:totals][:active_connections]) %></dd>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
177
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.total_connections") %></dt>
|
|
178
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:total_connections]) %></dd>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
181
|
+
<dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.streams") %></dt>
|
|
182
|
+
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:streams]) %></dd>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
187
|
+
<div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
188
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.insights.show.live_streams.per_stream_title") %></h3>
|
|
189
|
+
</div>
|
|
190
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
191
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
192
|
+
<tr>
|
|
193
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.stream") %></th>
|
|
194
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.broadcasts") %></th>
|
|
195
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.active_connections") %></th>
|
|
196
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.total_connections") %></th>
|
|
197
|
+
</tr>
|
|
198
|
+
</thead>
|
|
199
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
200
|
+
<% @live_stream_metrics[:streams].sort_by { |_name, data| -data[:broadcasts] }.each do |name, data| %>
|
|
201
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
202
|
+
<td data-label="Stream" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= name %></td>
|
|
203
|
+
<td data-label="Broadcasts" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:broadcasts]) %></td>
|
|
204
|
+
<td data-label="Active" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:active_connections]) %></td>
|
|
205
|
+
<td data-label="Total" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:total_connections]) %></td>
|
|
206
|
+
</tr>
|
|
207
|
+
<% end %>
|
|
208
|
+
<% if @live_stream_metrics[:streams].empty? %>
|
|
209
|
+
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.insights.show.live_streams.empty") %></td></tr>
|
|
210
|
+
<% end %>
|
|
211
|
+
</tbody>
|
|
212
|
+
</table>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<% end %>
|
|
216
|
+
|
|
162
217
|
<% if @stream_stats_available %>
|
|
163
218
|
<!-- Stream stats -->
|
|
164
219
|
<div class="mt-8">
|
data/config/locales/da.yml
CHANGED
|
@@ -222,6 +222,15 @@ da:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Kø
|
|
224
224
|
title: Forsinkelse efter kø
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Aktive forbindelser
|
|
227
|
+
broadcasts: Samlede udsendelser
|
|
228
|
+
empty: Ingen aktive streams i denne proces
|
|
229
|
+
per_stream_title: Per-stream tællere
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Live stream-tællere
|
|
233
|
+
total_connections: Samlede forbindelser
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Ingen jobstatistikker endnu
|
|
227
236
|
headers:
|
data/config/locales/de.yml
CHANGED
|
@@ -222,6 +222,15 @@ de:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Warteschlange
|
|
224
224
|
title: Latenz nach Warteschlange
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Aktive Verbindungen
|
|
227
|
+
broadcasts: Gesamt-Broadcasts
|
|
228
|
+
empty: Keine aktiven Streams in diesem Prozess
|
|
229
|
+
per_stream_title: Pro-Stream-Zähler
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Live-Stream-Zähler
|
|
233
|
+
total_connections: Gesamtverbindungen
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Noch keine Auftragsstatistiken
|
|
227
236
|
headers:
|
data/config/locales/en.yml
CHANGED
|
@@ -222,6 +222,15 @@ en:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Queue
|
|
224
224
|
title: Latency by Queue
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Active Connections
|
|
227
|
+
broadcasts: Total Broadcasts
|
|
228
|
+
empty: No streams active in this process
|
|
229
|
+
per_stream_title: Per-Stream Counters
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Live Stream Counters
|
|
233
|
+
total_connections: Total Connections
|
|
225
234
|
slowest:
|
|
226
235
|
empty: No job stats yet
|
|
227
236
|
headers:
|
data/config/locales/es.yml
CHANGED
|
@@ -222,6 +222,15 @@ es:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Cola
|
|
224
224
|
title: Latencia por Cola
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Conexiones activas
|
|
227
|
+
broadcasts: Emisiones totales
|
|
228
|
+
empty: No hay streams activos en este proceso
|
|
229
|
+
per_stream_title: Contadores por stream
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Contadores de streams en vivo
|
|
233
|
+
total_connections: Conexiones totales
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Aún no hay estadísticas de trabajos
|
|
227
236
|
headers:
|
data/config/locales/fi.yml
CHANGED
|
@@ -222,6 +222,15 @@ fi:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Jono
|
|
224
224
|
title: Viive jonon mukaan
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Aktiiviset yhteydet
|
|
227
|
+
broadcasts: Lähetykset yhteensä
|
|
228
|
+
empty: Ei aktiivisia striimejä tässä prosessissa
|
|
229
|
+
per_stream_title: Striimikohtaiset laskurit
|
|
230
|
+
stream: Striimi
|
|
231
|
+
streams: Striimit
|
|
232
|
+
title: Reaaliaikaiset striimilaskurit
|
|
233
|
+
total_connections: Yhteydet yhteensä
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Ei vielä tehtävätilastoja
|
|
227
236
|
headers:
|
data/config/locales/fr.yml
CHANGED
|
@@ -222,6 +222,15 @@ fr:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: File d'attente
|
|
224
224
|
title: Latence par file d'attente
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Connexions actives
|
|
227
|
+
broadcasts: Diffusions totales
|
|
228
|
+
empty: Aucun flux actif dans ce processus
|
|
229
|
+
per_stream_title: Compteurs par flux
|
|
230
|
+
stream: Flux
|
|
231
|
+
streams: Flux
|
|
232
|
+
title: Compteurs de flux en direct
|
|
233
|
+
total_connections: Connexions totales
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Pas encore de statistiques de tâches
|
|
227
236
|
headers:
|
data/config/locales/it.yml
CHANGED
|
@@ -222,6 +222,15 @@ it:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Coda
|
|
224
224
|
title: Latenza per coda
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Connessioni attive
|
|
227
|
+
broadcasts: Trasmissioni totali
|
|
228
|
+
empty: Nessuno stream attivo in questo processo
|
|
229
|
+
per_stream_title: Contatori per stream
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Stream
|
|
232
|
+
title: Contatori stream in tempo reale
|
|
233
|
+
total_connections: Connessioni totali
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Nessuna statistica lavori ancora
|
|
227
236
|
headers:
|
data/config/locales/ja.yml
CHANGED
|
@@ -222,6 +222,15 @@ ja:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: キュー
|
|
224
224
|
title: キュー別遅延
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: アクティブ接続
|
|
227
|
+
broadcasts: 総ブロードキャスト
|
|
228
|
+
empty: このプロセスにアクティブなストリームはありません
|
|
229
|
+
per_stream_title: ストリーム別カウンター
|
|
230
|
+
stream: ストリーム
|
|
231
|
+
streams: ストリーム
|
|
232
|
+
title: ライブストリームカウンター
|
|
233
|
+
total_connections: 総接続数
|
|
225
234
|
slowest:
|
|
226
235
|
empty: まだジョブ統計がありません
|
|
227
236
|
headers:
|
data/config/locales/nb.yml
CHANGED
|
@@ -222,6 +222,15 @@ nb:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Kø
|
|
224
224
|
title: Forsinkelse per kø
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Aktive tilkoblinger
|
|
227
|
+
broadcasts: Totale kringkastinger
|
|
228
|
+
empty: Ingen aktive strømmer i denne prosessen
|
|
229
|
+
per_stream_title: Per-strøm tellere
|
|
230
|
+
stream: Strøm
|
|
231
|
+
streams: Strømmer
|
|
232
|
+
title: Sanntids strømtellere
|
|
233
|
+
total_connections: Totale tilkoblinger
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Ingen jobbstatistikk ennå
|
|
227
236
|
headers:
|
data/config/locales/nl.yml
CHANGED
|
@@ -222,6 +222,15 @@ nl:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Wachtrij
|
|
224
224
|
title: Vertraging per wachtrij
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Actieve verbindingen
|
|
227
|
+
broadcasts: Totaal uitzendingen
|
|
228
|
+
empty: Geen actieve streams in dit proces
|
|
229
|
+
per_stream_title: Per-stream tellers
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Live stream-tellers
|
|
233
|
+
total_connections: Totale verbindingen
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Nog geen taakstatistieken
|
|
227
236
|
headers:
|
data/config/locales/pt.yml
CHANGED
|
@@ -222,6 +222,15 @@ pt:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Fila
|
|
224
224
|
title: Latência por Fila
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Conexões ativas
|
|
227
|
+
broadcasts: Transmissões totais
|
|
228
|
+
empty: Nenhum stream ativo neste processo
|
|
229
|
+
per_stream_title: Contadores por stream
|
|
230
|
+
stream: Stream
|
|
231
|
+
streams: Streams
|
|
232
|
+
title: Contadores de stream ao vivo
|
|
233
|
+
total_connections: Conexões totais
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Nenhuma estatística de tarefa ainda
|
|
227
236
|
headers:
|
data/config/locales/sv.yml
CHANGED
|
@@ -222,6 +222,15 @@ sv:
|
|
|
222
222
|
p95: P95 (ms)
|
|
223
223
|
queue: Kö
|
|
224
224
|
title: Fördröjning per kö
|
|
225
|
+
live_streams:
|
|
226
|
+
active_connections: Aktiva anslutningar
|
|
227
|
+
broadcasts: Totala sändningar
|
|
228
|
+
empty: Inga aktiva strömmar i denna process
|
|
229
|
+
per_stream_title: Per-ström mätare
|
|
230
|
+
stream: Ström
|
|
231
|
+
streams: Strömmar
|
|
232
|
+
title: Realtids strömmätare
|
|
233
|
+
total_connections: Totala anslutningar
|
|
225
234
|
slowest:
|
|
226
235
|
empty: Inga jobbstatistik än
|
|
227
236
|
headers:
|
|
@@ -25,9 +25,11 @@ module Pgbus
|
|
|
25
25
|
json = payload.is_a?(String) ? payload : JSON.generate(payload)
|
|
26
26
|
|
|
27
27
|
Instrumentation.instrument("pgbus.stream.notify", stream: stream_name, bytes: json.bytesize) do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
with_stale_connection_retry do
|
|
29
|
+
synchronized do
|
|
30
|
+
@pgmq.__send__(:with_connection) do |conn|
|
|
31
|
+
conn.exec_params("SELECT pg_notify($1, $2)", [channel, json])
|
|
32
|
+
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
end
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -136,6 +136,11 @@ module Pgbus
|
|
|
136
136
|
_autoload_trigger = Pgbus::Streams::TurboBroadcastable
|
|
137
137
|
Pgbus::Streams.install_turbo_broadcastable_patch!
|
|
138
138
|
|
|
139
|
+
if defined?(::Turbo::Broadcastable)
|
|
140
|
+
_autoload_trigger_broadcastable = Pgbus::Streams::BroadcastableOverride
|
|
141
|
+
Pgbus::Streams::BroadcastableOverride.install!(::Turbo::Broadcastable)
|
|
142
|
+
end
|
|
143
|
+
|
|
139
144
|
# Subscribe-side patch: override turbo_stream_from to render
|
|
140
145
|
# <pgbus-stream-source> (SSE) instead of <turbo-cable-stream-source>
|
|
141
146
|
# (ActionCable). Without this, third-party gems like
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Streams
|
|
5
|
+
# Runtime patch for `Turbo::Broadcastable` that adds `durable:` kwarg
|
|
6
|
+
# support to synchronous broadcast helpers. Applied at Rails engine boot
|
|
7
|
+
# time when `defined?(::Turbo::Broadcastable)` — see `Pgbus::Engine`.
|
|
8
|
+
#
|
|
9
|
+
# Instance methods extract `durable:` from kwargs and set a thread-local
|
|
10
|
+
# (`Thread.current[:pgbus_broadcast_durable]`) that
|
|
11
|
+
# `TurboBroadcastable#broadcast_stream_to` reads. The thread-local is
|
|
12
|
+
# always cleaned up after the broadcast, even on error.
|
|
13
|
+
#
|
|
14
|
+
# The `_later_to` variants are NOT overridden because turbo-rails
|
|
15
|
+
# enqueues them as background jobs — the thread-local cannot survive
|
|
16
|
+
# into the job execution context. For async broadcasts, use
|
|
17
|
+
# `streams_durable_patterns` or `streams_default_broadcast_mode` config.
|
|
18
|
+
#
|
|
19
|
+
# Class methods (`broadcasts_to`, `broadcasts_refreshes_to`) accept
|
|
20
|
+
# `durable:` and store it so the generated callbacks set the thread-local
|
|
21
|
+
# before each broadcast.
|
|
22
|
+
module BroadcastableOverride
|
|
23
|
+
BROADCAST_METHODS = %i[
|
|
24
|
+
broadcast_after_to
|
|
25
|
+
broadcast_before_to
|
|
26
|
+
broadcast_replace_to
|
|
27
|
+
broadcast_append_to
|
|
28
|
+
broadcast_prepend_to
|
|
29
|
+
broadcast_update_to
|
|
30
|
+
broadcast_remove_to
|
|
31
|
+
broadcast_refresh_to
|
|
32
|
+
broadcast_render_to
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
BROADCAST_ACTION_METHODS = %i[
|
|
36
|
+
broadcast_action_to
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
BROADCAST_METHODS.each do |method_name|
|
|
40
|
+
define_method(method_name) do |*streamables, **kwargs|
|
|
41
|
+
durable = kwargs.delete(:durable)
|
|
42
|
+
with_pgbus_durable(durable) { super(*streamables, **kwargs) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
BROADCAST_ACTION_METHODS.each do |method_name|
|
|
47
|
+
define_method(method_name) do |*streamables, action:, **kwargs|
|
|
48
|
+
durable = kwargs.delete(:durable)
|
|
49
|
+
with_pgbus_durable(durable) { super(*streamables, action: action, **kwargs) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module ClassMethods
|
|
54
|
+
def broadcasts_to(stream, durable: nil, inserts_by: :append, target: broadcast_target_default, **rendering)
|
|
55
|
+
if durable.nil?
|
|
56
|
+
after_create_commit lambda {
|
|
57
|
+
broadcast_action_later_to(
|
|
58
|
+
stream.try(:call, self) || send(stream),
|
|
59
|
+
action: inserts_by,
|
|
60
|
+
target: target.try(:call, self) || target,
|
|
61
|
+
**rendering
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
after_update_commit -> { broadcast_replace_later_to(stream.try(:call, self) || send(stream), **rendering) }
|
|
65
|
+
after_destroy_commit -> { broadcast_remove_to(stream.try(:call, self) || send(stream)) }
|
|
66
|
+
else
|
|
67
|
+
@pgbus_durable_streams ||= {}
|
|
68
|
+
@pgbus_durable_streams[stream] = durable
|
|
69
|
+
|
|
70
|
+
after_create_commit lambda {
|
|
71
|
+
broadcast_action_to(
|
|
72
|
+
stream.try(:call, self) || send(stream),
|
|
73
|
+
action: inserts_by,
|
|
74
|
+
target: target.try(:call, self) || target,
|
|
75
|
+
durable: durable,
|
|
76
|
+
**rendering
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
after_update_commit lambda {
|
|
80
|
+
broadcast_replace_to(stream.try(:call, self) || send(stream), durable: durable, **rendering)
|
|
81
|
+
}
|
|
82
|
+
after_destroy_commit lambda {
|
|
83
|
+
broadcast_remove_to(stream.try(:call, self) || send(stream), durable: durable)
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def broadcasts_refreshes_to(stream, durable: nil)
|
|
89
|
+
if durable.nil?
|
|
90
|
+
after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
|
|
91
|
+
else
|
|
92
|
+
@pgbus_durable_streams ||= {}
|
|
93
|
+
@pgbus_durable_streams[stream] = durable
|
|
94
|
+
|
|
95
|
+
after_commit lambda {
|
|
96
|
+
broadcast_refresh_to(stream.try(:call, self) || send(stream), durable: durable)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def pgbus_durable_streams
|
|
102
|
+
@pgbus_durable_streams || {}
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.install!(mod)
|
|
107
|
+
return if mod.ancestors.include?(self)
|
|
108
|
+
|
|
109
|
+
mod.prepend(self)
|
|
110
|
+
|
|
111
|
+
# Turbo::Broadcastable uses ActiveSupport::Concern, which extends
|
|
112
|
+
# each including class with Turbo::Broadcastable::ClassMethods.
|
|
113
|
+
# Prepending our ClassMethods onto that nested module ensures any
|
|
114
|
+
# class that includes Turbo::Broadcastable picks up our overrides
|
|
115
|
+
# (broadcasts_to, broadcasts_refreshes_to) automatically.
|
|
116
|
+
if defined?(::Turbo::Broadcastable::ClassMethods) &&
|
|
117
|
+
!::Turbo::Broadcastable::ClassMethods.ancestors.include?(ClassMethods)
|
|
118
|
+
::Turbo::Broadcastable::ClassMethods.prepend(ClassMethods)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def with_pgbus_durable(value)
|
|
125
|
+
return yield if value.nil?
|
|
126
|
+
|
|
127
|
+
previous = Thread.current[:pgbus_broadcast_durable]
|
|
128
|
+
Thread.current[:pgbus_broadcast_durable] = value
|
|
129
|
+
yield
|
|
130
|
+
ensure
|
|
131
|
+
Thread.current[:pgbus_broadcast_durable] = previous unless value.nil?
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -36,8 +36,13 @@ module Pgbus
|
|
|
36
36
|
module TurboBroadcastable
|
|
37
37
|
def broadcast_stream_to(*streamables, content:)
|
|
38
38
|
name = stream_name_from(streamables)
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
override = Thread.current[:pgbus_broadcast_durable]
|
|
40
|
+
durable = if override.nil?
|
|
41
|
+
Pgbus.configuration.streams_default_broadcast_mode == :durable
|
|
42
|
+
else
|
|
43
|
+
override
|
|
44
|
+
end
|
|
45
|
+
Pgbus.stream(name, durable: durable).broadcast(content)
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
data/lib/pgbus/version.rb
CHANGED
|
@@ -681,6 +681,19 @@ module Pgbus
|
|
|
681
681
|
# true AND the migration has been run. Controllers should gate
|
|
682
682
|
# rendering on `stream_stats_available?` to avoid showing empty
|
|
683
683
|
# sections.
|
|
684
|
+
def live_stream_metrics
|
|
685
|
+
counter = Pgbus::Web::Streamer.stream_counter
|
|
686
|
+
unless counter
|
|
687
|
+
empty_totals = { broadcasts: 0, active_connections: 0, total_connections: 0, streams: 0 }
|
|
688
|
+
return { streams: {}, totals: empty_totals }
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
{ streams: counter.snapshot, totals: counter.totals }
|
|
692
|
+
rescue StandardError => e
|
|
693
|
+
Pgbus.logger.debug { "[Pgbus::Web] Error fetching live stream metrics: #{e.message}" }
|
|
694
|
+
{ streams: {}, totals: { broadcasts: 0, active_connections: 0, total_connections: 0, streams: 0 } }
|
|
695
|
+
end
|
|
696
|
+
|
|
684
697
|
def stream_stats_available?
|
|
685
698
|
Pgbus.configuration.streams_stats_enabled && StreamStat.table_exists?
|
|
686
699
|
rescue StandardError => e
|
|
@@ -38,7 +38,8 @@ module Pgbus
|
|
|
38
38
|
def enqueue(envelopes)
|
|
39
39
|
written = []
|
|
40
40
|
envelopes.each do |envelope|
|
|
41
|
-
|
|
41
|
+
ephemeral = envelope.msg_id.negative?
|
|
42
|
+
next if !ephemeral && envelope.msg_id <= @last_msg_id_sent
|
|
42
43
|
|
|
43
44
|
bytes = Pgbus::Streams::Envelope.message(
|
|
44
45
|
id: envelope.msg_id,
|
|
@@ -48,7 +49,7 @@ module Pgbus
|
|
|
48
49
|
|
|
49
50
|
result = @writer.write(self, bytes, deadline_ms: @write_deadline_ms)
|
|
50
51
|
if result == :ok
|
|
51
|
-
@last_msg_id_sent = envelope.msg_id
|
|
52
|
+
@last_msg_id_sent = envelope.msg_id unless ephemeral
|
|
52
53
|
@last_write_at = monotonic
|
|
53
54
|
written << envelope
|
|
54
55
|
else
|
|
@@ -20,7 +20,7 @@ module Pgbus
|
|
|
20
20
|
# production the module-level Streamer.current(...) builds all of the
|
|
21
21
|
# defaults from the configuration.
|
|
22
22
|
class Instance
|
|
23
|
-
attr_reader :registry, :listener, :dispatcher, :heartbeat, :dispatch_queue
|
|
23
|
+
attr_reader :registry, :listener, :dispatcher, :heartbeat, :dispatch_queue, :stream_counter
|
|
24
24
|
|
|
25
25
|
def initialize(
|
|
26
26
|
client: Pgbus.client,
|
|
@@ -36,6 +36,7 @@ module Pgbus
|
|
|
36
36
|
@registry = registry || Registry.new
|
|
37
37
|
@dispatch_queue = dispatch_queue || Queue.new
|
|
38
38
|
|
|
39
|
+
@stream_counter = StreamCounter.new
|
|
39
40
|
@pg_connection = pg_connection || build_pg_connection
|
|
40
41
|
@listener = Listener.new(
|
|
41
42
|
pg_connection: @pg_connection,
|
|
@@ -49,7 +50,8 @@ module Pgbus
|
|
|
49
50
|
listener: @listener,
|
|
50
51
|
dispatch_queue: @dispatch_queue,
|
|
51
52
|
logger: @logger,
|
|
52
|
-
config: @config
|
|
53
|
+
config: @config,
|
|
54
|
+
stream_counter: @stream_counter
|
|
53
55
|
)
|
|
54
56
|
@heartbeat = Heartbeat.new(
|
|
55
57
|
registry: @registry,
|
|
@@ -37,16 +37,23 @@ module Pgbus
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Returns true if a matching connection was removed, false if it
|
|
41
|
+
# was not registered. Callers (e.g. StreamEventDispatcher) use the
|
|
42
|
+
# return value to make per-connection bookkeeping idempotent on
|
|
43
|
+
# duplicate DisconnectMessages — prune_dead can enqueue a
|
|
44
|
+
# DisconnectMessage on every wake while the first one is still
|
|
45
|
+
# waiting in the queue.
|
|
40
46
|
def unregister(connection)
|
|
41
47
|
@mutex.synchronize do
|
|
42
48
|
existing = @by_id.delete(connection.id)
|
|
43
|
-
return unless existing
|
|
49
|
+
return false unless existing
|
|
44
50
|
|
|
45
51
|
set = @by_stream[existing.stream_name]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
if set
|
|
53
|
+
set.delete(existing)
|
|
54
|
+
@by_stream.delete(existing.stream_name) if set.empty?
|
|
55
|
+
end
|
|
56
|
+
true
|
|
50
57
|
end
|
|
51
58
|
end
|
|
52
59
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Web
|
|
7
|
+
module Streamer
|
|
8
|
+
class StreamCounter
|
|
9
|
+
def initialize
|
|
10
|
+
@streams = Concurrent::Map.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def increment_broadcasts(stream_name)
|
|
14
|
+
counters_for(stream_name)[:broadcasts].increment
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def increment_connections(stream_name)
|
|
18
|
+
counters_for(stream_name)[:active_connections].increment
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def decrement_connections(stream_name)
|
|
22
|
+
counters_for(stream_name)[:active_connections].decrement
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def increment_total_connections(stream_name)
|
|
26
|
+
counters_for(stream_name)[:total_connections].increment
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def broadcasts(stream_name)
|
|
30
|
+
entry = @streams[stream_name]
|
|
31
|
+
entry ? entry[:broadcasts].value : 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def active_connections(stream_name)
|
|
35
|
+
entry = @streams[stream_name]
|
|
36
|
+
return 0 unless entry
|
|
37
|
+
|
|
38
|
+
[entry[:active_connections].value, 0].max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def total_connections(stream_name)
|
|
42
|
+
entry = @streams[stream_name]
|
|
43
|
+
entry ? entry[:total_connections].value : 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def snapshot
|
|
47
|
+
result = {}
|
|
48
|
+
@streams.each_pair do |name, counters|
|
|
49
|
+
result[name] = {
|
|
50
|
+
broadcasts: counters[:broadcasts].value,
|
|
51
|
+
active_connections: [counters[:active_connections].value, 0].max,
|
|
52
|
+
total_connections: counters[:total_connections].value
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def totals
|
|
59
|
+
total_broadcasts = 0
|
|
60
|
+
total_active = 0
|
|
61
|
+
total_conns = 0
|
|
62
|
+
stream_count = 0
|
|
63
|
+
|
|
64
|
+
@streams.each_pair do |_name, counters|
|
|
65
|
+
total_broadcasts += counters[:broadcasts].value
|
|
66
|
+
total_active += [counters[:active_connections].value, 0].max
|
|
67
|
+
total_conns += counters[:total_connections].value
|
|
68
|
+
stream_count += 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
broadcasts: total_broadcasts,
|
|
73
|
+
active_connections: total_active,
|
|
74
|
+
total_connections: total_conns,
|
|
75
|
+
streams: stream_count
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def counters_for(stream_name)
|
|
82
|
+
@streams.compute_if_absent(stream_name) do
|
|
83
|
+
{
|
|
84
|
+
broadcasts: Concurrent::AtomicFixnum.new(0),
|
|
85
|
+
active_connections: Concurrent::AtomicFixnum.new(0),
|
|
86
|
+
total_connections: Concurrent::AtomicFixnum.new(0)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -47,9 +47,11 @@ module Pgbus
|
|
|
47
47
|
|
|
48
48
|
DEFAULT_READ_LIMIT = 500
|
|
49
49
|
|
|
50
|
+
attr_reader :stream_counter
|
|
51
|
+
|
|
50
52
|
def initialize(client:, registry:, listener:, dispatch_queue:,
|
|
51
53
|
logger: Pgbus.logger, read_limit: DEFAULT_READ_LIMIT,
|
|
52
|
-
filters: nil, config: nil)
|
|
54
|
+
filters: nil, config: nil, stream_counter: nil)
|
|
53
55
|
@client = client
|
|
54
56
|
@registry = registry
|
|
55
57
|
@listener = listener
|
|
@@ -67,6 +69,7 @@ module Pgbus
|
|
|
67
69
|
# process-wide setting. Falls back to the global config
|
|
68
70
|
# for production call sites that don't specify one.
|
|
69
71
|
@config = config || Pgbus.configuration
|
|
72
|
+
@stream_counter = stream_counter || StreamCounter.new
|
|
70
73
|
# stream_name → Array<[connection, Array<Envelope>]>
|
|
71
74
|
@in_flight = Hash.new { |h, k| h[k] = [] }
|
|
72
75
|
# PGMQ full table name (pgbus_<prefix>_<name>) → logical stream
|
|
@@ -224,6 +227,7 @@ module Pgbus
|
|
|
224
227
|
end
|
|
225
228
|
|
|
226
229
|
prune_dead(registered)
|
|
230
|
+
@stream_counter.increment_broadcasts(stream)
|
|
227
231
|
|
|
228
232
|
record_stat(
|
|
229
233
|
stream_name: stream,
|
|
@@ -258,12 +262,14 @@ module Pgbus
|
|
|
258
262
|
end
|
|
259
263
|
|
|
260
264
|
prune_dead(registered)
|
|
265
|
+
@stream_counter.increment_broadcasts(stream)
|
|
261
266
|
|
|
262
267
|
record_stat(
|
|
263
268
|
stream_name: stream,
|
|
264
269
|
event_type: "broadcast",
|
|
265
270
|
started_at: started_at,
|
|
266
|
-
fanout: registered.size + in_flight_pairs.size
|
|
271
|
+
fanout: registered.size + in_flight_pairs.size,
|
|
272
|
+
ephemeral: true
|
|
267
273
|
)
|
|
268
274
|
end
|
|
269
275
|
|
|
@@ -312,18 +318,15 @@ module Pgbus
|
|
|
312
318
|
# here. Otherwise this stream's state is pinned for the
|
|
313
319
|
# life of the worker.
|
|
314
320
|
remove_in_flight(stream, connection)
|
|
321
|
+
@stream_counter.increment_total_connections(stream)
|
|
315
322
|
if connection.dead?
|
|
316
323
|
@scanned_cursor.delete(connection)
|
|
317
324
|
cleanup_stream_if_unused(stream)
|
|
318
325
|
else
|
|
326
|
+
@stream_counter.increment_connections(stream)
|
|
319
327
|
@registry.register(connection)
|
|
320
328
|
end
|
|
321
329
|
|
|
322
|
-
# Record the connect regardless of whether the connection
|
|
323
|
-
# survived the replay — a dead-before-register is still an
|
|
324
|
-
# operator-visible "connection attempt" and disconnects
|
|
325
|
-
# won't be recorded for it, so dropping it here would
|
|
326
|
-
# under-count.
|
|
327
330
|
record_stat(
|
|
328
331
|
stream_name: stream,
|
|
329
332
|
event_type: "connect",
|
|
@@ -345,8 +348,9 @@ module Pgbus
|
|
|
345
348
|
started_at = monotonic_ms
|
|
346
349
|
connection = msg.connection
|
|
347
350
|
stream = connection.stream_name
|
|
348
|
-
@registry.unregister(connection)
|
|
351
|
+
removed = @registry.unregister(connection)
|
|
349
352
|
@scanned_cursor.delete(connection)
|
|
353
|
+
@stream_counter.decrement_connections(stream) if removed
|
|
350
354
|
cleanup_stream_if_unused(stream)
|
|
351
355
|
|
|
352
356
|
record_stat(
|
|
@@ -479,8 +483,8 @@ module Pgbus
|
|
|
479
483
|
# if operators actually look at it. All failures are
|
|
480
484
|
# swallowed by StreamStat.record! itself so a stats-table
|
|
481
485
|
# outage cannot block the dispatcher.
|
|
482
|
-
def record_stat(stream_name:, event_type:, started_at:, fanout: nil)
|
|
483
|
-
return unless @config.streams_stats_enabled
|
|
486
|
+
def record_stat(stream_name:, event_type:, started_at:, fanout: nil, ephemeral: false)
|
|
487
|
+
return unless ephemeral || @config.streams_stats_enabled
|
|
484
488
|
|
|
485
489
|
Pgbus::StreamStat.record!(
|
|
486
490
|
stream_name: stream_name,
|
data/lib/pgbus/web/streamer.rb
CHANGED
|
@@ -37,8 +37,10 @@ module Pgbus
|
|
|
37
37
|
@current_mutex.synchronize { @current = instance }
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
def stream_counter
|
|
41
|
+
@current_mutex.synchronize { @current&.stream_counter }
|
|
42
|
+
end
|
|
43
|
+
|
|
42
44
|
def reset!
|
|
43
45
|
instance = nil
|
|
44
46
|
@current_mutex.synchronize do
|
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.8.
|
|
4
|
+
version: 0.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -308,6 +308,7 @@ files:
|
|
|
308
308
|
- lib/pgbus/serializer.rb
|
|
309
309
|
- lib/pgbus/stat_buffer.rb
|
|
310
310
|
- lib/pgbus/streams.rb
|
|
311
|
+
- lib/pgbus/streams/broadcastable_override.rb
|
|
311
312
|
- lib/pgbus/streams/cursor.rb
|
|
312
313
|
- lib/pgbus/streams/envelope.rb
|
|
313
314
|
- lib/pgbus/streams/filters.rb
|
|
@@ -337,6 +338,7 @@ files:
|
|
|
337
338
|
- lib/pgbus/web/streamer/io_writer.rb
|
|
338
339
|
- lib/pgbus/web/streamer/listener.rb
|
|
339
340
|
- lib/pgbus/web/streamer/registry.rb
|
|
341
|
+
- lib/pgbus/web/streamer/stream_counter.rb
|
|
340
342
|
- lib/pgbus/web/streamer/stream_event_dispatcher.rb
|
|
341
343
|
- lib/puma/plugin/pgbus_streams.rb
|
|
342
344
|
- lib/tasks/pgbus_autovacuum.rake
|