-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add Redis Cluster support #716
Conversation
CI failed at JRuby. Are these unrelated?
|
Yes, these test failures look unrelated. I currently don't have time to look at the PR so don't expect a decision any time soon |
lib/redis/cluster.rb
Outdated
end | ||
|
||
def asking | ||
try_cmd(find_node, :synchronize) { |client| client.call(%i[asking]) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long term... I would love to see redis-rb to get full support for Redis Cluster, perhaps starting from the POC I wrote here: https://github.com/antirez/redis-rb-cluster. I'm sure the same implementation, a bit polished and documented, would receive far more PRs/attention if part of Redis-rb. |
Hi, I also noticed that in redis-rb master branch there's a reference called Redis::Distributed, is that a redis cluster implementation? Anyone can explain me what is that, please? Thanks |
@samuelebistoletti I think https://redis.io/topics/partitioning#clients-supporting-consistent-hashing |
thanks. Do you think it's safe using your implementation of redis cluster in production? Or you are still testing it? |
@samuelebistoletti I think it's safe. I'm sure the same implementation as POC. But owners seem busy. |
I won't review it as I currently have neither the time or energy to review or maintain this. |
@badboy I understand. I am sorry. |
@djanowski @pietern @soveran @yaauie Excuse me. Could anyone start review when you're available? I am sorry to bother you while you are busy. Is there anything that I can do? |
If Redis-rb maintainers are not down to support cluster (understandably), what about creating a fork? I'll put 5 on it. 😸 |
Perhaps this could be shipped as a separate gem? |
I think it would be better a part of redis-rb than a separate gem for the reasons above. |
I understand that this doesn't exist as a separate gem and it is not likely to be merged in the short term. Can someone give an indication of the "best" way to use this? I am trying to connect to a cluster that I can use from with the Redis cli as: redis-cli -h $REDIS_CLUSTER -c Ideally I would like to find an equivalent of the |
@byroot Excuse me. Do you have time or plan to review this PR? |
I can manage some time for it, the problem is more than I don't have experience with Redis cluster, so I'll have to gather lots of context before I can even start to review. |
But yes, at first sight I'd like this merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor style / technical comments.
I'll do my best to do a proper review either this weekend or early next week
lib/redis/cluster.rb
Outdated
ttl -= 1 | ||
node.send(command, *args, &block) | ||
rescue TimeoutError, CannotConnectError, Errno::ECONNREFUSED, Errno::EACCES => err | ||
raise err if ttl <= 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to raise without argument here, so that the original backtrace is kept.
lib/redis/cluster.rb
Outdated
if err.message.start_with?('MOVED') | ||
redirection_node(err.message).send(command, *args, &block) | ||
elsif err.message.start_with?('ASK') | ||
raise err if ttl <= 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same raise issue here.
lib/redis/cluster.rb
Outdated
asking | ||
retry | ||
else | ||
raise err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same raise issue here.
lib/redis/cluster.rb
Outdated
# | ||
# @raise [ArgumentError] if addr is not a `String` or `Hash` | ||
def to_client_option(addr) | ||
if addr.is_a?(String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: You can use a case here:
case addr
when String
when Hash
else
end
lib/redis/cluster.rb
Outdated
response | ||
.split(/[\r\n]+/) | ||
.map { |str| str.split(':') } | ||
.map { |arr| [arr.first.to_sym, arr[1]] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the rationale for converting to a Symbol?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I thought that we are use to using Symbol as Hash key in Ruby. But I don't have rationales. Should we remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily, symbol keys are more for internal data structures.
I think it should be strings here, to be consistent with Redis#info
:
>> r.info
=> {"redis_version"=>"4.0.9", "redis_git_sha1"=>"00000000", ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And it would be a good idea to use the same parsing code:
Lines 277 to 279 in ddf058b
reply = Hash[reply.split("\r\n").map do |line| | |
line.split(":", 2) unless line =~ /^(#|$)/ | |
end.compact] |
We can probably extract it in some helper.
@supercaracal Having experimented with this further I found that we cannot use it as, for some reason, the commands for Streams (see https://redis.io/topics/streams-intro) do not work. As Streams are only appearing in the next version of Redis, which is still in beta, I do not thing this counts as a problem with this PR even though everything appears to work properly with the current master of the gem. I suspect there is some magic that automatically generates methods for Redis commands but this is not getting picked up properly by We are going to use Sentinel for the time being but I would be interested in moving over to using this in the future. |
@jrmhaig can you be more specific? What's the version of your redis server & the actual code your are using? |
I am using the current release candidate of Redis 5.0 from https://redis.io/download (version 4.9.101). I can do: irb(main):001:0> require 'redis'
=> true
irb(main):002:0> r = Redis.new host: 'localhost'
=> #<Redis client v4.0.1 for redis://localhost:6379/0>
irb(main):003:0> r.xadd 'test', '*', 'key', 'value'
=> "1529082632840-0" but if I try this with |
@jrmhaig I try to learn Redis Streams API on the weekend. Thank you for the information. |
@byroot Thank you for your reviewing. I fixed but CI was failure. It seems not related issues. Could you retry to build? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spotted other issues, however I'll stop here because at this point I believe this PR is attacking the problem from the wrong side.
Right now this PR adds a Redis::Cluster
class which holds a list of Redis
instances, and delegate commands to them.
Several of the issues I reported comes from this, and from reading the code it seems to be that the proper architecture should be for Redis::Cluster
to be a replacement for Redis::Client
and not Redis
itself.
So that in the end you will have just a few methods like call
, call_loop
etc, and just have to delegate to the proper Redis::Client
instance.
To paraphrase, I think the call order should be Redis -> Redis::Cluster -> Redis::Client
whereas this PR currently implement Redis::Cluster -> Redis -> Redis::Client
.
lib/redis/helpers/reply_helper.rb
Outdated
class Redis | ||
module Helpers | ||
# Helper methods for common processing of the reply data. | ||
class ReplyHelper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 issues here:
ReplyHelper
is never instantiated, so it shouldn't be class. When you need a namespace for some static method, just use a module.- I think than instead of introducing a new namespace etc, we should follow the existing pattern, so something like
HashifyInfo = lambda { ...
lib/redis/cluster.rb
Outdated
def try_cmd(node, command, *args, ttl: RETRY_COUNT, &block) | ||
ttl -= 1 | ||
node.send(command, *args, &block) | ||
rescue TimeoutError, CannotConnectError, Errno::ECONNREFUSED, Errno::EACCES => err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you tell me the reasoning behind this list of exceptions?
Why CannotConnectError
and not the more generic BaseConnectionError
?
Why Errno::ECONNREFUSED, Errno::EACCES
? Aren't they already rescued and re-raised as more specific errors by Redis::Client
?
Is it really sensible to retry a TimeoutError
? Do we have the guarantee that the command was not processed on the server side ? Otherwise we'd risk executing a non idempotent commend twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ported from POC . But I couldn't realize those problems. I would like to fix it.
lib/redis/cluster.rb
Outdated
# @return [Object] depends on the command | ||
def try_cmd(node, command, *args, ttl: RETRY_COUNT, &block) | ||
ttl -= 1 | ||
node.send(command, *args, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see 2 problems here:
- First, commands are public methods, so we should use
public_send
here, so that we don't allow calling private methods. - We have no way to distinguish between commands and utility methods on
Redis
, so if I'm understanding that code correctly, it will allow non-sensical things like this:
>> cluster._client
=> #<Redis::Client:0x007f92609182b0 @options={:host=>"127.0.0.1", :port=>7000,
@byroot Thank you for reviewing. I fixed the call order and others. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should cover more with the test, we should probably do like ditributed_test.rb
& test a wide array of commands using the Cluster client.
lib/redis/cluster.rb
Outdated
find_node.respond_to?(method_name, include_private) | ||
end | ||
|
||
def method_missing(method_name, *args, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we do without method_missing
? It seems to me that there is relatively few methods that would need to be implemented. I can see call
, call_loop
, call_pipeline
, maybe a couple others.
Even if it's using a macro to do some standard delegation, I'd feel much more confortable with a whitelist of delegated methods rather than method_missing
.
In case of Node:
|
In case of PHP (extension): https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#transactions
|
The transaction which executes on multiple nodes is not reliable. I think cluster client should raise |
Of course. Don't feel like you should not change things because I approved. |
afd521c
to
9690c1e
Compare
I fixed as below.
|
@byroot Are there any other concerns? |
3b0c85c
to
b0e035b
Compare
I fixed as below.
|
b0e035b
to
f48af99
Compare
I think our Redis Cluster client is ready for shipping. |
Really looks good. I'd have another nitpick ;)
After that I'd like to merge and if it requires further improvements it might as well happen in followup PRs. I'll also see if I can get publish access so that we can release a RC to get some feedback from users. |
|
f48af99
to
7f48c0b
Compare
@byroot I have fixed it. I am grateful for your support. Thank you so much. |
This was released as |
Hey @byroot amd @supercaracal, I'm trying to wrap around my head about using this in production on AWS. When we setup a new High Avaliable Redis Cluster in AWS we get a single endpoint, which AWS calls I can connect to this endpoint using That said, by reading this PR code, I'm supposed to pass an array of Redis hosts instead of a single one when establishing a connection. Are AWS and redis-rb approachs different, incompatible or I'm reading something wrong? |
We're able to specify single node to this gem.
It seems the
|
That's amazing @supercaracal. I just tried pointing the new gem to an AWS Elasticache configuration_endpoint_address and looks like the redirect is handled automatically. Thanks. |
It's already been cut over a month ago: https://rubygems.org/gems/redis/versions/4.1.0.beta1 |
We'd like to use Redis Cluster in session store such as.
There are some gems such as:
https://github.com/redis-store/redis-rails
https://github.com/redis-store/redis-store
These gems depend to redis-rb. But redis-rb doesn't support Redis Cluster.
https://github.com/redis-store/redis-rails/issues/72
redis-store/redis-activesupport#89
So, we add a client to redis-rb for Redis Cluster. We can check it at this sample.
https://github.com/supercaracal/redis-cluster-playground
ref: #546
ref: antirez/redis-rb-cluster#6
ref: antirez/redis-rb-cluster#8
ref: redis/node-redis#574
ref: redis/redis-py#931
ref: redis/redis-py#604
ref: https://github.com/go-redis/redis
ref: https://github.com/luin/ioredis
ref: https://github.com/xetorthio/jedis
ref: https://github.com/redisson/redisson
ref: https://github.com/phpredis/phpredis
ref: https://github.com/nrk/predis