Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

THREESCALE-8404: Add ACL and TLS support for Redis #350

Closed
wants to merge 63 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
89fe2ef
Update gems
jlledom Oct 25, 2023
bde5ea1
Use Boolifyed API
jlledom Oct 25, 2023
0677638
Use the new pipeline API
jlledom Oct 25, 2023
08e5a3b
Use new pipeline API for async mode
jlledom Oct 25, 2023
be9c1e6
Fix tests
jlledom Oct 25, 2023
b99e307
Accept new parameters and ENV variables
jlledom Oct 25, 2023
f0418d7
Update ENV vars descriptions
jlledom Nov 20, 2023
b739462
Gemfile: require redis ~> 5.0
jlledom Nov 20, 2023
c91b00a
Create a separate context for pipelines
jlledom Nov 21, 2023
c94bd27
Thread-isolate redis client in async mode
jlledom Nov 22, 2023
8d8c4af
Implement SSL and ACL in async mode
jlledom Nov 23, 2023
d5fd900
Fix config file missing quotes
jlledom Nov 27, 2023
5e8f66c
Revert async gems upgrade
jlledom Dec 1, 2023
8c28110
Remove thread-safety for the async client
jlledom Dec 14, 2023
cdc25bb
Fix worker_async_spec test suite
jlledom Dec 14, 2023
e928e84
Fix TLS connection in async mode
jlledom Dec 20, 2023
b7ffd43
Functions: start a redis TLS server
jlledom Dec 19, 2023
a2ef569
StorageSync: Add tests for TLS connections
jlledom Dec 19, 2023
a9c4dd9
Add connection tests for StorageAsync::Client
jlledom Dec 19, 2023
d12c72d
Update redis gems
jlledom Jan 16, 2024
6065bbe
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Jan 16, 2024
cbc9fec
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Jan 17, 2024
0972555
Storage config: ignore empty ssl params
jlledom Jan 18, 2024
f53877f
Add CA_PATH env variables for SSL
jlledom Jan 18, 2024
a81a4e9
Load a default CA cert if present
jlledom Jan 18, 2024
c429e71
Redis config: remove all empty keys
jlledom Jan 22, 2024
73da74b
Monkey patch the redis gem to fix timeout bug
jlledom Jan 22, 2024
decfeb9
Rename `compact` lambda to `empty`
jlledom Jan 22, 2024
312179a
Remove Airbrake integration
jlledom Jan 23, 2024
ad3dc2a
Update Bugsnag
jlledom Jan 23, 2024
18a0840
Fix Resque-Bugsnag integration
jlledom Jan 23, 2024
fc397b7
Remove comment: The limitation doesn't exist anymore
jlledom Feb 9, 2024
1ff0b66
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Feb 19, 2024
70034ab
Fix config compact method
jlledom Feb 19, 2024
e6d1e5d
Fix typo
jlledom Feb 22, 2024
70c95db
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Mar 7, 2024
c8bdef8
Redis: Add SSL Param
jlledom Mar 8, 2024
1279b06
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Mar 8, 2024
19dda59
Fix tests after merging master
jlledom Mar 8, 2024
048904b
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom Apr 26, 2024
a126a9b
Use single redis protocol
jlledom Apr 26, 2024
4dc7cab
Remove fake certificates for tests
jlledom Apr 29, 2024
821f08c
Async: Add support for logical databases
jlledom Apr 29, 2024
0970789
Redis protocol: Don't force DB 0
jlledom May 2, 2024
2bc318c
Path `async-redis` `SentinelsClient` class
jlledom May 2, 2024
e64a151
Add new Env variables
jlledom May 2, 2024
64d0f22
Fix tests
jlledom May 2, 2024
da556bf
Generate strongest keys for tests
jlledom May 3, 2024
f9bb6fa
Integrate with the operator
jlledom May 3, 2024
40d9932
Fix typo
jlledom May 6, 2024
6c45bca
Don't crash the fetcher on timeouts
jlledom May 6, 2024
34dfc4f
Update comments
jlledom May 6, 2024
6bbf375
Fix comment
jlledom May 8, 2024
7093a8c
Add empty line at EOF
jlledom May 8, 2024
c002ab2
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom May 8, 2024
578a0e2
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom May 20, 2024
4b6e8cf
Small fixes after merging from master
jlledom May 20, 2024
d996c17
Merge branch 'master' into THREESCALE-8404-redis-acl-tls
jlledom May 22, 2024
f0d0c53
Adapt to last changes to master
jlledom May 23, 2024
1cc4c2c
Refactor Async client
jlledom May 23, 2024
470e9d5
Move macros to helper
jlledom May 23, 2024
1e1e257
Simplify helper
jlledom May 23, 2024
cb28b7e
Set some default configs
jlledom May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/3scale/backend/storage_async.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require '3scale/backend/storage_async/methods'
require '3scale/backend/storage_async/client'
require '3scale/backend/storage_async/pipeline'
require '3scale/backend/storage_async/resque_extensions'
145 changes: 9 additions & 136 deletions lib/3scale/backend/storage_async/client.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'concurrent'
require 'async/io'
require 'async/redis/client'

Expand All @@ -13,6 +14,7 @@ module StorageAsync
# the Storage instance behaves likes the redis-rb client.
class Client
include Configurable
include Methods

DEFAULT_HOST = 'localhost'.freeze
private_constant :DEFAULT_HOST
Expand Down Expand Up @@ -46,124 +48,13 @@ def initialize(opts)
port ||= DEFAULT_PORT

endpoint = Async::IO::Endpoint.tcp(host, port)
@redis_async = Async::Redis::Client.new(
@redis_async = Concurrent::ThreadLocalVar.new{ Async::Redis::Client.new(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with such an approach if using proper async is not trivial. But it appears to me that async-redis was built to handle concurrency. So mayeb gove a little more explanation why do we need a separate client per thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async-redis relies on Fibers to implement concurrency, but concurrency doesn't mean parallelism. What async-redis does, AFAIK, is to switch between fibers on IO operations, e.g. when one fiber is waiting for answer from Redis. But all Fibers run in the same Thread. In Our case, we are calling Storage.instance from different Threads, @redis_async was being shared between threads and that caused an error Fiber called across threads which blocked the test.

@eguzki Does this make sense to you?

endpoint, limit: opts[:max_connections]
)
@building_pipeline = false
)}
end

# Now we are going to define the methods to run redis commands
# following the interface of the redis-rb lib.
#
# These are the different cases:
# 1) Methods that can be called directly. For example SET:
# @redis_async.call('SET', some_key)
# 2) Methods that need to be "boolified". These are methods for which
# redis-rb returns a boolean, but redis just returns an integer.
# For example, Redis returns 0 or 1 for the EXISTS command, but
# redis-rb transforms that into a boolean.
# 3) There are a few methods that need to be treated differently and
# do not fit in any of the previous categories. For example, SSCAN
# which accepts a hash of options in redis-rb.
#
# All of this might be simplified a bit in the future using the
# "methods" in async-redis
# https://github.com/socketry/async-redis/tree/master/lib/async/redis/methods
# but there are some commands missing, so for now, that's not an option.

METHODS_TO_BE_CALLED_DIRECTLY = [
:del,
:exists,
:expire,
:expireat,
:flushdb,
:get,
:hset,
:hmget,
:incr,
:incrby,
:keys,
:llen,
:lpop,
:lpush,
:lrange,
:ltrim,
:mget,
:ping,
:rpush,
:sadd,
:scard,
:setex,
:smembers,
:srem,
:sunion,
:ttl,
:zcard,
:zrangebyscore,
:zremrangebyscore,
:zrevrange
].freeze
private_constant :METHODS_TO_BE_CALLED_DIRECTLY

METHODS_TO_BE_CALLED_DIRECTLY.each do |method|
define_method(method) do |*args|
@redis_async.call(method, *args.flatten)
end
end

METHODS_TO_BOOLIFY = [
:exists?,
:sismember,
:sadd?,
:srem?,
:zadd
].freeze
private_constant :METHODS_TO_BOOLIFY

METHODS_TO_BOOLIFY.each do |method|
command = method.to_s.delete('?')
define_method(method) do |*args|
@redis_async.call(command, *args.flatten) > 0
end
end

def blpop(*args)
call_args = ['BLPOP'] + args

# redis-rb accepts a Hash as last arg that can contain :timeout.
if call_args.last.is_a? Hash
timeout = call_args.pop[:timeout]
call_args << timeout
end

@redis_async.call(*call_args.flatten)
end

def set(key, val, opts = {})
args = ['SET', key, val]

args += ['EX', opts[:ex]] if opts[:ex]
args << 'NX' if opts[:nx]

@redis_async.call(*args)
end

def sscan(key, cursor, opts = {})
args = ['SSCAN', key, cursor]

args += ['MATCH', opts[:match]] if opts[:match]
args += ['COUNT', opts[:count]] if opts[:count]

@redis_async.call(*args)
end

def scan(cursor, opts = {})
args = ['SCAN', cursor]

args += ['MATCH', opts[:match]] if opts[:match]
args += ['COUNT', opts[:count]] if opts[:count]

@redis_async.call(*args)
def call(*args)
@redis_async.value.call(*args)
end

# This method allows us to send pipelines like this:
Expand All @@ -178,31 +69,13 @@ def pipelined(&block)
# There's an important limitation: this assumes that the fiber will
# not yield in the block.

# When running a nested pipeline, we just need to continue
# accumulating commands.
if @building_pipeline
block.call self
return
end

@building_pipeline = true

original = @redis_async
pipeline = Pipeline.new
@redis_async = pipeline

begin
block.call self
ensure
@redis_async = original
@building_pipeline = false
end

pipeline.run(original)
block.call pipeline
pipeline.run(@redis_async.value)
end

def close
@redis_async.close
@redis_async.value.close
end
end

Expand Down
123 changes: 123 additions & 0 deletions lib/3scale/backend/storage_async/methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

module ThreeScale
module Backend
module StorageAsync
module Methods
# Now we are going to define the methods to run redis commands
# following the interface of the redis-rb lib.
#
# These are the different cases:
# 1) Methods that can be called directly. For example SET:
# @redis_async.call('SET', some_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# @redis_async.call('SET', some_key)
# call('SET', some_key)

Not sure if that's correct, but assuming that there is no mention of @redis_async in this file, and the calls are "delegated" to @redis_async anyway...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I copied that comment from the old file and didn't check it.

# 2) Methods that need to be "boolified". These are methods for which
# redis-rb returns a boolean, but redis just returns an integer.
# For example, Redis returns 0 or 1 for the EXISTS command, but
# redis-rb transforms that into a boolean.
# 3) There are a few methods that need to be treated differently and
# do not fit in any of the previous categories. For example, SSCAN
# which accepts a hash of options in redis-rb.
#
# All of this might be simplified a bit in the future using the
# "methods" in async-redis
# https://github.com/socketry/async-redis/tree/master/lib/async/redis/methods
# but there are some commands missing, so for now, that's not an option.

METHODS_TO_BE_CALLED_DIRECTLY = [
:del,
:exists,
:expire,
:expireat,
:flushdb,
:get,
:hset,
:hmget,
:incr,
:incrby,
:keys,
:llen,
:lpop,
:lpush,
:lrange,
:ltrim,
:mget,
:ping,
:rpush,
:sadd,
:scard,
:setex,
:smembers,
:srem,
:sunion,
:ttl,
:zcard,
:zrangebyscore,
:zremrangebyscore,
:zrevrange
].freeze
private_constant :METHODS_TO_BE_CALLED_DIRECTLY

METHODS_TO_BE_CALLED_DIRECTLY.each do |method|
define_method(method) do |*args|
call(method, *args.flatten)
end
end

METHODS_TO_BOOLIFY = [
:exists?,
:sismember,
:sadd?,
:srem?,
:zadd
].freeze
private_constant :METHODS_TO_BOOLIFY

METHODS_TO_BOOLIFY.each do |method|
command = method.to_s.delete('?')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the story behind all the methods that got an extra ? ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, probably because of

# 2) Methods that need to be "boolified". These are methods for which
# redis-rb returns a boolean, but redis just returns an integer.
# For example, Redis returns 0 or 1 for the EXISTS command, but
# redis-rb transforms that into a boolean.
?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aisonator uses the API from the redis-rb gem, which we use in sync mode. In order to work in both sync and async modes, our custom async mode replicates the redis-rb API. The Methods class implement this.

The API changed in redis-rb 5, now they addded the methods :exists? :sadd? and :srem? https://github.com/redis/redis-rb/blob/master/CHANGELOG.md#500. We have to adapt our API to reflect that.

For the commands coming with an ending ? we need to remove it to get the actual command to send to Redis.

define_method(method) do |*args|
call(command, *args.flatten) > 0
end
end

def blpop(*args)
call_args = ['BLPOP'] + args

# redis-rb accepts a Hash as last arg that can contain :timeout.
if call_args.last.is_a? Hash
timeout = call_args.pop[:timeout]
call_args << timeout
end

call(*call_args.flatten)
end

def set(key, val, opts = {})
args = ['SET', key, val]

args += ['EX', opts[:ex]] if opts[:ex]
args << 'NX' if opts[:nx]

call(*args)
end

def sscan(key, cursor, opts = {})
args = ['SSCAN', key, cursor]

args += ['MATCH', opts[:match]] if opts[:match]
args += ['COUNT', opts[:count]] if opts[:count]

call(*args)
end

def scan(cursor, opts = {})
args = ['SCAN', cursor]

args += ['MATCH', opts[:match]] if opts[:match]
args += ['COUNT', opts[:count]] if opts[:count]

call(*args)
end
end
end
end
end
7 changes: 7 additions & 0 deletions lib/3scale/backend/storage_async/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module StorageAsync
# This class accumulates commands and sends several of them in a single
# request, instead of sending them one by one.
class Pipeline
include Methods

Error = Class.new StandardError

Expand Down Expand Up @@ -55,6 +56,12 @@ def call(*args)
1
end

def pipelined(&block)
# When running a nested pipeline, we just need to continue
# accumulating commands.
block.call self
end

# Send to redis all the accumulated commands.
# Returns an array with the result for each command in the same order
# that they added with .call().
Expand Down