apisonator 2.100.0

Sign up to get free protection for your applications and to get access to all the features.
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