redis-clustering-5.4.1/0000755000004100000410000000000015037755477015060 5ustar www-datawww-dataredis-clustering-5.4.1/redis-clustering.gemspec0000644000004100000410000000442215037755477021712 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: redis-clustering 5.4.1 ruby lib Gem::Specification.new do |s| s.name = "redis-clustering".freeze s.version = "5.4.1" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/redis/redis-rb/issues", "changelog_uri" => "https://github.com/redis/redis-rb/blob/master/cluster/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/redis/5.4.1", "homepage_uri" => "https://github.com/redis/redis-rb/blob/master/cluster", "source_code_uri" => "https://github.com/redis/redis-rb/tree/v5.4.1/cluster" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Ezra Zygmuntowicz".freeze, "Taylor Weibley".freeze, "Matthew Clark".freeze, "Brian McKinney".freeze, "Salvatore Sanfilippo".freeze, "Luca Guidi".freeze, "Michel Martens".freeze, "Damian Janowski".freeze, "Pieter Noordhuis".freeze] s.date = "2025-07-17" s.description = " A Ruby client that tries to match Redis' Cluster API one-to-one, while still\n providing an idiomatic interface.\n".freeze s.email = ["redis-db@googlegroups.com".freeze] s.files = ["CHANGELOG.md".freeze, "LICENSE".freeze, "README.md".freeze, "lib/redis-clustering.rb".freeze, "lib/redis/cluster.rb".freeze, "lib/redis/cluster/client.rb".freeze, "lib/redis/cluster/transaction_adapter.rb".freeze, "lib/redis/cluster/version.rb".freeze] s.homepage = "https://github.com/redis/redis-rb/blob/master/cluster".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.7.0".freeze) s.rubygems_version = "3.3.15".freeze s.summary = "A Ruby client library for Redis Cluster".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_runtime_dependency(%q.freeze, ["= 5.4.1"]) s.add_runtime_dependency(%q.freeze, [">= 0.10.0"]) else s.add_dependency(%q.freeze, ["= 5.4.1"]) s.add_dependency(%q.freeze, [">= 0.10.0"]) end end redis-clustering-5.4.1/lib/0000755000004100000410000000000015037755477015626 5ustar www-datawww-dataredis-clustering-5.4.1/lib/redis/0000755000004100000410000000000015037755477016734 5ustar www-datawww-dataredis-clustering-5.4.1/lib/redis/cluster/0000755000004100000410000000000015037755477020415 5ustar www-datawww-dataredis-clustering-5.4.1/lib/redis/cluster/version.rb0000644000004100000410000000017315037755477022430 0ustar www-datawww-data# frozen_string_literal: true require "redis/version" class Redis class Cluster VERSION = Redis::VERSION end end redis-clustering-5.4.1/lib/redis/cluster/client.rb0000644000004100000410000001011015037755477022211 0ustar www-datawww-data# frozen_string_literal: true require 'redis-cluster-client' require 'redis/cluster/transaction_adapter' class Redis class Cluster class Client < RedisClient::Cluster ERROR_MAPPING = ::Redis::Client::ERROR_MAPPING.merge( RedisClient::Cluster::InitialSetupError => Redis::Cluster::InitialSetupError, RedisClient::Cluster::OrchestrationCommandNotSupported => Redis::Cluster::OrchestrationCommandNotSupported, RedisClient::Cluster::AmbiguousNodeError => Redis::Cluster::AmbiguousNodeError, RedisClient::Cluster::ErrorCollection => Redis::Cluster::CommandErrorCollection, RedisClient::Cluster::Transaction::ConsistencyError => Redis::Cluster::TransactionConsistencyError, RedisClient::Cluster::NodeMightBeDown => Redis::Cluster::NodeMightBeDown, ) class << self def config(**kwargs) super(protocol: 2, **kwargs) end def sentinel(**kwargs) super(protocol: 2, **kwargs) end def translate_error!(error, mapping: ERROR_MAPPING) case error when RedisClient::Cluster::ErrorCollection error.errors.each do |_node, node_error| if node_error.is_a?(RedisClient::AuthenticationError) raise mapping.fetch(node_error.class), node_error.message, node_error.backtrace end end remapped_node_errors = error.errors.map do |node_key, node_error| remapped = mapping.fetch(node_error.class, node_error.class).new(node_error.message) remapped.set_backtrace node_error.backtrace [node_key, remapped] end.to_h raise(Redis::Cluster::CommandErrorCollection.new(remapped_node_errors, error.message).tap do |remapped| remapped.set_backtrace error.backtrace end) else Redis::Client.translate_error!(error, mapping: mapping) end end end def initialize(*) handle_errors { super } end ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) def id server_url.join(' ') end def server_url @router.nil? ? @config.startup_nodes.keys : router.node_keys end def connected? true end def disable_reconnection yield # TODO: do we need this, is it doable? end def timeout config.read_timeout end def db 0 end undef_method :call undef_method :call_once undef_method :call_once_v undef_method :blocking_call def call_v(command, &block) handle_errors { super(command, &block) } end def blocking_call_v(timeout, command, &block) timeout += self.timeout if timeout && timeout > 0 handle_errors { super(timeout, command, &block) } end def pipelined(exception: true, &block) handle_errors { super(exception: exception, &block) } end def multi(watch: nil, &block) handle_errors { super(watch: watch, &block) } end def watch(*keys, &block) unless block_given? raise( Redis::Cluster::TransactionConsistencyError, 'A block is required if you use the cluster client.' ) end unless block.arity == 1 raise( Redis::Cluster::TransactionConsistencyError, 'Given block needs an argument if you use the cluster client.' ) end handle_errors do RedisClient::Cluster::OptimisticLocking.new(router).watch(keys) do |c, slot, asking| transaction = Redis::Cluster::TransactionAdapter.new( self, router, @command_builder, node: c, slot: slot, asking: asking ) result = yield transaction c.call('UNWATCH') unless transaction.lock_released? result end end end private def handle_errors yield rescue ::RedisClient::Error => error Redis::Cluster::Client.translate_error!(error) end end end end redis-clustering-5.4.1/lib/redis/cluster/transaction_adapter.rb0000644000004100000410000000475115037755477024776 0ustar www-datawww-data# frozen_string_literal: true require 'redis_client/cluster/transaction' class Redis class Cluster class TransactionAdapter class Internal < RedisClient::Cluster::Transaction def initialize(client, router, command_builder, node: nil, slot: nil, asking: false) @client = client super(router, command_builder, node: node, slot: slot, asking: asking) end def multi raise(Redis::Cluster::TransactionConsistencyError, "Can't nest multi transaction") end def exec # no need to do anything end def discard # no need to do anything end def watch(*_) raise(Redis::Cluster::TransactionConsistencyError, "Can't use watch in a transaction") end def unwatch # no need to do anything end private def method_missing(name, *args, **kwargs, &block) return call(name, *args, **kwargs, &block) if @client.respond_to?(name) super end def respond_to_missing?(name, include_private = false) return true if @client.respond_to?(name) super end end def initialize(client, router, command_builder, node: nil, slot: nil, asking: false) @client = client @router = router @command_builder = command_builder @node = node @slot = slot @asking = asking @lock_released = false end def lock_released? @lock_released end def multi @lock_released = true transaction = Redis::Cluster::TransactionAdapter::Internal.new( @client, @router, @command_builder, node: @node, slot: @slot, asking: @asking ) yield transaction transaction.execute end def exec # no need to do anything end def discard # no need to do anything end def watch(*_) raise(Redis::Cluster::TransactionConsistencyError, "Can't nest watch command if you use the cluster client") end def unwatch @lock_released = true @node.call('UNWATCH') end private def method_missing(name, *args, **kwargs, &block) return @client.public_send(name, *args, **kwargs, &block) if @client.respond_to?(name) super end def respond_to_missing?(name, include_private = false) return true if @client.respond_to?(name) super end end end end redis-clustering-5.4.1/lib/redis/cluster.rb0000644000004100000410000001175115037755477020747 0ustar www-datawww-data# frozen_string_literal: true require "redis" class Redis class Cluster < ::Redis # Raised when client connected to redis as cluster mode # and failed to fetch cluster state information by commands. class InitialSetupError < BaseError end # Raised when client connected to redis as cluster mode # and some cluster subcommands were called. class OrchestrationCommandNotSupported < BaseError def initialize(command, subcommand = '') str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase msg = "#{str} command should be used with care "\ 'only by applications orchestrating Redis Cluster, like redis-trib, '\ 'and the command if used out of the right context can leave the cluster '\ 'in a wrong state or cause data loss.' super(msg) end end # Raised when error occurs on any node of cluster. class CommandErrorCollection < BaseError attr_reader :errors # @param errors [Hash{String => Redis::CommandError}] # @param error_message [String] def initialize(errors, error_message = 'Command errors were replied on any node') @errors = errors super(error_message) end end # Raised when cluster client can't select node. class AmbiguousNodeError < BaseError end class TransactionConsistencyError < BaseError end class NodeMightBeDown < BaseError end def connection raise NotImplementedError, "Redis::Cluster doesn't implement #connection" end # Create a new client instance # # @param [Hash] options # @option options [Float] :timeout (5.0) timeout in seconds # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis` # @option options [Integer, Array] :reconnect_attempts Number of attempts trying to connect, # or a list of sleep duration between attempts. # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not # @option options [Array String, Integer}>] :nodes List of cluster nodes to contact # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not # @option options [Symbol] :replica_affinity scale reading strategy, currently supported: `:random`, `:latency` # @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and # client has to connect nodes via single endpoint with SSL/TLS # @option options [Class] :connector Class of custom connector # # @return [Redis::Cluster] a new client instance def initialize(*) # rubocop:disable Lint/UselessMethodDefinition super end ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) # Sends `CLUSTER *` command to random node and returns its reply. # # @see https://redis.io/commands#cluster Reference of cluster command # # @param subcommand [String, Symbol] the subcommand of cluster command # e.g. `:slots`, `:nodes`, `:slaves`, `:info` # # @return [Object] depends on the subcommand def cluster(subcommand, *args) subcommand = subcommand.to_s.downcase block = case subcommand when 'slots' HashifyClusterSlots when 'nodes' HashifyClusterNodes when 'slaves' HashifyClusterSlaves when 'info' HashifyInfo else Noop end send_command([:cluster, subcommand] + args, &block) end # Watch the given keys to determine execution of the MULTI/EXEC block. # # Using a block is required for a cluster client. It's different from a standalone client. # And you should use the block argument as a receiver if you call commands. # # An `#unwatch` is automatically issued if an exception is raised within the # block that is a subclass of StandardError and is not a ConnectionError. # # @param keys [String, Array] one or more keys to watch # @return [Object] returns the return value of the block # # @example A typical use case. # # The client is an instance of the internal adapter for the optimistic locking # redis.watch("{my}key") do |client| # if client.get("{my}key") == "some value" # # The tx is an instance of the internal adapter for the transaction # client.multi do |tx| # tx.set("{my}key", "other value") # tx.incr("{my}counter") # end # else # client.unwatch # end # end # #=> ["OK", 6] def watch(*keys, &block) synchronize { |c| c.watch(*keys, &block) } end private def initialize_client(options) cluster_config = RedisClient.cluster(**options, protocol: 2, client_implementation: ::Redis::Cluster::Client) cluster_config.new_client end end end require "redis/cluster/client" redis-clustering-5.4.1/lib/redis-clustering.rb0000644000004100000410000000006715037755477021441 0ustar www-datawww-data# frozen_string_literal: true require "redis/cluster" redis-clustering-5.4.1/LICENSE0000644000004100000410000000204415037755477016065 0ustar www-datawww-dataCopyright (c) 2009 Ezra Zygmuntowicz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.redis-clustering-5.4.1/README.md0000644000004100000410000001031415037755477016336 0ustar www-datawww-data# Redis::Cluster ## Getting started Install with: ``` $ gem install redis-clustering ``` You can connect to Redis by instantiating the `Redis::Cluster` class: ```ruby require "redis-clustering" redis = Redis::Cluster.new(nodes: (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }) ``` NB: Both `redis_cluster` and `redis-cluster` are unrelated and abandoned gems. ```ruby # Nodes can be passed to the client as an array of connection URLs. nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" } redis = Redis::Cluster.new(nodes: nodes) # You can also specify the options as a Hash. The options are the same as for a single server connection. (7000..7005).map { |port| { host: '127.0.0.1', port: port } } ``` You can also specify only a subset of the nodes, and the client will discover the missing ones using the [CLUSTER NODES](https://redis.io/commands/cluster-nodes) command. ```ruby Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) ``` If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys. ```ruby Redis::Cluster.new(nodes: nodes, replica: true) ``` Also, you can specify the `:replica_affinity` option if you want to prevent accessing cross availability zones. ```ruby Redis::Cluster.new(nodes: nodes, replica: true, replica_affinity: :latency) ``` The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model). ```ruby redis = Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) redis.mget('key1', 'key2') #=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot) redis.mget('{key}1', '{key}2') #=> [nil, nil] ``` * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening. * The client support permanent node failures, and will reroute requests to promoted slaves. * The client supports `MOVED` and `ASK` redirections transparently. ## Cluster mode with SSL/TLS Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this: ```ruby Redis::Cluster.new(nodes: %w[rediss://foo.example.com:6379]) ``` On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS. ```ruby Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com') ``` In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates. ## Transaction with an optimistic locking Since Redis cluster is a distributed system, several behaviors are different from a standalone server. Client libraries can make them compatible up to a point, but a part of features needs some restrictions. Especially, some cautions are needed to use the transaction feature with an optimistic locking. ```ruby # The client is an instance of the internal adapter for the optimistic locking redis.watch("{my}key") do |client| if client.get("{my}key") == "some value" # The tx is an instance of the internal adapter for the transaction client.multi do |tx| tx.set("{my}key", "other value") tx.incr("{my}counter") end else client.unwatch end end ``` In a cluster mode client, you need to pass a block if you call the watch method and you need to specify an argument to the block. Also, you should use the block argument as a receiver to call commands in the block. Although the above restrictions are needed, this implementations is compatible with a standalone client. ## MGET, MSET and DEL This gem allows you to use MGET, MSET and DEL specifying multiple keys without a hash tag. Cross-slot errors are prevented by an internal dedicated implementation. The underlying library makes the behavior possible. (ref. [redis-cluster-client](https://github.com/redis-rb/redis-cluster-client)) That said, we recommend to use a hash tag for these commands to the better performance. redis-clustering-5.4.1/CHANGELOG.md0000644000004100000410000000000015037755477016657 0ustar www-datawww-data