zmqp 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/HISTORY +7 -0
- data/LICENSE +20 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/bin/zmqp +7 -0
- data/features/step_definitions/zmqp_steps.rb +0 -0
- data/features/support/env.rb +10 -0
- data/features/support/world.rb +12 -0
- data/features/zmqp.feature +9 -0
- data/lib/version.rb +8 -0
- data/lib/zmqp.rb +127 -0
- data/lib/zmqp/client.rb +17 -0
- data/lib/zmqp/spec.rb +7 -0
- data/spec/amqp.yml +28 -0
- data/spec/mq_spec.rb +436 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/zmqp_spec.rb +186 -0
- data/tasks/common.rake +18 -0
- data/tasks/doc.rake +14 -0
- data/tasks/gem.rake +40 -0
- data/tasks/git.rake +34 -0
- data/tasks/spec.rake +16 -0
- data/tasks/version.rake +71 -0
- metadata +155 -0
data/.gitignore
ADDED
data/HISTORY
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Arvicco
|
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,76 @@
|
|
1
|
+
by: Arvicco
|
2
|
+
url: http://github.com/arvicco/zmqp
|
3
|
+
|
4
|
+
Description
|
5
|
+
===========
|
6
|
+
|
7
|
+
[Asynchronous AMQP library](http://github.com/tmm1/amqp) sets a standard for messaging.
|
8
|
+
It is well documented and has clean and simple interfaces for most common messaging tasks.
|
9
|
+
However, it implements AMQP standard that has some serious
|
10
|
+
[problems and limitation](http://www.imatix.com/articles:whats-wrong-with-amqp).
|
11
|
+
|
12
|
+
[ZeroMQ](http://www.zeromq.org) offers a viable alternative to AMQP, as well as 3-10 times
|
13
|
+
performance boost for most common messaging tasks. However, its interfaces are a very similar
|
14
|
+
to socket programming and look a bit frightening for uninitiated.
|
15
|
+
|
16
|
+
This library wraps ZeroMQ/ZMQMachine with interfaces that are very similar to the ones
|
17
|
+
offered by [tmm1/amqp](http://github.com/tmm1/amqp). That way, you'll be able to leverage
|
18
|
+
your existing code developed for AMQP, getting additional benefits such as:
|
19
|
+
|
20
|
+
* Performance boost (3-10 times for most common messaging tasks)
|
21
|
+
* No broker-related bottleneck for high-throughput applications
|
22
|
+
* Ability to flexibly extend your messaging architecture
|
23
|
+
* No external dependency on 3rd-party broker software (such as RabbitMQ)
|
24
|
+
|
25
|
+
Because ZMQis inherently threaded, it does not make sense to use this library with
|
26
|
+
anything less than MRI 1.9.2, JRuby or Rubinius. MRI 1.9.1 may or may not work, MRI 1.8.7
|
27
|
+
and its below are broken beyond repair (as far as threading goes).
|
28
|
+
|
29
|
+
!!! Currently this library is at pre-alpha stage. Please do not use it for anything serious.
|
30
|
+
|
31
|
+
Getting started
|
32
|
+
===============
|
33
|
+
|
34
|
+
First things first, start with:
|
35
|
+
|
36
|
+
$ gem install zmqp
|
37
|
+
|
38
|
+
Documentation
|
39
|
+
=============
|
40
|
+
|
41
|
+
To be added...
|
42
|
+
|
43
|
+
Credits
|
44
|
+
=======
|
45
|
+
|
46
|
+
(c) 2010 [Arvicco](http://github.com/arvicco)
|
47
|
+
|
48
|
+
This project was inspired by [tmm1/amqp](http://github.com/tmm1/amqp).
|
49
|
+
It is based on [ZMQMachine](http://github.com/chuckremes/zmqmachine).
|
50
|
+
Special thanks to Aman Gupta and Chuck Remes.
|
51
|
+
|
52
|
+
Resources
|
53
|
+
=========
|
54
|
+
|
55
|
+
* [ZeroMQ](http://www.zeromq.org) (iMatix/FastMQ/Intel, C++, GPL3)
|
56
|
+
|
57
|
+
* [Ruby bindings to ZeroMQ](http://github.com/chuckremes/ffi-rzmq)
|
58
|
+
|
59
|
+
* [Learn ZeroMQ by Example](http://github.com/andrewvc/learn-ruby-zeromq)
|
60
|
+
|
61
|
+
* Analysis of AMQP [problems](http://www.imatix.com/articles:whats-wrong-with-amqp)
|
62
|
+
|
63
|
+
* ZeroMQ's [analysis of the messaging technology market](http://www.zeromq.org/whitepapers:market-analysis)
|
64
|
+
|
65
|
+
* [A Critique of the Remote Procedure Call Paradigm](http://www.cs.vu.nl/~ast/publications/euteco-1988.pdf)
|
66
|
+
|
67
|
+
* [A Note on Distributed Computing](http://research.sun.com/techrep/1994/smli_tr-94-29.pdf)
|
68
|
+
|
69
|
+
* [Convenience Over Correctness](http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf)
|
70
|
+
|
71
|
+
* [Metaprotocol Taxonomy and Communications Patterns](http://hessian.caucho.com/doc/metaprotocol-taxonomy.xtp)
|
72
|
+
|
73
|
+
* Joe Armstrong on [Erlang messaging vs RPC](http://armstrongonsoftware.blogspot.com/2008/05/road-we-didnt-go-down.html)
|
74
|
+
|
75
|
+
== LICENSE:
|
76
|
+
Copyright (c) 2010 Arvicco. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
BASE_PATH = Pathname.new(__FILE__).dirname
|
3
|
+
LIB_PATH = BASE_PATH + 'lib'
|
4
|
+
PKG_PATH = BASE_PATH + 'pkg'
|
5
|
+
DOC_PATH = BASE_PATH + 'rdoc'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift LIB_PATH.to_s
|
8
|
+
|
9
|
+
require 'version'
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
NAME = 'zmqp'
|
13
|
+
CLASS_NAME = ZMQP
|
14
|
+
VERSION = CLASS_NAME::VERSION
|
15
|
+
|
16
|
+
# Load rakefile tasks
|
17
|
+
Dir['tasks/*.rake'].sort.each { |file| load file }
|
18
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/zmqp
ADDED
File without changes
|
data/lib/version.rb
ADDED
data/lib/zmqp.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'version'
|
2
|
+
|
3
|
+
module ZMQP
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
Bundler.require :default
|
7
|
+
|
8
|
+
# Requires ruby source file(s). Accepts either single filename/glob or Array of filenames/globs.
|
9
|
+
# Accepts following options:
|
10
|
+
# :*file*:: Lib(s) required relative to this file - defaults to __FILE__
|
11
|
+
# :*dir*:: Required lib(s) located under this dir name - defaults to gem name
|
12
|
+
#
|
13
|
+
def self.require_libs( libs, opts={} )
|
14
|
+
file = Pathname.new(opts[:file] || __FILE__)
|
15
|
+
[libs].flatten.each do |lib|
|
16
|
+
name = file.dirname + (opts[:dir] || file.basename('.*')) + lib.gsub(/(?<!.rb)$/, '.rb')
|
17
|
+
Pathname.glob(name.to_s).sort.each {|rb| require rb}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # module ZMQP
|
21
|
+
|
22
|
+
require 'zmqmachine'
|
23
|
+
|
24
|
+
# Did I say that ZMQP is a drop-in substitution for AMQP?
|
25
|
+
AMQP = ZMQP unless defined? AMQP
|
26
|
+
EM = ZM unless defined? EM
|
27
|
+
|
28
|
+
# Require all ruby source files located under directory lib/zmqp
|
29
|
+
# If you need files in specific order, you should specify it here before the glob
|
30
|
+
ZMQP.require_libs %W[**/*]
|
31
|
+
|
32
|
+
|
33
|
+
module ZMQP
|
34
|
+
class << self
|
35
|
+
@logging = false
|
36
|
+
attr_accessor :logging
|
37
|
+
attr_reader :conn, :closing
|
38
|
+
alias :closing? :closing
|
39
|
+
alias :connection :conn
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.connect *args
|
43
|
+
Client.connect *args
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.settings
|
47
|
+
@settings ||= {
|
48
|
+
# broker address
|
49
|
+
:host => '127.0.0.1',
|
50
|
+
:port => PORT,
|
51
|
+
|
52
|
+
# login details
|
53
|
+
:user => 'guest',
|
54
|
+
:pass => 'guest',
|
55
|
+
:vhost => '/',
|
56
|
+
|
57
|
+
# connection timeout
|
58
|
+
:timeout => nil,
|
59
|
+
|
60
|
+
# logging
|
61
|
+
:logging => false,
|
62
|
+
|
63
|
+
# ssl
|
64
|
+
:ssl => false
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Must be called to startup the connection to lightweight service broker (was:the AMQP server).
|
69
|
+
#
|
70
|
+
# The method takes several arguments and an optional block.
|
71
|
+
#
|
72
|
+
# This takes any option that is also accepted by *??* ZMQMachine::connect.
|
73
|
+
# Additionally, there are several ZMQP-specific options.
|
74
|
+
#
|
75
|
+
# * :user => String (default 'guest')
|
76
|
+
# The username as defined by the AMQP server.
|
77
|
+
# * :pass => String (default 'guest')
|
78
|
+
# The password for the associated :user as defined by the AMQP server.
|
79
|
+
# * :vhost => String (default '/')
|
80
|
+
# The virtual host as defined by the AMQP server.
|
81
|
+
# * :timeout => Numeric (default nil)
|
82
|
+
# Measured in seconds.
|
83
|
+
# * :logging => true | false (default false)
|
84
|
+
# Toggle the extremely verbose logging of all protocol communications
|
85
|
+
# between the client and the server. Extremely useful for debugging.
|
86
|
+
#
|
87
|
+
# AMQP.start do
|
88
|
+
# # default is to connect to localhost:5672
|
89
|
+
#
|
90
|
+
# # define queues, exchanges and bindings here.
|
91
|
+
# # also define all subscriptions and/or publishers
|
92
|
+
# # here.
|
93
|
+
#
|
94
|
+
# # this block never exits unless EM.stop_event_loop
|
95
|
+
# # is called.
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# Most code will use the MQ api. Any calls to MQ.direct / MQ.fanout /
|
99
|
+
# MQ.topic / MQ.queue will implicitly call #start. In those cases,
|
100
|
+
# it is sufficient to put your code inside of an EventMachine.run
|
101
|
+
# block. See the code examples in MQ for details.
|
102
|
+
#
|
103
|
+
def self.start *args, &blk
|
104
|
+
EM.run{
|
105
|
+
@conn ||= connect *args
|
106
|
+
@conn.callback(&blk) if blk
|
107
|
+
@conn
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
alias :run :start
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.stop
|
116
|
+
if @conn and not @closing
|
117
|
+
@closing = true
|
118
|
+
@conn.close{
|
119
|
+
yield if block_given?
|
120
|
+
@conn = nil
|
121
|
+
@closing = false
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
data/lib/zmqp/client.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module AMQP
|
2
|
+
|
3
|
+
# ZMQP Client impersonates AMQP Client/Connection.
|
4
|
+
# As such, it hides the difference between AMQP and ZMQ communication.
|
5
|
+
# This means that the Client should be encapsulating all the knowledge about
|
6
|
+
# ZMQ Sockets, socket handlers etc. It should probably do all the ZMQ "frame processing"
|
7
|
+
# inside, accepting only raw sum/pub/pop/etc from MQ and feeding it back raw
|
8
|
+
# data/exceptions
|
9
|
+
#
|
10
|
+
# Do we need Client at all? What is its role exactly?
|
11
|
+
# Maybe MQ instance alone is enough to hide Z/A difference, and actual communication
|
12
|
+
# will flow through "pseudo"-Exchanges and Queues anyway.
|
13
|
+
#
|
14
|
+
module Client
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/lib/zmqp/spec.rb
ADDED
data/spec/amqp.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# AMQP client configuration file
|
2
|
+
|
3
|
+
# These values will be used to configure the ampq gem, any values
|
4
|
+
# omitted will let the gem use it's own defaults.
|
5
|
+
|
6
|
+
# The configuration specifies the following keys:
|
7
|
+
# * user - Username for the broker
|
8
|
+
# * pass - Password for the broker
|
9
|
+
# * host - Hostname where the broker is running
|
10
|
+
# * vhost - Vhost to connect to
|
11
|
+
# * port - Port where the broker is running
|
12
|
+
# * ssl - Use ssl or not
|
13
|
+
# * timeout - Timeout
|
14
|
+
|
15
|
+
defaults: &defaults
|
16
|
+
user: guest
|
17
|
+
pass: guest
|
18
|
+
host: 10.211.55.2
|
19
|
+
vhost: /
|
20
|
+
|
21
|
+
development:
|
22
|
+
<<: *defaults
|
23
|
+
|
24
|
+
test:
|
25
|
+
<<: *defaults
|
26
|
+
|
27
|
+
production:
|
28
|
+
<<: *defaults
|
data/spec/mq_spec.rb
ADDED
@@ -0,0 +1,436 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'amqp-spec/rspec'
|
3
|
+
|
4
|
+
require 'mq'
|
5
|
+
|
6
|
+
# Mocking AMQP::Client::EM_CONNECTION_CLASS in order to
|
7
|
+
# specify MQ instance behavior without the need to start EM loop.
|
8
|
+
class MockConnection
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
EM.should_receive(:reactor_running?).and_return(true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def callback &block
|
15
|
+
callbacks << block
|
16
|
+
block.call(self) if block
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_channel mq
|
20
|
+
channels[key = (channels.keys.max || 0) + 1] = mq
|
21
|
+
key
|
22
|
+
end
|
23
|
+
|
24
|
+
def send data, opts = {}
|
25
|
+
messages << {data: data, opts: opts}
|
26
|
+
end
|
27
|
+
|
28
|
+
def connected?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def channels
|
33
|
+
@channels||={}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Not part of AMQP::Client::EM_CONNECTION_CLASS interface, for mock introspection only
|
37
|
+
def callbacks
|
38
|
+
@callbacks||=[]
|
39
|
+
end
|
40
|
+
|
41
|
+
def messages
|
42
|
+
@messages||=[]
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'MQ', 'as a class' do
|
50
|
+
# after{AMQP.cleanup_state} # Tips off Thread.current[:mq] call
|
51
|
+
subject { MQ }
|
52
|
+
|
53
|
+
its(:logging) { should be_false }
|
54
|
+
|
55
|
+
describe '.logging=' do
|
56
|
+
it 'is independent from AMQP.logging' do
|
57
|
+
#
|
58
|
+
AMQP.logging = true
|
59
|
+
MQ.logging.should be_false
|
60
|
+
MQ.logging = false
|
61
|
+
AMQP.logging.should == true
|
62
|
+
|
63
|
+
#
|
64
|
+
AMQP.logging = false
|
65
|
+
MQ.logging = true
|
66
|
+
AMQP.logging.should be_false
|
67
|
+
MQ.logging = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '.default' do
|
72
|
+
it 'creates MQ object and stashes it in Thread-local Hash' do
|
73
|
+
MQ.should_receive(:new).with(no_args).and_return('MQ mock')
|
74
|
+
Thread.current.should_receive(:[]).with(:mq)
|
75
|
+
Thread.current.should_receive(:[]=).with(:mq, 'MQ mock')
|
76
|
+
MQ.default.should == 'MQ mock'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '.id' do
|
81
|
+
it 'returns Thread-local uuid for mq' do
|
82
|
+
Thread.current.should_receive(:[]).with(:mq_id)
|
83
|
+
Thread.current.should_receive(:[]=).with(:mq_id, /.*-[0-9]{4,5}-[0-9]{10}/)
|
84
|
+
MQ.id.should =~ /.*-[0-9]{4,5}-[0-9]{10}/
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '.error' do
|
89
|
+
it 'is noop unless @error_callback was previously set' do
|
90
|
+
MQ.instance_variable_get(:@error_callback).should be_nil
|
91
|
+
MQ.error("Whatever").should be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is setting @error_callback if block is given' do
|
95
|
+
MQ.error("Whatever") { |message| message.should == "Whatever"; @callback_fired = true }
|
96
|
+
MQ.instance_variable_get(:@error_callback).should_not be_nil
|
97
|
+
@callback_fired.should be_false
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'is causes @error_callback to fire if it was set' do
|
101
|
+
MQ.error("Whatever")
|
102
|
+
@callback_fired.should be_true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '.method_missing' do
|
107
|
+
it 'routes all unrecognized methods to MQ.default' do
|
108
|
+
MQ.should_receive(:new).with(no_args).and_return(mock('schmock'))
|
109
|
+
MQ.default.should_receive(:abracadabra).with("Whatever")
|
110
|
+
MQ.abracadabra("Whatever")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe 'MQ', 'object, also vaguely known as "channel"' do
|
116
|
+
|
117
|
+
context 'when initialized with a mock connection' do
|
118
|
+
before { @conn = MockConnection.new }
|
119
|
+
after { AMQP.cleanup_state }
|
120
|
+
subject { MQ.new(@conn).tap { |mq| mq.succeed } } # Indicates that channel is connected
|
121
|
+
# its(:connection) { should be_nil } - relies on subject.send(:connection), not working
|
122
|
+
|
123
|
+
it 'has public accessors' do
|
124
|
+
subject.channel.should == 1 # Essentially, channel number
|
125
|
+
subject.consumers.should be_a Hash
|
126
|
+
subject.consumers.should be_empty
|
127
|
+
subject.exchanges.should be_a Hash
|
128
|
+
subject.exchanges.should be_empty
|
129
|
+
subject.queues.should be_a Hash
|
130
|
+
subject.queues.should be_empty
|
131
|
+
subject.rpcs.should be_a Hash
|
132
|
+
subject.rpcs.should be_empty
|
133
|
+
|
134
|
+
#connection was declared as both private and public accessor for some reason... Why?
|
135
|
+
subject.connection.should == @conn
|
136
|
+
subject.conn.should == @conn
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '#process_frame' do # The meat of mq operations
|
140
|
+
describe ' Frame::Header'
|
141
|
+
describe ' Frame::Body'
|
142
|
+
describe ' Frame::Method'
|
143
|
+
describe ' Protocol::Channel::OpenOk'
|
144
|
+
describe ' Protocol::Access::RequestOk'
|
145
|
+
describe ' Protocol::Basic::CancelOk'
|
146
|
+
describe ' Protocol::Queue::DeclareOk'
|
147
|
+
describe ' Protocol::Basic::GetOk'
|
148
|
+
describe ' Protocol::Basic::Deliver'
|
149
|
+
describe ' Protocol::Basic::GetEmpty'
|
150
|
+
describe ' Protocol::Channel::Close'
|
151
|
+
describe ' Protocol::Basic::ConsumeOk'
|
152
|
+
|
153
|
+
describe 'Protocol::Channel::CloseOk' do
|
154
|
+
it 'closes the channel down' do
|
155
|
+
@conn.channels.should_receive(:delete) { |key| key.should == 1; @conn.channels.clear }
|
156
|
+
subject.process_frame AMQP::Frame::Method.new(AMQP::Protocol::Channel::CloseOk.new)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'closes down connection if no other channels' do
|
160
|
+
@conn.should_receive(:close)
|
161
|
+
subject.process_frame AMQP::Frame::Method.new(AMQP::Protocol::Channel::CloseOk.new)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe '#send' do
|
167
|
+
it 'sends given data through its connection' do
|
168
|
+
args = [mock('data1'), mock('data2'), mock('data3')]
|
169
|
+
|
170
|
+
subject.send *args
|
171
|
+
|
172
|
+
@conn.messages[-3..-1].map { |message| message[:data] }.should == args
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'sets data`s ticket property if @ticket is set for MQ object' do
|
176
|
+
subject.instance_variable_set(:@ticket, ticket = 'mock ticket')
|
177
|
+
data = mock('data1')
|
178
|
+
data.should_receive(:ticket=).with(ticket)
|
179
|
+
|
180
|
+
subject.send data
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'is Mutex-synchronized' do
|
184
|
+
mutex = subject.instance_variable_get(:@_send_mutex)
|
185
|
+
mutex.should_receive(:synchronize)
|
186
|
+
subject.send mock('data1')
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe '#reset' do
|
191
|
+
it 'resets and reinitializes the channel, clears and resets its queues/exchanges' do
|
192
|
+
subject.queue('test').should_receive(:reset)
|
193
|
+
subject.fanout('fanout').should_receive(:reset)
|
194
|
+
subject.should_receive(:initialize).with(@conn)
|
195
|
+
|
196
|
+
subject.reset
|
197
|
+
subject.queues.should be_empty
|
198
|
+
subject.exchanges.should be_empty
|
199
|
+
subject.consumers.should be_empty
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe '#prefetch' do
|
204
|
+
it 'sends Protocol::Basic::Qos, setting :prefetch_count for broker' do
|
205
|
+
subject.prefetch 13
|
206
|
+
@conn.messages.last[:data].should be_an AMQP::Protocol::Basic::Qos
|
207
|
+
@conn.messages.last[:data].instance_variable_get(:@prefetch_count).should == 13
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'returns MQ object itself, allowing for method chains' do
|
211
|
+
subject.prefetch(1).should == subject
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
describe '#recover' do
|
216
|
+
it 'sends Protocol::Basic::Recover, asking broker to redeliver all unack`ed messages on this channel' do
|
217
|
+
subject.recover
|
218
|
+
@conn.messages.last[:data].should be_an AMQP::Protocol::Basic::Recover
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'by default, requeue property is nil, so messages will be redelivered to the original recipient' do
|
222
|
+
subject.recover
|
223
|
+
@conn.messages.last[:data].instance_variable_get(:@requeue).should == nil
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'you can set requeue to true, prompting broker to requeue the messages (to other subscribers, potentially)' do
|
227
|
+
subject.recover true
|
228
|
+
@conn.messages.last[:data].instance_variable_get(:@requeue).should == true
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'returns MQ object itself, allowing for method chains' do
|
232
|
+
subject.recover.should == subject
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#close' do
|
237
|
+
it 'can be simplified, getting rid of @closing ivar? Just set callback sending Protocol::Channel::Close...'
|
238
|
+
|
239
|
+
it 'sends Protocol::Channel::Close through @connection' do
|
240
|
+
subject.close
|
241
|
+
@conn.messages.last[:data].should be_an AMQP::Protocol::Channel::Close
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'does not actually delete the channel or close connection' do
|
245
|
+
@conn.channels.should_not_receive(:delete)
|
246
|
+
@conn.should_not_receive(:close)
|
247
|
+
subject.close
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'actual channel closing happens ONLY when Protocol::Channel::CloseOk is received' do
|
251
|
+
@conn.channels.should_receive(:delete) { |key| key.should == 1; @conn.channels.clear }
|
252
|
+
@conn.should_receive(:close)
|
253
|
+
subject.process_frame AMQP::Frame::Method.new(AMQP::Protocol::Channel::CloseOk.new)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe '#get_queue' do
|
258
|
+
it 'yields a FIFO queue to a given block' do
|
259
|
+
subject.get_queue do |fifo|
|
260
|
+
fifo.should == []
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'FIFO queue contains consumers that called Queue#pop' do
|
265
|
+
queue = subject.queue('test')
|
266
|
+
queue.pop
|
267
|
+
queue.pop
|
268
|
+
subject.get_queue do |fifo|
|
269
|
+
fifo.should have(2).consumers
|
270
|
+
fifo.each { |consumer| consumer.should == queue }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'is Mutex-synchronized' do
|
275
|
+
subject # Evaluates subject, tripping add_channel Mutex at initialize
|
276
|
+
mutex = Mutex.new
|
277
|
+
Mutex.should_receive(:new).and_return(mutex)
|
278
|
+
mutex.should_receive(:synchronize)
|
279
|
+
subject.get_queue {}
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe '#connected?' do # This is an addition to standard MQ interface
|
284
|
+
it 'delegates to @connection to determine its connectivity status' do
|
285
|
+
@conn.should_receive(:connected?).with(no_args)
|
286
|
+
subject.connected?
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe 'setting up exchanges/queues/rpcs' do
|
291
|
+
describe '#rpc' do
|
292
|
+
it 'creates new rpc and saves it into @rpcs Hash' do
|
293
|
+
MQ::RPC.should_receive(:new).with(subject, 'name', nil).and_return('mock_rpc')
|
294
|
+
subject.rpcs.should_receive(:[]=).with('name', 'mock_rpc')
|
295
|
+
rpc = subject.rpc 'name'
|
296
|
+
rpc.should == 'mock_rpc'
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'raises error rpc if no name given' do
|
300
|
+
expect { subject.rpc }.to raise_error ArgumentError
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'does not replace rpc with existing name' do
|
304
|
+
MQ::RPC.should_receive(:new).with(subject, 'name', nil).and_return('mock_rpc')
|
305
|
+
subject.rpc 'name'
|
306
|
+
MQ::RPC.should_not_receive(:new)
|
307
|
+
subject.rpcs.should_not_receive(:[]=)
|
308
|
+
rpc = subject.rpc 'name'
|
309
|
+
rpc.should == 'mock_rpc'
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
describe '#queue' do
|
314
|
+
it 'creates new Queue and saves it into @queues Hash' do
|
315
|
+
MQ::Queue.should_receive(:new).with(subject, 'name', {}).and_return('mock_queue')
|
316
|
+
subject.queues.should_receive(:[]=).with('name', 'mock_queue')
|
317
|
+
queue = subject.queue 'name'
|
318
|
+
queue.should == 'mock_queue'
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'raises error Queue if no name given' do
|
322
|
+
expect { subject.queue }.to raise_error ArgumentError
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'does not replace queue with existing name' do
|
326
|
+
MQ::Queue.should_receive(:new).with(subject, 'name', {}).and_return('mock_queue')
|
327
|
+
subject.queue 'name'
|
328
|
+
MQ::Queue.should_not_receive(:new)
|
329
|
+
subject.queues.should_not_receive(:[]=)
|
330
|
+
queue = subject.queue 'name'
|
331
|
+
queue.should == 'mock_queue'
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
describe '#direct' do
|
336
|
+
it 'creates new :direct Exchange and saves it into @exchanges Hash' do
|
337
|
+
MQ::Exchange.should_receive(:new).with(subject, :direct, 'name', {}).and_return('mock_exchange')
|
338
|
+
subject.exchanges.should_receive(:[]=).with('name', 'mock_exchange')
|
339
|
+
exchange = subject.direct 'name'
|
340
|
+
exchange.should == 'mock_exchange'
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'creates "amq.direct" Exchange if no name given' do
|
344
|
+
MQ::Exchange.should_receive(:new).with(subject, :direct, 'amq.direct', {}).and_return('mock_exchange')
|
345
|
+
subject.exchanges.should_receive(:[]=).with('amq.direct', 'mock_exchange')
|
346
|
+
exchange = subject.direct
|
347
|
+
exchange.should == 'mock_exchange'
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'does not replace exchange with existing name' do
|
351
|
+
MQ::Exchange.should_receive(:new).with(subject, :direct, 'name', {}).and_return('mock_exchange')
|
352
|
+
subject.direct 'name'
|
353
|
+
MQ::Exchange.should_not_receive(:new)
|
354
|
+
subject.exchanges.should_not_receive(:[]=)
|
355
|
+
exchange = subject.direct 'name'
|
356
|
+
exchange.should == 'mock_exchange'
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
describe '#fanout' do
|
361
|
+
it 'creates new :fanout Exchange and saves it into @exchanges Hash' do
|
362
|
+
MQ::Exchange.should_receive(:new).with(subject, :fanout, 'name', {}).and_return('mock_exchange')
|
363
|
+
subject.exchanges.should_receive(:[]=).with('name', 'mock_exchange')
|
364
|
+
exchange = subject.fanout 'name'
|
365
|
+
exchange.should == 'mock_exchange'
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'creates "amq.fanout" Exchange if no name given' do
|
369
|
+
MQ::Exchange.should_receive(:new).with(subject, :fanout, 'amq.fanout', {}).and_return('mock_exchange')
|
370
|
+
subject.exchanges.should_receive(:[]=).with('amq.fanout', 'mock_exchange')
|
371
|
+
exchange = subject.fanout
|
372
|
+
exchange.should == 'mock_exchange'
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'does not replace exchange with existing name' do
|
376
|
+
MQ::Exchange.should_receive(:new).with(subject, :fanout, 'name', {}).and_return('mock_exchange')
|
377
|
+
subject.fanout 'name'
|
378
|
+
MQ::Exchange.should_not_receive(:new)
|
379
|
+
subject.exchanges.should_not_receive(:[]=)
|
380
|
+
exchange = subject.fanout 'name'
|
381
|
+
exchange.should == 'mock_exchange'
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
describe '#topic' do
|
386
|
+
it 'creates new :topic Exchange and saves it into @exchanges Hash' do
|
387
|
+
MQ::Exchange.should_receive(:new).with(subject, :topic, 'name', {}).and_return('mock_exchange')
|
388
|
+
subject.exchanges.should_receive(:[]=).with('name', 'mock_exchange')
|
389
|
+
exchange = subject.topic 'name'
|
390
|
+
exchange.should == 'mock_exchange'
|
391
|
+
end
|
392
|
+
|
393
|
+
it 'creates "amq.topic" Exchange if no name given' do
|
394
|
+
MQ::Exchange.should_receive(:new).with(subject, :topic, 'amq.topic', {}).and_return('mock_exchange')
|
395
|
+
subject.exchanges.should_receive(:[]=).with('amq.topic', 'mock_exchange')
|
396
|
+
exchange = subject.topic
|
397
|
+
exchange.should == 'mock_exchange'
|
398
|
+
end
|
399
|
+
|
400
|
+
it 'does not replace exchange with existing name' do
|
401
|
+
MQ::Exchange.should_receive(:new).with(subject, :topic, 'name', {}).and_return('mock_exchange')
|
402
|
+
subject.topic 'name'
|
403
|
+
MQ::Exchange.should_not_receive(:new)
|
404
|
+
subject.exchanges.should_not_receive(:[]=)
|
405
|
+
exchange = subject.topic 'name'
|
406
|
+
exchange.should == 'mock_exchange'
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
describe '#headers' do
|
411
|
+
it 'creates new :headers Exchange and saves it into @exchanges Hash' do
|
412
|
+
MQ::Exchange.should_receive(:new).with(subject, :headers, 'name', {}).and_return('mock_exchange')
|
413
|
+
subject.exchanges.should_receive(:[]=).with('name', 'mock_exchange')
|
414
|
+
exchange = subject.headers 'name'
|
415
|
+
exchange.should == 'mock_exchange'
|
416
|
+
end
|
417
|
+
|
418
|
+
it 'creates "amq.match" Exchange if no name given' do
|
419
|
+
MQ::Exchange.should_receive(:new).with(subject, :headers, 'amq.match', {}).and_return('mock_exchange')
|
420
|
+
subject.exchanges.should_receive(:[]=).with('amq.match', 'mock_exchange')
|
421
|
+
exchange = subject.headers
|
422
|
+
exchange.should == 'mock_exchange'
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'does not replace exchange with existing name' do
|
426
|
+
MQ::Exchange.should_receive(:new).with(subject, :headers, 'name', {}).and_return('mock_exchange')
|
427
|
+
subject.headers 'name'
|
428
|
+
MQ::Exchange.should_not_receive(:new)
|
429
|
+
subject.exchanges.should_not_receive(:[]=)
|
430
|
+
exchange = subject.headers 'name'
|
431
|
+
exchange.should == 'mock_exchange'
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|