happn 1.1.6 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63d6c46bb2d768801f6c5391d84122dea75f093686ca0435f707be5b06c04f77
4
- data.tar.gz: 18df636fedb057452deed96f0e771290d78577b6035bc22352faf353ff1a4b71
3
+ metadata.gz: 97331b5fd43edeae737ee8937f4b838d9788285404dbef888e77a28109e3d384
4
+ data.tar.gz: 173f02ea108c6911b6077399765e3326a3b792fd5368d5e4ca89028f37406f9b
5
5
  SHA512:
6
- metadata.gz: 68d34bb4d5788a9190949028f6b151f51c78a8d16e87ed0966c04e542689aa304a223f44607a8c3ee57c715e581ee147054ad1588449a2163f3330e8ec60659f
7
- data.tar.gz: bf2478e793629583060b67e3a7a5398031346417e7a68e7734b9b61cb4dc83c0345d96ef64e59b1a970858b239d50ad8d5ac9d761a6f5306bfb7a5e30ffd8e6f
6
+ metadata.gz: 6b5c37de3a0d01ff984bd92cebf76c09b7905f7ddb556eb5757defca1bfb0957d488aa8e6909f922db84418ac623dd81b6c63cac5f5742f8d96efa3928e7ac5d
7
+ data.tar.gz: 95c55b9d1cadec849aaf6ad4107fbc05d4ba8411dc5aaaa62bb63c108bd7d2a1affe89545f82928913c155fcb43e9a78cfca58ea1a97bd8121ee1d2d3cb82fe6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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
+ ### [1.1.8] - 2026-08-03
9
+
10
+ * `Happn` no longer runs a handler several times for a single event. `"all"` is both the wildcard a query is stored under and a value an event may legitimately carry, and the dispatch used to list it twice: an event whose `emitter`, `kind` and `status` were all `"all"` ran each of its matching handlers 8 times, and 16 times when its `name` carried it too.
11
+ * `Happn` now reads the existing bindings of its queue on the vhost its broker connection uses, instead of always on the default one. On any other vhost it used to read someone else's bindings, and to unbind legitimate ones from its own queue as a result. Both `vhost` and `virtual_host` are honoured in `bunny_options`, with the precedence Bunny gives them.
12
+ * `Happn` now only unbinds the routing keys bound to its own exchange. It used to consider every binding of the queue, whatever its source, so the implicit binding each queue carries to the default exchange triggered a useless `unbind` on every start up.
13
+ * `Happn.stop` stops the consumption started by `Happn.start` and releases the thread it blocks. It cancels the consumer, lets the messages already handed over run to completion, then closes the broker connection.
14
+ * `Happn` builds an event from its payload about 5 times faster. The conversion of a payload key into an underscored symbol is now memoized instead of being recomputed on every key of every message.
15
+ * `Happn` allocates far fewer arrays while looking up the handlers of an event: the list under construction is filled in place instead of being reallocated on each of the 16 combinations looked up, which also lightens the garbage collector on a busy queue.
16
+ * `Happn.register` is now really private. It was meant to be, and read as such, but the `private` guarding it had no effect on a method defined on the module itself, leaving it callable from the outside.
17
+ * Publishing a version now requires the whole test suite to pass.
18
+
19
+ ### [1.1.7] - 2026-08-01
20
+
21
+ * **Breaking**: `required_ruby_version` is raised from `>= 3.0` to `>= 3.2`.
22
+ * `Happn::EventConsumer` now requires `json` explicitly.
23
+ * `Happn::Event#timestamp` returns `nil` again for an empty or blank timestamp, as it did up to 1.0.3 through `String#to_datetime`.
24
+ * Integration tests with RabbitMQ.
25
+
8
26
  ### [1.1.6] - 2026-07-31
9
27
 
10
28
  * Suite of tests
data/README.md CHANGED
@@ -7,7 +7,7 @@ This gem connects a single RabbitMQ queue and bind it automatically to its excha
7
7
 
8
8
  ## Requirements
9
9
 
10
- * Ruby >= 3.0
10
+ * Ruby >= 3.2
11
11
  * Tested with RabbitMQ 3.5.8
12
12
  * `happn-ruby` works with or without Rails (tested with Rails 4 and 5).
13
13
 
@@ -43,7 +43,8 @@ Each configuration is detailed below.
43
43
  * `Happn` consumes a single queue through the RabbitMQ's [Topic Exchange Model](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-topic).
44
44
  * If the queue does not exist when `Happn` starts, it is created automatically.
45
45
  * When connecting a queue, please be careful that each connection parameter must match the existing queue's parameters. For instance, the value of `x-queue-mode` must match to avoid a `PRECONDITION FAILED` error.
46
- * All bindings between queues and their exchange are reset when starting `Happn`. Based on all the projectors that have been registered (option `projector_classes`), `Happn` detects which events must be consumed and binds its queue to the exchange depending on these event matchers.
46
+ * The bindings between the queue and the exchange `Happn` consumes are reset when starting `Happn`. Based on all the projectors that have been registered (option `projector_classes`), `Happn` detects which events must be consumed, binds its queue to the exchange depending on these event matchers, and unbinds the routing keys that no longer match any projector. Bindings the same queue may have to _other_ exchanges are left untouched.
47
+ * `Happn` reads those existing bindings through the RabbitMQ management API, on the vhost its broker connection uses. To consume a vhost other than the default one, declare it in `bunny_options` (`vhost` or `virtual_host`, as Bunny accepts both): `Happn` follows it on both the AMQP connection and the management API.
47
48
 
48
49
  ## About projectors
49
50
 
@@ -72,6 +73,28 @@ Starting `Happn` consumes events sequentially. For instance, it can be started f
72
73
  end
73
74
  ```
74
75
 
76
+ ### Shutting Down
77
+
78
+ `Happn.start` blocks the calling thread for as long as the consumption lasts. `Happn.stop` releases it: it cancels the consumer, lets the messages already being handled run to completion, and closes the broker connection. It is meant to be called from another thread than the one blocked in `Happn.start`.
79
+
80
+ `Happn` installs no signal handler of its own, because only the application knows what a graceful shutdown means for it. Wiring one is up to you:
81
+
82
+ ```ruby
83
+ namespace :events do
84
+ desc "Listen all events and consume them."
85
+ task consume: :environment do
86
+ Happn.init
87
+ Signal.trap("TERM") { Thread.new { Happn.stop } }
88
+ Signal.trap("INT") { Thread.new { Happn.stop } }
89
+ Happn.start
90
+ end
91
+ end
92
+ ```
93
+
94
+ Signal handlers run in a context where very little is allowed, hence the `Thread.new`.
95
+
96
+ Without it, an orchestrator stopping the process interrupts the handler in flight. Nothing is lost, the message was never acknowledged, so the broker redelivers it, but it is consumed twice, once partially.
97
+
75
98
  ### Define a Projector
76
99
 
77
100
  A projector is a class that defines how to consume one or multiple types of events. This class must:
@@ -87,7 +110,7 @@ class LoggerProjector < Happn::Projector
87
110
  end
88
111
 
89
112
  on kind: "request", status: :new do |event|
90
- Rails.logger("This is a new request to the controller: #{event.data["controller_name"]}")
113
+ Rails.logger("This is a new request to the controller: #{event.data[:controller_name]}")
91
114
  end
92
115
  end
93
116
  end
@@ -114,6 +137,18 @@ The test suite is written with [RSpec](https://rspec.info) and needs no running
114
137
  bundle exec rake # or: bundle exec rspec
115
138
  ```
116
139
 
140
+ ## Running the integration tests
141
+
142
+ The test suite above needs no running RabbitMQ. A second suite, under `spec_integration/`, exercises `Happn`
143
+ against a real broker instead: queue and binding lifecycle, message routing, consumption and acknowledgement,
144
+ and the reject-and-exit contract when a projector handler raises.
145
+
146
+ ```bash
147
+ docker compose up -d --wait # starts RabbitMQ with the management plugin
148
+ bundle exec rake integration # or: bundle exec rspec spec_integration
149
+ docker compose down
150
+ ```
151
+
117
152
  ## Overall configuration options
118
153
 
119
154
  All options have a default value. However, all of them can be changed in your `Happn.configure` block.
@@ -133,7 +168,7 @@ All options have a default value. However, all of them can be changed in your `H
133
168
  | `rabbitmq_queue_mode` | `nil` | String | Optional | When creating the queue, this option can be passed to set `x-queue-mode`. For instance, a queue can be made _"lazy"_ by passing `"lazy"` as a value. See [RabbitMQ's documentation](https://www.rabbitmq.com/lazy-queues.html) for more details. | `lazy` |
134
169
  | `rabbitmq_prefetch_size` | `10` | Integer | Optional | Also known as RabbitMQ's QOS. From the [RabbitMQ's documentation](http://www.rabbitmq.com/consumer-prefetch.html): _"AMQP specifies the basic.qos method to allow you to limit the number of unacknowledged messages on a channel (or connection) when consuming (aka "prefetch count")."_ | `1000` |
135
170
  | `projector_classes` | `[]` | Array of constants | Required | All Projector classes to register. This value can be generated by reading all descendant classes from `Happn::Projector`. | `[MyProjector]` |
136
- | `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
+ | `bunny_options` | `{}` | Hash of symbols | Optional | Additional options to add when connecting the RabbitMQ broker. This overrides the existing options with the same name. A `vhost` (or `virtual_host`) declared here is also the vhost `Happn` queries through the management API. | `{ verify_peer: true }` |
137
172
  | `management_options` | `{}` | Hash of symbols | Optional | Additional options to add when accessing the RabbitMQ Managmement. This overrides the existing options with the same name. The options are defined at https://github.com/ruby-amqp/rabbitmq_http_api_client | `{ verify: false }` |
138
173
  | `on_error` | `nil` | `block` with an argument `exception` | false | When the consumption of an event raises an Error, the consumption exits. However, this block can be called before exiting the consumption execution. | `lambda { |exception| Raven.capture_exception(exception) }` (see Sentry's [documentation](https://github.com/getsentry/raven-ruby) |
139
174
 
@@ -145,12 +180,12 @@ no API key is stored in this repository, the workflow exchanges a short-lived Gi
145
180
  scoped RubyGems credential.
146
181
 
147
182
  1. Update `Happn::VERSION` in `lib/happn/version.rb` and the `CHANGELOG.md`
148
- 2. Commit and push these changes to `master`
183
+ 2. Commit and push these changes to `main`
149
184
  3. Tag the commit and push the tag:
150
185
 
151
186
  ```
152
- $ git tag -a v1.1.6 -m "Version 1.1.6"
153
- $ git push origin v1.1.6
187
+ $ git tag -a v1.1.8 -m "Version 1.1.8"
188
+ $ git push origin v1.1.8
154
189
  ```
155
190
 
156
191
  The workflow then checks that the tag matches `Happn::VERSION`, runs the tests, builds the gem
data/happn.gemspec CHANGED
@@ -15,20 +15,16 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = "https://github.com/crepesourcing/happn-ruby"
16
16
  spec.license = "MIT"
17
17
 
18
- spec.required_ruby_version = ">= 3.0"
18
+ spec.required_ruby_version = ">= 3.2"
19
19
 
20
20
  spec.metadata = {
21
21
  "source_code_uri" => spec.homepage,
22
- "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md",
22
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
23
23
  "bug_tracker_uri" => "#{spec.homepage}/issues",
24
24
  "rubygems_mfa_required" => "true"
25
25
  }
26
26
 
27
- spec.files = `git ls-files -z`.split("\x0").reject do |file|
28
- file.start_with?("spec/", ".github/", ".claude/") ||
29
- [".gitignore", ".rspec", "Gemfile", "Rakefile"].include?(file)
30
- end
31
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
27
+ spec.files = Dir.glob(["lib/**/*.rb", "CHANGELOG.md", "MIT-LICENSE", "README.md", "happn.gemspec"]).sort
32
28
  spec.require_paths = ["lib"]
33
29
 
34
30
  spec.add_development_dependency "bundler", ">=1.17"
data/lib/happn/event.rb CHANGED
@@ -1,88 +1,235 @@
1
1
  require "date"
2
2
 
3
3
  module Happn
4
+ # A single CREPE event, as it was consumed from the exchange.
5
+ #
6
+ # An event is built from the parsed payload of a message. That payload carries
7
+ # two entries: `meta`, describing the event itself, and `data`, describing what
8
+ # happened. A projector handler receives an instance of this class.
9
+ #
10
+ # ## Key conversion
11
+ #
12
+ # Every key of the payload, at every depth, is converted into an underscored
13
+ # Symbol when the event is built. A payload emitted in camel case is therefore
14
+ # read in snake case, and always through symbols:
15
+ #
16
+ # # {"meta" => {…}, "data" => {"requestMetadata" => {"controllerName" => "countries"}}}
17
+ # event.data[:request_metadata][:controller_name] # => "countries"
18
+ # event.data["request_metadata"] # => nil
19
+ #
20
+ # Dashes become underscores, `::` becomes `/`, and the capitals of an acronym
21
+ # are kept together: `"HTTPResponseCode"` is read as `:http_response_code`.
22
+ #
23
+ # ## Changes
24
+ #
25
+ # An entity change carries a `changes` entry, mapping an attribute to the pair
26
+ # of values it went through:
27
+ #
28
+ # event.changes # => { name: ["France", "Belgium"] }
29
+ #
30
+ # Events of another shape carry no such entry at all, a `request` for instance.
31
+ # {#changes} then returns `nil`, and the five methods reading through it raise
32
+ # a NoMethodError: guard them with {#changes} when a handler may be reached by
33
+ # events of several shapes.
34
+ #
35
+ # @example Reading an event in a projector
36
+ # on kind: "entity_change", name: "update country" do |event|
37
+ # Rails.logger.info("#{event.emitter} renamed a country at #{event.timestamp}")
38
+ # Rails.logger.info("from #{event.change_before(:name)} to #{event.change_after(:name)}")
39
+ # end
4
40
  class Event
5
41
 
42
+ # The whole `data` entry of the payload, keys converted.
43
+ #
44
+ # This is the hash the event holds, not a copy: {#changes=} and {#add_change}
45
+ # write into it, and so does anything the caller does to it.
46
+ #
47
+ # @return [Hash] the payload data, or whatever `data` held if it was no hash
48
+ attr_reader :data
49
+
50
+ # Builds an event from a parsed payload.
51
+ #
52
+ # @param args [Hash] the parsed payload, with its `"meta"` and `"data"`
53
+ # entries, both keyed by String
54
+ # @raise [KeyError] if the payload carries no `"meta"` or no `"data"` entry
6
55
  def initialize(args)
7
56
  @meta = deep_underscore_keys(args.fetch("meta"))
8
57
  @data = deep_underscore_keys(args.fetch("data"))
9
58
  end
10
59
 
11
- def data
12
- @data
13
- end
14
-
60
+ # The metadata the emitter attached to the user behind the event.
61
+ #
62
+ # @return [Hash, nil] nil when the payload carries no `user_metadata`
15
63
  def user_metadata
16
64
  @data[:user_metadata]
17
65
  end
18
66
 
67
+ # The metadata the emitter attached to the request behind the event.
68
+ #
69
+ # @return [Hash, nil] nil when the payload carries no `request_metadata`
19
70
  def request_metadata
20
71
  @data[:request_metadata]
21
72
  end
22
73
 
74
+ # The attributes the event changed, each mapped to its before and after
75
+ # values.
76
+ #
77
+ # @return [Hash{Symbol => Array}, nil] nil when the payload carries no
78
+ # `changes` entry, which is the case of every event that is not an entity
79
+ # change
23
80
  def changes
24
81
  @data[:changes]
25
82
  end
26
83
 
84
+ # Replaces the whole set of changes.
85
+ #
86
+ # Keys are taken as they are given: unlike the ones read from the payload,
87
+ # they go through no conversion.
88
+ #
89
+ # @param new_changes [Hash{Symbol => Array}] the changes to substitute
90
+ # @return [Hash{Symbol => Array}] the changes that were set
27
91
  def changes=(new_changes)
28
92
  @data[:changes] = new_changes
29
93
  end
30
94
 
95
+ # Records a change on an attribute.
96
+ #
97
+ # The "before" value is always `nil`: the method describes a value that was
98
+ # set, not a transition. An empty String is normalized into `nil`, so that a
99
+ # blank emitted value and an absent one are recorded alike.
100
+ #
101
+ # @example
102
+ # event.add_change(:name, "Belgium") # => [nil, "Belgium"]
103
+ # event.add_change(:name, "") # => [nil, nil]
104
+ #
105
+ # @param name [Symbol, String] the attribute the change bears on
106
+ # @param value [Object] the value the attribute was set to
107
+ # @return [Array] the pair of values recorded
108
+ # @raise [NoMethodError] if the payload carries no `changes` entry
31
109
  def add_change(name, value)
32
110
  new_value = value == "" ? nil : value
33
111
  changes[name.to_sym] = [nil, new_value]
34
112
  end
35
113
 
114
+ # The entities the event relates to.
115
+ #
116
+ # @return [Hash, nil] nil when the payload carries no `associations`
36
117
  def associations
37
118
  @data[:associations]
38
119
  end
39
120
 
121
+ # When the event was emitted.
122
+ #
123
+ # The raw value is parsed on every call, and its offset is kept as it was
124
+ # emitted rather than being normalized.
125
+ #
126
+ # @return [DateTime, nil] nil when the payload carries no timestamp, or an
127
+ # empty one
128
+ # @raise [Date::Error] if the timestamp cannot be parsed
40
129
  def timestamp
41
- DateTime.parse(@meta[:timestamp])
130
+ raw_timestamp = @meta[:timestamp]
131
+ if raw_timestamp.nil? || raw_timestamp.to_s.strip.empty?
132
+ nil
133
+ else
134
+ DateTime.parse(raw_timestamp)
135
+ end
42
136
  end
43
137
 
138
+ # The identifier the emitter gave the event.
139
+ #
140
+ # @return [String, nil] nil when the payload carries no id
44
141
  def id
45
142
  @meta[:id]
46
143
  end
47
144
 
145
+ # What the event says happened, matched by the `name` of a query.
146
+ #
147
+ # @return [String, nil] nil when the payload carries no name
48
148
  def name
49
149
  @meta[:name]
50
150
  end
51
151
 
152
+ # The state of the event, matched by the `status` of a query.
153
+ #
154
+ # @return [String, nil] nil when the payload carries no status
52
155
  def status
53
156
  @meta[:status]
54
157
  end
55
158
 
159
+ # The category of the event, matched by the `kind` of a query.
160
+ #
161
+ # @return [String, nil] nil when the payload carries no kind
56
162
  def kind
57
163
  @meta[:kind]
58
164
  end
59
165
 
166
+ # The application the event comes from, matched by the `emitter` of a query.
167
+ #
168
+ # @return [String, nil] nil when the payload carries no emitter
60
169
  def emitter
61
170
  @meta[:emitter]
62
171
  end
63
172
 
173
+ # The value an attribute was changed to.
174
+ #
175
+ # @param attribute_name [Symbol, String] the attribute to read
176
+ # @return [Object, nil] nil when the attribute did not change
177
+ # @raise [NoMethodError] if the payload carries no `changes` entry
64
178
  def change_after(attribute_name)
65
179
  changes[attribute_name.to_sym]&.last
66
180
  end
67
181
 
182
+ # The value an attribute was changed from.
183
+ #
184
+ # @param attribute_name [Symbol, String] the attribute to read
185
+ # @return [Object, nil] nil when the attribute did not change
186
+ # @raise [NoMethodError] if the payload carries no `changes` entry
68
187
  def change_before(attribute_name)
69
188
  changes[attribute_name.to_sym]&.first
70
189
  end
71
190
 
191
+ # Whether an attribute changed.
192
+ #
193
+ # @param attribute_name [Symbol, String] the attribute to look for
194
+ # @return [Boolean]
195
+ # @raise [NoMethodError] if the payload carries no `changes` entry
72
196
  def has_change?(attribute_name)
73
197
  !changes[attribute_name.to_sym].nil?
74
198
  end
75
199
 
200
+ # Drops the change recorded on an attribute.
201
+ #
202
+ # @param attribute_name [Symbol, String] the attribute to drop
203
+ # @return [Array, nil] the pair of values that was dropped, nil when the
204
+ # attribute did not change
205
+ # @raise [NoMethodError] if the payload carries no `changes` entry
76
206
  def delete_change(attribute_name)
77
207
  changes.delete(attribute_name.to_sym)
78
208
  end
79
209
 
210
+ # Underscoring a key is five regular expression passes and three String
211
+ # allocations, and an event stream keeps sending the very same keys. The cache
212
+ # is therefore bound by the vocabulary of the payloads, which is closed: an
213
+ # emitter putting a variable part inside a key name would make it grow forever.
214
+ UNDERSCORED_KEYS = {}
215
+ private_constant :UNDERSCORED_KEYS
216
+
80
217
  private
81
218
 
219
+ # Converts a payload key into the symbol the event is read through, memoized
220
+ # for every event at once.
221
+ #
222
+ # @param key [String] a key as the emitter wrote it
223
+ # @return [Symbol] its underscored form
82
224
  def underscore_key(key)
83
- underscore(key).to_sym
225
+ UNDERSCORED_KEYS[key] ||= underscore(key).to_sym
84
226
  end
85
227
 
228
+ # Converts the keys of a payload fragment, walking through hashes and arrays
229
+ # down to the values, which are left as they are.
230
+ #
231
+ # @param value [Object] a fragment of the payload
232
+ # @return [Object] the same fragment, keyed by underscored symbols
86
233
  def deep_underscore_keys(value)
87
234
  case value
88
235
  when Array
@@ -94,6 +241,10 @@ module Happn
94
241
  end
95
242
  end
96
243
 
244
+ # Underscores a word, splitting it on its capitals and turning `::` into `/`.
245
+ #
246
+ # @param camel_cased_word [String] the word to convert
247
+ # @return [String] its underscored, downcased form
97
248
  def underscore(camel_cased_word)
98
249
  camel_cased_word.gsub(/::/, '/').
99
250
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -1,4 +1,5 @@
1
1
  require "bunny"
2
+ require "json"
2
3
  require "rabbitmq/http/client"
3
4
 
4
5
  module Happn
@@ -16,6 +17,7 @@ module Happn
16
17
  automatically_recover: true
17
18
  }.merge(@configuration.bunny_options || {})
18
19
  @connection = Bunny.new(options)
20
+ @vhost = options[:virtual_host] || options[:vhost] || "/"
19
21
 
20
22
  management_options = {
21
23
  username: @configuration.rabbitmq_user,
@@ -42,6 +44,13 @@ module Happn
42
44
  consume
43
45
  end
44
46
 
47
+ def stop
48
+ @logger.info("Stopping events consumption...")
49
+ @consumer&.cancel
50
+ @consumer = nil
51
+ @connection.close if @connection.open?
52
+ end
53
+
45
54
  private
46
55
 
47
56
  def connect
@@ -59,9 +68,8 @@ module Happn
59
68
  arguments = {}
60
69
  arguments["x-queue-mode"] = @configuration.rabbitmq_queue_mode unless @configuration.rabbitmq_queue_mode.nil?
61
70
  @queue = @channel.queue(@queue_name, durable: true, arguments: arguments)
62
- exchange = @channel.send(:topic,
63
- @configuration.rabbitmq_exchange_name,
64
- durable: @configuration.rabbitmq_exchange_durable)
71
+ exchange = @channel.topic(@configuration.rabbitmq_exchange_name,
72
+ durable: @configuration.rabbitmq_exchange_durable)
65
73
 
66
74
  routing_keys = @subscription_repository.find_all.map do | subscription |
67
75
  subscription.query.to_routing_key
@@ -76,18 +84,19 @@ module Happn
76
84
  @logger.info("Ready!")
77
85
  end
78
86
 
87
+ # 'Queue#subscribe' blocks before it can return the consumer it built, so the
88
+ # consumer is built here instead: 'stop' needs a handle on it to cancel it.
89
+ # Bunny's fourth argument is 'no_ack', hence false for a manual acknowledgement.
79
90
  def consume
80
- options = {
81
- manual_ack: true,
82
- block: true
83
- }
84
- @queue.subscribe(options) do | delivery_info, _properties, event |
91
+ @consumer = Bunny::Consumer.new(@channel, @queue, @channel.generate_consumer_tag, false)
92
+ @consumer.on_delivery do | delivery_info, _properties, event |
85
93
  begin
86
94
  handle_message(event, delivery_info)
87
95
  rescue => exception
88
96
  handle_exception(exception, delivery_info)
89
97
  end
90
98
  end
99
+ @queue.subscribe_with(@consumer, block: true)
91
100
  end
92
101
 
93
102
  def handle_message(message, delivery_info)
@@ -110,8 +119,6 @@ module Happn
110
119
  raise exception
111
120
  end
112
121
 
113
- private
114
-
115
122
  def unbind_useless_routing_keys(queue, exchange, useful_routing_keys)
116
123
  all_routing_keys = find_all_routing_keys_of(queue)
117
124
  keys_to_remove = all_routing_keys - useful_routing_keys
@@ -121,8 +128,9 @@ module Happn
121
128
  end
122
129
 
123
130
  def find_all_routing_keys_of(queue)
124
- @management_client.list_queue_bindings("/", queue.name).map do | binding |
125
- binding.routing_key
131
+ exchange_name = @configuration.rabbitmq_exchange_name
132
+ @management_client.list_queue_bindings(@vhost, queue.name).filter_map do | binding |
133
+ binding.routing_key if binding.source == exchange_name
126
134
  end
127
135
  end
128
136
  end
@@ -25,16 +25,17 @@ module Happn
25
25
  end
26
26
 
27
27
  def find_subscriptions_for(event)
28
- possible_event_statuses = ["all", event.status.to_s]
29
- possible_event_emitters = ["all", event.emitter.to_s]
30
- possible_event_names = ["all", event.name.to_s]
31
- possible_event_kinds = ["all", event.kind.to_s]
28
+ possible_event_statuses = ["all", event.status.to_s].uniq
29
+ possible_event_emitters = ["all", event.emitter.to_s].uniq
30
+ possible_event_names = ["all", event.name.to_s].uniq
31
+ possible_event_kinds = ["all", event.kind.to_s].uniq
32
32
  subscriptions = []
33
33
  possible_event_statuses.each do | status |
34
34
  possible_event_emitters.each do | emitter |
35
35
  possible_event_kinds.each do | kind |
36
36
  possible_event_names.each do | name |
37
- subscriptions += @subscriptions.dig(status, emitter, kind, name) || []
37
+ found = @subscriptions.dig(status, emitter, kind, name)
38
+ subscriptions.concat(found) if found
38
39
  end
39
40
  end
40
41
  end
@@ -47,7 +48,7 @@ module Happn
47
48
  def flatten(item)
48
49
  if item.instance_of?(Hash)
49
50
  item.values.inject([]) do | result, value |
50
- result += flatten(value)
51
+ result.concat(flatten(value))
51
52
  end
52
53
  else
53
54
  item
data/lib/happn/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Happn
2
- VERSION = "1.1.6"
2
+ VERSION = "1.1.8"
3
3
  end
data/lib/happn.rb CHANGED
@@ -24,7 +24,7 @@ module Happn
24
24
  def self.init
25
25
  @logger = @configuration.logger || Logger.new(STDOUT)
26
26
  subscription_repository = SubscriptionRepository.new(@logger)
27
- Happn::register(@configuration.projector_classes, subscription_repository)
27
+ register(@configuration.projector_classes, subscription_repository)
28
28
  @event_consumer = EventConsumer.new(@logger, @configuration, subscription_repository)
29
29
  end
30
30
 
@@ -32,6 +32,10 @@ module Happn
32
32
  @event_consumer.start
33
33
  end
34
34
 
35
+ def self.stop
36
+ @event_consumer&.stop
37
+ end
38
+
35
39
  def self.create_queue_only
36
40
  Happn.init
37
41
  @event_consumer.wait_until_connected
@@ -54,11 +58,8 @@ module Happn
54
58
  config.on_error = nil
55
59
  config.bunny_options = {}
56
60
  config.management_options = {}
57
- config.on_error = nil
58
61
  end
59
62
 
60
- private
61
-
62
63
  def self.register(projector_classes, subscription_repository)
63
64
  @logger.info("#{projector_classes.size} projector are going to be registered...")
64
65
  projector_classes.each do | projector_class |
@@ -67,5 +68,6 @@ module Happn
67
68
  @logger.info("Projector '#{projector_class}' registered")
68
69
  end
69
70
  end
71
+ private_class_method :register
70
72
 
71
73
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: happn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Commuty
@@ -106,7 +106,7 @@ licenses:
106
106
  - MIT
107
107
  metadata:
108
108
  source_code_uri: https://github.com/crepesourcing/happn-ruby
109
- changelog_uri: https://github.com/crepesourcing/happn-ruby/blob/master/CHANGELOG.md
109
+ changelog_uri: https://github.com/crepesourcing/happn-ruby/blob/main/CHANGELOG.md
110
110
  bug_tracker_uri: https://github.com/crepesourcing/happn-ruby/issues
111
111
  rubygems_mfa_required: 'true'
112
112
  rdoc_options: []
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
116
  requirements:
117
117
  - - ">="
118
118
  - !ruby/object:Gem::Version
119
- version: '3.0'
119
+ version: '3.2'
120
120
  required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="