happn 1.1.7 → 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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +28 -5
- data/lib/happn/event.rb +151 -5
- data/lib/happn/event_consumer.rb +19 -12
- data/lib/happn/subscription_repository.rb +7 -6
- data/lib/happn/version.rb +1 -1
- data/lib/happn.rb +6 -3
- 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: 97331b5fd43edeae737ee8937f4b838d9788285404dbef888e77a28109e3d384
|
|
4
|
+
data.tar.gz: 173f02ea108c6911b6077399765e3326a3b792fd5368d5e4ca89028f37406f9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b5c37de3a0d01ff984bd92cebf76c09b7905f7ddb556eb5757defca1bfb0957d488aa8e6909f922db84418ac623dd81b6c63cac5f5742f8d96efa3928e7ac5d
|
|
7
|
+
data.tar.gz: 95c55b9d1cadec849aaf6ad4107fbc05d4ba8411dc5aaaa62bb63c108bd7d2a1affe89545f82928913c155fcb43e9a78cfca58ea1a97bd8121ee1d2d3cb82fe6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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
|
+
|
|
8
19
|
### [1.1.7] - 2026-08-01
|
|
9
20
|
|
|
10
21
|
* **Breaking**: `required_ruby_version` is raised from `>= 3.0` to `>= 3.2`.
|
data/README.md
CHANGED
|
@@ -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
|
-
*
|
|
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[
|
|
113
|
+
Rails.logger("This is a new request to the controller: #{event.data[:controller_name]}")
|
|
91
114
|
end
|
|
92
115
|
end
|
|
93
116
|
end
|
|
@@ -145,7 +168,7 @@ All options have a default value. However, all of them can be changed in your `H
|
|
|
145
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` |
|
|
146
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` |
|
|
147
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]` |
|
|
148
|
-
| `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 }` |
|
|
149
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 }` |
|
|
150
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) |
|
|
151
174
|
|
|
@@ -161,8 +184,8 @@ scoped RubyGems credential.
|
|
|
161
184
|
3. Tag the commit and push the tag:
|
|
162
185
|
|
|
163
186
|
```
|
|
164
|
-
$ git tag -a v1.1.
|
|
165
|
-
$ git push origin v1.1.
|
|
187
|
+
$ git tag -a v1.1.8 -m "Version 1.1.8"
|
|
188
|
+
$ git push origin v1.1.8
|
|
166
189
|
```
|
|
167
190
|
|
|
168
191
|
The workflow then checks that the tag matches `Happn::VERSION`, runs the tests, builds the gem
|
data/lib/happn/event.rb
CHANGED
|
@@ -1,42 +1,131 @@
|
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
130
|
raw_timestamp = @meta[:timestamp]
|
|
42
131
|
if raw_timestamp.nil? || raw_timestamp.to_s.strip.empty?
|
|
@@ -46,48 +135,101 @@ module Happn
|
|
|
46
135
|
end
|
|
47
136
|
end
|
|
48
137
|
|
|
138
|
+
# The identifier the emitter gave the event.
|
|
139
|
+
#
|
|
140
|
+
# @return [String, nil] nil when the payload carries no id
|
|
49
141
|
def id
|
|
50
142
|
@meta[:id]
|
|
51
143
|
end
|
|
52
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
|
|
53
148
|
def name
|
|
54
149
|
@meta[:name]
|
|
55
150
|
end
|
|
56
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
|
|
57
155
|
def status
|
|
58
156
|
@meta[:status]
|
|
59
157
|
end
|
|
60
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
|
|
61
162
|
def kind
|
|
62
163
|
@meta[:kind]
|
|
63
164
|
end
|
|
64
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
|
|
65
169
|
def emitter
|
|
66
170
|
@meta[:emitter]
|
|
67
171
|
end
|
|
68
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
|
|
69
178
|
def change_after(attribute_name)
|
|
70
179
|
changes[attribute_name.to_sym]&.last
|
|
71
180
|
end
|
|
72
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
|
|
73
187
|
def change_before(attribute_name)
|
|
74
188
|
changes[attribute_name.to_sym]&.first
|
|
75
189
|
end
|
|
76
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
|
|
77
196
|
def has_change?(attribute_name)
|
|
78
197
|
!changes[attribute_name.to_sym].nil?
|
|
79
198
|
end
|
|
80
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
|
|
81
206
|
def delete_change(attribute_name)
|
|
82
207
|
changes.delete(attribute_name.to_sym)
|
|
83
208
|
end
|
|
84
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
|
+
|
|
85
217
|
private
|
|
86
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
|
|
87
224
|
def underscore_key(key)
|
|
88
|
-
underscore(key).to_sym
|
|
225
|
+
UNDERSCORED_KEYS[key] ||= underscore(key).to_sym
|
|
89
226
|
end
|
|
90
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
|
|
91
233
|
def deep_underscore_keys(value)
|
|
92
234
|
case value
|
|
93
235
|
when Array
|
|
@@ -99,6 +241,10 @@ module Happn
|
|
|
99
241
|
end
|
|
100
242
|
end
|
|
101
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
|
|
102
248
|
def underscore(camel_cased_word)
|
|
103
249
|
camel_cased_word.gsub(/::/, '/').
|
|
104
250
|
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
data/lib/happn/event_consumer.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Happn
|
|
|
17
17
|
automatically_recover: true
|
|
18
18
|
}.merge(@configuration.bunny_options || {})
|
|
19
19
|
@connection = Bunny.new(options)
|
|
20
|
+
@vhost = options[:virtual_host] || options[:vhost] || "/"
|
|
20
21
|
|
|
21
22
|
management_options = {
|
|
22
23
|
username: @configuration.rabbitmq_user,
|
|
@@ -43,6 +44,13 @@ module Happn
|
|
|
43
44
|
consume
|
|
44
45
|
end
|
|
45
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
|
+
|
|
46
54
|
private
|
|
47
55
|
|
|
48
56
|
def connect
|
|
@@ -60,9 +68,8 @@ module Happn
|
|
|
60
68
|
arguments = {}
|
|
61
69
|
arguments["x-queue-mode"] = @configuration.rabbitmq_queue_mode unless @configuration.rabbitmq_queue_mode.nil?
|
|
62
70
|
@queue = @channel.queue(@queue_name, durable: true, arguments: arguments)
|
|
63
|
-
exchange = @channel.
|
|
64
|
-
|
|
65
|
-
durable: @configuration.rabbitmq_exchange_durable)
|
|
71
|
+
exchange = @channel.topic(@configuration.rabbitmq_exchange_name,
|
|
72
|
+
durable: @configuration.rabbitmq_exchange_durable)
|
|
66
73
|
|
|
67
74
|
routing_keys = @subscription_repository.find_all.map do | subscription |
|
|
68
75
|
subscription.query.to_routing_key
|
|
@@ -77,18 +84,19 @@ module Happn
|
|
|
77
84
|
@logger.info("Ready!")
|
|
78
85
|
end
|
|
79
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.
|
|
80
90
|
def consume
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
block: true
|
|
84
|
-
}
|
|
85
|
-
@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 |
|
|
86
93
|
begin
|
|
87
94
|
handle_message(event, delivery_info)
|
|
88
95
|
rescue => exception
|
|
89
96
|
handle_exception(exception, delivery_info)
|
|
90
97
|
end
|
|
91
98
|
end
|
|
99
|
+
@queue.subscribe_with(@consumer, block: true)
|
|
92
100
|
end
|
|
93
101
|
|
|
94
102
|
def handle_message(message, delivery_info)
|
|
@@ -111,8 +119,6 @@ module Happn
|
|
|
111
119
|
raise exception
|
|
112
120
|
end
|
|
113
121
|
|
|
114
|
-
private
|
|
115
|
-
|
|
116
122
|
def unbind_useless_routing_keys(queue, exchange, useful_routing_keys)
|
|
117
123
|
all_routing_keys = find_all_routing_keys_of(queue)
|
|
118
124
|
keys_to_remove = all_routing_keys - useful_routing_keys
|
|
@@ -122,8 +128,9 @@ module Happn
|
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
def find_all_routing_keys_of(queue)
|
|
125
|
-
@
|
|
126
|
-
|
|
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
|
|
127
134
|
end
|
|
128
135
|
end
|
|
129
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
|
-
|
|
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
|
|
51
|
+
result.concat(flatten(value))
|
|
51
52
|
end
|
|
52
53
|
else
|
|
53
54
|
item
|
data/lib/happn/version.rb
CHANGED
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
|
-
|
|
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
|
|
@@ -56,8 +60,6 @@ module Happn
|
|
|
56
60
|
config.management_options = {}
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
private
|
|
60
|
-
|
|
61
63
|
def self.register(projector_classes, subscription_repository)
|
|
62
64
|
@logger.info("#{projector_classes.size} projector are going to be registered...")
|
|
63
65
|
projector_classes.each do | projector_class |
|
|
@@ -66,5 +68,6 @@ module Happn
|
|
|
66
68
|
@logger.info("Projector '#{projector_class}' registered")
|
|
67
69
|
end
|
|
68
70
|
end
|
|
71
|
+
private_class_method :register
|
|
69
72
|
|
|
70
73
|
end
|