flu-rails 8.0.6 → 8.0.8
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 +16 -0
- data/README.md +6 -4
- data/lib/flu-rails/configuration.rb +2 -1
- data/lib/flu-rails/event_publisher.rb +59 -15
- data/lib/flu-rails/version.rb +1 -1
- data/lib/flu-rails.rb +22 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbe2d214138e198e66d6291a86e2614950e53f14c5556ee424cc98bd1a993981
|
|
4
|
+
data.tar.gz: c4884ce66ae50205a1a5033a3264da67dbd105d5f4e6210a6afbd9619161a1c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbd2777d9c2e08e2a46b2af5ed1baa357fa0bd16d75bf38b69352ef20318d4aa2b6ef75892a7e5438a2f31659e9fda808b0caef7897909e1c13928923e10c395
|
|
7
|
+
data.tar.gz: bb2e43abb4c22309616ca7c1bdca175922a89732cbd259978f634e9064e1f61a39bfc8b8a7007c0ba96f96b87dd573b1f5c71253ac1784921b0a97fa423d6eba
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
### [8.0.8] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
**Fixed**
|
|
11
|
+
|
|
12
|
+
* Keep the event publisher across code reloads. The railtie re-runs `init` on every reload, and `init` disconnected the publisher it replaced -- but a tracked model publishes through the very publisher it was tracked with, so every model the reload did not reload kept publishing on a connection that had just been closed, and nothing ever reopened it: `Flu::NotConnectedError` ("'connect' was never called, or 'disconnect' was") on every event from then on, until the process was restarted. The publisher now outlives the reloads, and only a change of kind (the real publisher for the dummy one, or the other way round) replaces and disconnects it.
|
|
13
|
+
* Reopen a connection Bunny has stopped recovering. Bunny reopens a connection it lost, but not one that never opened, nor one whose recovery attempts ran out: `publish` only ever reconnected after a fork, so a publisher left with such a connection raised on every event for the rest of the process. It now reopens it on the next publication, and no more than once every five seconds, since a broker that hangs rather than refuses costs a full `connect_timeout` per attempt.
|
|
14
|
+
|
|
15
|
+
**Changed**
|
|
16
|
+
|
|
17
|
+
* `connect` no longer retries an unreachable broker forever. It gives up after `max_connect_wait` seconds and raises `Flu::ConnectionLostError`. The railtie calls it from `to_prepare`, which runs on every code reload: waiting on a broker that never answers held the reload interlock, and the request behind it, for good.
|
|
18
|
+
* A broker that cannot be reached at startup no longer keeps the application from booting. `Flu.start` logs the failure, and publishing opens the connection once the broker answers again.
|
|
19
|
+
|
|
20
|
+
**Added**
|
|
21
|
+
|
|
22
|
+
* `max_connect_wait` (default `30`), how many seconds the startup retries a broker that does not answer before letting the application boot. `nil` waits for the broker for as long as it takes.
|
|
23
|
+
|
|
8
24
|
### [8.0.6] - 2026-08-19
|
|
9
25
|
|
|
10
26
|
**Fixed**
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ For now, events are generated from:
|
|
|
16
16
|
Add the gem to your project's Gemfile:
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
|
-
gem "flu-rails", "8.0.
|
|
19
|
+
gem "flu-rails", "8.0.8"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Then, create an initializer into your Rails app (`config/initializers/flu-rails.rb`)
|
|
@@ -50,7 +50,8 @@ Each configuration is detailed below.
|
|
|
50
50
|
### Start up
|
|
51
51
|
|
|
52
52
|
`flu-rails` starts automatically through its `Railtie`: there is nothing to call by hand.
|
|
53
|
-
Its startup waits
|
|
53
|
+
Its startup waits for its RabbitMQ exchange to be connected, for at most `max_connect_wait` seconds. A broker that is still not there by then does not keep the application from booting:
|
|
54
|
+
publishing reopens the connection itself once the broker answers again.
|
|
54
55
|
|
|
55
56
|
### Track changes on an ActiveRecord model
|
|
56
57
|
|
|
@@ -170,6 +171,7 @@ All options have a default value. However, all of them can be changed in your in
|
|
|
170
171
|
| `bunny_options` | `{}` | Hash of symbols | Optional | Additional options to add when connecting the RabbitMQ broker. This overrides the existing options with the same name. | `{ verify_peer: true }` |
|
|
171
172
|
| `max_pending_events` | `1000` | Integer | Optional | An event the broker refused waits for the connection to be back, and is published by the next commit of the thread or at the end of the request. This is how many a thread keeps waiting before handing the oldest to `on_publication_failure`. They are held in memory: a process that dies takes them with it. | `5000` |
|
|
172
173
|
| `on_publication_failure` | `nil` | Lambda | Optional | Called with the event and the error when an event cannot be published, instead of logging it. The transaction the event belongs to has already committed by then, so this is the last chance to keep it: store it and publish it again later. The event is `nil` when it could not even be built. | `lambda { |event, error| OutboxEvent.create!(payload: event&.to_json, error: error.message) }` |
|
|
174
|
+
| `max_connect_wait` | `30` | Integer or `nil` | Optional | How many seconds the startup retries a broker that does not answer before giving up and letting the application boot. Publishing reopens the connection itself once the broker answers again. `nil` waits for the broker for as long as it takes. | `nil` |
|
|
173
175
|
|
|
174
176
|
## How to execute tests
|
|
175
177
|
|
|
@@ -315,8 +317,8 @@ scoped RubyGems credential.
|
|
|
315
317
|
3. Tag the commit and push the tag:
|
|
316
318
|
|
|
317
319
|
```
|
|
318
|
-
$ git tag -a v8.0.
|
|
319
|
-
$ git push origin v8.0.
|
|
320
|
+
$ git tag -a v8.0.8 -m "Version 8.0.8"
|
|
321
|
+
$ git push origin v8.0.8
|
|
320
322
|
```
|
|
321
323
|
|
|
322
324
|
The workflow then checks that the tag matches `Flu::VERSION`, runs the tests, builds the gem
|
|
@@ -13,11 +13,15 @@ module Flu
|
|
|
13
13
|
CONNECTION_LOST_MESSAGE = "the connection to RabbitMQ is down. Bunny reopens it in the " \
|
|
14
14
|
"background when 'automatically_recover' is on, and publishing " \
|
|
15
15
|
"works again once it has."
|
|
16
|
+
CONNECTION_FAILED_MESSAGE = "could not reach RabbitMQ within %s seconds. Publishing reopens " \
|
|
17
|
+
"the connection itself once the broker answers again."
|
|
18
|
+
RECONNECTION_INTERVAL = 5
|
|
16
19
|
|
|
17
20
|
def initialize(configuration)
|
|
18
|
-
@logger
|
|
19
|
-
@configuration
|
|
20
|
-
@mutex
|
|
21
|
+
@logger = configuration.logger
|
|
22
|
+
@configuration = configuration
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
@next_attempt_at = 0
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def publish(event, persistent=true)
|
|
@@ -29,19 +33,20 @@ module Flu
|
|
|
29
33
|
raise ConnectionLostError, CONNECTION_LOST_MESSAGE
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
# Retries a broker that is not there yet, for at most 'max_connect_wait' seconds. Waiting on it
|
|
37
|
+
# forever would hold whatever called it -- the railtie calls it from 'to_prepare', which runs on
|
|
38
|
+
# every code reload, holding the reload interlock and the request that triggered it.
|
|
32
39
|
def connect
|
|
33
40
|
@mutex.synchronize do
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
-
end
|
|
41
|
+
next if connected?
|
|
42
|
+
give_up_at = deadline
|
|
43
|
+
begin
|
|
44
|
+
connect_to_exchange
|
|
45
|
+
rescue Bunny::TCPConnectionFailedForAllHosts
|
|
46
|
+
raise ConnectionLostError, format(CONNECTION_FAILED_MESSAGE, @configuration.max_connect_wait) if expired?(give_up_at)
|
|
47
|
+
@logger.warn("RabbitMQ connection failed, try again in 1 second.")
|
|
48
|
+
sleep 1
|
|
49
|
+
retry
|
|
45
50
|
end
|
|
46
51
|
end
|
|
47
52
|
end
|
|
@@ -66,6 +71,45 @@ module Flu
|
|
|
66
71
|
|
|
67
72
|
private
|
|
68
73
|
|
|
74
|
+
def now
|
|
75
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A nil 'max_connect_wait' waits on the broker for however long it takes.
|
|
79
|
+
def deadline
|
|
80
|
+
@configuration.max_connect_wait.nil? ? nil : now + @configuration.max_connect_wait
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def expired?(give_up_at)
|
|
84
|
+
!give_up_at.nil? && now >= give_up_at
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A connection Bunny is recovering comes back on its own. One that never opened, or that Bunny
|
|
88
|
+
# has given up on, comes back from here or not at all.
|
|
89
|
+
def abandoned?
|
|
90
|
+
return false if @connection.nil? || @connection.open?
|
|
91
|
+
!@connection.recovering_from_network_failure? &&
|
|
92
|
+
(!@connection.automatically_recover? ||
|
|
93
|
+
@connection.closed? ||
|
|
94
|
+
@connection.status == :not_connected)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A broker that hangs rather than refuses costs a full Bunny 'connect_timeout' per attempt, so
|
|
98
|
+
# publishing pays for one at most every 'RECONNECTION_INTERVAL' seconds.
|
|
99
|
+
def due_for_another_attempt?
|
|
100
|
+
return false if now < @next_attempt_at
|
|
101
|
+
@next_attempt_at = now + RECONNECTION_INTERVAL
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reopening is best effort: the caller is publishing, and an event that cannot go out is
|
|
106
|
+
# reported through the connection errors below rather than through whatever the broker refused.
|
|
107
|
+
def reconnect
|
|
108
|
+
@mutex.synchronize { connect_to_exchange unless connected? }
|
|
109
|
+
rescue StandardError => error
|
|
110
|
+
@logger.warn("Could not reopen the connection to RabbitMQ: #{error.class}: #{error.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
69
113
|
# A child inherits the parent's socket but none of the threads Bunny runs on it.
|
|
70
114
|
def forked?
|
|
71
115
|
!@pid.nil? && @pid != Process.pid
|
|
@@ -85,7 +129,7 @@ module Flu
|
|
|
85
129
|
# A connection that is down is reported as such rather than left to 'create_channel', which
|
|
86
130
|
# raises a bare 'RuntimeError' the caller has no way to tell from any other.
|
|
87
131
|
def exchange
|
|
88
|
-
|
|
132
|
+
reconnect if forked? || (abandoned? && due_for_another_attempt?)
|
|
89
133
|
cached = Thread.current[exchange_key]
|
|
90
134
|
return cached if cached && cached.channel.open?
|
|
91
135
|
raise NotConnectedError, NOT_CONNECTED_MESSAGE if @connection.nil?
|
data/lib/flu-rails/version.rb
CHANGED
data/lib/flu-rails.rb
CHANGED
|
@@ -73,10 +73,6 @@ module Flu
|
|
|
73
73
|
raise "configuration.application_name must not be nil" if @configuration.application_name.nil?
|
|
74
74
|
@logger = @configuration.logger
|
|
75
75
|
@event_factory = Flu::EventFactory.new(@configuration)
|
|
76
|
-
# The railtie re-runs 'init' on every code reload. The publisher being replaced owns an open
|
|
77
|
-
# connection, its channel and Bunny's heartbeat thread: dropping the reference to it without
|
|
78
|
-
# closing it leaks all three for the lifetime of the process.
|
|
79
|
-
@event_publisher&.disconnect
|
|
80
76
|
@event_publisher = create_event_publisher(@configuration)
|
|
81
77
|
extend_models_and_controllers
|
|
82
78
|
end
|
|
@@ -90,14 +86,24 @@ module Flu
|
|
|
90
86
|
end
|
|
91
87
|
end
|
|
92
88
|
|
|
89
|
+
# The railtie re-runs 'init' on every code reload. Building a new publisher on each of them left
|
|
90
|
+
# every reference the application had already taken on the previous one -- a tracked model
|
|
91
|
+
# publishes through the very publisher it was tracked with -- on a connection 'init' had closed
|
|
92
|
+
# and that nothing ever reopens. The publisher outlives the reloads, and only a change of kind
|
|
93
|
+
# replaces it: its connection, its channels and Bunny's heartbeat thread are then closed with it.
|
|
93
94
|
def self.create_event_publisher(configuration)
|
|
94
|
-
|
|
95
|
+
publisher_class = event_publisher_class
|
|
96
|
+
return @event_publisher if @event_publisher.instance_of?(publisher_class)
|
|
97
|
+
|
|
98
|
+
@event_publisher&.disconnect
|
|
99
|
+
if publisher_class == Flu::Dummy::InMemoryEventPublisher
|
|
95
100
|
logger.info("Loading Flu with a dummy event publisher (this will not connect any exchange)")
|
|
96
|
-
require_relative "flu-rails/dummy/in_memory_event_publisher"
|
|
97
|
-
Flu::Dummy::InMemoryEventPublisher.new(@configuration)
|
|
98
|
-
else
|
|
99
|
-
Flu::EventPublisher.new(@configuration)
|
|
100
101
|
end
|
|
102
|
+
publisher_class.new(configuration)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.event_publisher_class
|
|
106
|
+
is_testing_environment? ? Flu::Dummy::InMemoryEventPublisher : Flu::EventPublisher
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
def self.is_testing_environment?
|
|
@@ -112,8 +118,13 @@ module Flu
|
|
|
112
118
|
Flu::CoreExt.extend_controller_classes(@event_factory, @event_publisher, @logger)
|
|
113
119
|
end
|
|
114
120
|
|
|
121
|
+
# A broker that is not there yet must not keep the application from booting: publishing reopens
|
|
122
|
+
# the connection itself once the broker answers again.
|
|
115
123
|
def self.start
|
|
116
|
-
|
|
124
|
+
return unless config.auto_connect_to_exchange
|
|
125
|
+
@event_publisher.connect
|
|
126
|
+
rescue Flu::ConnectionLostError => error
|
|
127
|
+
config.logger&.error("Flu could not connect to RabbitMQ: #{error.message}")
|
|
117
128
|
end
|
|
118
129
|
|
|
119
130
|
def self.load_configuration
|
|
@@ -137,6 +148,7 @@ module Flu
|
|
|
137
148
|
config.bunny_options = {}
|
|
138
149
|
config.on_publication_failure = nil
|
|
139
150
|
config.max_pending_events = 1000
|
|
151
|
+
config.max_connect_wait = 30
|
|
140
152
|
end
|
|
141
153
|
end
|
|
142
154
|
|