pgbus 0.17.2 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +35 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/javascripts/pgbus/stream_source_element.js +34 -2
  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.rb +4 -0
  24. data/lib/pgbus/engine.rb +13 -7
  25. data/lib/pgbus/generators/migration_detector.rb +9 -1
  26. data/lib/pgbus/streams/presence.rb +317 -81
  27. data/lib/pgbus/version.rb +1 -1
  28. data/lib/pgbus/web/stream_app.rb +18 -6
  29. data/lib/pgbus/web/streamer/connection.rb +7 -1
  30. data/lib/pgbus/web/streamer/falcon_connection.rb +7 -1
  31. data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
  32. data/lib/pgbus/web/streamer/instance.rb +2 -1
  33. data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
  34. data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +8 -37
  35. metadata +13 -1
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ module Web
5
+ module Streamer
6
+ # Connection-driven presence (issues #169, #515), run on the
7
+ # StreamEventDispatcher thread, which owns AR connection release.
8
+ #
9
+ # On a seat table (Pgbus::Streams::Presence.seats_supported?) every SSE
10
+ # connection is its own row, keyed by connection id and carrying the
11
+ # client's seat; two tabs of one member are two rows. On an older
12
+ # table it falls back to one member-keyed row, as before.
13
+ #
14
+ # The heartbeat posts a PresenceTouchMessage each tick; #touch refreshes
15
+ # the live seats, re-joins any whose row was expired while the
16
+ # connection stayed open, and expires silent seats across all streams
17
+ # (rows of crashed processes included) at most every min_ttl / 2 after
18
+ # a successful sweep.
19
+ #
20
+ # Each connection remembers whether it joined as a seat or as a
21
+ # member-keyed row, so a table migrated while it is open still has
22
+ # its row removed on leave (and converted to a seat on the next touch).
23
+ #
24
+ # Every failure is logged and swallowed: a presence DB hiccup must not
25
+ # knock a live SSE connection out of the registry.
26
+ class PresenceTracker
27
+ def initialize(config:, presence_provider:, logger:, sweeper: nil, clock: nil)
28
+ @config = config
29
+ @presence_provider = presence_provider
30
+ @logger = logger
31
+ @sweeper = sweeper || method(:expire_seats)
32
+ @clock = clock || -> { ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) }
33
+ @last_sweep_at = nil
34
+ end
35
+
36
+ def join(connection, stream = connection.stream_name)
37
+ return unless @config&.stream_presence?(stream)
38
+
39
+ member = @config.presence_member_for(connection.context)
40
+ return unless member
41
+
42
+ presence = @presence_provider.call(stream)
43
+ seat = seats?(presence)
44
+ join_member(presence, connection, member, seat)
45
+ connection.presence_member = member[:id] if connection.respond_to?(:presence_member=)
46
+ connection.presence_seat = seat if connection.respond_to?(:presence_seat=)
47
+ rescue StandardError => e
48
+ log_failure("join", e)
49
+ end
50
+
51
+ def leave(connection, stream = connection.stream_name)
52
+ member_id = presence_member_of(connection)
53
+ return unless member_id
54
+
55
+ # Cleared first: a heartbeat touch already queued behind this
56
+ # disconnect must not re-join the seat it removes.
57
+ connection.presence_member = nil if connection.respond_to?(:presence_member=)
58
+ presence = @presence_provider.call(stream)
59
+ if joined_as_seat?(connection, presence)
60
+ presence.leave(connection_id: connection.id)
61
+ else
62
+ presence.leave(member_id: member_id)
63
+ end
64
+ rescue StandardError => e
65
+ log_failure("leave", e)
66
+ end
67
+
68
+ def touch(connections)
69
+ connections.select { |conn| presence_member_of(conn) }.group_by(&:stream_name).each do |stream, conns|
70
+ touch_stream(stream, conns)
71
+ rescue StandardError => e
72
+ log_failure("touch", e)
73
+ end
74
+ sweep_if_due
75
+ end
76
+
77
+ private
78
+
79
+ def join_member(presence, connection, member, seat)
80
+ metadata = member[:metadata] || {}
81
+ if seat
82
+ presence.join(member_id: member[:id], metadata: metadata,
83
+ connection_id: connection.id, seat: seat_of(connection))
84
+ else
85
+ presence.join(member_id: member[:id], metadata: metadata)
86
+ end
87
+ end
88
+
89
+ def touch_stream(stream, conns)
90
+ presence = @presence_provider.call(stream)
91
+ unless seats?(presence)
92
+ conns.each { |conn| presence.touch(member_id: presence_member_of(conn)) }
93
+ return
94
+ end
95
+
96
+ live = presence.touch(connection_id: conns.map(&:id))
97
+ conns.reject { |conn| live.include?(conn.id) }.each { |conn| rejoin_as_seat(presence, conn, stream) }
98
+ end
99
+
100
+ # A connection that joined as a member-keyed row before the table
101
+ # was migrated drops that row before it takes a seat.
102
+ def rejoin_as_seat(presence, connection, stream)
103
+ presence.leave(member_id: presence_member_of(connection)) unless joined_as_seat?(connection, presence)
104
+ join(connection, stream)
105
+ end
106
+
107
+ def sweep_if_due
108
+ return if Array(@config.streams_presence_patterns).empty?
109
+
110
+ at = now
111
+ return if @last_sweep_at && at - @last_sweep_at < @config.presence_min_ttl / 2.0
112
+
113
+ @sweeper.call
114
+ @last_sweep_at = at
115
+ rescue StandardError => e
116
+ log_failure("expiry", e)
117
+ end
118
+
119
+ def expire_seats
120
+ Pgbus::Streams::Presence.expire_seats!(
121
+ ttl_for: ->(stream) { @config.presence_ttl_for(stream) }, min_ttl: @config.presence_min_ttl
122
+ )
123
+ end
124
+
125
+ def seats?(presence)
126
+ presence.respond_to?(:seats?) && presence.seats?
127
+ end
128
+
129
+ # The mode recorded at join; a connection without the accessor
130
+ # follows the table's current schema.
131
+ def joined_as_seat?(connection, presence)
132
+ return connection.presence_seat if connection.respond_to?(:presence_seat)
133
+
134
+ seats?(presence)
135
+ end
136
+
137
+ def seat_of(connection)
138
+ connection.respond_to?(:seat) ? connection.seat : nil
139
+ end
140
+
141
+ def presence_member_of(connection)
142
+ connection.respond_to?(:presence_member) ? connection.presence_member : nil
143
+ end
144
+
145
+ def now
146
+ @clock.call
147
+ end
148
+
149
+ def log_failure(action, error)
150
+ @logger.error { "[Pgbus::Streamer::PresenceTracker] presence #{action} failed: #{error.class}: #{error.message}" }
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -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.
@@ -436,19 +437,11 @@ module Pgbus
436
437
  )
437
438
  end
438
439
 
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.
440
+ # Touches the presence rows of the given connections and expires
441
+ # silent seats. Posted by the Heartbeat each tick so idle but
442
+ # still-connected members don't get swept (see PresenceTracker).
443
443
  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
444
+ @presence.touch(msg.connections)
452
445
  end
453
446
 
454
447
  # If this stream has no remaining subscribers (registered or
@@ -640,35 +633,13 @@ module Pgbus
640
633
  connection.respond_to?(:id) && connection.id.to_s == exclude.to_s
641
634
  end
642
635
 
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.
636
+ # Connection-driven presence (issues #169, #515): see PresenceTracker.
649
637
  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}" }
638
+ @presence.join(connection, stream)
659
639
  end
660
640
 
661
641
  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
642
+ @presence.leave(connection, stream)
672
643
  end
673
644
 
674
645
  def monotonic_ms
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.17.3
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,7 @@ 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
308
319
  - lib/pgbus/current_attributes.rb
309
320
  - lib/pgbus/database_tasks_guard.rb
310
321
  - lib/pgbus/dead_letter_header.rb
@@ -466,6 +477,7 @@ files:
466
477
  - lib/pgbus/web/streamer/master_hub.rb
467
478
  - lib/pgbus/web/streamer/master_hub_boot.rb
468
479
  - lib/pgbus/web/streamer/outbound_pump.rb
480
+ - lib/pgbus/web/streamer/presence_tracker.rb
469
481
  - lib/pgbus/web/streamer/registry.rb
470
482
  - lib/pgbus/web/streamer/stream_counter.rb
471
483
  - lib/pgbus/web/streamer/stream_event_dispatcher.rb