mercury_amqp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|