apisonator 2.100.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +317 -0
- data/Gemfile +11 -0
- data/Gemfile.base +65 -0
- data/Gemfile.lock +319 -0
- data/Gemfile.on_prem +1 -0
- data/Gemfile.on_prem.lock +297 -0
- data/LICENSE +202 -0
- data/NOTICE +15 -0
- data/README.md +230 -0
- data/Rakefile +287 -0
- data/apisonator.gemspec +47 -0
- data/app/api/api.rb +13 -0
- data/app/api/internal/alert_limits.rb +32 -0
- data/app/api/internal/application_keys.rb +49 -0
- data/app/api/internal/application_referrer_filters.rb +43 -0
- data/app/api/internal/applications.rb +77 -0
- data/app/api/internal/errors.rb +54 -0
- data/app/api/internal/events.rb +42 -0
- data/app/api/internal/internal.rb +104 -0
- data/app/api/internal/metrics.rb +40 -0
- data/app/api/internal/service_tokens.rb +46 -0
- data/app/api/internal/services.rb +58 -0
- data/app/api/internal/stats.rb +42 -0
- data/app/api/internal/usagelimits.rb +62 -0
- data/app/api/internal/utilization.rb +23 -0
- data/bin/3scale_backend +223 -0
- data/bin/3scale_backend_worker +26 -0
- data/config.ru +4 -0
- data/config/puma.rb +192 -0
- data/config/schedule.rb +9 -0
- data/ext/mkrf_conf.rb +64 -0
- data/lib/3scale/backend.rb +67 -0
- data/lib/3scale/backend/alert_limit.rb +56 -0
- data/lib/3scale/backend/alerts.rb +137 -0
- data/lib/3scale/backend/analytics/kinesis.rb +3 -0
- data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
- data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
- data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
- data/lib/3scale/backend/analytics/redshift.rb +3 -0
- data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
- data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
- data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
- data/lib/3scale/backend/application.rb +330 -0
- data/lib/3scale/backend/application_events.rb +76 -0
- data/lib/3scale/backend/background_job.rb +65 -0
- data/lib/3scale/backend/configurable.rb +20 -0
- data/lib/3scale/backend/configuration.rb +151 -0
- data/lib/3scale/backend/configuration/loader.rb +42 -0
- data/lib/3scale/backend/constants.rb +19 -0
- data/lib/3scale/backend/cors.rb +84 -0
- data/lib/3scale/backend/distributed_lock.rb +67 -0
- data/lib/3scale/backend/environment.rb +21 -0
- data/lib/3scale/backend/error_storage.rb +52 -0
- data/lib/3scale/backend/errors.rb +343 -0
- data/lib/3scale/backend/event_storage.rb +120 -0
- data/lib/3scale/backend/experiment.rb +84 -0
- data/lib/3scale/backend/extensions.rb +5 -0
- data/lib/3scale/backend/extensions/array.rb +19 -0
- data/lib/3scale/backend/extensions/hash.rb +26 -0
- data/lib/3scale/backend/extensions/nil_class.rb +13 -0
- data/lib/3scale/backend/extensions/redis.rb +44 -0
- data/lib/3scale/backend/extensions/string.rb +13 -0
- data/lib/3scale/backend/extensions/time.rb +110 -0
- data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
- data/lib/3scale/backend/job_fetcher.rb +122 -0
- data/lib/3scale/backend/listener.rb +728 -0
- data/lib/3scale/backend/listener_metrics.rb +99 -0
- data/lib/3scale/backend/logging.rb +48 -0
- data/lib/3scale/backend/logging/external.rb +44 -0
- data/lib/3scale/backend/logging/external/impl.rb +93 -0
- data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
- data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
- data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
- data/lib/3scale/backend/logging/external/resque.rb +57 -0
- data/lib/3scale/backend/logging/logger.rb +18 -0
- data/lib/3scale/backend/logging/middleware.rb +62 -0
- data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
- data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
- data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
- data/lib/3scale/backend/logging/worker.rb +107 -0
- data/lib/3scale/backend/manifest.rb +80 -0
- data/lib/3scale/backend/memoizer.rb +277 -0
- data/lib/3scale/backend/metric.rb +275 -0
- data/lib/3scale/backend/metric/collection.rb +91 -0
- data/lib/3scale/backend/oauth.rb +4 -0
- data/lib/3scale/backend/oauth/token.rb +26 -0
- data/lib/3scale/backend/oauth/token_key.rb +30 -0
- data/lib/3scale/backend/oauth/token_storage.rb +313 -0
- data/lib/3scale/backend/oauth/token_value.rb +25 -0
- data/lib/3scale/backend/period.rb +3 -0
- data/lib/3scale/backend/period/boundary.rb +107 -0
- data/lib/3scale/backend/period/cache.rb +28 -0
- data/lib/3scale/backend/period/period.rb +402 -0
- data/lib/3scale/backend/queue_storage.rb +16 -0
- data/lib/3scale/backend/rack.rb +49 -0
- data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
- data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
- data/lib/3scale/backend/rack/prometheus.rb +19 -0
- data/lib/3scale/backend/saas.rb +6 -0
- data/lib/3scale/backend/saas_analytics.rb +4 -0
- data/lib/3scale/backend/server.rb +30 -0
- data/lib/3scale/backend/server/falcon.rb +52 -0
- data/lib/3scale/backend/server/puma.rb +71 -0
- data/lib/3scale/backend/service.rb +317 -0
- data/lib/3scale/backend/service_token.rb +97 -0
- data/lib/3scale/backend/stats.rb +8 -0
- data/lib/3scale/backend/stats/aggregator.rb +170 -0
- data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
- data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
- data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
- data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
- data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
- data/lib/3scale/backend/stats/cleaner.rb +195 -0
- data/lib/3scale/backend/stats/codes_commons.rb +14 -0
- data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
- data/lib/3scale/backend/stats/key_generator.rb +73 -0
- data/lib/3scale/backend/stats/keys.rb +104 -0
- data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
- data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
- data/lib/3scale/backend/stats/period_commons.rb +34 -0
- data/lib/3scale/backend/stats/stats_parser.rb +141 -0
- data/lib/3scale/backend/stats/storage.rb +113 -0
- data/lib/3scale/backend/statsd.rb +14 -0
- data/lib/3scale/backend/storable.rb +35 -0
- data/lib/3scale/backend/storage.rb +40 -0
- data/lib/3scale/backend/storage_async.rb +4 -0
- data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
- data/lib/3scale/backend/storage_async/client.rb +205 -0
- data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
- data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
- data/lib/3scale/backend/storage_helpers.rb +278 -0
- data/lib/3scale/backend/storage_key_helpers.rb +9 -0
- data/lib/3scale/backend/storage_sync.rb +43 -0
- data/lib/3scale/backend/transaction.rb +62 -0
- data/lib/3scale/backend/transactor.rb +177 -0
- data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
- data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
- data/lib/3scale/backend/transactor/notify_job.rb +47 -0
- data/lib/3scale/backend/transactor/process_job.rb +33 -0
- data/lib/3scale/backend/transactor/report_job.rb +84 -0
- data/lib/3scale/backend/transactor/status.rb +236 -0
- data/lib/3scale/backend/transactor/usage_report.rb +182 -0
- data/lib/3scale/backend/usage.rb +63 -0
- data/lib/3scale/backend/usage_limit.rb +115 -0
- data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
- data/lib/3scale/backend/util.rb +17 -0
- data/lib/3scale/backend/validators.rb +26 -0
- data/lib/3scale/backend/validators/base.rb +36 -0
- data/lib/3scale/backend/validators/key.rb +17 -0
- data/lib/3scale/backend/validators/limits.rb +57 -0
- data/lib/3scale/backend/validators/oauth_key.rb +15 -0
- data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
- data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
- data/lib/3scale/backend/validators/referrer.rb +60 -0
- data/lib/3scale/backend/validators/service_state.rb +15 -0
- data/lib/3scale/backend/validators/state.rb +15 -0
- data/lib/3scale/backend/version.rb +5 -0
- data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
- data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
- data/lib/3scale/backend/worker.rb +87 -0
- data/lib/3scale/backend/worker_async.rb +88 -0
- data/lib/3scale/backend/worker_metrics.rb +44 -0
- data/lib/3scale/backend/worker_sync.rb +32 -0
- data/lib/3scale/bundler_shim.rb +17 -0
- data/lib/3scale/prometheus_server.rb +10 -0
- data/lib/3scale/tasks/connectivity.rake +41 -0
- data/lib/3scale/tasks/helpers.rb +3 -0
- data/lib/3scale/tasks/helpers/environment.rb +23 -0
- data/lib/3scale/tasks/stats.rake +131 -0
- data/lib/3scale/tasks/swagger.rake +46 -0
- data/licenses.xml +1215 -0
- 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,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,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,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
|