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.
- 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
|