happn 1.1.0
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 +7 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +129 -0
- data/happn.gemspec +26 -0
- data/lib/happn/configuration.rb +20 -0
- data/lib/happn/event.rb +105 -0
- data/lib/happn/event_consumer.rb +129 -0
- data/lib/happn/projector.rb +17 -0
- data/lib/happn/query.rb +27 -0
- data/lib/happn/subscription.rb +12 -0
- data/lib/happn/subscription_repository.rb +57 -0
- data/lib/happn/version.rb +3 -0
- data/lib/happn.rb +71 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 06b2f20b019373ac23f020f1b5c060f5f5185dddba551ace75ef4fc9a129d8ec
|
|
4
|
+
data.tar.gz: b7b767f5e7abb62eb33661afaea52b7a2695674ddd2b18255002c18ad4923fed
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 94cf0746e1fac94dfde7da681c1abe4c4632411d79d46123d546b41bc84a87ece483e5a9e5b0094fba1e98518fc15d9aa202ea65cb8e05accce59176122059a0
|
|
7
|
+
data.tar.gz: c63c774536d302c301b33ba1d37e828abddf2eae9e9783b8e60ae44ea45601a3f221df5ecc13265381c00ac3ae5b25078e54e1782855b604889a06f2d38515ca
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
### [1.1.0] - 2026-07-30
|
|
9
|
+
|
|
10
|
+
* Remove the `activesupport` dependency: `Happn::Configuration` no longer relies on `ActiveSupport::Configurable`, and `Happn::Event` no longer relies on `String#to_datetime` / `Object#try`
|
|
11
|
+
* Relax the `bundler` development dependency from a pinned `1.12.5` to `>=1.17`
|
|
12
|
+
|
|
13
|
+
### [1.0.3] - 2024-12-04
|
|
14
|
+
|
|
15
|
+
* Replace the use of `to_time` to `to_datetime`
|
|
16
|
+
|
|
17
|
+
### [1.0.2] - 2021-11-06
|
|
18
|
+
|
|
19
|
+
* Breaking change : force every `port` to be an integer
|
|
20
|
+
* Add `management_options`
|
|
21
|
+
|
|
22
|
+
### [1.0.0] - 2021-11-05
|
|
23
|
+
|
|
24
|
+
* Add `bunny_options`
|
|
25
|
+
* Add `rabbitmq_management_scheme`
|
|
26
|
+
* MIT License
|
|
27
|
+
|
|
28
|
+
### [0.1.4]
|
|
29
|
+
|
|
30
|
+
* Add convenient method `Event.request_metadata`
|
|
31
|
+
|
|
32
|
+
### [0.1.3]
|
|
33
|
+
|
|
34
|
+
* Use of `rabbitmq_http_api_client >= 2.0.0` and `bunny >= 2.19.0`
|
|
35
|
+
|
|
36
|
+
### [0.1.2]
|
|
37
|
+
|
|
38
|
+
* Use of `rabbitmq_http_api_client:1.14.0`, which supports `faraday >= 1`
|
|
39
|
+
|
|
40
|
+
### [0.1.1]
|
|
41
|
+
|
|
42
|
+
* Happn now raise an exception instead of exiting the process when the consumption of an event fails, allowing the gem user to decide how to handle it.
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2018
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Gem for happn
|
|
2
|
+
|
|
3
|
+
Happn connects a RabbitMQ exchange and listens for CREPE events (possibly generated using `flu-rails`) sequentially.
|
|
4
|
+
Happn helps developers to create _"Projectors"_ that define how to match and consume events.
|
|
5
|
+
|
|
6
|
+
This gem connects a single RabbitMQ queue and bind it automatically to its exchange. These bindings are defined by developers through "matchers" when loading projectors.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
* Ruby 2.2
|
|
11
|
+
* Tested with RabbitMQ 3.5.8
|
|
12
|
+
* `happn-ruby` works with or without Rails (tested with Rails 4 and 5).
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add the gem to your project's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "happn", git: "https://github.com/crepesourcing/happn-ruby.git"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then, configure `Happn`. If you use Rails, you can create an initializer into your Rails app (`config/initializers/happn.rb`). This code can be called anywhere before starting `Happn.init`.
|
|
23
|
+
```ruby
|
|
24
|
+
Happn.configure do |config|
|
|
25
|
+
config.logger = Rails.logger
|
|
26
|
+
config.rabbitmq_host = ENV["RABBITMQ_HOST"]
|
|
27
|
+
config.rabbitmq_port = ENV["RABBITMQ_PORT"]
|
|
28
|
+
config.rabbitmq_management_port = ENV["RABBITMQ_MANAGEMENT_PORT"]
|
|
29
|
+
config.rabbitmq_user = ENV["RABBITMQ_USER"]
|
|
30
|
+
config.rabbitmq_password = ENV["RABBITMQ_PASSWORD"]
|
|
31
|
+
config.rabbitmq_queue_name = ENV["CONSUMER_QUEUE_NAME"]
|
|
32
|
+
config.rabbitmq_exchange_name = ENV["RABBITMQ_EXCHANGE_NAME"]
|
|
33
|
+
config.rabbitmq_exchange_durable = ENV["RABBITMQ_EXCHANGE_DURABLE"] == "true"
|
|
34
|
+
config.projector_classes = [LoggerProjector]
|
|
35
|
+
config.bunny_options = {}
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each configuration is detailed below.
|
|
40
|
+
|
|
41
|
+
## About queues
|
|
42
|
+
|
|
43
|
+
* `Happn` consumes a single queue through the RabbitMQ's [Topic Exchange Model](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-topic).
|
|
44
|
+
* If the queue does not exist when `Happn` starts, it is created automatically.
|
|
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.
|
|
47
|
+
|
|
48
|
+
## About projectors
|
|
49
|
+
|
|
50
|
+
A projector:
|
|
51
|
+
* defines one or multiple matchers to detect which events must be consumed. Matchers can be declared using 4 event properties
|
|
52
|
+
* `emitter`: _e.g._ `Facebook` or `MyInternalApi` (`:all` or no value mean "all emitters")
|
|
53
|
+
* `kind`: _e.g._ `entity_change` or `kind` (`:all` or no value mean "all kinds")
|
|
54
|
+
* `name`: _e.g._ `create country` or `request to destroy bunnies` (`:all` or no value mean "all names")
|
|
55
|
+
* `status`: _e.g._ `:new` or `:replayed` (`:all` or no value mean "all statuses")
|
|
56
|
+
* defines how to consume these events.
|
|
57
|
+
|
|
58
|
+
When a projector raises an Error, `Happn` stops.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Start Up
|
|
63
|
+
|
|
64
|
+
Starting `Happn` consumes events sequentially. For instance, it can be started from a `Rake` tasks:
|
|
65
|
+
```ruby
|
|
66
|
+
namespace :events do
|
|
67
|
+
desc "Listen all events and consume them."
|
|
68
|
+
task consume: :environment do
|
|
69
|
+
Happn.init
|
|
70
|
+
Happn.start
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Define a Projector
|
|
76
|
+
|
|
77
|
+
A projector is a class that defines how to consume one or multiple types of events. This class must:
|
|
78
|
+
|
|
79
|
+
* extend `Happn::Projector`.
|
|
80
|
+
* use its `on` method to declare _which_ events to match and _how_ to consume them. This must be done in a `define_handlers` method.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class LoggerProjector < Happn::Projector
|
|
84
|
+
def define_handlers
|
|
85
|
+
on emitter: "MyApplication", name: "create country" do |event|
|
|
86
|
+
Rails.logger("A country has been created and generated an event with id #{event.id}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
on kind: "request", status: :new do |event|
|
|
90
|
+
Rails.logger("This is a new request to the controller: #{event.data["controller_name"]}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Registering all projectors automatically
|
|
97
|
+
|
|
98
|
+
If all subclass of `Happn::Projector` should be registered seamlessly, the configuration can declare the following code (using Rails):
|
|
99
|
+
```ruby
|
|
100
|
+
Happn.configure do |config|
|
|
101
|
+
Rails.application.eager_load!
|
|
102
|
+
config.projector_classes = Happn::Projector.descendants
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
When Zeitwerk is defined and when `Rails.application.eager_load!` returns `false`, you should call `Zeitwerk::Loader.eager_load_all`.
|
|
107
|
+
|
|
108
|
+
## Overall configuration options
|
|
109
|
+
|
|
110
|
+
All options have a default value. However, all of them can be changed in your `Happn.configure` block.
|
|
111
|
+
|
|
112
|
+
| Option | Default Value | Type | Required? | Description | Example |
|
|
113
|
+
| ---- | ----- | ------ | ----- | ------ | ----- |
|
|
114
|
+
| `logger` | `Logger.new(STDOUT)`| Logger | Optional | The logger used by `happn` | `Rails.logger` |
|
|
115
|
+
| `rabbitmq_host` | `"localhost"` | String | Required | RabbitMQ exchange's host. | `"192.168.42.42"` |
|
|
116
|
+
| `rabbitmq_port` | `5672` | Integer | Required | RabbitMQ exchange's port. | `1234` |
|
|
117
|
+
| `rabbitmq_user` | `""` | String | Required | RabbitMQ exchange's username. | `"root"` |
|
|
118
|
+
| `rabbitmq_password` | `""` | String | Required | RabbitMQ exchange's password. | `"pouet"` |
|
|
119
|
+
| `rabbitmq_exchange_name` | `"events"` | String | Required | RabbitMQ exchange's name. | `"myproject"` |
|
|
120
|
+
| `rabbitmq_management_scheme` | `"http"` | String | Required | RabbitMQ exchange's management scheme. This scheme is used when `happn` must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. | `"https"` |
|
|
121
|
+
| `rabbitmq_management_port` | `15672` | Integer | Required | RabbitMQ exchange's management port. This port is used when `happn` must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. | `15671` |
|
|
122
|
+
| `rabbitmq_queue_name` | `"happn-queue"` | String | Required | The RabbitMQ queue to create, bind and consume. If the queue does not exist, it will be created at startup. | `"my-queue"` |
|
|
123
|
+
| `rabbitmq_exchange_durable` | `true` | Boolean | Optional | Make the RabbitMQ's exchange durable or not. From RabbitMQ's [documentation](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges): _"Durable exchanges survive broker restart whereas transient exchanges do not (they have to be redeclared when broker comes back online)."_ | `false` |
|
|
124
|
+
| `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` |
|
|
125
|
+
| `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` |
|
|
126
|
+
| `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]` |
|
|
127
|
+
| `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 }` |
|
|
128
|
+
| `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 }` |
|
|
129
|
+
| `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) |
|
data/happn.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "happn/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "happn"
|
|
8
|
+
spec.version = Happn::VERSION
|
|
9
|
+
spec.authors = ["Commuty"]
|
|
10
|
+
spec.email = ["support@commuty.net"]
|
|
11
|
+
spec.summary = "Gem to connect a RabbitMQ exchange and listen for events."
|
|
12
|
+
spec.description = "Gem to connect a RabbitMQ exchange and listen for events."
|
|
13
|
+
spec.homepage = "https://gitlab.spin42.me/commuty/happn"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", ">=1.17"
|
|
22
|
+
spec.add_development_dependency "rake", ">=12.0"
|
|
23
|
+
spec.add_development_dependency "rspec", "~>3.0"
|
|
24
|
+
spec.add_dependency "bunny", ">=2.19.0"
|
|
25
|
+
spec.add_dependency "rabbitmq_http_api_client", ">=2.0.0"
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Happn
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :logger,
|
|
4
|
+
:rabbitmq_host,
|
|
5
|
+
:rabbitmq_port,
|
|
6
|
+
:rabbitmq_management_port,
|
|
7
|
+
:rabbitmq_management_scheme,
|
|
8
|
+
:rabbitmq_user,
|
|
9
|
+
:rabbitmq_password,
|
|
10
|
+
:rabbitmq_queue_name,
|
|
11
|
+
:rabbitmq_exchange_name,
|
|
12
|
+
:rabbitmq_exchange_durable,
|
|
13
|
+
:rabbitmq_queue_mode,
|
|
14
|
+
:rabbitmq_prefetch_size,
|
|
15
|
+
:projector_classes,
|
|
16
|
+
:bunny_options,
|
|
17
|
+
:management_options,
|
|
18
|
+
:on_error
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/happn/event.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
|
|
3
|
+
module Happn
|
|
4
|
+
class Event
|
|
5
|
+
|
|
6
|
+
def initialize(args)
|
|
7
|
+
@meta = deep_underscore_keys(args.fetch("meta"))
|
|
8
|
+
@data = deep_underscore_keys(args.fetch("data"))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def data
|
|
12
|
+
@data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def user_metadata
|
|
16
|
+
@data[:user_metadata]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def request_metadata
|
|
20
|
+
@data[:request_metadata]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def changes
|
|
24
|
+
@data[:changes]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def changes=(new_changes)
|
|
28
|
+
@data[:changes] = new_changes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_change(name, value)
|
|
32
|
+
new_value = value == "" ? nil : value
|
|
33
|
+
changes[name.to_sym] = [nil, new_value]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def associations
|
|
37
|
+
@data[:associations]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def timestamp
|
|
41
|
+
DateTime.parse(@meta[:timestamp])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def id
|
|
45
|
+
@meta[:id]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def name
|
|
49
|
+
@meta[:name]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def status
|
|
53
|
+
@meta[:status]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def kind
|
|
57
|
+
@meta[:kind]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def emitter
|
|
61
|
+
@meta[:emitter]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def change_after(attribute_name)
|
|
65
|
+
changes[attribute_name.to_sym]&.last
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def change_before(attribute_name)
|
|
69
|
+
changes[attribute_name.to_sym]&.first
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def has_change?(attribute_name)
|
|
73
|
+
!changes[attribute_name].nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def delete_change(attribute_name)
|
|
77
|
+
changes.delete(attribute_name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def underscore_key(key)
|
|
83
|
+
underscore(key).to_sym
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def deep_underscore_keys(value)
|
|
87
|
+
case value
|
|
88
|
+
when Array
|
|
89
|
+
value.map(&method(:deep_underscore_keys))
|
|
90
|
+
when Hash
|
|
91
|
+
Hash[value.map { |key, value| [underscore_key(key), deep_underscore_keys(value)] }]
|
|
92
|
+
else
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def underscore(camel_cased_word)
|
|
98
|
+
camel_cased_word.gsub(/::/, '/').
|
|
99
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
|
100
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
|
101
|
+
tr("-", "_").
|
|
102
|
+
downcase
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require "bunny"
|
|
2
|
+
require "rabbitmq/http/client"
|
|
3
|
+
|
|
4
|
+
module Happn
|
|
5
|
+
class EventConsumer
|
|
6
|
+
def initialize(logger, configuration, subscription_repository)
|
|
7
|
+
@configuration = configuration
|
|
8
|
+
@logger = logger
|
|
9
|
+
@subscription_repository = subscription_repository
|
|
10
|
+
@queue_name = @configuration.rabbitmq_queue_name
|
|
11
|
+
options = {
|
|
12
|
+
host: @configuration.rabbitmq_host,
|
|
13
|
+
port: @configuration.rabbitmq_port&.to_i,
|
|
14
|
+
user: @configuration.rabbitmq_user,
|
|
15
|
+
password: @configuration.rabbitmq_password,
|
|
16
|
+
automatically_recover: true
|
|
17
|
+
}.merge(@configuration.bunny_options || {})
|
|
18
|
+
@connection = Bunny.new(options)
|
|
19
|
+
|
|
20
|
+
management_options = {
|
|
21
|
+
username: @configuration.rabbitmq_user,
|
|
22
|
+
password: @configuration.rabbitmq_password
|
|
23
|
+
}.merge(@configuration.management_options || {})
|
|
24
|
+
@management_client = RabbitMQ::HTTP::Client.new("#{@configuration.rabbitmq_management_scheme || "http"}://#{@configuration.rabbitmq_host}:#{@configuration.rabbitmq_management_port}/", management_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def wait_until_connected
|
|
28
|
+
connected = false
|
|
29
|
+
while !connected
|
|
30
|
+
begin
|
|
31
|
+
connect
|
|
32
|
+
connected = true
|
|
33
|
+
rescue Bunny::TCPConnectionFailedForAllHosts
|
|
34
|
+
@logger.warn("RabbitMQ connection failed, try again in 1 second.")
|
|
35
|
+
sleep 2
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start
|
|
41
|
+
wait_until_connected
|
|
42
|
+
consume
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def connect
|
|
48
|
+
@connection.start
|
|
49
|
+
channel_number = nil
|
|
50
|
+
consumer_pool_size = 1
|
|
51
|
+
consumer_pool_abort_on_exception = true
|
|
52
|
+
@channel = @connection.create_channel(channel_number, consumer_pool_size, consumer_pool_abort_on_exception)
|
|
53
|
+
@channel.on_uncaught_exception do |exception|
|
|
54
|
+
@configuration.on_error&.call(exception)
|
|
55
|
+
@logger.error("An error occurred. Exiting events consumption.")
|
|
56
|
+
exit(1)
|
|
57
|
+
end
|
|
58
|
+
@channel.basic_qos(@configuration.rabbitmq_prefetch_size)
|
|
59
|
+
arguments = {}
|
|
60
|
+
arguments["x-queue-mode"] = @configuration.rabbitmq_queue_mode unless @configuration.rabbitmq_queue_mode.nil?
|
|
61
|
+
@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)
|
|
65
|
+
|
|
66
|
+
routing_keys = @subscription_repository.find_all.map do | subscription |
|
|
67
|
+
subscription.query.to_routing_key
|
|
68
|
+
end
|
|
69
|
+
routing_keys.uniq.each do | routing_key |
|
|
70
|
+
@logger.info("Bind exchange to queue with routing key : #{routing_key}")
|
|
71
|
+
@queue.bind(exchange, routing_key: routing_key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unbind_useless_routing_keys(@queue, exchange, routing_keys)
|
|
75
|
+
|
|
76
|
+
@logger.info("Ready!")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def consume
|
|
80
|
+
options = {
|
|
81
|
+
manual_ack: true,
|
|
82
|
+
block: true
|
|
83
|
+
}
|
|
84
|
+
@queue.subscribe(options) do | delivery_info, _properties, event |
|
|
85
|
+
begin
|
|
86
|
+
handle_message(event, delivery_info)
|
|
87
|
+
rescue => exception
|
|
88
|
+
handle_exception(exception, delivery_info)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def handle_message(message, delivery_info)
|
|
94
|
+
event = Event.new(JSON.parse(message))
|
|
95
|
+
subscriptions = @subscription_repository.find_subscriptions_for(event)
|
|
96
|
+
|
|
97
|
+
@logger.info("Executing #{subscriptions.size} handlers for event '#{event.name}' with id: #{event.id}.")
|
|
98
|
+
subscriptions.each do | subscription |
|
|
99
|
+
projector = subscription.projector
|
|
100
|
+
handler = subscription.handler
|
|
101
|
+
projector.instance_exec(event, &handler)
|
|
102
|
+
end
|
|
103
|
+
@channel.acknowledge(delivery_info.delivery_tag, false)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_exception(exception, delivery_info)
|
|
107
|
+
@logger.error(exception)
|
|
108
|
+
@channel.reject(delivery_info.delivery_tag, true)
|
|
109
|
+
@logger.fatal("Can't handle event, exit.")
|
|
110
|
+
raise exception
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def unbind_useless_routing_keys(queue, exchange, useful_routing_keys)
|
|
116
|
+
all_routing_keys = find_all_routing_keys_of(queue)
|
|
117
|
+
keys_to_remove = all_routing_keys - useful_routing_keys
|
|
118
|
+
keys_to_remove.each do | routing_key |
|
|
119
|
+
@queue.unbind(exchange, routing_key: routing_key)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def find_all_routing_keys_of(queue)
|
|
124
|
+
@management_client.list_queue_bindings("/", queue.name).map do | binding |
|
|
125
|
+
binding.routing_key
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Happn
|
|
2
|
+
class Projector
|
|
3
|
+
|
|
4
|
+
def initialize(logger, subscription_repository)
|
|
5
|
+
@logger = logger
|
|
6
|
+
@subscription_repository = subscription_repository
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def define_handlers
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def on(emitter: :all, kind: :all, name: :all, status: :all, &block)
|
|
13
|
+
query = Query.new(emitter, kind, name, status)
|
|
14
|
+
@subscription_repository.register(query, self, &block)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/happn/query.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Happn
|
|
2
|
+
class Query
|
|
3
|
+
|
|
4
|
+
attr_reader :emitter, :kind, :name, :status
|
|
5
|
+
|
|
6
|
+
def initialize(emitter, kind, name, status)
|
|
7
|
+
if emitter.to_s.include?(".") || kind.to_s.include?(".") || name.to_s.include?(".") || status.to_s.include?(".")
|
|
8
|
+
raise "'Dot' is not a valid character"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
@emitter = emitter
|
|
12
|
+
@kind = kind
|
|
13
|
+
@name = name
|
|
14
|
+
@status = status
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_routing_key
|
|
18
|
+
"#{to_expression(@status)}.#{to_expression(@emitter)}.#{to_expression(@kind)}.#{to_expression(@name)}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def to_expression(query_expression)
|
|
24
|
+
query_expression == :all ? "*" : query_expression
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Happn
|
|
2
|
+
class SubscriptionRepository
|
|
3
|
+
|
|
4
|
+
def initialize(logger)
|
|
5
|
+
@subscriptions = {}
|
|
6
|
+
@logger = logger
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def register(query, projector, &block)
|
|
10
|
+
subscription = Subscription.new(query, projector, &block)
|
|
11
|
+
emitter = query.emitter.to_s
|
|
12
|
+
kind = query.kind.to_s
|
|
13
|
+
name = query.name.to_s
|
|
14
|
+
status = query.status.to_s
|
|
15
|
+
@subscriptions[status] ||= {}
|
|
16
|
+
@subscriptions[status][emitter] ||= {}
|
|
17
|
+
@subscriptions[status][emitter][kind] ||= {}
|
|
18
|
+
@subscriptions[status][emitter][kind][name] ||= []
|
|
19
|
+
@subscriptions[status][emitter][kind][name].push(subscription)
|
|
20
|
+
@logger.info("Subscribe projector '#{projector.class}' to query : [#{status}][#{emitter}][#{kind}][#{name}]")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_all
|
|
24
|
+
flatten(@subscriptions)
|
|
25
|
+
end
|
|
26
|
+
|
|
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]
|
|
32
|
+
subscriptions = []
|
|
33
|
+
possible_event_statuses.each do | status |
|
|
34
|
+
possible_event_emitters.each do | emitter |
|
|
35
|
+
possible_event_kinds.each do | kind |
|
|
36
|
+
possible_event_names.each do | name |
|
|
37
|
+
subscriptions += @subscriptions.dig(status, emitter, kind, name) || []
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
subscriptions
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def flatten(item)
|
|
48
|
+
if item.instance_of?(Hash)
|
|
49
|
+
item.values.inject([]) do | result, value |
|
|
50
|
+
result += flatten(value)
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
item
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/happn.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative "happn/event"
|
|
2
|
+
require_relative "happn/query"
|
|
3
|
+
require_relative "happn/version"
|
|
4
|
+
require_relative "happn/configuration"
|
|
5
|
+
require_relative "happn/event_consumer"
|
|
6
|
+
require_relative "happn/subscription"
|
|
7
|
+
require_relative "happn/subscription_repository"
|
|
8
|
+
require_relative "happn/projector"
|
|
9
|
+
require "logger"
|
|
10
|
+
|
|
11
|
+
module Happn
|
|
12
|
+
def self.configure
|
|
13
|
+
yield @configuration ||= Happn::Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.config
|
|
17
|
+
@configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.logger
|
|
21
|
+
@logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.init
|
|
25
|
+
@logger = @configuration.logger || Logger.new(STDOUT)
|
|
26
|
+
subscription_repository = SubscriptionRepository.new(@logger)
|
|
27
|
+
Happn::register(@configuration.projector_classes, subscription_repository)
|
|
28
|
+
@event_consumer = EventConsumer.new(@logger, @configuration, subscription_repository)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.start
|
|
32
|
+
@event_consumer.start
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.create_queue_only
|
|
36
|
+
Happn.init
|
|
37
|
+
@event_consumer.wait_until_connected
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
configure do |config|
|
|
41
|
+
config.logger = nil
|
|
42
|
+
config.rabbitmq_host = "localhost"
|
|
43
|
+
config.rabbitmq_port = 5672
|
|
44
|
+
config.rabbitmq_management_scheme = "http"
|
|
45
|
+
config.rabbitmq_management_port = 15672
|
|
46
|
+
config.rabbitmq_user = ""
|
|
47
|
+
config.rabbitmq_password = ""
|
|
48
|
+
config.rabbitmq_queue_name = "happn-queue"
|
|
49
|
+
config.rabbitmq_exchange_name = "events"
|
|
50
|
+
config.rabbitmq_exchange_durable = true
|
|
51
|
+
config.rabbitmq_queue_mode = nil
|
|
52
|
+
config.rabbitmq_prefetch_size = 10
|
|
53
|
+
config.projector_classes = []
|
|
54
|
+
config.on_error = nil
|
|
55
|
+
config.bunny_options = {}
|
|
56
|
+
config.management_options = {}
|
|
57
|
+
config.on_error = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def self.register(projector_classes, subscription_repository)
|
|
63
|
+
@logger.info("#{projector_classes.size} projector are going to be registered...")
|
|
64
|
+
projector_classes.each do | projector_class |
|
|
65
|
+
projector = projector_class.new(@logger, subscription_repository)
|
|
66
|
+
projector.define_handlers
|
|
67
|
+
@logger.info("Projector '#{projector_class}' registered")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: happn
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Commuty
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.17'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.17'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '12.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '12.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: bunny
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 2.19.0
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 2.19.0
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rabbitmq_http_api_client
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 2.0.0
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 2.0.0
|
|
82
|
+
description: Gem to connect a RabbitMQ exchange and listen for events.
|
|
83
|
+
email:
|
|
84
|
+
- support@commuty.net
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- CHANGELOG.md
|
|
90
|
+
- Gemfile
|
|
91
|
+
- MIT-LICENSE
|
|
92
|
+
- README.md
|
|
93
|
+
- happn.gemspec
|
|
94
|
+
- lib/happn.rb
|
|
95
|
+
- lib/happn/configuration.rb
|
|
96
|
+
- lib/happn/event.rb
|
|
97
|
+
- lib/happn/event_consumer.rb
|
|
98
|
+
- lib/happn/projector.rb
|
|
99
|
+
- lib/happn/query.rb
|
|
100
|
+
- lib/happn/subscription.rb
|
|
101
|
+
- lib/happn/subscription_repository.rb
|
|
102
|
+
- lib/happn/version.rb
|
|
103
|
+
homepage: https://gitlab.spin42.me/commuty/happn
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata: {}
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 4.0.17
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: Gem to connect a RabbitMQ exchange and listen for events.
|
|
124
|
+
test_files: []
|