apisonator 2.100.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +317 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.base +65 -0
  5. data/Gemfile.lock +319 -0
  6. data/Gemfile.on_prem +1 -0
  7. data/Gemfile.on_prem.lock +297 -0
  8. data/LICENSE +202 -0
  9. data/NOTICE +15 -0
  10. data/README.md +230 -0
  11. data/Rakefile +287 -0
  12. data/apisonator.gemspec +47 -0
  13. data/app/api/api.rb +13 -0
  14. data/app/api/internal/alert_limits.rb +32 -0
  15. data/app/api/internal/application_keys.rb +49 -0
  16. data/app/api/internal/application_referrer_filters.rb +43 -0
  17. data/app/api/internal/applications.rb +77 -0
  18. data/app/api/internal/errors.rb +54 -0
  19. data/app/api/internal/events.rb +42 -0
  20. data/app/api/internal/internal.rb +104 -0
  21. data/app/api/internal/metrics.rb +40 -0
  22. data/app/api/internal/service_tokens.rb +46 -0
  23. data/app/api/internal/services.rb +58 -0
  24. data/app/api/internal/stats.rb +42 -0
  25. data/app/api/internal/usagelimits.rb +62 -0
  26. data/app/api/internal/utilization.rb +23 -0
  27. data/bin/3scale_backend +223 -0
  28. data/bin/3scale_backend_worker +26 -0
  29. data/config.ru +4 -0
  30. data/config/puma.rb +192 -0
  31. data/config/schedule.rb +9 -0
  32. data/ext/mkrf_conf.rb +64 -0
  33. data/lib/3scale/backend.rb +67 -0
  34. data/lib/3scale/backend/alert_limit.rb +56 -0
  35. data/lib/3scale/backend/alerts.rb +137 -0
  36. data/lib/3scale/backend/analytics/kinesis.rb +3 -0
  37. data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
  38. data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
  39. data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
  40. data/lib/3scale/backend/analytics/redshift.rb +3 -0
  41. data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
  42. data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
  43. data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
  44. data/lib/3scale/backend/application.rb +330 -0
  45. data/lib/3scale/backend/application_events.rb +76 -0
  46. data/lib/3scale/backend/background_job.rb +65 -0
  47. data/lib/3scale/backend/configurable.rb +20 -0
  48. data/lib/3scale/backend/configuration.rb +151 -0
  49. data/lib/3scale/backend/configuration/loader.rb +42 -0
  50. data/lib/3scale/backend/constants.rb +19 -0
  51. data/lib/3scale/backend/cors.rb +84 -0
  52. data/lib/3scale/backend/distributed_lock.rb +67 -0
  53. data/lib/3scale/backend/environment.rb +21 -0
  54. data/lib/3scale/backend/error_storage.rb +52 -0
  55. data/lib/3scale/backend/errors.rb +343 -0
  56. data/lib/3scale/backend/event_storage.rb +120 -0
  57. data/lib/3scale/backend/experiment.rb +84 -0
  58. data/lib/3scale/backend/extensions.rb +5 -0
  59. data/lib/3scale/backend/extensions/array.rb +19 -0
  60. data/lib/3scale/backend/extensions/hash.rb +26 -0
  61. data/lib/3scale/backend/extensions/nil_class.rb +13 -0
  62. data/lib/3scale/backend/extensions/redis.rb +44 -0
  63. data/lib/3scale/backend/extensions/string.rb +13 -0
  64. data/lib/3scale/backend/extensions/time.rb +110 -0
  65. data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
  66. data/lib/3scale/backend/job_fetcher.rb +122 -0
  67. data/lib/3scale/backend/listener.rb +728 -0
  68. data/lib/3scale/backend/listener_metrics.rb +99 -0
  69. data/lib/3scale/backend/logging.rb +48 -0
  70. data/lib/3scale/backend/logging/external.rb +44 -0
  71. data/lib/3scale/backend/logging/external/impl.rb +93 -0
  72. data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
  73. data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
  74. data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
  75. data/lib/3scale/backend/logging/external/resque.rb +57 -0
  76. data/lib/3scale/backend/logging/logger.rb +18 -0
  77. data/lib/3scale/backend/logging/middleware.rb +62 -0
  78. data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
  79. data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
  80. data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
  81. data/lib/3scale/backend/logging/worker.rb +107 -0
  82. data/lib/3scale/backend/manifest.rb +80 -0
  83. data/lib/3scale/backend/memoizer.rb +277 -0
  84. data/lib/3scale/backend/metric.rb +275 -0
  85. data/lib/3scale/backend/metric/collection.rb +91 -0
  86. data/lib/3scale/backend/oauth.rb +4 -0
  87. data/lib/3scale/backend/oauth/token.rb +26 -0
  88. data/lib/3scale/backend/oauth/token_key.rb +30 -0
  89. data/lib/3scale/backend/oauth/token_storage.rb +313 -0
  90. data/lib/3scale/backend/oauth/token_value.rb +25 -0
  91. data/lib/3scale/backend/period.rb +3 -0
  92. data/lib/3scale/backend/period/boundary.rb +107 -0
  93. data/lib/3scale/backend/period/cache.rb +28 -0
  94. data/lib/3scale/backend/period/period.rb +402 -0
  95. data/lib/3scale/backend/queue_storage.rb +16 -0
  96. data/lib/3scale/backend/rack.rb +49 -0
  97. data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
  98. data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
  99. data/lib/3scale/backend/rack/prometheus.rb +19 -0
  100. data/lib/3scale/backend/saas.rb +6 -0
  101. data/lib/3scale/backend/saas_analytics.rb +4 -0
  102. data/lib/3scale/backend/server.rb +30 -0
  103. data/lib/3scale/backend/server/falcon.rb +52 -0
  104. data/lib/3scale/backend/server/puma.rb +71 -0
  105. data/lib/3scale/backend/service.rb +317 -0
  106. data/lib/3scale/backend/service_token.rb +97 -0
  107. data/lib/3scale/backend/stats.rb +8 -0
  108. data/lib/3scale/backend/stats/aggregator.rb +170 -0
  109. data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
  110. data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
  111. data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
  112. data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
  113. data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
  114. data/lib/3scale/backend/stats/cleaner.rb +195 -0
  115. data/lib/3scale/backend/stats/codes_commons.rb +14 -0
  116. data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
  117. data/lib/3scale/backend/stats/key_generator.rb +73 -0
  118. data/lib/3scale/backend/stats/keys.rb +104 -0
  119. data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
  120. data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
  121. data/lib/3scale/backend/stats/period_commons.rb +34 -0
  122. data/lib/3scale/backend/stats/stats_parser.rb +141 -0
  123. data/lib/3scale/backend/stats/storage.rb +113 -0
  124. data/lib/3scale/backend/statsd.rb +14 -0
  125. data/lib/3scale/backend/storable.rb +35 -0
  126. data/lib/3scale/backend/storage.rb +40 -0
  127. data/lib/3scale/backend/storage_async.rb +4 -0
  128. data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
  129. data/lib/3scale/backend/storage_async/client.rb +205 -0
  130. data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
  131. data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
  132. data/lib/3scale/backend/storage_helpers.rb +278 -0
  133. data/lib/3scale/backend/storage_key_helpers.rb +9 -0
  134. data/lib/3scale/backend/storage_sync.rb +43 -0
  135. data/lib/3scale/backend/transaction.rb +62 -0
  136. data/lib/3scale/backend/transactor.rb +177 -0
  137. data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
  138. data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
  139. data/lib/3scale/backend/transactor/notify_job.rb +47 -0
  140. data/lib/3scale/backend/transactor/process_job.rb +33 -0
  141. data/lib/3scale/backend/transactor/report_job.rb +84 -0
  142. data/lib/3scale/backend/transactor/status.rb +236 -0
  143. data/lib/3scale/backend/transactor/usage_report.rb +182 -0
  144. data/lib/3scale/backend/usage.rb +63 -0
  145. data/lib/3scale/backend/usage_limit.rb +115 -0
  146. data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
  147. data/lib/3scale/backend/util.rb +17 -0
  148. data/lib/3scale/backend/validators.rb +26 -0
  149. data/lib/3scale/backend/validators/base.rb +36 -0
  150. data/lib/3scale/backend/validators/key.rb +17 -0
  151. data/lib/3scale/backend/validators/limits.rb +57 -0
  152. data/lib/3scale/backend/validators/oauth_key.rb +15 -0
  153. data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
  154. data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
  155. data/lib/3scale/backend/validators/referrer.rb +60 -0
  156. data/lib/3scale/backend/validators/service_state.rb +15 -0
  157. data/lib/3scale/backend/validators/state.rb +15 -0
  158. data/lib/3scale/backend/version.rb +5 -0
  159. data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
  160. data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
  161. data/lib/3scale/backend/worker.rb +87 -0
  162. data/lib/3scale/backend/worker_async.rb +88 -0
  163. data/lib/3scale/backend/worker_metrics.rb +44 -0
  164. data/lib/3scale/backend/worker_sync.rb +32 -0
  165. data/lib/3scale/bundler_shim.rb +17 -0
  166. data/lib/3scale/prometheus_server.rb +10 -0
  167. data/lib/3scale/tasks/connectivity.rake +41 -0
  168. data/lib/3scale/tasks/helpers.rb +3 -0
  169. data/lib/3scale/tasks/helpers/environment.rb +23 -0
  170. data/lib/3scale/tasks/stats.rake +131 -0
  171. data/lib/3scale/tasks/swagger.rake +46 -0
  172. data/licenses.xml +1215 -0
  173. metadata +227 -0
@@ -0,0 +1,14 @@
1
+ module ThreeScale
2
+ module Backend
3
+ class Statsd
4
+ include Configurable
5
+
6
+ class << self
7
+ def instance
8
+ @instance ||= ::Statsd.new(configuration.statsd.host,
9
+ configuration.statsd.port)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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,4 @@
1
+ require '3scale/backend/storage_async/client'
2
+ require '3scale/backend/storage_async/pipeline'
3
+ require '3scale/backend/storage_async/async_redis'
4
+ require '3scale/backend/storage_async/resque_extensions'
@@ -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