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,35 @@
|
|
|
1
|
+
module ThreeScale
|
|
2
|
+
module Backend
|
|
3
|
+
# Mix this into objects that should be storable in the storage.
|
|
4
|
+
module Storable
|
|
5
|
+
include StorageKeyHelpers
|
|
6
|
+
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(attributes = {})
|
|
12
|
+
attributes.each do |key, value|
|
|
13
|
+
send("#{key}=", value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def storage
|
|
18
|
+
self.class.storage
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module ClassMethods
|
|
22
|
+
include StorageKeyHelpers
|
|
23
|
+
|
|
24
|
+
def storage
|
|
25
|
+
Storage.instance
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# returns array of attribute names
|
|
29
|
+
def attribute_names
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require '3scale/backend/storage_sync'
|
|
2
|
+
|
|
3
|
+
module ThreeScale
|
|
4
|
+
module Backend
|
|
5
|
+
class Storage
|
|
6
|
+
include Configurable
|
|
7
|
+
|
|
8
|
+
# Require async code conditionally to avoid potential side-effects
|
|
9
|
+
if configuration.redis.async
|
|
10
|
+
require '3scale/backend/storage_async'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Constant used for when batching of operations
|
|
14
|
+
# is desired/needed. Batching is performed when a lot
|
|
15
|
+
# of storage operations are need to be performed and we
|
|
16
|
+
# want to minimize database blocking of other clients
|
|
17
|
+
BATCH_SIZE = 400
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def instance(reset = false)
|
|
21
|
+
storage_client_class.instance(reset)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def new(options)
|
|
25
|
+
storage_client_class.new(options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def storage_client_class
|
|
31
|
+
if configuration.redis.async
|
|
32
|
+
Backend::StorageAsync::Client
|
|
33
|
+
else
|
|
34
|
+
Backend::StorageSync
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Monkey-patches the async-redis lib to provide a 'call_pipeline' method that
|
|
2
|
+
# sends multiple commands at once and returns an array of the responses for
|
|
3
|
+
# each of them.
|
|
4
|
+
|
|
5
|
+
module Async
|
|
6
|
+
module Redis
|
|
7
|
+
class Client
|
|
8
|
+
def call_pipeline(commands)
|
|
9
|
+
@pool.acquire do |connection|
|
|
10
|
+
commands.each do |command|
|
|
11
|
+
connection.write_request(command)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
connection.flush
|
|
15
|
+
|
|
16
|
+
commands.size.times.map { connection.read_response }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
require 'async/io'
|
|
2
|
+
require 'async/redis/client'
|
|
3
|
+
|
|
4
|
+
module ThreeScale
|
|
5
|
+
module Backend
|
|
6
|
+
module StorageAsync
|
|
7
|
+
|
|
8
|
+
# This is a wrapper for the Async-Redis client
|
|
9
|
+
# (https://github.com/socketry/async-redis).
|
|
10
|
+
# This class overrides some methods to provide the same interface that
|
|
11
|
+
# the redis-rb client provides.
|
|
12
|
+
# This is done to avoid modifying all the model classes which assume that
|
|
13
|
+
# the Storage instance behaves likes the redis-rb client.
|
|
14
|
+
class Client
|
|
15
|
+
include Configurable
|
|
16
|
+
|
|
17
|
+
DEFAULT_HOST = 'localhost'.freeze
|
|
18
|
+
private_constant :DEFAULT_HOST
|
|
19
|
+
|
|
20
|
+
DEFAULT_PORT = 22121
|
|
21
|
+
private_constant :DEFAULT_PORT
|
|
22
|
+
|
|
23
|
+
HOST_PORT_REGEX = /redis:\/\/(.*):(\d+)/
|
|
24
|
+
private_constant :HOST_PORT_REGEX
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_writer :instance
|
|
28
|
+
|
|
29
|
+
def instance(reset = false)
|
|
30
|
+
if reset || @instance.nil?
|
|
31
|
+
@instance = new(
|
|
32
|
+
Storage::Helpers.config_with(
|
|
33
|
+
configuration.redis,
|
|
34
|
+
options: { default_url: "#{DEFAULT_HOST}:#{DEFAULT_PORT}" }
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
else
|
|
38
|
+
@instance
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(opts)
|
|
44
|
+
host, port = opts[:url].match(HOST_PORT_REGEX).captures if opts[:url]
|
|
45
|
+
host ||= DEFAULT_HOST
|
|
46
|
+
port ||= DEFAULT_PORT
|
|
47
|
+
|
|
48
|
+
endpoint = Async::IO::Endpoint.tcp(host, port)
|
|
49
|
+
@redis_async = Async::Redis::Client.new(endpoint)
|
|
50
|
+
@building_pipeline = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Now we are going to define the methods to run redis commands
|
|
54
|
+
# following the interface of the redis-rb lib.
|
|
55
|
+
#
|
|
56
|
+
# These are the different cases:
|
|
57
|
+
# 1) Methods that can be called directly. For example SET:
|
|
58
|
+
# @redis_async.call('SET', some_key)
|
|
59
|
+
# 2) Methods that need to be "boolified". These are methods for which
|
|
60
|
+
# redis-rb returns a boolean, but redis just returns an integer.
|
|
61
|
+
# For example, Redis returns 0 or 1 for the EXISTS command, but
|
|
62
|
+
# redis-rb transforms that into a boolean.
|
|
63
|
+
# 3) There are a few methods that need to be treated differently and
|
|
64
|
+
# do not fit in any of the previous categories. For example, SSCAN
|
|
65
|
+
# which accepts a hash of options in redis-rb.
|
|
66
|
+
#
|
|
67
|
+
# All of this might be simplified a bit in the future using the
|
|
68
|
+
# "methods" in async-redis
|
|
69
|
+
# https://github.com/socketry/async-redis/tree/master/lib/async/redis/methods
|
|
70
|
+
# but there are some commands missing, so for now, that's not an option.
|
|
71
|
+
|
|
72
|
+
METHODS_TO_BE_CALLED_DIRECTLY = [
|
|
73
|
+
:del,
|
|
74
|
+
:expire,
|
|
75
|
+
:expireat,
|
|
76
|
+
:flushdb,
|
|
77
|
+
:get,
|
|
78
|
+
:hset,
|
|
79
|
+
:hmget,
|
|
80
|
+
:incr,
|
|
81
|
+
:incrby,
|
|
82
|
+
:keys,
|
|
83
|
+
:llen,
|
|
84
|
+
:lpop,
|
|
85
|
+
:lpush,
|
|
86
|
+
:lrange,
|
|
87
|
+
:ltrim,
|
|
88
|
+
:mget,
|
|
89
|
+
:ping,
|
|
90
|
+
:rpush,
|
|
91
|
+
:scard,
|
|
92
|
+
:setex,
|
|
93
|
+
:smembers,
|
|
94
|
+
:sunion,
|
|
95
|
+
:ttl,
|
|
96
|
+
:zcard,
|
|
97
|
+
:zrangebyscore,
|
|
98
|
+
:zremrangebyscore,
|
|
99
|
+
:zrevrange
|
|
100
|
+
].freeze
|
|
101
|
+
private_constant :METHODS_TO_BE_CALLED_DIRECTLY
|
|
102
|
+
|
|
103
|
+
METHODS_TO_BE_CALLED_DIRECTLY.each do |method|
|
|
104
|
+
define_method(method) do |*args|
|
|
105
|
+
@redis_async.call(method, *args.flatten)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
METHODS_TO_BOOLIFY = [
|
|
110
|
+
:exists,
|
|
111
|
+
:sismember,
|
|
112
|
+
:sadd,
|
|
113
|
+
:srem,
|
|
114
|
+
:zadd
|
|
115
|
+
].freeze
|
|
116
|
+
private_constant :METHODS_TO_BOOLIFY
|
|
117
|
+
|
|
118
|
+
METHODS_TO_BOOLIFY.each do |method|
|
|
119
|
+
define_method(method) do |*args|
|
|
120
|
+
@redis_async.call(method, *args.flatten) > 0
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def blpop(*args)
|
|
125
|
+
call_args = ['BLPOP'] + args
|
|
126
|
+
|
|
127
|
+
# redis-rb accepts a Hash as last arg that can contain :timeout.
|
|
128
|
+
if call_args.last.is_a? Hash
|
|
129
|
+
timeout = call_args.pop[:timeout]
|
|
130
|
+
call_args << timeout
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@redis_async.call(*call_args.flatten)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def set(key, val, opts = {})
|
|
137
|
+
args = ['SET', key, val]
|
|
138
|
+
|
|
139
|
+
args += ['EX', opts[:ex]] if opts[:ex]
|
|
140
|
+
args << 'NX' if opts[:nx]
|
|
141
|
+
|
|
142
|
+
@redis_async.call(*args)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def sscan(key, cursor, opts = {})
|
|
146
|
+
args = ['SSCAN', key, cursor]
|
|
147
|
+
|
|
148
|
+
args += ['MATCH', opts[:match]] if opts[:match]
|
|
149
|
+
args += ['COUNT', opts[:count]] if opts[:count]
|
|
150
|
+
|
|
151
|
+
@redis_async.call(*args)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def scan(cursor, opts = {})
|
|
155
|
+
args = ['SCAN', cursor]
|
|
156
|
+
|
|
157
|
+
args += ['MATCH', opts[:match]] if opts[:match]
|
|
158
|
+
args += ['COUNT', opts[:count]] if opts[:count]
|
|
159
|
+
|
|
160
|
+
@redis_async.call(*args)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# This method allows us to send pipelines like this:
|
|
164
|
+
# storage.pipelined do
|
|
165
|
+
# storage.get('a')
|
|
166
|
+
# storage.get('b')
|
|
167
|
+
# end
|
|
168
|
+
def pipelined(&block)
|
|
169
|
+
# This replaces the client with a Pipeline that accumulates the Redis
|
|
170
|
+
# commands run in a block and sends all of them in a single request.
|
|
171
|
+
#
|
|
172
|
+
# There's an important limitation: this assumes that the fiber will
|
|
173
|
+
# not yield in the block.
|
|
174
|
+
|
|
175
|
+
# When running a nested pipeline, we just need to continue
|
|
176
|
+
# accumulating commands.
|
|
177
|
+
if @building_pipeline
|
|
178
|
+
block.call
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@building_pipeline = true
|
|
183
|
+
|
|
184
|
+
original = @redis_async
|
|
185
|
+
pipeline = Pipeline.new
|
|
186
|
+
@redis_async = pipeline
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
block.call
|
|
190
|
+
ensure
|
|
191
|
+
@redis_async = original
|
|
192
|
+
@building_pipeline = false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
pipeline.run(original)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def close
|
|
199
|
+
@redis_async.close
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module ThreeScale
|
|
2
|
+
module Backend
|
|
3
|
+
module StorageAsync
|
|
4
|
+
|
|
5
|
+
# This class accumulates commands and sends several of them in a single
|
|
6
|
+
# request, instead of sending them one by one.
|
|
7
|
+
class Pipeline
|
|
8
|
+
|
|
9
|
+
Error = Class.new StandardError
|
|
10
|
+
|
|
11
|
+
class PipelineSharedBetweenFibers < Error
|
|
12
|
+
def initialize
|
|
13
|
+
super 'several fibers are modifying the same Pipeline'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# There are 2 groups of commands that need to be treated a bit
|
|
18
|
+
# differently to follow the same interface as the redis-rb lib.
|
|
19
|
+
# 1) The ones that need to return a bool when redis returns "1" or "0".
|
|
20
|
+
# 2) The ones that need to return whether the result is greater than 0.
|
|
21
|
+
|
|
22
|
+
CHECK_EQUALS_ONE = %w(EXISTS SISMEMBER).freeze
|
|
23
|
+
private_constant :CHECK_EQUALS_ONE
|
|
24
|
+
|
|
25
|
+
CHECK_GREATER_THAN_0 = %w(SADD SREM ZADD).freeze
|
|
26
|
+
private_constant :CHECK_GREATER_THAN_0
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
# Each command is an array where the first element is the name of the
|
|
30
|
+
# command ('SET', 'GET', etc.) and the rest of elements are the
|
|
31
|
+
# parameters for that command.
|
|
32
|
+
# Ex: ['SET', 'some_key', 42].
|
|
33
|
+
@commands = []
|
|
34
|
+
|
|
35
|
+
# Save the ID of the fiber that created the Pipeline so later we
|
|
36
|
+
# can check that this pipeline is not shared between fibers.
|
|
37
|
+
@fiber_id = Fiber.current.object_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# In the async-redis lib, all the commands are run with .call:
|
|
41
|
+
# client.call('GET', 'a'), client.call('SET', 'b', '1'), etc.
|
|
42
|
+
# This method just accumulates the commands and their params.
|
|
43
|
+
def call(*args)
|
|
44
|
+
if @fiber_id != Fiber.current.object_id
|
|
45
|
+
raise PipelineSharedBetweenFibers
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@commands << args
|
|
49
|
+
|
|
50
|
+
# Some Redis commands in StorageAsync compare the result with 0.
|
|
51
|
+
# For example, EXISTS. We return an integer so the comparison does
|
|
52
|
+
# not raise an error. It does not matter which int, because here we
|
|
53
|
+
# only care about adding the command to @commands.
|
|
54
|
+
|
|
55
|
+
1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Send to redis all the accumulated commands.
|
|
59
|
+
# Returns an array with the result for each command in the same order
|
|
60
|
+
# that they added with .call().
|
|
61
|
+
def run(redis_async_client)
|
|
62
|
+
responses = redis_async_client.call_pipeline(@commands)
|
|
63
|
+
|
|
64
|
+
responses.zip(@commands).map do |resp, cmd|
|
|
65
|
+
command_name = cmd.first.to_s.upcase
|
|
66
|
+
|
|
67
|
+
if CHECK_EQUALS_ONE.include?(command_name)
|
|
68
|
+
resp.to_i == 1
|
|
69
|
+
elsif CHECK_GREATER_THAN_0.include?(command_name)
|
|
70
|
+
resp.to_i > 0
|
|
71
|
+
else
|
|
72
|
+
resp
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# The async lib does not work well with the Resque gem. It crashes when running
|
|
2
|
+
# a pipeline in the enqueue method.
|
|
3
|
+
# This module mokey-patches that method.
|
|
4
|
+
|
|
5
|
+
module ThreeScale
|
|
6
|
+
module Backend
|
|
7
|
+
module StorageAsync
|
|
8
|
+
module ResqueExtensions
|
|
9
|
+
def enqueue(klass, *args)
|
|
10
|
+
queue = queue_from_class(klass)
|
|
11
|
+
|
|
12
|
+
# The redis client is hidden inside a data store that contains a
|
|
13
|
+
# namespace that contains the redis client. Both vars are called
|
|
14
|
+
# "redis".
|
|
15
|
+
async_client = Resque.redis.instance_variable_get(:@redis).instance_variable_get(:@redis)
|
|
16
|
+
|
|
17
|
+
# We need to add the "resque" namespace in the keys for all the
|
|
18
|
+
# commands.
|
|
19
|
+
async_client.pipelined do
|
|
20
|
+
async_client.sadd('resque:queues', queue.to_s)
|
|
21
|
+
async_client.rpush(
|
|
22
|
+
"resque:queue:#{queue}", Resque.encode(:class => klass.to_s, :args => args)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
module ThreeScale
|
|
2
|
+
module Backend
|
|
3
|
+
module StorageHelpers
|
|
4
|
+
private
|
|
5
|
+
def encode(stuff)
|
|
6
|
+
Yajl::Encoder.encode(stuff)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def decode(encoded_stuff)
|
|
10
|
+
stuff = Yajl::Parser.parse(encoded_stuff).symbolize_names
|
|
11
|
+
stuff[:timestamp] = Time.parse_to_utc(stuff[:timestamp]) if stuff[:timestamp]
|
|
12
|
+
stuff
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def storage
|
|
16
|
+
Storage.instance
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Storage
|
|
21
|
+
Error = Class.new StandardError
|
|
22
|
+
|
|
23
|
+
# this is a private error
|
|
24
|
+
UnspecifiedURIScheme = Class.new Error
|
|
25
|
+
private_constant :UnspecifiedURIScheme
|
|
26
|
+
|
|
27
|
+
class UnspecifiedURI < Error
|
|
28
|
+
def initialize
|
|
29
|
+
super "Redis URL not specified with url, " \
|
|
30
|
+
"proxy, server or default_url."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class InvalidURI < Error
|
|
35
|
+
def initialize(url, error)
|
|
36
|
+
super "The provided URL #{url.inspect} is not valid: #{error}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module Helpers
|
|
41
|
+
class << self
|
|
42
|
+
# CONN_OPTIONS - Redis client default connection options
|
|
43
|
+
CONN_OPTIONS = {
|
|
44
|
+
connect_timeout: 5,
|
|
45
|
+
read_timeout: 3,
|
|
46
|
+
write_timeout: 3,
|
|
47
|
+
# this is set to zero to avoid potential double transactions
|
|
48
|
+
# see https://github.com/redis/redis-rb/issues/668
|
|
49
|
+
reconnect_attempts: 0,
|
|
50
|
+
# use by default the C extension client
|
|
51
|
+
driver: :hiredis
|
|
52
|
+
}.freeze
|
|
53
|
+
private_constant :CONN_OPTIONS
|
|
54
|
+
|
|
55
|
+
# CONN_WHITELIST - Connection options that can be specified in config
|
|
56
|
+
# Note: we don't expose reconnect_attempts until the bug above is fixed
|
|
57
|
+
CONN_WHITELIST = [:connect_timeout, :read_timeout, :write_timeout].freeze
|
|
58
|
+
private_constant :CONN_WHITELIST
|
|
59
|
+
|
|
60
|
+
# Parameters regarding target server we will take from a config object
|
|
61
|
+
URL_WHITELIST = [:url, :proxy, :server, :sentinels, :role].freeze
|
|
62
|
+
private_constant :URL_WHITELIST
|
|
63
|
+
|
|
64
|
+
# these are the parameters we will take from a config object
|
|
65
|
+
CONFIG_WHITELIST = (URL_WHITELIST + CONN_WHITELIST).freeze
|
|
66
|
+
private_constant :CONFIG_WHITELIST
|
|
67
|
+
|
|
68
|
+
DEFAULT_SENTINEL_PORT = 26379
|
|
69
|
+
private_constant :DEFAULT_SENTINEL_PORT
|
|
70
|
+
|
|
71
|
+
# Generate an options hash suitable for Redis.new's constructor
|
|
72
|
+
#
|
|
73
|
+
# The options hash will overwrite any settings in the configuration,
|
|
74
|
+
# and will also accept a special "default_url" parameter to apply when
|
|
75
|
+
# no URL-related parameters are passed in (both in configuration and
|
|
76
|
+
# in options).
|
|
77
|
+
#
|
|
78
|
+
# The whitelist and defaults keyword parameters control the
|
|
79
|
+
# whitelisted configuration keys to add to the Redis parameters and
|
|
80
|
+
# the default values for unspecified keys. This way you don't need to
|
|
81
|
+
# rely on hardcoded defaults.
|
|
82
|
+
def config_with(config,
|
|
83
|
+
options: {},
|
|
84
|
+
whitelist: CONFIG_WHITELIST,
|
|
85
|
+
defaults: CONN_OPTIONS)
|
|
86
|
+
cfg_options = parse_dbcfg(config)
|
|
87
|
+
cfg = whitelist.each_with_object({}) do |k, h|
|
|
88
|
+
val = cfg_options[k]
|
|
89
|
+
h[k] = val if val
|
|
90
|
+
end.merge(options)
|
|
91
|
+
|
|
92
|
+
cfg_with_sentinels = cfg_sentinels_handler cfg
|
|
93
|
+
|
|
94
|
+
defaults.merge(ensure_url_param(cfg_with_sentinels))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Takes an object that can be converted to a Hash and returns a
|
|
100
|
+
# suitable options hash for using with our constructor.
|
|
101
|
+
#
|
|
102
|
+
# This is intended to be called on configuration objects for our
|
|
103
|
+
# database only, since it assumes that nil values need to be thrown
|
|
104
|
+
# out, but can also be used with hashes if you are ok with removing
|
|
105
|
+
# keys with nil values.
|
|
106
|
+
#
|
|
107
|
+
# Does not modify the original object.
|
|
108
|
+
def parse_dbcfg(dbcfg)
|
|
109
|
+
if !dbcfg.is_a? Hash
|
|
110
|
+
raise "can't convert #{dbcfg.inspect} to a Hash" if !dbcfg.respond_to? :to_h
|
|
111
|
+
|
|
112
|
+
dbcfg = dbcfg.to_h
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# unfortunately the current config object translates blank (not
|
|
116
|
+
# filled in) configuration options to nil, so we can't distinguish
|
|
117
|
+
# between non-specification and an actual nil value - so remove
|
|
118
|
+
# them to avoid overriding default values with nils.
|
|
119
|
+
dbcfg.reject do |_, v|
|
|
120
|
+
v.nil?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def to_redis_uri(maybe_uri)
|
|
125
|
+
raise UnspecifiedURI if maybe_uri.nil?
|
|
126
|
+
raise InvalidURI.new(maybe_uri, 'empty URL') if maybe_uri.empty?
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
validate_redis_uri maybe_uri
|
|
130
|
+
rescue URI::InvalidURIError, UnspecifiedURIScheme => e
|
|
131
|
+
begin
|
|
132
|
+
validate_redis_uri 'redis://' + maybe_uri
|
|
133
|
+
rescue
|
|
134
|
+
# tag and re-raise the original error to avoid confusion
|
|
135
|
+
raise InvalidURI.new(maybe_uri, e)
|
|
136
|
+
end
|
|
137
|
+
rescue => e
|
|
138
|
+
# tag exception
|
|
139
|
+
raise InvalidURI.new(maybe_uri, e)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Helper for the method above
|
|
144
|
+
#
|
|
145
|
+
# This raises unless maybe_uri is a valid URI.
|
|
146
|
+
def validate_redis_uri(maybe_uri)
|
|
147
|
+
# this might raise URI-specific exceptions
|
|
148
|
+
parsed_uri = URI.parse maybe_uri
|
|
149
|
+
# the parsing can succeed without scheme, so check for it and try
|
|
150
|
+
# to correct the URI.
|
|
151
|
+
raise UnspecifiedURIScheme if parsed_uri.scheme.nil?
|
|
152
|
+
# Check when host is parsed as scheme
|
|
153
|
+
raise URI::InvalidURIError if parsed_uri.host.nil? && parsed_uri.path.nil?
|
|
154
|
+
|
|
155
|
+
# return validated URI
|
|
156
|
+
maybe_uri
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# This ensures we always use the :url parameter (and removes others)
|
|
160
|
+
def ensure_url_param(options)
|
|
161
|
+
proxy = options.delete :proxy
|
|
162
|
+
server = options.delete :server
|
|
163
|
+
default_url = options.delete :default_url
|
|
164
|
+
|
|
165
|
+
# order of preference: url, proxy, server, default_url
|
|
166
|
+
options[:url] = [options[:url], proxy, server, default_url].find do |val|
|
|
167
|
+
val && !val.empty?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# not having a :url parameter at this point will throw up an
|
|
171
|
+
# exception when validating the url
|
|
172
|
+
options[:url] = to_redis_uri(options[:url])
|
|
173
|
+
|
|
174
|
+
options
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Expected sentinel input cfg format:
|
|
178
|
+
#
|
|
179
|
+
# Either a String with one or more URLs:
|
|
180
|
+
# "redis_url0,redis_url1,redis_url2,....,redis_urlN"
|
|
181
|
+
# Or an Array of Strings representing one URL each:
|
|
182
|
+
# ["redis_url0", "redis_url1", ..., "redis_urlN"]
|
|
183
|
+
# Or an Array of Hashes with ":host", ":port", and ":password" (optional) keys:
|
|
184
|
+
# [{ host: "srv0", port: 7379 }, { host: "srv1", port: 7379, password: "abc" }, ...]
|
|
185
|
+
#
|
|
186
|
+
# When using the String input, the comma "," character is the
|
|
187
|
+
# delimiter between URLs and the "\" character is the escaper that
|
|
188
|
+
# allows you to include commas "," and any other character verbatim in
|
|
189
|
+
# a URL.
|
|
190
|
+
#
|
|
191
|
+
# Parse to expected format by redis client
|
|
192
|
+
# [
|
|
193
|
+
# { host: "host0", port: "port0" },
|
|
194
|
+
# { host: "host1", port: "port1" },
|
|
195
|
+
# { host: "host2", port: "port2", password: "abc" },
|
|
196
|
+
# ...
|
|
197
|
+
# { host: "hostN", port: "portN" }
|
|
198
|
+
# ]
|
|
199
|
+
def cfg_sentinels_handler(options)
|
|
200
|
+
# get role attr and remove from options
|
|
201
|
+
# will only be validated and included when sentinels are valid
|
|
202
|
+
role = options.delete :role
|
|
203
|
+
sentinels = options.delete :sentinels
|
|
204
|
+
# The Redis client can't accept empty string or array of :sentinels
|
|
205
|
+
return options if sentinels.nil? || sentinels.empty?
|
|
206
|
+
|
|
207
|
+
sentinels = Splitter.split(sentinels) if sentinels.is_a? String
|
|
208
|
+
|
|
209
|
+
options[:sentinels] = sentinels.map do |sentinel|
|
|
210
|
+
if sentinel.is_a? Hash
|
|
211
|
+
next if sentinel.empty?
|
|
212
|
+
sentinel.fetch(:host) do
|
|
213
|
+
raise InvalidURI.new("(sentinel #{sentinel.inspect})",
|
|
214
|
+
'no host given')
|
|
215
|
+
end
|
|
216
|
+
sentinel
|
|
217
|
+
else
|
|
218
|
+
sentinel_to_hash sentinel
|
|
219
|
+
end
|
|
220
|
+
end.compact
|
|
221
|
+
|
|
222
|
+
# For the sentinels that do not have the :port key or
|
|
223
|
+
# the port key is nil we configure them with the default
|
|
224
|
+
# sentinel port
|
|
225
|
+
options[:sentinels].each do |sentinel|
|
|
226
|
+
sentinel[:port] ||= DEFAULT_SENTINEL_PORT
|
|
227
|
+
sentinel.delete(:password) if sentinel[:password].nil? || sentinel[:password].empty?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Handle role option when sentinels are validated
|
|
231
|
+
options[:role] = role if role && !role.empty?
|
|
232
|
+
options
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# helper to convert a sentinel object to a Hash
|
|
236
|
+
def sentinel_to_hash(sentinel)
|
|
237
|
+
return if sentinel.nil?
|
|
238
|
+
|
|
239
|
+
if sentinel.respond_to? :strip!
|
|
240
|
+
sentinel.strip!
|
|
241
|
+
# invalid string if it's empty after stripping
|
|
242
|
+
return if sentinel.empty?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
valid_uri_str = to_redis_uri(sentinel)
|
|
246
|
+
# it is safe to perform URI parsing now
|
|
247
|
+
uri = URI.parse valid_uri_str
|
|
248
|
+
|
|
249
|
+
{ host: uri.host, port: uri.port, password: uri.password }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# split a string by a delimiter character with escaping
|
|
253
|
+
module Splitter
|
|
254
|
+
def self.split(str, delimiter: ',', escaper: '\\')
|
|
255
|
+
escaping = false
|
|
256
|
+
|
|
257
|
+
str.each_char.inject(['']) do |ary, c|
|
|
258
|
+
if escaping
|
|
259
|
+
escaping = false
|
|
260
|
+
ary.last << c
|
|
261
|
+
elsif c == delimiter
|
|
262
|
+
ary << ''
|
|
263
|
+
elsif c == escaper
|
|
264
|
+
escaping = true
|
|
265
|
+
else
|
|
266
|
+
ary.last << c
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
ary
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
private_constant :Splitter
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|