cotton-tail 0.1.1

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
+ SHA256:
3
+ metadata.gz: 5a7811e3b76243326ce0e462653bc5f4a7d4b02c98c6a5cf6bf3fdaef747708e
4
+ data.tar.gz: 46ab061026f8c152f0ef767538f61281bb65c12959a9e59b7fee4eab280fd01e
5
+ SHA512:
6
+ metadata.gz: 41d5d78a2bea2832dbd0df1bc2c2f194ed48af0b267727a382d5bb2f23336ca9d2cebfbedee750cdbbd4e97f653945f3ea07d4300a4a36b02ea08f89fefb203d
7
+ data.tar.gz: 0657e3f6a68e9e8ac54251b1088081f3dd44d445a8846cf0ed7adbb1438c52d5a180b4583f849dca12fdc48ec735dd1ac648b3b482b35f00796def4843f4201f
checksums.yaml.gz.sig ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ .env
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ -I integration
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ Metrics/LineLength:
2
+ Max: 120
3
+ Exclude:
4
+ - 'lib/tasks/**/*'
5
+
6
+ Metrics/MethodLength:
7
+ Max: 20
8
+
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - 'spec/**/*'
12
+ - 'integration/**/*'
13
+ - '*.gemspec'
14
+ - 'examples/app.rb'
15
+
16
+ Style/Documentation:
17
+ Exclude:
18
+ - 'spec/**/*'
19
+ - 'integration/**/*'
20
+
21
+ Metrics/ModuleLength:
22
+ Exclude:
23
+ - 'spec/**/*'
24
+ - 'integration/**/*'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.1@cotton
data/Dockerfile ADDED
@@ -0,0 +1,21 @@
1
+ FROM ruby:alpine
2
+
3
+ # Use most recent versions of available packages
4
+ RUN sed -i -e 's/v[[:digit:]]\.[[:digit:]]/edge/g' /etc/apk/repositories
5
+
6
+ RUN apk update && apk add git yarn build-base
7
+
8
+ # create directory
9
+ WORKDIR /usr/src/cotton_tail
10
+
11
+ # copy Gemfile and lock
12
+ COPY Gemfile* ./
13
+ COPY cotton-tail.gemspec ./
14
+ COPY ./lib/cotton_tail/version.rb ./lib/cotton_tail/
15
+
16
+ RUN bundle install
17
+
18
+ # copy the contents to working directory
19
+ COPY . /usr/src/cotton_tail
20
+
21
+ CMD ash
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in cotton.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,83 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cotton-tail (0.1.1)
5
+ bunny (~> 2.12)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ amq-protocol (2.3.0)
11
+ ast (2.4.0)
12
+ benchmark-perf (0.4.0)
13
+ benchmark-trend (0.2.0)
14
+ bunny (2.12.0)
15
+ amq-protocol (~> 2.3, >= 2.3.0)
16
+ diff-lcs (1.3)
17
+ effin_utf8 (1.0)
18
+ faraday (0.13.1)
19
+ multipart-post (>= 1.2, < 3)
20
+ faraday_middleware (0.12.2)
21
+ faraday (>= 0.7.4, < 1.0)
22
+ hashie (3.6.0)
23
+ jaro_winkler (1.5.1)
24
+ multi_json (1.13.1)
25
+ multipart-post (2.0.0)
26
+ parallel (1.12.1)
27
+ parser (2.5.3.0)
28
+ ast (~> 2.4.0)
29
+ powerpack (0.1.2)
30
+ rabbitmq_http_api_client (1.9.1)
31
+ effin_utf8 (~> 1.0.0)
32
+ faraday (~> 0.13.0)
33
+ faraday_middleware (~> 0.12.0)
34
+ hashie (~> 3.5)
35
+ multi_json (~> 1.12)
36
+ rainbow (3.0.0)
37
+ rake (10.5.0)
38
+ rspec (3.8.0)
39
+ rspec-core (~> 3.8.0)
40
+ rspec-expectations (~> 3.8.0)
41
+ rspec-mocks (~> 3.8.0)
42
+ rspec-benchmark (0.4.0)
43
+ benchmark-perf (~> 0.4.0)
44
+ benchmark-trend (~> 0.2.0)
45
+ rspec (>= 3.0.0, < 4.0.0)
46
+ rspec-core (3.8.0)
47
+ rspec-support (~> 3.8.0)
48
+ rspec-expectations (3.8.2)
49
+ diff-lcs (>= 1.2.0, < 2.0)
50
+ rspec-support (~> 3.8.0)
51
+ rspec-its (1.2.0)
52
+ rspec-core (>= 3.0.0)
53
+ rspec-expectations (>= 3.0.0)
54
+ rspec-mocks (3.8.0)
55
+ diff-lcs (>= 1.2.0, < 2.0)
56
+ rspec-support (~> 3.8.0)
57
+ rspec-support (3.8.0)
58
+ rubocop (0.60.0)
59
+ jaro_winkler (~> 1.5.1)
60
+ parallel (~> 1.10)
61
+ parser (>= 2.5, != 2.5.1.1)
62
+ powerpack (~> 0.1)
63
+ rainbow (>= 2.2.2, < 4.0)
64
+ ruby-progressbar (~> 1.7)
65
+ unicode-display_width (~> 1.4.0)
66
+ ruby-progressbar (1.10.0)
67
+ unicode-display_width (1.4.0)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ bundler (~> 1.16)
74
+ cotton-tail!
75
+ rabbitmq_http_api_client (~> 1.9)
76
+ rake (~> 10.0)
77
+ rspec (~> 3.0)
78
+ rspec-benchmark (~> 0.4)
79
+ rspec-its (~> 1.2)
80
+ rubocop (~> 0.60)
81
+
82
+ BUNDLED WITH
83
+ 1.17.1
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # CottonTail
2
+
3
+ CottonTail provides a simple DSL for consuming messages from a RabbitMQ server
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cotton-tail'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install cotton-tail
20
+
21
+ ## Usage
22
+
23
+ ### Quick Start
24
+
25
+ You can look at the file `examples/app.rb` to see an example of what a CottonTail
26
+ App definition looks like.
27
+
28
+ To run the example locally you need to have a rabbitmq instance running. The
29
+ included `docker-compose` file can be used to start up a local instance of
30
+ rabbitmq.
31
+
32
+ `docker-compose up`
33
+
34
+ Once the rabbitmq service has completed startup (takes a few seconds) you can
35
+ start the example app.
36
+
37
+ `bundle exec examples/app.rb`
38
+
39
+ You should see
40
+
41
+ `Waiting for messages ...`
42
+
43
+ We've included bash scripts to publish messages for the example app. Execute them
44
+ in another terminal window to see output in the app window.
45
+
46
+ `examples/messages/say.hello`, `examples/messages/say.goodbye`, etc...
47
+
48
+ ## Development
49
+
50
+ After checking out the repo, install docker. Then, run `docker-compose` up to
51
+ spin up a local instance of rabbitmq. Run `rake spec:all` to run the tests.
52
+ You can also run `bin/console` for an interactive prompt that will allow you
53
+ to experiment.
54
+
55
+ ## Contributing
56
+
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jamesBrennan/cotton-tail.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ namespace :spec do
11
+ desc 'Run integration specs'
12
+ task :integration do
13
+ puts `docker-compose exec cotton-tail bundle exec rspec integration`
14
+ end
15
+
16
+ desc 'Run all specs'
17
+ task all: %w[spec spec:integration]
18
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cotton_tail'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/entrypoint.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ trap : TERM INT
3
+ tail -f /dev/null & wait
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDgDCCAmigAwIBAgIBATANBgkqhkiG9w0BAQUFADBDMRUwEwYDVQQDDAxicmVu
3
+ bmFubXVzaWMxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
4
+ A2NvbTAeFw0xODA3MDYwMDA2MzRaFw0xOTA3MDYwMDA2MzRaMEMxFTATBgNVBAMM
5
+ DGJyZW5uYW5tdXNpYzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
6
+ LGQBGRYDY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyxykNKqp
7
+ jD5gnQd2YHuRDNsLN5limGMDjSb38lMZAfJJugoWbSSeQM3+iVl26YWE5JeJpqDu
8
+ Rr/GOQ54bsev4IxCfPl8yIkhAao0i96+k1EeEb/i3pgZOfjXgGSTM71baCZzbqBH
9
+ A5ivcb0wqoHWyqrxNtc4XhSs5RtvtNl3IEe/bs5qqixt6PwsCorD6kAoUUNHNM9W
10
+ MLexivfiJooHOvVRmf1DQuJ2fDccKNHf7ZR4PuiguT4Z73bT2njCBm2o86o/clvR
11
+ WV0jasBOKvsgcg65U10klngBEG56nWSdIE9PrXGPqwBwA6jXttAwFLUNvuam5nGQ
12
+ V3MbCFDeHUUrtwIDAQABo38wfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNV
13
+ HQ4EFgQUPJGTSMevbRtDlY9WBrs21ae/+5YwIQYDVR0RBBowGIEWYnJlbm5hbm11
14
+ c2ljQGdtYWlsLmNvbTAhBgNVHRIEGjAYgRZicmVubmFubXVzaWNAZ21haWwuY29t
15
+ MA0GCSqGSIb3DQEBBQUAA4IBAQBr5Fm1ULBbLUYvX85iXI1Q7L7BZp53p9Q4TXrd
16
+ 0n0dE0qHJELwJQkErwZwMhOzaLqcctCwdzLkc9/VZSpLLuzv5bfEWP4EyTET5a+i
17
+ 4dn/Wko+6aNPVonmlHDhNYOPl3edxgxoD0WW08U7NJ4tGxJJURVv4yrCayT3xLCA
18
+ yzUUQaHKkLaXHSfH0yhha+pFpUlTsLeg9hrZ0jqSk4FzUrKbiieLY/f/p5Xr5QDg
19
+ fXe/xr/Sc+2wCjHPVE2J+auN5hk3KCp1I4s2fKqyLIwyhTEF3shuYfCpC8rt/YdN
20
+ cy9/lg5LCI3OvakzxL4Xt1Sq4h/xJZ06ydTVJ1wxfk6BXHrg
21
+ -----END CERTIFICATE-----
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'cotton_tail/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'cotton-tail'
9
+ spec.version = CottonTail::VERSION
10
+ spec.authors = ['James Brennan']
11
+ spec.email = ['brennanmusic@gmail.com']
12
+ spec.homepage = 'https://github.com/jamesBrennan/cotton-tail'
13
+
14
+ spec.summary = 'A simple multi-threaded amqp server'
15
+ spec.description = 'Simply and easily add AMQP messaging capabilities to
16
+ your services'
17
+
18
+ spec.license = 'MIT'
19
+
20
+ spec.cert_chain = ['certs/jamesbrennan.pem']
21
+ spec.signing_key = File.expand_path('~/.ssh/gem-private_key.pem') if $0 =~ /gem\z/
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
+ f.match(%r{^(test|spec|features|integration)/})
25
+ end
26
+
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ spec.add_dependency 'bunny', '~> 2.12'
32
+
33
+ spec.add_development_dependency 'bundler', '~> 1.16'
34
+ spec.add_development_dependency 'rabbitmq_http_api_client', '~> 1.9'
35
+ spec.add_development_dependency 'rake', '~> 10.0'
36
+ spec.add_development_dependency 'rspec', '~> 3.0'
37
+ spec.add_development_dependency 'rspec-benchmark', '~> 0.4'
38
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
39
+ spec.add_development_dependency 'rubocop', '~> 0.60'
40
+ end
@@ -0,0 +1,20 @@
1
+ version: '3.4'
2
+ services:
3
+ cotton-tail:
4
+ build: .
5
+ volumes:
6
+ - .:/usr/src/cotton-tail
7
+ entrypoint: bin/entrypoint.sh
8
+ links:
9
+ - rabbitmq
10
+ environment:
11
+ - AMQP_ADDRESS=amqp://rabbitmq:5672
12
+ - AMQP_MANAGER_URL=http://guest:guest@rabbitmq:15672
13
+ depends_on:
14
+ - rabbitmq
15
+
16
+ rabbitmq:
17
+ image: rabbitmq:3.7-management
18
+ ports:
19
+ - 5672:5672
20
+ - 15672:15672
data/examples/app.rb ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cotton_tail'
6
+
7
+ app = CottonTail::App.new.define do
8
+ # Create the queue 'hello_world_queue' if it does not exists
9
+ queue 'hello_world_queue', exclusive: true do
10
+ # Create a binding from the default topic exchange ('amq.topic') to
11
+ # the queue 'hello_world_queue'. When a message is received with the
12
+ # routing key 'say.hello' the block is executed.
13
+ handle 'say.hello' do
14
+ puts 'Hello world!'
15
+ end
16
+
17
+ handle 'say.goodbye' do
18
+ puts 'Goodbye cruel world!'
19
+ end
20
+
21
+ handle 'inspect.message' do |delivery_info, properties, message|
22
+ puts delivery_info: delivery_info
23
+ puts properties: properties
24
+ puts message: message
25
+ end
26
+ end
27
+
28
+ queue 'require_ack_queue', exclusive: true, manual_ack: true do
29
+ handle 'get.acked' do |delivery_info, _props, _msg, opts|
30
+ conn = opts[:conn]
31
+ delivery_tag = delivery_info[:delivery_tag]
32
+ puts "acking with #{delivery_tag}"
33
+ conn.ack(delivery_tag)
34
+ end
35
+
36
+ handle 'get.nacked' do |delivery_info, _props, _msg, opts|
37
+ conn = opts[:conn]
38
+ delivery_tag = delivery_info[:delivery_tag]
39
+ puts "nacking with #{delivery_tag}"
40
+ conn.nack(delivery_tag)
41
+ end
42
+ end
43
+ end
44
+
45
+ app.start
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ read -r -d '' MESSAGE << EOM
4
+ {
5
+ "properties":{},
6
+ "routing_key":"get.acked",
7
+ "payload": "ignored",
8
+ "payload_encoding": "string"
9
+ }
10
+ EOM
11
+
12
+ curl -i -XPOST -d"${MESSAGE}" http://guest:guest@localhost:15672/api/exchanges/%2f/amq.topic/publish
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ read -r -d '' MESSAGE << EOM
4
+ {
5
+ "properties":{},
6
+ "routing_key":"get.nacked",
7
+ "payload": "ignored",
8
+ "payload_encoding": "string"
9
+ }
10
+ EOM
11
+
12
+ curl -i -XPOST -d"${MESSAGE}" http://guest:guest@localhost:15672/api/exchanges/%2f/amq.topic/publish
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ read -r -d '' MESSAGE << EOM
4
+ {
5
+ "properties":{},
6
+ "routing_key":"inspect.message",
7
+ "payload": "this is the message message body",
8
+ "payload_encoding": "string"
9
+ }
10
+ EOM
11
+
12
+ curl -i -XPOST -d"${MESSAGE}" http://guest:guest@localhost:15672/api/exchanges/%2f/amq.topic/publish
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ read -r -d '' MESSAGE << EOM
4
+ {
5
+ "properties":{},
6
+ "routing_key":"say.goodbye",
7
+ "payload": "ignored",
8
+ "payload_encoding": "string"
9
+ }
10
+ EOM
11
+
12
+ curl -i -XPOST -d"${MESSAGE}" http://guest:guest@localhost:15672/api/exchanges/%2f/amq.topic/publish
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ read -r -d '' MESSAGE << EOM
4
+ {
5
+ "properties":{},
6
+ "routing_key":"say.hello",
7
+ "payload": "ignored",
8
+ "payload_encoding": "string"
9
+ }
10
+ EOM
11
+
12
+ curl -i -XPOST -d"${MESSAGE}" http://guest:guest@localhost:15672/api/exchanges/%2f/amq.topic/publish
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ # App is the main class for a CottonTail server
5
+ class App
6
+ def initialize(queue_strategy: Queue::Bunny, routing_strategy: Router)
7
+ @dependencies = {
8
+ queue_strategy: queue_strategy,
9
+ routing_strategy: routing_strategy
10
+ }
11
+ end
12
+
13
+ # Define message routing
14
+ def define(&block)
15
+ @definition = DSL::App.new(**@dependencies)
16
+ @definition.instance_eval(&block)
17
+ self
18
+ end
19
+
20
+ def queues
21
+ @definition.queues
22
+ end
23
+
24
+ # Get a single message queue
25
+ def queue(name)
26
+ queues[name]
27
+ end
28
+
29
+ # Start the app, process all pending messages, and then shutdown
30
+ def run
31
+ supervisors.map(&:run).each(&:join)
32
+ end
33
+
34
+ def start
35
+ supervisors.map(&:start)
36
+ puts 'Waiting for messages ...'
37
+
38
+ sleep 0.1 while running?
39
+ end
40
+
41
+ private
42
+
43
+ def supervisors
44
+ @supervisors ||= queues.map do |_name, queue|
45
+ Queue::Supervisor.new(queue, on_message: @definition.router)
46
+ end
47
+ end
48
+
49
+ def running?
50
+ supervisors.any?(&:running?)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ module DSL
5
+ # This is the top level DSL for defining the bindings and message routing of
6
+ # a cotton App
7
+ class App
8
+ attr_reader :queues
9
+
10
+ def initialize(queue_strategy:, routing_strategy:)
11
+ @queue_strategy = queue_strategy
12
+ @routing_strategy = routing_strategy
13
+ @queues = {}
14
+ end
15
+
16
+ # Define a new queue
17
+ def queue(name, **opts, &block)
18
+ @queue_strategy.call(name: name, **opts).tap do |queue_instance|
19
+ @queues[name] = queue_instance
20
+ queue_dsl = Queue.new(name, queue_instance, self)
21
+ queue_dsl.instance_eval(&block) if block_given?
22
+ end
23
+ end
24
+
25
+ # Creates a scope for nested bindings
26
+ #
27
+ # @example
28
+ # topic 'some.resource' do
29
+ # handle 'event.updated', lambda do
30
+ # puts "I'm bound to some.resource.event.updated"
31
+ # end
32
+ # end
33
+ #
34
+ # @param [String] routing_prefix The first part of the routing_key
35
+ def topic(routing_prefix, &block)
36
+ topic = Topic.new(routing_prefix, self)
37
+ topic.instance_eval(&block)
38
+ end
39
+
40
+ def handle(key, handler = nil, &block)
41
+ handler ||= block
42
+ router.route key, handler
43
+ end
44
+
45
+ def router
46
+ @router ||= @routing_strategy.call
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ module DSL
5
+ # Queue DSL
6
+ class Queue
7
+ def initialize(name, queue, context)
8
+ @name = name
9
+ @queue = queue
10
+ @context = context
11
+ end
12
+
13
+ def handle(key, handler = nil, &block)
14
+ bind(key)
15
+ @context.handle(key, handler, &block)
16
+ end
17
+
18
+ def topic(routing_prefix, &block)
19
+ topic = Topic.new(routing_prefix, self)
20
+ topic.instance_eval(&block)
21
+ end
22
+
23
+ private
24
+
25
+ def bind(key)
26
+ return unless @queue.respond_to?(:bind)
27
+
28
+ @queue.bind key
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ module DSL
5
+ # Topic DSL
6
+ class Topic
7
+ def initialize(routing_prefix, context)
8
+ @routing_prefix = routing_prefix
9
+ @context = context
10
+ end
11
+
12
+ def handle(routing_suffix, klass)
13
+ key = routing_key(routing_suffix)
14
+ @context.instance_eval { handle key, klass }
15
+ end
16
+
17
+ private
18
+
19
+ def routing_key(suffix)
20
+ [@routing_prefix, suffix].join('.')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ # DSL namespace
5
+ module DSL
6
+ autoload :App, 'cotton_tail/dsl/app'
7
+ autoload :Queue, 'cotton_tail/dsl/queue'
8
+ autoload :Topic, 'cotton_tail/dsl/topic'
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ module Queue
5
+ # A wrapper around a ::Bunny::Queue that makes it interchangeable with a
6
+ # standard Ruby::Queue
7
+ class Bunny
8
+ def self.call(name:, **opts)
9
+ new(name, **opts)
10
+ end
11
+
12
+ def initialize(name, conn: Connection.new, prefetch: 1, manual_ack: false, **opts)
13
+ @name = name
14
+ @prefetch = prefetch
15
+ @manual_ack = manual_ack
16
+ @conn = conn
17
+ @closed = false
18
+ @queue = conn.queue(@name, **opts)
19
+ end
20
+
21
+ def bind(routing_key)
22
+ @queue.bind('amq.topic', routing_key: routing_key)
23
+ end
24
+
25
+ def push(args)
26
+ routing_key, message = args
27
+ @conn.publish message, routing_key: routing_key
28
+ end
29
+
30
+ def close
31
+ @closed = true
32
+ end
33
+
34
+ def closed?
35
+ @closed
36
+ end
37
+
38
+ def empty?
39
+ @queue.message_count.zero?
40
+ end
41
+
42
+ def pop
43
+ return if empty?
44
+
45
+ delivery_info, *tail = @queue.pop(manual_ack: @manual_ack)
46
+ [delivery_info[:routing_key], delivery_info, *tail, conn: @conn]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'bunny'
5
+
6
+ module CottonTail
7
+ module Queue
8
+ # Wrapper for Bunny::Session
9
+ class Connection
10
+ extend Forwardable
11
+
12
+ def initialize(url = ENV.fetch('AMQP_ADDRESS', 'amqp://localhost:5672'))
13
+ @url = url
14
+ end
15
+
16
+ def chan(prefetch = 1)
17
+ @channels ||= Hash.new do |h, key|
18
+ h[key] = session.create_channel.tap do |ch|
19
+ ch.prefetch(prefetch)
20
+ end
21
+ end
22
+ @channels[prefetch]
23
+ end
24
+
25
+ def exchange
26
+ @exchange ||= chan.exchange('amq.topic')
27
+ end
28
+
29
+ def session
30
+ @session ||= ::Bunny.new(@url).tap(&:start)
31
+ end
32
+
33
+ def_delegators :chan, :ack, :nack, :queue
34
+ def_delegators :exchange, :publish
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ module Queue
5
+ # Simple in-memory queue
6
+ class Memory
7
+ def self.call(**_kwargs)
8
+ ::Queue.new
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module CottonTail
6
+ module Queue
7
+ # Queue Reader
8
+ class Reader
9
+ def self.spawn(queue, **kwargs)
10
+ Thread.new { new(queue, **kwargs).start }
11
+ end
12
+
13
+ def initialize(queue, on_message:)
14
+ @queue = queue
15
+ @on_message = on_message
16
+ end
17
+
18
+ def fiber
19
+ @fiber ||= Fiber.new do
20
+ Fiber.yield @queue.pop until @queue.empty? && @queue.closed?
21
+ end
22
+ end
23
+
24
+ def start
25
+ while fiber.alive?
26
+ args = fiber.resume
27
+ @on_message.call(*args) if args
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ module CottonTail
6
+ module Queue
7
+ # A supervisor for a single queue
8
+ class Supervisor
9
+ def initialize(queue, on_message:)
10
+ @queue = queue
11
+ @on_message = on_message
12
+ end
13
+
14
+ def start
15
+ process
16
+ end
17
+
18
+ # Start the supervisor, process all pending messages, and then stop
19
+ def run
20
+ @queue.close
21
+ start.tap(&:join)
22
+ end
23
+
24
+ def running?
25
+ true & process.status
26
+ end
27
+
28
+ private
29
+
30
+ def process
31
+ @process ||= Reader.spawn(@queue, on_message: @on_message)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ # Queue namespace
5
+ module Queue
6
+ autoload :Bunny, 'cotton_tail/queue/bunny'
7
+ autoload :Connection, 'cotton_tail/queue/connection'
8
+ autoload :Memory, 'cotton_tail/queue/memory'
9
+ autoload :Reader, 'cotton_tail/queue/reader'
10
+ autoload :Supervisor, 'cotton_tail/queue/supervisor'
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ # Register message handlers and dispatch messages to them
5
+ class Router
6
+ def self.call
7
+ new
8
+ end
9
+
10
+ def initialize
11
+ @handlers = {}
12
+ end
13
+
14
+ def route(key, handler)
15
+ @handlers[key] = handler
16
+ end
17
+
18
+ def dispatch(key, *args)
19
+ @handlers[key].call(*args)
20
+ end
21
+
22
+ alias call dispatch
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CottonTail
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Top level namespace for CottonTail
4
+ module CottonTail
5
+ autoload :App, 'cotton_tail/app'
6
+ autoload :DSL, 'cotton_tail/dsl'
7
+ autoload :Queue, 'cotton_tail/queue'
8
+ autoload :Router, 'cotton_tail/router'
9
+ autoload :Version, 'cotton_tail/version'
10
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,214 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cotton-tail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - James Brennan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDgDCCAmigAwIBAgIBATANBgkqhkiG9w0BAQUFADBDMRUwEwYDVQQDDAxicmVu
14
+ bmFubXVzaWMxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
15
+ A2NvbTAeFw0xODA3MDYwMDA2MzRaFw0xOTA3MDYwMDA2MzRaMEMxFTATBgNVBAMM
16
+ DGJyZW5uYW5tdXNpYzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
17
+ LGQBGRYDY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyxykNKqp
18
+ jD5gnQd2YHuRDNsLN5limGMDjSb38lMZAfJJugoWbSSeQM3+iVl26YWE5JeJpqDu
19
+ Rr/GOQ54bsev4IxCfPl8yIkhAao0i96+k1EeEb/i3pgZOfjXgGSTM71baCZzbqBH
20
+ A5ivcb0wqoHWyqrxNtc4XhSs5RtvtNl3IEe/bs5qqixt6PwsCorD6kAoUUNHNM9W
21
+ MLexivfiJooHOvVRmf1DQuJ2fDccKNHf7ZR4PuiguT4Z73bT2njCBm2o86o/clvR
22
+ WV0jasBOKvsgcg65U10klngBEG56nWSdIE9PrXGPqwBwA6jXttAwFLUNvuam5nGQ
23
+ V3MbCFDeHUUrtwIDAQABo38wfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNV
24
+ HQ4EFgQUPJGTSMevbRtDlY9WBrs21ae/+5YwIQYDVR0RBBowGIEWYnJlbm5hbm11
25
+ c2ljQGdtYWlsLmNvbTAhBgNVHRIEGjAYgRZicmVubmFubXVzaWNAZ21haWwuY29t
26
+ MA0GCSqGSIb3DQEBBQUAA4IBAQBr5Fm1ULBbLUYvX85iXI1Q7L7BZp53p9Q4TXrd
27
+ 0n0dE0qHJELwJQkErwZwMhOzaLqcctCwdzLkc9/VZSpLLuzv5bfEWP4EyTET5a+i
28
+ 4dn/Wko+6aNPVonmlHDhNYOPl3edxgxoD0WW08U7NJ4tGxJJURVv4yrCayT3xLCA
29
+ yzUUQaHKkLaXHSfH0yhha+pFpUlTsLeg9hrZ0jqSk4FzUrKbiieLY/f/p5Xr5QDg
30
+ fXe/xr/Sc+2wCjHPVE2J+auN5hk3KCp1I4s2fKqyLIwyhTEF3shuYfCpC8rt/YdN
31
+ cy9/lg5LCI3OvakzxL4Xt1Sq4h/xJZ06ydTVJ1wxfk6BXHrg
32
+ -----END CERTIFICATE-----
33
+ date: 2018-10-31 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: bunny
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.12'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.12'
49
+ - !ruby/object:Gem::Dependency
50
+ name: bundler
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.16'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.16'
63
+ - !ruby/object:Gem::Dependency
64
+ name: rabbitmq_http_api_client
65
+ requirement: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.9'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.9'
77
+ - !ruby/object:Gem::Dependency
78
+ name: rake
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '10.0'
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.0'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '3.0'
105
+ - !ruby/object:Gem::Dependency
106
+ name: rspec-benchmark
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '0.4'
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '0.4'
119
+ - !ruby/object:Gem::Dependency
120
+ name: rspec-its
121
+ requirement: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '1.2'
126
+ type: :development
127
+ prerelease: false
128
+ version_requirements: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '1.2'
133
+ - !ruby/object:Gem::Dependency
134
+ name: rubocop
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '0.60'
140
+ type: :development
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '0.60'
147
+ description: |-
148
+ Simply and easily add AMQP messaging capabilities to
149
+ your services
150
+ email:
151
+ - brennanmusic@gmail.com
152
+ executables: []
153
+ extensions: []
154
+ extra_rdoc_files: []
155
+ files:
156
+ - ".gitignore"
157
+ - ".rspec"
158
+ - ".rubocop.yml"
159
+ - ".ruby-version"
160
+ - Dockerfile
161
+ - Gemfile
162
+ - Gemfile.lock
163
+ - README.md
164
+ - Rakefile
165
+ - bin/console
166
+ - bin/entrypoint.sh
167
+ - certs/jamesbrennan.pem
168
+ - cotton-tail.gemspec
169
+ - docker-compose.yml
170
+ - examples/app.rb
171
+ - examples/messages/get.acked
172
+ - examples/messages/get.nacked
173
+ - examples/messages/inspect.message
174
+ - examples/messages/say.goodbye
175
+ - examples/messages/say.hello
176
+ - lib/cotton_tail.rb
177
+ - lib/cotton_tail/app.rb
178
+ - lib/cotton_tail/dsl.rb
179
+ - lib/cotton_tail/dsl/app.rb
180
+ - lib/cotton_tail/dsl/queue.rb
181
+ - lib/cotton_tail/dsl/topic.rb
182
+ - lib/cotton_tail/queue.rb
183
+ - lib/cotton_tail/queue/bunny.rb
184
+ - lib/cotton_tail/queue/connection.rb
185
+ - lib/cotton_tail/queue/memory.rb
186
+ - lib/cotton_tail/queue/reader.rb
187
+ - lib/cotton_tail/queue/supervisor.rb
188
+ - lib/cotton_tail/router.rb
189
+ - lib/cotton_tail/version.rb
190
+ homepage: https://github.com/jamesBrennan/cotton-tail
191
+ licenses:
192
+ - MIT
193
+ metadata: {}
194
+ post_install_message:
195
+ rdoc_options: []
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ requirements: []
209
+ rubyforge_project:
210
+ rubygems_version: 2.7.6
211
+ signing_key:
212
+ specification_version: 4
213
+ summary: A simple multi-threaded amqp server
214
+ test_files: []
metadata.gz.sig ADDED
Binary file