From 76bca8aa8ca05bd2db71c7ab3caa67e94ba2b34a Mon Sep 17 00:00:00 2001 From: Brusk Awat Date: Thu, 1 Dec 2022 16:35:46 +0300 Subject: [PATCH] Adds logic --- .rubocop.yml | 120 +++++++++++++++++++++++++--- Gemfile.lock | 81 +++++++++++++++++++ lib/rabbit_carrots.rb | 4 + lib/rabbit_carrots/configuration.rb | 14 ++++ lib/rabbit_carrots/connection.rb | 25 ++++++ lib/railtie.rb | 13 +++ lib/tasks/rmq.rake | 54 +++++++++++++ rabbit_carrots.gemspec | 17 ++-- 8 files changed, 311 insertions(+), 17 deletions(-) create mode 100644 Gemfile.lock create mode 100644 lib/rabbit_carrots/configuration.rb create mode 100644 lib/rabbit_carrots/connection.rb create mode 100644 lib/railtie.rb create mode 100644 lib/tasks/rmq.rake diff --git a/.rubocop.yml b/.rubocop.yml index e3462a7..8fac6d2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,13 +1,113 @@ +require: rubocop-rails + AllCops: - TargetRubyVersion: 2.6 - -Style/StringLiterals: + NewCops: enable + SuggestExtensions: false +Layout/SpaceBeforeBrackets: # (new in 1.7) Enabled: true - EnforcedStyle: double_quotes - -Style/StringLiteralsInInterpolation: +Layout/LineLength: + Max: 350 +Lint/AmbiguousAssignment: # (new in 1.7) Enabled: true - EnforcedStyle: double_quotes - -Layout/LineLength: - Max: 120 +Lint/DeprecatedConstants: # (new in 1.8) + Enabled: true +Lint/DuplicateBranch: # (new in 1.3) + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) + Enabled: true +Lint/EmptyBlock: # (new in 1.1) + Enabled: true +Lint/EmptyClass: # (new in 1.3) + Enabled: true +Lint/LambdaWithoutLiteralBlock: # (new in 1.8) + Enabled: true +Lint/NoReturnInBeginEndBlocks: # (new in 1.2) + Enabled: true +Lint/NumberedParameterAssignment: # (new in 1.9) + Enabled: true +Lint/OrAssignmentToConstant: # (new in 1.9) + Enabled: true +Lint/RedundantDirGlobSort: # (new in 1.8) + Enabled: true +Lint/SymbolConversion: # (new in 1.9) + Enabled: true +Lint/ToEnumArguments: # (new in 1.1) + Enabled: true +Lint/TripleQuotes: # (new in 1.9) + Enabled: true +Lint/UnexpectedBlockArity: # (new in 1.5) + Enabled: true +Lint/UnmodifiedReduceAccumulator: # (new in 1.1) + Enabled: true +Style/ArgumentsForwarding: # (new in 1.1) + Enabled: true +Style/CollectionCompact: # (new in 1.2) + Enabled: true +Style/DocumentDynamicEvalDefinition: # (new in 1.1) + Enabled: true +Style/Documentation: + Enabled: false +Style/FrozenStringLiteralComment: + Enabled: false +Style/EndlessMethod: # (new in 1.8) + Enabled: true +Style/HashConversion: # (new in 1.10) + Enabled: true +Style/HashExcept: # (new in 1.7) + Enabled: true +Style/IfWithBooleanLiteralBranches: # (new in 1.9) + Enabled: true +Style/NegatedIfElseCondition: # (new in 1.2) + Enabled: true +Style/NilLambda: # (new in 1.3) + Enabled: true +Style/RedundantArgument: # (new in 1.4) + Enabled: true +Style/SwapValues: # (new in 1.1) + Enabled: true +Rails/ActiveRecordCallbacksOrder: # (new in 2.7) + Enabled: true +Rails/AfterCommitOverride: # (new in 2.8) + Enabled: true +Rails/AttributeDefaultBlockValue: # (new in 2.9) + Enabled: true +Rails/FindById: # (new in 2.7) + Enabled: true +Rails/Inquiry: # (new in 2.7) + Enabled: true +Rails/MailerName: # (new in 2.7) + Enabled: true +Rails/MatchRoute: # (new in 2.7) + Enabled: true +Rails/NegateInclude: # (new in 2.7) + Enabled: true +Rails/Pluck: # (new in 2.7) + Enabled: true +Rails/PluckInWhere: # (new in 2.7) + Enabled: true +Rails/RenderInline: # (new in 2.7) + Enabled: true +Rails/RenderPlainText: # (new in 2.7) + Enabled: true +Rails/ShortI18n: # (new in 2.7) + Enabled: true +Rails/SquishedSQLHeredocs: # (new in 2.8) + Enabled: true +Rails/UniqueValidationWithoutIndex: + Enabled: false +Rails/WhereEquals: # (new in 2.9) + Enabled: true +Rails/WhereExists: # (new in 2.7) + Enabled: true +Rails/WhereNot: # (new in 2.8) + Enabled: true +Metrics/BlockLength: + Enabled: false +Metrics/AbcSize: + Enabled: false +Metrics/MethodLength: + Enabled: false +Metrics/CyclomaticComplexity: + Max: 15 +Metrics/PerceivedComplexity: + Max: 15 \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..98b18df --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,81 @@ +PATH + remote: . + specs: + rabbit_carrots (0.1.0) + activesupport (>= 6.0.0) + bunny (>= 2.19.0) + connection_pool (~> 2.3.0) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.0.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + amq-protocol (2.3.2) + ast (2.4.2) + bunny (2.19.0) + amq-protocol (~> 2.3, >= 2.3.1) + sorted_set (~> 1, >= 1.0.2) + concurrent-ruby (1.1.10) + connection_pool (2.3.0) + diff-lcs (1.5.0) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + json (2.6.2) + minitest (5.16.3) + parallel (1.22.1) + parser (3.1.2.1) + ast (~> 2.4.1) + rainbow (3.1.1) + rake (13.0.6) + rbtree (0.4.5) + regexp_parser (2.6.1) + rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rubocop (1.39.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.23.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.23.0) + parser (>= 3.1.1.0) + ruby-progressbar (1.11.0) + set (1.0.3) + sorted_set (1.0.3) + rbtree + set (~> 1.0) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) + unicode-display_width (2.3.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + rabbit_carrots! + rake (~> 13.0) + rspec (~> 3.0) + rubocop (~> 1.21) + +BUNDLED WITH + 2.3.23 diff --git a/lib/rabbit_carrots.rb b/lib/rabbit_carrots.rb index 60dc2ee..9cced03 100644 --- a/lib/rabbit_carrots.rb +++ b/lib/rabbit_carrots.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true require_relative "rabbit_carrots/version" +require 'rabbit_carrots/connection' +require 'rabbit_carrots/configuration' +require 'rabbit_carrots/railtie' if defined?(Rails) + module RabbitCarrots class Error < StandardError; end diff --git a/lib/rabbit_carrots/configuration.rb b/lib/rabbit_carrots/configuration.rb new file mode 100644 index 0000000..b861a1e --- /dev/null +++ b/lib/rabbit_carrots/configuration.rb @@ -0,0 +1,14 @@ +module RabbitCarrots + class << self + attr_accessor :configuration + end + + def self.configure + self.configuration ||= Configuration.new + yield(configuration) + end + + class Configuration + attr_accessor :rabbitmq_host, :rabbitmq_port, :rabbitmq_user, :rabbitmq_password, :rabbitmq_vhost, :routing_key_mappings, :event_bus_exchange_name + end +end diff --git a/lib/rabbit_carrots/connection.rb b/lib/rabbit_carrots/connection.rb new file mode 100644 index 0000000..f81bd6f --- /dev/null +++ b/lib/rabbit_carrots/connection.rb @@ -0,0 +1,25 @@ +require 'singleton' + +module RabbitCarrots + class Connection + include ::Singleton + attr_reader :connection + + def initialize + @connection = Bunny.new( + host: Configuration.rabbitmq_host, + port: Configuration.rabbitmq_port, + user: Configuration.rabbitmq_user, + password: Configuration.rabbitmq_password, + vhost: Configuration.rabbitmq_vhost + ) + @connection.start + end + + def channel + @channel ||= ConnectionPool.new do + connection.create_channel + end + end + end +end diff --git a/lib/railtie.rb b/lib/railtie.rb new file mode 100644 index 0000000..28a41d1 --- /dev/null +++ b/lib/railtie.rb @@ -0,0 +1,13 @@ +# lib/railtie.rb +require 'rabbit_carrots' + +module RabbitCarrots + class Railtie < Rails::Railtie + railtie_name :rabbit_carrots + + rake_tasks do + path = File.expand_path(__dir__) + Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f } + end + end +end \ No newline at end of file diff --git a/lib/tasks/rmq.rake b/lib/tasks/rmq.rake new file mode 100644 index 0000000..6754813 --- /dev/null +++ b/lib/tasks/rmq.rake @@ -0,0 +1,54 @@ +require 'bunny' + +namespace :rmq do + desc 'Listener for Queue' + task subscriber: :environment do + Rails.application.eager_load! + + channels = RabbitCarrots::Configuration.routing_key_mappings + + Rails.logger = Logger.new($stdout) + + # Run RMQ Subscriber for each channel + channels.each do |channel| + handler_class = channel[:handler] + + raise "#{handler_class.name} must respond to `handle!`" unless handler_class.respond_to?(:handle!) + + run_task(queue_name: channel[:queue], handler_class:, routing_keys: channel[:routing_keys]) + end + + # Infinite loop to keep the process running + loop do + sleep 1 + end + end +end + +def run_task(queue_name:, handler_class:, routing_keys:) + RabbitConnection.instance.channel.with do |channel| + exchange = channel.topic(RabbitCarrots::Configuration.event_bus_exchange_name, durable: true) + + Rails.logger.info "Listening on QUEUE: #{queue_name} for ROUTING KEYS: #{routing_keys}" + queue = channel.queue(queue_name, durable: true) + + routing_keys.map(&:strip).each { |k| queue.bind(exchange, routing_key: k) } + + queue.subscribe(block: false, manual_ack: true, prefetch: 10) do |delivery_info, properties, payload| + Rails.logger.info "Received from queue: #{queue_name}, Routing Keys: #{routing_keys}" + handler_class.handle!(channel, delivery_info, properties, payload) + channel.ack(delivery_info.delivery_tag, false) + rescue EventHandlers::Errors::NackMessage, JSON::ParserError => _e + Rails.logger.info "Nacked message: #{payload}" + channel.nack(delivery_info.delivery_tag, false, false) + rescue EventHandlers::Errors::NackAndRequeueMessage => _e + Rails.logger.info "Nacked and Requeued message: #{payload}" + channel.nack(delivery_info.delivery_tag, false, true) + rescue StandardError => e + Rails.logger.error "Error handling message: #{payload}. Error: #{e.message}" + channel.nack(delivery_info.delivery_tag, false, true) + end + + Rails.logger.info 'RUN TASK ENDED' + end +end diff --git a/rabbit_carrots.gemspec b/rabbit_carrots.gemspec index 9112dd1..33a6a13 100644 --- a/rabbit_carrots.gemspec +++ b/rabbit_carrots.gemspec @@ -8,17 +8,17 @@ Gem::Specification.new do |spec| spec.authors = ["Brusk Awat"] spec.email = ["broosk.edogawa@gmail.com"] - spec.summary = "TODO: Write a short summary, because RubyGems requires one." - spec.description = "TODO: Write a longer description or delete this line." - spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.summary = "A simple RabbitMQ consumer task" + spec.description = "A background task based on rake to consume RabbitMQ messages" + spec.homepage = "https://github.com/ditkrg" spec.license = "MIT" - spec.required_ruby_version = ">= 2.6.0" + spec.required_ruby_version = ">= 3.1.0" spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["source_code_uri"] = "https://github.com/ditkrg/rabbit_carrots" + spec.metadata["changelog_uri"] = "https://github.com/ditkrg/rabbit_carrots" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -32,7 +32,10 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency "bunny", ">= 2.19.0" + spec.add_dependency "connection_pool", "~> 2.3.0" + spec.add_dependency "activesupport", ">= 6.0.0" + # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html