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