ragnar 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +61 -0
- data/Rakefile +2 -0
- data/lib/ragnar.rb +28 -0
- data/lib/ragnar/connector.rb +28 -0
- data/lib/ragnar/exchange.rb +28 -0
- data/lib/ragnar/version.rb +3 -0
- data/ragnar.gemspec +25 -0
- data/spec/lib/ragnar/connector_spec.rb +69 -0
- data/spec/lib/ragnar/exchange_spec.rb +63 -0
- data/spec/lib/ragnar_spec.rb +51 -0
- data/spec/spec_helper.rb +6 -0
- metadata +124 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Ragnar is a client library that aims to provide some top-level helpers for creating AMQP pub/sub code that can be used across many services nodes without having to do a ton of connection/channel handling. This is very much a work in progress, so bear with it.
|
2
|
+
|
3
|
+
### Connections ###
|
4
|
+
|
5
|
+
Ragnar provides a convenience connector to run EventMachine and setup the AMQP connection. For **Unicorn**, place the connector call inside the `after_fork` block in order to get things going:
|
6
|
+
|
7
|
+
after_fork do |server, worker|
|
8
|
+
Ragnar::Connector.connect :host => 'localhost', :port => 5732
|
9
|
+
end
|
10
|
+
|
11
|
+
### Publishing ###
|
12
|
+
|
13
|
+
The following code will publish a message to the 'events' exchange under the routing key 'the.event.name'.
|
14
|
+
|
15
|
+
Ragnar.exchange(:topic, 'events') do |x|
|
16
|
+
x.publish('the.event.name', 'this is the message')
|
17
|
+
end
|
18
|
+
|
19
|
+
And the equivalent AMQP code:
|
20
|
+
|
21
|
+
AMQP.start do |connection, connect_ok|
|
22
|
+
channel = AMQP::Channel.new(connection)
|
23
|
+
exchange = channel.topic('events', :auto_delete => false)
|
24
|
+
exchange.publish('this is the message', :routing_key => 'the.event.name')
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
### Subscriptions ###
|
29
|
+
|
30
|
+
The following code with Ragnar creates a single topic subscription on the 'events' exchange, using the queue 'myservice.the.event.name' and routing key 'the.event.name'.
|
31
|
+
|
32
|
+
Ragnar.exchange(:topic, 'events') do |x|
|
33
|
+
x.queue_prefix = :myservice # optional
|
34
|
+
x.subscribe('the.event.name') do |message|
|
35
|
+
# subscription code
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
The equivalent AMQP code for the code above would be:
|
40
|
+
|
41
|
+
AMQP.start do |connection|
|
42
|
+
channel = AMQP::Channel.new(connection)
|
43
|
+
exchange = channel.topic('events', :auto_delete => false)
|
44
|
+
channel.queue('myservice.the.event.name').bind(exchange, :routing_key => 'the.event.name').subscribe do |headers, payload|
|
45
|
+
# subscription code
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
### Feedback ###
|
50
|
+
|
51
|
+
Feedback and comments are welcome:
|
52
|
+
|
53
|
+
* Web: [rand9.com][web]
|
54
|
+
* Twitter: [@localshred][twitter]
|
55
|
+
* Github: [github][]
|
56
|
+
|
57
|
+
Cheers
|
58
|
+
|
59
|
+
[web]: http://www.rand9.com "rand9.com"
|
60
|
+
[twitter]: http://twitter.com/localshred "Twitter: @localshred"
|
61
|
+
[github]: http://github.com/localshred "Github: localshred"
|
data/Rakefile
ADDED
data/lib/ragnar.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'ragnar/connector'
|
2
|
+
require 'ragnar/exchange'
|
3
|
+
|
4
|
+
module Ragnar
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def exchange type, name, options={}
|
9
|
+
exch = exchanges[key(type, name)] || store(Ragnar::Exchange.new(type, name, options))
|
10
|
+
yield(exch) if block_given?
|
11
|
+
exch
|
12
|
+
end
|
13
|
+
|
14
|
+
def store ex
|
15
|
+
exchanges[key(ex.type, ex.name)] = ex
|
16
|
+
end
|
17
|
+
|
18
|
+
def exchanges
|
19
|
+
@exchanges ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def key type, name
|
23
|
+
'%s-%s' % [type.to_s, name]
|
24
|
+
end
|
25
|
+
|
26
|
+
private :store, :exchanges, :key
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
|
3
|
+
# Provide a simple connector piece that runs EM and passes through AMQP connection options
|
4
|
+
module Ragnar
|
5
|
+
class Connector
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
# Store the connection for later retrieval
|
10
|
+
attr_accessor :connection
|
11
|
+
|
12
|
+
# Pass connection options through to AMQP
|
13
|
+
def connect opts={}
|
14
|
+
unless EM.reactor_running?
|
15
|
+
Thread.new { EventMachine.run }
|
16
|
+
sleep 0.5
|
17
|
+
end
|
18
|
+
@connection = AMQP.connect(opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def connected?
|
22
|
+
@connection && @connection.connected?
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Ragnar
|
2
|
+
class Exchange
|
3
|
+
|
4
|
+
attr_reader :exchange, :channel, :type, :name, :options
|
5
|
+
attr_accessor :queue_prefix
|
6
|
+
|
7
|
+
def initialize type, name, opts={}
|
8
|
+
@type, @name, @options = type, name, opts
|
9
|
+
@channel = AMQP::Channel.new(Ragnar::Connector.connection)
|
10
|
+
@exchange = @channel.__send__(@type, @name, @options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish routing_key, message, opts={}
|
14
|
+
@exchange.publish(message, opts.merge(:routing_key => routing_key))
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe name, opts={}, &block
|
18
|
+
@channel.queue(queue_name(name)).bind(@exchange, :routing_key => name).subscribe(opts, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def queue_name name
|
24
|
+
queue_prefix.nil? ? name : '%s.%s' % [queue_prefix, name]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/ragnar.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ragnar/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ragnar"
|
7
|
+
s.version = Ragnar::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["BJ Neislen"]
|
10
|
+
s.email = ["bj.neilsen@gmail.com"]
|
11
|
+
s.homepage = "http://www.rand9.com"
|
12
|
+
s.summary = %q{Provide top-level pub/sub methods with RabbitMQ (AMQP) for interacting with a larger service ecosystem}
|
13
|
+
s.description = %q{Provide top-level pub/sub methods with RabbitMQ (AMQP) for interacting with a larger service ecosystem}
|
14
|
+
|
15
|
+
s.rubyforge_project = "ragnar"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency 'amqp', '~>0.7.1'
|
23
|
+
s.add_development_dependency 'rspec', '~>2.5.0'
|
24
|
+
s.add_development_dependency 'evented-spec', '~>0.4.1'
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ragnar/connector'
|
3
|
+
|
4
|
+
describe Ragnar::Connector do
|
5
|
+
include EventedSpec::SpecHelper
|
6
|
+
include EventedSpec::AMQPSpec
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
Ragnar::Connector.connection = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.connect' do
|
13
|
+
|
14
|
+
it 'runs eventmachine and creates an AMQP connection' do
|
15
|
+
connection = Ragnar::Connector.connect
|
16
|
+
delayed(0.3) {
|
17
|
+
EM.reactor_running?.should be_true
|
18
|
+
connection.should be_connected
|
19
|
+
done
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'accepts an options hash and passes it through' do
|
24
|
+
opts = {:host => 'localhost', :port => '5762'}
|
25
|
+
AMQP.should_receive(:connect).with(opts)
|
26
|
+
Ragnar::Connector.connect(opts)
|
27
|
+
done
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'uses the existing reactor if it already exists' do
|
31
|
+
em do
|
32
|
+
EM.should_not_receive(:run)
|
33
|
+
Ragnar::Connector.connect
|
34
|
+
done
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.connection' do
|
41
|
+
|
42
|
+
it 'stores the connection' do
|
43
|
+
Ragnar::Connector.connect
|
44
|
+
delayed(0.3) {
|
45
|
+
Ragnar::Connector.connection.should be_connected
|
46
|
+
done
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does not create the connection for you' do
|
51
|
+
AMQP.should_not_receive(:connect)
|
52
|
+
Ragnar::Connector.connection.should be_nil
|
53
|
+
done
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '.connected?' do
|
59
|
+
it 'relays connection state' do
|
60
|
+
Ragnar::Connector.connected?.should be_false
|
61
|
+
Ragnar::Connector.connect
|
62
|
+
delayed(0.3) {
|
63
|
+
Ragnar::Connector.connected?.should be_true
|
64
|
+
done
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ragnar/connector'
|
3
|
+
require 'ragnar/exchange'
|
4
|
+
|
5
|
+
describe Ragnar::Exchange do
|
6
|
+
include EventedSpec::SpecHelper
|
7
|
+
include EventedSpec::AMQPSpec
|
8
|
+
|
9
|
+
describe '.new' do
|
10
|
+
it 'creates a channel and exchange' do
|
11
|
+
opts = {:durable => true}
|
12
|
+
name = 'exch_name'
|
13
|
+
|
14
|
+
Ragnar::Connector.should_receive(:connection).and_return(connection = mock('connection'))
|
15
|
+
AMQP::Channel.should_receive(:new).with(connection).and_return(channel = mock('channel'))
|
16
|
+
channel.should_receive(:topic).with(name, opts)
|
17
|
+
Ragnar::Exchange.new(:topic, name, opts)
|
18
|
+
done
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#queue_prefix' do
|
23
|
+
it 'sets a temporary queue prefix' do
|
24
|
+
exch = Ragnar::Exchange.new(:topic, 'name')
|
25
|
+
exch.queue_prefix = :my_service
|
26
|
+
exch.queue_prefix.should == :my_service
|
27
|
+
done
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#subscribe' do
|
32
|
+
it 'binds a queue to the channel and assigns the subscription to the queue' do
|
33
|
+
exch = Ragnar::Exchange.new(:topic, 'name')
|
34
|
+
subscription = Proc.new {|message| true }
|
35
|
+
exch.channel.should_receive(:queue).with('the.event.name').and_return(queue = mock('queue'))
|
36
|
+
queue.should_receive(:bind).with(exch.exchange, :routing_key => 'the.event.name').and_return(binding = mock('binding'))
|
37
|
+
binding.should_receive(:subscribe).with(subscription)
|
38
|
+
exch.subscribe('the.event.name', subscription)
|
39
|
+
done
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'uses the queue_prefix if present when setting the queue name' do
|
43
|
+
exch = Ragnar::Exchange.new(:topic, 'name')
|
44
|
+
exch.queue_prefix = :my_service
|
45
|
+
exch.channel.should_receive(:queue).with('my_service.the.event.name').and_return(queue = mock('queue'))
|
46
|
+
queue.should_receive(:bind).with(exch.exchange, :routing_key => 'the.event.name').and_return(binding = mock('binding'))
|
47
|
+
binding.should_receive(:subscribe)
|
48
|
+
exch.subscribe('the.event.name')
|
49
|
+
done
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#publish' do
|
54
|
+
it 'publishes messages to the exchange' do
|
55
|
+
exch = Ragnar::Exchange.new(:topic, 'name')
|
56
|
+
publish_opts = {:param => 'value', :routing_key => 'the.event.name'}
|
57
|
+
exch.exchange.should_receive(:publish).with('the message', publish_opts)
|
58
|
+
exch.publish('the.event.name', 'the message', publish_opts)
|
59
|
+
done
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ragnar'
|
3
|
+
|
4
|
+
describe Ragnar do
|
5
|
+
include EventedSpec::SpecHelper
|
6
|
+
include EventedSpec::AMQPSpec
|
7
|
+
|
8
|
+
describe '.exchange' do
|
9
|
+
context 'when the exchange does not exist' do
|
10
|
+
it 'creates a new exchange' do
|
11
|
+
opts = {:param => 'value'}
|
12
|
+
Ragnar::Exchange.should_receive(:new).with(:topic, 'exch_name', opts).and_return(mock('ex', :type => :topic, :name => 'exch_name'))
|
13
|
+
Ragnar.exchange(:topic, 'exch_name', opts)
|
14
|
+
done
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when the exchange already has been created locally' do
|
19
|
+
it 'uses the existing exchange' do
|
20
|
+
exch = Ragnar.exchange(:topic, 'exch_name')
|
21
|
+
Ragnar.exchange(:topic, 'exch_name').should === exch
|
22
|
+
done
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when passed a block with embedded subscriptions' do
|
27
|
+
it 'registers the subscriptions with the exchange' do
|
28
|
+
subscriber = Proc.new{|message| true }
|
29
|
+
exchange = Ragnar::Exchange.new(:topic, 'events')
|
30
|
+
Ragnar.should_receive(:store).and_return(exchange)
|
31
|
+
|
32
|
+
exchange.should_receive(:queue_prefix=).with(:my_service)
|
33
|
+
exchange.should_receive(:subscribe).with('the.message.route.1', subscriber)
|
34
|
+
exchange.should_receive(:subscribe).with('the.message.route.2', subscriber)
|
35
|
+
exchange.should_receive(:subscribe).with('the.message.route.3', subscriber)
|
36
|
+
exchange.should_receive(:subscribe).with('the.message.route.4', subscriber)
|
37
|
+
|
38
|
+
Ragnar.exchange(:topic, 'events') do |x|
|
39
|
+
x.queue_prefix = :my_service
|
40
|
+
x.subscribe('the.message.route.1', subscriber)
|
41
|
+
x.subscribe('the.message.route.2', subscriber)
|
42
|
+
x.subscribe('the.message.route.3', subscriber)
|
43
|
+
x.subscribe('the.message.route.4', subscriber)
|
44
|
+
end
|
45
|
+
|
46
|
+
done
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ragnar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- BJ Neislen
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-05-05 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: amqp
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 7
|
31
|
+
- 1
|
32
|
+
version: 0.7.1
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rspec
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 2
|
45
|
+
- 5
|
46
|
+
- 0
|
47
|
+
version: 2.5.0
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: evented-spec
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ~>
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
- 4
|
61
|
+
- 1
|
62
|
+
version: 0.4.1
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
description: Provide top-level pub/sub methods with RabbitMQ (AMQP) for interacting with a larger service ecosystem
|
66
|
+
email:
|
67
|
+
- bj.neilsen@gmail.com
|
68
|
+
executables: []
|
69
|
+
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files: []
|
73
|
+
|
74
|
+
files:
|
75
|
+
- .gitignore
|
76
|
+
- Gemfile
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- lib/ragnar.rb
|
80
|
+
- lib/ragnar/connector.rb
|
81
|
+
- lib/ragnar/exchange.rb
|
82
|
+
- lib/ragnar/version.rb
|
83
|
+
- ragnar.gemspec
|
84
|
+
- spec/lib/ragnar/connector_spec.rb
|
85
|
+
- spec/lib/ragnar/exchange_spec.rb
|
86
|
+
- spec/lib/ragnar_spec.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
has_rdoc: true
|
89
|
+
homepage: http://www.rand9.com
|
90
|
+
licenses: []
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
version: "0"
|
113
|
+
requirements: []
|
114
|
+
|
115
|
+
rubyforge_project: ragnar
|
116
|
+
rubygems_version: 1.3.7
|
117
|
+
signing_key:
|
118
|
+
specification_version: 3
|
119
|
+
summary: Provide top-level pub/sub methods with RabbitMQ (AMQP) for interacting with a larger service ecosystem
|
120
|
+
test_files:
|
121
|
+
- spec/lib/ragnar/connector_spec.rb
|
122
|
+
- spec/lib/ragnar/exchange_spec.rb
|
123
|
+
- spec/lib/ragnar_spec.rb
|
124
|
+
- spec/spec_helper.rb
|