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