clamour 0.0.3
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/.gitignore +15 -0
- data/.ruby-version +1 -0
- data/EXAMPLE.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +211 -0
- data/Rakefile +9 -0
- data/clamour.gemspec +31 -0
- data/foo_handler.rb +23 -0
- data/lib/clamour.rb +49 -0
- data/lib/clamour/bus.rb +134 -0
- data/lib/clamour/configuration.rb +32 -0
- data/lib/clamour/configuration/base.rb +15 -0
- data/lib/clamour/configuration/rabbit_mq_configuration.rb +9 -0
- data/lib/clamour/handler.rb +52 -0
- data/lib/clamour/message.rb +44 -0
- data/lib/clamour/message/receive.rb +15 -0
- data/lib/clamour/message/sent.rb +5 -0
- data/lib/clamour/railtie.rb +5 -0
- data/lib/clamour/registry.rb +47 -0
- data/lib/clamour/subscription.rb +36 -0
- data/lib/clamour/version.rb +3 -0
- data/lib/tasks/clamour_subscribe.rake +7 -0
- metadata +194 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c538fa93962a76a40adc4ec472568e4031afc62e
|
4
|
+
data.tar.gz: 8baedf6473d5583de04104b7d06c499a1ce91433
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b0e515196e09c2fc221fbdb7f3b7bee9299b3744cbaa68a45f0463ab6b3d469575cf062f90793c0ff6911c19d00c0f02c16d6ddd1c41d21b5f483cdd860ce019
|
7
|
+
data.tar.gz: e3f23238c4be0da7773f3509be978c04f8bc1f5a72c8b07858d336d326302ddaeea2af2d261f903c1a3af709c2e8240c9df19472896aa7f866015e97135894b0
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/EXAMPLE.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# End-to-end example
|
2
|
+
|
3
|
+
The first thing you need to do is to declare a message class, and a handler class.
|
4
|
+
Let's assume the handler writes a file in /tmp based on a message.
|
5
|
+
|
6
|
+
Here is a message, that is sent by an exquisitely sophisticated blog application:
|
7
|
+
|
8
|
+
```
|
9
|
+
require 'clamour'
|
10
|
+
|
11
|
+
module Blog
|
12
|
+
module Post
|
13
|
+
class Added
|
14
|
+
include Clamour::Message
|
15
|
+
|
16
|
+
attribute :name, String
|
17
|
+
attribute :content, String
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
To publish it run in ruby console:
|
24
|
+
|
25
|
+
message = Blog::Post::Added.new(name: 'My first blog post!', content: 'I am proud')
|
26
|
+
message.publish
|
27
|
+
|
28
|
+
Make sure the command does not hang.
|
29
|
+
|
30
|
+
First, make sure the message is received where you expect it to be. Try to receive what is wired through the rabbitmq.
|
31
|
+
Open a terminal, and type:
|
32
|
+
|
33
|
+
require 'clamour'
|
34
|
+
bus = Clamour::Bus.new
|
35
|
+
bus.subscribe do |delivery|
|
36
|
+
puts delivery.inspect
|
37
|
+
end
|
38
|
+
|
39
|
+
Next open another terminal, and publish the message as shown above. You should expect a message json to appear.
|
40
|
+
|
41
|
+
Then open another terminal, and start "subscription", i.e. process of enqueuing handler as a Sidekiq job:
|
42
|
+
|
43
|
+
require 'clamour'
|
44
|
+
Clamour::Subscription.new.perform
|
45
|
+
|
46
|
+
Then open yet another terminal, and run sidekiq there.
|
47
|
+
|
48
|
+
Now, if you publish the message again, you should see the handler as a sidekiq job, and a file in /tmp folder.
|
49
|
+
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sergey Ukustov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
# Clamour
|
2
|
+
|
3
|
+
Fancy messaging library for Ruby. It could, and should be used as a basis for asynchronous systems written in Ruby.
|
4
|
+
It uses [RabbitMQ](http://www.rabbitmq.com/) as a transport mechanism, and [Sidekiq](http://mperham.github.io/sidekiq/)
|
5
|
+
as a substrate to run message handlers.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```
|
12
|
+
gem 'clamour'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install clamour
|
22
|
+
|
23
|
+
## Configuration
|
24
|
+
|
25
|
+
Configuration of the messaging system is contained in an instance of `Clamour::Configuration` class.
|
26
|
+
You could instantiate it using a hash of parameters:
|
27
|
+
|
28
|
+
```
|
29
|
+
configuration = Clamour::Configuration.new(logger: MonoLogger.new(STDERR), enable_connection: false)
|
30
|
+
```
|
31
|
+
|
32
|
+
or you could use accessors:
|
33
|
+
|
34
|
+
```
|
35
|
+
configuration = Clamour::Configuration.new
|
36
|
+
configuration.exchange = 'com.example.exchange'
|
37
|
+
```
|
38
|
+
|
39
|
+
If you intend to use the default Clamour configuration stored in `Clamour.configuration`, you could use a shortcut:
|
40
|
+
|
41
|
+
```
|
42
|
+
Clamour.configure do |config|
|
43
|
+
config.rabbit_mq.host = '127.0.0.1'
|
44
|
+
config.rabbit_mq.user = 'admin'
|
45
|
+
config.rabbit_mq.pass = 'Ad$1n'
|
46
|
+
config.exchange = 'com.example.exchange'
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
You could put configuration code like this in a Rails initializer.
|
51
|
+
|
52
|
+
NB. By default connection to RabbitMQ is disabled when Rails is in test mode.
|
53
|
+
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
### Send a message
|
57
|
+
|
58
|
+
`Clamour::Message` is just a fancy hash serialized into
|
59
|
+
JSON (using fabulous [Oj](https://github.com/ohler55/oj) gem) to be sent over RabbitMQ.
|
60
|
+
A message has a special attribute `_type` which distinguishes different
|
61
|
+
messages, and is added to a hash of message attributes. By default it is set to a snake cased,
|
62
|
+
dot delimited message class name. For example,
|
63
|
+
|
64
|
+
```
|
65
|
+
class Foo::Blah
|
66
|
+
include Clamour::Message
|
67
|
+
attribute :bar, String
|
68
|
+
end
|
69
|
+
foo = Foo::Blah.new(bar: 'baz')
|
70
|
+
Oj.dump(foo, mode: :compat)
|
71
|
+
# => {"_type":"foo.blah","bar":"baz"}
|
72
|
+
```
|
73
|
+
|
74
|
+
To publish a message, call `#publish` on it. By default the method uses global configuration in `Clamour.configuration`.
|
75
|
+
If you want to publish the message to somewhere special, pass an additional parameter to call:
|
76
|
+
|
77
|
+
```
|
78
|
+
foo.publish
|
79
|
+
# is equal to
|
80
|
+
foo.publish(Clamour.configuration)
|
81
|
+
# but a call below is different:
|
82
|
+
foo.publish(white_rabbit_mq_configuration)
|
83
|
+
```
|
84
|
+
|
85
|
+
The method `#publish` here really wraps an original message into a message of class Clamour::Message::Sent.
|
86
|
+
Only latter is really serialized and sent over the wire. So, effectively `foo.publish` would send JSON like this:
|
87
|
+
|
88
|
+
```
|
89
|
+
{"_type":"clamour.message.sent","payload":{"_type":"foo.blah","bar":"baz"}}
|
90
|
+
```
|
91
|
+
|
92
|
+
To set attributes, please, see documentation on [Virtus](https://github.com/solnic/virtus). If you intend to use
|
93
|
+
more complex object as an attribute value than a String, Fixnum, or Boolean, make sure the value can be serialized to JSON.
|
94
|
+
You can check it by doing something like this:
|
95
|
+
```
|
96
|
+
complex_object = ExtraComplexObject.new
|
97
|
+
Oj.dump(complex_object, mode: :compat)
|
98
|
+
```
|
99
|
+
For specific criteria making an object serializable, refer to [Oj](https://github.com/ohler55/oj) documentation.
|
100
|
+
|
101
|
+
### Receive a message
|
102
|
+
|
103
|
+
To decide what action should be run when a message comes, `Clamour::Registry` is used.
|
104
|
+
Effectively it maps message type to an array of handler classes.
|
105
|
+
|
106
|
+
To register a handler for a message one could use method `#on`:
|
107
|
+
|
108
|
+
```
|
109
|
+
Clamour.registry.on Foo::Blah => Foo::Blah::Receive
|
110
|
+
```
|
111
|
+
|
112
|
+
or employ a shortcut for mass-registration:
|
113
|
+
|
114
|
+
```
|
115
|
+
Clamour.registry.change do
|
116
|
+
on Foo::Blah => Foo::Blah::Receive
|
117
|
+
on Rabbit::White::Appeared => Rabbit::White::Follow
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Handler registration could be put inside Rails initializer.
|
122
|
+
|
123
|
+
`Clamour::Bus#subscribe` gets every JSON delivery from RabbitMQ, and sends it to a provided block.
|
124
|
+
Registry is then used to determine what handler to invoke:
|
125
|
+
|
126
|
+
```
|
127
|
+
bus.subscribe do |delivered_hash|
|
128
|
+
message_type = delivered_hash[:_type]
|
129
|
+
registry.route(message_tye) do |handler_class, message_class|
|
130
|
+
# Instantiate handler and pass message
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Do something
|
136
|
+
|
137
|
+
An actual handler must run independently of the subscription process. For this few instruments could be used.
|
138
|
+
Clamour offloads handler running to Sidekiq. The previous code example effectively turns into
|
139
|
+
|
140
|
+
```
|
141
|
+
bus.subscribe do |delivered_hash|
|
142
|
+
message_type = delivered_hash[:_type]
|
143
|
+
registry.route(message_tye) do |handler_class, message_class|
|
144
|
+
handler_class.perform_async(message_class.new(delivered_hash) # Kind of
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
Real code is different, because of fancy fractal structure of the library: a message that you publish really is wrapped
|
150
|
+
inside `Clamour::Message::Sent`, and intercepted later by a handler of class `Clamour::Message::Receive`. The latter
|
151
|
+
routes wrapped message to an actual handler. You do not have to worry about it though.
|
152
|
+
|
153
|
+
A handler is a class that implements method `on_message(message)`, and includes module `Clamour::Handler`. You should expect `message` argument to
|
154
|
+
be an instance of the message class that you set in registry. For a registry
|
155
|
+
|
156
|
+
```
|
157
|
+
Clamour.registry.change do
|
158
|
+
on Messaging::Foo::Done => Messaging::Blah::Do
|
159
|
+
end
|
160
|
+
|
161
|
+
class Messaging::Blah::Do
|
162
|
+
include Clamour::Handler
|
163
|
+
|
164
|
+
# @param [Messaging::Foo::Done] message
|
165
|
+
def on_message(message)
|
166
|
+
# Do Something
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
### And now all together
|
172
|
+
|
173
|
+
If you use the gem inside a Rails application, create an initializer, for example in
|
174
|
+
"config/initializers/clamour.rb" to set up Clamour to accept message `Messaging::Foo::Done` and pass it to a handler
|
175
|
+
`Messaging::Blah::Do`:
|
176
|
+
|
177
|
+
```
|
178
|
+
require 'clamour'
|
179
|
+
|
180
|
+
Clamour.configure do |config|
|
181
|
+
config.exchange = 'special'
|
182
|
+
# And other changes to default configuration
|
183
|
+
end
|
184
|
+
|
185
|
+
Clamour.registry.change do
|
186
|
+
on Messaging::Foo::Done => Messaging::Blah::Do
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
Then you have to start a subsription process. If you use Rails, Sidekiq, and Foreman,
|
191
|
+
all you need to do is to add a line to your Procfile:
|
192
|
+
|
193
|
+
```
|
194
|
+
subscriber: bundle exec rake clamour:subscribe
|
195
|
+
```
|
196
|
+
|
197
|
+
If the technological stack is different, you could figure out what to do just by looking at `clamour:subscribe`
|
198
|
+
rake task source code.
|
199
|
+
|
200
|
+
## TODO
|
201
|
+
|
202
|
+
* More testing.
|
203
|
+
* Scheduled messages: send message in the future.
|
204
|
+
|
205
|
+
## Contributing
|
206
|
+
|
207
|
+
1. Fork it ( https://github.com/provectus/caruso/fork )
|
208
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
209
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
210
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
211
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/clamour.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'clamour/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'clamour'
|
8
|
+
spec.version = Clamour::VERSION
|
9
|
+
spec.authors = ['Sergey Ukustov']
|
10
|
+
spec.email = ['sergey@ukstv.me']
|
11
|
+
spec.summary = %q{Fancy messaging library}
|
12
|
+
spec.description = %q{Fancy messaging library for Ruby}
|
13
|
+
spec.homepage = ''
|
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_runtime_dependency 'amqp'
|
22
|
+
spec.add_runtime_dependency 'activesupport'
|
23
|
+
spec.add_runtime_dependency 'oj'
|
24
|
+
spec.add_runtime_dependency 'virtus'
|
25
|
+
spec.add_runtime_dependency 'mono_logger'
|
26
|
+
spec.add_runtime_dependency 'sidekiq'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'yard'
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
end
|
data/foo_handler.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'clamour'
|
2
|
+
|
3
|
+
class Foo
|
4
|
+
include Clamour::Message
|
5
|
+
|
6
|
+
attribute :blah, String
|
7
|
+
end
|
8
|
+
|
9
|
+
f = Foo.new(blah: 'yaa')
|
10
|
+
f.publish
|
11
|
+
|
12
|
+
class FooHandler
|
13
|
+
include Clamour::Handler
|
14
|
+
|
15
|
+
# @param [Foo] foo
|
16
|
+
def on_message(foo)
|
17
|
+
File.open('/tmp/a.txt', 'w') do |f|
|
18
|
+
f.puts Time.now.to_s
|
19
|
+
f.puts foo.blah
|
20
|
+
f.puts '----' * 10
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/clamour.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'clamour/version'
|
2
|
+
require 'active_support/dependencies/autoload'
|
3
|
+
|
4
|
+
module Clamour
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Bus
|
8
|
+
autoload :Configuration
|
9
|
+
autoload :Handler
|
10
|
+
autoload :Message
|
11
|
+
autoload :Registry
|
12
|
+
autoload :Message
|
13
|
+
autoload :Subscription
|
14
|
+
|
15
|
+
# Clamour-wide configuration.
|
16
|
+
#
|
17
|
+
# @return [Clamour::Configuration]
|
18
|
+
def self.configuration
|
19
|
+
@configuration ||= Clamour::Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Shortcut for Clamour-wide configuration.
|
23
|
+
# @yield [Clamour::Configuration]
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# Clamour.configure do |config|
|
27
|
+
# config.exchange = 'com.example.exchange'
|
28
|
+
# config.logger = MonoLogger.new(STDOUT)
|
29
|
+
# end
|
30
|
+
# @see Clamour::Configuration
|
31
|
+
def self.configure(&block)
|
32
|
+
block.call(configuration) if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Clamour-wide message handlers registry.
|
36
|
+
#
|
37
|
+
# @example To add handlers
|
38
|
+
# Clamour.registry.change do
|
39
|
+
# on Social::User::New => Social::User::Greeting::Send
|
40
|
+
# end
|
41
|
+
# @see Clamour::Registry
|
42
|
+
def self.registry
|
43
|
+
@registry ||= Clamour::Registry.new do
|
44
|
+
on Clamour::Message::Sent => Clamour::Message::Receive
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
require 'clamour/railtie' if defined?(Rails)
|
data/lib/clamour/bus.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
class Clamour::Bus
|
7
|
+
|
8
|
+
class WrongContentTypeError < RuntimeError
|
9
|
+
# If not JSON.
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Clamour::Configuration]
|
13
|
+
attr_reader :configuration
|
14
|
+
|
15
|
+
# @return [Hash]
|
16
|
+
attr_reader :connection_settings
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :exchange_name
|
20
|
+
|
21
|
+
# @return [Logger]
|
22
|
+
attr_reader :logger
|
23
|
+
|
24
|
+
# @param [Clamour::Configuration] configuration
|
25
|
+
def initialize(configuration = Clamour.configuration)
|
26
|
+
@configuration = configuration
|
27
|
+
@connection_settings = configuration.rabbit_mq.to_hash
|
28
|
+
@exchange_name = configuration.exchange
|
29
|
+
@logger = configuration.logger
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [Clamour::Message] message
|
33
|
+
def publish(message)
|
34
|
+
if EM.reactor_running?
|
35
|
+
em_publish(message)
|
36
|
+
else
|
37
|
+
EM.run do
|
38
|
+
em_publish(message) do
|
39
|
+
EM.stop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def subscribe(&block)
|
46
|
+
if EM.reactor_running?
|
47
|
+
em_subscribe(&block)
|
48
|
+
else
|
49
|
+
EM.run do
|
50
|
+
em_subscribe(&block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Clamour::Message] message
|
56
|
+
def em_publish(message, &block)
|
57
|
+
logger.debug "Message #{message.inspect} is going to be published"
|
58
|
+
if configuration.enable_connection?
|
59
|
+
AMQP.connect(connection_settings) do |connection|
|
60
|
+
AMQP::Channel.new(connection) do |channel|
|
61
|
+
channel.fanout(exchange_name, durable: true) do |exchange|
|
62
|
+
options = { content_type: 'application/json' }
|
63
|
+
exchange.publish(dump_json(message), options) do
|
64
|
+
logger.debug "Message #{message.inspect} is published to #{exchange_name}"
|
65
|
+
connection.disconnect do
|
66
|
+
block.call if block_given?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
logger.debug "Connection is disabled. Message #{message.inspect} is not really published"
|
74
|
+
block.call if block_given?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def em_subscribe(&block)
|
79
|
+
raise ArgumentError.new('You have to provide a block') unless block_given?
|
80
|
+
|
81
|
+
if configuration.enable_connection?
|
82
|
+
AMQP.connect(connection_settings) do |connection|
|
83
|
+
before_shutdown do
|
84
|
+
connection.close do
|
85
|
+
EM.stop
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
AMQP::Channel.new(connection) do |channel|
|
90
|
+
channel.fanout(exchange_name, durable: true) do |exchange|
|
91
|
+
EM.schedule do
|
92
|
+
channel.queue('', exclusive: true) do |queue|
|
93
|
+
queue.bind(exchange).subscribe do |header, delivery|
|
94
|
+
message_hash =
|
95
|
+
case header.content_type
|
96
|
+
when 'application/json'
|
97
|
+
ActiveSupport::HashWithIndifferentAccess.new(load_json(delivery))
|
98
|
+
else
|
99
|
+
raise WrongContentTypeError.new("Got #{delivery.inspect} for content type #{header.content_type}")
|
100
|
+
end
|
101
|
+
logger.debug "Got hash #{message_hash}"
|
102
|
+
block.call(message_hash)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
else
|
110
|
+
logger.info 'Connection is disabled. Doing nothing...'
|
111
|
+
before_shutdown { EM.stop }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Do something gentle on SIGINT
|
116
|
+
def before_shutdown(&block)
|
117
|
+
Signal.trap('INT') do
|
118
|
+
logger.info 'Shutting down on SIGINT...'
|
119
|
+
block.call if block_given?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param [Clamour::Message] message
|
124
|
+
# @return [String]
|
125
|
+
def dump_json(message)
|
126
|
+
Oj.dump(message, mode: :compat)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @param [String] json
|
130
|
+
# @return [Hash]
|
131
|
+
def load_json(json)
|
132
|
+
Oj.load(json)
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'clamour/version'
|
2
|
+
require 'active_support/dependencies/autoload'
|
3
|
+
require 'mono_logger'
|
4
|
+
|
5
|
+
class Clamour::Configuration
|
6
|
+
extend ActiveSupport::Autoload
|
7
|
+
|
8
|
+
autoload :Base
|
9
|
+
autoload :RabbitMqConfiguration
|
10
|
+
|
11
|
+
include Clamour::Configuration::Base
|
12
|
+
|
13
|
+
attribute :rabbit_mq, RabbitMqConfiguration, default: RabbitMqConfiguration.new
|
14
|
+
attribute :exchange, String, default: 'clamour.exchange'
|
15
|
+
attribute :logger, Logger, default: :default_logger
|
16
|
+
attribute :enable_connection, Boolean, default: :default_enable_connection
|
17
|
+
|
18
|
+
# @return [Logger]
|
19
|
+
def default_logger
|
20
|
+
MonoLogger.new(STDERR)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean]
|
24
|
+
def default_enable_connection
|
25
|
+
!(defined?(Rails) && Rails.env.test?)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean]
|
29
|
+
def enable_connection?
|
30
|
+
enable_connection
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Clamour::Configuration::Base
|
4
|
+
include Virtus.module
|
5
|
+
|
6
|
+
# @return [Boolean]
|
7
|
+
def eql?(other)
|
8
|
+
other.respond_to?(:attributes) && attributes.eql?(other.attributes)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Fixnum]
|
12
|
+
def hash
|
13
|
+
attributes.hash
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Clamour::Configuration::RabbitMqConfiguration
|
2
|
+
include Clamour::Configuration::Base
|
3
|
+
|
4
|
+
attribute :host, String, default: 'localhost'
|
5
|
+
attribute :port, Fixnum, default: 5672
|
6
|
+
attribute :user, String, default: 'guest'
|
7
|
+
attribute :pass, String, default: 'guest'
|
8
|
+
attribute :vhost, String, default: '/'
|
9
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
require 'active_support/core_ext/string'
|
6
|
+
|
7
|
+
module Clamour::Handler
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
include Sidekiq::Worker
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Special thing for internal purposes only. Just implement +on_message(message)+ method.
|
17
|
+
#
|
18
|
+
# @param [Clamour::Message] message
|
19
|
+
# @param [Clamour::Subscription] subscription
|
20
|
+
def perform(message, subscription)
|
21
|
+
perform_async(message.class.to_s, message.to_hash)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Like a usual Sidekiq job.
|
26
|
+
#
|
27
|
+
# @param [String] message_class_name
|
28
|
+
# @param [Hash] message_attributes
|
29
|
+
def perform(message_class_name, message_attributes)
|
30
|
+
message = restore_message(message_class_name, message_attributes)
|
31
|
+
on_message(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @abstract You must use +on_message+ method to act on a message.
|
35
|
+
# @param [Clamour::Message] message
|
36
|
+
def on_message(message)
|
37
|
+
raise NotImplementedError.new('You must override "on_message" method to act on a message')
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
# Deserialize message of Sidekiq-passed parameters.
|
43
|
+
#
|
44
|
+
# @param [String] message_class_name
|
45
|
+
# @param [Hash] message_attributes
|
46
|
+
# @return [Clamour::Message]
|
47
|
+
def restore_message(message_class_name, message_attributes)
|
48
|
+
message_class = message_class_name.constantize
|
49
|
+
message_class.new(message_attributes)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'active_support/dependencies/autoload'
|
4
|
+
require 'virtus'
|
5
|
+
|
6
|
+
# What is sent over the wire.
|
7
|
+
module Clamour::Message
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
include Virtus.module
|
11
|
+
|
12
|
+
autoload :Sent
|
13
|
+
autoload :Receive
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Voluntarily set message type.
|
17
|
+
#
|
18
|
+
# @param [String] new_value
|
19
|
+
# @example
|
20
|
+
# class Parcel < Clamour::Message
|
21
|
+
# of_type 'snail.mail'
|
22
|
+
# end
|
23
|
+
def of_type(new_value = nil)
|
24
|
+
@type = new_value.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
# Message type. By default it is snake cased class name.
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
def type
|
31
|
+
@type ||= ActiveSupport::Inflector.underscore(to_s).gsub('/', '.')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# It is highly unlikely someone would use `_type` as a name, so we use it to store service information,
|
36
|
+
# namely message type.
|
37
|
+
attribute :_type, String, default: ->(message, _) { message.class.type }
|
38
|
+
|
39
|
+
def publish(configuration = Clamour.configuration)
|
40
|
+
bus = Clamour::Bus.new(configuration)
|
41
|
+
sent_message = Clamour::Message::Sent.new(payload: self)
|
42
|
+
bus.publish(sent_message)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
require 'active_support/hash_with_indifferent_access'
|
3
|
+
|
4
|
+
# Unpack message and reroute it using the same {Clamour::Subscription}.
|
5
|
+
class Clamour::Message::Receive
|
6
|
+
include Clamour::Handler
|
7
|
+
|
8
|
+
# @param [Clamour::Message::Sent] wired
|
9
|
+
# @param [Clamour::Subscription] subscription
|
10
|
+
def self.perform(wired, subscription)
|
11
|
+
attributes = ActiveSupport::HashWithIndifferentAccess.new(wired.payload)
|
12
|
+
type = attributes[:_type]
|
13
|
+
subscription.route(type, attributes)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
require 'active_support/core_ext/array'
|
3
|
+
|
4
|
+
class Clamour::Registry
|
5
|
+
|
6
|
+
# @return [Hash<Set<Class>>] store handler classes
|
7
|
+
attr_reader :handlers
|
8
|
+
|
9
|
+
# @return [Hash]
|
10
|
+
attr_reader :message_classes
|
11
|
+
|
12
|
+
def initialize(&block)
|
13
|
+
@handlers = Hash.new { |mapping, type| mapping[type] = Set.new }
|
14
|
+
@message_classes = Hash.new
|
15
|
+
change(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def route(type, &block)
|
19
|
+
raise ArgumentError.new('Something has to be routed') if type.blank?
|
20
|
+
message_class = message_classes[type]
|
21
|
+
found_handlers = handlers[type]
|
22
|
+
if message_class.present? && found_handlers.present?
|
23
|
+
found_handlers.each do |handler|
|
24
|
+
block.call(handler, message_class)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
puts "Could not find message class or handler for #{type}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def change(&block)
|
32
|
+
instance_eval(&block)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def on(mappings = {})
|
37
|
+
mappings.each do |message_class, handlers_list|
|
38
|
+
raise ArgumentError.new("#{message_class} must include Clamour::Message") unless message_class < Clamour::Message
|
39
|
+
Array.wrap(handlers_list).flatten.compact.each do |handler|
|
40
|
+
raise ArgumentError.new("Handler #{handler} must be a class") unless handler.is_a?(Class)
|
41
|
+
message_type = message_class.type
|
42
|
+
message_classes[message_type] = message_class
|
43
|
+
handlers[message_type].add(handler)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Clamour::Subscription
|
2
|
+
# @return [Clamour::Configuration]
|
3
|
+
attr_reader :configuration
|
4
|
+
|
5
|
+
# @return [Logger]
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
# @return [Clamour::Registry]
|
9
|
+
attr_reader :registry
|
10
|
+
|
11
|
+
# @param [Clamour::Configuration] configuration
|
12
|
+
def initialize(configuration = Clamour.configuration, registry = Clamour.registry)
|
13
|
+
@configuration = configuration
|
14
|
+
@logger = configuration.logger
|
15
|
+
@registry = registry
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform
|
19
|
+
bus.subscribe do |received_hash|
|
20
|
+
type = received_hash[:_type]
|
21
|
+
route(type, received_hash)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [String] type
|
26
|
+
# @param [Hash] attributes
|
27
|
+
def route(type, attributes)
|
28
|
+
registry.route(type) do |handler_class, message_class|
|
29
|
+
handler_class.perform(message_class.new(attributes), self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def bus
|
34
|
+
@bus ||= Clamour::Bus.new(configuration)
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clamour
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sergey Ukustov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: amqp
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: oj
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: virtus
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mono_logger
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sidekiq
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: bundler
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.7'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.7'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '10.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '10.0'
|
139
|
+
description: Fancy messaging library for Ruby
|
140
|
+
email:
|
141
|
+
- sergey@ukstv.me
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".ruby-version"
|
148
|
+
- EXAMPLE.md
|
149
|
+
- Gemfile
|
150
|
+
- LICENSE.txt
|
151
|
+
- README.md
|
152
|
+
- Rakefile
|
153
|
+
- clamour.gemspec
|
154
|
+
- foo_handler.rb
|
155
|
+
- lib/clamour.rb
|
156
|
+
- lib/clamour/bus.rb
|
157
|
+
- lib/clamour/configuration.rb
|
158
|
+
- lib/clamour/configuration/base.rb
|
159
|
+
- lib/clamour/configuration/rabbit_mq_configuration.rb
|
160
|
+
- lib/clamour/handler.rb
|
161
|
+
- lib/clamour/message.rb
|
162
|
+
- lib/clamour/message/receive.rb
|
163
|
+
- lib/clamour/message/sent.rb
|
164
|
+
- lib/clamour/railtie.rb
|
165
|
+
- lib/clamour/registry.rb
|
166
|
+
- lib/clamour/subscription.rb
|
167
|
+
- lib/clamour/version.rb
|
168
|
+
- lib/tasks/clamour_subscribe.rake
|
169
|
+
homepage: ''
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.2.2
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: Fancy messaging library
|
193
|
+
test_files: []
|
194
|
+
has_rdoc:
|