zmqp 0.0.1
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.
- 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
|