rabbit_messaging 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '081fdf833501fd681239cacf04348b5736d2f445f8b4f995c9cb20f5a4decf2d'
4
+ data.tar.gz: 566727c03028779e6334f4c89fc2cb2a569dfc7c8dfa92f3399841b610b23f6a
5
+ SHA512:
6
+ metadata.gz: 118a2d651ce38c6cb60b76464ff1db259865374b0ab8454d9208a7811ee55790796bb8be3cdad0cfbec92750e3a0a97ba36491c9b0e9e697faddbbc3cb7d53aa
7
+ data.tar.gz: 5e56e561d5077c826d8b1ed5a13dcdf640f14093d5b05fe689fa4d55adc3d9830379e3cafcf289097a56ca78494b4cb33a63ad966456bd8730be1d749007f7dd
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ .ruby-version
11
+ /log/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,17 @@
1
+ inherit_gem:
2
+ rubocop-config-umbrellio: lib/rubocop.yml
3
+
4
+ AllCops:
5
+ DisplayCopNames: true
6
+ TargetRubyVersion: 2.3
7
+ Include:
8
+ - lib/**/*.rb
9
+ - spec/**/*.rb
10
+ - Gemfile
11
+ - Rakefile
12
+ - rabbit_messaging.gemspec
13
+ - bin/console
14
+ - environments/*.rb
15
+
16
+ Style/Alias:
17
+ EnforcedStyle: prefer_alias_method
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ matrix:
3
+ fast_finish: true
4
+ include:
5
+ - rvm: 2.3
6
+ - rvm: 2.4
7
+ - rvm: 2.5
8
+ - rvm: 2.6
9
+ - rvm: ruby-head
10
+ allow_failures:
11
+ - rvm: ruby-head
12
+ sudo: false
13
+ cache: bundler
14
+ before_install: gem install bundler
15
+ script:
16
+ - mkdir log
17
+ - bundle exec rake bundle:audit
18
+ - bundle exec rubocop
19
+ - bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Umbrellio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Rabbit (Rabbit Messaging) · [![Gem Version](https://badge.fury.io/rb/rabbit_messaging.svg)](https://badge.fury.io/rb/rabbit_messaging) [![Build Status](https://travis-ci.org/umbrellio/rabbit_messaging.svg?branch=master)](https://travis-ci.org/umbrellio/rabbit_messaging) [![Coverage Status](https://coveralls.io/repos/github/umbrellio/rabbit_messaging/badge.svg?branch=master)](https://coveralls.io/github/umbrellio/rabbit_messaging?branch=master)
2
+
3
+ Provides client and server support for RabbitMQ
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "rabbit_messaging"
9
+ ```
10
+
11
+ ```shell
12
+ $ bundle install
13
+ # --- or ---
14
+ $ gem install "rabbit_messaging"
15
+ ```
16
+
17
+ ```ruby
18
+ require "rabbit_messaging"
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ - [Configuration](#configuration)
24
+ - [Client](#client)
25
+ - [Server](#server)
26
+
27
+ ---
28
+
29
+ ### Configuration
30
+
31
+ - RabbitMQ connection configuration fetched from the `bunny_options` section
32
+ of `/config/sneakers.yml`
33
+
34
+ - `Rabbit.config` provides setters for following options:
35
+
36
+ * `group_id` (`Symbol`), *required*
37
+
38
+ Shared identifier which used to select api. As usual, it should be same as default project_id
39
+ (I.e. we have project 'support', which runs only one application in production.
40
+ So on, it's group_id should be :support)
41
+
42
+ * `project_id` (`Symbol`), *required*
43
+
44
+ Personal identifier which used to select exact service.
45
+ As usual, it should be same as default project_id with optional stage_id.
46
+ (I.e. we have project 'support', in production it's project_id is :support,
47
+ but in staging it uses :support1 and :support2 ids for corresponding stages)
48
+
49
+ * `hooks` (`Hash`)
50
+
51
+ :before_fork and :after_fork hooks, used same way as in unicorn / puma / que / etc
52
+
53
+ * `environment` (one of `:test`, `:development`, `:production`), *default:* `:production`
54
+
55
+ Internal environment of gem.
56
+
57
+ * `:test` environment stubs publishing and does not suppress errors
58
+ * `:development` environment auto-creates queues and uses default exchange
59
+ * `:production` environment enables handlers caching and gets maximum strictness
60
+
61
+ * `receiving_job_class_callable` (`Proc`)
62
+
63
+ Custom ActiveJob subclass to work with received messages.
64
+
65
+ * `exception_notifier` (`Proc`)
66
+
67
+ By default, exceptions are reported using `ExceptionNotifier` (see exception_notification gem).
68
+ You can provide your own notifier like this:
69
+
70
+ ```ruby
71
+ config.exception_notifier = proc { |e| MyCoolNotifier.notify!(e) }
72
+ ```
73
+
74
+ ---
75
+
76
+ ### Client
77
+
78
+ ```ruby
79
+ Rabbit.publish(
80
+ routing_key: :support,
81
+ event: :ping,
82
+ data: { foo: :bar }, # default is {}
83
+ exchange_name: 'fanout', # default is fine too
84
+ confirm_select: true, # setting this to false grants you great speed up and absolutelly no guarantees
85
+ headers: { "foo" => "bar" }, # custom arguments for routing, default is {}
86
+ )
87
+ ```
88
+
89
+ - This code sends messages via basic_publish with following parameters:
90
+
91
+ * `routing_key`: `"support"`
92
+ * `exchange`: `"group_id.project_id.fanout"` (default is `"group_id.poject_id"`)
93
+ * `mandatory`: `true` (same as confirm_select)
94
+
95
+ It is set to raise error if routing failed
96
+
97
+ * `persistent`: `true`
98
+ * `type`: `"ping"`
99
+ * `content_type`: `"application/json"` (always)
100
+ * `app_id`: `"group_id.project_id"`
101
+
102
+ - Messages are logged to `/log/rabbit.log`
103
+
104
+ ---
105
+
106
+ ### Server
107
+
108
+ - Server is supposed to run inside a daemon via the `daemons-rails` gem. Server is run with
109
+ `Rabbit::Daemon.run`. `before_fork` and `after_fork` procs in `Rabbit.config` are used
110
+ to teardown and setup external connections between daemon restarts, for example ORM connections
111
+
112
+ - After the server runs, received messages are handled by `Rabbit::EventHandler` subclasses.
113
+ Subclasses are selected by following code:
114
+ ```ruby
115
+ "rabbit/handler/#{group_id}/#{event}".camelize.constantize
116
+ ```
117
+
118
+ They use powerful `Tainbox` api to handle message data. Project_id also passed to them.
119
+
120
+ If you wish so, you can override `initialize(message)`, where message is an object
121
+ with simple api (@see lib/rabbit/receiving/message.rb)
122
+
123
+ Handlers can specify a queue their messages will be put in via a `queue_as` class macro (accepts
124
+ a string / symbol / block with `|message, arguments|` params)
125
+
126
+ - Received messages are logged to `/log/sneakers.log`, malformed messages are logged to
127
+ `/log/malformed_messages.log` and deleted from queue
128
+
129
+ ---
130
+
131
+ ## Contributing
132
+
133
+ Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/rabbit_messaging.
134
+
135
+ ## License
136
+
137
+ Released under MIT License
138
+
139
+ ## Authors
140
+
141
+ Team Umbrellio
142
+
143
+ ---
144
+
145
+ <a href="https://github.com/umbrellio/">
146
+ <img style="float: left;" src="https://umbrellio.github.io/Umbrellio/supported_by_umbrellio.svg"
147
+ alt="Supported by Umbrellio" width="439" height="72">
148
+ </a>
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "bundler/audit/task"
5
+ require "rspec/core/rake_task"
6
+ require "rubocop"
7
+ require "rubocop-rspec"
8
+ require "rubocop-performance"
9
+ require "rubocop/rake_task"
10
+
11
+ RuboCop::RakeTask.new(:rubocop) do |t|
12
+ config_path = File.expand_path(File.join(".rubocop.yml"), __dir__)
13
+
14
+ t.options = ["--config", config_path]
15
+ t.requires << "rubocop-rspec"
16
+ t.requires << "rubocop-performance"
17
+ end
18
+
19
+ RSpec::Core::RakeTask.new(:rspec)
20
+ Bundler::Audit::Task.new
21
+
22
+ task default: :rspec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rabbit_messaging"
5
+
6
+ require "pry"
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ Bundler.require(:default, :development)
6
+ require "active_job"
7
+ require "active_record"
8
+
9
+ def Rails.root
10
+ Pathname.new(__dir__).join("..")
11
+ end
12
+
13
+ ActiveJob::Base.queue_adapter = :inline
14
+ ActiveJob::Base.logger = Logger.new("/dev/null")
15
+
16
+ Rabbit.config.project_id = "test_project_id"
17
+ Rabbit.config.group_id = "test_group_id"
data/lib/rabbit.rb ADDED
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tainbox"
4
+
5
+ require "rabbit/version"
6
+ require "rabbit/daemon"
7
+ require "rabbit/publishing"
8
+ require "rabbit/event_handler"
9
+
10
+ require "rabbit/extensions/bunny/channel"
11
+
12
+ module Rabbit
13
+ InvalidConfig = Class.new(StandardError)
14
+ MessageNotDelivered = Class.new(StandardError)
15
+
16
+ class Config
17
+ include Tainbox
18
+
19
+ attribute :group_id, Symbol
20
+ attribute :project_id, Symbol
21
+ attribute :hooks, default: {}
22
+ attribute :environment, Symbol, default: :production
23
+ attribute :queue_name_conversion
24
+ attribute :receiving_job_class_callable
25
+ attribute :exception_notifier, default: -> { default_exception_notifier }
26
+
27
+ def validate!
28
+ raise InvalidConfig, "mising project_id" unless project_id
29
+ raise InvalidConfig, "mising group_id" unless group_id
30
+
31
+ unless environment.in? %i[test development production]
32
+ raise "environment should be one of (test, development, production)"
33
+ end
34
+ end
35
+
36
+ def app_name
37
+ [group_id, project_id].join(".")
38
+ end
39
+
40
+ alias_method :read_queue, :app_name
41
+
42
+ private
43
+
44
+ def default_exception_notifier
45
+ -> (e) { ExceptionNotifier.notify_exception(e) }
46
+ end
47
+ end
48
+
49
+ extend self
50
+
51
+ def config
52
+ @config ||= Config.new
53
+ yield(@config) if block_given?
54
+ @config
55
+ end
56
+
57
+ alias_method :configure, :config
58
+
59
+ def publish(message_options)
60
+ message = Publishing::Message.new(message_options)
61
+
62
+ if message.realtime?
63
+ Publishing.publish(message)
64
+ else
65
+ Publishing::Job.set(queue: default_queue_name).perform_later(message.to_hash)
66
+ end
67
+ end
68
+
69
+ def queue_name(queue, ignore_conversion: false)
70
+ return queue if ignore_conversion
71
+ config.queue_name_conversion ? config.queue_name_conversion.call(queue) : queue
72
+ end
73
+
74
+ def default_queue_name(ignore_conversion: false)
75
+ queue_name(:default, ignore_conversion: ignore_conversion)
76
+ end
77
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sneakers"
4
+ require "lamian"
5
+ require "sneakers/runner"
6
+
7
+ require "rabbit/extensions/bunny/channel"
8
+ require "rabbit/receiving/worker"
9
+
10
+ module Rabbit
11
+ module Daemon
12
+ extend self
13
+
14
+ def run
15
+ Sneakers.configure(
16
+ connection: connection,
17
+ env: Rails.env,
18
+ exchange_type: :direct,
19
+ exchange: Rabbit.config.app_name,
20
+ hooks: Rabbit.config.hooks,
21
+ supervisor: true,
22
+ daemonize: false,
23
+ exit_on_detach: true,
24
+ **config,
25
+ )
26
+
27
+ Sneakers.logger = Logger.new(Rails.root.join("log", "sneakers.log"))
28
+ Sneakers.logger.level = Logger::DEBUG
29
+ Lamian.extend_logger(Sneakers.logger)
30
+
31
+ Sneakers.server = true
32
+
33
+ Rabbit.config.validate!
34
+ Receiving::Worker.from_queue(Rabbit.config.read_queue)
35
+
36
+ Sneakers::Runner.new([Receiving::Worker]).run
37
+ end
38
+
39
+ def config
40
+ Rails.application.config_for("sneakers").symbolize_keys
41
+ end
42
+
43
+ def connection
44
+ bunny_config = config.delete(:bunny_options).to_h.symbolize_keys
45
+ Bunny.new(bunny_config)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tainbox"
4
+ require "active_support/core_ext/class/attribute"
5
+
6
+ class Rabbit::EventHandler
7
+ include Tainbox
8
+
9
+ attribute :project_id
10
+ attr_accessor :data
11
+ class_attribute :queue
12
+ class_attribute :ignore_queue_conversion, default: false
13
+
14
+ class << self
15
+ private
16
+
17
+ def queue_as(queue = nil, &block)
18
+ self.queue = queue || block
19
+ end
20
+ end
21
+
22
+ def initialize(message)
23
+ self.attributes = message.data
24
+ self.data = message.data
25
+ self.project_id = message.project_id
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bunny/channel"
4
+
5
+ class Bunny::Channel
6
+ module RabbitExtensions
7
+ def handle_basic_return(*)
8
+ @unconfirmed_set_mutex.synchronize { @only_acks_received = false } # fails confirm_select
9
+ super
10
+ end
11
+ end
12
+
13
+ prepend(RabbitExtensions)
14
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rabbit/publishing/message"
4
+
5
+ module Rabbit
6
+ module Publishing
7
+ autoload :Job, "rabbit/publishing/job"
8
+
9
+ MUTEX = Mutex.new
10
+
11
+ extend self
12
+
13
+ def publish(message)
14
+ return unless client
15
+
16
+ channel = channel(message.confirm_select?)
17
+ channel.basic_publish(*message.basic_publish_args)
18
+
19
+ if message.confirm_select? && !channel.wait_for_confirms
20
+ raise MessageNotDelivered, "RabbitMQ message not delivered: #{message}"
21
+ else
22
+ log(message)
23
+ end
24
+ rescue Timeout::Error
25
+ raise MessageNotDelivered, <<~MESSAGE
26
+ Timeout while sending message #{message}. Possible reasons:
27
+ - #{message.real_exchange_name} exchange is not found
28
+ - RabbitMQ is extremely high loaded
29
+ MESSAGE
30
+ end
31
+
32
+ def client
33
+ MUTEX.synchronize { @client ||= create_client }
34
+ end
35
+
36
+ def channel(confirm)
37
+ Thread.current[:bunny_channels] ||= {}
38
+ channel = Thread.current[:bunny_channels][confirm]
39
+ Thread.current[:bunny_channels][confirm] = create_channel(confirm) unless channel&.open?
40
+ Thread.current[:bunny_channels][confirm]
41
+ end
42
+
43
+ private
44
+
45
+ def create_queue_if_not_exists(channel, message)
46
+ channel.queue(message.routing_key, durable: true)
47
+ end
48
+
49
+ def create_client
50
+ return if Rabbit.config.environment == :test
51
+
52
+ config = Rails.application.config_for("sneakers") rescue {}
53
+ config = config["bunny_options"].to_h.symbolize_keys
54
+
55
+ begin
56
+ Bunny.new(config).start
57
+ rescue
58
+ raise unless Rabbit.config.environment == :development
59
+ end
60
+ end
61
+
62
+ def create_channel(confirm)
63
+ channel = client.create_channel
64
+ channel.confirm_select if confirm
65
+ channel
66
+ end
67
+
68
+ def log(message)
69
+ @logger ||= Logger.new(Rails.root.join("log", "rabbit.log"))
70
+
71
+ headers = [
72
+ message.real_exchange_name, message.routing_key, message.event,
73
+ message.confirm_select? ? "confirm" : "no-confirm"
74
+ ]
75
+
76
+ @logger.debug "#{headers.join ' / '}: #{message.data}"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rabbit/publishing"
4
+
5
+ module Rabbit::Publishing
6
+ class Job < ActiveJob::Base
7
+ def perform(message)
8
+ Rabbit::Publishing.publish(Message.new(message))
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tainbox"
4
+
5
+ module Rabbit::Publishing
6
+ class Message
7
+ include Tainbox
8
+
9
+ attribute :routing_key, String
10
+ attribute :event, String
11
+ attribute :data, default: {}
12
+ attribute :exchange_name, default: []
13
+ attribute :confirm_select, default: true
14
+ attribute :realtime, default: false
15
+ attribute :headers
16
+
17
+ alias_method :confirm_select?, :confirm_select
18
+ alias_method :realtime?, :realtime
19
+
20
+ def to_hash
21
+ {
22
+ **attributes,
23
+ data: JSON.parse(data.to_json),
24
+ }
25
+ end
26
+
27
+ def to_s
28
+ "#{real_exchange_name} -> #{routing_key} -> #{event}"
29
+ end
30
+
31
+ def basic_publish_args
32
+ Rabbit.config.validate!
33
+
34
+ raise "Routing key not specified" unless routing_key
35
+ raise "Event name not specified" unless event
36
+
37
+ options = {
38
+ mandatory: confirm_select?,
39
+ persistent: true,
40
+ type: event,
41
+ content_type: "application/json",
42
+ app_id: Rabbit.config.app_name,
43
+ headers: headers,
44
+ }
45
+
46
+ [JSON.dump(data), real_exchange_name, routing_key.to_s, options]
47
+ end
48
+
49
+ def exchange_name=(names)
50
+ super(Array(names).map(&:to_s))
51
+ end
52
+
53
+ def real_exchange_name
54
+ [Rabbit.config.group_id, Rabbit.config.project_id, *exchange_name].join(".")
55
+ end
56
+
57
+ private
58
+
59
+ def headers
60
+ super || {}
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbit
4
+ module Receiving
5
+ end
6
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rabbit"
4
+ require "rabbit/receiving"
5
+ require "rabbit/event_handler"
6
+
7
+ module Rabbit::Receiving::HandlerResolver
8
+ UnsupportedEvent = Class.new(StandardError)
9
+
10
+ class << self
11
+ def handler_for(message)
12
+ @handler_cache ||= Hash.new do |cache, (group_id, event)|
13
+ handler = unmemoized_handler_for(group_id, event)
14
+ cache[[group_id, event]] = handler if Rabbit.config.environment == :production
15
+ handler
16
+ end
17
+
18
+ @handler_cache[[message.group_id, message.event]]
19
+ end
20
+
21
+ private
22
+
23
+ def unmemoized_handler_for(group_id, event)
24
+ name = "rabbit/handler/#{group_id}/#{event}".camelize
25
+ handler = name.safe_constantize
26
+ if handler && handler < Rabbit::EventHandler
27
+ handler
28
+ else
29
+ raise UnsupportedEvent, "#{event.inspect} event from #{group_id.inspect} group is not " \
30
+ "supported, it requires a #{name.inspect} class inheriting from " \
31
+ "\"Rabbit::EventHandler\" to be defined"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lamian"
4
+
5
+ require "rabbit"
6
+ require "rabbit/receiving"
7
+ require "rabbit/receiving/message"
8
+ require "rabbit/receiving/handler_resolver"
9
+ require "rabbit/receiving/malformed_message"
10
+
11
+ class Rabbit::Receiving::Job < ActiveJob::Base
12
+ def perform(message, arguments)
13
+ Lamian.run do
14
+ begin
15
+ message = Rabbit::Receiving::Message.build(message, arguments)
16
+ handler = Rabbit::Receiving::HandlerResolver.handler_for(message)
17
+ handler.new(message).call
18
+ rescue Rabbit::Receiving::MalformedMessage => error
19
+ raise if Rabbit.config.environment == :test
20
+ Rabbit.config.exception_notifier.call(error)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbit::Receiving
4
+ class MalformedMessage < StandardError
5
+ attr_accessor :message_model, :errors
6
+
7
+ def self.logger
8
+ @logger ||= Logger.new(Rails.root.join("log", "malformed_messages.log"))
9
+ end
10
+
11
+ def self.raise!(message_model, errors, backtrace = caller(1))
12
+ error = new(message_model, errors)
13
+ logger.error error.message
14
+ raise error, error.message, backtrace
15
+ end
16
+
17
+ def initialize(message_model, errors)
18
+ self.message_model = message_model
19
+ self.errors = Array(errors)
20
+
21
+ errors_list = Array(errors).map { |e| " - #{e}" }.join("\n")
22
+
23
+ super(<<~MESSAGE)
24
+ Malformed message rejected for following reasons:
25
+ #{errors_list}
26
+ Message: #{message_model.attributes.inspect}
27
+ MESSAGE
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tainbox"
4
+
5
+ require "rabbit/receiving/malformed_message"
6
+
7
+ module Rabbit::Receiving
8
+ class Message
9
+ include Tainbox
10
+
11
+ attribute :group_id
12
+ attribute :project_id
13
+ attribute :event
14
+ attribute :data
15
+ attribute :original_message
16
+
17
+ attr_accessor :original_message
18
+
19
+ def self.build(message, type:, app_id:, **)
20
+ group_id, project_id = app_id.split(".")
21
+
22
+ new(group_id: group_id, project_id: project_id, event: type, data: message)
23
+ end
24
+
25
+ def data=(value)
26
+ self.original_message = value
27
+ super(JSON.parse(value).deep_symbolize_keys)
28
+ rescue JSON::ParserError => error
29
+ mark_as_malformed!("JSON::ParserError: #{error.message}")
30
+ end
31
+
32
+ def mark_as_malformed!(errors = "Error not specified")
33
+ MalformedMessage.raise!(self, errors, caller(1))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sneakers"
4
+
5
+ require "rabbit"
6
+ require "rabbit/receiving/message"
7
+ require "rabbit/receiving/malformed_message"
8
+ require "rabbit/receiving/handler_resolver"
9
+
10
+ module Rabbit::Receiving
11
+ autoload :Job, "rabbit/receiving/job"
12
+
13
+ class Worker
14
+ include Sneakers::Worker
15
+
16
+ def self.logger
17
+ @logger ||= Logger.new(Rails.root.join("log", "incoming_rabbit_messages.log"))
18
+ end
19
+
20
+ def work_with_params(message, delivery_info, arguments)
21
+ message = message.dup.force_encoding("UTF-8")
22
+ self.class.logger.debug([message, delivery_info, arguments].join(" / "))
23
+ job_class.set(queue: queue(message, arguments)).perform_later(message, arguments.to_h)
24
+ ack!
25
+ rescue => error
26
+ raise if Rabbit.config.environment == :test
27
+ Rabbit.config.exception_notifier.call(error)
28
+ requeue!
29
+ end
30
+
31
+ private
32
+
33
+ def queue(message, arguments)
34
+ message = Rabbit::Receiving::Message.build(message, arguments)
35
+ handler = Rabbit::Receiving::HandlerResolver.handler_for(message)
36
+ queue_name = handler.queue
37
+ ignore_conversion = handler.ignore_queue_conversion
38
+
39
+ return Rabbit.default_queue_name(ignore_conversion: ignore_conversion) unless queue_name
40
+
41
+ calculated_queue = begin
42
+ queue_name.is_a?(Proc) ? queue_name.call(message, arguments) : queue_name
43
+ end
44
+
45
+ Rabbit.queue_name(calculated_queue, ignore_conversion: ignore_conversion)
46
+ rescue
47
+ Rabbit.default_queue_name
48
+ end
49
+
50
+ def job_class
51
+ Rabbit.config.receiving_job_class_callable&.call || Job
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbit::TestHelpers
4
+ def send_rabbit_message(sender_id, event, data)
5
+ Rabbit::Receiving::Worker.new.work_with_params(data.to_json, {}, type: event, app_id: sender_id)
6
+ end
7
+
8
+ def expect_rabbit_message(args, strict: true)
9
+ expectation = if strict
10
+ args
11
+ else
12
+ -> (received_args) { expect(received_args).to match(args) }
13
+ end
14
+ expect(Rabbit).to receive(:publish).with(expectation).once
15
+ end
16
+
17
+ def expect_no_rabbit_messages
18
+ expect(Rabbit).not_to receive(:publish)
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabbit
4
+ VERSION = "0.6.0"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rabbit"
@@ -0,0 +1,38 @@
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 "rabbit/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.required_ruby_version = ">= 2.3.8"
9
+
10
+ spec.name = "rabbit_messaging"
11
+ spec.version = Rabbit::VERSION
12
+ spec.authors = ["Umbrellio"]
13
+ spec.email = ["oss@umbrellio.biz"]
14
+
15
+ spec.summary = "Rabbit (Rabbit Messaging)"
16
+ spec.description = "Rabbit (Rabbit Messaging)"
17
+ spec.homepage = "https://github.com/umbrellio/rabbit_messaging"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "bunny", "~> 2.0"
23
+ spec.add_runtime_dependency "exception_notification"
24
+ spec.add_runtime_dependency "lamian"
25
+ spec.add_runtime_dependency "rails"
26
+ spec.add_runtime_dependency "sneakers", "~> 2.0"
27
+ spec.add_runtime_dependency "tainbox"
28
+
29
+ spec.add_development_dependency "bundler"
30
+ spec.add_development_dependency "bundler-audit"
31
+ spec.add_development_dependency "coveralls"
32
+ spec.add_development_dependency "pry"
33
+ spec.add_development_dependency "rake"
34
+ spec.add_development_dependency "rspec"
35
+ spec.add_development_dependency "rspec-its"
36
+ spec.add_development_dependency "rubocop-config-umbrellio"
37
+ spec.add_development_dependency "simplecov"
38
+ end
metadata ADDED
@@ -0,0 +1,280 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rabbit_messaging
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Umbrellio
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-06-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bunny
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: exception_notification
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: lamian
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sneakers
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tainbox
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler-audit
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: coveralls
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rspec-its
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubocop-config-umbrellio
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: simplecov
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description: Rabbit (Rabbit Messaging)
224
+ email:
225
+ - oss@umbrellio.biz
226
+ executables: []
227
+ extensions: []
228
+ extra_rdoc_files: []
229
+ files:
230
+ - ".gitignore"
231
+ - ".rspec"
232
+ - ".rubocop.yml"
233
+ - ".travis.yml"
234
+ - Gemfile
235
+ - LICENSE.md
236
+ - README.md
237
+ - Rakefile
238
+ - bin/console
239
+ - bin/setup
240
+ - environments/development.rb
241
+ - lib/rabbit.rb
242
+ - lib/rabbit/daemon.rb
243
+ - lib/rabbit/event_handler.rb
244
+ - lib/rabbit/extensions/bunny/channel.rb
245
+ - lib/rabbit/publishing.rb
246
+ - lib/rabbit/publishing/job.rb
247
+ - lib/rabbit/publishing/message.rb
248
+ - lib/rabbit/receiving.rb
249
+ - lib/rabbit/receiving/handler_resolver.rb
250
+ - lib/rabbit/receiving/job.rb
251
+ - lib/rabbit/receiving/malformed_message.rb
252
+ - lib/rabbit/receiving/message.rb
253
+ - lib/rabbit/receiving/worker.rb
254
+ - lib/rabbit/test_helpers.rb
255
+ - lib/rabbit/version.rb
256
+ - lib/rabbit_messaging.rb
257
+ - rabbit_messaging.gemspec
258
+ homepage: https://github.com/umbrellio/rabbit_messaging
259
+ licenses: []
260
+ metadata: {}
261
+ post_install_message:
262
+ rdoc_options: []
263
+ require_paths:
264
+ - lib
265
+ required_ruby_version: !ruby/object:Gem::Requirement
266
+ requirements:
267
+ - - ">="
268
+ - !ruby/object:Gem::Version
269
+ version: 2.3.8
270
+ required_rubygems_version: !ruby/object:Gem::Requirement
271
+ requirements:
272
+ - - ">="
273
+ - !ruby/object:Gem::Version
274
+ version: '0'
275
+ requirements: []
276
+ rubygems_version: 3.0.3
277
+ signing_key:
278
+ specification_version: 4
279
+ summary: Rabbit (Rabbit Messaging)
280
+ test_files: []