pgbus 0.7.2 → 0.7.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/lib/pgbus/configuration.rb +2 -1
- data/lib/pgbus/event_bus/publisher.rb +23 -0
- data/lib/pgbus/testing/assertions.rb +59 -0
- data/lib/pgbus/testing/minitest.rb +32 -0
- data/lib/pgbus/testing/rspec.rb +76 -0
- data/lib/pgbus/testing.rb +123 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/stream_app.rb +7 -0
- data/lib/pgbus.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d07ecda9d90b73fa2ab8d9a34bf775b305bba9349dd5d49c99b587ea5b614bc
|
|
4
|
+
data.tar.gz: af072bf9e30ead0928db95896380384c49d8a49a59994864693aea80dd249bda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49eccf16ff1d0dcdec75f111bda474b343f3cf154b14569d19138f1b666457d71482037e390f4faa7136e21ead14b32a8a9408d18256843992e83476424a333c
|
|
7
|
+
data.tar.gz: 0d78309574c68a90b59b276b7648ac17cd8c74f3dbedebbd3d6cabe70a062a4d879cac19da8b2ab6ac78584f671ec7eda1db37fa459ecd761b81f0cd5c81ded0
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -103,7 +103,7 @@ module Pgbus
|
|
|
103
103
|
:streams_default_retention, :streams_retention, :streams_heartbeat_interval,
|
|
104
104
|
:streams_max_connections, :streams_idle_timeout, :streams_listen_health_check_ms,
|
|
105
105
|
:streams_write_deadline_ms, :streams_falcon_streaming_body,
|
|
106
|
-
:streams_stats_enabled
|
|
106
|
+
:streams_stats_enabled, :streams_test_mode
|
|
107
107
|
|
|
108
108
|
def initialize
|
|
109
109
|
@database_url = nil
|
|
@@ -209,6 +209,7 @@ module Pgbus
|
|
|
209
209
|
# gates pgbus_job_stats recording) on purpose — operators
|
|
210
210
|
# usually want job stats on and stream stats off, or vice versa.
|
|
211
211
|
@streams_stats_enabled = false
|
|
212
|
+
@streams_test_mode = false
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
def queue_name(name)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module Pgbus
|
|
4
6
|
module EventBus
|
|
5
7
|
module Publisher
|
|
@@ -7,6 +9,27 @@ module Pgbus
|
|
|
7
9
|
|
|
8
10
|
def publish(routing_key, payload, headers: nil, delay: 0)
|
|
9
11
|
event_data = build_event_data(payload)
|
|
12
|
+
|
|
13
|
+
if defined?(Pgbus::Testing) && !Pgbus::Testing.disabled?
|
|
14
|
+
event = Pgbus::Event.new(
|
|
15
|
+
event_id: event_data["event_id"],
|
|
16
|
+
payload: event_data["payload"],
|
|
17
|
+
published_at: event_data["published_at"] ? Time.parse(event_data["published_at"]) : nil,
|
|
18
|
+
routing_key: routing_key,
|
|
19
|
+
headers: headers
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
Pgbus::Testing.store.push_event(event)
|
|
23
|
+
|
|
24
|
+
if Pgbus::Testing.inline? && delay.to_i <= 0
|
|
25
|
+
Pgbus::EventBus::Registry.instance.handlers_for(routing_key).each do |subscriber|
|
|
26
|
+
subscriber.handler_class.new.handle(event)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return event_data
|
|
31
|
+
end
|
|
32
|
+
|
|
10
33
|
Pgbus.client.publish_to_topic(routing_key, event_data, headers: headers, delay: delay)
|
|
11
34
|
end
|
|
12
35
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Testing
|
|
5
|
+
# Framework-agnostic assertion helpers. Included by both RSpec and Minitest
|
|
6
|
+
# integrations. Can also be included directly in any test class.
|
|
7
|
+
#
|
|
8
|
+
# include Pgbus::Testing::Assertions
|
|
9
|
+
#
|
|
10
|
+
# assert_pgbus_published(count: 1, routing_key: "orders.created") do
|
|
11
|
+
# Order.create!(...)
|
|
12
|
+
# end
|
|
13
|
+
module Assertions
|
|
14
|
+
def pgbus_published_events(routing_key: nil)
|
|
15
|
+
Pgbus::Testing.store.events(routing_key: routing_key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def assert_pgbus_published(count:, routing_key: nil)
|
|
19
|
+
before = pgbus_published_events(routing_key: routing_key).size
|
|
20
|
+
yield
|
|
21
|
+
after = pgbus_published_events(routing_key: routing_key).size
|
|
22
|
+
actual = after - before
|
|
23
|
+
|
|
24
|
+
return if actual == count
|
|
25
|
+
|
|
26
|
+
suffix = routing_key ? " matching #{routing_key.inspect}" : ""
|
|
27
|
+
raise_assertion("Expected #{count} event(s) published#{suffix}, got #{actual}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def assert_no_pgbus_published(routing_key: nil)
|
|
31
|
+
before = pgbus_published_events(routing_key: routing_key).size
|
|
32
|
+
yield
|
|
33
|
+
after = pgbus_published_events(routing_key: routing_key).size
|
|
34
|
+
actual = after - before
|
|
35
|
+
|
|
36
|
+
return if actual.zero?
|
|
37
|
+
|
|
38
|
+
suffix = routing_key ? " matching #{routing_key.inspect}" : ""
|
|
39
|
+
raise_assertion("Expected no events published#{suffix}, got #{actual}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Execute the block, capturing events, then dispatch all captured events
|
|
43
|
+
# to their registered handlers.
|
|
44
|
+
def perform_published_events
|
|
45
|
+
yield
|
|
46
|
+
Pgbus::Testing.store.drain!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def raise_assertion(message)
|
|
52
|
+
# Use Minitest::Assertion if available, otherwise a generic RuntimeError
|
|
53
|
+
raise Minitest::Assertion, message if defined?(Minitest::Assertion)
|
|
54
|
+
|
|
55
|
+
raise message
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../testing"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module Testing
|
|
7
|
+
# Minitest integration for Pgbus test helpers.
|
|
8
|
+
#
|
|
9
|
+
# Include in your test_helper.rb:
|
|
10
|
+
#
|
|
11
|
+
# require "pgbus/testing/minitest"
|
|
12
|
+
#
|
|
13
|
+
# class ActiveSupport::TestCase
|
|
14
|
+
# include Pgbus::Testing::MinitestHelpers
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# This provides:
|
|
18
|
+
# - Automatic fake mode + store clearing per test
|
|
19
|
+
# - assert_pgbus_published / assert_no_pgbus_published
|
|
20
|
+
# - perform_published_events
|
|
21
|
+
# - pgbus_published_events
|
|
22
|
+
module MinitestHelpers
|
|
23
|
+
include Pgbus::Testing::Assertions
|
|
24
|
+
|
|
25
|
+
def before_setup
|
|
26
|
+
Pgbus::Testing.fake!
|
|
27
|
+
Pgbus::Testing.store.clear!
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../testing"
|
|
4
|
+
|
|
5
|
+
RSpec::Matchers.define :have_published_event do |expected_routing_key|
|
|
6
|
+
supports_block_expectations
|
|
7
|
+
|
|
8
|
+
chain(:with_payload) { |payload| @expected_payload = payload }
|
|
9
|
+
chain(:with_headers) { |headers| @expected_headers = headers }
|
|
10
|
+
chain(:exactly) { |count| @expected_count = count }
|
|
11
|
+
|
|
12
|
+
match do |block|
|
|
13
|
+
@before = Pgbus::Testing.store.events(routing_key: expected_routing_key).dup
|
|
14
|
+
block.call
|
|
15
|
+
@after = Pgbus::Testing.store.events(routing_key: expected_routing_key)
|
|
16
|
+
@new_events = @after - @before
|
|
17
|
+
|
|
18
|
+
return false if @new_events.empty?
|
|
19
|
+
|
|
20
|
+
if @expected_payload
|
|
21
|
+
@new_events = @new_events.select { |e| values_match?(@expected_payload, e.payload) }
|
|
22
|
+
return false if @new_events.empty?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if @expected_headers
|
|
26
|
+
@new_events = @new_events.select { |e| values_match?(@expected_headers, e.headers) }
|
|
27
|
+
return false if @new_events.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return false if @expected_count && @new_events.size != @expected_count
|
|
31
|
+
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
match_when_negated do |block|
|
|
36
|
+
@before = Pgbus::Testing.store.events(routing_key: expected_routing_key).dup
|
|
37
|
+
block.call
|
|
38
|
+
@after = Pgbus::Testing.store.events(routing_key: expected_routing_key)
|
|
39
|
+
@new_events = @after - @before
|
|
40
|
+
|
|
41
|
+
@new_events = @new_events.select { |e| values_match?(@expected_payload, e.payload) } if @expected_payload
|
|
42
|
+
|
|
43
|
+
@new_events = @new_events.select { |e| values_match?(@expected_headers, e.headers) } if @expected_headers
|
|
44
|
+
|
|
45
|
+
if @expected_count
|
|
46
|
+
@new_events.size != @expected_count
|
|
47
|
+
else
|
|
48
|
+
@new_events.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
failure_message do
|
|
53
|
+
parts = ["expected block to publish a #{expected_routing_key.inspect} event"]
|
|
54
|
+
parts << "with payload #{@expected_payload.inspect}" if @expected_payload
|
|
55
|
+
parts << "with headers #{@expected_headers.inspect}" if @expected_headers
|
|
56
|
+
parts << "exactly #{@expected_count} time(s)" if @expected_count
|
|
57
|
+
|
|
58
|
+
published = @new_events&.size || 0
|
|
59
|
+
parts << "but #{published} matching event(s) were published"
|
|
60
|
+
|
|
61
|
+
if published.positive? && @expected_payload
|
|
62
|
+
actual_payloads = @after.map(&:payload)
|
|
63
|
+
parts << "actual payloads: #{actual_payloads.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
parts.join(", ")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
failure_message_when_negated do
|
|
70
|
+
"expected block not to publish a #{expected_routing_key.inspect} event, but #{@new_events.size} were published"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
RSpec.configure do |config|
|
|
75
|
+
config.include Pgbus::Testing::Assertions
|
|
76
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pgbus"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
# Test helpers for Pgbus EventBus. Opt-in via explicit require — never
|
|
7
|
+
# autoloaded by Zeitwerk so this code never leaks into production.
|
|
8
|
+
#
|
|
9
|
+
# require "pgbus/testing" # core only
|
|
10
|
+
# require "pgbus/testing/rspec" # RSpec matchers + auto-config
|
|
11
|
+
# require "pgbus/testing/minitest" # Minitest assertions
|
|
12
|
+
#
|
|
13
|
+
# Three modes:
|
|
14
|
+
# :fake — capture published events in an in-memory store (default for tests)
|
|
15
|
+
# :inline — capture AND immediately dispatch to matching handlers
|
|
16
|
+
# :disabled — pass through to the real publisher (production behavior)
|
|
17
|
+
module Testing
|
|
18
|
+
MODES = %i[fake inline disabled].freeze
|
|
19
|
+
MODE_KEY = :__pgbus_test_mode
|
|
20
|
+
|
|
21
|
+
# Thread-safe in-memory store for events captured in fake/inline mode.
|
|
22
|
+
class EventStore
|
|
23
|
+
def initialize
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@events = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def push_event(event)
|
|
29
|
+
@mutex.synchronize { @events << event }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def events(routing_key: nil)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
result = @events.dup
|
|
35
|
+
result = result.select { |e| e.routing_key == routing_key } if routing_key
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def size
|
|
41
|
+
@mutex.synchronize { @events.size }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear!
|
|
45
|
+
@mutex.synchronize { @events.clear }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Dispatch all stored events to their matching handlers, then clear.
|
|
49
|
+
# Events are removed one at a time after successful dispatch so that
|
|
50
|
+
# a handler exception leaves unprocessed events in the store.
|
|
51
|
+
def drain!
|
|
52
|
+
loop do
|
|
53
|
+
event = @mutex.synchronize { @events.first }
|
|
54
|
+
break unless event
|
|
55
|
+
|
|
56
|
+
Pgbus::EventBus::Registry.instance.handlers_for(event.routing_key).each do |subscriber|
|
|
57
|
+
subscriber.handler_class.new.handle(event)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@mutex.synchronize { @events.shift }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Eagerly initialize the store so concurrent access never races on creation.
|
|
66
|
+
@store = EventStore.new
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
def mode!(mode, &block)
|
|
70
|
+
raise ArgumentError, "Unknown mode: #{mode}. Valid modes: #{MODES.join(", ")}" unless MODES.include?(mode)
|
|
71
|
+
|
|
72
|
+
sync_streams_test_mode!(mode)
|
|
73
|
+
|
|
74
|
+
unless block
|
|
75
|
+
Thread.main[MODE_KEY] = mode
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
old = Thread.current[MODE_KEY]
|
|
80
|
+
Thread.current[MODE_KEY] = mode
|
|
81
|
+
yield
|
|
82
|
+
ensure
|
|
83
|
+
if block
|
|
84
|
+
Thread.current[MODE_KEY] = old
|
|
85
|
+
sync_streams_test_mode!(old || :disabled)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def mode
|
|
90
|
+
Thread.current[MODE_KEY] || Thread.main[MODE_KEY] || :disabled
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def fake!(&) = mode!(:fake, &)
|
|
94
|
+
def inline!(&) = mode!(:inline, &)
|
|
95
|
+
def disabled!(&) = mode!(:disabled, &)
|
|
96
|
+
|
|
97
|
+
def fake? = mode == :fake
|
|
98
|
+
def inline? = mode == :inline
|
|
99
|
+
def disabled? = mode == :disabled
|
|
100
|
+
|
|
101
|
+
attr_reader :store
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# When entering fake/inline mode, enable streams_test_mode to prevent
|
|
106
|
+
# rack.hijack from spawning background threads that acquire DB
|
|
107
|
+
# connections outside the test transaction (see issue #133). When
|
|
108
|
+
# returning to :disabled mode, restore the previous setting.
|
|
109
|
+
def sync_streams_test_mode!(mode)
|
|
110
|
+
return unless defined?(Pgbus.configuration)
|
|
111
|
+
|
|
112
|
+
if mode == :disabled
|
|
113
|
+
Pgbus.configuration.streams_test_mode = false
|
|
114
|
+
Pgbus::Web::Streamer.reset! if defined?(Pgbus::Web::Streamer)
|
|
115
|
+
else
|
|
116
|
+
Pgbus.configuration.streams_test_mode = true
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
require_relative "testing/assertions"
|
data/lib/pgbus/version.rb
CHANGED
data/lib/pgbus/web/stream_app.rb
CHANGED
|
@@ -62,6 +62,8 @@ module Pgbus
|
|
|
62
62
|
cursor = parse_cursor(env, request)
|
|
63
63
|
return bad_request("invalid cursor: #{cursor}") if cursor.is_a?(String)
|
|
64
64
|
|
|
65
|
+
return test_mode_stub if config.streams_test_mode
|
|
66
|
+
|
|
65
67
|
return over_capacity if streamer.registry.size >= config.streams_max_connections
|
|
66
68
|
|
|
67
69
|
if config.streams_falcon_streaming_body
|
|
@@ -210,6 +212,11 @@ module Pgbus
|
|
|
210
212
|
def server_error
|
|
211
213
|
[500, { "content-type" => "text/plain" }, ["pgbus: internal error"]]
|
|
212
214
|
end
|
|
215
|
+
|
|
216
|
+
def test_mode_stub
|
|
217
|
+
body = ": pgbus test mode — connection accepted, no polling\n\n"
|
|
218
|
+
[200, sse_headers, [body]]
|
|
219
|
+
end
|
|
213
220
|
end
|
|
214
221
|
end
|
|
215
222
|
end
|
data/lib/pgbus.rb
CHANGED
|
@@ -39,6 +39,7 @@ module Pgbus
|
|
|
39
39
|
)
|
|
40
40
|
loader.ignore("#{__dir__}/generators")
|
|
41
41
|
loader.ignore("#{__dir__}/active_job")
|
|
42
|
+
loader.ignore("#{__dir__}/pgbus/testing")
|
|
42
43
|
# lib/puma/plugin/pgbus_streams.rb is a Puma plugin — it's required
|
|
43
44
|
# explicitly by the user from config/puma.rb via `plugin :pgbus_streams`.
|
|
44
45
|
# Without this ignore, Zeitwerk scans lib/puma/ under the pgbus loader
|
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.7.
|
|
4
|
+
version: 0.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -308,6 +308,10 @@ files:
|
|
|
308
308
|
- lib/pgbus/streams/turbo_broadcastable.rb
|
|
309
309
|
- lib/pgbus/streams/turbo_stream_override.rb
|
|
310
310
|
- lib/pgbus/streams/watermark_cache_middleware.rb
|
|
311
|
+
- lib/pgbus/testing.rb
|
|
312
|
+
- lib/pgbus/testing/assertions.rb
|
|
313
|
+
- lib/pgbus/testing/minitest.rb
|
|
314
|
+
- lib/pgbus/testing/rspec.rb
|
|
311
315
|
- lib/pgbus/uniqueness.rb
|
|
312
316
|
- lib/pgbus/version.rb
|
|
313
317
|
- lib/pgbus/web/authentication.rb
|