Skip to content

Update cache workload to be Redis Cluster compatible

Sylvester Chin requested to merge sc1-crossslot-pipeline into master

What does this MR do and why?

This MR updates cache-workloads to be compatible with Redis Cluster. The changes:

  • Implements the logic to enable cache workload to run on both Redis Cluster and a standalone Redis instance. It does so via Gitlab::Redis::CrossSlot::Pipeline which wraps over a Redis::Client.
  • Updates config/redis.yml.example to include cluster details for cache (!121918 (merged))

The initial version of this MR proposes a pipelined function that 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.

This MR also updates call-sites w.r.t the list of cross-slot calls for Gitlab::Redis::Cache gitlab-com/gl-infra/scalability#2320 (closed).

Parent issue: gitlab-com/gl-infra/scalability#1992 (closed)

Background

  • we started out patching mget into pipelined gets -- !104335 (closed)
  • then tried to generalise a pipeline wrapper -- !104829 (closed)
  • there were under-coverage issues for code-path accessed only when using a Redis Cluster, hence !121918 (merged) was created and merged into this MR.

This MR is a targeted attempt to add cross-slot pipeline functionality, redis-cache migration will be done in a separate MR.

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

  1. Set up a local Redis Cluster (try https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/data/announcements/0004_redis_cluster_support.yml)
  2. Update config/redis.cache.yml
➜  gitlab git:(sc1-crossslot-pipeline) ✗ cat config/redis.yml
---

development:
  cache:
    cluster:
      - redis://gdk.test:6001
  1. Run pipeline commands in gdk rails c
Click to expand to show `CrossSlot` usage on Redis Cluster
[2] pry(main)> rc.set('a', 1)
=> "OK"
[3] pry(main)> rc.set('b', 2)
=> "OK"
[4] pry(main)> rc.set('c', 3)
=> "OK"
[5] pry(main)> rc.set('d', 4)
=> "OK"
[6] pry(main)> rc.set('e', 5)
=> "OK"
[7] pry(main)> futures = []
keys = ['a', 'b', 'c', 'd', 'e']
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
  Gitlab::Redis::Cache.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
=> ["1", "OK", "2", "OK", "3", "OK", "4", "OK", "5", "OK"]
[8] pry(main)> futures
=> [#<Gitlab::Redis::CrossSlot::Future:0x0000000136076e80 @redis_future=<Redis::Future [:get, "a"]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136076b60 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136076908 @redis_future=<Redis::Future [:get, "b"]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136076660 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136076408 @redis_future=<Redis::Future [:get, "c"]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136076138 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136075eb8 @redis_future=<Redis::Future [:get, "d"]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136075c60 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x0000000136075a08 @redis_future=<Redis::Future [:get, "e"]>>,
 #<Gitlab::Redis::CrossSlot::Future:0x00000001360757b0 @redis_future=<Redis::Future [:set, "f", "1", "EX", 500]>>]
[9] pry(main)> futures.map(&:value)
=> ["1", "OK", "2", "OK", "3", "OK", "4", "OK", "5", "OK"]
[10] pry(main)> Gitlab::Redis::Cache.with {|r| r.ttl('f')}
=> 458
[11] pry(main)> Gitlab::Redis::Cache.with {|r| r.get('f')}
=> "1"
Click to show what happens CrossSlot is not used with Redis Cluster
[5] pry(main)> Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
[5] pry(main)*   Gitlab::Redis::Cache.with do |redis|
[5] pry(main)*     redis.pipelined do |p|
[5] pry(main)*       keys.each {|key| p.get(key)}
[5] pry(main)*     end
[5] pry(main)*   end
[5] pry(main)* end
Redis::Cluster::CrossSlotPipeliningError: Cluster client couldn't send pipelining to single node. The commands include cross slot keys. ["a", "b", "c", "d", "e"]
from /Users/sylvesterchin/.asdf/installs/ruby/3.0.5/lib/ruby/gems/3.0.0/gems/redis-4.8.0/lib/redis/cluster.rb:83:in `call_pipeline'
Click to expand that `CrossSlot` works with general non-Redis Cluster connections
[6] pry(main)> Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
[6] pry(main)*   Gitlab::Redis::Sessions.with do |redis|
[6] pry(main)*     redis.pipelined do |p|
[6] pry(main)*       p.set('a', 1)
[6] pry(main)*       p.set('b', 2)
[6] pry(main)*     end
[6] pry(main)*   end
[6] pry(main)* end
=> ["OK", "OK"]

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Sylvester Chin

Merge request reports