mercury_amqp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +5 -0
- data/README.md +85 -0
- data/Rakefile +9 -0
- data/lib/mercury/cps/methods.rb +23 -0
- data/lib/mercury/cps/seq.rb +23 -0
- data/lib/mercury/cps/seq_with_let.rb +67 -0
- data/lib/mercury/cps.rb +97 -0
- data/lib/mercury/fake/domain.rb +9 -0
- data/lib/mercury/fake/metadata.rb +24 -0
- data/lib/mercury/fake/queue.rb +80 -0
- data/lib/mercury/fake/queued_message.rb +17 -0
- data/lib/mercury/fake/subscriber.rb +13 -0
- data/lib/mercury/fake.rb +104 -0
- data/lib/mercury/mercury.rb +186 -0
- data/lib/mercury/monadic.rb +41 -0
- data/lib/mercury/received_message.rb +30 -0
- data/lib/mercury/sync.rb +20 -0
- data/lib/mercury/test_utils.rb +59 -0
- data/lib/mercury/utils.rb +9 -0
- data/lib/mercury/version.rb +3 -0
- data/lib/mercury/wire_serializer.rb +54 -0
- data/lib/mercury.rb +1 -0
- data/mercury_amqp.gemspec +33 -0
- data/spec/lib/mercury/cps_spec.rb +225 -0
- data/spec/lib/mercury/mercury_spec.rb +87 -0
- data/spec/lib/mercury/monadic_spec.rb +313 -0
- data/spec/lib/mercury/sync_spec.rb +33 -0
- data/spec/lib/mercury/utils_spec.rb +17 -0
- data/spec/lib/mercury/wire_serializer_spec.rb +29 -0
- data/spec/spec_helper.rb +39 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 22461cdd32d965f465191c9f4270ea23724d9d14
|
4
|
+
data.tar.gz: 4934514f73a40ee799e174eba6dabe11add998c4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de0a9fa90882b011bdd0011055f2546ad1a7b6d0ed9deadc5e0c7c3c7f759a5824267fa9e6c9c75a834b23f3413be306de6b303766c5e024af32a85f4c750225
|
7
|
+
data.tar.gz: 639003755c8e26bbdcf98b66178d1d32cd91a2c8b7ba4b6efb1a98996915a7715381077d06f895d892358edf719e5975da7ae34e77401cb531d62b85a4447a8b
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
mercury
|
2
|
+
=======
|
3
|
+
|
4
|
+
Mercury is a messaging layer intended to hide complexity for typical
|
5
|
+
messaging scenarios. It is backed by the AMQP gem and consequently
|
6
|
+
runs in an EventMachine reactor and has an asynchronous API. Mercury
|
7
|
+
consists of _sources_, _work queues_, and _listeners_. A message is
|
8
|
+
published to a source, to which one or more work queues and/or
|
9
|
+
listeners are attached. These map roughly to AMQP constructs:
|
10
|
+
|
11
|
+
- **source**: topic exchange
|
12
|
+
- **work queue**: durable named queue
|
13
|
+
- **listener**: temporary anonymous queue
|
14
|
+
- **tag**: routing key
|
15
|
+
|
16
|
+
At the moment, mercury is backed by AMQP and serializes messages as
|
17
|
+
JSON. In the future, additional transports and message formats may be
|
18
|
+
supported.
|
19
|
+
|
20
|
+
Mercury writes string messages directly without encoding; this allows
|
21
|
+
a client to pre-encode a message using an arbitrary encoding. The
|
22
|
+
receiving client receives the encoded bytes as the message content
|
23
|
+
(assuming the encoded message fails to parse as JSON).
|
24
|
+
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'mercury'
|
28
|
+
|
29
|
+
def run
|
30
|
+
EventMachine.run do
|
31
|
+
Mercury.open do |m|
|
32
|
+
m.start_worker('cooks', 'orders', method(:handle_message)) do
|
33
|
+
m.publish('orders', {'table' => 5, 'items' => ['salad', 'steak', 'cake']})
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_message(msg)
|
40
|
+
order = msg.content
|
41
|
+
cook(order)
|
42
|
+
msg.ack
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Notably, mercury also has a monadic interface that hides the explicit
|
47
|
+
continuation passing introduced by asynchrony, which has the effect of
|
48
|
+
flattening chained calls. This is particularly useful for testing,
|
49
|
+
where the same code plays both sides of a conversation. Compare:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require 'mercury'
|
53
|
+
|
54
|
+
Mercury.open do |m|
|
55
|
+
m.start_listener(source, proc{}) do
|
56
|
+
m.source_exists?(source) do |r1|
|
57
|
+
expect(r1).to be true
|
58
|
+
m.delete_source(source) do
|
59
|
+
m.source_exists?(source) do |r2|
|
60
|
+
expect(r2).to be false
|
61
|
+
m.close do
|
62
|
+
done
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# ... vs ...
|
71
|
+
|
72
|
+
require 'mercury/monadic'
|
73
|
+
|
74
|
+
seq do
|
75
|
+
let(:m) { Mercury::Monadic.open }
|
76
|
+
and_then { m.start_listener(source) }
|
77
|
+
let(:r1) { m.source_exists?(source) }
|
78
|
+
and_lift { expect(r1).to be true }
|
79
|
+
and_then { m.delete_source(source) }
|
80
|
+
let(:r2) { m.source_exists?(source) }
|
81
|
+
and_lift { expect(r2).to be false }
|
82
|
+
and_then { m.close }
|
83
|
+
and_lift { done }
|
84
|
+
end
|
85
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
class Cps
|
2
|
+
module Methods
|
3
|
+
def lift(&block)
|
4
|
+
Cps.lift(&block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def seq(&block)
|
8
|
+
Cps.seq(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def seql(&block)
|
12
|
+
Cps.seql(2, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def seqp(&block)
|
16
|
+
Cps.seqp(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cps(&block)
|
20
|
+
Cps.new(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Cps
|
2
|
+
|
3
|
+
# Syntactic sugar for and_then chains.
|
4
|
+
def self.seq(&block)
|
5
|
+
s = Seq.new
|
6
|
+
block.call(s.method(:chain))
|
7
|
+
s.m
|
8
|
+
end
|
9
|
+
|
10
|
+
class Seq
|
11
|
+
def m
|
12
|
+
@m ||= Cps.identity # we need an initial Cps to chain onto
|
13
|
+
end
|
14
|
+
|
15
|
+
def chain(proc=nil, &block)
|
16
|
+
@m = m.and_then(&(proc || block))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Method
|
22
|
+
alias_method :en, :call # to be called `th.en`
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'binding_of_caller'
|
2
|
+
|
3
|
+
class Cps
|
4
|
+
# Syntactic sugar for and_then chains.
|
5
|
+
def self.seql(depth=1, &block)
|
6
|
+
# EXPERIMENTAL
|
7
|
+
# The trick here is to execute the block in a context where
|
8
|
+
# 1. we can simulate local let-bound variables, and
|
9
|
+
# 2. the block can access variables and methods available
|
10
|
+
# outside the call to seql.
|
11
|
+
#
|
12
|
+
# To achieve this, we instance_exec the block in a SeqWithLet
|
13
|
+
# object, which provides the let bound variables (as methods)
|
14
|
+
# and uses method_missing to proxy other methods to the parent
|
15
|
+
# binding.
|
16
|
+
#
|
17
|
+
# Note: parent instance variables are not available inside the block.
|
18
|
+
# Note: keyword arguments are not proxied to methods called in the parent binding
|
19
|
+
context = SeqWithLet.new(binding.of_caller(depth))
|
20
|
+
context.instance_exec(&block)
|
21
|
+
context.__chain
|
22
|
+
end
|
23
|
+
|
24
|
+
class SeqWithLet
|
25
|
+
|
26
|
+
def and_then(&block)
|
27
|
+
@__chain = __chain.and_then(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def and_lift(&block)
|
31
|
+
@__chain = __chain.and_lift(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def let(name, &block)
|
35
|
+
and_then(&block)
|
36
|
+
and_then do |value|
|
37
|
+
__values[name] = value
|
38
|
+
Cps.lift{value}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(parent_binding)
|
43
|
+
@__parent_binding = parent_binding
|
44
|
+
end
|
45
|
+
|
46
|
+
def __chain
|
47
|
+
@__chain ||= Cps.identity
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(name, *args, &block)
|
51
|
+
__values.fetch(name) { __parent_call(name.to_s, *args, &block) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def __parent_call(name, *args, &block)
|
55
|
+
@__parent_caller ||= @__parent_binding.eval <<-EOD
|
56
|
+
proc do |name, *args, &block|
|
57
|
+
send(name, *args, &block)
|
58
|
+
end
|
59
|
+
EOD
|
60
|
+
@__parent_caller.call(name, *args, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def __values
|
64
|
+
@__values ||= {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/mercury/cps.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'mercury/cps/seq'
|
2
|
+
require 'mercury/cps/seq_with_let'
|
3
|
+
require 'mercury/cps/methods'
|
4
|
+
require 'mercury/utils'
|
5
|
+
|
6
|
+
# Async IO often involves CPS (continuation-passing style)
|
7
|
+
# code, where the continuation (a.k.a. "callback") is passed
|
8
|
+
# to a function to be invoked at a later time. CPS style
|
9
|
+
# can result in deep lexical nesting making code difficult
|
10
|
+
# to read.
|
11
|
+
# This monad hides the CPS plumbing, which allows code
|
12
|
+
# to be written in a flat style with no visible continuation
|
13
|
+
# passing. It basically wraps a CPS proc with methods for
|
14
|
+
# composing them.
|
15
|
+
# See http://codon.com/refactoring-ruby-with-monads
|
16
|
+
class Cps
|
17
|
+
attr_reader :cps
|
18
|
+
|
19
|
+
# @param [Proc] cps a CPS proc (signature: *args, &k)
|
20
|
+
def initialize(&cps)
|
21
|
+
@cps = cps
|
22
|
+
end
|
23
|
+
|
24
|
+
# Applies the wrapped proc. If the CPS return value
|
25
|
+
# is not needed, the continuation k may be omitted.
|
26
|
+
# Returns the return value of the continuation.
|
27
|
+
def run(*args, &k)
|
28
|
+
k ||= proc { |x| x }
|
29
|
+
cps.call(*args, &k)
|
30
|
+
end
|
31
|
+
|
32
|
+
# The "bind" operation; composes two Cps
|
33
|
+
# @param [Proc] pm a proc that takes the output of this
|
34
|
+
# Cps and returns a Cps
|
35
|
+
def and_then(&pm)
|
36
|
+
Cps.new do |*args, &k|
|
37
|
+
self.run(*args) do |*args2|
|
38
|
+
next_cps = pm.call(*args2)
|
39
|
+
next_cps.is_a?(Cps) or raise "'and_then' block did not return a Cps. Did you want 'and_lift'? at #{pm.source_location}"
|
40
|
+
next_cps.run(&k)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# equivalent to: and_then { lift { ... } }
|
46
|
+
def and_lift(&p)
|
47
|
+
and_then do |*args|
|
48
|
+
Cps.lift { p.call(*args) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a Cps for a non-CPS proc.
|
53
|
+
def self.lift(&p)
|
54
|
+
new { |*args, &k| k.call(p.call(*args)) }
|
55
|
+
end
|
56
|
+
|
57
|
+
# The identity function as a Cps.
|
58
|
+
def self.identity
|
59
|
+
new { |*args, &k| k.call(*args) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a Cps that executes the provided Cpses concurrently.
|
63
|
+
# Once all complete, their return values are passed to the continuation
|
64
|
+
# in an array with positions corresponding to the provided Cpses.
|
65
|
+
def self.concurrently(*cpss)
|
66
|
+
cpss = Utils.unsplat(cpss)
|
67
|
+
|
68
|
+
Cps.new do |*in_args, &k|
|
69
|
+
pending_completions = cpss
|
70
|
+
returned_args = []
|
71
|
+
cpss.each_with_index do |cps, i|
|
72
|
+
cps.run(*in_args) do |*out_args|
|
73
|
+
returned_args[i] = out_args
|
74
|
+
pending_completions.delete(cps)
|
75
|
+
if pending_completions.none?
|
76
|
+
k.call(returned_args)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Calls and_then for each x.
|
84
|
+
# @yieldparam x [Object] An item from xs
|
85
|
+
# @yieldparam *args [Objects] The value(s) passed from the last action
|
86
|
+
# @yieldreturn [Cps] The next action to add to the chain
|
87
|
+
def inject(xs, &block)
|
88
|
+
xs.inject(self) do |chain, x|
|
89
|
+
chain.and_then { |*args| block.call(x, *args) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# equivalent to Cps.identity.inject(...)
|
94
|
+
def self.inject(xs, &block)
|
95
|
+
Cps.identity.inject(xs, &block)
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Mercury
|
2
|
+
class Fake
|
3
|
+
class Metadata
|
4
|
+
def initialize(tag, dequeue, requeue)
|
5
|
+
@tag = tag
|
6
|
+
@dequeue = dequeue
|
7
|
+
@requeue = requeue
|
8
|
+
end
|
9
|
+
|
10
|
+
def routing_key
|
11
|
+
@tag
|
12
|
+
end
|
13
|
+
|
14
|
+
def ack
|
15
|
+
@dequeue.call
|
16
|
+
end
|
17
|
+
|
18
|
+
def reject(opts)
|
19
|
+
requeue = opts[:requeue]
|
20
|
+
requeue ? @requeue.call : @dequeue.call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'mercury/fake/queued_message'
|
3
|
+
|
4
|
+
class Mercury
|
5
|
+
class Fake
|
6
|
+
class Queue
|
7
|
+
attr_reader :source, :worker
|
8
|
+
|
9
|
+
def initialize(source, tag_filter, worker, require_ack)
|
10
|
+
@source = source
|
11
|
+
@tag_filter = tag_filter
|
12
|
+
@worker = worker
|
13
|
+
@require_ack = require_ack
|
14
|
+
@msgs = []
|
15
|
+
@subscribers = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_subscriber(s)
|
19
|
+
subscribers << s
|
20
|
+
deliver # new subscriber probably wants a message
|
21
|
+
end
|
22
|
+
|
23
|
+
def enqueue(msg, tag)
|
24
|
+
msgs.push(QueuedMessage.new(self, msg, tag, @require_ack))
|
25
|
+
deliver # new message. someone probably wants it.
|
26
|
+
end
|
27
|
+
|
28
|
+
def ack_or_reject_message(msg)
|
29
|
+
msgs.delete(msg) or raise 'tried to delete message that was not in queue!!'
|
30
|
+
msg.subscriber.busy = false
|
31
|
+
deliver # a subscriber just freed up
|
32
|
+
end
|
33
|
+
|
34
|
+
def nack(msg)
|
35
|
+
msg.delivered = false
|
36
|
+
msg.subscriber.busy = false
|
37
|
+
deliver
|
38
|
+
end
|
39
|
+
|
40
|
+
def binds?(source_name, tag)
|
41
|
+
source_name == source && tag_match?(tag_filter, tag)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
attr_reader :msgs, :subscribers, :tag_filter
|
46
|
+
|
47
|
+
def tag_match?(filter, tag)
|
48
|
+
# for wildcard description, see https://www.rabbitmq.com/tutorials/tutorial-five-python.html
|
49
|
+
pattern = Regexp.new(filter.gsub('*', '[^\.]+').gsub('#', '.*?'))
|
50
|
+
pattern.match(tag)
|
51
|
+
end
|
52
|
+
|
53
|
+
def deliver
|
54
|
+
EM.next_tick do
|
55
|
+
if idle_subscribers.any? && undelivered.any?
|
56
|
+
msg = undelivered.first
|
57
|
+
subscriber = idle_subscribers.sample
|
58
|
+
if @require_ack
|
59
|
+
msg.delivered = true
|
60
|
+
subscriber.busy = true
|
61
|
+
else
|
62
|
+
msgs.delete(msg)
|
63
|
+
end
|
64
|
+
msg.subscriber = subscriber
|
65
|
+
subscriber.handler.call(msg.received_msg)
|
66
|
+
deliver # continue delivering
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def undelivered
|
72
|
+
msgs.reject(&:delivered)
|
73
|
+
end
|
74
|
+
|
75
|
+
def idle_subscribers
|
76
|
+
subscribers.reject(&:busy)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'mercury/fake/metadata'
|
2
|
+
require 'mercury/received_message'
|
3
|
+
|
4
|
+
class Mercury
|
5
|
+
class Fake
|
6
|
+
class QueuedMessage
|
7
|
+
attr_reader :received_msg
|
8
|
+
attr_accessor :delivered, :subscriber
|
9
|
+
|
10
|
+
def initialize(queue, msg, tag, is_ackable)
|
11
|
+
metadata = Metadata.new(tag, proc{queue.ack_or_reject_message(self)}, proc{queue.nack(self)})
|
12
|
+
@received_msg = ReceivedMessage.new(msg, metadata, is_ackable: is_ackable)
|
13
|
+
@delivered = false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/mercury/fake.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'delegate'
|
3
|
+
require 'mercury/received_message'
|
4
|
+
require 'mercury/fake/domain'
|
5
|
+
require 'mercury/fake/metadata'
|
6
|
+
require 'mercury/fake/queue'
|
7
|
+
require 'mercury/fake/queued_message'
|
8
|
+
require 'mercury/fake/subscriber'
|
9
|
+
|
10
|
+
# This class simulates Mercury without using the AMQP gem.
|
11
|
+
# It can be useful for unit testing code that uses Mercury.
|
12
|
+
# The domain concept allows different mercury instances to
|
13
|
+
# hit different virtual servers; this should rarely be needed.
|
14
|
+
# This class cannot simulate behavior of server disconnections,
|
15
|
+
# broken sockets, etc.
|
16
|
+
class Mercury
|
17
|
+
class Fake
|
18
|
+
def initialize(domain=:default)
|
19
|
+
@domain = Fake.domains[domain]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.domains
|
23
|
+
@domains ||= Hash.new { |h, k| h[k] = Domain.new }
|
24
|
+
end
|
25
|
+
|
26
|
+
def close(&k)
|
27
|
+
@closed = true
|
28
|
+
ret(k)
|
29
|
+
end
|
30
|
+
|
31
|
+
def publish(source_name, msg, tag: '', &k)
|
32
|
+
assert_not_closed
|
33
|
+
queues.values.select{|q| q.binds?(source_name, tag)}.each{|q| q.enqueue(roundtrip(msg), tag)}
|
34
|
+
ret(k)
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_listener(source_name, handler, tag_filter: '#', &k)
|
38
|
+
start_worker_or_listener(source_name, handler, tag_filter, &k)
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_worker(worker_group, source_name, handler, tag_filter: '#', &k)
|
42
|
+
start_worker_or_listener(source_name, handler, tag_filter, worker_group, &k)
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_worker_or_listener(source_name, handler, tag_filter, worker_group=nil, &k)
|
46
|
+
assert_not_closed
|
47
|
+
q = ensure_queue(source_name, tag_filter, !!worker_group, worker_group)
|
48
|
+
ret(k) # it's important we show the "start" operation finishing before delivery starts (in add_subscriber)
|
49
|
+
q.add_subscriber(Subscriber.new(handler))
|
50
|
+
end
|
51
|
+
private :start_worker_or_listener
|
52
|
+
|
53
|
+
def delete_source(source_name, &k)
|
54
|
+
assert_not_closed
|
55
|
+
queues.delete_if{|_k, v| v.source == source_name}
|
56
|
+
ret(k)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_work_queue(worker_group, &k)
|
60
|
+
assert_not_closed
|
61
|
+
queues.delete_if{|_k, v| v.worker == worker_group}
|
62
|
+
ret(k)
|
63
|
+
end
|
64
|
+
|
65
|
+
def source_exists?(source, &k)
|
66
|
+
built_in_sources = %w(direct topic fanout headers match rabbitmq.log rabbitmq.trace).map{|x| "amq.#{x}"}
|
67
|
+
ret(k, (queues.values.map(&:source) + built_in_sources).include?(source))
|
68
|
+
end
|
69
|
+
|
70
|
+
def queue_exists?(worker, &k)
|
71
|
+
ret(k, queues.values.map(&:worker).include?(worker))
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def queues
|
77
|
+
@domain.queues
|
78
|
+
end
|
79
|
+
|
80
|
+
def ret(k, value=nil)
|
81
|
+
EM.next_tick{k.call(value)} if k
|
82
|
+
end
|
83
|
+
|
84
|
+
def roundtrip(msg)
|
85
|
+
ws = WireSerializer.new
|
86
|
+
ws.read(ws.write(msg))
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_queue(source, tag_filter, require_ack, worker=nil)
|
90
|
+
worker ||= SecureRandom.uuid
|
91
|
+
queues.fetch(unique_queue_name(source, tag_filter, worker)) do |k|
|
92
|
+
queues[k] = Queue.new(source, tag_filter, worker, require_ack)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def unique_queue_name(source, tag_filter, worker)
|
97
|
+
[source, tag_filter, worker].join('^')
|
98
|
+
end
|
99
|
+
|
100
|
+
def assert_not_closed
|
101
|
+
raise 'connection is closed' if @closed
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|