Draft: Add cross-slot pipelined API for cluster-safe pipelining
What does this MR do and why?
This MR proposes a pipelined_xs
function which performs pipelined operations in a redis cluster context. This function is supported in redis-rb v5 via redis-cluster-client. But for now since we are blocked on upgrades by Rails 7 (#367857 (comment 1065557960))
It creates a Future
for each command, groups them into n
pipeline requests (while maintaining ordering) and sends it out to each node. Each Gitlab::Patch::RedisPipline::Future
object will map to the redis-rb's Future
object.
irb(main):004:0> c = Redis.new(url: 'redis://127.0.0.1:6379')
=> #<Redis client v5.0.5 for redis://127.0.0.1:6379/0>
irb(main):005:0> c.set('a', 12)
=> "OK"
irb(main):006:0> f = nil
=> nil
irb(main):007:1* c.pipelined do |p|
irb(main):008:1* f = p.get('a')
irb(main):009:0> end
=> ["12"]
irb(main):010:0> f
=> <Redis::Future [:get, "a"]>
irb(main):011:0> f.value <-- still supported in redis-rb v5
=> "12"
Follow up of !104335 (comment 1181212601)
Screenshots or screen recordings
Screenshots are required for UI changes, and strongly recommended for all other merge requests.
How to set up and validate locally
See similar setup in !104335 (closed)
- Run pipeline command in
gdk rails c
[6] pry(main)>
futures = []
keys = ['a', 'b', 'c', 'd', 'e']
Gitlab::Redis::CacheCluster.with do |redis|
Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |p|
keys.each do |key|
futures << p.get(key)
futures << p.set('f', 1, ex: 500)
end
end
end
=> {"127.0.0.1:7001"=>[[:get, ["a"]], [:get, ["d"]], [:get, ["e"]]],
"127.0.0.1:7101"=>[[:set, ["f", 1, {:ex=>500}]], [:get, ["b"]], [:set, ["f", 1, {:ex=>500}]], [:set, ["f", 1, {:ex=>500}]], [:set, ["f", 1, {:ex=>500}]], [:set, ["f", 1, {:ex=>500}]]],
"127.0.0.1:7201"=>[[:get, ["c"]]]}
[7] pry(main)> futures
=> [#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebba08 @redis_future=<Redis::Future [:get, "a"]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebb788 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebb648 @redis_future=<Redis::Future [:get, "b"]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebb418 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebb260 @redis_future=<Redis::Future [:get, "c"]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebafe0 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebaf40 @redis_future=<Redis::Future [:get, "d"]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebad38 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebac48 @redis_future=<Redis::Future [:get, "e"]>>,
#<Gitlab::Patch::RedisPipeline::Future:0x000000011eebaa18 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>]
[8] pry(main)> futures.map(&:value)
=> ["a", "OK", "b", "OK", "c", "OK", "d", "OK", "e", "OK"]
[9] pry(main)> Gitlab::Redis::CacheCluster.with {|r| r.ttl('f')}
=> 429
[10] pry(main)> Gitlab::Redis::CacheCluster.with {|r| r.get('f')}
=> "1"
Compared to pipelined
[13] pry(main)> Gitlab::Redis::CacheCluster.with do |redis|
[13] pry(main)* redis.pipelined do |p|
[13] pry(main)* keys.each do |key|
futures << p.get(key)
futures << p.set('f', 1, ex: 500)
end
end
[13] pry(main)* keys.each do |key|
futures << p.get(key)
futures << p.set('f', 1, ex: 500)
end
end
end
Redis::Cluster::CrossSlotPipeliningError: Cluster client couldn't send pipelining to single node. The commands include cross slot keys. ["a", "f", "b", "c", "d", "e"]
from /Users/sylvesterchin/.asdf/installs/ruby/2.7.5/lib/ruby/gems/2.7.0/gems/redis-4.8.0/lib/redis/cluster.rb:83:in `call_pipeline'
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.