honest_pubsub 0.2.2
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/#.ruby-gemset# +0 -0
- data/.gitignore +22 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +92 -0
- data/Rakefile +1 -0
- data/bin/start_subscribers +14 -0
- data/config/pubsub.yml +17 -0
- data/honest_pubsub.gemspec +38 -0
- data/lib/honest_pubsub/cli.rb +94 -0
- data/lib/honest_pubsub/configuration.rb +38 -0
- data/lib/honest_pubsub/context.rb +70 -0
- data/lib/honest_pubsub/db_logger.rb +52 -0
- data/lib/honest_pubsub/exceptions/payload_error.rb +2 -0
- data/lib/honest_pubsub/logger.rb +49 -0
- data/lib/honest_pubsub/logging.rb +42 -0
- data/lib/honest_pubsub/message.rb +50 -0
- data/lib/honest_pubsub/middleware.rb +14 -0
- data/lib/honest_pubsub/publisher.rb +93 -0
- data/lib/honest_pubsub/railtie.rb +27 -0
- data/lib/honest_pubsub/server/client_queue_listener.rb +54 -0
- data/lib/honest_pubsub/server/client_worker.rb +86 -0
- data/lib/honest_pubsub/server/subscriber_server.rb +84 -0
- data/lib/honest_pubsub/server.rb +8 -0
- data/lib/honest_pubsub/subscriber.rb +69 -0
- data/lib/honest_pubsub/version.rb +3 -0
- data/lib/honest_pubsub.rb +46 -0
- data/spec/config.yml +20 -0
- data/spec/honest_pubsub/#subscriber_spec.rb# +9 -0
- data/spec/honest_pubsub/cli_spec.rb +145 -0
- data/spec/honest_pubsub/server/client_queue_listener_spec.rb +76 -0
- data/spec/honest_pubsub/server/client_worker_spec.rb +161 -0
- data/spec/honest_pubsub/subscriber_spec.rb +5 -0
- data/spec/logger_spec.rb +110 -0
- data/spec/message_spec.rb +65 -0
- data/spec/spec_helper.rb +49 -0
- metadata +259 -0
| @@ -0,0 +1,93 @@ | |
| 1 | 
            +
            module HonestPubsub
         | 
| 2 | 
            +
              class Publisher
         | 
| 3 | 
            +
                attr_reader :publisher
         | 
| 4 | 
            +
                attr_reader :channel
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                @@publisher = nil
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def self.instance
         | 
| 9 | 
            +
                  if @@publisher.nil?
         | 
| 10 | 
            +
                    @@publisher = ::HonestPubsub::Publisher.new
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                  @@publisher
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def initialize(exchange="honest")
         | 
| 16 | 
            +
                  @exchange = exchange
         | 
| 17 | 
            +
                  @disabled = false
         | 
| 18 | 
            +
                  @logger = ::HonestPubsub::Logger.new()
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  @config = Configuration.configuration
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  self
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def enable(value)
         | 
| 26 | 
            +
                  @disabled = !value
         | 
| 27 | 
            +
                  @disabled
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def start
         | 
| 31 | 
            +
                  if !@config[:enabled].nil? && @config[:enabled] == false
         | 
| 32 | 
            +
                    @disabled = true
         | 
| 33 | 
            +
                    return
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # grab server configuration from initialization file somewhere
         | 
| 37 | 
            +
                  begin
         | 
| 38 | 
            +
                    @connection = Bunny.new(Configuration.configuration[:connection])
         | 
| 39 | 
            +
                    @connection.start
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    @channel = @connection.create_channel
         | 
| 42 | 
            +
                    @publisher = @channel.topic(@exchange, :durable=>true, :auto_delete=>false)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  rescue => e
         | 
| 45 | 
            +
                    Airbrake.notify(e, parameters: {message: e.message}, environment_name: ENV['RAILS_ENV'] )
         | 
| 46 | 
            +
                    return
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
             | 
| 50 | 
            +
                  @publisher.on_return do |return_info, properties, content|
         | 
| 51 | 
            +
                    # contents are already transformed into message that we want to send
         | 
| 52 | 
            +
                    @logger.failed_publish(return_info[:routing_key], properties, content)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def publish(context, key, payload, enabled = true)
         | 
| 58 | 
            +
                  routing_key = "#{@exchange}.#{key}"
         | 
| 59 | 
            +
                  envelope = ::HonestPubsub::Message.new.serialize(context, key, payload)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  if @publisher.nil?
         | 
| 62 | 
            +
                    start
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  if @disabled || @publisher.nil? || !enabled
         | 
| 66 | 
            +
                    @logger.failed_publish(routing_key, {}, envelope)
         | 
| 67 | 
            +
                  else
         | 
| 68 | 
            +
                    tries = 2
         | 
| 69 | 
            +
                    begin
         | 
| 70 | 
            +
                      @publisher.publish(envelope.to_json, :persistent=>true, :mandatory=>true, :timestamp=>envelope[:ts], :content_type=>"application/json", :routing_key =>routing_key )
         | 
| 71 | 
            +
                      @logger.log_publish(routing_key, envelope)
         | 
| 72 | 
            +
                    rescue => e
         | 
| 73 | 
            +
                      tries -= 1
         | 
| 74 | 
            +
                      teardown
         | 
| 75 | 
            +
                      start
         | 
| 76 | 
            +
                      if tries > 0 && @publisher
         | 
| 77 | 
            +
                        retry
         | 
| 78 | 
            +
                      else
         | 
| 79 | 
            +
                        @logger.failed_publish(routing_key, {}, envelope)
         | 
| 80 | 
            +
                      end
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def teardown
         | 
| 88 | 
            +
                  @connection.close
         | 
| 89 | 
            +
                  @publisher = nil
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # This railtie provides Rails engine hooks
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module HonestPubsub
         | 
| 4 | 
            +
              class Railtie < ::Rails::Railtie
         | 
| 5 | 
            +
                initializer "honest_pubsub.insert_middleware" do |app|
         | 
| 6 | 
            +
                  app.config.middleware.use HonestPubsub::Middleware
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                console do
         | 
| 10 | 
            +
                  # Set up basic context to identify messages getting generated from console
         | 
| 11 | 
            +
                  HonestPubsub::Context.setup_context(
         | 
| 12 | 
            +
                    application: "#{HonestPubsub::Configuration.application_name}/console",
         | 
| 13 | 
            +
                    hostname: `hostname`.strip,
         | 
| 14 | 
            +
                    unique_id: Digest::SHA1.new.to_s
         | 
| 15 | 
            +
                  )
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                rake_tasks do
         | 
| 19 | 
            +
                  # Set up basic context to identify messages getting generated from rake tasks
         | 
| 20 | 
            +
                  HonestPubsub::Context.setup_context(
         | 
| 21 | 
            +
                    application: "#{HonestPubsub::Configuration.application_name}/rake",
         | 
| 22 | 
            +
                    hostname: `hostname`.strip,
         | 
| 23 | 
            +
                    unique_id: Digest::SHA1.new.to_s
         | 
| 24 | 
            +
                  )
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'celluloid/autostart'
         | 
| 3 | 
            +
            require 'celluloid/io'
         | 
| 4 | 
            +
            require 'honest_pubsub'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module HonestPubsub
         | 
| 7 | 
            +
              module Server
         | 
| 8 | 
            +
                class ClientQueueListener
         | 
| 9 | 
            +
                  include Celluloid
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_reader :worker_class
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def initialize(worker_class, request_key, queue, durable = true, topic = "honest")
         | 
| 14 | 
            +
                    @topic = topic
         | 
| 15 | 
            +
                    @request_key = request_key
         | 
| 16 | 
            +
                    @worker_class = worker_class
         | 
| 17 | 
            +
                    @queue_name = queue
         | 
| 18 | 
            +
                    @durable = durable
         | 
| 19 | 
            +
                    @subscriber = ::HonestPubsub::Subscriber.new(@request_key, @durable, @topic)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def start
         | 
| 23 | 
            +
                    @subscriber.start(@queue_name, false) do |delivery_info, properties, envelope|
         | 
| 24 | 
            +
                      message_received(delivery_info, properties, envelope)
         | 
| 25 | 
            +
                      true
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def shutdown
         | 
| 30 | 
            +
                    @subscriber.teardown if @subscriber.present?
         | 
| 31 | 
            +
                    # TODO -thl
         | 
| 32 | 
            +
                    # This the kosher thing to do in ruby?
         | 
| 33 | 
            +
                    teardown
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def message_received(delivery_info, properties, envelope)
         | 
| 37 | 
            +
                    begin
         | 
| 38 | 
            +
                      HonestPubsub.logger.debug( "Message recieved by listener with payload: #{envelope[:payload]}" )
         | 
| 39 | 
            +
                      worker = "#{@worker_class}".constantize.new(delivery_info, properties)
         | 
| 40 | 
            +
                      raise PayloadError.new("Invalid Payload") unless worker.valid_payload?(envelope[:payload])
         | 
| 41 | 
            +
                      HonestPubsub.logger.debug( "Calling worker perform on message" )
         | 
| 42 | 
            +
                      worker.perform(envelope[:context], envelope[:payload])
         | 
| 43 | 
            +
                    rescue => e
         | 
| 44 | 
            +
                      #puts "Error in worker #{e.message}!!!"
         | 
| 45 | 
            +
                      Airbrake.notify("Failed perform for #{self.class.name}", params: {info: delivery_info, properties: properties, envelope: envelope.to_hash } )
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  protected
         | 
| 50 | 
            +
                  def teardown
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'active_support/core_ext'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module HonestPubsub
         | 
| 5 | 
            +
              module Server
         | 
| 6 | 
            +
                class ClientWorker
         | 
| 7 | 
            +
                  @@registered_subscribers = []
         | 
| 8 | 
            +
                  class_attribute :payload_validators, :error_handlers, :subscribed_key, :subscribed_queue
         | 
| 9 | 
            +
                  attr_accessor :delivery_routing_data, :delivery_properties
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.inherited(klass)
         | 
| 12 | 
            +
                    @@registered_subscribers << klass
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # Specify the routing key that the subscriber class should listen to.
         | 
| 16 | 
            +
                  # @param [String] routing_key_name The routing key to subscribe to. Must be characters only separated by periods (.)
         | 
| 17 | 
            +
                  # @param [Hash] options Allowed option is :on to optionally specify a queue. If not provided, queue name is generated from the routing key
         | 
| 18 | 
            +
                  def self.subscribe_to(routing_key_name, options = {})
         | 
| 19 | 
            +
                    options.assert_valid_keys(:on)
         | 
| 20 | 
            +
                    unless validate_routing_key_name(routing_key_name)
         | 
| 21 | 
            +
                      raise ArgumentError.new("#{routing_key_name} is not supported. Only lower case characters separated by periods are allowed.")
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                    self.subscribed_key = routing_key_name
         | 
| 24 | 
            +
                    self.subscribed_queue = generated_queue_name(routing_key_name, options[:on])
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  # Sets the validator for payload
         | 
| 28 | 
            +
                  #
         | 
| 29 | 
            +
                  # @param validator The validator to use for validating the payload.
         | 
| 30 | 
            +
                  #                  Returns false if the payload is not valid.
         | 
| 31 | 
            +
                  #                  Proc must accept a payload as an argument.
         | 
| 32 | 
            +
                  def self.validates_payload_with(*validators)
         | 
| 33 | 
            +
                    self.payload_validators ||= []
         | 
| 34 | 
            +
                    self.payload_validators += validators
         | 
| 35 | 
            +
                    #
         | 
| 36 | 
            +
                    # executable = if validator.class <= Proc then
         | 
| 37 | 
            +
                    #                validator
         | 
| 38 | 
            +
                    #              elsif validator.class == Symbol
         | 
| 39 | 
            +
                    #                self.method(validator)
         | 
| 40 | 
            +
                    #              end
         | 
| 41 | 
            +
                    # self.payload_validators.push executable
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # Sets an error handler for the class
         | 
| 45 | 
            +
                  def self.handle_errors_with(handler)
         | 
| 46 | 
            +
                    error_handler = handler
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def initialize(delivery_routing_data, delivery_properties)
         | 
| 50 | 
            +
                    @delivery_routing_data = delivery_routing_data
         | 
| 51 | 
            +
                    @delivery_properties = delivery_properties
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  # Need to have a real perform method for the message to be processed.
         | 
| 55 | 
            +
                  def perform(payload)
         | 
| 56 | 
            +
                    raise "Need implementation for your worker."
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def routing_key
         | 
| 60 | 
            +
                    delivery_routing_data[:routing_key]
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  # Iterates over all the payload validators and returns false if any of them are false
         | 
| 64 | 
            +
                  def valid_payload?(payload)
         | 
| 65 | 
            +
                    return true unless payload_validators.present?
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    payload_validators.inject(true) { |is_valid, validator|
         | 
| 68 | 
            +
                      is_valid && (validator.respond_to?(:call) ? validator.call(payload) : send(validator, payload) )
         | 
| 69 | 
            +
                    }
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  private
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def self.validate_routing_key_name(key)
         | 
| 75 | 
            +
                    return true if key.blank?
         | 
| 76 | 
            +
                    key.match(/\A([a-z]+\.?)*([a-z]+)\Z/).present?
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def self.generated_queue_name(routing_key, queue_name)
         | 
| 80 | 
            +
                    return queue_name if queue_name.present?
         | 
| 81 | 
            +
                    [ HonestPubsub::Configuration.application_name.to_s.gsub(/[^\w\_]/, ''), routing_key.gsub(".", '_') ].reject(&:blank?).join('_')
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| @@ -0,0 +1,84 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'celluloid/autostart'
         | 
| 3 | 
            +
            require 'celluloid/io'
         | 
| 4 | 
            +
            require 'awesome_print'
         | 
| 5 | 
            +
            require 'active_support/all'
         | 
| 6 | 
            +
            require 'optparse'
         | 
| 7 | 
            +
            require 'fileutils'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require_relative './client_queue_listener'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            # In order to use the server, the caller must be able to bring in the necessary
         | 
| 12 | 
            +
            # classes for require before instantiating the instance.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            module HonestPubsub
         | 
| 15 | 
            +
              module Server
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                class SubscriberServer
         | 
| 18 | 
            +
                  include Celluloid
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def initialize(subscribers)
         | 
| 21 | 
            +
                    @workers = subscribers.map { |subscriber| create_queue_listeners(subscriber) }
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def start
         | 
| 25 | 
            +
                    @workers.each do |worker|
         | 
| 26 | 
            +
                      puts "Starting worker: #{worker.worker_class.name}"
         | 
| 27 | 
            +
                      worker.start
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    thread     = Thread.current
         | 
| 31 | 
            +
                    interrupts = ["HUP", "INT", "QUIT", "ABRT", "TERM"]
         | 
| 32 | 
            +
                    interrupts.each do |signal_name|
         | 
| 33 | 
            +
                      Signal.trap(signal_name) {
         | 
| 34 | 
            +
                        puts "Processing #{signal_name}"
         | 
| 35 | 
            +
                        thread.run
         | 
| 36 | 
            +
                      }
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    Thread.stop
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    ::HonestPubsub::Logger.new.log_service("all_services", :warn, "Starting shutdown of all services")
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    @workers.each do |worker|
         | 
| 44 | 
            +
                      ::HonestPubsub::Logger.new.log_service("all_services", :warn, "Tearing down worker: #{worker.worker_class.name}")
         | 
| 45 | 
            +
                      begin
         | 
| 46 | 
            +
                        STDOUT.puts "Tearing down subscriber for #{worker.worker_class.name}"
         | 
| 47 | 
            +
                        worker.shutdown
         | 
| 48 | 
            +
                      rescue => e
         | 
| 49 | 
            +
                        ::HonestPubsub::Logger.new.log_service("all_services", :warn, "#{worker.worker_class.name} - did not tear down correctly.  Error - #{e.message}")
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  ensure
         | 
| 53 | 
            +
                    HonestPubsub::CLI.instance.remove_pid
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  private
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def warn(message)
         | 
| 59 | 
            +
                    old_behavior = ActiveSupport::Deprecation.behavior
         | 
| 60 | 
            +
                    ActiveSupport::Deprecation.behavior = [:stderr, :log]
         | 
| 61 | 
            +
                    ActiveSupport::Deprecation.warn(message)
         | 
| 62 | 
            +
                  ensure
         | 
| 63 | 
            +
                    ActiveSupport::Deprecation.behavior = old_behavior
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def create_queue_listeners(subscriber)
         | 
| 67 | 
            +
                    routing_key           = subscriber.subscribed_key
         | 
| 68 | 
            +
                    subscribed_queue_name = subscriber.subscribed_queue
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    if routing_key.blank?
         | 
| 71 | 
            +
                      raise ArgumentError.new("Routing key must be provided in #{subscriber.name} using `subscribe_to routing_key`")
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    if subscribed_queue_name.blank?
         | 
| 75 | 
            +
                      raise ArgumentError.new("Queue Name must be provided in #{subscriber.name} using `subscribe_to routing_key, on: queue_name`")
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    STDOUT.puts "Setting up listener for request_key: #{routing_key} and queue:#{subscribed_queue_name}"
         | 
| 79 | 
            +
                    ClientQueueListener.new(subscriber, routing_key, subscribed_queue_name)
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
            end
         | 
| 84 | 
            +
             | 
| @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            module HonestPubsub
         | 
| 2 | 
            +
              class Subscriber
         | 
| 3 | 
            +
                attr_reader :listener
         | 
| 4 | 
            +
                attr_reader :exchange
         | 
| 5 | 
            +
                attr_reader :channel
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(routing_key, durable = true, topic="honest")
         | 
| 8 | 
            +
                  @initial_key = routing_key
         | 
| 9 | 
            +
                  @durable = durable
         | 
| 10 | 
            +
                  @topic = topic
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  if @initial_key.present?
         | 
| 13 | 
            +
                    @routing_key = "#{@topic}.#{@initial_key}.#"
         | 
| 14 | 
            +
                  else
         | 
| 15 | 
            +
                    @routing_key = "#{@topic}.#"
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                  @logger = ::HonestPubsub::Logger.new
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  self
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # name - used to ensure that certain consumers are actually listening to an exchange
         | 
| 23 | 
            +
                # pass in a lambda for this method to work.  We might only want to expose the content instead of
         | 
| 24 | 
            +
                # all 3 chunks.
         | 
| 25 | 
            +
                def start(name, blocking=false)
         | 
| 26 | 
            +
                  @connection = Bunny.new(Configuration.configuration[:connection])
         | 
| 27 | 
            +
                  begin
         | 
| 28 | 
            +
                    @connection.start
         | 
| 29 | 
            +
                  rescue => e
         | 
| 30 | 
            +
                    Airbrake.notify("RabbitMQ unreachable!", params: { message: e.message}, environment_name: ENV['RAILS_ENV'] )
         | 
| 31 | 
            +
                    raise e
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  @channel = @connection.create_channel
         | 
| 35 | 
            +
                  @exchange = @channel.topic(@topic, :durable=>@durable, :auto_delete=>false)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # FIX!!! -thl
         | 
| 38 | 
            +
                  # Need to ensure that the ids for a server will be reproducible in case a server
         | 
| 39 | 
            +
                  # goes down and has to get restarted.
         | 
| 40 | 
            +
                  if @initial_key.present?
         | 
| 41 | 
            +
                    @queue = "#{@initial_key}.#{name}"
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    @queue = "#{name}"
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  queue_arguments = {}
         | 
| 47 | 
            +
                  queue_arguments["x-dead-letter-exchange"] = Configuration.configuration[:dead_letter] if Configuration.configuration[:dead_letter].present?
         | 
| 48 | 
            +
                  @listener = @channel.queue(@queue, :arguments=>queue_arguments ).bind(@exchange, :routing_key => @routing_key, :exclusive=>false)
         | 
| 49 | 
            +
                  # Parameters for subscribe that might be useful:
         | 
| 50 | 
            +
                  # :block=>true - Used for long running consumer applications.  (backend servers?)
         | 
| 51 | 
            +
                  @consumer = @listener.subscribe(:consumer_tag=>name, :block=>blocking)
         | 
| 52 | 
            +
                  @consumer.on_delivery do |delivery_info, properties, contents|
         | 
| 53 | 
            +
                    HonestPubsub.logger.debug( "Message delivery with contents: #{contents}")
         | 
| 54 | 
            +
                    if delivery_info[:redelivered]
         | 
| 55 | 
            +
                      Airbrake.notify("PubSub Message redelivery", params: {info: delivery_info, props: properties, contents: contents}, environment_name: ENV['RAILS_ENV'] )
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                    message = ::HonestPubsub::Message.new.parse(contents)
         | 
| 58 | 
            +
                    @logger.log_receive(delivery_info[:routing_key], message)
         | 
| 59 | 
            +
                    yield delivery_info, properties, message
         | 
| 60 | 
            +
                    true
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def teardown
         | 
| 65 | 
            +
                  @consumer.cancel if @consumer.present?
         | 
| 66 | 
            +
                  @connection.close if @connection.present?
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            require "active_support/all"
         | 
| 2 | 
            +
            require "bunny"
         | 
| 3 | 
            +
            require "yaml"
         | 
| 4 | 
            +
            require "airbrake"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require "honest_pubsub/configuration"
         | 
| 7 | 
            +
            require "honest_pubsub/context"
         | 
| 8 | 
            +
            require "honest_pubsub/db_logger"
         | 
| 9 | 
            +
            require "honest_pubsub/logger"
         | 
| 10 | 
            +
            require "honest_pubsub/logging"
         | 
| 11 | 
            +
            require "honest_pubsub/message"
         | 
| 12 | 
            +
            require "honest_pubsub/publisher"
         | 
| 13 | 
            +
            require "honest_pubsub/subscriber"
         | 
| 14 | 
            +
            require "honest_pubsub/version"
         | 
| 15 | 
            +
            require "honest_pubsub/exceptions/payload_error"
         | 
| 16 | 
            +
            require 'honest_pubsub/server'
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            if defined?(Rails::Railtie)
         | 
| 19 | 
            +
              require "honest_pubsub/middleware"
         | 
| 20 | 
            +
              require "honest_pubsub/railtie"
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            module HonestPubsub
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              def self.root
         | 
| 26 | 
            +
                File.expand_path '../..', __FILE__
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            # This method publishes payload to rabbitmq.  All listeners with appropriate
         | 
| 30 | 
            +
            # routing keys will receive the payload.
         | 
| 31 | 
            +
            # @param [String] routing_key Identifier of the message type
         | 
| 32 | 
            +
            # @param [Hash] payload The data that will be passed to the subscriber
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def self.publish(routing_key, payload)
         | 
| 35 | 
            +
                Publisher.instance.publish(HonestPubsub::Context.instance, routing_key, payload)
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              def self.logger
         | 
| 39 | 
            +
                HonestPubsub::Logging.logger
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              # @param [Logger] logger Logger used by HonestPubsub
         | 
| 43 | 
            +
              def self.logger=(logger)
         | 
| 44 | 
            +
                HonestPubsub::Logging.logger = logger
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
    
        data/spec/config.yml
    ADDED
    
    | @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            test:
         | 
| 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: test_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: test_pubsub.log
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 20 | 
            +
              pid: pids/honest_pubsub
         | 
| @@ -0,0 +1,145 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'honest_pubsub/cli'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe HonestPubsub::CLI do
         | 
| 5 | 
            +
              let(:cli) { HonestPubsub::CLI.instance }
         | 
| 6 | 
            +
              after(:each) do
         | 
| 7 | 
            +
                cli.instance_variable_set(:@subscribers, nil)
         | 
| 8 | 
            +
                cli.instance_variable_set(:@pidfile, nil)
         | 
| 9 | 
            +
                cli.instance_variable_set(:@require_path, nil)
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              describe '#parse' do
         | 
| 13 | 
            +
                let(:options) { [] }
         | 
| 14 | 
            +
                before { cli.parse(options) }
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                context "pidfile" do
         | 
| 17 | 
            +
                  it "works when nothing is passed" do
         | 
| 18 | 
            +
                    expect(cli.pidfile).to be_nil
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  context "passed in as -P" do
         | 
| 22 | 
            +
                    let(:options) { [ "-P", "mypidfile.pid"]}
         | 
| 23 | 
            +
                    it "parses" do
         | 
| 24 | 
            +
                      expect(cli.pidfile).to eq('mypidfile.pid')
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  context "passed in as --pidfile" do
         | 
| 29 | 
            +
                    let(:options) { [ "--pidfile", "mypidfile.pid"]}
         | 
| 30 | 
            +
                    it "parses" do
         | 
| 31 | 
            +
                      expect(cli.pidfile).to eq('mypidfile.pid')
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                context "only" do
         | 
| 37 | 
            +
                  it "works when nothing is passed" do
         | 
| 38 | 
            +
                    expect(cli.subscribers).to be_nil
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  context "passed in as -o" do
         | 
| 42 | 
            +
                    let(:options) { [ "-o", "Foobar,Doobar"]}
         | 
| 43 | 
            +
                    it "parses" do
         | 
| 44 | 
            +
                      expect(cli.subscribers).to eq([ 'Foobar', 'Doobar'])
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  context "passed in as --only" do
         | 
| 49 | 
            +
                    let(:options) { [ "--only", "Foobar,Doobar"]}
         | 
| 50 | 
            +
                    it "parses" do
         | 
| 51 | 
            +
                      expect(cli.subscribers).to eq([ 'Foobar', 'Doobar'])
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                context "require" do
         | 
| 57 | 
            +
                  it "works when nothing is passed" do
         | 
| 58 | 
            +
                    expect(cli.require_path).to eq(".")
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  context "passed in as -r" do
         | 
| 62 | 
            +
                    let(:options) { [ "-r", "/path/to/my/file"]}
         | 
| 63 | 
            +
                    it "parses" do
         | 
| 64 | 
            +
                      expect(cli.require_path).to eq("/path/to/my/file")
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  context "passed in as --require" do
         | 
| 69 | 
            +
                    let(:options) { [ "--require", "/path/to/my/file"]}
         | 
| 70 | 
            +
                    it "parses" do
         | 
| 71 | 
            +
                      expect(cli.require_path).to eq("/path/to/my/file")
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              describe '#run' do
         | 
| 78 | 
            +
                describe "high-level steps to be completed" do
         | 
| 79 | 
            +
                  before {
         | 
| 80 | 
            +
                    allow(cli).to receive(:load_environment)
         | 
| 81 | 
            +
                    allow(cli).to receive(:write_pidfile)
         | 
| 82 | 
            +
                    allow(cli).to receive(:load_subscribers)
         | 
| 83 | 
            +
                    cli.run
         | 
| 84 | 
            +
                  }
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  it "runs" do
         | 
| 87 | 
            +
                    expect(cli).to have_received(:load_environment)
         | 
| 88 | 
            +
                    expect(cli).to have_received(:write_pidfile)
         | 
| 89 | 
            +
                    expect(cli).to have_received(:load_subscribers)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
              describe '#write_pidfile' do
         | 
| 95 | 
            +
                before {
         | 
| 96 | 
            +
                  cli.pidfile = pidfile
         | 
| 97 | 
            +
                  allow(File).to receive(:open)
         | 
| 98 | 
            +
                  cli.send(:write_pidfile)
         | 
| 99 | 
            +
                }
         | 
| 100 | 
            +
                context "no pidfile passed" do
         | 
| 101 | 
            +
                  let(:pidfile) { nil }
         | 
| 102 | 
            +
                  it "doesn't create a pidfile" do
         | 
| 103 | 
            +
                    expect(File).to_not have_received(:open)
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                context "pidfile path is supplied" do
         | 
| 108 | 
            +
                  let(:pidfile) { "/path/to/pidfile" }
         | 
| 109 | 
            +
                  it 'creates file' do
         | 
| 110 | 
            +
                    expect(File).to have_received(:open).with(pidfile, 'w')
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
              end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              describe '#load_subscribers' do
         | 
| 116 | 
            +
                let(:subscribers) { nil }
         | 
| 117 | 
            +
                let(:subscriber_server) { double('subscriber_server')}
         | 
| 118 | 
            +
                before {
         | 
| 119 | 
            +
                  cli.subscribers = subscribers
         | 
| 120 | 
            +
                  allow(subscriber_server).to receive(:start)
         | 
| 121 | 
            +
                  allow(HonestPubsub::Server::SubscriberServer).to receive(:new).and_return(subscriber_server)
         | 
| 122 | 
            +
                  cli.send(:load_subscribers)
         | 
| 123 | 
            +
                }
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                context "No subscribers passed through CLI" do
         | 
| 126 | 
            +
                  let(:all_subscribers) { HonestPubsub::Server::ClientWorker.class_variable_get(:@@registered_subscribers) }
         | 
| 127 | 
            +
                  it "loads all subscribers" do
         | 
| 128 | 
            +
                    expect(all_subscribers).to include(MyTestSubscriber1)
         | 
| 129 | 
            +
                    expect(all_subscribers).to include(MyTestSubscriber2)
         | 
| 130 | 
            +
                    expect(all_subscribers).to include(MyTestSubscriber3)
         | 
| 131 | 
            +
                    expect(HonestPubsub::Server::SubscriberServer).to have_received(:new).with(all_subscribers)
         | 
| 132 | 
            +
                    expect(subscriber_server).to have_received(:start)
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                context "Subscribers passed through CLI" do
         | 
| 137 | 
            +
                  let(:subscribers) { ['MyTestSubscriber1', 'MyTestSubscriber2'] }
         | 
| 138 | 
            +
                  it "loads all subscribers" do
         | 
| 139 | 
            +
                    expect(HonestPubsub::Server::SubscriberServer).to have_received(:new).with([MyTestSubscriber1, MyTestSubscriber2])
         | 
| 140 | 
            +
                    expect(subscriber_server).to have_received(:start)
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
              end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
            end
         |