aws_sqs_moniter 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws_sqs_moniter.rb +26 -2
  3. data/lib/aws_sqs_moniter/aws/arns.rb +29 -0
  4. data/lib/aws_sqs_moniter/aws/builder.rb +48 -0
  5. data/lib/aws_sqs_moniter/aws/builder/application_policy_builder.rb +73 -0
  6. data/lib/aws_sqs_moniter/aws/builder/queue_builder.rb +100 -0
  7. data/lib/aws_sqs_moniter/aws/builder/subscription_builder.rb +48 -0
  8. data/lib/aws_sqs_moniter/aws/builder/topic_builder.rb +27 -0
  9. data/lib/aws_sqs_moniter/aws/environmental_name.rb +13 -0
  10. data/lib/aws_sqs_moniter/configuration.rb +110 -0
  11. data/lib/aws_sqs_moniter/configuration/queue_configuration.rb +49 -0
  12. data/lib/aws_sqs_moniter/configuration/redrive_policy_configuration.rb +33 -0
  13. data/lib/aws_sqs_moniter/configuration/validatable.rb +15 -0
  14. data/lib/aws_sqs_moniter/dead_letters/retrier.rb +23 -0
  15. data/lib/aws_sqs_moniter/dead_letters/worker.rb +34 -0
  16. data/lib/aws_sqs_moniter/logging.rb +66 -0
  17. data/lib/aws_sqs_moniter/middleware/server/active_record/connection_pool.rb +15 -0
  18. data/lib/aws_sqs_moniter/middleware/server/active_record/idempotence.rb +24 -0
  19. data/lib/aws_sqs_moniter/middleware/server/active_record/retrier.rb +23 -0
  20. data/lib/aws_sqs_moniter/middleware/server/active_record/transaction.rb +36 -0
  21. data/lib/aws_sqs_moniter/middleware/server/airbrake.rb +24 -0
  22. data/lib/aws_sqs_moniter/monkey_patches/forbid_implicit_active_record_connection_checkout.rb +34 -0
  23. data/lib/aws_sqs_moniter/railtie.rb +10 -0
  24. data/lib/aws_sqs_moniter/typed_message.rb +37 -0
  25. data/lib/aws_sqs_moniter/version.rb +1 -1
  26. data/lib/aws_sqs_moniter/worker_registries/typed_message_registry.rb +99 -0
  27. data/lib/generators/aws_sqs_moniter/install_generator.rb +30 -0
  28. data/lib/generators/aws_sqs_moniter/templates/create_dead_letters_migration.rb +11 -0
  29. data/lib/generators/aws_sqs_moniter/templates/create_processed_messages_migration.rb +13 -0
  30. data/lib/generators/aws_sqs_moniter/templates/create_published_messages_migration.rb +20 -0
  31. data/lib/generators/aws_sqs_moniter/templates/dead_letter.rb +14 -0
  32. data/lib/generators/aws_sqs_moniter/templates/initializer.rb +32 -0
  33. data/lib/generators/aws_sqs_moniter/templates/processed_message.rb +12 -0
  34. data/lib/generators/aws_sqs_moniter/templates/published_message.rb +14 -0
  35. data/lib/generators/aws_sqs_moniter/templates/shoryuken.yml +16 -0
  36. data/lib/tasks/aws_sqs_moniter.rake +83 -0
  37. data/lib/tasks/aws_sqs_setup.rake +10 -0
  38. metadata +52 -12
  39. data/.gitignore +0 -14
  40. data/Gemfile +0 -4
  41. data/LICENSE.txt +0 -22
  42. data/README.md +0 -31
  43. data/aws_sqs_moniter-0.0.1.gem +0 -0
  44. data/aws_sqs_moniter.gemspec +0 -36
  45. data/test/test_helper.rb +0 -0
@@ -0,0 +1,49 @@
1
+ module AwsSqsMoniter
2
+ class Configuration
3
+ class QueueConfiguration
4
+ include Validatable
5
+
6
+ def initialize name
7
+ @name = name
8
+ @delay_seconds = 0
9
+ @message_retention_period = 1_209_600
10
+ @visibility_timeout = 30
11
+ end
12
+
13
+ attr_accessor :delay_seconds,
14
+ :message_retention_period,
15
+ :visibility_timeout
16
+
17
+ attr_reader :name
18
+
19
+ def redrive_policy
20
+ @redrive_policy ||= RedrivePolicyConfiguration.new self
21
+ end
22
+
23
+ def validate
24
+ unless (0..900).include? delay_seconds
25
+ errors << "#{name}.delay_seconds must be in the range 0..900"
26
+ end
27
+
28
+ unless (60..1_209_600).include? message_retention_period
29
+ errors << "#{name}.message_retention_period must be in the range 60..1209600"
30
+ end
31
+
32
+ unless (0..43_200).include? visibility_timeout
33
+ errors << "#{name}.visibility_timeout must be in the range 0..43200"
34
+ end
35
+
36
+ redrive_policy.valid?
37
+ errors.push *(redrive_policy.errors)
38
+ end
39
+
40
+ def copy_onto queue
41
+ queue.delay_seconds = delay_seconds
42
+ queue.message_retention_period = message_retention_period
43
+ queue.visibility_timeout = visibility_timeout
44
+
45
+ redrive_policy.copy_onto queue.redrive_policy if queue.respond_to? :redrive_policy
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ module AwsSqsMoniter
2
+ class Configuration
3
+ class RedrivePolicyConfiguration
4
+ include Validatable
5
+
6
+ def initialize owner
7
+ @owner = owner
8
+ @enabled = true
9
+ @max_receive_count = 10
10
+ end
11
+
12
+ attr_accessor :enabled,
13
+ :max_receive_count,
14
+ :dead_letter_queue
15
+
16
+ def validate
17
+ unless (1..1000).include? max_receive_count
18
+ errors << "#{@owner.name}.redrive_policy.max_receive_count must be in the range 1..1000"
19
+ end
20
+
21
+ if enabled && dead_letter_queue.blank?
22
+ errors << "#{@owner.name}.redrive_policy.dead_letter_queue is required"
23
+ end
24
+ end
25
+
26
+ def copy_onto redrive_policy
27
+ redrive_policy.enabled = enabled
28
+ redrive_policy.max_receive_count = max_receive_count
29
+ redrive_policy.dead_letter_queue = dead_letter_queue
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ module AwsSqsMoniter
2
+ class Configuration
3
+ module Validatable
4
+ def errors
5
+ @errors ||= []
6
+ end
7
+
8
+ def valid?
9
+ @errors = []
10
+ validate
11
+ @errors.empty?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module AwsSqsMoniter
2
+ module DeadLetters
3
+ class Retrier
4
+ def initialize logger = nil
5
+ @publisher = AwsSqsMoniter::MessagePublisher.new
6
+ @logger = logger || Shoryuken::Logging.logger
7
+ end
8
+
9
+ def retry scope
10
+ return if scope.count == 0
11
+
12
+ count = 0
13
+ scope.each do |message|
14
+ count += 1
15
+ @publisher.publish message.message
16
+ message.delete
17
+ end
18
+
19
+ @logger.info "Retried #{count} dead letter(s)."
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ require 'aws_sqs_moniter/middleware/server/airbrake'
2
+ require 'aws_sqs_moniter/middleware/server/active_record/connection_pool'
3
+ require 'aws_sqs_moniter/middleware/server/active_record/transaction'
4
+
5
+ module AwsSqsMoniter
6
+ module DeadLetters
7
+ class Worker
8
+ include Shoryuken::Worker
9
+
10
+ shoryuken_options(
11
+ auto_delete: true,
12
+ body_parser: :json,
13
+ subscriptions: {
14
+ financials_dlq: '*' })
15
+
16
+ server_middleware do |chain|
17
+ chain.remove Middleware::Server::Airbrake
18
+ chain.add Middleware::Server::ActiveRecord::ConnectionPool
19
+ chain.add Middleware::Server::ActiveRecord::Transaction, isolation: :repeatable_read
20
+ end
21
+
22
+ def perform sqs_message, payload
23
+ typed_message = TypedMessage.new sqs_message
24
+
25
+ return if DeadLetter.exists? message_id: typed_message.id
26
+
27
+ DeadLetter.create!(
28
+ sqs_id: sqs_message.message_id,
29
+ message_id: typed_message.id,
30
+ message: payload)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,66 @@
1
+ require 'logger'
2
+
3
+ module AwsSqsMoniter
4
+ module Logging
5
+ class Formatter < Logger::Formatter
6
+ def call severity, time, _program_name, message
7
+ data_hash = message.is_a?(Hash) ? message : { message: message }
8
+ error_hash = {}
9
+
10
+ %i(timestamp pid thread severity).each { |key| data_hash.delete key }
11
+
12
+ if (error_object = data_hash.delete(:error))
13
+ error_hash[:error] = error_object.to_s
14
+ error_hash[:backtrace] = format_backtrace(error_object.backtrace) if error_object.backtrace
15
+ end
16
+
17
+ timestamp = time.utc.iso8601
18
+ severity = severity.downcase
19
+ data = format_hash data_hash
20
+ error = format_hash error_hash
21
+ text = %(timestamp="#{timestamp}" pid="#{pid}" thread="#{thread}" severity="#{severity}" #{data} #{error}).strip
22
+
23
+ "#{text}\n"
24
+ end
25
+
26
+ private
27
+
28
+ def escape string
29
+ string.gsub(/"/, '"').gsub("\n", ' ')
30
+ end
31
+
32
+ def format_backtrace backtrace
33
+ backtrace.map { |line| %("#{line}") }.join(', ')
34
+ end
35
+
36
+ def format_hash hash
37
+ hash.map do |k, v|
38
+ v = escape v.to_s
39
+ %(#{k}="#{v}")
40
+ end.join ' '
41
+ end
42
+
43
+ def pid
44
+ Process.pid
45
+ end
46
+
47
+ def thread
48
+ Thread.current.object_id.to_s 36
49
+ end
50
+ end
51
+
52
+ class << self
53
+ attr_accessor :default_log_device
54
+ attr_writer :logger
55
+
56
+ def logger
57
+ @logger ||= begin
58
+ Logger.new(default_log_device || STDOUT).tap do |l|
59
+ l.level = Logger::INFO
60
+ l.formatter = Formatter.new
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,15 @@
1
+ module AwsSqsMoniter
2
+ module Middleware
3
+ module Server
4
+ module ActiveRecord
5
+ class ConnectionPool
6
+ def call(*_args)
7
+ ::ActiveRecord::Base.connection_pool.with_connection do
8
+ yield
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module AwsSqsMoniter
2
+ module Middleware
3
+ module Server
4
+ module ActiveRecord
5
+ class Idempotence
6
+ def initialize logger: Shoryuken::Logging.logger
7
+ @logger = logger
8
+ end
9
+
10
+ def call _worker, queue, sqs_msg, _body
11
+ message = TypedMessage.new sqs_msg
12
+
13
+ if ProcessedMessage.exists? message_id: message.id, queue: queue
14
+ @logger.info middleware: 'idempotence', ignored_message_id: message.id
15
+ else
16
+ yield
17
+ ProcessedMessage.log message
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require 'retryable'
2
+
3
+ module AwsSqsMoniter
4
+ module Middleware
5
+ module Server
6
+ module ActiveRecord
7
+ class Retrier
8
+ def initialize(options = {})
9
+ @options = { tries: 10, sleep: 0 }.merge options
10
+ end
11
+
12
+ def call(&block)
13
+ Retryable.retryable(@options.merge(on: [::ActiveRecord::RecordNotUnique])) do
14
+ Retryable.retryable(@options.merge(matching: /TRDeadlockDetected|TRSerializationFailure/)) do
15
+ yield block
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'retrier'
2
+
3
+ module AwsSqsMoniter
4
+ module Middleware
5
+ module Server
6
+ module ActiveRecord
7
+ class Transaction
8
+ RETRIER_OPTIONS = %i(tries sleep on_retriable_error)
9
+ TRANSACTION_OPTIONS = %i(requires_new joinable isolation)
10
+
11
+ def initialize options = {}
12
+ @transaction_options = options.select { |k, _| TRANSACTION_OPTIONS.include? k }
13
+ @retrier_default_options = options.select { |k, _| RETRIER_OPTIONS.include? k }
14
+ end
15
+
16
+ def call worker, _queue, _sqs_msg, _body
17
+ Retrier.new(retrier_options(worker)).call do
18
+ ::ActiveRecord::Base.transaction(@transaction_options) do
19
+ yield
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def retrier_options worker
27
+ options = @retrier_default_options.dup
28
+ on_error = options.delete :on_retriable_error
29
+ options[:exception_cb] = worker.method(on_error) unless on_error.to_s.empty?
30
+ options
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module AwsSqsMoniter
2
+ module Middleware
3
+ module Server
4
+ class Airbrake
5
+ def call(_worker, _queue, sqs_msg, body)
6
+ yield
7
+ rescue => e
8
+ parameters = {}
9
+
10
+ begin
11
+ message = TypedMessage.new sqs_msg
12
+ parameters.store :message, message.headers
13
+ rescue
14
+ parameters.store :unknown_message_format, body
15
+ end
16
+
17
+ ::Airbrake.notify_or_ignore e, parameters: parameters
18
+
19
+ raise e
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveRecord
2
+ class Base
3
+ class << self
4
+ def forbid_implicit_checkout!
5
+ Thread.current[:active_record_forbid_implicit_connections] = true
6
+ end
7
+
8
+ def implicit_checkout_forbidden?
9
+ !!Thread.current[:active_record_forbid_implicit_connections]
10
+ end
11
+
12
+ def connection_with_forbid_implicit(*args, &block)
13
+ if implicit_checkout_forbidden? && !connection_handler.retrieve_connection_pool(self).active_connection?
14
+ message = 'Implicit ActiveRecord checkout attempted when Thread :force_explicit_connections set!'
15
+
16
+ # I want to make SURE I see this error in test output, even though
17
+ # in some cases my code is swallowing the exception.
18
+ $stderr.puts(message) if Rails.env.test?
19
+
20
+ fail ImplicitConnectionForbiddenError, message
21
+ end
22
+
23
+ connection_without_forbid_implicit(*args, &block)
24
+ end
25
+
26
+ alias_method_chain :connection, :forbid_implicit
27
+ end
28
+ end
29
+
30
+ # We're refusing to give a connection when asked for. Same outcome
31
+ # as if the pool timed out on checkout, so let's subclass the exception
32
+ # used for that.
33
+ ImplicitConnectionForbiddenError = Class.new(::ActiveRecord::ConnectionTimeoutError)
34
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails'
2
+
3
+ module AwsSqsMoniter
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path('../../tasks/aws_sqs_moniter.rake', __FILE__)
7
+ load File.expand_path('../../tasks/aws_sqs_setup.rake', __FILE__)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ module AwsSqsMoniter
2
+ class TypedMessage
3
+ def initialize(message)
4
+ @message = message
5
+ end
6
+
7
+ def id
8
+ headers['id']
9
+ end
10
+
11
+ def type
12
+ headers['type']
13
+ end
14
+
15
+ def version
16
+ headers['version']
17
+ end
18
+
19
+ def body
20
+ hash['body']
21
+ end
22
+
23
+ def to_h
24
+ hash
25
+ end
26
+
27
+ private
28
+
29
+ def hash
30
+ @hash ||= JSON.parse(@message.body)
31
+ end
32
+
33
+ def headers
34
+ hash['header']
35
+ end
36
+ end
37
+ end