flu-rails 8.0.4 → 8.0.5
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 +46 -0
- data/README.md +12 -3
- data/flu-rails.gemspec +1 -0
- data/lib/flu-rails/action_controller_extender.rb +11 -11
- data/lib/flu-rails/active_record_extender.rb +18 -34
- data/lib/flu-rails/configuration.rb +3 -0
- data/lib/flu-rails/core_ext.rb +2 -0
- data/lib/flu-rails/dummy/in_memory_event_publisher.rb +2 -0
- data/lib/flu-rails/errors.rb +9 -0
- data/lib/flu-rails/event.rb +17 -2
- data/lib/flu-rails/event_factory.rb +8 -15
- data/lib/flu-rails/event_publisher.rb +42 -20
- data/lib/flu-rails/queue_repository.rb +7 -5
- data/lib/flu-rails/railtie.rb +2 -0
- data/lib/flu-rails/util.rb +2 -0
- data/lib/flu-rails/version.rb +3 -1
- data/lib/flu-rails.rb +5 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24d179705111c94185f7421297a692e1f962feeef2a9c2132a04e744d70a6503
|
|
4
|
+
data.tar.gz: 93526441dcfaea7cc3a5be4795c5e86cdb9a4acdbaf95b825e530259b3c89d64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd84b67b67c9f209fbab6c5309d719719e3d9f19df12e1d7816acc6690c084dad052e8590cc39b85cfcc9477b6c187d1ec352a3a8e55a932c5f51d22c062e458
|
|
7
|
+
data.tar.gz: e7e0eab197a600c06a10c1bb6ec801499cfe3d55327e94232b565830b5ee6fa69c4a5bf7c19ab3082f0492917d83e6c36581ce53600d5cbf9ef0259df1b620b1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.5] - 2026-08-03
|
|
9
|
+
|
|
10
|
+
**Fixed**
|
|
11
|
+
|
|
12
|
+
* `QueueRepository#find_all` no longer lists queues across every vhost on the broker. It called `list_queues` without a vhost, which lists every vhost's queues -- on a broker shared with other applications, that meant every other application's queues too, not just this one's.
|
|
13
|
+
* Sanitize the event name before building the routing key.
|
|
14
|
+
* [Breaking change?] Publish only Flu's own events from `flu_publish_events!`. It called `run_callbacks(:commit)`, which runs every `after_commit` callback registered on the record (including the host application's own (mailers, jobs, cache invalidation)) rather than just the one `track_entity_changes` installs. It now calls `flu_commit_changes` directly, the same way the real `after_commit` callback does.
|
|
15
|
+
* Guard the railtie require on `Rails::Railtie` rather than on `Rails`. Gems such as `rails-html-sanitizer` define an empty `Rails` namespace, which `is_testing_environment?` already accounted for a few lines below -- the railtie require itself did not, and raised `NameError: uninitialized constant Rails::Railtie` in that case.
|
|
16
|
+
* Stop assuming `ActionDispatch` is loaded when serializing an event. `Event#to_json` referenced `ActionDispatch::Http::UploadedFile` unconditionally to special-case uploaded files, on every value it serialized. A plain script that builds and serializes an event without the rest of Rails loaded raised `NameError: uninitialized constant Flu::Event::ActionDispatch` on the very first field.
|
|
17
|
+
* Track Single Table Inheritance subclasses. ActiveRecord hands the callbacks registered by `track_entity_changes` down to subclasses, but the class-level instance variables holding their settings were not inherited, so every save of an STI subclass of a tracked model raised `NoMethodError` on `nil`. `flu_is_tracked`, `flu_user_metadata_lambdas`, `flu_ignored_model_changes` and `flu_overriden_emitter_lambda` are now declared with `class_attribute`, which a subclass inherits until it sets its own value.
|
|
18
|
+
* Always clear the per-request tracking state, even when an action raises. The request id and the entity metadata are stored in thread-local storage and were cleared from an `after_action`, which does not run when the action raises. Since application servers reuse their threads, a failed request left its id behind for the next request served by that same thread, whose entity changes were then attributed to a request that had already died. `track_requests` now installs a single `around_action` that clears both from an `ensure` block.
|
|
19
|
+
|
|
20
|
+
**Added**
|
|
21
|
+
|
|
22
|
+
* `rabbitmq_vhost` configuration option (default `"/"`), used by both `EventPublisher`'s AMQP connection and `QueueRepository`'s management API calls, to operate on a vhost other than the default.
|
|
23
|
+
* `Flu::Error`, the base class of every error this gem raises on its own, and `Flu::NotConnectedError`.
|
|
24
|
+
|
|
25
|
+
**Changed**
|
|
26
|
+
|
|
27
|
+
* `EventFactory` no longer serializes an event to JSON before logging it, unless debug logging is actually enabled.
|
|
28
|
+
* Publishing through a publisher that holds no connection now raises `Flu::NotConnectedError`, naming what to call and the option that turns the automatic connection off, instead of a `NoMethodError` on `nil`.
|
|
29
|
+
* A `request` event is now built once every other `after_action` has run, so its `duration` covers them too. An action that raises still publishes nothing.
|
|
30
|
+
* `EventPublisher` opens one Bunny channel per thread instead of sharing a single one.
|
|
31
|
+
* `EventPublisher#connected?` now reports on the connection alone. The exchange is declared per thread, so it is no longer a single object whose absence says anything about the publisher as a whole.
|
|
32
|
+
|
|
33
|
+
**Performance**
|
|
34
|
+
|
|
35
|
+
* Optimize `deep_camelize` to use `each_with_object` and `ActiveSupport::Inflector#camelize` to improve performance and reduce memory consumption.
|
|
36
|
+
* Add `# frozen_string_literal: true` to all ruby files to reduce string allocations.
|
|
37
|
+
|
|
38
|
+
**Tests**
|
|
39
|
+
|
|
40
|
+
* Cover `rabbitmq_vhost` against a real broker: `EventPublisher#connect` fails rather than silently falling back to the default vhost when it points at one that does not exist, and `QueueRepository#find_all` scoped to a dedicated vhost does not see a queue declared on another one -- proving the cross-vhost leak above was real.
|
|
41
|
+
* Cover `duration`, which had no test at all: it is the elapsed wall-clock time since `request_start_time`, and a real controller request emits one that is a small, non-negative `Float`.
|
|
42
|
+
* Cover `Event#to_routing_key`: dots are stripped from the name, spaces are left untouched, a name that would push the routing key past 255 characters is truncated with a warning through `Flu.logger`, and none of this raises when `Flu.logger` was never set. Cover the same against a real broker: a name with dots is delivered under a single routing-key segment, and a 300-character name that used to make Bunny raise is published and delivered instead.
|
|
43
|
+
* Cover that `flu_publish_events!` does not run a host application's own `after_commit` callback, registered on the record's singleton class, exactly the way `run_callbacks(:commit)` used to run it too.
|
|
44
|
+
* Cover loading `flu-rails` with an empty `Rails` namespace defined, through the same kind of subprocess as the `ActionDispatch` example above.
|
|
45
|
+
* Cover that building an entity change, request or manual event does not serialize it when the logger's level is above `DEBUG`.
|
|
46
|
+
* Cover `Event#to_json` without `ActionDispatch` loaded, through a subprocess that never requires actionpack -- the main suite always has it loaded, so this is the only honest way to exercise the guard.
|
|
47
|
+
* Cover which channel a thread publishes on, against a real broker: each thread gets its own, reuses it across publications, and picks up a fresh one after the connection was closed under it.
|
|
48
|
+
* Cover `entity_metadata`, which had no test at all: its lambda is evaluated before the action runs, and its result is cleared afterwards.
|
|
49
|
+
* Cover what a raising action leaves behind in the thread-local tracking state.
|
|
50
|
+
* Add `simplecov` as a development dependency to track and report code coverage.
|
|
51
|
+
* Add support for the `RABBITMQ_VERSION` environment variable in `docker-compose.yml` to allow testing locally against different versions of RabbitMQ.
|
|
52
|
+
* Configure GitHub Actions CI to run the test suite against a matrix of RabbitMQ versions (3.12, 3.13, and 4.0).
|
|
53
|
+
|
|
8
54
|
### [8.0.4] - 2026-08-01
|
|
9
55
|
|
|
10
56
|
**Security**
|
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"
|
|
19
|
+
gem "flu-rails", "8.0.5"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Then, create an initializer into your Rails app (`config/initializers/flu-rails.rb`)
|
|
@@ -43,6 +43,7 @@ Each configuration is detailed below.
|
|
|
43
43
|
* RabbitMQ: no specific version is required by `flu-rails` itself. The broker is reached through
|
|
44
44
|
`bunny` 3.x and `rabbitmq_http_api_client` 3.x, which target the
|
|
45
45
|
[currently supported RabbitMQ release series](https://www.rabbitmq.com/release-information).
|
|
46
|
+
It is actively tested against RabbitMQ 3.12, 3.13, and 4.x.
|
|
46
47
|
|
|
47
48
|
## Usage
|
|
48
49
|
|
|
@@ -187,6 +188,14 @@ your machine, start a broker first:
|
|
|
187
188
|
$ bundle exec rspec
|
|
188
189
|
```
|
|
189
190
|
|
|
191
|
+
You can optionally specify the RabbitMQ version to use by providing the `RABBITMQ_VERSION` environment variable (defaults to `4-management-alpine`):
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
$ RABBITMQ_VERSION=3.13-management-alpine docker compose up -d rabbitmq
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Our continuous integration pipeline (via GitHub Actions) systematically runs the integration test suite against a matrix of RabbitMQ versions (3.12, 3.13, and 4.x) to ensure seamless backward and forward compatibility.
|
|
198
|
+
|
|
190
199
|
Without one, those examples are reported as skipped and the rest of the suite still runs. Set
|
|
191
200
|
`FLU_REQUIRE_RABBITMQ` to turn that skip into a failure — this is what the CI workflow does, so that
|
|
192
201
|
a build cannot report success without having reached RabbitMQ:
|
|
@@ -304,8 +313,8 @@ scoped RubyGems credential.
|
|
|
304
313
|
3. Tag the commit and push the tag:
|
|
305
314
|
|
|
306
315
|
```
|
|
307
|
-
$ git tag -a v8.0.
|
|
308
|
-
$ git push origin v8.0.
|
|
316
|
+
$ git tag -a v8.0.5 -m "Version 8.0.5"
|
|
317
|
+
$ git push origin v8.0.5
|
|
309
318
|
```
|
|
310
319
|
|
|
311
320
|
The workflow then checks that the tag matches `Flu::VERSION`, runs the tests, builds the gem
|
data/flu-rails.gemspec
CHANGED
|
@@ -35,5 +35,6 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_development_dependency "ostruct", "~> 0.6"
|
|
36
36
|
spec.add_development_dependency "rake", ">= 13.0"
|
|
37
37
|
spec.add_development_dependency "rspec", "~> 3.13"
|
|
38
|
+
spec.add_development_dependency "simplecov"
|
|
38
39
|
spec.add_development_dependency "sqlite3", "~> 2.9"
|
|
39
40
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_support/core_ext/string/inflections"
|
|
2
4
|
require "active_support/core_ext/time/zones"
|
|
3
5
|
require "random/formatter"
|
|
@@ -69,19 +71,17 @@ module Flu
|
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
prepend_around_action do |_controller, action|
|
|
73
75
|
flu_define_request_id
|
|
74
|
-
@request_start_time = Time.zone.now
|
|
75
|
-
end
|
|
76
|
-
prepend_before_action do
|
|
77
76
|
flu_define_request_entity_metadata
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
@request_start_time = Time.zone.now
|
|
78
|
+
begin
|
|
79
|
+
action.call
|
|
80
|
+
flu_track_request
|
|
81
|
+
ensure
|
|
82
|
+
flu_remove_request_entity_metadata
|
|
83
|
+
flu_remove_request_id
|
|
84
|
+
end
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_support/core_ext/object/try"
|
|
2
4
|
|
|
3
5
|
module Flu
|
|
4
6
|
class ActiveRecordExtender
|
|
5
7
|
def self.extend_models(event_factory, event_publisher)
|
|
6
8
|
ActiveRecord::Base.class_eval do
|
|
9
|
+
unless singleton_class.method_defined?(:flu_is_tracked)
|
|
10
|
+
class_attribute :flu_is_tracked, instance_accessor: false, default: false
|
|
11
|
+
class_attribute :flu_user_metadata_lambdas, instance_accessor: false, default: {}.freeze
|
|
12
|
+
class_attribute :flu_ignored_model_changes, instance_accessor: false, default: [].freeze
|
|
13
|
+
class_attribute :flu_overriden_emitter_lambda, instance_accessor: false, default: nil
|
|
14
|
+
end
|
|
15
|
+
|
|
7
16
|
define_singleton_method(:track_entity_changes) do |options = {}|
|
|
8
17
|
self.flu_is_tracked = true
|
|
9
18
|
self.flu_user_metadata_lambdas = options.fetch(:user_metadata, {})
|
|
@@ -17,38 +26,6 @@ module Flu
|
|
|
17
26
|
after_rollback { flu_rollback_changes }
|
|
18
27
|
end
|
|
19
28
|
|
|
20
|
-
def self.flu_ignored_model_changes=(ignored_model_changes)
|
|
21
|
-
@flu_ignored_model_changes = ignored_model_changes
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.flu_ignored_model_changes
|
|
25
|
-
@flu_ignored_model_changes
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.flu_overriden_emitter_lambda=(overriden_emitter_lambda)
|
|
29
|
-
@flu_overriden_emitter_lambda = overriden_emitter_lambda
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.flu_overriden_emitter_lambda
|
|
33
|
-
@flu_overriden_emitter_lambda
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.flu_user_metadata_lambdas=(user_metadata_lambdas)
|
|
37
|
-
@flu_user_metadata_lambdas = user_metadata_lambdas
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def self.flu_user_metadata_lambdas
|
|
41
|
-
@flu_user_metadata_lambdas
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.flu_is_tracked=(is_tracked)
|
|
45
|
-
@flu_is_tracked = is_tracked
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.flu_is_tracked
|
|
49
|
-
@flu_is_tracked || false
|
|
50
|
-
end
|
|
51
|
-
|
|
52
29
|
def self.flu_association_columns
|
|
53
30
|
@flu_association_columns ||= reflect_on_all_associations(:belongs_to).flat_map do |association|
|
|
54
31
|
column_names = [association.foreign_key]
|
|
@@ -61,8 +38,15 @@ module Flu
|
|
|
61
38
|
@flu_changes ||= []
|
|
62
39
|
end
|
|
63
40
|
|
|
64
|
-
def
|
|
65
|
-
|
|
41
|
+
# Defined with 'define_method' rather than 'def', for the same reason as the tracking
|
|
42
|
+
# helpers in 'ActionControllerExtender#extend_controllers': a bare 'def' opens a new scope
|
|
43
|
+
# and would not close over 'event_factory' and 'event_publisher'.
|
|
44
|
+
#
|
|
45
|
+
# Without them, publishing manually had no way to call 'flu_commit_changes' directly,
|
|
46
|
+
# and used 'run_callbacks(:commit)' instead, which runs *every* 'after_commit' callback on the record,
|
|
47
|
+
# including the hoste application's own (mailers, jobs, cache invalidation), not just Flu's.
|
|
48
|
+
define_method(:flu_publish_events!) do
|
|
49
|
+
flu_commit_changes(event_factory, event_publisher)
|
|
66
50
|
end
|
|
67
51
|
|
|
68
52
|
def flu_add_manual_event(name, data)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Flu
|
|
2
4
|
class Configuration
|
|
3
5
|
attr_accessor :development_environments,
|
|
4
6
|
:rejected_user_agents,
|
|
5
7
|
:logger,
|
|
6
8
|
:rabbitmq_host,
|
|
9
|
+
:rabbitmq_vhost,
|
|
7
10
|
:rabbitmq_management_scheme,
|
|
8
11
|
:rabbitmq_management_port,
|
|
9
12
|
:rabbitmq_port,
|
data/lib/flu-rails/core_ext.rb
CHANGED
data/lib/flu-rails/event.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "json"
|
|
2
4
|
|
|
3
5
|
module Flu
|
|
@@ -22,8 +24,11 @@ module Flu
|
|
|
22
24
|
@data = data || {}
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
ROUTING_KEY_MAX_LENGTH = 255
|
|
28
|
+
|
|
25
29
|
def to_routing_key
|
|
26
|
-
"#{@meta[:status]}.#{@meta[:emitter]}.#{@meta[:kind]}
|
|
30
|
+
prefix = "#{@meta[:status]}.#{@meta[:emitter]}.#{@meta[:kind]}."
|
|
31
|
+
"#{prefix}#{routing_key_name(prefix.length)}"
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def to_json(options=nil)
|
|
@@ -67,12 +72,22 @@ module Flu
|
|
|
67
72
|
|
|
68
73
|
private
|
|
69
74
|
|
|
75
|
+
def routing_key_name(prefix_length)
|
|
76
|
+
name = @meta[:name].to_s.delete(".")
|
|
77
|
+
budget = [ROUTING_KEY_MAX_LENGTH - prefix_length, 0].max
|
|
78
|
+
return name if name.length <= budget
|
|
79
|
+
|
|
80
|
+
Flu.logger&.warn("flu-rails: event name '#{@meta[:name]}' was truncated to #{budget} " \
|
|
81
|
+
"characters to keep its routing key within Bunny's #{ROUTING_KEY_MAX_LENGTH}-character limit.")
|
|
82
|
+
name[0, budget]
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
def map_complex_object(object)
|
|
71
86
|
if object.is_a?(Array)
|
|
72
87
|
map_array(object)
|
|
73
88
|
elsif object.is_a?(Hash)
|
|
74
89
|
map_hash(object)
|
|
75
|
-
elsif object.is_a?(ActionDispatch::Http::UploadedFile)
|
|
90
|
+
elsif defined?(ActionDispatch::Http::UploadedFile) && object.is_a?(ActionDispatch::Http::UploadedFile)
|
|
76
91
|
map_file(object)
|
|
77
92
|
else
|
|
78
93
|
object
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "securerandom"
|
|
2
4
|
require "active_support/core_ext/object/blank"
|
|
3
5
|
require "active_support/core_ext/string/inflections"
|
|
@@ -20,7 +22,7 @@ module Flu
|
|
|
20
22
|
raise ArgumentError, "data must have an controller_name" if !data.has_key?(:controller_name) || data[:controller_name].empty?
|
|
21
23
|
name = "request to #{data[:action_name]} #{data[:controller_name]}"
|
|
22
24
|
event = build_event(name, :request, data)
|
|
23
|
-
@logger.debug
|
|
25
|
+
@logger.debug { "Track action: #{event.to_json}" }
|
|
24
26
|
event
|
|
25
27
|
end
|
|
26
28
|
|
|
@@ -31,7 +33,7 @@ module Flu
|
|
|
31
33
|
raise ArgumentError, "data must have an entity_name" if !data.has_key?(:entity_name) || data[:entity_name].empty?
|
|
32
34
|
name = "#{data[:action_name]} #{data[:entity_name]}"
|
|
33
35
|
event = build_event(name, :entity_change, data)
|
|
34
|
-
@logger.debug
|
|
36
|
+
@logger.debug { "Track change: #{event.to_json}" }
|
|
35
37
|
event
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -40,7 +42,7 @@ module Flu
|
|
|
40
42
|
raise ArgumentError, "data must be a hash" if !data.is_a?(Hash)
|
|
41
43
|
raise ArgumentError, "name must not be nil or empty" if name.nil? || name.empty?
|
|
42
44
|
event = build_event(name.to_s, :manual, data)
|
|
43
|
-
@logger.debug
|
|
45
|
+
@logger.debug { "Track manual: #{event.to_json}" }
|
|
44
46
|
event
|
|
45
47
|
end
|
|
46
48
|
|
|
@@ -91,25 +93,16 @@ module Flu
|
|
|
91
93
|
def deep_camelize(value)
|
|
92
94
|
case value
|
|
93
95
|
when Array
|
|
94
|
-
value.map { |v| deep_camelize
|
|
96
|
+
value.map { |v| deep_camelize(v) }
|
|
95
97
|
when Hash
|
|
96
|
-
value.
|
|
97
|
-
camelized_hash[
|
|
98
|
-
camelized_hash
|
|
98
|
+
value.each_with_object({}) do |(k, v), camelized_hash|
|
|
99
|
+
camelized_hash[sanitize(k.to_s).camelize(:lower)] = deep_camelize(v)
|
|
99
100
|
end
|
|
100
101
|
else
|
|
101
102
|
sanitize(value)
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
105
|
-
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
|
106
|
-
if first_letter_in_uppercase
|
|
107
|
-
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
|
108
|
-
else
|
|
109
|
-
(lower_case_and_underscored_word[0] || "") + camelize(lower_case_and_underscored_word)[1..-1]
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
106
|
def extract_associations_from(entity, association_columns)
|
|
114
107
|
association_columns.reduce({}) do |associations, column_name|
|
|
115
108
|
associations[column_name] = entity[column_name]
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "bunny"
|
|
4
|
+
require_relative "errors"
|
|
2
5
|
require_relative "event"
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
module Flu
|
|
6
9
|
class EventPublisher
|
|
10
|
+
NOT_CONNECTED_MESSAGE = "no connection to RabbitMQ: 'connect' was never called, or " \
|
|
11
|
+
"'disconnect' was. The railtie calls it at boot unless " \
|
|
12
|
+
"'auto_connect_to_exchange' is false."
|
|
13
|
+
|
|
7
14
|
def initialize(configuration)
|
|
8
15
|
@logger = configuration.logger
|
|
9
16
|
@configuration = configuration
|
|
17
|
+
@exchange_key = :"flu_exchange_#{object_id}" # Channels are cached per thread
|
|
10
18
|
end
|
|
11
19
|
|
|
12
20
|
def publish(event, persistent=true)
|
|
13
21
|
routing_key = event.to_routing_key
|
|
14
|
-
@logger.debug
|
|
15
|
-
|
|
16
|
-
@logger.debug
|
|
22
|
+
@logger.debug { "Publishing event with id '#{event.id}' with routing key: #{routing_key}" }
|
|
23
|
+
exchange.publish(event.to_json, routing_key: routing_key, persistent: persistent)
|
|
24
|
+
@logger.debug { "Event published." }
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
def connect
|
|
@@ -32,43 +40,57 @@ module Flu
|
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def connected?
|
|
35
|
-
|
|
36
|
-
false
|
|
37
|
-
else
|
|
38
|
-
@connection.open?
|
|
39
|
-
end
|
|
43
|
+
!@connection.nil? && @connection.open?
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
# Closing the connection closes
|
|
43
|
-
# threads Bunny runs alongside it.
|
|
44
|
-
# The guard is on the connection alone
|
|
45
|
-
#
|
|
46
|
+
# Closing the connection closes every channel opened on it, and stops the heartbeat and
|
|
47
|
+
# recovery threads Bunny runs alongside it.
|
|
48
|
+
# The guard is on the connection alone: a connection that was opened before the exchange could
|
|
49
|
+
# be declared still has to be closed.
|
|
46
50
|
def disconnect
|
|
47
51
|
if !@connection.nil? && @connection.open?
|
|
48
|
-
@connection.close
|
|
52
|
+
@connection.close
|
|
49
53
|
end
|
|
50
54
|
@connection = nil
|
|
51
|
-
@
|
|
52
|
-
@exchange = nil
|
|
55
|
+
Thread.current[@exchange_key] = nil
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
private
|
|
56
59
|
|
|
60
|
+
# One channel per thread rather than one for the whole publisher.
|
|
61
|
+
# Bunny serialises every publication on the channel's own mutex.
|
|
62
|
+
#
|
|
63
|
+
# No bookkeeping of the channels handed out is needed.
|
|
64
|
+
# Closing the connection closes all of them, so a thread holding a closed channel simply opens a new one on its next publication:
|
|
65
|
+
# 'disconnect' and a reconnection are both covered without reaching into other threads.
|
|
66
|
+
# Only the absence of a connection is reported here. A connection that exists but is closed or
|
|
67
|
+
# recovering is Bunny's story to tell, and its own error says more about it than this one could.
|
|
68
|
+
def exchange
|
|
69
|
+
cached = Thread.current[@exchange_key]
|
|
70
|
+
return cached if cached && cached.channel.open?
|
|
71
|
+
raise NotConnectedError, NOT_CONNECTED_MESSAGE if @connection.nil?
|
|
72
|
+
Thread.current[@exchange_key] = declare_exchange
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def declare_exchange
|
|
76
|
+
channel = @connection.create_channel
|
|
77
|
+
channel.topic(@configuration.rabbitmq_exchange_name,
|
|
78
|
+
durable: @configuration.rabbitmq_exchange_durable)
|
|
79
|
+
end
|
|
80
|
+
|
|
57
81
|
def connect_to_exchange
|
|
58
82
|
options = {
|
|
59
83
|
host: @configuration.rabbitmq_host,
|
|
84
|
+
vhost: @configuration.rabbitmq_vhost,
|
|
60
85
|
port: @configuration.rabbitmq_port&.to_i,
|
|
61
86
|
user: @configuration.rabbitmq_user,
|
|
62
87
|
password: @configuration.rabbitmq_password,
|
|
63
88
|
automatically_recover: true
|
|
64
89
|
}.merge(@configuration.bunny_options || {})
|
|
65
|
-
|
|
90
|
+
|
|
66
91
|
@connection = Bunny.new(options)
|
|
67
92
|
@connection.start
|
|
68
|
-
@
|
|
69
|
-
@exchange = @channel.send(:topic,
|
|
70
|
-
@configuration.rabbitmq_exchange_name,
|
|
71
|
-
durable: @configuration.rabbitmq_exchange_durable)
|
|
93
|
+
Thread.current[@exchange_key] = declare_exchange
|
|
72
94
|
end
|
|
73
95
|
end
|
|
74
96
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "rabbitmq/http/client"
|
|
2
4
|
|
|
3
5
|
module Flu
|
|
@@ -9,23 +11,23 @@ module Flu
|
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def find_all
|
|
12
|
-
@management_client.list_queues
|
|
14
|
+
@management_client.list_queues(@configuration.rabbitmq_vhost)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def find_queue(name)
|
|
16
|
-
@management_client.queue_info(
|
|
18
|
+
@management_client.queue_info(@configuration.rabbitmq_vhost, name)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def find_bindings_for_queue(name)
|
|
20
|
-
@management_client.list_queue_bindings(
|
|
22
|
+
@management_client.list_queue_bindings(@configuration.rabbitmq_vhost, name)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def purge_queue(name)
|
|
24
|
-
@management_client.purge_queue(
|
|
26
|
+
@management_client.purge_queue(@configuration.rabbitmq_vhost, name)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def delete_queue(name)
|
|
28
|
-
@management_client.delete_queue(
|
|
30
|
+
@management_client.delete_queue(@configuration.rabbitmq_vhost, name)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
private
|
data/lib/flu-rails/railtie.rb
CHANGED
data/lib/flu-rails/util.rb
CHANGED
data/lib/flu-rails/version.rb
CHANGED
data/lib/flu-rails.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "logger"
|
|
2
4
|
require "json"
|
|
3
5
|
require "active_support/core_ext/module/introspection"
|
|
4
6
|
require_relative "flu-rails/version"
|
|
7
|
+
require_relative "flu-rails/errors"
|
|
5
8
|
require_relative "flu-rails/event"
|
|
6
9
|
require_relative "flu-rails/event_factory"
|
|
7
10
|
require_relative "flu-rails/queue_repository"
|
|
@@ -10,7 +13,7 @@ require_relative "flu-rails/core_ext"
|
|
|
10
13
|
require_relative "flu-rails/event_publisher"
|
|
11
14
|
require_relative "flu-rails/util"
|
|
12
15
|
require_relative "flu-rails/dummy/in_memory_event_publisher"
|
|
13
|
-
require_relative "flu-rails/railtie" if defined?(Rails)
|
|
16
|
+
require_relative "flu-rails/railtie" if defined?(Rails::Railtie)
|
|
14
17
|
|
|
15
18
|
module Flu
|
|
16
19
|
def self.configure
|
|
@@ -87,6 +90,7 @@ module Flu
|
|
|
87
90
|
config.rejected_user_agents = []
|
|
88
91
|
config.logger = ::Logger.new(STDOUT)
|
|
89
92
|
config.rabbitmq_host = "localhost"
|
|
93
|
+
config.rabbitmq_vhost = "/"
|
|
90
94
|
config.rabbitmq_port = 5672
|
|
91
95
|
config.rabbitmq_management_scheme = "http"
|
|
92
96
|
config.rabbitmq_management_port = 15672
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Loïc Vigneron
|
|
@@ -152,6 +152,20 @@ dependencies:
|
|
|
152
152
|
- - "~>"
|
|
153
153
|
- !ruby/object:Gem::Version
|
|
154
154
|
version: '3.13'
|
|
155
|
+
- !ruby/object:Gem::Dependency
|
|
156
|
+
name: simplecov
|
|
157
|
+
requirement: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - ">="
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: '0'
|
|
162
|
+
type: :development
|
|
163
|
+
prerelease: false
|
|
164
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
165
|
+
requirements:
|
|
166
|
+
- - ">="
|
|
167
|
+
- !ruby/object:Gem::Version
|
|
168
|
+
version: '0'
|
|
155
169
|
- !ruby/object:Gem::Dependency
|
|
156
170
|
name: sqlite3
|
|
157
171
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -185,6 +199,7 @@ files:
|
|
|
185
199
|
- lib/flu-rails/configuration.rb
|
|
186
200
|
- lib/flu-rails/core_ext.rb
|
|
187
201
|
- lib/flu-rails/dummy/in_memory_event_publisher.rb
|
|
202
|
+
- lib/flu-rails/errors.rb
|
|
188
203
|
- lib/flu-rails/event.rb
|
|
189
204
|
- lib/flu-rails/event_factory.rb
|
|
190
205
|
- lib/flu-rails/event_publisher.rb
|