pax_global_header00006660000000000000000000000064146613050710014515gustar00rootroot0000000000000052 comment=a1d6f68d44f078659ec21a82dc75b4254aff2b5a redis-rb-5.3.0/000077500000000000000000000000001466130507100132315ustar00rootroot00000000000000redis-rb-5.3.0/.github/000077500000000000000000000000001466130507100145715ustar00rootroot00000000000000redis-rb-5.3.0/.github/dependabot.yml000066400000000000000000000001661466130507100174240ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" redis-rb-5.3.0/.github/workflows/000077500000000000000000000000001466130507100166265ustar00rootroot00000000000000redis-rb-5.3.0/.github/workflows/test.yaml000066400000000000000000000147601466130507100205010ustar00rootroot00000000000000--- name: Test on: push: branches: - "*" pull_request: branches: - "*" jobs: lint: name: Rubocop timeout-minutes: 15 runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3.2" bundler-cache: true - name: Lint run: bundle exec rubocop rubies: name: Ruby timeout-minutes: 15 strategy: fail-fast: false matrix: ruby: ["3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "jruby-9.3.6.0"] runs-on: ubuntu-latest env: LOW_TIMEOUT: "0.01" REDIS_BRANCH: "7.2" steps: - name: Check out code uses: actions/checkout@v4 - 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 }} bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis run: make start - name: Test run: bundle exec rake test:redis test:distributed - name: Shutting down Redis run: make stop truffle: name: TruffleRuby timeout-minutes: 15 strategy: fail-fast: false matrix: suite: ["redis", "distributed"] runs-on: ubuntu-latest env: LOW_TIMEOUT: "0.01" REDIS_BRANCH: "7.2" TRUFFLERUBYOPT: "--engine.Mode=latency" steps: - name: Check out code uses: actions/checkout@v4 - 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: truffleruby bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis run: make start - name: Test run: bundle exec rake test:${{ matrix.suite }} - name: Shutting down Redis run: make stop drivers: name: Driver timeout-minutes: 15 strategy: fail-fast: false matrix: driver: ["hiredis"] runs-on: ubuntu-latest env: LOW_TIMEOUT: "0.01" DRIVER: ${{ matrix.driver }} REDIS_BRANCH: "7.2" steps: - name: Check out code uses: actions/checkout@v4 - 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: "2.6" bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis run: make start - name: Test run: bundle exec rake test:redis test:distributed - name: Shutting down Redis run: make stop redises: name: Redis timeout-minutes: 15 strategy: fail-fast: false matrix: redis: ["7.0", "6.2", "6.0", "5.0"] runs-on: ubuntu-latest env: LOW_TIMEOUT: "0.14" REDIS_BRANCH: ${{ matrix.redis }} steps: - name: Check out code uses: actions/checkout@v4 - 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: "2.6" bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-${{ matrix.redis }}-on-ubuntu-latest" - name: Booting up Redis run: make start - name: Test run: bundle exec rake test:redis test:distributed - name: Shutting down Redis run: make stop sentinel: name: Sentinel timeout-minutes: 15 strategy: fail-fast: false runs-on: ubuntu-latest env: LOW_TIMEOUT: "0.14" REDIS_BRANCH: "7.0" steps: - name: Check out code uses: actions/checkout@v4 - 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: "2.6" bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis run: make start_sentinel wait_for_sentinel - name: Test run: bundle exec rake test:sentinel - name: Shutting down Redis run: make stop_all cluster: name: Cluster timeout-minutes: 15 strategy: fail-fast: false runs-on: ubuntu-latest env: TIMEOUT: "15" LOW_TIMEOUT: "0.14" REDIS_BRANCH: "7.2" BUNDLE_GEMFILE: cluster/Gemfile steps: - name: Check out code uses: actions/checkout@v4 - 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: "2.7" bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis run: make start start_cluster create_cluster - name: Test run: bundle exec rake test:cluster - name: Shutting down Redis run: make stop_all redis-rb-5.3.0/.gitignore000066400000000000000000000003721466130507100152230ustar00rootroot00000000000000*.rdb *.swp Gemfile.lock *.gem /tmp/ /.idea /.yardoc /.bundle /cluster/.bundle /coverage/* /doc/ /examples/sentinel/sentinel.conf /nohup.out /pkg/* /rdsrv /redis/* /test/db /test/test.conf appendonly.aof appendonlydir temp-rewriteaof-*.aof .history redis-rb-5.3.0/.rubocop.yml000066400000000000000000000045621466130507100155120ustar00rootroot00000000000000inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.6 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 Lint/MissingSuper: 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/SlicingWithRange: Enabled: false Style/TrailingCommaInArrayLiteral: Enabled: false Style/TrailingCommaInArguments: Enabled: false Style/ParallelAssignment: Enabled: false Style/NumericPredicate: Enabled: false Style/IfUnlessModifier: Enabled: false Style/MutableConstant: Enabled: false # false positives Style/SignalException: Exclude: - 'lib/redis/connection/synchrony.rb' 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 Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/ElseAlignment: Enabled: false Layout/RescueEnsureAlignment: Enabled: false Naming/HeredocDelimiterNaming: Enabled: false Naming/VariableNumber: Enabled: false Naming/FileName: Enabled: false Naming/RescuedExceptionsVariableName: Enabled: false Naming/AccessorMethodName: Exclude: - lib/redis/connection/ruby.rb Naming/MethodParameterName: Enabled: false Metrics/BlockNesting: Enabled: false Style/HashTransformValues: Enabled: false Style/TrailingCommaInHashLiteral: Enabled: false Style/SymbolProc: Exclude: - 'test/**/*' Bundler/OrderedGems: Enabled: false Gemspec/RequiredRubyVersion: Exclude: - cluster/redis-clustering.gemspec redis-rb-5.3.0/.rubocop_todo.yml000066400000000000000000000014251466130507100165320ustar00rootroot00000000000000# This configuration was generated by # `rubocop --auto-gen-config` # on 2022-01-08 23:15:30 UTC using RuboCop version 1.11.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 2 Lint/HashCompareByIdentity: Exclude: - 'lib/redis.rb' # Offense count: 6 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'lib/redis.rb' - 'lib/redis/client.rb' - 'lib/redis/cluster.rb' - 'lib/redis/cluster/node.rb' - 'lib/redis/cluster/slot.rb' - 'lib/redis/pipeline.rb' redis-rb-5.3.0/.ruby-version000066400000000000000000000000061466130507100156720ustar00rootroot000000000000003.3.0 redis-rb-5.3.0/.yardopts000066400000000000000000000001041466130507100150720ustar00rootroot00000000000000--exclude redis/connection --exclude redis/compat --markup markdown redis-rb-5.3.0/CHANGELOG.md000066400000000000000000000535471466130507100150600ustar00rootroot00000000000000# Unreleased # 5.3.0 - Fix the return type of `hgetall` when used inside a `multi` transaction which is itself inside a pipeline. # 5.2.0 - Now require Ruby 2.6 because `redis-client` does. - Eagerly close subscribed connection when using `subscribe_with_timeout`. See #1259. - Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`. # 5.1.0 - `multi` now accept a `watch` keyword argument like `redis-client`. See #1236. - `bitcount` and `bitpos` now accept a `scale:` argument on Redis 7+. See #1242 - Added `expiretime` and `pexpiretime`. See #1248. # 5.0.8 - Fix `Redis#without_reconnect` for sentinel clients. Fix #1212. - Add `sentinel_username`, `sentinel_password` for sentinel clients. Bump `redis-client` to `>=0.17.0`. See #1213 # 5.0.7 - Fix compatibility with `redis-client 0.15.0` when using Redis Sentinel. Fix #1209. # 5.0.6 - Wait for an extra `config.read_timeout` in blocking commands rather than an arbitrary 100ms. See #1175. - Treat ReadOnlyError as ConnectionError. See #1168. # 5.0.5 - Fix automatic disconnection when the process was forked. See #1157. # 5.0.4 - Cast `ttl` argument to integer in `expire`, `setex` and a few others. # 5.0.3 - Add `OutOfMemoryError` as a subclass of `CommandError` # 5.0.2 - Fix `Redis#close` to properly reset the fork protection check. # 5.0.1 - Added a fake `Redis::Connections.drivers` method to be compatible with older sidekiq versions. # 5.0.0 - Default client timeout decreased from 5 seconds to 1 second. - Eagerly and strictly cast Integer and Float parameters. - Allow to call `subscribe`, `unsubscribe`, `psubscribe` and `punsubscribe` from a subscribed client. See #1131. - Use `MD5` for hashing server nodes in `Redis::Distributed`. This should improve keys distribution among servers. See #1089. - Changed `sadd` and `srem` to now always return an Integer. - Added `sadd?` and `srem?` which always return a Boolean. - Added support for `IDLE` paramter in `xpending`. - Cluster support has been moved to a `redis-clustering` companion gem. - `select` no longer record the current database. If the client has to reconnect after `select` was used, it will reconnect to the original database. - Better support Float timeout in blocking commands. See #977. - `Redis.new` will now raise an error if provided unknown options. - Removed positional timeout in blocking commands (`BLPOP`, etc). Timeout now must be passed as an option: `r.blpop("key", timeout: 2.5)` - Removed `logger` option. - Removed `reconnect_delay_max` and `reconnect_delay`, you can pass precise sleep durations to `reconnect_attempts` instead. - Require Ruby 2.5+. - Removed the deprecated `queue` and `commit` methods. Use `pipelined` instead. - Removed the deprecated `Redis::Future#==`. - Removed the deprecated `pipelined` and `multi` signature. Commands now MUST be called on the block argument, not the original redis instance. - Removed `Redis.current`. You shouldn't assume there is a single global Redis connection, use a connection pool instead, and libaries using Redis should accept a Redis instance (or connection pool) as a config. E.g. `MyLibrary.redis = Redis.new(...)`. - Removed the `synchrony` driver. - Removed `Redis.exists_returns_integer`, it's now always enabled. # 4.8.1 * Automatically reconnect after fork regardless of `reconnect_attempts` # 4.8.0 * Introduce `sadd?` and `srem?` as boolean returning versions of `sadd` and `srem`. * Deprecate `sadd` and `srem` returning a boolean when called with a single argument. To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = false`. * Deprecate passing `timeout` as a positional argument in blocking commands (`brpop`, `blop`, etc). # 4.7.1 * Gracefully handle OpenSSL 3.0 EOF Errors (`OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`). See #1106 This happens frequently on heroku-22. # 4.7.0 * Support single endpoint architecture with SSL/TLS in cluster mode. See #1086. * `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097. * Support IPv6 URLs. * Add `Redis#with` for better compatibility with `connection_pool` usage. * Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073. # 4.6.0 * Deprecate `Redis.current`. * Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059. ```ruby redis.pipelined do redis.get("key") end ``` should be replaced by: ```ruby redis.pipelined do |pipeline| pipeline.get("key") end ``` * Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059. ```ruby redis.multi do redis.get("key") end ``` should be replaced by: ```ruby redis.multi do |transaction| transaction.get("key") end ``` * Deprecate `Redis#queue` and `Redis#commit`. See #1059. * Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055. * `Redis#synchronize` is now private like it should always have been. * Add `Redis.silence_deprecations=` to turn off deprecation warnings. If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`. It is however heavily recommended to fix them instead when possible. * Add `Redis.raise_deprecations=` to turn deprecation warnings into errors. This makes it easier to identitify the source of deprecated APIs usage. It is recommended to set `Redis.raise_deprecations = true` in development and test environments. * Add new options to ZRANGE. See #1053. * Add ZRANGESTORE command. See #1053. * Add SCAN support for `Redis::Cluster`. See #1049. * Add COPY command. See #1053. See #1048. * Add ZDIFFSTORE command. See #1046. * Add ZDIFF command. See #1044. * Add ZUNION command. See #1042. * Add HRANDFIELD command. See #1040. # 4.5.1 * Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username, redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password. This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038. # 4.5.0 * Handle parts of the command using incompatible encodings. See #1037. * Add GET option to SET command. See #1036. * Add ZRANDMEMBER command. See #1035. * Add LMOVE/BLMOVE commands. See #1034. * Add ZMSCORE command. See #1032. * Add LT/GT options to ZADD. See #1033. * Add SMISMEMBER command. See #1031. * Add EXAT/PXAT options to SET. See #1028. * Add GETDEL/GETEX commands. See #1024. * `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`. * Fix Redis < 6 detection during connect. See #1025. * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026. # 4.4.0 * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019. * Add support for `XAUTOCLAIM`. See #1018. * Properly issue `READONLY` when reconnecting to replicas. Fix #1017. * Make `del` a noop if passed an empty list of keys. See #998. * Add support for `ZINTER`. See #995. # 4.3.1 * Fix password authentication against redis server 5 and older. # 4.3.0 * Add the TYPE argument to scan and scan_each. See #985. * Support AUTH command for ACL. See #967. # 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-5.3.0/Gemfile000066400000000000000000000002361466130507100145250ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'minitest' gem 'rake' gem 'rubocop', '~> 1.25.1' gem 'mocha' gem 'hiredis-client' redis-rb-5.3.0/LICENSE000066400000000000000000000020441466130507100142360ustar00rootroot00000000000000Copyright (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-5.3.0/README.md000066400000000000000000000326251466130507100145200ustar00rootroot00000000000000# redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][rdoc-master-image]][rdoc-master-link] 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)`). 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") ``` To connect a Redis instance using [ACL](https://redis.io/topics/acl), use: ```ruby redis = Redis.new(username: 'myname', 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]. ## Connection Pooling and Thread safety The client does not provide connection pooling. Each `Redis` instance has one and only one connection to the server, and use of this connection is protected by a mutex. As such it is heavilly recommended to use the [`connection_pool` gem](https://github.com/mperham/connection_pool), e.g.: ```ruby module MyApp def self.redis @redis ||= ConnectionPool::Wrapper.new do Redis.new(url: ENV["REDIS_URL"]) end end end MyApp.redis.incr("some-counter") ``` ## 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(name: "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. To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication. ```ruby SENTINELS = [{ host: '127.0.0.1', port: 26380}, { host: '127.0.0.1', port: 26381}] redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, sentinel_username: 'appuser', sentinel_password: 'mysecret', role: :master) ``` If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials ```ruby # Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels: SENTINELS = [{ host: '127.0.0.1', port: 26380 }, { host: '127.0.0.1', port: 26381 }] redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret') ``` So you have to provide Sentinel credential and Redis explictly even they are the same ```ruby # Use 'mysecret' to authenticate against the mymaster instance and sentinel SENTINELS = [{ host: '127.0.0.1', port: 26380 }, { host: '127.0.0.1', port: 26381 }] redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret') ``` Also the `name`, `password`, `username` and `db` for Redis instance can be passed as an url: ```ruby redis = Redis.new(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master) ``` ## Cluster support [Clustering](https://redis.io/topics/cluster-spec). is supported via the [`redis-clustering` gem](cluster/). ## 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 |pipeline| pipeline.set "foo", "bar" pipeline.incr "baz" end # => ["OK", 1] ``` Commands must be called on the yielded objects. If you call methods on the original client objects from inside a pipeline, they will be sent immediately: ```ruby redis.pipelined do |pipeline| pipeline.set "foo", "bar" redis.incr "baz" # => 1 end # => ["OK"] ``` ### Exception management The `exception` flag in the `#pipelined` is a feature that modifies the pipeline execution behavior. When set to `false`, it doesn't raise an exception when a command error occurs. Instead, it allows the pipeline to execute all commands, and any failed command will be available in the returned array. (Defaults to `true`) ```ruby results = redis.pipelined(exception: false) do |pipeline| pipeline.set('key1', 'value1') pipeline.lpush('key1', 'something') # This will fail pipeline.set('key2', 'value2') end # results => ["OK", #, "OK"] results.each do |result| if result.is_a?(Redis::CommandError) # Do something with the failed result end end ``` ### 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 |transaction| transaction.set "foo", "bar" transaction.incr "baz" end # => ["OK", 1] ``` ### Futures Replies to commands in a pipeline can be accessed via the *futures* they emit. All calls on the pipeline object 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 set = incr = nil redis.pipelined do |pipeline| set = pipeline.set "foo", "bar" incr = pipeline.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 Redis::BaseError => 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. Starting in version 5.0, the default for each is 1. Before that, it was 5. 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 **By default**, this gem will only **retry a connection once** and then fail, but the client allows you to configure how many `reconnect_attempts` it should complete before declaring a connection as failed. ```ruby Redis.new(reconnect_attempts: 0) Redis.new(reconnect_attempts: 3) ``` If you wish to wait between reconnection attempts, you can instead pass a list of durations: ```ruby Redis.new(reconnect_attempts: [ 0, # retry immediately 0.25, # retry a second time after 250ms 1, # retry a third and final time after another 1s ]) ``` If you wish to disable reconnection only for some commands, you can use `disable_reconnection`: ```ruby redis.get("some-key") # this may be retried redis.disable_reconnection do redis.incr("some-counter") # this won't be retried. end ``` ## SSL/TLS Support 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")) } ) ``` [OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.5.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html ## 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. ## hiredis binding By default, redis-rb uses Ruby's socket library to talk with Redis. 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-client`: ```ruby gem "redis" gem "hiredis-client" ``` If your application doesn't call `Bundler.require`, you may have to require it explictly: ```ruby require "hiredis-client" ```` This makes the hiredis driver the default. If you want to be certain hiredis is being used, when instantiating the client object, specify hiredis: ```ruby redis = Redis.new(driver: :hiredis) ``` ## Testing This library is tested against recent Ruby and Redis versions. Check [Github Actions][gh-actions-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. [rdoc-master-image]: https://img.shields.io/badge/docs-rdoc.info-blue.svg [rdoc-master-link]: https://rubydoc.info/github/redis/redis-rb [redis-commands]: https://redis.io/commands [redis-home]: https://redis.io [redis-url]: https://www.iana.org/assignments/uri-schemes/prov/redis [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg [gh-actions-link]: https://github.com/redis/redis-rb/actions [rubydoc]: https://rubydoc.info/gems/redis redis-rb-5.3.0/Rakefile000066400000000000000000000017541466130507100147050ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/gem_tasks' Bundler::GemHelper.install_tasks(dir: "cluster", name: "redis-clustering") require 'rake/testtask' namespace :test do groups = %i(redis distributed sentinel) groups.each do |group| Rake::TestTask.new(group) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/#{group}/**/*_test.rb"] t.options = '-v' if ENV['CI'] || ENV['VERBOSE'] end end lost_tests = Dir["test/**/*_test.rb"] - groups.map { |g| Dir["test/#{g}/**/*_test.rb"] }.flatten unless lost_tests.empty? abort "The following test files are in no group:\n#{lost_tests.join("\n")}" end Rake::TestTask.new(:cluster) do |t| t.libs << "cluster/test" << "test" t.libs << "cluster/lib" << "lib" t.test_files = FileList["cluster/test/**/*_test.rb"] t.options = '-v' if ENV['CI'] || ENV['VERBOSE'] end end task test: ["test:redis", "test:distributed", "test:sentinel", "test:cluster"] task default: :test redis-rb-5.3.0/bin/000077500000000000000000000000001466130507100140015ustar00rootroot00000000000000redis-rb-5.3.0/bin/build000077500000000000000000000031071466130507100150270ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true TARBALL = ARGV[0] require 'digest/sha1' require 'English' 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-5.3.0/bin/cluster_creator000077500000000000000000000005531466130507100171320ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true puts ARGV.join(" ") require 'bundler/setup' $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require_relative '../cluster/test/support/orchestrator' urls = ARGV.map { |host_port| "redis://#{host_port}" } orchestrator = ClusterOrchestrator.new(urls, timeout: 3.0) orchestrator.rebuild orchestrator.close redis-rb-5.3.0/bin/console000077500000000000000000000002241466130507100153670ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require 'redis' require 'irb' IRB.start redis-rb-5.3.0/cluster/000077500000000000000000000000001466130507100147125ustar00rootroot00000000000000redis-rb-5.3.0/cluster/CHANGELOG.md000066400000000000000000000000001466130507100165110ustar00rootroot00000000000000redis-rb-5.3.0/cluster/Gemfile000066400000000000000000000002731466130507100162070ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'redis', path: File.expand_path("..", __dir__) gem 'minitest' gem 'rake' gem 'rubocop', '~> 1.25.1' gem 'mocha' redis-rb-5.3.0/cluster/LICENSE000066400000000000000000000020441466130507100157170ustar00rootroot00000000000000Copyright (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-5.3.0/cluster/README.md000066400000000000000000000074651466130507100162050ustar00rootroot00000000000000# Redis::Cluster ## Getting started Install with: ``` $ gem install redis-clustering ``` You can connect to Redis by instantiating the `Redis::Cluster` class: ```ruby require "redis-clustering" redis = Redis::Cluster.new(nodes: (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }) ``` NB: Both `redis_cluster` and `redis-cluster` are unrelated and abandoned gems. ```ruby # Nodes can be passed to the client as an array of connection URLs. nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" } redis = Redis::Cluster.new(nodes: nodes) # You can also specify the options as a Hash. The options are the same as for a single server connection. (7000..7005).map { |port| { host: '127.0.0.1', port: port } } ``` You can also specify only a subset of the nodes, and the client will discover the missing ones using the [CLUSTER NODES](https://redis.io/commands/cluster-nodes) command. ```ruby Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) ``` If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys. ```ruby Redis::Cluster.new(nodes: nodes, replica: true) ``` Also, you can specify the `:replica_affinity` option if you want to prevent accessing cross availability zones. ```ruby Redis::Cluster.new(nodes: nodes, replica: true, replica_affinity: :latency) ``` The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model). ```ruby redis = Redis::Cluster.new(nodes: %w[redis://127.0.0.1:7000]) redis.mget('key1', 'key2') #=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot) redis.mget('{key}1', '{key}2') #=> [nil, nil] ``` * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening. * The client support permanent node failures, and will reroute requests to promoted slaves. * The client supports `MOVED` and `ASK` redirections transparently. ## Cluster mode with SSL/TLS Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this: ```ruby Redis::Cluster.new(nodes: %w[rediss://foo.example.com:6379]) ``` On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS. ```ruby Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com') ``` In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates. ## Transaction with an optimistic locking Since Redis cluster is a distributed system, several behaviors are different from a standalone server. Client libraries can make them compatible up to a point, but a part of features needs some restrictions. Especially, some cautions are needed to use the transaction feature with an optimistic locking. ```ruby # The client is an instance of the internal adapter for the optimistic locking redis.watch("{my}key") do |client| if client.get("{my}key") == "some value" # The tx is an instance of the internal adapter for the transaction client.multi do |tx| tx.set("{my}key", "other value") tx.incr("{my}counter") end else client.unwatch end end ``` In a cluster mode client, you need to pass a block if you call the watch method and you need to specify an argument to the block. Also, you should use the block argument as a receiver to call commands in the block. Although the above restrictions are needed, this implementations is compatible with a standalone client. redis-rb-5.3.0/cluster/bin/000077500000000000000000000000001466130507100154625ustar00rootroot00000000000000redis-rb-5.3.0/cluster/bin/console000077500000000000000000000002351466130507100170520ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'irb' require 'bundler/setup' require 'redis/cluster' IRB.start(File.expand_path('..', __dir__)) redis-rb-5.3.0/cluster/lib/000077500000000000000000000000001466130507100154605ustar00rootroot00000000000000redis-rb-5.3.0/cluster/lib/redis-clustering.rb000066400000000000000000000000671466130507100212730ustar00rootroot00000000000000# frozen_string_literal: true require "redis/cluster" redis-rb-5.3.0/cluster/lib/redis/000077500000000000000000000000001466130507100165665ustar00rootroot00000000000000redis-rb-5.3.0/cluster/lib/redis/cluster.rb000066400000000000000000000117511466130507100206010ustar00rootroot00000000000000# frozen_string_literal: true require "redis" class Redis class Cluster < ::Redis # Raised when client connected to redis as cluster mode # and failed to fetch cluster state information by commands. class InitialSetupError < BaseError end # Raised when client connected to redis as cluster mode # and some cluster subcommands were called. class OrchestrationCommandNotSupported < BaseError def initialize(command, subcommand = '') str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase msg = "#{str} command should be used with care "\ 'only by applications orchestrating Redis Cluster, like redis-trib, '\ 'and the command if used out of the right context can leave the cluster '\ 'in a wrong state or cause data loss.' super(msg) end end # Raised when error occurs on any node of cluster. class CommandErrorCollection < BaseError attr_reader :errors # @param errors [Hash{String => Redis::CommandError}] # @param error_message [String] def initialize(errors, error_message = 'Command errors were replied on any node') @errors = errors super(error_message) end end # Raised when cluster client can't select node. class AmbiguousNodeError < BaseError end class TransactionConsistencyError < BaseError end class NodeMightBeDown < BaseError end def connection raise NotImplementedError, "Redis::Cluster doesn't implement #connection" end # Create a new client instance # # @param [Hash] options # @option options [Float] :timeout (5.0) timeout in seconds # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis` # @option options [Integer, Array] :reconnect_attempts Number of attempts trying to connect, # or a list of sleep duration between attempts. # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not # @option options [Array String, Integer}>] :nodes List of cluster nodes to contact # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not # @option options [Symbol] :replica_affinity scale reading strategy, currently supported: `:random`, `:latency` # @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and # client has to connect nodes via single endpoint with SSL/TLS # @option options [Class] :connector Class of custom connector # # @return [Redis::Cluster] a new client instance def initialize(*) # rubocop:disable Lint/UselessMethodDefinition super end ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) # Sends `CLUSTER *` command to random node and returns its reply. # # @see https://redis.io/commands#cluster Reference of cluster command # # @param subcommand [String, Symbol] the subcommand of cluster command # e.g. `:slots`, `:nodes`, `:slaves`, `:info` # # @return [Object] depends on the subcommand def cluster(subcommand, *args) subcommand = subcommand.to_s.downcase block = case subcommand when 'slots' HashifyClusterSlots when 'nodes' HashifyClusterNodes when 'slaves' HashifyClusterSlaves when 'info' HashifyInfo else Noop end send_command([:cluster, subcommand] + args, &block) end # Watch the given keys to determine execution of the MULTI/EXEC block. # # Using a block is required for a cluster client. It's different from a standalone client. # And you should use the block argument as a receiver if you call commands. # # An `#unwatch` is automatically issued if an exception is raised within the # block that is a subclass of StandardError and is not a ConnectionError. # # @param keys [String, Array] one or more keys to watch # @return [Object] returns the return value of the block # # @example A typical use case. # # The client is an instance of the internal adapter for the optimistic locking # redis.watch("{my}key") do |client| # if client.get("{my}key") == "some value" # # The tx is an instance of the internal adapter for the transaction # client.multi do |tx| # tx.set("{my}key", "other value") # tx.incr("{my}counter") # end # else # client.unwatch # end # end # #=> ["OK", 6] def watch(*keys, &block) synchronize { |c| c.watch(*keys, &block) } end private def initialize_client(options) cluster_config = RedisClient.cluster(**options, protocol: 2, client_implementation: ::Redis::Cluster::Client) cluster_config.new_client end end end require "redis/cluster/client" redis-rb-5.3.0/cluster/lib/redis/cluster/000077500000000000000000000000001466130507100202475ustar00rootroot00000000000000redis-rb-5.3.0/cluster/lib/redis/cluster/client.rb000066400000000000000000000101101466130507100220430ustar00rootroot00000000000000# frozen_string_literal: true require 'redis-cluster-client' require 'redis/cluster/transaction_adapter' class Redis class Cluster class Client < RedisClient::Cluster ERROR_MAPPING = ::Redis::Client::ERROR_MAPPING.merge( RedisClient::Cluster::InitialSetupError => Redis::Cluster::InitialSetupError, RedisClient::Cluster::OrchestrationCommandNotSupported => Redis::Cluster::OrchestrationCommandNotSupported, RedisClient::Cluster::AmbiguousNodeError => Redis::Cluster::AmbiguousNodeError, RedisClient::Cluster::ErrorCollection => Redis::Cluster::CommandErrorCollection, RedisClient::Cluster::Transaction::ConsistencyError => Redis::Cluster::TransactionConsistencyError, RedisClient::Cluster::NodeMightBeDown => Redis::Cluster::NodeMightBeDown, ) class << self def config(**kwargs) super(protocol: 2, **kwargs) end def sentinel(**kwargs) super(protocol: 2, **kwargs) end def translate_error!(error, mapping: ERROR_MAPPING) case error when RedisClient::Cluster::ErrorCollection error.errors.each do |_node, node_error| if node_error.is_a?(RedisClient::AuthenticationError) raise mapping.fetch(node_error.class), node_error.message, node_error.backtrace end end remapped_node_errors = error.errors.map do |node_key, node_error| remapped = mapping.fetch(node_error.class, node_error.class).new(node_error.message) remapped.set_backtrace node_error.backtrace [node_key, remapped] end.to_h raise(Redis::Cluster::CommandErrorCollection.new(remapped_node_errors, error.message).tap do |remapped| remapped.set_backtrace error.backtrace end) else Redis::Client.translate_error!(error, mapping: mapping) end end end def initialize(*) handle_errors { super } end ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) def id server_url.join(' ') end def server_url @router.nil? ? @config.startup_nodes.keys : router.node_keys end def connected? true end def disable_reconnection yield # TODO: do we need this, is it doable? end def timeout config.read_timeout end def db 0 end undef_method :call undef_method :call_once undef_method :call_once_v undef_method :blocking_call def call_v(command, &block) handle_errors { super(command, &block) } end def blocking_call_v(timeout, command, &block) timeout += self.timeout if timeout && timeout > 0 handle_errors { super(timeout, command, &block) } end def pipelined(exception: true, &block) handle_errors { super(exception: exception, &block) } end def multi(watch: nil, &block) handle_errors { super(watch: watch, &block) } end def watch(*keys, &block) unless block_given? raise( Redis::Cluster::TransactionConsistencyError, 'A block is required if you use the cluster client.' ) end unless block.arity == 1 raise( Redis::Cluster::TransactionConsistencyError, 'Given block needs an argument if you use the cluster client.' ) end handle_errors do RedisClient::Cluster::OptimisticLocking.new(router).watch(keys) do |c, slot, asking| transaction = Redis::Cluster::TransactionAdapter.new( self, router, @command_builder, node: c, slot: slot, asking: asking ) result = yield transaction c.call('UNWATCH') unless transaction.lock_released? result end end end private def handle_errors yield rescue ::RedisClient::Error => error Redis::Cluster::Client.translate_error!(error) end end end end redis-rb-5.3.0/cluster/lib/redis/cluster/transaction_adapter.rb000066400000000000000000000047511466130507100246300ustar00rootroot00000000000000# frozen_string_literal: true require 'redis_client/cluster/transaction' class Redis class Cluster class TransactionAdapter class Internal < RedisClient::Cluster::Transaction def initialize(client, router, command_builder, node: nil, slot: nil, asking: false) @client = client super(router, command_builder, node: node, slot: slot, asking: asking) end def multi raise(Redis::Cluster::TransactionConsistencyError, "Can't nest multi transaction") end def exec # no need to do anything end def discard # no need to do anything end def watch(*_) raise(Redis::Cluster::TransactionConsistencyError, "Can't use watch in a transaction") end def unwatch # no need to do anything end private def method_missing(name, *args, **kwargs, &block) return call(name, *args, **kwargs, &block) if @client.respond_to?(name) super end def respond_to_missing?(name, include_private = false) return true if @client.respond_to?(name) super end end def initialize(client, router, command_builder, node: nil, slot: nil, asking: false) @client = client @router = router @command_builder = command_builder @node = node @slot = slot @asking = asking @lock_released = false end def lock_released? @lock_released end def multi @lock_released = true transaction = Redis::Cluster::TransactionAdapter::Internal.new( @client, @router, @command_builder, node: @node, slot: @slot, asking: @asking ) yield transaction transaction.execute end def exec # no need to do anything end def discard # no need to do anything end def watch(*_) raise(Redis::Cluster::TransactionConsistencyError, "Can't nest watch command if you use the cluster client") end def unwatch @lock_released = true @node.call('UNWATCH') end private def method_missing(name, *args, **kwargs, &block) return @client.public_send(name, *args, **kwargs, &block) if @client.respond_to?(name) super end def respond_to_missing?(name, include_private = false) return true if @client.respond_to?(name) super end end end end redis-rb-5.3.0/cluster/lib/redis/cluster/version.rb000066400000000000000000000001731466130507100222620ustar00rootroot00000000000000# frozen_string_literal: true require "redis/version" class Redis class Cluster VERSION = Redis::VERSION end end redis-rb-5.3.0/cluster/redis-clustering.gemspec000066400000000000000000000025531466130507100215470ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/redis/version" Gem::Specification.new do |s| s.name = "redis-clustering" s.version = Redis::VERSION github_root = "https://github.com/redis/redis-rb" s.homepage = "#{github_root}/blob/master/cluster" s.summary = "A Ruby client library for Redis Cluster" s.description = <<-EOS A Ruby client that tries to match Redis' Cluster 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" => "#{github_root}/issues", "changelog_uri" => "#{s.homepage}/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/redis/#{s.version}", "homepage_uri" => s.homepage, "source_code_uri" => "#{github_root}/tree/v#{s.version}/cluster" } 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.7.0' s.add_runtime_dependency('redis', s.version) s.add_runtime_dependency('redis-cluster-client', '>= 0.10.0') end redis-rb-5.3.0/cluster/test/000077500000000000000000000000001466130507100156715ustar00rootroot00000000000000redis-rb-5.3.0/cluster/test/blocking_commands_test.rb000066400000000000000000000006001466130507100227220ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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, concurrent: true }, &blk) end end redis-rb-5.3.0/cluster/test/client_internals_test.rb000066400000000000000000000035401466130507100226140ustar00rootroot00000000000000# frozen_string_literal: true require "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_connected? assert_equal true, redis.connected? end def test_close redis.close end def test_disconnect! redis.disconnect! end def test_asking assert_equal 'OK', redis.asking end def test_id expected = '127.0.0.1:16380 '\ '127.0.0.1:16381 '\ '127.0.0.1:16382' assert_equal expected, redis.id end def test_inspect expected = "#' assert_equal expected, redis.inspect end def test_acl_auth_success target_version "6.0.0" do with_acl do |username, password| nodes = DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" } r = _new_client(nodes: nodes) assert_equal('PONG', r.ping) end end end def test_acl_auth_failure target_version "6.0.0" do with_acl do |username, _| assert_raises(Redis::Cluster::InitialSetupError) do nodes = DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" } r = _new_client(nodes: nodes) r.ping end end end end end redis-rb-5.3.0/cluster/test/client_pipelining_test.rb000066400000000000000000000042741466130507100227600ustar00rootroot00000000000000# frozen_string_literal: true require "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 result = redis.pipelined do |pipeline| pipeline.set(:a, 1) pipeline.set(:b, 2) pipeline.set(:c, 3) pipeline.set(:d, 4) pipeline.set(:e, 5) pipeline.set(:f, 6) end assert_equal ["OK"] * 6, result result = redis.pipelined do |pipeline| pipeline.get(:a) pipeline.get(:b) pipeline.get(:c) pipeline.get(:d) pipeline.get(:e) pipeline.get(:f) end assert_equal 1.upto(6).map(&:to_s), result end def test_pipeline_unmapped_errors_are_bubbled_up ex = Class.new(StandardError) assert_raises(ex) do redis.pipelined do |_pipe| raise ex, "boom" end end end def test_pipeline_error_subclasses_are_mapped ex = Class.new(RedisClient::ConnectionError) assert_raises(Redis::ConnectionError) do redis.pipelined do |_pipe| raise ex, "tick tock" end end end end redis-rb-5.3.0/cluster/test/client_replicas_test.rb000066400000000000000000000007151466130507100224200ustar00rootroot00000000000000# frozen_string_literal: true require "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 end redis-rb-5.3.0/cluster/test/client_transactions_test.rb000066400000000000000000000056121466130507100233270ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # ruby -w -Itest test/cluster_client_transactions_test.rb class TestClusterClientTransactions < Minitest::Test include Helper::Cluster def test_cluster_client_does_support_transaction_by_single_key actual = redis.multi do |tx| tx.set('counter', '0') tx.incr('counter') tx.incr('counter') end assert_equal(['OK', 1, 2], actual) assert_equal('2', redis.get('counter')) end def test_cluster_client_does_support_transaction_by_hashtag actual = redis.multi do |tx| tx.mset('{key}1', 1, '{key}2', 2) tx.mset('{key}3', 3, '{key}4', 4) end assert_equal(%w[OK OK], actual) assert_equal(%w[1 2 3 4], redis.mget('{key}1', '{key}2', '{key}3', '{key}4')) end def test_cluster_client_does_not_support_transaction_by_multiple_keys assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.multi do |tx| tx.set('key1', 1) tx.set('key2', 2) tx.set('key3', 3) tx.set('key4', 4) end end assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.multi do |tx| tx.mset('key1', 1, 'key2', 2) tx.mset('key3', 3, 'key4', 4) end end (1..4).each do |i| assert_nil(redis.get("key#{i}")) end end def test_cluster_client_does_support_transaction_with_optimistic_locking redis.mset('{key}1', '1', '{key}2', '2') another = Fiber.new do cli = build_another_client cli.mset('{key}1', '3', '{key}2', '4') cli.close Fiber.yield end redis.watch('{key}1', '{key}2') do |client| another.resume v1 = client.get('{key}1') v2 = client.get('{key}2') client.multi do |tx| tx.set('{key}1', v2) tx.set('{key}2', v1) end end assert_equal %w[3 4], redis.mget('{key}1', '{key}2') end def test_cluster_client_can_be_used_compatible_with_standalone_client redis.set('{my}key', 'value') redis.set('{my}counter', '0') redis.watch('{my}key', '{my}counter') do |client| if client.get('{my}key') == 'value' client.multi do |tx| tx.set('{my}key', 'updated value') tx.incr('{my}counter') end else client.unwatch end end assert_equal('updated value', redis.get('{my}key')) assert_equal('1', redis.get('{my}counter')) another = Fiber.new do cli = build_another_client cli.set('{my}key', 'another value') cli.close Fiber.yield end redis.watch('{my}key', '{my}counter') do |client| another.resume if client.get('{my}key') == 'value' client.multi do |tx| tx.set('{my}key', 'latest value') tx.incr('{my}counter') end else client.unwatch end end assert_equal('another value', redis.get('{my}key')) assert_equal('1', redis.get('{my}counter')) end end redis-rb-5.3.0/cluster/test/commands_on_cluster_test.rb000066400000000000000000000122551466130507100233200ustar00rootroot00000000000000# frozen_string_literal: true require "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_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) sample_slot.fetch('replicas').each do |replica| assert_equal true, replica.key?('ip') assert_equal true, replica.key?('port') assert_equal true, replica.key?('node_id') end 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-5.3.0/cluster/test/commands_on_connection_test.rb000066400000000000000000000016041466130507100237720ustar00rootroot00000000000000# frozen_string_literal: true require "helper" require 'lint/authentication' # ruby -w -Itest test/cluster_commands_on_connection_test.rb # @see https://redis.io/commands#connection class TestClusterCommandsOnConnection < Minitest::Test include Helper::Cluster include Lint::Authentication 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 def mock(*args, &block) redis_cluster_mock(*args, &block) end end redis-rb-5.3.0/cluster/test/commands_on_geo_test.rb000066400000000000000000000037411466130507100224110ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # ruby -w -Itest test/cluster_commands_on_geo_test.rb # @see https://redis.io/commands#geo class TestClusterCommandsOnGeo < Minitest::Test include Helper::Cluster def add_sicily redis.geoadd('Sicily', 13.361389, 38.115556, 'Palermo', 15.087269, 37.502669, 'Catania') end def test_geoadd assert_equal 2, add_sicily end def test_geohash add_sicily assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania]) end def test_geopos 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 def test_geodist 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 def test_georadius 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 def test_georadiusbymember redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento') add_sicily assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km') end end redis-rb-5.3.0/cluster/test/commands_on_hashes_test.rb000066400000000000000000000003701466130507100231050ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/cluster/test/commands_on_hyper_log_log_test.rb000066400000000000000000000005471466130507100244710ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/cluster/test/commands_on_keys_test.rb000066400000000000000000000074121466130507100226110ustar00rootroot00000000000000# frozen_string_literal: true require "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_equal 2, redis.del('key1', 'key2') 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') 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 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 def test_unlink 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 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 def test_scan_each require 'securerandom' 1000.times do |n| redis.set("test-#{::SecureRandom.uuid}", n) end 1000.times do |n| redis.set("random-#{::SecureRandom.uuid}", n) end keys_result = redis.keys('test-*') scan_result = redis.scan_each(match: 'test-*').to_a assert_equal(keys_result.size, 1000) assert_equal(scan_result.size, 1000) assert_equal(scan_result.sort, keys_result.sort) end end redis-rb-5.3.0/cluster/test/commands_on_lists_test.rb000066400000000000000000000006611466130507100227730ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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_lmove target_version "6.2" do assert_raises(Redis::CommandError) { super } end end def test_rpoplpush assert_raises(Redis::CommandError) { super } end end redis-rb-5.3.0/cluster/test/commands_on_pub_sub_test.rb000066400000000000000000000054641466130507100233020ustar00rootroot00000000000000# frozen_string_literal: true require "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 = {} thread = Thread.new do redis.subscribe('channel1', 'channel2') do |on| on.subscribe { sub_cnt += 1 } on.message do |c, msg| messages[c] = msg redis.unsubscribe if messages.size == 2 end end end Thread.pass until sub_cnt == 2 publisher = build_another_client assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'channel*') assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) publisher.publish('channel1', 'one') publisher.publish('channel2', 'two') publisher.publish('channel3', 'three') thread.join assert_equal(2, messages.size) assert_equal('one', messages['channel1']) assert_equal('two', messages['channel2']) end def test_publish_psubscribe_punsubscribe_pubsub sub_cnt = 0 messages = {} thread = Thread.new do redis.psubscribe('guc*', 'her*') do |on| on.psubscribe { sub_cnt += 1 } on.pmessage do |_ptn, c, msg| messages[c] = msg redis.punsubscribe if messages.size == 2 end end end Thread.pass until sub_cnt == 2 publisher = build_another_client assert_equal 2, publisher.pubsub(:numpat) publisher.publish('burberry1', 'one') publisher.publish('gucci2', 'two') publisher.publish('hermes3', 'three') thread.join assert_equal(2, messages.size) assert_equal('two', messages['gucci2']) assert_equal('three', messages['hermes3']) end def test_spublish_ssubscribe_sunsubscribe_pubsub omit_version('7.0.0') sub_cnt = 0 messages = {} thread = Thread.new do redis.ssubscribe('channel1', 'channel2') do |on| on.ssubscribe { sub_cnt += 1 } on.smessage do |c, msg| messages[c] = msg redis.sunsubscribe if messages.size == 2 end end end Thread.pass until sub_cnt == 2 publisher = build_another_client assert_equal %w[channel1 channel2], publisher.pubsub(:shardchannels, 'channel*') assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, publisher.pubsub(:shardnumsub, 'channel1', 'channel2', 'channel3')) publisher.spublish('channel1', 'one') publisher.spublish('channel2', 'two') publisher.spublish('channel3', 'three') thread.join assert_equal(2, messages.size) assert_equal('one', messages['channel1']) assert_equal('two', messages['channel2']) end end redis-rb-5.3.0/cluster/test/commands_on_scripting_test.rb000066400000000000000000000030331466130507100236330ustar00rootroot00000000000000# frozen_string_literal: true require "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 assert_equal 'OK', redis.script(:debug, 'yes') assert_equal 'OK', redis.script(:debug, 'no') 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-5.3.0/cluster/test/commands_on_server_test.rb000066400000000000000000000117421466130507100231450ustar00rootroot00000000000000# frozen_string_literal: true require "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| err = assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do redis.bgsave end assert_includes err.message, err_msg assert_kind_of Redis::CommandError, err.errors.values.first 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 assert_instance_of Hash, a_client_info assert_includes a_client_info, 'addr' 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 assert_equal 'OK', redis.client(:reply, 'ON') 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], ] assert_equal(expected, redis.command(:info, :get, :set).map { |c| c.first(6) }) end def test_config_get assert_equal ['hash-max-ziplist-entries'], redis.config(:get, 'hash-max-ziplist-entrie*').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 assert_instance_of String, redis.memory(:doctor) end def test_memory_help assert_instance_of Array, redis.memory(:help) end def test_memory_malloc_stats assert_instance_of String, redis.memory('malloc-stats') end def test_memory_purge assert_equal 'OK', redis.memory(:purge) end def test_memory_stats assert_instance_of Array, redis.memory(:stats) end def test_memory_usage redis.set('key1', 'Hello World') assert_operator redis.memory(:usage, 'key1'), :>, 0 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-5.3.0/cluster/test/commands_on_sets_test.rb000066400000000000000000000014031466130507100226060ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/cluster/test/commands_on_sorted_sets_test.rb000066400000000000000000000027671466130507100242040ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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_zrangestore assert_raises(Redis::CommandError) { super } end def test_zinter assert_raises(Redis::CommandError) { super } end def test_zinter_with_aggregate assert_raises(Redis::CommandError) { super } end def test_zinter_with_weights assert_raises(Redis::CommandError) { super } end 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_zunion assert_raises(Redis::CommandError) { super } end def test_zunion_with_aggregate assert_raises(Redis::CommandError) { super } end def test_zunion_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 def test_zdiff assert_raises(Redis::CommandError) { super } end def test_zdiffstore assert_raises(Redis::CommandError) { super } end end redis-rb-5.3.0/cluster/test/commands_on_streams_test.rb000066400000000000000000000034061466130507100233130ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/cluster/test/commands_on_strings_test.rb000066400000000000000000000005041466130507100233220ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/cluster/test/commands_on_transactions_test.rb000066400000000000000000000035651466130507100243530ustar00rootroot00000000000000# frozen_string_literal: true require "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(LocalJumpError) do redis.multi end assert_empty(redis.multi {}) assert_equal([1], redis.multi { |r| r.incr('counter') }) end def test_unwatch assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.unwatch end end def test_watch assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.watch('{key}1', '{key}2') end assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.watch('{key}1', '{key}2') {} end assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.watch('{key}1', '{key}2') do |cli| cli.watch('{key}3') end end assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.watch('key1', 'key2') do |cli| cli.multi do |tx| tx.set('key1', '1') tx.set('key2', '2') end end end assert_raises(Redis::Cluster::TransactionConsistencyError) do redis.watch('{hey}1', '{hey}2') do |cli| cli.multi do |tx| tx.set('{key}1', '1') tx.set('{key}2', '2') end end end assert_equal('hello', redis.watch('{key}1', '{key}2') { |_| 'hello' }) redis.watch('{key}1', '{key}2') do |cli| cli.multi do |tx| tx.set('{key}1', '1') tx.set('{key}2', '2') end end assert_equal %w[1 2], redis.mget('{key}1', '{key}2') end end redis-rb-5.3.0/cluster/test/commands_on_value_types_test.rb000066400000000000000000000005571466130507100242010ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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 def test_copy assert_raises(Redis::CommandError) { super } end end redis-rb-5.3.0/cluster/test/helper.rb000066400000000000000000000074261466130507100175060ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../../test/helper" $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require "redis-clustering" require_relative 'support/orchestrator' module Helper module Cluster include Generic DEFAULT_HOST = '127.0.0.1' DEFAULT_PORTS = (16_380..16_385).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).transform_keys { |k| k.to_s.downcase } else {} end commands[:cluster] = lambda { |subcommand, *args| subcommand = subcommand.downcase if cluster_subcommands.key?(subcommand) cluster_subcommands[subcommand].call(*args) else case subcommand.downcase 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(nodes: 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], nodes: _default_nodes }.merge(options) end def _new_client(options = {}) Redis::Cluster.new(_format_options(options).merge(driver: ENV['DRIVER'])) end end end redis-rb-5.3.0/cluster/test/support/000077500000000000000000000000001466130507100174055ustar00rootroot00000000000000redis-rb-5.3.0/cluster/test/support/orchestrator.rb000066400000000000000000000172111466130507100224530ustar00rootroot00000000000000# frozen_string_literal: true require '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: [0, 0.5, 1, 1.5]) 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| 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 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| c.flushall(async: true) rescue Redis::CommandError # READONLY You can't write against a read only slave. nil 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| c.cluster('set-config-epoch', i + 1) rescue Redis::CommandError # ERR Node config epoch is already non-zero nil 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: 60) 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: 60) 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: 60) 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: 60) 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: 60) key = 0 wait_for_state(clients, max_attempts) do |client| 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 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-5.3.0/examples/000077500000000000000000000000001466130507100150475ustar00rootroot00000000000000redis-rb-5.3.0/examples/basic.rb000066400000000000000000000002341466130507100164540ustar00rootroot00000000000000# 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-5.3.0/examples/consistency.rb000066400000000000000000000072771466130507100177520ustar00rootroot00000000000000# 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)}" 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-5.3.0/examples/dist_redis.rb000066400000000000000000000015341466130507100175300ustar00rootroot00000000000000# 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-5.3.0/examples/incr-decr.rb000066400000000000000000000003451466130507100172440ustar00rootroot00000000000000# 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-5.3.0/examples/list.rb000066400000000000000000000007321466130507100163510ustar00rootroot00000000000000# 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-5.3.0/examples/pubsub.rb000066400000000000000000000015351466130507100167000ustar00rootroot00000000000000# 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-5.3.0/examples/sentinel.rb000066400000000000000000000020051466130507100172120ustar00rootroot00000000000000# 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-5.3.0/examples/sentinel/000077500000000000000000000000001466130507100166705ustar00rootroot00000000000000redis-rb-5.3.0/examples/sentinel/sentinel.conf000066400000000000000000000005051466130507100213600ustar00rootroot00000000000000sentinel 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-5.3.0/examples/sentinel/start000077500000000000000000000025201466130507100177520ustar00rootroot00000000000000#! /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| Process.kill(:INT, pid) rescue Errno::ESRCH 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-5.3.0/examples/sets.rb000066400000000000000000000010261466130507100163510ustar00rootroot00000000000000# 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-5.3.0/examples/unicorn/000077500000000000000000000000001466130507100165245ustar00rootroot00000000000000redis-rb-5.3.0/examples/unicorn/config.ru000066400000000000000000000001721466130507100203410ustar00rootroot00000000000000# frozen_string_literal: true run lambda { |_env| [200, { "Content-Type" => "text/plain" }, [MyApp.redis.randomkey]] } redis-rb-5.3.0/examples/unicorn/unicorn.rb000066400000000000000000000010561466130507100205300ustar00rootroot00000000000000# 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| MyApp.redis.disconnect! end redis-rb-5.3.0/lib/000077500000000000000000000000001466130507100137775ustar00rootroot00000000000000redis-rb-5.3.0/lib/redis.rb000066400000000000000000000125261466130507100154400ustar00rootroot00000000000000# frozen_string_literal: true require "monitor" require "redis/errors" require "redis/commands" class Redis BASE_PATH = __dir__ Deprecated = Class.new(StandardError) class << self attr_accessor :silence_deprecations, :raise_deprecations def deprecate!(message) unless silence_deprecations if raise_deprecations raise Deprecated, message else ::Kernel.warn(message) end end end end # soft-deprecated # We added this back for older sidekiq releases module Connection class << self def drivers [RedisClient.default_driver] end end end include Commands SERVER_URL_OPTIONS = %i(url host port path).freeze # 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 (1.0) timeout in seconds # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds # @option options [String] :username Username to authenticate against server # @option options [String] :password Password to authenticate against server # @option options [Integer] :db (0) Database to select after connect and on reconnects # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis` # @option options [String] :id ID for the client connection, assigns name to current connection by sending # `CLIENT SETNAME` # @option options [Integer, Array] :reconnect_attempts Number of attempts trying to connect, # or a list of sleep duration between attempts. # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not # @option options [String] :name The name of the server group to connect to. # @option options [Array] :sentinels List of sentinels to contact # # @return [Redis] a new client instance def initialize(options = {}) @monitor = Monitor.new @options = options.dup @options[:reconnect_attempts] = 1 unless @options.key?(:reconnect_attempts) if ENV["REDIS_URL"] && SERVER_URL_OPTIONS.none? { |o| @options.key?(o) } @options[:url] = ENV["REDIS_URL"] end inherit_socket = @options.delete(:inherit_socket) @subscription_client = nil @client = initialize_client(@options) @client.inherit_socket! if inherit_socket end # Run code without the client reconnecting def without_reconnect(&block) @client.disable_reconnection(&block) end # Test whether or not the client is connected def connected? @client.connected? || @subscription_client&.connected? end # Disconnect the client as quickly and silently as possible. def close @client.close @subscription_client&.close end alias disconnect! close def with yield self end def _client @client end def pipelined(exception: true) synchronize do |client| client.pipelined(exception: exception) do |raw_pipeline| yield PipelinedConnection.new(raw_pipeline, exception: exception) end end end def id @client.id || @client.server_url end def inspect "#" end def dup self.class.new(@options) end def connection { host: @client.host, port: @client.port, db: @client.db, id: id, location: "#{@client.host}:#{@client.port}" } end private def initialize_client(options) if options.key?(:cluster) raise "Redis Cluster support was moved to the `redis-clustering` gem." end if options.key?(:sentinels) Client.sentinel(**options).new_client else Client.config(**options).new_client end end def synchronize @monitor.synchronize { yield(@client) } end def send_command(command, &block) @monitor.synchronize do @client.call_v(command, &block) end rescue ::RedisClient::Error => error Client.translate_error!(error) end def send_blocking_command(command, timeout, &block) @monitor.synchronize do @client.blocking_call_v(timeout, command, &block) end end def _subscription(method, timeout, channels, block) if block if @subscription_client raise SubscriptionError, "This client is already subscribed" end begin @subscription_client = SubscribedClient.new(@client.pubsub) if timeout > 0 @subscription_client.send(method, timeout, *channels, &block) else @subscription_client.send(method, *channels, &block) end ensure @subscription_client&.close @subscription_client = nil end else unless @subscription_client raise SubscriptionError, "This client is not subscribed" end @subscription_client.call_v([method].concat(channels)) end end end require "redis/version" require "redis/client" require "redis/pipeline" require "redis/subscribe" redis-rb-5.3.0/lib/redis/000077500000000000000000000000001466130507100151055ustar00rootroot00000000000000redis-rb-5.3.0/lib/redis/client.rb000066400000000000000000000057271466130507100167230ustar00rootroot00000000000000# frozen_string_literal: true require 'redis-client' class Redis class Client < ::RedisClient ERROR_MAPPING = { RedisClient::ConnectionError => Redis::ConnectionError, RedisClient::CommandError => Redis::CommandError, RedisClient::ReadTimeoutError => Redis::TimeoutError, RedisClient::CannotConnectError => Redis::CannotConnectError, RedisClient::AuthenticationError => Redis::CannotConnectError, RedisClient::FailoverError => Redis::CannotConnectError, RedisClient::PermissionError => Redis::PermissionError, RedisClient::WrongTypeError => Redis::WrongTypeError, RedisClient::ReadOnlyError => Redis::ReadOnlyError, RedisClient::ProtocolError => Redis::ProtocolError, RedisClient::OutOfMemoryError => Redis::OutOfMemoryError, } class << self def config(**kwargs) super(protocol: 2, **kwargs) end def sentinel(**kwargs) super(protocol: 2, **kwargs, client_implementation: ::RedisClient) end def translate_error!(error, mapping: ERROR_MAPPING) redis_error = translate_error_class(error.class, mapping: mapping) raise redis_error, error.message, error.backtrace end private def translate_error_class(error_class, mapping: ERROR_MAPPING) mapping.fetch(error_class) rescue IndexError if (client_error = error_class.ancestors.find { |a| mapping[a] }) mapping[error_class] = mapping[client_error] else raise end end end def id config.id end def server_url config.server_url end def timeout config.read_timeout end def db config.db end def host config.host unless config.path end def port config.port unless config.path end def path config.path end def username config.username end def password config.password end undef_method :call undef_method :call_once undef_method :call_once_v undef_method :blocking_call def call_v(command, &block) super(command, &block) rescue ::RedisClient::Error => error Client.translate_error!(error) end def blocking_call_v(timeout, command, &block) if timeout && timeout > 0 # Can't use the command timeout argument as the connection timeout # otherwise it would be very racy. So we add the regular read_timeout on top # to account for the network delay. timeout += config.read_timeout end super(timeout, command, &block) rescue ::RedisClient::Error => error Client.translate_error!(error) end def pipelined(exception: true) super rescue ::RedisClient::Error => error Client.translate_error!(error) end def multi(watch: nil) super rescue ::RedisClient::Error => error Client.translate_error!(error) end def inherit_socket! @inherit_socket = true end end end redis-rb-5.3.0/lib/redis/commands.rb000066400000000000000000000135251466130507100172410ustar00rootroot00000000000000# frozen_string_literal: true require "redis/commands/bitmaps" require "redis/commands/cluster" require "redis/commands/connection" require "redis/commands/geo" require "redis/commands/hashes" require "redis/commands/hyper_log_log" require "redis/commands/keys" require "redis/commands/lists" require "redis/commands/pubsub" require "redis/commands/scripting" require "redis/commands/server" require "redis/commands/sets" require "redis/commands/sorted_sets" require "redis/commands/streams" require "redis/commands/strings" require "redis/commands/transactions" class Redis module Commands include Bitmaps include Cluster include Connection include Geo include Hashes include HyperLogLog include Keys include Lists include Pubsub include Scripting include Server include Sets include SortedSets include Streams include Strings include Transactions # 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| value != 0 unless value.nil? } 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 } Pairify = lambda { |value| if value.respond_to?(:each_slice) value.each_slice(2).to_a 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 } HashifyStreamAutoclaim = lambda { |reply| { 'next' => reply[0], 'entries' => reply[1].compact.map do |entry, values| [entry, values.each_slice(2)&.to_h] end } } HashifyStreamAutoclaimJustId = lambda { |reply| { 'next' => reply[0], 'entries' => reply[1] } } 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 } # 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) send_command(command) 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 send_command([: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 private def method_missing(*command) # rubocop:disable Style/MissingRespondToMissing send_command(command) end end end redis-rb-5.3.0/lib/redis/commands/000077500000000000000000000000001466130507100167065ustar00rootroot00000000000000redis-rb-5.3.0/lib/redis/commands/bitmaps.rb000066400000000000000000000054301466130507100206740ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Bitmaps # 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) send_command([:setbit, key, offset, value]) 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) send_command([:getbit, key, offset]) 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 # @param [String, Symbol] scale the scale of the offset range # e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits # @return [Integer] the number of bits set to 1 def bitcount(key, start = 0, stop = -1, scale: nil) command = [:bitcount, key, start, stop] command << scale if scale send_command(command) 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) keys.flatten!(1) command = [:bitop, operation, destkey] command.concat(keys) send_command(command) 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 # @param [String, Symbol] scale the scale of the offset range # e.g. 'BYTE' - interpreted as a range of bytes, 'BIT' - interpreted as a range of bits # @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, scale: nil) raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start command = [:bitpos, key, bit] command << start if start command << stop if stop command << scale if scale send_command(command) end end end end redis-rb-5.3.0/lib/redis/commands/cluster.rb000066400000000000000000000014531466130507100207170ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Cluster # 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) send_command([:cluster, subcommand] + args) 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 send_command(%i[asking]) end end end end redis-rb-5.3.0/lib/redis/commands/connection.rb000066400000000000000000000023321466130507100213720ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Connection # Authenticate to the server. # # @param [Array] args includes both username and password # or only password # @return [String] `OK` # @see https://redis.io/commands/auth AUTH command def auth(*args) send_command([:auth, *args]) end # Ping the server. # # @param [optional, String] message # @return [String] `PONG` def ping(message = nil) send_command([:ping, message].compact) end # Echo the given string. # # @param [String] value # @return [String] def echo(value) send_command([:echo, value]) 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) send_command([:select, db]) end # Close the connection. # # @return [String] `OK` def quit synchronize do |client| client.call_v([:quit]) rescue ConnectionError ensure client.close end end end end end redis-rb-5.3.0/lib/redis/commands/geo.rb000066400000000000000000000066421466130507100200150ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Geo # 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) send_command([:geoadd, key, *member]) 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) send_command([:geohash, key, member]) 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) send_command([:georadius, *geoarguments]) 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) send_command([:georadiusbymember, *geoarguments]) 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) send_command([:geopos, key, member]) 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') send_command([:geodist, key, member1, member2, unit]) end private def _geoarguments(*args, options: nil, sort: nil, count: nil) args << sort if sort args << 'COUNT' << Integer(count) if count args << options if options args end end end end redis-rb-5.3.0/lib/redis/commands/hashes.rb000066400000000000000000000177321466130507100205200ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Hashes # Get the number of fields in a hash. # # @param [String] key # @return [Integer] number of fields in the hash def hlen(key) send_command([:hlen, key]) 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) send_command([:hset, key, *attrs]) 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) send_command([:hsetnx, key, field, value], &Boolify) 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) send_command([:hmset, key] + attrs) 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.flatten) end # Get the value of a hash field. # # @param [String] key # @param [String] field # @return [String] def hget(key, field) send_command([:hget, key, field]) 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) fields.flatten!(1) send_command([:hmget, key].concat(fields), &blk) 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) fields.flatten!(1) hmget(key, fields) do |reply| if reply.is_a?(Array) Hash[fields.zip(reply)] else reply end end end # Get one or more random fields from a hash. # # @example Get one random field # redis.hrandfield("hash") # # => "f1" # @example Get multiple random fields # redis.hrandfield("hash", 2) # # => ["f1, "f2"] # @example Get multiple random fields with values # redis.hrandfield("hash", 2, with_values: true) # # => [["f1", "s1"], ["f2", "s2"]] # # @param [String] key # @param [Integer] count # @param [Hash] options # - `:with_values => true`: include values in output # # @return [nil, String, Array, Array<[String, Float]>] # - when `key` does not exist, `nil` # - when `count` is not specified, a field name # - when `count` is specified and `:with_values` is not specified, an array of field names # - when `:with_values` is specified, an array with `[field, value]` pairs def hrandfield(key, count = nil, withvalues: false, with_values: withvalues) if with_values && count.nil? raise ArgumentError, "count argument must be specified" end args = [:hrandfield, key] args << count if count args << "WITHVALUES" if with_values parser = Pairify if with_values send_command(args, &parser) 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) fields.flatten!(1) send_command([:hdel, key].concat(fields)) 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) send_command([:hexists, key, field], &Boolify) 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) send_command([:hincrby, key, field, Integer(increment)]) 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) send_command([:hincrbyfloat, key, field, Float(increment)], &Floatify) end # Get all the fields in a hash. # # @param [String] key # @return [Array] def hkeys(key) send_command([:hkeys, key]) end # Get all the values in a hash. # # @param [String] key # @return [Array] def hvals(key) send_command([:hvals, key]) end # Get all the fields and values in a hash. # # @param [String] key # @return [Hash] def hgetall(key) send_command([:hgetall, key], &Hashify) 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 # # See the [Redis Server HSCAN documentation](https://redis.io/docs/latest/commands/hscan/) for further details 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 # # See the [Redis Server HSCAN documentation](https://redis.io/docs/latest/commands/hscan/) for further details 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 end end end redis-rb-5.3.0/lib/redis/commands/hyper_log_log.rb000066400000000000000000000024641466130507100220720ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module HyperLogLog # 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) send_command([:pfadd, key, member], &Boolify) 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) send_command([:pfcount] + keys.flatten(1)) 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) send_command([:pfmerge, dest_key, *source_key], &BoolifySet) end end end end redis-rb-5.3.0/lib/redis/commands/keys.rb000066400000000000000000000375101466130507100202140ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Keys # 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"]] # @example Retrieve a batch of keys of a certain type # redis.scan(92, :type => "zset") # # => ["173", ["sortedset:14", "sortedset:78"]] # # @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 # - `:type => String`: return keys only of the given type # # @return [String, Array] the next cursor and all found keys # # See the [Redis Server SCAN documentation](https://redis.io/docs/latest/commands/scan/) for further details 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 # @example Execute block for each key of a type # redis.scan_each(:type => "hash") {|key| puts redis.type(key)} # # => "hash" # # => "hash" # # @param [Hash] options # - `:match => String`: only return keys matching the pattern # - `:count => Integer`: return count keys at most per iteration # - `:type => String`: return keys only of the given type # # @return [Enumerator] an enumerator for all found keys # # See the [Redis Server SCAN documentation](https://redis.io/docs/latest/commands/scan/) for further details 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 # Remove the expiration from a key. # # @param [String] key # @return [Boolean] whether the timeout was removed or not def persist(key) send_command([:persist, key], &Boolify) end # Set a key's time to live in seconds. # # @param [String] key # @param [Integer] seconds time to live # @param [Hash] options # - `:nx => true`: Set expiry only when the key has no expiry. # - `:xx => true`: Set expiry only when the key has an existing expiry. # - `:gt => true`: Set expiry only when the new expiry is greater than current one. # - `:lt => true`: Set expiry only when the new expiry is less than current one. # @return [Boolean] whether the timeout was set or not def expire(key, seconds, nx: nil, xx: nil, gt: nil, lt: nil) args = [:expire, key, Integer(seconds)] args << "NX" if nx args << "XX" if xx args << "GT" if gt args << "LT" if lt send_command(args, &Boolify) 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 # @param [Hash] options # - `:nx => true`: Set expiry only when the key has no expiry. # - `:xx => true`: Set expiry only when the key has an existing expiry. # - `:gt => true`: Set expiry only when the new expiry is greater than current one. # - `:lt => true`: Set expiry only when the new expiry is less than current one. # @return [Boolean] whether the timeout was set or not def expireat(key, unix_time, nx: nil, xx: nil, gt: nil, lt: nil) args = [:expireat, key, Integer(unix_time)] args << "NX" if nx args << "XX" if xx args << "GT" if gt args << "LT" if lt send_command(args, &Boolify) end # Get a key's expiry time specified as number of seconds from UNIX Epoch # # @param [String] key # @return [Integer] expiry time specified as number of seconds from UNIX Epoch def expiretime(key) send_command([:expiretime, key]) 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) send_command([:ttl, key]) end # Set a key's time to live in milliseconds. # # @param [String] key # @param [Integer] milliseconds time to live # @param [Hash] options # - `:nx => true`: Set expiry only when the key has no expiry. # - `:xx => true`: Set expiry only when the key has an existing expiry. # - `:gt => true`: Set expiry only when the new expiry is greater than current one. # - `:lt => true`: Set expiry only when the new expiry is less than current one. # @return [Boolean] whether the timeout was set or not def pexpire(key, milliseconds, nx: nil, xx: nil, gt: nil, lt: nil) args = [:pexpire, key, Integer(milliseconds)] args << "NX" if nx args << "XX" if xx args << "GT" if gt args << "LT" if lt send_command(args, &Boolify) 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. # @param [Hash] options # - `:nx => true`: Set expiry only when the key has no expiry. # - `:xx => true`: Set expiry only when the key has an existing expiry. # - `:gt => true`: Set expiry only when the new expiry is greater than current one. # - `:lt => true`: Set expiry only when the new expiry is less than current one. # @return [Boolean] whether the timeout was set or not def pexpireat(key, ms_unix_time, nx: nil, xx: nil, gt: nil, lt: nil) args = [:pexpireat, key, Integer(ms_unix_time)] args << "NX" if nx args << "XX" if xx args << "GT" if gt args << "LT" if lt send_command(args, &Boolify) end # Get a key's expiry time specified as number of milliseconds from UNIX Epoch # # @param [String] key # @return [Integer] expiry time specified as number of milliseconds from UNIX Epoch def pexpiretime(key) send_command([:pexpiretime, key]) 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) send_command([:pttl, key]) end # Return a serialized version of the value stored at a key. # # @param [String] key # @return [String] serialized_value def dump(key) send_command([:dump, key]) 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 send_command(args) 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) send_command(args) end # Delete one or more keys. # # @param [String, Array] keys # @return [Integer] number of keys that were deleted def del(*keys) keys.flatten!(1) return 0 if keys.empty? send_command([:del] + keys) end # Unlink one or more keys. # # @param [String, Array] keys # @return [Integer] number of keys that were unlinked def unlink(*keys) send_command([:unlink] + keys) end # Determine how many of the keys exists. # # @param [String, Array] keys # @return [Integer] def exists(*keys) send_command([:exists, *keys]) end # Determine if any of the keys exists. # # @param [String, Array] keys # @return [Boolean] def exists?(*keys) send_command([:exists, *keys]) do |value| value > 0 end end # Find all keys matching the given pattern. # # @param [String] pattern # @return [Array] # # See the [Redis Server KEYS documentation](https://redis.io/docs/latest/commands/keys/) for further details def keys(pattern = "*") send_command([:keys, pattern]) do |reply| if reply.is_a?(String) reply.split(" ") else reply 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) send_command([:move, key, db], &Boolify) end # Copy a value from one key to another. # # @example Copy a value to another key # redis.set "foo", "value" # # => "OK" # redis.copy "foo", "bar" # # => true # redis.get "bar" # # => "value" # # @example Copy a value to a key in another database # redis.set "foo", "value" # # => "OK" # redis.copy "foo", "bar", db: 2 # # => true # redis.select 2 # # => "OK" # redis.get "bar" # # => "value" # # @param [String] source # @param [String] destination # @param [Integer] db # @param [Boolean] replace removes the `destination` key before copying value to it # @return [Boolean] whether the key was copied or not def copy(source, destination, db: nil, replace: false) command = [:copy, source, destination] command << "DB" << db if db command << "REPLACE" if replace send_command(command, &Boolify) end def object(*args) send_command([:object] + args) end # Return a random key from the keyspace. # # @return [String] def randomkey send_command([:randomkey]) 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) send_command([:rename, old_name, new_name]) 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) send_command([:renamenx, old_name, new_name], &Boolify) 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 send_command(args) do |reply| if get.size > 1 && !store reply.each_slice(get.size).to_a if reply else reply end end end # Determine the type stored at key. # # @param [String] key # @return [String] `string`, `list`, `set`, `zset`, `hash` or `none` def type(key) send_command([:type, key]) end private def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block) # SSCAN/ZSCAN/HSCAN already prepend the key to +args+. args << cursor args << "MATCH" << match if match args << "COUNT" << Integer(count) if count args << "TYPE" << type if type send_command([command] + args, &block) end end end end redis-rb-5.3.0/lib/redis/commands/lists.rb000066400000000000000000000312251466130507100203740ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Lists # Get the length of a list. # # @param [String] key # @return [Integer] def llen(key) send_command([:llen, key]) end # Remove the first/last element in a list, append/prepend it to another list and return it. # # @param [String] source source key # @param [String] destination destination key # @param [String, Symbol] where_source from where to remove the element from the source list # e.g. 'LEFT' - from head, 'RIGHT' - from tail # @param [String, Symbol] where_destination where to push the element to the source list # e.g. 'LEFT' - to head, 'RIGHT' - to tail # # @return [nil, String] the element, or nil when the source key does not exist # # @note This command comes in place of the now deprecated RPOPLPUSH. # Doing LMOVE RIGHT LEFT is equivalent. def lmove(source, destination, where_source, where_destination) where_source, where_destination = _normalize_move_wheres(where_source, where_destination) send_command([:lmove, source, destination, where_source, where_destination]) end # Remove the first/last element in a list and append/prepend it # to another list and return it, or block until one is available. # # @example With timeout # element = redis.blmove("foo", "bar", "LEFT", "RIGHT", timeout: 5) # # => nil on timeout # # => "element" on success # @example Without timeout # element = redis.blmove("foo", "bar", "LEFT", "RIGHT") # # => "element" # # @param [String] source source key # @param [String] destination destination key # @param [String, Symbol] where_source from where to remove the element from the source list # e.g. 'LEFT' - from head, 'RIGHT' - from tail # @param [String, Symbol] where_destination where to push the element to the source list # e.g. 'LEFT' - to head, 'RIGHT' - to tail # @param [Hash] options # - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout # # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired # def blmove(source, destination, where_source, where_destination, timeout: 0) where_source, where_destination = _normalize_move_wheres(where_source, where_destination) command = [:blmove, source, destination, where_source, where_destination, timeout] send_blocking_command(command, timeout) 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) send_command([:lpush, key, value]) 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) send_command([:lpushx, key, value]) 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) send_command([:rpush, key, value]) 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) send_command([:rpushx, key, value]) end # Remove and get the first elements in a list. # # @param [String] key # @param [Integer] count number of elements to remove # @return [nil, String, Array] the values of the first elements def lpop(key, count = nil) command = [:lpop, key] command << Integer(count) if count send_command(command) end # Remove and get the last elements in a list. # # @param [String] key # @param [Integer] count number of elements to remove # @return [nil, String, Array] the values of the last elements def rpop(key, count = nil) command = [:rpop, key] command << Integer(count) if count send_command(command) 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) send_command([:rpoplpush, source, destination]) 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 => [Float, 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 => [Float, 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 => [Float, 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, timeout: 0) command = [:brpoplpush, source, destination, timeout] send_blocking_command(command, timeout) end # Pops one or more elements from the first non-empty list key from the list # of provided key names. If lists are empty, blocks until timeout has passed. # # @example Popping a element # redis.blmpop(1.0, 'list') # #=> ['list', ['a']] # @example With count option # redis.blmpop(1.0, 'list', count: 2) # #=> ['list', ['a', 'b']] # # @params timeout [Float] a float value specifying the maximum number of seconds to block) elapses. # A timeout of zero can be used to block indefinitely. # @params key [String, Array] one or more keys with lists # @params modifier [String] # - when `"LEFT"` - the elements popped are those from the left of the list # - when `"RIGHT"` - the elements popped are those from the right of the list # @params count [Integer] a number of elements to pop # # @return [Array>] list of popped elements or nil def blmpop(timeout, *keys, modifier: "LEFT", count: nil) raise ArgumentError, "Pick either LEFT or RIGHT" unless modifier == "LEFT" || modifier == "RIGHT" args = [:lmpop, keys.size, *keys, modifier] args << "COUNT" << Integer(count) if count send_blocking_command(args, timeout) end # Pops one or more elements from the first non-empty list key from the list # of provided key names. # # @example Popping a element # redis.lmpop('list') # #=> ['list', ['a']] # @example With count option # redis.lmpop('list', count: 2) # #=> ['list', ['a', 'b']] # # @params key [String, Array] one or more keys with lists # @params modifier [String] # - when `"LEFT"` - the elements popped are those from the left of the list # - when `"RIGHT"` - the elements popped are those from the right of the list # @params count [Integer] a number of elements to pop # # @return [Array>] list of popped elements or nil def lmpop(*keys, modifier: "LEFT", count: nil) raise ArgumentError, "Pick either LEFT or RIGHT" unless modifier == "LEFT" || modifier == "RIGHT" args = [:lmpop, keys.size, *keys, modifier] args << "COUNT" << Integer(count) if count send_command(args) end # Get an element from a list by its index. # # @param [String] key # @param [Integer] index # @return [String] def lindex(key, index) send_command([:lindex, key, Integer(index)]) 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) send_command([:linsert, key, where, pivot, value]) 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) send_command([:lrange, key, Integer(start), Integer(stop)]) 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) send_command([:lrem, key, Integer(count), value]) 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) send_command([:lset, key, Integer(index), value]) 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) send_command([:ltrim, key, Integer(start), Integer(stop)]) end private def _bpop(cmd, args, &blk) timeout = if args.last.is_a?(Hash) options = args.pop options[:timeout] end timeout ||= 0 unless timeout.is_a?(Integer) || timeout.is_a?(Float) raise ArgumentError, "timeout must be an Integer or Float, got: #{timeout.class}" end args.flatten!(1) command = [cmd].concat(args) command << timeout send_blocking_command(command, timeout, &blk) end def _normalize_move_wheres(where_source, where_destination) where_source = where_source.to_s.upcase where_destination = where_destination.to_s.upcase if where_source != "LEFT" && where_source != "RIGHT" raise ArgumentError, "where_source must be 'LEFT' or 'RIGHT'" end if where_destination != "LEFT" && where_destination != "RIGHT" raise ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'" end [where_source, where_destination] end end end end redis-rb-5.3.0/lib/redis/commands/pubsub.rb000066400000000000000000000056621466130507100205440ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Pubsub # Post a message to a channel. def publish(channel, message) send_command([:publish, channel, message]) end def subscribed? !@subscription_client.nil? end # Listen for messages published to the given channels. def subscribe(*channels, &block) _subscription(:subscribe, 0, channels, block) 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) _subscription(:subscribe_with_timeout, timeout, channels, block) end # Stop listening for messages posted to the given channels. def unsubscribe(*channels) _subscription(:unsubscribe, 0, channels, nil) end # Listen for messages published to channels matching the given patterns. # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/) # for further details def psubscribe(*channels, &block) _subscription(:psubscribe, 0, channels, block) end # Listen for messages published to channels matching the given patterns. # Throw a timeout error if there is no messages for a timeout period. # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/) # for further details def psubscribe_with_timeout(timeout, *channels, &block) _subscription(:psubscribe_with_timeout, timeout, channels, block) end # Stop listening for messages posted to channels matching the given patterns. # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/) # for further details def punsubscribe(*channels) _subscription(:punsubscribe, 0, channels, nil) end # Inspect the state of the Pub/Sub subsystem. # Possible subcommands: channels, numsub, numpat. def pubsub(subcommand, *args) send_command([:pubsub, subcommand] + args) end # Post a message to a channel in a shard. def spublish(channel, message) send_command([:spublish, channel, message]) end # Listen for messages published to the given channels in a shard. def ssubscribe(*channels, &block) _subscription(:ssubscribe, 0, channels, block) end # Listen for messages published to the given channels in a shard. # Throw a timeout error if there is no messages for a timeout period. def ssubscribe_with_timeout(timeout, *channels, &block) _subscription(:ssubscribe_with_timeout, timeout, channels, block) end # Stop listening for messages posted to the given channels in a shard. def sunsubscribe(*channels) _subscription(:sunsubscribe, 0, channels, nil) end end end end redis-rb-5.3.0/lib/redis/commands/scripting.rb000066400000000000000000000073471466130507100212500ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Scripting # 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" arg = args.first send_command([:script, :exists, arg]) do |reply| reply = reply.map { |r| Boolify.call(r) } if arg.is_a?(Array) reply else reply.first end end else send_command([:script, subcommand] + args) 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 private 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] || [] send_command([cmd, script, keys.length] + keys + argv) end end end end redis-rb-5.3.0/lib/redis/commands/server.rb000066400000000000000000000121321466130507100205400ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Server # Asynchronously rewrite the append-only file. # # @return [String] `OK` def bgrewriteaof send_command([:bgrewriteaof]) end # Asynchronously save the dataset to disk. # # @return [String] `OK` def bgsave send_command([:bgsave]) 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) send_command([:config, action] + args) do |reply| if reply.is_a?(Array) && action == :get Hashify.call(reply) else reply 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, *args) send_command([: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 # Return the number of keys in the selected database. # # @return [Integer] def dbsize send_command([:dbsize]) end # Remove all keys from all databases. # # @param [Hash] options # - `:async => Boolean`: async flush (default: false) # @return [String] `OK` def flushall(options = nil) if options && options[:async] send_command(%i[flushall async]) else send_command([:flushall]) 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) if options && options[:async] send_command(%i[flushdb async]) else send_command([:flushdb]) end end # Get information and statistics about the server. # # @param [String, Symbol] cmd e.g. "commandstats" # @return [Hash] def info(cmd = nil) send_command([: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 # Get the UNIX time stamp of the last successful save to disk. # # @return [Integer] def lastsave send_command([:lastsave]) 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 synchronize do |client| client = client.pubsub client.call_v([:monitor]) loop do yield client.next_event end end end # Synchronously save the dataset to disk. # # @return [String] def save send_command([:save]) end # Synchronously save the dataset to disk and then shut down the server. def shutdown synchronize do |client| client.disable_reconnection do client.call_v([:shutdown]) rescue ConnectionError # This means Redis has probably exited. nil end end end # Make the server a slave of another instance, or promote it as master. def slaveof(host, port) send_command([:slaveof, host, port]) 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) args = [:slowlog, subcommand] args << Integer(length) if length send_command(args) end # Internal command used for replication. def sync send_command([:sync]) 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 send_command([:time]) do |reply| reply&.map(&:to_i) end end def debug(*args) send_command([:debug] + args) end end end end redis-rb-5.3.0/lib/redis/commands/sets.rb000066400000000000000000000157341466130507100202230ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Sets # Get the number of members in a set. # # @param [String] key # @return [Integer] def scard(key) send_command([:scard, key]) end # Add one or more members to a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Integer] The number of members that were successfully added def sadd(key, *members) members.flatten!(1) send_command([:sadd, key].concat(members)) end # Add one or more members to a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Boolean] Wether at least one member was successfully added. def sadd?(key, *members) members.flatten!(1) send_command([:sadd, key].concat(members), &Boolify) end # Remove one or more members from a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Integer] The number of members that were successfully removed def srem(key, *members) members.flatten!(1) send_command([:srem, key].concat(members)) end # Remove one or more members from a set. # # @param [String] key # @param [String, Array] member one member, or array of members # @return [Boolean] Wether at least one member was successfully removed. def srem?(key, *members) members.flatten!(1) send_command([:srem, key].concat(members), &Boolify) 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) if count.nil? send_command([:spop, key]) else send_command([:spop, key, Integer(count)]) end end # Get one or more random members from a set. # # @param [String] key # @param [Integer] count # @return [String] def srandmember(key, count = nil) if count.nil? send_command([:srandmember, key]) else send_command([:srandmember, key, count]) 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) send_command([:smove, source, destination, member], &Boolify) end # Determine if a given value is a member of a set. # # @param [String] key # @param [String] member # @return [Boolean] def sismember(key, member) send_command([:sismember, key, member], &Boolify) end # Determine if multiple values are members of a set. # # @param [String] key # @param [String, Array] members # @return [Array] def smismember(key, *members) members.flatten!(1) send_command([:smismember, key].concat(members)) do |reply| reply.map(&Boolify) end end # Get all the members in a set. # # @param [String] key # @return [Array] def smembers(key) send_command([:smembers, key]) end # Subtract multiple sets. # # @param [String, Array] keys keys pointing to sets to subtract # @return [Array] members in the difference def sdiff(*keys) keys.flatten!(1) send_command([:sdiff].concat(keys)) 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) keys.flatten!(1) send_command([:sdiffstore, destination].concat(keys)) end # Intersect multiple sets. # # @param [String, Array] keys keys pointing to sets to intersect # @return [Array] members in the intersection def sinter(*keys) keys.flatten!(1) send_command([:sinter].concat(keys)) 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) keys.flatten!(1) send_command([:sinterstore, destination].concat(keys)) end # Add multiple sets. # # @param [String, Array] keys keys pointing to sets to unify # @return [Array] members in the union def sunion(*keys) keys.flatten!(1) send_command([:sunion].concat(keys)) 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) keys.flatten!(1) send_command([:sunionstore, destination].concat(keys)) 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 # # See the [Redis Server SSCAN documentation](https://redis.io/docs/latest/commands/sscan/) for further details 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 # # See the [Redis Server SSCAN documentation](https://redis.io/docs/latest/commands/sscan/) for further details 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 end end end redis-rb-5.3.0/lib/redis/commands/sorted_sets.rb000066400000000000000000001017271466130507100216010ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module SortedSets # Get the number of members in a sorted set. # # @example # redis.zcard("zset") # # => 4 # # @param [String] key # @return [Integer] def zcard(key) send_command([:zcard, key]) 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) # - `:lt => true`: Only update existing elements if the new score # is less than the current score # - `:gt => true`: Only update existing elements if the new score # is greater than the current score # - `: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, lt: nil, gt: nil, ch: nil, incr: nil) command = [:zadd, key] command << "NX" if nx command << "XX" if xx command << "LT" if lt command << "GT" if gt command << "CH" if ch command << "INCR" if incr if args.size == 1 && args[0].is_a?(Array) members_to_add = args[0] return 0 if members_to_add.empty? # Variadic: return float if INCR, integer if !INCR send_command(command + members_to_add, &(incr ? Floatify : nil)) elsif args.size == 2 # Single pair: return float if INCR, boolean if !INCR send_command(command + args, &(incr ? Floatify : Boolify)) else raise ArgumentError, "wrong number of arguments" 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) send_command([:zincrby, key, increment, member], &Floatify) 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) if member.is_a?(Array) members_to_remove = member return 0 if members_to_remove.empty? end send_command([: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 # 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) command = [:zpopmax, key] command << Integer(count) if count send_command(command) do |members| members = FloatifyPairs.call(members) 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) command = [:zpopmin, key] command << Integer(count) if count send_command(command) do |members| members = FloatifyPairs.call(members) count.to_i > 1 ? members : members.first end end # Removes and returns up to count members with scores in the sorted set stored at key. # # @example Popping a member # redis.bzmpop('zset') # #=> ['zset', ['a', 1.0]] # @example With count option # redis.bzmpop('zset', count: 2) # #=> ['zset', [['a', 1.0], ['b', 2.0]] # # @params timeout [Float] a float value specifying the maximum number of seconds to block) elapses. # A timeout of zero can be used to block indefinitely. # @params key [String, Array] one or more keys with sorted sets # @params modifier [String] # - when `"MIN"` - the elements popped are those with lowest scores # - when `"MAX"` - the elements popped are those with the highest scores # @params count [Integer] a number of members to pop # # @return [Array>] list of popped elements and scores def bzmpop(timeout, *keys, modifier: "MIN", count: nil) raise ArgumentError, "Pick either MIN or MAX" unless modifier == "MIN" || modifier == "MAX" args = [:bzmpop, timeout, keys.size, *keys, modifier] args << "COUNT" << Integer(count) if count send_blocking_command(args, timeout) do |response| response&.map do |entry| case entry when String then entry when Array then entry.map { |pair| FloatifyPairs.call(pair) }.flatten(1) end end end end # Removes and returns up to count members with scores in the sorted set stored at key. # # @example Popping a member # redis.zmpop('zset') # #=> ['zset', ['a', 1.0]] # @example With count option # redis.zmpop('zset', count: 2) # #=> ['zset', [['a', 1.0], ['b', 2.0]] # # @params key [String, Array] one or more keys with sorted sets # @params modifier [String] # - when `"MIN"` - the elements popped are those with lowest scores # - when `"MAX"` - the elements popped are those with the highest scores # @params count [Integer] a number of members to pop # # @return [Array>] list of popped elements and scores def zmpop(*keys, modifier: "MIN", count: nil) raise ArgumentError, "Pick either MIN or MAX" unless modifier == "MIN" || modifier == "MAX" args = [:zmpop, keys.size, *keys, modifier] args << "COUNT" << Integer(count) if count send_command(args) do |response| response&.map do |entry| case entry when String then entry when Array then entry.map { |pair| FloatifyPairs.call(pair) }.flatten(1) end end 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) send_command([:zscore, key, member], &Floatify) end # Get the scores associated with the given members in a sorted set. # # @example Get the scores for members "a" and "b" # redis.zmscore("zset", "a", "b") # # => [32.0, 48.0] # # @param [String] key # @param [String, Array] members # @return [Array] scores of the members def zmscore(key, *members) send_command([:zmscore, key, *members]) do |reply| reply.map(&Floatify) end end # Get one or more random members from a sorted set. # # @example Get one random member # redis.zrandmember("zset") # # => "a" # @example Get multiple random members # redis.zrandmember("zset", 2) # # => ["a", "b"] # @example Get multiple random members with scores # redis.zrandmember("zset", 2, with_scores: true) # # => [["a", 2.0], ["b", 3.0]] # # @param [String] key # @param [Integer] count # @param [Hash] options # - `:with_scores => true`: include scores in output # # @return [nil, String, Array, Array<[String, Float]>] # - when `key` does not exist or set is empty, `nil` # - when `count` is not specified, a member # - when `count` is specified and `:with_scores` is not specified, an array of members # - when `:with_scores` is specified, an array with `[member, score]` pairs def zrandmember(key, count = nil, withscores: false, with_scores: withscores) if with_scores && count.nil? raise ArgumentError, "count argument must be specified" end args = [:zrandmember, key] args << Integer(count) if count if with_scores args << "WITHSCORES" block = FloatifyPairs end send_command(args, &block) end # Return a range of members in a sorted set, by index, score or lexicographical ordering. # # @example Retrieve all members from a sorted set, by index # 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 # - `:by_score => false`: return members by score # - `:by_lex => false`: return members by lexicographical ordering # - `:rev => false`: reverse the ordering, from highest to lowest # - `:limit => [offset, count]`: skip `offset` members, return a maximum of # `count` members # - `: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, byscore: false, by_score: byscore, bylex: false, by_lex: bylex, rev: false, limit: nil, withscores: false, with_scores: withscores) if by_score && by_lex raise ArgumentError, "only one of :by_score or :by_lex can be specified" end args = [:zrange, key, start, stop] if by_score args << "BYSCORE" elsif by_lex args << "BYLEX" end args << "REV" if rev if limit args << "LIMIT" args.concat(limit.map { |l| Integer(l) }) end if with_scores args << "WITHSCORES" block = FloatifyPairs end send_command(args, &block) end # Select a range of members in a sorted set, by index, score or lexicographical ordering # and store the resulting sorted set in a new key. # # @example # redis.zadd("foo", [[1.0, "s1"], [2.0, "s2"], [3.0, "s3"]]) # redis.zrangestore("bar", "foo", 0, 1) # # => 2 # redis.zrange("bar", 0, -1) # # => ["s1", "s2"] # # @return [Integer] the number of elements in the resulting sorted set # @see #zrange def zrangestore(dest_key, src_key, start, stop, byscore: false, by_score: byscore, bylex: false, by_lex: bylex, rev: false, limit: nil) if by_score && by_lex raise ArgumentError, "only one of :by_score or :by_lex can be specified" end args = [:zrangestore, dest_key, src_key, start, stop] if by_score args << "BYSCORE" elsif by_lex args << "BYLEX" end args << "REV" if rev if limit args << "LIMIT" args.concat(limit.map { |l| Integer(l) }) end send_command(args) 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, Integer(start), Integer(stop)] if with_scores args << "WITHSCORES" block = FloatifyPairs end send_command(args, &block) end # Determine the index of a member in a sorted set. # # @param [String] key # @param [String] member # @return [Integer] def zrank(key, member) send_command([:zrank, key, member]) 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) send_command([:zrevrank, key, member]) 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) send_command([:zremrangebyrank, key, start, stop]) 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) send_command([:zlexcount, key, min, max]) 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.map { |l| Integer(l) }) end send_command(args) 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.map { |l| Integer(l) }) end send_command(args) 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.map { |l| Integer(l) }) end send_command(args, &block) 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.map { |l| Integer(l) }) end send_command(args, &block) 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) send_command([:zremrangebyscore, key, min, max]) 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) send_command([:zcount, key, min, max]) end # Return the intersection of multiple sorted sets # # @example Retrieve the intersection of `2*zsetA` and `1*zsetB` # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0]) # # => ["v1", "v2"] # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`, and their scores # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true) # # => [["v1", 3.0], ["v2", 6.0]] # # @param [String, Array] keys one or more keys to intersect # @param [Hash] options # - `:weights => [Float, Float, ...]`: weights to associate with source # sorted sets # - `:aggregate => String`: aggregate function to use (sum, min, max, ...) # - `: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 zinter(*args) _zsets_operation(:zinter, *args) end ruby2_keywords(:zinter) if respond_to?(:ruby2_keywords, true) # 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 => [Array]`: 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(*args) _zsets_operation_store(:zinterstore, *args) end ruby2_keywords(:zinterstore) if respond_to?(:ruby2_keywords, true) # Return the union of multiple sorted sets # # @example Retrieve the union of `2*zsetA` and `1*zsetB` # redis.zunion("zsetA", "zsetB", :weights => [2.0, 1.0]) # # => ["v1", "v2"] # @example Retrieve the union of `2*zsetA` and `1*zsetB`, and their scores # redis.zunion("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true) # # => [["v1", 3.0], ["v2", 6.0]] # # @param [String, Array] keys one or more keys to union # @param [Hash] options # - `:weights => [Array]`: weights to associate with source # sorted sets # - `:aggregate => String`: aggregate function to use (sum, min, max) # - `: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 zunion(*args) _zsets_operation(:zunion, *args) end ruby2_keywords(:zunion) if respond_to?(:ruby2_keywords, true) # 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(*args) _zsets_operation_store(:zunionstore, *args) end ruby2_keywords(:zunionstore) if respond_to?(:ruby2_keywords, true) # Return the difference between the first and all successive input sorted sets # # @example # redis.zadd("zsetA", [[1.0, "v1"], [2.0, "v2"]]) # redis.zadd("zsetB", [[3.0, "v2"], [2.0, "v3"]]) # redis.zdiff("zsetA", "zsetB") # => ["v1"] # @example With scores # redis.zadd("zsetA", [[1.0, "v1"], [2.0, "v2"]]) # redis.zadd("zsetB", [[3.0, "v2"], [2.0, "v3"]]) # redis.zdiff("zsetA", "zsetB", :with_scores => true) # => [["v1", 1.0]] # # @param [String, Array] keys one or more keys to compute the difference # @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 zdiff(*keys, with_scores: false) _zsets_operation(:zdiff, *keys, with_scores: with_scores) end # Compute the difference between the first and all successive input sorted sets # and store the resulting sorted set in a new key # # @example # redis.zadd("zsetA", [[1.0, "v1"], [2.0, "v2"]]) # redis.zadd("zsetB", [[3.0, "v2"], [2.0, "v3"]]) # redis.zdiffstore("zsetA", "zsetB") # # => 1 # # @param [String] destination destination key # @param [Array] keys source keys # @return [Integer] number of elements in the resulting sorted set def zdiffstore(*args) _zsets_operation_store(:zdiffstore, *args) end ruby2_keywords(:zdiffstore) if respond_to?(:ruby2_keywords, true) # 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 # # See the [Redis Server ZSCAN documentation](https://redis.io/docs/latest/commands/zscan/) for further details 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 # # See the [Redis Server ZSCAN documentation](https://redis.io/docs/latest/commands/zscan/) for further details 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 private def _zsets_operation(cmd, *keys, weights: nil, aggregate: nil, with_scores: false) keys.flatten!(1) command = [cmd, keys.size].concat(keys) if weights command << "WEIGHTS" command.concat(weights) end command << "AGGREGATE" << aggregate if aggregate if with_scores command << "WITHSCORES" block = FloatifyPairs end send_command(command, &block) end def _zsets_operation_store(cmd, destination, keys, weights: nil, aggregate: nil) keys.flatten!(1) command = [cmd, destination, keys.size].concat(keys) if weights command << "WEIGHTS" command.concat(weights) end command << "AGGREGATE" << aggregate if aggregate send_command(command) end end end end redis-rb-5.3.0/lib/redis/commands/streams.rb000066400000000000000000000440021466130507100207110ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Streams # 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 block = case subcommand.to_s.downcase when 'stream' then Hashify when 'groups', 'consumers' then proc { |r| r.map(&Hashify) } end send_command(args, &block) 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, nomkstream: 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 # @option opts [Boolean] :nomkstream whether to add NOMKSTREAM, default is not to add # # @return [String] the entry id def xadd(key, entry, approximate: nil, maxlen: nil, nomkstream: nil, id: '*') args = [:xadd, key] args << 'NOMKSTREAM' if nomkstream if maxlen args << "MAXLEN" args << "~" if approximate args << maxlen end args << id args.concat(entry.flatten) send_command(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) # @example With strategy # redis.xtrim('mystream', '1-0', strategy: 'MINID') # # @overload xtrim(key, maxlen, strategy: 'MAXLEN', approximate: true) # @param key [String] the stream key # @param maxlen [Integer] max length of entries # @param strategy [String] the limit strategy, must be MAXLEN # @param approximate [Boolean] whether to add `~` modifier of maxlen or not # @param limit [Integer] maximum count of entries to be evicted # @overload xtrim(key, minid, strategy: 'MINID', approximate: true) # @param key [String] the stream key # @param minid [String] minimum id of entries # @param strategy [String] the limit strategy, must be MINID # @param approximate [Boolean] whether to add `~` modifier of minid or not # @param limit [Integer] maximum count of entries to be evicted # # @return [Integer] the number of entries actually deleted def xtrim(key, len_or_id, strategy: 'MAXLEN', approximate: false, limit: nil) strategy = strategy.to_s.upcase args = [:xtrim, key, strategy] args << '~' if approximate args << len_or_id args.concat(['LIMIT', limit]) if limit send_command(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) send_command(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 send_command(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 send_command(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) send_command([: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 send_command(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) send_command(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 send_command(args, &blk) end # Transfers ownership of pending stream entries that match the specified criteria. # # @example Claim next pending message stuck > 5 minutes and mark as retry # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0') # @example Claim 50 next pending messages stuck > 5 minutes and mark as retry # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', count: 50) # @example Claim next pending message stuck > 5 minutes and don't mark as retry # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', justid: true) # @example Claim next pending message after this id stuck > 5 minutes and mark as retry # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '1641321233-0') # # @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 start [String] entry id to start scanning from or 0-0 for everything # @param count [Integer] number of messages to claim (default 1) # @param justid [Boolean] whether to fetch just an array of entry ids or not. # Does not increment retry count when true # # @return [Hash{String => Hash}] the entries successfully claimed # @return [Array] the entry ids successfully claimed if justid option is `true` def xautoclaim(key, group, consumer, min_idle_time, start, count: nil, justid: false) args = [:xautoclaim, key, group, consumer, min_idle_time, start] if count args << 'COUNT' << count.to_s end args << 'JUSTID' if justid blk = justid ? HashifyStreamAutoclaimJustId : HashifyStreamAutoclaim send_command(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 idle time options # redis.xpending('mystream', 'mygroup', '-', '+', 10, idle: 9000) # @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 # # @option opts [Integer] :idle pending message minimum idle time in milliseconds # # @return [Hash] the summary of pending entries # @return [Array] the pending entries details if options were specified def xpending(key, group, *args, idle: nil) command_args = [:xpending, key, group] command_args << 'IDLE' << Integer(idle) if idle 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 send_command(command_args, &blk) end private 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) if blocking_timeout_msec.nil? send_command(args, &HashifyStreams) elsif blocking_timeout_msec.to_f.zero? send_blocking_command(args, 0, &HashifyStreams) else send_blocking_command(args, blocking_timeout_msec.to_f / 1_000, &HashifyStreams) end end end end end redis-rb-5.3.0/lib/redis/commands/strings.rb000066400000000000000000000232431466130507100207300ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Strings # 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) send_command([:decr, key]) 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) send_command([:decrby, key, Integer(decrement)]) 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) send_command([:incr, key]) 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) send_command([:incrby, key, Integer(increment)]) 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) send_command([:incrbyfloat, key, Float(increment)], &Floatify) 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. # - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds. # - `:pxat => Integer` : Set the specified Unix time at which the key will expire, 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. # - `:get => true`: Return the old string stored at key, or nil if key did not exist. # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true` def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil) args = [:set, key, value.to_s] args << "EX" << Integer(ex) if ex args << "PX" << Integer(px) if px args << "EXAT" << Integer(exat) if exat args << "PXAT" << Integer(pxat) if pxat args << "NX" if nx args << "XX" if xx args << "KEEPTTL" if keepttl args << "GET" if get if nx || xx send_command(args, &BoolifySet) else send_command(args) 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) send_command([:setex, key, Integer(ttl), value.to_s]) 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) send_command([:psetex, key, Integer(ttl), value.to_s]) 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) send_command([:setnx, key, value.to_s], &Boolify) 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) send_command([:mset] + args) 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.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) send_command([:msetnx, *args], &Boolify) 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.flatten) end # Get the value of a key. # # @param [String] key # @return [String] def get(key) send_command([:get, key]) 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) keys.flatten!(1) send_command([:mget, *keys], &blk) 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) send_command([:setrange, key, Integer(offset), value.to_s]) 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) send_command([:getrange, key, Integer(start), Integer(stop)]) 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) send_command([:append, key, value]) 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) send_command([:getset, key, value.to_s]) end # Get the value of key and delete the key. This command is similar to GET, # except for the fact that it also deletes the key on success. # # @param [String] key # @return [String] the old value stored in the key, or `nil` if the key # did not exist def getdel(key) send_command([:getdel, key]) end # Get the value of key and optionally set its expiration. GETEX is similar to # GET, but is a write command with additional options. When no options are # provided, GETEX behaves like GET. # # @param [String] key # @param [Hash] options # - `:ex => Integer`: Set the specified expire time, in seconds. # - `:px => Integer`: Set the specified expire time, in milliseconds. # - `:exat => true`: Set the specified Unix time at which the key will # expire, in seconds. # - `:pxat => true`: Set the specified Unix time at which the key will # expire, in milliseconds. # - `:persist => true`: Remove the time to live associated with the key. # @return [String] The value of key, or nil when key does not exist. def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false) args = [:getex, key] args << "EX" << Integer(ex) if ex args << "PX" << Integer(px) if px args << "EXAT" << Integer(exat) if exat args << "PXAT" << Integer(pxat) if pxat args << "PERSIST" if persist send_command(args) 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) send_command([:strlen, key]) end end end end redis-rb-5.3.0/lib/redis/commands/transactions.rb000066400000000000000000000057351466130507100217550ustar00rootroot00000000000000# frozen_string_literal: true class Redis module Commands module Transactions # Mark the start of a transaction block. # # @example With a block # redis.multi do |multi| # multi.set("key", "value") # multi.incr("counter") # end # => ["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 [Array<...>] # - an array with replies # # @see #watch # @see #unwatch def multi synchronize do |client| client.multi do |raw_transaction| yield MultiConnection.new(raw_transaction) end 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_v([: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 send_command([:unwatch]) 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 send_command([:exec]) end # Discard all commands issued after MULTI. # # @return [String] `"OK"` # # @see #multi # @see #exec def discard send_command([:discard]) end end end end redis-rb-5.3.0/lib/redis/distributed.rb000066400000000000000000000732131466130507100177620ustar00rootroot00000000000000# frozen_string_literal: true require "redis/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.map(&: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) options.delete(:tag) options.delete(:ring) @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 def close on_each_node :close 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, **kwargs) node_for(key).expire(key, seconds, **kwargs) end # Set the expiration for a key as a UNIX timestamp. def expireat(key, unix_time, **kwargs) node_for(key).expireat(key, unix_time, **kwargs) end # Get the expiration for a key as a UNIX timestamp. def expiretime(key) node_for(key).expiretime(key) 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, **kwarg) node_for(key).pexpire(key, milliseconds, **kwarg) end # Set the expiration for a key as number of milliseconds from UNIX Epoch. def pexpireat(key, ms_unix_time, **kwarg) node_for(key).pexpireat(key, ms_unix_time, **kwarg) end # Get the expiration for a key as number of milliseconds from UNIX Epoch. def pexpiretime(key) node_for(key).pexpiretime(key) 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) args.flatten!(1) 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) args.flatten!(1) 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) args.flatten!(1) 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 # Determine if any of the keys exists. def exists?(*args) args.flatten!(1) 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 # Copy a value from one key to another. def copy(source, destination, **options) ensure_same_node(:copy, [source, destination]) do |node| node.copy(source, destination, **options) end 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(*) 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(*) 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 value of a key and delete it. def getdel(key) node_for(key).getdel(key) end # Get the value of a key and sets its time to live based on options. def getex(key, **options) node_for(key).getex(key, **options) end # Get the values of all the given keys as an Array. def mget(*keys) keys.flatten!(1) mapped_mget(*keys).values_at(*keys) end # Get the values of all the given keys as a Hash. def mapped_mget(*keys) keys.flatten!(1) 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, scale: nil) node_for(key).bitcount(key, start, stop, scale: scale) end # Perform a bitwise operation between strings and store the resulting string in a key. def bitop(operation, destkey, *keys) keys.flatten!(1) 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, scale: nil) node_for(key).bitpos(key, bit, start, stop, scale: scale) 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 # Remove the first/last element in a list, append/prepend it to another list and return it. def lmove(source, destination, where_source, where_destination) ensure_same_node(:lmove, [source, destination]) do |node| node.lmove(source, destination, where_source, where_destination) end end # Remove the first/last element in a list and append/prepend it # to another list and return it, or block until one is available. def blmove(source, destination, where_source, where_destination, timeout: 0) ensure_same_node(:lmove, [source, destination]) do |node| node.blmove(source, destination, where_source, where_destination, timeout: timeout) end 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 elements in a list. def lpop(key, count = nil) node_for(key).lpop(key, count) end # Remove and get the last elements in a list. def rpop(key, count = nil) node_for(key).rpop(key, count) 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] end args.flatten!(1) ensure_same_node(cmd, args) do |node| if timeout node.__send__(cmd, args, timeout: timeout) else node.__send__(cmd, args) 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 def bzpopmax(*args) _bpop(:bzpopmax, args) do |reply| reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply end end def bzpopmin(*args) _bpop(:bzpopmin, args) do |reply| reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply end 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, **options) ensure_same_node(:brpoplpush, [source, destination]) do |node| node.brpoplpush(source, destination, **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 # Iterate over keys, blocking and removing elements from the first non empty liist found. def blmpop(timeout, *keys, modifier: "LEFT", count: nil) ensure_same_node(:blmpop, keys) do |node| node.blmpop(timeout, *keys, modifier: modifier, count: count) end end # Iterate over keys, removing elements from the first non list found. def lmpop(*keys, modifier: "LEFT", count: nil) ensure_same_node(:lmpop, keys) do |node| node.lmpop(*keys, modifier: modifier, count: count) end 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, *members) node_for(key).sadd(key, *members) end # Add one or more members to a set. def sadd?(key, *members) node_for(key).sadd?(key, *members) end # Remove one or more members from a set. def srem(key, *members) node_for(key).srem(key, *members) end # Remove one or more members from a set. def srem?(key, *members) node_for(key).srem?(key, *members) 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 # Determine if multiple values are members of a set. def smismember(key, *members) node_for(key).smismember(key, *members) 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) keys.flatten!(1) 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) keys.flatten!(1) ensure_same_node(:sdiffstore, [destination].concat(keys)) do |node| node.sdiffstore(destination, keys) end end # Intersect multiple sets. def sinter(*keys) keys.flatten!(1) 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) keys.flatten!(1) ensure_same_node(:sinterstore, [destination].concat(keys)) do |node| node.sinterstore(destination, keys) end end # Add multiple sets. def sunion(*keys) keys.flatten!(1) 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) keys.flatten!(1) ensure_same_node(:sunionstore, [destination].concat(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 # Get one or more random members from a sorted set. def zrandmember(key, count = nil, **options) node_for(key).zrandmember(key, count, **options) end # Get the scores associated with the given members in a sorted set. def zmscore(key, *members) node_for(key).zmscore(key, *members) end # Iterate over keys, blocking and removing members from the first non empty sorted set found. def bzmpop(timeout, *keys, modifier: "MIN", count: nil) ensure_same_node(:bzmpop, keys) do |node| node.bzmpop(timeout, *keys, modifier: modifier, count: count) end end # Iterate over keys, removing members from the first non empty sorted set found. def zmpop(*keys, modifier: "MIN", count: nil) ensure_same_node(:zmpop, keys) do |node| node.zmpop(*keys, modifier: modifier, count: count) end end # Return a range of members in a sorted set, by index, score or lexicographical ordering. def zrange(key, start, stop, **options) node_for(key).zrange(key, start, stop, **options) end # Select a range of members in a sorted set, by index, score or lexicographical ordering # and store the resulting sorted set in a new key. def zrangestore(dest_key, src_key, start, stop, **options) ensure_same_node(:zrangestore, [dest_key, src_key]) do |node| node.zrangestore(dest_key, src_key, start, stop, **options) end 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 # Get the intersection of multiple sorted sets def zinter(*keys, **options) keys.flatten!(1) ensure_same_node(:zinter, keys) do |node| node.zinter(keys, **options) end end # Intersect multiple sorted sets and store the resulting sorted set in a new # key. def zinterstore(destination, *keys, **options) keys.flatten!(1) ensure_same_node(:zinterstore, [destination].concat(keys)) do |node| node.zinterstore(destination, keys, **options) end end # Return the union of multiple sorted sets. def zunion(*keys, **options) keys.flatten!(1) ensure_same_node(:zunion, keys) do |node| node.zunion(keys, **options) end end # Add multiple sorted sets and store the resulting sorted set in a new key. def zunionstore(destination, *keys, **options) keys.flatten!(1) ensure_same_node(:zunionstore, [destination].concat(keys)) do |node| node.zunionstore(destination, keys, **options) end end # Return the difference between the first and all successive input sorted sets. def zdiff(*keys, **options) keys.flatten!(1) ensure_same_node(:zdiff, keys) do |node| node.zdiff(keys, **options) end end # Compute the difference between the first and all successive input sorted sets # and store the resulting sorted set in a new key. def zdiffstore(destination, *keys, **options) keys.flatten!(1) ensure_same_node(:zdiffstore, [destination] + keys) do |node| node.zdiffstore(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) 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) fields.flatten!(1) node_for(key).hmget(key, fields) end def mapped_hmget(key, *fields) fields.flatten!(1) node_for(key).mapped_hmget(key, fields) end def hrandfield(key, count = nil, **options) node_for(key).hrandfield(key, count, **options) end # Delete one or more hash fields. def hdel(key, *fields) fields.flatten!(1) 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 SubscriptionError, "Can't unsubscribe if not subscribed." unless subscribed? @subscribed_node.unsubscribe(*channels) end # Listen for messages published to channels matching the given patterns. # See the [Redis Server PSUBSCRIBE documentation](https://redis.io/docs/latest/commands/psubscribe/) # for further details def psubscribe(*channels, &block) raise NotImplementedError end # Stop listening for messages posted to channels matching the given # patterns. # See the [Redis Server PUNSUBSCRIBE documentation](https://redis.io/docs/latest/commands/punsubscribe/) # for further details 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 node_for(@watch_key).multi(&block) 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 = key.to_s key[@tag, 1] if key.match?(@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-5.3.0/lib/redis/errors.rb000066400000000000000000000027421466130507100167530ustar00rootroot00000000000000# frozen_string_literal: true class Redis # Base error for all redis-rb errors. class BaseError < StandardError 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 class PermissionError < CommandError end class WrongTypeError < CommandError end class OutOfMemoryError < CommandError 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 # Generally raised during Redis failover scenarios class ReadOnlyError < BaseConnectionError end # Raised when client options are invalid. class InvalidClientOptionError < BaseError end class SubscriptionError < BaseError end end redis-rb-5.3.0/lib/redis/hash_ring.rb000066400000000000000000000040611466130507100173750ustar00rootroot00000000000000# frozen_string_literal: true require 'zlib' require 'digest/md5' 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 = server_hash_for("#{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 = server_hash_for("#{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) hash = hash_for(key) idx = binary_search(@sorted_keys, hash) @ring[@sorted_keys[idx]] end def iter_nodes(key) return [nil, nil] if @ring.empty? crc = hash_for(key) pos = binary_search(@sorted_keys, crc) @ring.size.times do |n| yield @ring[@sorted_keys[(pos + n) % @ring.size]] end end private def hash_for(key) Zlib.crc32(key) end def server_hash_for(key) Digest::MD5.digest(key).unpack1("L>") end # Find the closest index in HashRing with value <= the given value def binary_search(ary, value) upper = ary.size lower = 0 while lower < upper mid = (lower + upper) / 2 if ary[mid] > value upper = mid else lower = mid + 1 end end upper - 1 end end end redis-rb-5.3.0/lib/redis/pipeline.rb000066400000000000000000000054031466130507100172410ustar00rootroot00000000000000# frozen_string_literal: true require "delegate" class Redis class PipelinedConnection attr_accessor :db def initialize(pipeline, futures = [], exception: true) @pipeline = pipeline @futures = futures @exception = exception end include Commands def pipelined yield self end def multi transaction = MultiConnection.new(@pipeline, @futures) send_command([:multi]) size = @futures.size yield transaction multi_future = MultiFuture.new(@futures[size..-1]) @pipeline.call_v([:exec]) do |result| multi_future._set(result) end @futures << multi_future multi_future end private def synchronize yield self end def send_command(command, &block) future = Future.new(command, block, @exception) @pipeline.call_v(command) do |result| future._set(result) end @futures << future future end def send_blocking_command(command, timeout, &block) future = Future.new(command, block, @exception) @pipeline.blocking_call_v(timeout, command) do |result| future._set(result) end @futures << future future end end class MultiConnection < PipelinedConnection def multi raise Redis::BaseError, "Can't nest multi transaction" end private # Blocking commands inside transaction behave like non-blocking. # It shouldn't be done though. # https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction def send_blocking_command(command, _timeout, &block) send_command(command, &block) 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 def initialize(command, coerce, exception) @command = command @object = FutureNotReady @coerce = coerce @exception = exception end def inspect "" end def _set(object) @object = @coerce ? @coerce.call(object) : object value end def value ::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError) @object end def is_a?(other) self.class.ancestors.include?(other) end def class Future end end class MultiFuture < Future def initialize(futures) @futures = futures @command = [:exec] @object = FutureNotReady end def _set(replies) @object = if replies @futures.map.with_index do |future, index| future._set(replies[index]) future.value end else replies end end end end redis-rb-5.3.0/lib/redis/subscribe.rb000066400000000000000000000055251466130507100174220ustar00rootroot00000000000000# frozen_string_literal: true class Redis class SubscribedClient def initialize(client) @client = client @write_monitor = Monitor.new end def call_v(command) @write_monitor.synchronize do @client.call_v(command) end 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 ssubscribe(*channels, &block) subscription("ssubscribe", "sunsubscribe", channels, block) end def ssubscribe_with_timeout(timeout, *channels, &block) subscription("ssubscribe", "sunsubscribe", channels, block, timeout) end def unsubscribe(*channels) call_v([:unsubscribe, *channels]) end def punsubscribe(*channels) call_v([:punsubscribe, *channels]) end def sunsubscribe(*channels) call_v([:sunsubscribe, *channels]) end def close @client.close end protected def subscription(start, stop, channels, block, timeout = 0) sub = Subscription.new(&block) case start when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys else call_v([start, *channels]) end while event = @client.next_event(timeout) if event.is_a?(::RedisClient::CommandError) raise Client::ERROR_MAPPING.fetch(event.class), event.message end type, *rest = event if callback = sub.callbacks[type] callback.call(*rest) end break if type == stop && rest.last == 0 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 = {} 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 def ssubscribe(&block) @callbacks["ssubscribe"] = block end def sunsubscribe(&block) @callbacks["sunsubscribe"] = block end def smessage(&block) @callbacks["smessage"] = block end end end redis-rb-5.3.0/lib/redis/version.rb000066400000000000000000000001031466130507100171110ustar00rootroot00000000000000# frozen_string_literal: true class Redis VERSION = '5.3.0' end redis-rb-5.3.0/makefile000066400000000000000000000077411466130507100147420ustar00rootroot00000000000000REDIS_BRANCH ?= 7.2 ROOT_DIR :=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) TMP := tmp CONF := ${ROOT_DIR}/test/support/conf/redis-${REDIS_BRANCH}.conf 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 := ${TMP}/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 := 16380 16381 16382 16383 16384 16385 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 REDIS_SOCKET_PATH=${SOCKET_PATH} bundle exec rake test stop: @$(call kill-redis,${PID_PATH});\ start: ${BINARY} @cp ${CONF} ${TMP}/redis.conf; \ ${BINARY} ${TMP}/redis.conf \ --daemonize yes\ --pidfile ${PID_PATH}\ --port ${PORT}\ --unixsocket ${SOCKET_PATH} stop_slave: @$(call kill-redis,${SLAVE_PID_PATH}) start_slave: start @${BINARY}\ --daemonize yes\ --pidfile ${SLAVE_PID_PATH}\ --port ${SLAVE_PORT}\ --unixsocket ${SLAVE_SOCKET_PATH}\ --slaveof 127.0.0.1 ${PORT} stop_sentinel: stop_slave stop @$(call kill-redis,${SENTINEL_PID_PATHS}) @rm -f ${TMP}/sentinel*.conf || true start_sentinel: start start_slave @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: MAX_ATTEMPTS_FOR_WAIT ?= 60 wait_for_sentinel: @for port in ${SENTINEL_PORTS}; do\ i=0;\ while : ; do\ if [ $${i} -ge ${MAX_ATTEMPTS_FOR_WAIT} ]; then\ echo "Max attempts exceeded: $${i} times";\ exit 1;\ fi;\ 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;\ i=$$(( $${i}+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 no\ --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-5.3.0/redis.gemspec000066400000000000000000000023361466130507100157100ustar00rootroot00000000000000# 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.6.0' s.add_runtime_dependency('redis-client', '>= 0.22.0') end redis-rb-5.3.0/test/000077500000000000000000000000001466130507100142105ustar00rootroot00000000000000redis-rb-5.3.0/test/db/000077500000000000000000000000001466130507100145755ustar00rootroot00000000000000redis-rb-5.3.0/test/db/.gitkeep000066400000000000000000000000001466130507100162140ustar00rootroot00000000000000redis-rb-5.3.0/test/distributed/000077500000000000000000000000001466130507100165325ustar00rootroot00000000000000redis-rb-5.3.0/test/distributed/blocking_commands_test.rb000066400000000000000000000013501466130507100235660ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedBlockingCommands < Minitest::Test include Helper::Distributed include Lint::BlockingCommands def test_blmove_raises target_version "6.2" do assert_raises(Redis::Distributed::CannotDistribute) do r.blmove('foo', 'bar', 'LEFT', 'RIGHT') end end end def test_blpop_raises assert_raises(Redis::Distributed::CannotDistribute) do r.blpop(%w[foo bar]) end end def test_brpop_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpop(%w[foo bar]) end end def test_brpoplpush_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpoplpush('foo', 'bar') end end end redis-rb-5.3.0/test/distributed/commands_on_hashes_test.rb000066400000000000000000000006201466130507100237440ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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-5.3.0/test/distributed/commands_on_hyper_log_log_test.rb000066400000000000000000000007511466130507100253270ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedCommandsOnHyperLogLog < Minitest::Test include Helper::Distributed include Lint::HyperLogLog def test_pfmerge assert_raises Redis::Distributed::CannotDistribute do super end end def test_pfcount_multiple_keys_diff_nodes assert_raises Redis::Distributed::CannotDistribute do r.pfadd 'foo', 's1' r.pfadd 'bar', 's2' assert r.pfcount('res', 'foo', 'bar') end end end redis-rb-5.3.0/test/distributed/commands_on_lists_test.rb000066400000000000000000000011321466130507100236260ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedCommandsOnLists < Minitest::Test include Helper::Distributed include Lint::Lists def test_lmove target_version "6.2" do assert_raises Redis::Distributed::CannotDistribute do r.lmove('foo', 'bar', 'LEFT', 'RIGHT') end end end 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-5.3.0/test/distributed/commands_on_sets_test.rb000066400000000000000000000040271466130507100234540ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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-5.3.0/test/distributed/commands_on_sorted_sets_test.rb000066400000000000000000000040221466130507100250270ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedCommandsOnSortedSets < Minitest::Test include Helper::Distributed include Lint::SortedSets def test_zrangestore assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zinter assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zinter_with_aggregate assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zinter_with_weights assert_raises(Redis::Distributed::CannotDistribute) { super } end 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_zunion assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zunion_with_aggregate assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zunion_with_weights assert_raises(Redis::Distributed::CannotDistribute) { super } 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 def test_zdiff assert_raises(Redis::Distributed::CannotDistribute) { super } end def test_zdiffstore assert_raises(Redis::Distributed::CannotDistribute) { super } end end redis-rb-5.3.0/test/distributed/commands_on_strings_test.rb000066400000000000000000000033051466130507100241650ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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") 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 assert_raises Redis::Distributed::CannotDistribute do r.set("foo", "a") r.set("bar", "b") r.bitop(:and, "foo&bar", "foo", "bar") 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-5.3.0/test/distributed/commands_on_value_types_test.rb000066400000000000000000000047751466130507100250500ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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 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 def test_unlink_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.unlink(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink(["bar", "baz"]) assert_equal [], r.keys("*").sort 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 def test_copy r.set("foo", "s1") assert_raises Redis::Distributed::CannotDistribute do r.copy("foo", "bar") end end end redis-rb-5.3.0/test/distributed/commands_requiring_clustering_test.rb000066400000000000000000000110421466130507100262410ustar00rootroot00000000000000# frozen_string_literal: true require "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_lmove target_version "6.2" do r.rpush("{qux}foo", "s1") r.rpush("{qux}foo", "s2") r.rpush("{qux}bar", "s3") r.rpush("{qux}bar", "s4") assert_equal "s1", r.lmove("{qux}foo", "{qux}bar", "LEFT", "RIGHT") assert_equal ["s2"], r.lrange("{qux}foo", 0, -1) assert_equal ["s3", "s4", "s1"], r.lrange("{qux}bar", 0, -1) end 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 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".b, r.get("{qux}~foo") end end redis-rb-5.3.0/test/distributed/connection_handling_test.rb000066400000000000000000000005311466130507100241200ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/distributed/distributed_test.rb000066400000000000000000000024711466130507100224440ustar00rootroot00000000000000# frozen_string_literal: true require "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 @r = Redis::Distributed.new NODES, 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 @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 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-5.3.0/test/distributed/internals_test.rb000066400000000000000000000045351466130507100221240ustar00rootroot00000000000000# frozen_string_literal: true require "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.server_url }) 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.server_url }) 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.server_url }) 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-5.3.0/test/distributed/key_tags_test.rb000066400000000000000000000024661466130507100217340ustar00rootroot00000000000000# frozen_string_literal: true require "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}/13") 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-5.3.0/test/distributed/persistence_control_commands_test.rb000066400000000000000000000010271466130507100260630ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/distributed/publish_subscribe_test.rb000066400000000000000000000036321466130507100236310ustar00rootroot00000000000000# frozen_string_literal: true require "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 thread = Thread.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 Thread.pass until @subscribed Redis::Distributed.new(NODES).publish("foo", "s1") thread.join assert @subscribed assert_equal 1, @t1 assert @unsubscribed assert_equal 0, @t2 assert_equal "s1", @message end def test_subscribe_within_subscribe @channels = [] thread = Thread.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 thread.join assert_equal ["foo", "bar"], @channels end def test_other_commands_within_a_subscribe r.subscribe("foo") do |on| on.subscribe do |_channel, _total| r.set("bar", "s2") r.unsubscribe("foo") end end end def test_subscribe_without_a_block assert_raises Redis::SubscriptionError do r.subscribe("foo") end end end redis-rb-5.3.0/test/distributed/remote_server_control_commands_test.rb000066400000000000000000000024771466130507100264320ustar00rootroot00000000000000# frozen_string_literal: true require "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 r.nodes.each do |n| n.config(:resetstat) n.get("foo") n.get("bar") end r.info(:commandstats).each do |info| assert_equal '2', info['get']['calls'] 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 # 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 redis-rb-5.3.0/test/distributed/scripting_test.rb000066400000000000000000000047121466130507100221240ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedScripting < Minitest::Test include Helper::Distributed def to_sha(script) r.script(:load, script).first end def test_script_exists 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 def test_script_flush sha = to_sha("return 1") assert r.script(:exists, sha).first assert_equal ["OK"], r.script(:flush) assert !r.script(:exists, sha).first end def test_script_kill redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| assert_equal ["KILL"], redis.script(:kill) end end def test_eval 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 def test_eval_with_options_hash 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 def test_evalsha 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 def test_evalsha_with_options_hash 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 redis-rb-5.3.0/test/distributed/sorting_test.rb000066400000000000000000000005701466130507100216050ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/distributed/transactions_test.rb000066400000000000000000000027371466130507100226370ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestDistributedTransactions < Minitest::Test include Helper::Distributed 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 |transaction| transaction.incrby("{qux}foo", 3) transaction.incrby("{qux}bar", 6) transaction.incrby("{qux}baz", 9) end assert_equal [3, 6, 10], result end end end redis-rb-5.3.0/test/helper.rb000066400000000000000000000131151466130507100160150ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require "minitest/autorun" require "mocha/minitest" $VERBOSE = true ENV["DRIVER"] ||= "ruby" require "redis" Redis.silence_deprecations = true require "redis/distributed" require_relative "support/redis_mock" if ENV["DRIVER"] == "hiredis" require "hiredis-client" end 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 if ENV['REDIS_SOCKET_PATH'].nil? sock_file = File.expand_path('../tmp/redis.sock', __dir__) unless File.exist?(sock_file) abort "Couldn't locate the redis unix socket, did you run `make start`?" end ENV['REDIS_SOCKET_PATH'] = sock_file end Dir[File.expand_path('lint/**/*.rb', __dir__)].sort.each do |f| require f 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 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, :redis alias r redis def setup @redis = init _new_client # Run GC to make sure orphaned connections are closed. GC.start super end def teardown redis&.close 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 with_db(index) r.select(index) yield 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 def with_acl admin = _new_client admin.acl('SETUSER', 'johndoe', 'on', '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes', '+readonly', '>mysecret') yield('johndoe', 'mysecret') ensure admin.acl('DELUSER', 'johndoe') admin.close end def with_default_user_password admin = _new_client admin.acl('SETUSER', 'default', '>mysecret') yield('default', 'mysecret') ensure admin.acl('SETUSER', 'default', 'nopass') admin.close end end module Client include Generic private def _format_options(options) OPTIONS.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 } Redis.new(opts.merge(options)) end def build_slave_role_client(options = {}) _new_client(options.merge(role: :slave)) end private def wait_for_quorum redis = build_sentinel_client 50.times do if redis.sentinel('ckquorum', MASTER_NAME).start_with?('OK 3 usable Sentinels') return else sleep 0.1 end rescue sleep 0.1 end raise "ckquorum timeout" end def _format_options(options = {}) { url: "redis://#{MASTER_NAME}", sentinels: [{ host: LOCALHOST, port: SENTINEL_PORT }], role: :master, timeout: TIMEOUT, }.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], }.merge(options) end def _new_client(options = {}) Redis::Distributed.new(NODES, _format_options(options).merge(driver: ENV["conn"])) end end end redis-rb-5.3.0/test/lint/000077500000000000000000000000001466130507100151565ustar00rootroot00000000000000redis-rb-5.3.0/test/lint/authentication.rb000066400000000000000000000014411466130507100205220ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Authentication def test_auth_with_password mock(auth: ->(*_) { '+OK' }) do |r| assert_equal 'OK', r.auth('mysecret') end mock(auth: ->(*_) { '-ERR some error' }) do |r| assert_raises(Redis::BaseError) { r.auth('mysecret') } end end def test_auth_for_acl target_version "6.0.0" do with_acl do |username, password| assert_raises(Redis::CannotConnectError) { redis.auth(username, 'wrongpassword') } assert_equal 'OK', redis.auth(username, password) assert_equal 'PONG', redis.ping assert_raises(Redis::BaseError) { redis.echo('foo') } end end end def mock(*args, &block) redis_mock(*args, &block) end end end redis-rb-5.3.0/test/lint/blocking_commands.rb000066400000000000000000000124461466130507100211630ustar00rootroot00000000000000# 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: TIMEOUT }, &blk) end def build_mock_commands(options = {}) { blmove: lambda do |*args| sleep options[:delay] if options.key?(:delay) to_protocol(args.last) end, 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_blmove target_version "6.2" do assert_equal 's1', r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT') assert_equal ['s2'], r.lrange('{zap}foo', 0, -1) assert_equal ['s1', 's2', 's1'], r.lrange('{zap}bar', 0, -1) end end def test_blmove_timeout target_version "6.2" do mock do |r| assert_equal '0', r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT') assert_equal LOW_TIMEOUT.to_s, r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT) end 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 assert_raises ArgumentError do assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", timeout: FakeDuration.new(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_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_bzpopmin assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', timeout: 1) end def test_bzpopmin_float_timeout target_version "6.0" do assert_nil r.bzpopmin('{szap}aaa', '{szap}bbb', timeout: LOW_TIMEOUT) end end def test_bzpopmax assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', timeout: 1) end def test_bzpopmax_float_timeout target_version "6.0" do assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', timeout: LOW_TIMEOUT) end end def test_blmove_socket_timeout target_version "6.2" do mock(delay: TIMEOUT * 5) do |r| assert_raises(Redis::TimeoutError) do r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT) end end end end def test_blpop_socket_timeout mock(delay: 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: 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: TIMEOUT * 5) do |r| assert_raises(Redis::TimeoutError) do r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) end end end end end redis-rb-5.3.0/test/lint/hashes.rb000066400000000000000000000126521466130507100167640ustar00rootroot00000000000000# 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 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 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 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 def test_variadic_hdel 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 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_hrandfield target_version("6.2") do assert_nil r.hrandfield("foo") assert_equal [], r.hrandfield("foo", 1) error = assert_raises(ArgumentError) do r.hrandfield("foo", with_values: true) end assert_equal "count argument must be specified", error.message r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert ["f1", "f2"].include?(r.hrandfield("foo")) assert_equal ["f1", "f2"], r.hrandfield("foo", 2).sort assert_equal 4, r.hrandfield("foo", -4).size r.hrandfield("foo", 2, with_values: true) do |(field, value)| assert ["f1", "f2"].include?(field) assert ["s1", "s2"].include?(value) end end 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_equal({}, r.hgetall("foo")) r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") assert_equal({ "f1" => "s1", "f2" => "s2" }, r.hgetall("foo")) 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_equal({ "f1" => "s1" }, r.mapped_hmget("foo", "f1")) assert_equal({ "f1" => "s1", "f2" => "s2" }, r.mapped_hmget("foo", "f1", "f2")) end def test_mapped_hmget_in_a_pipeline_returns_hash r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") result = r.pipelined do |pipeline| pipeline.mapped_hmget("foo", "f1", "f2") end assert_equal({ "f1" => "s1", "f2" => "s2" }, result[0]) 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 r.hincrbyfloat("foo", "f1", 1.23) assert_equal 1.23, Float(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, Float(r.hget("foo", "f1")) end def test_hstrlen 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 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-5.3.0/test/lint/hyper_log_log.rb000066400000000000000000000031071466130507100203350ustar00rootroot00000000000000# frozen_string_literal: true module Lint module HyperLogLog def test_pfadd 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 def test_variadic_pfadd assert_equal true, r.pfadd("foo", ["s1", "s2"]) assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) assert_equal 3, r.pfcount("foo") end def test_pfcount assert_equal 0, r.pfcount("foo") assert_equal true, r.pfadd("foo", "s1") assert_equal 1, r.pfcount("foo") end def test_variadic_pfcount 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 def test_variadic_pfcount_expanded 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 def test_pfmerge r.pfadd 'foo', 's1' r.pfadd 'bar', 's2' assert_equal true, r.pfmerge('res', 'foo', 'bar') assert_equal 2, r.pfcount('res') 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-5.3.0/test/lint/lists.rb000066400000000000000000000136661466130507100166550ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Lists def test_lmove target_version "6.2" do r.lpush("foo", "s1") r.lpush("foo", "s2") # foo = [s2, s1] r.lpush("bar", "s3") r.lpush("bar", "s4") # bar = [s4, s3] assert_nil r.lmove("nonexistent", "foo", "LEFT", "LEFT") assert_equal "s2", r.lmove("foo", "foo", "LEFT", "RIGHT") # foo = [s1, s2] assert_equal "s1", r.lmove("foo", "foo", "LEFT", "LEFT") # foo = [s1, s2] assert_equal "s1", r.lmove("foo", "bar", "LEFT", "RIGHT") # foo = [s2], bar = [s4, s3, s1] assert_equal ["s2"], r.lrange("foo", 0, -1) assert_equal ["s4", "s3", "s1"], r.lrange("bar", 0, -1) assert_equal "s2", r.lmove("foo", "bar", "LEFT", "LEFT") # foo = [], bar = [s2, s4, s3, s1] assert_nil r.lmove("foo", "bar", "LEFT", "LEFT") # foo = [], bar = [s2, s4, s3, s1] assert_equal ["s2", "s4", "s3", "s1"], r.lrange("bar", 0, -1) error = assert_raises(ArgumentError) do r.lmove("foo", "bar", "LEFT", "MIDDLE") end assert_equal "where_destination must be 'LEFT' or 'RIGHT'", error.message end end 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 assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"]) assert_equal 3, r.llen("foo") assert_equal "s3", r.lpop("foo") 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 assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"]) assert_equal 3, r.llen("foo") assert_equal "s3", r.rpop("foo") 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") assert_nil r.lpop("nonexistent") end def test_lpop_count target_version("6.2") do r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal ["s1", "s2"], r.lpop("foo", 2) assert_equal 0, r.llen("foo") end 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") assert_nil r.rpop("nonexistent") end def test_rpop_count target_version("6.2") do r.rpush "foo", "s1" r.rpush "foo", "s2" assert_equal 2, r.llen("foo") assert_equal ["s2", "s1"], r.rpop("foo", 2) assert_equal 0, r.llen("foo") end 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 def test_blmpop target_version('7.0') do assert_nil r.blmpop(1.0, '{1}foo') r.lpush('{1}foo', %w[a b c d e f g]) assert_equal ['{1}foo', ['g']], r.blmpop(1.0, '{1}foo') assert_equal ['{1}foo', ['f', 'e']], r.blmpop(1.0, '{1}foo', count: 2) r.lpush('{1}foo2', %w[a b]) assert_equal ['{1}foo', ['a']], r.blmpop(1.0, '{1}foo', '{1}foo2', modifier: "RIGHT") end end def test_lmpop target_version('7.0') do assert_nil r.lmpop('{1}foo') r.lpush('{1}foo', %w[a b c d e f g]) assert_equal ['{1}foo', ['g']], r.lmpop('{1}foo') assert_equal ['{1}foo', ['f', 'e']], r.lmpop('{1}foo', count: 2) r.lpush('{1}foo2', %w[a b]) assert_equal ['{1}foo', ['a']], r.lmpop('{1}foo', '{1}foo2', modifier: "RIGHT") end end end end redis-rb-5.3.0/test/lint/sets.rb000066400000000000000000000173751466130507100164760ustar00rootroot00000000000000# frozen_string_literal: true module Lint module Sets def test_sadd assert_equal 1, r.sadd("foo", "s1") assert_equal 1, r.sadd("foo", "s2") assert_equal 0, r.sadd("foo", "s1") assert_equal ["s1", "s2"], r.smembers("foo").sort end 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 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 def test_variadic_sadd? assert_equal true, r.sadd?("foo", ["s1", "s2"]) assert_equal true, r.sadd?("foo", ["s1", "s2", "s3"]) assert_equal false, r.sadd?("foo", ["s1", "s2"]) assert_equal ["s1", "s2", "s3"], r.smembers("foo").sort end def test_srem r.sadd("foo", "s1") r.sadd("foo", "s2") assert_equal 1, r.srem("foo", "s1") assert_equal 0, r.srem("foo", "s3") assert_equal ["s2"], r.smembers("foo") 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 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 def test_variadic_srem? r.sadd("foo", "s1") r.sadd("foo", "s2") r.sadd("foo", "s3") assert_equal true, r.srem?("foo", ["s1", "aaa"]) assert_equal false, r.srem?("foo", ["bbb", "ccc", "ddd"]) assert_equal true, r.srem?("foo", "eee", "s3") assert_equal ["s2"], r.smembers("foo") 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 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 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_smismember target_version("6.2") do assert_equal [false], r.smismember("foo", "s1") r.sadd "foo", "s1" assert_equal [true], r.smismember("foo", "s1") r.sadd "foo", "s3" assert_equal [true, false, true], r.smismember("foo", "s1", "s2", "s3") end 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-5.3.0/test/lint/sorted_sets.rb000066400000000000000000000640241466130507100200470ustar00rootroot00000000000000# 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" # 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 def test_zadd_keywords target_version "6.2" do # LT option r.zadd("foo", 2, "s1") r.zadd("foo", 3, "s1", lt: true) assert_equal 2.0, r.zscore("foo", "s1") r.zadd("foo", 1, "s1", lt: true) assert_equal 1.0, r.zscore("foo", "s1") assert_equal true, r.zadd("foo", 3, "s2", lt: true) # adds new member r.del "foo" # GT option r.zadd("foo", 2, "s1") r.zadd("foo", 1, "s1", gt: true) assert_equal 2.0, r.zscore("foo", "s1") r.zadd("foo", 3, "s1", gt: true) assert_equal 3.0, r.zscore("foo", "s1") assert_equal true, r.zadd("foo", 1, "s2", gt: true) # adds new member r.del "foo" # Incompatible options combination assert_raises(Redis::CommandError) { r.zadd("foo", 1, "s1", nx: true, gt: true) } end end def test_variadic_zadd # Non-nested array with pairs assert_equal 0, r.zcard("foo") assert_equal 2, r.zadd("foo", [1, "s1", 2, "s2"]) assert_equal 2, r.zcard("foo") 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 2, r.zcard("foo") assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]]) assert_equal 3, r.zcard("foo") r.del "foo" # Empty array assert_equal 0, r.zcard("foo") assert_equal 0, r.zadd("foo", []) assert_equal 0, 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"]) } # 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 def test_variadic_zadd_keywords target_version "6.2" do # LT option r.zadd("foo", 2, "s1") assert_equal 1, r.zadd("foo", [3, "s1", 2, "s2"], lt: true, ch: true) assert_equal 2.0, r.zscore("foo", "s1") assert_equal 1, r.zadd("foo", [1, "s1"], lt: true, ch: true) r.del "foo" # GT option r.zadd("foo", 2, "s1") assert_equal 1, r.zadd("foo", [1, "s1", 2, "s2"], gt: true, ch: true) assert_equal 2.0, r.zscore("foo", "s1") assert_equal 1, r.zadd("foo", [3, "s1"], gt: true, ch: true) r.del "foo" 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 r.zadd("foo", 1, "s1") r.zadd("foo", 2, "s2") r.zadd("foo", 3, "s3") assert_equal 3, r.zcard("foo") assert_equal 0, r.zrem("foo", []) assert_equal 3, r.zcard("foo") assert_equal 1, r.zrem("foo", ["s1", "aaa"]) assert_equal 2, r.zcard("foo") assert_equal 0, r.zrem("foo", ["bbb", "ccc", "ddd"]) assert_equal 2, r.zcard("foo") assert_equal 1, r.zrem("foo", ["eee", "s3"]) assert_equal 1, r.zcard("foo") 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_zrange_with_byscore target_version("6.2") do r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal ["s2", "s3"], r.zrange("foo", 2, 3, byscore: true) assert_equal ["s2", "s1"], r.zrange("foo", 2, 1, byscore: true, rev: true) end end def test_zrange_with_bylex target_version("6.2") 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.zrange("foo", "[a", "[a\xff", bylex: true) assert_equal %w[aaren abagael], r.zrange("foo", "[a", "[a\xff", bylex: true, limit: [0, 2]) assert_equal %w[abby abbygail], r.zrange("foo", "(abb", "(abb\xff", bylex: true) assert_equal %w[abbygail], r.zrange("foo", "(abby", "(abby\xff", bylex: true) end end def test_zrangestore target_version("6.2") do r.zadd "foo", 1, "s1" r.zadd "foo", 2, "s2" r.zadd "foo", 3, "s3" assert_equal 2, r.zrangestore("bar", "foo", 0, 1) assert_equal ["s1", "s2"], r.zrange("bar", 0, -1) assert_equal 2, r.zrangestore("baz", "foo", 2, 3, by_score: true) assert_equal ["s2", "s3"], r.zrange("baz", 0, -1) end 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_zmscore target_version("6.2") do r.zadd "foo", 1, "s1" assert_equal [1.0], r.zmscore("foo", "s1") assert_equal [nil], r.zmscore("foo", "s2") r.zadd "foo", "-inf", "s2" r.zadd "foo", "+inf", "s3" assert_equal [1.0, nil], r.zmscore("foo", "s1", "s4") assert_equal [-Float::INFINITY, +Float::INFINITY], r.zmscore("foo", "s2", "s3") end end def test_zrandmember target_version("6.2") do assert_nil r.zrandmember("foo") r.zadd "foo", 1.0, "s1" r.zrem "foo", "s1" assert_nil r.zrandmember("foo") assert_equal [], r.zrandmember("foo", 1) r.zadd "foo", 1.0, "s1" r.zadd "foo", 2.0, "s2" r.zadd "foo", 3.0, "s3" 3.times do assert ["s1", "s2", "s3"].include?(r.zrandmember("foo")) end assert_equal 2, r.zrandmember("foo", 2).size assert_equal 3, r.zrandmember("foo", 4).size assert_equal 5, r.zrandmember("foo", -5).size r.zrandmember("foo", 2, with_scores: true).each do |(member, score)| assert ["s1", "s2", "s3"].include?(member) assert_instance_of Float, score end end 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 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 def test_zpopmin 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 def test_bzmpop target_version('7.0') do assert_nil r.bzmpop(1.0, '{1}foo') r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d]) assert_equal ['{1}foo', [['a', 0.0]]], r.bzmpop(1.0, '{1}foo') assert_equal ['{1}foo', [['b', 1.0], ['c', 2.0], ['d', 3.0]]], r.bzmpop(1.0, '{1}foo', count: 4) r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d]) r.zadd('{1}foo2', %w[0 a 1 b 2 c 3 d]) assert_equal ['{1}foo', [['d', 3.0]]], r.bzmpop(1.0, '{1}foo', '{1}foo2', modifier: "MAX") end end def test_zmpop target_version('7.0') do assert_nil r.zmpop('{1}foo') r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d]) assert_equal ['{1}foo', [['a', 0.0]]], r.zmpop('{1}foo') assert_equal ['{1}foo', [['b', 1.0], ['c', 2.0], ['d', 3.0]]], r.zmpop('{1}foo', count: 4) r.zadd('{1}foo', %w[0 a 1 b 2 c 3 d]) r.zadd('{1}foo2', %w[0 a 1 b 2 c 3 d]) assert_equal ['{1}foo', [['d', 3.0]]], r.zmpop('{1}foo', '{1}foo2', modifier: "MAX") 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 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 def test_zrangebylex 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 def test_zrevrangebylex 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 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_zunion target_version("6.2") do r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'bar', 3, 's1' r.zadd 'bar', 5, 's3' assert_equal %w[s2 s1 s3], r.zunion('foo', 'bar') assert_equal [['s2', 2.0], ['s1', 4.0], ['s3', 5.0]], r.zunion('foo', 'bar', with_scores: true) end end def test_zunion_with_weights target_version("6.2") do 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', 100, 's4' assert_equal %w[s1 s2 s3 s4], r.zunion('foo', 'bar') assert_equal [['s1', 1.0], ['s2', 22.0], ['s3', 33.0], ['s4', 100.0]], r.zunion('foo', 'bar', with_scores: true) assert_equal %w[s1 s2 s3 s4], r.zunion('foo', 'bar', weights: [10, 1]) assert_equal [['s1', 10.0], ['s2', 40.0], ['s3', 60.0], ['s4', 100.0]], r.zunion('foo', 'bar', weights: [10, 1], with_scores: true) end end def test_zunion_with_aggregate target_version("6.2") do r.zadd 'foo', 1, 's1' r.zadd 'foo', 20, 's2' r.zadd 'foo', 3, 's3' r.zadd 'bar', 2, 's2' r.zadd 'bar', 30, 's3' r.zadd 'bar', 100, 's4' assert_equal %w[s1 s2 s3 s4], r.zunion('foo', 'bar', aggregate: :min) assert_equal [['s1', 1.0], ['s2', 2.0], ['s3', 3.0], ['s4', 100.0]], r.zunion('foo', 'bar', aggregate: :min, with_scores: true) assert_equal %w[s1 s2 s3 s4], r.zunion('foo', 'bar', aggregate: :max) assert_equal [['s1', 1.0], ['s2', 20.0], ['s3', 30.0], ['s4', 100.0]], r.zunion('foo', 'bar', aggregate: :max, with_scores: true) end 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_zdiff target_version("6.2") do r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'bar', 3, 's1' r.zadd 'bar', 5, 's3' assert_equal [], r.zdiff('foo', 'foo') assert_equal ['s1', 's2'], r.zdiff('foo') assert_equal ['s2'], r.zdiff('foo', 'bar') assert_equal [['s2', 2.0]], r.zdiff('foo', 'bar', with_scores: true) end end def test_zdiffstore target_version("6.2") do r.zadd 'foo', 1, 's1' r.zadd 'foo', 2, 's2' r.zadd 'bar', 3, 's1' r.zadd 'bar', 5, 's3' assert_equal 0, r.zdiffstore('baz', ['foo', 'foo']) assert_equal 2, r.zdiffstore('baz', ['foo']) assert_equal ['s1', 's2'], r.zrange('baz', 0, -1) assert_equal 1, r.zdiffstore('baz', ['foo', 'bar']) assert_equal ['s2'], r.zrange('baz', 0, -1) end end def test_zinter target_version("6.2") do r.zadd 'foo', 1, 's1' r.zadd 'bar', 2, 's1' r.zadd 'foo', 3, 's3' r.zadd 'bar', 4, 's4' assert_equal ['s1'], r.zinter('foo', 'bar') assert_equal [['s1', 3.0]], r.zinter('foo', 'bar', with_scores: true) end end def test_zinter_with_weights target_version("6.2") do 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 %w[s2 s3], r.zinter('foo', 'bar') assert_equal [['s2', 22.0], ['s3', 33.0]], r.zinter('foo', 'bar', with_scores: true) assert_equal %w[s2 s3], r.zinter('foo', 'bar', weights: [10, 1]) assert_equal [['s2', 40.0], ['s3', 60.0]], r.zinter('foo', 'bar', weights: [10, 1], with_scores: true) end end def test_zinter_with_aggregate target_version("6.2") do 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 %w[s2 s3], r.zinter('foo', 'bar') assert_equal [['s2', 22.0], ['s3', 33.0]], r.zinter('foo', 'bar', with_scores: true) assert_equal %w[s2 s3], r.zinter('foo', 'bar', aggregate: :min) assert_equal [['s2', 2.0], ['s3', 3.0]], r.zinter('foo', 'bar', aggregate: :min, with_scores: true) assert_equal %w[s2 s3], r.zinter('foo', 'bar', aggregate: :max) assert_equal [['s2', 20.0], ['s3', 30.0]], r.zinter('foo', 'bar', aggregate: :max, with_scores: true) end 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-5.3.0/test/lint/streams.rb000066400000000000000000000742511466130507100171720ustar00rootroot00000000000000# 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_nomkstream_option omit_version('6.2.0') actual = redis.xadd('s1', { f1: 'v1', f2: 'v2' }, nomkstream: true) assert_nil actual actual = redis.xadd('s1', { f1: 'v1', f2: 'v2' }, nomkstream: false) assert_match ENTRY_ID_FORMAT, actual end def test_xadd_with_invalid_arguments assert_raises(TypeError) { 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_limit_option omit_version('6.2.0') begin original = redis.config(:get, 'stream-node-max-entries')['stream-node-max-entries'] redis.config(:set, 'stream-node-max-entries', 1) redis.xadd('s1', { f: 'v1' }) redis.xadd('s1', { f: 'v2' }) redis.xadd('s1', { f: 'v3' }) redis.xadd('s1', { f: 'v4' }) assert_equal 1, redis.xtrim('s1', 0, approximate: true, limit: 1) error = assert_raises(Redis::CommandError) { redis.xtrim('s1', 0, limit: 1) } assert_includes error.message, "ERR syntax error, LIMIT cannot be used without the special ~ option" ensure redis.config(:set, 'stream-node-max-entries', original) end end def test_xtrim_with_maxlen_strategy redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v1' }, id: '0-2') redis.xadd('s1', { f: 'v1' }, id: '1-0') redis.xadd('s1', { f: 'v1' }, id: '1-1') assert_equal(2, redis.xtrim('s1', 2, strategy: 'MAXLEN')) end def test_xtrim_with_minid_strategy omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v1' }, id: '0-2') redis.xadd('s1', { f: 'v1' }, id: '1-0') redis.xadd('s1', { f: 'v1' }, id: '1-1') assert_equal(2, redis.xtrim('s1', '1-0', strategy: 'MINID')) end def test_xtrim_with_approximate_minid_strategy omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xadd('s1', { f: 'v1' }, id: '0-2') redis.xadd('s1', { f: 'v1' }, id: '1-0') redis.xadd('s1', { f: 'v1' }, id: '1-1') assert_equal(0, redis.xtrim('s1', '1-0', strategy: 'MINID', approximate: true)) end def test_xtrim_with_invalid_strategy omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }) error = assert_raises(Redis::CommandError) { redis.xtrim('s1', '1-0', strategy: '') } assert_includes error.message, "ERR syntax error" end def test_xtrim_with_not_existed_stream assert_equal 0, redis.xtrim('not-existed-stream', 2) end def test_xtrim_with_invalid_arguments if version >= '6.2' assert_raises(Redis::CommandError) { redis.xtrim('', '') } assert_equal 0, redis.xtrim('s1', 0) assert_raises(Redis::CommandError) { redis.xtrim('s1', -1, approximate: true) } else assert_equal 0, redis.xtrim('', '') assert_equal 0, redis.xtrim('s1', 0) assert_equal 0, redis.xtrim('s1', -1, approximate: true) end 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_raises(TypeError) { redis.xdel(nil, nil) } assert_raises(TypeError) { 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_raises(TypeError) { 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_raises(TypeError) { 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_raises(TypeError) { 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 thread = Thread.new do prepared = true actual = redis.xread('s1', 0, block: 0) end Thread.pass until prepared redis2 = init _new_client redis2.xadd('s1', { f: 'v1' }, id: '0-1') thread.join(3) assert_equal(['v1'], actual.fetch('s1').map { |i| i.last['f'] }) end def test_xread_with_invalid_arguments assert_raises(TypeError) { 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(TypeError) { 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_xreadgroup_a_trimmed_entry redis.xgroup(:create, 'k1', 'g1', '0', mkstream: true) entry_id = redis.xadd('k1', { value: 'v1' }) assert_equal({ 'k1' => [[entry_id, { 'value' => 'v1' }]] }, redis.xreadgroup('g1', 'c1', 'k1', '>')) assert_equal({ 'k1' => [[entry_id, { 'value' => 'v1' }]] }, redis.xreadgroup('g1', 'c1', 'k1', '0')) redis.xtrim('k1', 0) assert_equal({ 'k1' => [[entry_id, nil]] }, redis.xreadgroup('g1', 'c1', 'k1', '0')) 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_raises(TypeError) { 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(TypeError) { redis.xclaim(nil, nil, nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xclaim('', '', '', '', '') } end def test_xautoclaim omit_version('6.2.0') 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.xautoclaim('s1', 'g1', 'c2', 10, '0-0') assert_equal '0-0', actual['next'] assert_equal %w(0-2 0-3), actual['entries'].map(&:first) assert_equal(%w(v2 v3), actual['entries'].map { |i| i.last['f'] }) end def test_xautoclaim_with_justid_option omit_version('6.2.0') 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.xautoclaim('s1', 'g1', 'c2', 10, '0-0', justid: true) assert_equal '0-0', actual['next'] assert_equal %w(0-2 0-3), actual['entries'] end def test_xautoclaim_with_count_option omit_version('6.2.0') 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.xautoclaim('s1', 'g1', 'c2', 10, '0-0', count: 1) assert_equal '0-3', actual['next'] assert_equal %w(0-2), actual['entries'].map(&:first) assert_equal(%w(v2), actual['entries'].map { |i| i.last['f'] }) end def test_xautoclaim_with_larger_interval omit_version('6.2.0') 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.xautoclaim('s1', 'g1', 'c2', 36_000, '0-0') assert_equal '0-0', actual['next'] assert_equal [], actual['entries'] end def test_xautoclaim_with_deleted_entry omit_version('6.2.0') 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.xdel('s1', '0-2') sleep 0.01 actual = redis.xautoclaim('s1', 'g1', 'c2', 0, '0-0') assert_equal '0-0', actual['next'] assert_equal [], actual['entries'] 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_idle_options target_version "6.2" do 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', '-', '+', 10) assert_equal 2, actual.size actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10) assert_equal 0, actual.size sleep 0.1 actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10) assert_equal 2, actual.size redis.xadd('s1', { f: 'v4' }, id: '0-4') redis.xreadgroup('g1', 'c2', 's1', '>') actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 1000) assert_equal 0, actual.size actual = redis.xpending('s1', 'g1', '-', '+', 10) assert_equal 3, actual.size actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 10) assert_equal 2, actual.size sleep 0.01 actual = redis.xpending('s1', 'g1', '-', '+', 10, idle: 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 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-5.3.0/test/lint/strings.rb000066400000000000000000000224111466130507100171740ustar00rootroot00000000000000# 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 (0..255).each do |i| str = "#{i.chr}---#{i.chr}" r.set("foo", str) assert_equal str, r.get("foo") end end def test_set_with_ex r.set("foo", "bar", ex: 2) assert_in_range 0..2, r.ttl("foo") end def test_set_with_px r.set("foo", "bar", px: 2000) assert_in_range 0..2, r.ttl("foo") end def test_set_with_exat target_version "6.2" do r.set("foo", "bar", exat: Time.now.to_i + 2) assert_in_range 0..2, r.ttl("foo") end end def test_set_with_pxat target_version "6.2" do r.set("foo", "bar", pxat: (1000 * Time.now.to_i) + 2000) assert_in_range 0..2, r.ttl("foo") end end def test_set_with_nx 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 def test_set_with_xx 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 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_set_with_get target_version "6.2" do r.set("foo", "qux") assert_equal "qux", r.set("foo", "bar", get: true) assert_equal "bar", r.get("foo") assert_nil r.set("baz", "bar", get: true) assert_equal "bar", r.get("baz") 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 assert r.psetex("foo", 1000, "bar") assert_equal "bar", r.get("foo") assert [0, 1].include? r.ttl("foo") end def test_psetex_with_non_string_value 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 def test_getex target_version "6.2" do assert r.setex("foo", 1000, "bar") assert_equal "bar", r.getex("foo", persist: true) assert_equal(-1, r.ttl("foo")) end end def test_getdel target_version "6.2" do assert r.set("foo", "bar") assert_equal "bar", r.getdel("foo") assert_nil r.get("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 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 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 r.set("foo", "abcde") assert_equal 10, r.bitcount("foo", 1, 3) assert_equal 17, r.bitcount("foo", 0, -1) end def test_bitcount_bits_range target_version "7.0" do r.set("foo", "abcde") assert_equal 10, r.bitcount("foo", 8, 31, scale: :bit) assert_equal 17, r.bitcount("foo", 0, -1, scale: :byte) 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 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 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') 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 |pipeline| pipeline.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 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(: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".b, r.get('~foo{1}') end end end redis-rb-5.3.0/test/lint/value_types.rb000066400000000000000000000162701466130507100200510ustar00rootroot00000000000000# frozen_string_literal: true module Lint module ValueTypes def test_exists assert_equal 0, r.exists("foo") r.set("foo", "s1") assert_equal 1, r.exists("foo") assert_equal 1, r.exists(["foo"]) 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") 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") assert_equal true, r.exists?(["{1}foo"]) r.set("{1}bar", "s1") assert_equal true, r.exists?("{1}foo", "{1}bar") 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") target_version "7.0.0" do r.set("bar", "s2") refute r.expire("bar", 5, xx: true) assert r.expire("bar", 5, nx: true) refute r.expire("bar", 5, nx: true) assert r.expire("bar", 5, xx: true) r.expire("bar", 10) refute r.expire("bar", 15, lt: true) refute r.expire("bar", 5, gt: true) assert r.expire("bar", 15, gt: true) assert r.expire("bar", 5, lt: true) end end def test_pexpire r.set("foo", "s1") assert r.pexpire("foo", 2000) assert_in_range 0..2, r.ttl("foo") end def test_pexpire_keywords target_version "7.0.0" do r.set("bar", "s2") refute r.pexpire("bar", 5_000, xx: true) assert r.pexpire("bar", 5_000, nx: true) refute r.pexpire("bar", 5_000, nx: true) assert r.pexpire("bar", 5_000, xx: true) r.pexpire("bar", 10_000) refute r.pexpire("bar", 15_000, lt: true) refute r.pexpire("bar", 5_000, gt: true) assert r.pexpire("bar", 15_000, gt: true) assert r.pexpire("bar", 5_000, lt: true) 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_expireat_keywords target_version "7.0.0" do r.set("bar", "s2") refute r.expireat("bar", (Time.now + 5).to_i, xx: true) assert r.expireat("bar", (Time.now + 5).to_i, nx: true) refute r.expireat("bar", (Time.now + 5).to_i, nx: true) assert r.expireat("bar", (Time.now + 5).to_i, xx: true) r.expireat("bar", (Time.now + 10).to_i) refute r.expireat("bar", (Time.now + 15).to_i, lt: true) refute r.expireat("bar", (Time.now + 5).to_i, gt: true) assert r.expireat("bar", (Time.now + 15).to_i, gt: true) assert r.expireat("bar", (Time.now + 5).to_i, lt: true) end end def test_expiretime target_version "7.0.0" do r.set("foo", "blar") assert_equal(-1, r.expiretime("foo")) exp_time = (Time.now + 2).to_i r.expireat("foo", exp_time) assert_equal exp_time, r.expiretime("foo") assert_equal(-2, r.expiretime("key-that-exists-not")) end end def test_pexpireat r.set("foo", "s1") assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000) assert_in_range 0..2, r.ttl("foo") end def test_pexpireat_keywords target_version "7.0.0" do r.set("bar", "s2") refute r.pexpireat("bar", (Time.now + 5).to_i * 1_000, xx: true) assert r.pexpireat("bar", (Time.now + 5).to_i * 1_000, nx: true) refute r.pexpireat("bar", (Time.now + 5).to_i * 1_000, nx: true) assert r.pexpireat("bar", (Time.now + 5).to_i * 1_000, xx: true) r.pexpireat("bar", (Time.now + 10).to_i * 1_000) refute r.pexpireat("bar", (Time.now + 15).to_i * 1_000, lt: true) refute r.pexpireat("bar", (Time.now + 5).to_i * 1_000, gt: true) assert r.pexpireat("bar", (Time.now + 15).to_i * 1_000, gt: true) assert r.pexpireat("bar", (Time.now + 5).to_i * 1_000, lt: true) end end def test_pexpiretime target_version "7.0.0" do r.set("foo", "blar") assert_equal(-1, r.pexpiretime("foo")) exp_time = (Time.now + 2).to_i * 1_000 r.pexpireat("foo", exp_time) assert_equal exp_time, r.pexpiretime("foo") assert_equal(-2, r.pexpiretime("key-that-exists-not")) 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 r.set("foo", "s1") r.expire("foo", 2) assert_in_range 1..2000, r.pttl("foo") end def test_dump_and_restore 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 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 def test_copy target_version("6.2") do with_db(14) do r.flushdb r.set "foo", "s1" r.set "bar", "s2" assert r.copy("foo", "baz") assert_equal "s1", r.get("baz") assert !r.copy("foo", "bar") assert r.copy("foo", "bar", replace: true) assert_equal "s1", r.get("bar") end with_db(15) do r.set "foo", "s3" r.set "bar", "s4" end with_db(14) do assert r.copy("foo", "baz", db: 15) assert_equal "s1", r.get("foo") assert !r.copy("foo", "bar", db: 15) assert r.copy("foo", "bar", db: 15, replace: true) end with_db(15) do assert_equal "s1", r.get("baz") assert_equal "s1", r.get("bar") end end end end end redis-rb-5.3.0/test/redis/000077500000000000000000000000001466130507100153165ustar00rootroot00000000000000redis-rb-5.3.0/test/redis/bitpos_test.rb000066400000000000000000000025731466130507100202110ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestBitpos < Minitest::Test include Helper::Client def test_bitpos_empty_zero r.del "foo" assert_equal(0, r.bitpos("foo", 0)) end def test_bitpos_empty_one r.del "foo" assert_equal(-1, r.bitpos("foo", 1)) end def test_bitpos_zero r.set "foo", "\xff\xf0\x00" assert_equal(12, r.bitpos("foo", 0)) end def test_bitpos_one r.set "foo", "\x00\x0f\x00" assert_equal(12, r.bitpos("foo", 1)) end def test_bitpos_zero_end_is_given 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 def test_bitpos_one_intervals 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 def test_bitpos_one_intervals_bit_range target_version "7.0" do r.set "foo", "\x00\xff\x00" assert_equal(8, r.bitpos("foo", 1, 8, -1, scale: 'bit')) assert_equal(-1, r.bitpos("foo", 1, 8, -1, scale: 'byte')) end end def test_bitpos_raise_exception_if_stop_not_start assert_raises(ArgumentError) do r.bitpos("foo", 0, nil, 2) end end end redis-rb-5.3.0/test/redis/blocking_commands_test.rb000066400000000000000000000030311466130507100223500ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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_operator delay, :<=, (t2 - t1) end end def test_blmove_disable_client_timeout target_version "6.2" do assert_takes_longer_than_client_timeout do |r| assert_equal '0', r.blmove('foo', 'bar', 'LEFT', 'RIGHT') end 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 |transaction| transaction.brpoplpush('foo', 'bar') transaction.brpoplpush('foo', 'bar', timeout: 2) end assert_equal [nil, nil], results end def test_brpoplpush_in_pipeline mock do |r| results = r.pipelined do |transaction| transaction.brpoplpush('foo', 'bar') transaction.brpoplpush('foo', 'bar', timeout: 2) end assert_equal ['0', '2'], results end end end redis-rb-5.3.0/test/redis/client_test.rb000066400000000000000000000021711466130507100201610ustar00rootroot00000000000000# frozen_string_literal: true require "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_error_translate_subclasses error = Class.new(RedisClient::CommandError) assert_equal Redis::CommandError, Redis::Client.send(:translate_error_class, error) assert_raises KeyError do Redis::Client.send(:translate_error_class, StandardError) end end def test_mixed_encoding r.call("MSET", "fée", "\x00\xFF".b, "じ案".encode(Encoding::SHIFT_JIS), "\t".encode(Encoding::ASCII)) assert_equal "\x00\xFF".b, r.call("GET", "fée") assert_equal "\t", r.call("GET", "じ案".encode(Encoding::SHIFT_JIS)) r.call("SET", "\x00\xFF", "fée") assert_equal "fée", r.call("GET", "\x00\xFF".b) end end redis-rb-5.3.0/test/redis/commands_on_geo_test.rb000066400000000000000000000065311466130507100220360ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsGeo < Minitest::Test include Helper::Client def setup super added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania") assert_equal 2, added_items_count end def test_geoadd_with_array_params added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"]) assert_equal 2, added_items_count end def test_georadius_with_same_params r.geoadd("Chad", 15, 15, "Kanem") nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc') assert_equal %w(Kanem), nearest_cities end def test_georadius_with_sort 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 def test_georadius_with_count city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1) assert_equal %w(Catania), city end def test_georadius_with_options_count_sort city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1) assert_equal [["Palermo", "190.4424"]], city end def test_georadiusbymember_with_sort 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 def test_georadiusbymember_with_count city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1) assert_equal %w(Catania), city end def test_georadiusbymember_with_options_count_sort city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1) assert_equal [["Palermo", "166.2742"]], city end def test_geopos 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 def test_geopos_nonexistant_location location = r.geopos("Sicily", "Rome") assert_equal [nil], location locations = r.geopos("Sicily", ["Rome", "Catania"]) assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations end def test_geodist 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 def test_geodist_with_nonexistant_location distination = r.geodist("Sicily", "Palermo", "Rome") assert_nil distination end def test_geohash geohash = r.geohash("Sicily", "Palermo") assert_equal ["sqc8b49rny0"], geohash geohashes = r.geohash("Sicily", ["Palermo", "Catania"]) assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes end def test_geohash_with_nonexistant_location geohashes = r.geohash("Sicily", ["Palermo", "Rome"]) assert_equal ["sqc8b49rny0", nil], geohashes end end redis-rb-5.3.0/test/redis/commands_on_hashes_test.rb000066400000000000000000000002211466130507100225250ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnHashes < Minitest::Test include Helper::Client include Lint::Hashes end redis-rb-5.3.0/test/redis/commands_on_hyper_log_log_test.rb000066400000000000000000000002331466130507100241060ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnHyperLogLog < Minitest::Test include Helper::Client include Lint::HyperLogLog end redis-rb-5.3.0/test/redis/commands_on_lists_test.rb000066400000000000000000000002171466130507100224150ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnLists < Minitest::Test include Helper::Client include Lint::Lists end redis-rb-5.3.0/test/redis/commands_on_sets_test.rb000066400000000000000000000002151466130507100222330ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnSets < Minitest::Test include Helper::Client include Lint::Sets end redis-rb-5.3.0/test/redis/commands_on_sorted_sets_test.rb000066400000000000000000000002311466130507100236110ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnSortedSets < Minitest::Test include Helper::Client include Lint::SortedSets end redis-rb-5.3.0/test/redis/commands_on_streams_test.rb000066400000000000000000000003551466130507100227400ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # 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-5.3.0/test/redis/commands_on_strings_test.rb000066400000000000000000000002231466130507100227450ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestCommandsOnStrings < Minitest::Test include Helper::Client include Lint::Strings end redis-rb-5.3.0/test/redis/commands_on_value_types_test.rb000066400000000000000000000120751466130507100236240ustar00rootroot00000000000000# frozen_string_literal: true require "helper" 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 0, r.del("") 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 0, r.del([]) 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 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 def test_unlink_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.unlink(["foo"]) assert_equal ["bar", "baz"], r.keys("*").sort assert_equal 2, r.unlink(["bar", "baz"]) assert_equal [], r.keys("*").sort 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 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 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 redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis| assert_equal "FLUSHALL ASYNC", redis.flushall(async: true) 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-5.3.0/test/redis/connection_handling_test.rb000066400000000000000000000057571466130507100227230ustar00rootroot00000000000000# frozen_string_literal: true require "helper" require 'lint/authentication' class TestConnectionHandling < Minitest::Test include Helper::Client include Lint::Authentication 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.close assert_equal "bar", 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_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 end redis-rb-5.3.0/test/redis/connection_test.rb000066400000000000000000000051121466130507100210400ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestConnection < Minitest::Test include Helper::Client def test_provides_a_meaningful_inspect assert_equal "#", r.inspect end def test_connection_with_user_and_password target_version "6.0" do with_acl do |username, password| redis = Redis.new(OPTIONS.merge(username: username, password: password)) assert_equal "PONG", redis.ping end end end def test_connection_with_default_user_and_password target_version "6.0" do with_default_user_password do |_username, password| redis = Redis.new(OPTIONS.merge(password: password)) assert_equal "PONG", redis.ping end end end def test_connection_information assert_equal "localhost", r.connection.fetch(:host) assert_equal 6381, r.connection.fetch(:port) assert_equal 15, r.connection.fetch(:db) assert_equal "localhost:6381", r.connection.fetch(:location) assert_equal "redis://localhost: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", redis.connection.fetch(:id) end def test_default_id_with_host_and_port_and_ssl redis = Redis.new(OPTIONS.merge(host: 'host', port: '1234', db: 0, ssl: true)) assert_equal "rediss://host:1234", 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)) assert_equal "redis://host:1234", redis.connection.fetch(:id) end def test_default_id_with_path redis = Redis.new(OPTIONS.merge(path: "/tmp/redis.sock", db: 0)) assert_equal "unix:///tmp/redis.sock", 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)) assert_equal "unix:///tmp/redis.sock", 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://localhost:6381/15", id assert_equal "redis://localhost:6381/15", connection_id end end redis-rb-5.3.0/test/redis/encoding_test.rb000066400000000000000000000006561466130507100204770ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestEncoding < Minitest::Test include Helper::Client def test_returns_properly_encoded_strings r.set "foo", "שלום" assert_equal "Shalom שלום", "Shalom #{r.get('foo')}" refute_predicate "\xFF", :valid_encoding? r.set("bar", "\xFF") bytes = r.get("bar") assert_equal "\xFF".b, bytes assert_predicate bytes, :valid_encoding? end end redis-rb-5.3.0/test/redis/error_replies_test.rb000066400000000000000000000021131466130507100215530ustar00rootroot00000000000000# frozen_string_literal: true require "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 r.unknown_command rescue => ex ensure assert ex.message =~ /unknown command/i end end def test_raise_first_error_reply_in_pipeline with_reconnection_check do 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 redis-rb-5.3.0/test/redis/fork_safety_test.rb000066400000000000000000000007711466130507100212230ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestForkSafety < Minitest::Test include Helper::Client def setup skip("Fork unavailable") unless Process.respond_to?(:fork) end def test_fork_safety redis = Redis.new(OPTIONS) pid = fork do 1000.times do assert_equal "OK", redis.set("key", "foo") end end 1000.times do assert_equal "PONG", redis.ping end _, status = Process.wait2(pid) assert_predicate(status, :success?) end end redis-rb-5.3.0/test/redis/helper_test.rb000066400000000000000000000005571466130507100201700ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/redis/internals_test.rb000066400000000000000000000177371466130507100207200ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestInternals < Minitest::Test include Helper::Client 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 # We use a larger timeout for TruffleRuby # https://github.com/redis/redis-rb/pull/1128#issuecomment-1218490684 r = init(_new_client(timeout: TIMEOUT * 5)) large = "\u3042" * 4_000_000 r.setex("foo", 10, large) result = r.get("foo") assert_equal result, large 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_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 def test_time # 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 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 = {}, &block) @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), &block) end def test_retry_by_default close_on_ping([0]) do |redis| assert_equal "1", redis.ping 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: [0.01, 0.02, 0.04]) do |redis| redis._client.config.expects(:sleep).with(0.01).returns(true) redis._client.config.expects(:sleep).with(0.02).returns(true) redis._client.config.expects(:sleep).with(0.04).returns(true) assert_equal "3", redis.ping end end def test_retry_pipeline_first_command close_on_ping([0]) do |redis| results = redis.pipelined do |pipeline| pipeline.ping end assert_equal ["1"], results end end def close_on_connection(seq, &block) @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, &block) end def test_retry_on_write_error_by_default close_on_connection([0]) do |redis| assert_equal "1", redis._client.call_v(["x" * 128 * 1024]) 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_v(["x" * 128 * 1024]) end end end end def test_connecting_to_unix_domain_socket Redis.new(OPTIONS.merge(path: ENV.fetch("REDIS_SOCKET_PATH"))).ping end 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 def test_client_options redis = Redis.new(OPTIONS.merge(host: "host", port: 1234, db: 1)) assert_equal "host", redis._client.host assert_equal 1234, redis._client.port assert_equal 1, redis._client.db 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 af_family_supported(Socket::AF_INET) do def test_connect_ipv4 af_test("127.0.0.1") end end af_family_supported(Socket::AF_INET6) do def test_connect_ipv6 af_test("::1") 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 def test_reconnect_on_readonly_errors tcp_server = TCPServer.new("127.0.0.1", 0) tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) port = tcp_server.addr[1] server_thread = Thread.new do session = tcp_server.accept io = RedisClient::RubyConnection::BufferedIO.new(session, read_timeout: 1, write_timeout: 1) 2.times do command = RedisClient::RESP3.load(io) case command.first.upcase when "PING" session.write("+PONG\r\n") when "SET" session.write("-READONLY You can't write against a read only replica.\r\n") else session.write("-ERR Unknown command #{command.first}\r\n") end end session.close end redis = Redis.new(host: "127.0.0.1", port: port, timeout: 2, reconnect_attempts: 0) assert_equal "PONG", redis.ping assert_raises Redis::ReadOnlyError do redis.set("foo", "bar") end refute_predicate redis, :connected? ensure server_thread&.kill end end redis-rb-5.3.0/test/redis/persistence_control_commands_test.rb000066400000000000000000000010011466130507100246370ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/redis/pipelining_commands_test.rb000066400000000000000000000155671466130507100227370ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestPipeliningCommands < Minitest::Test include Helper::Client def test_bulk_commands r.pipelined do |p| p.lpush "foo", "s1" p.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 |p| p.mset("foo", "s1", "bar", "s2") p.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 |p| p.lpush "foo", "s1" p.lpush "foo", "s2" p.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 |p| p.mset("baz", "s3", "qux", "s4") p.lpush "foo", "s1" p.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 |p| p.set "foo", "bar" p.get "foo" p.get "bar" end assert_equal ["OK", "bar", nil], result end def test_assignment_of_results_inside_the_block r.pipelined do |p| @first = p.sadd?("foo", 1) @second = p.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 |p| p.doesnt_exist @first = p.sadd?("foo", 1) @second = p.sadd?("foo", 1) end end assert_raises(Redis::FutureNotReady) { @first.value } assert_raises(Redis::FutureNotReady) { @second.value } end def test_assignment_of_results_inside_the_block_without_raising_exception r.pipelined(exception: false) do |p| @first = p.doesnt_exist @second = p.sadd?("foo", 1) @third = p.sadd?("foo", 1) end assert_equal RedisClient::CommandError, @first.value.class assert_equal true, @second.value assert_equal false, @third.value end def test_assignment_of_results_inside_a_nested_block r.pipelined do |p| @first = p.sadd?("foo", 1) p.pipelined do |p2| @second = p2.sadd?("foo", 1) end end assert_equal true, @first.value assert_equal false, @second.value end def test_nested_pipelining_returns_without_raising_exception result = r.pipelined(exception: false) do |p1| p1.doesnt_exist p1.set("foo", "42") p1.pipelined do |p2| p2.doesnt_exist_again p2.set("bar", "99") end end assert result[0].is_a?(RedisClient::CommandError) assert_equal ["doesnt_exist"], result[0].command assert_equal "OK", result[1] assert result[2].is_a?(RedisClient::CommandError) assert_equal ["doesnt_exist_again"], result[2].command assert_equal "OK", result[3] assert_equal "42", r.get("foo") assert_equal "99", r.get("bar") end def test_futures_raise_when_confused_with_something_else r.pipelined do |p| @result = p.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 |p| assert_raises(Redis::FutureNotReady) do p.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 |p| p.zadd("set", "1", "one") @result = p.zincryby("set", "fail", "one") end end end def test_futures_can_be_identified r.pipelined do |p| @result = p.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 |p| p.set("foo", "s1") p.pipelined do |p2| p2.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 |p| p.info end assert result.first.is_a?(Hash) end def test_config_get_in_a_pipeline_returns_hash result = r.pipelined do |p| p.config(:get, "*") end assert result.first.is_a?(Hash) end def test_hgetall_in_a_pipeline_returns_hash r.hmset("hash", "field", "value") future = nil result = r.pipelined do |p| future = p.hgetall("hash") end assert_equal([{ "field" => "value" }], result) assert_equal({ "field" => "value" }, future.value) end def test_zpopmax_in_a_pipeline_produces_future r.zadd("sortedset", 1.0, "value") future = nil result = r.pipelined do |pipeline| future = pipeline.zpopmax("sortedset") end assert_equal [["value", 1.0]], result assert_equal ["value", 1.0], future.value end def test_hgetall_in_a_multi_in_a_pipeline_returns_hash future = nil result = r.pipelined do |p| p.multi do |m| m.hmset("hash", "field", "value", "field2", "value2") future = m.hgetall("hash") end end if Gem::Version.new(Redis::VERSION) > Gem::Version.new("4.8") result = result.last end assert_equal({ "field" => "value", "field2" => "value2" }, result.last) assert_equal({ "field" => "value", "field2" => "value2" }, future.value) end def test_keys_in_a_pipeline r.set("key", "value") result = r.pipelined do |p| p.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_interrupt_preserves_client original = r._client Redis::PipelinedConnection.stubs(:new).raises(Interrupt) assert_raises(Interrupt) { r.pipelined {} } assert_equal r._client, original end end redis-rb-5.3.0/test/redis/publish_subscribe_test.rb000066400000000000000000000204651466130507100224200ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestPublishSubscribe < Minitest::Test include Helper::Client def setup @threads = {} super end def teardown super @threads.each do |thread, redis| if redis.subscribed? redis.unsubscribe redis.punsubscribe end redis.close begin thread.join(2) or warn("leaked thread") rescue RedisClient::ConnectionError end end end class TestError < StandardError end def test_subscribe_and_unsubscribe @subscribed = false @unsubscribed = false thread = new_thread do |r| r.subscribe(channel_name) 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 Thread.pass until @subscribed redis.publish(channel_name, "s1") thread.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 thread = new_thread do |r| r.psubscribe("channel:*") 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 Thread.pass until @subscribed redis.publish(channel_name, "s1") thread.join assert @subscribed assert_equal 1, @t1 assert @unsubscribed assert_equal 0, @t2 assert_equal "s1", @message end def test_pubsub_with_channels_and_numsub_subcommnads @subscribed = false thread = new_thread do |r| r.subscribe(channel_name) do |on| on.subscribe { |_channel, _total| @subscribed = true } on.message { |_channel, _message| r.unsubscribe } end end Thread.pass until @subscribed channels_result = redis.pubsub(:channels) channels_result.delete('__sentinel__:hello') numsub_result = redis.pubsub(:numsub, channel_name, 'boo') redis.publish(channel_name, "s1") thread.join assert_includes channels_result, channel_name assert_equal [channel_name, 1, 'boo', 0], numsub_result end def test_subscribe_connection_usable_after_raise @subscribed = false thread = new_thread do |r| r.subscribe(channel_name) do |on| on.subscribe do |_channel, _total| @subscribed = true end on.message do |_channel, _message| r.unsubscribe raise TestError end end rescue TestError end # Wait until the subscription is active before publishing Thread.pass until @subscribed redis.publish(channel_name, "s1") thread.join assert_equal "PONG", r.ping end def test_psubscribe_connection_usable_after_raise @subscribed = false thread = new_thread do |r| r.psubscribe("channel:*") do |on| on.psubscribe do |_pattern, _total| @subscribed = true end on.pmessage do |_pattern, _channel, _message| raise TestError end end rescue TestError end # Wait until the subscription is active before publishing Thread.pass until @subscribed redis.publish(channel_name, "s1") thread.join assert_equal "PONG", r.ping end def test_subscribe_within_subscribe @channels = Queue.new thread = new_thread do |r| r.subscribe(channel_name) do |on| on.subscribe do |channel, _total| @channels << channel r.subscribe("bar") if channel == channel_name r.unsubscribe if channel == "bar" end end end thread.join assert_equal [channel_name, "bar"], [@channels.pop, @channels.pop] assert_empty @channels end def test_other_commands_within_a_subscribe r.subscribe(channel_name) do |on| on.subscribe do |_channel, _total| r.set("bar", "s2") r.unsubscribe(channel_name) end end end def test_subscribe_without_a_block error = assert_raises Redis::SubscriptionError do r.subscribe(channel_name) end assert_includes "This client is not subscribed", error.message end def test_unsubscribe_without_a_subscribe error = assert_raises Redis::SubscriptionError do r.unsubscribe end assert_includes "This client is not subscribed", error.message error = assert_raises Redis::SubscriptionError do r.punsubscribe end assert_includes "This client is not subscribed", error.message 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 r.subscribe_with_timeout(LOW_TIMEOUT, channel_name) do |on| on.message do |_channel, _message| received = true end end refute received end def test_psubscribe_with_timeout received = false r.psubscribe_with_timeout(LOW_TIMEOUT, "channel:*") do |on| on.message do |_channel, _message| received = true end end refute received end def test_unsubscribe_from_another_thread @unsubscribed = @subscribed = false @subscribed_redis = nil @messages = Queue.new thread = new_thread do |r| @subscribed_redis = r r.subscribe(channel_name) do |on| on.subscribe do |_channel, _total| @subscribed = true end on.message do |channel, message| @messages << [channel, message] end on.unsubscribe do |_channel, _total| @unsubscribed = true end end end Thread.pass until @subscribed redis.publish(channel_name, "test") assert_equal [channel_name, "test"], @messages.pop assert_empty @messages @subscribed_redis.unsubscribe # this shouldn't block refute_nil thread.join(2) assert_equal true, @unsubscribed end def test_subscribe_from_another_thread @events = Queue.new @subscribed_redis = nil thread = new_thread do |r| r.subscribe(channel_name) do |on| @subscribed_redis = r on.subscribe do |channel, _total| @events << ["subscribed", channel] end on.message do |channel, message| @events << ["message", channel, message] end on.unsubscribe do |channel, _total| @events << ["unsubscribed", channel] end end end Thread.pass until @subscribed_redis&.subscribed? redis.publish(channel_name, "test") @subscribed_redis.subscribe("#{channel_name}:2") redis.publish("#{channel_name}:2", "test-2") @subscribed_redis.unsubscribe(channel_name) @subscribed_redis.unsubscribe # this shouldn't block refute_nil thread.join(2) expected = [ ["subscribed", channel_name], ["message", channel_name, "test"], ["subscribed", "#{channel_name}:2"], ["message", "#{channel_name}:2", "test-2"], ["unsubscribed", channel_name], ["unsubscribed", "#{channel_name}:2"] ] assert_equal(expected, expected.map { @events.pop }) assert_empty @events end private def new_thread(&block) redis = Redis.new(OPTIONS) thread = Thread.new(redis, &block) thread.report_on_exception = true @threads[thread] = redis thread end def channel_name @channel_name ||= "channel:#{rand}" end end redis-rb-5.3.0/test/redis/remote_server_control_commands_test.rb000066400000000000000000000055661466130507100252200ustar00rootroot00000000000000# frozen_string_literal: true require "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 r.config(:resetstat) r.get("foo") r.get("bar") result = r.info(:commandstats) assert_equal '2', result['get']['calls'] end def test_monitor_redis log = [] thread = Thread.new do Redis.new(OPTIONS).monitor do |line| log << line break if line =~ /set/ end end Thread.pass while log.empty? # Faster than sleep r.set "foo", "s1" thread.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" || encoding == "listpack", "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 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 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 assert_nil r.client(:getname) r.client(:setname, 'redis-rb') name = r.client(:getname) assert_equal 'redis-rb', name end end redis-rb-5.3.0/test/redis/scanning_test.rb000066400000000000000000000206241466130507100205060ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestScanning < Minitest::Test include Helper::Client def test_scan_basic 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 def test_scan_count 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 def test_scan_match 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 def test_scan_type target_version "6.0.0" do r.debug :populate, 1000 r.zadd("foo", [1, "s1", 2, "s2", 3, "s3"]) r.zadd("bar", [6, "s1", 5, "s2", 4, "s3"]) r.hset("baz", "k1", "v1") cursor = 0 all_keys = [] loop do cursor, keys = r.scan cursor, type: "zset" all_keys += keys break if cursor == "0" end assert_equal 2, all_keys.uniq.size end end def test_scan_each_enumerator 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 def test_scan_each_enumerator_match 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 def test_scan_each_enumerator_type target_version "6.0.0" do r.debug :populate, 1000 r.zadd("key:zset", [1, "s1", 2, "s2", 3, "s3"]) r.hset("key:hash:1", "k1", "v1") r.hset("key:hash:2", "k2", "v2") keys_from_scan = r.scan_each(type: "hash").to_a.uniq all_keys = r.keys "key:hash:*" assert all_keys.sort == keys_from_scan.sort end end def test_scan_each_block 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 def test_scan_each_block_match 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 def test_sscan_each_enumerator 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 def test_sscan_each_enumerator_match 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 def test_sscan_each_enumerator_block 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 def test_sscan_each_enumerator_block_match 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 def test_hscan_with_encoding %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 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 def test_hscan_each_enumerator 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 def test_hscan_each_enumerator_match 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 def test_hscan_each_block 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 def test_hscan_each_block_match 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 def test_zscan_with_encoding %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 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 def test_zscan_each_enumerator 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 def test_zscan_each_enumerator_match 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 def test_zscan_each_block 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 def test_zscan_each_block_match 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 redis-rb-5.3.0/test/redis/scripting_test.rb000066400000000000000000000036661466130507100207170ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestScripting < Minitest::Test include Helper::Client def to_sha(script) r.script(:load, script) end def test_script_exists 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 def test_script_flush sha = to_sha("return 1") assert r.script(:exists, sha) assert_equal "OK", r.script(:flush) assert !r.script(:exists, sha) end def test_script_kill redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| assert_equal "KILL", redis.script(:kill) end end def test_eval 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 def test_eval_with_options_hash 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 def test_evalsha 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 def test_evalsha_with_options_hash 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 redis-rb-5.3.0/test/redis/sorting_test.rb000066400000000000000000000027221466130507100203720ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/redis/ssl_test.rb000066400000000000000000000035641466130507100175130ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class SslTest < Minitest::Test include Helper::Client 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(host: "127.0.0.1", 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(Redis::CannotConnectError) 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(Redis::CannotConnectError) 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(host: "127.0.0.1", port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) assert_equal redis.set("boom", "a" * 10_000_000), "OK" 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-5.3.0/test/redis/thread_safety_test.rb000066400000000000000000000007751466130507100215350ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestThreadSafety < Minitest::Test include Helper::Client 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 end redis-rb-5.3.0/test/redis/transactions_test.rb000066400000000000000000000163521466130507100214210ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestTransactions < Minitest::Test include Helper::Client def test_multi_discard assert_raises(LocalJumpError) do r.multi end 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_multi_in_pipeline foo_future = bar_future = nil multi_future = nil response = r.pipelined do |pipeline| multi_future = pipeline.multi do |multi| multi.set("foo", "s1") foo_future = multi.get("foo") end pipeline.multi do |multi| multi.set("bar", "s2") bar_future = multi.get("bar") end end assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response) assert_equal ["OK", "s1"], multi_future.value assert_equal "s1", foo_future.value assert_equal "s2", bar_future.value 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 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 end end assert_equal "OK", @first.value assert_raises(Redis::FutureNotReady) { @second.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 |transaction| transaction.info end assert_instance_of Hash, info end def test_transformed_replies_inside_multi_exec_block r.multi do |transaction| @info = transaction.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::MultiConnection.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-5.3.0/test/redis/unknown_commands_test.rb000066400000000000000000000003631466130507100222640ustar00rootroot00000000000000# frozen_string_literal: true require "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-5.3.0/test/redis/url_param_test.rb000066400000000000000000000056731466130507100206770ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class TestUrlParam < Minitest::Test include Helper::Client def test_url_defaults_to_localhost redis = Redis.new assert_equal "localhost", 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_unescape_password_from_url 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_override_url_if_path_option_is_passed redis = Redis.new url: "redis://:secr3t@foo.com/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_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_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 "localhost", redis._client.host end def test_ipv6_url redis = Redis.new url: "redis://[::1]" assert_equal "::1", redis._client.host end def test_user_and_password redis = Redis.new(url: 'redis://johndoe:mysecret@foo.com:999/2') assert_equal('johndoe', redis._client.username) assert_equal('mysecret', redis._client.password) assert_equal('foo.com', redis._client.host) assert_equal(999, redis._client.port) assert_equal(2, redis._client.db) end end redis-rb-5.3.0/test/sentinel/000077500000000000000000000000001466130507100160315ustar00rootroot00000000000000redis-rb-5.3.0/test/sentinel/sentinel_command_test.rb000066400000000000000000000034321466130507100227360ustar00rootroot00000000000000# frozen_string_literal: true require "helper" # @see https://redis.io/topics/sentinel#sentinel-commands Sentinel commands class SentinelCommandsTest < Minitest::Test include Helper::Sentinel def test_sentinel_command_master wait_for_quorum 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 wait_for_quorum 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 wait_for_quorum 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 wait_for_quorum 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 wait_for_quorum 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-5.3.0/test/sentinel/sentinel_test.rb000066400000000000000000000316671466130507100212530ustar00rootroot00000000000000# frozen_string_literal: true require "helper" class SentinelTest < Minitest::Test include Helper::Sentinel def test_sentinel_master_role_connection wait_for_quorum actual = redis.role assert_equal 'master', actual[0] end def test_sentinel_slave_role_connection wait_for_quorum redis = build_slave_role_client actual = redis.role assert_equal 'slave', actual[0] assert_equal MASTER_PORT.to_i, actual[2] end def test_without_reconnect wait_for_quorum redis.without_reconnect do redis.get("key") end 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] case command when "get-master-addr-by-name" ["127.0.0.1", "6381"] when "sentinels" [] else raise "Unexpected command #{[command, *args].inspect}" end 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], ["sentinels", "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] case command when "get-master-addr-by-name" ["127.0.0.1", "6381"] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end 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 [%w[get-master-addr-by-name master1]], commands[:s1] assert_equal [%w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s2] end def test_sentinel_with_non_sentinel_options commands = { s1: [], m1: [] } sentinel = lambda do |port| { auth: lambda do |*args| commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] "-ERR unknown command 'select'" end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end end } end master = { auth: lambda do |*args| commands[:m1] << ['auth', *args] '+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], ["sentinels", "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 |*args| commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] '-ERR unknown command `select`' end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end end } end master = { auth: lambda do |*args| commands[:s1] << ['auth', *args] '-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 }] r = Redis.new(name: 'master1', sentinels: s, role: :master, sentinel_password: 'foo') assert r.ping end end assert_equal [%w[auth foo], %w[get-master-addr-by-name master1], ["sentinels", "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 |*args| commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| commands[:s1] << ['select', db] '-ERR unknown command `select`' end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end end, } end master = { auth: lambda do |*args| commands[:m1] << ['auth', *args] '+OK' end, role: lambda do commands[:m1] << ['role'] ['master'] end, sentinel: lambda do |command, *args| commands[:s2] << [command, *args] 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 }] r = Redis.new(name: 'master1', sentinels: s, role: :master, password: 'bar', sentinel_password: 'foo') assert r.ping end end assert_equal [%w[auth foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1] assert_equal [%w[auth bar], %w[role]], commands[:m1] end def test_authentication_with_acl commands = { s1: [], m1: [] } sentinel = lambda do |port| { auth: lambda do |user, pass| commands[:s1] << ['auth', user, 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] case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end end } end master = { auth: lambda do |user, pass| commands[:m1] << ['auth', user, 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 }] r = Redis.new(name: 'master1', sentinels: s, role: :master, username: 'alice', password: 'bar', sentinel_username: 'bob', sentinel_password: 'foo') assert r.ping end end assert_equal [%w[auth bob foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1] assert_equal [%w[auth alice 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| case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ] else raise "Unexpected command #{[command, *args].inspect}" end end } end master = { role: lambda do ["slave"] end } ex = assert_raises(Redis::CannotConnectError) 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, reconnect_attempts: 0) assert redis.ping end end end assert_match(/Expected to connect to a master, but the server is a replica/, 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 = [] fails = Hash.new(0) handler = lambda do |id, port| { sentinel: lambda do |command, *_args| connections << id if fails[id] < 2 fails[id] += 1 :close else case command when "get-master-addr-by-name" ["127.0.0.1", port.to_s] when "sentinels" [ ["ip", "127.0.0.1", "port", "26381"], ["ip", "127.0.0.1", "port", "26382"], ] else raise "Unexpected command #{[command, *args].inspect}" end 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 s1 s2 s2 s1 s1], connections connections.clear fails.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 end redis-rb-5.3.0/test/support/000077500000000000000000000000001466130507100157245ustar00rootroot00000000000000redis-rb-5.3.0/test/support/conf/000077500000000000000000000000001466130507100166515ustar00rootroot00000000000000redis-rb-5.3.0/test/support/conf/redis-5.0.conf000066400000000000000000000000261466130507100211240ustar00rootroot00000000000000appendonly no save "" redis-rb-5.3.0/test/support/conf/redis-6.0.conf000066400000000000000000000000261466130507100211250ustar00rootroot00000000000000appendonly no save "" redis-rb-5.3.0/test/support/conf/redis-6.2.conf000066400000000000000000000000261466130507100211270ustar00rootroot00000000000000appendonly no save "" redis-rb-5.3.0/test/support/conf/redis-7.0.conf000066400000000000000000000000571466130507100211320ustar00rootroot00000000000000appendonly no save "" enable-debug-command yes redis-rb-5.3.0/test/support/conf/redis-7.2.conf000066400000000000000000000000571466130507100211340ustar00rootroot00000000000000appendonly no save "" enable-debug-command yes redis-rb-5.3.0/test/support/redis_mock.rb000066400000000000000000000065121466130507100203740ustar00rootroot00000000000000# 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) @concurrent = options.delete(:concurrent) 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(&block) loop do if @concurrent Thread.new(@server.accept) do |session| block.call(session) ensure session.close end else session = @server.accept begin return if yield(session) == :exit ensure session.close end 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.downcase.to_sym] blk ||= ->(*_) { "+OK" } response = blk.call(*argv) # Convert a nil response to :close response ||= :close case response when :exit break :exit when :close break :close when 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-5.3.0/test/support/ssl/000077500000000000000000000000001466130507100165255ustar00rootroot00000000000000redis-rb-5.3.0/test/support/ssl/gen_certs.sh000077500000000000000000000020521466130507100210340ustar00rootroot00000000000000#!/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-5.3.0/test/support/ssl/trusted-ca.crt000066400000000000000000000024761466130507100213230ustar00rootroot00000000000000-----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-5.3.0/test/support/ssl/trusted-ca.key000066400000000000000000000032541466130507100213160ustar00rootroot00000000000000-----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-5.3.0/test/support/ssl/trusted-cert.crt000066400000000000000000000107101466130507100216630ustar00rootroot00000000000000Certificate: 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-5.3.0/test/support/ssl/trusted-cert.key000066400000000000000000000032541466130507100216700ustar00rootroot00000000000000-----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-5.3.0/test/support/ssl/untrusted-ca.crt000066400000000000000000000025631466130507100216630ustar00rootroot00000000000000-----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-5.3.0/test/support/ssl/untrusted-ca.key000066400000000000000000000032501466130507100216550ustar00rootroot00000000000000-----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-5.3.0/test/support/ssl/untrusted-cert.crt000066400000000000000000000110431466130507100222260ustar00rootroot00000000000000Certificate: 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-5.3.0/test/support/ssl/untrusted-cert.key000066400000000000000000000032501466130507100222270ustar00rootroot00000000000000-----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-5.3.0/test/test.conf.erb000066400000000000000000000002631466130507100166060ustar00rootroot00000000000000dir <%= REDIS_DIR %> pidfile <%= REDIS_PID %> port 6381 unixsocket <%= REDIS_SOCKET %> timeout 300 loglevel debug logfile <%= REDIS_LOG %> databases 16 daemonize yes