mercury_amqp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ .idea
2
+ .bundle
3
+ Gemfile.lock
4
+ *~
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hyperion.gemspec
4
+ gemspec
5
+
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,9 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+ rescue LoadError
8
+ # no rspec available
9
+ end
@@ -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
@@ -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,9 @@
1
+ class Mercury
2
+ class Fake
3
+ class Domain
4
+ def queues
5
+ @queues ||= {}
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,13 @@
1
+ class Mercury
2
+ class Fake
3
+ class Subscriber
4
+ attr_reader :handler
5
+ attr_accessor :busy
6
+
7
+ def initialize(handler)
8
+ @handler = handler
9
+ @busy = false
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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