msgr 0.0.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/.gitignore +23 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +42 -0
- data/Rakefile +17 -0
- data/gemfiles/Gemfile.rails-3-2 +6 -0
- data/gemfiles/Gemfile.rails-4-0 +6 -0
- data/lib/msgr.rb +36 -0
- data/lib/msgr/binding.rb +34 -0
- data/lib/msgr/client.rb +82 -0
- data/lib/msgr/connection.rb +78 -0
- data/lib/msgr/dispatcher.rb +26 -0
- data/lib/msgr/errors.rb +21 -0
- data/lib/msgr/logging.rb +12 -0
- data/lib/msgr/message.rb +18 -0
- data/lib/msgr/pool.rb +165 -0
- data/lib/msgr/route.rb +26 -0
- data/lib/msgr/routes.rb +18 -0
- data/lib/msgr/version.rb +11 -0
- data/msgr.gemspec +29 -0
- data/scripts/simple_test.rb +114 -0
- data/spec/msgr/client_spec.rb +5 -0
- data/spec/msgr/consumer_spec.rb +5 -0
- data/spec/msgr/route_spec.rb +63 -0
- data/spec/msgr/routes_spec.rb +59 -0
- data/spec/msgr_spec.rb +5 -0
- data/spec/spec_helper.rb +18 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5a4c0e2fbb4ebe739cd8441da5136c32974e33e3
|
4
|
+
data.tar.gz: 8cd8950010d87d4c3a4c90fa26a2f4544bfd1908
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b8268da9f156fa7be5b4da0768e766d90242d60186aa7a79d9d229270ac5957bf0e498c5edbb963bc8d0abb759e91545536e23444ff4ce6ab25bc8931518c96
|
7
|
+
data.tar.gz: 5af9d36176f5b99c4249c89a2819d4ab3682c90320d44a0c219b077ed2d3d4f7d9f6dd6e0899452d7d1c3b390ea89a2fdf53f9f2e41c91cb9cd58b55c24598db
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
.idea
|
19
|
+
pmip
|
20
|
+
*.iml
|
21
|
+
.rbx
|
22
|
+
.rspec
|
23
|
+
log/*.log
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jan Graichen
|
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,42 @@
|
|
1
|
+
# Msgr - *A Rails-like Messaging Framework*
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/msgr)
|
4
|
+
[](https://travis-ci.org/jgraichen/msgr)
|
5
|
+
[](https://coveralls.io/r/jgraichen/msgr)
|
6
|
+
[](https://codeclimate.com/github/jgraichen/msgr)
|
7
|
+
[](https://gemnasium.com/jgraichen/msgr)
|
8
|
+
[](http://rubydoc.info/github/jgraichen/msgr/master/frames)
|
9
|
+
|
10
|
+
You know it and ou like it. Using Rails you can just declare your routes and
|
11
|
+
create a controller. Now it just works.
|
12
|
+
|
13
|
+
With *Msgr* you can do the same for asynchronous AMQP messaging. Just define
|
14
|
+
your routes, create your consumer and watch you app processing messages.
|
15
|
+
|
16
|
+
*Note: Msgr is still under heavy development.*
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
gem 'msgr'
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install msgr
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
TODO: Write usage instructions here
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
1. Fork it
|
39
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
40
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
41
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
42
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rake'
|
8
|
+
require 'bundler/gem_tasks'
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
|
11
|
+
task 'default' => 'ci'
|
12
|
+
task 'ci' => 'spec'
|
13
|
+
|
14
|
+
desc 'Run all specs'
|
15
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
16
|
+
t.pattern = 'spec/msgr/**/*_spec.rb'
|
17
|
+
end
|
data/lib/msgr.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'msgr/version'
|
2
|
+
require 'celluloid'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
|
8
|
+
require 'msgr/logging'
|
9
|
+
require 'msgr/binding'
|
10
|
+
require 'msgr/client'
|
11
|
+
require 'msgr/connection'
|
12
|
+
require 'msgr/dispatcher'
|
13
|
+
require 'msgr/errors'
|
14
|
+
require 'msgr/message'
|
15
|
+
require 'msgr/pool'
|
16
|
+
require 'msgr/route'
|
17
|
+
require 'msgr/routes'
|
18
|
+
|
19
|
+
module Msgr
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def logger
|
23
|
+
@logger ||= Logger.new($stdout).tap do |logger|
|
24
|
+
logger.level = Logger::Severity::INFO
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
# stub
|
30
|
+
end
|
31
|
+
|
32
|
+
def publish
|
33
|
+
# stub
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/msgr/binding.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Msgr
|
2
|
+
# A single binding
|
3
|
+
class Binding
|
4
|
+
attr_reader :connection, :route, :subscription, :dispatcher
|
5
|
+
|
6
|
+
def initialize(connection, route, dispatcher)
|
7
|
+
@connection = connection
|
8
|
+
@route = route
|
9
|
+
@dispatcher = dispatcher
|
10
|
+
|
11
|
+
queue = connection.queue route.name
|
12
|
+
|
13
|
+
queue.bind connection.exchange, routing_key: route.key
|
14
|
+
|
15
|
+
@subscription = queue.subscribe ack: true, &method(:call)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Called from Bunny Thread Pool. Will create message object from
|
19
|
+
# provided bunny data and dispatch message to connection.
|
20
|
+
#
|
21
|
+
def call(info, metadata, payload)
|
22
|
+
message = Message.new(connection, info, metadata, payload, route)
|
23
|
+
dispatcher.dispatch :call, message
|
24
|
+
rescue => error
|
25
|
+
Msgr.logger.warn(self) { "Error received within bunny subscribe handler: #{error.inspect}." }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Cancel subscription to not receive any more messages.
|
29
|
+
#
|
30
|
+
def release
|
31
|
+
subscription.cancel if subscription
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/msgr/client.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Msgr
|
4
|
+
|
5
|
+
class Client
|
6
|
+
include Celluloid
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
attr_reader :pool, :uri
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@uri = URI.parse config[:uri] ? config.delete(:uri) : 'amqp://localhost/'
|
13
|
+
@uri.protocol = 'amqps' if config[:secure]
|
14
|
+
@uri.user = config.delete :user if config[:user]
|
15
|
+
@uri.password = config.delete :password if config[:password]
|
16
|
+
@uri.host = config.delete :host if config[:host]
|
17
|
+
@uri.port = config.delete(:port).to_i if config[:port]
|
18
|
+
@uri.path = "/#{config.delete :vhost}".gsub /\/+/, '/' if config[:vhost]
|
19
|
+
|
20
|
+
@config = config
|
21
|
+
@bunny = Bunny.new @uri.to_s
|
22
|
+
@pool = Pool.new Dispatcher, autostart: false
|
23
|
+
|
24
|
+
@uri.password = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def running?; @running end
|
28
|
+
def log_name; self.class.name end
|
29
|
+
|
30
|
+
def routes
|
31
|
+
@routes ||= Routes.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def reload
|
35
|
+
raise StandardError.new 'Client not running.' unless running?
|
36
|
+
log(:info) { 'Reload client.' }
|
37
|
+
|
38
|
+
@connection.release
|
39
|
+
@connection.terminate
|
40
|
+
|
41
|
+
log(:debug) { 'Create new connection.' }
|
42
|
+
|
43
|
+
@connection = Connection.new @bunny, routes, pool
|
44
|
+
|
45
|
+
log(:info) { 'Client reloaded.' }
|
46
|
+
end
|
47
|
+
|
48
|
+
def start
|
49
|
+
log(:info) { "Start client to #{uri}" }
|
50
|
+
|
51
|
+
@bunny.start
|
52
|
+
@pool.start
|
53
|
+
|
54
|
+
@running = true
|
55
|
+
@connection = Connection.new @bunny, routes, pool
|
56
|
+
|
57
|
+
log(:info) { "Client started. pool: #{pool.size}" }
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
return unless running?
|
62
|
+
|
63
|
+
@running = false
|
64
|
+
log(:info) { 'Graceful shutdown client...' }
|
65
|
+
|
66
|
+
@connection.release
|
67
|
+
@pool.stop
|
68
|
+
|
69
|
+
log(:debug) { 'Terminating...' }
|
70
|
+
|
71
|
+
@connection.terminate
|
72
|
+
@pool.terminate
|
73
|
+
@bunny.stop
|
74
|
+
|
75
|
+
log(:info) { 'Terminated.' }
|
76
|
+
end
|
77
|
+
|
78
|
+
def publish(routing_key, payload)
|
79
|
+
@connection.publish payload, routing_key: routing_key
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
class Connection
|
4
|
+
include Celluloid
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
attr_reader :conn, :pool, :routes
|
8
|
+
finalizer :close
|
9
|
+
|
10
|
+
def initialize(conn, routes, pool)
|
11
|
+
@conn = conn
|
12
|
+
@pool = pool
|
13
|
+
@routes = routes
|
14
|
+
|
15
|
+
@channel = conn.create_channel
|
16
|
+
@channel.prefetch(10)
|
17
|
+
|
18
|
+
rebind
|
19
|
+
end
|
20
|
+
|
21
|
+
def rebind(routes = nil)
|
22
|
+
routes = self.routes unless routes
|
23
|
+
|
24
|
+
# First release old bindings
|
25
|
+
release
|
26
|
+
|
27
|
+
# Create new bindings
|
28
|
+
routes.each { |route| bindings << Binding.new(Actor.current, route, pool) }
|
29
|
+
|
30
|
+
log(:debug) { 'New routes bound.' }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Used to store al bindings. Allows use to
|
34
|
+
# release bindings when receiver should not longer
|
35
|
+
# receive messages but channel need to be open
|
36
|
+
# to allow further acknowledgments.
|
37
|
+
#
|
38
|
+
def bindings
|
39
|
+
@bindings ||= []
|
40
|
+
end
|
41
|
+
|
42
|
+
def queue(name)
|
43
|
+
@channel.queue name, durable: true
|
44
|
+
end
|
45
|
+
|
46
|
+
def exchange
|
47
|
+
@exchange ||= @channel.topic 'msgr', durable: true
|
48
|
+
end
|
49
|
+
|
50
|
+
# Release all bindings but do not close channel. Will not
|
51
|
+
# longer receive any message but channel can be used to
|
52
|
+
# acknowledge currently processing messages.
|
53
|
+
#
|
54
|
+
def release
|
55
|
+
return unless bindings.any?
|
56
|
+
|
57
|
+
log(:debug) { 'Release all bindings.' }
|
58
|
+
|
59
|
+
bindings.each { |binding| binding.release }
|
60
|
+
bindings.clear
|
61
|
+
end
|
62
|
+
|
63
|
+
def publish(payload, opts = {})
|
64
|
+
log(:debug) { "Publish message to #{opts[:routing_key]}" }
|
65
|
+
|
66
|
+
exchange.publish payload, opts.merge(persistent: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ack(delivery_tag)
|
70
|
+
@channel.ack delivery_tag
|
71
|
+
end
|
72
|
+
|
73
|
+
def close
|
74
|
+
@channel.close if @channel.open?
|
75
|
+
log(:debug) { 'Connection closed.' }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
# The Dispatcher receives incoming messages,
|
4
|
+
# process them through a middleware stack and
|
5
|
+
# delegate them to a new and fresh consumer instance.
|
6
|
+
#
|
7
|
+
class Dispatcher
|
8
|
+
include Logging
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(message)
|
15
|
+
log(:debug) { "Receive dispatched message: #{message.payload}" }
|
16
|
+
|
17
|
+
message.ack
|
18
|
+
|
19
|
+
log(:debug) { 'Dispatched message acknowledged.' }
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
self.class.name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/msgr/errors.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
# Abstract error base class
|
4
|
+
class CausedByError < StandardError
|
5
|
+
attr_accessor :cause
|
6
|
+
|
7
|
+
def initialize(*args)
|
8
|
+
opts = args.extract_options!
|
9
|
+
@cause = opts.delete(:cause)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
cause ? "#{super}\n caused by:\n#{cause.to_s}" : super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ConnectionError < CausedByError
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/msgr/logging.rb
ADDED
data/lib/msgr/message.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
class Message
|
4
|
+
attr_reader :delivery_info, :metadata, :payload
|
5
|
+
|
6
|
+
def initialize(connection, delivery_info, metadata, payload, route)
|
7
|
+
@connection = connection
|
8
|
+
@delivery_info = delivery_info
|
9
|
+
@metadata = metadata
|
10
|
+
@payload = payload
|
11
|
+
@route = route
|
12
|
+
end
|
13
|
+
|
14
|
+
def ack
|
15
|
+
@connection.ack delivery_info.delivery_tag
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/msgr/pool.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
class Pool
|
4
|
+
include Celluloid
|
5
|
+
include Logging
|
6
|
+
attr_reader :size
|
7
|
+
|
8
|
+
def initialize(runner_klass, opts = {})
|
9
|
+
@runner_klass = runner_klass
|
10
|
+
@runner_args = opts[:args] ? Array(opts[:args]) : []
|
11
|
+
@size = (opts[:size] || Celluloid.cores).to_i
|
12
|
+
@running = false
|
13
|
+
|
14
|
+
log(:debug) { "Inialize size => #{@size}" }
|
15
|
+
|
16
|
+
start if opts[:autostart].nil? || opts[:autostart]
|
17
|
+
every(30) { log_status } if opts[:nostats].nil? || opts[:nostats]
|
18
|
+
end
|
19
|
+
|
20
|
+
def running?
|
21
|
+
@running
|
22
|
+
end
|
23
|
+
|
24
|
+
def idle; @idle ||= [] end
|
25
|
+
def busy; @busy ||= [] end
|
26
|
+
|
27
|
+
def start
|
28
|
+
return if running?
|
29
|
+
|
30
|
+
log(:debug) { 'Spin up worker pool' }
|
31
|
+
@running = true
|
32
|
+
|
33
|
+
idle.clear
|
34
|
+
busy.clear
|
35
|
+
|
36
|
+
@size.times.map do |index|
|
37
|
+
idle << Worker.new_link(Actor.current, index, @runner_klass, @runner_args)
|
38
|
+
end
|
39
|
+
|
40
|
+
log(:debug) { 'Startup done. Invoke worker polling.' }
|
41
|
+
|
42
|
+
idle.each { |worker| async.poll worker }
|
43
|
+
end
|
44
|
+
|
45
|
+
def log_status
|
46
|
+
log(:info) { "[STATUS] Idle: #{idle.size} Busy: #{busy.size}" }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Request a graceful shutdown of all pool workers.
|
50
|
+
#
|
51
|
+
def stop
|
52
|
+
log(:debug) { 'Graceful shutdown requested.' }
|
53
|
+
|
54
|
+
@running = false
|
55
|
+
idle.each { |worker| worker.terminate }
|
56
|
+
idle.clear
|
57
|
+
|
58
|
+
if busy.any?
|
59
|
+
log(:debug) { "Wait for #{busy.size} workers to terminate." }
|
60
|
+
|
61
|
+
wait :shutdown
|
62
|
+
end
|
63
|
+
|
64
|
+
log(:debug) { 'Graceful shutdown done.' }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if a worker is available.
|
68
|
+
#
|
69
|
+
# @return [Boolean] True if at least on idle worker is available, false otherwise.
|
70
|
+
#
|
71
|
+
def available?
|
72
|
+
idle.any?
|
73
|
+
end
|
74
|
+
|
75
|
+
def messages
|
76
|
+
@message ||= []
|
77
|
+
end
|
78
|
+
|
79
|
+
# Dispatch given message to a worker.
|
80
|
+
#
|
81
|
+
def dispatch(message, *args)
|
82
|
+
messages.push [message, args]
|
83
|
+
after(0) { signal :dispatch }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Called by worker to indicated it has finished processing.
|
87
|
+
#
|
88
|
+
# @param [Pool::Worker] worker Worker that finished processing.
|
89
|
+
#
|
90
|
+
def executed(worker)
|
91
|
+
busy.delete worker
|
92
|
+
|
93
|
+
if running?
|
94
|
+
idle << worker
|
95
|
+
poll worker
|
96
|
+
else
|
97
|
+
log(:debug) { "Terminate worker. Still #{busy.size} to go..." }
|
98
|
+
|
99
|
+
worker.terminate if worker.alive?
|
100
|
+
if busy.empty?
|
101
|
+
log(:debug) { 'All worker down. Signal :shutdown.' }
|
102
|
+
after(0) { signal :shutdown }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def poll(worker)
|
108
|
+
return unless worker.alive?
|
109
|
+
|
110
|
+
if running?
|
111
|
+
if (message = exclusive { messages.shift })
|
112
|
+
idle.delete worker
|
113
|
+
busy << worker
|
114
|
+
|
115
|
+
worker.dispatch message[0], message[1]
|
116
|
+
else
|
117
|
+
after(1) { poll worker }
|
118
|
+
end
|
119
|
+
else
|
120
|
+
worker.terminate if worker.alive?
|
121
|
+
after(0) { signal(:shutdown) } if @busy.empty?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_s
|
126
|
+
"#{self.class.name}[#{@runner_klass}]<#{object_id}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
# Worker actor capsuling worker logic and dispatching
|
130
|
+
# tasks to custom runner object.
|
131
|
+
#
|
132
|
+
class Worker
|
133
|
+
include Celluloid
|
134
|
+
include Logging
|
135
|
+
attr_reader :pool, :index, :runner
|
136
|
+
|
137
|
+
def initialize(pool, index, runner_klass, runner_args)
|
138
|
+
@pool = pool
|
139
|
+
@poolname = pool.to_s
|
140
|
+
@index = index
|
141
|
+
@runner = runner_klass.new *runner_args
|
142
|
+
|
143
|
+
log(:debug) { 'Worker ready.' }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Dispatch given method and argument to custom runner.
|
147
|
+
# Arguments are used to call `#send` on runner instance.
|
148
|
+
#
|
149
|
+
def dispatch(method, args)
|
150
|
+
log(:debug) { "Dispatch to runner: #{runner.class.name}##{method.to_s}" }
|
151
|
+
|
152
|
+
# Send method to custom runner.
|
153
|
+
runner.send method, *args
|
154
|
+
rescue => error
|
155
|
+
log(:error) { "Received error from runner: #{error.message}\n#{error.backtrace.join(" \n")}" }
|
156
|
+
ensure
|
157
|
+
pool.executed Actor.current
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_s
|
161
|
+
"#{@poolname}[##{index}]"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/msgr/route.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
class Route
|
4
|
+
attr_reader :consumer, :action, :opts, :key
|
5
|
+
alias_method :routing_key, :key
|
6
|
+
|
7
|
+
def initialize(key, opts = {})
|
8
|
+
@key = key.to_s
|
9
|
+
@opts = opts
|
10
|
+
|
11
|
+
raise ArgumentError.new 'Routing key required.' unless @key.present?
|
12
|
+
raise ArgumentError.new 'Missing `to` options.' unless @opts[:to]
|
13
|
+
|
14
|
+
if (match = /\A(?<consumer>\w+)#(?<action>\w+)\z/.match(opts[:to].strip.to_s))
|
15
|
+
@consumer = "#{match[:consumer].camelize}Consumer"
|
16
|
+
@action = match[:action].underscore
|
17
|
+
else
|
18
|
+
raise ArgumentError.new "Invalid consumer format: #{opts[:to].strip.to_s.inspect}. Must be `consumer_class#action`."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
"msgr.consumer-#{key}//#{consumer}##{action}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/msgr/routes.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Msgr
|
2
|
+
|
3
|
+
class Routes
|
4
|
+
delegate :each, to: :@routes
|
5
|
+
|
6
|
+
def routes
|
7
|
+
@routes ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def configure(&block)
|
11
|
+
instance_eval &block
|
12
|
+
end
|
13
|
+
|
14
|
+
def route(key, opts = {})
|
15
|
+
routes << Msgr::Route.new(key, opts)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/msgr/version.rb
ADDED
data/msgr.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'msgr/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'msgr'
|
8
|
+
spec.version = Msgr::VERSION
|
9
|
+
spec.authors = ['Jan Graichen']
|
10
|
+
spec.email = ['jg@altimos.de']
|
11
|
+
spec.description = %q{}
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.homepage = 'https://github.com/jgraichen/msgr'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
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_dependency 'activesupport'
|
22
|
+
spec.add_dependency 'bunny', '~> 0.10.0'
|
23
|
+
spec.add_dependency 'celluloid'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec'
|
28
|
+
spec.add_development_dependency 'coveralls'
|
29
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'msgr'
|
2
|
+
|
3
|
+
Msgr.logger.level = Logger::Severity::DEBUG
|
4
|
+
|
5
|
+
@client = Msgr::Client.new uri: 'amqp://msgr:msgr@localhost'
|
6
|
+
|
7
|
+
@client.routes.configure do
|
8
|
+
route 'abc.#', to: 'test#index'
|
9
|
+
route 'cde.#', to: 'test#index'
|
10
|
+
route '#', to: 'test#another_action'
|
11
|
+
end
|
12
|
+
|
13
|
+
@client.start
|
14
|
+
|
15
|
+
10.times do |i|
|
16
|
+
@client.publish 'abc.XXX', "Message #{i} #{rand}"
|
17
|
+
end
|
18
|
+
|
19
|
+
sleep 5
|
20
|
+
|
21
|
+
@client.routes.configure do
|
22
|
+
route 'abc.#', to: 'test#index'
|
23
|
+
end
|
24
|
+
|
25
|
+
@client.reload
|
26
|
+
|
27
|
+
10.times do |i|
|
28
|
+
@client.publish 'abc.XXX', "Message #{i} #{rand}"
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
sleep
|
33
|
+
rescue Interrupt
|
34
|
+
@client.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
#class Dispatcher
|
38
|
+
# include Msgr::Logging
|
39
|
+
#
|
40
|
+
# def call(message)
|
41
|
+
# log(:info) { message }
|
42
|
+
# sleep 5 * rand
|
43
|
+
# log(:info) { 'Done' }
|
44
|
+
# end
|
45
|
+
#end
|
46
|
+
#
|
47
|
+
#pool = Msgr::Pool.new Dispatcher, size: 10
|
48
|
+
#pool.start
|
49
|
+
#
|
50
|
+
#100.times do |i|
|
51
|
+
# pool.dispatch(:call, "Message ##{i}")
|
52
|
+
#end
|
53
|
+
#
|
54
|
+
#sleep 5
|
55
|
+
#
|
56
|
+
#pool.stop
|
57
|
+
#pool.terminate
|
58
|
+
#
|
59
|
+
#Msgr.logger.info('[ROOT]') { 'Pool terminated.' }
|
60
|
+
|
61
|
+
#require 'celluloid'
|
62
|
+
#
|
63
|
+
#class Worker
|
64
|
+
# include Celluloid
|
65
|
+
#
|
66
|
+
# def do_work
|
67
|
+
# sleep 15
|
68
|
+
# end
|
69
|
+
#end
|
70
|
+
#
|
71
|
+
#logger = Logger.new $stdout
|
72
|
+
#
|
73
|
+
#pool = Worker.pool
|
74
|
+
#
|
75
|
+
#logger.info 'Start work'
|
76
|
+
#
|
77
|
+
#4.times do |i|
|
78
|
+
# pool.async.do_work
|
79
|
+
#end
|
80
|
+
#
|
81
|
+
#logger.info 'Wait'
|
82
|
+
#
|
83
|
+
#sleep 5
|
84
|
+
#
|
85
|
+
#logger.info 'Terminate'
|
86
|
+
#
|
87
|
+
#pool.terminate
|
88
|
+
#
|
89
|
+
#logger.info 'Done.'
|
90
|
+
|
91
|
+
|
92
|
+
#require 'bunny'
|
93
|
+
#
|
94
|
+
#bunny = Bunny.new 'amqp://msgr:msgr@localhost'
|
95
|
+
#bunny.start
|
96
|
+
#
|
97
|
+
#channel = bunny.create_channel
|
98
|
+
#exchange = channel.topic 'msgr.topic'
|
99
|
+
#queue = channel.queue 'msgr.test.single-queue'
|
100
|
+
#
|
101
|
+
#queue.bind(exchange, routing_key: 'a.b.#')
|
102
|
+
#queue.bind(exchange, routing_key: 'a.c.#')
|
103
|
+
#queue.subscribe do |delivery_info, metadata, payload|
|
104
|
+
# puts "#{delivery_info.routing_key} #{payload}"
|
105
|
+
#end
|
106
|
+
#
|
107
|
+
#sleep 1
|
108
|
+
#
|
109
|
+
#10.times { |i| exchange.publish "Message ##{i}", routing_key: [ 'a.b.c', 'a.c.d', 'a.b', 'a' ].sample; sleep 0.2 }
|
110
|
+
#
|
111
|
+
#sleep 10
|
112
|
+
#
|
113
|
+
#channel.close
|
114
|
+
#bunny.close
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Msgr::Route do
|
4
|
+
let(:routing_key) { 'routing.key.#' }
|
5
|
+
let(:options) { {to: 'test#index'} }
|
6
|
+
let(:args) { [routing_key, options] }
|
7
|
+
let(:route) { Msgr::Route.new *args }
|
8
|
+
subject { route }
|
9
|
+
|
10
|
+
describe '#initialize' do
|
11
|
+
it 'should require `to` option' do
|
12
|
+
expect {
|
13
|
+
Msgr::Route.new(routing_key, {})
|
14
|
+
}.to raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should require routing_key' do
|
18
|
+
expect {
|
19
|
+
Msgr::Route.new nil, options
|
20
|
+
}.to raise_error(ArgumentError)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should require not empty routing_key' do
|
24
|
+
expect {
|
25
|
+
Msgr::Route.new '', options
|
26
|
+
}.to raise_error ArgumentError, /routing key required/i
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should require `to: "consumer#action` format' do
|
30
|
+
expect {
|
31
|
+
Msgr::Route.new routing_key, to: 'abc'
|
32
|
+
}.to raise_error ArgumentError, /invalid consumer format/i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#consumer' do
|
37
|
+
it 'should return consumer class name' do
|
38
|
+
expect(route.consumer).to eq 'TestConsumer'
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with underscore consumer name' do
|
42
|
+
let(:options) { super().merge to: 'test_resource_foo#index' }
|
43
|
+
|
44
|
+
it 'should return camelized method name' do
|
45
|
+
expect(route.consumer).to eq 'TestResourceFooConsumer'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#action' do
|
51
|
+
it 'should return action method name' do
|
52
|
+
expect(route.action).to eq 'index'
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with camelCase action name' do
|
56
|
+
let(:options) { super().merge to: 'test#myActionMethod' }
|
57
|
+
|
58
|
+
it 'should return underscore method name' do
|
59
|
+
expect(route.action).to eq 'my_action_method'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Msgr::Routes do
|
4
|
+
let(:routes) { Msgr::Routes.new }
|
5
|
+
|
6
|
+
describe '#configure' do
|
7
|
+
let(:block) { Proc.new{} }
|
8
|
+
|
9
|
+
it 'should evaluate given block within instance context' do
|
10
|
+
expect(routes).to receive(:instance_eval).with { |p| p == block }
|
11
|
+
|
12
|
+
routes.configure &block
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should allow to call instance method in gven block' do
|
16
|
+
expect(routes).to receive(:test_instance_method).with(:abc)
|
17
|
+
|
18
|
+
routes.configure do
|
19
|
+
test_instance_method :abc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#each' do
|
25
|
+
before do
|
26
|
+
routes.configure do
|
27
|
+
route 'abc.#', to: 'test#index'
|
28
|
+
route 'edf.#', to: 'test#index'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:each) { routes.each }
|
33
|
+
|
34
|
+
it 'should iterate over configured routes' do
|
35
|
+
expect(each).to have(2).items
|
36
|
+
|
37
|
+
expect(each.map(&:routing_key)).to be == %w(abc.# edf.#)
|
38
|
+
expect(each.map(&:consumer)).to be == %w(TestConsumer TestConsumer)
|
39
|
+
expect(each.map(&:action)).to be == %w(index index)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#route' do
|
44
|
+
let(:subject) { -> { routes.route 'routing.key', to: 'test2#index2' } }
|
45
|
+
let(:last_route) { routes.routes.last }
|
46
|
+
|
47
|
+
it 'should add a new route' do
|
48
|
+
expect { subject.call }.to change{ routes.routes.size }.from(0).to(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should add given route' do
|
52
|
+
subject.call
|
53
|
+
|
54
|
+
expect(last_route.routing_key).to eq 'routing.key'
|
55
|
+
expect(last_route.consumer).to eq 'Test2Consumer'
|
56
|
+
expect(last_route.action).to eq 'index2'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/spec/msgr_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Coverage
|
2
|
+
require 'coveralls'
|
3
|
+
Coveralls.wear! do
|
4
|
+
add_filter 'spec'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rspec/autorun'
|
8
|
+
require 'msgr'
|
9
|
+
|
10
|
+
Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |f| require f }
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.order = 'random'
|
14
|
+
config.expect_with :rspec do |c|
|
15
|
+
# Only allow expect syntax
|
16
|
+
c.syntax = :expect
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: msgr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jan Graichen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
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: bunny
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.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.10.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: celluloid
|
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: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
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: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
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: coveralls
|
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
|
+
description: ''
|
112
|
+
email:
|
113
|
+
- jg@altimos.de
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- .travis.yml
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- gemfiles/Gemfile.rails-3-2
|
125
|
+
- gemfiles/Gemfile.rails-4-0
|
126
|
+
- lib/msgr.rb
|
127
|
+
- lib/msgr/binding.rb
|
128
|
+
- lib/msgr/client.rb
|
129
|
+
- lib/msgr/connection.rb
|
130
|
+
- lib/msgr/dispatcher.rb
|
131
|
+
- lib/msgr/errors.rb
|
132
|
+
- lib/msgr/logging.rb
|
133
|
+
- lib/msgr/message.rb
|
134
|
+
- lib/msgr/pool.rb
|
135
|
+
- lib/msgr/route.rb
|
136
|
+
- lib/msgr/routes.rb
|
137
|
+
- lib/msgr/version.rb
|
138
|
+
- msgr.gemspec
|
139
|
+
- scripts/simple_test.rb
|
140
|
+
- spec/msgr/client_spec.rb
|
141
|
+
- spec/msgr/consumer_spec.rb
|
142
|
+
- spec/msgr/route_spec.rb
|
143
|
+
- spec/msgr/routes_spec.rb
|
144
|
+
- spec/msgr_spec.rb
|
145
|
+
- spec/spec_helper.rb
|
146
|
+
homepage: https://github.com/jgraichen/msgr
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
metadata: {}
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 2.0.6
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: ''
|
170
|
+
test_files:
|
171
|
+
- spec/msgr/client_spec.rb
|
172
|
+
- spec/msgr/consumer_spec.rb
|
173
|
+
- spec/msgr/route_spec.rb
|
174
|
+
- spec/msgr/routes_spec.rb
|
175
|
+
- spec/msgr_spec.rb
|
176
|
+
- spec/spec_helper.rb
|