pax_global_header00006660000000000000000000000064137556753010014526gustar00rootroot0000000000000052 comment=7587668c1fe5c1f30387d80b5682a93678be40aa redis-rb-4.2.5/000077500000000000000000000000001375567530100132455ustar00rootroot00000000000000redis-rb-4.2.5/.github/000077500000000000000000000000001375567530100146055ustar00rootroot00000000000000redis-rb-4.2.5/.github/workflows/000077500000000000000000000000001375567530100166425ustar00rootroot00000000000000redis-rb-4.2.5/.github/workflows/test.yaml000066400000000000000000000072401375567530100205100ustar00rootroot00000000000000--- name: Test on: push: branches: - "*" pull_request: branches: - "*" jobs: lint: name: Rubocop timeout-minutes: 30 runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v2 - name: Set up Ruby uses: actions/setup-ruby@v1 with: ruby-version: "2.4" - name: Set up Gems run: | gem update --system --no-document gem install bundler --no-document bundle install --jobs 4 --retry 3 --path=.bundle - name: Lint run: bundle exec rubocop main: name: Main timeout-minutes: 30 strategy: fail-fast: false matrix: os: ["ubuntu-latest"] redis: ["6.0"] ruby: ["2.7", "2.6", "2.5", "2.4"] driver: ["ruby", "hiredis", "synchrony"] runs-on: ${{ matrix.os }} env: VERBOSE: true TIMEOUT: 30 LOW_TIMEOUT: 0.01 DRIVER: ${{ matrix.driver }} REDIS_BRANCH: ${{ matrix.redis }} steps: - name: Check out code uses: actions/checkout@v2 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" echo "LOW_TIMEOUT=${LOW_TIMEOUT}" echo "DRIVER=${DRIVER}" echo "REDIS_BRANCH=${REDIS_BRANCH}" - name: Set up Ruby uses: actions/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Cache dependent gems uses: actions/cache@v1 with: path: .bundle key: "local-bundle-ruby-${{ matrix.ruby }}-on-${{ matrix.os }}-0001" - name: Set up Gems run: | gem update --system --no-document gem install bundler --no-document bundle install --jobs 4 --retry 3 --path=.bundle - name: Cache local temporary directory uses: actions/cache@v1 with: path: tmp key: "local-tmp-redis-${{ matrix.redis }}-on-${{ matrix.os }}" - name: Booting up Redis run: make start_all - name: Test run: make test env: RUBYOPT: "--enable-frozen-string-literal" - name: Shutting down Redis run: make stop_all sub: name: Sub timeout-minutes: 30 strategy: fail-fast: false matrix: os: ["ubuntu-latest"] redis: ["5.0", "4.0", "3.2", "3.0"] ruby: ["jruby-9.2.9.0", "2.3"] driver: ["ruby"] runs-on: ${{ matrix.os }} env: VERBOSE: true TIMEOUT: 30 LOW_TIMEOUT: 0.14 DRIVER: ${{ matrix.driver }} REDIS_BRANCH: ${{ matrix.redis }} steps: - name: Check out code uses: actions/checkout@v2 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" echo "LOW_TIMEOUT=${LOW_TIMEOUT}" echo "DRIVER=${DRIVER}" echo "REDIS_BRANCH=${REDIS_BRANCH}" - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Cache dependent gems uses: actions/cache@v1 with: path: .bundle key: "local-bundle-ruby-${{ matrix.ruby }}-on-${{ matrix.os }}-0001" - name: Set up Gems run: | gem update --system --no-document gem install bundler --no-document bundle install --jobs 4 --retry 3 --path=.bundle - name: Cache local temporary directory uses: actions/cache@v1 with: path: tmp key: "local-tmp-redis-${{ matrix.redis }}-on-${{ matrix.os }}" - name: Booting up Redis run: make start_all - name: Test run: make test - name: Shutting down Redis run: make stop_all redis-rb-4.2.5/.gitignore000066400000000000000000000003211375567530100152310ustar00rootroot00000000000000*.rdb *.swp Gemfile.lock *.gem /tmp/ /.idea /.yardoc /.bundle /coverage/* /doc/ /examples/sentinel/sentinel.conf /nohup.out /pkg/* /rdsrv /redis/* /test/db /test/test.conf appendonly.aof temp-rewriteaof-*.aof redis-rb-4.2.5/.rubocop.yml000066400000000000000000000034011375567530100155150ustar00rootroot00000000000000AllCops: TargetRubyVersion: 2.3 Layout/LineLength: Max: 120 Exclude: - 'test/**/*' Layout/CaseIndentation: EnforcedStyle: end Lint/RescueException: Enabled: false Lint/SuppressedException: Enabled: false Lint/AssignmentInCondition: Enabled: false Lint/UnifiedInteger: Enabled: false Lint/UnderscorePrefixedVariableName: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/AbcSize: Enabled: false Metrics/BlockLength: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/ParameterLists: Enabled: false Metrics/PerceivedComplexity: Enabled: false Style/PercentLiteralDelimiters: Enabled: false Style/ParallelAssignment: Enabled: false Style/NumericPredicate: Enabled: false Style/IfUnlessModifier: Enabled: false Style/SignalException: Exclude: - 'lib/redis/connection/synchrony.rb' Style/MethodMissingSuper: Enabled: false Style/StringLiterals: Enabled: false Style/DoubleNegation: Enabled: false Style/MultipleComparison: Enabled: false Style/GuardClause: Enabled: false Style/Semicolon: Enabled: false Style/Documentation: Enabled: false Style/FormatStringToken: Enabled: false Style/FormatString: Enabled: false Style/RescueStandardError: Enabled: false Style/WordArray: Enabled: false Lint/NonLocalExitFromIterator: Enabled: false Lint/EndAlignment: EnforcedStyleAlignWith: variable Layout/ElseAlignment: Enabled: false Naming/HeredocDelimiterNaming: Enabled: false Naming/FileName: Enabled: false Naming/RescuedExceptionsVariableName: Enabled: false Naming/AccessorMethodName: Exclude: - lib/redis/connection/ruby.rb Naming/MethodParameterName: Enabled: false redis-rb-4.2.5/.yardopts000066400000000000000000000001041375567530100151060ustar00rootroot00000000000000--exclude redis/connection --exclude redis/compat --markup markdown redis-rb-4.2.5/CHANGELOG.md000066400000000000000000000352451375567530100150670ustar00rootroot00000000000000# Unreleased # 4.2.5 * Optimize the ruby connector write buffering. See #964. # 4.2.4 * Fix bytesize calculations in the ruby connector, and work on a copy of the buffer. Fix #961, #962. # 4.2.3 * Use io/wait instead of IO.select in the ruby connector. See #960. * Use exception free non blocking IOs in the ruby connector. See #926. * Prevent corruption of the client when an interrupt happen during inside a pipeline block. See #945. # 4.2.2 * Fix `WATCH` support for `Redis::Distributed`. See #941. * Fix handling of empty stream responses. See #905, #929. # 4.2.1 * Fix `exists?` returning an actual boolean when called with multiple keys. See #918. * Setting `Redis.exists_returns_integer = false` disables warning message about new behaviour. See #920. # 4.2.0 * Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations. * Deprecate the synchrony driver. It will be removed in 5.0 and hopefully maintained as a separate gem. See #915. * Make `Redis#exists` variadic, will return an Integer if called with multiple keys. * Add `Redis#exists?` to get a Boolean if any of the keys exists. * `Redis#exists` when called with a single key will warn that future versions will return an Integer. Set `Redis.exists_returns_integer = true` to opt-in to the new behavior. * Support `keepttl` ooption in `set`. See #913. * Optimized initialization of Redis::Cluster. See #912. * Accept sentinel options even with string key. See #599. * Verify TLS connections by default. See #900. * Make `Redis#hset` variadic. It now returns an integer, not a boolean. See #910. # 4.1.4 * Alias `Redis#disconnect` as `#close`. See #901. * Handle clusters with multiple slot ranges. See #894. * Fix password authentication to a redis cluster. See #889. * Handle recursive MOVED responses. See #882. * Increase buffer size in the ruby connector. See #880. * Fix thread safety of `Redis.queue`. See #878. * Deprecate `Redis::Future#==` as it's likely to be a mistake. See #876. * Support `KEEPTTL` option for SET command. See #913. # 4.1.3 * Fix the client hanging forever when connecting with SSL to a non-SSL server. See #835. # 4.1.2 * Fix several authentication problems with sentinel. See #850 and #856. * Explicitly drop Ruby 2.2 support. # 4.1.1 * Fix error handling in multi blocks. See #754. * Fix geoadd to accept arrays like georadius and georadiusbymember. See #841. * Fix georadius command failing when long == lat. See #841. * Fix timeout error in xread block: 0. See #837. * Fix incompatibility issue with redis-objects. See #834. * Properly handle Errno::EADDRNOTAVAIL on connect. * Fix password authentication to sentinel instances. See #813. # 4.1.0 * Add Redis Cluster support. See #716. * Add streams support. See #799 and #811. * Add ZPOP* support. See #812. * Fix issues with integer-like objects as BPOP timeout # 4.0.3 * Fix raising command error for first command in pipeline. See #788. * Fix the gemspec to stop exposing a `build` executable. See #785. * Add `:reconnect_delay` and `:reconnect_delay_max` options. See #778. # 4.0.2 * Added `Redis#unlink`. See #766. * `Redis.new` now accept a custom connector via `:connector`. See #591. * `Redis#multi` no longer perform empty transactions. See #747. * `Redis#hdel` now accepts hash keys as multiple arguments like `#del`. See #755. * Allow to skip SSL verification. See #745. * Add Geo commands: `geoadd`, `geohash`, `georadius`, `georadiusbymember`, `geopos`, `geodist`. See #730. # 4.0.1 * `Redis::Distributed` now supports `mget` and `mapped_mget`. See #687. * `Redis::Distributed` now supports `sscan` and `sscan_each`. See #572. * `Redis#connection` returns a hash with connection information. You shouldn't need to call `Redis#_client`, ever. * `Redis#flushdb` and `Redis#flushall` now support the `:async` option. See #706. # 4.0 * Removed `Redis.connect`. Use `Redis.new`. * Removed `Redis#[]` and `Redis#[]=` aliases. * Added support for `CLIENT` commands. The lower-level client can be accessed via `Redis#_client`. * Dropped official support for Ruby < 2.2.2. # 3.3.5 * Fixed Ruby 1.8 compatibility after backporting `Redis#connection`. See #719. # 3.3.4 (yanked) * `Redis#connection` returns a hash with connection information. You shouldn't need to call `Redis#_client`, ever. # 3.3.3 * Improved timeout handling after dropping Timeout module. # 3.3.2 * Added support for `SPOP` with COUNT. See #628. * Fixed connection glitches when using SSL. See #644. # 3.3.1 * Remove usage of Timeout::timeout, refactor into using low level non-blocking writes. This fixes a memory leak due to Timeout creating threads on each invocation. # 3.3.0 * Added support for SSL/TLS. Redis doesn't support SSL natively, so you still need to run a terminating proxy on Redis' side. See #496. * Added `read_timeout` and `write_timeout` options. See #437, #482. * Added support for pub/sub with timeouts. See #329. * Added `Redis#call`, `Redis#queue` and `Redis#commit` as a more minimal API to the client. * Deprecated `Redis#disconnect!` in favor of `Redis#close`. # 3.2.2 * Added support for `ZADD` options `NX`, `XX`, `CH`, `INCR`. See #547. * Added support for sentinel commands. See #556. * New `:id` option allows you to identify the client against Redis. See #510. * `Redis::Distributed` will raise when adding two nodes with the same ID. See #354. # 3.2.1 * Added support for `PUBSUB` command. * More low-level socket errors are now raised as `CannotConnectError`. * Added `:connect_timeout` option. * Added support for `:limit` option for `ZREVRANGEBYLEX`. * Fixed an issue where connections become inconsistent when using Ruby's Timeout module outside of the client (see #501, #502). * Added `Redis#disconnect!` as a public-API way of disconnecting the client (without needing to use `QUIT`). See #506. * Fixed Sentinel support with Hiredis. * Fixed Sentinel support when using authentication and databases. * Improved resilience when trying to contact sentinels. # 3.2.0 * Redis Sentinel support. # 3.1.0 * Added debug log sanitization (#428). * Added support for HyperLogLog commands (Redis 2.8.9, #432). * Added support for `BITPOS` command (Redis 2.9.11, #412). * The client will now automatically reconnect after a fork (#414). * If you want to disable the fork-safety check and prefer to share the connection across child processes, you can now pass the `inherit_socket` option (#409). * If you want the client to attempt to reconnect more than once, you can now pass the `reconnect_attempts` option (#347) # 3.0.7 * Added method `Redis#dup` to duplicate a Redis connection. * IPv6 support. # 3.0.6 * Added support for `SCAN` and variants. # 3.0.5 * Fix calling #select from a pipeline (#309). * Added method `Redis#connected?`. * Added support for `MIGRATE` (Redis 2.6). * Support extended SET command (#343, thanks to @benubois). # 3.0.4 * Ensure #watch without a block returns "OK" (#332). * Make futures identifiable (#330). * Fix an issue preventing STORE in a SORT with multiple GETs (#328). # 3.0.3 * Blocking list commands (`BLPOP`, `BRPOP`, `BRPOPLPUSH`) use a socket timeout equal to the sum of the command's timeout and the Redis client's timeout, instead of disabling socket timeout altogether. * Ruby 2.0 compatibility. * Added support for `DUMP` and `RESTORE` (Redis 2.6). * Added support for `BITCOUNT` and `BITOP` (Redis 2.6). * Call `#to_s` on value argument for `SET`, `SETEX`, `PSETEX`, `GETSET`, `SETNX`, and `SETRANGE`. # 3.0.2 * Unescape CGI escaped password in URL. * Fix test to check availability of `UNIXSocket`. * Fix handling of score = +/- infinity for sorted set commands. * Replace array splats with concatenation where possible. * Raise if `EXEC` returns an error. * Passing a nil value in options hash no longer overwrites the default. * Allow string keys in options hash passed to `Redis.new` or `Redis.connect`. * Fix uncaught error triggering unrelated error (synchrony driver). See f7ffd5f1a628029691084de69e5b46699bb8b96d and #248. # 3.0.1 * Fix reconnect logic not kicking in on a write error. See 427dbd52928af452f35aa0a57b621bee56cdcb18 and #238. # 3.0.0 ### Upgrading from 2.x to 3.0 The following items are the most important changes to review when upgrading from redis-rb 2.x. A full list of changes can be found below. * The methods for the following commands have changed the arguments they take, their return value, or both. * `BLPOP`, `BRPOP`, `BRPOPLPUSH` * `SORT` * `MSETNX` * `ZRANGE`, `ZREVRANGE`, `ZRANGEBYSCORE`, `ZREVRANGEBYSCORE` * `ZINCRBY`, `ZSCORE` * The return value from `#pipelined` and `#multi` no longer contains unprocessed replies, but the same replies that would be returned if the command had not been executed in these blocks. * The client raises custom errors on connection errors, instead of `RuntimeError` and errors in the `Errno` family. ### Changes * Added support for scripting commands (Redis 2.6). Scripts can be executed using `#eval` and `#evalsha`. Both can commands can either take two arrays to specify `KEYS` and `ARGV`, or take a hash containing `:keys` and `:argv` to specify `KEYS` and `ARGV`. ```ruby redis.eval("return ARGV[1] * ARGV[2]", :argv => [2, 3]) # => 6 ``` Subcommands of the `SCRIPT` command can be executed via the `#script` method. For example: ```ruby redis.script(:load, "return ARGV[1] * ARGV[2]") # => "58db5d365a1922f32e7aa717722141ea9c2b0cf3" redis.script(:exists, "58db5d365a1922f32e7aa717722141ea9c2b0cf3") # => true redis.script(:flush) # => "OK" ``` * The repository now lives at [https://github.com/redis/redis-rb](https://github.com/redis/redis-rb). Thanks, Ezra! * Added support for `PEXPIRE`, `PEXPIREAT`, `PTTL`, `PSETEX`, `INCRYBYFLOAT`, `HINCRYBYFLOAT` and `TIME` (Redis 2.6). * `Redis.current` is now thread unsafe, because the client itself is thread safe. In the future you'll be able to do something like: ```ruby Redis.current = Redis::Pool.connect ``` This makes `Redis.current` actually usable in multi-threaded environments, while not affecting those running a single thread. * Change API for `BLPOP`, `BRPOP` and `BRPOPLPUSH`. Both `BLPOP` and `BRPOP` now take a single argument equal to a string key, or an array with string keys, followed by an optional hash with a `:timeout` key. When not specified, the timeout defaults to `0` to not time out. ```ruby redis.blpop(["list1", "list2"], :timeout => 1.0) ``` `BRPOPLPUSH` also takes an optional hash with a `:timeout` key as last argument for consistency. When not specified, the timeout defaults to `0` to not time out. ```ruby redis.brpoplpush("some_list", "another_list", :timeout => 1.0) ``` * When `SORT` is passed multiple key patterns to get via the `:get` option, it now returns an array per result element, holding all `GET` substitutions. * The `MSETNX` command now returns a boolean. * The `ZRANGE`, `ZREVRANGE`, `ZRANGEBYSCORE` and `ZREVRANGEBYSCORE` commands now return an array containing `[String, Float]` pairs when `:with_scores => true` is passed. For example: ```ruby redis.zrange("zset", 0, -1, :with_scores => true) # => [["foo", 1.0], ["bar", 2.0]] ``` * The `ZINCRBY` and `ZSCORE` commands now return a `Float` score instead of a string holding a representation of the score. * The client now raises custom exceptions where it makes sense. If by any chance you were rescuing low-level exceptions (`Errno::*`), you should now rescue as follows: Errno::ECONNRESET -> Redis::ConnectionError Errno::EPIPE -> Redis::ConnectionError Errno::ECONNABORTED -> Redis::ConnectionError Errno::EBADF -> Redis::ConnectionError Errno::EINVAL -> Redis::ConnectionError Errno::EAGAIN -> Redis::TimeoutError Errno::ECONNREFUSED -> Redis::CannotConnectError * Always raise exceptions originating from erroneous command invocation inside pipelines and MULTI/EXEC blocks. The old behavior (swallowing exceptions) could cause application bugs to go unnoticed. * Implement futures for assigning values inside pipelines and MULTI/EXEC blocks. Futures are assigned their value after the pipeline or MULTI/EXEC block has executed. ```ruby $redis.pipelined do @future = $redis.get "key" end puts @future.value ``` * Ruby 1.8.6 is officially not supported. * Support `ZCOUNT` in `Redis::Distributed` (Michael Dungan). * Pipelined commands now return the same replies as when called outside a pipeline. In the past, pipelined replies were returned without post-processing. * Support `SLOWLOG` command (Michael Bernstein). * Calling `SHUTDOWN` effectively disconnects the client (Stefan Kaes). * Basic support for mapping commands so that they can be renamed on the server. * Connecting using a URL now checks that a host is given. It's just a small sanity check, cf. #126 * Support variadic commands introduced in Redis 2.4. # 2.2.2 * Added method `Redis::Distributed#hsetnx`. # 2.2.1 * Internal API: Client#call and family are now called with a single array argument, since splatting a large number of arguments (100K+) results in a stack overflow on 1.9.2. * The `INFO` command can optionally take a subcommand. When the subcommand is `COMMANDSTATS`, the client will properly format the returned statistics per command. Subcommands for `INFO` are available since Redis v2.3.0 (unstable). * Change `IO#syswrite` back to the buffered `IO#write` since some Rubies do short writes for large (1MB+) buffers and some don't (see issue #108). # 2.2.0 * Added method `Redis#without_reconnect` that ensures the client will not try to reconnect when running the code inside the specified block. * Thread-safe by default. Thread safety can be explicitly disabled by passing `:thread_safe => false` as argument. * Commands called inside a MULTI/EXEC no longer raise error replies, since a successful EXEC means the commands inside the block were executed. * MULTI/EXEC blocks are pipelined. * Don't disconnect on error replies. * Use `IO#syswrite` instead of `IO#write` because write buffering is not necessary. * Connect to a unix socket by passing the `:path` option as argument. * The timeout value is coerced into a float, allowing sub-second timeouts. * Accept both `:with_scores` _and_ `:withscores` as argument to sorted set commands. * Use [hiredis](https://github.com/pietern/hiredis-rb) (v0.3 or higher) by requiring "redis/connection/hiredis". * Use [em-synchrony](https://github.com/igrigorik/em-synchrony) by requiring "redis/connection/synchrony". # 2.1.1 See commit log. redis-rb-4.2.5/Gemfile000066400000000000000000000004271375567530100145430ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'minitest' gem 'rake' gem 'rubocop', '0.81' # Using jruby-openssl 0.10.0, we get NPEs in jruby tests: https://github.com/redis/redis-rb/issues/756 platform :jruby do gem 'jruby-openssl', '<0.10.0' end redis-rb-4.2.5/LICENSE000066400000000000000000000020441375567530100142520ustar00rootroot00000000000000Copyright (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-rb-4.2.5/README.md000066400000000000000000000340501375567530100145260ustar00rootroot00000000000000# redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] ![](https://github.com/redis/redis-rb/workflows/Test/badge.svg?branch=master) A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still providing an idiomatic interface. See [RubyDoc.info][rubydoc] for the API docs of the latest published gem. ## Getting started Install with: ``` $ gem install redis ``` You can connect to Redis by instantiating the `Redis` class: ```ruby require "redis" redis = Redis.new ``` This assumes Redis was started with a default configuration, and is listening on `localhost`, port 6379. If you need to connect to a remote server or a different port, try: ```ruby redis = Redis.new(host: "10.0.1.1", port: 6380, db: 15) ``` You can also specify connection options as a [`redis://` URL][redis-url]: ```ruby redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15") ``` The client expects passwords with special chracters to be URL-encoded (i.e. `CGI.escape(password)`). By default, the client will try to read the `REDIS_URL` environment variable and use that as URL to connect to. The above statement is therefore equivalent to setting this environment variable and calling `Redis.new` without arguments. To connect to Redis listening on a Unix socket, try: ```ruby redis = Redis.new(path: "/tmp/redis.sock") ``` To connect to a password protected Redis instance, use: ```ruby redis = Redis.new(password: "mysecret") ``` The Redis class exports methods that are named identical to the commands they execute. The arguments these methods accept are often identical to the arguments specified on the [Redis website][redis-commands]. For instance, the `SET` and `GET` commands can be called like this: ```ruby redis.set("mykey", "hello world") # => "OK" redis.get("mykey") # => "hello world" ``` All commands, their arguments, and return values are documented and available on [RubyDoc.info][rubydoc]. ## Sentinel support The client is able to perform automatic failover by using [Redis Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+ if you want to use this feature. To connect using Sentinel, use: ```ruby SENTINELS = [{ host: "127.0.0.1", port: 26380 }, { host: "127.0.0.1", port: 26381 }] redis = Redis.new(url: "redis://mymaster", sentinels: SENTINELS, role: :master) ``` * The master name identifies a group of Redis instances composed of a master and one or more slaves (`mymaster` in the example). * It is possible to optionally provide a role. The allowed roles are `master` and `slave`. When the role is `slave`, the client will try to connect to a random slave of the specified master. If a role is not specified, the client will connect to the master. * When using the Sentinel support you need to specify a list of sentinels to connect to. The list does not need to enumerate all your Sentinel instances, but a few so that if one is down the client will try the next one. The client is able to remember the last Sentinel that was able to reply correctly and will use it for the next requests. If you want to [authenticate](https://redis.io/topics/sentinel#configuring-sentinel-instances-with-authentication) Sentinel itself, you must specify the `password` option per instance. ```ruby SENTINELS = [{ host: '127.0.0.1', port: 26380, password: 'mysecret' }, { host: '127.0.0.1', port: 26381, password: 'mysecret' }] redis = Redis.new(host: 'mymaster', sentinels: SENTINELS, role: :master) ``` ## Cluster support `redis-rb` supports [clustering](https://redis.io/topics/cluster-spec). ```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.new(cluster: 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.new(cluster: %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.new(cluster: nodes, replica: true) ``` The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model). ```ruby redis = Redis.new(cluster: %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. ## Storing objects Redis "string" types can be used to store serialized Ruby objects, for example with JSON: ```ruby require "json" redis.set "foo", [1, 2, 3].to_json # => OK JSON.parse(redis.get("foo")) # => [1, 2, 3] ``` ## Pipelining When multiple commands are executed sequentially, but are not dependent, the calls can be *pipelined*. This means that the client doesn't wait for reply of the first command before sending the next command. The advantage is that multiple commands are sent at once, resulting in faster overall execution. The client can be instructed to pipeline commands by using the `#pipelined` method. After the block is executed, the client sends all commands to Redis and gathers their replies. These replies are returned by the `#pipelined` method. ```ruby redis.pipelined do redis.set "foo", "bar" redis.incr "baz" end # => ["OK", 1] ``` ### Executing commands atomically You can use `MULTI/EXEC` to run a number of commands in an atomic fashion. This is similar to executing a pipeline, but the commands are preceded by a call to `MULTI`, and followed by a call to `EXEC`. Like the regular pipeline, the replies to the commands are returned by the `#multi` method. ```ruby redis.multi do redis.set "foo", "bar" redis.incr "baz" end # => ["OK", 1] ``` ### Futures Replies to commands in a pipeline can be accessed via the *futures* they emit (since redis-rb 3.0). All calls inside a pipeline block return a `Future` object, which responds to the `#value` method. When the pipeline has successfully executed, all futures are assigned their respective replies and can be used. ```ruby redis.pipelined do @set = redis.set "foo", "bar" @incr = redis.incr "baz" end @set.value # => "OK" @incr.value # => 1 ``` ## Error Handling In general, if something goes wrong you'll get an exception. For example, if it can't connect to the server a `Redis::CannotConnectError` error will be raised. ```ruby begin redis.ping rescue StandardError => e e.inspect # => # e.message # => Timed out connecting to Redis on 10.0.1.1:6380 end ``` See lib/redis/errors.rb for information about what exceptions are possible. ## Timeouts The client allows you to configure connect, read, and write timeouts. Passing a single `timeout` option will set all three values: ```ruby Redis.new(:timeout => 1) ``` But you can use specific values for each of them: ```ruby Redis.new( :connect_timeout => 0.2, :read_timeout => 1.0, :write_timeout => 0.5 ) ``` All timeout values are specified in seconds. When using pub/sub, you can subscribe to a channel using a timeout as well: ```ruby redis = Redis.new(reconnect_attempts: 0) redis.subscribe_with_timeout(5, "news") do |on| on.message do |channel, message| # ... end end ``` If no message is received after 5 seconds, the client will unsubscribe. ## Reconnections The client allows you to configure how many `reconnect_attempts` it should complete before declaring a connection as failed. Furthermore, you may want to control the maximum duration between reconnection attempts with `reconnect_delay` and `reconnect_delay_max`. ```ruby Redis.new( :reconnect_attempts => 10, :reconnect_delay => 1.5, :reconnect_delay_max => 10.0, ) ``` The delay values are specified in seconds. With the above configuration, the client would attempt 10 reconnections, exponentially increasing the duration between each attempt but it never waits longer than `reconnect_delay_max`. This is the retry algorithm: ```ruby attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min ``` **By default**, this gem will only **retry a connection once** and then fail, but with the above configuration the reconnection attempt would look like this: #|Attempt wait time|Total wait time :-:|:-:|:-: 1|1.5s|1.5s 2|3.0s|4.5s 3|6.0s|10.5s 4|10.0s|20.5s 5|10.0s|30.5s 6|10.0s|40.5s 7|10.0s|50.5s 8|10.0s|60.5s 9|10.0s|70.5s 10|10.0s|80.5s So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying to reconnect, this is likely fine in long-running background processes, but if you use Redis to drive your website you might want to have a lower `reconnect_delay_max` or have less `reconnect_attempts`. ## SSL/TLS Support This library supports natively terminating client side SSL/TLS connections when talking to Redis via a server-side proxy such as [stunnel], [hitch], or [ghostunnel]. To enable SSL support, pass the `:ssl => true` option when configuring the Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis). You will also need to pass in an `:ssl_params => { ... }` hash used to configure the `OpenSSL::SSL::SSLContext` object used for the connection: ```ruby redis = Redis.new( :url => "rediss://:p4ssw0rd@10.0.1.1:6381/15", :ssl_params => { :ca_file => "/path/to/ca.crt" } ) ``` The options given to `:ssl_params` are passed directly to the `OpenSSL::SSL::SSLContext#set_params` method and can be any valid attribute of the SSL context. Please see the [OpenSSL::SSL::SSLContext documentation] for all of the available attributes. Here is an example of passing in params that can be used for SSL client certificate authentication (a.k.a. mutual TLS): ```ruby redis = Redis.new( :url => "rediss://:p4ssw0rd@10.0.1.1:6381/15", :ssl_params => { :ca_file => "/path/to/ca.crt", :cert => OpenSSL::X509::Certificate.new(File.read("client.crt")), :key => OpenSSL::PKey::RSA.new(File.read("client.key")) } ) ``` [stunnel]: https://www.stunnel.org/ [hitch]: https://hitch-tls.org/ [ghostunnel]: https://github.com/square/ghostunnel [OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.3.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html *NOTE:* SSL is only supported by the default "Ruby" driver ## Expert-Mode Options - `inherit_socket: true`: disable safety check that prevents a forked child from sharing a socket with its parent; this is potentially useful in order to mitigate connection churn when: - many short-lived forked children of one process need to talk to redis, AND - your own code prevents the parent process from using the redis connection while a child is alive Improper use of `inherit_socket` will result in corrupted and/or incorrect responses. ## Alternate drivers By default, redis-rb uses Ruby's socket library to talk with Redis. To use an alternative connection driver it should be specified as option when instantiating the client object. These instructions are only valid for **redis-rb 3.0**. For instructions on how to use alternate drivers from **redis-rb 2.2**, please refer to an [older README][readme-2.2.2]. [readme-2.2.2]: https://github.com/redis/redis-rb/blob/v2.2.2/README.md ### hiredis The hiredis driver uses the connection facility of hiredis-rb. In turn, hiredis-rb is a binding to the official hiredis client library. It optimizes for speed, at the cost of portability. Because it is a C extension, JRuby is not supported (by default). It is best to use hiredis when you have large replies (for example: `LRANGE`, `SMEMBERS`, `ZRANGE`, etc.) and/or use big pipelines. In your Gemfile, include hiredis: ```ruby gem "redis", "~> 3.0.1" gem "hiredis", "~> 0.4.5" ``` When instantiating the client object, specify hiredis: ```ruby redis = Redis.new(:driver => :hiredis) ``` ### synchrony The synchrony driver adds support for [em-synchrony][em-synchrony]. This makes redis-rb work with EventMachine's asynchronous I/O, while not changing the exposed API. The hiredis gem needs to be available as well, because the synchrony driver uses hiredis for parsing the Redis protocol. [em-synchrony]: https://github.com/igrigorik/em-synchrony In your Gemfile, include em-synchrony and hiredis: ```ruby gem "redis", "~> 3.0.1" gem "hiredis", "~> 0.4.5" gem "em-synchrony" ``` When instantiating the client object, specify synchrony: ```ruby redis = Redis.new(:driver => :synchrony) ``` ## Testing This library is tested against recent Ruby and Redis versions. Check [Travis][travis-link] for the exact versions supported. ## See Also - [async-redis](https://github.com/socketry/async-redis) — An [async](https://github.com/socketry/async) compatible Redis client. ## Contributors Several people contributed to redis-rb, but we would like to especially mention Ezra Zygmuntowicz. Ezra introduced the Ruby community to many new cool technologies, like Redis. He wrote the first version of this client and evangelized Redis in Rubyland. Thank you, Ezra. ## Contributing [Fork the project](https://github.com/redis/redis-rb) and send pull requests. [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg [inchpages-link]: https://inch-ci.org/github/redis/redis-rb [redis-commands]: https://redis.io/commands [redis-home]: https://redis.io [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis [travis-home]: https://travis-ci.org/ [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master [travis-link]: https://travis-ci.org/redis/redis-rb [rubydoc]: http://www.rubydoc.info/gems/redis redis-rb-4.2.5/Rakefile000066400000000000000000000010411375567530100147060ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new :test do |t| if ENV['SOCKET_PATH'].nil? sock_file = Dir.glob("#{__dir__}/**/redis.sock").first if sock_file.nil? puts '`SOCKET_PATH` environment variable required' exit 1 end ENV['SOCKET_PATH'] = sock_file end t.libs = %w(lib test) if ARGV.size == 1 t.pattern = 'test/*_test.rb' else t.test_files = ARGV[1..-1] end t.options = '-v' if ENV['CI'] || ENV['VERBOSE'] end task default: :test redis-rb-4.2.5/benchmarking/000077500000000000000000000000001375567530100156755ustar00rootroot00000000000000redis-rb-4.2.5/benchmarking/cluster.rb000066400000000000000000000055041375567530100177070ustar00rootroot00000000000000# frozen_string_literal: true # Execute the following commands before execution # # `$ make start` # `$ make start_cluster` # `$ make create_cluster` require 'redis' require 'benchmark' HOST = '127.0.0.1' STANDALONE_PORT = 6381 CLUSTER_PORT = 7000 N = (ARGV.first || 100_000).to_i rn = Redis.new(host: HOST, port: STANDALONE_PORT) rc = Redis.new(host: HOST, port: CLUSTER_PORT) rm = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}]) rs = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}], replica: true) Benchmark.bmbm do |bm| bm.report('client: normal, server: standalone, command: SET, key: fixed') do N.times { rn.set('foo', '42') } end bm.report('client: normal, server: standalone, command: GET, key: fixed') do N.times { rn.get('foo') } end bm.report('client: normal, server: cluster, command: SET, key: fixed') do N.times { rc.set('bar', '42') } end bm.report('client: normal, server: cluster, command: GET, key: fixed') do N.times { rc.get('bar') } end bm.report('client: cluster, server: cluster, command: SET, key: fixed') do N.times { rm.set('baz', '42') } end rm.wait(1, 0) bm.report('client: cluster, server: cluster, command: GET, key: fixed') do N.times { rm.get('baz') } end bm.report('client: cluster, server: cluster, command: SET, key: fixed, replica: true') do N.times { rs.set('zap', '42') } end rs.wait(1, 0) bm.report('client: cluster, server: cluster, command: GET, key: fixed, replica: true') do N.times { rs.get('zap') } end bm.report('client: normal, server: standalone, command: SET, key: variable') do N.times { |i| rn.set("foo:#{i}", '42') } end bm.report('client: normal, server: standalone, command: GET, key: variable') do N.times { |i| rn.get("foo:#{i}") } end bm.report('client: cluster, server: cluster, command: SET, key: variable') do N.times { |i| rm.set("bar:#{i}", '42') } end rm.wait(1, 0) bm.report('client: cluster, server: cluster, command: GET, key: variable') do N.times { |i| rm.get("bar:#{i}") } end bm.report('client: cluster, server: cluster, command: SET, key: variable, replica: true') do N.times { |i| rs.set("baz:#{i}", '42') } end rs.wait(1, 0) bm.report('client: cluster, server: cluster, command: GET, key: variable, replica: true') do N.times { |i| rs.get("baz:#{i}") } end rn.set('bar', 0) bm.report('client: normal, server: standalone, command: INCR, key: fixed') do N.times { rn.incr('bar') } end rc.set('bar', 0) bm.report('client: normal, server: cluster, command: INCR, key: fixed') do N.times { rc.incr('bar') } end rm.set('bar', 0) bm.report('client: cluster, server: cluster, command: INCR, key: fixed') do N.times { rm.incr('bar') } end end redis-rb-4.2.5/benchmarking/cluster_slot.rb000066400000000000000000000014431375567530100207460ustar00rootroot00000000000000# frozen_string_literal: true require 'redis' require 'benchmark' N = (ARGV.first || 100_000).to_i available_slots = { "127.0.0.1:7000" => [0..5460], "127.0.0.1:7003" => [0..5460], "127.0.0.1:7001" => [5461..10_922], "127.0.0.1:7004" => [5461..10_922], "127.0.0.1:7002" => [10_923..16_383], "127.0.0.1:7005" => [10_923..16_383] } node_flags = { "127.0.0.1:7000" => "master", "127.0.0.1:7002" => "master", "127.0.0.1:7001" => "master", "127.0.0.1:7005" => "slave", "127.0.0.1:7004" => "slave", "127.0.0.1:7003" => "slave" } Benchmark.bmbm do |bm| bm.report('Slot.new') do allocs = GC.stat(:total_allocated_objects) N.times do Redis::Cluster::Slot.new(available_slots, node_flags, false) end puts GC.stat(:total_allocated_objects) - allocs end end redis-rb-4.2.5/benchmarking/logging.rb000066400000000000000000000024301375567530100176470ustar00rootroot00000000000000# frozen_string_literal: true # Run with # # $ ruby -Ilib benchmarking/logging.rb # begin require "bench" rescue LoadError warn "`gem install bench` and try again." exit 1 end require "redis" require "logger" def log(level, namespace = nil) logger = (namespace || Kernel).const_get(:Logger).new("/dev/null") logger.level = (namespace || Logger).const_get(level) logger end def stress(redis) redis.flushdb n = (ARGV.shift || 2000).to_i n.times do |i| key = "foo:#{i}" redis.set key, i redis.get key end end default = Redis.new logging_redises = [ Redis.new(logger: log(:DEBUG)), Redis.new(logger: log(:INFO)) ] begin require "log4r" logging_redises += [ Redis.new(logger: log(:DEBUG, Log4r)), Redis.new(logger: log(:INFO, Log4r)) ] rescue LoadError warn "Log4r not installed. `gem install log4r` if you want to compare it against Ruby's " \ "Logger (spoiler: it's much faster)." end benchmark "Default options (no logger)" do stress(default) end logging_redises.each do |redis| logger = redis._client.logger case logger when Logger level = Logger::SEV_LABEL[logger.level] when Log4r::Logger level = logger.levels[logger.level] end benchmark "#{logger.class} on #{level}" do stress(redis) end end run 10 redis-rb-4.2.5/benchmarking/pipeline.rb000066400000000000000000000016251375567530100200330ustar00rootroot00000000000000# frozen_string_literal: true require "benchmark" $LOAD_PATH.push File.join(File.dirname(__FILE__), 'lib') require 'redis' ITERATIONS = 10_000 @r = Redis.new Benchmark.bmbm do |benchmark| benchmark.report("set") do @r.flushdb ITERATIONS.times do |i| @r.set("foo#{i}", "Hello world!") @r.get("foo#{i}") end end benchmark.report("set (pipelined)") do @r.flushdb @r.pipelined do ITERATIONS.times do |i| @r.set("foo#{i}", "Hello world!") @r.get("foo#{i}") end end end benchmark.report("lpush+ltrim") do @r.flushdb ITERATIONS.times do |i| @r.lpush "lpush#{i}", i @r.ltrim "ltrim#{i}", 0, 30 end end benchmark.report("lpush+ltrim (pipelined)") do @r.flushdb @r.pipelined do ITERATIONS.times do |i| @r.lpush "lpush#{i}", i @r.ltrim "ltrim#{i}", 0, 30 end end end end redis-rb-4.2.5/benchmarking/speed.rb000066400000000000000000000005321375567530100173220ustar00rootroot00000000000000# frozen_string_literal: true # Run with # # $ ruby -Ilib benchmarking/speed.rb # require "benchmark" require "redis" r = Redis.new n = (ARGV.shift || 20_000).to_i elapsed = Benchmark.realtime do # n sets, n gets n.times do |i| key = "foo#{i}" r[key] = key * 10 r[key] end end puts '%.2f Kops' % (2 * n / 1000 / elapsed) redis-rb-4.2.5/benchmarking/suite.rb000066400000000000000000000013251375567530100173540ustar00rootroot00000000000000# frozen_string_literal: true require 'fileutils' def run_in_background(command) fork { system command } end def with_all_segments(&block) 0.upto(9) do |segment_number| block_size = 100_000 start_index = segment_number * block_size end_index = start_index + block_size - 1 block.call(start_index, end_index) end end # with_all_segments do |start_index, end_index| # puts "Initializing keys from #{start_index} to #{end_index}" # system "ruby worker.rb initialize #{start_index} #{end_index} 0" # end with_all_segments do |start_index, end_index| run_in_background "ruby worker.rb write #{start_index} #{end_index} 10" run_in_background "ruby worker.rb read #{start_index} #{end_index} 1" end redis-rb-4.2.5/benchmarking/worker.rb000066400000000000000000000026601375567530100175370ustar00rootroot00000000000000# frozen_string_literal: true BENCHMARK_ROOT = File.dirname(__FILE__) REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib") $LOAD_PATH << REDIS_ROOT require 'redis' require 'benchmark' def show_usage puts <<-EOL Usage: worker.rb [read:write] EOL end def shift_from_argv value = ARGV.shift unless value show_usage exit(-1) end value end operation = shift_from_argv.to_sym start_index = shift_from_argv.to_i end_index = shift_from_argv.to_i sleep_msec = shift_from_argv.to_i sleep_duration = sleep_msec / 1000.0 redis = Redis.new case operation when :initialize start_index.upto(end_index) do |i| redis[i] = 0 end when :clear start_index.upto(end_index) do |i| redis.delete(i) end when :read, :write puts "Starting to #{operation} at segment #{end_index + 1}" loop do t1 = Time.now start_index.upto(end_index) do |i| case operation when :read redis.get(i) when :write redis.incr(i) else raise "Unknown operation: #{operation}" end sleep sleep_duration end t2 = Time.now requests_processed = end_index - start_index time = t2 - t1 puts "#{t2.strftime('%H:%M')} [segment #{end_index + 1}] : Processed #{requests_processed} requests " \ "in #{time} seconds - #{(requests_processed / time).round} requests/sec" end else raise "Unknown operation: #{operation}" end redis-rb-4.2.5/bin/000077500000000000000000000000001375567530100140155ustar00rootroot00000000000000redis-rb-4.2.5/bin/build000077500000000000000000000030651375567530100150460ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true TARBALL = ARGV[0] require 'digest/sha1' require 'fileutils' class Builder TARBALL_CACHE_EXPIRATION = 60 * 10 def initialize(redis_branch, tmp_dir) @redis_branch = redis_branch @tmp_dir = tmp_dir @build_dir = File.join(@tmp_dir, "cache", "redis-#{redis_branch}") end def run download_tarball_if_needed if old_checkum != checksum build update_checksum end 0 end private def download_tarball_if_needed return if File.exist?(tarball_path) && File.mtime(tarball_path) > Time.now - TARBALL_CACHE_EXPIRATION command!('wget', '-q', tarball_url, '-O', tarball_path) end def tarball_path File.join(@tmp_dir, "redis-#{@redis_branch}.tar.gz") end def tarball_url "https://github.com/antirez/redis/archive/#{@redis_branch}.tar.gz" end def build FileUtils.rm_rf(@build_dir) FileUtils.mkdir_p(@build_dir) command!('tar', 'xf', tarball_path, '-C', File.expand_path('../', @build_dir)) Dir.chdir(@build_dir) do command!('make') end end def update_checksum File.write(checksum_path, checksum) end def old_checkum File.read(checksum_path) rescue Errno::ENOENT nil end def checksum_path File.join(@build_dir, 'build.checksum') end def checksum @checksum ||= Digest::SHA1.file(tarball_path).hexdigest end def command!(*args) puts "$ #{args.join(' ')}" raise "Command failed with status #{$CHILD_STATUS.exitstatus}" unless system(*args) end end exit Builder.new(ARGV[0], ARGV[1]).run redis-rb-4.2.5/bin/cluster_creator000077500000000000000000000004101375567530100171360ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require_relative '../test/support/cluster/orchestrator' urls = ARGV.map { |host_port| "redis://#{host_port}" } orchestrator = ClusterOrchestrator.new(urls, timeout: 30.0) orchestrator.rebuild orchestrator.close redis-rb-4.2.5/bin/console000077500000000000000000000002241375567530100154030ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require 'redis' require 'irb' IRB.start redis-rb-4.2.5/bors.toml000066400000000000000000000010161375567530100151050ustar00rootroot00000000000000# Gate on Travis CI status = ["continuous-integration/travis-ci/push"] # Set bors's timeout to 6 hours # # bors's timeout should always be twice a long as the test suite takes. # This is to allow Travis to fast-fail a test; if one of the builders # immediately reports a failure, then bors will move on to the next batch, # leaving the slower builders to work through the already-doomed run and # the next one. # # At the time it was written, a run had taken 3 hours. # bors's default timeout is 4 hours. timeout_sec = 14400 redis-rb-4.2.5/examples/000077500000000000000000000000001375567530100150635ustar00rootroot00000000000000redis-rb-4.2.5/examples/basic.rb000066400000000000000000000002341375567530100164700ustar00rootroot00000000000000# frozen_string_literal: true require 'redis' r = Redis.new r.del('foo') puts p 'set foo to "bar"' r['foo'] = 'bar' puts p 'value of foo' p r['foo'] redis-rb-4.2.5/examples/consistency.rb000066400000000000000000000073041375567530100177550ustar00rootroot00000000000000# frozen_string_literal: true # This file implements a simple consistency test for Redis-rb (or any other # Redis environment if you pass a different client object) where a client # writes to the database using INCR in order to increment keys, but actively # remember the value the key should have. Before every write a read is performed # to check if the value in the database matches the value expected. # # In this way this program can check for lost writes, or acknowledged writes # that were executed. # # Copyright (C) 2013-2014 Salvatore Sanfilippo # # 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. require 'redis' class ConsistencyTester def initialize(redis) @r = redis @working_set = 10_000 @keyspace = 100_000 @writes = 0 @reads = 0 @failed_writes = 0 @failed_reads = 0 @lost_writes = 0 @not_ack_writes = 0 @delay = 0 @cached = {} # We take our view of data stored in the DB. @prefix = [Process.pid.to_s, Time.now.usec, @r.object_id, ""].join("|") @errtime = {} end def genkey # Write more often to a small subset of keys ks = rand > 0.5 ? @keyspace : @working_set @prefix + "key_" + rand(ks).to_s end def check_consistency(key, value) expected = @cached[key] return unless expected # We lack info about previous state. if expected > value @lost_writes += expected - value elsif expected < value @not_ack_writes += value - expected end end def puterr(msg) puts msg if !@errtime[msg] || Time.now.to_i != @errtime[msg] @errtime[msg] = Time.now.to_i end def test last_report = Time.now.to_i loop do # Read key = genkey begin val = @r.get(key) check_consistency(key, val.to_i) @reads += 1 rescue => e puterr "Reading: #{e.class}: #{e.message} (#{e.backtrace.first})" @failed_reads += 1 end # Write begin @cached[key] = @r.incr(key).to_i @writes += 1 rescue => e puterr "Writing: #{e.class}: #{e.message} (#{e.backtrace.first})" @failed_writes += 1 end # Report sleep @delay next unless Time.now.to_i != last_report report = "#{@reads} R (#{@failed_reads} err) | " \ "#{@writes} W (#{@failed_writes} err) | " report += "#{@lost_writes} lost | " if @lost_writes > 0 report += "#{@not_ack_writes} noack | " if @not_ack_writes > 0 last_report = Time.now.to_i puts report end end end SENTINELS = [{ host: "127.0.0.1", port: 26_379 }, { host: "127.0.0.1", port: 26_380 }].freeze r = Redis.new(url: "redis://master1", sentinels: SENTINELS, role: :master) tester = ConsistencyTester.new(r) tester.test redis-rb-4.2.5/examples/dist_redis.rb000066400000000000000000000015341375567530100175440ustar00rootroot00000000000000# frozen_string_literal: true require "redis" require "redis/distributed" r = Redis::Distributed.new %w[ redis://localhost:6379 redis://localhost:6380 redis://localhost:6381 redis://localhost:6382 ] r.flushdb r['urmom'] = 'urmom' r['urdad'] = 'urdad' r['urmom1'] = 'urmom1' r['urdad1'] = 'urdad1' r['urmom2'] = 'urmom2' r['urdad2'] = 'urdad2' r['urmom3'] = 'urmom3' r['urdad3'] = 'urdad3' p r['urmom'] p r['urdad'] p r['urmom1'] p r['urdad1'] p r['urmom2'] p r['urdad2'] p r['urmom3'] p r['urdad3'] r.rpush 'listor', 'foo1' r.rpush 'listor', 'foo2' r.rpush 'listor', 'foo3' r.rpush 'listor', 'foo4' r.rpush 'listor', 'foo5' p r.rpop('listor') p r.rpop('listor') p r.rpop('listor') p r.rpop('listor') p r.rpop('listor') puts "key distribution:" r.ring.nodes.each do |node| p [node.client(:getname), node.keys("*")] end r.flushdb p r.keys('*') redis-rb-4.2.5/examples/incr-decr.rb000066400000000000000000000003451375567530100172600ustar00rootroot00000000000000# frozen_string_literal: true require 'redis' r = Redis.new puts p 'incr' r.del 'counter' p r.incr('counter') p r.incr('counter') p r.incr('counter') puts p 'decr' p r.decr('counter') p r.decr('counter') p r.decr('counter') redis-rb-4.2.5/examples/list.rb000066400000000000000000000007321375567530100163650ustar00rootroot00000000000000# frozen_string_literal: true require 'rubygems' require 'redis' r = Redis.new r.del 'logs' puts p "pushing log messages into a LIST" r.rpush 'logs', 'some log message' r.rpush 'logs', 'another log message' r.rpush 'logs', 'yet another log message' r.rpush 'logs', 'also another log message' puts p 'contents of logs LIST' p r.lrange('logs', 0, -1) puts p 'Trim logs LIST to last 2 elements(easy circular buffer)' r.ltrim('logs', -2, -1) p r.lrange('logs', 0, -1) redis-rb-4.2.5/examples/pubsub.rb000066400000000000000000000015351375567530100167140ustar00rootroot00000000000000# frozen_string_literal: true require "redis" puts <<~EOS To play with this example use redis-cli from another terminal, like this: $ redis-cli publish one hello Finally force the example to exit sending the 'exit' message with: $ redis-cli publish two exit EOS redis = Redis.new trap(:INT) { puts; exit } begin redis.subscribe(:one, :two) do |on| on.subscribe do |channel, subscriptions| puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)" end on.message do |channel, message| puts "##{channel}: #{message}" redis.unsubscribe if message == "exit" end on.unsubscribe do |channel, subscriptions| puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)" end end rescue Redis::BaseConnectionError => error puts "#{error}, retrying in 1s" sleep 1 retry end redis-rb-4.2.5/examples/sentinel.rb000066400000000000000000000020051375567530100172260ustar00rootroot00000000000000# frozen_string_literal: true require 'redis' # This example creates a master-slave setup with a sentinel, then connects to # it and sends write commands in a loop. # # After 30 seconds, the master dies. You will be able to see how a new master # is elected and things continue to work as if nothing happened. # # To run this example: # # $ ruby -I./lib examples/sentinel.rb # at_exit do begin Process.kill(:INT, @redises) rescue Errno::ESRCH end Process.waitall end @redises = spawn("examples/sentinel/start") SENTINELS = [{ host: "127.0.0.1", port: 26_379 }, { host: "127.0.0.1", port: 26_380 }].freeze r = Redis.new(url: "redis://master1", sentinels: SENTINELS, role: :master) # Set keys into a loop. # # The example traps errors so that you can actually try to failover while # running the script to see redis-rb reconfiguring. (0..1_000_000).each do |i| begin r.set(i, i) $stdout.write("SET (#{i} times)\n") if i % 100 == 0 rescue $stdout.write("E") end sleep(0.01) end redis-rb-4.2.5/examples/sentinel/000077500000000000000000000000001375567530100167045ustar00rootroot00000000000000redis-rb-4.2.5/examples/sentinel/sentinel.conf000066400000000000000000000005051375567530100213740ustar00rootroot00000000000000sentinel monitor master1 127.0.0.1 6380 2 sentinel down-after-milliseconds master1 5000 sentinel failover-timeout master1 15000 sentinel parallel-syncs master1 1 sentinel monitor master2 127.0.0.1 6381 2 sentinel down-after-milliseconds master2 5000 sentinel failover-timeout master2 15000 sentinel parallel-syncs master2 1 redis-rb-4.2.5/examples/sentinel/start000077500000000000000000000025461375567530100177760ustar00rootroot00000000000000#! /usr/bin/env ruby # frozen_string_literal: true # This is a helper script used together with examples/sentinel.rb # It runs two Redis masters, two slaves for each of them, and two sentinels. # After 30 seconds, the first master dies. # # You don't need to run this script yourself. Rather, use examples/sentinel.rb. require "fileutils" pids = [] at_exit do pids.each do |pid| begin Process.kill(:INT, pid) rescue Errno::ESRCH end end Process.waitall end base = __dir__ # Masters pids << spawn("redis-server --port 6380 --loglevel warning") pids << spawn("redis-server --port 6381 --loglevel warning") # Slaves of Master 1 pids << spawn("redis-server --port 63800 --slaveof 127.0.0.1 6380 --loglevel warning") pids << spawn("redis-server --port 63801 --slaveof 127.0.0.1 6380 --loglevel warning") # Slaves of Master 2 pids << spawn("redis-server --port 63810 --slaveof 127.0.0.1 6381 --loglevel warning") pids << spawn("redis-server --port 63811 --slaveof 127.0.0.1 6381 --loglevel warning") FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel1.conf") FileUtils.cp(File.join(base, "sentinel.conf"), "tmp/sentinel2.conf") # Sentinels pids << spawn("redis-server tmp/sentinel1.conf --sentinel --port 26379") pids << spawn("redis-server tmp/sentinel2.conf --sentinel --port 26380") sleep 30 Process.kill(:KILL, pids[0]) Process.waitall redis-rb-4.2.5/examples/sets.rb000066400000000000000000000010261375567530100163650ustar00rootroot00000000000000# frozen_string_literal: true require 'rubygems' require 'redis' r = Redis.new r.del 'foo-tags' r.del 'bar-tags' puts p "create a set of tags on foo-tags" r.sadd 'foo-tags', 'one' r.sadd 'foo-tags', 'two' r.sadd 'foo-tags', 'three' puts p "create a set of tags on bar-tags" r.sadd 'bar-tags', 'three' r.sadd 'bar-tags', 'four' r.sadd 'bar-tags', 'five' puts p 'foo-tags' p r.smembers('foo-tags') puts p 'bar-tags' p r.smembers('bar-tags') puts p 'intersection of foo-tags and bar-tags' p r.sinter('foo-tags', 'bar-tags') redis-rb-4.2.5/examples/unicorn/000077500000000000000000000000001375567530100165405ustar00rootroot00000000000000redis-rb-4.2.5/examples/unicorn/config.ru000066400000000000000000000001741375567530100203570ustar00rootroot00000000000000# frozen_string_literal: true run lambda { |_env| [200, { "Content-Type" => "text/plain" }, [Redis.current.randomkey]] } redis-rb-4.2.5/examples/unicorn/unicorn.rb000066400000000000000000000010601375567530100205370ustar00rootroot00000000000000# frozen_string_literal: true require "redis" worker_processes 3 # If you set the connection to Redis *before* forking, # you will cause forks to share a file descriptor. # # This causes a concurrency problem by which one fork # can read or write to the socket while others are # performing other operations. # # Most likely you'll be getting ProtocolError exceptions # mentioning a wrong initial byte in the reply. # # Thus we need to connect to Redis after forking the # worker processes. after_fork do |_server, _worker| Redis.current.disconnect! end redis-rb-4.2.5/lib/000077500000000000000000000000001375567530100140135ustar00rootroot00000000000000redis-rb-4.2.5/lib/redis.rb000066400000000000000000003157361375567530100154650ustar00rootroot00000000000000# frozen_string_literal: true require "monitor" require_relative "redis/errors" class Redis class << self attr_reader :exists_returns_integer def exists_returns_integer=(value) unless value message = "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \ "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \ "`exists?` instead." ::Kernel.warn(message) end @exists_returns_integer = value end attr_writer :current end def self.current @current ||= Redis.new end include MonitorMixin # Create a new client instance # # @param [Hash] options # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection: # `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket # connection: `unix://[path to Redis socket]`. This overrides all other options. # @option options [String] :host ("127.0.0.1") server hostname # @option options [Integer] :port (6379) server port # @option options [String] :path path to server socket (overrides host and port) # @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 [String] :password Password to authenticate against server # @option options [Integer] :db (0) Database to select after initial connect # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony` # @option options [String] :id ID for the client connection, assigns name to current connection by sending # `CLIENT SETNAME` # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer # @option options [Integer] :reconnect_attempts Number of attempts trying to connect # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not # @option options [Array] :sentinels List of sentinels to contact # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave` # @option options [Array String, Integer}>] :cluster List of cluster nodes to contact # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not # @option options [Class] :connector Class of custom connector # # @return [Redis] a new client instance def initialize(options = {}) @options = options.dup @cluster_mode = options.key?(:cluster) client = @cluster_mode ? Cluster : Client @original_client = @client = client.new(options) @queue = Hash.new { |h, k| h[k] = [] } super() # Monitor#initialize end def synchronize mon_synchronize { yield(@client) } end # Run code with the client reconnecting def with_reconnect(val = true, &blk) synchronize do |client| client.with_reconnect(val, &blk) end end # Run code without the client reconnecting def without_reconnect(&blk) with_reconnect(false, &blk) end # Test whether or not the client is connected def connected? @original_client.connected? end # Disconnect the client as quickly and silently as possible. def close @original_client.disconnect end alias disconnect! close # Sends a command to Redis and returns its reply. # # Replies are converted to Ruby objects according to the RESP protocol, so # you can expect a Ruby array, integer or nil when Redis sends one. Higher # level transformations, such as converting an array of pairs into a Ruby # hash, are up to consumers. # # Redis error replies are raised as Ruby exceptions. def call(*command) synchronize do |client| client.call(command) end end # Queues a command for pipelining. # # Commands in the queue are executed with the Redis#commit method. # # See http://redis.io/topics/pipelining for more details. # def queue(*command) synchronize do @queue[Thread.current.object_id] << command end end # Sends all commands in the queue. # # See http://redis.io/topics/pipelining for more details. # def commit synchronize do |client| begin pipeline = Pipeline.new(client) @queue[Thread.current.object_id].each do |command| pipeline.call(command) end client.call_pipelined(pipeline) ensure @queue.delete(Thread.current.object_id) end end end def _client @client end # Authenticate to the server. # # @param [String] password must match the password specified in the # `requirepass` directive in the configuration file # @return [String] `OK` def auth(password) synchronize do |client| client.call([:auth, password]) end end # Change the selected database for the current connection. # # @param [Integer] db zero-based index of the DB to use (0 to 15) # @return [String] `OK` def select(db) synchronize do |client| client.db = db client.call([:select, db]) end end # Ping the server. # # @param [optional, String] message # @return [String] `PONG` def ping(message = nil) synchronize do |client| client.call([:ping, message].compact) end end # Echo the given string. # # @param [String] value # @return [String] def echo(value) synchronize do |client| client.call([:echo, value]) end end # Close the connection. # # @return [String] `OK` def quit synchronize do |client| begin client.call([:quit]) rescue ConnectionError ensure client.disconnect end end end # Asynchronously rewrite the append-only file. # # @return [String] `OK` def bgrewriteaof synchronize do |client| client.call([:bgrewriteaof]) end end # Asynchronously save the dataset to disk. # # @return [String] `OK` def bgsave synchronize do |client| client.call([:bgsave]) end end # Get or set server configuration parameters. # # @param [Symbol] action e.g. `:get`, `:set`, `:resetstat` # @return [String, Hash] string reply, or hash when retrieving more than one # property with `CONFIG GET` def config(action, *args) synchronize do |client| client.call([:config, action] + args) do |reply| if reply.is_a?(Array) && action == :get Hashify.call(reply) else reply end end end end # Manage client connections. # # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname` # @return [String, Hash] depends on subcommand def client(subcommand = nil, *args) synchronize do |client| client.call([:client, subcommand] + args) do |reply| if subcommand.to_s == "list" reply.lines.map do |line| entries = line.chomp.split(/[ =]/) Hash[entries.each_slice(2).to_a] end else reply end end end end # Return the number of keys in the selected database. # # @return [Integer] def dbsize synchronize do |client| client.call([:dbsize]) end end def debug(*args) synchronize do |client| client.call([:debug] + args) end end # Remove all keys from all databases. # # @param [Hash] options # - `:async => Boolean`: async flush (default: false) # @return [String] `OK` def flushall(options = nil) synchronize do |client| if options && options[:async] client.call(%i[flushall async]) else client.call([:flushall]) end end end # Remove all keys from the current database. # # @param [Hash] options # - `:async => Boolean`: async flush (default: false) # @return [String] `OK` def flushdb(options = nil) synchronize do |client| if options && options[:async] client.call(%i[flushdb async]) else client.call([:flushdb]) end end end # Get information and statistics about the server. # # @param [String, Symbol] cmd e.g. "commandstats" # @return [Hash] def info(cmd = nil) synchronize do |client| client.call([:info, cmd].compact) do |reply| if reply.is_a?(String) reply = HashifyInfo.call(reply) if cmd && cmd.to_s == "commandstats" # Extract nested hashes for INFO COMMANDSTATS reply = Hash[reply.map do |k, v| v = v.split(",").map { |e| e.split("=") } [k[/^cmdstat_(.*)$/, 1], Hash[v]] end] end end reply end end end # Get the UNIX time stamp of the last successful save to disk. # # @return [Integer] def lastsave synchronize do |client| client.call([:lastsave]) end end # Listen for all requests received by the server in real time. # # There is no way to interrupt this command. # # @yield a block to be called for every line of output # @yieldparam [String] line timestamp and command that was executed def monitor(&block) synchronize do |client| client.call_loop([:monitor], &block) end end # Synchronously save the dataset to disk. # # @return [String] def save synchronize do |client| client.call([:save]) end end # Synchronously save the dataset to disk and then shut down the server. def shutdown synchronize do |client| client.with_reconnect(false) do begin client.call([:shutdown]) rescue ConnectionError # This means Redis has probably exited. nil end end end end # Make the server a slave of another instance, or promote it as master. def slaveof(host, port) synchronize do |client| client.call([:slaveof, host, port]) end end # Interact with the slowlog (get, len, reset) # # @param [String] subcommand e.g. `get`, `len`, `reset` # @param [Integer] length maximum number of entries to return # @return [Array, Integer, String] depends on subcommand def slowlog(subcommand, length = nil) synchronize do |client| args = [:slowlog, subcommand] args << length if length client.call args end end # Internal command used for replication. def sync synchronize do |client| client.call([:sync]) end end # Return the server time. # # @example # r.time # => [ 1333093196, 606806 ] # # @return [Array] tuple of seconds since UNIX epoch and # microseconds in the current second def time synchronize do |client| client.call([:time]) do |reply| reply&.map(&:to_i) end end end # Remove the expiration from a key. # # @param [String] key # @return [Boolean] whether the timeout was removed or not def persist(key) synchronize do |client| client.call([:persist, key], &Boolify) end end # Set a key's time to live in seconds. # # @param [String] key # @param [Integer] seconds time to live # @return [Boolean] whether the timeout was set or not def expire(key, seconds) synchronize do |client| client.call([:expire, key, seconds], &Boolify) end end # Set the expiration for a key as a UNIX timestamp. # # @param [String] key # @param [Integer] unix_time expiry time specified as a UNIX timestamp # @return [Boolean] whether the timeout was set or not def expireat(key, unix_time) synchronize do |client| client.call([:expireat, key, unix_time], &Boolify) end end # Get the time to live (in seconds) for a key. # # @param [String] key # @return [Integer] remaining time to live in seconds. # # In Redis 2.6 or older the command returns -1 if the key does not exist or if # the key exist but has no associated expire. # # Starting with Redis 2.8 the return value in case of error changed: # # - The command returns -2 if the key does not exist. # - The command returns -1 if the key exists but has no associated expire. def ttl(key) synchronize do |client| client.call([:ttl, key]) end end # Set a key's time to live in milliseconds. # # @param [String] key # @param [Integer] milliseconds time to live # @return [Boolean] whether the timeout was set or not def pexpire(key, milliseconds) synchronize do |client| client.call([:pexpire, key, milliseconds], &Boolify) end end # Set the expiration for a key as number of milliseconds from UNIX Epoch. # # @param [String] key # @param [Integer] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch. # @return [Boolean] whether the timeout was set or not def pexpireat(key, ms_unix_time) synchronize do |client| client.call([:pexpireat, key, ms_unix_time], &Boolify) end end # Get the time to live (in milliseconds) for a key. # # @param [String] key # @return [Integer] remaining time to live in milliseconds # In Redis 2.6 or older the command returns -1 if the key does not exist or if # the key exist but has no associated expire. # # Starting with Redis 2.8 the return value in case of error changed: # # - The command returns -2 if the key does not exist. # - The command returns -1 if the key exists but has no associated expire. def pttl(key) synchronize do |client| client.call([:pttl, key]) end end # Return a serialized version of the value stored at a key. # # @param [String] key # @return [String] serialized_value def dump(key) synchronize do |client| client.call([:dump, key]) end end # Create a key using the serialized value, previously obtained using DUMP. # # @param [String] key # @param [String] ttl # @param [String] serialized_value # @param [Hash] options # - `:replace => Boolean`: if false, raises an error if key already exists # @raise [Redis::CommandError] # @return [String] `"OK"` def restore(key, ttl, serialized_value, replace: nil) args = [:restore, key, ttl, serialized_value] args << 'REPLACE' if replace synchronize do |client| client.call(args) end end # Transfer a key from the connected instance to another instance. # # @param [String, Array] key # @param [Hash] options # - `:host => String`: host of instance to migrate to # - `:port => Integer`: port of instance to migrate to # - `:db => Integer`: database to migrate to (default: same as source) # - `:timeout => Integer`: timeout (default: same as connection timeout) # - `:copy => Boolean`: Do not remove the key from the local instance. # - `:replace => Boolean`: Replace existing key on the remote instance. # @return [String] `"OK"` def migrate(key, options) args = [:migrate] args << (options[:host] || raise(':host not specified')) args << (options[:port] || raise(':port not specified')) args << (key.is_a?(String) ? key : '') args << (options[:db] || @client.db).to_i args << (options[:timeout] || @client.timeout).to_i args << 'COPY' if options[:copy] args << 'REPLACE' if options[:replace] args += ['KEYS', *key] if key.is_a?(Array) synchronize { |client| client.call(args) } end # Delete one or more keys. # # @param [String, Array] keys # @return [Integer] number of keys that were deleted def del(*keys) synchronize do |client| client.call([:del] + keys) end end # Unlink one or more keys. # # @param [String, Array] keys # @return [Integer] number of keys that were unlinked def unlink(*keys) synchronize do |client| client.call([:unlink] + keys) end end # Determine how many of the keys exists. # # @param [String, Array] keys # @return [Integer] def exists(*keys) if !Redis.exists_returns_integer && keys.size == 1 if Redis.exists_returns_integer.nil? message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \ "(#{::Kernel.caller(1, 1).first})\n" ::Kernel.warn(message) end exists?(*keys) else _exists(*keys) end end def _exists(*keys) synchronize do |client| client.call([:exists, *keys]) end end # Determine if any of the keys exists. # # @param [String, Array] keys # @return [Boolean] def exists?(*keys) synchronize do |client| client.call([:exists, *keys]) do |value| value > 0 end end end # Find all keys matching the given pattern. # # @param [String] pattern # @return [Array] def keys(pattern = "*") synchronize do |client| client.call([:keys, pattern]) do |reply| if reply.is_a?(String) reply.split(" ") else reply end end end end # Move a key to another database. # # @example Move a key to another database # redis.set "foo", "bar" # # => "OK" # redis.move "foo", 2 # # => true # redis.exists "foo" # # => false # redis.select 2 # # => "OK" # redis.exists "foo" # # => true # redis.get "foo" # # => "bar" # # @param [String] key # @param [Integer] db # @return [Boolean] whether the key was moved or not def move(key, db) synchronize do |client| client.call([:move, key, db], &Boolify) end end def object(*args) synchronize do |client| client.call([:object] + args) end end # Return a random key from the keyspace. # # @return [String] def randomkey synchronize do |client| client.call([:randomkey]) end end # Rename a key. If the new key already exists it is overwritten. # # @param [String] old_name # @param [String] new_name # @return [String] `OK` def rename(old_name, new_name) synchronize do |client| client.call([:rename, old_name, new_name]) end end # Rename a key, only if the new key does not exist. # # @param [String] old_name # @param [String] new_name # @return [Boolean] whether the key was renamed or not def renamenx(old_name, new_name) synchronize do |client| client.call([:renamenx, old_name, new_name], &Boolify) end end # Sort the elements in a list, set or sorted set. # # @example Retrieve the first 2 elements from an alphabetically sorted "list" # redis.sort("list", :order => "alpha", :limit => [0, 2]) # # => ["a", "b"] # @example Store an alphabetically descending list in "target" # redis.sort("list", :order => "desc alpha", :store => "target") # # => 26 # # @param [String] key # @param [Hash] options # - `:by => String`: use external key to sort elements by # - `:limit => [offset, count]`: skip `offset` elements, return a maximum # of `count` elements # - `:get => [String, Array]`: single key or array of keys to # retrieve per element in the result # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA` # - `:store => String`: key to store the result at # # @return [Array, Array>, Integer] # - when `:get` is not specified, or holds a single element, an array of elements # - when `:get` is specified, and holds more than one element, an array of # elements where every element is an array with the result for every # element specified in `:get` # - when `:store` is specified, the number of elements in the stored result def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil) args = [:sort, key] args << "BY" << by if by if limit args << "LIMIT" args.concat(limit) end get = Array(get) get.each do |item| args << "GET" << item end args.concat(order.split(" ")) if order args << "STORE" << store if store synchronize do |client| client.call(args) do |reply| if get.size > 1 && !store reply.each_slice(get.size).to_a if reply else reply end end end end # Determine the type stored at key. # # @param [String] key # @return [String] `string`, `list`, `set`, `zset`, `hash` or `none` def type(key) synchronize do |client| client.call([:type, key]) end end # Decrement the integer value of a key by one. # # @example # redis.decr("value") # # => 4 # # @param [String] key # @return [Integer] value after decrementing it def decr(key) synchronize do |client| client.call([:decr, key]) end end # Decrement the integer value of a key by the given number. # # @example # redis.decrby("value", 5) # # => 0 # # @param [String] key # @param [Integer] decrement # @return [Integer] value after decrementing it def decrby(key, decrement) synchronize do |client| client.call([:decrby, key, decrement]) end end # Increment the integer value of a key by one. # # @example # redis.incr("value") # # => 6 # # @param [String] key # @return [Integer] value after incrementing it def incr(key) synchronize do |client| client.call([:incr, key]) end end # Increment the integer value of a key by the given integer number. # # @example # redis.incrby("value", 5) # # => 10 # # @param [String] key # @param [Integer] increment # @return [Integer] value after incrementing it def incrby(key, increment) synchronize do |client| client.call([:incrby, key, increment]) end end # Increment the numeric value of a key by the given float number. # # @example # redis.incrbyfloat("value", 1.23) # # => 1.23 # # @param [String] key # @param [Float] increment # @return [Float] value after incrementing it def incrbyfloat(key, increment) synchronize do |client| client.call([:incrbyfloat, key, increment], &Floatify) end end # Set the string value of a key. # # @param [String] key # @param [String] value # @param [Hash] options # - `:ex => Integer`: Set the specified expire time, in seconds. # - `:px => Integer`: Set the specified expire time, in milliseconds. # - `:nx => true`: Only set the key if it does not already exist. # - `:xx => true`: Only set the key if it already exist. # - `:keepttl => true`: Retain the time to live associated with the key. # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true` def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil) args = [:set, key, value.to_s] args << "EX" << ex if ex args << "PX" << px if px args << "NX" if nx args << "XX" if xx args << "KEEPTTL" if keepttl synchronize do |client| if nx || xx client.call(args, &BoolifySet) else client.call(args) end end end # Set the time to live in seconds of a key. # # @param [String] key # @param [Integer] ttl # @param [String] value # @return [String] `"OK"` def setex(key, ttl, value) synchronize do |client| client.call([:setex, key, ttl, value.to_s]) end end # Set the time to live in milliseconds of a key. # # @param [String] key # @param [Integer] ttl # @param [String] value # @return [String] `"OK"` def psetex(key, ttl, value) synchronize do |client| client.call([:psetex, key, ttl, value.to_s]) end end # Set the value of a key, only if the key does not exist. # # @param [String] key # @param [String] value # @return [Boolean] whether the key was set or not def setnx(key, value) synchronize do |client| client.call([:setnx, key, value.to_s], &Boolify) end end # Set one or more values. # # @example # redis.mset("key1", "v1", "key2", "v2") # # => "OK" # # @param [Array] args array of keys and values # @return [String] `"OK"` # # @see #mapped_mset def mset(*args) synchronize do |client| client.call([:mset] + args) end end # Set one or more values. # # @example # redis.mapped_mset({ "f1" => "v1", "f2" => "v2" }) # # => "OK" # # @param [Hash] hash keys mapping to values # @return [String] `"OK"` # # @see #mset def mapped_mset(hash) mset(hash.to_a.flatten) end # Set one or more values, only if none of the keys exist. # # @example # redis.msetnx("key1", "v1", "key2", "v2") # # => true # # @param [Array] args array of keys and values # @return [Boolean] whether or not all values were set # # @see #mapped_msetnx def msetnx(*args) synchronize do |client| client.call([:msetnx, *args], &Boolify) end end # Set one or more values, only if none of the keys exist. # # @example # redis.mapped_msetnx({ "key1" => "v1", "key2" => "v2" }) # # => true # # @param [Hash] hash keys mapping to values # @return [Boolean] whether or not all values were set # # @see #msetnx def mapped_msetnx(hash) msetnx(hash.to_a.flatten) end # Get the value of a key. # # @param [String] key # @return [String] def get(key) synchronize do |client| client.call([:get, key]) end end # Get the values of all the given keys. # # @example # redis.mget("key1", "key2") # # => ["v1", "v2"] # # @param [Array] keys # @return [Array] an array of values for the specified keys # # @see #mapped_mget def mget(*keys, &blk) synchronize do |client| client.call([:mget, *keys], &blk) end end # Get the values of all the given keys. # # @example # redis.mapped_mget("key1", "key2") # # => { "key1" => "v1", "key2" => "v2" } # # @param [Array] keys array of keys # @return [Hash] a hash mapping the specified keys to their values # # @see #mget def mapped_mget(*keys) mget(*keys) do |reply| if reply.is_a?(Array) Hash[keys.zip(reply)] else reply end end end # Overwrite part of a string at key starting at the specified offset. # # @param [String] key # @param [Integer] offset byte offset # @param [String] value # @return [Integer] length of the string after it was modified def setrange(key, offset, value) synchronize do |client| client.call([:setrange, key, offset, value.to_s]) end end # Get a substring of the string stored at a key. # # @param [String] key # @param [Integer] start zero-based start offset # @param [Integer] stop zero-based end offset. Use -1 for representing # the end of the string # @return [Integer] `0` or `1` def getrange(key, start, stop) synchronize do |client| client.call([:getrange, key, start, stop]) end end # Sets or clears the bit at offset in the string value stored at key. # # @param [String] key # @param [Integer] offset bit offset # @param [Integer] value bit value `0` or `1` # @return [Integer] the original bit value stored at `offset` def setbit(key, offset, value) synchronize do |client| client.call([:setbit, key, offset, value]) end end # Returns the bit value at offset in the string value stored at key. # # @param [String] key # @param [Integer] offset bit offset # @return [Integer] `0` or `1` def getbit(key, offset) synchronize do |client| client.call([:getbit, key, offset]) end end # Append a value to a key. # # @param [String] key # @param [String] value value to append # @return [Integer] length of the string after appending def append(key, value) synchronize do |client| client.call([:append, key, value]) end end # Count the number of set bits in a range of the string value stored at key. # # @param [String] key # @param [Integer] start start index # @param [Integer] stop stop index # @return [Integer] the number of bits set to 1 def bitcount(key, start = 0, stop = -1) synchronize do |client| client.call([:bitcount, key, start, stop]) end end # Perform a bitwise operation between strings and store the resulting string in a key. # # @param [String] operation e.g. `and`, `or`, `xor`, `not` # @param [String] destkey destination key # @param [String, Array] keys one or more source keys to perform `operation` # @return [Integer] the length of the string stored in `destkey` def bitop(operation, destkey, *keys) synchronize do |client| client.call([:bitop, operation, destkey, *keys]) end end # Return the position of the first bit set to 1 or 0 in a string. # # @param [String] key # @param [Integer] bit whether to look for the first 1 or 0 bit # @param [Integer] start start index # @param [Integer] stop stop index # @return [Integer] the position of the first 1/0 bit. # -1 if looking for 1 and it is not found or start and stop are given. def bitpos(key, bit, start = nil, stop = nil) raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start synchronize do |client| command = [:bitpos, key, bit] command << start if start command << stop if stop client.call(command) end end # Set the string value of a key and return its old value. # # @param [String] key # @param [String] value value to replace the current value with # @return [String] the old value stored in the key, or `nil` if the key # did not exist def getset(key, value) synchronize do |client| client.call([:getset, key, value.to_s]) end end # Get the length of the value stored in a key. # # @param [String] key # @return [Integer] the length of the value stored in the key, or 0 # if the key does not exist def strlen(key) synchronize do |client| client.call([:strlen, key]) end end # Get the length of a list. # # @param [String] key # @return [Integer] def llen(key) synchronize do |client| client.call([:llen, key]) end end # Prepend one or more values to a list, creating the list if it doesn't exist # # @param [String] key # @param [String, Array] value string value, or array of string values to push # @return [Integer] the length of the list after the push operation def lpush(key, value) synchronize do |client| client.call([:lpush, key, value]) end end # Prepend a value to a list, only if the list exists. # # @param [String] key # @param [String] value # @return [Integer] the length of the list after the push operation def lpushx(key, value) synchronize do |client| client.call([:lpushx, key, value]) end end # Append one or more values to a list, creating the list if it doesn't exist # # @param [String] key # @param [String, Array] value string value, or array of string values to push # @return [Integer] the length of the list after the push operation def rpush(key, value) synchronize do |client| client.call([:rpush, key, value]) end end # Append a value to a list, only if the list exists. # # @param [String] key # @param [String] value # @return [Integer] the length of the list after the push operation def rpushx(key, value) synchronize do |client| client.call([:rpushx, key, value]) end end # Remove and get the first element in a list. # # @param [String] key # @return [String] def lpop(key) synchronize do |client| client.call([:lpop, key]) end end # Remove and get the last element in a list. # # @param [String] key # @return [String] def rpop(key) synchronize do |client| client.call([:rpop, key]) end end # Remove the last element in a list, append it to another list and return it. # # @param [String] source source key # @param [String] destination destination key # @return [nil, String] the element, or nil when the source key does not exist def rpoplpush(source, destination) synchronize do |client| client.call([:rpoplpush, source, destination]) end end def _bpop(cmd, args, &blk) timeout = if args.last.is_a?(Hash) options = args.pop options[:timeout] elsif args.last.respond_to?(:to_int) # Issue deprecation notice in obnoxious mode... args.pop.to_int end timeout ||= 0 if args.size > 1 # Issue deprecation notice in obnoxious mode... end keys = args.flatten synchronize do |client| command = [cmd, keys, timeout] timeout += client.timeout if timeout > 0 client.call_with_timeout(command, timeout, &blk) end end # Remove and get the first element in a list, or block until one is available. # # @example With timeout # list, element = redis.blpop("list", :timeout => 5) # # => nil on timeout # # => ["list", "element"] on success # @example Without timeout # list, element = redis.blpop("list") # # => ["list", "element"] # @example Blocking pop on multiple lists # list, element = redis.blpop(["list", "another_list"]) # # => ["list", "element"] # # @param [String, Array] keys one or more keys to perform the # blocking pop on # @param [Hash] options # - `:timeout => Integer`: timeout in seconds, defaults to no timeout # # @return [nil, [String, String]] # - `nil` when the operation timed out # - tuple of the list that was popped from and element was popped otherwise def blpop(*args) _bpop(:blpop, args) end # Remove and get the last element in a list, or block until one is available. # # @param [String, Array] keys one or more keys to perform the # blocking pop on # @param [Hash] options # - `:timeout => Integer`: timeout in seconds, defaults to no timeout # # @return [nil, [String, String]] # - `nil` when the operation timed out # - tuple of the list that was popped from and element was popped otherwise # # @see #blpop def brpop(*args) _bpop(:brpop, args) end # Pop a value from a list, push it to another list and return it; or block # until one is available. # # @param [String] source source key # @param [String] destination destination key # @param [Hash] options # - `:timeout => Integer`: timeout in seconds, defaults to no timeout # # @return [nil, String] # - `nil` when the operation timed out # - the element was popped and pushed otherwise def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout) synchronize do |client| command = [:brpoplpush, source, destination, timeout] timeout += client.timeout if timeout > 0 client.call_with_timeout(command, timeout) end end # Get an element from a list by its index. # # @param [String] key # @param [Integer] index # @return [String] def lindex(key, index) synchronize do |client| client.call([:lindex, key, index]) end end # Insert an element before or after another element in a list. # # @param [String] key # @param [String, Symbol] where `BEFORE` or `AFTER` # @param [String] pivot reference element # @param [String] value # @return [Integer] length of the list after the insert operation, or `-1` # when the element `pivot` was not found def linsert(key, where, pivot, value) synchronize do |client| client.call([:linsert, key, where, pivot, value]) end end # Get a range of elements from a list. # # @param [String] key # @param [Integer] start start index # @param [Integer] stop stop index # @return [Array] def lrange(key, start, stop) synchronize do |client| client.call([:lrange, key, start, stop]) end end # Remove elements from a list. # # @param [String] key # @param [Integer] count number of elements to remove. Use a positive # value to remove the first `count` occurrences of `value`. A negative # value to remove the last `count` occurrences of `value`. Or zero, to # remove all occurrences of `value` from the list. # @param [String] value # @return [Integer] the number of removed elements def lrem(key, count, value) synchronize do |client| client.call([:lrem, key, count, value]) end end # Set the value of an element in a list by its index. # # @param [String] key # @param [Integer] index # @param [String] value # @return [String] `OK` def lset(key, index, value) synchronize do |client| client.call([:lset, key, index, value]) end end # Trim a list to the specified range. # # @param [String] key # @param [Integer] start start index # @param [Integer] stop stop index # @return [String] `OK` def ltrim(key, start, stop) synchronize do |client| client.call([:ltrim, key, start, stop]) end end # Get the number of members in a set. # # @param [String] key # @return [Integer] def scard(key) synchronize do |client| client.call([:scard, key]) end end # Add one or more members to a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Boolean, Integer] `Boolean` when a single member is specified, # holding whether or not adding the member succeeded, or `Integer` when an # array of members is specified, holding the number of members that were # successfully added def sadd(key, member) synchronize do |client| client.call([:sadd, key, member]) do |reply| if member.is_a? Array # Variadic: return integer reply else # Single argument: return boolean Boolify.call(reply) end end end end # Remove one or more members from a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Boolean, Integer] `Boolean` when a single member is specified, # holding whether or not removing the member succeeded, or `Integer` when an # array of members is specified, holding the number of members that were # successfully removed def srem(key, member) synchronize do |client| client.call([:srem, key, member]) do |reply| if member.is_a? Array # Variadic: return integer reply else # Single argument: return boolean Boolify.call(reply) end end end end # Remove and return one or more random member from a set. # # @param [String] key # @return [String] # @param [Integer] count def spop(key, count = nil) synchronize do |client| if count.nil? client.call([:spop, key]) else client.call([:spop, key, count]) end end end # Get one or more random members from a set. # # @param [String] key # @param [Integer] count # @return [String] def srandmember(key, count = nil) synchronize do |client| if count.nil? client.call([:srandmember, key]) else client.call([:srandmember, key, count]) end end end # Move a member from one set to another. # # @param [String] source source key # @param [String] destination destination key # @param [String] member member to move from `source` to `destination` # @return [Boolean] def smove(source, destination, member) synchronize do |client| client.call([:smove, source, destination, member], &Boolify) end end # Determine if a given value is a member of a set. # # @param [String] key # @param [String] member # @return [Boolean] def sismember(key, member) synchronize do |client| client.call([:sismember, key, member], &Boolify) end end # Get all the members in a set. # # @param [String] key # @return [Array] def smembers(key) synchronize do |client| client.call([:smembers, key]) end end # Subtract multiple sets. # # @param [String, Array] keys keys pointing to sets to subtract # @return [Array] members in the difference def sdiff(*keys) synchronize do |client| client.call([:sdiff, *keys]) end end # Subtract multiple sets and store the resulting set in a key. # # @param [String] destination destination key # @param [String, Array] keys keys pointing to sets to subtract # @return [Integer] number of elements in the resulting set def sdiffstore(destination, *keys) synchronize do |client| client.call([:sdiffstore, destination, *keys]) end end # Intersect multiple sets. # # @param [String, Array] keys keys pointing to sets to intersect # @return [Array] members in the intersection def sinter(*keys) synchronize do |client| client.call([:sinter, *keys]) end end # Intersect multiple sets and store the resulting set in a key. # # @param [String] destination destination key # @param [String, Array] keys keys pointing to sets to intersect # @return [Integer] number of elements in the resulting set def sinterstore(destination, *keys) synchronize do |client| client.call([:sinterstore, destination, *keys]) end end # Add multiple sets. # # @param [String, Array] keys keys pointing to sets to unify # @return [Array] members in the union def sunion(*keys) synchronize do |client| client.call([:sunion, *keys]) end end # Add multiple sets and store the resulting set in a key. # # @param [String] destination destination key # @param [String, Array] keys keys pointing to sets to unify # @return [Integer] number of elements in the resulting set def sunionstore(destination, *keys) synchronize do |client| client.call([:sunionstore, destination, *keys]) end end # Get the number of members in a sorted set. # # @example # redis.zcard("zset") # # => 4 # # @param [String] key # @return [Integer] def zcard(key) synchronize do |client| client.call([:zcard, key]) end end # Add one or more members to a sorted set, or update the score for members # that already exist. # # @example Add a single `[score, member]` pair to a sorted set # redis.zadd("zset", 32.0, "member") # @example Add an array of `[score, member]` pairs to a sorted set # redis.zadd("zset", [[32.0, "a"], [64.0, "b"]]) # # @param [String] key # @param [[Float, String], Array<[Float, String]>] args # - a single `[score, member]` pair # - an array of `[score, member]` pairs # @param [Hash] options # - `:xx => true`: Only update elements that already exist (never # add elements) # - `:nx => true`: Don't update already existing elements (always # add new elements) # - `:ch => true`: Modify the return value from the number of new # elements added, to the total number of elements changed (CH is an # abbreviation of changed); changed elements are new elements added # and elements already existing for which the score was updated # - `:incr => true`: When this option is specified ZADD acts like # ZINCRBY; only one score-element pair can be specified in this mode # # @return [Boolean, Integer, Float] # - `Boolean` when a single pair is specified, holding whether or not it was # **added** to the sorted set. # - `Integer` when an array of pairs is specified, holding the number of # pairs that were **added** to the sorted set. # - `Float` when option :incr is specified, holding the score of the member # after incrementing it. def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil) command = [:zadd, key] command << "NX" if nx command << "XX" if xx command << "CH" if ch command << "INCR" if incr synchronize do |client| if args.size == 1 && args[0].is_a?(Array) # Variadic: return float if INCR, integer if !INCR client.call(command + args[0], &(incr ? Floatify : nil)) elsif args.size == 2 # Single pair: return float if INCR, boolean if !INCR client.call(command + args, &(incr ? Floatify : Boolify)) else raise ArgumentError, "wrong number of arguments" end end end # Increment the score of a member in a sorted set. # # @example # redis.zincrby("zset", 32.0, "a") # # => 64.0 # # @param [String] key # @param [Float] increment # @param [String] member # @return [Float] score of the member after incrementing it def zincrby(key, increment, member) synchronize do |client| client.call([:zincrby, key, increment, member], &Floatify) end end # Remove one or more members from a sorted set. # # @example Remove a single member from a sorted set # redis.zrem("zset", "a") # @example Remove an array of members from a sorted set # redis.zrem("zset", ["a", "b"]) # # @param [String] key # @param [String, Array] member # - a single member # - an array of members # # @return [Boolean, Integer] # - `Boolean` when a single member is specified, holding whether or not it # was removed from the sorted set # - `Integer` when an array of pairs is specified, holding the number of # members that were removed to the sorted set def zrem(key, member) synchronize do |client| client.call([:zrem, key, member]) do |reply| if member.is_a? Array # Variadic: return integer reply else # Single argument: return boolean Boolify.call(reply) end end end end # Removes and returns up to count members with the highest scores in the sorted set stored at key. # # @example Popping a member # redis.zpopmax('zset') # #=> ['b', 2.0] # @example With count option # redis.zpopmax('zset', 2) # #=> [['b', 2.0], ['a', 1.0]] # # @params key [String] a key of the sorted set # @params count [Integer] a number of members # # @return [Array] element and score pair if count is not specified # @return [Array>] list of popped elements and scores def zpopmax(key, count = nil) synchronize do |client| members = client.call([:zpopmax, key, count].compact, &FloatifyPairs) count.to_i > 1 ? members : members.first end end # Removes and returns up to count members with the lowest scores in the sorted set stored at key. # # @example Popping a member # redis.zpopmin('zset') # #=> ['a', 1.0] # @example With count option # redis.zpopmin('zset', 2) # #=> [['a', 1.0], ['b', 2.0]] # # @params key [String] a key of the sorted set # @params count [Integer] a number of members # # @return [Array] element and score pair if count is not specified # @return [Array>] list of popped elements and scores def zpopmin(key, count = nil) synchronize do |client| members = client.call([:zpopmin, key, count].compact, &FloatifyPairs) count.to_i > 1 ? members : members.first end end # Removes and returns up to count members with the highest scores in the sorted set stored at keys, # or block until one is available. # # @example Popping a member from a sorted set # redis.bzpopmax('zset', 1) # #=> ['zset', 'b', 2.0] # @example Popping a member from multiple sorted sets # redis.bzpopmax('zset1', 'zset2', 1) # #=> ['zset1', 'b', 2.0] # # @params keys [Array] one or multiple keys of the sorted sets # @params timeout [Integer] the maximum number of seconds to block # # @return [Array] a touple of key, member and score # @return [nil] when no element could be popped and the timeout expired def bzpopmax(*args) _bpop(:bzpopmax, args) do |reply| reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply end end # Removes and returns up to count members with the lowest scores in the sorted set stored at keys, # or block until one is available. # # @example Popping a member from a sorted set # redis.bzpopmin('zset', 1) # #=> ['zset', 'a', 1.0] # @example Popping a member from multiple sorted sets # redis.bzpopmin('zset1', 'zset2', 1) # #=> ['zset1', 'a', 1.0] # # @params keys [Array] one or multiple keys of the sorted sets # @params timeout [Integer] the maximum number of seconds to block # # @return [Array] a touple of key, member and score # @return [nil] when no element could be popped and the timeout expired def bzpopmin(*args) _bpop(:bzpopmin, args) do |reply| reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply end end # Get the score associated with the given member in a sorted set. # # @example Get the score for member "a" # redis.zscore("zset", "a") # # => 32.0 # # @param [String] key # @param [String] member # @return [Float] score of the member def zscore(key, member) synchronize do |client| client.call([:zscore, key, member], &Floatify) end end # Return a range of members in a sorted set, by index. # # @example Retrieve all members from a sorted set # redis.zrange("zset", 0, -1) # # => ["a", "b"] # @example Retrieve all members and their scores from a sorted set # redis.zrange("zset", 0, -1, :with_scores => true) # # => [["a", 32.0], ["b", 64.0]] # # @param [String] key # @param [Integer] start start index # @param [Integer] stop stop index # @param [Hash] options # - `:with_scores => true`: include scores in output # # @return [Array, Array<[String, Float]>] # - when `:with_scores` is not specified, an array of members # - when `:with_scores` is specified, an array with `[member, score]` pairs def zrange(key, start, stop, withscores: false, with_scores: withscores) args = [:zrange, key, start, stop] if with_scores args << "WITHSCORES" block = FloatifyPairs end synchronize do |client| client.call(args, &block) end end # Return a range of members in a sorted set, by index, with scores ordered # from high to low. # # @example Retrieve all members from a sorted set # redis.zrevrange("zset", 0, -1) # # => ["b", "a"] # @example Retrieve all members and their scores from a sorted set # redis.zrevrange("zset", 0, -1, :with_scores => true) # # => [["b", 64.0], ["a", 32.0]] # # @see #zrange def zrevrange(key, start, stop, withscores: false, with_scores: withscores) args = [:zrevrange, key, start, stop] if with_scores args << "WITHSCORES" block = FloatifyPairs end synchronize do |client| client.call(args, &block) end end # Determine the index of a member in a sorted set. # # @param [String] key # @param [String] member # @return [Integer] def zrank(key, member) synchronize do |client| client.call([:zrank, key, member]) end end # Determine the index of a member in a sorted set, with scores ordered from # high to low. # # @param [String] key # @param [String] member # @return [Integer] def zrevrank(key, member) synchronize do |client| client.call([:zrevrank, key, member]) end end # Remove all members in a sorted set within the given indexes. # # @example Remove first 5 members # redis.zremrangebyrank("zset", 0, 4) # # => 5 # @example Remove last 5 members # redis.zremrangebyrank("zset", -5, -1) # # => 5 # # @param [String] key # @param [Integer] start start index # @param [Integer] stop stop index # @return [Integer] number of members that were removed def zremrangebyrank(key, start, stop) synchronize do |client| client.call([:zremrangebyrank, key, start, stop]) end end # Count the members, with the same score in a sorted set, within the given lexicographical range. # # @example Count members matching a # redis.zlexcount("zset", "[a", "[a\xff") # # => 1 # @example Count members matching a-z # redis.zlexcount("zset", "[a", "[z\xff") # # => 26 # # @param [String] key # @param [String] min # - inclusive minimum is specified by prefixing `(` # - exclusive minimum is specified by prefixing `[` # @param [String] max # - inclusive maximum is specified by prefixing `(` # - exclusive maximum is specified by prefixing `[` # # @return [Integer] number of members within the specified lexicographical range def zlexcount(key, min, max) synchronize do |client| client.call([:zlexcount, key, min, max]) end end # Return a range of members with the same score in a sorted set, by lexicographical ordering # # @example Retrieve members matching a # redis.zrangebylex("zset", "[a", "[a\xff") # # => ["aaren", "aarika", "abagael", "abby"] # @example Retrieve the first 2 members matching a # redis.zrangebylex("zset", "[a", "[a\xff", :limit => [0, 2]) # # => ["aaren", "aarika"] # # @param [String] key # @param [String] min # - inclusive minimum is specified by prefixing `(` # - exclusive minimum is specified by prefixing `[` # @param [String] max # - inclusive maximum is specified by prefixing `(` # - exclusive maximum is specified by prefixing `[` # @param [Hash] options # - `:limit => [offset, count]`: skip `offset` members, return a maximum of # `count` members # # @return [Array, Array<[String, Float]>] def zrangebylex(key, min, max, limit: nil) args = [:zrangebylex, key, min, max] if limit args << "LIMIT" args.concat(limit) end synchronize do |client| client.call(args) end end # Return a range of members with the same score in a sorted set, by reversed lexicographical ordering. # Apart from the reversed ordering, #zrevrangebylex is similar to #zrangebylex. # # @example Retrieve members matching a # redis.zrevrangebylex("zset", "[a", "[a\xff") # # => ["abbygail", "abby", "abagael", "aaren"] # @example Retrieve the last 2 members matching a # redis.zrevrangebylex("zset", "[a", "[a\xff", :limit => [0, 2]) # # => ["abbygail", "abby"] # # @see #zrangebylex def zrevrangebylex(key, max, min, limit: nil) args = [:zrevrangebylex, key, max, min] if limit args << "LIMIT" args.concat(limit) end synchronize do |client| client.call(args) end end # Return a range of members in a sorted set, by score. # # @example Retrieve members with score `>= 5` and `< 100` # redis.zrangebyscore("zset", "5", "(100") # # => ["a", "b"] # @example Retrieve the first 2 members with score `>= 0` # redis.zrangebyscore("zset", "0", "+inf", :limit => [0, 2]) # # => ["a", "b"] # @example Retrieve members and their scores with scores `> 5` # redis.zrangebyscore("zset", "(5", "+inf", :with_scores => true) # # => [["a", 32.0], ["b", 64.0]] # # @param [String] key # @param [String] min # - inclusive minimum score is specified verbatim # - exclusive minimum score is specified by prefixing `(` # @param [String] max # - inclusive maximum score is specified verbatim # - exclusive maximum score is specified by prefixing `(` # @param [Hash] options # - `:with_scores => true`: include scores in output # - `:limit => [offset, count]`: skip `offset` members, return a maximum of # `count` members # # @return [Array, Array<[String, Float]>] # - when `:with_scores` is not specified, an array of members # - when `:with_scores` is specified, an array with `[member, score]` pairs def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil) args = [:zrangebyscore, key, min, max] if with_scores args << "WITHSCORES" block = FloatifyPairs end if limit args << "LIMIT" args.concat(limit) end synchronize do |client| client.call(args, &block) end end # Return a range of members in a sorted set, by score, with scores ordered # from high to low. # # @example Retrieve members with score `< 100` and `>= 5` # redis.zrevrangebyscore("zset", "(100", "5") # # => ["b", "a"] # @example Retrieve the first 2 members with score `<= 0` # redis.zrevrangebyscore("zset", "0", "-inf", :limit => [0, 2]) # # => ["b", "a"] # @example Retrieve members and their scores with scores `> 5` # redis.zrevrangebyscore("zset", "+inf", "(5", :with_scores => true) # # => [["b", 64.0], ["a", 32.0]] # # @see #zrangebyscore def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil) args = [:zrevrangebyscore, key, max, min] if with_scores args << "WITHSCORES" block = FloatifyPairs end if limit args << "LIMIT" args.concat(limit) end synchronize do |client| client.call(args, &block) end end # Remove all members in a sorted set within the given scores. # # @example Remove members with score `>= 5` and `< 100` # redis.zremrangebyscore("zset", "5", "(100") # # => 2 # @example Remove members with scores `> 5` # redis.zremrangebyscore("zset", "(5", "+inf") # # => 2 # # @param [String] key # @param [String] min # - inclusive minimum score is specified verbatim # - exclusive minimum score is specified by prefixing `(` # @param [String] max # - inclusive maximum score is specified verbatim # - exclusive maximum score is specified by prefixing `(` # @return [Integer] number of members that were removed def zremrangebyscore(key, min, max) synchronize do |client| client.call([:zremrangebyscore, key, min, max]) end end # Count the members in a sorted set with scores within the given values. # # @example Count members with score `>= 5` and `< 100` # redis.zcount("zset", "5", "(100") # # => 2 # @example Count members with scores `> 5` # redis.zcount("zset", "(5", "+inf") # # => 2 # # @param [String] key # @param [String] min # - inclusive minimum score is specified verbatim # - exclusive minimum score is specified by prefixing `(` # @param [String] max # - inclusive maximum score is specified verbatim # - exclusive maximum score is specified by prefixing `(` # @return [Integer] number of members in within the specified range def zcount(key, min, max) synchronize do |client| client.call([:zcount, key, min, max]) end end # Intersect multiple sorted sets and store the resulting sorted set in a new # key. # # @example Compute the intersection of `2*zsetA` with `1*zsetB`, summing their scores # redis.zinterstore("zsetC", ["zsetA", "zsetB"], :weights => [2.0, 1.0], :aggregate => "sum") # # => 4 # # @param [String] destination destination key # @param [Array] keys source keys # @param [Hash] options # - `:weights => [Float, Float, ...]`: weights to associate with source # sorted sets # - `:aggregate => String`: aggregate function to use (sum, min, max, ...) # @return [Integer] number of elements in the resulting sorted set def zinterstore(destination, keys, weights: nil, aggregate: nil) args = [:zinterstore, destination, keys.size, *keys] if weights args << "WEIGHTS" args.concat(weights) end args << "AGGREGATE" << aggregate if aggregate synchronize do |client| client.call(args) end end # Add multiple sorted sets and store the resulting sorted set in a new key. # # @example Compute the union of `2*zsetA` with `1*zsetB`, summing their scores # redis.zunionstore("zsetC", ["zsetA", "zsetB"], :weights => [2.0, 1.0], :aggregate => "sum") # # => 8 # # @param [String] destination destination key # @param [Array] keys source keys # @param [Hash] options # - `:weights => [Float, Float, ...]`: weights to associate with source # sorted sets # - `:aggregate => String`: aggregate function to use (sum, min, max, ...) # @return [Integer] number of elements in the resulting sorted set def zunionstore(destination, keys, weights: nil, aggregate: nil) args = [:zunionstore, destination, keys.size, *keys] if weights args << "WEIGHTS" args.concat(weights) end args << "AGGREGATE" << aggregate if aggregate synchronize do |client| client.call(args) end end # Get the number of fields in a hash. # # @param [String] key # @return [Integer] number of fields in the hash def hlen(key) synchronize do |client| client.call([:hlen, key]) end end # Set one or more hash values. # # @example # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2 # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2 # # @param [String] key # @param [Array | Hash] attrs array or hash of fields and values # @return [Integer] The number of fields that were added to the hash def hset(key, *attrs) attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash) synchronize do |client| client.call([:hset, key, *attrs]) end end # Set the value of a hash field, only if the field does not exist. # # @param [String] key # @param [String] field # @param [String] value # @return [Boolean] whether or not the field was **added** to the hash def hsetnx(key, field, value) synchronize do |client| client.call([:hsetnx, key, field, value], &Boolify) end end # Set one or more hash values. # # @example # redis.hmset("hash", "f1", "v1", "f2", "v2") # # => "OK" # # @param [String] key # @param [Array] attrs array of fields and values # @return [String] `"OK"` # # @see #mapped_hmset def hmset(key, *attrs) synchronize do |client| client.call([:hmset, key] + attrs) end end # Set one or more hash values. # # @example # redis.mapped_hmset("hash", { "f1" => "v1", "f2" => "v2" }) # # => "OK" # # @param [String] key # @param [Hash] hash a non-empty hash with fields mapping to values # @return [String] `"OK"` # # @see #hmset def mapped_hmset(key, hash) hmset(key, hash.to_a.flatten) end # Get the value of a hash field. # # @param [String] key # @param [String] field # @return [String] def hget(key, field) synchronize do |client| client.call([:hget, key, field]) end end # Get the values of all the given hash fields. # # @example # redis.hmget("hash", "f1", "f2") # # => ["v1", "v2"] # # @param [String] key # @param [Array] fields array of fields # @return [Array] an array of values for the specified fields # # @see #mapped_hmget def hmget(key, *fields, &blk) synchronize do |client| client.call([:hmget, key] + fields, &blk) end end # Get the values of all the given hash fields. # # @example # redis.mapped_hmget("hash", "f1", "f2") # # => { "f1" => "v1", "f2" => "v2" } # # @param [String] key # @param [Array] fields array of fields # @return [Hash] a hash mapping the specified fields to their values # # @see #hmget def mapped_hmget(key, *fields) hmget(key, *fields) do |reply| if reply.is_a?(Array) Hash[fields.zip(reply)] else reply end end end # Delete one or more hash fields. # # @param [String] key # @param [String, Array] field # @return [Integer] the number of fields that were removed from the hash def hdel(key, *fields) synchronize do |client| client.call([:hdel, key, *fields]) end end # Determine if a hash field exists. # # @param [String] key # @param [String] field # @return [Boolean] whether or not the field exists in the hash def hexists(key, field) synchronize do |client| client.call([:hexists, key, field], &Boolify) end end # Increment the integer value of a hash field by the given integer number. # # @param [String] key # @param [String] field # @param [Integer] increment # @return [Integer] value of the field after incrementing it def hincrby(key, field, increment) synchronize do |client| client.call([:hincrby, key, field, increment]) end end # Increment the numeric value of a hash field by the given float number. # # @param [String] key # @param [String] field # @param [Float] increment # @return [Float] value of the field after incrementing it def hincrbyfloat(key, field, increment) synchronize do |client| client.call([:hincrbyfloat, key, field, increment], &Floatify) end end # Get all the fields in a hash. # # @param [String] key # @return [Array] def hkeys(key) synchronize do |client| client.call([:hkeys, key]) end end # Get all the values in a hash. # # @param [String] key # @return [Array] def hvals(key) synchronize do |client| client.call([:hvals, key]) end end # Get all the fields and values in a hash. # # @param [String] key # @return [Hash] def hgetall(key) synchronize do |client| client.call([:hgetall, key], &Hashify) end end # Post a message to a channel. def publish(channel, message) synchronize do |client| client.call([:publish, channel, message]) end end def subscribed? synchronize do |client| client.is_a? SubscribedClient end end # Listen for messages published to the given channels. def subscribe(*channels, &block) synchronize do |_client| _subscription(:subscribe, 0, channels, block) end end # Listen for messages published to the given channels. Throw a timeout error # if there is no messages for a timeout period. def subscribe_with_timeout(timeout, *channels, &block) synchronize do |_client| _subscription(:subscribe_with_timeout, timeout, channels, block) end end # Stop listening for messages posted to the given channels. def unsubscribe(*channels) synchronize do |client| raise "Can't unsubscribe if not subscribed." unless subscribed? client.unsubscribe(*channels) end end # Listen for messages published to channels matching the given patterns. def psubscribe(*channels, &block) synchronize do |_client| _subscription(:psubscribe, 0, channels, block) end end # Listen for messages published to channels matching the given patterns. # Throw a timeout error if there is no messages for a timeout period. def psubscribe_with_timeout(timeout, *channels, &block) synchronize do |_client| _subscription(:psubscribe_with_timeout, timeout, channels, block) end end # Stop listening for messages posted to channels matching the given patterns. def punsubscribe(*channels) synchronize do |client| raise "Can't unsubscribe if not subscribed." unless subscribed? client.punsubscribe(*channels) end end # Inspect the state of the Pub/Sub subsystem. # Possible subcommands: channels, numsub, numpat. def pubsub(subcommand, *args) synchronize do |client| client.call([:pubsub, subcommand] + args) end end # Watch the given keys to determine execution of the MULTI/EXEC block. # # Using a block is optional, but is necessary for thread-safety. # # An `#unwatch` is automatically issued if an exception is raised within the # block that is a subclass of StandardError and is not a ConnectionError. # # @example With a block # redis.watch("key") do # if redis.get("key") == "some value" # redis.multi do |multi| # multi.set("key", "other value") # multi.incr("counter") # end # else # redis.unwatch # end # end # # => ["OK", 6] # # @example Without a block # redis.watch("key") # # => "OK" # # @param [String, Array] keys one or more keys to watch # @return [Object] if using a block, returns the return value of the block # @return [String] if not using a block, returns `OK` # # @see #unwatch # @see #multi def watch(*keys) synchronize do |client| res = client.call([:watch, *keys]) if block_given? begin yield(self) rescue ConnectionError raise rescue StandardError unwatch raise end else res end end end # Forget about all watched keys. # # @return [String] `OK` # # @see #watch # @see #multi def unwatch synchronize do |client| client.call([:unwatch]) end end def pipelined synchronize do |prior_client| begin @client = Pipeline.new(prior_client) yield(self) prior_client.call_pipeline(@client) ensure @client = prior_client end end end # Mark the start of a transaction block. # # Passing a block is optional. # # @example With a block # redis.multi do |multi| # multi.set("key", "value") # multi.incr("counter") # end # => ["OK", 6] # # @example Without a block # redis.multi # # => "OK" # redis.set("key", "value") # # => "QUEUED" # redis.incr("counter") # # => "QUEUED" # redis.exec # # => ["OK", 6] # # @yield [multi] the commands that are called inside this block are cached # and written to the server upon returning from it # @yieldparam [Redis] multi `self` # # @return [String, Array<...>] # - when a block is not given, `OK` # - when a block is given, an array with replies # # @see #watch # @see #unwatch def multi synchronize do |prior_client| if !block_given? prior_client.call([:multi]) else begin @client = Pipeline::Multi.new(prior_client) yield(self) prior_client.call_pipeline(@client) ensure @client = prior_client end end end end # Execute all commands issued after MULTI. # # Only call this method when `#multi` was called **without** a block. # # @return [nil, Array<...>] # - when commands were not executed, `nil` # - when commands were executed, an array with their replies # # @see #multi # @see #discard def exec synchronize do |client| client.call([:exec]) end end # Discard all commands issued after MULTI. # # Only call this method when `#multi` was called **without** a block. # # @return [String] `"OK"` # # @see #multi # @see #exec def discard synchronize do |client| client.call([:discard]) end end # Control remote script registry. # # @example Load a script # sha = redis.script(:load, "return 1") # # => # @example Check if a script exists # redis.script(:exists, sha) # # => true # @example Check if multiple scripts exist # redis.script(:exists, [sha, other_sha]) # # => [true, false] # @example Flush the script registry # redis.script(:flush) # # => "OK" # @example Kill a running script # redis.script(:kill) # # => "OK" # # @param [String] subcommand e.g. `exists`, `flush`, `load`, `kill` # @param [Array] args depends on subcommand # @return [String, Boolean, Array, ...] depends on subcommand # # @see #eval # @see #evalsha def script(subcommand, *args) subcommand = subcommand.to_s.downcase if subcommand == "exists" synchronize do |client| arg = args.first client.call([:script, :exists, arg]) do |reply| reply = reply.map { |r| Boolify.call(r) } if arg.is_a?(Array) reply else reply.first end end end else synchronize do |client| client.call([:script, subcommand] + args) end end end def _eval(cmd, args) script = args.shift options = args.pop if args.last.is_a?(Hash) options ||= {} keys = args.shift || options[:keys] || [] argv = args.shift || options[:argv] || [] synchronize do |client| client.call([cmd, script, keys.length] + keys + argv) end end # Evaluate Lua script. # # @example EVAL without KEYS nor ARGV # redis.eval("return 1") # # => 1 # @example EVAL with KEYS and ARGV as array arguments # redis.eval("return { KEYS, ARGV }", ["k1", "k2"], ["a1", "a2"]) # # => [["k1", "k2"], ["a1", "a2"]] # @example EVAL with KEYS and ARGV in a hash argument # redis.eval("return { KEYS, ARGV }", :keys => ["k1", "k2"], :argv => ["a1", "a2"]) # # => [["k1", "k2"], ["a1", "a2"]] # # @param [Array] keys optional array with keys to pass to the script # @param [Array] argv optional array with arguments to pass to the script # @param [Hash] options # - `:keys => Array`: optional array with keys to pass to the script # - `:argv => Array`: optional array with arguments to pass to the script # @return depends on the script # # @see #script # @see #evalsha def eval(*args) _eval(:eval, args) end # Evaluate Lua script by its SHA. # # @example EVALSHA without KEYS nor ARGV # redis.evalsha(sha) # # => # @example EVALSHA with KEYS and ARGV as array arguments # redis.evalsha(sha, ["k1", "k2"], ["a1", "a2"]) # # => # @example EVALSHA with KEYS and ARGV in a hash argument # redis.evalsha(sha, :keys => ["k1", "k2"], :argv => ["a1", "a2"]) # # => # # @param [Array] keys optional array with keys to pass to the script # @param [Array] argv optional array with arguments to pass to the script # @param [Hash] options # - `:keys => Array`: optional array with keys to pass to the script # - `:argv => Array`: optional array with arguments to pass to the script # @return depends on the script # # @see #script # @see #eval def evalsha(*args) _eval(:evalsha, args) end def _scan(command, cursor, args, match: nil, count: nil, &block) # SSCAN/ZSCAN/HSCAN already prepend the key to +args+. args << cursor args << "MATCH" << match if match args << "COUNT" << count if count synchronize do |client| client.call([command] + args, &block) end end # Scan the keyspace # # @example Retrieve the first batch of keys # redis.scan(0) # # => ["4", ["key:21", "key:47", "key:42"]] # @example Retrieve a batch of keys matching a pattern # redis.scan(4, :match => "key:1?") # # => ["92", ["key:13", "key:18"]] # # @param [String, Integer] cursor the cursor of the iteration # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [String, Array] the next cursor and all found keys def scan(cursor, **options) _scan(:scan, cursor, [], **options) end # Scan the keyspace # # @example Retrieve all of the keys (with possible duplicates) # redis.scan_each.to_a # # => ["key:21", "key:47", "key:42"] # @example Execute block for each key matching a pattern # redis.scan_each(:match => "key:1?") {|key| puts key} # # => key:13 # # => key:18 # # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [Enumerator] an enumerator for all found keys def scan_each(**options, &block) return to_enum(:scan_each, **options) unless block_given? cursor = 0 loop do cursor, keys = scan(cursor, **options) keys.each(&block) break if cursor == "0" end end # Scan a hash # # @example Retrieve the first batch of key/value pairs in a hash # redis.hscan("hash", 0) # # @param [String, Integer] cursor the cursor of the iteration # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [String, Array<[String, String]>] the next cursor and all found keys def hscan(key, cursor, **options) _scan(:hscan, cursor, [key], **options) do |reply| [reply[0], reply[1].each_slice(2).to_a] end end # Scan a hash # # @example Retrieve all of the key/value pairs in a hash # redis.hscan_each("hash").to_a # # => [["key70", "70"], ["key80", "80"]] # # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [Enumerator] an enumerator for all found keys def hscan_each(key, **options, &block) return to_enum(:hscan_each, key, **options) unless block_given? cursor = 0 loop do cursor, values = hscan(key, cursor, **options) values.each(&block) break if cursor == "0" end end # Scan a sorted set # # @example Retrieve the first batch of key/value pairs in a hash # redis.zscan("zset", 0) # # @param [String, Integer] cursor the cursor of the iteration # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [String, Array<[String, Float]>] the next cursor and all found # members and scores def zscan(key, cursor, **options) _scan(:zscan, cursor, [key], **options) do |reply| [reply[0], FloatifyPairs.call(reply[1])] end end # Scan a sorted set # # @example Retrieve all of the members/scores in a sorted set # redis.zscan_each("zset").to_a # # => [["key70", "70"], ["key80", "80"]] # # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [Enumerator] an enumerator for all found scores and members def zscan_each(key, **options, &block) return to_enum(:zscan_each, key, **options) unless block_given? cursor = 0 loop do cursor, values = zscan(key, cursor, **options) values.each(&block) break if cursor == "0" end end # Scan a set # # @example Retrieve the first batch of keys in a set # redis.sscan("set", 0) # # @param [String, Integer] cursor the cursor of the iteration # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [String, Array] the next cursor and all found members def sscan(key, cursor, **options) _scan(:sscan, cursor, [key], **options) end # Scan a set # # @example Retrieve all of the keys in a set # redis.sscan_each("set").to_a # # => ["key1", "key2", "key3"] # # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # # @return [Enumerator] an enumerator for all keys in the set def sscan_each(key, **options, &block) return to_enum(:sscan_each, key, **options) unless block_given? cursor = 0 loop do cursor, keys = sscan(key, cursor, **options) keys.each(&block) break if cursor == "0" end end # Add one or more members to a HyperLogLog structure. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Boolean] true if at least 1 HyperLogLog internal register was altered. false otherwise. def pfadd(key, member) synchronize do |client| client.call([:pfadd, key, member], &Boolify) end end # Get the approximate cardinality of members added to HyperLogLog structure. # # If called with multiple keys, returns the approximate cardinality of the # union of the HyperLogLogs contained in the keys. # # @param [String, Array] keys # @return [Integer] def pfcount(*keys) synchronize do |client| client.call([:pfcount] + keys) end end # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of # the observed Sets of the source HyperLogLog structures. # # @param [String] dest_key destination key # @param [String, Array] source_key source key, or array of keys # @return [Boolean] def pfmerge(dest_key, *source_key) synchronize do |client| client.call([:pfmerge, dest_key, *source_key], &BoolifySet) end end # Adds the specified geospatial items (latitude, longitude, name) to the specified key # # @param [String] key # @param [Array] member arguemnts for member or members: longitude, latitude, name # @return [Integer] number of elements added to the sorted set def geoadd(key, *member) synchronize do |client| client.call([:geoadd, key, *member]) end end # Returns geohash string representing position for specified members of the specified key. # # @param [String] key # @param [String, Array] member one member or array of members # @return [Array] returns array containg geohash string if member is present, nil otherwise def geohash(key, member) synchronize do |client| client.call([:geohash, key, member]) end end # Query a sorted set representing a geospatial index to fetch members matching a # given maximum distance from a point # # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi) # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest # or the farthest to the nearest relative to the center # @param [Integer] count limit the results to the first N matching items # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information # @return [Array] may be changed with `options` def georadius(*args, **geoptions) geoarguments = _geoarguments(*args, **geoptions) synchronize do |client| client.call([:georadius, *geoarguments]) end end # Query a sorted set representing a geospatial index to fetch members matching a # given maximum distance from an already existing member # # @param [Array] args key, member, radius, unit(m|km|ft|mi) # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest # to the nearest relative to the center # @param [Integer] count limit the results to the first N matching items # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information # @return [Array] may be changed with `options` def georadiusbymember(*args, **geoptions) geoarguments = _geoarguments(*args, **geoptions) synchronize do |client| client.call([:georadiusbymember, *geoarguments]) end end # Returns longitude and latitude of members of a geospatial index # # @param [String] key # @param [String, Array] member one member or array of members # @return [Array, nil>] returns array of elements, where each # element is either array of longitude and latitude or nil def geopos(key, member) synchronize do |client| client.call([:geopos, key, member]) end end # Returns the distance between two members of a geospatial index # # @param [String ]key # @param [Array] members # @param ['m', 'km', 'mi', 'ft'] unit # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise. def geodist(key, member1, member2, unit = 'm') synchronize do |client| client.call([:geodist, key, member1, member2, unit]) end end # Returns the stream information each subcommand. # # @example stream # redis.xinfo(:stream, 'mystream') # @example groups # redis.xinfo(:groups, 'mystream') # @example consumers # redis.xinfo(:consumers, 'mystream', 'mygroup') # # @param subcommand [String] e.g. `stream` `groups` `consumers` # @param key [String] the stream key # @param group [String] the consumer group name, required if subcommand is `consumers` # # @return [Hash] information of the stream if subcommand is `stream` # @return [Array] information of the consumer groups if subcommand is `groups` # @return [Array] information of the consumers if subcommand is `consumers` def xinfo(subcommand, key, group = nil) args = [:xinfo, subcommand, key, group].compact synchronize do |client| client.call(args) do |reply| case subcommand.to_s.downcase when 'stream' then Hashify.call(reply) when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) } else reply end end end end # Add new entry to the stream. # # @example Without options # redis.xadd('mystream', f1: 'v1', f2: 'v2') # @example With options # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true) # # @param key [String] the stream key # @param entry [Hash] one or multiple field-value pairs # @param opts [Hash] several options for `XADD` command # # @option opts [String] :id the entry id, default value is `*`, it means auto generation # @option opts [Integer] :maxlen max length of entries # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not # # @return [String] the entry id def xadd(key, entry, approximate: nil, maxlen: nil, id: '*') args = [:xadd, key] if maxlen args << "MAXLEN" args << "~" if approximate args << maxlen end args << id args.concat(entry.to_a.flatten) synchronize { |client| client.call(args) } end # Trims older entries of the stream if needed. # # @example Without options # redis.xtrim('mystream', 1000) # @example With options # redis.xtrim('mystream', 1000, approximate: true) # # @param key [String] the stream key # @param mexlen [Integer] max length of entries # @param approximate [Boolean] whether to add `~` modifier of maxlen or not # # @return [Integer] the number of entries actually deleted def xtrim(key, maxlen, approximate: false) args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact synchronize { |client| client.call(args) } end # Delete entries by entry ids. # # @example With splatted entry ids # redis.xdel('mystream', '0-1', '0-2') # @example With arrayed entry ids # redis.xdel('mystream', ['0-1', '0-2']) # # @param key [String] the stream key # @param ids [Array] one or multiple entry ids # # @return [Integer] the number of entries actually deleted def xdel(key, *ids) args = [:xdel, key].concat(ids.flatten) synchronize { |client| client.call(args) } end # Fetches entries of the stream in ascending order. # # @example Without options # redis.xrange('mystream') # @example With a specific start # redis.xrange('mystream', '0-1') # @example With a specific start and end # redis.xrange('mystream', '0-1', '0-3') # @example With count options # redis.xrange('mystream', count: 10) # # @param key [String] the stream key # @param start [String] first entry id of range, default value is `-` # @param end [String] last entry id of range, default value is `+` # @param count [Integer] the number of entries as limit # # @return [Array>] the ids and entries pairs def xrange(key, start = '-', range_end = '+', count: nil) args = [:xrange, key, start, range_end] args.concat(['COUNT', count]) if count synchronize { |client| client.call(args, &HashifyStreamEntries) } end # Fetches entries of the stream in descending order. # # @example Without options # redis.xrevrange('mystream') # @example With a specific end # redis.xrevrange('mystream', '0-3') # @example With a specific end and start # redis.xrevrange('mystream', '0-3', '0-1') # @example With count options # redis.xrevrange('mystream', count: 10) # # @param key [String] the stream key # @param end [String] first entry id of range, default value is `+` # @param start [String] last entry id of range, default value is `-` # @params count [Integer] the number of entries as limit # # @return [Array>] the ids and entries pairs def xrevrange(key, range_end = '+', start = '-', count: nil) args = [:xrevrange, key, range_end, start] args.concat(['COUNT', count]) if count synchronize { |client| client.call(args, &HashifyStreamEntries) } end # Returns the number of entries inside a stream. # # @example With key # redis.xlen('mystream') # # @param key [String] the stream key # # @return [Integer] the number of entries def xlen(key) synchronize { |client| client.call([:xlen, key]) } end # Fetches entries from one or multiple streams. Optionally blocking. # # @example With a key # redis.xread('mystream', '0-0') # @example With multiple keys # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0]) # @example With count option # redis.xread('mystream', '0-0', count: 2) # @example With block option # redis.xread('mystream', '$', block: 1000) # # @param keys [Array] one or multiple stream keys # @param ids [Array] one or multiple entry ids # @param count [Integer] the number of entries as limit per stream # @param block [Integer] the number of milliseconds as blocking timeout # # @return [Hash{String => Hash{String => Hash}}] the entries def xread(keys, ids, count: nil, block: nil) args = [:xread] args << 'COUNT' << count if count args << 'BLOCK' << block.to_i if block _xread(args, keys, ids, block) end # Manages the consumer group of the stream. # # @example With `create` subcommand # redis.xgroup(:create, 'mystream', 'mygroup', '$') # @example With `setid` subcommand # redis.xgroup(:setid, 'mystream', 'mygroup', '$') # @example With `destroy` subcommand # redis.xgroup(:destroy, 'mystream', 'mygroup') # @example With `delconsumer` subcommand # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1') # # @param subcommand [String] `create` `setid` `destroy` `delconsumer` # @param key [String] the stream key # @param group [String] the consumer group name # @param id_or_consumer [String] # * the entry id or `$`, required if subcommand is `create` or `setid` # * the consumer name, required if subcommand is `delconsumer` # @param mkstream [Boolean] whether to create an empty stream automatically or not # # @return [String] `OK` if subcommand is `create` or `setid` # @return [Integer] effected count if subcommand is `destroy` or `delconsumer` def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false) args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact synchronize { |client| client.call(args) } end # Fetches a subset of the entries from one or multiple streams related with the consumer group. # Optionally blocking. # # @example With a key # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>') # @example With multiple keys # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >]) # @example With count option # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2) # @example With block option # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000) # @example With noack option # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true) # # @param group [String] the consumer group name # @param consumer [String] the consumer name # @param keys [Array] one or multiple stream keys # @param ids [Array] one or multiple entry ids # @param opts [Hash] several options for `XREADGROUP` command # # @option opts [Integer] :count the number of entries as limit # @option opts [Integer] :block the number of milliseconds as blocking timeout # @option opts [Boolean] :noack whether message loss is acceptable or not # # @return [Hash{String => Hash{String => Hash}}] the entries def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil) args = [:xreadgroup, 'GROUP', group, consumer] args << 'COUNT' << count if count args << 'BLOCK' << block.to_i if block args << 'NOACK' if noack _xread(args, keys, ids, block) end # Removes one or multiple entries from the pending entries list of a stream consumer group. # # @example With a entry id # redis.xack('mystream', 'mygroup', '1526569495631-0') # @example With splatted entry ids # redis.xack('mystream', 'mygroup', '0-1', '0-2') # @example With arrayed entry ids # redis.xack('mystream', 'mygroup', %w[0-1 0-2]) # # @param key [String] the stream key # @param group [String] the consumer group name # @param ids [Array] one or multiple entry ids # # @return [Integer] the number of entries successfully acknowledged def xack(key, group, *ids) args = [:xack, key, group].concat(ids.flatten) synchronize { |client| client.call(args) } end # Changes the ownership of a pending entry # # @example With splatted entry ids # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2') # @example With arrayed entry ids # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2]) # @example With idle option # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000) # @example With time option # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000) # @example With retrycount option # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10) # @example With force option # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true) # @example With justid option # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true) # # @param key [String] the stream key # @param group [String] the consumer group name # @param consumer [String] the consumer name # @param min_idle_time [Integer] the number of milliseconds # @param ids [Array] one or multiple entry ids # @param opts [Hash] several options for `XCLAIM` command # # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time # @option opts [Integer] :retrycount the number of retry counter # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not # # @return [Hash{String => Hash}] the entries successfully claimed # @return [Array] the entry ids successfully claimed if justid option is `true` def xclaim(key, group, consumer, min_idle_time, *ids, **opts) args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten) args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle] args.concat(['TIME', opts[:time].to_i]) if opts[:time] args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount] args << 'FORCE' if opts[:force] args << 'JUSTID' if opts[:justid] blk = opts[:justid] ? Noop : HashifyStreamEntries synchronize { |client| client.call(args, &blk) } end # Fetches not acknowledging pending entries # # @example With key and group # redis.xpending('mystream', 'mygroup') # @example With range options # redis.xpending('mystream', 'mygroup', '-', '+', 10) # @example With range and consumer options # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1') # # @param key [String] the stream key # @param group [String] the consumer group name # @param start [String] start first entry id of range # @param end [String] end last entry id of range # @param count [Integer] count the number of entries as limit # @param consumer [String] the consumer name # # @return [Hash] the summary of pending entries # @return [Array] the pending entries details if options were specified def xpending(key, group, *args) command_args = [:xpending, key, group] case args.size when 0, 3, 4 command_args.concat(args) else raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)" end summary_needed = args.empty? blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails synchronize { |client| client.call(command_args, &blk) } end # Interact with the sentinel command (masters, master, slaves, failover) # # @param [String] subcommand e.g. `masters`, `master`, `slaves` # @param [Array] args depends on subcommand # @return [Array, Hash, String] depends on subcommand def sentinel(subcommand, *args) subcommand = subcommand.to_s.downcase synchronize do |client| client.call([:sentinel, subcommand] + args) do |reply| case subcommand when "get-master-addr-by-name" reply else if reply.is_a?(Array) if reply[0].is_a?(Array) reply.map(&Hashify) else Hashify.call(reply) end else reply end end end end end # 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 # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected block = Noop unless @cluster_mode synchronize do |client| client.call([:cluster, subcommand] + args, &block) end end # Sends `ASKING` command to random node and returns its reply. # # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection # # @return [String] `'OK'` def asking synchronize { |client| client.call(%i[asking]) } end def id @original_client.id end def inspect "#" end def dup self.class.new(@options) end def connection return @original_client.connection_info if @cluster_mode { host: @original_client.host, port: @original_client.port, db: @original_client.db, id: @original_client.id, location: @original_client.location } end def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing synchronize do |client| client.call([command] + args) end end private # Commands returning 1 for true and 0 for false may be executed in a pipeline # where the method call will return nil. Propagate the nil instead of falsely # returning false. Boolify = lambda { |value| case value when 1 true when 0 false else value end } BoolifySet = lambda { |value| case value when "OK" true when nil false else value end } Hashify = lambda { |value| if value.respond_to?(:each_slice) value.each_slice(2).to_h else value end } Floatify = lambda { |value| case value when "inf" Float::INFINITY when "-inf" -Float::INFINITY when String Float(value) else value end } FloatifyPairs = lambda { |value| return value unless value.respond_to?(:each_slice) value.each_slice(2).map do |member, score| [member, Floatify.call(score)] end } HashifyInfo = lambda { |reply| lines = reply.split("\r\n").grep_v(/^(#|$)/) lines.map! { |line| line.split(':', 2) } lines.compact! lines.to_h } HashifyStreams = lambda { |reply| case reply when nil {} else reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h end } EMPTY_STREAM_RESPONSE = [nil].freeze private_constant :EMPTY_STREAM_RESPONSE HashifyStreamEntries = lambda { |reply| reply.compact.map do |entry_id, values| [entry_id, values.each_slice(2).to_h] end } HashifyStreamPendings = lambda { |reply| { 'size' => reply[0], 'min_entry_id' => reply[1], 'max_entry_id' => reply[2], 'consumers' => reply[3].nil? ? {} : reply[3].to_h } } HashifyStreamPendingDetails = lambda { |reply| reply.map do |arr| { 'entry_id' => arr[0], 'consumer' => arr[1], 'elapsed' => arr[2], 'count' => arr[3] } end } HashifyClusterNodeInfo = lambda { |str| arr = str.split(' ') { 'node_id' => arr[0], 'ip_port' => arr[1], 'flags' => arr[2].split(','), 'master_node_id' => arr[3], 'ping_sent' => arr[4], 'pong_recv' => arr[5], 'config_epoch' => arr[6], 'link_state' => arr[7], 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-')) } } HashifyClusterSlots = lambda { |reply| reply.map do |arr| first_slot, last_slot = arr[0..1] master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] } replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } } { 'start_slot' => first_slot, 'end_slot' => last_slot, 'master' => master, 'replicas' => replicas } end } HashifyClusterNodes = lambda { |reply| reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) } } HashifyClusterSlaves = lambda { |reply| reply.map { |str| HashifyClusterNodeInfo.call(str) } } Noop = ->(reply) { reply } def _geoarguments(*args, options: nil, sort: nil, count: nil) args.push sort if sort args.push 'count', count if count args.push options if options args end def _subscription(method, timeout, channels, block) return @client.call([method] + channels) if subscribed? begin original, @client = @client, SubscribedClient.new(@client) if timeout > 0 @client.send(method, timeout, *channels, &block) else @client.send(method, *channels, &block) end ensure @client = original end end def _xread(args, keys, ids, blocking_timeout_msec) keys = keys.is_a?(Array) ? keys : [keys] ids = ids.is_a?(Array) ? ids : [ids] args << 'STREAMS' args.concat(keys) args.concat(ids) synchronize do |client| if blocking_timeout_msec.nil? client.call(args, &HashifyStreams) elsif blocking_timeout_msec.to_f.zero? client.call_without_timeout(args, &HashifyStreams) else timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0 client.call_with_timeout(args, timeout, &HashifyStreams) end end end end require_relative "redis/version" require_relative "redis/connection" require_relative "redis/client" require_relative "redis/cluster" require_relative "redis/pipeline" require_relative "redis/subscribe" redis-rb-4.2.5/lib/redis/000077500000000000000000000000001375567530100151215ustar00rootroot00000000000000redis-rb-4.2.5/lib/redis/client.rb000066400000000000000000000403671375567530100167360ustar00rootroot00000000000000# frozen_string_literal: true require_relative "errors" require "socket" require "cgi" class Redis class Client # Defaults are also used for converting string keys to symbols. DEFAULTS = { url: -> { ENV["REDIS_URL"] }, scheme: "redis", host: "127.0.0.1", port: 6379, path: nil, read_timeout: nil, write_timeout: nil, connect_timeout: nil, timeout: 5.0, password: nil, db: 0, driver: nil, id: nil, tcp_keepalive: 0, reconnect_attempts: 1, reconnect_delay: 0, reconnect_delay_max: 0.5, inherit_socket: false, logger: nil, sentinels: nil, role: nil }.freeze attr_reader :options def scheme @options[:scheme] end def host @options[:host] end def port @options[:port] end def path @options[:path] end def read_timeout @options[:read_timeout] end def connect_timeout @options[:connect_timeout] end def timeout @options[:read_timeout] end def password @options[:password] end def db @options[:db] end def db=(db) @options[:db] = db.to_i end def driver @options[:driver] end def inherit_socket? @options[:inherit_socket] end attr_accessor :logger attr_reader :connection attr_reader :command_map def initialize(options = {}) @options = _parse_options(options) @reconnect = true @logger = @options[:logger] @connection = nil @command_map = {} @pending_reads = 0 @connector = if !@options[:sentinels].nil? Connector::Sentinel.new(@options) elsif options.include?(:connector) && options[:connector].respond_to?(:new) options.delete(:connector).new(@options) else Connector.new(@options) end end def connect @pid = Process.pid # Don't try to reconnect when the connection is fresh with_reconnect(false) do establish_connection call [:auth, password] if password call [:select, db] if db != 0 call [:client, :setname, @options[:id]] if @options[:id] @connector.check(self) end self end def id @options[:id] || "redis://#{location}/#{db}" end def location path || "#{host}:#{port}" end def call(command) reply = process([command]) { read } raise reply if reply.is_a?(CommandError) if block_given? yield reply else reply end end def call_loop(command, timeout = 0) error = nil result = with_socket_timeout(timeout) do process([command]) do loop do reply = read if reply.is_a?(CommandError) error = reply break else yield reply end end end end # Raise error when previous block broke out of the loop. raise error if error # Result is set to the value that the provided block used to break. result end def call_pipeline(pipeline) return [] if pipeline.futures.empty? with_reconnect pipeline.with_reconnect? do begin pipeline.finish(call_pipelined(pipeline)).tap do self.db = pipeline.db if pipeline.db end rescue ConnectionError => e return nil if pipeline.shutdown? # Assume the pipeline was sent in one piece, but execution of # SHUTDOWN caused none of the replies for commands that were executed # prior to it from coming back around. raise e end end end def call_pipelined(pipeline) return [] if pipeline.futures.empty? # The method #ensure_connected (called from #process) reconnects once on # I/O errors. To make an effort in making sure that commands are not # executed more than once, only allow reconnection before the first reply # has been read. When an error occurs after the first reply has been # read, retrying would re-execute the entire pipeline, thus re-issuing # already successfully executed commands. To circumvent this, don't retry # after the first reply has been read successfully. commands = pipeline.commands result = Array.new(commands.size) reconnect = @reconnect begin exception = nil process(commands) do pipeline.timeouts.each_with_index do |timeout, i| reply = if timeout with_socket_timeout(timeout) { read } else read end result[i] = reply @reconnect = false exception = reply if exception.nil? && reply.is_a?(CommandError) end end raise exception if exception ensure @reconnect = reconnect end result end def call_with_timeout(command, timeout, &blk) with_socket_timeout(timeout) do call(command, &blk) end rescue ConnectionError retry end def call_without_timeout(command, &blk) call_with_timeout(command, 0, &blk) end def process(commands) logging(commands) do ensure_connected do commands.each do |command| if command_map[command.first] command = command.dup command[0] = command_map[command.first] end write(command) end yield if block_given? end end end def connected? !!(connection && connection.connected?) end def disconnect connection.disconnect if connected? end alias close disconnect def reconnect disconnect connect end def io yield rescue TimeoutError => e1 # Add a message to the exception without destroying the original stack e2 = TimeoutError.new("Connection timed out") e2.set_backtrace(e1.backtrace) raise e2 rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last] end def read io do value = connection.read @pending_reads -= 1 value end end def write(command) io do @pending_reads += 1 connection.write(command) end end def with_socket_timeout(timeout) connect unless connected? original = @options[:read_timeout] begin connection.timeout = timeout @options[:read_timeout] = timeout # for reconnection yield ensure connection.timeout = self.timeout if connected? @options[:read_timeout] = original end end def without_socket_timeout(&blk) with_socket_timeout(0, &blk) end def with_reconnect(val = true) original, @reconnect = @reconnect, val yield ensure @reconnect = original end def without_reconnect(&blk) with_reconnect(false, &blk) end protected def logging(commands) return yield unless @logger&.debug? begin commands.each do |name, *args| logged_args = args.map do |a| if a.respond_to?(:inspect) then a.inspect elsif a.respond_to?(:to_s) then a.to_s else # handle poorly-behaved descendants of BasicObject klass = a.instance_exec { (class << self; self end).superclass } "\#<#{klass}:#{a.__id__}>" end end @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}") end t1 = Time.now yield ensure @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1 end end def establish_connection server = @connector.resolve.dup @options[:host] = server[:host] @options[:port] = Integer(server[:port]) if server.include?(:port) @connection = @options[:driver].connect(@options) @pending_reads = 0 rescue TimeoutError, SocketError, Errno::EADDRNOTAVAIL, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ENOENT, Errno::ETIMEDOUT, Errno::EINVAL => error raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})" end def ensure_connected disconnect if @pending_reads > 0 attempts = 0 begin attempts += 1 if connected? unless inherit_socket? || Process.pid == @pid raise InheritedError, "Tried to use a connection from a child process without reconnecting. " \ "You need to reconnect to Redis after forking " \ "or set :inherit_socket to true." end else connect end yield rescue BaseConnectionError disconnect if attempts <= @options[:reconnect_attempts] && @reconnect sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)), @options[:reconnect_delay_max]].min Kernel.sleep(sleep_t) retry else raise end rescue Exception disconnect raise end end def _parse_options(options) return options if options[:_parsed] defaults = DEFAULTS.dup options = options.dup defaults.keys.each do |key| # Fill in defaults if needed defaults[key] = defaults[key].call if defaults[key].respond_to?(:call) # Symbolize only keys that are needed options[key] = options[key.to_s] if options.key?(key.to_s) end url = options[:url] url = defaults[:url] if url.nil? # Override defaults from URL if given if url require "uri" uri = URI(url) if uri.scheme == "unix" defaults[:path] = uri.path elsif uri.scheme == "redis" || uri.scheme == "rediss" defaults[:scheme] = uri.scheme defaults[:host] = uri.host if uri.host defaults[:port] = uri.port if uri.port defaults[:password] = CGI.unescape(uri.password) if uri.password defaults[:db] = uri.path[1..-1].to_i if uri.path defaults[:role] = :master else raise ArgumentError, "invalid uri scheme '#{uri.scheme}'" end defaults[:ssl] = true if uri.scheme == "rediss" end # Use default when option is not specified or nil defaults.keys.each do |key| options[key] = defaults[key] if options[key].nil? end if options[:path] # Unix socket options[:scheme] = "unix" options.delete(:host) options.delete(:port) else # TCP socket options[:host] = options[:host].to_s options[:port] = options[:port].to_i end if options.key?(:timeout) options[:connect_timeout] ||= options[:timeout] options[:read_timeout] ||= options[:timeout] options[:write_timeout] ||= options[:timeout] end options[:connect_timeout] = Float(options[:connect_timeout]) options[:read_timeout] = Float(options[:read_timeout]) options[:write_timeout] = Float(options[:write_timeout]) options[:reconnect_attempts] = options[:reconnect_attempts].to_i options[:reconnect_delay] = options[:reconnect_delay].to_f options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f options[:db] = options[:db].to_i options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last case options[:tcp_keepalive] when Hash %i[time intvl probes].each do |key| unless options[:tcp_keepalive][key].is_a?(Integer) raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer" end end when Integer if options[:tcp_keepalive] >= 60 options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 } elsif options[:tcp_keepalive] >= 30 options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 } elsif options[:tcp_keepalive] >= 5 options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 } end end options[:_parsed] = true options end def _parse_driver(driver) driver = driver.to_s if driver.is_a?(Symbol) if driver.is_a?(String) begin require_relative "connection/#{driver}" rescue LoadError, NameError begin require "connection/#{driver}" rescue LoadError, NameError => error raise "Cannot load driver #{driver.inspect}: #{error.message}" end end driver = Connection.const_get(driver.capitalize) end driver end class Connector def initialize(options) @options = options.dup end def resolve @options end def check(client); end class Sentinel < Connector def initialize(options) super(options) @options[:db] = DEFAULTS.fetch(:db) @sentinels = @options.delete(:sentinels).dup @role = (@options[:role] || "master").to_s @master = @options[:host] end def check(client) # Check the instance is really of the role we are looking for. # We can't assume the command is supported since it was introduced # recently and this client should work with old stuff. begin role = client.call([:role])[0] rescue Redis::CommandError # Assume the test is passed if we can't get a reply from ROLE... role = @role end if role != @role client.disconnect raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}." end end def resolve result = case @role when "master" resolve_master when "slave" resolve_slave else raise ArgumentError, "Unknown instance role #{@role}" end result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.") end def sentinel_detect @sentinels.each do |sentinel| client = Client.new(@options.merge({ host: sentinel[:host] || sentinel["host"], port: sentinel[:port] || sentinel["port"], password: sentinel[:password] || sentinel["password"], reconnect_attempts: 0 })) begin if result = yield(client) # This sentinel responded. Make sure we ask it first next time. @sentinels.delete(sentinel) @sentinels.unshift(sentinel) return result end rescue BaseConnectionError ensure client.disconnect end end raise CannotConnectError, "No sentinels available." end def resolve_master sentinel_detect do |client| if reply = client.call(["sentinel", "get-master-addr-by-name", @master]) { host: reply[0], port: reply[1] } end end end def resolve_slave sentinel_detect do |client| if reply = client.call(["sentinel", "slaves", @master]) slaves = reply.map { |s| s.each_slice(2).to_h } slaves.each { |s| s['flags'] = s.fetch('flags').split(',') } slaves.reject! { |s| s.fetch('flags').include?('s_down') } if slaves.empty? raise CannotConnectError, 'No slaves available.' else slave = slaves.sample { host: slave.fetch('ip'), port: slave.fetch('port') } end end end end end end end end redis-rb-4.2.5/lib/redis/cluster.rb000066400000000000000000000214341375567530100171330ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'errors' require_relative 'client' require_relative 'cluster/command' require_relative 'cluster/command_loader' require_relative 'cluster/key_slot_converter' require_relative 'cluster/node' require_relative 'cluster/node_key' require_relative 'cluster/node_loader' require_relative 'cluster/option' require_relative 'cluster/slot' require_relative 'cluster/slot_loader' class Redis # Redis Cluster client # # @see https://github.com/antirez/redis-rb-cluster POC implementation # @see https://redis.io/topics/cluster-spec Redis Cluster specification # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial # # Copyright (C) 2013 Salvatore Sanfilippo class Cluster def initialize(options = {}) @option = Option.new(options) @node, @slot = fetch_cluster_info!(@option) @command = fetch_command_details(@node) end def id @node.map(&:id).sort.join(' ') end # db feature is disabled in cluster mode def db 0 end # db feature is disabled in cluster mode def db=(_db); end def timeout @node.first.timeout end def connected? @node.any?(&:connected?) end def disconnect @node.each(&:disconnect) true end def connection_info @node.sort_by(&:id).map do |client| { host: client.host, port: client.port, db: client.db, id: client.id, location: client.location } end end def with_reconnect(val = true, &block) try_send(@node.sample, :with_reconnect, val, &block) end def call(command, &block) send_command(command, &block) end def call_loop(command, timeout = 0, &block) node = assign_node(command) try_send(node, :call_loop, command, timeout, &block) end def call_pipeline(pipeline) node_keys, command_keys = extract_keys_in_pipeline(pipeline) raise CrossSlotPipeliningError, command_keys if node_keys.size > 1 node = find_node(node_keys.first) try_send(node, :call_pipeline, pipeline) end def call_with_timeout(command, timeout, &block) node = assign_node(command) try_send(node, :call_with_timeout, command, timeout, &block) end def call_without_timeout(command, &block) call_with_timeout(command, 0, &block) end def process(commands, &block) if commands.size == 1 && %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) && commands.first.size == 1 # Node is indeterminate. We do just a best-effort try here. @node.process_all(commands, &block) else node = assign_node(commands.first) try_send(node, :process, commands, &block) end end private def fetch_cluster_info!(option) node = Node.new(option.per_node_key) available_slots = SlotLoader.load(node) node_flags = NodeLoader.load_flags(node) option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) }) [Node.new(option.per_node_key, node_flags, option.use_replica?), Slot.new(available_slots, node_flags, option.use_replica?)] ensure node&.each(&:disconnect) end def fetch_command_details(nodes) details = CommandLoader.load(nodes) Command.new(details) end def send_command(command, &block) cmd = command.first.to_s.downcase case cmd when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save' @node.call_all(command, &block).first when 'flushall', 'flushdb' @node.call_master(command, &block).first when 'wait' then @node.call_master(command, &block).reduce(:+) when 'keys' then @node.call_slave(command, &block).flatten.sort when 'dbsize' then @node.call_slave(command, &block).reduce(:+) when 'lastsave' then @node.call_all(command, &block).sort when 'role' then @node.call_all(command, &block) when 'config' then send_config_command(command, &block) when 'client' then send_client_command(command, &block) when 'cluster' then send_cluster_command(command, &block) when 'readonly', 'readwrite', 'shutdown' raise OrchestrationCommandNotSupported, cmd when 'memory' then send_memory_command(command, &block) when 'script' then send_script_command(command, &block) when 'pubsub' then send_pubsub_command(command, &block) when 'discard', 'exec', 'multi', 'unwatch' raise AmbiguousNodeError, cmd else node = assign_node(command) try_send(node, :call, command, &block) end end def send_config_command(command, &block) case command[1].to_s.downcase when 'resetstat', 'rewrite', 'set' @node.call_all(command, &block).first else assign_node(command).call(command, &block) end end def send_memory_command(command, &block) case command[1].to_s.downcase when 'stats' then @node.call_all(command, &block) when 'purge' then @node.call_all(command, &block).first else assign_node(command).call(command, &block) end end def send_client_command(command, &block) case command[1].to_s.downcase when 'list' then @node.call_all(command, &block).flatten when 'pause', 'reply', 'setname' @node.call_all(command, &block).first else assign_node(command).call(command, &block) end end def send_cluster_command(command, &block) subcommand = command[1].to_s.downcase case subcommand when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate', 'reset', 'set-config-epoch', 'setslot' raise OrchestrationCommandNotSupported, 'cluster', subcommand when 'saveconfig' then @node.call_all(command, &block).first else assign_node(command).call(command, &block) end end def send_script_command(command, &block) case command[1].to_s.downcase when 'debug', 'kill' @node.call_all(command, &block).first when 'flush', 'load' @node.call_master(command, &block).first else assign_node(command).call(command, &block) end end def send_pubsub_command(command, &block) case command[1].to_s.downcase when 'channels' then @node.call_all(command, &block).flatten.uniq.sort when 'numsub' @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] } .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } } when 'numpat' then @node.call_all(command, &block).reduce(:+) else assign_node(command).call(command, &block) end end # @see https://redis.io/topics/cluster-spec#redirection-and-resharding # Redirection and resharding def try_send(node, method_name, *args, retry_count: 3, &block) node.public_send(method_name, *args, &block) rescue CommandError => err if err.message.start_with?('MOVED') raise if retry_count <= 0 node = assign_redirection_node(err.message) retry_count -= 1 retry elsif err.message.start_with?('ASK') raise if retry_count <= 0 node = assign_asking_node(err.message) node.call(%i[asking]) retry_count -= 1 retry else raise end rescue CannotConnectError update_cluster_info! raise end def assign_redirection_node(err_msg) _, slot, node_key = err_msg.split(' ') slot = slot.to_i @slot.put(slot, node_key) find_node(node_key) end def assign_asking_node(err_msg) _, _, node_key = err_msg.split(' ') find_node(node_key) end def assign_node(command) node_key = find_node_key(command) find_node(node_key) end def find_node_key(command) key = @command.extract_first_key(command) return if key.empty? slot = KeySlotConverter.convert(key) return unless @slot.exists?(slot) if @command.should_send_to_master?(command) @slot.find_node_key_of_master(slot) else @slot.find_node_key_of_slave(slot) end end def find_node(node_key) return @node.sample if node_key.nil? @node.find_by(node_key) rescue Node::ReloadNeeded update_cluster_info!(node_key) @node.find_by(node_key) end def update_cluster_info!(node_key = nil) unless node_key.nil? host, port = NodeKey.split(node_key) @option.add_node(host, port) end @node.map(&:disconnect) @node, @slot = fetch_cluster_info!(@option) end def extract_keys_in_pipeline(pipeline) node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?) [node_keys, command_keys] end end end redis-rb-4.2.5/lib/redis/cluster/000077500000000000000000000000001375567530100166025ustar00rootroot00000000000000redis-rb-4.2.5/lib/redis/cluster/command.rb000066400000000000000000000042471375567530100205540ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' class Redis class Cluster # Keep details about Redis commands for Redis Cluster Client. # @see https://redis.io/commands/command class Command def initialize(details) @details = pick_details(details) end def extract_first_key(command) i = determine_first_key_position(command) return '' if i == 0 key = command[i].to_s hash_tag = extract_hash_tag(key) hash_tag.empty? ? key : hash_tag end def should_send_to_master?(command) dig_details(command, :write) end def should_send_to_slave?(command) dig_details(command, :readonly) end private def pick_details(details) details.map do |command, detail| [command, { first_key_position: detail[:first], write: detail[:flags].include?('write'), readonly: detail[:flags].include?('readonly') }] end.to_h end def dig_details(command, key) name = command.first.to_s return unless @details.key?(name) @details.fetch(name).fetch(key) end def determine_first_key_position(command) case command.first.to_s.downcase when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3 when 'object' then 2 when 'memory' command[1].to_s.casecmp('usage').zero? ? 2 : 0 when 'scan', 'sscan', 'hscan', 'zscan' determine_optional_key_position(command, 'match') when 'xread', 'xreadgroup' determine_optional_key_position(command, 'streams') else dig_details(command, :first_key_position).to_i end end def determine_optional_key_position(command, option_name) idx = command.map(&:to_s).map(&:downcase).index(option_name) idx.nil? ? 0 : idx + 1 end # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags def extract_hash_tag(key) s = key.index('{') e = key.index('}', s.to_i + 1) return '' if s.nil? || e.nil? key[s + 1..e - 1] end end end end redis-rb-4.2.5/lib/redis/cluster/command_loader.rb000066400000000000000000000015061375567530100220750ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' class Redis class Cluster # Load details about Redis commands for Redis Cluster Client # @see https://redis.io/commands/command module CommandLoader module_function def load(nodes) details = {} nodes.each do |node| details = fetch_command_details(node) details.empty? ? next : break end details end def fetch_command_details(node) node.call(%i[command]).map do |reply| [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }] end.to_h rescue CannotConnectError, ConnectionError, CommandError {} # can retry on another node end private_class_method :fetch_command_details end end end redis-rb-4.2.5/lib/redis/cluster/key_slot_converter.rb000066400000000000000000000063761375567530100230630ustar00rootroot00000000000000# frozen_string_literal: true class Redis class Cluster # Key to slot converter for Redis Cluster Client # # We can test it by `CLUSTER KEYSLOT` command. # # @see https://github.com/antirez/redis-rb-cluster # Reference implementation in Ruby # @see https://redis.io/topics/cluster-spec#appendix # Reference implementation in ANSI C # @see https://redis.io/commands/cluster-keyslot # CLUSTER KEYSLOT command reference # # Copyright (C) 2013 Salvatore Sanfilippo module KeySlotConverter XMODEM_CRC16_LOOKUP = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ].freeze HASH_SLOTS = 16_384 module_function # Convert key into slot. # # @param key [String] the key of the redis command # # @return [Integer] slot number def convert(key) crc = 0 key.each_byte do |b| crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff] end crc % HASH_SLOTS end end end end redis-rb-4.2.5/lib/redis/cluster/node.rb000066400000000000000000000044741375567530100200650ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' class Redis class Cluster # Keep client list of node for Redis Cluster Client class Node include Enumerable ReloadNeeded = Class.new(StandardError) ROLE_SLAVE = 'slave' def initialize(options, node_flags = {}, with_replica = false) @with_replica = with_replica @node_flags = node_flags @clients = build_clients(options) end def each(&block) @clients.values.each(&block) end def sample @clients.values.sample end def find_by(node_key) @clients.fetch(node_key) rescue KeyError raise ReloadNeeded end def call_all(command, &block) try_map { |_, client| client.call(command, &block) }.values end def call_master(command, &block) try_map do |node_key, client| next if slave?(node_key) client.call(command, &block) end.values end def call_slave(command, &block) return call_master(command, &block) if replica_disabled? try_map do |node_key, client| next if master?(node_key) client.call(command, &block) end.values end def process_all(commands, &block) try_map { |_, client| client.process(commands, &block) }.values end private def replica_disabled? !@with_replica end def master?(node_key) !slave?(node_key) end def slave?(node_key) @node_flags[node_key] == ROLE_SLAVE end def build_clients(options) clients = options.map do |node_key, option| next if replica_disabled? && slave?(node_key) client = Client.new(option) client.call(%i[readonly]) if slave?(node_key) [node_key, client] end clients.compact.to_h end def try_map errors = {} results = {} @clients.each do |node_key, client| begin reply = yield(node_key, client) results[node_key] = reply unless reply.nil? rescue CommandError => err errors[node_key] = err next end end return results if errors.empty? raise CommandErrorCollection, errors end end end end redis-rb-4.2.5/lib/redis/cluster/node_key.rb000066400000000000000000000012011375567530100207160ustar00rootroot00000000000000# frozen_string_literal: true class Redis class Cluster # Node key's format is `:`. # It is different from node id. # Node id is internal identifying code in Redis Cluster. module NodeKey DELIMITER = ':' module_function def optionize(node_key) host, port = split(node_key) { host: host, port: port } end def split(node_key) node_key.split(DELIMITER) end def build_from_uri(uri) "#{uri.host}#{DELIMITER}#{uri.port}" end def build_from_host_port(host, port) "#{host}#{DELIMITER}#{port}" end end end end redis-rb-4.2.5/lib/redis/cluster/node_loader.rb000066400000000000000000000016341375567530100214060ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' class Redis class Cluster # Load and hashify node info for Redis Cluster Client module NodeLoader module_function def load_flags(nodes) info = {} nodes.each do |node| info = fetch_node_info(node) info.empty? ? next : break end return info unless info.empty? raise CannotConnectError, 'Redis client could not connect to any cluster nodes' end def fetch_node_info(node) node.call(%i[cluster nodes]) .split("\n") .map { |str| str.split(' ') } .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] } .to_h rescue CannotConnectError, ConnectionError, CommandError {} # can retry on another node end private_class_method :fetch_node_info end end end redis-rb-4.2.5/lib/redis/cluster/option.rb000066400000000000000000000052421375567530100204420ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' require_relative 'node_key' require 'uri' class Redis class Cluster # Keep options for Redis Cluster Client class Option DEFAULT_SCHEME = 'redis' SECURE_SCHEME = 'rediss' VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze def initialize(options) options = options.dup node_addrs = options.delete(:cluster) @node_opts = build_node_options(node_addrs) @replica = options.delete(:replica) == true add_common_node_option_if_needed(options, @node_opts, :scheme) add_common_node_option_if_needed(options, @node_opts, :password) @options = options end def per_node_key @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] } .to_h end def use_replica? @replica end def update_node(addrs) @node_opts = build_node_options(addrs) end def add_node(host, port) @node_opts << { host: host, port: port } end private def build_node_options(addrs) raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array) addrs.map { |addr| parse_node_addr(addr) } end def parse_node_addr(addr) case addr when String parse_node_url(addr) when Hash parse_node_option(addr) else raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash' end end def parse_node_url(addr) uri = URI(addr) raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme) db = uri.path.split('/')[1]&.to_i { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? } rescue URI::InvalidURIError => err raise InvalidClientOptionError, err.message end def parse_node_option(addr) addr = addr.map { |k, v| [k.to_sym, v] }.to_h if addr.values_at(:host, :port).any?(&:nil?) raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' end addr end # Redis cluster node returns only host and port information. # So we should complement additional information such as: # scheme, password and so on. def add_common_node_option_if_needed(options, node_opts, key) return options if options[key].nil? && node_opts.first[key].nil? options[key] ||= node_opts.first[key] end end end end redis-rb-4.2.5/lib/redis/cluster/slot.rb000066400000000000000000000041731375567530100201150ustar00rootroot00000000000000# frozen_string_literal: true class Redis class Cluster # Keep slot and node key map for Redis Cluster Client class Slot ROLE_SLAVE = 'slave' def initialize(available_slots, node_flags = {}, with_replica = false) @with_replica = with_replica @node_flags = node_flags @map = build_slot_node_key_map(available_slots) end def exists?(slot) @map.key?(slot) end def find_node_key_of_master(slot) return nil unless exists?(slot) @map[slot][:master] end def find_node_key_of_slave(slot) return nil unless exists?(slot) return find_node_key_of_master(slot) if replica_disabled? @map[slot][:slaves].sample end def put(slot, node_key) # Since we're sharing a hash for build_slot_node_key_map, duplicate it # if it already exists instead of preserving as-is. @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] } if master?(node_key) @map[slot][:master] = node_key elsif !@map[slot][:slaves].include?(node_key) @map[slot][:slaves] << node_key end nil end private def replica_disabled? !@with_replica end def master?(node_key) !slave?(node_key) end def slave?(node_key) @node_flags[node_key] == ROLE_SLAVE end # available_slots is mapping of node_key to list of slot ranges def build_slot_node_key_map(available_slots) by_ranges = {} available_slots.each do |node_key, slots_arr| by_ranges[slots_arr] ||= { master: nil, slaves: [] } if master?(node_key) by_ranges[slots_arr][:master] = node_key elsif !by_ranges[slots_arr][:slaves].include?(node_key) by_ranges[slots_arr][:slaves] << node_key end end by_slot = {} by_ranges.each do |slots_arr, nodes| slots_arr.each do |slots| slots.each do |slot| by_slot[slot] = nodes end end end by_slot end end end end redis-rb-4.2.5/lib/redis/cluster/slot_loader.rb000066400000000000000000000026461375567530100214460ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../errors' require_relative 'node_key' class Redis class Cluster # Load and hashify slot info for Redis Cluster Client module SlotLoader module_function def load(nodes) info = {} nodes.each do |node| info = fetch_slot_info(node) info.empty? ? next : break end return info unless info.empty? raise CannotConnectError, 'Redis client could not connect to any cluster nodes' end def fetch_slot_info(node) hash_with_default_arr = Hash.new { |h, k| h[k] = [] } node.call(%i[cluster slots]) .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) } .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] } rescue CannotConnectError, ConnectionError, CommandError {} # can retry on another node end def parse_slot_info(arr, default_ip:) first_slot, last_slot = arr[0..1] slot_range = (first_slot..last_slot).freeze arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] } end def stringify_node_key(arr, default_ip) ip, port = arr ip = default_ip if ip.empty? # When cluster is down NodeKey.build_from_host_port(ip, port) end private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key end end end redis-rb-4.2.5/lib/redis/connection.rb000066400000000000000000000010571375567530100176100ustar00rootroot00000000000000# frozen_string_literal: true require_relative "connection/registry" # If a connection driver was required before this file, the array # Redis::Connection.drivers will contain one or more classes. The last driver # in this array will be used as default driver. If this array is empty, we load # the plain Ruby driver as our default. Another driver can be required at a # later point in time, causing it to be the last element of the #drivers array # and therefore be chosen by default. require_relative "connection/ruby" if Redis::Connection.drivers.empty? redis-rb-4.2.5/lib/redis/connection/000077500000000000000000000000001375567530100172605ustar00rootroot00000000000000redis-rb-4.2.5/lib/redis/connection/command_helper.rb000066400000000000000000000014111375567530100225570ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Connection module CommandHelper COMMAND_DELIMITER = "\r\n" def build_command(args) command = [nil] args.each do |i| if i.is_a? Array i.each do |j| j = j.to_s command << "$#{j.bytesize}" command << j end else i = i.to_s command << "$#{i.bytesize}" command << i end end command[0] = "*#{(command.length - 1) / 2}" # Trailing delimiter command << "" command.join(COMMAND_DELIMITER) end protected def encode(string) string.force_encoding(Encoding.default_external) end end end end redis-rb-4.2.5/lib/redis/connection/hiredis.rb000066400000000000000000000032651375567530100212420ustar00rootroot00000000000000# frozen_string_literal: true require_relative "registry" require_relative "../errors" require "hiredis/connection" require "timeout" class Redis module Connection class Hiredis def self.connect(config) connection = ::Hiredis::Connection.new connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i if config[:scheme] == "unix" connection.connect_unix(config[:path], connect_timeout) elsif config[:scheme] == "rediss" || config[:ssl] raise NotImplementedError, "SSL not supported by hiredis driver" else connection.connect(config[:host], config[:port], connect_timeout) end instance = new(connection) instance.timeout = config[:read_timeout] instance rescue Errno::ETIMEDOUT raise TimeoutError end def initialize(connection) @connection = connection end def connected? @connection&.connected? end def timeout=(timeout) # Hiredis works with microsecond timeouts @connection.timeout = Integer(timeout * 1_000_000) end def disconnect @connection.disconnect @connection = nil end def write(command) @connection.write(command.flatten(1)) rescue Errno::EAGAIN raise TimeoutError end def read reply = @connection.read reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) reply rescue Errno::EAGAIN raise TimeoutError rescue RuntimeError => err raise ProtocolError, err.message end end end end Redis::Connection.drivers << Redis::Connection::Hiredis redis-rb-4.2.5/lib/redis/connection/registry.rb000066400000000000000000000006021375567530100214530ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Connection # Store a list of loaded connection drivers in the Connection module. # Redis::Client uses the last required driver by default, and will be aware # of the loaded connection drivers if the user chooses to override the # default connection driver. def self.drivers @drivers ||= [] end end end redis-rb-4.2.5/lib/redis/connection/ruby.rb000066400000000000000000000275701375567530100206010ustar00rootroot00000000000000# frozen_string_literal: true require_relative "registry" require_relative "command_helper" require_relative "../errors" require "socket" require "timeout" begin require "openssl" rescue LoadError # Not all systems have OpenSSL support end class Redis module Connection module SocketMixin CRLF = "\r\n" def initialize(*args) super(*args) @timeout = @write_timeout = nil @buffer = "".dup end def timeout=(timeout) @timeout = (timeout if timeout && timeout > 0) end def write_timeout=(timeout) @write_timeout = (timeout if timeout && timeout > 0) end def read(nbytes) result = @buffer.slice!(0, nbytes) result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes result end def gets while (crlf = @buffer.index(CRLF)).nil? @buffer << _read_from_socket(16_384) end @buffer.slice!(0, crlf + CRLF.bytesize) end def _read_from_socket(nbytes) loop do case chunk = read_nonblock(nbytes, exception: false) when :wait_readable unless wait_readable(@timeout) raise Redis::TimeoutError end when :wait_writable unless wait_writable(@timeout) raise Redis::TimeoutError end when nil raise Errno::ECONNRESET when String return chunk end end end def write(buffer) return super(buffer) unless @write_timeout bytes_to_write = buffer.bytesize total_bytes_written = 0 loop do case bytes_written = write_nonblock(buffer, exception: false) when :wait_readable unless wait_readable(@write_timeout) raise Redis::TimeoutError end when :wait_writable unless wait_writable(@write_timeout) raise Redis::TimeoutError end when nil raise Errno::ECONNRESET when Integer total_bytes_written += bytes_written if total_bytes_written >= bytes_to_write return total_bytes_written end buffer = buffer.byteslice(bytes_written..-1) end end end end if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" require "timeout" class TCPSocket < ::TCPSocket include SocketMixin def self.connect(host, port, timeout) Timeout.timeout(timeout) do sock = new(host, port) sock end rescue Timeout::Error raise TimeoutError end end if defined?(::UNIXSocket) class UNIXSocket < ::UNIXSocket include SocketMixin def self.connect(path, timeout) Timeout.timeout(timeout) do sock = new(path) sock end rescue Timeout::Error raise TimeoutError end # JRuby raises Errno::EAGAIN on #read_nonblock even when it # says it is readable (1.6.6, in both 1.8 and 1.9 mode). # Use the blocking #readpartial method instead. def _read_from_socket(nbytes) readpartial(nbytes) rescue EOFError raise Errno::ECONNRESET end end end else class TCPSocket < ::Socket include SocketMixin def self.connect_addrinfo(addrinfo, port, timeout) sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0) sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3]) begin sock.connect_nonblock(sockaddr) rescue Errno::EINPROGRESS raise TimeoutError unless sock.wait_writable(timeout) begin sock.connect_nonblock(sockaddr) rescue Errno::EISCONN end end sock end def self.connect(host, port, timeout) # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3) # # From the man page for getaddrinfo(3): # # If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4 # addresses are returned in the list pointed to by res only if the # local system has at least one IPv4 address configured, and IPv6 # addresses are returned only if the local system has at least one # IPv6 address configured. The loopback address is not considered # for this case as valid as a configured address. # # We do want the IPv6 loopback address to be returned if applicable, # even if it is the only configured IPv6 address on the machine. # Also see: https://github.com/redis/redis-rb/pull/394. addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM) # From the man page for getaddrinfo(3): # # Normally, the application should try using the addresses in the # order in which they are returned. The sorting function used # within getaddrinfo() is defined in RFC 3484 [...]. # addrinfo.each_with_index do |ai, i| begin return connect_addrinfo(ai, port, timeout) rescue SystemCallError # Raise if this was our last attempt. raise if addrinfo.length == i + 1 end end end end class UNIXSocket < ::Socket include SocketMixin def self.connect(path, timeout) sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0) sockaddr = ::Socket.pack_sockaddr_un(path) begin sock.connect_nonblock(sockaddr) rescue Errno::EINPROGRESS raise TimeoutError unless sock.wait_writable(timeout) begin sock.connect_nonblock(sockaddr) rescue Errno::EISCONN end end sock end end end if defined?(OpenSSL) class SSLSocket < ::OpenSSL::SSL::SSLSocket include SocketMixin unless method_defined?(:wait_readable) def wait_readable(timeout = nil) to_io.wait_readable(timeout) end end unless method_defined?(:wait_writable) def wait_writable(timeout = nil) to_io.wait_writable(timeout) end end def self.connect(host, port, timeout, ssl_params) # Note: this is using Redis::Connection::TCPSocket tcp_sock = TCPSocket.connect(host, port, timeout) ctx = OpenSSL::SSL::SSLContext.new # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS ctx.set_params(ssl_params || {}) ssl_sock = new(tcp_sock, ctx) ssl_sock.hostname = host begin # Initiate the socket connection in the background. If it doesn't fail # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS) # indicating the connection is in progress. # Unlike waiting for a tcp socket to connect, you can't time out ssl socket # connections during the connect phase properly, because IO.select only partially works. # Instead, you have to retry. ssl_sock.connect_nonblock rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable if ssl_sock.wait_readable(timeout) retry else raise TimeoutError end rescue IO::WaitWritable if ssl_sock.wait_writable(timeout) retry else raise TimeoutError end end unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || ( ctx.respond_to?(:verify_hostname) && !ctx.verify_hostname ) ssl_sock.post_connection_check(host) end ssl_sock end end end class Ruby include Redis::Connection::CommandHelper MINUS = "-" PLUS = "+" COLON = ":" DOLLAR = "$" ASTERISK = "*" def self.connect(config) if config[:scheme] == "unix" raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl] sock = UNIXSocket.connect(config[:path], config[:connect_timeout]) elsif config[:scheme] == "rediss" || config[:ssl] sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params]) else sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout]) end instance = new(sock) instance.timeout = config[:read_timeout] instance.write_timeout = config[:write_timeout] instance.set_tcp_keepalive config[:tcp_keepalive] instance.set_tcp_nodelay if sock.is_a? TCPSocket instance end if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } def set_tcp_keepalive(keepalive) return unless keepalive.is_a?(Hash) @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time]) @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl]) @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes]) end def get_tcp_keepalive { time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int, intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int, probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int } end else def set_tcp_keepalive(keepalive); end def get_tcp_keepalive { } end end # disables Nagle's Algorithm, prevents multiple round trips with MULTI if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c } def set_tcp_nodelay @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) end else def set_tcp_nodelay; end end def initialize(sock) @sock = sock end def connected? !!@sock end def disconnect @sock.close rescue ensure @sock = nil end def timeout=(timeout) @sock.timeout = timeout if @sock.respond_to?(:timeout=) end def write_timeout=(timeout) @sock.write_timeout = timeout end def write(command) @sock.write(build_command(command)) end def read line = @sock.gets reply_type = line.slice!(0, 1) format_reply(reply_type, line) rescue Errno::EAGAIN raise TimeoutError end def format_reply(reply_type, line) case reply_type when MINUS then format_error_reply(line) when PLUS then format_status_reply(line) when COLON then format_integer_reply(line) when DOLLAR then format_bulk_reply(line) when ASTERISK then format_multi_bulk_reply(line) else raise ProtocolError, reply_type end end def format_error_reply(line) CommandError.new(line.strip) end def format_status_reply(line) line.strip end def format_integer_reply(line) line.to_i end def format_bulk_reply(line) bulklen = line.to_i return if bulklen == -1 reply = encode(@sock.read(bulklen)) @sock.read(2) # Discard CRLF. reply end def format_multi_bulk_reply(line) n = line.to_i return if n == -1 Array.new(n) { read } end end end end Redis::Connection.drivers << Redis::Connection::Ruby redis-rb-4.2.5/lib/redis/connection/synchrony.rb000066400000000000000000000066111375567530100216450ustar00rootroot00000000000000# frozen_string_literal: true require_relative "command_helper" require_relative "registry" require_relative "../errors" require "em-synchrony" require "hiredis/reader" Kernel.warn( "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0. " \ "We're looking for people to maintain it as a separate gem, see https://github.com/redis/redis-rb/issues/915" ) class Redis module Connection class RedisClient < EventMachine::Connection include EventMachine::Deferrable attr_accessor :timeout def post_init @req = nil @connected = false @reader = ::Hiredis::Reader.new end def connection_completed @connected = true succeed end def connected? @connected end def receive_data(data) @reader.feed(data) loop do begin reply = @reader.gets rescue RuntimeError => err @req.fail [:error, ProtocolError.new(err.message)] break end break if reply == false reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) @req.succeed [:reply, reply] end end def read @req = EventMachine::DefaultDeferrable.new @req.timeout(@timeout, :timeout) if @timeout > 0 EventMachine::Synchrony.sync @req end def send(data) callback { send_data data } end def unbind @connected = false if @req @req.fail [:error, Errno::ECONNRESET] @req = nil else fail end end end class Synchrony include Redis::Connection::CommandHelper def self.connect(config) if config[:scheme] == "unix" begin conn = EventMachine.connect_unix_domain(config[:path], RedisClient) rescue RuntimeError => e if e.message == "no connection" raise Errno::ECONNREFUSED else raise e end end elsif config[:scheme] == "rediss" || config[:ssl] raise NotImplementedError, "SSL not supported by synchrony driver" else conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c| c.pending_connect_timeout = [config[:connect_timeout], 0.1].max end end fiber = Fiber.current conn.callback { fiber.resume } conn.errback { fiber.resume :refused } raise Errno::ECONNREFUSED if Fiber.yield == :refused instance = new(conn) instance.timeout = config[:read_timeout] instance end def initialize(connection) @connection = connection end def connected? @connection&.connected? end def timeout=(timeout) @connection.timeout = timeout end def disconnect @connection.close_connection @connection = nil end def write(command) @connection.send(build_command(command)) end def read type, payload = @connection.read if type == :reply payload elsif type == :error raise payload elsif type == :timeout raise TimeoutError else raise "Unknown type #{type.inspect}" end end end end end Redis::Connection.drivers << Redis::Connection::Synchrony redis-rb-4.2.5/lib/redis/distributed.rb000066400000000000000000000607121375567530100177760ustar00rootroot00000000000000# frozen_string_literal: true require_relative "hash_ring" class Redis class Distributed class CannotDistribute < RuntimeError def initialize(command) @command = command end def message "#{@command.to_s.upcase} cannot be used in Redis::Distributed because the keys involved need " \ "to be on the same server or because we cannot guarantee that the operation will be atomic." end end attr_reader :ring def initialize(node_configs, options = {}) @tag = options[:tag] || /^\{(.+?)\}/ @ring = options[:ring] || HashRing.new @node_configs = node_configs.dup @default_options = options.dup node_configs.each { |node_config| add_node(node_config) } @subscribed_node = nil @watch_key = nil end def node_for(key) key = key_tag(key.to_s) || key.to_s raise CannotDistribute, :watch if @watch_key && @watch_key != key @ring.get_node(key) end def nodes @ring.nodes end def add_node(options) options = { url: options } if options.is_a?(String) options = @default_options.merge(options) @ring.add_node Redis.new(options) end # Change the selected database for the current connection. def select(db) on_each_node :select, db end # Ping the server. def ping on_each_node :ping end # Echo the given string. def echo(value) on_each_node :echo, value end # Close the connection. def quit on_each_node :quit end # Asynchronously save the dataset to disk. def bgsave on_each_node :bgsave end # Return the number of keys in the selected database. def dbsize on_each_node :dbsize end # Remove all keys from all databases. def flushall on_each_node :flushall end # Remove all keys from the current database. def flushdb on_each_node :flushdb end # Get information and statistics about the server. def info(cmd = nil) on_each_node :info, cmd end # Get the UNIX time stamp of the last successful save to disk. def lastsave on_each_node :lastsave end # Listen for all requests received by the server in real time. def monitor raise NotImplementedError end # Synchronously save the dataset to disk. def save on_each_node :save end # Get server time: an UNIX timestamp and the elapsed microseconds in the current second. def time on_each_node :time end # Remove the expiration from a key. def persist(key) node_for(key).persist(key) end # Set a key's time to live in seconds. def expire(key, seconds) node_for(key).expire(key, seconds) end # Set the expiration for a key as a UNIX timestamp. def expireat(key, unix_time) node_for(key).expireat(key, unix_time) end # Get the time to live (in seconds) for a key. def ttl(key) node_for(key).ttl(key) end # Set a key's time to live in milliseconds. def pexpire(key, milliseconds) node_for(key).pexpire(key, milliseconds) end # Set the expiration for a key as number of milliseconds from UNIX Epoch. def pexpireat(key, ms_unix_time) node_for(key).pexpireat(key, ms_unix_time) end # Get the time to live (in milliseconds) for a key. def pttl(key) node_for(key).pttl(key) end # Return a serialized version of the value stored at a key. def dump(key) node_for(key).dump(key) end # Create a key using the serialized value, previously obtained using DUMP. def restore(key, ttl, serialized_value, **options) node_for(key).restore(key, ttl, serialized_value, **options) end # Transfer a key from the connected instance to another instance. def migrate(_key, _options) raise CannotDistribute, :migrate end # Delete a key. def del(*args) keys_per_node = args.group_by { |key| node_for(key) } keys_per_node.inject(0) do |sum, (node, keys)| sum + node.del(*keys) end end # Unlink keys. def unlink(*args) keys_per_node = args.group_by { |key| node_for(key) } keys_per_node.inject(0) do |sum, (node, keys)| sum + node.unlink(*keys) end end # Determine if a key exists. def exists(*args) if !Redis.exists_returns_integer && args.size == 1 message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \ "use `exists?` instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = true. " \ "(#{::Kernel.caller(1, 1).first})\n" if defined?(::Warning) ::Warning.warn(message) else warn(message) end exists?(*args) else keys_per_node = args.group_by { |key| node_for(key) } keys_per_node.inject(0) do |sum, (node, keys)| sum + node._exists(*keys) end end end # Determine if any of the keys exists. def exists?(*args) keys_per_node = args.group_by { |key| node_for(key) } keys_per_node.each do |node, keys| return true if node.exists?(*keys) end false end # Find all keys matching the given pattern. def keys(glob = "*") on_each_node(:keys, glob).flatten end # Move a key to another database. def move(key, db) node_for(key).move(key, db) end # Return a random key from the keyspace. def randomkey raise CannotDistribute, :randomkey end # Rename a key. def rename(old_name, new_name) ensure_same_node(:rename, [old_name, new_name]) do |node| node.rename(old_name, new_name) end end # Rename a key, only if the new key does not exist. def renamenx(old_name, new_name) ensure_same_node(:renamenx, [old_name, new_name]) do |node| node.renamenx(old_name, new_name) end end # Sort the elements in a list, set or sorted set. def sort(key, **options) keys = [key, options[:by], options[:store], *Array(options[:get])].compact ensure_same_node(:sort, keys) do |node| node.sort(key, **options) end end # Determine the type stored at key. def type(key) node_for(key).type(key) end # Decrement the integer value of a key by one. def decr(key) node_for(key).decr(key) end # Decrement the integer value of a key by the given number. def decrby(key, decrement) node_for(key).decrby(key, decrement) end # Increment the integer value of a key by one. def incr(key) node_for(key).incr(key) end # Increment the integer value of a key by the given integer number. def incrby(key, increment) node_for(key).incrby(key, increment) end # Increment the numeric value of a key by the given float number. def incrbyfloat(key, increment) node_for(key).incrbyfloat(key, increment) end # Set the string value of a key. def set(key, value, **options) node_for(key).set(key, value, **options) end # Set the time to live in seconds of a key. def setex(key, ttl, value) node_for(key).setex(key, ttl, value) end # Set the time to live in milliseconds of a key. def psetex(key, ttl, value) node_for(key).psetex(key, ttl, value) end # Set the value of a key, only if the key does not exist. def setnx(key, value) node_for(key).setnx(key, value) end # Set multiple keys to multiple values. def mset(*_args) raise CannotDistribute, :mset end def mapped_mset(_hash) raise CannotDistribute, :mapped_mset end # Set multiple keys to multiple values, only if none of the keys exist. def msetnx(*_args) raise CannotDistribute, :msetnx end def mapped_msetnx(_hash) raise CannotDistribute, :mapped_msetnx end # Get the value of a key. def get(key) node_for(key).get(key) end # Get the values of all the given keys as an Array. def mget(*keys) mapped_mget(*keys).values_at(*keys) end # Get the values of all the given keys as a Hash. def mapped_mget(*keys) keys.group_by { |k| node_for k }.inject({}) do |results, (node, subkeys)| results.merge! node.mapped_mget(*subkeys) end end # Overwrite part of a string at key starting at the specified offset. def setrange(key, offset, value) node_for(key).setrange(key, offset, value) end # Get a substring of the string stored at a key. def getrange(key, start, stop) node_for(key).getrange(key, start, stop) end # Sets or clears the bit at offset in the string value stored at key. def setbit(key, offset, value) node_for(key).setbit(key, offset, value) end # Returns the bit value at offset in the string value stored at key. def getbit(key, offset) node_for(key).getbit(key, offset) end # Append a value to a key. def append(key, value) node_for(key).append(key, value) end # Count the number of set bits in a range of the string value stored at key. def bitcount(key, start = 0, stop = -1) node_for(key).bitcount(key, start, stop) end # Perform a bitwise operation between strings and store the resulting string in a key. def bitop(operation, destkey, *keys) ensure_same_node(:bitop, [destkey] + keys) do |node| node.bitop(operation, destkey, *keys) end end # Return the position of the first bit set to 1 or 0 in a string. def bitpos(key, bit, start = nil, stop = nil) node_for(key).bitpos(key, bit, start, stop) end # Set the string value of a key and return its old value. def getset(key, value) node_for(key).getset(key, value) end # Get the length of the value stored in a key. def strlen(key) node_for(key).strlen(key) end def [](key) get(key) end def []=(key, value) set(key, value) end # Get the length of a list. def llen(key) node_for(key).llen(key) end # Prepend one or more values to a list. def lpush(key, value) node_for(key).lpush(key, value) end # Prepend a value to a list, only if the list exists. def lpushx(key, value) node_for(key).lpushx(key, value) end # Append one or more values to a list. def rpush(key, value) node_for(key).rpush(key, value) end # Append a value to a list, only if the list exists. def rpushx(key, value) node_for(key).rpushx(key, value) end # Remove and get the first element in a list. def lpop(key) node_for(key).lpop(key) end # Remove and get the last element in a list. def rpop(key) node_for(key).rpop(key) end # Remove the last element in a list, append it to another list and return # it. def rpoplpush(source, destination) ensure_same_node(:rpoplpush, [source, destination]) do |node| node.rpoplpush(source, destination) end end def _bpop(cmd, args) timeout = if args.last.is_a?(Hash) options = args.pop options[:timeout] elsif args.last.respond_to?(:to_int) # Issue deprecation notice in obnoxious mode... args.pop.to_int end if args.size > 1 # Issue deprecation notice in obnoxious mode... end keys = args.flatten ensure_same_node(cmd, keys) do |node| if timeout node.__send__(cmd, keys, timeout: timeout) else node.__send__(cmd, keys) end end end # Remove and get the first element in a list, or block until one is # available. def blpop(*args) _bpop(:blpop, args) end # Remove and get the last element in a list, or block until one is # available. def brpop(*args) _bpop(:brpop, args) end # Pop a value from a list, push it to another list and return it; or block # until one is available. def brpoplpush(source, destination, deprecated_timeout = 0, **options) ensure_same_node(:brpoplpush, [source, destination]) do |node| node.brpoplpush(source, destination, deprecated_timeout, **options) end end # Get an element from a list by its index. def lindex(key, index) node_for(key).lindex(key, index) end # Insert an element before or after another element in a list. def linsert(key, where, pivot, value) node_for(key).linsert(key, where, pivot, value) end # Get a range of elements from a list. def lrange(key, start, stop) node_for(key).lrange(key, start, stop) end # Remove elements from a list. def lrem(key, count, value) node_for(key).lrem(key, count, value) end # Set the value of an element in a list by its index. def lset(key, index, value) node_for(key).lset(key, index, value) end # Trim a list to the specified range. def ltrim(key, start, stop) node_for(key).ltrim(key, start, stop) end # Get the number of members in a set. def scard(key) node_for(key).scard(key) end # Add one or more members to a set. def sadd(key, member) node_for(key).sadd(key, member) end # Remove one or more members from a set. def srem(key, member) node_for(key).srem(key, member) end # Remove and return a random member from a set. def spop(key, count = nil) node_for(key).spop(key, count) end # Get a random member from a set. def srandmember(key, count = nil) node_for(key).srandmember(key, count) end # Move a member from one set to another. def smove(source, destination, member) ensure_same_node(:smove, [source, destination]) do |node| node.smove(source, destination, member) end end # Determine if a given value is a member of a set. def sismember(key, member) node_for(key).sismember(key, member) end # Get all the members in a set. def smembers(key) node_for(key).smembers(key) end # Scan a set def sscan(key, cursor, **options) node_for(key).sscan(key, cursor, **options) end # Scan a set and return an enumerator def sscan_each(key, **options, &block) node_for(key).sscan_each(key, **options, &block) end # Subtract multiple sets. def sdiff(*keys) ensure_same_node(:sdiff, keys) do |node| node.sdiff(*keys) end end # Subtract multiple sets and store the resulting set in a key. def sdiffstore(destination, *keys) ensure_same_node(:sdiffstore, [destination] + keys) do |node| node.sdiffstore(destination, *keys) end end # Intersect multiple sets. def sinter(*keys) ensure_same_node(:sinter, keys) do |node| node.sinter(*keys) end end # Intersect multiple sets and store the resulting set in a key. def sinterstore(destination, *keys) ensure_same_node(:sinterstore, [destination] + keys) do |node| node.sinterstore(destination, *keys) end end # Add multiple sets. def sunion(*keys) ensure_same_node(:sunion, keys) do |node| node.sunion(*keys) end end # Add multiple sets and store the resulting set in a key. def sunionstore(destination, *keys) ensure_same_node(:sunionstore, [destination] + keys) do |node| node.sunionstore(destination, *keys) end end # Get the number of members in a sorted set. def zcard(key) node_for(key).zcard(key) end # Add one or more members to a sorted set, or update the score for members # that already exist. def zadd(key, *args) node_for(key).zadd(key, *args) end ruby2_keywords(:zadd) if respond_to?(:ruby2_keywords, true) # Increment the score of a member in a sorted set. def zincrby(key, increment, member) node_for(key).zincrby(key, increment, member) end # Remove one or more members from a sorted set. def zrem(key, member) node_for(key).zrem(key, member) end # Get the score associated with the given member in a sorted set. def zscore(key, member) node_for(key).zscore(key, member) end # Return a range of members in a sorted set, by index. def zrange(key, start, stop, **options) node_for(key).zrange(key, start, stop, **options) end # Return a range of members in a sorted set, by index, with scores ordered # from high to low. def zrevrange(key, start, stop, **options) node_for(key).zrevrange(key, start, stop, **options) end # Determine the index of a member in a sorted set. def zrank(key, member) node_for(key).zrank(key, member) end # Determine the index of a member in a sorted set, with scores ordered from # high to low. def zrevrank(key, member) node_for(key).zrevrank(key, member) end # Remove all members in a sorted set within the given indexes. def zremrangebyrank(key, start, stop) node_for(key).zremrangebyrank(key, start, stop) end # Return a range of members in a sorted set, by score. def zrangebyscore(key, min, max, **options) node_for(key).zrangebyscore(key, min, max, **options) end # Return a range of members in a sorted set, by score, with scores ordered # from high to low. def zrevrangebyscore(key, max, min, **options) node_for(key).zrevrangebyscore(key, max, min, **options) end # Remove all members in a sorted set within the given scores. def zremrangebyscore(key, min, max) node_for(key).zremrangebyscore(key, min, max) end # Get the number of members in a particular score range. def zcount(key, min, max) node_for(key).zcount(key, min, max) end # Intersect multiple sorted sets and store the resulting sorted set in a new # key. def zinterstore(destination, keys, **options) ensure_same_node(:zinterstore, [destination] + keys) do |node| node.zinterstore(destination, keys, **options) end end # Add multiple sorted sets and store the resulting sorted set in a new key. def zunionstore(destination, keys, **options) ensure_same_node(:zunionstore, [destination] + keys) do |node| node.zunionstore(destination, keys, **options) end end # Get the number of fields in a hash. def hlen(key) node_for(key).hlen(key) end # Set multiple hash fields to multiple values. def hset(key, *attrs) node_for(key).hset(key, *attrs) end # Set the value of a hash field, only if the field does not exist. def hsetnx(key, field, value) node_for(key).hsetnx(key, field, value) end # Set multiple hash fields to multiple values. def hmset(key, *attrs) node_for(key).hmset(key, *attrs) end def mapped_hmset(key, hash) node_for(key).hmset(key, *hash.to_a.flatten) end # Get the value of a hash field. def hget(key, field) node_for(key).hget(key, field) end # Get the values of all the given hash fields. def hmget(key, *fields) node_for(key).hmget(key, *fields) end def mapped_hmget(key, *fields) Hash[*fields.zip(hmget(key, *fields)).flatten] end # Delete one or more hash fields. def hdel(key, *fields) node_for(key).hdel(key, *fields) end # Determine if a hash field exists. def hexists(key, field) node_for(key).hexists(key, field) end # Increment the integer value of a hash field by the given integer number. def hincrby(key, field, increment) node_for(key).hincrby(key, field, increment) end # Increment the numeric value of a hash field by the given float number. def hincrbyfloat(key, field, increment) node_for(key).hincrbyfloat(key, field, increment) end # Get all the fields in a hash. def hkeys(key) node_for(key).hkeys(key) end # Get all the values in a hash. def hvals(key) node_for(key).hvals(key) end # Get all the fields and values in a hash. def hgetall(key) node_for(key).hgetall(key) end # Post a message to a channel. def publish(channel, message) node_for(channel).publish(channel, message) end def subscribed? !!@subscribed_node end # Listen for messages published to the given channels. def subscribe(channel, *channels, &block) if channels.empty? @subscribed_node = node_for(channel) @subscribed_node.subscribe(channel, &block) else ensure_same_node(:subscribe, [channel] + channels) do |node| @subscribed_node = node node.subscribe(channel, *channels, &block) end end end # Stop listening for messages posted to the given channels. def unsubscribe(*channels) raise "Can't unsubscribe if not subscribed." unless subscribed? @subscribed_node.unsubscribe(*channels) end # Listen for messages published to channels matching the given patterns. def psubscribe(*channels, &block) raise NotImplementedError end # Stop listening for messages posted to channels matching the given # patterns. def punsubscribe(*channels) raise NotImplementedError end # Watch the given keys to determine execution of the MULTI/EXEC block. def watch(*keys, &block) ensure_same_node(:watch, keys) do |node| @watch_key = key_tag(keys.first) || keys.first.to_s begin node.watch(*keys, &block) rescue StandardError @watch_key = nil raise end end end # Forget about all watched keys. def unwatch raise CannotDistribute, :unwatch unless @watch_key result = node_for(@watch_key).unwatch @watch_key = nil result end def pipelined raise CannotDistribute, :pipelined end # Mark the start of a transaction block. def multi(&block) raise CannotDistribute, :multi unless @watch_key result = node_for(@watch_key).multi(&block) @watch_key = nil if block_given? result end # Execute all commands issued after MULTI. def exec raise CannotDistribute, :exec unless @watch_key result = node_for(@watch_key).exec @watch_key = nil result end # Discard all commands issued after MULTI. def discard raise CannotDistribute, :discard unless @watch_key result = node_for(@watch_key).discard @watch_key = nil result end # Control remote script registry. def script(subcommand, *args) on_each_node(:script, subcommand, *args) end # Add one or more members to a HyperLogLog structure. def pfadd(key, member) node_for(key).pfadd(key, member) end # Get the approximate cardinality of members added to HyperLogLog structure. def pfcount(*keys) ensure_same_node(:pfcount, keys.flatten(1)) do |node| node.pfcount(keys) end end # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of # the observed Sets of the source HyperLogLog structures. def pfmerge(dest_key, *source_key) ensure_same_node(:pfmerge, [dest_key, *source_key]) do |node| node.pfmerge(dest_key, *source_key) end end def _eval(cmd, args) script = args.shift options = args.pop if args.last.is_a?(Hash) options ||= {} keys = args.shift || options[:keys] || [] argv = args.shift || options[:argv] || [] ensure_same_node(cmd, keys) do |node| node.send(cmd, script, keys, argv) end end # Evaluate Lua script. def eval(*args) _eval(:eval, args) end # Evaluate Lua script by its SHA. def evalsha(*args) _eval(:evalsha, args) end def inspect "#" end def dup self.class.new(@node_configs, @default_options) end protected def on_each_node(command, *args) nodes.map do |node| node.send(command, *args) end end def node_index_for(key) nodes.index(node_for(key)) end def key_tag(key) key.to_s[@tag, 1] if @tag end def ensure_same_node(command, keys) all = true tags = keys.map do |key| tag = key_tag(key) all = false unless tag tag end if (all && tags.uniq.size != 1) || (!all && keys.uniq.size != 1) # Not 1 unique tag or not 1 unique key raise CannotDistribute, command end yield(node_for(keys.first)) end end end redis-rb-4.2.5/lib/redis/errors.rb000066400000000000000000000053311375567530100167640ustar00rootroot00000000000000# frozen_string_literal: true class Redis # Base error for all redis-rb errors. class BaseError < RuntimeError end # Raised by the connection when a protocol error occurs. class ProtocolError < BaseError def initialize(reply_type) super(<<-EOS.gsub(/(?:^|\n)\s*/, " ")) Got '#{reply_type}' as initial reply byte. If you're in a forking environment, such as Unicorn, you need to connect to Redis after forking. EOS end end # Raised by the client when command execution returns an error reply. class CommandError < BaseError end # Base error for connection related errors. class BaseConnectionError < BaseError end # Raised when connection to a Redis server cannot be made. class CannotConnectError < BaseConnectionError end # Raised when connection to a Redis server is lost. class ConnectionError < BaseConnectionError end # Raised when performing I/O times out. class TimeoutError < BaseConnectionError end # Raised when the connection was inherited by a child process. class InheritedError < BaseConnectionError end # Raised when client options are invalid. class InvalidClientOptionError < BaseError end class Cluster # 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 def initialize(command) super("Cluster client doesn't know which node the #{command} command should be sent to.") end end # Raised when commands in pipelining include cross slot keys. class CrossSlotPipeliningError < BaseError def initialize(keys) super("Cluster client couldn't send pipelining to single node. "\ "The commands include cross slot keys. #{keys}") end end end end redis-rb-4.2.5/lib/redis/hash_ring.rb000066400000000000000000000041271375567530100174140ustar00rootroot00000000000000# frozen_string_literal: true require 'zlib' class Redis class HashRing POINTS_PER_SERVER = 160 # this is the default in libmemcached attr_reader :ring, :sorted_keys, :replicas, :nodes # nodes is a list of objects that have a proper to_s representation. # replicas indicates how many virtual points should be used pr. node, # replicas are required to improve the distribution. def initialize(nodes = [], replicas = POINTS_PER_SERVER) @replicas = replicas @ring = {} @nodes = [] @sorted_keys = [] nodes.each do |node| add_node(node) end end # Adds a `node` to the hash ring (including a number of replicas). def add_node(node) @nodes << node @replicas.times do |i| key = Zlib.crc32("#{node.id}:#{i}") @ring[key] = node @sorted_keys << key end @sorted_keys.sort! end def remove_node(node) @nodes.reject! { |n| n.id == node.id } @replicas.times do |i| key = Zlib.crc32("#{node.id}:#{i}") @ring.delete(key) @sorted_keys.reject! { |k| k == key } end end # get the node in the hash ring for this key def get_node(key) get_node_pos(key)[0] end def get_node_pos(key) return [nil, nil] if @ring.empty? crc = Zlib.crc32(key) idx = HashRing.binary_search(@sorted_keys, crc) [@ring[@sorted_keys[idx]], idx] end def iter_nodes(key) return [nil, nil] if @ring.empty? _, pos = get_node_pos(key) @ring.size.times do |n| yield @ring[@sorted_keys[(pos + n) % @ring.size]] end end # Find the closest index in HashRing with value <= the given value def self.binary_search(ary, value) upper = ary.size - 1 lower = 0 idx = 0 while lower <= upper idx = (lower + upper) / 2 comp = ary[idx] <=> value if comp == 0 return idx elsif comp > 0 upper = idx - 1 else lower = idx + 1 end end upper = ary.size - 1 if upper < 0 upper end end end redis-rb-4.2.5/lib/redis/pipeline.rb000066400000000000000000000074561375567530100172670ustar00rootroot00000000000000# frozen_string_literal: true class Redis class Pipeline attr_accessor :db attr_reader :client attr :futures def initialize(client) @client = client.is_a?(Pipeline) ? client.client : client @with_reconnect = true @shutdown = false @futures = [] end def timeout client.timeout end def with_reconnect? @with_reconnect end def without_reconnect? !@with_reconnect end def shutdown? @shutdown end def empty? @futures.empty? end def call(command, timeout: nil, &block) # A pipeline that contains a shutdown should not raise ECONNRESET when # the connection is gone. @shutdown = true if command.first == :shutdown future = Future.new(command, block, timeout) @futures << future future end def call_with_timeout(command, timeout, &block) call(command, timeout: timeout, &block) end def call_pipeline(pipeline) @shutdown = true if pipeline.shutdown? @futures.concat(pipeline.futures) @db = pipeline.db nil end def commands @futures.map(&:_command) end def timeouts @futures.map(&:timeout) end def with_reconnect(val = true) @with_reconnect = false unless val yield end def without_reconnect(&blk) with_reconnect(false, &blk) end def finish(replies, &blk) if blk futures.each_with_index.map do |future, i| future._set(blk.call(replies[i])) end else futures.each_with_index.map do |future, i| future._set(replies[i]) end end end class Multi < self def finish(replies) exec = replies.last return if exec.nil? # The transaction failed because of WATCH. # EXEC command failed. raise exec if exec.is_a?(CommandError) if exec.size < futures.size # Some command wasn't recognized by Redis. command_error = replies.detect { |r| r.is_a?(CommandError) } raise command_error end super(exec) do |reply| # Because an EXEC returns nested replies, hiredis won't be able to # convert an error reply to a CommandError instance itself. This is # specific to MULTI/EXEC, so we solve this here. reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply end end def timeouts if empty? [] else [nil, *super, nil] end end def commands if empty? [] else [[:multi]] + super + [[:exec]] end end end end class FutureNotReady < RuntimeError def initialize super("Value will be available once the pipeline executes.") end end class Future < BasicObject FutureNotReady = ::Redis::FutureNotReady.new attr_reader :timeout def initialize(command, transformation, timeout) @command = command @transformation = transformation @timeout = timeout @object = FutureNotReady end def ==(_other) message = +"The methods == and != are deprecated for Redis::Future and will be removed in 4.2.0" message << " - You probably meant to call .value == or .value !=" message << " (#{::Kernel.caller(1, 1).first})\n" ::Kernel.warn(message) super end def inspect "" end def _set(object) @object = @transformation ? @transformation.call(object) : object value end def _command @command end def value ::Kernel.raise(@object) if @object.is_a?(::RuntimeError) @object end def is_a?(other) self.class.ancestors.include?(other) end def class Future end end end redis-rb-4.2.5/lib/redis/subscribe.rb000066400000000000000000000037261375567530100174370ustar00rootroot00000000000000# frozen_string_literal: true class Redis class SubscribedClient def initialize(client) @client = client end def call(command) @client.process([command]) end def subscribe(*channels, &block) subscription("subscribe", "unsubscribe", channels, block) end def subscribe_with_timeout(timeout, *channels, &block) subscription("subscribe", "unsubscribe", channels, block, timeout) end def psubscribe(*channels, &block) subscription("psubscribe", "punsubscribe", channels, block) end def psubscribe_with_timeout(timeout, *channels, &block) subscription("psubscribe", "punsubscribe", channels, block, timeout) end def unsubscribe(*channels) call([:unsubscribe, *channels]) end def punsubscribe(*channels) call([:punsubscribe, *channels]) end protected def subscription(start, stop, channels, block, timeout = 0) sub = Subscription.new(&block) unsubscribed = false @client.call_loop([start, *channels], timeout) do |line| type, *rest = line sub.callbacks[type].call(*rest) unsubscribed = type == stop && rest.last == 0 break if unsubscribed end # No need to unsubscribe here. The real client closes the connection # whenever an exception is raised (see #ensure_connected). end end class Subscription attr :callbacks def initialize @callbacks = Hash.new do |hash, key| hash[key] = ->(*_) {} end yield(self) end def subscribe(&block) @callbacks["subscribe"] = block end def unsubscribe(&block) @callbacks["unsubscribe"] = block end def message(&block) @callbacks["message"] = block end def psubscribe(&block) @callbacks["psubscribe"] = block end def punsubscribe(&block) @callbacks["punsubscribe"] = block end def pmessage(&block) @callbacks["pmessage"] = block end end end redis-rb-4.2.5/lib/redis/version.rb000066400000000000000000000001031375567530100171250ustar00rootroot00000000000000# frozen_string_literal: true class Redis VERSION = '4.2.5' end redis-rb-4.2.5/makefile000066400000000000000000000070541375567530100147530ustar00rootroot00000000000000REDIS_BRANCH ?= 6.0 TMP := tmp BUILD_DIR := ${TMP}/cache/redis-${REDIS_BRANCH} TARBALL := ${TMP}/redis-${REDIS_BRANCH}.tar.gz BINARY := ${BUILD_DIR}/src/redis-server REDIS_CLIENT := ${BUILD_DIR}/src/redis-cli REDIS_TRIB := ${BUILD_DIR}/src/redis-trib.rb PID_PATH := ${BUILD_DIR}/redis.pid SOCKET_PATH := ${BUILD_DIR}/redis.sock PORT := 6381 SLAVE_PORT := 6382 SLAVE_PID_PATH := ${BUILD_DIR}/redis_slave.pid SLAVE_SOCKET_PATH := ${BUILD_DIR}/redis_slave.sock HA_GROUP_NAME := master1 SENTINEL_PORTS := 6400 6401 6402 SENTINEL_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${SENTINEL_PORTS})) CLUSTER_PORTS := 7000 7001 7002 7003 7004 7005 CLUSTER_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${CLUSTER_PORTS})) CLUSTER_CONF_PATHS := $(addprefix ${TMP}/nodes,$(addsuffix .conf,${CLUSTER_PORTS})) CLUSTER_ADDRS := $(addprefix 127.0.0.1:,${CLUSTER_PORTS}) define kill-redis (ls $1 > /dev/null 2>&1 && kill $$(cat $1) && rm -f $1) || true endef all: start_all test stop_all start_all: start start_slave start_sentinel wait_for_sentinel start_cluster create_cluster stop_all: stop_sentinel stop_slave stop stop_cluster ${TMP}: @mkdir -p $@ ${BINARY}: ${TMP} @bin/build ${REDIS_BRANCH} $< test: @env SOCKET_PATH=${SOCKET_PATH} bundle exec rake test stop: @$(call kill-redis,${PID_PATH}) start: ${BINARY} @${BINARY}\ --daemonize yes\ --pidfile ${PID_PATH}\ --port ${PORT}\ --unixsocket ${SOCKET_PATH} stop_slave: @$(call kill-redis,${SLAVE_PID_PATH}) start_slave: ${BINARY} @${BINARY}\ --daemonize yes\ --pidfile ${SLAVE_PID_PATH}\ --port ${SLAVE_PORT}\ --unixsocket ${SLAVE_SOCKET_PATH}\ --slaveof 127.0.0.1 ${PORT} stop_sentinel: @$(call kill-redis,${SENTINEL_PID_PATHS}) @rm -f ${TMP}/sentinel*.conf || true start_sentinel: ${BINARY} @for port in ${SENTINEL_PORTS}; do\ conf=${TMP}/sentinel$$port.conf;\ touch $$conf;\ echo '' > $$conf;\ echo 'sentinel monitor ${HA_GROUP_NAME} 127.0.0.1 ${PORT} 2' >> $$conf;\ echo 'sentinel down-after-milliseconds ${HA_GROUP_NAME} 5000' >> $$conf;\ echo 'sentinel failover-timeout ${HA_GROUP_NAME} 30000' >> $$conf;\ echo 'sentinel parallel-syncs ${HA_GROUP_NAME} 1' >> $$conf;\ ${BINARY} $$conf\ --daemonize yes\ --pidfile ${TMP}/redis$$port.pid\ --port $$port\ --sentinel;\ done wait_for_sentinel: @for port in ${SENTINEL_PORTS}; do\ while : ; do\ if [ $$(${REDIS_CLIENT} -p $${port} SENTINEL SLAVES ${HA_GROUP_NAME} | wc -l) -gt 1 ]; then\ break;\ fi;\ echo 'Waiting for Redis sentinel to be ready...';\ sleep 1;\ done;\ done stop_cluster: @$(call kill-redis,${CLUSTER_PID_PATHS}) @rm -f appendonly.aof || true @rm -f ${CLUSTER_CONF_PATHS} || true start_cluster: ${BINARY} @for port in ${CLUSTER_PORTS}; do\ ${BINARY}\ --daemonize yes\ --appendonly yes\ --cluster-enabled yes\ --cluster-config-file ${TMP}/nodes$$port.conf\ --cluster-node-timeout 5000\ --pidfile ${TMP}/redis$$port.pid\ --port $$port\ --unixsocket ${TMP}/redis$$port.sock;\ done create_cluster: @bin/cluster_creator ${CLUSTER_ADDRS} clean: @(test -d ${BUILD_DIR} && cd ${BUILD_DIR}/src && make clean distclean) || true .PHONY: all test stop start stop_slave start_slave stop_sentinel start_sentinel\ stop_cluster start_cluster create_cluster stop_all start_all clean redis-rb-4.2.5/redis.gemspec000066400000000000000000000024471375567530100157270ustar00rootroot00000000000000# frozen_string_literal: true require "./lib/redis/version" Gem::Specification.new do |s| s.name = "redis" s.version = Redis::VERSION s.homepage = "https://github.com/redis/redis-rb" s.summary = "A Ruby client library for Redis" s.description = <<-EOS A Ruby client that tries to match Redis' API one-to-one, while still providing an idiomatic interface. EOS s.license = "MIT" s.authors = [ "Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark", "Brian McKinney", "Salvatore Sanfilippo", "Luca Guidi", "Michel Martens", "Damian Janowski", "Pieter Noordhuis" ] s.email = ["redis-db@googlegroups.com"] s.metadata = { "bug_tracker_uri" => "#{s.homepage}/issues", "changelog_uri" => "#{s.homepage}/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/redis/#{s.version}", "homepage_uri" => s.homepage, "source_code_uri" => "#{s.homepage}/tree/v#{s.version}" } s.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "lib/**/*"] s.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) } s.required_ruby_version = '>= 2.3.0' s.add_development_dependency("em-synchrony") s.add_development_dependency("hiredis") s.add_development_dependency("mocha") end redis-rb-4.2.5/test/000077500000000000000000000000001375567530100142245ustar00rootroot00000000000000redis-rb-4.2.5/test/bitpos_test.rb000066400000000000000000000027101375567530100171100ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestBitpos < Minitest::Test include Helper::Client def test_bitpos_empty_zero target_version "2.9.11" do r.del "foo" assert_equal(0, r.bitpos("foo", 0)) end end def test_bitpos_empty_one target_version "2.9.11" do r.del "foo" assert_equal(-1, r.bitpos("foo", 1)) end end def test_bitpos_zero target_version "2.9.11" do r.set "foo", "\xff\xf0\x00" assert_equal(12, r.bitpos("foo", 0)) end end def test_bitpos_one target_version "2.9.11" do r.set "foo", "\x00\x0f\x00" assert_equal(12, r.bitpos("foo", 1)) end end def test_bitpos_zero_end_is_given target_version "2.9.11" do r.set "foo", "\xff\xff\xff" assert_equal(24, r.bitpos("foo", 0)) assert_equal(24, r.bitpos("foo", 0, 0)) assert_equal(-1, r.bitpos("foo", 0, 0, -1)) end end def test_bitpos_one_intervals target_version "2.9.11" do r.set "foo", "\x00\xff\x00" assert_equal(8, r.bitpos("foo", 1, 0, -1)) assert_equal(8, r.bitpos("foo", 1, 1, -1)) assert_equal(-1, r.bitpos("foo", 1, 2, -1)) assert_equal(-1, r.bitpos("foo", 1, 2, 200)) assert_equal(8, r.bitpos("foo", 1, 1, 1)) end end def test_bitpos_raise_exception_if_stop_not_start target_version "2.9.11" do assert_raises(ArgumentError) do r.bitpos("foo", 0, nil, 2) end end end end redis-rb-4.2.5/test/blocking_commands_test.rb000066400000000000000000000025211375567530100212610ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/blocking_commands' class TestBlockingCommands < Minitest::Test include Helper::Client include Lint::BlockingCommands def assert_takes_longer_than_client_timeout timeout = LOW_TIMEOUT delay = timeout * 5 mock(delay: delay) do |r| t1 = Time.now yield(r) t2 = Time.now assert timeout == r._client.timeout assert delay <= (t2 - t1) end end def test_blpop_disable_client_timeout assert_takes_longer_than_client_timeout do |r| assert_equal %w[foo 0], r.blpop('foo') end end def test_brpop_disable_client_timeout assert_takes_longer_than_client_timeout do |r| assert_equal %w[foo 0], r.brpop('foo') end end def test_brpoplpush_disable_client_timeout assert_takes_longer_than_client_timeout do |r| assert_equal '0', r.brpoplpush('foo', 'bar') end end def test_brpoplpush_in_transaction results = r.multi do r.brpoplpush('foo', 'bar') r.brpoplpush('foo', 'bar', timeout: 2) end assert_equal [nil, nil], results end def test_brpoplpush_in_pipeline mock do |r| results = r.pipelined do r.brpoplpush('foo', 'bar') r.brpoplpush('foo', 'bar', timeout: 2) end assert_equal ['0', '2'], results end end end redis-rb-4.2.5/test/client_test.rb000066400000000000000000000027751375567530100171010ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestClient < Minitest::Test include Helper::Client def test_call result = r.call("PING") assert_equal result, "PONG" end def test_call_with_arguments result = r.call("SET", "foo", "bar") assert_equal result, "OK" end def test_call_integers result = r.call("INCR", "foo") assert_equal result, 1 end def test_call_raise assert_raises(Redis::CommandError) do r.call("INCR") end end def test_queue_commit r.queue("SET", "foo", "bar") r.queue("GET", "foo") result = r.commit assert_equal result, ["OK", "bar"] end def test_commit_raise r.queue("SET", "foo", "bar") r.queue("INCR") assert_raises(Redis::CommandError) do r.commit end end def test_queue_after_error r.queue("SET", "foo", "bar") r.queue("INCR") assert_raises(Redis::CommandError) do r.commit end r.queue("SET", "foo", "bar") r.queue("INCR", "baz") result = r.commit assert_equal result, ["OK", 1] end def test_client_with_custom_connector custom_connector = Class.new(Redis::Client::Connector) do def resolve @options[:host] = '127.0.0.5' @options[:port] = '999' @options end end error = assert_raises do new_redis = _new_client(connector: custom_connector) new_redis.ping end assert_equal 'Error connecting to Redis on 127.0.0.5:999 (Errno::ECONNREFUSED)', error.message end end redis-rb-4.2.5/test/cluster_abnormal_state_test.rb000066400000000000000000000026741375567530100223550ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_abnormal_state_test.rb class TestClusterAbnormalState < Minitest::Test include Helper::Cluster def test_the_state_of_cluster_down redis_cluster_down do assert_raises(Redis::CommandError, 'CLUSTERDOWN Hash slot not served') do redis.set('key1', 1) end assert_equal 'fail', redis.cluster(:info).fetch('cluster_state') end end def test_the_state_of_cluster_failover redis_cluster_failover do 10.times do |i| assert_equal 'OK', r.set("key#{i}", i) end 10.times do |i| assert_equal i.to_s, r.get("key#{i}") end assert_equal 'ok', redis.cluster(:info).fetch('cluster_state') end end def test_the_state_of_cluster_node_failure redis_cluster_fail_master do assert_raises(Redis::CannotConnectError, 'Error connecting to Redis on 127.0.0.1:7002') do r.set('key0', 0) end 10.times do |i| assert_equal 'OK', r.set("key#{i}", i) end 10.times do |i| assert_equal i.to_s, r.get("key#{i}") end assert_equal 'ok', redis.cluster(:info).fetch('cluster_state') end end def test_raising_error_when_nodes_are_not_cluster_mode assert_raises(Redis::CannotConnectError, 'Redis client could not connect to any cluster nodes') do build_another_client(cluster: %W[redis://127.0.0.1:#{PORT}]) end end end redis-rb-4.2.5/test/cluster_blocking_commands_test.rb000066400000000000000000000006411375567530100230230ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/blocking_commands' # ruby -w -Itest test/cluster_blocking_commands_test.rb class TestClusterBlockingCommands < Minitest::Test include Helper::Cluster include Lint::BlockingCommands def mock(options = {}, &blk) commands = build_mock_commands(options) redis_cluster_mock(commands, { timeout: LOW_TIMEOUT }, &blk) end end redis-rb-4.2.5/test/cluster_client_internals_test.rb000066400000000000000000000037531375567530100227160ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_internals_test.rb class TestClusterClientInternals < Minitest::Test include Helper::Cluster def test_handle_multiple_servers 100.times { |i| redis.set(i.to_s, "hogehoge#{i}") } 100.times { |i| assert_equal "hogehoge#{i}", redis.get(i.to_s) } end def test_info_of_cluster_mode_is_enabled assert_equal '1', redis.info['cluster_enabled'] end def test_unknown_commands_does_not_work_by_default assert_raises(Redis::CommandError) do redis.not_yet_implemented_command('boo', 'foo') end end def test_with_reconnect assert_equal('Hello World', redis.with_reconnect { 'Hello World' }) end def test_without_reconnect assert_equal('Hello World', redis.without_reconnect { 'Hello World' }) end def test_connected? assert_equal true, redis.connected? end def test_close assert_equal true, redis.close end def test_disconnect! assert_equal true, redis.disconnect! end def test_asking assert_equal 'OK', redis.asking end def test_id expected = 'redis://127.0.0.1:7000/0 '\ 'redis://127.0.0.1:7001/0 '\ 'redis://127.0.0.1:7002/0' assert_equal expected, redis.id end def test_inspect expected = "#' assert_equal expected, redis.inspect end def test_dup assert_instance_of Redis, redis.dup end def test_connection expected = [ { host: '127.0.0.1', port: 7000, db: 0, id: 'redis://127.0.0.1:7000/0', location: '127.0.0.1:7000' }, { host: '127.0.0.1', port: 7001, db: 0, id: 'redis://127.0.0.1:7001/0', location: '127.0.0.1:7001' }, { host: '127.0.0.1', port: 7002, db: 0, id: 'redis://127.0.0.1:7002/0', location: '127.0.0.1:7002' } ] assert_equal expected, redis.connection end end redis-rb-4.2.5/test/cluster_client_key_hash_tags_test.rb000066400000000000000000000116311375567530100235220ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_key_hash_tags_test.rb class TestClusterClientKeyHashTags < Minitest::Test include Helper::Cluster def build_described_class option = Redis::Cluster::Option.new(cluster: ['redis://127.0.0.1:7000']) node = Redis::Cluster::Node.new(option.per_node_key) details = Redis::Cluster::CommandLoader.load(node) Redis::Cluster::Command.new(details) end def test_key_extraction described_class = build_described_class assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1]) assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.following]) assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.followers]) assert_equal 'foo{}{bar}', described_class.extract_first_key(%w[get foo{}{bar}]) assert_equal '{bar', described_class.extract_first_key(%w[get foo{{bar}}zap]) assert_equal 'bar', described_class.extract_first_key(%w[get foo{bar}{zap}]) assert_equal '', described_class.extract_first_key([:get, '']) assert_equal '', described_class.extract_first_key([:get, nil]) assert_equal '', described_class.extract_first_key([:get]) assert_equal '', described_class.extract_first_key([:set, '', 1]) assert_equal '', described_class.extract_first_key([:set, nil, 1]) assert_equal '', described_class.extract_first_key([:set]) # Keyless commands assert_equal '', described_class.extract_first_key([:auth, 'password']) assert_equal '', described_class.extract_first_key(%i[client kill]) assert_equal '', described_class.extract_first_key(%i[cluster addslots]) assert_equal '', described_class.extract_first_key(%i[command]) assert_equal '', described_class.extract_first_key(%i[command count]) assert_equal '', described_class.extract_first_key(%i[config get]) assert_equal '', described_class.extract_first_key(%i[debug segfault]) assert_equal '', described_class.extract_first_key([:echo, 'Hello World']) assert_equal '', described_class.extract_first_key([:flushall, 'ASYNC']) assert_equal '', described_class.extract_first_key([:flushdb, 'ASYNC']) assert_equal '', described_class.extract_first_key([:info, 'cluster']) assert_equal '', described_class.extract_first_key(%i[memory doctor]) assert_equal '', described_class.extract_first_key([:ping, 'Hi']) assert_equal '', described_class.extract_first_key([:psubscribe, 'channel']) assert_equal '', described_class.extract_first_key([:pubsub, 'channels', '*']) assert_equal '', described_class.extract_first_key([:publish, 'channel', 'Hi']) assert_equal '', described_class.extract_first_key([:punsubscribe, 'channel']) assert_equal '', described_class.extract_first_key([:subscribe, 'channel']) assert_equal '', described_class.extract_first_key([:unsubscribe, 'channel']) assert_equal '', described_class.extract_first_key(%w[script exists sha1 sha1]) assert_equal '', described_class.extract_first_key([:select, 1]) assert_equal '', described_class.extract_first_key([:shutdown, 'SAVE']) assert_equal '', described_class.extract_first_key([:slaveof, '127.0.0.1', 6379]) assert_equal '', described_class.extract_first_key([:slowlog, 'get', 2]) assert_equal '', described_class.extract_first_key([:swapdb, 0, 1]) assert_equal '', described_class.extract_first_key([:wait, 1, 0]) # 2nd argument is not a key assert_equal 'key1', described_class.extract_first_key([:eval, 'script', 2, 'key1', 'key2', 'first', 'second']) assert_equal '', described_class.extract_first_key([:eval, 'return 0', 0]) assert_equal 'key1', described_class.extract_first_key([:evalsha, 'sha1', 2, 'key1', 'key2', 'first', 'second']) assert_equal '', described_class.extract_first_key([:evalsha, 'return 0', 0]) assert_equal 'key1', described_class.extract_first_key([:migrate, '127.0.0.1', 6379, 'key1', 0, 5000]) assert_equal 'key1', described_class.extract_first_key([:memory, :usage, 'key1']) assert_equal 'key1', described_class.extract_first_key([:object, 'refcount', 'key1']) assert_equal 'mystream', described_class.extract_first_key([:xread, 'COUNT', 2, 'STREAMS', 'mystream', 0]) assert_equal 'mystream', described_class.extract_first_key([:xreadgroup, 'GROUP', 'mygroup', 'Bob', 'COUNT', 2, 'STREAMS', 'mystream', '>']) end def test_whether_the_command_effect_is_readonly_or_not described_class = build_described_class assert_equal true, described_class.should_send_to_master?([:set]) assert_equal false, described_class.should_send_to_slave?([:set]) assert_equal false, described_class.should_send_to_master?([:get]) assert_equal true, described_class.should_send_to_slave?([:get]) target_version('3.2.0') do assert_equal false, described_class.should_send_to_master?([:info]) assert_equal false, described_class.should_send_to_slave?([:info]) end end end redis-rb-4.2.5/test/cluster_client_options_test.rb000066400000000000000000000133771375567530100224150ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_options_test.rb class TestClusterClientOptions < Minitest::Test include Helper::Cluster def test_option_class option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: true) assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) assert_equal true, option.use_replica? option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: false) assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) assert_equal false, option.use_replica? option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000]) assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) assert_equal false, option.use_replica? option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace]) assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key) option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis') assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key) option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar') assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key) option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000/0], db: 1) assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000, db: 0 } }, option.per_node_key) option = Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1', port: 7000 }]) assert_equal({ '127.0.0.1:7000' => { host: '127.0.0.1', port: 7000 } }, option.per_node_key) assert_raises(Redis::InvalidClientOptionError) do Redis::Cluster::Option.new(cluster: nil) end assert_raises(Redis::InvalidClientOptionError) do Redis::Cluster::Option.new(cluster: %w[invalid_uri]) end assert_raises(Redis::InvalidClientOptionError) do Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1' }]) end end def test_client_accepts_valid_node_configs nodes = ['redis://127.0.0.1:7000', 'redis://127.0.0.1:7001', { host: '127.0.0.1', port: '7002' }, { 'host' => '127.0.0.1', port: 7003 }, 'redis://127.0.0.1:7004', 'redis://127.0.0.1:7005'] build_another_client(cluster: nodes) end def test_client_accepts_valid_options build_another_client(timeout: TIMEOUT) end def test_client_ignores_invalid_options build_another_client(invalid_option: true) end def test_client_works_even_if_so_many_unavailable_nodes_specified min = 7000 max = min + Process.getrlimit(Process::RLIMIT_NOFILE).first / 3 * 2 nodes = (min..max).map { |port| "redis://127.0.0.1:#{port}" } redis = build_another_client(cluster: nodes) assert_equal 'PONG', redis.ping end def test_client_does_not_accept_db_specified_url assert_raises(Redis::CannotConnectError, 'Could not connect to any nodes') do build_another_client(cluster: ['redis://127.0.0.1:7000/1/namespace']) end assert_raises(Redis::CannotConnectError, 'Could not connect to any nodes') do build_another_client(cluster: [{ host: '127.0.0.1', port: '7000' }], db: 1) end end def test_client_does_not_accept_unconnectable_node_url_only nodes = ['redis://127.0.0.1:7006'] assert_raises(Redis::CannotConnectError, 'Could not connect to any nodes') do build_another_client(cluster: nodes) end end def test_client_accepts_unconnectable_node_url_included nodes = ['redis://127.0.0.1:7000', 'redis://127.0.0.1:7006'] build_another_client(cluster: nodes) end def test_client_does_not_accept_http_scheme_url nodes = ['http://127.0.0.1:80'] assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme 'http'") do build_another_client(cluster: nodes) end end def test_client_does_not_accept_blank_included_config nodes = [''] assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do build_another_client(cluster: nodes) end end def test_client_does_not_accept_bool_included_config nodes = [true] assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do build_another_client(cluster: nodes) end end def test_client_does_not_accept_nil_included_config nodes = [nil] assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do build_another_client(cluster: nodes) end end def test_client_does_not_accept_array_included_config nodes = [[]] assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do build_another_client(cluster: nodes) end end def test_client_does_not_accept_empty_hash_included_config nodes = [{}] assert_raises(Redis::InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys') do build_another_client(cluster: nodes) end end def test_client_does_not_accept_object_included_config nodes = [Object.new] assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must includes String or Hash') do build_another_client(cluster: nodes) end end def test_client_does_not_accept_not_array_config nodes = :not_array assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must be Array') do build_another_client(cluster: nodes) end end end redis-rb-4.2.5/test/cluster_client_pipelining_test.rb000066400000000000000000000033031375567530100230440ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_pipelining_test.rb class TestClusterClientPipelining < Minitest::Test include Helper::Cluster def test_pipelining_with_a_hash_tag p1 = p2 = p3 = p4 = p5 = p6 = nil redis.pipelined do |r| r.set('{Presidents.of.USA}:1', 'George Washington') r.set('{Presidents.of.USA}:2', 'John Adams') r.set('{Presidents.of.USA}:3', 'Thomas Jefferson') r.set('{Presidents.of.USA}:4', 'James Madison') r.set('{Presidents.of.USA}:5', 'James Monroe') r.set('{Presidents.of.USA}:6', 'John Quincy Adams') p1 = r.get('{Presidents.of.USA}:1') p2 = r.get('{Presidents.of.USA}:2') p3 = r.get('{Presidents.of.USA}:3') p4 = r.get('{Presidents.of.USA}:4') p5 = r.get('{Presidents.of.USA}:5') p6 = r.get('{Presidents.of.USA}:6') end [p1, p2, p3, p4, p5, p6].each do |actual| assert_equal true, actual.is_a?(Redis::Future) end assert_equal('George Washington', p1.value) assert_equal('John Adams', p2.value) assert_equal('Thomas Jefferson', p3.value) assert_equal('James Madison', p4.value) assert_equal('James Monroe', p5.value) assert_equal('John Quincy Adams', p6.value) end def test_pipelining_without_hash_tags assert_raises(Redis::Cluster::CrossSlotPipeliningError) do redis.pipelined do redis.set(:a, 1) redis.set(:b, 2) redis.set(:c, 3) redis.set(:d, 4) redis.set(:e, 5) redis.set(:f, 6) redis.get(:a) redis.get(:b) redis.get(:c) redis.get(:d) redis.get(:e) redis.get(:f) end end end end redis-rb-4.2.5/test/cluster_client_replicas_test.rb000066400000000000000000000016121375567530100225110ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_replicas_test.rb class TestClusterClientReplicas < Minitest::Test include Helper::Cluster def test_client_can_command_with_replica r = build_another_client(replica: true) 100.times do |i| assert_equal 'OK', r.set("key#{i}", i) end r.wait(1, TIMEOUT.to_i * 1000) 100.times do |i| assert_equal i.to_s, r.get("key#{i}") end end def test_client_can_flush_with_replica r = build_another_client(replica: true) assert_equal 'OK', r.flushall assert_equal 'OK', r.flushdb end def test_some_reference_commands_are_sent_to_slaves_if_needed r = build_another_client(replica: true) 5.times { |i| r.set("key#{i}", i) } r.wait(1, TIMEOUT.to_i * 1000) assert_equal %w[key0 key1 key2 key3 key4], r.keys assert_equal 5, r.dbsize end end redis-rb-4.2.5/test/cluster_client_slots_test.rb000066400000000000000000000136131375567530100220570ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_slots_test.rb class TestClusterClientSlots < Minitest::Test include Helper::Cluster def test_slot_class slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10]) assert_equal false, slot.exists?(0) assert_equal true, slot.exists?(1) assert_equal true, slot.exists?(10) assert_equal false, slot.exists?(11) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) assert_nil slot.find_node_key_of_master(11) assert_nil slot.find_node_key_of_slave(11) assert_nil slot.put(1, '127.0.0.1:7001') end def test_slot_class_with_multiple_slot_ranges slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10, 30..40]) assert_equal false, slot.exists?(0) assert_equal true, slot.exists?(1) assert_equal true, slot.exists?(10) assert_equal false, slot.exists?(11) assert_equal true, slot.exists?(30) assert_equal true, slot.exists?(40) assert_equal false, slot.exists?(41) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(30) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(40) assert_nil slot.find_node_key_of_master(11) assert_nil slot.find_node_key_of_slave(11) assert_nil slot.find_node_key_of_master(41) assert_nil slot.find_node_key_of_slave(41) assert_nil slot.put(1, '127.0.0.1:7001') assert_nil slot.put(30, '127.0.0.1:7001') end def test_slot_class_with_node_flags_and_replicas slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, true) assert_equal false, slot.exists?(0) assert_equal true, slot.exists?(1) assert_equal true, slot.exists?(10) assert_equal false, slot.exists?(11) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) assert_nil slot.find_node_key_of_master(11) assert_nil slot.find_node_key_of_slave(11) assert_nil slot.put(1, '127.0.0.1:7002') end def test_slot_class_with_node_flags_replicas_and_slot_range slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10, 30..40], '127.0.0.1:7001' => [1..10, 30..40] }, { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, true) assert_equal false, slot.exists?(0) assert_equal true, slot.exists?(1) assert_equal true, slot.exists?(10) assert_equal false, slot.exists?(11) assert_equal true, slot.exists?(30) assert_equal false, slot.exists?(41) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) assert_nil slot.find_node_key_of_master(11) assert_nil slot.find_node_key_of_slave(11) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(30) assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(30) assert_nil slot.find_node_key_of_master(41) assert_nil slot.find_node_key_of_slave(41) assert_nil slot.put(1, '127.0.0.1:7002') end def test_slot_class_with_node_flags_and_without_replicas slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, false) assert_equal false, slot.exists?(0) assert_equal true, slot.exists?(1) assert_equal true, slot.exists?(10) assert_equal false, slot.exists?(11) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) assert_nil slot.find_node_key_of_master(11) assert_nil slot.find_node_key_of_slave(11) assert_nil slot.put(1, '127.0.0.1:7002') end def test_slot_class_with_empty_slots slot = Redis::Cluster::Slot.new({}) assert_equal false, slot.exists?(0) assert_equal false, slot.exists?(1) assert_nil slot.find_node_key_of_master(0) assert_nil slot.find_node_key_of_slave(0) assert_nil slot.find_node_key_of_master(1) assert_nil slot.find_node_key_of_slave(1) assert_nil slot.put(1, '127.0.0.1:7001') end def test_redirection_when_slot_is_resharding 100.times { |i| redis.set("{key}#{i}", i) } redis_cluster_resharding(12_539, src: '127.0.0.1:7002', dest: '127.0.0.1:7000') do 100.times { |i| assert_equal i.to_s, redis.get("{key}#{i}") } end end end redis-rb-4.2.5/test/cluster_client_transactions_test.rb000066400000000000000000000032341375567530100234210ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_client_transactions_test.rb class TestClusterClientTransactions < Minitest::Test include Helper::Cluster def test_transaction_with_hash_tag rc1 = redis rc2 = build_another_client rc1.multi do |cli| 100.times { |i| cli.set("{key}#{i}", i) } end 100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") } 100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") } end def test_transaction_without_hash_tag rc1 = redis rc2 = build_another_client assert_raises(Redis::Cluster::CrossSlotPipeliningError) do rc1.multi do |cli| 100.times { |i| cli.set("key#{i}", i) } end end 100.times { |i| assert_nil rc1.get("key#{i}") } 100.times { |i| assert_nil rc2.get("key#{i}") } end def test_transaction_with_replicas rc1 = build_another_client(replica: true) rc2 = build_another_client(replica: true) rc1.multi do |cli| 100.times { |i| cli.set("{key}#{i}", i) } end rc1.wait(1, TIMEOUT.to_i * 1000) 100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") } 100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") } end def test_transaction_with_watch rc1 = redis rc2 = build_another_client rc1.set('{key}1', 100) rc1.watch('{key}1') rc2.set('{key}1', 200) val = rc1.get('{key}1').to_i val += 1 rc1.multi do |cli| cli.set('{key}1', val) cli.set('{key}2', 300) end assert_equal '200', rc1.get('{key}1') assert_equal '200', rc2.get('{key}1') assert_nil rc1.get('{key}2') assert_nil rc2.get('{key}2') end end redis-rb-4.2.5/test/cluster_commands_on_cluster_test.rb000066400000000000000000000133671375567530100234210ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_cluster_test.rb # @see https://redis.io/commands#cluster class TestClusterCommandsOnCluster < Minitest::Test include Helper::Cluster def test_cluster_addslots assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER ADDSLOTS command should be...') do redis.cluster(:addslots, 0, 1, 2) end end def test_cluster_count_failure_reports assert_raises(Redis::CommandError, 'ERR Unknown node unknown-node-id') do redis.cluster('count-failure-reports', 'unknown-node-id') end node_id = redis.cluster(:nodes).first.fetch('node_id') assert_equal true, (redis.cluster('count-failure-reports', node_id) >= 0) end def test_cluster_countkeysinslot assert_equal true, (redis.cluster(:countkeysinslot, 0) >= 0) assert_equal true, (redis.cluster(:countkeysinslot, 16_383) >= 0) assert_raises(Redis::CommandError, 'ERR Invalid slot') do redis.cluster(:countkeysinslot, -1) end assert_raises(Redis::CommandError, 'ERR Invalid slot') do redis.cluster(:countkeysinslot, 16_384) end end def test_cluster_delslots assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER DELSLOTS command should be...') do redis.cluster(:delslots, 0, 1, 2) end end def test_cluster_failover assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER FAILOVER command should be...') do redis.cluster(:failover, 'FORCE') end end def test_cluster_forget assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER FORGET command should be...') do redis.cluster(:forget, 'unknown-node-id') end end def test_cluster_getkeysinslot assert_instance_of Array, redis.cluster(:getkeysinslot, 0, 3) end def test_cluster_info info = redis.cluster(:info) assert_equal '3', info.fetch('cluster_size') end def test_cluster_keyslot assert_equal Redis::Cluster::KeySlotConverter.convert('hogehoge'), redis.cluster(:keyslot, 'hogehoge') assert_equal Redis::Cluster::KeySlotConverter.convert('12345'), redis.cluster(:keyslot, '12345') assert_equal Redis::Cluster::KeySlotConverter.convert('foo'), redis.cluster(:keyslot, 'boo{foo}woo') assert_equal Redis::Cluster::KeySlotConverter.convert('antirez.is.cool'), redis.cluster(:keyslot, 'antirez.is.cool') assert_equal Redis::Cluster::KeySlotConverter.convert(''), redis.cluster(:keyslot, '') end def test_cluster_meet assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER MEET command should be...') do redis.cluster(:meet, '127.0.0.1', 11_211) end end def test_cluster_nodes cluster_nodes = redis.cluster(:nodes) sample_node = cluster_nodes.first assert_equal 6, cluster_nodes.length assert_equal true, sample_node.key?('node_id') assert_equal true, sample_node.key?('ip_port') assert_equal true, sample_node.key?('flags') assert_equal true, sample_node.key?('master_node_id') assert_equal true, sample_node.key?('ping_sent') assert_equal true, sample_node.key?('pong_recv') assert_equal true, sample_node.key?('config_epoch') assert_equal true, sample_node.key?('link_state') assert_equal true, sample_node.key?('slots') end def test_cluster_replicate assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER REPLICATE command should be...') do redis.cluster(:replicate) end end def test_cluster_reset assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER RESET command should be...') do redis.cluster(:reset) end end def test_cluster_saveconfig assert_equal 'OK', redis.cluster(:saveconfig) end def test_cluster_set_config_epoch assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER SET-CONFIG-EPOCH command should be...') do redis.cluster('set-config-epoch') end end def test_cluster_setslot assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER SETSLOT command should be...') do redis.cluster(:setslot) end end def test_cluster_slaves cluster_nodes = redis.cluster(:nodes) sample_master_node_id = cluster_nodes.find { |n| n.fetch('master_node_id') == '-' }.fetch('node_id') sample_slave_node_id = cluster_nodes.find { |n| n.fetch('master_node_id') != '-' }.fetch('node_id') assert_equal 'slave', redis.cluster(:slaves, sample_master_node_id).first.fetch('flags').first assert_raises(Redis::CommandError, 'ERR The specified node is not a master') do redis.cluster(:slaves, sample_slave_node_id) end end def test_cluster_slots slots = redis.cluster(:slots) sample_slot = slots.first assert_equal 3, slots.length assert_equal true, sample_slot.key?('start_slot') assert_equal true, sample_slot.key?('end_slot') assert_equal true, sample_slot.key?('master') assert_equal true, sample_slot.fetch('master').key?('ip') assert_equal true, sample_slot.fetch('master').key?('port') assert_equal true, sample_slot.fetch('master').key?('node_id') assert_equal true, sample_slot.key?('replicas') assert_equal true, sample_slot.fetch('replicas').is_a?(Array) assert_equal true, sample_slot.fetch('replicas').first.key?('ip') assert_equal true, sample_slot.fetch('replicas').first.key?('port') assert_equal true, sample_slot.fetch('replicas').first.key?('node_id') end def test_readonly assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'READONLY command should be...') do redis.readonly end end def test_readwrite assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'READWRITE command should be...') do redis.readwrite end end end redis-rb-4.2.5/test/cluster_commands_on_connection_test.rb000066400000000000000000000016311375567530100240660ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_connection_test.rb # @see https://redis.io/commands#connection class TestClusterCommandsOnConnection < Minitest::Test include Helper::Cluster def test_auth redis_cluster_mock(auth: ->(*_) { '+OK' }) do |redis| assert_equal 'OK', redis.auth('my-password-123') end end def test_echo assert_equal 'hogehoge', redis.echo('hogehoge') end def test_ping assert_equal 'hogehoge', redis.ping('hogehoge') end def test_quit redis2 = build_another_client assert_equal 'OK', redis2.quit end def test_select assert_raises(Redis::CommandError, 'ERR SELECT is not allowed in cluster mode') do redis.select(1) end end def test_swapdb assert_raises(Redis::CommandError, 'ERR SWAPDB is not allowed in cluster mode') do redis.swapdb(1, 2) end end end redis-rb-4.2.5/test/cluster_commands_on_geo_test.rb000066400000000000000000000045371375567530100225110ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_geo_test.rb # @see https://redis.io/commands#geo class TestClusterCommandsOnGeo < Minitest::Test include Helper::Cluster MIN_REDIS_VERSION = '3.2.0' def add_sicily redis.geoadd('Sicily', 13.361389, 38.115556, 'Palermo', 15.087269, 37.502669, 'Catania') end def test_geoadd target_version(MIN_REDIS_VERSION) do assert_equal 2, add_sicily end end def test_geohash target_version(MIN_REDIS_VERSION) do add_sicily assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania]) end end def test_geopos target_version(MIN_REDIS_VERSION) do add_sicily expected = [%w[13.36138933897018433 38.11555639549629859], %w[15.08726745843887329 37.50266842333162032], nil] assert_equal expected, redis.geopos('Sicily', %w[Palermo Catania NonExisting]) end end def test_geodist target_version(MIN_REDIS_VERSION) do add_sicily assert_equal '166274.1516', redis.geodist('Sicily', 'Palermo', 'Catania') assert_equal '166.2742', redis.geodist('Sicily', 'Palermo', 'Catania', 'km') assert_equal '103.3182', redis.geodist('Sicily', 'Palermo', 'Catania', 'mi') end end def test_georadius target_version(MIN_REDIS_VERSION) do add_sicily expected = [%w[Palermo 190.4424], %w[Catania 56.4413]] assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST') expected = [['Palermo', %w[13.36138933897018433 38.11555639549629859]], ['Catania', %w[15.08726745843887329 37.50266842333162032]]] assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHCOORD') expected = [['Palermo', '190.4424', %w[13.36138933897018433 38.11555639549629859]], ['Catania', '56.4413', %w[15.08726745843887329 37.50266842333162032]]] assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD') end end def test_georadiusbymember target_version(MIN_REDIS_VERSION) do redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento') add_sicily assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km') end end end redis-rb-4.2.5/test/cluster_commands_on_hashes_test.rb000066400000000000000000000004401375567530100231770ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hashes' # ruby -w -Itest test/cluster_commands_on_hashes_test.rb # @see https://redis.io/commands#hash class TestClusterCommandsOnHashes < Minitest::Test include Helper::Cluster include Lint::Hashes end redis-rb-4.2.5/test/cluster_commands_on_hyper_log_log_test.rb000066400000000000000000000006261375567530100245630ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hyper_log_log' # ruby -w -Itest test/cluster_commands_on_hyper_log_log_test.rb # @see https://redis.io/commands#hyperloglog class TestClusterCommandsOnHyperLogLog < Minitest::Test include Helper::Cluster include Lint::HyperLogLog def test_pfmerge assert_raises Redis::CommandError do super end end end redis-rb-4.2.5/test/cluster_commands_on_keys_test.rb000066400000000000000000000072501375567530100227050ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_keys_test.rb # @see https://redis.io/commands#generic class TestClusterCommandsOnKeys < Minitest::Test include Helper::Cluster def set_some_keys redis.set('key1', 'Hello') redis.set('key2', 'World') redis.set('{key}1', 'Hello') redis.set('{key}2', 'World') end def test_del set_some_keys assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.del('key1', 'key2') end assert_equal 2, redis.del('{key}1', '{key}2') end def test_migrate redis.set('mykey', 1) assert_raises(Redis::CommandError, 'ERR Target instance replied with error: MOVED 14687 127.0.0.1:7002') do # We cannot move between cluster nodes. redis.migrate('mykey', host: '127.0.0.1', port: 7000) end redis_cluster_mock(migrate: ->(*_) { '-IOERR error or timeout writing to target instance' }) do |redis| assert_raises(Redis::CommandError, 'IOERR error or timeout writing to target instance') do redis.migrate('mykey', host: '127.0.0.1', port: 11_211) end end redis_cluster_mock(migrate: ->(*_) { '+OK' }) do |redis| assert_equal 'OK', redis.migrate('mykey', host: '127.0.0.1', port: 6379) end end def test_object redis.lpush('mylist', 'Hello World') assert_equal 1, redis.object('refcount', 'mylist') expected_encoding = version < '3.2.0' ? 'ziplist' : 'quicklist' assert_equal expected_encoding, redis.object('encoding', 'mylist') assert(redis.object('idletime', 'mylist') >= 0) redis.set('foo', 1000) assert_equal 'int', redis.object('encoding', 'foo') redis.set('bar', '1000bar') assert_equal 'embstr', redis.object('encoding', 'bar') end def test_randomkey set_some_keys assert_equal true, redis.randomkey.is_a?(String) end def test_rename set_some_keys assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.rename('key1', 'key3') end assert_equal 'OK', redis.rename('{key}1', '{key}3') end def test_renamenx set_some_keys assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.renamenx('key1', 'key2') end assert_equal false, redis.renamenx('{key}1', '{key}2') end def test_sort redis.lpush('mylist', 3) redis.lpush('mylist', 1) redis.lpush('mylist', 5) redis.lpush('mylist', 2) redis.lpush('mylist', 4) assert_equal %w[1 2 3 4 5], redis.sort('mylist') end def test_touch target_version('3.2.1') do set_some_keys assert_equal 1, redis.touch('key1') assert_equal 1, redis.touch('key2') if version < '6' assert_equal 1, redis.touch('key1', 'key2') else assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.touch('key1', 'key2') end end assert_equal 2, redis.touch('{key}1', '{key}2') end end def test_unlink target_version('4.0.0') do set_some_keys assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.unlink('key1', 'key2', 'key3') end assert_equal 2, redis.unlink('{key}1', '{key}2', '{key}3') end end def test_wait set_some_keys assert_equal 3, redis.wait(1, TIMEOUT.to_i * 1000) end def test_scan set_some_keys cursor = 0 all_keys = [] loop do cursor, keys = redis.scan(cursor, match: '{key}*') all_keys += keys break if cursor == '0' end assert_equal 2, all_keys.uniq.size end end redis-rb-4.2.5/test/cluster_commands_on_lists_test.rb000066400000000000000000000005511375567530100230650ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/lists' # ruby -w -Itest test/cluster_commands_on_lists_test.rb # @see https://redis.io/commands#list class TestClusterCommandsOnLists < Minitest::Test include Helper::Cluster include Lint::Lists def test_rpoplpush assert_raises(Redis::CommandError) { super } end end redis-rb-4.2.5/test/cluster_commands_on_pub_sub_test.rb000066400000000000000000000067261375567530100234000ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_pub_sub_test.rb # @see https://redis.io/commands#pubsub class TestClusterCommandsOnPubSub < Minitest::Test include Helper::Cluster def test_publish_subscribe_unsubscribe_pubsub sub_cnt = 0 messages = {} wire = Wire.new do redis.subscribe('channel1', 'channel2') do |on| on.subscribe { |_c, t| sub_cnt = t } on.unsubscribe { |_c, t| sub_cnt = t } on.message do |c, msg| messages[c] = msg # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments redis.unsubscribe if messages.size == 2 end end end Wire.pass until sub_cnt == 2 publisher = build_another_client assert_equal %w[channel1 channel2], publisher.pubsub(:channels) assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'cha*') assert_equal [], publisher.pubsub(:channels, 'chachacha*') assert_equal({}, publisher.pubsub(:numsub)) assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) assert_equal 0, publisher.pubsub(:numpat) publisher.publish('channel1', 'one') publisher.publish('channel2', 'two') wire.join assert_equal({ 'channel1' => 'one', 'channel2' => 'two' }, messages.sort.to_h) assert_equal [], publisher.pubsub(:channels) assert_equal [], publisher.pubsub(:channels, 'cha*') assert_equal [], publisher.pubsub(:channels, 'chachacha*') assert_equal({}, publisher.pubsub(:numsub)) assert_equal({ 'channel1' => 0, 'channel2' => 0, 'channel3' => 0 }, publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) assert_equal 0, publisher.pubsub(:numpat) end def test_publish_psubscribe_punsubscribe_pubsub sub_cnt = 0 messages = {} wire = Wire.new do redis.psubscribe('guc*', 'her*') do |on| on.psubscribe { |_c, t| sub_cnt = t } on.punsubscribe { |_c, t| sub_cnt = t } on.pmessage do |_ptn, chn, msg| messages[chn] = msg # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments redis.punsubscribe if messages.size == 2 end end end Wire.pass until sub_cnt == 2 publisher = build_another_client assert_equal [], publisher.pubsub(:channels) assert_equal [], publisher.pubsub(:channels, 'bur*') assert_equal [], publisher.pubsub(:channels, 'guc*') assert_equal [], publisher.pubsub(:channels, 'her*') assert_equal({}, publisher.pubsub(:numsub)) assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3')) assert_equal 2, publisher.pubsub(:numpat) publisher.publish('burberry1', 'one') publisher.publish('gucci2', 'two') publisher.publish('hermes3', 'three') wire.join assert_equal({ 'gucci2' => 'two', 'hermes3' => 'three' }, messages.sort.to_h) assert_equal [], publisher.pubsub(:channels) assert_equal [], publisher.pubsub(:channels, 'bur*') assert_equal [], publisher.pubsub(:channels, 'guc*') assert_equal [], publisher.pubsub(:channels, 'her*') assert_equal({}, publisher.pubsub(:numsub)) assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3')) assert_equal 0, publisher.pubsub(:numpat) end end redis-rb-4.2.5/test/cluster_commands_on_scripting_test.rb000066400000000000000000000031171375567530100237320ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_scripting_test.rb # @see https://redis.io/commands#scripting class TestClusterCommandsOnScripting < Minitest::Test include Helper::Cluster def test_eval script = 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}' argv = %w[first second] keys = %w[key1 key2] assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.eval(script, keys: keys, argv: argv) end keys = %w[{key}1 {key}2] expected = %w[{key}1 {key}2 first second] assert_equal expected, redis.eval(script, keys: keys, argv: argv) end def test_evalsha sha = redis.script(:load, 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}') expected = %w[{key}1 {key}2 first second] assert_equal expected, redis.evalsha(sha, keys: %w[{key}1 {key}2], argv: %w[first second]) end def test_script_debug target_version('3.2.0') do assert_equal 'OK', redis.script(:debug, 'yes') assert_equal 'OK', redis.script(:debug, 'no') end end def test_script_exists sha = redis.script(:load, 'return 1') assert_equal true, redis.script(:exists, sha) assert_equal false, redis.script(:exists, 'unknownsha') end def test_script_flush assert_equal 'OK', redis.script(:flush) end def test_script_kill redis_cluster_mock(kill: -> { '+OK' }) do |redis| assert_equal 'OK', redis.script(:kill) end end def test_script_load assert_equal 'e0e1f9fabfc9d4800c877a703b823ac0578ff8db', redis.script(:load, 'return 1') end end redis-rb-4.2.5/test/cluster_commands_on_server_test.rb000066400000000000000000000133751375567530100232450ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_server_test.rb # @see https://redis.io/commands#server class TestClusterCommandsOnServer < Minitest::Test include Helper::Cluster def test_bgrewriteaof assert_equal 'Background append only file rewriting started', redis.bgrewriteaof end def test_bgsave redis_cluster_mock(bgsave: ->(*_) { '+OK' }) do |redis| assert_equal 'OK', redis.bgsave end err_msg = 'ERR An AOF log rewriting in progress: '\ "can't BGSAVE right now. "\ 'Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever possible.' redis_cluster_mock(bgsave: ->(*_) { "-Error #{err_msg}" }) do |redis| assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do redis.bgsave end end end def test_client_kill redis_cluster_mock(client: ->(*_) { '-Error ERR No such client' }) do |redis| assert_raises(Redis::CommandError, 'ERR No such client') do redis.client(:kill, '127.0.0.1:6379') end end redis_cluster_mock(client: ->(*_) { '+OK' }) do |redis| assert_equal 'OK', redis.client(:kill, '127.0.0.1:6379') end end def test_client_list a_client_info = redis.client(:list).first actual = a_client_info.keys.sort expected = %w[addr age cmd db events fd flags id idle multi name obl oll omem psub qbuf qbuf-free sub] expected << 'user' << 'argv-mem' << 'tot-mem' if version >= '6' assert_equal expected.sort, actual.sort end def test_client_getname redis.client(:setname, 'my-client-01') assert_equal 'my-client-01', redis.client(:getname) end def test_client_pause assert_equal 'OK', redis.client(:pause, 0) end def test_client_reply target_version('3.2.0') do assert_equal 'OK', redis.client(:reply, 'ON') end end def test_client_setname assert_equal 'OK', redis.client(:setname, 'my-client-01') end def test_command assert_instance_of Array, redis.command end def test_command_count assert_equal true, (redis.command(:count) > 0) end def test_command_getkeys assert_equal %w[a c e], redis.command(:getkeys, :mset, 'a', 'b', 'c', 'd', 'e', 'f') end def test_command_info expected = [ ['get', 2, %w[readonly fast], 1, 1, 1], ['set', -3, %w[write denyoom], 1, 1, 1], ['eval', -3, %w[noscript movablekeys], 0, 0, 0] ] if version >= '6' expected[0] << ["@read", "@string", "@fast"] expected[1] << ["@write", "@string", "@slow"] expected[2] << ["@slow", "@scripting"] end assert_equal expected, redis.command(:info, :get, :set, :eval) end def test_config_get expected_keys = if version < '3.2.0' %w[hash-max-ziplist-entries list-max-ziplist-entries set-max-intset-entries zset-max-ziplist-entries] else %w[hash-max-ziplist-entries set-max-intset-entries zset-max-ziplist-entries] end assert_equal expected_keys, redis.config(:get, '*max-*-entries*').keys.sort end def test_config_rewrite redis_cluster_mock(config: ->(*_) { '-Error ERR Rewriting config file: Permission denied' }) do |redis| assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do redis.config(:rewrite) end end redis_cluster_mock(config: ->(*_) { '+OK' }) do |redis| assert_equal 'OK', redis.config(:rewrite) end end def test_config_set assert_equal 'OK', redis.config(:set, 'hash-max-ziplist-entries', 512) end def test_config_resetstat assert_equal 'OK', redis.config(:resetstat) end def test_config_db_size 10.times { |i| redis.set("key#{i}", 1) } assert_equal 10, redis.dbsize end def test_debug_object # DEBUG OBJECT is a debugging command that should not be used by clients. end def test_debug_segfault # DEBUG SEGFAULT performs an invalid memory access that crashes Redis. # It is used to simulate bugs during the development. end def test_flushall assert_equal 'OK', redis.flushall end def test_flushdb assert_equal 'OK', redis.flushdb end def test_info assert_equal({ 'cluster_enabled' => '1' }, redis.info(:cluster)) end def test_lastsave assert_instance_of Array, redis.lastsave end def test_memory_doctor target_version('4.0.0') do assert_instance_of String, redis.memory(:doctor) end end def test_memory_help target_version('4.0.0') do assert_instance_of Array, redis.memory(:help) end end def test_memory_malloc_stats target_version('4.0.0') do assert_instance_of String, redis.memory('malloc-stats') end end def test_memory_purge target_version('4.0.0') do assert_equal 'OK', redis.memory(:purge) end end def test_memory_stats target_version('4.0.0') do assert_instance_of Array, redis.memory(:stats) end end def test_memory_usage target_version('4.0.0') do redis.set('key1', 'Hello World') assert_operator redis.memory(:usage, 'key1'), :>, 0 end end def test_monitor # Add MONITOR command test end def test_role assert_equal %w[master master master], redis.role.map(&:first) end def test_save assert_equal 'OK', redis.save end def test_shutdown assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'SHUTDOWN command should be...') do redis.shutdown end end def test_slaveof assert_raises(Redis::CommandError, 'ERR SLAVEOF not allowed in cluster mode.') do redis.slaveof(:no, :one) end end def test_slowlog assert_instance_of Array, redis.slowlog(:get, 1) end def test_sync # Internal command used for replication end def test_time assert_instance_of Array, redis.time end end redis-rb-4.2.5/test/cluster_commands_on_sets_test.rb000066400000000000000000000014511375567530100227050ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sets' # ruby -w -Itest test/cluster_commands_on_sets_test.rb # @see https://redis.io/commands#set class TestClusterCommandsOnSets < Minitest::Test include Helper::Cluster include Lint::Sets def test_sdiff assert_raises(Redis::CommandError) { super } end def test_sdiffstore assert_raises(Redis::CommandError) { super } end def test_sinter assert_raises(Redis::CommandError) { super } end def test_sinterstore assert_raises(Redis::CommandError) { super } end def test_smove assert_raises(Redis::CommandError) { super } end def test_sunion assert_raises(Redis::CommandError) { super } end def test_sunionstore assert_raises(Redis::CommandError) { super } end end redis-rb-4.2.5/test/cluster_commands_on_sorted_sets_test.rb000066400000000000000000000015121375567530100242630ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sorted_sets' # ruby -w -Itest test/cluster_commands_on_sorted_sets_test.rb # @see https://redis.io/commands#sorted_set class TestClusterCommandsOnSortedSets < Minitest::Test include Helper::Cluster include Lint::SortedSets def test_zinterstore assert_raises(Redis::CommandError) { super } end def test_zinterstore_with_aggregate assert_raises(Redis::CommandError) { super } end def test_zinterstore_with_weights assert_raises(Redis::CommandError) { super } end def test_zunionstore assert_raises(Redis::CommandError) { super } end def test_zunionstore_with_aggregate assert_raises(Redis::CommandError) { super } end def test_zunionstore_with_weights assert_raises(Redis::CommandError) { super } end end redis-rb-4.2.5/test/cluster_commands_on_streams_test.rb000066400000000000000000000034571375567530100234150ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/streams' # ruby -w -Itest test/cluster_commands_on_streams_test.rb # @see https://redis.io/commands#stream class TestClusterCommandsOnStreams < Minitest::Test include Helper::Cluster include Lint::Streams def test_xread_with_multiple_keys err_msg = "CROSSSLOT Keys in request don't hash to the same slot" assert_raises(Redis::CommandError, err_msg) { super } end def test_xread_with_multiple_keys_and_hash_tags redis.xadd('{s}1', { f: 'v01' }, id: '0-1') redis.xadd('{s}1', { f: 'v02' }, id: '0-2') redis.xadd('{s}2', { f: 'v11' }, id: '1-1') redis.xadd('{s}2', { f: 'v12' }, id: '1-2') actual = redis.xread(%w[{s}1 {s}2], %w[0-1 1-1]) assert_equal %w(0-2), actual['{s}1'].map(&:first) assert_equal(%w(v02), actual['{s}1'].map { |i| i.last['f'] }) assert_equal %w(1-2), actual['{s}2'].map(&:first) assert_equal(%w(v12), actual['{s}2'].map { |i| i.last['f'] }) end def test_xreadgroup_with_multiple_keys err_msg = "CROSSSLOT Keys in request don't hash to the same slot" assert_raises(Redis::CommandError, err_msg) { super } end def test_xreadgroup_with_multiple_keys_and_hash_tags redis.xadd('{s}1', { f: 'v01' }, id: '0-1') redis.xgroup(:create, '{s}1', 'g1', '$') redis.xadd('{s}2', { f: 'v11' }, id: '1-1') redis.xgroup(:create, '{s}2', 'g1', '$') redis.xadd('{s}1', { f: 'v02' }, id: '0-2') redis.xadd('{s}2', { f: 'v12' }, id: '1-2') actual = redis.xreadgroup('g1', 'c1', %w[{s}1 {s}2], %w[> >]) assert_equal %w(0-2), actual['{s}1'].map(&:first) assert_equal(%w(v02), actual['{s}1'].map { |i| i.last['f'] }) assert_equal %w(1-2), actual['{s}2'].map(&:first) assert_equal(%w(v12), actual['{s}2'].map { |i| i.last['f'] }) end end redis-rb-4.2.5/test/cluster_commands_on_strings_test.rb000066400000000000000000000005551375567530100234240ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/strings' # ruby -w -Itest test/cluster_commands_on_strings_test.rb # @see https://redis.io/commands#string class TestClusterCommandsOnStrings < Minitest::Test include Helper::Cluster include Lint::Strings def mock(*args, &block) redis_cluster_mock(*args, &block) end end redis-rb-4.2.5/test/cluster_commands_on_transactions_test.rb000066400000000000000000000016151375567530100244410ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # ruby -w -Itest test/cluster_commands_on_transactions_test.rb # @see https://redis.io/commands#transactions class TestClusterCommandsOnTransactions < Minitest::Test include Helper::Cluster def test_discard assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.discard end end def test_exec assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.exec end end def test_multi assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.multi end end def test_unwatch assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.unwatch end end def test_watch assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do redis.watch('key1', 'key2') end assert_equal 'OK', redis.watch('{key}1', '{key}2') end end redis-rb-4.2.5/test/cluster_commands_on_value_types_test.rb000066400000000000000000000005241375567530100242670ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/value_types' # ruby -w -Itest test/cluster_commands_on_value_types_test.rb class TestClusterCommandsOnValueTypes < Minitest::Test include Helper::Cluster include Lint::ValueTypes def test_move assert_raises(Redis::CommandError) { super } end end redis-rb-4.2.5/test/command_map_test.rb000066400000000000000000000010571375567530100200660ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestCommandMap < Minitest::Test include Helper::Client def test_override_existing_commands r.set("counter", 1) assert_equal 2, r.incr("counter") r._client.command_map[:incr] = :decr assert_equal 1, r.incr("counter") end def test_override_non_existing_commands r.set("key", "value") assert_raises Redis::CommandError do r.idontexist("key") end r._client.command_map[:idontexist] = :get assert_equal "value", r.idontexist("key") end end redis-rb-4.2.5/test/commands_on_geo_test.rb000066400000000000000000000077621375567530100207530ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestCommandsGeo < Minitest::Test include Helper::Client def setup super target_version "3.2.0" do added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania") assert_equal 2, added_items_count end end def test_geoadd_with_array_params target_version "3.2.0" do added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"]) assert_equal 2, added_items_count end end def test_georadius_with_same_params target_version "3.2.0" do r.geoadd("Chad", 15, 15, "Kanem") nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc') assert_equal %w(Kanem), nearest_cities end end def test_georadius_with_sort target_version "3.2.0" do nearest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'asc') assert_equal %w(Catania Palermo), nearest_cities farthest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'desc') assert_equal %w(Palermo Catania), farthest_cities end end def test_georadius_with_count target_version "3.2.0" do city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1) assert_equal %w(Catania), city end end def test_georadius_with_options_count_sort target_version "3.2.0" do city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1) assert_equal [["Palermo", "190.4424"]], city end end def test_georadiusbymember_with_sort target_version "3.2.0" do nearest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'asc') assert_equal %w(Catania Palermo), nearest_cities farthest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'desc') assert_equal %w(Palermo Catania), farthest_cities end end def test_georadiusbymember_with_count target_version "3.2.0" do city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1) assert_equal %w(Catania), city end end def test_georadiusbymember_with_options_count_sort target_version "3.2.0" do city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1) assert_equal [["Palermo", "166.2742"]], city end end def test_geopos target_version "3.2.0" do location = r.geopos("Sicily", "Catania") assert_equal [["15.08726745843887329", "37.50266842333162032"]], location locations = r.geopos("Sicily", ["Palermo", "Catania"]) assert_equal [["13.36138933897018433", "38.11555639549629859"], ["15.08726745843887329", "37.50266842333162032"]], locations end end def test_geopos_nonexistant_location target_version "3.2.0" do location = r.geopos("Sicily", "Rome") assert_equal [nil], location locations = r.geopos("Sicily", ["Rome", "Catania"]) assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations end end def test_geodist target_version "3.2.0" do distination_in_meters = r.geodist("Sicily", "Palermo", "Catania") assert_equal "166274.1516", distination_in_meters distination_in_feet = r.geodist("Sicily", "Palermo", "Catania", 'ft') assert_equal "545518.8700", distination_in_feet end end def test_geodist_with_nonexistant_location target_version "3.2.0" do distination = r.geodist("Sicily", "Palermo", "Rome") assert_nil distination end end def test_geohash target_version "3.2.0" do geohash = r.geohash("Sicily", "Palermo") assert_equal ["sqc8b49rny0"], geohash geohashes = r.geohash("Sicily", ["Palermo", "Catania"]) assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes end end def test_geohash_with_nonexistant_location target_version "3.2.0" do geohashes = r.geohash("Sicily", ["Palermo", "Rome"]) assert_equal ["sqc8b49rny0", nil], geohashes end end end redis-rb-4.2.5/test/commands_on_hashes_test.rb000066400000000000000000000002711375567530100214400ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hashes' class TestCommandsOnHashes < Minitest::Test include Helper::Client include Lint::Hashes end redis-rb-4.2.5/test/commands_on_hyper_log_log_test.rb000066400000000000000000000003121375567530100230120ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hyper_log_log' class TestCommandsOnHyperLogLog < Minitest::Test include Helper::Client include Lint::HyperLogLog end redis-rb-4.2.5/test/commands_on_lists_test.rb000066400000000000000000000002661375567530100213270ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/lists' class TestCommandsOnLists < Minitest::Test include Helper::Client include Lint::Lists end redis-rb-4.2.5/test/commands_on_sets_test.rb000066400000000000000000000002631375567530100211440ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sets' class TestCommandsOnSets < Minitest::Test include Helper::Client include Lint::Sets end redis-rb-4.2.5/test/commands_on_sorted_sets_test.rb000066400000000000000000000003061375567530100225220ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sorted_sets' class TestCommandsOnSortedSets < Minitest::Test include Helper::Client include Lint::SortedSets end redis-rb-4.2.5/test/commands_on_streams_test.rb000066400000000000000000000004261375567530100216450ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/streams' # ruby -w -Itest test/commands_on_streams_test.rb # @see https://redis.io/commands#stream class TestCommandsOnStreams < Minitest::Test include Helper::Client include Lint::Streams end redis-rb-4.2.5/test/commands_on_strings_test.rb000066400000000000000000000002741375567530100216610ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/strings' class TestCommandsOnStrings < Minitest::Test include Helper::Client include Lint::Strings end redis-rb-4.2.5/test/commands_on_value_types_test.rb000066400000000000000000000123761375567530100225360ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" require_relative "lint/value_types" class TestCommandsOnValueTypes < Minitest::Test include Helper::Client include Lint::ValueTypes def test_del r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.del("foo") assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.del("bar", "baz") assert_equal [], r.keys("*").sort end def test_del_with_array_argument r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.del(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.del(["bar", "baz"]) assert_equal [], r.keys("*").sort end def test_unlink target_version "4.0.0" do r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.unlink("foo") assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink("bar", "baz") assert_equal [], r.keys("*").sort end end def test_unlink_with_array_argument target_version "4.0.0" do r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.unlink(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink(["bar", "baz"]) assert_equal [], r.keys("*").sort end end def test_randomkey assert r.randomkey.to_s.empty? r.set("foo", "s1") assert_equal "foo", r.randomkey r.set("bar", "s2") 4.times do assert ["foo", "bar"].include?(r.randomkey) end end def test_rename r.set("foo", "s1") r.rename "foo", "bar" assert_equal "s1", r.get("bar") assert_nil r.get("foo") end def test_renamenx r.set("foo", "s1") r.set("bar", "s2") assert_equal false, r.renamenx("foo", "bar") assert_equal "s1", r.get("foo") assert_equal "s2", r.get("bar") end def test_dbsize assert_equal 0, r.dbsize r.set("foo", "s1") assert_equal 1, r.dbsize end def test_flushdb # Test defaults r.set("foo", "s1") r.set("bar", "s2") assert_equal 2, r.dbsize r.flushdb assert_equal 0, r.dbsize # Test sync r.set("foo", "s1") r.set("bar", "s2") assert_equal 2, r.dbsize r.flushdb(async: false) assert_equal 0, r.dbsize # Test async target_version "3.9.101" do r.set("foo", "s1") r.set("bar", "s2") assert_equal 2, r.dbsize r.flushdb(async: true) assert_equal 0, r.dbsize redis_mock(flushdb: ->(args) { "+FLUSHDB #{args.upcase}" }) do |redis| assert_equal "FLUSHDB ASYNC", redis.flushdb(async: true) end end end def test_flushall # Test defaults redis_mock(flushall: -> { "+FLUSHALL" }) do |redis| assert_equal "FLUSHALL", redis.flushall end # Test sync redis_mock(flushall: -> { "+FLUSHALL" }) do |redis| assert_equal "FLUSHALL", redis.flushall(async: false) end # Test async target_version "3.9.101" do redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis| assert_equal "FLUSHALL ASYNC", redis.flushall(async: true) end end end def test_migrate redis_mock(migrate: ->(*args) { args }) do |redis| options = { host: "127.0.0.1", port: 1234 } ex = assert_raises(RuntimeError) do redis.migrate("foo", options.reject { |key, _| key == :host }) end assert ex.message =~ /host not specified/ ex = assert_raises(RuntimeError) do redis.migrate("foo", options.reject { |key, _| key == :port }) end assert ex.message =~ /port not specified/ default_db = redis._client.db.to_i default_timeout = redis._client.timeout.to_i # Test defaults actual = redis.migrate("foo", options) expected = ["127.0.0.1", "1234", "foo", default_db.to_s, default_timeout.to_s] assert_equal expected, actual # Test db override actual = redis.migrate("foo", options.merge(db: default_db + 1)) expected = ["127.0.0.1", "1234", "foo", (default_db + 1).to_s, default_timeout.to_s] assert_equal expected, actual # Test timeout override actual = redis.migrate("foo", options.merge(timeout: default_timeout + 1)) expected = ["127.0.0.1", "1234", "foo", default_db.to_s, (default_timeout + 1).to_s] assert_equal expected, actual # Test copy override actual = redis.migrate('foo', options.merge(copy: true)) expected = ['127.0.0.1', '1234', 'foo', default_db.to_s, default_timeout.to_s, 'COPY'] assert_equal expected, actual # Test replace override actual = redis.migrate('foo', options.merge(replace: true)) expected = ['127.0.0.1', '1234', 'foo', default_db.to_s, default_timeout.to_s, 'REPLACE'] assert_equal expected, actual # Test multiple keys actual = redis.migrate(%w[foo bar baz], options) expected = ['127.0.0.1', '1234', '', default_db.to_s, default_timeout.to_s, 'KEYS', 'foo', 'bar', 'baz'] assert_equal expected, actual end end end redis-rb-4.2.5/test/connection_handling_test.rb000066400000000000000000000131071375567530100216150ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestConnectionHandling < Minitest::Test include Helper::Client def test_auth commands = { auth: ->(password) { @auth = password; "+OK" }, get: ->(_key) { @auth == "secret" ? "$3\r\nbar" : "$-1" } } redis_mock(commands, password: "secret") do |redis| assert_equal "bar", redis.get("foo") end end def test_id commands = { client: ->(cmd, name) { @name = [cmd, name]; "+OK" }, ping: -> { "+PONG" } } redis_mock(commands, id: "client-name") do |redis| assert_equal "PONG", redis.ping end assert_equal ["setname", "client-name"], @name end def test_ping assert_equal "PONG", r.ping end def test_select r.set "foo", "bar" r.select 14 assert_nil r.get("foo") r._client.disconnect assert_nil r.get("foo") end def test_quit r.quit assert !r._client.connected? end def test_close quit = 0 commands = { quit: lambda do quit += 1 "+OK" end } redis_mock(commands) do |redis| assert_equal 0, quit redis.quit assert_equal 1, quit redis.ping redis.close assert_equal 1, quit assert !redis.connected? end end def test_disconnect quit = 0 commands = { quit: lambda do quit += 1 "+OK" end } redis_mock(commands) do |redis| assert_equal 0, quit redis.quit assert_equal 1, quit redis.ping redis.disconnect! assert_equal 1, quit assert !redis.connected? end end def test_shutdown commands = { shutdown: -> { :exit } } redis_mock(commands) do |redis| # SHUTDOWN does not reply: test that it does not raise here. assert_nil redis.shutdown end end def test_shutdown_with_error connections = 0 commands = { select: ->(*_) { connections += 1; "+OK\r\n" }, connections: -> { ":#{connections}\r\n" }, shutdown: -> { "-ERR could not shutdown\r\n" } } redis_mock(commands) do |redis| connections = redis.connections # SHUTDOWN replies with an error: test that it gets raised assert_raises Redis::CommandError do redis.shutdown end # The connection should remain in tact assert_equal connections, redis.connections end end def test_shutdown_from_pipeline commands = { shutdown: -> { :exit } } redis_mock(commands) do |redis| result = redis.pipelined do redis.shutdown end assert_nil result assert !redis._client.connected? end end def test_shutdown_with_error_from_pipeline connections = 0 commands = { select: ->(*_) { connections += 1; "+OK\r\n" }, connections: -> { ":#{connections}\r\n" }, shutdown: -> { "-ERR could not shutdown\r\n" } } redis_mock(commands) do |redis| connections = redis.connections # SHUTDOWN replies with an error: test that it gets raised assert_raises Redis::CommandError do redis.pipelined do redis.shutdown end end # The connection should remain in tact assert_equal connections, redis.connections end end def test_shutdown_from_multi_exec commands = { multi: -> { "+OK\r\n" }, shutdown: -> { "+QUEUED\r\n" }, exec: -> { :exit } } redis_mock(commands) do |redis| result = redis.multi do redis.shutdown end assert_nil result assert !redis._client.connected? end end def test_shutdown_with_error_from_multi_exec connections = 0 commands = { select: ->(*_) { connections += 1; "+OK\r\n" }, connections: -> { ":#{connections}\r\n" }, multi: -> { "+OK\r\n" }, shutdown: -> { "+QUEUED\r\n" }, exec: -> { "*1\r\n-ERR could not shutdown\r\n" } } redis_mock(commands) do |redis| connections = redis.connections # SHUTDOWN replies with an error: test that it gets returned # We should test for Redis::CommandError here, but hiredis doesn't yet do # custom error classes. err = nil begin redis.multi { redis.shutdown } rescue => err end assert err.is_a?(StandardError) # The connection should remain intact assert_equal connections, redis.connections end end def test_slaveof redis_mock(slaveof: ->(host, port) { "+SLAVEOF #{host} #{port}" }) do |redis| assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381) end end def test_bgrewriteaof redis_mock(bgrewriteaof: -> { "+BGREWRITEAOF" }) do |redis| assert_equal "BGREWRITEAOF", redis.bgrewriteaof end end def test_config_get refute_nil r.config(:get, "*")["timeout"] config = r.config(:get, "timeout") assert_equal ["timeout"], config.keys assert !config.values.compact.empty? end def test_config_set assert_equal "OK", r.config(:set, "timeout", 200) assert_equal "200", r.config(:get, "*")["timeout"] assert_equal "OK", r.config(:set, "timeout", 100) assert_equal "100", r.config(:get, "*")["timeout"] ensure r.config :set, "timeout", 300 end driver(:ruby, :hiredis) do def test_consistency_on_multithreaded_env t = nil commands = { set: ->(_key, _value) { t.kill; "+OK\r\n" }, incr: ->(_key) { ":1\r\n" } } redis_mock(commands) do |redis| t = Thread.new do redis.set("foo", "bar") end t.join assert_equal 1, redis.incr("baz") end end end end redis-rb-4.2.5/test/connection_test.rb000066400000000000000000000036421375567530100177540ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestConnection < Minitest::Test include Helper::Client def test_provides_a_meaningful_inspect assert_equal "#", r.inspect end def test_connection_information assert_equal "127.0.0.1", r.connection.fetch(:host) assert_equal 6381, r.connection.fetch(:port) assert_equal 15, r.connection.fetch(:db) assert_equal "127.0.0.1:6381", r.connection.fetch(:location) assert_equal "redis://127.0.0.1:6381/15", r.connection.fetch(:id) end def test_default_id_with_host_and_port redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0)) assert_equal "redis://host:1234/0", redis.connection.fetch(:id) end def test_default_id_with_host_and_port_and_explicit_scheme redis = Redis.new(OPTIONS.merge(host: "host", port: "1234", db: 0, scheme: "foo")) assert_equal "redis://host:1234/0", redis.connection.fetch(:id) end def test_default_id_with_path redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0)) assert_equal "redis:///tmp/redis.sock/0", redis.connection.fetch(:id) end def test_default_id_with_path_and_explicit_scheme redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0, scheme: "foo")) assert_equal "redis:///tmp/redis.sock/0", redis.connection.fetch(:id) end def test_override_id redis = Redis.new(OPTIONS.merge(id: "test")) assert_equal "test", redis.connection.fetch(:id) end def test_id_inside_multi redis = Redis.new(OPTIONS) id = nil connection_id = nil redis.multi do id = redis.id connection_id = redis.connection.fetch(:id) end assert_equal "redis://127.0.0.1:6381/15", id assert_equal "redis://127.0.0.1:6381/15", connection_id end end redis-rb-4.2.5/test/db/000077500000000000000000000000001375567530100146115ustar00rootroot00000000000000redis-rb-4.2.5/test/db/.gitkeep000066400000000000000000000000001375567530100162300ustar00rootroot00000000000000redis-rb-4.2.5/test/distributed_blocking_commands_test.rb000066400000000000000000000022201375567530100236570ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/blocking_commands' class TestDistributedBlockingCommands < Minitest::Test include Helper::Distributed include Lint::BlockingCommands def test_blpop_raises assert_raises(Redis::Distributed::CannotDistribute) do r.blpop(%w[foo bar]) end end def test_blpop_raises_with_old_prototype assert_raises(Redis::Distributed::CannotDistribute) do r.blpop('foo', 'bar', 0) end end def test_brpop_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpop(%w[foo bar]) end end def test_brpop_raises_with_old_prototype assert_raises(Redis::Distributed::CannotDistribute) do r.brpop('foo', 'bar', 0) end end def test_brpoplpush_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpoplpush('foo', 'bar') end end def test_brpoplpush_raises_with_old_prototype assert_raises(Redis::Distributed::CannotDistribute) do r.brpoplpush('foo', 'bar', 0) end end def test_bzpopmin # Not implemented yet end def test_bzpopmax # Not implemented yet end end redis-rb-4.2.5/test/distributed_commands_on_hashes_test.rb000066400000000000000000000006701375567530100240450ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hashes' class TestDistributedCommandsOnHashes < Minitest::Test include Helper::Distributed include Lint::Hashes def test_hscan # Not implemented yet end def test_hstrlen # Not implemented yet end def test_mapped_hmget_in_a_pipeline_returns_hash assert_raises(Redis::Distributed::CannotDistribute) do super end end end redis-rb-4.2.5/test/distributed_commands_on_hyper_log_log_test.rb000066400000000000000000000011641375567530100254220ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/hyper_log_log' class TestDistributedCommandsOnHyperLogLog < Minitest::Test include Helper::Distributed include Lint::HyperLogLog def test_pfmerge target_version '2.8.9' do assert_raises Redis::Distributed::CannotDistribute do super end end end def test_pfcount_multiple_keys_diff_nodes target_version '2.8.9' do assert_raises Redis::Distributed::CannotDistribute do r.pfadd 'foo', 's1' r.pfadd 'bar', 's2' assert r.pfcount('res', 'foo', 'bar') end end end end redis-rb-4.2.5/test/distributed_commands_on_lists_test.rb000066400000000000000000000007201375567530100237240ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/lists' class TestDistributedCommandsOnLists < Minitest::Test include Helper::Distributed include Lint::Lists def test_rpoplpush assert_raises Redis::Distributed::CannotDistribute do r.rpoplpush('foo', 'bar') end end def test_brpoplpush assert_raises Redis::Distributed::CannotDistribute do r.brpoplpush('foo', 'bar', timeout: 1) end end end redis-rb-4.2.5/test/distributed_commands_on_sets_test.rb000066400000000000000000000040751375567530100235530ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sets' class TestDistributedCommandsOnSets < Minitest::Test include Helper::Distributed include Lint::Sets def test_smove assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'bar', 's2' r.smove('foo', 'bar', 's1') end end def test_sinter assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sinter('foo', 'bar') end end def test_sinterstore assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sinterstore('baz', 'foo', 'bar') end end def test_sunion assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sunion('foo', 'bar') end end def test_sunionstore assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sunionstore('baz', 'foo', 'bar') end end def test_sdiff assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sdiff('foo', 'bar') end end def test_sdiffstore assert_raises Redis::Distributed::CannotDistribute do r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sdiffstore('baz', 'foo', 'bar') end end def test_sscan r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' cursor, vals = r.sscan 'foo', 0 assert_equal '0', cursor assert_equal %w[s1 s2], vals.sort end def test_sscan_each r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' vals = r.sscan_each('foo').to_a assert_equal %w[s1 s2], vals.sort end end redis-rb-4.2.5/test/distributed_commands_on_sorted_sets_test.rb000066400000000000000000000023141375567530100251250ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' require_relative 'lint/sorted_sets' class TestDistributedCommandsOnSortedSets < Minitest::Test include Helper::Distributed include Lint::SortedSets def test_zinterstore assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zinterstore_with_aggregate assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zinterstore_with_weights assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zlexcount # Not implemented yet end def test_zpopmax # Not implemented yet end def test_zpopmin # Not implemented yet end def test_zrangebylex # Not implemented yet end def test_zremrangebylex # Not implemented yet end def test_zrevrangebylex # Not implemented yet end def test_zscan # Not implemented yet end def test_zunionstore assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zunionstore_with_aggregate assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zunionstore_with_weights assert_raises(Redis::Distributed::CannotDistribute) { super } end end redis-rb-4.2.5/test/distributed_commands_on_strings_test.rb000066400000000000000000000033351375567530100242640ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" require_relative "lint/strings" class TestDistributedCommandsOnStrings < Minitest::Test include Helper::Distributed include Lint::Strings def test_mget r.set("foo", "s1") r.set("bar", "s2") assert_equal ["s1", "s2"], r.mget("foo", "bar") assert_equal ["s1", "s2", nil], r.mget("foo", "bar", "baz") end def test_mget_mapped r.set("foo", "s1") r.set("bar", "s2") response = r.mapped_mget("foo", "bar") assert_equal "s1", response["foo"] assert_equal "s2", response["bar"] response = r.mapped_mget("foo", "bar", "baz") assert_equal "s1", response["foo"] assert_equal "s2", response["bar"] assert_nil response["baz"] end def test_mset assert_raises Redis::Distributed::CannotDistribute do r.mset(:foo, "s1", :bar, "s2") end end def test_mset_mapped assert_raises Redis::Distributed::CannotDistribute do r.mapped_mset(foo: "s1", bar: "s2") end end def test_msetnx assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "s1") r.msetnx(:foo, "s2", :bar, "s3") end end def test_msetnx_mapped assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "s1") r.mapped_msetnx(foo: "s2", bar: "s3") end end def test_bitop target_version "2.5.10" do assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "a") r.set("bar", "b") r.bitop(:and, "foo&bar", "foo", "bar") end end end def test_mapped_mget_in_a_pipeline_returns_hash assert_raises Redis::Distributed::CannotDistribute do super end end def test_bitfield # Not implemented yet end end redis-rb-4.2.5/test/distributed_commands_on_value_types_test.rb000066400000000000000000000050121375567530100251250ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" require_relative "lint/value_types" class TestDistributedCommandsOnValueTypes < Minitest::Test include Helper::Distributed include Lint::ValueTypes def test_del r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.del("foo") assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.del("bar", "baz") assert_equal [], r.keys("*").sort end def test_del_with_array_argument r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.del(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.del(["bar", "baz"]) assert_equal [], r.keys("*").sort end def test_unlink target_version "4.0.0" do r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.unlink("foo") assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink("bar", "baz") assert_equal [], r.keys("*").sort end end def test_unlink_with_array_argument target_version "4.0.0" do r.set "foo", "s1" r.set "bar", "s2" r.set "baz", "s3" assert_equal ["bar", "baz", "foo"], r.keys("*").sort assert_equal 1, r.unlink(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink(["bar", "baz"]) assert_equal [], r.keys("*").sort end end def test_randomkey assert_raises Redis::Distributed::CannotDistribute do r.randomkey end end def test_rename assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "s1") r.rename "foo", "bar" end assert_equal "s1", r.get("foo") assert_nil r.get("bar") end def test_renamenx assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "s1") r.rename "foo", "bar" end assert_equal "s1", r.get("foo") assert_nil r.get("bar") end def test_dbsize assert_equal [0], r.dbsize r.set("foo", "s1") assert_equal [1], r.dbsize end def test_flushdb r.set("foo", "s1") r.set("bar", "s2") assert_equal [2], r.dbsize r.flushdb assert_equal [0], r.dbsize end def test_migrate r.set("foo", "s1") assert_raises Redis::Distributed::CannotDistribute do r.migrate("foo", {}) end end end redis-rb-4.2.5/test/distributed_commands_requiring_clustering_test.rb000066400000000000000000000103431375567530100263400ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedCommandsRequiringClustering < Minitest::Test include Helper::Distributed def test_rename r.set("{qux}foo", "s1") r.rename "{qux}foo", "{qux}bar" assert_equal "s1", r.get("{qux}bar") assert_nil r.get("{qux}foo") end def test_renamenx r.set("{qux}foo", "s1") r.set("{qux}bar", "s2") assert_equal false, r.renamenx("{qux}foo", "{qux}bar") assert_equal "s1", r.get("{qux}foo") assert_equal "s2", r.get("{qux}bar") end def test_brpoplpush r.rpush "{qux}foo", "s1" r.rpush "{qux}foo", "s2" assert_equal "s2", r.brpoplpush("{qux}foo", "{qux}bar", timeout: 1) assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) end def test_rpoplpush r.rpush "{qux}foo", "s1" r.rpush "{qux}foo", "s2" assert_equal "s2", r.rpoplpush("{qux}foo", "{qux}bar") assert_equal ["s2"], r.lrange("{qux}bar", 0, -1) assert_equal "s1", r.rpoplpush("{qux}foo", "{qux}bar") assert_equal ["s1", "s2"], r.lrange("{qux}bar", 0, -1) end def test_smove r.sadd "{qux}foo", "s1" r.sadd "{qux}bar", "s2" assert r.smove("{qux}foo", "{qux}bar", "s1") assert r.sismember("{qux}bar", "s1") end def test_sinter r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" assert_equal ["s2"], r.sinter("{qux}foo", "{qux}bar") end def test_sinterstore r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" r.sinterstore("{qux}baz", "{qux}foo", "{qux}bar") assert_equal ["s2"], r.smembers("{qux}baz") end def test_sunion r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" r.sadd "{qux}bar", "s3" assert_equal ["s1", "s2", "s3"], r.sunion("{qux}foo", "{qux}bar").sort end def test_sunionstore r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" r.sadd "{qux}bar", "s3" r.sunionstore("{qux}baz", "{qux}foo", "{qux}bar") assert_equal ["s1", "s2", "s3"], r.smembers("{qux}baz").sort end def test_sdiff r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" r.sadd "{qux}bar", "s3" assert_equal ["s1"], r.sdiff("{qux}foo", "{qux}bar") assert_equal ["s3"], r.sdiff("{qux}bar", "{qux}foo") end def test_sdiffstore r.sadd "{qux}foo", "s1" r.sadd "{qux}foo", "s2" r.sadd "{qux}bar", "s2" r.sadd "{qux}bar", "s3" r.sdiffstore("{qux}baz", "{qux}foo", "{qux}bar") assert_equal ["s1"], r.smembers("{qux}baz") end def test_sort r.set("{qux}foo:1", "s1") r.set("{qux}foo:2", "s2") r.rpush("{qux}bar", "1") r.rpush("{qux}bar", "2") assert_equal ["s1"], r.sort("{qux}bar", get: "{qux}foo:*", limit: [0, 1]) assert_equal ["s2"], r.sort("{qux}bar", get: "{qux}foo:*", limit: [0, 1], order: "desc alpha") end def test_sort_with_an_array_of_gets r.set("{qux}foo:1:a", "s1a") r.set("{qux}foo:1:b", "s1b") r.set("{qux}foo:2:a", "s2a") r.set("{qux}foo:2:b", "s2b") r.rpush("{qux}bar", "1") r.rpush("{qux}bar", "2") assert_equal [["s1a", "s1b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"], limit: [0, 1]) assert_equal [["s2a", "s2b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"], limit: [0, 1], order: "desc alpha") assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("{qux}bar", get: ["{qux}foo:*:a", "{qux}foo:*:b"]) end def test_sort_with_store r.set("{qux}foo:1", "s1") r.set("{qux}foo:2", "s2") r.rpush("{qux}bar", "1") r.rpush("{qux}bar", "2") r.sort("{qux}bar", get: "{qux}foo:*", store: "{qux}baz") assert_equal ["s1", "s2"], r.lrange("{qux}baz", 0, -1) end def test_bitop target_version "2.5.10" do r.set("{qux}foo", "a") r.set("{qux}bar", "b") r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar") assert_equal "\x60", r.get("{qux}foo&bar") r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar") assert_equal "\x63", r.get("{qux}foo|bar") r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar") assert_equal "\x03", r.get("{qux}foo^bar") r.bitop(:not, "{qux}~foo", "{qux}foo") assert_equal "\x9E", r.get("{qux}~foo") end end end redis-rb-4.2.5/test/distributed_connection_handling_test.rb000066400000000000000000000005421375567530100242160ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedConnectionHandling < Minitest::Test include Helper::Distributed def test_ping assert_equal ["PONG"], r.ping end def test_select r.set "foo", "bar" r.select 14 assert_nil r.get("foo") r.select 15 assert_equal "bar", r.get("foo") end end redis-rb-4.2.5/test/distributed_internals_test.rb000066400000000000000000000045161375567530100222170ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedInternals < Minitest::Test include Helper::Distributed def test_provides_a_meaningful_inspect nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] redis = Redis::Distributed.new nodes assert_equal "#", redis.inspect end def test_default_as_urls nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] redis = Redis::Distributed.new nodes assert_equal(["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node._client.id }) end def test_default_as_config_hashes nodes = [OPTIONS.merge(host: '127.0.0.1'), OPTIONS.merge(host: 'somehost', port: PORT.next)] redis = Redis::Distributed.new nodes assert_equal(["redis://127.0.0.1:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.id }) end def test_as_mix_and_match nodes = ["redis://127.0.0.1:7389/15", OPTIONS.merge(host: 'somehost'), OPTIONS.merge(host: 'somehost', port: PORT.next)] redis = Redis::Distributed.new nodes assert_equal(["redis://127.0.0.1:7389/15", "redis://somehost:#{PORT}/15", "redis://somehost:#{PORT.next}/15"], redis.nodes.map { |node| node._client.id }) end def test_override_id nodes = [OPTIONS.merge(host: '127.0.0.1', id: "test"), OPTIONS.merge(host: 'somehost', port: PORT.next, id: "test1")] redis = Redis::Distributed.new nodes assert_equal redis.nodes.first._client.id, "test" assert_equal redis.nodes.last._client.id, "test1" assert_equal "#", redis.inspect end def test_can_be_duped_to_create_a_new_connection redis = Redis::Distributed.new(NODES) clients = redis.info[0]["connected_clients"].to_i r2 = redis.dup r2.ping assert_equal clients + 1, redis.info[0]["connected_clients"].to_i end def test_keeps_options_after_dup r1 = Redis::Distributed.new(NODES, tag: /^(\w+):/) assert_raises(Redis::Distributed::CannotDistribute) do r1.sinter("foo", "bar") end assert_equal [], r1.sinter("baz:foo", "baz:bar") r2 = r1.dup assert_raises(Redis::Distributed::CannotDistribute) do r2.sinter("foo", "bar") end assert_equal [], r2.sinter("baz:foo", "baz:bar") end end redis-rb-4.2.5/test/distributed_key_tags_test.rb000066400000000000000000000024771375567530100220320ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedKeyTags < Minitest::Test include Helper include Helper::Distributed def test_hashes_consistently r1 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] r2 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] r3 = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] assert_equal r1.node_for("foo").id, r2.node_for("foo").id assert_equal r1.node_for("foo").id, r3.node_for("foo").id end def test_allows_clustering_of_keys r = Redis::Distributed.new(NODES) r.add_node("redis://127.0.0.1:#{PORT}/14") r.flushdb 100.times do |i| r.set "{foo}users:#{i}", i end assert_equal([0, 100], r.nodes.map { |node| node.keys.size }) end def test_distributes_keys_if_no_clustering_is_used r.add_node("redis://127.0.0.1:#{PORT}/14") r.flushdb r.set "users:1", 1 r.set "users:4", 4 assert_equal([1, 1], r.nodes.map { |node| node.keys.size }) end def test_allows_passing_a_custom_tag_extractor r = Redis::Distributed.new(NODES, tag: /^(.+?):/) r.add_node("redis://127.0.0.1:#{PORT}/14") r.flushdb 100.times do |i| r.set "foo:users:#{i}", i end assert_equal([0, 100], r.nodes.map { |node| node.keys.size }) end end redis-rb-4.2.5/test/distributed_persistence_control_commands_test.rb000066400000000000000000000010401375567530100261520ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedPersistenceControlCommands < Minitest::Test include Helper::Distributed def test_save redis_mock(save: -> { "+SAVE" }) do |redis| assert_equal ["SAVE"], redis.save end end def test_bgsave redis_mock(bgsave: -> { "+BGSAVE" }) do |redis| assert_equal ["BGSAVE"], redis.bgsave end end def test_lastsave redis_mock(lastsave: -> { "+LASTSAVE" }) do |redis| assert_equal ["LASTSAVE"], redis.lastsave end end end redis-rb-4.2.5/test/distributed_publish_subscribe_test.rb000066400000000000000000000036511375567530100237260ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedPublishSubscribe < Minitest::Test include Helper::Distributed def test_subscribe_and_unsubscribe assert_raises Redis::Distributed::CannotDistribute do r.subscribe("foo", "bar") {} end assert_raises Redis::Distributed::CannotDistribute do r.subscribe("{qux}foo", "bar") {} end end def test_subscribe_and_unsubscribe_with_tags @subscribed = false @unsubscribed = false wire = Wire.new do r.subscribe("foo") do |on| on.subscribe do |_channel, total| @subscribed = true @t1 = total end on.message do |_channel, message| if message == "s1" r.unsubscribe @message = message end end on.unsubscribe do |_channel, total| @unsubscribed = true @t2 = total end end end # Wait until the subscription is active before publishing Wire.pass until @subscribed Redis::Distributed.new(NODES).publish("foo", "s1") wire.join assert @subscribed assert_equal 1, @t1 assert @unsubscribed assert_equal 0, @t2 assert_equal "s1", @message end def test_subscribe_within_subscribe @channels = [] wire = Wire.new do r.subscribe("foo") do |on| on.subscribe do |channel, _total| @channels << channel r.subscribe("bar") if channel == "foo" r.unsubscribe if channel == "bar" end end end wire.join assert_equal ["foo", "bar"], @channels end def test_other_commands_within_a_subscribe assert_raises Redis::CommandError do r.subscribe("foo") do |on| on.subscribe do |_channel, _total| r.set("bar", "s2") end end end end def test_subscribe_without_a_block assert_raises LocalJumpError do r.subscribe("foo") end end end redis-rb-4.2.5/test/distributed_remote_server_control_commands_test.rb000066400000000000000000000027211375567530100265160ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedRemoteServerControlCommands < Minitest::Test include Helper::Distributed def test_info keys = [ "redis_version", "uptime_in_seconds", "uptime_in_days", "connected_clients", "used_memory", "total_connections_received", "total_commands_processed" ] infos = r.info infos.each do |info| keys.each do |k| msg = "expected #info to include #{k}" assert info.keys.include?(k), msg end end end def test_info_commandstats target_version "2.5.7" do r.nodes.each do |n| n.config(:resetstat) n.config(:get, :port) end r.info(:commandstats).each do |info| assert_equal '2', info['config']['calls'] # CONFIG RESETSTAT + CONFIG GET = twice end end end def test_monitor r.monitor rescue Exception => ex ensure assert ex.is_a?(NotImplementedError) end def test_echo assert_equal ["foo bar baz\n"], r.echo("foo bar baz\n") end def test_time target_version "2.5.4" do # Test that the difference between the time that Ruby reports and the time # that Redis reports is minimal (prevents the test from being racy). r.time.each do |rv| redis_usec = rv[0] * 1_000_000 + rv[1] ruby_usec = Integer(Time.now.to_f * 1_000_000) assert((ruby_usec - redis_usec).abs < 500_000) end end end end redis-rb-4.2.5/test/distributed_scripting_test.rb000066400000000000000000000055771375567530100222320ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedScripting < Minitest::Test include Helper::Distributed def to_sha(script) r.script(:load, script).first end def test_script_exists target_version "2.5.9" do # 2.6-rc1 a = to_sha("return 1") b = a.succ assert_equal [true], r.script(:exists, a) assert_equal [false], r.script(:exists, b) assert_equal [[true]], r.script(:exists, [a]) assert_equal [[false]], r.script(:exists, [b]) assert_equal [[true, false]], r.script(:exists, [a, b]) end end def test_script_flush target_version "2.5.9" do # 2.6-rc1 sha = to_sha("return 1") assert r.script(:exists, sha).first assert_equal ["OK"], r.script(:flush) assert !r.script(:exists, sha).first end end def test_script_kill target_version "2.5.9" do # 2.6-rc1 redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| assert_equal ["KILL"], redis.script(:kill) end end end def test_eval target_version "2.5.9" do # 2.6-rc1 assert_raises(Redis::Distributed::CannotDistribute) do r.eval("return #KEYS") end assert_raises(Redis::Distributed::CannotDistribute) do r.eval("return KEYS", ["k1", "k2"]) end assert_equal ["k1"], r.eval("return KEYS", ["k1"]) assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"]) end end def test_eval_with_options_hash target_version "2.5.9" do # 2.6-rc1 assert_raises(Redis::Distributed::CannotDistribute) do r.eval("return #KEYS", {}) end assert_raises(Redis::Distributed::CannotDistribute) do r.eval("return KEYS", { keys: ["k1", "k2"] }) end assert_equal ["k1"], r.eval("return KEYS", { keys: ["k1"] }) assert_equal ["a1", "a2"], r.eval("return ARGV", { keys: ["k1"], argv: ["a1", "a2"] }) end end def test_evalsha target_version "2.5.9" do # 2.6-rc1 assert_raises(Redis::Distributed::CannotDistribute) do r.evalsha(to_sha("return #KEYS")) end assert_raises(Redis::Distributed::CannotDistribute) do r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) end assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"]) assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"]) end end def test_evalsha_with_options_hash target_version "2.5.9" do # 2.6-rc1 assert_raises(Redis::Distributed::CannotDistribute) do r.evalsha(to_sha("return #KEYS"), {}) end assert_raises(Redis::Distributed::CannotDistribute) do r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) end assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), { keys: ["k1"] }) assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { keys: ["k1"], argv: ["a1", "a2"] }) end end end redis-rb-4.2.5/test/distributed_sorting_test.rb000066400000000000000000000006011375567530100216740ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedSorting < Minitest::Test include Helper::Distributed def test_sort assert_raises(Redis::Distributed::CannotDistribute) do r.set("foo:1", "s1") r.set("foo:2", "s2") r.rpush("bar", "1") r.rpush("bar", "2") r.sort("bar", get: "foo:*", limit: [0, 1]) end end end redis-rb-4.2.5/test/distributed_test.rb000066400000000000000000000027401375567530100201350ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributed < Minitest::Test include Helper::Distributed def test_handle_multiple_servers @r = Redis::Distributed.new ["redis://127.0.0.1:#{PORT}/15", *NODES] 100.times do |idx| @r.set(idx.to_s, "foo#{idx}") end 100.times do |idx| assert_equal "foo#{idx}", @r.get(idx.to_s) end assert_equal "0", @r.keys("*").min assert_equal "string", @r.type("1") end def test_add_nodes logger = Logger.new("/dev/null") @r = Redis::Distributed.new NODES, logger: logger, timeout: 10 assert_equal "127.0.0.1", @r.nodes[0]._client.host assert_equal PORT, @r.nodes[0]._client.port assert_equal 15, @r.nodes[0]._client.db assert_equal 10, @r.nodes[0]._client.timeout assert_equal logger, @r.nodes[0]._client.logger @r.add_node("redis://127.0.0.1:6380/14") assert_equal "127.0.0.1", @r.nodes[1]._client.host assert_equal 6380, @r.nodes[1]._client.port assert_equal 14, @r.nodes[1]._client.db assert_equal 10, @r.nodes[1]._client.timeout assert_equal logger, @r.nodes[1]._client.logger end def test_pipelining_commands_cannot_be_distributed assert_raises Redis::Distributed::CannotDistribute do r.pipelined do r.lpush "foo", "s1" r.lpush "foo", "s2" end end end def test_unknown_commands_does_not_work_by_default assert_raises NoMethodError do r.not_yet_implemented_command end end end redis-rb-4.2.5/test/distributed_transactions_test.rb000066400000000000000000000042271375567530100227270ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestDistributedTransactions < Minitest::Test include Helper::Distributed def test_multi_discard r.set("foo", 1) r.watch("foo") r.multi r.set("foo", 2) assert_raises Redis::Distributed::CannotDistribute do r.set("bar", 1) end r.discard assert_equal('1', r.get("foo")) end def test_multi_discard_without_watch @foo = nil assert_raises Redis::Distributed::CannotDistribute do r.multi { @foo = 1 } end assert_nil @foo assert_raises Redis::Distributed::CannotDistribute do r.discard end end def test_watch_unwatch_without_clustering assert_raises Redis::Distributed::CannotDistribute do r.watch("foo", "bar") end r.watch("{qux}foo", "{qux}bar") do assert_raises Redis::Distributed::CannotDistribute do r.get("{baz}foo") end r.unwatch end assert_raises Redis::Distributed::CannotDistribute do r.unwatch end end def test_watch_with_exception assert_raises StandardError do r.watch("{qux}foo", "{qux}bar") do raise StandardError, "woops" end end assert_equal "OK", r.set("{other}baz", 1) end def test_watch_unwatch assert_equal "OK", r.watch("{qux}foo", "{qux}bar") assert_equal "OK", r.unwatch end def test_watch_multi_with_block r.set("{qux}baz", 1) r.watch("{qux}foo", "{qux}bar", "{qux}baz") do assert_equal '1', r.get("{qux}baz") result = r.multi do r.incrby("{qux}foo", 3) r.incrby("{qux}bar", 6) r.incrby("{qux}baz", 9) end assert_equal [3, 6, 10], result end end def test_watch_multi_exec_without_block r.set("{qux}baz", 1) assert_equal "OK", r.watch("{qux}foo", "{qux}bar", "{qux}baz") assert_equal '1', r.get("{qux}baz") assert_raises Redis::Distributed::CannotDistribute do r.get("{foo}baz") end assert_equal "OK", r.multi assert_equal "QUEUED", r.incrby("{qux}baz", 1) assert_equal "QUEUED", r.incrby("{qux}baz", 1) assert_equal [2, 3], r.exec assert_equal "OK", r.set("{other}baz", 1) end end redis-rb-4.2.5/test/encoding_test.rb000066400000000000000000000004731375567530100174020ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestEncoding < Minitest::Test include Helper::Client def test_returns_properly_encoded_strings with_external_encoding("UTF-8") do r.set "foo", "שלום" assert_equal "Shalom שלום", "Shalom " + r.get("foo") end end end redis-rb-4.2.5/test/error_replies_test.rb000066400000000000000000000027111375567530100204650ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestErrorReplies < Minitest::Test include Helper::Client # Every test shouldn't disconnect from the server. Also, when error replies are # in play, the protocol should never get into an invalid state where there are # pending replies in the connection. Calling INFO after every test ensures that # the protocol is still in a valid state. def with_reconnection_check before = r.info["total_connections_received"] yield(r) after = r.info["total_connections_received"] ensure assert_equal before, after end def test_error_reply_for_single_command with_reconnection_check do begin r.unknown_command rescue => ex ensure assert ex.message =~ /unknown command/i end end end def test_raise_first_error_reply_in_pipeline with_reconnection_check do begin r.pipelined do r.set("foo", "s1") r.incr("foo") # not an integer r.lpush("foo", "value") # wrong kind of value end rescue => ex ensure assert ex.message =~ /not an integer/i end end end def test_recover_from_raise_in__call_loop with_reconnection_check do begin r._client.call_loop([:invalid_monitor]) do assert false # Should never be executed end rescue => ex ensure assert ex.message =~ /unknown command/i end end end end redis-rb-4.2.5/test/fork_safety_test.rb000066400000000000000000000030461375567530100201270ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestForkSafety < Minitest::Test include Helper::Client driver(:ruby, :hiredis) do def test_fork_safety redis = Redis.new(OPTIONS) redis.set "foo", 1 child_pid = fork do begin # InheritedError triggers a reconnect, # so we need to disable reconnects to force # the exception bubble up redis.without_reconnect do redis.set "foo", 2 end exit! 0 rescue Redis::InheritedError exit! 127 end end _, status = Process.wait2(child_pid) assert_equal 127, status.exitstatus assert_equal "1", redis.get("foo") rescue NotImplementedError => error raise unless error.message =~ /fork is not available/ end def test_fork_safety_with_enabled_inherited_socket redis = Redis.new(OPTIONS.merge(inherit_socket: true)) redis.set "foo", 1 child_pid = fork do begin # InheritedError triggers a reconnect, # so we need to disable reconnects to force # the exception bubble up redis.without_reconnect do redis.set "foo", 2 end exit! 0 rescue Redis::InheritedError exit! 127 end end _, status = Process.wait2(child_pid) assert_equal 0, status.exitstatus assert_equal "2", redis.get("foo") rescue NotImplementedError => error raise unless error.message =~ /fork is not available/ end end end redis-rb-4.2.5/test/helper.rb000066400000000000000000000210131375567530100160250ustar00rootroot00000000000000# frozen_string_literal: true require "minitest/autorun" require "mocha/minitest" require "logger" require "stringio" $VERBOSE = true ENV["DRIVER"] ||= "ruby" require_relative "../lib/redis" require_relative "../lib/redis/distributed" require_relative "../lib/redis/connection/#{ENV['DRIVER']}" require_relative "support/redis_mock" require_relative "support/connection/#{ENV['DRIVER']}" require_relative 'support/cluster/orchestrator' PORT = 6381 DB = 15 TIMEOUT = Float(ENV['TIMEOUT'] || 1.0) LOW_TIMEOUT = Float(ENV['LOW_TIMEOUT'] || 0.01) # for blocking-command tests OPTIONS = { port: PORT, db: DB, timeout: TIMEOUT }.freeze def driver(*drivers, &blk) class_eval(&blk) if drivers.map(&:to_s).include?(ENV["DRIVER"]) end module Helper def run if respond_to?(:around) around { super } else super end end def silent verbose, $VERBOSE = $VERBOSE, false begin yield ensure $VERBOSE = verbose end end def with_external_encoding(encoding) original_encoding = Encoding.default_external begin silent { Encoding.default_external = Encoding.find(encoding) } yield ensure silent { Encoding.default_external = original_encoding } end end class Version include Comparable attr :parts def initialize(version) @parts = case version when Version version.parts else version.to_s.split(".") end end def <=>(other) other = Version.new(other) length = [parts.length, other.parts.length].max length.times do |i| a, b = parts[i], other.parts[i] return -1 if a.nil? return +1 if b.nil? return a.to_i <=> b.to_i if a != b end 0 end end module Generic include Helper attr_reader :log attr_reader :redis alias r redis def setup @log = StringIO.new @redis = init _new_client # Run GC to make sure orphaned connections are closed. GC.start super end def teardown redis&.quit super end def init(redis) redis.select 14 redis.flushdb redis.select 15 redis.flushdb redis rescue Redis::CannotConnectError puts <<-MSG Cannot connect to Redis. Make sure Redis is running on localhost, port #{PORT}. This testing suite connects to the database 15. Try this once: $ make clean Then run the build again: $ make MSG exit 1 end def redis_mock(commands, options = {}) RedisMock.start(commands, options) do |port| yield _new_client(options.merge(port: port)) end end def redis_mock_with_handler(handler, options = {}) RedisMock.start_with_handler(handler, options) do |port| yield _new_client(options.merge(port: port)) end end def assert_in_range(range, value) assert range.include?(value), "expected #{value} to be in #{range.inspect}" end def target_version(target) if version < target skip("Requires Redis > #{target}") if respond_to?(:skip) else yield end end def omit_version(min_ver) skip("Requires Redis > #{min_ver}") if version < min_ver end def version Version.new(redis.info['redis_version']) end end module Client include Generic private def _format_options(options) OPTIONS.merge(logger: ::Logger.new(@log)).merge(options) end def _new_client(options = {}) Redis.new(_format_options(options).merge(driver: ENV["DRIVER"])) end end module Sentinel include Generic MASTER_PORT = PORT.to_s SLAVE_PORT = '6382' SENTINEL_PORT = '6400' SENTINEL_PORTS = %w[6400 6401 6402].freeze MASTER_NAME = 'master1' LOCALHOST = '127.0.0.1' def build_sentinel_client(options = {}) opts = { host: LOCALHOST, port: SENTINEL_PORT, timeout: TIMEOUT, logger: ::Logger.new(@log) } Redis.new(opts.merge(options)) end def build_slave_role_client(options = {}) _new_client(options.merge(role: :slave)) end private def _format_options(options = {}) { url: "redis://#{MASTER_NAME}", sentinels: [{ host: LOCALHOST, port: SENTINEL_PORT }], role: :master, timeout: TIMEOUT, logger: ::Logger.new(@log) }.merge(options) end def _new_client(options = {}) Redis.new(_format_options(options).merge(driver: ENV['DRIVER'])) end end module Distributed include Generic NODES = ["redis://127.0.0.1:#{PORT}/#{DB}"].freeze def version Version.new(redis.info.first["redis_version"]) end private def _format_options(options) { timeout: OPTIONS[:timeout], logger: ::Logger.new(@log) }.merge(options) end def _new_client(options = {}) Redis::Distributed.new(NODES, _format_options(options).merge(driver: ENV["conn"])) end end module Cluster include Generic DEFAULT_HOST = '127.0.0.1' DEFAULT_PORTS = (7000..7005).freeze ClusterSlotsRawReply = lambda { |host, port| # @see https://redis.io/topics/protocol <<-REPLY.delete(' ') *1\r *4\r :0\r :16383\r *3\r $#{host.size}\r #{host}\r :#{port}\r $40\r 649fa246273043021a05f547a79478597d3f1dc5\r *3\r $#{host.size}\r #{host}\r :#{port}\r $40\r 649fa246273043021a05f547a79478597d3f1dc5\r REPLY } ClusterNodesRawReply = lambda { |host, port| line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\ 'myself,master - 0 1530797742000 1 connected 0-16383' "$#{line.size}\r\n#{line}\r\n" } def init(redis) redis.flushall redis rescue Redis::CannotConnectError puts <<-MSG Cannot connect to Redis Cluster. Make sure Redis is running on localhost, port #{DEFAULT_PORTS}. Try this once: $ make stop_cluster Then run the build again: $ make MSG exit! 1 end def build_another_client(options = {}) _new_client(options) end def redis_cluster_mock(commands, options = {}) host = DEFAULT_HOST port = nil cluster_subcommands = if commands.key?(:cluster) commands.delete(:cluster) .map { |k, v| [k.to_s.downcase, v] } .to_h else {} end commands[:cluster] = lambda { |subcommand, *args| if cluster_subcommands.key?(subcommand) cluster_subcommands[subcommand].call(*args) else case subcommand when 'slots' then ClusterSlotsRawReply.call(host, port) when 'nodes' then ClusterNodesRawReply.call(host, port) else '+OK' end end } commands[:command] = ->(*_) { "*0\r\n" } RedisMock.start(commands, options) do |po| port = po scheme = options[:ssl] ? 'rediss' : 'redis' nodes = %W[#{scheme}://#{host}:#{port}] yield _new_client(options.merge(cluster: nodes)) end end def redis_cluster_down trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) trib.down yield ensure trib.rebuild trib.close end def redis_cluster_failover trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) trib.failover yield ensure trib.rebuild trib.close end def redis_cluster_fail_master trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) trib.fail_serving_master yield ensure trib.restart_cluster_nodes trib.rebuild trib.close end # @param slot [Integer] # @param src [String] : # @param dest [String] : def redis_cluster_resharding(slot, src:, dest:) trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) trib.start_resharding(slot, src, dest) yield trib.finish_resharding(slot, dest) ensure trib.rebuild trib.close end private def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS) ports.map { |port| "redis://#{host}:#{port}" } end def _format_options(options) { timeout: OPTIONS[:timeout], logger: ::Logger.new(@log), cluster: _default_nodes }.merge(options) end def _new_client(options = {}) Redis.new(_format_options(options).merge(driver: ENV['DRIVER'])) end end end redis-rb-4.2.5/test/helper_test.rb000066400000000000000000000005701375567530100170710ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestHelper < Minitest::Test include Helper def test_version_comparison v = Version.new("2.0.1") assert v > "1" assert v > "2" assert v < "3" assert v < "10" assert v < "2.1" assert v < "2.0.2" assert v < "2.0.1.1" assert v < "2.0.10" assert v == "2.0.1" end end redis-rb-4.2.5/test/internals_test.rb000066400000000000000000000230661375567530100176160ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestInternals < Minitest::Test include Helper::Client def test_logger r.ping assert log.string["[Redis] command=PING"] assert log.string =~ /\[Redis\] call_time=\d+\.\d+ ms/ end def test_large_payload # see: https://github.com/redis/redis-rb/issues/962 # large payloads will trigger write_nonblock to write a portion # of the payload in connection/ruby.rb _write_to_socket large = "\u3042" * 4_000_000 r.setex("foo", 10, large) result = r.get("foo") assert_equal result, large end def test_logger_with_pipelining r.pipelined do r.set "foo", "bar" r.get "foo" end assert log.string[" command=SET args=\"foo\" \"bar\""] assert log.string[" command=GET args=\"foo\""] end def test_recovers_from_failed_commands # See https://github.com/redis/redis-rb/issues#issue/28 assert_raises(Redis::CommandError) do r.command_that_doesnt_exist end r.info end def test_raises_on_protocol_errors redis_mock(ping: ->(*_) { "foo" }) do |redis| assert_raises(Redis::ProtocolError) do redis.ping end end end def test_redis_current assert_equal "127.0.0.1", Redis.current._client.host assert_equal 6379, Redis.current._client.port assert_equal 0, Redis.current._client.db Redis.current = Redis.new(OPTIONS.merge(port: 6380, db: 1)) t = Thread.new do assert_equal "127.0.0.1", Redis.current._client.host assert_equal 6380, Redis.current._client.port assert_equal 1, Redis.current._client.db end t.join assert_equal "127.0.0.1", Redis.current._client.host assert_equal 6380, Redis.current._client.port assert_equal 1, Redis.current._client.db end def test_redis_connected? fresh_client = _new_client assert !fresh_client.connected? fresh_client.ping assert fresh_client.connected? fresh_client.quit assert !fresh_client.connected? end def test_timeout Redis.new(OPTIONS.merge(timeout: 0)) end driver(:ruby) do def test_tcp_keepalive keepalive = { time: 20, intvl: 10, probes: 5 } redis = Redis.new(OPTIONS.merge(tcp_keepalive: keepalive)) redis.ping connection = redis._client.connection actual_keepalive = connection.get_tcp_keepalive %i[time intvl probes].each do |key| assert_equal actual_keepalive[key], keepalive[key] if actual_keepalive.key?(key) end end end def test_time target_version "2.5.4" do # Test that the difference between the time that Ruby reports and the time # that Redis reports is minimal (prevents the test from being racy). rv = r.time redis_usec = rv[0] * 1_000_000 + rv[1] ruby_usec = Integer(Time.now.to_f * 1_000_000) assert((ruby_usec - redis_usec).abs < 500_000) end end def test_connection_timeout opts = OPTIONS.merge(host: "10.255.255.254", connect_timeout: 0.1, timeout: 5.0) start_time = Time.now assert_raises Redis::CannotConnectError do Redis.new(opts).ping end assert((Time.now - start_time) <= opts[:timeout]) end def test_missing_socket opts = { path: '/missing.sock' } assert_raises Redis::CannotConnectError do Redis.new(opts).ping end end def close_on_ping(seq, options = {}) @request = 0 command = lambda do idx = @request @request += 1 rv = "+%d" % idx rv = nil if seq.include?(idx) rv end redis_mock({ ping: command }, { timeout: 0.1 }.merge(options)) do |redis| yield(redis) end end def test_retry_by_default close_on_ping([0]) do |redis| assert_equal "1", redis.ping end end def test_retry_when_wrapped_in_with_reconnect_true close_on_ping([0]) do |redis| redis.with_reconnect(true) do assert_equal "1", redis.ping end end end def test_dont_retry_when_wrapped_in_with_reconnect_false close_on_ping([0]) do |redis| assert_raises Redis::ConnectionError do redis.with_reconnect(false) do redis.ping end end end end def test_dont_retry_when_wrapped_in_without_reconnect close_on_ping([0]) do |redis| assert_raises Redis::ConnectionError do redis.without_reconnect do redis.ping end end end end def test_retry_only_once_when_read_raises_econnreset close_on_ping([0, 1]) do |redis| assert_raises Redis::ConnectionError do redis.ping end assert !redis._client.connected? end end def test_retry_with_custom_reconnect_attempts close_on_ping([0, 1], reconnect_attempts: 2) do |redis| assert_equal "2", redis.ping end end def test_retry_with_custom_reconnect_attempts_can_still_fail close_on_ping([0, 1, 2], reconnect_attempts: 2) do |redis| assert_raises Redis::ConnectionError do redis.ping end assert !redis._client.connected? end end def test_retry_with_custom_reconnect_attempts_and_exponential_backoff close_on_ping([0, 1, 2], reconnect_attempts: 3, reconnect_delay_max: 0.5, reconnect_delay: 0.01) do |redis| Kernel.expects(:sleep).with(0.01).returns(true) Kernel.expects(:sleep).with(0.02).returns(true) Kernel.expects(:sleep).with(0.04).returns(true) assert_equal "3", redis.ping end end def test_don_t_retry_when_second_read_in_pipeline_raises_econnreset close_on_ping([1]) do |redis| assert_raises Redis::ConnectionError do redis.pipelined do redis.ping redis.ping # Second #read times out end end assert !redis._client.connected? end end def close_on_connection(seq) @n = 0 read_command = lambda do |session| Array.new(session.gets[1..-3].to_i) do bytes = session.gets[1..-3].to_i arg = session.read(bytes) session.read(2) # Discard \r\n arg end end handler = lambda do |session| n = @n @n += 1 select = read_command.call(session) if select[0].downcase == "select" session.write("+OK\r\n") else raise "Expected SELECT" end unless seq.include?(n) session.write("+#{n}\r\n") while read_command.call(session) end end redis_mock_with_handler(handler) do |redis| yield(redis) end end def test_retry_on_write_error_by_default close_on_connection([0]) do |redis| assert_equal "1", redis._client.call(["x" * 128 * 1024]) end end def test_retry_on_write_error_when_wrapped_in_with_reconnect_true close_on_connection([0]) do |redis| redis.with_reconnect(true) do assert_equal "1", redis._client.call(["x" * 128 * 1024]) end end end def test_dont_retry_on_write_error_when_wrapped_in_with_reconnect_false close_on_connection([0]) do |redis| assert_raises Redis::ConnectionError do redis.with_reconnect(false) do redis._client.call(["x" * 128 * 1024]) end end end end def test_dont_retry_on_write_error_when_wrapped_in_without_reconnect close_on_connection([0]) do |redis| assert_raises Redis::ConnectionError do redis.without_reconnect do redis._client.call(["x" * 128 * 1024]) end end end end def test_connecting_to_unix_domain_socket Redis.new(OPTIONS.merge(path: ENV.fetch("SOCKET_PATH"))).ping end driver(:ruby, :hiredis) do def test_bubble_timeout_without_retrying serv = TCPServer.new(6380) redis = Redis.new(port: 6380, timeout: 0.1) assert_raises(Redis::TimeoutError) do redis.ping end ensure serv&.close end end def test_client_options redis = Redis.new(OPTIONS.merge(host: "host", port: 1234, db: 1, scheme: "foo")) assert_equal "host", redis._client.options[:host] assert_equal 1234, redis._client.options[:port] assert_equal 1, redis._client.options[:db] assert_equal "foo", redis._client.options[:scheme] end def test_resolves_localhost Redis.new(OPTIONS.merge(host: 'localhost')).ping end class << self def af_family_supported(af_type) hosts = { Socket::AF_INET => "127.0.0.1", Socket::AF_INET6 => "::1" } begin s = Socket.new(af_type, Socket::SOCK_STREAM, 0) begin tries = 5 begin sa = Socket.pack_sockaddr_in(Random.rand(1024..64_099), hosts[af_type]) s.bind(sa) rescue Errno::EADDRINUSE => e # On JRuby (9.1.15.0), if IPv6 is globally disabled on the system, # we get an EADDRINUSE with belows message. return if e.message =~ /Protocol family unavailable/ tries -= 1 retry if tries > 0 raise end yield rescue Errno::EADDRNOTAVAIL ensure s.close end rescue Errno::ESOCKTNOSUPPORT end end end def af_test(host) commands = { ping: ->(*_) { "+pong" } } redis_mock(commands, host: host, &:ping) end driver(:ruby) do af_family_supported(Socket::AF_INET) do def test_connect_ipv4 af_test("127.0.0.1") end end end driver(:ruby) do af_family_supported(Socket::AF_INET6) do def test_connect_ipv6 af_test("::1") end end end def test_can_be_duped_to_create_a_new_connection clients = r.info["connected_clients"].to_i r2 = r.dup r2.ping assert_equal clients + 1, r.info["connected_clients"].to_i end end redis-rb-4.2.5/test/lint/000077500000000000000000000000001375567530100151725ustar00rootroot00000000000000redis-rb-4.2.5/test/lint/blocking_commands.rb000066400000000000000000000132551375567530100211760ustar00rootroot00000000000000# frozen_string_literal: true module Lint module BlockingCommands def setup super r.rpush('{zap}foo', 's1') r.rpush('{zap}foo', 's2') r.rpush('{zap}bar', 's1') r.rpush('{zap}bar', 's2') r.zadd('{szap}foo', %w[0 a 1 b 2 c]) r.zadd('{szap}bar', %w[0 c 1 d 2 e]) end def to_protocol(obj) case obj when String "$#{obj.length}\r\n#{obj}\r\n" when Array "*#{obj.length}\r\n" + obj.map { |e| to_protocol(e) }.join else raise end end def mock(options = {}, &blk) commands = build_mock_commands(options) redis_mock(commands, { timeout: LOW_TIMEOUT }, &blk) end def build_mock_commands(options = {}) { blpop: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol([args.first, args.last]) end, brpop: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol([args.first, args.last]) end, brpoplpush: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol(args.last) end, bzpopmax: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol([args.first, args.last]) end, bzpopmin: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol([args.first, args.last]) end } end def test_blpop assert_equal ['{zap}foo', 's1'], r.blpop('{zap}foo') assert_equal ['{zap}foo', 's2'], r.blpop(['{zap}foo']) assert_equal ['{zap}bar', 's1'], r.blpop(['{zap}bar', '{zap}foo']) assert_equal ['{zap}bar', 's2'], r.blpop(['{zap}foo', '{zap}bar']) end def test_blpop_timeout mock do |r| assert_equal ['{zap}foo', '0'], r.blpop('{zap}foo') assert_equal ['{zap}foo', LOW_TIMEOUT.to_s], r.blpop('{zap}foo', timeout: LOW_TIMEOUT) end end class FakeDuration def initialize(int) @int = int end def to_int @int end end def test_blpop_integer_like_timeout mock do |r| assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", FakeDuration.new(1)) end end def test_blpop_with_old_prototype assert_equal ['{zap}foo', 's1'], r.blpop('{zap}foo', 0) assert_equal ['{zap}foo', 's2'], r.blpop('{zap}foo', 0) assert_equal ['{zap}bar', 's1'], r.blpop('{zap}bar', '{zap}foo', 0) assert_equal ['{zap}bar', 's2'], r.blpop('{zap}foo', '{zap}bar', 0) end def test_blpop_timeout_with_old_prototype mock do |r| assert_equal ['{zap}foo', '0'], r.blpop('{zap}foo', 0) assert_equal ['{zap}foo', '1'], r.blpop('{zap}foo', 1) end end def test_brpop assert_equal ['{zap}foo', 's2'], r.brpop('{zap}foo') assert_equal ['{zap}foo', 's1'], r.brpop(['{zap}foo']) assert_equal ['{zap}bar', 's2'], r.brpop(['{zap}bar', '{zap}foo']) assert_equal ['{zap}bar', 's1'], r.brpop(['{zap}foo', '{zap}bar']) end def test_brpop_timeout mock do |r| assert_equal ['{zap}foo', '0'], r.brpop('{zap}foo') assert_equal ['{zap}foo', LOW_TIMEOUT.to_s], r.brpop('{zap}foo', timeout: LOW_TIMEOUT) end end def test_brpop_with_old_prototype assert_equal ['{zap}foo', 's2'], r.brpop('{zap}foo', 0) assert_equal ['{zap}foo', 's1'], r.brpop('{zap}foo', 0) assert_equal ['{zap}bar', 's2'], r.brpop('{zap}bar', '{zap}foo', 0) assert_equal ['{zap}bar', 's1'], r.brpop('{zap}foo', '{zap}bar', 0) end def test_brpop_timeout_with_old_prototype mock do |r| assert_equal ['{zap}foo', '0'], r.brpop('{zap}foo', 0) assert_equal ['{zap}foo', '1'], r.brpop('{zap}foo', 1) end end def test_brpoplpush assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux') assert_equal ['s2'], r.lrange('{zap}qux', 0, -1) end def test_brpoplpush_timeout mock do |r| assert_equal '0', r.brpoplpush('{zap}foo', '{zap}bar') assert_equal LOW_TIMEOUT.to_s, r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) end end def test_brpoplpush_with_old_prototype assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux', 0) assert_equal ['s2'], r.lrange('{zap}qux', 0, -1) end def test_brpoplpush_timeout_with_old_prototype mock do |r| assert_equal '0', r.brpoplpush('{zap}foo', '{zap}bar', 0) assert_equal '1', r.brpoplpush('{zap}foo', '{zap}bar', 1) end end def test_bzpopmin target_version('5.0.0') do assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', 1) assert_nil r.bzpopmin('{szap}aaa', '{szap}bbb', 2) end end def test_bzpopmax target_version('5.0.0') do assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', 1) assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', 1) end end driver(:ruby, :hiredis) do def test_blpop_socket_timeout mock(delay: LOW_TIMEOUT * 5) do |r| assert_raises(Redis::TimeoutError) do r.blpop('{zap}foo', timeout: LOW_TIMEOUT) end end end def test_brpop_socket_timeout mock(delay: LOW_TIMEOUT * 5) do |r| assert_raises(Redis::TimeoutError) do r.brpop('{zap}foo', timeout: LOW_TIMEOUT) end end end def test_brpoplpush_socket_timeout mock(delay: LOW_TIMEOUT * 5) do |r| assert_raises(Redis::TimeoutError) do r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) end end end end end end redis-rb-4.2.5/test/lint/hashes.rb000066400000000000000000000116231375567530100167750ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Hashes def test_hset_and_hget assert_equal 1, r.hset("foo", "f1", "s1") assert_equal "s1", r.hget("foo", "f1") end def test_variadic_hset target_version "4.0.0" do assert_equal 2, r.hset("foo", "f1", "s1", "f2", "s2") assert_equal "s1", r.hget("foo", "f1") assert_equal "s2", r.hget("foo", "f2") assert_equal 2, r.hset("bar", { "f1" => "s1", "f2" => "s2" }) assert_equal "s1", r.hget("bar", "f1") assert_equal "s2", r.hget("bar", "f2") end end def test_hsetnx r.hset("foo", "f1", "s1") r.hsetnx("foo", "f1", "s2") assert_equal "s1", r.hget("foo", "f1") r.del("foo") r.hsetnx("foo", "f1", "s2") assert_equal "s2", r.hget("foo", "f1") end def test_hdel r.hset("foo", "f1", "s1") assert_equal "s1", r.hget("foo", "f1") assert_equal 1, r.hdel("foo", "f1") assert_nil r.hget("foo", "f1") end def test_splat_hdel target_version "2.3.9" do r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert_equal "s1", r.hget("foo", "f1") assert_equal "s2", r.hget("foo", "f2") assert_equal 2, r.hdel("foo", "f1", "f2") assert_nil r.hget("foo", "f1") assert_nil r.hget("foo", "f2") end end def test_variadic_hdel target_version "2.3.9" do r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert_equal "s1", r.hget("foo", "f1") assert_equal "s2", r.hget("foo", "f2") assert_equal 2, r.hdel("foo", ["f1", "f2"]) assert_nil r.hget("foo", "f1") assert_nil r.hget("foo", "f2") end end def test_hexists assert_equal false, r.hexists("foo", "f1") r.hset("foo", "f1", "s1") assert r.hexists("foo", "f1") end def test_hlen assert_equal 0, r.hlen("foo") r.hset("foo", "f1", "s1") assert_equal 1, r.hlen("foo") r.hset("foo", "f2", "s2") assert_equal 2, r.hlen("foo") end def test_hkeys assert_equal [], r.hkeys("foo") r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert_equal ["f1", "f2"], r.hkeys("foo") end def test_hvals assert_equal [], r.hvals("foo") r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert_equal ["s1", "s2"], r.hvals("foo") end def test_hgetall assert(r.hgetall("foo") == {}) r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert(r.hgetall("foo") == { "f1" => "s1", "f2" => "s2" }) end def test_hmset r.hmset("hash", "foo1", "bar1", "foo2", "bar2") assert_equal "bar1", r.hget("hash", "foo1") assert_equal "bar2", r.hget("hash", "foo2") end def test_hmset_with_invalid_arguments assert_raises(Redis::CommandError) do r.hmset("hash", "foo1", "bar1", "foo2", "bar2", "foo3") end end def test_mapped_hmset r.mapped_hmset("foo", f1: "s1", f2: "s2") assert_equal "s1", r.hget("foo", "f1") assert_equal "s2", r.hget("foo", "f2") end def test_hmget r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") r.hset("foo", "f3", "s3") assert_equal ["s2", "s3"], r.hmget("foo", "f2", "f3") end def test_hmget_mapped r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") r.hset("foo", "f3", "s3") assert(r.mapped_hmget("foo", "f1") == { "f1" => "s1" }) assert(r.mapped_hmget("foo", "f1", "f2") == { "f1" => "s1", "f2" => "s2" }) end def test_mapped_hmget_in_a_pipeline_returns_hash r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") result = r.pipelined do r.mapped_hmget("foo", "f1", "f2") end assert_equal result[0], { "f1" => "s1", "f2" => "s2" } end def test_hincrby r.hincrby("foo", "f1", 1) assert_equal "1", r.hget("foo", "f1") r.hincrby("foo", "f1", 2) assert_equal "3", r.hget("foo", "f1") r.hincrby("foo", "f1", -1) assert_equal "2", r.hget("foo", "f1") end def test_hincrbyfloat target_version "2.5.4" do r.hincrbyfloat("foo", "f1", 1.23) assert_equal "1.23", r.hget("foo", "f1") r.hincrbyfloat("foo", "f1", 0.77) assert_equal "2", r.hget("foo", "f1") r.hincrbyfloat("foo", "f1", -0.1) assert_equal "1.9", r.hget("foo", "f1") end end def test_hstrlen target_version('3.2.0') do redis.hmset('foo', 'f1', 'HelloWorld', 'f2', 99, 'f3', -256) assert_equal 10, r.hstrlen('foo', 'f1') assert_equal 2, r.hstrlen('foo', 'f2') assert_equal 4, r.hstrlen('foo', 'f3') end end def test_hscan redis.hmset('foo', 'f1', 'Jack', 'f2', 33) expected = ['0', [%w[f1 Jack], %w[f2 33]]] assert_equal expected, redis.hscan('foo', 0) end end end redis-rb-4.2.5/test/lint/hyper_log_log.rb000066400000000000000000000035631375567530100203570ustar00rootroot00000000000000# frozen_string_literal: true module Lint module HyperLogLog def test_pfadd target_version "2.8.9" do assert_equal true, r.pfadd("foo", "s1") assert_equal true, r.pfadd("foo", "s2") assert_equal false, r.pfadd("foo", "s1") assert_equal 2, r.pfcount("foo") end end def test_variadic_pfadd target_version "2.8.9" do assert_equal true, r.pfadd("foo", ["s1", "s2"]) assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) assert_equal 3, r.pfcount("foo") end end def test_pfcount target_version "2.8.9" do assert_equal 0, r.pfcount("foo") assert_equal true, r.pfadd("foo", "s1") assert_equal 1, r.pfcount("foo") end end def test_variadic_pfcount target_version "2.8.9" do assert_equal 0, r.pfcount(["{1}foo", "{1}bar"]) assert_equal true, r.pfadd("{1}foo", "s1") assert_equal true, r.pfadd("{1}bar", "s1") assert_equal true, r.pfadd("{1}bar", "s2") assert_equal 2, r.pfcount("{1}foo", "{1}bar") end end def test_variadic_pfcount_expanded target_version "2.8.9" do assert_equal 0, r.pfcount("{1}foo", "{1}bar") assert_equal true, r.pfadd("{1}foo", "s1") assert_equal true, r.pfadd("{1}bar", "s1") assert_equal true, r.pfadd("{1}bar", "s2") assert_equal 2, r.pfcount("{1}foo", "{1}bar") end end def test_pfmerge target_version '2.8.9' do r.pfadd 'foo', 's1' r.pfadd 'bar', 's2' assert_equal true, r.pfmerge('res', 'foo', 'bar') assert_equal 2, r.pfcount('res') end end def test_variadic_pfmerge_expanded redis.pfadd('{1}foo', %w[foo bar zap a]) redis.pfadd('{1}bar', %w[a b c foo]) assert_equal true, redis.pfmerge('{1}baz', '{1}foo', '{1}bar') end end end redis-rb-4.2.5/test/lint/lists.rb000066400000000000000000000070531375567530100166620ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Lists def test_lpush r.lpush "foo", "s1" r.lpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal "s2", r.lpop("foo") end def test_variadic_lpush target_version "2.3.9" do # 2.4-rc6 assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"]) assert_equal 3, r.llen("foo") assert_equal "s3", r.lpop("foo") end end def test_lpushx r.lpushx "foo", "s1" r.lpush "foo", "s2" r.lpushx "foo", "s3" assert_equal 2, r.llen("foo") assert_equal ["s3", "s2"], r.lrange("foo", 0, -1) end def test_rpush r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal "s2", r.rpop("foo") end def test_variadic_rpush target_version "2.3.9" do # 2.4-rc6 assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"]) assert_equal 3, r.llen("foo") assert_equal "s3", r.rpop("foo") end end def test_rpushx r.rpushx "foo", "s1" r.rpush "foo", "s2" r.rpushx "foo", "s3" assert_equal 2, r.llen("foo") assert_equal ["s2", "s3"], r.lrange("foo", 0, -1) end def test_llen r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") end def test_lrange r.rpush "foo", "s1" r.rpush "foo", "s2" r.rpush "foo", "s3" assert_equal ["s2", "s3"], r.lrange("foo", 1, -1) assert_equal ["s1", "s2"], r.lrange("foo", 0, 1) assert_equal [], r.lrange("bar", 0, -1) end def test_ltrim r.rpush "foo", "s1" r.rpush "foo", "s2" r.rpush "foo", "s3" r.ltrim "foo", 0, 1 assert_equal 2, r.llen("foo") assert_equal ["s1", "s2"], r.lrange("foo", 0, -1) end def test_lindex r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal "s1", r.lindex("foo", 0) assert_equal "s2", r.lindex("foo", 1) end def test_lset r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal "s2", r.lindex("foo", 1) assert r.lset("foo", 1, "s3") assert_equal "s3", r.lindex("foo", 1) assert_raises Redis::CommandError do r.lset("foo", 4, "s3") end end def test_lrem r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 1, r.lrem("foo", 1, "s1") assert_equal ["s2"], r.lrange("foo", 0, -1) end def test_lpop r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal "s1", r.lpop("foo") assert_equal 1, r.llen("foo") end def test_rpop r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal "s2", r.rpop("foo") assert_equal 1, r.llen("foo") end def test_linsert r.rpush "foo", "s1" r.rpush "foo", "s3" r.linsert "foo", :before, "s3", "s2" assert_equal ["s1", "s2", "s3"], r.lrange("foo", 0, -1) assert_raises(Redis::CommandError) do r.linsert "foo", :anywhere, "s3", "s2" end end def test_rpoplpush r.rpush 'foo', 's1' r.rpush 'foo', 's2' assert_equal 's2', r.rpoplpush('foo', 'bar') assert_equal ['s2'], r.lrange('bar', 0, -1) assert_equal 's1', r.rpoplpush('foo', 'bar') assert_equal %w[s1 s2], r.lrange('bar', 0, -1) end def test_variadic_rpoplpush_expand redis.rpush('{1}foo', %w[a b c]) redis.rpush('{1}bar', %w[d e f]) assert_equal 'c', redis.rpoplpush('{1}foo', '{1}bar') end end end redis-rb-4.2.5/test/lint/sets.rb000066400000000000000000000151541375567530100165030ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Sets def test_sadd assert_equal true, r.sadd("foo", "s1") assert_equal true, r.sadd("foo", "s2") assert_equal false, r.sadd("foo", "s1") assert_equal ["s1", "s2"], r.smembers("foo").sort end def test_variadic_sadd target_version "2.3.9" do # 2.4-rc6 assert_equal 2, r.sadd("foo", ["s1", "s2"]) assert_equal 1, r.sadd("foo", ["s1", "s2", "s3"]) assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort end end def test_srem r.sadd("foo", "s1") r.sadd("foo", "s2") assert_equal true, r.srem("foo", "s1") assert_equal false, r.srem("foo", "s3") assert_equal ["s2"], r.smembers("foo") end def test_variadic_srem target_version "2.3.9" do # 2.4-rc6 r.sadd("foo", "s1") r.sadd("foo", "s2") r.sadd("foo", "s3") assert_equal 1, r.srem("foo", ["s1", "aaa"]) assert_equal 0, r.srem("foo", ["bbb", "ccc", "ddd"]) assert_equal 1, r.srem("foo", ["eee", "s3"]) assert_equal ["s2"], r.smembers("foo") end end def test_spop r.sadd "foo", "s1" r.sadd "foo", "s2" assert ["s1", "s2"].include?(r.spop("foo")) assert ["s1", "s2"].include?(r.spop("foo")) assert_nil r.spop("foo") end def test_spop_with_positive_count target_version "3.2.0" do r.sadd "foo", "s1" r.sadd "foo", "s2" r.sadd "foo", "s3" r.sadd "foo", "s4" pops = r.spop("foo", 3) assert !(["s1", "s2", "s3", "s4"] & pops).empty? assert_equal 3, pops.size assert_equal 1, r.scard("foo") end end def test_scard assert_equal 0, r.scard("foo") r.sadd "foo", "s1" assert_equal 1, r.scard("foo") r.sadd "foo", "s2" assert_equal 2, r.scard("foo") end def test_sismember assert_equal false, r.sismember("foo", "s1") r.sadd "foo", "s1" assert_equal true, r.sismember("foo", "s1") assert_equal false, r.sismember("foo", "s2") end def test_smembers assert_equal [], r.smembers("foo") r.sadd "foo", "s1" r.sadd "foo", "s2" assert_equal ["s1", "s2"], r.smembers("foo").sort end def test_srandmember r.sadd "foo", "s1" r.sadd "foo", "s2" 4.times do assert ["s1", "s2"].include?(r.srandmember("foo")) end assert_equal 2, r.scard("foo") end def test_srandmember_with_positive_count r.sadd "foo", "s1" r.sadd "foo", "s2" r.sadd "foo", "s3" r.sadd "foo", "s4" 4.times do assert !(["s1", "s2", "s3", "s4"] & r.srandmember("foo", 3)).empty? assert_equal 3, r.srandmember("foo", 3).size end assert_equal 4, r.scard("foo") end def test_srandmember_with_negative_count r.sadd "foo", "s1" r.sadd "foo", "s2" r.sadd "foo", "s3" r.sadd "foo", "s4" 4.times do assert !(["s1", "s2", "s3", "s4"] & r.srandmember("foo", -6)).empty? assert_equal 6, r.srandmember("foo", -6).size end assert_equal 4, r.scard("foo") end def test_smove r.sadd 'foo', 's1' r.sadd 'bar', 's2' assert r.smove('foo', 'bar', 's1') assert r.sismember('bar', 's1') end def test_sinter r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' assert_equal ['s2'], r.sinter('foo', 'bar') end def test_variadic_smove_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal true, r.smove('{1}foo', '{1}bar', 's2') end def test_variadic_sinter_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal %w[s3], r.sinter('{1}foo', '{1}bar') end def test_sinterstore r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sinterstore('baz', 'foo', 'bar') assert_equal ['s2'], r.smembers('baz') end def test_variadic_sinterstore_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal 1, r.sinterstore('{1}baz', '{1}foo', '{1}bar') end def test_sunion r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' assert_equal %w[s1 s2 s3], r.sunion('foo', 'bar').sort end def test_variadic_sunion_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal %w[s1 s2 s3 s4 s5], r.sunion('{1}foo', '{1}bar').sort end def test_sunionstore r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sunionstore('baz', 'foo', 'bar') assert_equal %w[s1 s2 s3], r.smembers('baz').sort end def test_variadic_sunionstore_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal 5, r.sunionstore('{1}baz', '{1}foo', '{1}bar') end def test_sdiff r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' assert_equal ['s1'], r.sdiff('foo', 'bar') assert_equal ['s3'], r.sdiff('bar', 'foo') end def test_variadic_sdiff_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal %w[s1 s2], r.sdiff('{1}foo', '{1}bar').sort end def test_sdiffstore r.sadd 'foo', 's1' r.sadd 'foo', 's2' r.sadd 'bar', 's2' r.sadd 'bar', 's3' r.sdiffstore('baz', 'foo', 'bar') assert_equal ['s1'], r.smembers('baz') end def test_variadic_sdiffstore_expand r.sadd('{1}foo', 's1') r.sadd('{1}foo', 's2') r.sadd('{1}foo', 's3') r.sadd('{1}bar', 's3') r.sadd('{1}bar', 's4') r.sadd('{1}bar', 's5') assert_equal 2, r.sdiffstore('{1}baz', '{1}foo', '{1}bar') end def test_sscan r.sadd('foo', %w[1 2 3 foo foobar feelsgood]) assert_equal %w[0 feelsgood foo foobar], r.sscan('foo', 0, match: 'f*').flatten.sort end end end redis-rb-4.2.5/test/lint/sorted_sets.rb000066400000000000000000000421101375567530100200530ustar00rootroot00000000000000# frozen_string_literal: true module Lint module SortedSets def test_zadd assert_equal 0, r.zcard("foo") assert_equal true, r.zadd("foo", 1, "s1") assert_equal false, r.zadd("foo", 1, "s1") assert_equal 1, r.zcard("foo") r.del "foo" target_version "3.0.2" do # XX option assert_equal 0, r.zcard("foo") assert_equal false, r.zadd("foo", 1, "s1", xx: true) r.zadd("foo", 1, "s1") assert_equal false, r.zadd("foo", 2, "s1", xx: true) assert_equal 2, r.zscore("foo", "s1") r.del "foo" # NX option assert_equal 0, r.zcard("foo") assert_equal true, r.zadd("foo", 1, "s1", nx: true) assert_equal false, r.zadd("foo", 2, "s1", nx: true) assert_equal 1, r.zscore("foo", "s1") assert_equal 1, r.zcard("foo") r.del "foo" # CH option assert_equal 0, r.zcard("foo") assert_equal true, r.zadd("foo", 1, "s1", ch: true) assert_equal false, r.zadd("foo", 1, "s1", ch: true) assert_equal true, r.zadd("foo", 2, "s1", ch: true) assert_equal 1, r.zcard("foo") r.del "foo" # INCR option assert_equal 1.0, r.zadd("foo", 1, "s1", incr: true) assert_equal 11.0, r.zadd("foo", 10, "s1", incr: true) assert_equal(-Float::INFINITY, r.zadd("bar", "-inf", "s1", incr: true)) assert_equal(+Float::INFINITY, r.zadd("bar", "+inf", "s2", incr: true)) r.del 'foo' r.del 'bar' # Incompatible options combination assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", xx: true, nx: true) } end end def test_variadic_zadd target_version "2.3.9" do # 2.4-rc6 # Non-nested array with pairs assert_equal 0, r.zcard("foo") assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"]) assert_equal 1, r.zadd("foo", [4, "s1", 5, "s2", 6, "s3"]) assert_equal 3, r.zcard("foo") r.del "foo" # Nested array with pairs assert_equal 0, r.zcard("foo") assert_equal 2, r.zadd("foo", [[1, "s1"], [2, "s2"]]) assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]]) assert_equal 3, r.zcard("foo") r.del "foo" # Wrong number of arguments assert_raises(Redis::CommandError) { r.zadd("foo", ["bar"]) } assert_raises(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) } end target_version "3.0.2" do # XX option assert_equal 0, r.zcard("foo") assert_equal 0, r.zadd("foo", [1, "s1", 2, "s2"], xx: true) r.zadd("foo", [1, "s1", 2, "s2"]) assert_equal 0, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], xx: true) assert_equal 2, r.zscore("foo", "s1") assert_equal 3, r.zscore("foo", "s2") assert_nil r.zscore("foo", "s3") assert_equal 2, r.zcard("foo") r.del "foo" # NX option assert_equal 0, r.zcard("foo") assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], nx: true) assert_equal 1, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], nx: true) assert_equal 1, r.zscore("foo", "s1") assert_equal 2, r.zscore("foo", "s2") assert_equal 4, r.zscore("foo", "s3") assert_equal 3, r.zcard("foo") r.del "foo" # CH option assert_equal 0, r.zcard("foo") assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"], ch: true) assert_equal 2, r.zadd("foo", [1, "s1", 3, "s2", 4, "s3"], ch: true) assert_equal 3, r.zcard("foo") r.del "foo" # INCR option assert_equal 1.0, r.zadd("foo", [1, "s1"], incr: true) assert_equal 11.0, r.zadd("foo", [10, "s1"], incr: true) assert_equal(-Float::INFINITY, r.zadd("bar", ["-inf", "s1"], incr: true)) assert_equal(+Float::INFINITY, r.zadd("bar", ["+inf", "s2"], incr: true)) assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1", 2, "s2"], incr: true) } r.del 'foo' r.del 'bar' # Incompatible options combination assert_raises(Redis::CommandError) { r.zadd("foo", [1, "s1"], xx: true, nx: true) } end end def test_zrem r.zadd("foo", 1, "s1") r.zadd("foo", 2, "s2") assert_equal 2, r.zcard("foo") assert_equal true, r.zrem("foo", "s1") assert_equal false, r.zrem("foo", "s1") assert_equal 1, r.zcard("foo") end def test_variadic_zrem target_version "2.3.9" do # 2.4-rc6 r.zadd("foo", 1, "s1") r.zadd("foo", 2, "s2") r.zadd("foo", 3, "s3") assert_equal 3, r.zcard("foo") assert_equal 1, r.zrem("foo", ["s1", "aaa"]) assert_equal 0, r.zrem("foo", ["bbb", "ccc", "ddd"]) assert_equal 1, r.zrem("foo", ["eee", "s3"]) assert_equal 1, r.zcard("foo") end end def test_zincrby rv = r.zincrby "foo", 1, "s1" assert_equal 1.0, rv rv = r.zincrby "foo", 10, "s1" assert_equal 11.0, rv rv = r.zincrby "bar", "-inf", "s1" assert_equal(-Float::INFINITY, rv) rv = r.zincrby "bar", "+inf", "s2" assert_equal(+Float::INFINITY, rv) end def test_zrank r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal 2, r.zrank("foo", "s3") end def test_zrevrank r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal 0, r.zrevrank("foo", "s3") end def test_zrange r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal ["s1", "s2"], r.zrange("foo", 0, 1) assert_equal [["s1", 1.0], ["s2", 2.0]], r.zrange("foo", 0, 1, with_scores: true) assert_equal [["s1", 1.0], ["s2", 2.0]], r.zrange("foo", 0, 1, withscores: true) r.zadd "bar", "-inf", "s1" r.zadd "bar", "+inf", "s2" assert_equal [["s1", -Float::INFINITY], ["s2", +Float::INFINITY]], r.zrange("bar", 0, 1, with_scores: true) assert_equal [["s1", -Float::INFINITY], ["s2", +Float::INFINITY]], r.zrange("bar", 0, 1, withscores: true) end def test_zrevrange r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal ["s3", "s2"], r.zrevrange("foo", 0, 1) assert_equal [["s3", 3.0], ["s2", 2.0]], r.zrevrange("foo", 0, 1, with_scores: true) assert_equal [["s3", 3.0], ["s2", 2.0]], r.zrevrange("foo", 0, 1, withscores: true) r.zadd "bar", "-inf", "s1" r.zadd "bar", "+inf", "s2" assert_equal [["s2", +Float::INFINITY], ["s1", -Float::INFINITY]], r.zrevrange("bar", 0, 1, with_scores: true) assert_equal [["s2", +Float::INFINITY], ["s1", -Float::INFINITY]], r.zrevrange("bar", 0, 1, withscores: true) end def test_zrangebyscore r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal ["s2", "s3"], r.zrangebyscore("foo", 2, 3) end def test_zrevrangebyscore r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal ["s3", "s2"], r.zrevrangebyscore("foo", 3, 2) end def test_zrangebyscore_with_limit r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" r.zadd "foo", 4, "s4" assert_equal ["s2"], r.zrangebyscore("foo", 2, 4, limit: [0, 1]) assert_equal ["s3"], r.zrangebyscore("foo", 2, 4, limit: [1, 1]) assert_equal ["s3", "s4"], r.zrangebyscore("foo", 2, 4, limit: [1, 2]) end def test_zrevrangebyscore_with_limit r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" r.zadd "foo", 4, "s4" assert_equal ["s4"], r.zrevrangebyscore("foo", 4, 2, limit: [0, 1]) assert_equal ["s3"], r.zrevrangebyscore("foo", 4, 2, limit: [1, 1]) assert_equal ["s3", "s2"], r.zrevrangebyscore("foo", 4, 2, limit: [1, 2]) end def test_zrangebyscore_with_withscores r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" r.zadd "foo", 4, "s4" assert_equal [["s2", 2.0]], r.zrangebyscore("foo", 2, 4, limit: [0, 1], with_scores: true) assert_equal [["s3", 3.0]], r.zrangebyscore("foo", 2, 4, limit: [1, 1], with_scores: true) assert_equal [["s2", 2.0]], r.zrangebyscore("foo", 2, 4, limit: [0, 1], withscores: true) assert_equal [["s3", 3.0]], r.zrangebyscore("foo", 2, 4, limit: [1, 1], withscores: true) r.zadd "bar", "-inf", "s1" r.zadd "bar", "+inf", "s2" assert_equal [["s1", -Float::INFINITY]], r.zrangebyscore("bar", -Float::INFINITY, +Float::INFINITY, limit: [0, 1], with_scores: true) assert_equal [["s2", +Float::INFINITY]], r.zrangebyscore("bar", -Float::INFINITY, +Float::INFINITY, limit: [1, 1], with_scores: true) assert_equal [["s1", -Float::INFINITY]], r.zrangebyscore("bar", -Float::INFINITY, +Float::INFINITY, limit: [0, 1], withscores: true) assert_equal [["s2", +Float::INFINITY]], r.zrangebyscore("bar", -Float::INFINITY, +Float::INFINITY, limit: [1, 1], withscores: true) end def test_zrevrangebyscore_with_withscores r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" r.zadd "foo", 4, "s4" assert_equal [["s4", 4.0]], r.zrevrangebyscore("foo", 4, 2, limit: [0, 1], with_scores: true) assert_equal [["s3", 3.0]], r.zrevrangebyscore("foo", 4, 2, limit: [1, 1], with_scores: true) assert_equal [["s4", 4.0]], r.zrevrangebyscore("foo", 4, 2, limit: [0, 1], withscores: true) assert_equal [["s3", 3.0]], r.zrevrangebyscore("foo", 4, 2, limit: [1, 1], withscores: true) r.zadd "bar", "-inf", "s1" r.zadd "bar", "+inf", "s2" assert_equal [["s2", +Float::INFINITY]], r.zrevrangebyscore("bar", +Float::INFINITY, -Float::INFINITY, limit: [0, 1], with_scores: true) assert_equal [["s1", -Float::INFINITY]], r.zrevrangebyscore("bar", +Float::INFINITY, -Float::INFINITY, limit: [1, 1], with_scores: true) assert_equal [["s2", +Float::INFINITY]], r.zrevrangebyscore("bar", +Float::INFINITY, -Float::INFINITY, limit: [0, 1], withscores: true) assert_equal [["s1", -Float::INFINITY]], r.zrevrangebyscore("bar", +Float::INFINITY, -Float::INFINITY, limit: [1, 1], withscores: true) end def test_zcard assert_equal 0, r.zcard("foo") r.zadd "foo", 1, "s1" assert_equal 1, r.zcard("foo") end def test_zscore r.zadd "foo", 1, "s1" assert_equal 1.0, r.zscore("foo", "s1") assert_nil r.zscore("foo", "s2") assert_nil r.zscore("bar", "s1") r.zadd "bar", "-inf", "s1" r.zadd "bar", "+inf", "s2" assert_equal(-Float::INFINITY, r.zscore("bar", "s1")) assert_equal(+Float::INFINITY, r.zscore("bar", "s2")) end def test_zremrangebyrank r.zadd "foo", 10, "s1" r.zadd "foo", 20, "s2" r.zadd "foo", 30, "s3" r.zadd "foo", 40, "s4" assert_equal 3, r.zremrangebyrank("foo", 1, 3) assert_equal ["s1"], r.zrange("foo", 0, -1) end def test_zremrangebyscore r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" r.zadd "foo", 4, "s4" assert_equal 3, r.zremrangebyscore("foo", 2, 4) assert_equal ["s1"], r.zrange("foo", 0, -1) end def test_zpopmax target_version('5.0.0') do r.zadd('foo', %w[0 a 1 b 2 c 3 d]) assert_equal ['d', 3.0], r.zpopmax('foo') assert_equal [['c', 2.0], ['b', 1.0]], r.zpopmax('foo', 2) assert_equal [['a', 0.0]], r.zrange('foo', 0, -1, with_scores: true) end end def test_zpopmin target_version('5.0.0') do r.zadd('foo', %w[0 a 1 b 2 c 3 d]) assert_equal ['a', 0.0], r.zpopmin('foo') assert_equal [['b', 1.0], ['c', 2.0]], r.zpopmin('foo', 2) assert_equal [['d', 3.0]], r.zrange('foo', 0, -1, with_scores: true) end end def test_zremrangebylex r.zadd('foo', %w[0 a 0 b 0 c 0 d 0 e 0 f 0 g]) assert_equal 5, r.zremrangebylex('foo', '(b', '[g') end def test_zlexcount target_version '2.8.9' do r.zadd 'foo', 0, 'aaren' r.zadd 'foo', 0, 'abagael' r.zadd 'foo', 0, 'abby' r.zadd 'foo', 0, 'abbygail' assert_equal 4, r.zlexcount('foo', '[a', "[a\xff") assert_equal 4, r.zlexcount('foo', '[aa', "[ab\xff") assert_equal 3, r.zlexcount('foo', '(aaren', "[ab\xff") assert_equal 2, r.zlexcount('foo', '[aba', '(abbygail') assert_equal 1, r.zlexcount('foo', '(aaren', '(abby') end end def test_zrangebylex target_version '2.8.9' do r.zadd 'foo', 0, 'aaren' r.zadd 'foo', 0, 'abagael' r.zadd 'foo', 0, 'abby' r.zadd 'foo', 0, 'abbygail' assert_equal %w[aaren abagael abby abbygail], r.zrangebylex('foo', '[a', "[a\xff") assert_equal %w[aaren abagael], r.zrangebylex('foo', '[a', "[a\xff", limit: [0, 2]) assert_equal %w[abby abbygail], r.zrangebylex('foo', '(abb', "(abb\xff") assert_equal %w[abbygail], r.zrangebylex('foo', '(abby', "(abby\xff") end end def test_zrevrangebylex target_version '2.9.9' do r.zadd 'foo', 0, 'aaren' r.zadd 'foo', 0, 'abagael' r.zadd 'foo', 0, 'abby' r.zadd 'foo', 0, 'abbygail' assert_equal %w[abbygail abby abagael aaren], r.zrevrangebylex('foo', "[a\xff", '[a') assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "[a\xff", '[a', limit: [0, 2]) assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "(abb\xff", '(abb') assert_equal %w[abbygail], r.zrevrangebylex('foo', "(abby\xff", '(abby') end end def test_zcount r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'foo', 3, 's3' assert_equal 2, r.zcount('foo', 2, 3) end def test_zunionstore r.zadd 'foo', 1, 's1' r.zadd 'bar', 2, 's2' r.zadd 'foo', 3, 's3' r.zadd 'bar', 4, 's4' assert_equal 4, r.zunionstore('foobar', %w[foo bar]) assert_equal %w[s1 s2 s3 s4], r.zrange('foobar', 0, -1) end def test_zunionstore_with_weights r.zadd 'foo', 1, 's1' r.zadd 'foo', 3, 's3' r.zadd 'bar', 20, 's2' r.zadd 'bar', 40, 's4' assert_equal 4, r.zunionstore('foobar', %w[foo bar]) assert_equal %w[s1 s3 s2 s4], r.zrange('foobar', 0, -1) assert_equal 4, r.zunionstore('foobar', %w[foo bar], weights: [10, 1]) assert_equal %w[s1 s2 s3 s4], r.zrange('foobar', 0, -1) end def test_zunionstore_with_aggregate r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'bar', 4, 's2' r.zadd 'bar', 3, 's3' assert_equal 3, r.zunionstore('foobar', %w[foo bar]) assert_equal %w[s1 s3 s2], r.zrange('foobar', 0, -1) assert_equal 3, r.zunionstore('foobar', %w[foo bar], aggregate: :min) assert_equal %w[s1 s2 s3], r.zrange('foobar', 0, -1) assert_equal 3, r.zunionstore('foobar', %w[foo bar], aggregate: :max) assert_equal %w[s1 s3 s2], r.zrange('foobar', 0, -1) end def test_zunionstore_expand r.zadd('{1}foo', %w[0 a 1 b 2 c]) r.zadd('{1}bar', %w[0 c 1 d 2 e]) assert_equal 5, r.zunionstore('{1}baz', %w[{1}foo {1}bar]) end def test_zinterstore r.zadd 'foo', 1, 's1' r.zadd 'bar', 2, 's1' r.zadd 'foo', 3, 's3' r.zadd 'bar', 4, 's4' assert_equal 1, r.zinterstore('foobar', %w[foo bar]) assert_equal ['s1'], r.zrange('foobar', 0, -1) end def test_zinterstore_with_weights r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'foo', 3, 's3' r.zadd 'bar', 20, 's2' r.zadd 'bar', 30, 's3' r.zadd 'bar', 40, 's4' assert_equal 2, r.zinterstore('foobar', %w[foo bar]) assert_equal %w[s2 s3], r.zrange('foobar', 0, -1) assert_equal 2, r.zinterstore('foobar', %w[foo bar], weights: [10, 1]) assert_equal %w[s2 s3], r.zrange('foobar', 0, -1) assert_equal 40.0, r.zscore('foobar', 's2') assert_equal 60.0, r.zscore('foobar', 's3') end def test_zinterstore_with_aggregate r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'foo', 3, 's3' r.zadd 'bar', 20, 's2' r.zadd 'bar', 30, 's3' r.zadd 'bar', 40, 's4' assert_equal 2, r.zinterstore('foobar', %w[foo bar]) assert_equal %w[s2 s3], r.zrange('foobar', 0, -1) assert_equal 22.0, r.zscore('foobar', 's2') assert_equal 33.0, r.zscore('foobar', 's3') assert_equal 2, r.zinterstore('foobar', %w[foo bar], aggregate: :min) assert_equal %w[s2 s3], r.zrange('foobar', 0, -1) assert_equal 2.0, r.zscore('foobar', 's2') assert_equal 3.0, r.zscore('foobar', 's3') assert_equal 2, r.zinterstore('foobar', %w[foo bar], aggregate: :max) assert_equal %w[s2 s3], r.zrange('foobar', 0, -1) assert_equal 20.0, r.zscore('foobar', 's2') assert_equal 30.0, r.zscore('foobar', 's3') end def test_zinterstore_expand r.zadd '{1}foo', %w[0 s1 1 s2 2 s3] r.zadd '{1}bar', %w[0 s3 1 s4 2 s5] assert_equal 1, r.zinterstore('{1}baz', %w[{1}foo {1}bar], weights: [2.0, 3.0]) end def test_zscan r.zadd('foo', %w[0 a 1 b 2 c]) expected = ['0', [['a', 0.0], ['b', 1.0], ['c', 2.0]]] assert_equal expected, r.zscan('foo', 0) end end end redis-rb-4.2.5/test/lint/streams.rb000066400000000000000000000556641375567530100172150ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Streams MIN_REDIS_VERSION = '4.9.0' ENTRY_ID_FORMAT = /\d+-\d+/.freeze def setup super omit_version(MIN_REDIS_VERSION) end def test_xinfo_with_stream_subcommand redis.xadd('s1', { f: 'v1' }) redis.xadd('s1', { f: 'v2' }) redis.xadd('s1', { f: 'v3' }) redis.xadd('s1', { f: 'v4' }) redis.xgroup(:create, 's1', 'g1', '$') actual = redis.xinfo(:stream, 's1') assert_match ENTRY_ID_FORMAT, actual['last-generated-id'] assert_equal 4, actual['length'] assert_equal 1, actual['groups'] assert_equal true, actual.key?('radix-tree-keys') assert_equal true, actual.key?('radix-tree-nodes') assert_kind_of Array, actual['first-entry'] assert_kind_of Array, actual['last-entry'] end def test_xinfo_with_groups_subcommand redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') actual = redis.xinfo(:groups, 's1').first assert_equal 0, actual['consumers'] assert_equal 0, actual['pending'] assert_equal 'g1', actual['name'] assert_match ENTRY_ID_FORMAT, actual['last-delivered-id'] end def test_xinfo_with_consumers_subcommand redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') assert_equal [], redis.xinfo(:consumers, 's1', 'g1') end def test_xinfo_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xinfo('', '', '') } assert_raises(Redis::CommandError) { redis.xinfo(nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xinfo(:stream, nil) } assert_raises(Redis::CommandError) { redis.xinfo(:groups, nil) } assert_raises(Redis::CommandError) { redis.xinfo(:consumers, nil) } assert_raises(Redis::CommandError) { redis.xinfo(:consumers, 's1', nil) } end def test_xadd_with_entry_as_splatted_params assert_match ENTRY_ID_FORMAT, redis.xadd('s1', { f1: 'v1', f2: 'v2' }) end def test_xadd_with_entry_as_a_hash_literal entry = { f1: 'v1', f2: 'v2' } assert_match ENTRY_ID_FORMAT, redis.xadd('s1', entry) end def test_xadd_with_entry_id_option entry_id = "#{Time.now.strftime('%s%L')}-14" assert_equal entry_id, redis.xadd('s1', { f1: 'v1', f2: 'v2' }, id: entry_id) end def test_xadd_with_invalid_entry_id_option entry_id = 'invalid-format-entry-id' assert_raises(Redis::CommandError, 'ERR Invalid stream ID specified as stream command argument') do redis.xadd('s1', { f1: 'v1', f2: 'v2' }, id: entry_id) end end def test_xadd_with_old_entry_id_option redis.xadd('s1', { f1: 'v1', f2: 'v2' }, id: '0-1') err_msg = 'ERR The ID specified in XADD is equal or smaller than the target stream top item' assert_raises(Redis::CommandError, err_msg) do redis.xadd('s1', { f1: 'v1', f2: 'v2' }, id: '0-0') end end def test_xadd_with_maxlen_and_approximate_option actual = redis.xadd('s1', { f1: 'v1', f2: 'v2' }, maxlen: 2, approximate: true) assert_match ENTRY_ID_FORMAT, actual end def test_xadd_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xadd(nil, {}) } assert_raises(Redis::CommandError) { redis.xadd('', {}) } assert_raises(Redis::CommandError) { redis.xadd('s1', {}) } end def test_xtrim redis.xadd('s1', { f: 'v1' }) redis.xadd('s1', { f: 'v2' }) redis.xadd('s1', { f: 'v3' }) redis.xadd('s1', { f: 'v4' }) assert_equal 2, redis.xtrim('s1', 2) end def test_xtrim_with_approximate_option redis.xadd('s1', { f: 'v1' }) redis.xadd('s1', { f: 'v2' }) redis.xadd('s1', { f: 'v3' }) redis.xadd('s1', { f: 'v4' }) assert_equal 0, redis.xtrim('s1', 2, approximate: true) end def test_xtrim_with_not_existed_stream assert_equal 0, redis.xtrim('not-existed-stream', 2) end def test_xtrim_with_invalid_arguments assert_equal 0, redis.xtrim('', '') assert_equal 0, redis.xtrim(nil, nil) assert_equal 0, redis.xtrim('s1', 0) assert_equal 0, redis.xtrim('s1', -1, approximate: true) end def test_xdel_with_splatted_entry_ids redis.xadd('s1', { f: '1' }, id: '0-1') redis.xadd('s1', { f: '2' }, id: '0-2') assert_equal 2, redis.xdel('s1', '0-1', '0-2', '0-3') end def test_xdel_with_arrayed_entry_ids redis.xadd('s1', { f: '1' }, id: '0-1') assert_equal 1, redis.xdel('s1', ['0-1', '0-2']) end def test_xdel_with_invalid_entry_ids assert_equal 0, redis.xdel('s1', 'invalid_format') end def test_xdel_with_invalid_arguments assert_equal 0, redis.xdel(nil, nil) assert_equal 0, redis.xdel(nil, [nil]) assert_equal 0, redis.xdel('', '') assert_equal 0, redis.xdel('', ['']) assert_raises(Redis::CommandError) { redis.xdel('s1', []) } end def test_xrange redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') actual = redis.xrange('s1') assert_equal(%w(v1 v2 v3), actual.map { |i| i.last['f'] }) end def test_xrange_with_start_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrange('s1', '0-2') assert_equal %w(0-2 0-3), actual.map(&:first) end def test_xrange_with_end_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrange('s1', '-', '0-2') assert_equal %w(0-1 0-2), actual.map(&:first) end def test_xrange_with_start_and_end_options redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrange('s1', '0-2', '0-2') assert_equal %w(0-2), actual.map(&:first) end def test_xrange_with_incomplete_entry_id_options redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '1-1') redis.xadd('s1', { f: 'v' }, id: '2-1') actual = redis.xrange('s1', '0', '1') assert_equal 2, actual.size assert_equal %w(0-1 1-1), actual.map(&:first) end def test_xrange_with_count_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrange('s1', count: 2) assert_equal %w(0-1 0-2), actual.map(&:first) end def test_xrange_with_not_existed_stream_key assert_equal([], redis.xrange('not-existed')) end def test_xrange_with_invalid_entry_id_options assert_raises(Redis::CommandError) { redis.xrange('s1', 'invalid', 'invalid') } end def test_xrange_with_invalid_arguments assert_equal([], redis.xrange(nil)) assert_equal([], redis.xrange('')) end def test_xrevrange redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') actual = redis.xrevrange('s1') assert_equal %w(0-3 0-2 0-1), actual.map(&:first) assert_equal(%w(v3 v2 v1), actual.map { |i| i.last['f'] }) end def test_xrevrange_with_start_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrevrange('s1', '+', '0-2') assert_equal %w(0-3 0-2), actual.map(&:first) end def test_xrevrange_with_end_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrevrange('s1', '0-2') assert_equal %w(0-2 0-1), actual.map(&:first) end def test_xrevrange_with_start_and_end_options redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrevrange('s1', '0-2', '0-2') assert_equal %w(0-2), actual.map(&:first) end def test_xrevrange_with_incomplete_entry_id_options redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '1-1') redis.xadd('s1', { f: 'v' }, id: '2-1') actual = redis.xrevrange('s1', '1', '0') assert_equal 2, actual.size assert_equal '1-1', actual.first.first end def test_xrevrange_with_count_option redis.xadd('s1', { f: 'v' }, id: '0-1') redis.xadd('s1', { f: 'v' }, id: '0-2') redis.xadd('s1', { f: 'v' }, id: '0-3') actual = redis.xrevrange('s1', count: 2) assert_equal 2, actual.size assert_equal '0-3', actual.first.first end def test_xrevrange_with_not_existed_stream_key assert_equal([], redis.xrevrange('not-existed')) end def test_xrevrange_with_invalid_entry_id_options assert_raises(Redis::CommandError) { redis.xrevrange('s1', 'invalid', 'invalid') } end def test_xrevrange_with_invalid_arguments assert_equal([], redis.xrevrange(nil)) assert_equal([], redis.xrevrange('')) end def test_xlen redis.xadd('s1', { f: 'v1' }) redis.xadd('s1', { f: 'v2' }) assert_equal 2, redis.xlen('s1') end def test_xlen_with_not_existed_key assert_equal 0, redis.xlen('not-existed') end def test_xlen_with_invalid_key assert_equal 0, redis.xlen(nil) assert_equal 0, redis.xlen('') end def test_xread_with_a_key redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v2' }, id: '0-2') actual = redis.xread('s1', 0) assert_equal(%w(v1 v2), actual.fetch('s1').map { |i| i.last['f'] }) end def test_xread_with_multiple_keys redis.xadd('s1', { f: 'v01' }, id: '0-1') redis.xadd('s1', { f: 'v02' }, id: '0-2') redis.xadd('s2', { f: 'v11' }, id: '1-1') redis.xadd('s2', { f: 'v12' }, id: '1-2') actual = redis.xread(%w[s1 s2], %w[0-1 1-1]) assert_equal 1, actual['s1'].size assert_equal 1, actual['s2'].size assert_equal 'v02', actual['s1'][0].last['f'] assert_equal 'v12', actual['s2'][0].last['f'] end def test_xread_with_count_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v2' }, id: '0-2') actual = redis.xread('s1', 0, count: 1) assert_equal 1, actual['s1'].size end def test_xread_with_block_option actual = redis.xread('s1', '$', block: LOW_TIMEOUT * 1000) assert_equal({}, actual) end def test_xread_does_not_raise_timeout_error_when_the_block_option_is_zero_msec prepared = false actual = nil wire = Wire.new do prepared = true actual = redis.xread('s1', 0, block: 0) end Wire.pass until prepared redis.dup.xadd('s1', { f: 'v1' }, id: '0-1') wire.join assert_equal(['v1'], actual.fetch('s1').map { |i| i.last['f'] }) end def test_xread_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xread(nil, nil) } assert_raises(Redis::CommandError) { redis.xread('', '') } assert_raises(Redis::CommandError) { redis.xread([], []) } assert_raises(Redis::CommandError) { redis.xread([''], ['']) } assert_raises(Redis::CommandError) { redis.xread('s1', '0-0', count: 'a') } assert_raises(Redis::CommandError) { redis.xread('s1', %w[0-0 0-0]) } end def test_xgroup_with_create_subcommand redis.xadd('s1', { f: 'v' }) assert_equal 'OK', redis.xgroup(:create, 's1', 'g1', '$') end def test_xgroup_with_create_subcommand_and_mkstream_option err_msg = 'ERR The XGROUP subcommand requires the key to exist. '\ 'Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.' assert_raises(Redis::CommandError, err_msg) { redis.xgroup(:create, 's2', 'g1', '$') } assert_equal 'OK', redis.xgroup(:create, 's2', 'g1', '$', mkstream: true) end def test_xgroup_with_create_subcommand_and_existed_stream_key redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') assert_raises(Redis::CommandError, 'BUSYGROUP Consumer Group name already exists') do redis.xgroup(:create, 's1', 'g1', '$') end end def test_xgroup_with_setid_subcommand redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') assert_equal 'OK', redis.xgroup(:setid, 's1', 'g1', '0') end def test_xgroup_with_destroy_subcommand redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') assert_equal 1, redis.xgroup(:destroy, 's1', 'g1') end def test_xgroup_with_delconsumer_subcommand redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') assert_equal 0, redis.xgroup(:delconsumer, 's1', 'g1', 'c1') end def test_xgroup_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xgroup(nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xgroup('', '', '') } end def test_xreadgroup_with_a_key redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') actual = redis.xreadgroup('g1', 'c1', 's1', '>') assert_equal 2, actual['s1'].size assert_equal 'v2', actual['s1'][0].last['f'] assert_equal 'v3', actual['s1'][1].last['f'] end def test_xreadgroup_with_multiple_keys redis.xadd('s1', { f: 'v01' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s2', { f: 'v11' }, id: '1-1') redis.xgroup(:create, 's2', 'g1', '$') redis.xadd('s1', { f: 'v02' }, id: '0-2') redis.xadd('s2', { f: 'v12' }, id: '1-2') actual = redis.xreadgroup('g1', 'c1', %w[s1 s2], %w[> >]) assert_equal 1, actual['s1'].size assert_equal 1, actual['s2'].size assert_equal 'v02', actual['s1'][0].last['f'] assert_equal 'v12', actual['s2'][0].last['f'] end def test_xreadgroup_with_count_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') actual = redis.xreadgroup('g1', 'c1', 's1', '>', count: 1) assert_equal 1, actual['s1'].size end def test_xreadgroup_with_noack_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') actual = redis.xreadgroup('g1', 'c1', 's1', '>', noack: true) assert_equal 2, actual['s1'].size end def test_xreadgroup_with_block_option redis.xadd('s1', { f: 'v' }) redis.xgroup(:create, 's1', 'g1', '$') actual = redis.xreadgroup('g1', 'c1', 's1', '>', block: LOW_TIMEOUT * 1000) assert_equal({}, actual) end def test_xreadgroup_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xreadgroup(nil, nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xreadgroup('', '', '', '') } assert_raises(Redis::CommandError) { redis.xreadgroup('', '', [], []) } assert_raises(Redis::CommandError) { redis.xreadgroup('', '', [''], ['']) } redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') assert_raises(Redis::CommandError) { redis.xreadgroup('g1', 'c1', 's1', '>', count: 'a') } assert_raises(Redis::CommandError) { redis.xreadgroup('g1', 'c1', 's1', %w[> >]) } end def test_xack_with_a_entry_id redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xreadgroup('g1', 'c1', 's1', '>') redis.xadd('s1', { f: 'v3' }, id: '0-3') assert_equal 1, redis.xack('s1', 'g1', '0-2') end def test_xack_with_splatted_entry_ids redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') redis.xadd('s1', { f: 'v4' }, id: '0-4') redis.xadd('s1', { f: 'v5' }, id: '0-5') assert_equal 2, redis.xack('s1', 'g1', '0-2', '0-3') end def test_xack_with_arrayed_entry_ids redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') redis.xadd('s1', { f: 'v4' }, id: '0-4') redis.xadd('s1', { f: 'v5' }, id: '0-5') assert_equal 2, redis.xack('s1', 'g1', %w[0-2 0-3]) end def test_xack_with_invalid_arguments assert_equal 0, redis.xack(nil, nil, nil) assert_equal 0, redis.xack('', '', '') assert_raises(Redis::CommandError) { redis.xack('', '', []) } assert_equal 0, redis.xack('', '', ['']) end def test_xclaim_with_splatted_entry_ids redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3') assert_equal %w(0-2 0-3), actual.map(&:first) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_arrayed_entry_ids redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, %w[0-2 0-3]) assert_equal %w(0-2 0-3), actual.map(&:first) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_idle_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3', idle: 0) assert_equal %w(0-2 0-3), actual.map(&:first) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_time_option time = Time.now.strftime('%s%L') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3', time: time) assert_equal %w(0-2 0-3), actual.map(&:first) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_retrycount_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3', retrycount: 10) assert_equal %w(0-2 0-3), actual.map(&:first) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_force_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3', force: true) assert_equal(%w(0-2 0-3), actual.map(&:first)) assert_equal(%w(v2 v3), actual.map { |i| i.last['f'] }) end def test_xclaim_with_justid_option redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') sleep 0.01 actual = redis.xclaim('s1', 'g1', 'c2', 10, '0-2', '0-3', justid: true) assert_equal 2, actual.size assert_equal '0-2', actual[0] assert_equal '0-3', actual[1] end def test_xclaim_with_invalid_arguments assert_raises(Redis::CommandError) { redis.xclaim(nil, nil, nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xclaim('', '', '', '', '') } end def test_xpending redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') actual = redis.xpending('s1', 'g1') assert_equal 2, actual['size'] assert_equal '0-2', actual['min_entry_id'] assert_equal '0-3', actual['max_entry_id'] assert_equal '2', actual['consumers']['c1'] end def test_xpending_with_range_options redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') redis.xadd('s1', { f: 'v4' }, id: '0-4') redis.xreadgroup('g1', 'c2', 's1', '>') actual = redis.xpending('s1', 'g1', '-', '+', 10) assert_equal 3, actual.size assert_equal '0-2', actual[0]['entry_id'] assert_equal 'c1', actual[0]['consumer'] assert_equal true, actual[0]['elapsed'] >= 0 assert_equal 1, actual[0]['count'] assert_equal '0-3', actual[1]['entry_id'] assert_equal 'c1', actual[1]['consumer'] assert_equal true, actual[1]['elapsed'] >= 0 assert_equal 1, actual[1]['count'] assert_equal '0-4', actual[2]['entry_id'] assert_equal 'c2', actual[2]['consumer'] assert_equal true, actual[2]['elapsed'] >= 0 assert_equal 1, actual[2]['count'] end def test_xpending_with_range_and_consumer_options redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') redis.xadd('s1', { f: 'v2' }, id: '0-2') redis.xadd('s1', { f: 'v3' }, id: '0-3') redis.xreadgroup('g1', 'c1', 's1', '>') redis.xadd('s1', { f: 'v4' }, id: '0-4') redis.xreadgroup('g1', 'c2', 's1', '>') actual = redis.xpending('s1', 'g1', '-', '+', 10, 'c1') assert_equal 2, actual.size assert_equal '0-2', actual[0]['entry_id'] assert_equal 'c1', actual[0]['consumer'] assert_equal true, actual[0]['elapsed'] >= 0 assert_equal 1, actual[0]['count'] assert_equal '0-3', actual[1]['entry_id'] assert_equal 'c1', actual[1]['consumer'] assert_equal true, actual[1]['elapsed'] >= 0 assert_equal 1, actual[1]['count'] end end end redis-rb-4.2.5/test/lint/strings.rb000066400000000000000000000207111375567530100172110ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Strings def mock(*args, &block) redis_mock(*args, &block) end def test_set_and_get r.set("foo", "s1") assert_equal "s1", r.get("foo") end def test_set_and_get_with_newline_characters r.set("foo", "1\n") assert_equal "1\n", r.get("foo") end def test_set_and_get_with_non_string_value value = ["a", "b"] r.set("foo", value) assert_equal value.to_s, r.get("foo") end def test_set_and_get_with_ascii_characters with_external_encoding("ASCII-8BIT") do (0..255).each do |i| str = "#{i.chr}---#{i.chr}" r.set("foo", str) assert_equal str, r.get("foo") end end end def test_set_with_ex target_version "2.6.12" do r.set("foo", "bar", ex: 2) assert_in_range 0..2, r.ttl("foo") end end def test_set_with_px target_version "2.6.12" do r.set("foo", "bar", px: 2000) assert_in_range 0..2, r.ttl("foo") end end def test_set_with_nx target_version "2.6.12" do r.set("foo", "qux", nx: true) assert !r.set("foo", "bar", nx: true) assert_equal "qux", r.get("foo") r.del("foo") assert r.set("foo", "bar", nx: true) assert_equal "bar", r.get("foo") end end def test_set_with_xx target_version "2.6.12" do r.set("foo", "qux") assert r.set("foo", "bar", xx: true) assert_equal "bar", r.get("foo") r.del("foo") assert !r.set("foo", "bar", xx: true) end end def test_set_with_keepttl target_version "6.0.0" do r.set("foo", "qux", ex: 2) assert_in_range 0..2, r.ttl("foo") r.set("foo", "bar", keepttl: true) assert_in_range 0..2, r.ttl("foo") end end def test_setex assert r.setex("foo", 1, "bar") assert_equal "bar", r.get("foo") assert [0, 1].include? r.ttl("foo") end def test_setex_with_non_string_value value = ["b", "a", "r"] assert r.setex("foo", 1, value) assert_equal value.to_s, r.get("foo") assert [0, 1].include? r.ttl("foo") end def test_psetex target_version "2.5.4" do assert r.psetex("foo", 1000, "bar") assert_equal "bar", r.get("foo") assert [0, 1].include? r.ttl("foo") end end def test_psetex_with_non_string_value target_version "2.5.4" do value = ["b", "a", "r"] assert r.psetex("foo", 1000, value) assert_equal value.to_s, r.get("foo") assert [0, 1].include? r.ttl("foo") end end def test_getset r.set("foo", "bar") assert_equal "bar", r.getset("foo", "baz") assert_equal "baz", r.get("foo") end def test_getset_with_non_string_value r.set("foo", "zap") value = ["b", "a", "r"] assert_equal "zap", r.getset("foo", value) assert_equal value.to_s, r.get("foo") end def test_setnx r.set("foo", "qux") assert !r.setnx("foo", "bar") assert_equal "qux", r.get("foo") r.del("foo") assert r.setnx("foo", "bar") assert_equal "bar", r.get("foo") end def test_setnx_with_non_string_value value = ["b", "a", "r"] r.set("foo", "qux") assert !r.setnx("foo", value) assert_equal "qux", r.get("foo") r.del("foo") assert r.setnx("foo", value) assert_equal value.to_s, r.get("foo") end def test_incr assert_equal 1, r.incr("foo") assert_equal 2, r.incr("foo") assert_equal 3, r.incr("foo") end def test_incrby assert_equal 1, r.incrby("foo", 1) assert_equal 3, r.incrby("foo", 2) assert_equal 6, r.incrby("foo", 3) end def test_incrbyfloat target_version "2.5.4" do assert_equal 1.23, r.incrbyfloat("foo", 1.23) assert_equal 2, r.incrbyfloat("foo", 0.77) assert_equal 1.9, r.incrbyfloat("foo", -0.1) end end def test_decr r.set("foo", 3) assert_equal 2, r.decr("foo") assert_equal 1, r.decr("foo") assert_equal 0, r.decr("foo") end def test_decrby r.set("foo", 6) assert_equal 3, r.decrby("foo", 3) assert_equal 1, r.decrby("foo", 2) assert_equal 0, r.decrby("foo", 1) end def test_append r.set "foo", "s" r.append "foo", "1" assert_equal "s1", r.get("foo") end def test_getbit r.set("foo", "a") assert_equal 1, r.getbit("foo", 1) assert_equal 1, r.getbit("foo", 2) assert_equal 0, r.getbit("foo", 3) assert_equal 0, r.getbit("foo", 4) assert_equal 0, r.getbit("foo", 5) assert_equal 0, r.getbit("foo", 6) assert_equal 1, r.getbit("foo", 7) end def test_setbit r.set("foo", "a") r.setbit("foo", 6, 1) assert_equal "c", r.get("foo") end def test_bitcount target_version "2.5.10" do r.set("foo", "abcde") assert_equal 10, r.bitcount("foo", 1, 3) assert_equal 17, r.bitcount("foo", 0, -1) end end def test_getrange r.set("foo", "abcde") assert_equal "bcd", r.getrange("foo", 1, 3) assert_equal "abcde", r.getrange("foo", 0, -1) end def test_setrange r.set("foo", "abcde") r.setrange("foo", 1, "bar") assert_equal "abare", r.get("foo") end def test_setrange_with_non_string_value r.set("foo", "abcde") value = ["b", "a", "r"] r.setrange("foo", 2, value) assert_equal "ab#{value}", r.get("foo") end def test_strlen r.set "foo", "lorem" assert_equal 5, r.strlen("foo") end def test_bitfield target_version('3.2.0') do mock(bitfield: ->(*_) { "*2\r\n:1\r\n:0\r\n" }) do |redis| assert_equal [1, 0], redis.bitfield('foo', 'INCRBY', 'i5', 100, 1, 'GET', 'u4', 0) end end end def test_mget r.set('{1}foo', 's1') r.set('{1}bar', 's2') assert_equal %w[s1 s2], r.mget('{1}foo', '{1}bar') assert_equal ['s1', 's2', nil], r.mget('{1}foo', '{1}bar', '{1}baz') end def test_mget_mapped r.set('{1}foo', 's1') r.set('{1}bar', 's2') response = r.mapped_mget('{1}foo', '{1}bar') assert_equal 's1', response['{1}foo'] assert_equal 's2', response['{1}bar'] response = r.mapped_mget('{1}foo', '{1}bar', '{1}baz') assert_equal 's1', response['{1}foo'] assert_equal 's2', response['{1}bar'] assert_nil response['{1}baz'] end def test_mapped_mget_in_a_pipeline_returns_hash r.set('{1}foo', 's1') r.set('{1}bar', 's2') result = r.pipelined do r.mapped_mget('{1}foo', '{1}bar') end assert_equal({ '{1}foo' => 's1', '{1}bar' => 's2' }, result[0]) end def test_mset r.mset('{1}foo', 's1', '{1}bar', 's2') assert_equal 's1', r.get('{1}foo') assert_equal 's2', r.get('{1}bar') end def test_mset_mapped r.mapped_mset('{1}foo' => 's1', '{1}bar' => 's2') assert_equal 's1', r.get('{1}foo') assert_equal 's2', r.get('{1}bar') end def test_msetnx r.set('{1}foo', 's1') assert_equal false, r.msetnx('{1}foo', 's2', '{1}bar', 's3') assert_equal 's1', r.get('{1}foo') assert_nil r.get('{1}bar') r.del('{1}foo') assert_equal true, r.msetnx('{1}foo', 's2', '{1}bar', 's3') assert_equal 's2', r.get('{1}foo') assert_equal 's3', r.get('{1}bar') end def test_msetnx_mapped r.set('{1}foo', 's1') assert_equal false, r.mapped_msetnx('{1}foo' => 's2', '{1}bar' => 's3') assert_equal 's1', r.get('{1}foo') assert_nil r.get('{1}bar') r.del('{1}foo') assert_equal true, r.mapped_msetnx('{1}foo' => 's2', '{1}bar' => 's3') assert_equal 's2', r.get('{1}foo') assert_equal 's3', r.get('{1}bar') end def test_bitop with_external_encoding('UTF-8') do target_version '2.5.10' do r.set('foo{1}', 'a') r.set('bar{1}', 'b') r.bitop(:and, 'foo&bar{1}', 'foo{1}', 'bar{1}') assert_equal "\x60", r.get('foo&bar{1}') r.bitop(:or, 'foo|bar{1}', 'foo{1}', 'bar{1}') assert_equal "\x63", r.get('foo|bar{1}') r.bitop(:xor, 'foo^bar{1}', 'foo{1}', 'bar{1}') assert_equal "\x03", r.get('foo^bar{1}') r.bitop(:not, '~foo{1}', 'foo{1}') assert_equal "\x9E", r.get('~foo{1}') end end end end end redis-rb-4.2.5/test/lint/value_types.rb000066400000000000000000000072721375567530100200670ustar00rootroot00000000000000# frozen_string_literal: true module Lint module ValueTypes def test_exists assert_equal false, r.exists("foo") r.set("foo", "s1") assert_equal true, r.exists("foo") end def test_exists_integer previous_exists_returns_integer = Redis.exists_returns_integer Redis.exists_returns_integer = true assert_equal 0, r.exists("foo") r.set("foo", "s1") assert_equal 1, r.exists("foo") ensure Redis.exists_returns_integer = previous_exists_returns_integer end def test_variadic_exists assert_equal 0, r.exists("{1}foo", "{1}bar") r.set("{1}foo", "s1") assert_equal 1, r.exists("{1}foo", "{1}bar") r.set("{1}bar", "s2") assert_equal 2, r.exists("{1}foo", "{1}bar") end def test_exists? assert_equal false, r.exists?("{1}foo", "{1}bar") r.set("{1}foo", "s1") assert_equal true, r.exists?("{1}foo") r.set("{1}bar", "s1") assert_equal true, r.exists?("{1}foo", "{1}bar") end def test_type assert_equal "none", r.type("foo") r.set("foo", "s1") assert_equal "string", r.type("foo") end def test_keys r.set("f", "s1") r.set("fo", "s2") r.set("foo", "s3") assert_equal ["f", "fo", "foo"], r.keys("f*").sort end def test_expire r.set("foo", "s1") assert r.expire("foo", 2) assert_in_range 0..2, r.ttl("foo") end def test_pexpire target_version "2.5.4" do r.set("foo", "s1") assert r.pexpire("foo", 2000) assert_in_range 0..2, r.ttl("foo") end end def test_expireat r.set("foo", "s1") assert r.expireat("foo", (Time.now + 2).to_i) assert_in_range 0..2, r.ttl("foo") end def test_pexpireat target_version "2.5.4" do r.set("foo", "s1") assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000) assert_in_range 0..2, r.ttl("foo") end end def test_persist r.set("foo", "s1") r.expire("foo", 1) r.persist("foo") assert(r.ttl("foo") == -1) end def test_ttl r.set("foo", "s1") r.expire("foo", 2) assert_in_range 0..2, r.ttl("foo") end def test_pttl target_version "2.5.4" do r.set("foo", "s1") r.expire("foo", 2) assert_in_range 1..2000, r.pttl("foo") end end def test_dump_and_restore target_version "2.5.7" do r.set("foo", "a") v = r.dump("foo") r.del("foo") assert r.restore("foo", 1000, v) assert_equal "a", r.get("foo") assert [0, 1].include? r.ttl("foo") r.rpush("bar", ["b", "c", "d"]) w = r.dump("bar") r.del("bar") assert r.restore("bar", 1000, w) assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1) assert [0, 1].include? r.ttl("bar") r.set("bar", "somethingelse") assert_raises(Redis::CommandError) { r.restore("bar", 1000, w) } # ensure by default replace is false assert_raises(Redis::CommandError) { r.restore("bar", 1000, w, replace: false) } assert_equal "somethingelse", r.get("bar") assert r.restore("bar", 1000, w, replace: true) assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1) assert [0, 1].include? r.ttl("bar") end end def test_move r.select 14 r.flushdb r.set "bar", "s3" r.select 15 r.set "foo", "s1" r.set "bar", "s2" assert r.move("foo", 14) assert_nil r.get("foo") assert !r.move("bar", 14) assert_equal "s2", r.get("bar") r.select 14 assert_equal "s1", r.get("foo") assert_equal "s3", r.get("bar") end end end redis-rb-4.2.5/test/persistence_control_commands_test.rb000066400000000000000000000010121375567530100235470ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestPersistenceControlCommands < Minitest::Test include Helper::Client def test_save redis_mock(save: -> { "+SAVE" }) do |redis| assert_equal "SAVE", redis.save end end def test_bgsave redis_mock(bgsave: -> { "+BGSAVE" }) do |redis| assert_equal "BGSAVE", redis.bgsave end end def test_lastsave redis_mock(lastsave: -> { "+LASTSAVE" }) do |redis| assert_equal "LASTSAVE", redis.lastsave end end end redis-rb-4.2.5/test/pipelining_commands_test.rb000066400000000000000000000127051375567530100216340ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestPipeliningCommands < Minitest::Test include Helper::Client def test_bulk_commands r.pipelined do r.lpush "foo", "s1" r.lpush "foo", "s2" end assert_equal 2, r.llen("foo") assert_equal "s2", r.lpop("foo") assert_equal "s1", r.lpop("foo") end def test_multi_bulk_commands r.pipelined do r.mset("foo", "s1", "bar", "s2") r.mset("baz", "s3", "qux", "s4") end assert_equal "s1", r.get("foo") assert_equal "s2", r.get("bar") assert_equal "s3", r.get("baz") assert_equal "s4", r.get("qux") end def test_bulk_and_multi_bulk_commands_mixed r.pipelined do r.lpush "foo", "s1" r.lpush "foo", "s2" r.mset("baz", "s3", "qux", "s4") end assert_equal 2, r.llen("foo") assert_equal "s2", r.lpop("foo") assert_equal "s1", r.lpop("foo") assert_equal "s3", r.get("baz") assert_equal "s4", r.get("qux") end def test_multi_bulk_and_bulk_commands_mixed r.pipelined do r.mset("baz", "s3", "qux", "s4") r.lpush "foo", "s1" r.lpush "foo", "s2" end assert_equal 2, r.llen("foo") assert_equal "s2", r.lpop("foo") assert_equal "s1", r.lpop("foo") assert_equal "s3", r.get("baz") assert_equal "s4", r.get("qux") end def test_pipelined_with_an_empty_block r.pipelined do end assert_equal 0, r.dbsize end def test_returning_the_result_of_a_pipeline result = r.pipelined do r.set "foo", "bar" r.get "foo" r.get "bar" end assert_equal ["OK", "bar", nil], result end def test_assignment_of_results_inside_the_block r.pipelined do @first = r.sadd("foo", 1) @second = r.sadd("foo", 1) end assert_equal true, @first.value assert_equal false, @second.value end # Although we could support accessing the values in these futures, # it doesn't make a lot of sense. def test_assignment_of_results_inside_the_block_with_errors assert_raises(Redis::CommandError) do r.pipelined do r.doesnt_exist @first = r.sadd("foo", 1) @second = r.sadd("foo", 1) end end assert_raises(Redis::FutureNotReady) { @first.value } assert_raises(Redis::FutureNotReady) { @second.value } end def test_assignment_of_results_inside_a_nested_block r.pipelined do @first = r.sadd("foo", 1) r.pipelined do @second = r.sadd("foo", 1) end end assert_equal true, @first.value assert_equal false, @second.value end def test_futures_raise_when_confused_with_something_else r.pipelined do @result = r.sadd("foo", 1) end assert_raises(NoMethodError) { @result.to_s } end def test_futures_raise_when_trying_to_access_their_values_too_early r.pipelined do assert_raises(Redis::FutureNotReady) do r.sadd("foo", 1).value end end end def test_futures_raise_when_command_errors_and_needs_transformation assert_raises(Redis::CommandError) do r.pipelined do @result = r.zrange("a", "b", 5, with_scores: true) end end end def test_futures_warn_when_tested_for_equality r.pipelined do @result = r.sadd("foo", 1) end assert_output(nil, /deprecated/) do @result == 1 end end def test_futures_can_be_identified r.pipelined do @result = r.sadd("foo", 1) end assert_equal true, @result.is_a?(Redis::Future) assert_equal true, @result.is_a?(::BasicObject) assert_equal Redis::Future, @result.class end def test_returning_the_result_of_an_empty_pipeline result = r.pipelined do end assert_equal [], result end def test_nesting_pipeline_blocks r.pipelined do r.set("foo", "s1") r.pipelined do r.set("bar", "s2") end end assert_equal "s1", r.get("foo") assert_equal "s2", r.get("bar") end def test_info_in_a_pipeline_returns_hash result = r.pipelined do r.info end assert result.first.is_a?(Hash) end def test_config_get_in_a_pipeline_returns_hash result = r.pipelined do r.config(:get, "*") end assert result.first.is_a?(Hash) end def test_hgetall_in_a_pipeline_returns_hash r.hmset("hash", "field", "value") result = r.pipelined do r.hgetall("hash") end assert_equal result.first, { "field" => "value" } end def test_keys_in_a_pipeline r.set("key", "value") result = r.pipelined do r.keys("*") end assert_equal ["key"], result.first end def test_pipeline_yields_a_connection r.pipelined do |p| p.set("foo", "bar") end assert_equal "bar", r.get("foo") end def test_pipeline_select r.select 1 r.set("db", "1") r.pipelined do |p| p.select 2 p.set("db", "2") end r.select 1 assert_equal "1", r.get("db") r.select 2 assert_equal "2", r.get("db") end def test_pipeline_select_client_db r.select 1 r.pipelined do |p2| p2.select 2 end assert_equal 2, r._client.db end def test_nested_pipeline_select_client_db r.select 1 r.pipelined do |p2| p2.select 2 p2.pipelined do |p3| p3.select 3 end end assert_equal 3, r._client.db end def test_pipeline_interrupt_preserves_client original = r._client Redis::Pipeline.stubs(:new).raises(Interrupt) assert_raises(Interrupt) { r.pipelined {} } assert_equal r._client, original end end redis-rb-4.2.5/test/publish_subscribe_test.rb000066400000000000000000000142161375567530100213230ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestPublishSubscribe < Minitest::Test include Helper::Client class TestError < StandardError end def test_subscribe_and_unsubscribe @subscribed = false @unsubscribed = false wire = Wire.new do r.subscribe("foo") do |on| on.subscribe do |_channel, total| @subscribed = true @t1 = total end on.message do |_channel, message| if message == "s1" r.unsubscribe @message = message end end on.unsubscribe do |_channel, total| @unsubscribed = true @t2 = total end end end # Wait until the subscription is active before publishing Wire.pass until @subscribed Redis.new(OPTIONS).publish("foo", "s1") wire.join assert @subscribed assert_equal 1, @t1 assert @unsubscribed assert_equal 0, @t2 assert_equal "s1", @message end def test_psubscribe_and_punsubscribe @subscribed = false @unsubscribed = false wire = Wire.new do r.psubscribe("f*") do |on| on.psubscribe do |_pattern, total| @subscribed = true @t1 = total end on.pmessage do |_pattern, _channel, message| if message == "s1" r.punsubscribe @message = message end end on.punsubscribe do |_pattern, total| @unsubscribed = true @t2 = total end end end # Wait until the subscription is active before publishing Wire.pass until @subscribed Redis.new(OPTIONS).publish("foo", "s1") wire.join assert @subscribed assert_equal 1, @t1 assert @unsubscribed assert_equal 0, @t2 assert_equal "s1", @message end def test_pubsub_with_numpat_subcommand target_version("2.8.0") do @subscribed = false wire = Wire.new do r.psubscribe("f*") do |on| on.psubscribe { |_channel, _total| @subscribed = true } on.pmessage { |_pattern, _channel, _message| r.punsubscribe } end end Wire.pass until @subscribed redis = Redis.new(OPTIONS) numpat_result = redis.pubsub(:numpat) redis.publish("foo", "s1") wire.join assert_equal redis.pubsub(:numpat), 0 assert_equal numpat_result, 1 end end def test_pubsub_with_channels_and_numsub_subcommnads target_version("2.8.0") do @subscribed = false wire = Wire.new do r.subscribe("foo") do |on| on.subscribe { |_channel, _total| @subscribed = true } on.message { |_channel, _message| r.unsubscribe } end end Wire.pass until @subscribed redis = Redis.new(OPTIONS) channels_result = redis.pubsub(:channels) channels_result.delete('__sentinel__:hello') numsub_result = redis.pubsub(:numsub, 'foo', 'boo') redis.publish("foo", "s1") wire.join assert_equal channels_result, ['foo'] assert_equal numsub_result, ['foo', 1, 'boo', 0] end end def test_subscribe_connection_usable_after_raise @subscribed = false wire = Wire.new do begin r.subscribe("foo") do |on| on.subscribe do |_channel, _total| @subscribed = true end on.message do |_channel, _message| raise TestError end end rescue TestError end end # Wait until the subscription is active before publishing Wire.pass until @subscribed Redis.new(OPTIONS).publish("foo", "s1") wire.join assert_equal "PONG", r.ping end def test_psubscribe_connection_usable_after_raise @subscribed = false wire = Wire.new do begin r.psubscribe("f*") do |on| on.psubscribe do |_pattern, _total| @subscribed = true end on.pmessage do |_pattern, _channel, _message| raise TestError end end rescue TestError end end # Wait until the subscription is active before publishing Wire.pass until @subscribed Redis.new(OPTIONS).publish("foo", "s1") wire.join assert_equal "PONG", r.ping end def test_subscribe_within_subscribe @channels = [] wire = Wire.new do r.subscribe("foo") do |on| on.subscribe do |channel, _total| @channels << channel r.subscribe("bar") if channel == "foo" r.unsubscribe if channel == "bar" end end end wire.join assert_equal ["foo", "bar"], @channels end def test_other_commands_within_a_subscribe assert_raises Redis::CommandError do r.subscribe("foo") do |on| on.subscribe do |_channel, _total| r.set("bar", "s2") end end end end def test_subscribe_without_a_block assert_raises LocalJumpError do r.subscribe("foo") end end def test_unsubscribe_without_a_subscribe assert_raises RuntimeError do r.unsubscribe end assert_raises RuntimeError do r.punsubscribe end end def test_subscribe_past_a_timeout # For some reason, a thread here doesn't reproduce the issue. sleep = %(sleep 0.05) publish = %{ruby -rsocket -e 't=TCPSocket.new("127.0.0.1",#{OPTIONS[:port]});t.write("publish foo bar\\r\\n");t.read(4);t.close'} cmd = [sleep, publish].join('; ') IO.popen(cmd, 'r+') do |_pipe| received = false r.subscribe 'foo' do |on| on.message do |_channel, _message| received = true r.unsubscribe end end assert received end end def test_subscribe_with_timeout received = false assert_raises Redis::TimeoutError do r.subscribe_with_timeout(LOW_TIMEOUT, "foo") do |on| on.message do |_channel, _message| received = true end end end assert !received end def test_psubscribe_with_timeout received = false assert_raises Redis::TimeoutError do r.psubscribe_with_timeout(LOW_TIMEOUT, "f*") do |on| on.message do |_channel, _message| received = true end end end assert !received end end redis-rb-4.2.5/test/remote_server_control_commands_test.rb000066400000000000000000000066061375567530100241220ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestRemoteServerControlCommands < Minitest::Test include Helper::Client def test_info keys = [ "redis_version", "uptime_in_seconds", "uptime_in_days", "connected_clients", "used_memory", "total_connections_received", "total_commands_processed" ] info = r.info keys.each do |k| msg = "expected #info to include #{k}" assert info.keys.include?(k), msg end end def test_info_commandstats target_version "2.5.7" do r.config(:resetstat) r.config(:get, :port) result = r.info(:commandstats) assert_equal '2', result['config']['calls'] end end def test_monitor_redis_lt_2_5_0 return unless version < "2.5.0" log = [] wire = Wire.new do Redis.new(OPTIONS).monitor do |line| log << line break if log.size == 3 end end Wire.pass while log.empty? # Faster than sleep r.set "foo", "s1" wire.join assert log[-1]['(db 15) "set" "foo" "s1"'] end def test_monitor_redis_gte_2_5_0 return unless version >= "2.5.0" log = [] wire = Wire.new do Redis.new(OPTIONS).monitor do |line| log << line break if line =~ /set/ end end Wire.pass while log.empty? # Faster than sleep r.set "foo", "s1" wire.join assert log[-1] =~ /\b15\b.* "set" "foo" "s1"/ end def test_monitor_returns_value_for_break result = r.monitor do |line| break line end assert_equal "OK", result end def test_echo assert_equal "foo bar baz\n", r.echo("foo bar baz\n") end def test_debug r.set "foo", "s1" assert r.debug(:object, "foo").is_a?(String) end def test_object r.lpush "list", "value" assert_equal 1, r.object(:refcount, "list") encoding = r.object(:encoding, "list") assert encoding == "ziplist" || encoding == "quicklist", "Wrong encoding for list" assert r.object(:idletime, "list").is_a?(Integer) end def test_sync redis_mock(sync: -> { "+OK" }) do |redis| assert_equal "OK", redis.sync end end def test_slowlog r.slowlog(:reset) result = r.slowlog(:len) assert_equal 0, result end def test_client assert_equal r.instance_variable_get(:@client), r._client end def test_client_list return if version < "2.4.0" keys = [ "addr", "fd", "name", "age", "idle", "flags", "db", "sub", "psub", "multi", "qbuf", "qbuf-free", "obl", "oll", "omem", "events", "cmd" ] clients = r.client(:list) clients.each do |client| keys.each do |k| msg = "expected #client(:list) to include #{k}" assert client.keys.include?(k), msg end end end def test_client_kill return if version < "2.6.9" r.client(:setname, 'redis-rb') clients = r.client(:list) i = clients.index { |client| client['name'] == 'redis-rb' } assert_equal "OK", r.client(:kill, clients[i]["addr"]) clients = r.client(:list) i = clients.index { |client| client['name'] == 'redis-rb' } assert_nil i end def test_client_getname_and_setname return if version < "2.6.9" assert_nil r.client(:getname) r.client(:setname, 'redis-rb') name = r.client(:getname) assert_equal 'redis-rb', name end end redis-rb-4.2.5/test/scanning_test.rb000066400000000000000000000230211375567530100174060ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestScanning < Minitest::Test include Helper::Client def test_scan_basic target_version "2.7.105" do r.debug :populate, 1000 cursor = 0 all_keys = [] loop do cursor, keys = r.scan cursor all_keys += keys break if cursor == "0" end assert_equal 1000, all_keys.uniq.size end end def test_scan_count target_version "2.7.105" do r.debug :populate, 1000 cursor = 0 all_keys = [] loop do cursor, keys = r.scan cursor, count: 5 all_keys += keys break if cursor == "0" end assert_equal 1000, all_keys.uniq.size end end def test_scan_match target_version "2.7.105" do r.debug :populate, 1000 cursor = 0 all_keys = [] loop do cursor, keys = r.scan cursor, match: "key:1??" all_keys += keys break if cursor == "0" end assert_equal 100, all_keys.uniq.size end end def test_scan_each_enumerator target_version "2.7.105" do r.debug :populate, 1000 scan_enumerator = r.scan_each assert_equal true, scan_enumerator.is_a?(::Enumerator) keys_from_scan = scan_enumerator.to_a.uniq all_keys = r.keys "*" assert all_keys.sort == keys_from_scan.sort end end def test_scan_each_enumerator_match target_version "2.7.105" do r.debug :populate, 1000 keys_from_scan = r.scan_each(match: "key:1??").to_a.uniq all_keys = r.keys "key:1??" assert all_keys.sort == keys_from_scan.sort end end def test_scan_each_block target_version "2.7.105" do r.debug :populate, 100 keys_from_scan = [] r.scan_each do |key| keys_from_scan << key end all_keys = r.keys "*" assert all_keys.sort == keys_from_scan.uniq.sort end end def test_scan_each_block_match target_version "2.7.105" do r.debug :populate, 100 keys_from_scan = [] r.scan_each(match: "key:1?") do |key| keys_from_scan << key end all_keys = r.keys "key:1?" assert all_keys.sort == keys_from_scan.uniq.sort end end def test_sscan_with_encoding target_version "2.7.105" do %i[intset hashtable].each do |enc| r.del "set" prefix = "" prefix = "ele:" if enc == :hashtable elements = [] 100.times { |j| elements << "#{prefix}#{j}" } r.sadd "set", elements assert_equal enc.to_s, r.object("encoding", "set") cursor = 0 all_keys = [] loop do cursor, keys = r.sscan "set", cursor all_keys += keys break if cursor == "0" end assert_equal 100, all_keys.uniq.size end end end def test_sscan_each_enumerator target_version "2.7.105" do elements = [] 100.times { |j| elements << "ele:#{j}" } r.sadd "set", elements scan_enumerator = r.sscan_each("set") assert_equal true, scan_enumerator.is_a?(::Enumerator) keys_from_scan = scan_enumerator.to_a.uniq all_keys = r.smembers("set") assert all_keys.sort == keys_from_scan.sort end end def test_sscan_each_enumerator_match target_version "2.7.105" do elements = [] 100.times { |j| elements << "ele:#{j}" } r.sadd "set", elements keys_from_scan = r.sscan_each("set", match: "ele:1?").to_a.uniq all_keys = r.smembers("set").grep(/^ele:1.$/) assert all_keys.sort == keys_from_scan.sort end end def test_sscan_each_enumerator_block target_version "2.7.105" do elements = [] 100.times { |j| elements << "ele:#{j}" } r.sadd "set", elements keys_from_scan = [] r.sscan_each("set") do |key| keys_from_scan << key end all_keys = r.smembers("set") assert all_keys.sort == keys_from_scan.uniq.sort end end def test_sscan_each_enumerator_block_match target_version "2.7.105" do elements = [] 100.times { |j| elements << "ele:#{j}" } r.sadd "set", elements keys_from_scan = [] r.sscan_each("set", match: "ele:1?") do |key| keys_from_scan << key end all_keys = r.smembers("set").grep(/^ele:1.$/) assert all_keys.sort == keys_from_scan.uniq.sort end end def test_hscan_with_encoding target_version "2.7.105" do %i[ziplist hashtable].each do |enc| r.del "set" count = 1000 count = 30 if enc == :ziplist elements = [] count.times { |j| elements << "key:#{j}" << j.to_s } r.hmset "hash", *elements assert_equal enc.to_s, r.object("encoding", "hash") cursor = 0 all_key_values = [] loop do cursor, key_values = r.hscan "hash", cursor all_key_values.concat key_values break if cursor == "0" end keys2 = [] all_key_values.each do |k, v| assert_equal "key:#{v}", k keys2 << k end assert_equal count, keys2.uniq.size end end end def test_hscan_each_enumerator target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << "key:#{j}" << j.to_s } r.hmset "hash", *elements scan_enumerator = r.hscan_each("hash") assert_equal true, scan_enumerator.is_a?(::Enumerator) keys_from_scan = scan_enumerator.to_a.uniq all_keys = r.hgetall("hash").to_a assert all_keys.sort == keys_from_scan.sort end end def test_hscan_each_enumerator_match target_version "2.7.105" do count = 100 elements = [] count.times { |j| elements << "key:#{j}" << j.to_s } r.hmset "hash", *elements keys_from_scan = r.hscan_each("hash", match: "key:1?").to_a.uniq all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ } assert all_keys.sort == keys_from_scan.sort end end def test_hscan_each_block target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << "key:#{j}" << j.to_s } r.hmset "hash", *elements keys_from_scan = [] r.hscan_each("hash") do |field, value| keys_from_scan << [field, value] end all_keys = r.hgetall("hash").to_a assert all_keys.sort == keys_from_scan.uniq.sort end end def test_hscan_each_block_match target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << "key:#{j}" << j.to_s } r.hmset "hash", *elements keys_from_scan = [] r.hscan_each("hash", match: "key:1?") do |field, value| keys_from_scan << [field, value] end all_keys = r.hgetall("hash").to_a.select { |k, _v| k =~ /^key:1.$/ } assert all_keys.sort == keys_from_scan.uniq.sort end end def test_zscan_with_encoding target_version "2.7.105" do %i[ziplist skiplist].each do |enc| r.del "zset" count = 1000 count = 30 if enc == :ziplist elements = [] count.times { |j| elements << j << "key:#{j}" } r.zadd "zset", elements assert_equal enc.to_s, r.object("encoding", "zset") cursor = 0 all_key_scores = [] loop do cursor, key_scores = r.zscan "zset", cursor all_key_scores.concat key_scores break if cursor == "0" end keys2 = [] all_key_scores.each do |k, v| assert_equal true, v.is_a?(Float) assert_equal "key:#{Integer(v)}", k keys2 << k end assert_equal count, keys2.uniq.size end end end def test_zscan_each_enumerator target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << j << "key:#{j}" } r.zadd "zset", elements scan_enumerator = r.zscan_each "zset" assert_equal true, scan_enumerator.is_a?(::Enumerator) scores_from_scan = scan_enumerator.to_a.uniq member_scores = r.zrange("zset", 0, -1, with_scores: true) assert member_scores.sort == scores_from_scan.sort end end def test_zscan_each_enumerator_match target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << j << "key:#{j}" } r.zadd "zset", elements scores_from_scan = r.zscan_each("zset", match: "key:1??").to_a.uniq member_scores = r.zrange("zset", 0, -1, with_scores: true) filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ } assert filtered_members.sort == scores_from_scan.sort end end def test_zscan_each_block target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << j << "key:#{j}" } r.zadd "zset", elements scores_from_scan = [] r.zscan_each("zset") do |member, score| scores_from_scan << [member, score] end member_scores = r.zrange("zset", 0, -1, with_scores: true) assert member_scores.sort == scores_from_scan.sort end end def test_zscan_each_block_match target_version "2.7.105" do count = 1000 elements = [] count.times { |j| elements << j << "key:#{j}" } r.zadd "zset", elements scores_from_scan = [] r.zscan_each("zset", match: "key:1??") do |member, score| scores_from_scan << [member, score] end member_scores = r.zrange("zset", 0, -1, with_scores: true) filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ } assert filtered_members.sort == scores_from_scan.sort end end end redis-rb-4.2.5/test/scripting_test.rb000066400000000000000000000045131375567530100176150ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestScripting < Minitest::Test include Helper::Client def to_sha(script) r.script(:load, script) end def test_script_exists target_version "2.5.9" do # 2.6-rc1 a = to_sha("return 1") b = a.succ assert_equal true, r.script(:exists, a) assert_equal false, r.script(:exists, b) assert_equal [true], r.script(:exists, [a]) assert_equal [false], r.script(:exists, [b]) assert_equal [true, false], r.script(:exists, [a, b]) end end def test_script_flush target_version "2.5.9" do # 2.6-rc1 sha = to_sha("return 1") assert r.script(:exists, sha) assert_equal "OK", r.script(:flush) assert !r.script(:exists, sha) end end def test_script_kill target_version "2.5.9" do # 2.6-rc1 redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| assert_equal "KILL", redis.script(:kill) end end end def test_eval target_version "2.5.9" do # 2.6-rc1 assert_equal 0, r.eval("return #KEYS") assert_equal 0, r.eval("return #ARGV") assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"]) assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"]) end end def test_eval_with_options_hash target_version "2.5.9" do # 2.6-rc1 assert_equal 0, r.eval("return #KEYS", {}) assert_equal 0, r.eval("return #ARGV", {}) assert_equal ["k1", "k2"], r.eval("return KEYS", { keys: ["k1", "k2"] }) assert_equal ["a1", "a2"], r.eval("return ARGV", { argv: ["a1", "a2"] }) end end def test_evalsha target_version "2.5.9" do # 2.6-rc1 assert_equal 0, r.evalsha(to_sha("return #KEYS")) assert_equal 0, r.evalsha(to_sha("return #ARGV")) assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"]) end end def test_evalsha_with_options_hash target_version "2.5.9" do # 2.6-rc1 assert_equal 0, r.evalsha(to_sha("return #KEYS"), {}) assert_equal 0, r.evalsha(to_sha("return #ARGV"), {}) assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { argv: ["a1", "a2"] }) end end end redis-rb-4.2.5/test/sentinel_command_test.rb000066400000000000000000000032731375567530100211340ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' # @see https://redis.io/topics/sentinel#sentinel-commands Sentinel commands class SentinelCommandsTest < Minitest::Test include Helper::Sentinel def test_sentinel_command_master redis = build_sentinel_client result = redis.sentinel('master', MASTER_NAME) assert_equal result['name'], MASTER_NAME assert_equal result['ip'], LOCALHOST end def test_sentinel_command_masters redis = build_sentinel_client result = redis.sentinel('masters') assert_equal result[0]['name'], MASTER_NAME assert_equal result[0]['ip'], LOCALHOST assert_equal result[0]['port'], MASTER_PORT end def test_sentinel_command_slaves redis = build_sentinel_client result = redis.sentinel('slaves', MASTER_NAME) assert_equal result[0]['name'], "#{LOCALHOST}:#{SLAVE_PORT}" assert_equal result[0]['ip'], LOCALHOST assert_equal result[0]['port'], SLAVE_PORT end def test_sentinel_command_sentinels redis = build_sentinel_client result = redis.sentinel('sentinels', MASTER_NAME) assert_equal result[0]['ip'], LOCALHOST actual_ports = result.map { |r| r['port'] }.sort expected_ports = SENTINEL_PORTS[1..-1] assert_equal actual_ports, expected_ports end def test_sentinel_command_get_master_by_name redis = build_sentinel_client result = redis.sentinel('get-master-addr-by-name', MASTER_NAME) assert_equal result, [LOCALHOST, MASTER_PORT] end def test_sentinel_command_ckquorum redis = build_sentinel_client result = redis.sentinel('ckquorum', MASTER_NAME) assert_equal result, 'OK 3 usable Sentinels. Quorum and failover authorization can be reached' end end redis-rb-4.2.5/test/sentinel_test.rb000066400000000000000000000231751375567530100174410ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'helper' class SentinelTest < Minitest::Test include Helper::Sentinel def test_sentinel_master_role_connection actual = redis.role assert_equal 'master', actual[0] assert_equal SLAVE_PORT, actual[2][0][1] end def test_sentinel_slave_role_connection redis = build_slave_role_client actual = redis.role assert_equal 'slave', actual[0] assert_equal MASTER_PORT.to_i, actual[2] end def test_the_client_can_connect_to_available_slaves commands = { sentinel: lambda do |*_| [ ['ip', '127.0.0.1', 'port', '6382', 'flags', 'slave'], ['ip', '127.0.0.1', 'port', '6383', 'flags', 's_down,slave,disconnected'] ] end } RedisMock.start(commands) do |port| redis = build_slave_role_client(sentinels: [{ host: '127.0.0.1', port: port }]) assert_equal 'PONG', redis.ping end end def test_the_client_raises_error_when_there_is_no_available_slaves commands = { sentinel: lambda do |*_| [ ['ip', '127.0.0.1', 'port', '6382', 'flags', 's_down,slave,disconnected'], ['ip', '127.0.0.1', 'port', '6383', 'flags', 's_down,slave,disconnected'] ] end } RedisMock.start(commands) do |port| redis = build_slave_role_client(sentinels: [{ host: '127.0.0.1', port: port }]) assert_raises(Redis::CannotConnectError) { redis.ping } end end def test_sentinel_failover sentinels = [{ host: "127.0.0.1", port: 26_381 }, { host: "127.0.0.1", port: 26_382 }] commands = { s1: [], s2: [] } s1 = { sentinel: lambda do |command, *args| commands[:s1] << [command, *args] "$-1" # Nil end } s2 = { sentinel: lambda do |command, *args| commands[:s2] << [command, *args] ["127.0.0.1", "6381"] end } RedisMock.start(s1) do |s1_port| RedisMock.start(s2) do |s2_port| sentinels[0][:port] = s1_port sentinels[1][:port] = s2_port redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master) assert redis.ping end end assert_equal commands[:s1], [%w[get-master-addr-by-name master1]] assert_equal commands[:s2], [%w[get-master-addr-by-name master1]] end def test_sentinel_failover_prioritize_healthy_sentinel sentinels = [{ host: "127.0.0.1", port: 26_381 }, { host: "127.0.0.1", port: 26_382 }] commands = { s1: [], s2: [] } s1 = { sentinel: lambda do |command, *args| commands[:s1] << [command, *args] "$-1" # Nil end } s2 = { sentinel: lambda do |command, *args| commands[:s2] << [command, *args] ["127.0.0.1", "6381"] end } RedisMock.start(s1) do |s1_port| RedisMock.start(s2) do |s2_port| sentinels[0][:port] = s1_port sentinels[1][:port] = s2_port redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master) assert redis.ping redis.quit assert redis.ping end end assert_equal commands[:s1], [%w[get-master-addr-by-name master1]] assert_equal commands[:s2], [%w[get-master-addr-by-name master1], %w[get-master-addr-by-name master1]] end def test_sentinel_with_non_sentinel_options commands = { s1: [], m1: [] } sentinel = lambda do |port| { auth: lambda do |pass| commands[:s1] << ['auth', pass] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] "-ERR unknown command 'select'" end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] ['127.0.0.1', port.to_s] end } end master = { auth: lambda do |pass| commands[:m1] << ['auth', pass] '+OK' end, role: lambda do commands[:m1] << ['role'] ['master'] end } RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| s = [{ host: '127.0.0.1', port: sen_port }] redis = Redis.new(url: 'redis://:foo@master1/15', sentinels: s, role: :master) assert redis.ping end end assert_equal [%w[get-master-addr-by-name master1]], commands[:s1] assert_equal [%w[auth foo], %w[role]], commands[:m1] end def test_authentication_for_sentinel commands = { s1: [], m1: [] } sentinel = lambda do |port| { auth: lambda do |pass| commands[:s1] << ['auth', pass] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] '-ERR unknown command `select`' end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] ['127.0.0.1', port.to_s] end } end master = { auth: lambda do |pass| commands[:m1] << ['auth', pass] '-ERR Client sent AUTH, but no password is set' end, role: lambda do commands[:m1] << ['role'] ['master'] end } RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| s = [{ host: '127.0.0.1', port: sen_port, password: 'foo' }] r = Redis.new(host: 'master1', sentinels: s, role: :master) assert r.ping end end assert_equal [%w[auth foo], %w[get-master-addr-by-name master1]], commands[:s1] assert_equal [%w[role]], commands[:m1] end def test_authentication_for_sentinel_and_redis commands = { s1: [], m1: [] } sentinel = lambda do |port| { auth: lambda do |pass| commands[:s1] << ['auth', pass] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] '-ERR unknown command `select`' end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] ['127.0.0.1', port.to_s] end } end master = { auth: lambda do |pass| commands[:m1] << ['auth', pass] '+OK' end, role: lambda do commands[:m1] << ['role'] ['master'] end } RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| s = [{ host: '127.0.0.1', port: sen_port, password: 'foo' }] r = Redis.new(host: 'master1', sentinels: s, role: :master, password: 'bar') assert r.ping end end assert_equal [%w[auth foo], %w[get-master-addr-by-name master1]], commands[:s1] assert_equal [%w[auth bar], %w[role]], commands[:m1] end def test_sentinel_role_mismatch sentinels = [{ host: "127.0.0.1", port: 26_381 }] sentinel = lambda do |port| { sentinel: lambda do |_command, *_args| ["127.0.0.1", port.to_s] end } end master = { role: lambda do ["slave"] end } ex = assert_raises(Redis::ConnectionError) do RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| sentinels[0][:port] = sen_port redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master) assert redis.ping end end end assert_match(/Instance role mismatch/, ex.message) end def test_sentinel_retries sentinels = [{ host: "127.0.0.1", port: 26_381 }, { host: "127.0.0.1", port: 26_382 }] connections = [] handler = lambda do |id, port| { sentinel: lambda do |_command, *_args| connections << id if connections.count(id) < 2 :close else ["127.0.0.1", port.to_s] end end } end master = { role: lambda do ["master"] end } RedisMock.start(master) do |master_port| RedisMock.start(handler.call(:s1, master_port)) do |s1_port| RedisMock.start(handler.call(:s2, master_port)) do |s2_port| sentinels[0][:port] = s1_port sentinels[1][:port] = s2_port redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master, reconnect_attempts: 1) assert redis.ping end end end assert_equal %i[s1 s2 s1], connections connections.clear ex = assert_raises(Redis::CannotConnectError) do RedisMock.start(master) do |master_port| RedisMock.start(handler.call(:s1, master_port)) do |s1_port| RedisMock.start(handler.call(:s2, master_port)) do |s2_port| sentinels[0][:port] = s1_port + 1 sentinels[1][:port] = s2_port + 2 redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master, reconnect_attempts: 0) assert redis.ping end end end end assert_match(/No sentinels available/, ex.message) end def test_sentinel_with_string_option_keys commands = [] master = { role: lambda do ['master'] end } sentinel = lambda do |port| { sentinel: lambda do |command, *args| commands << [command, *args] ['127.0.0.1', port.to_s] end } end RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| sentinels = [{ 'host' => '127.0.0.1', 'port' => sen_port }] redis = Redis.new(url: 'redis://master1', 'sentinels' => sentinels, 'role' => :master) assert redis.ping end end assert_equal [%w[get-master-addr-by-name master1]], commands end end redis-rb-4.2.5/test/sorting_test.rb000066400000000000000000000027331375567530100173020ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestSorting < Minitest::Test include Helper::Client def test_sort r.set("foo:1", "s1") r.set("foo:2", "s2") r.rpush("bar", "1") r.rpush("bar", "2") assert_equal ["s1"], r.sort("bar", get: "foo:*", limit: [0, 1]) assert_equal ["s2"], r.sort("bar", get: "foo:*", limit: [0, 1], order: "desc alpha") end def test_sort_with_an_array_of_gets r.set("foo:1:a", "s1a") r.set("foo:1:b", "s1b") r.set("foo:2:a", "s2a") r.set("foo:2:b", "s2b") r.rpush("bar", "1") r.rpush("bar", "2") assert_equal [["s1a", "s1b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"], limit: [0, 1]) assert_equal [["s2a", "s2b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"], limit: [0, 1], order: "desc alpha") assert_equal [["s1a", "s1b"], ["s2a", "s2b"]], r.sort("bar", get: ["foo:*:a", "foo:*:b"]) end def test_sort_with_store r.set("foo:1", "s1") r.set("foo:2", "s2") r.rpush("bar", "1") r.rpush("bar", "2") r.sort("bar", get: "foo:*", store: "baz") assert_equal ["s1", "s2"], r.lrange("baz", 0, -1) end def test_sort_with_an_array_of_gets_and_with_store r.set("foo:1:a", "s1a") r.set("foo:1:b", "s1b") r.set("foo:2:a", "s2a") r.set("foo:2:b", "s2b") r.rpush("bar", "1") r.rpush("bar", "2") r.sort("bar", get: ["foo:*:a", "foo:*:b"], store: 'baz') assert_equal ["s1a", "s1b", "s2a", "s2b"], r.lrange("baz", 0, -1) end end redis-rb-4.2.5/test/ssl_test.rb000066400000000000000000000044211375567530100164120ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class SslTest < Minitest::Test include Helper::Client driver(:ruby) do def test_connection_to_non_ssl_server assert_raises(Redis::CannotConnectError) do redis = Redis.new(OPTIONS.merge(ssl: true, timeout: LOW_TIMEOUT)) redis.ping end end def test_verified_ssl_connection RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port| redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) assert_equal redis.ping, "PONG" end end def test_unverified_ssl_connection assert_raises(OpenSSL::SSL::SSLError) do RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) redis.ping end end end def test_verify_certificates_by_default assert_raises(OpenSSL::SSL::SSLError) do RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| redis = Redis.new(port: port, ssl: true) redis.ping end end end def test_ssl_blocking RedisMock.start({}, ssl_server_opts("trusted")) do |port| redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) assert_equal redis.set("boom", "a" * 10_000_000), "OK" end end end driver(:hiredis, :synchrony) do def test_ssl_not_implemented_exception assert_raises(NotImplementedError) do RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port| redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) redis.ping end end end end private def ssl_server_opts(prefix) ssl_cert = File.join(cert_path, "#{prefix}-cert.crt") ssl_key = File.join(cert_path, "#{prefix}-cert.key") { ssl: true, ssl_params: { cert: OpenSSL::X509::Certificate.new(File.read(ssl_cert)), key: OpenSSL::PKey::RSA.new(File.read(ssl_key)) } } end def ssl_ca_file File.join(cert_path, "trusted-ca.crt") end def cert_path File.expand_path('support/ssl', __dir__) end end redis-rb-4.2.5/test/support/000077500000000000000000000000001375567530100157405ustar00rootroot00000000000000redis-rb-4.2.5/test/support/cluster/000077500000000000000000000000001375567530100174215ustar00rootroot00000000000000redis-rb-4.2.5/test/support/cluster/orchestrator.rb000066400000000000000000000176101375567530100224720ustar00rootroot00000000000000# frozen_string_literal: true require_relative '../../../lib/redis' class ClusterOrchestrator SLOT_SIZE = 16_384 def initialize(node_addrs, timeout: 30.0) raise 'Redis Cluster requires at least 3 master nodes.' if node_addrs.size < 3 @clients = node_addrs.map do |addr| Redis.new(url: addr, timeout: timeout, reconnect_attempts: 10, reconnect_delay: 1.5, reconnect_delay_max: 10.0) end @timeout = timeout end def restart_cluster_nodes system('make', '--no-print-directory', 'start_cluster', out: File::NULL, err: File::NULL) end def rebuild flush_all_data(@clients) reset_cluster(@clients) assign_slots(@clients) save_config_epoch(@clients) meet_each_other(@clients) wait_meeting(@clients) replicate(@clients) save_config(@clients) wait_cluster_building(@clients) wait_replication(@clients) wait_cluster_recovering(@clients) end def down flush_all_data(@clients) reset_cluster(@clients) end def fail_serving_master master, slave = take_replication_pairs(@clients) master.shutdown attempt_count = 1 max_attempts = 500 attempt_count.step(max_attempts) do |i| return if slave.role == 'master' || i >= max_attempts attempt_count += 1 sleep 0.1 end end def failover master, slave = take_replication_pairs(@clients) wait_replication_delay(@clients, @timeout) slave.cluster(:failover, :takeover) wait_failover(to_node_key(master), to_node_key(slave), @clients) wait_replication_delay(@clients, @timeout) wait_cluster_recovering(@clients) end def start_resharding(slot, src_node_key, dest_node_key, slice_size: 10) node_map = hashify_node_map(@clients.first) src_node_id = node_map.fetch(src_node_key) src_client = find_client(@clients, src_node_key) dest_node_id = node_map.fetch(dest_node_key) dest_client = find_client(@clients, dest_node_key) dest_host, dest_port = dest_node_key.split(':') dest_client.cluster(:setslot, slot, 'IMPORTING', src_node_id) src_client.cluster(:setslot, slot, 'MIGRATING', dest_node_id) keys_count = src_client.cluster(:countkeysinslot, slot) loop do break if keys_count <= 0 keys = src_client.cluster(:getkeysinslot, slot, slice_size) break if keys.empty? keys.each do |k| begin src_client.migrate(k, host: dest_host, port: dest_port) rescue Redis::CommandError => err raise unless err.message.start_with?('IOERR') src_client.migrate(k, host: dest_host, port: dest_port, replace: true) # retry once ensure keys_count -= 1 end end end end def finish_resharding(slot, dest_node_key) node_map = hashify_node_map(@clients.first) @clients.first.cluster(:setslot, slot, 'NODE', node_map.fetch(dest_node_key)) end def close @clients.each(&:quit) end private def flush_all_data(clients) clients.each do |c| begin c.flushall rescue Redis::CommandError # READONLY You can't write against a read only slave. nil end end end def reset_cluster(clients) clients.each { |c| c.cluster(:reset) } end def assign_slots(clients) masters = take_masters(clients) slot_slice = SLOT_SIZE / masters.size mod = SLOT_SIZE % masters.size slot_sizes = Array.new(masters.size, slot_slice) mod.downto(1) { |i| slot_sizes[i] += 1 } slot_idx = 0 masters.zip(slot_sizes).each do |c, s| slot_range = slot_idx..slot_idx + s - 1 c.cluster(:addslots, *slot_range.to_a) slot_idx += s end end def save_config_epoch(clients) clients.each_with_index do |c, i| begin c.cluster('set-config-epoch', i + 1) rescue Redis::CommandError # ERR Node config epoch is already non-zero nil end end end def meet_each_other(clients) first_client = clients.first target_info = first_client.connection target_host = target_info.fetch(:host) target_port = target_info.fetch(:port) clients.each do |client| next if first_client.id == client.id client.cluster(:meet, target_host, target_port) end end def wait_meeting(clients, max_attempts: 600) size = clients.size.to_s wait_for_state(clients, max_attempts) do |client| info = hashify_cluster_info(client) info['cluster_known_nodes'] == size end end def replicate(clients) node_map = hashify_node_map(clients.first) masters = take_masters(clients) take_slaves(clients).each_with_index do |slave, i| master_info = masters[i].connection master_host = master_info.fetch(:host) master_port = master_info.fetch(:port) loop do begin master_node_id = node_map.fetch("#{master_host}:#{master_port}") slave.cluster(:replicate, master_node_id) rescue Redis::CommandError # ERR Unknown node [key] sleep 0.1 node_map = hashify_node_map(clients.first) next end break end end end def save_config(clients) clients.each { |c| c.cluster(:saveconfig) } end def wait_cluster_building(clients, max_attempts: 600) wait_for_state(clients, max_attempts) do |client| info = hashify_cluster_info(client) info['cluster_state'] == 'ok' end end def wait_replication(clients, max_attempts: 600) wait_for_state(clients, max_attempts) do |client| flags = hashify_cluster_node_flags(client) flags.values.select { |f| f == 'slave' }.size == 3 end end def wait_failover(master_key, slave_key, clients, max_attempts: 600) wait_for_state(clients, max_attempts) do |client| flags = hashify_cluster_node_flags(client) flags[master_key] == 'slave' && flags[slave_key] == 'master' end end def wait_replication_delay(clients, timeout_sec) timeout_msec = timeout_sec.to_i * 1000 wait_for_state(clients, clients.size + 1) do |client| client.wait(1, timeout_msec) if client.role.first == 'master' true end end def wait_cluster_recovering(clients, max_attempts: 600) key = 0 wait_for_state(clients, max_attempts) do |client| begin client.get(key) if client.role.first == 'master' true rescue Redis::CommandError => err if err.message.start_with?('CLUSTERDOWN') false elsif err.message.start_with?('MOVED') key += 1 false else true end end end end def wait_for_state(clients, max_attempts) attempt_count = 1 clients.each do |client| attempt_count.step(max_attempts) do |i| break if i >= max_attempts attempt_count += 1 break if yield(client) sleep 0.1 end end end def hashify_cluster_info(client) client.cluster(:info).split("\r\n").map { |str| str.split(':') }.to_h end def hashify_cluster_node_flags(client) client.cluster(:nodes) .split("\n") .map { |str| str.split(' ') } .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] } .to_h end def hashify_node_map(client) client.cluster(:nodes) .split("\n") .map { |str| str.split(' ') } .map { |arr| [arr[1].split('@').first, arr[0]] } .to_h end def take_masters(clients) size = clients.size / 2 return clients if size < 3 clients.take(size) end def take_slaves(clients) size = clients.size / 2 return [] if size < 3 clients[size..size * 2] end def take_replication_pairs(clients) [take_masters(clients).last, take_slaves(clients).last] end def find_client(clients, node_key) clients.find { |cli| node_key == to_node_key(cli) } end def to_node_key(client) con = client.connection "#{con.fetch(:host)}:#{con.fetch(:port)}" end end redis-rb-4.2.5/test/support/connection/000077500000000000000000000000001375567530100200775ustar00rootroot00000000000000redis-rb-4.2.5/test/support/connection/hiredis.rb000066400000000000000000000001011375567530100220430ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../wire/thread" redis-rb-4.2.5/test/support/connection/ruby.rb000066400000000000000000000001011375567530100213750ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../wire/thread" redis-rb-4.2.5/test/support/connection/synchrony.rb000066400000000000000000000003421375567530100224570ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../wire/synchrony" module Helper def around rv = nil EM.synchrony do begin rv = yield ensure EM.stop end end rv end end redis-rb-4.2.5/test/support/redis_mock.rb000066400000000000000000000061241375567530100204070ustar00rootroot00000000000000# frozen_string_literal: true require "socket" module RedisMock class Server def initialize(options = {}) tcp_server = TCPServer.new(options[:host] || "127.0.0.1", 0) tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) if options[:ssl] ctx = OpenSSL::SSL::SSLContext.new ssl_params = options.fetch(:ssl_params, {}) ctx.set_params(ssl_params) unless ssl_params.empty? @server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx) else @server = tcp_server end end def port @server.addr[1] end def start(&block) @thread = Thread.new { run(&block) } end def shutdown @thread.kill end def run loop do session = @server.accept begin return if yield(session) == :exit ensure session.close end end rescue => ex warn "Error running mock server: #{ex.class}: #{ex.message}" warn ex.backtrace retry ensure @server.close end end # Starts a mock Redis server in a thread. # # The server will use the lambda handler passed as argument to handle # connections. For example: # # handler = lambda { |session| session.close } # RedisMock.start_with_handler(handler) do # # Every connection will be closed immediately # end # def self.start_with_handler(blk, options = {}) server = Server.new(options) port = server.port begin server.start(&blk) yield(port) ensure server.shutdown end end # Starts a mock Redis server in a thread. # # The server will reply with a `+OK` to all commands, but you can # customize it by providing a hash. For example: # # RedisMock.start(:ping => lambda { "+PONG" }) do |port| # assert_equal "PONG", Redis.new(:port => port).ping # end # def self.start(commands, options = {}, &blk) handler = lambda do |session| while line = session.gets argv = Array.new(line[1..-3].to_i) do bytes = session.gets[1..-3].to_i arg = session.read(bytes) session.read(2) # Discard \r\n arg end command = argv.shift blk = commands[command.to_sym] blk ||= ->(*_) { "+OK" } response = blk.call(*argv) # Convert a nil response to :close response ||= :close if response == :exit break :exit elsif response == :close break :close elsif response.is_a?(Array) session.write("*%d\r\n" % response.size) response.each do |resp| if resp.is_a?(Array) session.write("*%d\r\n" % resp.size) resp.each do |r| session.write("$%d\r\n%s\r\n" % [r.length, r]) end else session.write("$%d\r\n%s\r\n" % [resp.length, resp]) end end else session.write(response) session.write("\r\n") unless response.end_with?("\r\n") end end end start_with_handler(handler, options, &blk) end end redis-rb-4.2.5/test/support/ssl/000077500000000000000000000000001375567530100165415ustar00rootroot00000000000000redis-rb-4.2.5/test/support/ssl/gen_certs.sh000077500000000000000000000020521375567530100210500ustar00rootroot00000000000000#!/bin/sh get_subject() { if [ "$1" = "trusted" ] then echo "/C=IT/ST=Sicily/L=Catania/O=Redis/OU=Security/CN=127.0.0.1" else echo "/C=XX/ST=Untrusted/L=Evilville/O=Evil Hacker/OU=Attack Department/CN=127.0.0.1" fi } # Generate two CAs: one to be considered trusted, and one that's untrusted for type in trusted untrusted; do rm -rf ./demoCA mkdir -p ./demoCA mkdir -p ./demoCA/certs mkdir -p ./demoCA/crl mkdir -p ./demoCA/newcerts mkdir -p ./demoCA/private touch ./demoCA/index.txt openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ${type}-ca.key openssl req -new -x509 -days 12500 -key ${type}-ca.key -sha256 -out ${type}-ca.crt -subj "$(get_subject $type)" openssl x509 -in ${type}-ca.crt -noout -next_serial -out ./demoCA/serial openssl req -newkey rsa:2048 -keyout ${type}-cert.key -nodes -out ${type}-cert.req -subj "$(get_subject $type)" openssl ca -days 12500 -cert ${type}-ca.crt -keyfile ${type}-ca.key -out ${type}-cert.crt -infiles ${type}-cert.req rm ${type}-cert.req done rm -rf ./demoCA redis-rb-4.2.5/test/support/ssl/trusted-ca.crt000066400000000000000000000024761375567530100213370ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDsTCCApmgAwIBAgIUbNLy3vMeDQUSLVREVOSc8ElYPoMwDQYJKoZIhvcNAQEL BQAwZzELMAkGA1UEBhMCSVQxDzANBgNVBAgMBlNpY2lseTEQMA4GA1UEBwwHQ2F0 YW5pYTEOMAwGA1UECgwFUmVkaXMxETAPBgNVBAsMCFNlY3VyaXR5MRIwEAYDVQQD DAkxMjcuMC4wLjEwIBcNMjAwODIxMDMxOTE1WhgPMjA1NDExMTEwMzE5MTVaMGcx CzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZTaWNpbHkxEDAOBgNVBAcMB0NhdGFuaWEx DjAMBgNVBAoMBVJlZGlzMREwDwYDVQQLDAhTZWN1cml0eTESMBAGA1UEAwwJMTI3 LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3aqkNfQcXHfp YVZLE+EdMtR9cXlbAQAOX6LSAzh0ZC/LVTMXJvMrbjVqCE7Khm3A5lwXcnJ8EPJn Tj7B4Nc3aOjyn5U+WtAikJizN1exFEuHl0h16oERpVj17nxDycSfoZcMoPyEzqXK BQpiV4eNvWYu2NTeXQNbWPs84LUGJjb8WGhk1AhiGjeX5AN3yBnwGQN35+bWRCS0 66ITMPqrEGx47PemN+2ECL5wkMTFIQIRPbiKdsm39pxVKqEl5dzc57CoCN5kI/AA iiwjsDGApB6Xk9QzOpLRwdNEp96C3IjrWaJ//Obn4a4XkXUCLqtI8SGsQLujT9OH opns8/nYKwIDAQABo1MwUTAdBgNVHQ4EFgQURpoiXGek1Dk2H3dLqEF1YntLsOcw HwYDVR0jBBgwFoAURpoiXGek1Dk2H3dLqEF1YntLsOcwDwYDVR0TAQH/BAUwAwEB /zANBgkqhkiG9w0BAQsFAAOCAQEAB8cAFmMS/WrSCsedpyYG3s4bZSx3yxaXDBbE tseOQa1z3OUml1XH6DP0B3dioGRkL8O6C2wqqVdJyB4gqlG0kWD5nqFkYIh09pYM +SaUa1FzQVdDENNTMqB20MeOLLk8BAFX1kKRkC8Jm+6VFKtB+bW5nZ4qDDP4KMfr vZdL+Xo8+vYSsWztx0u4RCUKLlfUbcG8G7kTe4GoHXzwrvldmY9xARJgXXHMlLit gTORsdLj0jAlheTvfmW9/nc0H3edDly7DbueT0tFoeY02gkqayRXUVrnJ/Otmvj1 pzEBSVA7Ri6cohiQVxOHmurwvwsxgZamPlou6ZZWY0tzkeLEbQ== -----END CERTIFICATE----- redis-rb-4.2.5/test/support/ssl/trusted-ca.key000066400000000000000000000032541375567530100213320ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDdqqQ19Bxcd+lh VksT4R0y1H1xeVsBAA5fotIDOHRkL8tVMxcm8ytuNWoITsqGbcDmXBdycnwQ8mdO PsHg1zdo6PKflT5a0CKQmLM3V7EUS4eXSHXqgRGlWPXufEPJxJ+hlwyg/ITOpcoF CmJXh429Zi7Y1N5dA1tY+zzgtQYmNvxYaGTUCGIaN5fkA3fIGfAZA3fn5tZEJLTr ohMw+qsQbHjs96Y37YQIvnCQxMUhAhE9uIp2ybf2nFUqoSXl3NznsKgI3mQj8ACK LCOwMYCkHpeT1DM6ktHB00Sn3oLciOtZon/85ufhrheRdQIuq0jxIaxAu6NP04ei mezz+dgrAgMBAAECggEARrXqkDOA4JZ34k8OwBato8tZANu/hgAolaVw7QoTRupg KJuVpR0pG4z6eA/6Vwun31Q9Por6vMU24yTt3/WHfXXh/7oyG/INNKchdGQK3viB FmdNBjOKF37bZOpLDZAlg/yVUL18+Ba27Qi0+ksJkgOIqi6tiGpLt4TdlKjqf0Gv EPslFgvxIAoAjUZFhkanDY06FHe+1Bpue+1O5Cg+cL1YzNZy5XSDprvL4o8EsAuM fOoWDxxq0Jt0Mq+asYmqkVTwvmsiQzJoaTh8gM28Owkp9PSk4L/uY1gXO015InQ4 ZyK+ypETfTmtfVXrHrfWS96FQvXZmbyRP/fszVsFEQKBgQD0mBgiY5Bc6/0bdEOc dBTyhJvxdQ+PxOHQP5mmgJJlCfwby4VdwapFgpZ3OZBiVsb79C8sueIqsgxN2zIx fB5Q7iHqkslVCvRkE0LdWAce8sWZgHqSnKoUTSZTReU4BJis0AwaSR8Nrbxb1UWm GWX7ppgZYnabhkf8MHLtmPqRBwKBgQDoANf2A56//rPzvS22MZuqL9FzkfIy+t/S WUHek6p9He2QtJ6W+tyjwLhKFOwMyl/1nYH7il/mQQhdijaVxHo9w6KmZiPw30Zc eaSn01mpBj1ID2ZXDffWOYAeO32PhBcyw+85ucIMIrBJJ+CXqS6ceQj1t/PpJ5Y8 KdE41/mKvQKBgQDTdrtG3/VroMtO9RGPLfz+Pw/jjWVK0ti4BoR8oyPuHtfL4AUJ renb9q7HnQjrPEMEiXRPotWaPBzPIvceOUSsi3TfLNDLqZDpBI4Gd5iQdSvJLn7K So/wxVKhJAisiazFm4kbIKSsWsxCSPzSQZseGkXdjHcmts19hxWVvXDD+QKBgQC7 m5MHub2x/EGApEZGwq7iXHC/SBHW78/2xX7igf6n1n+5OJXV+V5afQmJvolzfmNC tu/ZfPg3tfcRzSZ+zbccIwtwC8Cck7DOLv/bRqmGaSk9EFbtprn3XeAgknLijypD PvZAc9pa/eIYBksz2Pd8SNPZ/7sZm419cUNi+CMu8QKBgQDh4xL3a9soUptVftlp Mjw0ww9mNVsIAOWKq7KRUyVPJhCcJwDKr3D6i1hONqzP9jQe+qpiCJ1lRcFop7b1 SjXA38BdZ2YDAJzQHEmkJCg+ZJx08hfBbFt9XQpKZ/3zKw8hQpme7TsF0NaH7M0e HdX2uqhE7Go49EbvEMRu+jWEUw== -----END PRIVATE KEY----- redis-rb-4.2.5/test/support/ssl/trusted-cert.crt000066400000000000000000000107101375567530100216770ustar00rootroot00000000000000Certificate: Data: Version: 3 (0x2) Serial Number: 6c:d2:f2:de:f3:1e:0d:05:12:2d:54:44:54:e4:9c:f0:49:58:3e:84 Signature Algorithm: sha256WithRSAEncryption Issuer: C=IT, ST=Sicily, L=Catania, O=Redis, OU=Security, CN=127.0.0.1 Validity Not Before: Aug 21 03:19:15 2020 GMT Not After : Nov 11 03:19:15 2054 GMT Subject: C=IT, ST=Sicily, O=Redis, OU=Security, CN=127.0.0.1 Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:e0:e6:bf:ae:5a:e7:f7:01:4a:53:dd:4b:d8:d2: 47:5b:20:b4:01:31:c2:bf:0a:07:e6:2a:60:a8:bf: 1f:01:3a:3f:b0:96:8b:0a:1a:2c:88:b9:bb:dc:3b: e7:9c:86:bd:43:f1:87:0d:56:5c:cf:58:31:ec:a4: 91:0b:a8:2c:76:57:f0:c4:98:c7:f8:bd:74:b2:d5: 30:ff:12:e3:2a:f0:c3:e9:18:81:9f:d1:43:46:c2: 89:61:3b:62:cb:8a:6b:21:a5:8a:59:4c:af:c8:8e: d2:3d:4a:77:7f:ac:f6:69:f6:e4:b7:47:30:a2:30: a0:2c:21:6b:a3:f8:c3:de:f1:63:62:09:72:71:38: 6d:02:5b:3a:3d:03:22:67:36:4f:97:91:55:e0:9c: c7:e8:63:bf:2c:d9:8d:53:fe:ae:d0:de:10:87:ef: 99:76:84:4e:bb:a6:fe:22:3e:09:98:54:2d:e7:a3: 54:a4:57:b2:53:a9:df:56:da:b5:1b:be:7f:e3:ae: 08:f8:f8:20:33:4f:29:4b:6d:24:d1:10:c4:e0:05: 25:07:cb:be:6d:c7:ff:89:e0:17:77:76:db:cb:4d: 75:e7:13:c1:6f:1f:5f:a4:9b:4c:b8:a9:38:e9:0a: 39:de:41:45:96:71:6b:eb:7a:27:6f:92:93:b0:aa: 35:71 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:FALSE Netscape Comment: OpenSSL Generated Certificate X509v3 Subject Key Identifier: 56:AD:FB:49:9F:F6:B2:C0:07:21:57:D7:CE:6A:B2:ED:D7:30:74:57 X509v3 Authority Key Identifier: keyid:46:9A:22:5C:67:A4:D4:39:36:1F:77:4B:A8:41:75:62:7B:4B:B0:E7 Signature Algorithm: sha256WithRSAEncryption 5f:29:4e:fc:07:63:13:fd:84:91:90:cb:c5:f5:76:b2:b9:98: 15:42:d4:ef:44:fc:7a:60:35:9a:fb:ac:d4:c1:18:5b:c3:19: b3:4b:29:ee:e2:15:85:d5:1b:05:f5:62:86:87:aa:81:86:42: 12:25:ac:9e:f3:c6:51:c3:3d:0e:d2:00:db:74:bb:0f:d0:5f: bb:c5:8f:8f:79:45:0b:78:82:40:0c:fb:aa:fc:ef:5e:48:6c: e9:2b:4c:ac:a5:ab:e6:18:d7:8b:a6:4f:44:31:d3:81:d9:71: 2d:ed:76:9b:91:f5:ca:38:4e:ad:a9:66:00:a7:27:31:74:65: 11:a9:fa:11:91:03:d5:64:f5:43:98:6b:31:e5:f2:87:8c:4f: 52:8a:5c:a7:92:07:89:ab:44:8a:c4:87:07:1f:6a:f6:e4:7c: 42:31:bf:47:1a:5c:98:00:d2:aa:6c:75:ba:3d:24:9b:f3:e4: 04:c8:28:ea:97:4d:47:99:fc:69:f8:2c:62:44:a9:c8:82:0b: 26:85:bd:fb:e4:97:df:58:73:bf:09:1e:5d:73:05:16:42:47: 02:fb:b8:9d:81:1f:23:8b:b5:6c:5b:02:6d:3f:07:44:44:24: 19:ec:6e:57:10:7e:4a:cc:ac:18:79:e0:08:67:cd:6c:ee:61: fb:7d:46:22 -----BEGIN CERTIFICATE----- MIIDxzCCAq+gAwIBAgIUbNLy3vMeDQUSLVREVOSc8ElYPoQwDQYJKoZIhvcNAQEL BQAwZzELMAkGA1UEBhMCSVQxDzANBgNVBAgMBlNpY2lseTEQMA4GA1UEBwwHQ2F0 YW5pYTEOMAwGA1UECgwFUmVkaXMxETAPBgNVBAsMCFNlY3VyaXR5MRIwEAYDVQQD DAkxMjcuMC4wLjEwIBcNMjAwODIxMDMxOTE1WhgPMjA1NDExMTEwMzE5MTVaMFUx CzAJBgNVBAYTAklUMQ8wDQYDVQQIDAZTaWNpbHkxDjAMBgNVBAoMBVJlZGlzMREw DwYDVQQLDAhTZWN1cml0eTESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Oa/rlrn9wFKU91L2NJHWyC0ATHCvwoH5ipg qL8fATo/sJaLChosiLm73DvnnIa9Q/GHDVZcz1gx7KSRC6gsdlfwxJjH+L10stUw /xLjKvDD6RiBn9FDRsKJYTtiy4prIaWKWUyvyI7SPUp3f6z2afbkt0cwojCgLCFr o/jD3vFjYglycThtAls6PQMiZzZPl5FV4JzH6GO/LNmNU/6u0N4Qh++ZdoROu6b+ Ij4JmFQt56NUpFeyU6nfVtq1G75/464I+PggM08pS20k0RDE4AUlB8u+bcf/ieAX d3bby0115xPBbx9fpJtMuKk46Qo53kFFlnFr63onb5KTsKo1cQIDAQABo3sweTAJ BgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0 aWZpY2F0ZTAdBgNVHQ4EFgQUVq37SZ/2ssAHIVfXzmqy7dcwdFcwHwYDVR0jBBgw FoAURpoiXGek1Dk2H3dLqEF1YntLsOcwDQYJKoZIhvcNAQELBQADggEBAF8pTvwH YxP9hJGQy8X1drK5mBVC1O9E/HpgNZr7rNTBGFvDGbNLKe7iFYXVGwX1YoaHqoGG QhIlrJ7zxlHDPQ7SANt0uw/QX7vFj495RQt4gkAM+6r8715IbOkrTKylq+YY14um T0Qx04HZcS3tdpuR9co4Tq2pZgCnJzF0ZRGp+hGRA9Vk9UOYazHl8oeMT1KKXKeS B4mrRIrEhwcfavbkfEIxv0caXJgA0qpsdbo9JJvz5ATIKOqXTUeZ/Gn4LGJEqciC CyaFvfvkl99Yc78JHl1zBRZCRwL7uJ2BHyOLtWxbAm0/B0REJBnsblcQfkrMrBh5 4AhnzWzuYft9RiI= -----END CERTIFICATE----- redis-rb-4.2.5/test/support/ssl/trusted-cert.key000066400000000000000000000032541375567530100217040ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDg5r+uWuf3AUpT 3UvY0kdbILQBMcK/CgfmKmCovx8BOj+wlosKGiyIubvcO+echr1D8YcNVlzPWDHs pJELqCx2V/DEmMf4vXSy1TD/EuMq8MPpGIGf0UNGwolhO2LLimshpYpZTK/IjtI9 Snd/rPZp9uS3RzCiMKAsIWuj+MPe8WNiCXJxOG0CWzo9AyJnNk+XkVXgnMfoY78s 2Y1T/q7Q3hCH75l2hE67pv4iPgmYVC3no1SkV7JTqd9W2rUbvn/jrgj4+CAzTylL bSTREMTgBSUHy75tx/+J4Bd3dtvLTXXnE8FvH1+km0y4qTjpCjneQUWWcWvreidv kpOwqjVxAgMBAAECggEAULL7vLhp27vey9DwUlDBwfUuIe+VDa+vvese2+4YVfxs thSOt4VEzZq3ygLEzOmcKDEWYLbIfq4K2/sBAMnLintrrV+VAbAZm8Hb3usMEHBs G8vrV0ljdpR/byA8BwUYA+6+geR+ftygm9WIo4uQr90jnJAy5z/DeZJUaXXt8qTG pCnflCLrsBhsJFNQjqDvUnw08Cd34Nkx9gNlsGQYmWnRgqHERQuHN4bw/1Tx3Vnu memX77TlNoMttXca3cHjJ6UnKSjTxGBfco3VLlO0QpTdFaQVO6svvLjNFtodVQM3 RrL5cyWk+2qOLHLY+YUE8zImZmvK7JClr7JomQQwQQKBgQD+NSMbhaTKoWfN/1NH Efrw1vTF62nfbC41jk5eenxhOhgqWT9vfeAi7lKXW3pY2ZA3dMjN/XCFr/xexVQ6 3R0p48lscmN5cslFB6vks4O/iK4J6t+xCLZzgi2XRCg2UT+nRPcbW8bPFTxpb2+x ++SFEHy0DC7pR1+XYDj5iqkHrQKBgQDifLY5rgVJxpVR42GCkUOUMC20GJisCqHv ABG9X8gjYKn/prSeEx5mCCKfwHkKo+aJrOWQ9TspjduG1Pelmxx4g5G3EBhobSIW vqsAfqVr3UWawlYNfA4+ek0m+xl55d1s/CNaXx+xmVjyzIBQwKqUAx67gtVULMCH 9PX36B5tVQKBgQDxB/kt014ZM0l1rS6NKKNDUM3uC/Tq/2whI7lzI7hjh+352X2o fTXUaRyunvI25LM1oen0RuY2HFOymG/xEE7itTT7OsrPEON+LHPz+bJmHXbHuIg5 GAXHKBuKXfmy5v7v3xhePHsZRw1s+1hw7mITOTrEjPi+AArHQVlEYxE6UQKBgQCD +B0KEO895LtfArnvpYsWDtiipu5W2L8wjv7HNMdebdXAhDecIBHHbBgYs8MTwxry v87oHyyA8wqmTvOaCH6Xbjp6y6MdPfHuBN2JJUJoTn9fRLt1kgKOvx6zhv56O8lA 1s4Wu3SxPGRK3YQrCYibRBIlOn/pU0ZAMikccaFBHQKBgQCRsNhjKfHDRPCzhMnu SyOSIcB43JHOl9JeTvjmAnjE3m06pjppp+sHyR2FxghIz5IGepAVLKy6jbYyJATp ubb0br+1Jk8MdjprAd82dN9xqw/kt/rbCsJof0e38+aoRY06HjFRz4zXScOLG0ag Ym1C4aDdCQRZQmSmSHiVBRN70Q== -----END PRIVATE KEY----- redis-rb-4.2.5/test/support/ssl/untrusted-ca.crt000066400000000000000000000025631375567530100216770ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIID2TCCAsGgAwIBAgIUDRXLZuA0a5kneb7e8vKxFhCnawUwDQYJKoZIhvcNAQEL BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1c3RlZDESMBAGA1UEBwwJ RXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEaMBgGA1UECwwRQXR0YWNr IERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTAgFw0yMDA4MjEwMzE5MjRa GA8yMDU0MTExMTAzMTkyNFowezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1 c3RlZDESMBAGA1UEBwwJRXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEa MBgGA1UECwwRQXR0YWNrIERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL0fgnv0EeX0CAaiBw1CqnBA w6Z7jtu9siTEbUE6rUkTVkwnqFoPcIEu/zj/vGlmHK3+GjnFIK9y4TIsyPKPqneC SLlYaF5Y/0B1Kho5NLk0oJrZEuco6cUJ+Ip8FHhvFVmftkGCZo28gFOH8OvARVIP 6PdcY0oLT6V8LIMW8VzZj+WNqSOGGnJ4GJwE6euI79gUs21KSIFkq9hjvK8MPUQs 8CaebCR+Z4DkoOAqhQjKevCAss0nXQYxuWYgM/ZiCqUEFRP8wR3a10kuE2gdePK7 AgE2QCR1FIUONTwEh5diiycWVTBC3Yp/gNys2de7AZ7K5tjAzqH1C6R8uHMGFXEC AwEAAaNTMFEwHQYDVR0OBBYEFC5GEu92pkUiyhhx2BDcBKeMbm72MB8GA1UdIwQY MBaAFC5GEu92pkUiyhhx2BDcBKeMbm72MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI hvcNAQELBQADggEBAAOpKzMjFjPC/j5YF7RpC5DguzoaCEV/IydsNWiObLAKU9u2 25eZzBIQFSQRUxCdWeI9jbXtF5ngy052Y5Ih9VKHIVWifUrZYO8s1xHG295Z2jaW hz8i9jdqK8U+1k6teLSjUo/87eL8hKptELv9net0T7aykx1e87rZy9sZm4G12uZc goW30H0F8M6nkyYLApSWjx/gibdWkDlCQXCbY8YXuZDuwhnB53/WGv5R9ym55plp MzmLu8xi0Ow3XbyvUzWNtSTpDMfcSrNc69+qr1DLDHW7ZZMsLZj7ONYrkqAbuKhi weYzff5/gaTxILtIRJx9Z7Vc0IUtA+lcZHjwRms= -----END CERTIFICATE----- redis-rb-4.2.5/test/support/ssl/untrusted-ca.key000066400000000000000000000032501375567530100216710ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9H4J79BHl9AgG ogcNQqpwQMOme47bvbIkxG1BOq1JE1ZMJ6haD3CBLv84/7xpZhyt/ho5xSCvcuEy LMjyj6p3gki5WGheWP9AdSoaOTS5NKCa2RLnKOnFCfiKfBR4bxVZn7ZBgmaNvIBT h/DrwEVSD+j3XGNKC0+lfCyDFvFc2Y/ljakjhhpyeBicBOnriO/YFLNtSkiBZKvY Y7yvDD1ELPAmnmwkfmeA5KDgKoUIynrwgLLNJ10GMblmIDP2YgqlBBUT/MEd2tdJ LhNoHXjyuwIBNkAkdRSFDjU8BIeXYosnFlUwQt2Kf4DcrNnXuwGeyubYwM6h9Quk fLhzBhVxAgMBAAECggEBALaaq/RezsFHBFDTNRfanJJSFhazClalLFJPzmXC7/m0 0Agr6mM6sRgqdodkdVkXHO3qgQvyiAKfW0yE7Wk2yhMmGm3LLMqcB6kG96XmQj/o zoF0wsmrOTvkyrN75o/6QZUNnn5WGAsWTJlakoYuWUBI2FmuPLgLf9V6tcfE6TsJ s/ovMBxq/bDd+QEvgVXqNNClLKWhbN1vSEfGQxkrZQGbo5iQdoJjQI1dR6xRJR5n COrKw9AWRLpW/c8xsmuSEayKn+tJURKBAw0xhituUtKPJD+0uWwRQBCi72we8kv+ 0MYLGBvIiU98J16EEimHQXtt7GU/uaAG1CD4NTBAIyECgYEA67hFC232j0U9skf9 WA7vHGuu9tOdQyO6t2VpWPuKpvXqDz+y/qB+v6JInGlqF+DmgEtdvThv2C2BxpDe 512szEzLL13BcIPJE2XYXWf79Y6zpY1rIJfcDC0smlSEd0SGv0/lvSNtNVewR9/j F1siw8+hp3l6zx88mZKEU35uSCUCgYEAzWTyax/HUQA98bhZ7cXdwd64GcjIcsny 6kQaZSCn02gw8YEnxrwWn4I/h6hS2TVnAQFpKjYUuBYHRvMAKGpPg9Jsc/1Af/oc z8Pjx7uUYENOyaYXzs2ZtCE0VpPHPbZBZTUSzzBLyxqq0QUXkA4s4+2zNF8SFsg7 GEg2fonIYF0CgYEArlhPsSF3IQbMmEWIy43YK0Q2V9ey1IrjumvmnGsIZW8z3G13 3b8loGXOoOmTD/BHbJLR1Xedud4Gw7A5PhVaDo2qJvGIdsjye0dz3bpgcIJIu2U6 3BOWLOdouwlSJMjphSz6Noeyaabe+npNA+RjdULoRO+j9vgaoVfuSbcUqIUCgYBd Dis2lYM8E5v888TqkQbTWxCVvf3y48QGlyxOPOlMQpxKDnXy+CxXwC8ASyad+i/c qML4uN/SN0i8wEOGDARSePdh5Y9fa/W5u8prJ3Ul19jOS03mCAhnL9QClZljQDuI mu8Wp47vSfmyEViHj6SO75aNV7VeVQFREwZ9dfcukQKBgCOC7OVPF8lmOCFKDonp NWutEY5YFUnnDBi7CWLZxwesSWF7RBRvOQD+Pe0uedMeHZEMld9WZ2tO9fT+4HBu QtqJ3fCqxZkrkPOCrQK/A9orYw+9VAXuerqVyolYE7hjWuJtx43NIHgz3jJ/G7pK MS782OTQErMtMg/jN6HOryM6 -----END PRIVATE KEY----- redis-rb-4.2.5/test/support/ssl/untrusted-cert.crt000066400000000000000000000110431375567530100222420ustar00rootroot00000000000000Certificate: Data: Version: 3 (0x2) Serial Number: 0d:15:cb:66:e0:34:6b:99:27:79:be:de:f2:f2:b1:16:10:a7:6b:06 Signature Algorithm: sha256WithRSAEncryption Issuer: C=XX, ST=Untrusted, L=Evilville, O=Evil Hacker, OU=Attack Department, CN=127.0.0.1 Validity Not Before: Aug 21 03:19:25 2020 GMT Not After : Nov 11 03:19:25 2054 GMT Subject: C=XX, ST=Untrusted, O=Evil Hacker, OU=Attack Department, CN=127.0.0.1 Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:c4:9c:33:ed:3d:fe:f9:0c:b7:46:56:04:a8:56: 0e:3b:55:34:a5:e2:22:ab:90:e9:f1:f9:25:44:01: 74:00:cb:25:27:e4:53:21:14:91:2b:9a:00:60:31: 6f:e7:65:88:93:99:5c:0b:b7:44:b0:b1:b6:5f:5d: d2:db:ab:84:51:31:2a:c3:73:67:a0:aa:04:47:c5: 60:5b:2f:39:fa:09:3b:09:47:97:ae:a8:ec:a1:7e: d1:22:7c:f1:1c:6d:b5:fe:3d:6e:96:fb:b4:70:25: 81:94:50:c9:ac:6f:dc:cd:5d:f9:1e:ed:18:8a:57: 3a:05:7f:f1:dd:12:af:86:b7:8e:b7:5d:2c:d7:c0: 6f:6d:98:5f:40:e4:fa:a3:ed:2c:43:a0:ac:6a:6a: 6c:41:e8:84:d2:1c:59:63:ec:d0:a5:c7:1f:50:85: e3:a8:54:95:bd:04:cb:99:5c:2a:6d:ee:04:ad:d7: 93:89:37:7c:a2:fd:f6:4e:c2:7a:4c:b2:f3:82:13: c3:a7:ef:c3:5a:ce:fb:de:08:b7:57:fb:18:c2:57: 40:9b:1a:b1:00:85:49:5e:93:c9:9c:02:1f:e9:76: 76:0f:59:2e:84:be:31:bd:09:73:c8:a3:92:23:3a: c0:03:99:d9:7e:98:9a:83:ea:69:39:69:d2:e0:b2: 48:0b Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:FALSE Netscape Comment: OpenSSL Generated Certificate X509v3 Subject Key Identifier: E9:0A:98:2C:F0:CA:F7:5B:4B:D4:2C:64:62:44:65:17:5D:AE:71:0E X509v3 Authority Key Identifier: keyid:2E:46:12:EF:76:A6:45:22:CA:18:71:D8:10:DC:04:A7:8C:6E:6E:F6 Signature Algorithm: sha256WithRSAEncryption a9:0a:60:a5:79:13:e8:ba:90:0e:49:73:59:bb:28:29:1c:36: 29:ff:dd:16:11:5c:8e:a3:dd:7c:9a:cc:26:df:f4:07:23:79: 5f:30:b9:e3:47:33:25:92:ce:ef:6d:37:a5:01:f5:a2:58:32: a9:24:7b:df:22:fb:c4:c5:e2:92:ac:94:ab:c5:38:ef:70:21: dd:a2:b4:9e:49:d9:32:23:87:ef:44:69:23:63:6f:96:73:73: b3:3d:ba:52:b9:94:dc:5d:50:13:d0:8d:af:6d:34:98:c0:ad: e1:b6:78:06:85:2a:e0:2c:a6:d0:f7:f4:79:79:04:72:ea:3b: 3c:43:0f:9e:5f:c5:11:64:9a:93:cb:df:0d:e6:3a:bc:5a:9c: 0e:6d:4b:2e:c3:5d:d9:8e:8d:93:8b:48:fa:85:87:ce:4b:88: 45:a7:c3:e2:eb:26:28:09:9f:58:cd:b0:a8:fb:4a:51:d8:13: 18:50:31:9e:20:0e:26:4c:be:10:54:62:34:2a:ca:23:88:0d: 81:6a:65:37:1c:14:b3:bf:63:11:cd:0b:1b:a2:fd:1e:f8:55: 82:e8:92:1f:59:f2:07:90:32:a4:c0:f9:cb:b8:9d:b2:f7:26: 73:0b:24:54:44:0a:96:20:f8:bd:4b:2b:ef:6b:79:00:c0:d8: 1f:24:50:b3 -----BEGIN CERTIFICATE----- MIID7TCCAtWgAwIBAgIUDRXLZuA0a5kneb7e8vKxFhCnawYwDQYJKoZIhvcNAQEL BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1c3RlZDESMBAGA1UEBwwJ RXZpbHZpbGxlMRQwEgYDVQQKDAtFdmlsIEhhY2tlcjEaMBgGA1UECwwRQXR0YWNr IERlcGFydG1lbnQxEjAQBgNVBAMMCTEyNy4wLjAuMTAgFw0yMDA4MjEwMzE5MjVa GA8yMDU0MTExMTAzMTkyNVowZzELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVVudHJ1 c3RlZDEUMBIGA1UECgwLRXZpbCBIYWNrZXIxGjAYBgNVBAsMEUF0dGFjayBEZXBh cnRtZW50MRIwEAYDVQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDEnDPtPf75DLdGVgSoVg47VTSl4iKrkOnx+SVEAXQAyyUn5FMh FJErmgBgMW/nZYiTmVwLt0SwsbZfXdLbq4RRMSrDc2egqgRHxWBbLzn6CTsJR5eu qOyhftEifPEcbbX+PW6W+7RwJYGUUMmsb9zNXfke7RiKVzoFf/HdEq+Gt463XSzX wG9tmF9A5Pqj7SxDoKxqamxB6ITSHFlj7NClxx9QheOoVJW9BMuZXCpt7gSt15OJ N3yi/fZOwnpMsvOCE8On78NazvveCLdX+xjCV0CbGrEAhUlek8mcAh/pdnYPWS6E vjG9CXPIo5IjOsADmdl+mJqD6mk5adLgskgLAgMBAAGjezB5MAkGA1UdEwQCMAAw LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G A1UdDgQWBBTpCpgs8Mr3W0vULGRiRGUXXa5xDjAfBgNVHSMEGDAWgBQuRhLvdqZF IsoYcdgQ3ASnjG5u9jANBgkqhkiG9w0BAQsFAAOCAQEAqQpgpXkT6LqQDklzWbso KRw2Kf/dFhFcjqPdfJrMJt/0ByN5XzC540czJZLO7203pQH1olgyqSR73yL7xMXi kqyUq8U473Ah3aK0nknZMiOH70RpI2NvlnNzsz26UrmU3F1QE9CNr200mMCt4bZ4 BoUq4Cym0Pf0eXkEcuo7PEMPnl/FEWSak8vfDeY6vFqcDm1LLsNd2Y6Nk4tI+oWH zkuIRafD4usmKAmfWM2wqPtKUdgTGFAxniAOJky+EFRiNCrKI4gNgWplNxwUs79j Ec0LG6L9HvhVguiSH1nyB5AypMD5y7idsvcmcwskVEQKliD4vUsr72t5AMDYHyRQ sw== -----END CERTIFICATE----- redis-rb-4.2.5/test/support/ssl/untrusted-cert.key000066400000000000000000000032501375567530100222430ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEnDPtPf75DLdG VgSoVg47VTSl4iKrkOnx+SVEAXQAyyUn5FMhFJErmgBgMW/nZYiTmVwLt0SwsbZf XdLbq4RRMSrDc2egqgRHxWBbLzn6CTsJR5euqOyhftEifPEcbbX+PW6W+7RwJYGU UMmsb9zNXfke7RiKVzoFf/HdEq+Gt463XSzXwG9tmF9A5Pqj7SxDoKxqamxB6ITS HFlj7NClxx9QheOoVJW9BMuZXCpt7gSt15OJN3yi/fZOwnpMsvOCE8On78Nazvve CLdX+xjCV0CbGrEAhUlek8mcAh/pdnYPWS6EvjG9CXPIo5IjOsADmdl+mJqD6mk5 adLgskgLAgMBAAECggEAenQXW2HLlm43EBWvHPFMN+QfwFmR4m2FZ/IXJb4J9ByS bcAljmry58cpCMCBxAtW/yb7T0i7/ZkRz1/uXmb7KF6JFeag2k5KEDF8jA5j+7kY DfWLIXuQthz4QJS0z1H9kfXNFTh774VMqYWPtliNm1M2P+7H5BHjz10a1Og4bpx4 UzUpFQcPaao0Bu9Vvwj9kjzu8siZPWbqXexqt3S0sgpyCgvjozAUHMsLK8PJm08J 5QhOG0as5siEKnNYrRNxgbaebxDxanSuQAYnLsku2rlyZqnDtoCVzVgPfF3/hzD1 Qs9W3bdiolRYxxo6rhjGrvv+KVG/wavJSYbBL6l4wQKBgQDz845M/XYlxYl27s5U i/BkB4yJge7DsfTWJpR7Zf3snlda3QEF/BsBRyFErA7stRfhOGEYDFrYaYgoVJGQ oZrVqVuwKgdmbsJoVOek0Ab4PguIEJYPBLy3KHCIAoeMZXBiUk3o/pww4kvTyFcB 8FiJRlLFd2298Lvowf2k6iBZOwKBgQDOUhRdD4Lkyi3N1Y+NGtFQsAPSiABbF/9f 0QF45Gkp53TCWnhZeF82+yGHnJ2y7xusC15nfv1HTkLL/UeibmVBX//v1SoAAPIq 9/+ftvOnEkLVQJ+WGmmtgazqcdg5/3lC1zfw4u2OjCZOVYArJXpi8bFQerf04BN6 Jh2NcQpfcQKBgQDdFcPHHoXuoWF9edtgYBqSbQz+qdS7YhHj6r7yPnKr+KxuWpBM 3jeTJuWNmOlFuLFVmYTVCI1kR+/vrQTnMK5kKMJBmzVtrb9eUmREx4spewF0ZKO6 JK7qxymE+dXidSQu1yxolibzXoMeAhhoV2vFrQfikePRGdUSkozO4qhCdQKBgQCl d459VANWGg/CFJScRfW5EHEAV7JxXD2jSqwzmHv+73HkrUn392HlZmLtr92Js9ot kLCVsHLQzSMlFmxtCLyMQcGxRvP4LMoLS/nmzYN7alnPTZSvfV9jl6xmGgef/BP0 V0a2GkkLGbte95NjBxuwXsYmFUWTTmJQhGEPHqmDAQKBgDMNCGWVVceGB6UBeXfW kU7Egr8b3/wMJBy4wmilHIlCxtka6hLzx3+kTqLFIYlCq2sy7fvyLc8dX5bEQ7tZ v1Zd10mqvfWKBFm/8D691fxiwfBHAXNFRACmBtRb2NJVGL7CFCuuIAt/cyQTb+7l NsZKEc1x306JFtvKhdqIAWeY -----END PRIVATE KEY----- redis-rb-4.2.5/test/support/wire/000077500000000000000000000000001375567530100167065ustar00rootroot00000000000000redis-rb-4.2.5/test/support/wire/synchrony.rb000066400000000000000000000007571375567530100213000ustar00rootroot00000000000000# frozen_string_literal: true class Wire < Fiber # We cannot run this fiber explicitly because EM schedules it. Resuming the # current fiber on the next tick to let the reactor do work. def self.pass f = Fiber.current EM.next_tick { f.resume } Fiber.yield end def self.sleep(sec) EM::Synchrony.sleep(sec) end def initialize(&blk) super # Schedule run in next tick EM.next_tick { resume } end def join self.class.pass while alive? end end redis-rb-4.2.5/test/support/wire/thread.rb000066400000000000000000000001511375567530100204770ustar00rootroot00000000000000# frozen_string_literal: true class Wire < Thread def self.sleep(sec) Kernel.sleep(sec) end end redis-rb-4.2.5/test/synchrony_driver.rb000066400000000000000000000033441375567530100201640ustar00rootroot00000000000000# frozen_string_literal: true require "em-synchrony" require "em-synchrony/connection_pool" require_relative "../lib/redis" require_relative "../lib/redis/connection/synchrony" require_relative "helper" PORT = 6381 OPTIONS = { port: PORT, db: 15 }.freeze # # if running under Eventmachine + Synchrony (Ruby 1.9+), then # we can simulate the blocking API while performing the network # IO via the EM reactor. # EM.synchrony do r = Redis.new OPTIONS r.flushdb r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal "s2", r.rpop("foo") r.set("foo", "bar") assert_equal "bar", r.getset("foo", "baz") assert_equal "baz", r.get("foo") r.set("foo", "a") assert_equal 1, r.getbit("foo", 1) assert_equal 1, r.getbit("foo", 2) assert_equal 0, r.getbit("foo", 3) assert_equal 0, r.getbit("foo", 4) assert_equal 0, r.getbit("foo", 5) assert_equal 0, r.getbit("foo", 6) assert_equal 1, r.getbit("foo", 7) r.flushdb # command pipelining r.pipelined do r.lpush "foo", "s1" r.lpush "foo", "s2" end assert_equal 2, r.llen("foo") assert_equal "s2", r.lpop("foo") assert_equal "s1", r.lpop("foo") assert_equal "OK", r._client.call(:quit) assert_equal "PONG", r.ping rpool = EM::Synchrony::ConnectionPool.new(size: 5) { Redis.new OPTIONS } result = rpool.watch 'foo' do |rd| assert_kind_of Redis, rd rd.set "foo", "s1" rd.multi do |multi| multi.set "foo", "s2" end end assert_nil result assert_equal "s1", rpool.get("foo") result = rpool.watch "foo" do |rd| assert_kind_of Redis, rd rd.multi do |multi| multi.set "foo", "s3" end end assert_equal ["OK"], result assert_equal "s3", rpool.get("foo") EM.stop end redis-rb-4.2.5/test/test.conf.erb000066400000000000000000000002631375567530100166220ustar00rootroot00000000000000dir <%= REDIS_DIR %> pidfile <%= REDIS_PID %> port 6381 unixsocket <%= REDIS_SOCKET %> timeout 300 loglevel debug logfile <%= REDIS_LOG %> databases 16 daemonize yes redis-rb-4.2.5/test/thread_safety_test.rb000066400000000000000000000021151375567530100204310ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestThreadSafety < Minitest::Test include Helper::Client driver(:ruby, :hiredis) do def test_thread_safety redis = Redis.new(OPTIONS) redis.set "foo", 1 redis.set "bar", 2 sample = 100 t1 = Thread.new do @foos = Array.new(sample) { redis.get "foo" } end t2 = Thread.new do @bars = Array.new(sample) { redis.get "bar" } end t1.join t2.join assert_equal ["1"], @foos.uniq assert_equal ["2"], @bars.uniq end def test_thread_safety_queue_commit redis = Redis.new(OPTIONS) redis.set "foo", 1 redis.set "bar", 2 sample = 100 t1 = Thread.new do sample.times do r.queue("get", "foo") end @foos = r.commit end t2 = Thread.new do sample.times do r.queue("get", "bar") end @bars = r.commit end t1.join t2.join assert_equal ["1"], @foos.uniq assert_equal ["2"], @bars.uniq end end end redis-rb-4.2.5/test/transactions_test.rb000066400000000000000000000156031375567530100203250ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestTransactions < Minitest::Test include Helper::Client def test_multi_discard r.multi assert_equal "QUEUED", r.set("foo", "1") assert_equal "QUEUED", r.get("foo") r.discard assert_nil r.get("foo") end def test_multi_exec_with_a_block r.multi do |multi| multi.set "foo", "s1" end assert_equal "s1", r.get("foo") end def test_multi_exec_with_a_block_doesn_t_return_replies_for_multi_and_exec r1, r2, nothing_else = r.multi do |multi| multi.set "foo", "s1" multi.get "foo" end assert_equal "OK", r1 assert_equal "s1", r2 assert_nil nothing_else end def test_assignment_inside_multi_exec_block r.multi do |m| @first = m.sadd("foo", 1) @second = m.sadd("foo", 1) end assert_equal true, @first.value assert_equal false, @second.value end # Although we could support accessing the values in these futures, # it doesn't make a lot of sense. def test_assignment_inside_multi_exec_block_with_delayed_command_errors assert_raises(Redis::CommandError) do r.multi do |m| @first = m.set("foo", "s1") @second = m.incr("foo") # not an integer @third = m.lpush("foo", "value") # wrong kind of value end end assert_equal "OK", @first.value assert_raises(Redis::CommandError) { @second.value } assert_raises(Redis::FutureNotReady) { @third.value } end def test_assignment_inside_multi_exec_block_with_immediate_command_errors assert_raises(Redis::CommandError) do r.multi do |m| m.doesnt_exist @first = m.sadd("foo", 1) @second = m.sadd("foo", 1) end end assert_raises(Redis::FutureNotReady) { @first.value } assert_raises(Redis::FutureNotReady) { @second.value } end def test_raise_immediate_errors_in_multi_exec assert_raises(RuntimeError) do r.multi do |multi| multi.set "bar", "s2" raise "Some error" end end assert_nil r.get("bar") assert_nil r.get("baz") end def test_transformed_replies_as_return_values_for_multi_exec_block info, = r.multi do |_m| r.info end assert info.is_a?(Hash) end def test_transformed_replies_inside_multi_exec_block r.multi do |_m| @info = r.info end assert @info.value.is_a?(Hash) end def test_raise_command_errors_when_reply_is_not_transformed assert_raises(Redis::CommandError) do r.multi do |m| m.set("foo", "s1") m.incr("foo") # not an integer m.lpush("foo", "value") # wrong kind of value end end assert_equal "s1", r.get("foo") end def test_empty_multi_exec result = nil redis_mock(exec: ->(*_) { "-ERROR" }) do |redis| result = redis.multi {} end assert_equal [], result end def test_raise_command_errors_when_reply_is_transformed_from_int_to_boolean assert_raises(Redis::CommandError) do r.multi do |m| m.set("foo", 1) m.sadd("foo", 2) end end end def test_raise_command_errors_when_reply_is_transformed_from_ok_to_boolean assert_raises(Redis::CommandError) do r.multi do |m| m.set("foo", 1, ex: 0, nx: true) end end end def test_raise_command_errors_when_reply_is_transformed_to_float assert_raises(Redis::CommandError) do r.multi do |m| m.set("foo", 1) m.zscore("foo", "b") end end end def test_raise_command_errors_when_reply_is_transformed_to_floats assert_raises(Redis::CommandError) do r.multi do |m| m.zrange("a", "b", 5, with_scores: true) end end end def test_raise_command_errors_when_reply_is_transformed_to_hash assert_raises(Redis::CommandError) do r.multi do |m| m.set("foo", 1) m.hgetall("foo") end end end def test_raise_command_errors_when_accessing_futures_after_multi_exec begin r.multi do |m| m.set("foo", "s1") @counter = m.incr("foo") # not an integer end rescue Exception # Not gonna deal with it end # We should test for Redis::Error here, but hiredis doesn't yet do # custom error classes. err = nil begin @counter.value rescue => err end assert err.is_a?(RuntimeError) end def test_multi_with_a_block_yielding_the_client r.multi do |multi| multi.set "foo", "s1" end assert_equal "s1", r.get("foo") end def test_multi_with_interrupt_preserves_client original = r._client Redis::Pipeline.stubs(:new).raises(Interrupt) assert_raises(Interrupt) { r.multi {} } assert_equal r._client, original end def test_raise_command_error_when_exec_fails redis_mock(exec: ->(*_) { "-ERROR" }) do |redis| assert_raises(Redis::CommandError) do redis.multi do |m| m.set "foo", "s1" end end end end def test_watch res = r.watch "foo" assert_equal "OK", res end def test_watch_with_an_unmodified_key r.watch "foo" r.multi do |multi| multi.set "foo", "s1" end assert_equal "s1", r.get("foo") end def test_watch_with_an_unmodified_key_passed_as_array r.watch ["foo", "bar"] r.multi do |multi| multi.set "foo", "s1" end assert_equal "s1", r.get("foo") end def test_watch_with_a_modified_key r.watch "foo" r.set "foo", "s1" res = r.multi do |multi| multi.set "foo", "s2" end assert_nil res assert_equal "s1", r.get("foo") end def test_watch_with_a_modified_key_passed_as_array r.watch ["foo", "bar"] r.set "foo", "s1" res = r.multi do |multi| multi.set "foo", "s2" end assert_nil res assert_equal "s1", r.get("foo") end def test_watch_with_a_block_and_an_unmodified_key result = r.watch "foo" do |rd| assert_same r, rd rd.multi do |multi| multi.set "foo", "s1" end end assert_equal ["OK"], result assert_equal "s1", r.get("foo") end def test_watch_with_a_block_and_a_modified_key result = r.watch "foo" do |rd| assert_same r, rd rd.set "foo", "s1" rd.multi do |multi| multi.set "foo", "s2" end end assert_nil result assert_equal "s1", r.get("foo") end def test_watch_with_a_block_that_raises_an_exception r.set("foo", "s1") begin r.watch "foo" do raise "test" end rescue RuntimeError end r.set("foo", "s2") # If the watch was still set from within the block above, this multi/exec # would fail. This proves that raising an exception above unwatches. r.multi do |multi| multi.set "foo", "s3" end assert_equal "s3", r.get("foo") end def test_unwatch_with_a_modified_key r.watch "foo" r.set "foo", "s1" r.unwatch r.multi do |multi| multi.set "foo", "s2" end assert_equal "s2", r.get("foo") end end redis-rb-4.2.5/test/unknown_commands_test.rb000066400000000000000000000003741375567530100211740ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestUnknownCommands < Minitest::Test include Helper::Client def test_should_try_to_work assert_raises Redis::CommandError do r.not_yet_implemented_command end end end redis-rb-4.2.5/test/url_param_test.rb000066400000000000000000000102301375567530100175660ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestUrlParam < Minitest::Test include Helper::Client def test_url_defaults_to_______________ redis = Redis.new assert_equal "127.0.0.1", redis._client.host assert_equal 6379, redis._client.port assert_equal 0, redis._client.db assert_nil redis._client.password end def test_allows_to_pass_in_a_url redis = Redis.new url: "redis://:secr3t@foo.com:999/2" assert_equal "foo.com", redis._client.host assert_equal 999, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_allows_to_pass_in_a_url_with_string_key redis = Redis.new "url" => "redis://:secr3t@foo.com:999/2" assert_equal "foo.com", redis._client.host assert_equal 999, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_unescape_password_from_url redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2" assert_equal "secr3t:", redis._client.password end def test_unescape_password_from_url_with_string_key redis = Redis.new "url" => "redis://:secr3t%3A@foo.com:999/2" assert_equal "secr3t:", redis._client.password end def test_does_not_unescape_password_when_explicitly_passed redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2", password: "secr3t%3A" assert_equal "secr3t%3A", redis._client.password end def test_does_not_unescape_password_when_explicitly_passed_with_string_key redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2", "password" => "secr3t%3A" assert_equal "secr3t%3A", redis._client.password end def test_override_url_if_path_option_is_passed redis = Redis.new url: "redis://:secr3t@foo.com/foo:999/2", path: "/tmp/redis.sock" assert_equal "/tmp/redis.sock", redis._client.path assert_nil redis._client.host assert_nil redis._client.port end def test_override_url_if_path_option_is_passed_with_string_key redis = Redis.new :url => "redis://:secr3t@foo.com/foo:999/2", "path" => "/tmp/redis.sock" assert_equal "/tmp/redis.sock", redis._client.path assert_nil redis._client.host assert_nil redis._client.port end def test_overrides_url_if_another_connection_option_is_passed redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: 1000 assert_equal "foo.com", redis._client.host assert_equal 1000, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_overrides_url_if_another_connection_option_is_passed_with_string_key redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => 1000 assert_equal "foo.com", redis._client.host assert_equal 1000, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_does_not_overrides_url_if_a_nil_option_is_passed redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: nil assert_equal "foo.com", redis._client.host assert_equal 999, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_does_not_overrides_url_if_a_nil_option_is_passed_with_string_key redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => nil assert_equal "foo.com", redis._client.host assert_equal 999, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password end def test_does_not_modify_the_passed_options options = { url: "redis://:secr3t@foo.com:999/2" } Redis.new(options) assert(options == { url: "redis://:secr3t@foo.com:999/2" }) end def test_uses_redis_url_over_default_if_available ENV["REDIS_URL"] = "redis://:secr3t@foo.com:999/2" redis = Redis.new assert_equal "foo.com", redis._client.host assert_equal 999, redis._client.port assert_equal 2, redis._client.db assert_equal "secr3t", redis._client.password ENV.delete("REDIS_URL") end def test_defaults_to_localhost redis = Redis.new(url: "redis:///") assert_equal "127.0.0.1", redis._client.host end end