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.
- checksums.yaml +7 -0
- data/.drone.yml +9 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +13 -0
- data/README.md +464 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/fly_hermes +57 -0
- data/hermes_messenger_of_the_gods.gemspec +38 -0
- data/lib/hermes_messenger_of_the_gods.rb +57 -0
- data/lib/hermes_messenger_of_the_gods/concerns/base.rb +42 -0
- data/lib/hermes_messenger_of_the_gods/concerns/grpc_protobuf.rb +62 -0
- data/lib/hermes_messenger_of_the_gods/concerns/message.rb +194 -0
- data/lib/hermes_messenger_of_the_gods/concerns/mono_message.rb +52 -0
- data/lib/hermes_messenger_of_the_gods/concerns/worker.rb +173 -0
- data/lib/hermes_messenger_of_the_gods/configuration.rb +29 -0
- data/lib/hermes_messenger_of_the_gods/endpoint_builder.rb +41 -0
- data/lib/hermes_messenger_of_the_gods/endpoints.rb +3 -0
- data/lib/hermes_messenger_of_the_gods/endpoints/base.rb +113 -0
- data/lib/hermes_messenger_of_the_gods/endpoints/sns.rb +21 -0
- data/lib/hermes_messenger_of_the_gods/endpoints/sqs.rb +114 -0
- data/lib/hermes_messenger_of_the_gods/exceptions.rb +27 -0
- data/lib/hermes_messenger_of_the_gods/logging_helpers.rb +30 -0
- data/lib/hermes_messenger_of_the_gods/output/basic.rb +63 -0
- data/lib/hermes_messenger_of_the_gods/status_server.rb +48 -0
- data/lib/hermes_messenger_of_the_gods/testing/array_endpoint.rb +46 -0
- data/lib/hermes_messenger_of_the_gods/testing/dispatch_matcher.rb +52 -0
- data/lib/hermes_messenger_of_the_gods/testing/rspec_helpers.rb +64 -0
- data/lib/hermes_messenger_of_the_gods/version.rb +3 -0
- data/packageGems.sh +13 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.bundlecache +0 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.circleci/config.yml +32 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.gitignore +11 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.rspec +3 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.rubocop.yml +42 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.ruby-version +1 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/.travis.yml +5 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Gemfile +6 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Gemfile.lock +75 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Makefile +1 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/README.md +78 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/Rakefile +6 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/bundle +105 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/coderay +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/console +14 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/grpc_tools_ruby_protoc +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/grpc_tools_ruby_protoc_plugin +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/htmldiff +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ldiff +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/pry +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rake +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rspec +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/rubocop +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ruby-parse +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/ruby-rewrite +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/bin/setup +8 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer.rb +123 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/encoder.rb +63 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/generation_helpers.rb +23 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/reflector.rb +66 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/lib/protobuf3_fixer/version.rb +3 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/protobuf3_fixer.gemspec +54 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/bundle +105 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/coderay +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/grpc_tools_ruby_protoc +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/grpc_tools_ruby_protoc_plugin +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/htmldiff +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ldiff +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/pry +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rake +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rspec +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/rubocop +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ruby-parse +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/rubocop/ruby-rewrite +29 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/build_from_hash_spec.rb +20 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/encoding/encoding_options_spec.rb +23 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/extra_fields/json_decode_of_superset_spec.rb +54 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/generation_helpers_spec.rb +37 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/protobuf3_fixer_spec.rb +5 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/spec_helper.rb +17 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/compiled_protobuffs/source/superset_pb.rb +44 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/compiled_protobuffs/source/timestamp_pb.rb +27 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/protobuffs/source/superset.proto +40 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/support/protobuffs/source/timestamp.proto +18 -0
- data/vendor/cache/Protobuf3Fixer-5f9f1a2d2da9/spec/well_known_type_fixes/timestamp_spec.rb +79 -0
- data/vendor/cache/activemodel-6.0.3.1.gem +0 -0
- data/vendor/cache/activesupport-6.0.3.1.gem +0 -0
- data/vendor/cache/addressable-2.6.0.gem +0 -0
- data/vendor/cache/aws-eventstream-1.1.0.gem +0 -0
- data/vendor/cache/aws-partitions-1.329.0.gem +0 -0
- data/vendor/cache/aws-sdk-core-3.99.2.gem +0 -0
- data/vendor/cache/aws-sdk-sns-1.25.1.gem +0 -0
- data/vendor/cache/aws-sdk-sqs-1.27.1.gem +0 -0
- data/vendor/cache/aws-sigv4-1.1.4.gem +0 -0
- data/vendor/cache/codeclimate-test-reporter-1.0.9.gem +0 -0
- data/vendor/cache/coderay-1.1.2.gem +0 -0
- data/vendor/cache/concurrent-ruby-1.1.6.gem +0 -0
- data/vendor/cache/crack-0.4.3.gem +0 -0
- data/vendor/cache/diff-lcs-1.3.gem +0 -0
- data/vendor/cache/docile-1.1.5.gem +0 -0
- data/vendor/cache/google-protobuf-3.12.2-universal-darwin.gem +0 -0
- data/vendor/cache/google-protobuf-3.12.2-x86_64-linux.gem +0 -0
- data/vendor/cache/google-protobuf-3.12.2.gem +0 -0
- data/vendor/cache/googleapis-common-protos-types-1.0.2.gem +0 -0
- data/vendor/cache/grpc-1.18.0-universal-darwin.gem +0 -0
- data/vendor/cache/grpc-1.18.0-x86_64-linux.gem +0 -0
- data/vendor/cache/grpc-1.18.0.gem +0 -0
- data/vendor/cache/hashdiff-0.3.8.gem +0 -0
- data/vendor/cache/i18n-1.8.3.gem +0 -0
- data/vendor/cache/jmespath-1.4.0.gem +0 -0
- data/vendor/cache/json-2.1.0.gem +0 -0
- data/vendor/cache/memory_profiler-0.9.12.gem +0 -0
- data/vendor/cache/method_source-0.9.2.gem +0 -0
- data/vendor/cache/minitest-5.14.1.gem +0 -0
- data/vendor/cache/pry-0.12.2.gem +0 -0
- data/vendor/cache/public_suffix-3.0.3.gem +0 -0
- data/vendor/cache/rake-10.5.0.gem +0 -0
- data/vendor/cache/rspec-3.8.0.gem +0 -0
- data/vendor/cache/rspec-core-3.8.0.gem +0 -0
- data/vendor/cache/rspec-expectations-3.8.2.gem +0 -0
- data/vendor/cache/rspec-mocks-3.8.0.gem +0 -0
- data/vendor/cache/rspec-support-3.8.0.gem +0 -0
- data/vendor/cache/safe_yaml-1.0.4.gem +0 -0
- data/vendor/cache/simplecov-0.13.0.gem +0 -0
- data/vendor/cache/simplecov-html-0.10.2.gem +0 -0
- data/vendor/cache/thor-1.0.1.gem +0 -0
- data/vendor/cache/thread_safe-0.3.6.gem +0 -0
- data/vendor/cache/timecop-0.9.1.gem +0 -0
- data/vendor/cache/tzinfo-1.2.7.gem +0 -0
- data/vendor/cache/webmock-3.5.1.gem +0 -0
- data/vendor/cache/zeitwerk-2.3.0.gem +0 -0
- 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,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
|