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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +35 -0
- data/Rakefile +26 -0
- data/app/assets/javascripts/pgbus/stream_source_element.js +34 -2
- data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
- data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
- data/app/controllers/pgbus/frontends_controller.rb +25 -11
- data/app/frontend/pgbus/application.min.js +2 -0
- data/app/frontend/pgbus/application.min.js.map +1 -0
- data/app/frontend/pgbus/modules/charts.js +24 -6
- data/app/frontend/pgbus/modules/charts.min.js +2 -0
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
- data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
- data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
- data/app/views/layouts/pgbus/application.html.erb +0 -1
- data/config/routes.rb +1 -1
- data/lib/generators/pgbus/add_presence_generator.rb +2 -1
- data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
- data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
- data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
- data/lib/pgbus/configuration/presence_settings.rb +103 -0
- data/lib/pgbus/configuration.rb +4 -0
- data/lib/pgbus/engine.rb +13 -7
- data/lib/pgbus/generators/migration_detector.rb +9 -1
- data/lib/pgbus/streams/presence.rb +317 -81
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/stream_app.rb +18 -6
- data/lib/pgbus/web/streamer/connection.rb +7 -1
- data/lib/pgbus/web/streamer/falcon_connection.rb +7 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
- data/lib/pgbus/web/streamer/instance.rb +2 -1
- data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +8 -37
- 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
|
|
440
|
-
#
|
|
441
|
-
# still-connected members don't get swept
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|