pgbus 0.17.2 → 0.18.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +55 -9
  4. data/Rakefile +26 -0
  5. data/app/assets/javascripts/pgbus/stream_source_element.js +37 -4
  6. data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
  7. data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
  8. data/app/controllers/pgbus/frontends_controller.rb +25 -11
  9. data/app/frontend/pgbus/application.min.js +2 -0
  10. data/app/frontend/pgbus/application.min.js.map +1 -0
  11. data/app/frontend/pgbus/modules/charts.js +24 -6
  12. data/app/frontend/pgbus/modules/charts.min.js +2 -0
  13. data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
  14. data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
  15. data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
  16. data/app/views/layouts/pgbus/application.html.erb +0 -1
  17. data/config/routes.rb +1 -1
  18. data/lib/generators/pgbus/add_presence_generator.rb +2 -1
  19. data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
  20. data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
  21. data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
  22. data/lib/pgbus/configuration/presence_settings.rb +103 -0
  23. data/lib/pgbus/configuration/stream_authorization.rb +36 -0
  24. data/lib/pgbus/configuration.rb +16 -1
  25. data/lib/pgbus/engine.rb +13 -7
  26. data/lib/pgbus/generators/migration_detector.rb +9 -1
  27. data/lib/pgbus/streams/envelope.rb +3 -2
  28. data/lib/pgbus/streams/presence.rb +317 -81
  29. data/lib/pgbus/version.rb +1 -1
  30. data/lib/pgbus/web/stream_app.rb +43 -14
  31. data/lib/pgbus/web/streamer/connection.rb +26 -9
  32. data/lib/pgbus/web/streamer/falcon_connection.rb +23 -9
  33. data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
  34. data/lib/pgbus/web/streamer/instance.rb +25 -8
  35. data/lib/pgbus/web/streamer/peer_close_watcher.rb +183 -0
  36. data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
  37. data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +26 -39
  38. data/lib/pgbus.rb +2 -0
  39. metadata +15 -1
@@ -105,6 +105,7 @@ module Pgbus
105
105
  # process-wide setting. Falls back to the global config
106
106
  # for production call sites that don't specify one.
107
107
  @config = config || Pgbus.configuration
108
+ @presence = PresenceTracker.new(config: @config, presence_provider: @presence_provider, logger: @logger)
108
109
  # Two write deadlines (issue #315 item 3). Fanout writes run serially
109
110
  # on this single thread, so a slow client stalls the connections
110
111
  # queued behind it — the short fanout deadline bounds that stall.
@@ -400,6 +401,16 @@ module Pgbus
400
401
  @stream_counter.increment_connections(stream)
401
402
  @registry.register(connection)
402
403
  presence_join(connection, stream)
404
+ # Step 6: announce the connection id only now (issue #521). A
405
+ # client that writes keyed by it right after pgbus:connected
406
+ # (Presence#set_state) must find the registered connection and
407
+ # its presence row. If that write finds the socket gone, run the
408
+ # disconnect now rather than leave the connection registered,
409
+ # and its seat on the roster, until the next heartbeat. A later
410
+ # DisconnectMessage for it (heartbeat, peer-close watcher) is a
411
+ # no-op: unregister reports false and the seat has already left.
412
+ connection.write_connected
413
+ handle_disconnect(DisconnectMessage.new(connection: connection)) if connection.dead?
403
414
  end
404
415
 
405
416
  record_stat(
@@ -408,10 +419,12 @@ module Pgbus
408
419
  started_at: started_at
409
420
  )
410
421
  rescue StandardError => e
411
- # Same leak path for exceptions in steps 1-4. Mark dead and
422
+ # Same leak path for exceptions in steps 1-6. Mark dead and
412
423
  # scrub state so a transient failure on a single connect
413
424
  # doesn't permanently bloat @full_to_logical or leave a
414
- # dangling LISTEN on the PG connection.
425
+ # dangling LISTEN on the PG connection. A connection already
426
+ # registered in step 5 is dead now, so the heartbeat sweep
427
+ # unregisters it (and leaves its seat) like any dead connection.
415
428
  remove_in_flight(stream, connection)
416
429
  @scanned_cursor.delete(connection)
417
430
  cleanup_stream_if_unused(stream)
@@ -428,6 +441,10 @@ module Pgbus
428
441
  @stream_counter.decrement_connections(stream) if removed
429
442
  presence_leave(connection, stream)
430
443
  cleanup_stream_if_unused(stream)
444
+ # A second DisconnectMessage for the same connection (heartbeat,
445
+ # peer-close watcher, a failed pgbus:connected write) is a no-op
446
+ # and must not count as another disconnect.
447
+ return unless removed
431
448
 
432
449
  record_stat(
433
450
  stream_name: stream,
@@ -436,19 +453,11 @@ module Pgbus
436
453
  )
437
454
  end
438
455
 
439
- # Touches (refreshes last_seen_at for) the presence members on the
440
- # given connections. Posted by the Heartbeat each tick so idle but
441
- # still-connected members don't get swept. Connections without a
442
- # presence member (non-presence streams, anonymous) are skipped.
456
+ # Touches the presence rows of the given connections and expires
457
+ # silent seats. Posted by the Heartbeat each tick so idle but
458
+ # still-connected members don't get swept (see PresenceTracker).
443
459
  def handle_presence_touch(msg)
444
- msg.connections.each do |connection|
445
- member_id = presence_member_of(connection)
446
- next unless member_id
447
-
448
- @presence_provider.call(connection.stream_name).touch(member_id: member_id)
449
- rescue StandardError => e
450
- @logger.error { "[Pgbus::Streamer::StreamEventDispatcher] presence touch failed: #{e.class}: #{e.message}" }
451
- end
460
+ @presence.touch(msg.connections)
452
461
  end
453
462
 
454
463
  # If this stream has no remaining subscribers (registered or
@@ -640,35 +649,13 @@ module Pgbus
640
649
  connection.respond_to?(:id) && connection.id.to_s == exclude.to_s
641
650
  end
642
651
 
643
- # Connection-driven presence (issue #169). Auto-joins a member when
644
- # the stream is configured for presence and the connection's
645
- # authorize-context yields a member id. Stores the member id on the
646
- # connection so handle_disconnect/handle_presence_touch can act on
647
- # it. Failures are logged and swallowed: a presence DB hiccup must
648
- # not knock a live SSE connection out of the registry.
652
+ # Connection-driven presence (issues #169, #515): see PresenceTracker.
649
653
  def presence_join(connection, stream)
650
- return unless @config&.stream_presence?(stream)
651
-
652
- member = @config.presence_member_for(connection.context)
653
- return unless member
654
-
655
- @presence_provider.call(stream).join(member_id: member[:id], metadata: member[:metadata] || {})
656
- connection.presence_member = member[:id] if connection.respond_to?(:presence_member=)
657
- rescue StandardError => e
658
- @logger.error { "[Pgbus::Streamer::StreamEventDispatcher] presence join failed: #{e.class}: #{e.message}" }
654
+ @presence.join(connection, stream)
659
655
  end
660
656
 
661
657
  def presence_leave(connection, stream)
662
- member_id = presence_member_of(connection)
663
- return unless member_id
664
-
665
- @presence_provider.call(stream).leave(member_id: member_id)
666
- rescue StandardError => e
667
- @logger.error { "[Pgbus::Streamer::StreamEventDispatcher] presence leave failed: #{e.class}: #{e.message}" }
668
- end
669
-
670
- def presence_member_of(connection)
671
- connection.respond_to?(:presence_member) ? connection.presence_member : nil
658
+ @presence.leave(connection, stream)
672
659
  end
673
660
 
674
661
  def monotonic_ms
data/lib/pgbus.rb CHANGED
@@ -289,8 +289,10 @@ module Pgbus
289
289
  # Do NOT call close — the parent's @pgmq_mutex is in undefined
290
290
  # state post-fork and attempting to acquire it can deadlock.
291
291
  # The next call to Pgbus.client will lazily create a fresh one.
292
+ # Cached streams hold the client they were built with, so they go too.
292
293
  def reset_client!
293
294
  @client = nil
295
+ @stream_cache = nil
294
296
  end
295
297
 
296
298
  def logger
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.2
4
+ version: 0.18.0
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/assets/javascripts/pgbus/stream_source_element.min.js
127
+ - app/assets/javascripts/pgbus/stream_source_element.min.js.map
126
128
  - app/controllers/concerns/pgbus/job_listing.rb
127
129
  - app/controllers/concerns/pgbus/process_listing.rb
128
130
  - app/controllers/pgbus/api/insights_controller.rb
@@ -143,11 +145,17 @@ files:
143
145
  - app/controllers/pgbus/queues_controller.rb
144
146
  - app/controllers/pgbus/recurring_tasks_controller.rb
145
147
  - app/frontend/pgbus/application.js
148
+ - app/frontend/pgbus/application.min.js
149
+ - app/frontend/pgbus/application.min.js.map
146
150
  - app/frontend/pgbus/modules/charts.js
151
+ - app/frontend/pgbus/modules/charts.min.js
152
+ - app/frontend/pgbus/modules/charts.min.js.map
147
153
  - app/frontend/pgbus/style.css
148
154
  - app/frontend/pgbus/tailwind.css
149
155
  - app/frontend/pgbus/vendor/apexcharts.js
150
156
  - app/frontend/pgbus/vendor/turbo.js
157
+ - app/frontend/pgbus/vendor/turbo.min.js
158
+ - app/frontend/pgbus/vendor/turbo.min.js.map
151
159
  - app/helpers/pgbus/application_helper.rb
152
160
  - app/helpers/pgbus/button_helper.rb
153
161
  - app/helpers/pgbus/dashboard_helper.rb
@@ -244,6 +252,7 @@ files:
244
252
  - lib/generators/pgbus/add_job_stats_queue_index_generator.rb
245
253
  - lib/generators/pgbus/add_outbox_generator.rb
246
254
  - lib/generators/pgbus/add_presence_generator.rb
255
+ - lib/generators/pgbus/add_presence_seats_generator.rb
247
256
  - lib/generators/pgbus/add_processed_event_completion_generator.rb
248
257
  - lib/generators/pgbus/add_queue_states_generator.rb
249
258
  - lib/generators/pgbus/add_recurring_generator.rb
@@ -261,6 +270,7 @@ files:
261
270
  - lib/generators/pgbus/templates/add_job_stats_queue_index.rb.erb
262
271
  - lib/generators/pgbus/templates/add_outbox.rb.erb
263
272
  - lib/generators/pgbus/templates/add_presence.rb.erb
273
+ - lib/generators/pgbus/templates/add_presence_seats.rb.erb
264
274
  - lib/generators/pgbus/templates/add_processed_event_completion.rb.erb
265
275
  - lib/generators/pgbus/templates/add_queue_states.rb.erb
266
276
  - lib/generators/pgbus/templates/add_recurring_tables.rb.erb
@@ -305,6 +315,8 @@ files:
305
315
  - lib/pgbus/concurrency/semaphore.rb
306
316
  - lib/pgbus/configuration.rb
307
317
  - lib/pgbus/configuration/capsule_dsl.rb
318
+ - lib/pgbus/configuration/presence_settings.rb
319
+ - lib/pgbus/configuration/stream_authorization.rb
308
320
  - lib/pgbus/current_attributes.rb
309
321
  - lib/pgbus/database_tasks_guard.rb
310
322
  - lib/pgbus/dead_letter_header.rb
@@ -466,6 +478,8 @@ files:
466
478
  - lib/pgbus/web/streamer/master_hub.rb
467
479
  - lib/pgbus/web/streamer/master_hub_boot.rb
468
480
  - lib/pgbus/web/streamer/outbound_pump.rb
481
+ - lib/pgbus/web/streamer/peer_close_watcher.rb
482
+ - lib/pgbus/web/streamer/presence_tracker.rb
469
483
  - lib/pgbus/web/streamer/registry.rb
470
484
  - lib/pgbus/web/streamer/stream_counter.rb
471
485
  - lib/pgbus/web/streamer/stream_event_dispatcher.rb