hermes_messenger_of_the_gods 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +9 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/Gemfile +13 -0
  8. data/README.md +464 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/exe/fly_hermes +57 -0
  13. data/hermes_messenger_of_the_gods.gemspec +38 -0
  14. data/lib/hermes_messenger_of_the_gods.rb +57 -0
  15. data/lib/hermes_messenger_of_the_gods/concerns/base.rb +42 -0
  16. data/lib/hermes_messenger_of_the_gods/concerns/grpc_protobuf.rb +62 -0
  17. data/lib/hermes_messenger_of_the_gods/concerns/message.rb +194 -0
  18. data/lib/hermes_messenger_of_the_gods/concerns/mono_message.rb +52 -0
  19. data/lib/hermes_messenger_of_the_gods/concerns/worker.rb +173 -0
  20. data/lib/hermes_messenger_of_the_gods/configuration.rb +29 -0
  21. data/lib/hermes_messenger_of_the_gods/endpoint_builder.rb +41 -0
  22. data/lib/hermes_messenger_of_the_gods/endpoints.rb +3 -0
  23. data/lib/hermes_messenger_of_the_gods/endpoints/base.rb +113 -0
  24. data/lib/hermes_messenger_of_the_gods/endpoints/sns.rb +21 -0
  25. data/lib/hermes_messenger_of_the_gods/endpoints/sqs.rb +114 -0
  26. data/lib/hermes_messenger_of_the_gods/exceptions.rb +27 -0
  27. data/lib/hermes_messenger_of_the_gods/logging_helpers.rb +30 -0
  28. data/lib/hermes_messenger_of_the_gods/output/basic.rb +63 -0
  29. data/lib/hermes_messenger_of_the_gods/status_server.rb +48 -0
  30. data/lib/hermes_messenger_of_the_gods/testing/array_endpoint.rb +46 -0
  31. data/lib/hermes_messenger_of_the_gods/testing/dispatch_matcher.rb +52 -0
  32. data/lib/hermes_messenger_of_the_gods/testing/rspec_helpers.rb +64 -0
  33. data/lib/hermes_messenger_of_the_gods/version.rb +3 -0
  34. data/packageGems.sh +13 -0
  35. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.bundlecache +0 -0
  36. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.circleci/config.yml +32 -0
  37. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.gitignore +11 -0
  38. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.rspec +3 -0
  39. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.rubocop.yml +42 -0
  40. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.ruby-version +1 -0
  41. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.travis.yml +5 -0
  42. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Gemfile +6 -0
  43. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Gemfile.lock +75 -0
  44. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Makefile +1 -0
  45. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/README.md +78 -0
  46. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Rakefile +6 -0
  47. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/bundle +105 -0
  48. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/coderay +29 -0
  49. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/console +14 -0
  50. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/grpc_tools_ruby_protoc +29 -0
  51. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/grpc_tools_ruby_protoc_plugin +29 -0
  52. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/htmldiff +29 -0
  53. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ldiff +29 -0
  54. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/pry +29 -0
  55. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rake +29 -0
  56. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rspec +29 -0
  57. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rubocop +29 -0
  58. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ruby-parse +29 -0
  59. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ruby-rewrite +29 -0
  60. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/setup +8 -0
  61. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer.rb +123 -0
  62. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/encoder.rb +63 -0
  63. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/generation_helpers.rb +23 -0
  64. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/reflector.rb +66 -0
  65. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/version.rb +3 -0
  66. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/protobuf3_fixer.gemspec +54 -0
  67. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/bundle +105 -0
  68. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/coderay +29 -0
  69. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/grpc_tools_ruby_protoc +29 -0
  70. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/grpc_tools_ruby_protoc_plugin +29 -0
  71. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/htmldiff +29 -0
  72. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ldiff +29 -0
  73. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/pry +29 -0
  74. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rake +29 -0
  75. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rspec +29 -0
  76. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rubocop +29 -0
  77. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ruby-parse +29 -0
  78. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ruby-rewrite +29 -0
  79. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/build_from_hash_spec.rb +20 -0
  80. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/encoding/encoding_options_spec.rb +23 -0
  81. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/extra_fields/json_decode_of_superset_spec.rb +54 -0
  82. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/generation_helpers_spec.rb +37 -0
  83. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/protobuf3_fixer_spec.rb +5 -0
  84. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/spec_helper.rb +17 -0
  85. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/compiled_protobuffs/source/superset_pb.rb +44 -0
  86. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/compiled_protobuffs/source/timestamp_pb.rb +27 -0
  87. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/protobuffs/source/superset.proto +40 -0
  88. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/protobuffs/source/timestamp.proto +18 -0
  89. data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/well_known_type_fixes/timestamp_spec.rb +79 -0
  90. data/vendor/cache/activemodel-6.0.3.1.gem +0 -0
  91. data/vendor/cache/activesupport-6.0.3.1.gem +0 -0
  92. data/vendor/cache/addressable-2.6.0.gem +0 -0
  93. data/vendor/cache/aws-eventstream-1.1.0.gem +0 -0
  94. data/vendor/cache/aws-partitions-1.329.0.gem +0 -0
  95. data/vendor/cache/aws-sdk-core-3.99.2.gem +0 -0
  96. data/vendor/cache/aws-sdk-sns-1.25.1.gem +0 -0
  97. data/vendor/cache/aws-sdk-sqs-1.27.1.gem +0 -0
  98. data/vendor/cache/aws-sigv4-1.1.4.gem +0 -0
  99. data/vendor/cache/codeclimate-test-reporter-1.0.9.gem +0 -0
  100. data/vendor/cache/coderay-1.1.2.gem +0 -0
  101. data/vendor/cache/concurrent-ruby-1.1.6.gem +0 -0
  102. data/vendor/cache/crack-0.4.3.gem +0 -0
  103. data/vendor/cache/diff-lcs-1.3.gem +0 -0
  104. data/vendor/cache/docile-1.1.5.gem +0 -0
  105. data/vendor/cache/google-protobuf-3.12.2-universal-darwin.gem +0 -0
  106. data/vendor/cache/google-protobuf-3.12.2-x86_64-linux.gem +0 -0
  107. data/vendor/cache/google-protobuf-3.12.2.gem +0 -0
  108. data/vendor/cache/googleapis-common-protos-types-1.0.2.gem +0 -0
  109. data/vendor/cache/grpc-1.18.0-universal-darwin.gem +0 -0
  110. data/vendor/cache/grpc-1.18.0-x86_64-linux.gem +0 -0
  111. data/vendor/cache/grpc-1.18.0.gem +0 -0
  112. data/vendor/cache/hashdiff-0.3.8.gem +0 -0
  113. data/vendor/cache/i18n-1.8.3.gem +0 -0
  114. data/vendor/cache/jmespath-1.4.0.gem +0 -0
  115. data/vendor/cache/json-2.1.0.gem +0 -0
  116. data/vendor/cache/memory_profiler-0.9.12.gem +0 -0
  117. data/vendor/cache/method_source-0.9.2.gem +0 -0
  118. data/vendor/cache/minitest-5.14.1.gem +0 -0
  119. data/vendor/cache/pry-0.12.2.gem +0 -0
  120. data/vendor/cache/public_suffix-3.0.3.gem +0 -0
  121. data/vendor/cache/rake-10.5.0.gem +0 -0
  122. data/vendor/cache/rspec-3.8.0.gem +0 -0
  123. data/vendor/cache/rspec-core-3.8.0.gem +0 -0
  124. data/vendor/cache/rspec-expectations-3.8.2.gem +0 -0
  125. data/vendor/cache/rspec-mocks-3.8.0.gem +0 -0
  126. data/vendor/cache/rspec-support-3.8.0.gem +0 -0
  127. data/vendor/cache/safe_yaml-1.0.4.gem +0 -0
  128. data/vendor/cache/simplecov-0.13.0.gem +0 -0
  129. data/vendor/cache/simplecov-html-0.10.2.gem +0 -0
  130. data/vendor/cache/thor-1.0.1.gem +0 -0
  131. data/vendor/cache/thread_safe-0.3.6.gem +0 -0
  132. data/vendor/cache/timecop-0.9.1.gem +0 -0
  133. data/vendor/cache/tzinfo-1.2.7.gem +0 -0
  134. data/vendor/cache/webmock-3.5.1.gem +0 -0
  135. data/vendor/cache/zeitwerk-2.3.0.gem +0 -0
  136. metadata +362 -0
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ require 'hermes_messenger_of_the_gods/concerns/message'
6
+ require 'hermes_messenger_of_the_gods/concerns/worker'
7
+
8
+ module HermesMessengerOfTheGods
9
+ module Concerns
10
+ module MonoMessage
11
+ extend ActiveSupport::Concern
12
+
13
+ include HermesMessengerOfTheGods::Concerns::Message
14
+
15
+ class_methods do
16
+ def endpoints=(*args)
17
+ super
18
+ worker_klass.endpoints = endpoints
19
+ end
20
+
21
+ def circuit_breaker_errors=(*args)
22
+ super
23
+ worker_klass.circuit_breaker_errors = circuit_breaker_errors
24
+ end
25
+
26
+ def max_consecutive_failures=(*args)
27
+ super
28
+ worker_klass.max_consecutive_failures = max_consecutive_failures
29
+ end
30
+
31
+ def build_worker
32
+ worker_klass.build_worker
33
+ end
34
+
35
+ def work_off
36
+ build_worker.work_off
37
+ end
38
+
39
+ def worker_klass
40
+ me = self
41
+ @worker_klass ||= Class.new do
42
+ include HermesMessengerOfTheGods::Concerns::Worker
43
+ self.endpoints = me.endpoints
44
+ self.circuit_breaker_errors = me.circuit_breaker_errors
45
+ self.max_consecutive_failures = me.max_consecutive_failures
46
+ self.deserialize_with = me
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,173 @@
1
+ require 'active_support'
2
+
3
+ require_relative './base'
4
+ require_relative '../status_server'
5
+
6
+ module HermesMessengerOfTheGods
7
+ module Concerns
8
+ module Worker
9
+ extend ActiveSupport::Concern
10
+
11
+ include HermesMessengerOfTheGods::Concerns::Base
12
+
13
+ included do
14
+ attr_writer :name
15
+ attr_accessor :consecutive_failures, :last_task_performed
16
+
17
+ def health_check_enabled?
18
+ ENV.fetch('ENABLE_HERMES_HEALTH_CHECK', 'false') != 'false'
19
+ end
20
+
21
+ def expected_work_frequency
22
+ ENV.fetch('HERMES_MINIMUM_WORK_FREQUENCY', 60).to_i
23
+ end
24
+
25
+ def healthy?
26
+ !endpoint.has_pending_work? || (
27
+ last_task_performed &&
28
+ Time.now - last_task_performed < expected_work_frequency
29
+ )
30
+ end
31
+
32
+ def work_off
33
+ WorkerStatusServer.start!(worker: self) if health_check_enabled?
34
+
35
+ instrument(:starting)
36
+ self.consecutive_failures = 0
37
+ endpoint.work_off do |job, original_message|
38
+ self.last_task_performed = Time.now
39
+
40
+ built_job = deserialize(job, original_message)
41
+ instrument(:starting_job, job: built_job)
42
+ built_job.validate! if built_job.respond_to?(:validate!)
43
+ run_job(built_job)
44
+ handle_success(built_job, job)
45
+ rescue StandardError => e
46
+ handle_failure(job, e)
47
+
48
+ max_consecutive_failures = self.class.max_consecutive_failures
49
+
50
+ if max_consecutive_failures.is_a?(Integer) && HermesMessengerOfTheGods.config.kill_on_consecutive_failures && consecutive_failures >= max_consecutive_failures
51
+ instrument(:consecutive_failure_shutdown)
52
+ return Process.kill('TERM', Process.getpgid(Process.ppid))
53
+ end
54
+
55
+ circuit_breaker_error = self.class.circuit_breaker_errors[e.class]
56
+
57
+ matching_circuit_breaker_error = self.class.circuit_breaker_errors.keys.detect { |potential_circuit_breaker| e.is_a?(potential_circuit_breaker) }
58
+
59
+ if matching_circuit_breaker_error
60
+ circuit_breaker_error = self.class.circuit_breaker_errors[matching_circuit_breaker_error]
61
+ sleep_time = circuit_breaker_error[:sleep] || 0
62
+ Kernel.sleep(sleep_time) unless sleep_time.zero?
63
+ raise e if !!circuit_breaker_error[:fatal]
64
+ end
65
+ throw(:skip_delete, true)
66
+ end
67
+ rescue StandardError => e
68
+ instrument(:fatal_error, exception: e)
69
+ raise
70
+ ensure
71
+ endpoint.teardown
72
+ end
73
+
74
+ def run_job(job)
75
+ instrument(:run_job, job: job) do
76
+ if respond_to?(:perform)
77
+ perform(job)
78
+ elsif job.respond_to?(:perform)
79
+ job.perform
80
+ else
81
+ raise 'You need to define a run_job method in the worker, or a perform method on the message'
82
+ end
83
+ end
84
+ end
85
+
86
+ def handle_success(built_job, raw_job)
87
+ instrument(:success, job: built_job)
88
+ endpoint.handle_success(raw_job)
89
+ self.consecutive_failures = 0
90
+ end
91
+
92
+ def handle_failure(job, e)
93
+ instrument(:failure, job: job, error: e)
94
+ endpoint.handle_failure(job, e)
95
+ self.consecutive_failures ||= 0
96
+ self.consecutive_failures += 1
97
+ end
98
+
99
+ def deserialize(raw_job, original_message)
100
+ instrument(:deserialization, job: raw_job) do
101
+ deserialize_method = self.class.deserialize_method || :from_message
102
+ self.class.deserialize_with.send(deserialize_method, raw_job).tap do |obj|
103
+ obj.instance_variable_set :@original_message, original_message
104
+ end
105
+ end
106
+ end
107
+
108
+ def endpoint
109
+ @endpoint ||= endpoints.values.first
110
+ end
111
+
112
+ def log_message_prefix
113
+ "Worker #{name}(pid: #{$PROCESS_ID})"
114
+ end
115
+
116
+ def name
117
+ @name ||= self.class.to_s
118
+ end
119
+ end
120
+
121
+ class_methods do
122
+ attr_reader :endpoints
123
+ attr_accessor :deserialize_with, :deserialize_method
124
+
125
+ def build_worker
126
+ new
127
+ end
128
+
129
+ def work_off
130
+ build_worker.work_off
131
+ end
132
+
133
+ def max_consecutive_failures
134
+ @max_consecutive_failures
135
+ end
136
+
137
+ def max_consecutive_failures=(val)
138
+ raise 'Expected an Integer' unless val.is_a?(Integer) || val.is_a?(NilClass)
139
+
140
+ @max_consecutive_failures = val
141
+ end
142
+
143
+ def circuit_breaker_errors
144
+ @circuit_breaker_errors || {}
145
+ end
146
+
147
+ def circuit_breaker_errors=(val)
148
+ val ||= {}
149
+ raise 'Expected a hash' unless val.is_a?(Hash)
150
+
151
+ val.each do |err, _actions|
152
+ val[err][:sleep] ||= 0
153
+ val[err][:fatal] ||= false
154
+ end
155
+
156
+ val.each do |_err, actions|
157
+ raise ':sleep must be a number' unless actions[:sleep].is_a?(Numeric)
158
+ raise ':fatal must be a boolean' unless [true, false].include?(actions[:fatal])
159
+ end
160
+
161
+ @circuit_breaker_errors = val
162
+ end
163
+
164
+ def endpoints=(val)
165
+ raise 'Expected an endpoint' unless val.is_a?(Hash)
166
+ raise 'Workers can only have one defined endpoint' unless val.length == 1
167
+
168
+ @endpoints = val
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,29 @@
1
+ require 'logger'
2
+
3
+ module HermesMessengerOfTheGods
4
+ class Configuration
5
+ class << self
6
+ def attr_accessor_with_default(meth, default)
7
+ attr_writer meth
8
+
9
+ define_method(meth) do
10
+ if instance_variable_defined?("@#{meth}")
11
+ instance_variable_get("@#{meth}")
12
+ else
13
+ instance_variable_set("@#{meth}", default)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # Log location for HMOTG info messages. Should respond like your
20
+ # typical Ruby Logger defaults to logging via STDOUT
21
+ attr_accessor_with_default :logger, Logger.new(STDOUT)
22
+
23
+ attr_accessor_with_default :quiet, false
24
+
25
+ attr_accessor_with_default :stub_dispatch, false
26
+
27
+ attr_accessor_with_default :kill_on_consecutive_failures, false
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ require 'active_support'
2
+
3
+ module HermesMessengerOfTheGods
4
+ module EndpointBuilder
5
+ def self.build(name, klass, *args)
6
+ klass.new(name, *args)
7
+ end
8
+
9
+ module Helpers
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ def endpoints
14
+ @endpoints ||= self.class.build_endpoints
15
+ end
16
+ end
17
+
18
+ class_methods do
19
+ def is_valid_endpoint_creation_method(method)
20
+ method.to_s =~ /(\w+)_endpoint\z/ &&
21
+ ("HermesMessengerOfTheGods::Endpoints::#{$1.camelize}".constantize rescue nil)
22
+ end
23
+
24
+ def method_missing(method, *args, &blk)
25
+ klass = is_valid_endpoint_creation_method(method)
26
+ if klass
27
+ {klass: klass, args: args}
28
+ else
29
+ super(method, *args, &blk)
30
+ end
31
+ end
32
+
33
+ def build_endpoints
34
+ self.endpoints.each.with_object({}) do |(k,v), hsh|
35
+ hsh[k] = EndpointBuilder.build(k, v[:klass], *v[:args])
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ require 'hermes_messenger_of_the_gods/endpoints/base'
2
+ require 'hermes_messenger_of_the_gods/endpoints/sns'
3
+ require 'hermes_messenger_of_the_gods/endpoints/sqs'
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../concerns/base'
4
+
5
+ module HermesMessengerOfTheGods
6
+ module Endpoints
7
+ class Base
8
+ extend ActiveModel::Callbacks
9
+
10
+ include HermesMessengerOfTheGods::Concerns::Base
11
+
12
+ DEFAULT_OPTIONS = { retries: 3, jitter: true, backoff: :linear, jsonify: true }.freeze
13
+ DEFAULT_RETRYS = {
14
+ linear: ->(n) { n }, # Wait 1 sec, then two secondns, then three...
15
+ exponential: ->(n) { (2**n) / (n + 1).to_f } # Wait 1, then 1.3, then 2 sec
16
+ }.freeze
17
+
18
+ attr_accessor :options, :endpoint, :errors, :result, :name
19
+
20
+ define_model_callbacks :dispatch
21
+
22
+ around_dispatch { |_, blk| instrument(:dispatch, &blk) }
23
+
24
+ def initialize(name, endpoint, options = {})
25
+ self.name = name
26
+ self.options = self.class::DEFAULT_OPTIONS.merge(options)
27
+ self.endpoint = endpoint
28
+ self.errors = []
29
+ end
30
+
31
+ def dispatch(*args)
32
+ dispatch!(*args)
33
+ rescue StandardError
34
+ false
35
+ end
36
+
37
+ def dispatch!(message, options = {})
38
+ run_callbacks :dispatch do
39
+ retry_number = 0
40
+ begin
41
+ self.result = transmit(transform_message(message), message, options)
42
+ rescue StandardError => e
43
+ errors << e
44
+ retry_number += 1
45
+ if retry_number < max_retries && retry_from(e)
46
+ instrument(:dispatch_failure, try: retry_number, exception: e)
47
+ backoff(retry_number)
48
+ retry
49
+ else
50
+ instrument(:final_failure, try: retry_number, exception: e)
51
+ raise
52
+ end
53
+ end
54
+ end
55
+
56
+ true
57
+ end
58
+
59
+ def backoff(retry_number)
60
+ return unless options[:backoff]
61
+
62
+ backoff_arg = if options[:backoff].respond_to?(:call)
63
+ options[:backoff]
64
+ else
65
+ DEFAULT_RETRYS[options[:backoff]]
66
+ end
67
+ sleep_time = backoff_arg.call(retry_number)
68
+
69
+ if sleep_time
70
+ sleep(sleep_time * (options[:jitter] ? rand(0.3..1) : 1))
71
+ end
72
+ end
73
+
74
+ def transform_message(message)
75
+ transformer_name = "to_#{self.class.to_s.demodulize.underscore}_message"
76
+
77
+ if options[:transformer]
78
+ options[:transformer].respond_to?(:call) ?
79
+ options[:transformer].call(message) :
80
+ message.send(options[:transformer])
81
+ elsif message.respond_to?(transformer_name)
82
+ message.public_send(transformer_name)
83
+ elsif message.respond_to?(:_build_for_transmission)
84
+ message._build_for_transmission
85
+ else
86
+ message
87
+ end
88
+ end
89
+
90
+ def retry_from(exception)
91
+ !exception.is_a?(HermesMessengerOfTheGods::Endpoints::FatalError)
92
+ end
93
+
94
+ def max_retries
95
+ options[:retries] || 0
96
+ end
97
+
98
+ def teardown; end
99
+
100
+ def handle_success(_job); end
101
+
102
+ def handle_failure(_job, e); end
103
+
104
+ def fetch_option(option, *args, &blk)
105
+ if options[option].respond_to?(:call)
106
+ options[option].call(*args, &blk)
107
+ else
108
+ options[option]
109
+ end || {}
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,21 @@
1
+ require 'aws-sdk-sns'
2
+ require 'json'
3
+
4
+ module HermesMessengerOfTheGods
5
+ module Endpoints
6
+ class Sns < Base
7
+ def sns_topic
8
+ @sns_topic ||= Aws::SNS::Topic.new(self.endpoint,
9
+ self.options[:client_options] || {}
10
+ )
11
+ end
12
+
13
+ def transmit(message, raw_message, dispatch_options = {})
14
+ pub_opts = fetch_option(:publish_options, raw_message) || {}
15
+
16
+ message = JSON.dump(message) if self.options[:jsonify]
17
+ sns_topic.publish(pub_opts.merge(dispatch_options, message: message))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+ require 'json'
5
+ require_relative './base'
6
+
7
+ module HermesMessengerOfTheGods
8
+ module Endpoints
9
+ class Sqs < Base
10
+ VISIBILITY_EXTEND_DURATION = 120
11
+ VISIBILITY_EXTEND_FREQUENCY = 60
12
+
13
+ def poller
14
+ @poller ||= Aws::SQS::QueuePoller.new(endpoint)
15
+ end
16
+
17
+ def poll_options
18
+ (options[:poll_options] || {}).merge(skip_delete: true)
19
+ end
20
+
21
+ def work_off(&blk)
22
+ poller.poll(poll_options) do |messages, _stats|
23
+ messages = Array.wrap(messages)
24
+
25
+ working_messages(messages) do
26
+ completed = messages.select { |msg| work_message(msg, &blk) }
27
+ poller.delete_messages(completed) unless completed.empty?
28
+ end
29
+ end
30
+ end
31
+
32
+ def work_message(message)
33
+ message_body = decode_message(message)
34
+
35
+ skip_delete = catch(:skip_delete) do
36
+ yield(message_body, message)
37
+ false
38
+ end
39
+
40
+ !skip_delete
41
+ rescue StandardError => e
42
+ instrument(:read_failure, exception: e)
43
+ false
44
+ end
45
+
46
+ def queue
47
+ @queue ||= Aws::SQS::Queue.new(endpoint, options[:client_options] || {})
48
+ end
49
+
50
+ def queue_data
51
+ queue.reload.data
52
+ end
53
+
54
+ def has_pending_work?
55
+ data = queue_data.attributes
56
+
57
+ approximate_pending_messages = data["ApproximateNumberOfMessages"].to_i -
58
+ data["ApproximateNumberOfMessagesNotVisible"].to_i -
59
+ data["ApproximateNumberOfMessagesDelayed"].to_i
60
+
61
+ # Just in case the math is off
62
+ approximate_pending_messages > 0
63
+ end
64
+
65
+ def transmit(message, raw_message, dispatch_options = {})
66
+ send_opts = fetch_option(:send_options, raw_message) || {}
67
+
68
+ message = JSON.dump(message) if options[:jsonify]
69
+ queue.send_message(send_opts.merge(dispatch_options, message_body: message))
70
+ end
71
+
72
+ private
73
+
74
+ def decode_message(message)
75
+ message_body = JSON.parse(message.body)
76
+ message_body = JSON.parse(message_body['Message']) if options[:from_sns]
77
+ message_body
78
+ end
79
+
80
+ def working_messages(messages)
81
+ thread = start_visibility_update_thread(messages)
82
+
83
+ yield
84
+ ensure
85
+ thread&.terminate
86
+ end
87
+
88
+ def start_visibility_update_thread(messages)
89
+ start_work_time = Time.now
90
+ Thread.new do
91
+ loop do
92
+ new_time = (Time.now - start_work_time) + VISIBILITY_EXTEND_DURATION
93
+ queue.change_message_visibility_batch(
94
+ entries: messages.collect do |message|
95
+ {
96
+ id: SecureRandom.uuid,
97
+ receipt_handle: message.receipt_handle,
98
+ visibility_timeout: new_time
99
+ }
100
+ end
101
+ )
102
+
103
+ sleep VISIBILITY_EXTEND_FREQUENCY
104
+ rescue StandardError => e
105
+ STDERR.puts 'Error received trying to extend visibility'
106
+ STDERR.puts e.message
107
+
108
+ raise
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end