Skip to content

feat: support redis v4#6

Merged
tonytonyjan merged 1 commit into
masterfrom
feat/CU-86ex6zrq0_support-redis-v4
May 4, 2026
Merged

feat: support redis v4#6
tonytonyjan merged 1 commit into
masterfrom
feat/CU-86ex6zrq0_support-redis-v4

Conversation

@tonytonyjan
Copy link
Copy Markdown
Collaborator

In redis v4, the client inside a Redis instance is stateful and it will replace the client inside the Redis#multi block, which is different from v5.

It causes the issue that the Redis::Future instance is returned instead of the actual value when calling any command inside the Redis#multi block, which is not expected in our case.

redis.multi do
  redis.get('foo').class
  # => Redis::Future in v4
  # => NilClass in v5
end

@tonytonyjan tonytonyjan self-assigned this Apr 9, 2026
@tonytonyjan tonytonyjan review requested due to automatic review settings April 9, 2026 13:24
@tonytonyjan tonytonyjan force-pushed the feat/CU-86ex6zrq0_support-redis-v4 branch from af6de49 to 6380b87 Compare April 10, 2026 04:11
Copy link
Copy Markdown
Contributor

@choznerol choznerol left a comment

Choose a reason for hiding this comment

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

LGTM

Comment thread lib/cover_rage/stores/redis.rb Outdated
module Stores
class Redis
KEY = 'cover_rage_records'
IS_REDIS_LESS_THAN_5 = Gem::Version.new(::Redis::VERSION) < Gem::Version.new('5')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick(non-blocking):

"Is Redis less than 5" feels like "Is Redis instance count less than 5" to me at first glance.

Wdyt about "Is Reids below V5"?

Suggested change
IS_REDIS_LESS_THAN_5 = Gem::Version.new(::Redis::VERSION) < Gem::Version.new('5')
IS_REDIS_BELOW_V5 = Gem::Version.new(::Redis::VERSION) < Gem::Version.new('5')

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, I've updated the PR 🙏

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It seems that the IS_REDIS_BELOW_V5 change has not yet been applied to this branch. I will approve it after the update 🙇

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I thought I force pushed 😓

I've made the change, please review again, thank you 🙏

Comment thread lib/cover_rage/stores/redis.rb Outdated

def list
result = @redis.hgetall(KEY)
client = Thread.current[:redis_multi] && IS_REDIS_LESS_THAN_5 ? @redis_for_less_than_v5 : @redis
Copy link
Copy Markdown
Contributor

@choznerol choznerol Apr 10, 2026

Choose a reason for hiding this comment

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

question:

Both the before and after versions use @client for v5 transactions.

If I understand this v5.0.0 changelog entry correctly:

Removed the deprecated pipelined and multi signature. Commands now MUST be called on the block argument, not the original redis instance.

Shouldn't we always use Thread.current[:redis_multi] ("the block argument") instead of @redis ("the original redis instance") within a transaction, whether v4 or v5?

v4 v5
inside transaction Thread.current[:redis_multi] Thread.current[:redis_multi] (and @client will be nil)
outside transaction @redis @redis
Suggested change
client = Thread.current[:redis_multi] && IS_REDIS_LESS_THAN_5 ? @redis_for_less_than_v5 : @redis
client = Thread.current[:redis_multi] || @redis

Copy link
Copy Markdown
Collaborator Author

@tonytonyjan tonytonyjan Apr 13, 2026

Choose a reason for hiding this comment

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

A classical read & write transaction in Redis is:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

With redis gem, it becomes:

redis.watch 'mykey' do
  val = redis.get('mykey') # read operation is outside of the multi block
  redis.multi do |multi|
    multi.write(val + 1) # write operation is insdie of the multi block
  end
end

The issue of lib/cover_rage/recorder.rb is that the read and write both happen] in #transaction block:

@store.transaction do
records_to_save = Record.merge(@store.list, records)
@store.update(records_to_save)
end

Shouldn't we always use Thread.current[:redis_multi] ("the block argument") instead of @redis ("the original redis instance") within a transaction, whether v4 or v5?

If we always use Thread.current[:redis_multi], the #list will not return the exact value, and return an instance of Redis::Future instead.

I hope it answers your question. Any suggestions for improvement are appreciated. 🙏

Copy link
Copy Markdown
Contributor

@choznerol choznerol Apr 13, 2026

Choose a reason for hiding this comment

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

Ah I understand now. It's the Redis::Future that can't be read immediately. Thanks for the explaination 🙏 🙏

Comment thread lib/cover_rage/stores/redis.rb Outdated

def list
result = @redis.hgetall(KEY)
client = Thread.current[:redis_multi] && IS_REDIS_LESS_THAN_5 ? @redis_for_less_than_v5 : @redis

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh wait, you already use WATCH...MULTI...EXEC to detect race and rety. That's probably safe 🙏.

@requiemformemories
Copy link
Copy Markdown

suggestion(non-blocking):

When I first read this code, it was a bit hard to follow which client is used in different actions and why. I am thinking maybe we can extract read_client and write_client as private methods, making the intent clearer.

like this:

 def transaction(&)                                                                                                                                 
    loop do               
      break if @redis.watch(KEY) do
        @redis.multi do |multi|                                                                                                                      
          Thread.current[:redis_multi] = multi
          yield                                                                                                                                      
        ensure            
          Thread.current[:redis_multi] = nil
        end
      end
    end
  end

  def update(records)                                                                                                                                
    arguments = records.flat_map { |r| [r.path, JSON.dump(r.to_h)] }
    write_client.hset(KEY, *arguments)                                                                                                               
  end                     
                                                                                                                                                     
  def list                
    result = read_client.hgetall(KEY)
    return [] if result.empty?                                                                                                                       
    result.map { |_, value| Record.new(**JSON.parse(value)) }
  end                                                                                                                                                
                          
  private                                                                                                                                            
                          
  def read_client
    # In Redis v4 and below, @redis becomes stateful inside a multi block and returns Redis::Future instead of real 
    # values. Use a separate connection to perform the read outside the transaction.
    return @redis_for_less_than_v5 if Thread.current[:redis_multi] && IS_REDIS_LESS_THAN_5
    @redis                                                                                                                                           
  end
                                                                                                                                                     
  def write_client        
    Thread.current[:redis_multi] || @redis
  end                                                                                                                                                

Or I suggest to at least simply adding a comment above return @redis_for_less_than_v5 if Thread.current[:redis_multi] && IS_REDIS_LESS_THAN_5.

@tonytonyjan tonytonyjan force-pushed the feat/CU-86ex6zrq0_support-redis-v4 branch from 6380b87 to 4c48807 Compare April 13, 2026 07:39
Copy link
Copy Markdown

@requiemformemories requiemformemories left a comment

Choose a reason for hiding this comment

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

LGTM!

In redis v4, the client inside a `Redis` instance is stateful and it will [replace the client](https://github.com/redis/redis-rb/blob/v4.8.1/lib/redis.rb#L225) inside the `Redis#multi` block, which is different from v5.

It causes the issue that the `Redis::Future` instance is returned instead of the actual value when calling any command inside the `Redis#multi` block, which is not expected in our case.

```ruby
redis.multi do
  redis.get('foo').class
  # => Redis::Future in v4
  # => NilClass in v5
end
```
@tonytonyjan tonytonyjan force-pushed the feat/CU-86ex6zrq0_support-redis-v4 branch from 4c48807 to 1674404 Compare May 4, 2026 06:03
@tonytonyjan tonytonyjan merged commit 824a16c into master May 4, 2026
4 checks passed
@tonytonyjan tonytonyjan deleted the feat/CU-86ex6zrq0_support-redis-v4 branch May 4, 2026 06:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants