flu-rails 8.0.8 → 8.0.9
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 +10 -0
- data/README.md +6 -3
- data/flu-rails.gemspec +3 -0
- data/lib/flu-rails/event_publisher.rb +75 -15
- data/lib/flu-rails/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbc282f411276d4ad52ca02b624367edafd40b6396eafe57f6c4250c9c18da29
|
|
4
|
+
data.tar.gz: 28fcf8776ec3aa50318bfbed9d7779482be0f76cd3e51222c965c7ace9acb316
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6085eec4b84cb2a42eee67eebc5eb6752c21a8f1c889d59cb3ce215fa2e91efd7f13b31d816662fb96b3dc8ab30b79b20bf6cb44049ff964277793d2a71ea571
|
|
7
|
+
data.tar.gz: f6ef03cd47e961a3234e21e5d51bce950da55326e5df21d305c8080be3e31eb7356339b546b68daec73c05ef83ea72752526860d553cbe4ece04474e57c0c4a7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.9] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
* Enforce gem `json < 3`
|
|
11
|
+
|
|
12
|
+
**Fixed**
|
|
13
|
+
|
|
14
|
+
* Close the channel of a thread that has ended. Since 8.0.5, each thread publishes on its own Bunny channel, and that channel was never closed when the thread ended. RabbitMQ allows 2047 channels per connection, so a server that keeps creating and ending threads (Puma does, when `min_threads` is lower than `max_threads`) reached that limit after about a day. From then on, every new thread failed to publish with `RuntimeError: Cannot open a channel: max number of channels on connection reached`, while the older threads kept publishing normally, which made the problem look random. The channels of the threads that have ended are now closed each time a new channel is opened.
|
|
15
|
+
* Publish from a fiber on the channel of its thread. The channel was stored in `Thread.current[]`, which is per fiber, not per thread: a publication made from an `Enumerator` or a streaming response opened one more channel, and never closed it either.
|
|
16
|
+
* Publish on the channel Bunny reopens rather than on a new one. After a lost connection, Bunny reopens the connection first, then the channels it had on it. A thread that published in between opened one more channel, or hung for 15 seconds when Bunny was still in the handshake, and the one Bunny was reopening stayed open on the broker with no thread to use it. Publishing in between now raises `Flu::ConnectionLostError`, as it does while the connection is down, and goes on with the reopened channel once Bunny is done.
|
|
17
|
+
|
|
8
18
|
### [8.0.8] - 2026-08-26
|
|
9
19
|
|
|
10
20
|
**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.9"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Then, create an initializer into your Rails app (`config/initializers/flu-rails.rb`)
|
|
@@ -53,6 +53,9 @@ Each configuration is detailed below.
|
|
|
53
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
54
|
publishing reopens the connection itself once the broker answers again.
|
|
55
55
|
|
|
56
|
+
It opens one connection per process, and one channel on it per thread that publishes. A fiber publishes on the channel of its thread.
|
|
57
|
+
The channels of the threads that have ended are closed the next time a thread opens one, so a server that keeps creating and ending threads (Puma does, when `min_threads` is lower than `max_threads`) does not accumulate them.
|
|
58
|
+
|
|
56
59
|
### Track changes on an ActiveRecord model
|
|
57
60
|
|
|
58
61
|
All subclasses of `ActiveRecord::Base` that call `track_entity_changes` are "tracked". _E.g._:
|
|
@@ -317,8 +320,8 @@ scoped RubyGems credential.
|
|
|
317
320
|
3. Tag the commit and push the tag:
|
|
318
321
|
|
|
319
322
|
```
|
|
320
|
-
$ git tag -a v8.0.
|
|
321
|
-
$ git push origin v8.0.
|
|
323
|
+
$ git tag -a v8.0.9 -m "Version 8.0.9"
|
|
324
|
+
$ git push origin v8.0.9
|
|
322
325
|
```
|
|
323
326
|
|
|
324
327
|
The workflow then checks that the tag matches `Flu::VERSION`, runs the tests, builds the gem
|
data/flu-rails.gemspec
CHANGED
|
@@ -28,6 +28,9 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_dependency "activerecord", "~> 8.0"
|
|
29
29
|
spec.add_dependency "activesupport", "~> 8.0"
|
|
30
30
|
spec.add_dependency "bunny", "~> 3.1"
|
|
31
|
+
# json 3 refuses the options Faraday 2.14.3 still passes to 'JSON.parse' as a hash.
|
|
32
|
+
# Fixed on Faraday's main branch, not released yet.
|
|
33
|
+
spec.add_dependency "json", "< 3"
|
|
31
34
|
spec.add_dependency "logger", "~> 1.7"
|
|
32
35
|
spec.add_dependency "rabbitmq_http_api_client", "~> 3.2"
|
|
33
36
|
|
|
@@ -22,6 +22,8 @@ module Flu
|
|
|
22
22
|
@configuration = configuration
|
|
23
23
|
@mutex = Mutex.new
|
|
24
24
|
@next_attempt_at = 0
|
|
25
|
+
@exchanges = {}
|
|
26
|
+
@exchanges_mutex = Mutex.new
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def publish(event, persistent=true)
|
|
@@ -65,7 +67,7 @@ module Flu
|
|
|
65
67
|
@connection.close if connected?
|
|
66
68
|
@connection = nil
|
|
67
69
|
@pid = nil
|
|
68
|
-
|
|
70
|
+
forget_exchanges
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -115,26 +117,80 @@ module Flu
|
|
|
115
117
|
!@pid.nil? && @pid != Process.pid
|
|
116
118
|
end
|
|
117
119
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
#
|
|
120
|
+
# One channel per thread, kept in '@exchanges' (thread => exchange).
|
|
121
|
+
#
|
|
122
|
+
# Why not 'Thread.current[]': it is per fiber, not per thread, and a channel stored there is
|
|
123
|
+
# never closed when the thread ends. RabbitMQ allows 2047 channels per connection, so a server
|
|
124
|
+
# that keeps creating and ending threads hit that limit after a day and could not publish
|
|
125
|
+
# from any new thread. Now, each time a channel is opened, the channels of the threads that
|
|
126
|
+
# have ended are closed.
|
|
125
127
|
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
128
|
+
# Until then, '@exchanges' still references the ended threads and what
|
|
129
|
+
# their thread-local variables hold: a weak reference would lose the channel before it is closed.
|
|
130
|
+
#
|
|
131
|
+
# A thread whose channel is closed opens a new one on its next publication.
|
|
132
|
+
# That is what makes 'disconnect' and a reconnection safe.
|
|
129
133
|
# A connection that is down is reported as such rather than left to 'create_channel', which
|
|
130
134
|
# raises a bare 'RuntimeError' the caller has no way to tell from any other.
|
|
135
|
+
# The connection is checked before the cached channel: Bunny marks the channels open before it
|
|
136
|
+
# announces it is done, so a thread that read its channel closed, then the announcement, would
|
|
137
|
+
# open one more.
|
|
131
138
|
def exchange
|
|
132
139
|
reconnect if forked? || (abandoned? && due_for_another_attempt?)
|
|
133
|
-
cached = Thread.current[exchange_key]
|
|
134
|
-
return cached if cached && cached.channel.open?
|
|
135
140
|
raise NotConnectedError, NOT_CONNECTED_MESSAGE if @connection.nil?
|
|
136
141
|
raise ConnectionLostError, CONNECTION_LOST_MESSAGE unless @connection.open?
|
|
137
|
-
|
|
142
|
+
raise ConnectionLostError, CONNECTION_LOST_MESSAGE if being_reopened?
|
|
143
|
+
cached = @exchanges_mutex.synchronize { @exchanges[Thread.current] }
|
|
144
|
+
return cached if cached && cached.channel.open?
|
|
145
|
+
remember_exchange(declare_exchange)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Bunny reopens the connection first, then the channels it had on it. A channel opened in between
|
|
149
|
+
# stays open on the broker with no thread to use it, and the session does not show that window:
|
|
150
|
+
# 'open?' is true as soon as the socket is. Bunny announces each recovery attempt and its
|
|
151
|
+
# completion instead. The session is kept rather than a flag: a child process inherits the flag
|
|
152
|
+
# of a connection its parent was reopening, and opens one of its own.
|
|
153
|
+
# Bunny holds one callback of each: the ones the application gave in 'bunny_options' are called
|
|
154
|
+
# from here.
|
|
155
|
+
def hold_publishing_while_bunny_reopens(session, options)
|
|
156
|
+
started = options[:recovery_attempt_started]
|
|
157
|
+
completed = options[:recovery_completed]
|
|
158
|
+
session.before_recovery_attempt_starts { @being_reopened = session; started&.call }
|
|
159
|
+
session.after_recovery_completed { @being_reopened = nil if @being_reopened.equal?(session); completed&.call }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def being_reopened?
|
|
163
|
+
@being_reopened&.equal?(@connection)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def remember_exchange(exchange)
|
|
167
|
+
ended = @exchanges_mutex.synchronize do
|
|
168
|
+
@exchanges[Thread.current] = exchange
|
|
169
|
+
@exchanges.keys.reject(&:alive?).map { |thread| @exchanges.delete(thread) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
unless ended.empty?
|
|
173
|
+
@logger.debug { "Closing the channels of #{ended.size} threads that have ended." }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
ended.each { |orphan| close_channel(orphan.channel) }
|
|
177
|
+
exchange
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Forgets the channels without closing them.
|
|
181
|
+
# Their connection is gone: closed by 'disconnect',
|
|
182
|
+
# lost, or inherited from a parent process
|
|
183
|
+
# (closing that one would close the parent's socket).
|
|
184
|
+
def forget_exchanges
|
|
185
|
+
@exchanges_mutex.synchronize { @exchanges.clear }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Closing may fail (the broker may have closed the channel already). That is fine: the thread
|
|
189
|
+
# is gone, and the publication in progress must not fail because of it.
|
|
190
|
+
def close_channel(channel)
|
|
191
|
+
channel.close if channel.open?
|
|
192
|
+
rescue StandardError => error
|
|
193
|
+
@logger.debug { "Could not close the channel of an ended thread: #{error.class}: #{error.message}" }
|
|
138
194
|
end
|
|
139
195
|
|
|
140
196
|
def declare_exchange
|
|
@@ -153,10 +209,14 @@ module Flu
|
|
|
153
209
|
automatically_recover: true
|
|
154
210
|
}.merge(@configuration.bunny_options || {})
|
|
155
211
|
|
|
212
|
+
# Before 'start': when it fails, the channels of the previous connection must be gone already,
|
|
213
|
+
# or the forking thread would publish on the parent's channel in a child that has no broker.
|
|
214
|
+
forget_exchanges
|
|
156
215
|
@connection = Bunny.new(options)
|
|
216
|
+
hold_publishing_while_bunny_reopens(@connection, options)
|
|
157
217
|
@connection.start
|
|
158
218
|
@pid = Process.pid
|
|
159
|
-
|
|
219
|
+
remember_exchange(declare_exchange)
|
|
160
220
|
end
|
|
161
221
|
end
|
|
162
222
|
end
|
data/lib/flu-rails/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flu-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.0.
|
|
4
|
+
version: 8.0.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Loïc Vigneron
|
|
@@ -68,6 +68,20 @@ dependencies:
|
|
|
68
68
|
- - "~>"
|
|
69
69
|
- !ruby/object:Gem::Version
|
|
70
70
|
version: '3.1'
|
|
71
|
+
- !ruby/object:Gem::Dependency
|
|
72
|
+
name: json
|
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - "<"
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '3'
|
|
78
|
+
type: :runtime
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - "<"
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '3'
|
|
71
85
|
- !ruby/object:Gem::Dependency
|
|
72
86
|
name: logger
|
|
73
87
|
requirement: !ruby/object:Gem::Requirement
|