banter 0.4.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 +22 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +113 -0
- data/Rakefile +1 -0
- data/banter.gemspec +37 -0
- data/bin/start_subscribers +14 -0
- data/config/pubsub.yml +17 -0
- data/lib/banter.rb +47 -0
- data/lib/banter/cli.rb +94 -0
- data/lib/banter/configuration.rb +42 -0
- data/lib/banter/context.rb +70 -0
- data/lib/banter/db_logger.rb +52 -0
- data/lib/banter/exceptions/payload_validation_error.rb +4 -0
- data/lib/banter/logger.rb +49 -0
- data/lib/banter/logging.rb +42 -0
- data/lib/banter/message.rb +50 -0
- data/lib/banter/middleware.rb +14 -0
- data/lib/banter/publisher.rb +93 -0
- data/lib/banter/railtie.rb +27 -0
- data/lib/banter/server.rb +7 -0
- data/lib/banter/server/client_queue_listener.rb +56 -0
- data/lib/banter/server/rabbit_mq_subscriber.rb +75 -0
- data/lib/banter/server/subscriber_server.rb +84 -0
- data/lib/banter/subscriber.rb +100 -0
- data/lib/banter/version.rb +3 -0
- data/spec/banter/cli_spec.rb +145 -0
- data/spec/banter/server/client_queue_listener_spec.rb +76 -0
- data/spec/banter/server/client_worker_spec.rb +161 -0
- data/spec/banter/server/rabbitmq_subscriber_spec.rb +5 -0
- data/spec/config.yml +20 -0
- data/spec/logger_spec.rb +110 -0
- data/spec/message_spec.rb +65 -0
- data/spec/spec_helper.rb +49 -0
- metadata +261 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f98c954f1577641de0413fcdb61bd4de641159ee
|
4
|
+
data.tar.gz: cca0062d0018c28a2b6d3f1ff9951735cda37361
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c9df28a97f6ed9ba8764dd77002a5726092c8f60b5a20b3b3c89ed653f45699a4dc2def024b3f5471157dca3f39e65605850c3a73cafebf2db00c93ca675fbaf
|
7
|
+
data.tar.gz: 4d1abbfb632f94924f6e7cd69353032804cc2a3b2a6f72ee79099f6ce4528106773504b7987dd8bc49b84c57ba0fcae483e2982831321f0f9f9db25295529029
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
.project
|
7
|
+
.ruby-gemset
|
8
|
+
Gemfile.lock
|
9
|
+
InstalledFiles
|
10
|
+
_yardoc
|
11
|
+
coverage
|
12
|
+
doc/
|
13
|
+
lib/bundler/man
|
14
|
+
pkg
|
15
|
+
rdoc
|
16
|
+
spec/reports
|
17
|
+
test/tmp
|
18
|
+
test/version_tmp
|
19
|
+
tmp
|
20
|
+
.rspec
|
21
|
+
.idea
|
22
|
+
test_pubsub.log
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 The Honest Company
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# Banter
|
2
|
+
|
3
|
+
Simple Publishers and subscribers for Ruby using RabbitMQ
|
4
|
+
|
5
|
+
[](http://badge.fury.io/rb/banter)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'banter'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install banter
|
20
|
+
|
21
|
+
You also need to install RabbitMQ
|
22
|
+
|
23
|
+
$ brew install rabbitmq
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
There are two sides to this gem, publishers and subscribers
|
28
|
+
|
29
|
+
### Publishers
|
30
|
+
|
31
|
+
Publishing to a message is super simple
|
32
|
+
|
33
|
+
Banter.publish('user.created', { id: 3, name: 'Foo Bar', email: 'foobar@email.com')
|
34
|
+
|
35
|
+
Additionally you can setup a context that is passed down to the subscribers. This context can be set up in a `before_filter`
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class ApplicationController < ActionController::Base
|
39
|
+
before_filter :setup_pubsub_context
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def setup_pubsub_context
|
44
|
+
Banter::Context.setup_context
|
45
|
+
unique_id: request.uuid,
|
46
|
+
orig_ip_address: request.remote_ip,
|
47
|
+
application: "my_app/web:#{self.class.name.underscore}/#{action_name}",
|
48
|
+
user_id: current_user.try(:id)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Context is automatically cleared up by the gem for a Rails Application. Otherwise, you can call
|
54
|
+
`Banter::Context.clear!`
|
55
|
+
|
56
|
+
### Subscribers
|
57
|
+
|
58
|
+
You can declare a subscriber class as follows
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class UserWelcomeSubscriber < Banter::Subscriber
|
62
|
+
subscribe_to "user_created" # The message prefix that you're subscribing to
|
63
|
+
# subscribe_to "user_created", on: 'welcome_emails_queue' # If you want to specify the queue name
|
64
|
+
|
65
|
+
def perform(payload)
|
66
|
+
# My awesome logic goes here...
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
You also run subscribers in a separate process to consume the message using the provided executable
|
72
|
+
|
73
|
+
`bundle exec start_subscribers`
|
74
|
+
|
75
|
+
You can execute `bundle exec start_subscribers --help` to see all the various options that it providers
|
76
|
+
```
|
77
|
+
Usage: bundle exec start_subscribers [options]
|
78
|
+
-P, --pidfile PATH path to pidfile
|
79
|
+
-o, --only [SUBSCRIBERS] comma separated name of subsriber classes that should be run
|
80
|
+
-r, --require [PATH|DIR] Location of Rails application with workers or file to require
|
81
|
+
-v, --version Print version and exit
|
82
|
+
|
83
|
+
```
|
84
|
+
|
85
|
+
#### Validations
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class UserWelcomeSubscriber < Banter::Subscriber
|
89
|
+
subscribe_to "user_created" # The message prefix that you're subscribing to
|
90
|
+
validates_payload_with :id_present
|
91
|
+
validates_payload_with do |payload|
|
92
|
+
payload[:email].present?
|
93
|
+
end
|
94
|
+
|
95
|
+
def perform(payload)
|
96
|
+
# My awesome logic goes here...
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def id_present(payload)
|
102
|
+
payload[:id].present?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
1. Fork it
|
110
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
111
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
112
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
113
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/banter.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
# require all files in the folder?
|
6
|
+
require 'banter/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "banter"
|
10
|
+
spec.version = Banter::VERSION
|
11
|
+
spec.authors = ["The Honest Company", "Thanh Lim", "Tushar Ranka", "Joel Jackson"]
|
12
|
+
spec.email = ["webadmin@honest.com", "tusharranka@gmail.com", "jackson.joel@gmail.com"]
|
13
|
+
spec.description = "Publish & subscribe to messages"
|
14
|
+
spec.summary = "Library for pub-sub (Message Bus)"
|
15
|
+
spec.homepage = ""
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0.0"
|
26
|
+
spec.add_development_dependency "debugger"
|
27
|
+
spec.add_runtime_dependency "activesupport", ">= 3.2"
|
28
|
+
spec.add_runtime_dependency "awesome_print", "~> 1.2.0"
|
29
|
+
spec.add_runtime_dependency "bunny", ">= 1.2"
|
30
|
+
spec.add_runtime_dependency "airbrake", ">= 3.1"
|
31
|
+
spec.add_runtime_dependency "hashie", ">= 1.2"
|
32
|
+
spec.add_runtime_dependency "json", ">= 1.8"
|
33
|
+
spec.add_runtime_dependency "celluloid", ">= 0.15"
|
34
|
+
spec.add_runtime_dependency "celluloid-io", ">= 0.15"
|
35
|
+
|
36
|
+
|
37
|
+
end
|
data/config/pubsub.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
development:
|
2
|
+
# Documentation for parameters for rabbit and bunny is located here: http://rubybunny.info/articles/connecting.html
|
3
|
+
connection:
|
4
|
+
host: localhost
|
5
|
+
port: 5672
|
6
|
+
# username: rabbit
|
7
|
+
# password: rabbit
|
8
|
+
heartbeat: 60 # in seconds
|
9
|
+
log_level: 0
|
10
|
+
log_file: rabbit.log
|
11
|
+
network_recovery_interval: 10 # in seconds
|
12
|
+
continuation_timeout: 4000 # in milliseconds
|
13
|
+
|
14
|
+
logger:
|
15
|
+
enabled: true
|
16
|
+
level: warn
|
17
|
+
file: pubsub.log
|
data/lib/banter.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "bunny"
|
3
|
+
require "yaml"
|
4
|
+
require "airbrake"
|
5
|
+
|
6
|
+
require "banter/configuration"
|
7
|
+
require "banter/context"
|
8
|
+
require "banter/db_logger"
|
9
|
+
require "banter/logger"
|
10
|
+
require "banter/logging"
|
11
|
+
require "banter/message"
|
12
|
+
require "banter/publisher"
|
13
|
+
require "banter/server/rabbit_mq_subscriber"
|
14
|
+
require "banter/version"
|
15
|
+
require "banter/exceptions/payload_validation_error"
|
16
|
+
require 'banter/server'
|
17
|
+
require 'banter/subscriber'
|
18
|
+
|
19
|
+
if defined?(Rails::Railtie)
|
20
|
+
require "banter/middleware"
|
21
|
+
require "banter/railtie"
|
22
|
+
end
|
23
|
+
|
24
|
+
module Banter
|
25
|
+
|
26
|
+
def self.root
|
27
|
+
File.expand_path '../..', __FILE__
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method publishes payload to rabbitmq. All listeners with appropriate
|
31
|
+
# routing keys will receive the payload.
|
32
|
+
# @param [String] routing_key Identifier of the message type
|
33
|
+
# @param [Hash] payload The data that will be passed to the subscriber
|
34
|
+
|
35
|
+
def self.publish(routing_key, payload)
|
36
|
+
Publisher.instance.publish(Banter::Context.instance, routing_key, payload)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.logger
|
40
|
+
Banter::Logging.logger
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Logger] logger Logger used by Banter
|
44
|
+
def self.logger=(logger)
|
45
|
+
Banter::Logging.logger = logger
|
46
|
+
end
|
47
|
+
end
|
data/lib/banter/cli.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# This class handles parsing and running the command line interface for executing subscribers
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
|
5
|
+
require 'singleton'
|
6
|
+
require 'optparse'
|
7
|
+
require 'banter/server'
|
8
|
+
|
9
|
+
module Banter
|
10
|
+
class CLI
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
attr_accessor :pidfile, :subscribers
|
14
|
+
|
15
|
+
# Method to support parsing of arguments passed through the command line
|
16
|
+
def parse(args = ARGV)
|
17
|
+
optparse = OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: bundle exec start_subscribers [options]"
|
19
|
+
opts.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
|
20
|
+
@pidfile = arg
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-o", "--only [SUBSCRIBERS]", "comma separated name of subsriber classes that should be run") do |subscribers|
|
24
|
+
@subscribers = subscribers.split(/\,/)
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
|
28
|
+
@require_path = arg
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on '-v', '--version', "Print version and exit" do |arg|
|
32
|
+
puts "Banter #{Banter::VERSION}"
|
33
|
+
abort
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
optparse.parse!(args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
load_environment
|
42
|
+
write_pidfile
|
43
|
+
load_subscribers
|
44
|
+
end
|
45
|
+
|
46
|
+
def require_path
|
47
|
+
@require_path || "."
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# @return [Array] returns array of subscriber classes that will be executed by the CLI
|
52
|
+
def subscriber_classes
|
53
|
+
if subscribers.present?
|
54
|
+
subscribers.map(&:constantize)
|
55
|
+
else
|
56
|
+
Banter::Subscriber.class_variable_get(:@@registered_subscribers)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_pid
|
61
|
+
return unless pidfile
|
62
|
+
File.delete(pidfile) if File.exist?(pidfile)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def load_environment
|
68
|
+
if require_path
|
69
|
+
raise ArgumentError, "#{require_path} does not exist" unless File.exist?(require_path)
|
70
|
+
end
|
71
|
+
|
72
|
+
if File.directory?(require_path)
|
73
|
+
require 'rails'
|
74
|
+
require File.expand_path("#{require_path}/config/environment.rb")
|
75
|
+
::Rails.application.eager_load!
|
76
|
+
else
|
77
|
+
require require_path
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def write_pidfile
|
82
|
+
return unless pidfile
|
83
|
+
File.open(pidfile, 'w') do |f|
|
84
|
+
f.puts Process.pid
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def load_subscribers
|
89
|
+
Banter::Server::SubscriberServer.new(subscriber_classes).start
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Banter
|
2
|
+
class Configuration
|
3
|
+
@@conf = nil
|
4
|
+
|
5
|
+
def self.configure_with(environment_name, yaml_file = nil)
|
6
|
+
@@yaml_file = yaml_file.nil? ? "config/pubsub.yml" : yaml_file
|
7
|
+
@@all_conf = Hashie::Mash.new(YAML.load_file(@@yaml_file) )
|
8
|
+
@@conf = @@all_conf[environment_name.to_sym]
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.configuration
|
13
|
+
self.configure_with(self.environment) if @@conf.nil?
|
14
|
+
@@conf
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.environment
|
18
|
+
val = ENV["RAILS_ENV"] || ENV["RACK_ENV"]
|
19
|
+
val = if val.present?
|
20
|
+
val.to_sym
|
21
|
+
else
|
22
|
+
if defined?(Rails)
|
23
|
+
Rails.env
|
24
|
+
else
|
25
|
+
raise "No environment can be found for configuration!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.default_queue_ttl
|
31
|
+
24.hours * 1000
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.application_name
|
35
|
+
@application_name ||= if defined?(Rails)
|
36
|
+
Rails.application.class.name.to_s.gsub("::Application", '')
|
37
|
+
else
|
38
|
+
'banter'
|
39
|
+
end.downcase
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Banter
|
2
|
+
class Context
|
3
|
+
# acceptable values
|
4
|
+
# employee - employee id that the context could have
|
5
|
+
# user_id - ender user id of call
|
6
|
+
# unique_id - unique identifier (could be request.uuid most of the time)
|
7
|
+
# orig_ip_address - ip address of originating requester
|
8
|
+
def initialize(params_hash = {})
|
9
|
+
@data = Hashie::Mash.new(params_hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
def user_id
|
13
|
+
@data.user_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def employee_id
|
17
|
+
@data.employee_id
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json
|
21
|
+
@data.as_json
|
22
|
+
end
|
23
|
+
|
24
|
+
def unique_id
|
25
|
+
@data.unique_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def originating_ip_address
|
29
|
+
@data.orig_ip_address
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize
|
33
|
+
@data.as_json
|
34
|
+
end
|
35
|
+
|
36
|
+
def application
|
37
|
+
@data.application
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.instance
|
41
|
+
Thread.current['pubsub_context'] || {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.setup_context(attrs = {})
|
45
|
+
Thread.current['pubsub_context'] = self.new(attrs)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.clear!
|
49
|
+
Thread.current['pubsub_context'] = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# USE AS INFREQUENTLY AS POSSIBLE. Only use it if you absolutely must
|
53
|
+
# as really, we want context in the object, and not the kitchen sink,
|
54
|
+
# which is what this will turn out to be if we use regularly use this
|
55
|
+
# mechanism.
|
56
|
+
def method_missing(method_name, *arguments, &block)
|
57
|
+
if @data.key?(method_name)
|
58
|
+
@data[method_name]
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.from_json(data_hash)
|
66
|
+
::Banter::Context.new(data_hash)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|