apisonator 2.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +317 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.base +65 -0
  5. data/Gemfile.lock +319 -0
  6. data/Gemfile.on_prem +1 -0
  7. data/Gemfile.on_prem.lock +297 -0
  8. data/LICENSE +202 -0
  9. data/NOTICE +15 -0
  10. data/README.md +230 -0
  11. data/Rakefile +287 -0
  12. data/apisonator.gemspec +47 -0
  13. data/app/api/api.rb +13 -0
  14. data/app/api/internal/alert_limits.rb +32 -0
  15. data/app/api/internal/application_keys.rb +49 -0
  16. data/app/api/internal/application_referrer_filters.rb +43 -0
  17. data/app/api/internal/applications.rb +77 -0
  18. data/app/api/internal/errors.rb +54 -0
  19. data/app/api/internal/events.rb +42 -0
  20. data/app/api/internal/internal.rb +104 -0
  21. data/app/api/internal/metrics.rb +40 -0
  22. data/app/api/internal/service_tokens.rb +46 -0
  23. data/app/api/internal/services.rb +58 -0
  24. data/app/api/internal/stats.rb +42 -0
  25. data/app/api/internal/usagelimits.rb +62 -0
  26. data/app/api/internal/utilization.rb +23 -0
  27. data/bin/3scale_backend +223 -0
  28. data/bin/3scale_backend_worker +26 -0
  29. data/config.ru +4 -0
  30. data/config/puma.rb +192 -0
  31. data/config/schedule.rb +9 -0
  32. data/ext/mkrf_conf.rb +64 -0
  33. data/lib/3scale/backend.rb +67 -0
  34. data/lib/3scale/backend/alert_limit.rb +56 -0
  35. data/lib/3scale/backend/alerts.rb +137 -0
  36. data/lib/3scale/backend/analytics/kinesis.rb +3 -0
  37. data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
  38. data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
  39. data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
  40. data/lib/3scale/backend/analytics/redshift.rb +3 -0
  41. data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
  42. data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
  43. data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
  44. data/lib/3scale/backend/application.rb +330 -0
  45. data/lib/3scale/backend/application_events.rb +76 -0
  46. data/lib/3scale/backend/background_job.rb +65 -0
  47. data/lib/3scale/backend/configurable.rb +20 -0
  48. data/lib/3scale/backend/configuration.rb +151 -0
  49. data/lib/3scale/backend/configuration/loader.rb +42 -0
  50. data/lib/3scale/backend/constants.rb +19 -0
  51. data/lib/3scale/backend/cors.rb +84 -0
  52. data/lib/3scale/backend/distributed_lock.rb +67 -0
  53. data/lib/3scale/backend/environment.rb +21 -0
  54. data/lib/3scale/backend/error_storage.rb +52 -0
  55. data/lib/3scale/backend/errors.rb +343 -0
  56. data/lib/3scale/backend/event_storage.rb +120 -0
  57. data/lib/3scale/backend/experiment.rb +84 -0
  58. data/lib/3scale/backend/extensions.rb +5 -0
  59. data/lib/3scale/backend/extensions/array.rb +19 -0
  60. data/lib/3scale/backend/extensions/hash.rb +26 -0
  61. data/lib/3scale/backend/extensions/nil_class.rb +13 -0
  62. data/lib/3scale/backend/extensions/redis.rb +44 -0
  63. data/lib/3scale/backend/extensions/string.rb +13 -0
  64. data/lib/3scale/backend/extensions/time.rb +110 -0
  65. data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
  66. data/lib/3scale/backend/job_fetcher.rb +122 -0
  67. data/lib/3scale/backend/listener.rb +728 -0
  68. data/lib/3scale/backend/listener_metrics.rb +99 -0
  69. data/lib/3scale/backend/logging.rb +48 -0
  70. data/lib/3scale/backend/logging/external.rb +44 -0
  71. data/lib/3scale/backend/logging/external/impl.rb +93 -0
  72. data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
  73. data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
  74. data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
  75. data/lib/3scale/backend/logging/external/resque.rb +57 -0
  76. data/lib/3scale/backend/logging/logger.rb +18 -0
  77. data/lib/3scale/backend/logging/middleware.rb +62 -0
  78. data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
  79. data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
  80. data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
  81. data/lib/3scale/backend/logging/worker.rb +107 -0
  82. data/lib/3scale/backend/manifest.rb +80 -0
  83. data/lib/3scale/backend/memoizer.rb +277 -0
  84. data/lib/3scale/backend/metric.rb +275 -0
  85. data/lib/3scale/backend/metric/collection.rb +91 -0
  86. data/lib/3scale/backend/oauth.rb +4 -0
  87. data/lib/3scale/backend/oauth/token.rb +26 -0
  88. data/lib/3scale/backend/oauth/token_key.rb +30 -0
  89. data/lib/3scale/backend/oauth/token_storage.rb +313 -0
  90. data/lib/3scale/backend/oauth/token_value.rb +25 -0
  91. data/lib/3scale/backend/period.rb +3 -0
  92. data/lib/3scale/backend/period/boundary.rb +107 -0
  93. data/lib/3scale/backend/period/cache.rb +28 -0
  94. data/lib/3scale/backend/period/period.rb +402 -0
  95. data/lib/3scale/backend/queue_storage.rb +16 -0
  96. data/lib/3scale/backend/rack.rb +49 -0
  97. data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
  98. data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
  99. data/lib/3scale/backend/rack/prometheus.rb +19 -0
  100. data/lib/3scale/backend/saas.rb +6 -0
  101. data/lib/3scale/backend/saas_analytics.rb +4 -0
  102. data/lib/3scale/backend/server.rb +30 -0
  103. data/lib/3scale/backend/server/falcon.rb +52 -0
  104. data/lib/3scale/backend/server/puma.rb +71 -0
  105. data/lib/3scale/backend/service.rb +317 -0
  106. data/lib/3scale/backend/service_token.rb +97 -0
  107. data/lib/3scale/backend/stats.rb +8 -0
  108. data/lib/3scale/backend/stats/aggregator.rb +170 -0
  109. data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
  110. data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
  111. data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
  112. data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
  113. data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
  114. data/lib/3scale/backend/stats/cleaner.rb +195 -0
  115. data/lib/3scale/backend/stats/codes_commons.rb +14 -0
  116. data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
  117. data/lib/3scale/backend/stats/key_generator.rb +73 -0
  118. data/lib/3scale/backend/stats/keys.rb +104 -0
  119. data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
  120. data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
  121. data/lib/3scale/backend/stats/period_commons.rb +34 -0
  122. data/lib/3scale/backend/stats/stats_parser.rb +141 -0
  123. data/lib/3scale/backend/stats/storage.rb +113 -0
  124. data/lib/3scale/backend/statsd.rb +14 -0
  125. data/lib/3scale/backend/storable.rb +35 -0
  126. data/lib/3scale/backend/storage.rb +40 -0
  127. data/lib/3scale/backend/storage_async.rb +4 -0
  128. data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
  129. data/lib/3scale/backend/storage_async/client.rb +205 -0
  130. data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
  131. data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
  132. data/lib/3scale/backend/storage_helpers.rb +278 -0
  133. data/lib/3scale/backend/storage_key_helpers.rb +9 -0
  134. data/lib/3scale/backend/storage_sync.rb +43 -0
  135. data/lib/3scale/backend/transaction.rb +62 -0
  136. data/lib/3scale/backend/transactor.rb +177 -0
  137. data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
  138. data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
  139. data/lib/3scale/backend/transactor/notify_job.rb +47 -0
  140. data/lib/3scale/backend/transactor/process_job.rb +33 -0
  141. data/lib/3scale/backend/transactor/report_job.rb +84 -0
  142. data/lib/3scale/backend/transactor/status.rb +236 -0
  143. data/lib/3scale/backend/transactor/usage_report.rb +182 -0
  144. data/lib/3scale/backend/usage.rb +63 -0
  145. data/lib/3scale/backend/usage_limit.rb +115 -0
  146. data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
  147. data/lib/3scale/backend/util.rb +17 -0
  148. data/lib/3scale/backend/validators.rb +26 -0
  149. data/lib/3scale/backend/validators/base.rb +36 -0
  150. data/lib/3scale/backend/validators/key.rb +17 -0
  151. data/lib/3scale/backend/validators/limits.rb +57 -0
  152. data/lib/3scale/backend/validators/oauth_key.rb +15 -0
  153. data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
  154. data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
  155. data/lib/3scale/backend/validators/referrer.rb +60 -0
  156. data/lib/3scale/backend/validators/service_state.rb +15 -0
  157. data/lib/3scale/backend/validators/state.rb +15 -0
  158. data/lib/3scale/backend/version.rb +5 -0
  159. data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
  160. data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
  161. data/lib/3scale/backend/worker.rb +87 -0
  162. data/lib/3scale/backend/worker_async.rb +88 -0
  163. data/lib/3scale/backend/worker_metrics.rb +44 -0
  164. data/lib/3scale/backend/worker_sync.rb +32 -0
  165. data/lib/3scale/bundler_shim.rb +17 -0
  166. data/lib/3scale/prometheus_server.rb +10 -0
  167. data/lib/3scale/tasks/connectivity.rake +41 -0
  168. data/lib/3scale/tasks/helpers.rb +3 -0
  169. data/lib/3scale/tasks/helpers/environment.rb +23 -0
  170. data/lib/3scale/tasks/stats.rake +131 -0
  171. data/lib/3scale/tasks/swagger.rake +46 -0
  172. data/licenses.xml +1215 -0
  173. metadata +227 -0
@@ -0,0 +1,84 @@
1
+ require 'scientist'
2
+ require 'statsd'
3
+
4
+ module ThreeScale
5
+ module Backend
6
+ # This class allows us to perform experiments using the Scientist gem from
7
+ # GitHub. It is useful for comparing two different behaviors when doing a
8
+ # refactoring. The idea is that we can use the old code in the normal flow
9
+ # of the program and at the same time, we can compare its results and
10
+ # performance against new code. This class notifies the logger if the result
11
+ # of the two fragments of code does not match. Also, it sends their
12
+ # execution time to StatsD.
13
+ #
14
+ # To use this class, you need to declare the old behavior in a 'use' block,
15
+ # and the new one in a 'try' block. Also, when instantiating the class, we
16
+ # need to give the experiment a name, and define the % of times that it
17
+ # will run.
18
+ # exp = ThreeScale::Backend::Experiment.new('my-experiment', 50)
19
+ # exp.use { old_method }
20
+ # exp.try { new_method }
21
+ # exp.run
22
+ #
23
+ # 'run' returns the result of the block sent to the 'use' method. Its
24
+ # return value is the one we should use in our program. On the other hand,
25
+ # 'try' will swallow any exception raised inside the block. The operations
26
+ # inside the 'try' block should be idempotent. Avoid operations like
27
+ # writing to the DB.
28
+
29
+ class Experiment
30
+ include Scientist::Experiment
31
+ include Backend::Logging
32
+
33
+ ResultsMismatch = Class.new(StandardError)
34
+
35
+ RANDOM = Random.new
36
+ private_constant :RANDOM
37
+
38
+ def initialize(name, perc_exec)
39
+ @name = name
40
+ @perc_exec = perc_exec
41
+ @base_metric = "backend.#{environment}.experiments.#{name}"
42
+ end
43
+
44
+ def publish(result)
45
+ # This only works for the first candidate (try block)
46
+ send_to_statsd(result.control.duration, result.candidates.first.duration)
47
+ check_mismatch(result)
48
+ end
49
+
50
+ def enabled?
51
+ RANDOM.rand(100) < perc_exec
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :name, :perc_exec, :base_metric
57
+
58
+ def send_to_statsd(control_duration, candidate_duration)
59
+ statsd.timing("#{base_metric}.control", control_duration)
60
+ statsd.timing("#{base_metric}.candidate", candidate_duration)
61
+ end
62
+
63
+ def check_mismatch(result)
64
+ if result.mismatched?
65
+ msg = mismatch_msg(result.control.value, result.candidates.first.value)
66
+ logger.notify(ResultsMismatch.new(msg))
67
+ end
68
+ end
69
+
70
+ def mismatch_msg(control_value, candidate_value)
71
+ "There was a mismatch when running the experiment #{name}. "\
72
+ "control = #{control_value}, candidate = #{candidate_value}''"
73
+ end
74
+
75
+ def statsd
76
+ Statsd.instance
77
+ end
78
+
79
+ def environment
80
+ ThreeScale::Backend.environment
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ require '3scale/backend/extensions/hash'
2
+ require '3scale/backend/extensions/array'
3
+ require '3scale/backend/extensions/nil_class'
4
+ require '3scale/backend/extensions/string'
5
+ require '3scale/backend/extensions/time'
@@ -0,0 +1,19 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Extensions
4
+ module Array
5
+ def valid_encoding?
6
+ self.each do |v|
7
+ if v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
8
+ return false unless v.valid_encoding?
9
+ end
10
+ end
11
+ return true
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Array.send(:include, ThreeScale::Backend::Extensions::Array)
19
+
@@ -0,0 +1,26 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Extensions
4
+ module Hash
5
+ def symbolize_names
6
+ inject({}) do |memo, (key, value)|
7
+ memo[key.to_sym] = value
8
+ memo
9
+ end
10
+ end
11
+
12
+ def valid_encoding?
13
+ self.each do |k, v|
14
+ return false if k.is_a?(String) && !k.valid_encoding?
15
+ if v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
16
+ return false unless v.valid_encoding?
17
+ end
18
+ end
19
+ return true
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ Hash.send(:include, ThreeScale::Backend::Extensions::Hash)
@@ -0,0 +1,13 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Extensions
4
+ module NilClass
5
+ def blank?
6
+ true
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ NilClass.send(:include, ThreeScale::Backend::Extensions::NilClass)
@@ -0,0 +1,44 @@
1
+ # Monkey-patches a method in Redis::Client::Connector::Sentinel to fix a bug
2
+ # with sentinel passwords. It applies the fix in
3
+ # https://github.com/redis/redis-rb/pull/856.
4
+ #
5
+ # The fix was included in 4.1.2, but we cannot upgrade because that version
6
+ # drops support for ruby < 2.3.0 which we still need to support.
7
+ #
8
+ # This should only be temporary. It should be deleted when updating the gem.
9
+ class Redis
10
+ class Client
11
+ class Connector
12
+ class Sentinel
13
+ def sentinel_detect
14
+ @sentinels.each do |sentinel|
15
+ client = Redis::Client.new(@options.merge({:host => sentinel[:host],
16
+ :port => sentinel[:port],
17
+ password: sentinel[:password],
18
+ :reconnect_attempts => 0,
19
+ }))
20
+
21
+ begin
22
+ if result = yield(client)
23
+ # This sentinel responded. Make sure we ask it first next time.
24
+ @sentinels.delete(sentinel)
25
+ @sentinels.unshift(sentinel)
26
+
27
+ return result
28
+ end
29
+ rescue BaseConnectionError
30
+ rescue RuntimeError => exception
31
+ # Needed because when the sentinel address cannot be resolved it
32
+ # raises this instead of "BaseConnectionError"
33
+ raise unless exception.message =~ /Name or service not known/
34
+ ensure
35
+ client.disconnect
36
+ end
37
+ end
38
+
39
+ raise CannotConnectError, "No sentinels available."
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module Extensions
4
+ module String
5
+ def blank?
6
+ self !~ /\S/
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ String.send(:include, ThreeScale::Backend::Extensions::String)
@@ -0,0 +1,110 @@
1
+ module ThreeScale
2
+ module Backend
3
+ module TimeHacks
4
+ ONE_MINUTE = 60
5
+ ONE_HOUR = 60 * ONE_MINUTE
6
+ ONE_DAY = 24 * ONE_HOUR
7
+
8
+ def beginning_of_bucket(seconds_in_bucket)
9
+ if seconds_in_bucket > 30 || seconds_in_bucket < 1 || !seconds_in_bucket.is_a?(Integer)
10
+ raise Exception, "seconds_in_bucket cannot be larger than 30 seconds or smaller than 1"
11
+ end
12
+ norm_sec = (sec/seconds_in_bucket)*seconds_in_bucket
13
+ self.class.utc(year, month, day, hour, min, norm_sec)
14
+ end
15
+
16
+ # Formats the time using as little characters as possible, but still keeping
17
+ # readability.
18
+ #
19
+ # == Examples
20
+ #
21
+ # Time.utc(2010, 5, 6, 17, 24, 22).to_compact_s # "20100506172422"
22
+ # Time.utc(2010, 5, 6, 17, 24, 00).to_compact_s # "201005061724"
23
+ # Time.utc(2010, 5, 6, 17, 00, 00).to_compact_s # "2010050617"
24
+ # Time.utc(2010, 5, 6, 00, 00, 00).to_compact_s # "20100506"
25
+ #
26
+ # Careful with cases where hours, minutes or seconds have 2 digits and
27
+ # the second one is a 0. You might find them a bit counter-intuitive
28
+ # (notice the missing 0 at the end of the resulting string):
29
+ # Time.utc(2016, 1, 2, 10, 11, 10).to_compact_s # "2016010210111"
30
+ # Time.utc(2016, 1, 2, 18, 10, 0).to_compact_s # "20160102181"
31
+ # Time.utc(2016, 1, 2, 10, 0, 0).to_compact_s # "201601021"
32
+ #
33
+ # That behavior does not happen with days ending with a 0:
34
+ # Time.utc(2016, 1, 20, 0, 0, 0).to_compact_s # "20160120"
35
+
36
+ # Leap seconds would map to 60, so include it.
37
+ MODS = 61.times.map do |i|
38
+ i % 10
39
+ end.freeze
40
+ private_constant :MODS
41
+
42
+ DIVS = 61.times.map do |i|
43
+ i / 10
44
+ end.freeze
45
+ private_constant :DIVS
46
+
47
+ # This function is equivalent to:
48
+ # strftime('%Y%m%d%H%M%S').sub(/0{0,6}$/, '').
49
+ #
50
+ # When profiling, we found that this method was one of the ones which
51
+ # consumed more CPU time so we decided to optimize it.
52
+ def to_compact_s
53
+ s = year * 10000 + month * 100 + day
54
+ if sec != 0
55
+ s = s * 100000 + hour * 1000 + min * 10
56
+ MODS[sec] == 0 ? s + DIVS[sec] : s * 10 + sec
57
+ elsif min != 0
58
+ s = s * 1000 + hour * 10
59
+ MODS[min] == 0 ? s + DIVS[min] : s * 10 + min
60
+ elsif hour != 0
61
+ s = s * 10
62
+ MODS[hour] == 0 ? s + DIVS[hour] : s * 10 + hour
63
+ else
64
+ s
65
+ end.to_s
66
+ end
67
+
68
+ def to_not_compact_s
69
+ (year * 10000000000 + month * 100000000 + day * 1000000 +
70
+ hour * 10000 + min * 100 + sec).to_s
71
+ end
72
+
73
+ module ClassMethods
74
+ def parse_to_utc(input)
75
+ input = input.to_s
76
+
77
+ # Test firts for a UNIX timestamp, since it is the most useful way to specify UTC time
78
+ parse_unix_timestamp(input) || parse_non_unix_timestamp(input)
79
+ end
80
+
81
+ private
82
+
83
+ def parse_non_unix_timestamp(ts)
84
+ parts = Date._parse ts
85
+
86
+ if parts.has_key?(:year) && parts.has_key?(:mon) && parts.has_key?(:mday)
87
+ utc_time = Time.utc(parts[:year],
88
+ parts[:mon],
89
+ parts[:mday],
90
+ parts[:hour],
91
+ parts[:min],
92
+ parts[:sec],
93
+ parts[:sec_fraction])
94
+
95
+ parts[:offset] ? utc_time - parts[:offset] : utc_time
96
+ end
97
+ rescue
98
+ # if nothing can be parsed, just return nil
99
+ end
100
+
101
+ def parse_unix_timestamp(ts)
102
+ Time.at(Integer ts) rescue nil
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ Time.send(:include, ThreeScale::Backend::TimeHacks)
110
+ Time.extend(ThreeScale::Backend::TimeHacks::ClassMethods)
@@ -0,0 +1,141 @@
1
+ require '3scale/backend/logging'
2
+
3
+ module ThreeScale
4
+ module Backend
5
+ class FailedJobsScheduler
6
+ TTL_RESCHEDULE_S = 30
7
+ private_constant :TTL_RESCHEDULE_S
8
+
9
+ # We are going to reschedule a job only if the remaining time for the TTL
10
+ # is at least SAFE_MARGIN_S. This is to minimize the chance of having 2
11
+ # jobs running at the same time.
12
+ SAFE_MARGIN_S = 0.25*TTL_RESCHEDULE_S
13
+ private_constant :SAFE_MARGIN_S
14
+
15
+ # We need to limit the amount of failed jobs that we reschedule each
16
+ # time. Even a small Redis downtime can cause lots of failed jobs and we
17
+ # want to avoid spending more time than the TTL defined above. Otherwise,
18
+ # several reschedule jobs might try to run at the same time.
19
+ MAX_JOBS_TO_RESCHEDULE = 20_000
20
+ private_constant :MAX_JOBS_TO_RESCHEDULE
21
+
22
+ PATTERN_INTEGER_JOB_ERROR = /undefined method `\[\]=' for .*:Integer*/.freeze
23
+ private_constant :PATTERN_INTEGER_JOB_ERROR
24
+
25
+ NO_JOBS_IN_QUEUE_ERROR = "undefined method `[]=' for nil:NilClass".freeze
26
+ private_constant :NO_JOBS_IN_QUEUE_ERROR
27
+
28
+ class << self
29
+ include Backend::Logging
30
+
31
+ def reschedule_failed_jobs
32
+ # There might be several callers trying to requeue failed jobs at the
33
+ # same time. We need to use a lock to avoid rescheduling the same
34
+ # failed job more than once.
35
+ key = dist_lock.lock
36
+
37
+ ttl_expiration_time = Time.now + TTL_RESCHEDULE_S
38
+ rescheduled = failed_while_rescheduling = 0
39
+
40
+ if key
41
+ number_of_jobs_to_reschedule.times do
42
+ break unless time_for_another_reschedule?(ttl_expiration_time)
43
+
44
+ requeue_result = requeue_oldest_failed_job
45
+
46
+ if requeue_result[:rescheduled?]
47
+ rescheduled += 1
48
+ else
49
+ failed_while_rescheduling += 1
50
+ end
51
+
52
+ # :ok_to_remove? is false only when the requeue() call fails
53
+ # because there are no more jobs in the queue.
54
+ requeue_result[:ok_to_remove?] ? remove_oldest_failed_job : break
55
+ end
56
+
57
+ dist_lock.unlock if key == dist_lock.current_lock_key
58
+ end
59
+
60
+ { rescheduled: rescheduled,
61
+ failed_while_rescheduling: failed_while_rescheduling,
62
+ failed_current: failed_queue.count }
63
+ end
64
+
65
+ private
66
+
67
+ def dist_lock
68
+ @dist_lock ||= DistributedLock.new(
69
+ self.name, TTL_RESCHEDULE_S, Resque.redis)
70
+ end
71
+
72
+ def time_for_another_reschedule?(ttl_expiration_time)
73
+ remaining = ttl_expiration_time - Time.now
74
+ remaining >= SAFE_MARGIN_S
75
+ end
76
+
77
+ def failed_queue
78
+ @failed_jobs_queue ||= Resque::Failure
79
+ end
80
+
81
+ def number_of_jobs_to_reschedule
82
+ [failed_queue.count, MAX_JOBS_TO_RESCHEDULE].min
83
+ end
84
+
85
+ # Returns a hash with two symbol keys. ':rescheduled?' is a boolean
86
+ # that indicates whether the job has been rescheduled successfully.
87
+ # ':ok_to_remove?' is a boolean that indicates whether we should remove
88
+ # the job from the queue. That is true when the job has been
89
+ # rescheduled successfully and when we want to discard the job because
90
+ # the error it raised.
91
+ def requeue_oldest_failed_job
92
+ failed_queue.requeue(0)
93
+ { rescheduled?: true, ok_to_remove?: true }
94
+ rescue Resque::Helpers::DecodeException => e
95
+ # This means we tried to dequeue a job with invalid encoding.
96
+ # We just want to delete it from the queue.
97
+ logger.notify(e)
98
+ { rescheduled?: false, ok_to_remove?: true }
99
+ rescue Exception => e
100
+ logger.notify(e)
101
+ { rescheduled?: false, ok_to_remove?: ok_to_remove?(e.message)}
102
+ end
103
+
104
+ def ok_to_remove?(error_msg)
105
+ # The dist lock we use does not guarantee mutual exclusion in all
106
+ # cases. This can result in a 'NoMethodError' if requeue is
107
+ # called with an index that is no longer valid.
108
+ # The error msg raised in this case is the one defined in
109
+ # NO_JOBS_IN_QUEUE_ERROR. We do not want to remove the job in this
110
+ # case because the one we wanted to remove no longer exists.
111
+ #
112
+ # There are other cases that can result in a 'NoMethodError'.
113
+ # The format that Resque expects for a job is a hash with fields
114
+ # like payload, args, failed_at, timestamp, etc. However,
115
+ # we have seen Fixnums enqueued. The root cause of that is not
116
+ # clear, but it is a problem. A Fixnum does not raise a
117
+ # DecodeException, but when Resque receives that 'job', it
118
+ # raises a 'NoMethodError' because it tries to call [] (remember
119
+ # that it expects a hash) on that Fixnum.
120
+ # The error msg raised in this case matches the pattern
121
+ # PATTERN_INTEGER_JOB_ERROR.
122
+ # We need to make sure that we remove 'jobs' like this from the
123
+ # queue, otherwise, they'll be retried forever.
124
+
125
+ if PATTERN_INTEGER_JOB_ERROR =~ error_msg
126
+ true
127
+ elsif NO_JOBS_IN_QUEUE_ERROR == error_msg
128
+ false
129
+ else # Unknown error. Remove the job to avoid retrying it forever.
130
+ true
131
+ end
132
+ end
133
+
134
+ def remove_oldest_failed_job
135
+ failed_queue.remove(0)
136
+ end
137
+
138
+ end
139
+ end
140
+ end
141
+ end