stefanwille / crystal-redis

Full featured Redis client for Crystal
MIT License
381 stars 61 forks source link

Non block version of `multi` #129

Open hugopl opened 2 years ago

hugopl commented 2 years ago

Hi,

I need to start transaction for more than one redis server and write the same set of keys for all them in this transaction. With current multi this isn't possible since it requires a block. But I can't just call:

@redis_clients.each do |redis|
  redis.multi do |multi|
    write_keys(multi)
  end
end

Since the write_keys procedure is slow and memory hungry. The solution is to just send the MULTI/EXEC command myself, but I was thinking if a patch to allow a non-blocking version of multi would be acceptable.

The only implementation issue I foresee is how to restore the Redis::Strategy::SingleStatement to the Redis object without make everything coupled.... another option would be to not change the @strategy when using non-blocking multi...

I can create a patch for this once is decided what is acceptable and what isn't, so I don't waste my time nether time of others :wink: .

hugopl commented 2 years ago

I didn't fully test this yet, but the monkey patch I'll need to use will be something like this:

class Redis
  def multi : Redis::TransactionApi
    @strategy = strategy = Redis::Strategy::Transaction.new(connection)
    strategy.begin
    Redis::TransactionApi.new(strategy, @namespace.to_s)
  rescue ex : Redis::ConnectionError | Redis::CommandTimeoutError
    close
    raise ex
  end

  def exec
    strategy = @strategy.as?(Redis::Strategy::Transaction)
    raise Redis::Error.new("Not in a multi call") if strategy.nil?

    strategy.commit.as(Array(RedisValue))
  rescue ex : Redis::ConnectionError | Redis::CommandTimeoutError
    close
    raise ex
  ensure
    connection = @connection
    @strategy = Redis::Strategy::SingleStatement.new(connection) if connection
  end
end
hugopl commented 8 months ago

BTW I'm using this monkey patch in production for a year now.

# 🐒️ patch to allow use of MULTI commands without a block
#
# An upstream issue was filed at: https://github.com/stefanwille/crystal-redis/issues/129
class Redis
  def multi : Redis::TransactionApi
    raise Redis::Error.new("Nested transaction") if @strategy.is_a?(Redis::Strategy::Transaction)

    @strategy = strategy = Redis::Strategy::Transaction.new(connection)
    strategy.begin
    Redis::TransactionApi.new(strategy, @namespace.to_s)
  rescue ex : Redis::ConnectionError | Redis::CommandTimeoutError
    close
    raise ex
  end

  def exec
    strategy = @strategy.as?(Redis::Strategy::Transaction)
    raise Redis::Error.new("Not in a multi call") if strategy.nil?

    strategy.commit.as(Array(RedisValue))
  rescue ex : Redis::ConnectionError | Redis::CommandTimeoutError
    close
    raise ex
  ensure
    connection = @connection
    @strategy = Redis::Strategy::SingleStatement.new(connection) if connection
  end
end