banter 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ 2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in banter.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,113 @@
1
+ # Banter
2
+
3
+ Simple Publishers and subscribers for Ruby using RabbitMQ
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/banter.svg)](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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/banter/cli'
4
+
5
+ begin
6
+ cli = Banter::CLI.instance
7
+ cli.parse
8
+ cli.run
9
+ rescue => e
10
+ raise e if $DEBUG
11
+ STDERR.puts e.message
12
+ STDERR.puts e.backtrace.join("\n")
13
+ exit 1
14
+ end
@@ -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
@@ -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
@@ -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