redis-clustering-5.3.0/0000775000175000017500000000000014707477515013641 5ustar n1vedn1vedredis-clustering-5.3.0/CHANGELOG.md0000644000175000017500000000000014707477515015436 0ustar n1vedn1vedredis-clustering-5.3.0/LICENSE0000644000175000017500000000204414707477515014644 0ustar n1vedn1vedCopyright (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.3.0/README.md0000644000175000017500000000746514707477515015132 0ustar n1vedn1ved# 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. redis-clustering-5.3.0/lib/0000775000175000017500000000000014707477515014407 5ustar n1vedn1vedredis-clustering-5.3.0/lib/redis-clustering.rb0000644000175000017500000000006714707477515020220 0ustar n1vedn1ved# frozen_string_literal: true require "redis/cluster" redis-clustering-5.3.0/lib/redis/0000775000175000017500000000000014707477515015515 5ustar n1vedn1vedredis-clustering-5.3.0/lib/redis/cluster.rb0000644000175000017500000001175114707477515017526 0ustar n1vedn1ved# 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.3.0/lib/redis/cluster/0000775000175000017500000000000014707477515017176 5ustar n1vedn1vedredis-clustering-5.3.0/lib/redis/cluster/client.rb0000644000175000017500000001011014707477515020770 0ustar n1vedn1ved# 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.3.0/lib/redis/cluster/transaction_adapter.rb0000644000175000017500000000475114707477515023555 0ustar n1vedn1ved# 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.3.0/lib/redis/cluster/version.rb0000644000175000017500000000017314707477515021207 0ustar n1vedn1ved# frozen_string_literal: true require "redis/version" class Redis class Cluster VERSION = Redis::VERSION end end redis-clustering-5.3.0/redis-clustering.gemspec0000664000175000017500000000404114707477515020470 0ustar n1vedn1ved######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: redis-clustering 5.3.0 ruby lib Gem::Specification.new do |s| s.name = "redis-clustering".freeze s.version = "5.3.0" 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.3.0", "homepage_uri" => "https://github.com/redis/redis-rb/blob/master/cluster", "source_code_uri" => "https://github.com/redis/redis-rb/tree/v5.3.0/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 = "2024-08-21" 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.4.20".freeze s.summary = "A Ruby client library for Redis Cluster".freeze s.specification_version = 4 s.add_runtime_dependency(%q.freeze, ["= 5.3.0"]) s.add_runtime_dependency(%q.freeze, [">= 0.10.0"]) end