diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 728531486ed90a50099ea87e32ebf74d06bbad2e..4f8d5d27592a293e266bf93500a0bccc168171ef 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,40 +10,33 @@ on: jobs: lint: name: Rubocop - timeout-minutes: 30 + timeout-minutes: 15 runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.4" - - name: Set up Gems - run: | - gem update --system --no-document - gem install bundler --no-document - bundle install --jobs 4 --retry 3 --path=.bundle + ruby-version: "3.2" + bundler-cache: true - name: Lint run: bundle exec rubocop rubies: name: Ruby - timeout-minutes: 30 + timeout-minutes: 15 strategy: fail-fast: false matrix: - ruby: ["3.1", "3.0", "2.7", "2.6", "2.5", "2.4", "jruby-9.3.6.0"] + ruby: ["3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "jruby-9.3.6.0"] runs-on: ubuntu-latest env: - VERBOSE: "true" - TIMEOUT: "30" LOW_TIMEOUT: "0.01" - DRIVER: ruby - REDIS_BRANCH: "7.0" + REDIS_BRANCH: "7.2" steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" @@ -56,7 +49,7 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Cache local temporary directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" @@ -67,23 +60,59 @@ jobs: - 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: 30 + timeout-minutes: 15 strategy: fail-fast: false matrix: - driver: ["hiredis", "synchrony"] + driver: ["hiredis"] runs-on: ubuntu-latest env: - VERBOSE: "true" - TIMEOUT: "30" LOW_TIMEOUT: "0.01" DRIVER: ${{ matrix.driver }} - REDIS_BRANCH: "7.0" + REDIS_BRANCH: "7.2" steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" @@ -93,10 +122,10 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.4" + ruby-version: "2.6" bundler-cache: true - name: Cache local temporary directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" @@ -109,21 +138,18 @@ jobs: redises: name: Redis - timeout-minutes: 30 + timeout-minutes: 15 strategy: fail-fast: false matrix: - redis: ["6.2", "6.0", "5.0"] + redis: ["7.0", "6.2", "6.0", "5.0"] runs-on: ubuntu-latest env: - VERBOSE: "true" - TIMEOUT: "30" LOW_TIMEOUT: "0.14" - DRIVER: ruby REDIS_BRANCH: ${{ matrix.redis }} steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" @@ -133,10 +159,10 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.4" + ruby-version: "2.6" bundler-cache: true - name: Cache local temporary directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-${{ matrix.redis }}-on-ubuntu-latest" @@ -149,19 +175,16 @@ jobs: sentinel: name: Sentinel - timeout-minutes: 30 + timeout-minutes: 15 strategy: fail-fast: false runs-on: ubuntu-latest env: - VERBOSE: "true" - TIMEOUT: "30" LOW_TIMEOUT: "0.14" - DRIVER: ruby REDIS_BRANCH: "7.0" steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" @@ -171,15 +194,15 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.4" + ruby-version: "2.6" bundler-cache: true - name: Cache local temporary directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis - run: make start_all + run: make start_sentinel wait_for_sentinel - name: Test run: bundle exec rake test:sentinel - name: Shutting down Redis @@ -187,19 +210,18 @@ jobs: cluster: name: Cluster - timeout-minutes: 30 + timeout-minutes: 15 strategy: fail-fast: false runs-on: ubuntu-latest env: - VERBOSE: "true" - TIMEOUT: "30" + TIMEOUT: "15" LOW_TIMEOUT: "0.14" - DRIVER: ruby - REDIS_BRANCH: "7.0" + REDIS_BRANCH: "7.2" + BUNDLE_GEMFILE: cluster/Gemfile steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Print environment variables run: | echo "TIMEOUT=${TIMEOUT}" @@ -209,15 +231,15 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.4" + ruby-version: "2.7" bundler-cache: true - name: Cache local temporary directory - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis - run: make start_all + run: make start start_cluster create_cluster - name: Test run: bundle exec rake test:cluster - name: Shutting down Redis diff --git a/.gitignore b/.gitignore index a791b8e300d667f077add10919c67942b9849085..19ff57df03ed43e8e0d0b2e303785bca529094ac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ Gemfile.lock /.idea /.yardoc /.bundle +/cluster/.bundle /coverage/* /doc/ /examples/sentinel/sentinel.conf diff --git a/.rubocop.yml b/.rubocop.yml index e9aa0b8fdb09f20cd6c520afc429da3460e38874..fc1171cddbd473582333a7d82b4440cbeec8fb93 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: 2.4 + TargetRubyVersion: 2.6 Layout/LineLength: Max: 120 @@ -56,9 +56,15 @@ Metrics/PerceivedComplexity: Style/PercentLiteralDelimiters: Enabled: false +Style/SlicingWithRange: + Enabled: false + Style/TrailingCommaInArrayLiteral: Enabled: false +Style/TrailingCommaInArguments: + Enabled: false + Style/ParallelAssignment: Enabled: false @@ -68,6 +74,9 @@ Style/NumericPredicate: Style/IfUnlessModifier: Enabled: false +Style/MutableConstant: + Enabled: false # false positives + Style/SignalException: Exclude: - 'lib/redis/connection/synchrony.rb' @@ -139,6 +148,16 @@ Metrics/BlockNesting: Style/HashTransformValues: Enabled: false +Style/TrailingCommaInHashLiteral: + Enabled: false + Style/SymbolProc: Exclude: - - 'test/**/*' \ No newline at end of file + - 'test/**/*' + +Bundler/OrderedGems: + Enabled: false + +Gemspec/RequiredRubyVersion: + Exclude: + - cluster/redis-clustering.gemspec diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 85908a89a5ae2b78264da480dc0801a88c4ffaa2..2a3d18715d20f489dceecc70e353689282a60ebd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,20 +11,6 @@ Lint/HashCompareByIdentity: Exclude: - 'lib/redis.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Lint/RedundantStringCoercion: - Exclude: - - 'examples/consistency.rb' - -# Offense count: 2 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. -# SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 -Naming/VariableNumber: - Exclude: - - 'test/remote_server_control_commands_test.rb' - # Offense count: 6 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? diff --git a/CHANGELOG.md b/CHANGELOG.md index 224bebb58293c8adf0b24ce38d2c28215e5b5458..bbdc021bc137458dc3eb0aadd575cebeb9143e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,85 @@ # Unreleased +# 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 = true`. + 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 diff --git a/Gemfile b/Gemfile index b997f52de4b76ff29be5ee4ddf8641aa815aaea4..00615dee1e628e9925d85f078af9a115e3dd6e30 100644 --- a/Gemfile +++ b/Gemfile @@ -6,4 +6,7 @@ gemspec gem 'minitest' gem 'rake' -gem 'rubocop', '~> 1.0', '< 1.12' +gem 'rubocop', '~> 1.25.1' +gem 'mocha' + +gem 'hiredis-client' diff --git a/README.md b/README.md index 202ec869deef80e5288fdcfa5cebac5d88e3baed..d4ee0ab93c3c43809e7bf39ae91acc060956651d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -# redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link] +# 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. +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. @@ -38,10 +37,6 @@ redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15") The client expects passwords with special chracters to be URL-encoded (i.e. `CGI.escape(password)`). -By default, the client will try to read the `REDIS_URL` environment variable -and use that as URL to connect to. The above statement is therefore equivalent -to setting this environment variable and calling `Redis.new` without arguments. - To connect to Redis listening on a Unix socket, try: ```ruby @@ -76,6 +71,26 @@ redis.get("mykey") 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 @@ -88,7 +103,7 @@ To connect using Sentinel, use: SENTINELS = [{ host: "127.0.0.1", port: 26380 }, { host: "127.0.0.1", port: 26381 }] -redis = Redis.new(url: "redis://mymaster", sentinels: SENTINELS, role: :master) +redis = Redis.new(name: "mymaster", sentinels: SENTINELS, role: :master) ``` * The master name identifies a group of Redis instances composed of a master @@ -105,85 +120,44 @@ but a few so that if one is down the client will try the next one. The client is able to remember the last Sentinel that was able to reply correctly and will use it for the next requests. -If you want to [authenticate](https://redis.io/topics/sentinel#configuring-sentinel-instances-with-authentication) Sentinel itself, you must specify the `password` option per instance. +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, password: 'mysecret' }, - { host: '127.0.0.1', port: 26381, password: 'mysecret' }] +SENTINELS = [{ host: '127.0.0.1', port: 26380}, + { host: '127.0.0.1', port: 26381}] -redis = Redis.new(host: 'mymaster', sentinels: SENTINELS, role: :master) +redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, sentinel_username: 'appuser', sentinel_password: 'mysecret', role: :master) ``` -## Cluster support - -`redis-rb` supports [clustering](https://redis.io/topics/cluster-spec). +If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials ```ruby -# Nodes can be passed to the client as an array of connection URLs. -nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" } -redis = Redis.new(cluster: nodes) +# 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 }] -# 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 } } +redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret') ``` -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. +So you have to provide Sentinel credential and Redis explictly even they are the same ```ruby -Redis.new(cluster: %w[redis://127.0.0.1:7000]) -``` - -If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys. +# 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 }] -```ruby -Redis.new(cluster: nodes, replica: true) +redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret') ``` -The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model). +Also the `name`, `password`, `username` and `db` for Redis instance can be passed as an url: ```ruby -redis = Redis.new(cluster: %w[redis://127.0.0.1:7000]) - -redis.mget('key1', 'key2') -#=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot) - -redis.mget('{key}1', '{key}2') -#=> [nil, nil] +redis = Redis.new(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master) ``` -* 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.new(cluster: %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.new(cluster: %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. - -## Storing objects - -Redis "string" types can be used to store serialized Ruby objects, for -example with JSON: - -```ruby -require "json" - -redis.set "foo", [1, 2, 3].to_json -# => OK +## Cluster support -JSON.parse(redis.get("foo")) -# => [1, 2, 3] -``` +[Clustering](https://redis.io/topics/cluster-spec). is supported via the [`redis-clustering` gem](cluster/). ## Pipelining @@ -206,6 +180,39 @@ 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", #<RedisClient::WrongTypeError: WRONGTYPE Operation against a key holding the wrong kind of value>, "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 @@ -225,21 +232,22 @@ end ### Futures Replies to commands in a pipeline can be accessed via the *futures* they -emit (since redis-rb 3.0). All calls on the pipeline object return a +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" + set = pipeline.set "foo", "bar" + incr = pipeline.incr "baz" end -@set.value +set.value # => "OK" -@incr.value +incr.value # => 1 ``` @@ -251,7 +259,7 @@ it can't connect to the server a `Redis::CannotConnectError` error will be raise ```ruby begin redis.ping -rescue StandardError => e +rescue Redis::BaseError => e e.inspect # => #<Redis::CannotConnectError: Timed out connecting to Redis on 10.0.1.1:6380> @@ -298,55 +306,37 @@ If no message is received after 5 seconds, the client will unsubscribe. ## Reconnections -The client allows you to configure how many `reconnect_attempts` it should -complete before declaring a connection as failed. Furthermore, you may want -to control the maximum duration between reconnection attempts with -`reconnect_delay` and `reconnect_delay_max`. +**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 => 10, - :reconnect_delay => 1.5, - :reconnect_delay_max => 10.0, -) +Redis.new(reconnect_attempts: 0) +Redis.new(reconnect_attempts: 3) ``` -The delay values are specified in seconds. With the above configuration, the -client would attempt 10 reconnections, exponentially increasing the duration -between each attempt but it never waits longer than `reconnect_delay_max`. - -This is the retry algorithm: +If you wish to wait between reconnection attempts, you can instead pass a list +of durations: ```ruby -attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min +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 +]) ``` -**By default**, this gem will only **retry a connection once** and then fail, but with the -above configuration the reconnection attempt would look like this: - -#|Attempt wait time|Total wait time -:-:|:-:|:-: -1|1.5s|1.5s -2|3.0s|4.5s -3|6.0s|10.5s -4|10.0s|20.5s -5|10.0s|30.5s -6|10.0s|40.5s -7|10.0s|50.5s -8|10.0s|60.5s -9|10.0s|70.5s -10|10.0s|80.5s - -So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying -to reconnect, this is likely fine in long-running background processes, but if -you use Redis to drive your website you might want to have a lower -`reconnect_delay_max` or have less `reconnect_attempts`. +If you wish to disable reconnection only for some commands, you can use +`disable_reconnection`: -## SSL/TLS Support +```ruby +redis.get("some-key") # this may be retried +redis.disable_reconnection do + redis.incr("some-counter") # this won't be retried. +end +``` -This library supports natively terminating client side SSL/TLS connections -when talking to Redis via a server-side proxy such as [stunnel], [hitch], -or [ghostunnel]. +## 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). @@ -381,13 +371,7 @@ redis = Redis.new( ) ``` -[stunnel]: https://www.stunnel.org/ -[hitch]: https://hitch-tls.org/ -[ghostunnel]: https://github.com/square/ghostunnel -[OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.3.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html - -*NOTE:* SSL is only supported by the default "Ruby" driver - +[OpenSSL::SSL::SSLContext documentation]: http://ruby-doc.org/stdlib-2.5.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html ## Expert-Mode Options @@ -401,17 +385,9 @@ redis = Redis.new( Improper use of `inherit_socket` will result in corrupted and/or incorrect responses. -## Alternate drivers +## hiredis binding By default, redis-rb uses Ruby's socket library to talk with Redis. -To use an alternative connection driver it should be specified as option -when instantiating the client object. These instructions are only valid -for **redis-rb 3.0**. For instructions on how to use alternate drivers from -**redis-rb 2.2**, please refer to an [older README][readme-2.2.2]. - -[readme-2.2.2]: https://github.com/redis/redis-rb/blob/v2.2.2/README.md - -### hiredis The hiredis driver uses the connection facility of hiredis-rb. In turn, hiredis-rb is a binding to the official hiredis client library. It @@ -421,41 +397,27 @@ 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: +In your Gemfile, include `hiredis-client`: ```ruby -gem "redis", "~> 3.0.1" -gem "hiredis", "~> 0.4.5" +gem "redis" +gem "hiredis-client" ``` -When instantiating the client object, specify hiredis: +If your application doesn't call `Bundler.require`, you may have +to require it explictly: ```ruby -redis = Redis.new(:driver => :hiredis) -``` - -### synchrony - -The synchrony driver adds support for [em-synchrony][em-synchrony]. -This makes redis-rb work with EventMachine's asynchronous I/O, while not -changing the exposed API. The hiredis gem needs to be available as -well, because the synchrony driver uses hiredis for parsing the Redis -protocol. +require "hiredis-client" +```` -[em-synchrony]: https://github.com/igrigorik/em-synchrony - -In your Gemfile, include em-synchrony and hiredis: - -```ruby -gem "redis", "~> 3.0.1" -gem "hiredis", "~> 0.4.5" -gem "em-synchrony" -``` +This makes the hiredis driver the default. -When instantiating the client object, specify synchrony: +If you want to be certain hiredis is being used, when instantiating +the client object, specify hiredis: ```ruby -redis = Redis.new(:driver => :synchrony) +redis = Redis.new(driver: :hiredis) ``` ## Testing @@ -480,11 +442,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra. requests. -[inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg -[inchpages-link]: https://inch-ci.org/github/redis/redis-rb -[redis-commands]: https://redis.io/commands -[redis-home]: https://redis.io -[redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis -[gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg -[gh-actions-link]: https://github.com/redis/redis-rb/actions -[rubydoc]: http://www.rubydoc.info/gems/redis +[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 diff --git a/Rakefile b/Rakefile index c8d8c4d904674fda3644a660f18654a703d59d4f..15ff13e315022d2a649f18dbaf7310ea2d7b8524 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,12 @@ # 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 cluster) + groups = %i(redis distributed sentinel) groups.each do |group| Rake::TestTask.new(group) do |t| t.libs << "test" @@ -18,6 +20,13 @@ namespace :test do 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"] diff --git a/benchmarking/cluster.rb b/benchmarking/cluster.rb deleted file mode 100644 index 8a5b9ff5e39b802b8861576734c268412c4a50a8..0000000000000000000000000000000000000000 --- a/benchmarking/cluster.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -# Execute the following commands before execution -# -# `$ make start` -# `$ make start_cluster` -# `$ make create_cluster` - -require 'redis' -require 'benchmark' - -HOST = '127.0.0.1' -STANDALONE_PORT = 6381 -CLUSTER_PORT = 7000 -N = (ARGV.first || 100_000).to_i - -rn = Redis.new(host: HOST, port: STANDALONE_PORT) -rc = Redis.new(host: HOST, port: CLUSTER_PORT) -rm = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}]) -rs = Redis.new(cluster: %W[redis://#{HOST}:#{CLUSTER_PORT}], replica: true) - -Benchmark.bmbm do |bm| - bm.report('client: normal, server: standalone, command: SET, key: fixed') do - N.times { rn.set('foo', '42') } - end - - bm.report('client: normal, server: standalone, command: GET, key: fixed') do - N.times { rn.get('foo') } - end - - bm.report('client: normal, server: cluster, command: SET, key: fixed') do - N.times { rc.set('bar', '42') } - end - - bm.report('client: normal, server: cluster, command: GET, key: fixed') do - N.times { rc.get('bar') } - end - - bm.report('client: cluster, server: cluster, command: SET, key: fixed') do - N.times { rm.set('baz', '42') } - end - - rm.wait(1, 0) - bm.report('client: cluster, server: cluster, command: GET, key: fixed') do - N.times { rm.get('baz') } - end - - bm.report('client: cluster, server: cluster, command: SET, key: fixed, replica: true') do - N.times { rs.set('zap', '42') } - end - - rs.wait(1, 0) - bm.report('client: cluster, server: cluster, command: GET, key: fixed, replica: true') do - N.times { rs.get('zap') } - end - - bm.report('client: normal, server: standalone, command: SET, key: variable') do - N.times { |i| rn.set("foo:#{i}", '42') } - end - - bm.report('client: normal, server: standalone, command: GET, key: variable') do - N.times { |i| rn.get("foo:#{i}") } - end - - bm.report('client: cluster, server: cluster, command: SET, key: variable') do - N.times { |i| rm.set("bar:#{i}", '42') } - end - - rm.wait(1, 0) - bm.report('client: cluster, server: cluster, command: GET, key: variable') do - N.times { |i| rm.get("bar:#{i}") } - end - - bm.report('client: cluster, server: cluster, command: SET, key: variable, replica: true') do - N.times { |i| rs.set("baz:#{i}", '42') } - end - - rs.wait(1, 0) - bm.report('client: cluster, server: cluster, command: GET, key: variable, replica: true') do - N.times { |i| rs.get("baz:#{i}") } - end - - rn.set('bar', 0) - bm.report('client: normal, server: standalone, command: INCR, key: fixed') do - N.times { rn.incr('bar') } - end - - rc.set('bar', 0) - bm.report('client: normal, server: cluster, command: INCR, key: fixed') do - N.times { rc.incr('bar') } - end - - rm.set('bar', 0) - bm.report('client: cluster, server: cluster, command: INCR, key: fixed') do - N.times { rm.incr('bar') } - end -end diff --git a/benchmarking/cluster_slot.rb b/benchmarking/cluster_slot.rb deleted file mode 100644 index a374aff8627c279dccad4767ac939748e72ff559..0000000000000000000000000000000000000000 --- a/benchmarking/cluster_slot.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'redis' -require 'benchmark' - -N = (ARGV.first || 100_000).to_i - -available_slots = { - "127.0.0.1:7000" => [0..5460], - "127.0.0.1:7003" => [0..5460], - "127.0.0.1:7001" => [5461..10_922], - "127.0.0.1:7004" => [5461..10_922], - "127.0.0.1:7002" => [10_923..16_383], - "127.0.0.1:7005" => [10_923..16_383] -} - -node_flags = { - "127.0.0.1:7000" => "master", - "127.0.0.1:7002" => "master", - "127.0.0.1:7001" => "master", - "127.0.0.1:7005" => "slave", - "127.0.0.1:7004" => "slave", - "127.0.0.1:7003" => "slave" -} - -Benchmark.bmbm do |bm| - bm.report('Slot.new') do - allocs = GC.stat(:total_allocated_objects) - - N.times do - Redis::Cluster::Slot.new(available_slots, node_flags, false) - end - - puts GC.stat(:total_allocated_objects) - allocs - end -end diff --git a/benchmarking/logging.rb b/benchmarking/logging.rb deleted file mode 100644 index 7b09a4609235fed17748d752ccc4014d8f9f7271..0000000000000000000000000000000000000000 --- a/benchmarking/logging.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -# Run with -# -# $ ruby -Ilib benchmarking/logging.rb -# - -begin - require "bench" -rescue LoadError - warn "`gem install bench` and try again." - exit 1 -end - -require "redis" -require "logger" - -def log(level, namespace = nil) - logger = (namespace || Kernel).const_get(:Logger).new("/dev/null") - logger.level = (namespace || Logger).const_get(level) - logger -end - -def stress(redis) - redis.flushdb - - n = (ARGV.shift || 2000).to_i - - n.times do |i| - key = "foo:#{i}" - redis.set key, i - redis.get key - end -end - -default = Redis.new - -logging_redises = [ - Redis.new(logger: log(:DEBUG)), - Redis.new(logger: log(:INFO)) -] - -begin - require "log4r" - - logging_redises += [ - Redis.new(logger: log(:DEBUG, Log4r)), - Redis.new(logger: log(:INFO, Log4r)) - ] -rescue LoadError - warn "Log4r not installed. `gem install log4r` if you want to compare it against Ruby's " \ - "Logger (spoiler: it's much faster)." -end - -benchmark "Default options (no logger)" do - stress(default) -end - -logging_redises.each do |redis| - logger = redis._client.logger - - case logger - when Logger - level = Logger::SEV_LABEL[logger.level] - when Log4r::Logger - level = logger.levels[logger.level] - end - - benchmark "#{logger.class} on #{level}" do - stress(redis) - end -end - -run 10 diff --git a/benchmarking/pipeline.rb b/benchmarking/pipeline.rb deleted file mode 100644 index 84b20dcc70c5befef11e5b77402433f02331874f..0000000000000000000000000000000000000000 --- a/benchmarking/pipeline.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require "benchmark" - -$LOAD_PATH.push File.join(File.dirname(__FILE__), 'lib') - -require 'redis' - -ITERATIONS = 10_000 - -@r = Redis.new - -Benchmark.bmbm do |benchmark| - benchmark.report("set") do - @r.flushdb - - ITERATIONS.times do |i| - @r.set("foo#{i}", "Hello world!") - @r.get("foo#{i}") - end - end - - benchmark.report("set (pipelined)") do - @r.flushdb - - @r.pipelined do - ITERATIONS.times do |i| - @r.set("foo#{i}", "Hello world!") - @r.get("foo#{i}") - end - end - end - - benchmark.report("lpush+ltrim") do - @r.flushdb - - ITERATIONS.times do |i| - @r.lpush "lpush#{i}", i - @r.ltrim "ltrim#{i}", 0, 30 - end - end - - benchmark.report("lpush+ltrim (pipelined)") do - @r.flushdb - - @r.pipelined do - ITERATIONS.times do |i| - @r.lpush "lpush#{i}", i - @r.ltrim "ltrim#{i}", 0, 30 - end - end - end -end diff --git a/benchmarking/speed.rb b/benchmarking/speed.rb deleted file mode 100644 index cb5bc46a74fff5791ff2bfb2bbf4ec3872829795..0000000000000000000000000000000000000000 --- a/benchmarking/speed.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -$LOAD_PATH.push File.join(__dir__, 'lib') - -require "benchmark" -require "redis" - -r = Redis.new -n = (ARGV.shift || 20_000).to_i - -elapsed = Benchmark.realtime do - # n sets, n gets - n.times do |i| - key = "foo#{i}" - r.set(key, key * 10) - r.get(key) - end -end - -puts '%.2f Kops' % (2 * n / 1000 / elapsed) diff --git a/benchmarking/suite.rb b/benchmarking/suite.rb deleted file mode 100644 index de7dfe0efc525688df04e9375c2f6116be6aee24..0000000000000000000000000000000000000000 --- a/benchmarking/suite.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' - -def run_in_background(command) - fork { system command } -end - -def with_all_segments(&block) - 0.upto(9) do |segment_number| - block_size = 100_000 - start_index = segment_number * block_size - end_index = start_index + block_size - 1 - block.call(start_index, end_index) - end -end - -# with_all_segments do |start_index, end_index| -# puts "Initializing keys from #{start_index} to #{end_index}" -# system "ruby worker.rb initialize #{start_index} #{end_index} 0" -# end - -with_all_segments do |start_index, end_index| - run_in_background "ruby worker.rb write #{start_index} #{end_index} 10" - run_in_background "ruby worker.rb read #{start_index} #{end_index} 1" -end diff --git a/benchmarking/worker.rb b/benchmarking/worker.rb deleted file mode 100644 index 15c6e5ac1c3a128cc173105e4fff151854961821..0000000000000000000000000000000000000000 --- a/benchmarking/worker.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -BENCHMARK_ROOT = File.dirname(__FILE__) -REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib") - -$LOAD_PATH << REDIS_ROOT -require 'redis' -require 'benchmark' - -def show_usage - puts <<-EOL - Usage: worker.rb [read:write] <start_index> <end_index> <sleep_msec> - EOL -end - -def shift_from_argv - value = ARGV.shift - unless value - show_usage - exit(-1) - end - value -end - -operation = shift_from_argv.to_sym -start_index = shift_from_argv.to_i -end_index = shift_from_argv.to_i -sleep_msec = shift_from_argv.to_i -sleep_duration = sleep_msec / 1000.0 - -redis = Redis.new - -case operation -when :initialize - - start_index.upto(end_index) do |i| - redis[i] = 0 - end - -when :clear - - start_index.upto(end_index) do |i| - redis.delete(i) - end - -when :read, :write - - puts "Starting to #{operation} at segment #{end_index + 1}" - - loop do - t1 = Time.now - start_index.upto(end_index) do |i| - case operation - when :read - redis.get(i) - when :write - redis.incr(i) - else - raise "Unknown operation: #{operation}" - end - sleep sleep_duration - end - t2 = Time.now - - requests_processed = end_index - start_index - time = t2 - t1 - puts "#{t2.strftime('%H:%M')} [segment #{end_index + 1}] : Processed #{requests_processed} requests " \ - "in #{time} seconds - #{(requests_processed / time).round} requests/sec" - end - - else - raise "Unknown operation: #{operation}" -end diff --git a/bin/cluster_creator b/bin/cluster_creator index cacca0c5f248e27fd9153820ad24e772b28ef60b..5e9761fa7ad233ea7d7c28f89a5e166d0e3f783a 100755 --- a/bin/cluster_creator +++ b/bin/cluster_creator @@ -1,11 +1,13 @@ #!/usr/bin/env ruby - # frozen_string_literal: true +puts ARGV.join(" ") +require 'bundler/setup' + $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) -require_relative '../test/support/cluster/orchestrator' +require_relative '../cluster/test/support/orchestrator' urls = ARGV.map { |host_port| "redis://#{host_port}" } -orchestrator = ClusterOrchestrator.new(urls, timeout: 30.0) +orchestrator = ClusterOrchestrator.new(urls, timeout: 3.0) orchestrator.rebuild orchestrator.close diff --git a/bors.toml b/bors.toml deleted file mode 100644 index e22ed32805cf9d3122efbc790195c50e0b18b52e..0000000000000000000000000000000000000000 --- a/bors.toml +++ /dev/null @@ -1,14 +0,0 @@ -# Gate on Travis CI -status = ["continuous-integration/travis-ci/push"] - -# Set bors's timeout to 6 hours -# -# bors's timeout should always be twice a long as the test suite takes. -# This is to allow Travis to fast-fail a test; if one of the builders -# immediately reports a failure, then bors will move on to the next batch, -# leaving the slower builders to work through the already-doomed run and -# the next one. -# -# At the time it was written, a run had taken 3 hours. -# bors's default timeout is 4 hours. -timeout_sec = 14400 diff --git a/cluster/CHANGELOG.md b/cluster/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/cluster/Gemfile b/cluster/Gemfile new file mode 100644 index 0000000000000000000000000000000000000000..ff5d5832afeb20b45760d04e3a5dadfe0949bb86 --- /dev/null +++ b/cluster/Gemfile @@ -0,0 +1,11 @@ +# 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' diff --git a/cluster/LICENSE b/cluster/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..5e648fa9d911c0593ab260692786e57475bb7e90 --- /dev/null +++ b/cluster/LICENSE @@ -0,0 +1,20 @@ +Copyright (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. \ No newline at end of file diff --git a/cluster/README.md b/cluster/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d4722207716eff7721bd90aea541f7cb2dc078b0 --- /dev/null +++ b/cluster/README.md @@ -0,0 +1,77 @@ +# 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. diff --git a/cluster/lib/redis-clustering.rb b/cluster/lib/redis-clustering.rb new file mode 100644 index 0000000000000000000000000000000000000000..d0343944035491ea25439e64d3052a2b62847f6b --- /dev/null +++ b/cluster/lib/redis-clustering.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "redis/cluster" diff --git a/cluster/lib/redis/cluster.rb b/cluster/lib/redis/cluster.rb new file mode 100644 index 0000000000000000000000000000000000000000..5abbb628c72975672d64c7573245eb978ad1826c --- /dev/null +++ b/cluster/lib/redis/cluster.rb @@ -0,0 +1,112 @@ +# 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<Integer, Float>] :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, Hash{Symbol => 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 + + def watch(*keys, &block) + synchronize { |c| c.call_v([: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" diff --git a/cluster/lib/redis/cluster/client.rb b/cluster/lib/redis/cluster/client.rb new file mode 100644 index 0000000000000000000000000000000000000000..a0e889926496b181e64761bb76e9113f61b01595 --- /dev/null +++ b/cluster/lib/redis/cluster/client.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'redis-cluster-client' + +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 + @router.node_keys.join(' ') + end + + def server_url + @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 + + private + + def handle_errors + yield + rescue ::RedisClient::Error => error + Redis::Cluster::Client.translate_error!(error) + end + end + end +end diff --git a/cluster/lib/redis/cluster/version.rb b/cluster/lib/redis/cluster/version.rb new file mode 100644 index 0000000000000000000000000000000000000000..1e5507a70c3ef79b380a8159463c0918f6b3ea73 --- /dev/null +++ b/cluster/lib/redis/cluster/version.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "redis/version" + +class Redis + class Cluster + VERSION = Redis::VERSION + end +end diff --git a/cluster/redis-clustering.gemspec b/cluster/redis-clustering.gemspec new file mode 100644 index 0000000000000000000000000000000000000000..05194217bd52874268cfaa93f5f14778af95cf47 --- /dev/null +++ b/cluster/redis-clustering.gemspec @@ -0,0 +1,51 @@ +# 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.7.11') +end diff --git a/test/cluster/blocking_commands_test.rb b/cluster/test/blocking_commands_test.rb similarity index 78% rename from test/cluster/blocking_commands_test.rb rename to cluster/test/blocking_commands_test.rb index 1e6acf6f51134e4424f6d6232c2560fbcb9709b0..6adc0265639f1070da80277c81d1ed072624edba 100644 --- a/test/cluster/blocking_commands_test.rb +++ b/cluster/test/blocking_commands_test.rb @@ -9,6 +9,6 @@ class TestClusterBlockingCommands < Minitest::Test def mock(options = {}, &blk) commands = build_mock_commands(options) - redis_cluster_mock(commands, { timeout: LOW_TIMEOUT }, &blk) + redis_cluster_mock(commands, { timeout: LOW_TIMEOUT, concurrent: true }, &blk) end end diff --git a/test/cluster/client_internals_test.rb b/cluster/test/client_internals_test.rb similarity index 50% rename from test/cluster/client_internals_test.rb rename to cluster/test/client_internals_test.rb index 02e3eeb21cab8854bddf29d84b17d36ffcbb847b..2f5d660d52b7222e28015afcfae86e7c7cdd0423 100644 --- a/test/cluster/client_internals_test.rb +++ b/cluster/test/client_internals_test.rb @@ -21,24 +21,16 @@ class TestClusterClientInternals < Minitest::Test end end - def test_with_reconnect - assert_equal('Hello World', redis.with_reconnect { 'Hello World' }) - end - - def test_without_reconnect - assert_equal('Hello World', redis.without_reconnect { 'Hello World' }) - end - def test_connected? assert_equal true, redis.connected? end def test_close - assert_equal true, redis.close + redis.close end def test_disconnect! - assert_equal true, redis.disconnect! + redis.disconnect! end def test_asking @@ -46,39 +38,25 @@ class TestClusterClientInternals < Minitest::Test end def test_id - expected = 'redis://127.0.0.1:7000/0 '\ - 'redis://127.0.0.1:7001/0 '\ - 'redis://127.0.0.1:7002/0' + 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 = "#<Redis client v#{Redis::VERSION} for "\ - 'redis://127.0.0.1:7000/0 '\ - 'redis://127.0.0.1:7001/0 '\ - 'redis://127.0.0.1:7002/0>' + '127.0.0.1:16380 '\ + '127.0.0.1:16381 '\ + '127.0.0.1:16382>' assert_equal expected, redis.inspect end - def test_dup - assert_instance_of Redis, redis.dup - end - - def test_connection - expected = [ - { host: '127.0.0.1', port: 7000, db: 0, id: 'redis://127.0.0.1:7000/0', location: '127.0.0.1:7000' }, - { host: '127.0.0.1', port: 7001, db: 0, id: 'redis://127.0.0.1:7001/0', location: '127.0.0.1:7001' }, - { host: '127.0.0.1', port: 7002, db: 0, id: 'redis://127.0.0.1:7002/0', location: '127.0.0.1:7002' } - ] - - assert_equal expected, redis.connection - end - def test_acl_auth_success target_version "6.0.0" do with_acl do |username, password| - r = _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" }) + r = _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:#{password}@#{DEFAULT_HOST}:#{port}" }) assert_equal('PONG', r.ping) end end @@ -88,7 +66,7 @@ class TestClusterClientInternals < Minitest::Test target_version "6.0.0" do with_acl do |username, _| assert_raises(Redis::Cluster::InitialSetupError) do - _new_client(cluster: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" }) + _new_client(nodes: DEFAULT_PORTS.map { |port| "redis://#{username}:wrongpassword@#{DEFAULT_HOST}:#{port}" }) end end end diff --git a/test/cluster/client_pipelining_test.rb b/cluster/test/client_pipelining_test.rb similarity index 61% rename from test/cluster/client_pipelining_test.rb rename to cluster/test/client_pipelining_test.rb index 62d6bcbae8d2694a6dc151a22843801998d2c286..4116c9a536e6de16b80fffc588fd9779b27b3f6f 100644 --- a/test/cluster/client_pipelining_test.rb +++ b/cluster/test/client_pipelining_test.rb @@ -38,35 +38,42 @@ class TestClusterClientPipelining < Minitest::Test end def test_pipelining_without_hash_tags - assert_raises(Redis::Cluster::CrossSlotPipeliningError) do - redis.pipelined do - redis.set(:a, 1) - redis.set(:b, 2) - redis.set(:c, 3) - redis.set(:d, 4) - redis.set(:e, 5) - redis.set(:f, 6) + 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 - redis.get(:a) - redis.get(:b) - redis.get(:c) - redis.get(:d) - redis.get(:e) - redis.get(:f) - end + 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_pipelining_with_multiple_replicas - rc = build_another_client(replica: true) - rc.instance_variable_get(:@client).instance_variable_get(:@slot).instance_variable_get(:@map).each do |_, v| - v[:slaves] << v[:master] if v[:slaves].size < 2 # reproducing multiple replicas + 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 - rc.pipelined do |r| - 10.times { r.get('key1') } + 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 - - rc.close end end diff --git a/cluster/test/client_replicas_test.rb b/cluster/test/client_replicas_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..e6926d52e3000fc21315ab51b429e8edaf2f5790 --- /dev/null +++ b/cluster/test/client_replicas_test.rb @@ -0,0 +1,22 @@ +# 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 diff --git a/cluster/test/client_transactions_test.rb b/cluster/test/client_transactions_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..4258200e7f6de138f90340b5159c9cb40a20c27c --- /dev/null +++ b/cluster/test/client_transactions_test.rb @@ -0,0 +1,72 @@ +# 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 |r| + r.set('counter', '0') + r.incr('counter') + r.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 |r| + r.mset('{key}1', 1, '{key}2', 2) + r.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 |r| + r.set('key1', 1) + r.set('key2', 2) + r.set('key3', 3) + r.set('key4', 4) + end + end + + assert_raises(Redis::Cluster::TransactionConsistencyError) do + redis.multi do |r| + r.mset('key1', 1, 'key2', 2) + r.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 |tx| + another.resume + v1 = redis.get('{key}1') + v2 = redis.get('{key}2') + tx.call('SET', '{key}1', v2) + tx.call('SET', '{key}2', v1) + end + + assert_equal %w[3 4], redis.mget('{key}1', '{key}2') + end +end diff --git a/test/cluster/commands_on_cluster_test.rb b/cluster/test/commands_on_cluster_test.rb similarity index 86% rename from test/cluster/commands_on_cluster_test.rb rename to cluster/test/commands_on_cluster_test.rb index 4500e35e2726582656f7d68d74067f9056370429..a6fea1bbdd9c51344dc268fba407b0ca1b21f337 100644 --- a/test/cluster/commands_on_cluster_test.rb +++ b/cluster/test/commands_on_cluster_test.rb @@ -63,14 +63,6 @@ class TestClusterCommandsOnCluster < Minitest::Test assert_equal '3', info.fetch('cluster_size') end - def test_cluster_keyslot - assert_equal Redis::Cluster::KeySlotConverter.convert('hogehoge'), redis.cluster(:keyslot, 'hogehoge') - assert_equal Redis::Cluster::KeySlotConverter.convert('12345'), redis.cluster(:keyslot, '12345') - assert_equal Redis::Cluster::KeySlotConverter.convert('foo'), redis.cluster(:keyslot, 'boo{foo}woo') - assert_equal Redis::Cluster::KeySlotConverter.convert('antirez.is.cool'), redis.cluster(:keyslot, 'antirez.is.cool') - assert_equal Redis::Cluster::KeySlotConverter.convert(''), redis.cluster(:keyslot, '') - end - def test_cluster_meet assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER MEET command should be...') do redis.cluster(:meet, '127.0.0.1', 11_211) @@ -146,9 +138,11 @@ class TestClusterCommandsOnCluster < Minitest::Test assert_equal true, sample_slot.fetch('master').key?('node_id') assert_equal true, sample_slot.key?('replicas') assert_equal true, sample_slot.fetch('replicas').is_a?(Array) - assert_equal true, sample_slot.fetch('replicas').first.key?('ip') - assert_equal true, sample_slot.fetch('replicas').first.key?('port') - assert_equal true, sample_slot.fetch('replicas').first.key?('node_id') + 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 diff --git a/test/cluster/commands_on_connection_test.rb b/cluster/test/commands_on_connection_test.rb similarity index 100% rename from test/cluster/commands_on_connection_test.rb rename to cluster/test/commands_on_connection_test.rb diff --git a/cluster/test/commands_on_geo_test.rb b/cluster/test/commands_on_geo_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..6dc94a9675f2b43382b54ac39192ed1517ba70a6 --- /dev/null +++ b/cluster/test/commands_on_geo_test.rb @@ -0,0 +1,60 @@ +# 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 diff --git a/test/cluster/commands_on_hashes_test.rb b/cluster/test/commands_on_hashes_test.rb similarity index 100% rename from test/cluster/commands_on_hashes_test.rb rename to cluster/test/commands_on_hashes_test.rb diff --git a/test/cluster/commands_on_hyper_log_log_test.rb b/cluster/test/commands_on_hyper_log_log_test.rb similarity index 100% rename from test/cluster/commands_on_hyper_log_log_test.rb rename to cluster/test/commands_on_hyper_log_log_test.rb diff --git a/test/cluster/commands_on_keys_test.rb b/cluster/test/commands_on_keys_test.rb similarity index 82% rename from test/cluster/commands_on_keys_test.rb rename to cluster/test/commands_on_keys_test.rb index 0d319e74d9cddd0d0f21aeb77128e8090aa494e0..6227e5b46e3a9ec75638494eca471366fcb6f59f 100644 --- a/test/cluster/commands_on_keys_test.rb +++ b/cluster/test/commands_on_keys_test.rb @@ -47,8 +47,6 @@ class TestClusterCommandsOnKeys < Minitest::Test def test_object redis.lpush('mylist', 'Hello World') assert_equal 1, redis.object('refcount', 'mylist') - expected_encoding = version < '3.2.0' ? 'ziplist' : 'quicklist' - assert_equal expected_encoding, redis.object('encoding', 'mylist') assert(redis.object('idletime', 'mylist') >= 0) redis.set('foo', 1000) @@ -93,29 +91,25 @@ class TestClusterCommandsOnKeys < Minitest::Test end def test_touch - target_version('3.2.1') do - set_some_keys - assert_equal 1, redis.touch('key1') - assert_equal 1, redis.touch('key2') - if version < '6' - assert_equal 1, redis.touch('key1', 'key2') - else - assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do - redis.touch('key1', 'key2') - end + 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 - assert_equal 2, redis.touch('{key}1', '{key}2') end + assert_equal 2, redis.touch('{key}1', '{key}2') end def test_unlink - target_version('4.0.0') do - set_some_keys - assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do - redis.unlink('key1', 'key2', 'key3') - end - assert_equal 2, redis.unlink('{key}1', '{key}2', '{key}3') + 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 diff --git a/test/cluster/commands_on_lists_test.rb b/cluster/test/commands_on_lists_test.rb similarity index 100% rename from test/cluster/commands_on_lists_test.rb rename to cluster/test/commands_on_lists_test.rb diff --git a/cluster/test/commands_on_pub_sub_test.rb b/cluster/test/commands_on_pub_sub_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..107f35a7b73142eec2580f2e53933904cc9abc15 --- /dev/null +++ b/cluster/test/commands_on_pub_sub_test.rb @@ -0,0 +1,108 @@ +# 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 diff --git a/test/cluster/commands_on_scripting_test.rb b/cluster/test/commands_on_scripting_test.rb similarity index 91% rename from test/cluster/commands_on_scripting_test.rb rename to cluster/test/commands_on_scripting_test.rb index 0ad8a6abc7998702e225bfe065e254d279316a88..b0af1dda09be9b968314805a117925a078814b87 100644 --- a/test/cluster/commands_on_scripting_test.rb +++ b/cluster/test/commands_on_scripting_test.rb @@ -28,10 +28,8 @@ class TestClusterCommandsOnScripting < Minitest::Test end def test_script_debug - target_version('3.2.0') do - assert_equal 'OK', redis.script(:debug, 'yes') - assert_equal 'OK', redis.script(:debug, 'no') - end + assert_equal 'OK', redis.script(:debug, 'yes') + assert_equal 'OK', redis.script(:debug, 'no') end def test_script_exists diff --git a/test/cluster/commands_on_server_test.rb b/cluster/test/commands_on_server_test.rb similarity index 78% rename from test/cluster/commands_on_server_test.rb rename to cluster/test/commands_on_server_test.rb index f52400a7172413def5004da373ecbfa7f46fa134..3fc36a7a15441e1e16f1f2a832c438b164fe5afd 100644 --- a/test/cluster/commands_on_server_test.rb +++ b/cluster/test/commands_on_server_test.rb @@ -21,9 +21,11 @@ class TestClusterCommandsOnServer < Minitest::Test 'Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever possible.' redis_cluster_mock(bgsave: ->(*_) { "-Error #{err_msg}" }) do |redis| - assert_raises(Redis::Cluster::CommandErrorCollection, 'Command error replied on any node') do + 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 @@ -41,12 +43,8 @@ class TestClusterCommandsOnServer < Minitest::Test def test_client_list a_client_info = redis.client(:list).first - actual = a_client_info.keys.sort - expected = %w[addr age cmd db events fd flags id idle multi name obl oll omem psub qbuf qbuf-free sub] - expected << 'user' << 'argv-mem' << 'tot-mem' if version >= '6' - expected << 'laddr' << 'redir' if version >= '6.2' - expected << "multi-mem" << "rbp" << "rbs" << "resp" << "ssub" if version >= '7.0' - assert_equal expected.sort, actual.sort + assert_instance_of Hash, a_client_info + assert_includes a_client_info, 'addr' end def test_client_getname @@ -59,9 +57,7 @@ class TestClusterCommandsOnServer < Minitest::Test end def test_client_reply - target_version('3.2.0') do - assert_equal 'OK', redis.client(:reply, 'ON') - end + assert_equal 'OK', redis.client(:reply, 'ON') end def test_client_setname @@ -143,40 +139,28 @@ class TestClusterCommandsOnServer < Minitest::Test end def test_memory_doctor - target_version('4.0.0') do - assert_instance_of String, redis.memory(:doctor) - end + assert_instance_of String, redis.memory(:doctor) end def test_memory_help - target_version('4.0.0') do - assert_instance_of Array, redis.memory(:help) - end + assert_instance_of Array, redis.memory(:help) end def test_memory_malloc_stats - target_version('4.0.0') do - assert_instance_of String, redis.memory('malloc-stats') - end + assert_instance_of String, redis.memory('malloc-stats') end def test_memory_purge - target_version('4.0.0') do - assert_equal 'OK', redis.memory(:purge) - end + assert_equal 'OK', redis.memory(:purge) end def test_memory_stats - target_version('4.0.0') do - assert_instance_of Array, redis.memory(:stats) - end + assert_instance_of Array, redis.memory(:stats) end def test_memory_usage - target_version('4.0.0') do - redis.set('key1', 'Hello World') - assert_operator redis.memory(:usage, 'key1'), :>, 0 - end + redis.set('key1', 'Hello World') + assert_operator redis.memory(:usage, 'key1'), :>, 0 end def test_monitor diff --git a/test/cluster/commands_on_sets_test.rb b/cluster/test/commands_on_sets_test.rb similarity index 100% rename from test/cluster/commands_on_sets_test.rb rename to cluster/test/commands_on_sets_test.rb diff --git a/test/cluster/commands_on_sorted_sets_test.rb b/cluster/test/commands_on_sorted_sets_test.rb similarity index 100% rename from test/cluster/commands_on_sorted_sets_test.rb rename to cluster/test/commands_on_sorted_sets_test.rb diff --git a/test/cluster/commands_on_streams_test.rb b/cluster/test/commands_on_streams_test.rb similarity index 100% rename from test/cluster/commands_on_streams_test.rb rename to cluster/test/commands_on_streams_test.rb diff --git a/test/cluster/commands_on_strings_test.rb b/cluster/test/commands_on_strings_test.rb similarity index 100% rename from test/cluster/commands_on_strings_test.rb rename to cluster/test/commands_on_strings_test.rb diff --git a/cluster/test/commands_on_transactions_test.rb b/cluster/test/commands_on_transactions_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..0877f4c7beea8951801b7607c4ad483369165bf2 --- /dev/null +++ b/cluster/test/commands_on_transactions_test.rb @@ -0,0 +1,66 @@ +# 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_raises(ArgumentError) do + redis.multi {} + end + + 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('key1', 'key2') do |tx| + tx.call('SET', 'key1', '1') + tx.call('SET', 'key2', '2') + end + end + + assert_raises(Redis::Cluster::TransactionConsistencyError) do + redis.watch('{hey}1', '{hey}2') do |tx| + tx.call('SET', '{key}1', '1') + tx.call('SET', '{key}2', '2') + end + end + + redis.watch('{key}1', '{key}2') do |tx| + tx.call('SET', '{key}1', '1') + tx.call('SET', '{key}2', '2') + end + + assert_equal %w[1 2], redis.mget('{key}1', '{key}2') + end +end diff --git a/test/cluster/commands_on_value_types_test.rb b/cluster/test/commands_on_value_types_test.rb similarity index 100% rename from test/cluster/commands_on_value_types_test.rb rename to cluster/test/commands_on_value_types_test.rb diff --git a/cluster/test/helper.rb b/cluster/test/helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6ba79aa2d1bf7b00df81f33b473e66b5b71b4a6 --- /dev/null +++ b/cluster/test/helper.rb @@ -0,0 +1,161 @@ +# 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] <ip>:<port> + # @param dest [String] <ip>:<port> + 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 diff --git a/test/support/cluster/orchestrator.rb b/cluster/test/support/orchestrator.rb similarity index 83% rename from test/support/cluster/orchestrator.rb rename to cluster/test/support/orchestrator.rb index 85336aaf29f0d6e27a74be644938695ddff86efb..fd52dec35e9ea55a5f5701432388fe762b87cada 100644 --- a/test/support/cluster/orchestrator.rb +++ b/cluster/test/support/orchestrator.rb @@ -9,11 +9,7 @@ class ClusterOrchestrator raise 'Redis Cluster requires at least 3 master nodes.' if node_addrs.size < 3 @clients = node_addrs.map do |addr| - Redis.new(url: addr, - timeout: timeout, - reconnect_attempts: 10, - reconnect_delay: 1.5, - reconnect_delay_max: 10.0) + Redis.new(url: addr, timeout: timeout, reconnect_attempts: [0, 0.5, 1, 1.5]) end @timeout = timeout end @@ -82,15 +78,13 @@ class ClusterOrchestrator break if keys.empty? keys.each do |k| - begin - src_client.migrate(k, host: dest_host, port: dest_port) - rescue Redis::CommandError => err - raise unless err.message.start_with?('IOERR') + src_client.migrate(k, host: dest_host, port: dest_port) + 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 + src_client.migrate(k, host: dest_host, port: dest_port, replace: true) # retry once + ensure + keys_count -= 1 end end end @@ -108,12 +102,10 @@ class ClusterOrchestrator def flush_all_data(clients) clients.each do |c| - begin - c.flushall - rescue Redis::CommandError - # READONLY You can't write against a read only slave. - nil - end + c.flushall(async: true) + rescue Redis::CommandError + # READONLY You can't write against a read only slave. + nil end end @@ -138,12 +130,10 @@ class ClusterOrchestrator def save_config_epoch(clients) clients.each_with_index do |c, i| - begin - c.cluster('set-config-epoch', i + 1) - rescue Redis::CommandError - # ERR Node config epoch is already non-zero - nil - end + c.cluster('set-config-epoch', i + 1) + rescue Redis::CommandError + # ERR Node config epoch is already non-zero + nil end end @@ -160,7 +150,7 @@ class ClusterOrchestrator end end - def wait_meeting(clients, max_attempts: 600) + def wait_meeting(clients, max_attempts: 60) size = clients.size.to_s wait_for_state(clients, max_attempts) do |client| @@ -198,21 +188,21 @@ class ClusterOrchestrator clients.each { |c| c.cluster(:saveconfig) } end - def wait_cluster_building(clients, max_attempts: 600) + 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: 600) + 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: 600) + 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' @@ -227,21 +217,19 @@ class ClusterOrchestrator end end - def wait_cluster_recovering(clients, max_attempts: 600) + def wait_cluster_recovering(clients, max_attempts: 60) key = 0 wait_for_state(clients, max_attempts) do |client| - begin - client.get(key) if client.role.first == 'master' + 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 - rescue Redis::CommandError => err - if err.message.start_with?('CLUSTERDOWN') - false - elsif err.message.start_with?('MOVED') - key += 1 - false - else - true - end end end end diff --git a/examples/consistency.rb b/examples/consistency.rb index 3c54eaf5fdbcfc821745db5ec21681569a8b1843..e56e049df7d2802e027363c30a5c1bd495a5a761 100644 --- a/examples/consistency.rb +++ b/examples/consistency.rb @@ -52,7 +52,7 @@ class ConsistencyTester def genkey # Write more often to a small subset of keys ks = rand > 0.5 ? @keyspace : @working_set - "#{@prefix}key_#{rand(ks).to_s}" + "#{@prefix}key_#{rand(ks)}" end def check_consistency(key, value) diff --git a/examples/sentinel/start b/examples/sentinel/start index 2ffbfa307d06967cc45b985591102024419a3f81..547d63bb5fa70330ce0c97e5252430119fa977bc 100755 --- a/examples/sentinel/start +++ b/examples/sentinel/start @@ -13,10 +13,8 @@ pids = [] at_exit do pids.each do |pid| - begin - Process.kill(:INT, pid) - rescue Errno::ESRCH - end + Process.kill(:INT, pid) + rescue Errno::ESRCH end Process.waitall diff --git a/examples/unicorn/config.ru b/examples/unicorn/config.ru index 81e5a523d3af2a5548fef1d8a4a7112be1f1e82a..d49d5690470c9da7b5eeb679d7db56b5992a1182 100644 --- a/examples/unicorn/config.ru +++ b/examples/unicorn/config.ru @@ -1,5 +1,5 @@ # frozen_string_literal: true run lambda { |_env| - [200, { "Content-Type" => "text/plain" }, [Redis.current.randomkey]] + [200, { "Content-Type" => "text/plain" }, [MyApp.redis.randomkey]] } diff --git a/examples/unicorn/unicorn.rb b/examples/unicorn/unicorn.rb index 939e0e6dc175268dd504d3414779ef38d9d1ae61..a47f1a0fb49f6ea3f6830428ee0ef4e7d5b430f8 100644 --- a/examples/unicorn/unicorn.rb +++ b/examples/unicorn/unicorn.rb @@ -18,5 +18,5 @@ worker_processes 3 # worker processes. after_fork do |_server, _worker| - Redis.current.disconnect! + MyApp.redis.disconnect! end diff --git a/lib/redis.rb b/lib/redis.rb index 1bb1eb377a77ef37ba2e2a6cff3a0d719eac9f76..09d8fd95c98087cdddff9c1be4a63bdc7509e1d4 100644 --- a/lib/redis.rb +++ b/lib/redis.rb @@ -6,26 +6,10 @@ require "redis/commands" class Redis BASE_PATH = __dir__ - @exists_returns_integer = true - @sadd_returns_boolean = true - Deprecated = Class.new(StandardError) class << self - attr_reader :exists_returns_integer - attr_accessor :silence_deprecations, :raise_deprecations, :sadd_returns_boolean - - def exists_returns_integer=(value) - unless value - deprecate!( - "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \ - "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \ - "`exists?` instead." - ) - end - - @exists_returns_integer = value - end + attr_accessor :silence_deprecations, :raise_deprecations def deprecate!(message) unless silence_deprecations @@ -36,20 +20,22 @@ class Redis end end end + end - def current - deprecate!("`Redis.current` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})") - @current ||= Redis.new - end - - def current=(redis) - deprecate!("`Redis.current=` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})") - @current = redis + # 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 @@ -59,56 +45,49 @@ class Redis # @option options [String] :host ("127.0.0.1") server hostname # @option options [Integer] :port (6379) server port # @option options [String] :path path to server socket (overrides host and port) - # @option options [Float] :timeout (5.0) timeout in seconds + # @option options [Float] :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 initial connect - # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony` + # @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 [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated - # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer - # @option options [Integer] :reconnect_attempts Number of attempts trying to connect + # @option options [Integer, Array<Integer, Float>] :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 - # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave` - # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact - # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not - # @option options [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] a new client instance def initialize(options = {}) - @options = options.dup - @cluster_mode = options.key?(:cluster) - client = @cluster_mode ? Cluster : Client - @original_client = @client = client.new(options) - @queue = Hash.new { |h, k| h[k] = [] } @monitor = Monitor.new - end - - # Run code with the client reconnecting - def with_reconnect(val = true, &blk) - synchronize do |client| - client.with_reconnect(val, &blk) + @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(&blk) - with_reconnect(false, &blk) + def without_reconnect(&block) + @client.disable_reconnection(&block) end # Test whether or not the client is connected def connected? - @original_client.connected? + @client.connected? || @subscription_client&.connected? end # Disconnect the client as quickly and silently as possible. def close - @original_client.disconnect + @client.close + @subscription_client&.close end alias disconnect! close @@ -116,127 +95,20 @@ class Redis yield self end - # @deprecated Queues a command for pipelining. - # - # Commands in the queue are executed with the Redis#commit method. - # - # See http://redis.io/topics/pipelining for more details. - # - def queue(*command) - ::Redis.deprecate!( - "Redis#queue is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead." \ - "(called from: #{caller(1, 1).first})" - ) - - synchronize do - @queue[Thread.current.object_id] << command - end - end - - # @deprecated Sends all commands in the queue. - # - # See http://redis.io/topics/pipelining for more details. - # - def commit - ::Redis.deprecate!( - "Redis#commit is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead. " \ - "(called from: #{Kernel.caller(1, 1).first})" - ) - - synchronize do |client| - begin - pipeline = Pipeline.new(client) - @queue[Thread.current.object_id].each do |command| - pipeline.call(command) - end - - client.call_pipelined(pipeline) - ensure - @queue.delete(Thread.current.object_id) - end - end - end - def _client @client end - def pipelined(&block) - deprecation_displayed = false - if block&.arity == 0 - Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 5)) - deprecation_displayed = true - end - - synchronize do |prior_client| - begin - pipeline = Pipeline.new(prior_client) - @client = deprecation_displayed ? pipeline : DeprecatedPipeline.new(pipeline) - pipelined_connection = PipelinedConnection.new(pipeline) - yield pipelined_connection - prior_client.call_pipeline(pipeline) - ensure - @client = prior_client - end - end - end - - # Mark the start of a transaction block. - # - # Passing a block is optional. - # - # @example With a block - # redis.multi do |multi| - # multi.set("key", "value") - # multi.incr("counter") - # end # => ["OK", 6] - # - # @example Without a block - # redis.multi - # # => "OK" - # redis.set("key", "value") - # # => "QUEUED" - # redis.incr("counter") - # # => "QUEUED" - # redis.exec - # # => ["OK", 6] - # - # @yield [multi] the commands that are called inside this block are cached - # and written to the server upon returning from it - # @yieldparam [Redis] multi `self` - # - # @return [String, Array<...>] - # - when a block is not given, `OK` - # - when a block is given, an array with replies - # - # @see #watch - # @see #unwatch - def multi(&block) - if block_given? - deprecation_displayed = false - if block&.arity == 0 - Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5)) - deprecation_displayed = true - end - - synchronize do |prior_client| - begin - pipeline = Pipeline::Multi.new(prior_client) - @client = deprecation_displayed ? pipeline : DeprecatedMulti.new(pipeline) - pipelined_connection = PipelinedConnection.new(pipeline) - yield pipelined_connection - prior_client.call_pipeline(pipeline) - ensure - @client = prior_client - end + def pipelined(exception: true) + synchronize do |client| + client.pipelined(exception: exception) do |raw_pipeline| + yield PipelinedConnection.new(raw_pipeline, exception: exception) end - else - send_command([:multi]) end end def id - @original_client.id + @client.id || @client.server_url end def inspect @@ -248,54 +120,75 @@ class Redis end def connection - return @original_client.connection_info if @cluster_mode - { - host: @original_client.host, - port: @original_client.port, - db: @original_client.db, - id: @original_client.id, - location: @original_client.location + 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(command, &block) + @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.call_with_timeout(command, timeout, &block) + @client.blocking_call_v(timeout, command, &block) end end def _subscription(method, timeout, channels, block) - return @client.call([method] + channels) if subscribed? + if block + if @subscription_client + raise SubscriptionError, "This client is already subscribed" + end - begin - original, @client = @client, SubscribedClient.new(@client) - if timeout > 0 - @client.send(method, timeout, *channels, &block) - else - @client.send(method, *channels, &block) + 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 - ensure - @client = original + + @subscription_client.call_v([method].concat(channels)) end end end require "redis/version" -require "redis/connection" require "redis/client" -require "redis/cluster" require "redis/pipeline" require "redis/subscribe" diff --git a/lib/redis/client.rb b/lib/redis/client.rb index 3e620faa9e33e1765cef8ba5dcbbae7bd9e35b6a..d23ed9122cba6fc42cd9351690d6edc90a6188d5 100644 --- a/lib/redis/client.rb +++ b/lib/redis/client.rb @@ -1,667 +1,124 @@ # frozen_string_literal: true -require "socket" -require "cgi" -require "redis/errors" +require 'redis-client' class Redis - class Client - # Defaults are also used for converting string keys to symbols. - DEFAULTS = { - url: -> { ENV["REDIS_URL"] }, - scheme: "redis", - host: "127.0.0.1", - port: 6379, - path: nil, - read_timeout: nil, - write_timeout: nil, - connect_timeout: nil, - timeout: 5.0, - username: nil, - password: nil, - db: 0, - driver: nil, - id: nil, - tcp_keepalive: 0, - reconnect_attempts: 1, - reconnect_delay: 0, - reconnect_delay_max: 0.5, - inherit_socket: false, - logger: nil, - sentinels: nil, - role: nil - }.freeze - - attr_reader :options, :connection, :command_map - - def scheme - @options[:scheme] - end - - def host - @options[:host] - end - - def port - @options[:port] - end - - def path - @options[:path] - end - - def read_timeout - @options[:read_timeout] - end - - def connect_timeout - @options[:connect_timeout] - end - - def timeout - @options[:read_timeout] - end - - def username - @options[:username] - end - - def password - @options[:password] - end - - def db - @options[:db] - end - - def db=(db) - @options[:db] = db.to_i - end - - def driver - @options[:driver] - end - - def inherit_socket? - @options[:inherit_socket] - end - - attr_accessor :logger - - def initialize(options = {}) - @options = _parse_options(options) - @reconnect = true - @logger = @options[:logger] - @connection = nil - @command_map = {} - - @pending_reads = 0 - - @connector = - if !@options[:sentinels].nil? - Connector::Sentinel.new(@options) - elsif options.include?(:connector) && options[:connector].respond_to?(:new) - options.delete(:connector).new(@options) + 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 - Connector.new(@options) - end - end - - def connect - @pid = Process.pid - - # Don't try to reconnect when the connection is fresh - with_reconnect(false) do - establish_connection - if password - if username - begin - call [:auth, username, password] - rescue CommandError => err # Likely on Redis < 6 - case err.message - when /ERR wrong number of arguments for 'auth' command/ - call [:auth, password] - when /WRONGPASS invalid username-password pair/ - begin - call [:auth, password] - rescue CommandError - raise err - end - ::Redis.deprecate!( - "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \ - " the provided password was for the default user. This will start failing in redis-rb 5.0.0." - ) - else - raise - end - end - else - call [:auth, password] - end + raise end - - call [:readonly] if @options[:readonly] - call [:select, db] if db != 0 - call [:client, :setname, @options[:id]] if @options[:id] - @connector.check(self) end - - self end def id - @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}" + config.id end - def location - path || "#{host}:#{port}" + def server_url + config.server_url end - def call(command) - reply = process([command]) { read } - raise reply if reply.is_a?(CommandError) - - if block_given? && reply != 'QUEUED' - yield reply - else - reply - end - end - - def call_loop(command, timeout = 0) - error = nil - - result = with_socket_timeout(timeout) do - process([command]) do - loop do - reply = read - if reply.is_a?(CommandError) - error = reply - break - else - yield reply - end - end - end - end - - # Raise error when previous block broke out of the loop. - raise error if error - - # Result is set to the value that the provided block used to break. - result - end - - def call_pipeline(pipeline) - return [] if pipeline.futures.empty? - - with_reconnect pipeline.with_reconnect? do - begin - pipeline.finish(call_pipelined(pipeline)).tap do - self.db = pipeline.db if pipeline.db - end - rescue ConnectionError => e - return nil if pipeline.shutdown? - - # Assume the pipeline was sent in one piece, but execution of - # SHUTDOWN caused none of the replies for commands that were executed - # prior to it from coming back around. - raise e - end - end - end - - def call_pipelined(pipeline) - return [] if pipeline.futures.empty? - - # The method #ensure_connected (called from #process) reconnects once on - # I/O errors. To make an effort in making sure that commands are not - # executed more than once, only allow reconnection before the first reply - # has been read. When an error occurs after the first reply has been - # read, retrying would re-execute the entire pipeline, thus re-issuing - # already successfully executed commands. To circumvent this, don't retry - # after the first reply has been read successfully. - - commands = pipeline.commands - - result = Array.new(commands.size) - reconnect = @reconnect - - begin - exception = nil - - process(commands) do - pipeline.timeouts.each_with_index do |timeout, i| - reply = if timeout - with_socket_timeout(timeout) { read } - else - read - end - result[i] = reply - @reconnect = false - exception = reply if exception.nil? && reply.is_a?(CommandError) - end - end - - raise exception if exception - ensure - @reconnect = reconnect - end - - result - end - - def call_with_timeout(command, extra_timeout, &blk) - timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout - with_socket_timeout(timeout) do - call(command, &blk) - end - rescue ConnectionError - retry - end - - def call_without_timeout(command, &blk) - call_with_timeout(command, 0, &blk) - end - - def process(commands) - logging(commands) do - ensure_connected do - commands.each do |command| - if command_map[command.first] - command = command.dup - command[0] = command_map[command.first] - end - - write(command) - end - - yield if block_given? - end - end - end - - def connected? - !!(connection && connection.connected?) - end - - def disconnect - connection.disconnect if connected? + def timeout + config.read_timeout end - alias close disconnect - def reconnect - disconnect - connect + def db + config.db end - def io - yield - rescue TimeoutError => e1 - # Add a message to the exception without destroying the original stack - e2 = TimeoutError.new("Connection timed out") - e2.set_backtrace(e1.backtrace) - raise e2 - rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e - raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last] + def host + config.host unless config.path end - def read - io do - value = connection.read - @pending_reads -= 1 - value - end + def port + config.port unless config.path end - def write(command) - io do - @pending_reads += 1 - connection.write(command) - end + def path + config.path end - def with_socket_timeout(timeout) - connect unless connected? - original = @options[:read_timeout] - - begin - connection.timeout = timeout - @options[:read_timeout] = timeout # for reconnection - yield - ensure - connection.timeout = self.timeout if connected? - @options[:read_timeout] = original - end + def username + config.username end - def without_socket_timeout(&blk) - with_socket_timeout(0, &blk) + def password + config.password end - def with_reconnect(val = true) - original, @reconnect = @reconnect, val - yield - ensure - @reconnect = original - end + undef_method :call + undef_method :call_once + undef_method :call_once_v + undef_method :blocking_call - def without_reconnect(&blk) - with_reconnect(false, &blk) + def call_v(command, &block) + super(command, &block) + rescue ::RedisClient::Error => error + Client.translate_error!(error) end - protected - - def logging(commands) - return yield unless @logger&.debug? - - begin - commands.each do |name, *args| - logged_args = args.map do |a| - if a.respond_to?(:inspect) then a.inspect - elsif a.respond_to?(:to_s) then a.to_s - else - # handle poorly-behaved descendants of BasicObject - klass = a.instance_exec { (class << self; self end).superclass } - "\#<#{klass}:#{a.__id__}>" - end - end - @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}") - end - - t1 = Time.now - yield - ensure - @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1 + 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 - end - - def establish_connection - server = @connector.resolve.dup - - @options[:host] = server[:host] - @options[:port] = Integer(server[:port]) if server.include?(:port) - @connection = @options[:driver].connect(@options) - @pending_reads = 0 - rescue TimeoutError, - SocketError, - Errno::EADDRNOTAVAIL, - Errno::ECONNREFUSED, - Errno::EHOSTDOWN, - Errno::EHOSTUNREACH, - Errno::ENETUNREACH, - Errno::ENOENT, - Errno::ETIMEDOUT, - Errno::EINVAL => error - - raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})" + super(timeout, command, &block) + rescue ::RedisClient::Error => error + Client.translate_error!(error) end - def ensure_connected - disconnect if @pending_reads > 0 - - attempts = 0 - - begin - attempts += 1 - - if connected? - unless inherit_socket? || Process.pid == @pid - raise InheritedError, - "Tried to use a connection from a child process without reconnecting. " \ - "You need to reconnect to Redis after forking " \ - "or set :inherit_socket to true." - end - else - connect - end - - yield - rescue BaseConnectionError - disconnect - - if attempts <= @options[:reconnect_attempts] && @reconnect - sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)), - @options[:reconnect_delay_max]].min - - Kernel.sleep(sleep_t) - retry - else - raise - end - rescue Exception - disconnect - raise - end - end - - def _parse_options(options) - return options if options[:_parsed] - - defaults = DEFAULTS.dup - options = options.dup - - defaults.each_key do |key| - # Fill in defaults if needed - defaults[key] = defaults[key].call if defaults[key].respond_to?(:call) - - # Symbolize only keys that are needed - options[key] = options[key.to_s] if options.key?(key.to_s) - end - - url = options[:url] - url = defaults[:url] if url.nil? - - # Override defaults from URL if given - if url - require "uri" - - uri = URI(url) - - case uri.scheme - when "unix" - defaults[:path] = uri.path - when "redis", "rediss" - defaults[:scheme] = uri.scheme - defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host - defaults[:port] = uri.port if uri.port - defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty? - defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty? - defaults[:db] = uri.path[1..-1].to_i if uri.path - defaults[:role] = :master - else - raise ArgumentError, "invalid uri scheme '#{uri.scheme}'" - end - - defaults[:ssl] = true if uri.scheme == "rediss" - end - - # Use default when option is not specified or nil - defaults.each_key do |key| - options[key] = defaults[key] if options[key].nil? - end - - if options[:path] - # Unix socket - options[:scheme] = "unix" - options.delete(:host) - options.delete(:port) - else - # TCP socket - options[:host] = options[:host].to_s - options[:port] = options[:port].to_i - end - - if options.key?(:timeout) - options[:connect_timeout] ||= options[:timeout] - options[:read_timeout] ||= options[:timeout] - options[:write_timeout] ||= options[:timeout] - end - - options[:connect_timeout] = Float(options[:connect_timeout]) - options[:read_timeout] = Float(options[:read_timeout]) - options[:write_timeout] = Float(options[:write_timeout]) - - options[:reconnect_attempts] = options[:reconnect_attempts].to_i - options[:reconnect_delay] = options[:reconnect_delay].to_f - options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f - - options[:db] = options[:db].to_i - options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last - - case options[:tcp_keepalive] - when Hash - %i[time intvl probes].each do |key| - unless options[:tcp_keepalive][key].is_a?(Integer) - raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer" - end - end - - when Integer - if options[:tcp_keepalive] >= 60 - options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 } - - elsif options[:tcp_keepalive] >= 30 - options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 } - - elsif options[:tcp_keepalive] >= 5 - options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 } - end - end - - options[:_parsed] = true - - options + def pipelined(exception: true) + super + rescue ::RedisClient::Error => error + Client.translate_error!(error) end - def _parse_driver(driver) - driver = driver.to_s if driver.is_a?(Symbol) - - if driver.is_a?(String) - begin - require_relative "connection/#{driver}" - rescue LoadError, NameError - begin - require "redis/connection/#{driver}" - rescue LoadError, NameError => error - raise "Cannot load driver #{driver.inspect}: #{error.message}" - end - end - - driver = Connection.const_get(driver.capitalize) - end - - driver + def multi(watch: nil) + super + rescue ::RedisClient::Error => error + Client.translate_error!(error) end - class Connector - def initialize(options) - @options = options.dup - end - - def resolve - @options - end - - def check(client); end - - class Sentinel < Connector - def initialize(options) - super(options) - - @options[:db] = DEFAULTS.fetch(:db) - - @sentinels = @options.delete(:sentinels).dup - @role = (@options[:role] || "master").to_s - @master = @options[:host] - end - - def check(client) - # Check the instance is really of the role we are looking for. - # We can't assume the command is supported since it was introduced - # recently and this client should work with old stuff. - begin - role = client.call([:role])[0] - rescue Redis::CommandError - # Assume the test is passed if we can't get a reply from ROLE... - role = @role - end - - if role != @role - client.disconnect - raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}." - end - end - - def resolve - result = case @role - when "master" - resolve_master - when "slave" - resolve_slave - else - raise ArgumentError, "Unknown instance role #{@role}" - end - - result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.") - end - - def sentinel_detect - @sentinels.each do |sentinel| - client = Client.new(@options.merge({ - host: sentinel[:host] || sentinel["host"], - port: sentinel[:port] || sentinel["port"], - username: sentinel[:username] || sentinel["username"], - password: sentinel[:password] || sentinel["password"], - reconnect_attempts: 0 - })) - - begin - if result = yield(client) - # This sentinel responded. Make sure we ask it first next time. - @sentinels.delete(sentinel) - @sentinels.unshift(sentinel) - - return result - end - rescue BaseConnectionError - ensure - client.disconnect - end - end - - raise CannotConnectError, "No sentinels available." - end - - def resolve_master - sentinel_detect do |client| - if reply = client.call(["sentinel", "get-master-addr-by-name", @master]) - { host: reply[0], port: reply[1] } - end - end - end - - def resolve_slave - sentinel_detect do |client| - if reply = client.call(["sentinel", "slaves", @master]) - slaves = reply.map { |s| s.each_slice(2).to_h } - slaves.each { |s| s['flags'] = s.fetch('flags').split(',') } - slaves.reject! { |s| s.fetch('flags').include?('s_down') } - - if slaves.empty? - raise CannotConnectError, 'No slaves available.' - else - slave = slaves.sample - { - host: slave.fetch('ip'), - port: slave.fetch('port') - } - end - end - end - end - end + def inherit_socket! + @inherit_socket = true end end end diff --git a/lib/redis/cluster.rb b/lib/redis/cluster.rb deleted file mode 100644 index a71a45f1dabfcf9c9b31fc7a7997e411c23ade99..0000000000000000000000000000000000000000 --- a/lib/redis/cluster.rb +++ /dev/null @@ -1,315 +0,0 @@ -# frozen_string_literal: true - -require_relative 'errors' -require_relative 'client' -require_relative 'cluster/command' -require_relative 'cluster/command_loader' -require_relative 'cluster/key_slot_converter' -require_relative 'cluster/node' -require_relative 'cluster/node_key' -require_relative 'cluster/node_loader' -require_relative 'cluster/option' -require_relative 'cluster/slot' -require_relative 'cluster/slot_loader' - -class Redis - # Redis Cluster client - # - # @see https://github.com/antirez/redis-rb-cluster POC implementation - # @see https://redis.io/topics/cluster-spec Redis Cluster specification - # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial - # - # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com> - class Cluster - def initialize(options = {}) - @option = Option.new(options) - @node, @slot = fetch_cluster_info!(@option) - @command = fetch_command_details(@node) - end - - def id - @node.map(&:id).sort.join(' ') - end - - # db feature is disabled in cluster mode - def db - 0 - end - - # db feature is disabled in cluster mode - def db=(_db); end - - def timeout - @node.first.timeout - end - - def connected? - @node.any?(&:connected?) - end - - def disconnect - @node.each(&:disconnect) - true - end - - def connection_info - @node.sort_by(&:id).map do |client| - { - host: client.host, - port: client.port, - db: client.db, - id: client.id, - location: client.location - } - end - end - - def with_reconnect(val = true, &block) - try_send(@node.sample, :with_reconnect, val, &block) - end - - def call(command, &block) - send_command(command, &block) - end - - def call_loop(command, timeout = 0, &block) - node = assign_node(command) - try_send(node, :call_loop, command, timeout, &block) - end - - def call_pipeline(pipeline) - node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq - if node_keys.size > 1 - raise(CrossSlotPipeliningError, - pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq) - end - - try_send(find_node(node_keys.first), :call_pipeline, pipeline) - end - - def call_with_timeout(command, timeout, &block) - node = assign_node(command) - try_send(node, :call_with_timeout, command, timeout, &block) - end - - def call_without_timeout(command, &block) - call_with_timeout(command, 0, &block) - end - - def process(commands, &block) - if commands.size == 1 && - %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) && - commands.first.size == 1 - - # Node is indeterminate. We do just a best-effort try here. - @node.process_all(commands, &block) - else - node = assign_node(commands.first) - try_send(node, :process, commands, &block) - end - end - - private - - def fetch_cluster_info!(option) - node = Node.new(option.per_node_key) - available_slots = SlotLoader.load(node) - node_flags = NodeLoader.load_flags(node) - option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) }) - [Node.new(option.per_node_key, node_flags, option.use_replica?), - Slot.new(available_slots, node_flags, option.use_replica?)] - ensure - node&.each(&:disconnect) - end - - def fetch_command_details(nodes) - details = CommandLoader.load(nodes) - Command.new(details) - end - - def send_command(command, &block) - cmd = command.first.to_s.downcase - case cmd - when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save' - @node.call_all(command, &block).first - when 'flushall', 'flushdb' - @node.call_master(command, &block).first - when 'wait' then @node.call_master(command, &block).reduce(:+) - when 'keys' then @node.call_slave(command, &block).flatten.sort - when 'dbsize' then @node.call_slave(command, &block).reduce(:+) - when 'scan' then _scan(command, &block) - when 'lastsave' then @node.call_all(command, &block).sort - when 'role' then @node.call_all(command, &block) - when 'config' then send_config_command(command, &block) - when 'client' then send_client_command(command, &block) - when 'cluster' then send_cluster_command(command, &block) - when 'readonly', 'readwrite', 'shutdown' - raise OrchestrationCommandNotSupported, cmd - when 'memory' then send_memory_command(command, &block) - when 'script' then send_script_command(command, &block) - when 'pubsub' then send_pubsub_command(command, &block) - when 'discard', 'exec', 'multi', 'unwatch' - raise AmbiguousNodeError, cmd - else - node = assign_node(command) - try_send(node, :call, command, &block) - end - end - - def send_config_command(command, &block) - case command[1].to_s.downcase - when 'resetstat', 'rewrite', 'set' - @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_memory_command(command, &block) - case command[1].to_s.downcase - when 'stats' then @node.call_all(command, &block) - when 'purge' then @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_client_command(command, &block) - case command[1].to_s.downcase - when 'list' then @node.call_all(command, &block).flatten - when 'pause', 'reply', 'setname' - @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_cluster_command(command, &block) - subcommand = command[1].to_s.downcase - case subcommand - when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate', - 'reset', 'set-config-epoch', 'setslot' - raise OrchestrationCommandNotSupported, 'cluster', subcommand - when 'saveconfig' then @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_script_command(command, &block) - case command[1].to_s.downcase - when 'debug', 'kill' - @node.call_all(command, &block).first - when 'flush', 'load' - @node.call_master(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_pubsub_command(command, &block) - case command[1].to_s.downcase - when 'channels' then @node.call_all(command, &block).flatten.uniq.sort - when 'numsub' - @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] } - .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } } - when 'numpat' then @node.call_all(command, &block).reduce(:+) - else assign_node(command).call(command, &block) - end - end - - # @see https://redis.io/topics/cluster-spec#redirection-and-resharding - # Redirection and resharding - def try_send(node, method_name, *args, retry_count: 3, &block) - node.public_send(method_name, *args, &block) - rescue CommandError => err - if err.message.start_with?('MOVED') - raise if retry_count <= 0 - - node = assign_redirection_node(err.message) - retry_count -= 1 - retry - elsif err.message.start_with?('ASK') - raise if retry_count <= 0 - - node = assign_asking_node(err.message) - node.call(%i[asking]) - retry_count -= 1 - retry - else - raise - end - rescue CannotConnectError - update_cluster_info! - raise - end - - def _scan(command, &block) - input_cursor = Integer(command[1]) - - client_index = input_cursor % 256 - raw_cursor = input_cursor >> 8 - - clients = @node.scale_reading_clients - - client = clients[client_index] - return ['0', []] unless client - - command[1] = raw_cursor.to_s - - result_cursor, result_keys = client.call(command, &block) - result_cursor = Integer(result_cursor) - - if result_cursor == 0 - client_index += 1 - end - - [((result_cursor << 8) + client_index).to_s, result_keys] - end - - def assign_redirection_node(err_msg) - _, slot, node_key = err_msg.split(' ') - slot = slot.to_i - @slot.put(slot, node_key) - find_node(node_key) - end - - def assign_asking_node(err_msg) - _, _, node_key = err_msg.split(' ') - find_node(node_key) - end - - def assign_node(command) - node_key = find_node_key(command) - find_node(node_key) - end - - def find_node_key(command, primary_only: false) - key = @command.extract_first_key(command) - return if key.empty? - - slot = KeySlotConverter.convert(key) - return unless @slot.exists?(slot) - - if @command.should_send_to_master?(command) || primary_only - @slot.find_node_key_of_master(slot) - else - @slot.find_node_key_of_slave(slot) - end - end - - def find_node(node_key) - return @node.sample if node_key.nil? - - @node.find_by(node_key) - rescue Node::ReloadNeeded - update_cluster_info!(node_key) - @node.find_by(node_key) - end - - def update_cluster_info!(node_key = nil) - unless node_key.nil? - host, port = NodeKey.split(node_key) - @option.add_node(host, port) - end - - @node.map(&:disconnect) - @node, @slot = fetch_cluster_info!(@option) - end - end -end diff --git a/lib/redis/cluster/command.rb b/lib/redis/cluster/command.rb deleted file mode 100644 index 9386136e079bfc75a6d388da038b556817b00c9d..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/command.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' - -class Redis - class Cluster - # Keep details about Redis commands for Redis Cluster Client. - # @see https://redis.io/commands/command - class Command - def initialize(details) - @details = pick_details(details) - end - - def extract_first_key(command) - i = determine_first_key_position(command) - return '' if i == 0 - - key = command[i].to_s - hash_tag = extract_hash_tag(key) - hash_tag.empty? ? key : hash_tag - end - - def should_send_to_master?(command) - dig_details(command, :write) - end - - def should_send_to_slave?(command) - dig_details(command, :readonly) - end - - private - - def pick_details(details) - details.transform_values do |detail| - { - first_key_position: detail[:first], - write: detail[:flags].include?('write'), - readonly: detail[:flags].include?('readonly') - } - end - end - - def dig_details(command, key) - name = command.first.to_s - return unless @details.key?(name) - - @details.fetch(name).fetch(key) - end - - def determine_first_key_position(command) - case command.first.to_s.downcase - when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3 - when 'object' then 2 - when 'memory' - command[1].to_s.casecmp('usage').zero? ? 2 : 0 - when 'xread', 'xreadgroup' - determine_optional_key_position(command, 'streams') - else - dig_details(command, :first_key_position).to_i - end - end - - def determine_optional_key_position(command, option_name) - idx = command.map(&:to_s).map(&:downcase).index(option_name) - idx.nil? ? 0 : idx + 1 - end - - # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags - def extract_hash_tag(key) - s = key.index('{') - e = key.index('}', s.to_i + 1) - - return '' if s.nil? || e.nil? - - key[s + 1..e - 1] - end - end - end -end diff --git a/lib/redis/cluster/command_loader.rb b/lib/redis/cluster/command_loader.rb deleted file mode 100644 index a840ae672b8a7f977ce0eca8fec534a1088c8fa3..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/command_loader.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' - -class Redis - class Cluster - # Load details about Redis commands for Redis Cluster Client - # @see https://redis.io/commands/command - module CommandLoader - module_function - - def load(nodes) - errors = nodes.map do |node| - begin - return fetch_command_details(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - end - - raise InitialSetupError, errors - end - - def fetch_command_details(node) - node.call(%i[command]).map do |reply| - [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }] - end.to_h - end - - private_class_method :fetch_command_details - end - end -end diff --git a/lib/redis/cluster/key_slot_converter.rb b/lib/redis/cluster/key_slot_converter.rb deleted file mode 100644 index 72ae3f6742d082839327f84bc2b8b2622e57791e..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/key_slot_converter.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Key to slot converter for Redis Cluster Client - # - # We can test it by `CLUSTER KEYSLOT` command. - # - # @see https://github.com/antirez/redis-rb-cluster - # Reference implementation in Ruby - # @see https://redis.io/topics/cluster-spec#appendix - # Reference implementation in ANSI C - # @see https://redis.io/commands/cluster-keyslot - # CLUSTER KEYSLOT command reference - # - # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com> - module KeySlotConverter - XMODEM_CRC16_LOOKUP = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 - ].freeze - - HASH_SLOTS = 16_384 - - module_function - - # Convert key into slot. - # - # @param key [String] the key of the redis command - # - # @return [Integer] slot number - def convert(key) - crc = 0 - key.each_byte do |b| - crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff] - end - - crc % HASH_SLOTS - end - end - end -end diff --git a/lib/redis/cluster/node.rb b/lib/redis/cluster/node.rb deleted file mode 100644 index 30ab54275a2170d564cd536289f59ca9768c5010..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/node.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' - -class Redis - class Cluster - # Keep client list of node for Redis Cluster Client - class Node - include Enumerable - - ReloadNeeded = Class.new(StandardError) - - ROLE_SLAVE = 'slave' - - def initialize(options, node_flags = {}, with_replica = false) - @with_replica = with_replica - @node_flags = node_flags - @clients = build_clients(options) - end - - def each(&block) - @clients.values.each(&block) - end - - def sample - @clients.values.sample - end - - def find_by(node_key) - @clients.fetch(node_key) - rescue KeyError - raise ReloadNeeded - end - - def call_all(command, &block) - try_map { |_, client| client.call(command, &block) }.values - end - - def call_master(command, &block) - try_map do |node_key, client| - next if slave?(node_key) - - client.call(command, &block) - end.values - end - - def call_slave(command, &block) - return call_master(command, &block) if replica_disabled? - - try_map do |node_key, client| - next if master?(node_key) - - client.call(command, &block) - end.values - end - - def process_all(commands, &block) - try_map { |_, client| client.process(commands, &block) }.values - end - - def scale_reading_clients - reading_clients = [] - - @clients.each do |node_key, client| - next unless replica_disabled? ? master?(node_key) : slave?(node_key) - - reading_clients << client - end - - reading_clients - end - - private - - def replica_disabled? - !@with_replica - end - - def master?(node_key) - !slave?(node_key) - end - - def slave?(node_key) - @node_flags[node_key] == ROLE_SLAVE - end - - def build_clients(options) - clients = options.map do |node_key, option| - next if replica_disabled? && slave?(node_key) - - option = option.merge(readonly: true) if slave?(node_key) - - client = Client.new(option) - [node_key, client] - end - - clients.compact.to_h - end - - def try_map - errors = {} - results = {} - - @clients.each do |node_key, client| - begin - reply = yield(node_key, client) - results[node_key] = reply unless reply.nil? - rescue CommandError => err - errors[node_key] = err - next - end - end - - return results if errors.empty? - - raise CommandErrorCollection, errors - end - end - end -end diff --git a/lib/redis/cluster/node_key.rb b/lib/redis/cluster/node_key.rb deleted file mode 100644 index f0e825a6009dbe51e71a3a44ec6e3de873095765..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/node_key.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Node key's format is `<ip>:<port>`. - # It is different from node id. - # Node id is internal identifying code in Redis Cluster. - module NodeKey - DELIMITER = ':' - - module_function - - def optionize(node_key) - host, port = split(node_key) - { host: host, port: port } - end - - def split(node_key) - node_key.split(DELIMITER) - end - - def build_from_uri(uri) - "#{uri.host}#{DELIMITER}#{uri.port}" - end - - def build_from_host_port(host, port) - "#{host}#{DELIMITER}#{port}" - end - end - end -end diff --git a/lib/redis/cluster/node_loader.rb b/lib/redis/cluster/node_loader.rb deleted file mode 100644 index 689aafcb6b30cc07a5c550c900c43b93eefea1ae..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/node_loader.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' - -class Redis - class Cluster - # Load and hashify node info for Redis Cluster Client - module NodeLoader - module_function - - def load_flags(nodes) - errors = nodes.map do |node| - begin - return fetch_node_info(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - end - - raise InitialSetupError, errors - end - - def fetch_node_info(node) - node.call(%i[cluster nodes]) - .split("\n") - .map { |str| str.split(' ') } - .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] } - .to_h - end - - private_class_method :fetch_node_info - end - end -end diff --git a/lib/redis/cluster/option.rb b/lib/redis/cluster/option.rb deleted file mode 100644 index 7721149c37b1c9935a864d02f446d96303055a85..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/option.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' -require_relative 'node_key' -require 'uri' - -class Redis - class Cluster - # Keep options for Redis Cluster Client - class Option - DEFAULT_SCHEME = 'redis' - SECURE_SCHEME = 'rediss' - VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze - - def initialize(options) - options = options.dup - node_addrs = options.delete(:cluster) - @node_opts = build_node_options(node_addrs) - @replica = options.delete(:replica) == true - @fixed_hostname = options.delete(:fixed_hostname) - add_common_node_option_if_needed(options, @node_opts, :scheme) - add_common_node_option_if_needed(options, @node_opts, :username) - add_common_node_option_if_needed(options, @node_opts, :password) - @options = options - end - - def per_node_key - @node_opts.map do |opt| - node_key = NodeKey.build_from_host_port(opt[:host], opt[:port]) - options = @options.merge(opt) - options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty? - [node_key, options] - end.to_h - end - - def use_replica? - @replica - end - - def update_node(addrs) - @node_opts = build_node_options(addrs) - end - - def add_node(host, port) - @node_opts << { host: host, port: port } - end - - private - - def build_node_options(addrs) - raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array) - - addrs.map { |addr| parse_node_addr(addr) } - end - - def parse_node_addr(addr) - case addr - when String - parse_node_url(addr) - when Hash - parse_node_option(addr) - else - raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash' - end - end - - def parse_node_url(addr) - uri = URI(addr) - raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme) - - db = uri.path.split('/')[1]&.to_i - username = uri.user ? URI.decode_www_form_component(uri.user) : nil - password = uri.password ? URI.decode_www_form_component(uri.password) : nil - - { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db } - .reject { |_, v| v.nil? || v == '' } - rescue URI::InvalidURIError => err - raise InvalidClientOptionError, err.message - end - - def parse_node_option(addr) - addr = addr.map { |k, v| [k.to_sym, v] }.to_h - if addr.values_at(:host, :port).any?(&:nil?) - raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' - end - - addr - end - - # Redis cluster node returns only host and port information. - # So we should complement additional information such as: - # scheme, username, password and so on. - def add_common_node_option_if_needed(options, node_opts, key) - return options if options[key].nil? && node_opts.first[key].nil? - - options[key] ||= node_opts.first[key] - end - end - end -end diff --git a/lib/redis/cluster/slot.rb b/lib/redis/cluster/slot.rb deleted file mode 100644 index c89cc1a978b3cca98c326a84a0d47e00d79b6a6d..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/slot.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Keep slot and node key map for Redis Cluster Client - class Slot - ROLE_SLAVE = 'slave' - - def initialize(available_slots, node_flags = {}, with_replica = false) - @with_replica = with_replica - @node_flags = node_flags - @map = build_slot_node_key_map(available_slots) - end - - def exists?(slot) - @map.key?(slot) - end - - def find_node_key_of_master(slot) - return nil unless exists?(slot) - - @map[slot][:master] - end - - def find_node_key_of_slave(slot) - return nil unless exists?(slot) - return find_node_key_of_master(slot) if replica_disabled? - - @map[slot][:slaves].sample - end - - def put(slot, node_key) - # Since we're sharing a hash for build_slot_node_key_map, duplicate it - # if it already exists instead of preserving as-is. - @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] } - - if master?(node_key) - @map[slot][:master] = node_key - elsif !@map[slot][:slaves].include?(node_key) - @map[slot][:slaves] << node_key - end - - nil - end - - private - - def replica_disabled? - !@with_replica - end - - def master?(node_key) - !slave?(node_key) - end - - def slave?(node_key) - @node_flags[node_key] == ROLE_SLAVE - end - - # available_slots is mapping of node_key to list of slot ranges - def build_slot_node_key_map(available_slots) - by_ranges = {} - available_slots.each do |node_key, slots_arr| - by_ranges[slots_arr] ||= { master: nil, slaves: [] } - - if master?(node_key) - by_ranges[slots_arr][:master] = node_key - elsif !by_ranges[slots_arr][:slaves].include?(node_key) - by_ranges[slots_arr][:slaves] << node_key - end - end - - by_slot = {} - by_ranges.each do |slots_arr, nodes| - slots_arr.each do |slots| - slots.each do |slot| - by_slot[slot] = nodes - end - end - end - - by_slot - end - end - end -end diff --git a/lib/redis/cluster/slot_loader.rb b/lib/redis/cluster/slot_loader.rb deleted file mode 100644 index ee5af2c60cac95ab1bd33522c5481f15c06ff719..0000000000000000000000000000000000000000 --- a/lib/redis/cluster/slot_loader.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' -require 'redis/cluster/node_key' - -class Redis - class Cluster - # Load and hashify slot info for Redis Cluster Client - module SlotLoader - module_function - - def load(nodes) - errors = nodes.map do |node| - begin - return fetch_slot_info(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - end - - raise InitialSetupError, errors - end - - def fetch_slot_info(node) - hash_with_default_arr = Hash.new { |h, k| h[k] = [] } - node.call(%i[cluster slots]) - .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) } - .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] } - end - - def parse_slot_info(arr, default_ip:) - first_slot, last_slot = arr[0..1] - slot_range = (first_slot..last_slot).freeze - arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] } - end - - def stringify_node_key(arr, default_ip) - ip, port = arr - ip = default_ip if ip.empty? # When cluster is down - NodeKey.build_from_host_port(ip, port) - end - - private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key - end - end -end diff --git a/lib/redis/commands.rb b/lib/redis/commands.rb index 8a153e14c361fbe59fc59ecf4d3938d21e818c1d..ddf264d952c7289493d01c0f69ec574c2d0e11bd 100644 --- a/lib/redis/commands.rb +++ b/lib/redis/commands.rb @@ -40,12 +40,7 @@ class Redis # where the method call will return nil. Propagate the nil instead of falsely # returning false. Boolify = lambda { |value| - case value - when Integer - value > 0 - else - value - end + value != 0 unless value.nil? } BoolifySet = lambda { |value| @@ -124,7 +119,9 @@ class Redis HashifyStreamAutoclaim = lambda { |reply| { 'next' => reply[0], - 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] } + 'entries' => reply[1].compact.map do |entry, values| + [entry, values.each_slice(2)&.to_h] + end } } diff --git a/lib/redis/commands/bitmaps.rb b/lib/redis/commands/bitmaps.rb index a8d9fdfe40fc1ecc8d64ad0e1df0cdf99542adb0..c9f172825e3e5711e91318bd91129ceb8038bedc 100644 --- a/lib/redis/commands/bitmaps.rb +++ b/lib/redis/commands/bitmaps.rb @@ -27,9 +27,13 @@ class Redis # @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) - send_command([:bitcount, key, start, stop]) + 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. @@ -39,7 +43,10 @@ class Redis # @param [String, Array<String>] keys one or more source keys to perform `operation` # @return [Integer] the length of the string stored in `destkey` def bitop(operation, destkey, *keys) - send_command([: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. @@ -48,14 +55,17 @@ class Redis # @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) + 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 diff --git a/lib/redis/commands/cluster.rb b/lib/redis/commands/cluster.rb index 49c5776a49df91737ec1e00bf080c844dd5df18e..390b4974645765d12e33fbc03268a58aacd8bf04 100644 --- a/lib/redis/commands/cluster.rb +++ b/lib/redis/commands/cluster.rb @@ -12,24 +12,7 @@ class Redis # # @return [Object] depends on the subcommand def cluster(subcommand, *args) - subcommand = subcommand.to_s.downcase - block = case subcommand - when 'slots' - HashifyClusterSlots - when 'nodes' - HashifyClusterNodes - when 'slaves' - HashifyClusterSlaves - when 'info' - HashifyInfo - else - Noop - end - - # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected - block = Noop unless @cluster_mode - - send_command([:cluster, subcommand] + args, &block) + send_command([:cluster, subcommand] + args) end # Sends `ASKING` command to random node and returns its reply. diff --git a/lib/redis/commands/connection.rb b/lib/redis/commands/connection.rb index c63dfbc9d356c1fe43f2e35cbdeaa9a8e8401a11..50c39e8e70b982823983b17124cf9483d4038b89 100644 --- a/lib/redis/commands/connection.rb +++ b/lib/redis/commands/connection.rb @@ -34,10 +34,7 @@ class Redis # @param [Integer] db zero-based index of the DB to use (0 to 15) # @return [String] `OK` def select(db) - synchronize do |client| - client.db = db - client.call([:select, db]) - end + send_command([:select, db]) end # Close the connection. @@ -45,12 +42,10 @@ class Redis # @return [String] `OK` def quit synchronize do |client| - begin - client.call([:quit]) - rescue ConnectionError - ensure - client.disconnect - end + client.call_v([:quit]) + rescue ConnectionError + ensure + client.close end end end diff --git a/lib/redis/commands/geo.rb b/lib/redis/commands/geo.rb index 003858fd0a049f51e2d819654298f9f1c08ba952..fe0d211cec49f7d87ea5e10acac2f0dd2c039d02 100644 --- a/lib/redis/commands/geo.rb +++ b/lib/redis/commands/geo.rb @@ -74,9 +74,9 @@ class Redis private def _geoarguments(*args, options: nil, sort: nil, count: nil) - args.push sort if sort - args.push 'count', count if count - args.push options if options + args << sort if sort + args << 'COUNT' << Integer(count) if count + args << options if options args end end diff --git a/lib/redis/commands/hashes.rb b/lib/redis/commands/hashes.rb index 5531ad0698306a761665ab3496cb6ddda79295b7..a9a505afe60931a87633eb4248e49043b8c6c0d8 100644 --- a/lib/redis/commands/hashes.rb +++ b/lib/redis/commands/hashes.rb @@ -63,7 +63,7 @@ class Redis # # @see #hmset def mapped_hmset(key, hash) - hmset(key, hash.to_a.flatten) + hmset(key, hash.flatten) end # Get the value of a hash field. @@ -87,7 +87,8 @@ class Redis # # @see #mapped_hmget def hmget(key, *fields, &blk) - send_command([:hmget, key] + fields, &blk) + fields.flatten!(1) + send_command([:hmget, key].concat(fields), &blk) end # Get the values of all the given hash fields. @@ -102,7 +103,8 @@ class Redis # # @see #hmget def mapped_hmget(key, *fields) - hmget(key, *fields) do |reply| + fields.flatten!(1) + hmget(key, fields) do |reply| if reply.is_a?(Array) Hash[fields.zip(reply)] else @@ -152,7 +154,8 @@ class Redis # @param [String, Array<String>] field # @return [Integer] the number of fields that were removed from the hash def hdel(key, *fields) - send_command([:hdel, key, *fields]) + fields.flatten!(1) + send_command([:hdel, key].concat(fields)) end # Determine if a hash field exists. @@ -171,7 +174,7 @@ class Redis # @param [Integer] increment # @return [Integer] value of the field after incrementing it def hincrby(key, field, increment) - send_command([: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. @@ -181,7 +184,7 @@ class Redis # @param [Float] increment # @return [Float] value of the field after incrementing it def hincrbyfloat(key, field, increment) - send_command([:hincrbyfloat, key, field, increment], &Floatify) + send_command([:hincrbyfloat, key, field, Float(increment)], &Floatify) end # Get all the fields in a hash. diff --git a/lib/redis/commands/hyper_log_log.rb b/lib/redis/commands/hyper_log_log.rb index 39e31c7daa9488574c7d70557ccf0882d24ddc63..c7834b7109de578dc0b4083919651c9d8adead4f 100644 --- a/lib/redis/commands/hyper_log_log.rb +++ b/lib/redis/commands/hyper_log_log.rb @@ -20,7 +20,7 @@ class Redis # @param [String, Array<String>] keys # @return [Integer] def pfcount(*keys) - send_command([: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 diff --git a/lib/redis/commands/keys.rb b/lib/redis/commands/keys.rb index 75d8c6f07d34f763af788251680a93b6e21a54fe..f507a924f3c4e82554f764515c5bf6a7bd648201 100644 --- a/lib/redis/commands/keys.rb +++ b/lib/redis/commands/keys.rb @@ -76,7 +76,7 @@ class Redis # - `: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, seconds] + args = [:expire, key, Integer(seconds)] args << "NX" if nx args << "XX" if xx args << "GT" if gt @@ -96,7 +96,7 @@ class Redis # - `: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, unix_time] + args = [:expireat, key, Integer(unix_time)] args << "NX" if nx args << "XX" if xx args << "GT" if gt @@ -105,6 +105,14 @@ class Redis 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 @@ -132,7 +140,7 @@ class Redis # - `: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, milliseconds] + args = [:pexpire, key, Integer(milliseconds)] args << "NX" if nx args << "XX" if xx args << "GT" if gt @@ -152,7 +160,7 @@ class Redis # - `: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, ms_unix_time] + args = [:pexpireat, key, Integer(ms_unix_time)] args << "NX" if nx args << "XX" if xx args << "GT" if gt @@ -161,6 +169,14 @@ class Redis 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 @@ -249,24 +265,6 @@ class Redis # @param [String, Array<String>] keys # @return [Integer] def exists(*keys) - if !Redis.exists_returns_integer && keys.size == 1 - if Redis.exists_returns_integer.nil? - message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \ - "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \ - "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \ - "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0.0. " \ - "(#{::Kernel.caller(1, 1).first})\n" - - ::Redis.deprecate!(message) - end - - exists?(*keys) - else - _exists(*keys) - end - end - - def _exists(*keys) send_command([:exists, *keys]) end @@ -445,7 +443,7 @@ class Redis args << cursor args << "MATCH" << match if match - args << "COUNT" << count if count + args << "COUNT" << Integer(count) if count args << "TYPE" << type if type send_command([command] + args, &block) diff --git a/lib/redis/commands/lists.rb b/lib/redis/commands/lists.rb index bf04bb303b37f2287296365a01e008510393a0ee..08a619a2106e23ec9878a1beba7d97c08ec65a57 100644 --- a/lib/redis/commands/lists.rb +++ b/lib/redis/commands/lists.rb @@ -48,7 +48,7 @@ class Redis # @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 => Numeric`: timeout in seconds, defaults to no timeout + # - `: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 # @@ -99,10 +99,10 @@ class Redis # # @param [String] key # @param [Integer] count number of elements to remove - # @return [String, Array<String>] the values of the first elements + # @return [nil, String, Array<String>] the values of the first elements def lpop(key, count = nil) command = [:lpop, key] - command << count if count + command << Integer(count) if count send_command(command) end @@ -110,10 +110,10 @@ class Redis # # @param [String] key # @param [Integer] count number of elements to remove - # @return [String, Array<String>] the values of the last elements + # @return [nil, String, Array<String>] the values of the last elements def rpop(key, count = nil) command = [:rpop, key] - command << count if count + command << Integer(count) if count send_command(command) end @@ -142,7 +142,7 @@ class Redis # @param [String, Array<String>] keys one or more keys to perform the # blocking pop on # @param [Hash] options - # - `:timeout => Integer`: timeout in seconds, defaults to no timeout + # - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout # # @return [nil, [String, String]] # - `nil` when the operation timed out @@ -156,7 +156,7 @@ class Redis # @param [String, Array<String>] keys one or more keys to perform the # blocking pop on # @param [Hash] options - # - `:timeout => Integer`: timeout in seconds, defaults to no timeout + # - `:timeout => [Float, Integer]`: timeout in seconds, defaults to no timeout # # @return [nil, [String, String]] # - `nil` when the operation timed out @@ -173,23 +173,77 @@ class Redis # @param [String] source source key # @param [String] destination destination key # @param [Hash] options - # - `:timeout => Integer`: timeout in seconds, defaults to no timeout + # - `: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, deprecated_timeout = 0, timeout: deprecated_timeout) + 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<String>] 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<String, Array<String, Float>>] 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<String>] 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<String, Array<String, Float>>] 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, index]) + send_command([:lindex, key, Integer(index)]) end # Insert an element before or after another element in a list. @@ -211,7 +265,7 @@ class Redis # @param [Integer] stop stop index # @return [Array<String>] def lrange(key, start, stop) - send_command([:lrange, key, start, stop]) + send_command([:lrange, key, Integer(start), Integer(stop)]) end # Remove elements from a list. @@ -224,7 +278,7 @@ class Redis # @param [String] value # @return [Integer] the number of removed elements def lrem(key, count, value) - send_command([:lrem, key, count, value]) + send_command([:lrem, key, Integer(count), value]) end # Set the value of an element in a list by its index. @@ -234,7 +288,7 @@ class Redis # @param [String] value # @return [String] `OK` def lset(key, index, value) - send_command([:lset, key, index, value]) + send_command([:lset, key, Integer(index), value]) end # Trim a list to the specified range. @@ -244,7 +298,7 @@ class Redis # @param [Integer] stop stop index # @return [String] `OK` def ltrim(key, start, stop) - send_command([:ltrim, key, start, stop]) + send_command([:ltrim, key, Integer(start), Integer(stop)]) end private @@ -253,21 +307,16 @@ class Redis timeout = if args.last.is_a?(Hash) options = args.pop options[:timeout] - elsif args.last.respond_to?(:to_int) - last_arg = args.pop - ::Redis.deprecate!( - "Passing the timeout as a positional argument is deprecated, it should be passed as a keyword argument:\n" \ - " redis.#{cmd}(#{args.map(&:inspect).join(', ')}, timeout: #{last_arg.to_int})" \ - "(called from: #{caller(2, 1).first})" - ) - last_arg.to_int 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 - keys = args.flatten - - command = [cmd, keys, timeout] + args.flatten!(1) + command = [cmd].concat(args) + command << timeout send_blocking_command(command, timeout, &blk) end diff --git a/lib/redis/commands/pubsub.rb b/lib/redis/commands/pubsub.rb index 09da592fb2c47f492699506d7d7222d2d5260fa8..ccdababff71bb3c6328103c16146c07cf4ab3c66 100644 --- a/lib/redis/commands/pubsub.rb +++ b/lib/redis/commands/pubsub.rb @@ -9,57 +9,39 @@ class Redis end def subscribed? - synchronize do |client| - client.is_a? SubscribedClient - end + !@subscription_client.nil? end # Listen for messages published to the given channels. def subscribe(*channels, &block) - synchronize do |_client| - _subscription(:subscribe, 0, channels, block) - end + _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) - synchronize do |_client| - _subscription(:subscribe_with_timeout, timeout, channels, block) - end + _subscription(:subscribe_with_timeout, timeout, channels, block) end # Stop listening for messages posted to the given channels. def unsubscribe(*channels) - synchronize do |client| - raise "Can't unsubscribe if not subscribed." unless subscribed? - - client.unsubscribe(*channels) - end + _subscription(:unsubscribe, 0, channels, nil) end # Listen for messages published to channels matching the given patterns. def psubscribe(*channels, &block) - synchronize do |_client| - _subscription(:psubscribe, 0, channels, block) - end + _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. def psubscribe_with_timeout(timeout, *channels, &block) - synchronize do |_client| - _subscription(:psubscribe_with_timeout, timeout, channels, block) - end + _subscription(:psubscribe_with_timeout, timeout, channels, block) end # Stop listening for messages posted to channels matching the given patterns. def punsubscribe(*channels) - synchronize do |client| - raise "Can't unsubscribe if not subscribed." unless subscribed? - - client.punsubscribe(*channels) - end + _subscription(:punsubscribe, 0, channels, nil) end # Inspect the state of the Pub/Sub subsystem. @@ -67,6 +49,27 @@ class Redis 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 diff --git a/lib/redis/commands/server.rb b/lib/redis/commands/server.rb index 327d0c80e1ef307be34a7e6610e31aa2ef6e9a1f..eca57782f02f2ef6e8e9a381a7954fb1cebe8d81 100644 --- a/lib/redis/commands/server.rb +++ b/lib/redis/commands/server.rb @@ -36,7 +36,7 @@ class Redis # # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname` # @return [String, Hash] depends on subcommand - def client(subcommand = nil, *args) + def client(subcommand, *args) send_command([:client, subcommand] + args) do |reply| if subcommand.to_s == "list" reply.lines.map do |line| @@ -117,9 +117,13 @@ class Redis # # @yield a block to be called for every line of output # @yieldparam [String] line timestamp and command that was executed - def monitor(&block) + def monitor synchronize do |client| - client.call_loop([:monitor], &block) + client = client.pubsub + client.call_v([:monitor]) + loop do + yield client.next_event + end end end @@ -133,13 +137,11 @@ class Redis # Synchronously save the dataset to disk and then shut down the server. def shutdown synchronize do |client| - client.with_reconnect(false) do - begin - client.call([:shutdown]) - rescue ConnectionError - # This means Redis has probably exited. - nil - end + client.disable_reconnection do + client.call_v([:shutdown]) + rescue ConnectionError + # This means Redis has probably exited. + nil end end end @@ -155,11 +157,9 @@ class Redis # @param [Integer] length maximum number of entries to return # @return [Array<String>, Integer, String] depends on subcommand def slowlog(subcommand, length = nil) - synchronize do |client| - args = [:slowlog, subcommand] - args << length if length - client.call args - end + args = [:slowlog, subcommand] + args << Integer(length) if length + send_command(args) end # Internal command used for replication. diff --git a/lib/redis/commands/sets.rb b/lib/redis/commands/sets.rb index 71e15dbe1d945112e740801d576efea692469dcf..e0575139725c88fe3561620b708f23524669cd9c 100644 --- a/lib/redis/commands/sets.rb +++ b/lib/redis/commands/sets.rb @@ -15,56 +15,40 @@ class Redis # # @param [String] key # @param [String, Array<String>] member one member, or array of members - # @return [Boolean, Integer] `Boolean` when a single member is specified, - # holding whether or not adding the member succeeded, or `Integer` when an - # array of members is specified, holding the number of members that were - # successfully added - def sadd(key, member) - block = if Redis.sadd_returns_boolean && !member.is_a?(Array) - ::Redis.deprecate!( - "Redis#sadd will always return an Integer in Redis 5.0.0. Use Redis#sadd? instead." \ - "(called from: #{caller(1, 1).first})" - ) - Boolify - end - send_command([:sadd, key, member], &block) + # @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<String>] member one member, or array of members - # @return [Boolean] Whether or not at least one member was added. - def sadd?(key, member) - send_command([:sadd, key, member], &Boolify) + # @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<String>] member one member, or array of members - # @return [Boolean, Integer] `Boolean` when a single member is specified, - # holding whether or not removing the member succeeded, or `Integer` when an - # array of members is specified, holding the number of members that were - # successfully removed - def srem(key, member) - block = if Redis.sadd_returns_boolean && !member.is_a?(Array) - ::Redis.deprecate!( - "Redis#sadd will always return an Integer in Redis 5.0.0. Use Redis#sadd? instead." \ - "(called from: #{caller(1, 1).first})" - ) - Boolify - end - send_command([:srem, key, member], &block) + # @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<String>] member one member, or array of members - # @return [Boolean] `Boolean` Whether or not a member was removed. - def srem?(key, member) - send_command([:srem, key, member], &Boolify) + # @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. @@ -76,7 +60,7 @@ class Redis if count.nil? send_command([:spop, key]) else - send_command([:spop, key, count]) + send_command([:spop, key, Integer(count)]) end end @@ -118,7 +102,8 @@ class Redis # @param [String, Array<String>] members # @return [Array<Boolean>] def smismember(key, *members) - send_command([:smismember, key, *members]) do |reply| + members.flatten!(1) + send_command([:smismember, key].concat(members)) do |reply| reply.map(&Boolify) end end @@ -136,7 +121,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to subtract # @return [Array<String>] members in the difference def sdiff(*keys) - send_command([:sdiff, *keys]) + keys.flatten!(1) + send_command([:sdiff].concat(keys)) end # Subtract multiple sets and store the resulting set in a key. @@ -145,7 +131,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to subtract # @return [Integer] number of elements in the resulting set def sdiffstore(destination, *keys) - send_command([:sdiffstore, destination, *keys]) + keys.flatten!(1) + send_command([:sdiffstore, destination].concat(keys)) end # Intersect multiple sets. @@ -153,7 +140,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to intersect # @return [Array<String>] members in the intersection def sinter(*keys) - send_command([:sinter, *keys]) + keys.flatten!(1) + send_command([:sinter].concat(keys)) end # Intersect multiple sets and store the resulting set in a key. @@ -162,7 +150,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to intersect # @return [Integer] number of elements in the resulting set def sinterstore(destination, *keys) - send_command([:sinterstore, destination, *keys]) + keys.flatten!(1) + send_command([:sinterstore, destination].concat(keys)) end # Add multiple sets. @@ -170,7 +159,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to unify # @return [Array<String>] members in the union def sunion(*keys) - send_command([:sunion, *keys]) + keys.flatten!(1) + send_command([:sunion].concat(keys)) end # Add multiple sets and store the resulting set in a key. @@ -179,7 +169,8 @@ class Redis # @param [String, Array<String>] keys keys pointing to sets to unify # @return [Integer] number of elements in the resulting set def sunionstore(destination, *keys) - send_command([:sunionstore, destination, *keys]) + keys.flatten!(1) + send_command([:sunionstore, destination].concat(keys)) end # Scan a set diff --git a/lib/redis/commands/sorted_sets.rb b/lib/redis/commands/sorted_sets.rb index c77e41b3f771c90a812b7240c77131e75d51e302..f07f952758425a3d2ba0be85fea30e88cc1f8767 100644 --- a/lib/redis/commands/sorted_sets.rb +++ b/lib/redis/commands/sorted_sets.rb @@ -136,7 +136,9 @@ class Redis # @return [Array<String, Float>] element and score pair if count is not specified # @return [Array<Array<String, Float>>] list of popped elements and scores def zpopmax(key, count = nil) - send_command([:zpopmax, key, count].compact) do |members| + 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 @@ -157,12 +159,80 @@ class Redis # @return [Array<String, Float>] element and score pair if count is not specified # @return [Array<Array<String, Float>>] list of popped elements and scores def zpopmin(key, count = nil) - send_command([:zpopmin, key, count].compact) do |members| + 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<String>] 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<String, Array<String, Float>>] 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<String>] 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<String, Array<String, Float>>] 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. # @@ -261,7 +331,7 @@ class Redis end args = [:zrandmember, key] - args << count if count + args << Integer(count) if count if with_scores args << "WITHSCORES" @@ -313,7 +383,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end if with_scores @@ -354,7 +424,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end send_command(args) @@ -372,7 +442,7 @@ class Redis # # @see #zrange def zrevrange(key, start, stop, withscores: false, with_scores: withscores) - args = [:zrevrange, key, start, stop] + args = [:zrevrange, key, Integer(start), Integer(stop)] if with_scores args << "WITHSCORES" @@ -466,7 +536,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end send_command(args) @@ -488,7 +558,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end send_command(args) @@ -531,7 +601,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end send_command(args, &block) @@ -561,7 +631,7 @@ class Redis if limit args << "LIMIT" - args.concat(limit) + args.concat(limit.map { |l| Integer(l) }) end send_command(args, &block) @@ -778,7 +848,8 @@ class Redis private def _zsets_operation(cmd, *keys, weights: nil, aggregate: nil, with_scores: false) - command = [cmd, keys.size, *keys] + keys.flatten!(1) + command = [cmd, keys.size].concat(keys) if weights command << "WEIGHTS" @@ -796,7 +867,8 @@ class Redis end def _zsets_operation_store(cmd, destination, keys, weights: nil, aggregate: nil) - command = [cmd, destination, keys.size, *keys] + keys.flatten!(1) + command = [cmd, destination, keys.size].concat(keys) if weights command << "WEIGHTS" diff --git a/lib/redis/commands/streams.rb b/lib/redis/commands/streams.rb index 1cd34dd7352bc0a53f14f2b572f4e03e51a98353..e3c7506884d7acc1ccb92df5617da6bb29f41ca9 100644 --- a/lib/redis/commands/streams.rb +++ b/lib/redis/commands/streams.rb @@ -21,15 +21,12 @@ class Redis # @return [Array<Hash>] information of the consumers if subcommand is `consumers` def xinfo(subcommand, key, group = nil) args = [:xinfo, subcommand, key, group].compact - synchronize do |client| - client.call(args) do |reply| - case subcommand.to_s.downcase - when 'stream' then Hashify.call(reply) - when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) } - else reply - end - end + 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. @@ -37,7 +34,7 @@ class Redis # @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) + # 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 @@ -46,17 +43,19 @@ class Redis # @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, 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.to_a.flatten) + args.concat(entry.flatten) send_command(args) end @@ -66,14 +65,30 @@ class Redis # redis.xtrim('mystream', 1000) # @example With options # redis.xtrim('mystream', 1000, approximate: true) - # - # @param key [String] the stream key - # @param mexlen [Integer] max length of entries - # @param approximate [Boolean] whether to add `~` modifier of maxlen or not + # @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, maxlen, approximate: false) - args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact + 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 @@ -113,7 +128,7 @@ class Redis def xrange(key, start = '-', range_end = '+', count: nil) args = [:xrange, key, start, range_end] args.concat(['COUNT', count]) if count - synchronize { |client| client.call(args, &HashifyStreamEntries) } + send_command(args, &HashifyStreamEntries) end # Fetches entries of the stream in descending order. @@ -334,6 +349,8 @@ class Redis # 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') # @@ -344,10 +361,13 @@ class Redis # @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<Hash>] the pending entries details if options were specified - def xpending(key, group, *args) + 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) diff --git a/lib/redis/commands/strings.rb b/lib/redis/commands/strings.rb index 69f92852f7fd12522ecb0e1e3af0796316c8615c..6080a5d6dbf0acb1888a5598d7430c8599ecd474 100644 --- a/lib/redis/commands/strings.rb +++ b/lib/redis/commands/strings.rb @@ -25,7 +25,7 @@ class Redis # @param [Integer] decrement # @return [Integer] value after decrementing it def decrby(key, decrement) - send_command([:decrby, key, decrement]) + send_command([:decrby, key, Integer(decrement)]) end # Increment the integer value of a key by one. @@ -50,7 +50,7 @@ class Redis # @param [Integer] increment # @return [Integer] value after incrementing it def incrby(key, increment) - send_command([:incrby, key, increment]) + send_command([:incrby, key, Integer(increment)]) end # Increment the numeric value of a key by the given float number. @@ -63,7 +63,7 @@ class Redis # @param [Float] increment # @return [Float] value after incrementing it def incrbyfloat(key, increment) - send_command([:incrbyfloat, key, increment], &Floatify) + send_command([:incrbyfloat, key, Float(increment)], &Floatify) end # Set the string value of a key. @@ -82,10 +82,10 @@ class Redis # @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" << ex if ex - args << "PX" << px if px - args << "EXAT" << exat if exat - args << "PXAT" << pxat if pxat + 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 @@ -105,7 +105,7 @@ class Redis # @param [String] value # @return [String] `"OK"` def setex(key, ttl, value) - send_command([:setex, key, ttl, value.to_s]) + send_command([:setex, key, Integer(ttl), value.to_s]) end # Set the time to live in milliseconds of a key. @@ -115,7 +115,7 @@ class Redis # @param [String] value # @return [String] `"OK"` def psetex(key, ttl, value) - send_command([:psetex, key, ttl, value.to_s]) + send_command([:psetex, key, Integer(ttl), value.to_s]) end # Set the value of a key, only if the key does not exist. @@ -152,7 +152,7 @@ class Redis # # @see #mset def mapped_mset(hash) - mset(hash.to_a.flatten) + mset(hash.flatten) end # Set one or more values, only if none of the keys exist. @@ -180,7 +180,7 @@ class Redis # # @see #msetnx def mapped_msetnx(hash) - msetnx(hash.to_a.flatten) + msetnx(hash.flatten) end # Get the value of a key. @@ -202,6 +202,7 @@ class Redis # # @see #mapped_mget def mget(*keys, &blk) + keys.flatten!(1) send_command([:mget, *keys], &blk) end @@ -232,7 +233,7 @@ class Redis # @param [String] value # @return [Integer] length of the string after it was modified def setrange(key, offset, value) - send_command([:setrange, key, offset, value.to_s]) + send_command([:setrange, key, Integer(offset), value.to_s]) end # Get a substring of the string stored at a key. @@ -243,7 +244,7 @@ class Redis # the end of the string # @return [Integer] `0` or `1` def getrange(key, start, stop) - send_command([:getrange, key, start, stop]) + send_command([:getrange, key, Integer(start), Integer(stop)]) end # Append a value to a key. @@ -291,10 +292,10 @@ class Redis # @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" << ex if ex - args << "PX" << px if px - args << "EXAT" << exat if exat - args << "PXAT" << pxat if pxat + 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) diff --git a/lib/redis/commands/transactions.rb b/lib/redis/commands/transactions.rb index ac294abf8f0a45a60a87a9d909be116e2d99cab5..c0dcf984d985fa2464633adef61939b47847ec16 100644 --- a/lib/redis/commands/transactions.rb +++ b/lib/redis/commands/transactions.rb @@ -5,48 +5,26 @@ class Redis module Transactions # Mark the start of a transaction block. # - # Passing a block is optional. - # # @example With a block # redis.multi do |multi| # multi.set("key", "value") # multi.incr("counter") # end # => ["OK", 6] # - # @example Without a block - # redis.multi - # # => "OK" - # redis.set("key", "value") - # # => "QUEUED" - # redis.incr("counter") - # # => "QUEUED" - # redis.exec - # # => ["OK", 6] - # # @yield [multi] the commands that are called inside this block are cached # and written to the server upon returning from it # @yieldparam [Redis] multi `self` # - # @return [String, Array<...>] - # - when a block is not given, `OK` - # - when a block is given, an array with replies + # @return [Array<...>] + # - an array with replies # # @see #watch # @see #unwatch - def multi(&block) # :nodoc: - if block_given? - if block&.arity == 0 - Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5)) - end - - synchronize do |prior_client| - pipeline = Pipeline::Multi.new(prior_client) - pipelined_connection = PipelinedConnection.new(pipeline) - yield pipelined_connection - prior_client.call_pipeline(pipeline) + def multi + synchronize do |client| + client.multi do |raw_transaction| + yield MultiConnection.new(raw_transaction) end - else - send_command([:multi]) end end @@ -82,7 +60,7 @@ class Redis # @see #multi def watch(*keys) synchronize do |client| - res = client.call([:watch, *keys]) + res = client.call_v([:watch] + keys) if block_given? begin @@ -125,8 +103,6 @@ class Redis # Discard all commands issued after MULTI. # - # Only call this method when `#multi` was called **without** a block. - # # @return [String] `"OK"` # # @see #multi diff --git a/lib/redis/connection.rb b/lib/redis/connection.rb deleted file mode 100644 index b90a921499e0ac3dcb1119a2ddf9778dadda0804..0000000000000000000000000000000000000000 --- a/lib/redis/connection.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "redis/connection/registry" - -# If a connection driver was required before this file, the array -# Redis::Connection.drivers will contain one or more classes. The last driver -# in this array will be used as default driver. If this array is empty, we load -# the plain Ruby driver as our default. Another driver can be required at a -# later point in time, causing it to be the last element of the #drivers array -# and therefore be chosen by default. -require_relative "connection/ruby" if Redis::Connection.drivers.empty? diff --git a/lib/redis/connection/command_helper.rb b/lib/redis/connection/command_helper.rb deleted file mode 100644 index f14f90a875b4de9e653aa6bd739969d609353de4..0000000000000000000000000000000000000000 --- a/lib/redis/connection/command_helper.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -class Redis - module Connection - module CommandHelper - COMMAND_DELIMITER = "\r\n" - - def build_command(args) - command = [nil] - - args.each do |i| - if i.is_a? Array - i.each do |j| - j = j.to_s - j = j.encoding == Encoding::BINARY ? j : j.b - command << "$#{j.bytesize}" - command << j - end - else - i = i.to_s - i = i.encoding == Encoding::BINARY ? i : i.b - command << "$#{i.bytesize}" - command << i - end - end - - command[0] = "*#{(command.length - 1) / 2}" - - # Trailing delimiter - command << "" - command.join(COMMAND_DELIMITER) - end - - protected - - def encode(string) - string.force_encoding(Encoding.default_external) - end - end - end -end diff --git a/lib/redis/connection/hiredis.rb b/lib/redis/connection/hiredis.rb deleted file mode 100644 index 2e0a472221c488eac181e4fd2dfbf418f69193eb..0000000000000000000000000000000000000000 --- a/lib/redis/connection/hiredis.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require "redis/connection/registry" -require "redis/errors" - -require "hiredis/connection" -require "timeout" - -class Redis - module Connection - class Hiredis - def self.connect(config) - connection = ::Hiredis::Connection.new - connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i - - if config[:scheme] == "unix" - connection.connect_unix(config[:path], connect_timeout) - elsif config[:scheme] == "rediss" || config[:ssl] - raise NotImplementedError, "SSL not supported by hiredis driver" - else - connection.connect(config[:host], config[:port], connect_timeout) - end - - instance = new(connection) - instance.timeout = config[:read_timeout] - instance - rescue Errno::ETIMEDOUT - raise TimeoutError - end - - def initialize(connection) - @connection = connection - end - - def connected? - @connection&.connected? - end - - def timeout=(timeout) - # Hiredis works with microsecond timeouts - @connection.timeout = Integer(timeout * 1_000_000) - end - - def disconnect - @connection.disconnect - @connection = nil - end - - def write(command) - @connection.write(command.flatten(1)) - rescue Errno::EAGAIN - raise TimeoutError - end - - def read - reply = @connection.read - reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) - reply - rescue Errno::EAGAIN - raise TimeoutError - rescue RuntimeError => err - raise ProtocolError, err.message - end - end - end -end - -Redis::Connection.drivers << Redis::Connection::Hiredis diff --git a/lib/redis/connection/registry.rb b/lib/redis/connection/registry.rb deleted file mode 100644 index 26ff8335d4a8a9f0683aeaa9d881c0b43cc361a0..0000000000000000000000000000000000000000 --- a/lib/redis/connection/registry.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class Redis - module Connection - # Store a list of loaded connection drivers in the Connection module. - # Redis::Client uses the last required driver by default, and will be aware - # of the loaded connection drivers if the user chooses to override the - # default connection driver. - def self.drivers - @drivers ||= [] - end - end -end diff --git a/lib/redis/connection/ruby.rb b/lib/redis/connection/ruby.rb deleted file mode 100644 index f3740453b8ed667fc3a0f14a183921e2391dc3f1..0000000000000000000000000000000000000000 --- a/lib/redis/connection/ruby.rb +++ /dev/null @@ -1,437 +0,0 @@ -# frozen_string_literal: true - -require "redis/connection/registry" -require "redis/connection/command_helper" -require "redis/errors" - -require "socket" -require "timeout" - -begin - require "openssl" -rescue LoadError - # Not all systems have OpenSSL support -end - -class Redis - module Connection - module SocketMixin - CRLF = "\r\n" - - def initialize(*args) - super(*args) - - @timeout = @write_timeout = nil - @buffer = "".b - end - - def timeout=(timeout) - @timeout = (timeout if timeout && timeout > 0) - end - - def write_timeout=(timeout) - @write_timeout = (timeout if timeout && timeout > 0) - end - - def read(nbytes) - result = @buffer.slice!(0, nbytes) - - buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT) - result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes - - result - end - - def gets - while (crlf = @buffer.index(CRLF)).nil? - @buffer << _read_from_socket(16_384) - end - - @buffer.slice!(0, crlf + CRLF.bytesize) - end - - def _read_from_socket(nbytes, buffer = nil) - loop do - case chunk = read_nonblock(nbytes, buffer, exception: false) - when :wait_readable - unless wait_readable(@timeout) - raise Redis::TimeoutError - end - when :wait_writable - unless wait_writable(@timeout) - raise Redis::TimeoutError - end - when nil - raise Errno::ECONNRESET - when String - return chunk - end - end - end - - def write(buffer) - return super(buffer) unless @write_timeout - - bytes_to_write = buffer.bytesize - total_bytes_written = 0 - loop do - case bytes_written = write_nonblock(buffer, exception: false) - when :wait_readable - unless wait_readable(@write_timeout) - raise Redis::TimeoutError - end - when :wait_writable - unless wait_writable(@write_timeout) - raise Redis::TimeoutError - end - when nil - raise Errno::ECONNRESET - when Integer - total_bytes_written += bytes_written - - if total_bytes_written >= bytes_to_write - return total_bytes_written - end - - buffer = buffer.byteslice(bytes_written..-1) - end - end - end - end - - if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" - - require "timeout" - - class TCPSocket < ::TCPSocket - include SocketMixin - - def self.connect(host, port, timeout) - Timeout.timeout(timeout) do - sock = new(host, port) - sock - end - rescue Timeout::Error - raise TimeoutError - end - end - - if defined?(::UNIXSocket) - - class UNIXSocket < ::UNIXSocket - include SocketMixin - - def self.connect(path, timeout) - Timeout.timeout(timeout) do - sock = new(path) - sock - end - rescue Timeout::Error - raise TimeoutError - end - - # JRuby raises Errno::EAGAIN on #read_nonblock even when it - # says it is readable (1.6.6, in both 1.8 and 1.9 mode). - # Use the blocking #readpartial method instead. - - def _read_from_socket(nbytes, _buffer = nil) - # JRuby: Throw away the buffer as we won't need it - # but still need to support the max arity of 2 - readpartial(nbytes) - rescue EOFError - raise Errno::ECONNRESET - end - end - - end - - else - - class TCPSocket < ::Socket - include SocketMixin - - def self.connect_addrinfo(addrinfo, port, timeout) - sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0) - sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3]) - - begin - sock.connect_nonblock(sockaddr) - rescue Errno::EINPROGRESS - raise TimeoutError unless sock.wait_writable(timeout) - - begin - sock.connect_nonblock(sockaddr) - rescue Errno::EISCONN - end - end - - sock - end - - def self.connect(host, port, timeout) - # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3) - # - # From the man page for getaddrinfo(3): - # - # If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4 - # addresses are returned in the list pointed to by res only if the - # local system has at least one IPv4 address configured, and IPv6 - # addresses are returned only if the local system has at least one - # IPv6 address configured. The loopback address is not considered - # for this case as valid as a configured address. - # - # We do want the IPv6 loopback address to be returned if applicable, - # even if it is the only configured IPv6 address on the machine. - # Also see: https://github.com/redis/redis-rb/pull/394. - addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM) - - # From the man page for getaddrinfo(3): - # - # Normally, the application should try using the addresses in the - # order in which they are returned. The sorting function used - # within getaddrinfo() is defined in RFC 3484 [...]. - # - addrinfo.each_with_index do |ai, i| - begin - return connect_addrinfo(ai, port, timeout) - rescue SystemCallError - # Raise if this was our last attempt. - raise if addrinfo.length == i + 1 - end - end - end - end - - class UNIXSocket < ::Socket - include SocketMixin - - def self.connect(path, timeout) - sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0) - sockaddr = ::Socket.pack_sockaddr_un(path) - - begin - sock.connect_nonblock(sockaddr) - rescue Errno::EINPROGRESS - raise TimeoutError unless sock.wait_writable(timeout) - - begin - sock.connect_nonblock(sockaddr) - rescue Errno::EISCONN - end - end - - sock - end - end - - end - - if defined?(OpenSSL) - class SSLSocket < ::OpenSSL::SSL::SSLSocket - include SocketMixin - - unless method_defined?(:wait_readable) - def wait_readable(timeout = nil) - to_io.wait_readable(timeout) - end - end - - unless method_defined?(:wait_writable) - def wait_writable(timeout = nil) - to_io.wait_writable(timeout) - end - end - - def self.connect(host, port, timeout, ssl_params) - # NOTE: this is using Redis::Connection::TCPSocket - tcp_sock = TCPSocket.connect(host, port, timeout) - - ctx = OpenSSL::SSL::SSLContext.new - - # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS - ctx.set_params(ssl_params || {}) - - ssl_sock = new(tcp_sock, ctx) - ssl_sock.hostname = host - - begin - # Initiate the socket connection in the background. If it doesn't fail - # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS) - # indicating the connection is in progress. - # Unlike waiting for a tcp socket to connect, you can't time out ssl socket - # connections during the connect phase properly, because IO.select only partially works. - # Instead, you have to retry. - ssl_sock.connect_nonblock - rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable - if ssl_sock.wait_readable(timeout) - retry - else - raise TimeoutError - end - rescue IO::WaitWritable - if ssl_sock.wait_writable(timeout) - retry - else - raise TimeoutError - end - end - - unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || ( - ctx.respond_to?(:verify_hostname) && - !ctx.verify_hostname - ) - ssl_sock.post_connection_check(host) - end - - ssl_sock - end - end - end - - class Ruby - include Redis::Connection::CommandHelper - - MINUS = "-" - PLUS = "+" - COLON = ":" - DOLLAR = "$" - ASTERISK = "*" - - def self.connect(config) - if config[:scheme] == "unix" - raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl] - - sock = UNIXSocket.connect(config[:path], config[:connect_timeout]) - elsif config[:scheme] == "rediss" || config[:ssl] - sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params]) - else - sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout]) - end - - instance = new(sock) - instance.timeout = config[:read_timeout] - instance.write_timeout = config[:write_timeout] - instance.set_tcp_keepalive config[:tcp_keepalive] - instance.set_tcp_nodelay if sock.is_a? TCPSocket - instance - end - - if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } - def set_tcp_keepalive(keepalive) - return unless keepalive.is_a?(Hash) - - @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) - @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time]) - @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl]) - @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes]) - end - - def get_tcp_keepalive - { - time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int, - intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int, - probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int - } - end - else - def set_tcp_keepalive(keepalive); end - - def get_tcp_keepalive - { - } - end - end - - # disables Nagle's Algorithm, prevents multiple round trips with MULTI - if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c } - def set_tcp_nodelay - @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) - end - else - def set_tcp_nodelay; end - end - - def initialize(sock) - @sock = sock - end - - def connected? - !!@sock - end - - def disconnect - @sock.close - rescue - ensure - @sock = nil - end - - def timeout=(timeout) - @sock.timeout = timeout if @sock.respond_to?(:timeout=) - end - - def write_timeout=(timeout) - @sock.write_timeout = timeout - end - - def write(command) - @sock.write(build_command(command)) - end - - def read - line = @sock.gets - reply_type = line.slice!(0, 1) - format_reply(reply_type, line) - rescue Errno::EAGAIN - raise TimeoutError - rescue OpenSSL::SSL::SSLError => ssl_error - if ssl_error.message.match?(/SSL_read: unexpected eof while reading/i) - raise EOFError, ssl_error.message - else - raise - end - end - - def format_reply(reply_type, line) - case reply_type - when MINUS then format_error_reply(line) - when PLUS then format_status_reply(line) - when COLON then format_integer_reply(line) - when DOLLAR then format_bulk_reply(line) - when ASTERISK then format_multi_bulk_reply(line) - else raise ProtocolError, reply_type - end - end - - def format_error_reply(line) - CommandError.new(line.strip) - end - - def format_status_reply(line) - line.strip - end - - def format_integer_reply(line) - line.to_i - end - - def format_bulk_reply(line) - bulklen = line.to_i - return if bulklen == -1 - - reply = encode(@sock.read(bulklen)) - @sock.read(2) # Discard CRLF. - reply - end - - def format_multi_bulk_reply(line) - n = line.to_i - return if n == -1 - - Array.new(n) { read } - end - end - end -end - -Redis::Connection.drivers << Redis::Connection::Ruby diff --git a/lib/redis/connection/synchrony.rb b/lib/redis/connection/synchrony.rb deleted file mode 100644 index 35eeaa34caeb7dbc0537bda3d8f209c06d115523..0000000000000000000000000000000000000000 --- a/lib/redis/connection/synchrony.rb +++ /dev/null @@ -1,148 +0,0 @@ -# frozen_string_literal: true - -require "redis/connection/registry" -require "redis/connection/command_helper" -require "redis/errors" - -require "em-synchrony" -require "hiredis/reader" - -::Redis.deprecate!( - "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0.0. " \ - "We're looking for people to maintain it as a separate gem, see https://github.com/redis/redis-rb/issues/915" -) - -class Redis - module Connection - class RedisClient < EventMachine::Connection - include EventMachine::Deferrable - - attr_accessor :timeout - - def post_init - @req = nil - @connected = false - @reader = ::Hiredis::Reader.new - end - - def connection_completed - @connected = true - succeed - end - - def connected? - @connected - end - - def receive_data(data) - @reader.feed(data) - - loop do - begin - reply = @reader.gets - rescue RuntimeError => err - @req.fail [:error, ProtocolError.new(err.message)] - break - end - - break if reply == false - - reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError) - @req.succeed [:reply, reply] - end - end - - def read - @req = EventMachine::DefaultDeferrable.new - @req.timeout(@timeout, :timeout) if @timeout > 0 - EventMachine::Synchrony.sync @req - end - - def send(data) - callback { send_data data } - end - - def unbind - @connected = false - if @req - @req.fail [:error, Errno::ECONNRESET] - @req = nil - else - fail - end - end - end - - class Synchrony - include Redis::Connection::CommandHelper - - def self.connect(config) - if config[:scheme] == "unix" - begin - conn = EventMachine.connect_unix_domain(config[:path], RedisClient) - rescue RuntimeError => e - if e.message == "no connection" - raise Errno::ECONNREFUSED - else - raise e - end - end - elsif config[:scheme] == "rediss" || config[:ssl] - raise NotImplementedError, "SSL not supported by synchrony driver" - else - conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c| - c.pending_connect_timeout = [config[:connect_timeout], 0.1].max - end - end - - fiber = Fiber.current - conn.callback { fiber.resume } - conn.errback { fiber.resume :refused } - - raise Errno::ECONNREFUSED if Fiber.yield == :refused - - instance = new(conn) - instance.timeout = config[:read_timeout] - instance - end - - def initialize(connection) - @connection = connection - end - - def connected? - @connection&.connected? - end - - def timeout=(timeout) - @connection.timeout = timeout - end - - def disconnect - @connection.close_connection - @connection = nil - end - - def write(command) - @connection.send(build_command(command)) - end - - def read - type, payload = @connection.read - - case type - when :reply - payload - when :error - raise payload - when :timeout - raise TimeoutError - else - raise "Unknown type #{type.inspect}" - end - end - end - end -end - -Redis::Connection.drivers << Redis::Connection::Synchrony diff --git a/lib/redis/distributed.rb b/lib/redis/distributed.rb index 3e7ccc95fc1dec2a160d5c598ae044377a099b30..33185d7c35a2638e3dfc86d58e9c70e12895afaf 100644 --- a/lib/redis/distributed.rb +++ b/lib/redis/distributed.rb @@ -20,7 +20,7 @@ class Redis def initialize(node_configs, options = {}) @tag = options[:tag] || /^\{(.+?)\}/ @ring = options[:ring] || HashRing.new - @node_configs = node_configs.dup + @node_configs = node_configs.map(&:dup) @default_options = options.dup node_configs.each { |node_config| add_node(node_config) } @subscribed_node = nil @@ -41,6 +41,8 @@ class Redis 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 @@ -64,6 +66,10 @@ class Redis on_each_node :quit end + def close + on_each_node :close + end + # Asynchronously save the dataset to disk. def bgsave on_each_node :bgsave @@ -124,6 +130,11 @@ class Redis 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) @@ -139,6 +150,11 @@ class Redis 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) @@ -161,6 +177,7 @@ class Redis # 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) @@ -169,6 +186,7 @@ class Redis # 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) @@ -177,23 +195,16 @@ class Redis # Determine if a key exists. def exists(*args) - if !Redis.exists_returns_integer && args.size == 1 - ::Redis.deprecate!( - "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \ - "use `exists?` instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = true. " \ - "(#{::Kernel.caller(1, 1).first})\n" - ) - exists?(*args) - else - keys_per_node = args.group_by { |key| node_for(key) } - keys_per_node.inject(0) do |sum, (node, keys)| - sum + node._exists(*keys) - end + 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) @@ -297,7 +308,7 @@ class Redis end # Set multiple keys to multiple values. - def mset(*_args) + def mset(*) raise CannotDistribute, :mset end @@ -306,7 +317,7 @@ class Redis end # Set multiple keys to multiple values, only if none of the keys exist. - def msetnx(*_args) + def msetnx(*) raise CannotDistribute, :msetnx end @@ -331,11 +342,13 @@ class Redis # 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 @@ -367,20 +380,21 @@ class Redis end # Count the number of set bits in a range of the string value stored at key. - def bitcount(key, start = 0, stop = -1) - node_for(key).bitcount(key, start, stop) + 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) + node.bitop(operation, destkey, keys) end end # Return the position of the first bit set to 1 or 0 in a string. - def bitpos(key, bit, start = nil, stop = nil) - node_for(key).bitpos(key, bit, start, stop) + 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. @@ -463,23 +477,15 @@ class Redis timeout = if args.last.is_a?(Hash) options = args.pop options[:timeout] - elsif args.last.respond_to?(:to_int) - last_arg = args.pop - ::Redis.deprecate!( - "Passing the timeout as a positional argument is deprecated, it should be passed as a keyword argument:\n" \ - " redis.#{cmd}(#{args.map(&:inspect).join(', ')}, timeout: #{last_arg.to_int})" \ - "(called from: #{caller(2, 1).first})" - ) - last_arg.to_int end - keys = args.flatten + args.flatten!(1) - ensure_same_node(cmd, keys) do |node| + ensure_same_node(cmd, args) do |node| if timeout - node.__send__(cmd, keys, timeout: timeout) + node.__send__(cmd, args, timeout: timeout) else - node.__send__(cmd, keys) + node.__send__(cmd, args) end end end @@ -490,6 +496,18 @@ class Redis _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) @@ -498,9 +516,9 @@ class Redis # Pop a value from a list, push it to another list and return it; or block # until one is available. - def brpoplpush(source, destination, deprecated_timeout = 0, **options) + def brpoplpush(source, destination, **options) ensure_same_node(:brpoplpush, [source, destination]) do |node| - node.brpoplpush(source, destination, deprecated_timeout, **options) + node.brpoplpush(source, destination, **options) end end @@ -534,29 +552,43 @@ class Redis 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, member) - node_for(key).sadd(key, member) + def sadd(key, *members) + node_for(key).sadd(key, *members) end # Add one or more members to a set. - def sadd?(key, member) - node_for(key).sadd?(key, member) + def sadd?(key, *members) + node_for(key).sadd?(key, *members) end # Remove one or more members from a set. - def srem(key, member) - node_for(key).srem(key, member) + def srem(key, *members) + node_for(key).srem(key, *members) end # Remove one or more members from a set. - def srem?(key, member) - node_for(key).srem?(key, member) + def srem?(key, *members) + node_for(key).srem?(key, *members) end # Remove and return a random member from a set. @@ -603,43 +635,49 @@ class Redis # Subtract multiple sets. def sdiff(*keys) + keys.flatten!(1) ensure_same_node(:sdiff, keys) do |node| - node.sdiff(*keys) + node.sdiff(keys) end end # Subtract multiple sets and store the resulting set in a key. def sdiffstore(destination, *keys) - ensure_same_node(:sdiffstore, [destination] + keys) do |node| - node.sdiffstore(destination, *keys) + 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) + node.sinter(keys) end end # Intersect multiple sets and store the resulting set in a key. def sinterstore(destination, *keys) - ensure_same_node(:sinterstore, [destination] + keys) do |node| - node.sinterstore(destination, *keys) + 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) + node.sunion(keys) end end # Add multiple sets and store the resulting set in a key. def sunionstore(destination, *keys) - ensure_same_node(:sunionstore, [destination] + keys) do |node| - node.sunionstore(destination, *keys) + keys.flatten!(1) + ensure_same_node(:sunionstore, [destination].concat(keys)) do |node| + node.sunionstore(destination, keys) end end @@ -680,6 +718,20 @@ class Redis 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) @@ -738,43 +790,49 @@ class Redis # 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) + 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) - ensure_same_node(:zinterstore, [destination] + keys) do |node| + 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) + 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) - ensure_same_node(:zunionstore, [destination] + keys) do |node| + 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) + 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) + def zdiffstore(destination, *keys, **options) + keys.flatten!(1) ensure_same_node(:zdiffstore, [destination] + keys) do |node| node.zdiffstore(destination, keys, **options) end @@ -801,7 +859,7 @@ class Redis end def mapped_hmset(key, hash) - node_for(key).hmset(key, *hash.to_a.flatten) + node_for(key).hmset(key, hash) end # Get the value of a hash field. @@ -811,11 +869,13 @@ class Redis # Get the values of all the given hash fields. def hmget(key, *fields) - node_for(key).hmget(key, *fields) + fields.flatten!(1) + node_for(key).hmget(key, fields) end def mapped_hmget(key, *fields) - Hash[*fields.zip(hmget(key, *fields)).flatten] + fields.flatten!(1) + node_for(key).mapped_hmget(key, fields) end def hrandfield(key, count = nil, **options) @@ -824,7 +884,8 @@ class Redis # Delete one or more hash fields. def hdel(key, *fields) - node_for(key).hdel(key, *fields) + fields.flatten!(1) + node_for(key).hdel(key, fields) end # Determine if a hash field exists. @@ -881,7 +942,7 @@ class Redis # Stop listening for messages posted to the given channels. def unsubscribe(*channels) - raise "Can't unsubscribe if not subscribed." unless subscribed? + raise SubscriptionError, "Can't unsubscribe if not subscribed." unless subscribed? @subscribed_node.unsubscribe(*channels) end @@ -928,9 +989,7 @@ class Redis def multi(&block) raise CannotDistribute, :multi unless @watch_key - result = node_for(@watch_key).multi(&block) - @watch_key = nil if block_given? - result + node_for(@watch_key).multi(&block) end # Execute all commands issued after MULTI. @@ -1020,7 +1079,8 @@ class Redis end def key_tag(key) - key.to_s[@tag, 1] if @tag + key = key.to_s + key[@tag, 1] if key.match?(@tag) end def ensure_same_node(command, keys) diff --git a/lib/redis/errors.rb b/lib/redis/errors.rb index 74a77271309d0cbe1498c1a66e462370b8a0df99..bef5a8d4516fbdf836b6a18e739230c456fbc920 100644 --- a/lib/redis/errors.rb +++ b/lib/redis/errors.rb @@ -2,7 +2,7 @@ class Redis # Base error for all redis-rb errors. - class BaseError < RuntimeError + class BaseError < StandardError end # Raised by the connection when a protocol error occurs. @@ -20,6 +20,15 @@ class Redis 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 @@ -40,58 +49,14 @@ class Redis 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 Cluster - # Raised when client connected to redis as cluster mode - # and failed to fetch cluster state information by commands. - class InitialSetupError < BaseError - # @param errors [Array<Redis::BaseError>] - def initialize(errors) - super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}") - end - 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 - def initialize(command) - super("Cluster client doesn't know which node the #{command} command should be sent to.") - end - end - - # Raised when commands in pipelining include cross slot keys. - class CrossSlotPipeliningError < BaseError - def initialize(keys) - super("Cluster client couldn't send pipelining to single node. "\ - "The commands include cross slot keys. #{keys}") - end - end + class SubscriptionError < BaseError end end diff --git a/lib/redis/hash_ring.rb b/lib/redis/hash_ring.rb index d84084cfdff169080ad217a3f307effbc3148556..15f38d55647fb451bc91a9b450c06f1ab9f73e02 100644 --- a/lib/redis/hash_ring.rb +++ b/lib/redis/hash_ring.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'zlib' +require 'digest/md5' class Redis class HashRing @@ -25,7 +26,7 @@ class Redis def add_node(node) @nodes << node @replicas.times do |i| - key = Zlib.crc32("#{node.id}:#{i}") + key = server_hash_for("#{node.id}:#{i}") @ring[key] = node @sorted_keys << key end @@ -35,7 +36,7 @@ class Redis def remove_node(node) @nodes.reject! { |n| n.id == node.id } @replicas.times do |i| - key = Zlib.crc32("#{node.id}:#{i}") + key = server_hash_for("#{node.id}:#{i}") @ring.delete(key) @sorted_keys.reject! { |k| k == key } end @@ -43,47 +44,46 @@ class Redis # get the node in the hash ring for this key def get_node(key) - get_node_pos(key)[0] - end - - def get_node_pos(key) - return [nil, nil] if @ring.empty? - - crc = Zlib.crc32(key) - idx = HashRing.binary_search(@sorted_keys, crc) - [@ring[@sorted_keys[idx]], idx] + 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? - _, pos = get_node_pos(key) + 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 self.binary_search(ary, value) - upper = ary.size - 1 + def binary_search(ary, value) + upper = ary.size lower = 0 - idx = 0 - - while lower <= upper - idx = (lower + upper) / 2 - comp = ary[idx] <=> value - if comp == 0 - return idx - elsif comp > 0 - upper = idx - 1 + while lower < upper + mid = (lower + upper) / 2 + if ary[mid] > value + upper = mid else - lower = idx + 1 + lower = mid + 1 end end - upper = ary.size - 1 if upper < 0 - upper + upper - 1 end end end diff --git a/lib/redis/pipeline.rb b/lib/redis/pipeline.rb index 11ed00095c2156cd7e8b9028a1f680f097ef1db8..0e415aa984381be934619588523be1fd8d40a827 100644 --- a/lib/redis/pipeline.rb +++ b/lib/redis/pipeline.rb @@ -4,27 +4,31 @@ require "delegate" class Redis class PipelinedConnection - def initialize(pipeline) + attr_accessor :db + + def initialize(pipeline, futures = [], exception: true) @pipeline = pipeline + @futures = futures + @exception = exception end include Commands - def db - @pipeline.db - end - - def db=(db) - @pipeline.db = db - end - def pipelined yield self end - def call_pipeline(pipeline) - @pipeline.call_pipeline(pipeline) - nil + 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 @@ -34,204 +38,36 @@ class Redis end def send_command(command, &block) - @pipeline.call(command, &block) - end - - def send_blocking_command(command, timeout, &block) - @pipeline.call_with_timeout(command, timeout, &block) - end - end - - class Pipeline - REDIS_INTERNAL_PATH = File.expand_path("..", __dir__).freeze - # Redis use MonitorMixin#synchronize and this class use DelegateClass which we want to filter out. - # Both are in the stdlib so we can simply filter the entire stdlib out. - STDLIB_PATH = File.expand_path("..", MonitorMixin.instance_method(:synchronize).source_location.first).freeze - - class << self - def deprecation_warning(method, caller_locations) # :nodoc: - callsite = caller_locations.find { |l| !l.path.start_with?(REDIS_INTERNAL_PATH, STDLIB_PATH) } - callsite ||= caller_locations.last # The caller_locations should be large enough, but just in case. - ::Redis.deprecate! <<~MESSAGE - Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0. - - redis.#{method} do - redis.get("key") - end - - should be replaced by - - redis.#{method} do |pipeline| - pipeline.get("key") - end - - (called from #{callsite}} - MESSAGE + future = Future.new(command, block, @exception) + @pipeline.call_v(command) do |result| + future._set(result) end - end - - attr_accessor :db - attr_reader :client - - attr :futures - alias materialized_futures futures - - def initialize(client) - @client = client.is_a?(Pipeline) ? client.client : client - @with_reconnect = true - @shutdown = false - @futures = [] - end - - def timeout - client.timeout - end - - def with_reconnect? - @with_reconnect - end - - def without_reconnect? - !@with_reconnect - end - - def shutdown? - @shutdown - end - - def empty? - @futures.empty? - end - - def call(command, timeout: nil, &block) - # A pipeline that contains a shutdown should not raise ECONNRESET when - # the connection is gone. - @shutdown = true if command.first == :shutdown - future = Future.new(command, block, timeout) @futures << future future end - def call_with_timeout(command, timeout, &block) - call(command, timeout: timeout, &block) - end - - def call_pipeline(pipeline) - @shutdown = true if pipeline.shutdown? - @futures.concat(pipeline.materialized_futures) - @db = pipeline.db - nil - end - - def commands - @futures.map(&:_command) - end - - def timeouts - @futures.map(&:timeout) - end - - def with_reconnect(val = true) - @with_reconnect = false unless val - yield - end - - def without_reconnect(&blk) - with_reconnect(false, &blk) - end - - def finish(replies, &blk) - if blk - futures.each_with_index.map do |future, i| - future._set(blk.call(replies[i])) - end - else - futures.each_with_index.map do |future, i| - future._set(replies[i]) - end - end - end - - class Multi < self - def finish(replies) - exec = replies.last - - return if exec.nil? # The transaction failed because of WATCH. - - # EXEC command failed. - raise exec if exec.is_a?(CommandError) - - if exec.size < futures.size - # Some command wasn't recognized by Redis. - command_error = replies.detect { |r| r.is_a?(CommandError) } - raise command_error - end - - super(exec) do |reply| - # Because an EXEC returns nested replies, hiredis won't be able to - # convert an error reply to a CommandError instance itself. This is - # specific to MULTI/EXEC, so we solve this here. - reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply - end - end - - def materialized_futures - if empty? - [] - else - [ - Future.new([:multi], nil, 0), - *futures, - MultiFuture.new(futures) - ] - end - end - - def timeouts - if empty? - [] - else - [nil, *super, nil] - end - end - - def commands - if empty? - [] - else - [[:multi]] + super + [[:exec]] - 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 DeprecatedPipeline < DelegateClass(Pipeline) - def initialize(pipeline) - super(pipeline) - @deprecation_displayed = false + class MultiConnection < PipelinedConnection + def multi + raise Redis::Error, "Can't nest multi transaction" end - def __getobj__ - unless @deprecation_displayed - Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 10)) - @deprecation_displayed = true - end - @delegate_dc_obj - end - end + private - class DeprecatedMulti < DelegateClass(Pipeline::Multi) - def initialize(pipeline) - super(pipeline) - @deprecation_displayed = false - end - - def __getobj__ - unless @deprecation_displayed - Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 10)) - @deprecation_displayed = true - end - @delegate_dc_obj + # 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 @@ -244,23 +80,11 @@ class Redis class Future < BasicObject FutureNotReady = ::Redis::FutureNotReady.new - attr_reader :timeout - - def initialize(command, transformation, timeout) + def initialize(command, coerce, exception) @command = command - @transformation = transformation - @timeout = timeout @object = FutureNotReady - end - - def ==(_other) - message = +"The methods == and != are deprecated for Redis::Future and will be removed in 5.0.0" - message << " - You probably meant to call .value == or .value !=" - message << " (#{::Kernel.caller(1, 1).first})\n" - - ::Redis.deprecate!(message) - - super + @coerce = coerce + @exception = exception end def inspect @@ -268,16 +92,12 @@ class Redis end def _set(object) - @object = @transformation ? @transformation.call(object) : object + @object = @coerce ? @coerce.call(object) : object value end - def _command - @command - end - def value - ::Kernel.raise(@object) if @object.is_a?(::RuntimeError) + ::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError) @object end @@ -294,13 +114,16 @@ class Redis def initialize(futures) @futures = futures @command = [:exec] + @object = FutureNotReady end def _set(replies) - @futures.each_with_index do |future, index| - future._set(replies[index]) + if replies + @futures.each_with_index do |future, index| + future._set(replies[index]) + end end - replies + @object = replies end end end diff --git a/lib/redis/subscribe.rb b/lib/redis/subscribe.rb index 5573b98f1c5d87365bbab0e8b3bf59b7131867fe..f95e29dfb255529a062f7e035e3fd7dacdead15e 100644 --- a/lib/redis/subscribe.rb +++ b/lib/redis/subscribe.rb @@ -4,10 +4,13 @@ class Redis class SubscribedClient def initialize(client) @client = client + @write_monitor = Monitor.new end - def call(command) - @client.process([command]) + def call_v(command) + @write_monitor.synchronize do + @client.call_v(command) + end end def subscribe(*channels, &block) @@ -26,12 +29,28 @@ class Redis 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([:unsubscribe, *channels]) + call_v([:unsubscribe, *channels]) end def punsubscribe(*channels) - call([:punsubscribe, *channels]) + call_v([:punsubscribe, *channels]) + end + + def sunsubscribe(*channels) + call_v([:sunsubscribe, *channels]) + end + + def close + @client.close end protected @@ -39,13 +58,21 @@ class Redis def subscription(start, stop, channels, block, timeout = 0) sub = Subscription.new(&block) - unsubscribed = false + case start + when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys + else call_v([start, *channels]) + end - @client.call_loop([start, *channels], timeout) do |line| - type, *rest = line - sub.callbacks[type].call(*rest) - unsubscribed = type == stop && rest.last == 0 - break if unsubscribed + 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). @@ -56,10 +83,7 @@ class Redis attr :callbacks def initialize - @callbacks = Hash.new do |hash, key| - hash[key] = ->(*_) {} - end - + @callbacks = {} yield(self) end @@ -86,5 +110,17 @@ class Redis 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 diff --git a/lib/redis/version.rb b/lib/redis/version.rb index b0cbed457354281a6f08d1955e04d7e31cd181bc..4d34fe45db19935e115ecb03a3a4bc580e959818 100644 --- a/lib/redis/version.rb +++ b/lib/redis/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Redis - VERSION = '4.8.0' + VERSION = '5.2.0' end diff --git a/makefile b/makefile index df25e4b215cad1d5939ae723cd30126a0fdffa29..9d9079f75db2376bfed6f26da7d6fdd6650f5486 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ -REDIS_BRANCH ?= 7.0 +REDIS_BRANCH ?= 7.2 ROOT_DIR :=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) TMP := tmp CONF := ${ROOT_DIR}/test/support/conf/redis-${REDIS_BRANCH}.conf @@ -16,7 +16,7 @@ SLAVE_SOCKET_PATH := ${BUILD_DIR}/redis_slave.sock HA_GROUP_NAME := master1 SENTINEL_PORTS := 6400 6401 6402 SENTINEL_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${SENTINEL_PORTS})) -CLUSTER_PORTS := 7000 7001 7002 7003 7004 7005 +CLUSTER_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}) @@ -54,7 +54,7 @@ start: ${BINARY} stop_slave: @$(call kill-redis,${SLAVE_PID_PATH}) -start_slave: ${BINARY} +start_slave: start @${BINARY}\ --daemonize yes\ --pidfile ${SLAVE_PID_PATH}\ @@ -62,11 +62,11 @@ start_slave: ${BINARY} --unixsocket ${SLAVE_SOCKET_PATH}\ --slaveof 127.0.0.1 ${PORT} -stop_sentinel: +stop_sentinel: stop_slave stop @$(call kill-redis,${SENTINEL_PID_PATHS}) @rm -f ${TMP}/sentinel*.conf || true -start_sentinel: ${BINARY} +start_sentinel: start start_slave @for port in ${SENTINEL_PORTS}; do\ conf=${TMP}/sentinel$$port.conf;\ touch $$conf;\ @@ -109,7 +109,7 @@ start_cluster: ${BINARY} @for port in ${CLUSTER_PORTS}; do\ ${BINARY}\ --daemonize yes\ - --appendonly yes\ + --appendonly no\ --cluster-enabled yes\ --cluster-config-file ${TMP}/nodes$$port.conf\ --cluster-node-timeout 5000\ diff --git a/redis.gemspec b/redis.gemspec index afdcaab8b053b302ee63272b3efb11f1e892f210..90711d76c60d5412d2b1ffb8c26e50cf40660ca4 100644 --- a/redis.gemspec +++ b/redis.gemspec @@ -43,9 +43,7 @@ Gem::Specification.new do |s| 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.4.0' + s.required_ruby_version = '>= 2.6.0' - s.add_development_dependency("em-synchrony") - s.add_development_dependency("hiredis") - s.add_development_dependency("mocha") + s.add_runtime_dependency('redis-client', '>= 0.22.0') end diff --git a/test/cluster/abnormal_state_test.rb b/test/cluster/abnormal_state_test.rb deleted file mode 100644 index 081ecc99b60ac54389e3bd8cf031fa3880b89157..0000000000000000000000000000000000000000 --- a/test/cluster/abnormal_state_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_abnormal_state_test.rb -class TestClusterAbnormalState < Minitest::Test - include Helper::Cluster - - def test_the_state_of_cluster_down - redis_cluster_down do - assert_raises(Redis::CommandError, 'CLUSTERDOWN Hash slot not served') do - redis.set('key1', 1) - end - - assert_equal 'fail', redis.cluster(:info).fetch('cluster_state') - end - end - - def test_the_state_of_cluster_failover - redis_cluster_failover do - 10.times do |i| - assert_equal 'OK', r.set("key#{i}", i) - end - - 10.times do |i| - assert_equal i.to_s, r.get("key#{i}") - end - - assert_equal 'ok', redis.cluster(:info).fetch('cluster_state') - end - end - - def test_the_state_of_cluster_node_failure - redis_cluster_fail_master do - assert_raises(Redis::CannotConnectError, 'Error connecting to Redis on 127.0.0.1:7002') do - r.set('key0', 0) - end - - 10.times do |i| - assert_equal 'OK', r.set("key#{i}", i) - end - - 10.times do |i| - assert_equal i.to_s, r.get("key#{i}") - end - - assert_equal 'ok', redis.cluster(:info).fetch('cluster_state') - end - end - - def test_raising_error_when_nodes_are_not_cluster_mode - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: %W[redis://127.0.0.1:#{PORT}]) - end - end -end diff --git a/test/cluster/client_key_hash_tags_test.rb b/test/cluster/client_key_hash_tags_test.rb deleted file mode 100644 index b436739eaeb852424ef79b9d331ff9c4d844cd2a..0000000000000000000000000000000000000000 --- a/test/cluster/client_key_hash_tags_test.rb +++ /dev/null @@ -1,107 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_client_key_hash_tags_test.rb -class TestClusterClientKeyHashTags < Minitest::Test - include Helper::Cluster - - def build_described_class(urls = ['redis://127.0.0.1:7000']) - option = Redis::Cluster::Option.new(cluster: urls) - node = Redis::Cluster::Node.new(option.per_node_key) - details = Redis::Cluster::CommandLoader.load(node) - Redis::Cluster::Command.new(details) - end - - def test_key_extraction - described_class = build_described_class - - assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1]) - assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.following]) - assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.followers]) - assert_equal 'foo{}{bar}', described_class.extract_first_key(%w[get foo{}{bar}]) - assert_equal '{bar', described_class.extract_first_key(%w[get foo{{bar}}zap]) - assert_equal 'bar', described_class.extract_first_key(%w[get foo{bar}{zap}]) - assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - - assert_equal '', described_class.extract_first_key([:get, '']) - assert_equal '', described_class.extract_first_key([:get, nil]) - assert_equal '', described_class.extract_first_key([:get]) - - assert_equal '', described_class.extract_first_key([:set, '', 1]) - assert_equal '', described_class.extract_first_key([:set, nil, 1]) - assert_equal '', described_class.extract_first_key([:set]) - - # Keyless commands - assert_equal '', described_class.extract_first_key([:scan, 0]) - assert_equal '', described_class.extract_first_key([:scan, 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal '', described_class.extract_first_key([:auth, 'password']) - assert_equal '', described_class.extract_first_key(%i[client kill]) - assert_equal '', described_class.extract_first_key(%i[cluster addslots]) - assert_equal '', described_class.extract_first_key(%i[command]) - assert_equal '', described_class.extract_first_key(%i[command count]) - assert_equal '', described_class.extract_first_key(%i[config get]) - assert_equal '', described_class.extract_first_key(%i[debug segfault]) - assert_equal '', described_class.extract_first_key([:echo, 'Hello World']) - assert_equal '', described_class.extract_first_key([:flushall, 'ASYNC']) - assert_equal '', described_class.extract_first_key([:flushdb, 'ASYNC']) - assert_equal '', described_class.extract_first_key([:info, 'cluster']) - assert_equal '', described_class.extract_first_key(%i[memory doctor]) - assert_equal '', described_class.extract_first_key([:ping, 'Hi']) - assert_equal '', described_class.extract_first_key([:psubscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:pubsub, 'channels', '*']) - assert_equal '', described_class.extract_first_key([:publish, 'channel', 'Hi']) - assert_equal '', described_class.extract_first_key([:punsubscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:subscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:unsubscribe, 'channel']) - assert_equal '', described_class.extract_first_key(%w[script exists sha1 sha1]) - assert_equal '', described_class.extract_first_key([:select, 1]) - assert_equal '', described_class.extract_first_key([:shutdown, 'SAVE']) - assert_equal '', described_class.extract_first_key([:slaveof, '127.0.0.1', 6379]) - assert_equal '', described_class.extract_first_key([:slowlog, 'get', 2]) - assert_equal '', described_class.extract_first_key([:swapdb, 0, 1]) - assert_equal '', described_class.extract_first_key([:wait, 1, 0]) - - # 2nd argument is not a key - assert_equal 'key1', described_class.extract_first_key([:eval, 'script', 2, 'key1', 'key2', 'first', 'second']) - assert_equal '', described_class.extract_first_key([:eval, 'return 0', 0]) - assert_equal 'key1', described_class.extract_first_key([:evalsha, 'sha1', 2, 'key1', 'key2', 'first', 'second']) - assert_equal '', described_class.extract_first_key([:evalsha, 'return 0', 0]) - assert_equal 'key1', described_class.extract_first_key([:migrate, '127.0.0.1', 6379, 'key1', 0, 5000]) - assert_equal 'key1', described_class.extract_first_key([:memory, :usage, 'key1']) - assert_equal 'key1', described_class.extract_first_key([:object, 'refcount', 'key1']) - assert_equal 'mystream', described_class.extract_first_key([:xread, 'COUNT', 2, 'STREAMS', 'mystream', 0]) - assert_equal 'mystream', described_class.extract_first_key([:xreadgroup, 'GROUP', 'mygroup', 'Bob', 'COUNT', 2, 'STREAMS', 'mystream', '>']) - end - - def test_whether_the_command_effect_is_readonly_or_not - described_class = build_described_class - - assert_equal true, described_class.should_send_to_master?([:set]) - assert_equal false, described_class.should_send_to_slave?([:set]) - - assert_equal false, described_class.should_send_to_master?([:get]) - assert_equal true, described_class.should_send_to_slave?([:get]) - - target_version('3.2.0') do - assert_equal false, described_class.should_send_to_master?([:info]) - assert_equal false, described_class.should_send_to_slave?([:info]) - end - end - - def test_cannot_build_details_from_bad_urls - assert_raises(Redis::Cluster::InitialSetupError) do - build_described_class(['redis://127.0.0.1:7006']) - end - end - - def test_builds_details_from_a_mix_of_good_and_bad_urls - described_class = build_described_class(['redis://127.0.0.1:7006', 'redis://127.0.0.1:7000']) - assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1]) - end -end diff --git a/test/cluster/client_options_test.rb b/test/cluster/client_options_test.rb deleted file mode 100644 index 01ba7311fe2515be60eb8dd279c64eae6c019215..0000000000000000000000000000000000000000 --- a/test/cluster/client_options_test.rb +++ /dev/null @@ -1,181 +0,0 @@ -# frozen_string_literal: true - -require 'uri' -require 'helper' - -# ruby -w -Itest test/cluster_client_options_test.rb -class TestClusterClientOptions < Minitest::Test - include Helper::Cluster - - def test_option_class - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: true) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal true, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: false) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal false, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal false, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', username: 'johndoe', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis') - assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://bazzap:@127.0.0.1:7000], username: 'foobar') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %W[redis://#{URI.encode_www_form_component('!&<123-abc>')}:@127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %W[redis://:#{URI.encode_www_form_component('!&<123-abc>')}@127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000/0], db: 1) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000, db: 0 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1', port: 7000 }]) - assert_equal({ '127.0.0.1:7000' => { host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: 'foo-endpoint.example.com') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: 'foo-endpoint.example.com', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: '') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: nil) - end - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: %w[invalid_uri]) - end - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1' }]) - end - end - - def test_client_accepts_valid_node_configs - nodes = ['redis://127.0.0.1:7000', - 'redis://127.0.0.1:7001', - { host: '127.0.0.1', port: '7002' }, - { 'host' => '127.0.0.1', port: 7003 }, - 'redis://127.0.0.1:7004', - 'redis://127.0.0.1:7005'] - - build_another_client(cluster: nodes) - end - - def test_client_accepts_valid_options - build_another_client(timeout: TIMEOUT) - end - - def test_client_ignores_invalid_options - build_another_client(invalid_option: true) - end - - def test_client_works_even_if_so_many_unavailable_nodes_specified - min = 7000 - max = min + Process.getrlimit(Process::RLIMIT_NOFILE).first / 3 * 2 - nodes = (min..max).map { |port| "redis://127.0.0.1:#{port}" } - redis = build_another_client(cluster: nodes) - - assert_equal 'PONG', redis.ping - end - - def test_client_does_not_accept_db_specified_url - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: ['redis://127.0.0.1:7000/1/namespace']) - end - - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: [{ host: '127.0.0.1', port: '7000' }], db: 1) - end - end - - def test_client_does_not_accept_unconnectable_node_url_only - nodes = ['redis://127.0.0.1:7006'] - - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: nodes) - end - end - - def test_client_accepts_unconnectable_node_url_included - nodes = ['redis://127.0.0.1:7000', 'redis://127.0.0.1:7006'] - - build_another_client(cluster: nodes) - end - - def test_client_does_not_accept_http_scheme_url - nodes = ['http://127.0.0.1:80'] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme 'http'") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_blank_included_config - nodes = [''] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_bool_included_config - nodes = [true] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_nil_included_config - nodes = [nil] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_array_included_config - nodes = [[]] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_empty_hash_included_config - nodes = [{}] - - assert_raises(Redis::InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys') do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_object_included_config - nodes = [Object.new] - - assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must includes String or Hash') do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_not_array_config - nodes = :not_array - - assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must be Array') do - build_another_client(cluster: nodes) - end - end -end diff --git a/test/cluster/client_replicas_test.rb b/test/cluster/client_replicas_test.rb deleted file mode 100644 index 7cb8602a4116bd2319d00cb7a659b7c57642b507..0000000000000000000000000000000000000000 --- a/test/cluster/client_replicas_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -# 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 - - def test_client_can_flush_with_replica - r = build_another_client(replica: true) - - assert_equal 'OK', r.flushall - assert_equal 'OK', r.flushdb - end - - def test_some_reference_commands_are_sent_to_slaves_if_needed - skip("This test is very flaky") if ENV["CI"] - r = build_another_client(replica: true) - - 5.times { |i| r.set("key#{i}", i) } - - r.wait(1, TIMEOUT.to_i * 1000) - - assert_equal %w[key0 key1 key2 key3 key4], r.keys - assert_equal 5, r.dbsize - end -end diff --git a/test/cluster/client_slots_test.rb b/test/cluster/client_slots_test.rb deleted file mode 100644 index 95cf28abacaf232002c7cf8536c1d1a417fc6e54..0000000000000000000000000000000000000000 --- a/test/cluster/client_slots_test.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_client_slots_test.rb -class TestClusterClientSlots < Minitest::Test - include Helper::Cluster - - def test_slot_class - slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10]) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7001') - end - - def test_slot_class_with_multiple_slot_ranges - slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10, 30..40]) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - assert_equal true, slot.exists?(30) - assert_equal true, slot.exists?(40) - assert_equal false, slot.exists?(41) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(30) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(40) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - assert_nil slot.find_node_key_of_master(41) - assert_nil slot.find_node_key_of_slave(41) - - assert_nil slot.put(1, '127.0.0.1:7001') - assert_nil slot.put(30, '127.0.0.1:7001') - end - - def test_slot_class_with_node_flags_and_replicas - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - true) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_node_flags_replicas_and_slot_range - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10, 30..40], '127.0.0.1:7001' => [1..10, 30..40] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - true) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - assert_equal true, slot.exists?(30) - assert_equal false, slot.exists?(41) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(30) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(30) - assert_nil slot.find_node_key_of_master(41) - assert_nil slot.find_node_key_of_slave(41) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_node_flags_and_without_replicas - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - false) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_empty_slots - slot = Redis::Cluster::Slot.new({}) - - assert_equal false, slot.exists?(0) - assert_equal false, slot.exists?(1) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_nil slot.find_node_key_of_master(1) - assert_nil slot.find_node_key_of_slave(1) - - assert_nil slot.put(1, '127.0.0.1:7001') - end - - def test_redirection_when_slot_is_resharding - 100.times { |i| redis.set("{key}#{i}", i) } - - redis_cluster_resharding(12_539, src: '127.0.0.1:7002', dest: '127.0.0.1:7000') do - 100.times { |i| assert_equal i.to_s, redis.get("{key}#{i}") } - end - end -end diff --git a/test/cluster/client_transactions_test.rb b/test/cluster/client_transactions_test.rb deleted file mode 100644 index bce56fc7e0fc24523f079ac19dba0dc866b9864d..0000000000000000000000000000000000000000 --- a/test/cluster/client_transactions_test.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_client_transactions_test.rb -class TestClusterClientTransactions < Minitest::Test - include Helper::Cluster - - def test_transaction_with_hash_tag - rc1 = redis - rc2 = build_another_client - - rc1.multi do |cli| - 100.times { |i| cli.set("{key}#{i}", i) } - end - - 100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") } - 100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") } - end - - def test_transaction_without_hash_tag - rc1 = redis - rc2 = build_another_client - - assert_raises(Redis::Cluster::CrossSlotPipeliningError) do - rc1.multi do |cli| - 100.times { |i| cli.set("key#{i}", i) } - end - end - - 100.times { |i| assert_nil rc1.get("key#{i}") } - 100.times { |i| assert_nil rc2.get("key#{i}") } - end - - def test_transaction_with_replicas - rc1 = build_another_client(replica: true) - rc2 = build_another_client(replica: true) - - rc1.multi do |cli| - 100.times { |i| cli.set("{key}#{i}", i) } - end - - rc1.wait(1, TIMEOUT.to_i * 1000) - - 100.times { |i| assert_equal i.to_s, rc1.get("{key}#{i}") } - 100.times { |i| assert_equal i.to_s, rc2.get("{key}#{i}") } - end - - def test_transaction_with_watch - rc1 = redis - rc2 = build_another_client - - rc1.set('{key}1', 100) - rc1.watch('{key}1') - - rc2.set('{key}1', 200) - val = rc1.get('{key}1').to_i - val += 1 - - rc1.multi do |cli| - cli.set('{key}1', val) - cli.set('{key}2', 300) - end - - assert_equal '200', rc1.get('{key}1') - assert_equal '200', rc2.get('{key}1') - - assert_nil rc1.get('{key}2') - assert_nil rc2.get('{key}2') - end -end diff --git a/test/cluster/commands_on_geo_test.rb b/test/cluster/commands_on_geo_test.rb deleted file mode 100644 index 23a6050bf9bf0bea6ea700e3f43866bfb8af83d0..0000000000000000000000000000000000000000 --- a/test/cluster/commands_on_geo_test.rb +++ /dev/null @@ -1,74 +0,0 @@ -# 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 - - MIN_REDIS_VERSION = '3.2.0' - - def add_sicily - redis.geoadd('Sicily', - 13.361389, 38.115556, 'Palermo', - 15.087269, 37.502669, 'Catania') - end - - def test_geoadd - target_version(MIN_REDIS_VERSION) do - assert_equal 2, add_sicily - end - end - - def test_geohash - target_version(MIN_REDIS_VERSION) do - add_sicily - assert_equal %w[sqc8b49rny0 sqdtr74hyu0], redis.geohash('Sicily', %w[Palermo Catania]) - end - end - - def test_geopos - target_version(MIN_REDIS_VERSION) do - add_sicily - expected = [%w[13.36138933897018433 38.11555639549629859], - %w[15.08726745843887329 37.50266842333162032], - nil] - assert_equal expected, redis.geopos('Sicily', %w[Palermo Catania NonExisting]) - end - end - - def test_geodist - target_version(MIN_REDIS_VERSION) do - add_sicily - assert_equal '166274.1516', redis.geodist('Sicily', 'Palermo', 'Catania') - assert_equal '166.2742', redis.geodist('Sicily', 'Palermo', 'Catania', 'km') - assert_equal '103.3182', redis.geodist('Sicily', 'Palermo', 'Catania', 'mi') - end - end - - def test_georadius - target_version(MIN_REDIS_VERSION) do - add_sicily - - expected = [%w[Palermo 190.4424], %w[Catania 56.4413]] - assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST') - - expected = [['Palermo', %w[13.36138933897018433 38.11555639549629859]], - ['Catania', %w[15.08726745843887329 37.50266842333162032]]] - assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHCOORD') - - expected = [['Palermo', '190.4424', %w[13.36138933897018433 38.11555639549629859]], - ['Catania', '56.4413', %w[15.08726745843887329 37.50266842333162032]]] - assert_equal expected, redis.georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD') - end - end - - def test_georadiusbymember - target_version(MIN_REDIS_VERSION) do - redis.geoadd('Sicily', 13.583333, 37.316667, 'Agrigento') - add_sicily - assert_equal %w[Agrigento Palermo], redis.georadiusbymember('Sicily', 'Agrigento', 100, 'km') - end - end -end diff --git a/test/cluster/commands_on_pub_sub_test.rb b/test/cluster/commands_on_pub_sub_test.rb deleted file mode 100644 index 8f4c2ca53bec0f4d125117bd116c82ecb1dc1577..0000000000000000000000000000000000000000 --- a/test/cluster/commands_on_pub_sub_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -# 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 = {} - - wire = Wire.new do - redis.subscribe('channel1', 'channel2') do |on| - on.subscribe { |_c, t| sub_cnt = t } - on.unsubscribe { |_c, t| sub_cnt = t } - on.message do |c, msg| - messages[c] = msg - # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments - redis.unsubscribe if messages.size == 2 - end - end - end - - Wire.pass until sub_cnt == 2 - - publisher = build_another_client - - assert_equal %w[channel1 channel2], publisher.pubsub(:channels) - assert_equal %w[channel1 channel2], publisher.pubsub(:channels, 'cha*') - assert_equal [], publisher.pubsub(:channels, 'chachacha*') - assert_equal({}, publisher.pubsub(:numsub)) - assert_equal({ 'channel1' => 1, 'channel2' => 1, 'channel3' => 0 }, - publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) - assert_equal 0, publisher.pubsub(:numpat) - - publisher.publish('channel1', 'one') - publisher.publish('channel2', 'two') - - wire.join - - assert_equal({ 'channel1' => 'one', 'channel2' => 'two' }, messages.sort.to_h) - - assert_equal [], publisher.pubsub(:channels) - assert_equal [], publisher.pubsub(:channels, 'cha*') - assert_equal [], publisher.pubsub(:channels, 'chachacha*') - assert_equal({}, publisher.pubsub(:numsub)) - assert_equal({ 'channel1' => 0, 'channel2' => 0, 'channel3' => 0 }, - publisher.pubsub(:numsub, 'channel1', 'channel2', 'channel3')) - assert_equal 0, publisher.pubsub(:numpat) - end - - def test_publish_psubscribe_punsubscribe_pubsub - sub_cnt = 0 - messages = {} - - wire = Wire.new do - redis.psubscribe('guc*', 'her*') do |on| - on.psubscribe { |_c, t| sub_cnt = t } - on.punsubscribe { |_c, t| sub_cnt = t } - on.pmessage do |_ptn, chn, msg| - messages[chn] = msg - # FIXME: blocking occurs when `unsubscribe` method was called with channel arguments - redis.punsubscribe if messages.size == 2 - end - end - end - - Wire.pass until sub_cnt == 2 - - publisher = build_another_client - - assert_equal [], publisher.pubsub(:channels) - assert_equal [], publisher.pubsub(:channels, 'bur*') - assert_equal [], publisher.pubsub(:channels, 'guc*') - assert_equal [], publisher.pubsub(:channels, 'her*') - assert_equal({}, publisher.pubsub(:numsub)) - assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3')) - assert_equal 2, publisher.pubsub(:numpat) - - publisher.publish('burberry1', 'one') - publisher.publish('gucci2', 'two') - publisher.publish('hermes3', 'three') - - wire.join - - assert_equal({ 'gucci2' => 'two', 'hermes3' => 'three' }, messages.sort.to_h) - - assert_equal [], publisher.pubsub(:channels) - assert_equal [], publisher.pubsub(:channels, 'bur*') - assert_equal [], publisher.pubsub(:channels, 'guc*') - assert_equal [], publisher.pubsub(:channels, 'her*') - assert_equal({}, publisher.pubsub(:numsub)) - assert_equal({ 'burberry1' => 0, 'gucci2' => 0, 'hermes3' => 0 }, publisher.pubsub(:numsub, 'burberry1', 'gucci2', 'hermes3')) - assert_equal 0, publisher.pubsub(:numpat) - end -end diff --git a/test/cluster/commands_on_transactions_test.rb b/test/cluster/commands_on_transactions_test.rb deleted file mode 100644 index e2964e027d432de1ad8010ec286ea970e5d05785..0000000000000000000000000000000000000000 --- a/test/cluster/commands_on_transactions_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -# 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(Redis::Cluster::AmbiguousNodeError) do - redis.multi - end - end - - def test_unwatch - assert_raises(Redis::Cluster::AmbiguousNodeError) do - redis.unwatch - end - end - - def test_watch - assert_raises(Redis::CommandError, "CROSSSLOT Keys in request don't hash to the same slot") do - redis.watch('key1', 'key2') - end - - assert_equal 'OK', redis.watch('{key}1', '{key}2') - end -end diff --git a/test/distributed/blocking_commands_test.rb b/test/distributed/blocking_commands_test.rb index e7e09997577f81b646e26578e7ca4e1d585e6d80..2d1c538e79e9e22246cbfe1bd56954231ab61f3d 100644 --- a/test/distributed/blocking_commands_test.rb +++ b/test/distributed/blocking_commands_test.rb @@ -20,41 +20,15 @@ class TestDistributedBlockingCommands < Minitest::Test end end - def test_blpop_raises_with_old_prototype - assert_raises(Redis::Distributed::CannotDistribute) do - r.blpop('foo', 'bar', 0) - end - end - def test_brpop_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpop(%w[foo bar]) end end - def test_brpop_raises_with_old_prototype - assert_raises(Redis::Distributed::CannotDistribute) do - r.brpop('foo', 'bar', 0) - end - end - def test_brpoplpush_raises assert_raises(Redis::Distributed::CannotDistribute) do r.brpoplpush('foo', 'bar') end end - - def test_brpoplpush_raises_with_old_prototype - assert_raises(Redis::Distributed::CannotDistribute) do - r.brpoplpush('foo', 'bar', 0) - end - end - - def test_bzpopmin - # Not implemented yet - end - - def test_bzpopmax - # Not implemented yet - end end diff --git a/test/distributed/commands_on_hyper_log_log_test.rb b/test/distributed/commands_on_hyper_log_log_test.rb index 38dc85dd640df047cb701abef3e62f21d16c4608..4d91427f55ce717f5a110c95c0a0cfb322fd7d75 100644 --- a/test/distributed/commands_on_hyper_log_log_test.rb +++ b/test/distributed/commands_on_hyper_log_log_test.rb @@ -7,21 +7,17 @@ class TestDistributedCommandsOnHyperLogLog < Minitest::Test include Lint::HyperLogLog def test_pfmerge - target_version '2.8.9' do - assert_raises Redis::Distributed::CannotDistribute do - super - end + assert_raises Redis::Distributed::CannotDistribute do + super end end def test_pfcount_multiple_keys_diff_nodes - target_version '2.8.9' do - assert_raises Redis::Distributed::CannotDistribute do - r.pfadd 'foo', 's1' - r.pfadd 'bar', 's2' + assert_raises Redis::Distributed::CannotDistribute do + r.pfadd 'foo', 's1' + r.pfadd 'bar', 's2' - assert r.pfcount('res', 'foo', 'bar') - end + assert r.pfcount('res', 'foo', 'bar') end end end diff --git a/test/distributed/commands_on_strings_test.rb b/test/distributed/commands_on_strings_test.rb index 5d66ec0a3c7a30c9ab797725822c9cbcbc41f7cf..85e439a0c3973664f28080ac4c3a0d447a0217e7 100644 --- a/test/distributed/commands_on_strings_test.rb +++ b/test/distributed/commands_on_strings_test.rb @@ -12,6 +12,7 @@ class TestDistributedCommandsOnStrings < Minitest::Test 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 @@ -57,13 +58,11 @@ class TestDistributedCommandsOnStrings < Minitest::Test end def test_bitop - target_version "2.5.10" do - assert_raises Redis::Distributed::CannotDistribute do - r.set("foo", "a") - r.set("bar", "b") + assert_raises Redis::Distributed::CannotDistribute do + r.set("foo", "a") + r.set("bar", "b") - r.bitop(:and, "foo&bar", "foo", "bar") - end + r.bitop(:and, "foo&bar", "foo", "bar") end end diff --git a/test/distributed/commands_on_value_types_test.rb b/test/distributed/commands_on_value_types_test.rb index 12797cd66b2c3dff6bff12b441e30c0db31e00d4..876641b7440cc0b2aa7db9ebdde22fd67457e802 100644 --- a/test/distributed/commands_on_value_types_test.rb +++ b/test/distributed/commands_on_value_types_test.rb @@ -39,39 +39,35 @@ class TestDistributedCommandsOnValueTypes < Minitest::Test end def test_unlink - target_version "4.0.0" do - r.set "foo", "s1" - r.set "bar", "s2" - r.set "baz", "s3" + r.set "foo", "s1" + r.set "bar", "s2" + r.set "baz", "s3" - assert_equal ["bar", "baz", "foo"], r.keys("*").sort + assert_equal ["bar", "baz", "foo"], r.keys("*").sort - assert_equal 1, r.unlink("foo") + assert_equal 1, r.unlink("foo") - assert_equal ["bar", "baz"], r.keys("*").sort + assert_equal ["bar", "baz"], r.keys("*").sort - assert_equal 2, r.unlink("bar", "baz") + assert_equal 2, r.unlink("bar", "baz") - assert_equal [], r.keys("*").sort - end + assert_equal [], r.keys("*").sort end def test_unlink_with_array_argument - target_version "4.0.0" do - r.set "foo", "s1" - r.set "bar", "s2" - r.set "baz", "s3" + r.set "foo", "s1" + r.set "bar", "s2" + r.set "baz", "s3" - assert_equal ["bar", "baz", "foo"], r.keys("*").sort + assert_equal ["bar", "baz", "foo"], r.keys("*").sort - assert_equal 1, r.unlink(["foo"]) + assert_equal 1, r.unlink(["foo"]) - assert_equal ["bar", "baz"], r.keys("*").sort + assert_equal ["bar", "baz"], r.keys("*").sort - assert_equal 2, r.unlink(["bar", "baz"]) + assert_equal 2, r.unlink(["bar", "baz"]) - assert_equal [], r.keys("*").sort - end + assert_equal [], r.keys("*").sort end def test_randomkey diff --git a/test/distributed/commands_requiring_clustering_test.rb b/test/distributed/commands_requiring_clustering_test.rb index 9711b10e9274b093cde03e4033b01f531a68a32e..db90025233aa25fb26f7ed9faf08f08a7aee8ab3 100644 --- a/test/distributed/commands_requiring_clustering_test.rb +++ b/test/distributed/commands_requiring_clustering_test.rb @@ -159,18 +159,16 @@ class TestDistributedCommandsRequiringClustering < Minitest::Test end def test_bitop - target_version "2.5.10" do - r.set("{qux}foo", "a") - r.set("{qux}bar", "b") - - r.bitop(:and, "{qux}foo&bar", "{qux}foo", "{qux}bar") - assert_equal "\x60", r.get("{qux}foo&bar") - r.bitop(:or, "{qux}foo|bar", "{qux}foo", "{qux}bar") - assert_equal "\x63", r.get("{qux}foo|bar") - r.bitop(:xor, "{qux}foo^bar", "{qux}foo", "{qux}bar") - assert_equal "\x03", r.get("{qux}foo^bar") - r.bitop(:not, "{qux}~foo", "{qux}foo") - assert_equal "\x9E", r.get("{qux}~foo") - end + 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 diff --git a/test/distributed/distributed_test.rb b/test/distributed/distributed_test.rb index d03b52d6060db877afc331a572e20d8d3ba351fc..8a5878498f83c34eb5c2a1ac8303b6054dc5c839 100644 --- a/test/distributed/distributed_test.rb +++ b/test/distributed/distributed_test.rb @@ -21,15 +21,12 @@ class TestDistributed < Minitest::Test end def test_add_nodes - logger = Logger.new("/dev/null") - - @r = Redis::Distributed.new NODES, logger: logger, timeout: 10 + @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 - assert_equal logger, @r.nodes[0]._client.logger @r.add_node("redis://127.0.0.1:6380/14") @@ -37,7 +34,6 @@ class TestDistributed < Minitest::Test assert_equal 6380, @r.nodes[1]._client.port assert_equal 14, @r.nodes[1]._client.db assert_equal 10, @r.nodes[1]._client.timeout - assert_equal logger, @r.nodes[1]._client.logger end def test_pipelining_commands_cannot_be_distributed diff --git a/test/distributed/internals_test.rb b/test/distributed/internals_test.rb index ac3fad205b34c447eaf880243fdd0b784292af0f..454828021411f3af5dc5da42a8b66641ff7c6c8b 100644 --- a/test/distributed/internals_test.rb +++ b/test/distributed/internals_test.rb @@ -15,19 +15,19 @@ class TestDistributedInternals < Minitest::Test def test_default_as_urls nodes = ["redis://127.0.0.1:#{PORT}/15", *NODES] redis = Redis::Distributed.new nodes - assert_equal(["redis://127.0.0.1:#{PORT}/15", *NODES], redis.nodes.map { |node| node._client.id }) + 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.id }) + 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.id }) + 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 diff --git a/test/distributed/key_tags_test.rb b/test/distributed/key_tags_test.rb index b36e0c1d1609157635fda0505b5f99780b6df350..415779ad6f57b86615049385ee05883c06645790 100644 --- a/test/distributed/key_tags_test.rb +++ b/test/distributed/key_tags_test.rb @@ -28,7 +28,7 @@ class TestDistributedKeyTags < Minitest::Test end def test_distributes_keys_if_no_clustering_is_used - r.add_node("redis://127.0.0.1:#{PORT}/14") + r.add_node("redis://127.0.0.1:#{PORT}/13") r.flushdb r.set "users:1", 1 diff --git a/test/distributed/publish_subscribe_test.rb b/test/distributed/publish_subscribe_test.rb index 93d1d2b7d862bcdb829e4d5a858b5aa7aebc85a3..34f8d69b67ffd0bebd4fdc1818a3a1a5b02fef1c 100644 --- a/test/distributed/publish_subscribe_test.rb +++ b/test/distributed/publish_subscribe_test.rb @@ -19,7 +19,7 @@ class TestDistributedPublishSubscribe < Minitest::Test @subscribed = false @unsubscribed = false - wire = Wire.new do + thread = Thread.new do r.subscribe("foo") do |on| on.subscribe do |_channel, total| @subscribed = true @@ -41,11 +41,11 @@ class TestDistributedPublishSubscribe < Minitest::Test end # Wait until the subscription is active before publishing - Wire.pass until @subscribed + Thread.pass until @subscribed Redis::Distributed.new(NODES).publish("foo", "s1") - wire.join + thread.join assert @subscribed assert_equal 1, @t1 @@ -57,7 +57,7 @@ class TestDistributedPublishSubscribe < Minitest::Test def test_subscribe_within_subscribe @channels = [] - wire = Wire.new do + thread = Thread.new do r.subscribe("foo") do |on| on.subscribe do |channel, _total| @channels << channel @@ -68,23 +68,22 @@ class TestDistributedPublishSubscribe < Minitest::Test end end - wire.join + thread.join assert_equal ["foo", "bar"], @channels end def test_other_commands_within_a_subscribe - assert_raises Redis::CommandError do - r.subscribe("foo") do |on| - on.subscribe do |_channel, _total| - r.set("bar", "s2") - end + 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 LocalJumpError do + assert_raises Redis::SubscriptionError do r.subscribe("foo") end end diff --git a/test/distributed/remote_server_control_commands_test.rb b/test/distributed/remote_server_control_commands_test.rb index 4c20c7d7846a84264851f0de3b950cab10293b64..2d2516d01efda3c4d2b473efc67115629513f55e 100644 --- a/test/distributed/remote_server_control_commands_test.rb +++ b/test/distributed/remote_server_control_commands_test.rb @@ -27,16 +27,14 @@ class TestDistributedRemoteServerControlCommands < Minitest::Test end def test_info_commandstats - target_version "2.5.7" do - r.nodes.each do |n| - n.config(:resetstat) - n.get("foo") - n.get("bar") - end + 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 + r.info(:commandstats).each do |info| + assert_equal '2', info['get']['calls'] end end @@ -52,15 +50,13 @@ class TestDistributedRemoteServerControlCommands < Minitest::Test end def test_time - target_version "2.5.4" do - # Test that the difference between the time that Ruby reports and the time - # that Redis reports is minimal (prevents the test from being racy). - r.time.each do |rv| - redis_usec = rv[0] * 1_000_000 + rv[1] - ruby_usec = Integer(Time.now.to_f * 1_000_000) + # 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 + assert((ruby_usec - redis_usec).abs < 500_000) end end end diff --git a/test/distributed/scripting_test.rb b/test/distributed/scripting_test.rb index cf69ac0a02df9c4f9ca2488dd38967a1c621b409..eda11d78a475966e497b58afb29ed74c4b0fc618 100644 --- a/test/distributed/scripting_test.rb +++ b/test/distributed/scripting_test.rb @@ -10,92 +10,78 @@ class TestDistributedScripting < Minitest::Test end def test_script_exists - target_version "2.5.9" do # 2.6-rc1 - a = to_sha("return 1") - b = a.succ - - assert_equal [true], r.script(:exists, a) - assert_equal [false], r.script(:exists, b) - assert_equal [[true]], r.script(:exists, [a]) - assert_equal [[false]], r.script(:exists, [b]) - assert_equal [[true, false]], r.script(:exists, [a, b]) - end + 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 - target_version "2.5.9" do # 2.6-rc1 - sha = to_sha("return 1") - assert r.script(:exists, sha).first - assert_equal ["OK"], r.script(:flush) - assert !r.script(:exists, sha).first - end + 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 - target_version "2.5.9" do # 2.6-rc1 - redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| - assert_equal ["KILL"], redis.script(:kill) - end + redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| + assert_equal ["KILL"], redis.script(:kill) end end def test_eval - target_version "2.5.9" do # 2.6-rc1 - assert_raises(Redis::Distributed::CannotDistribute) do - r.eval("return #KEYS") - end - - assert_raises(Redis::Distributed::CannotDistribute) do - r.eval("return KEYS", ["k1", "k2"]) - end + assert_raises(Redis::Distributed::CannotDistribute) do + r.eval("return #KEYS") + end - assert_equal ["k1"], r.eval("return KEYS", ["k1"]) - assert_equal ["a1", "a2"], r.eval("return ARGV", ["k1"], ["a1", "a2"]) + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_raises(Redis::Distributed::CannotDistribute) do - r.eval("return #KEYS", {}) - end - - assert_raises(Redis::Distributed::CannotDistribute) do - r.eval("return KEYS", { keys: ["k1", "k2"] }) - end + assert_raises(Redis::Distributed::CannotDistribute) do + r.eval("return #KEYS", {}) + end - assert_equal ["k1"], r.eval("return KEYS", { keys: ["k1"] }) - assert_equal ["a1", "a2"], r.eval("return ARGV", { keys: ["k1"], argv: ["a1", "a2"] }) + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_raises(Redis::Distributed::CannotDistribute) do - r.evalsha(to_sha("return #KEYS")) - end - - assert_raises(Redis::Distributed::CannotDistribute) do - r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) - end + assert_raises(Redis::Distributed::CannotDistribute) do + r.evalsha(to_sha("return #KEYS")) + end - assert_equal ["k1"], r.evalsha(to_sha("return KEYS"), ["k1"]) - assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), ["k1"], ["a1", "a2"]) + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_raises(Redis::Distributed::CannotDistribute) do - r.evalsha(to_sha("return #KEYS"), {}) - end - - assert_raises(Redis::Distributed::CannotDistribute) do - r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) - end + assert_raises(Redis::Distributed::CannotDistribute) do + r.evalsha(to_sha("return #KEYS"), {}) + 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"] }) + 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 diff --git a/test/distributed/transactions_test.rb b/test/distributed/transactions_test.rb index dfe9e565fdb59675d40ac2df6d929e1d65fd2164..6be3101346a950bf1436fedb54639e52a36b565a 100644 --- a/test/distributed/transactions_test.rb +++ b/test/distributed/transactions_test.rb @@ -5,22 +5,6 @@ require "helper" class TestDistributedTransactions < Minitest::Test include Helper::Distributed - def test_multi_discard - r.set("foo", 1) - - r.watch("foo") - r.multi - r.set("foo", 2) - - assert_raises Redis::Distributed::CannotDistribute do - r.set("bar", 1) - end - - r.discard - - assert_equal('1', r.get("foo")) - end - def test_multi_discard_without_watch @foo = nil @@ -74,31 +58,13 @@ class TestDistributedTransactions < Minitest::Test r.watch("{qux}foo", "{qux}bar", "{qux}baz") do assert_equal '1', r.get("{qux}baz") - result = r.multi do - r.incrby("{qux}foo", 3) - r.incrby("{qux}bar", 6) - r.incrby("{qux}baz", 9) + 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 - - def test_watch_multi_exec_without_block - r.set("{qux}baz", 1) - - assert_equal "OK", r.watch("{qux}foo", "{qux}bar", "{qux}baz") - assert_equal '1', r.get("{qux}baz") - - assert_raises Redis::Distributed::CannotDistribute do - r.get("{foo}baz") - end - - assert_equal "OK", r.multi - assert_equal "QUEUED", r.incrby("{qux}baz", 1) - assert_equal "QUEUED", r.incrby("{qux}baz", 1) - assert_equal [2, 3], r.exec - - assert_equal "OK", r.set("{other}baz", 1) - end end diff --git a/test/helper.rb b/test/helper.rb index 609d74303b13cbdc067431025fe87256bd0f12b9..d3fb5290dd3a9fcf259f7e8e093bb22b2b16454a 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) + require "minitest/autorun" require "mocha/minitest" -require "logger" -require "stringio" $VERBOSE = true @@ -13,11 +13,12 @@ require "redis" Redis.silence_deprecations = true require "redis/distributed" -require "redis/connection/#{ENV['DRIVER']}" require_relative "support/redis_mock" -require_relative "support/connection/#{ENV['DRIVER']}" -require_relative 'support/cluster/orchestrator' + +if ENV["DRIVER"] == "hiredis" + require "hiredis-client" +end PORT = 6381 DB = 15 @@ -35,10 +36,6 @@ if ENV['REDIS_SOCKET_PATH'].nil? ENV['REDIS_SOCKET_PATH'] = sock_file end -def driver(*drivers, &blk) - class_eval(&blk) if drivers.map(&:to_s).include?(ENV["DRIVER"]) -end - Dir[File.expand_path('lint/**/*.rb', __dir__)].sort.each do |f| require f end @@ -62,17 +59,6 @@ module Helper end end - def with_external_encoding(encoding) - original_encoding = Encoding.default_external - - begin - silent { Encoding.default_external = Encoding.find(encoding) } - yield - ensure - silent { Encoding.default_external = original_encoding } - end - end - class Version include Comparable @@ -110,7 +96,6 @@ module Helper alias r redis def setup - @log = StringIO.new @redis = init _new_client # Run GC to make sure orphaned connections are closed. @@ -119,7 +104,7 @@ module Helper end def teardown - redis&.quit + redis&.close super end @@ -189,7 +174,7 @@ module Helper def with_acl admin = _new_client admin.acl('SETUSER', 'johndoe', 'on', - '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes', + '+ping', '+select', '+command', '+cluster|slots', '+cluster|nodes', '+readonly', '>mysecret') yield('johndoe', 'mysecret') ensure @@ -213,7 +198,7 @@ module Helper private def _format_options(options) - OPTIONS.merge(logger: ::Logger.new(@log)).merge(options) + OPTIONS.merge(options) end def _new_client(options = {}) @@ -232,7 +217,7 @@ module Helper LOCALHOST = '127.0.0.1' def build_sentinel_client(options = {}) - opts = { host: LOCALHOST, port: SENTINEL_PORT, timeout: TIMEOUT, logger: ::Logger.new(@log) } + opts = { host: LOCALHOST, port: SENTINEL_PORT, timeout: TIMEOUT } Redis.new(opts.merge(options)) end @@ -242,11 +227,25 @@ module Helper 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, logger: ::Logger.new(@log) + role: :master, timeout: TIMEOUT, }.merge(options) end @@ -269,7 +268,6 @@ module Helper def _format_options(options) { timeout: OPTIONS[:timeout], - logger: ::Logger.new(@log) }.merge(options) end @@ -277,158 +275,4 @@ module Helper Redis::Distributed.new(NODES, _format_options(options).merge(driver: ENV["conn"])) end end - - module Cluster - include Generic - - DEFAULT_HOST = '127.0.0.1' - DEFAULT_PORTS = (7000..7005).freeze - - ClusterSlotsRawReply = lambda { |host, port| - # @see https://redis.io/topics/protocol - <<-REPLY.delete(' ') - *1\r - *4\r - :0\r - :16383\r - *3\r - $#{host.size}\r - #{host}\r - :#{port}\r - $40\r - 649fa246273043021a05f547a79478597d3f1dc5\r - *3\r - $#{host.size}\r - #{host}\r - :#{port}\r - $40\r - 649fa246273043021a05f547a79478597d3f1dc5\r - REPLY - } - - ClusterNodesRawReply = lambda { |host, port| - line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\ - 'myself,master - 0 1530797742000 1 connected 0-16383' - "$#{line.size}\r\n#{line}\r\n" - } - - def init(redis) - redis.flushall - redis - rescue Redis::CannotConnectError - puts <<-MSG - - Cannot connect to Redis Cluster. - - Make sure Redis is running on localhost, port #{DEFAULT_PORTS}. - - Try this once: - - $ make stop_cluster - - Then run the build again: - - $ make - - MSG - exit! 1 - end - - def build_another_client(options = {}) - _new_client(options) - end - - def redis_cluster_mock(commands, options = {}) - host = DEFAULT_HOST - port = nil - - cluster_subcommands = if commands.key?(:cluster) - commands.delete(:cluster) - .map { |k, v| [k.to_s.downcase, v] } - .to_h - else - {} - end - - commands[:cluster] = lambda { |subcommand, *args| - if cluster_subcommands.key?(subcommand) - cluster_subcommands[subcommand].call(*args) - else - case subcommand - when 'slots' then ClusterSlotsRawReply.call(host, port) - when 'nodes' then ClusterNodesRawReply.call(host, port) - else '+OK' - end - end - } - - commands[:command] = ->(*_) { "*0\r\n" } - - RedisMock.start(commands, options) do |po| - port = po - scheme = options[:ssl] ? 'rediss' : 'redis' - nodes = %W[#{scheme}://#{host}:#{port}] - yield _new_client(options.merge(cluster: nodes)) - end - end - - def redis_cluster_down - trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) - trib.down - yield - ensure - trib.rebuild - trib.close - end - - def redis_cluster_failover - trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) - trib.failover - yield - ensure - trib.rebuild - trib.close - end - - def redis_cluster_fail_master - trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) - trib.fail_serving_master - yield - ensure - trib.restart_cluster_nodes - trib.rebuild - trib.close - end - - # @param slot [Integer] - # @param src [String] <ip>:<port> - # @param dest [String] <ip>:<port> - def redis_cluster_resharding(slot, src:, dest:) - trib = ClusterOrchestrator.new(_default_nodes, timeout: TIMEOUT) - trib.start_resharding(slot, src, dest) - yield - trib.finish_resharding(slot, dest) - ensure - trib.rebuild - trib.close - end - - private - - def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS) - ports.map { |port| "redis://#{host}:#{port}" } - end - - def _format_options(options) - { - timeout: OPTIONS[:timeout], - logger: ::Logger.new(@log), - cluster: _default_nodes - }.merge(options) - end - - def _new_client(options = {}) - Redis.new(_format_options(options).merge(driver: ENV['DRIVER'])) - end - end end diff --git a/test/lint/authentication.rb b/test/lint/authentication.rb index 6dad12ef20e1f2a90c2c97710d12619faf1393c7..7f960e0317e5f0d7b1c50148de3fb012a2012ddf 100644 --- a/test/lint/authentication.rb +++ b/test/lint/authentication.rb @@ -15,7 +15,7 @@ module Lint def test_auth_for_acl target_version "6.0.0" do with_acl do |username, password| - assert_raises(Redis::BaseError) { redis.auth(username, 'wrongpassword') } + 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') } diff --git a/test/lint/blocking_commands.rb b/test/lint/blocking_commands.rb index 3500b53d05d507c5e6e7e62e8bf0f6556984cbd0..604400bf01092970eeeeffef2764f568ff17822c 100644 --- a/test/lint/blocking_commands.rb +++ b/test/lint/blocking_commands.rb @@ -27,7 +27,7 @@ module Lint def mock(options = {}, &blk) commands = build_mock_commands(options) - redis_mock(commands, { timeout: LOW_TIMEOUT }, &blk) + redis_mock(commands, { timeout: TIMEOUT }, &blk) end def build_mock_commands(options = {}) @@ -101,22 +101,8 @@ module Lint end def test_blpop_integer_like_timeout - mock do |r| - assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", FakeDuration.new(1)) - end - end - - def test_blpop_with_old_prototype - assert_equal ['{zap}foo', 's1'], r.blpop('{zap}foo', 0) - assert_equal ['{zap}foo', 's2'], r.blpop('{zap}foo', 0) - assert_equal ['{zap}bar', 's1'], r.blpop('{zap}bar', '{zap}foo', 0) - assert_equal ['{zap}bar', 's2'], r.blpop('{zap}foo', '{zap}bar', 0) - end - - def test_blpop_timeout_with_old_prototype - mock do |r| - assert_equal ['{zap}foo', '0'], r.blpop('{zap}foo', 0) - assert_equal ['{zap}foo', '1'], r.blpop('{zap}foo', 1) + assert_raises ArgumentError do + assert_equal ["{zap}foo", "1"], r.blpop("{zap}foo", timeout: FakeDuration.new(1)) end end @@ -134,20 +120,6 @@ module Lint end end - def test_brpop_with_old_prototype - assert_equal ['{zap}foo', 's2'], r.brpop('{zap}foo', 0) - assert_equal ['{zap}foo', 's1'], r.brpop('{zap}foo', 0) - assert_equal ['{zap}bar', 's2'], r.brpop('{zap}bar', '{zap}foo', 0) - assert_equal ['{zap}bar', 's1'], r.brpop('{zap}foo', '{zap}bar', 0) - end - - def test_brpop_timeout_with_old_prototype - mock do |r| - assert_equal ['{zap}foo', '0'], r.brpop('{zap}foo', 0) - assert_equal ['{zap}foo', '1'], r.brpop('{zap}foo', 1) - end - end - def test_brpoplpush assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux') assert_equal ['s2'], r.lrange('{zap}qux', 0, -1) @@ -160,64 +132,56 @@ module Lint end end - def test_brpoplpush_with_old_prototype - assert_equal 's2', r.brpoplpush('{zap}foo', '{zap}qux', 0) - assert_equal ['s2'], r.lrange('{zap}qux', 0, -1) + def test_bzpopmin + assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', timeout: 1) end - def test_brpoplpush_timeout_with_old_prototype - mock do |r| - assert_equal '0', r.brpoplpush('{zap}foo', '{zap}bar', 0) - assert_equal '1', r.brpoplpush('{zap}foo', '{zap}bar', 1) + 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_bzpopmin - target_version('5.0.0') do - assert_equal ['{szap}foo', 'a', 0.0], r.bzpopmin('{szap}foo', '{szap}bar', 1) - assert_nil r.bzpopmin('{szap}aaa', '{szap}bbb', 2) - end + def test_bzpopmax + assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', timeout: 1) end - def test_bzpopmax - target_version('5.0.0') do - assert_equal ['{szap}foo', 'c', 2.0], r.bzpopmax('{szap}foo', '{szap}bar', 1) - assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', 1) + def test_bzpopmax_float_timeout + target_version "6.0" do + assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', timeout: LOW_TIMEOUT) end end - driver(:ruby, :hiredis) do - def test_blmove_socket_timeout - target_version "6.2" do - mock(delay: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT) - 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: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.blpop('{zap}foo', timeout: LOW_TIMEOUT) - 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: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.brpop('{zap}foo', timeout: LOW_TIMEOUT) - 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: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) - 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 diff --git a/test/lint/hashes.rb b/test/lint/hashes.rb index 4dc40e9f59d031eac9f562b34d9ad4fc3670803d..a6d470220a98b0ba790517947027357ab0280296 100644 --- a/test/lint/hashes.rb +++ b/test/lint/hashes.rb @@ -9,17 +9,15 @@ module Lint end def test_variadic_hset - target_version "4.0.0" do - assert_equal 2, r.hset("foo", "f1", "s1", "f2", "s2") + 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 "s1", r.hget("foo", "f1") + assert_equal "s2", r.hget("foo", "f2") - assert_equal 2, r.hset("bar", { "f1" => "s1", "f2" => "s2" }) + 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 + assert_equal "s1", r.hget("bar", "f1") + assert_equal "s2", r.hget("bar", "f2") end def test_hsetnx @@ -45,33 +43,29 @@ module Lint end def test_splat_hdel - target_version "2.3.9" do - r.hset("foo", "f1", "s1") - r.hset("foo", "f2", "s2") + 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 "s1", r.hget("foo", "f1") + assert_equal "s2", r.hget("foo", "f2") - assert_equal 2, r.hdel("foo", "f1", "f2") + assert_equal 2, r.hdel("foo", "f1", "f2") - assert_nil r.hget("foo", "f1") - assert_nil r.hget("foo", "f2") - end + assert_nil r.hget("foo", "f1") + assert_nil r.hget("foo", "f2") end def test_variadic_hdel - target_version "2.3.9" do - r.hset("foo", "f1", "s1") - r.hset("foo", "f2", "s2") + 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 "s1", r.hget("foo", "f1") + assert_equal "s2", r.hget("foo", "f2") - assert_equal 2, r.hdel("foo", ["f1", "f2"]) + assert_equal 2, r.hdel("foo", ["f1", "f2"]) - assert_nil r.hget("foo", "f1") - assert_nil r.hget("foo", "f2") - end + assert_nil r.hget("foo", "f1") + assert_nil r.hget("foo", "f2") end def test_hexists @@ -137,12 +131,12 @@ module Lint end def test_hgetall - assert(r.hgetall("foo") == {}) + assert_equal({}, r.hgetall("foo")) r.hset("foo", "f1", "s1") r.hset("foo", "f2", "s2") - assert(r.hgetall("foo") == { "f1" => "s1", "f2" => "s2" }) + assert_equal({ "f1" => "s1", "f2" => "s2" }, r.hgetall("foo")) end def test_hmset @@ -178,19 +172,19 @@ module Lint r.hset("foo", "f2", "s2") r.hset("foo", "f3", "s3") - assert(r.mapped_hmget("foo", "f1") == { "f1" => "s1" }) - assert(r.mapped_hmget("foo", "f1", "f2") == { "f1" => "s1", "f2" => "s2" }) + 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 - r.mapped_hmget("foo", "f1", "f2") + result = r.pipelined do |pipeline| + pipeline.mapped_hmget("foo", "f1", "f2") end - assert_equal result[0], { "f1" => "s1", "f2" => "s2" } + assert_equal({ "f1" => "s1", "f2" => "s2" }, result[0]) end def test_hincrby @@ -208,28 +202,24 @@ module Lint end def test_hincrbyfloat - target_version "2.5.4" do - r.hincrbyfloat("foo", "f1", 1.23) + r.hincrbyfloat("foo", "f1", 1.23) - assert_equal 1.23, Float(r.hget("foo", "f1")) + assert_equal 1.23, Float(r.hget("foo", "f1")) - r.hincrbyfloat("foo", "f1", 0.77) + r.hincrbyfloat("foo", "f1", 0.77) - assert_equal "2", r.hget("foo", "f1") + assert_equal "2", r.hget("foo", "f1") - r.hincrbyfloat("foo", "f1", -0.1) + r.hincrbyfloat("foo", "f1", -0.1) - assert_equal 1.9, Float(r.hget("foo", "f1")) - end + assert_equal 1.9, Float(r.hget("foo", "f1")) end def test_hstrlen - target_version('3.2.0') do - redis.hmset('foo', 'f1', 'HelloWorld', 'f2', 99, 'f3', -256) - assert_equal 10, r.hstrlen('foo', 'f1') - assert_equal 2, r.hstrlen('foo', 'f2') - assert_equal 4, r.hstrlen('foo', 'f3') - end + 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 diff --git a/test/lint/hyper_log_log.rb b/test/lint/hyper_log_log.rb index 7db1c14b4afb93c682a2642a295671ee43dcc473..ba2f3c596b53cd9a12ed1942a805d3a6d635dba3 100644 --- a/test/lint/hyper_log_log.rb +++ b/test/lint/hyper_log_log.rb @@ -3,66 +3,54 @@ module Lint module HyperLogLog def test_pfadd - target_version "2.8.9" do - assert_equal true, r.pfadd("foo", "s1") - assert_equal true, r.pfadd("foo", "s2") - assert_equal false, r.pfadd("foo", "s1") + assert_equal 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 + assert_equal 2, r.pfcount("foo") end def test_variadic_pfadd - target_version "2.8.9" do - assert_equal true, r.pfadd("foo", ["s1", "s2"]) - assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) + assert_equal true, r.pfadd("foo", ["s1", "s2"]) + assert_equal true, r.pfadd("foo", ["s1", "s2", "s3"]) - assert_equal 3, r.pfcount("foo") - end + assert_equal 3, r.pfcount("foo") end def test_pfcount - target_version "2.8.9" do - assert_equal 0, r.pfcount("foo") + assert_equal 0, r.pfcount("foo") - assert_equal true, r.pfadd("foo", "s1") + assert_equal true, r.pfadd("foo", "s1") - assert_equal 1, r.pfcount("foo") - end + assert_equal 1, r.pfcount("foo") end def test_variadic_pfcount - target_version "2.8.9" do - assert_equal 0, r.pfcount(["{1}foo", "{1}bar"]) + 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 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 + assert_equal 2, r.pfcount("{1}foo", "{1}bar") end def test_variadic_pfcount_expanded - target_version "2.8.9" do - assert_equal 0, r.pfcount("{1}foo", "{1}bar") + 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 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 + assert_equal 2, r.pfcount("{1}foo", "{1}bar") end def test_pfmerge - target_version '2.8.9' do - r.pfadd 'foo', 's1' - r.pfadd 'bar', 's2' + r.pfadd 'foo', 's1' + r.pfadd 'bar', 's2' - assert_equal true, r.pfmerge('res', 'foo', 'bar') - assert_equal 2, r.pfcount('res') - end + assert_equal true, r.pfmerge('res', 'foo', 'bar') + assert_equal 2, r.pfcount('res') end def test_variadic_pfmerge_expanded diff --git a/test/lint/lists.rb b/test/lint/lists.rb index 534ab8b8e971b31d821f3ea775607552aa0bb8ad..ea18254dfd679b75f3efad4a21aee54a92a1049b 100644 --- a/test/lint/lists.rb +++ b/test/lint/lists.rb @@ -38,11 +38,9 @@ module Lint end def test_variadic_lpush - target_version "2.3.9" do # 2.4-rc6 - assert_equal 3, r.lpush("foo", ["s1", "s2", "s3"]) - assert_equal 3, r.llen("foo") - assert_equal "s3", r.lpop("foo") - end + 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 @@ -63,11 +61,9 @@ module Lint end def test_variadic_rpush - target_version "2.3.9" do # 2.4-rc6 - assert_equal 3, r.rpush("foo", ["s1", "s2", "s3"]) - assert_equal 3, r.llen("foo") - assert_equal "s3", r.rpop("foo") - end + 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 @@ -144,6 +140,7 @@ module Lint 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 @@ -164,6 +161,7 @@ module Lint 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 @@ -204,5 +202,31 @@ module Lint 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 diff --git a/test/lint/sets.rb b/test/lint/sets.rb index 3d5d25c04460d238711d6bf2a8a960b094d39405..bfaa5cf8451e476829d3e8f482c0fec3cdc2b294 100644 --- a/test/lint/sets.rb +++ b/test/lint/sets.rb @@ -3,9 +3,9 @@ module Lint module Sets def test_sadd - assert_equal true, r.sadd("foo", "s1") - assert_equal true, r.sadd("foo", "s2") - assert_equal false, r.sadd("foo", "s1") + assert_equal 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 @@ -13,26 +13,32 @@ module Lint def test_sadd? assert_equal true, r.sadd?("foo", "s1") assert_equal true, r.sadd?("foo", "s2") - assert_equal false, r.sadd?("foo", ["s1", "s2"]) + assert_equal false, r.sadd?("foo", "s1") assert_equal ["s1", "s2"], r.smembers("foo").sort end def test_variadic_sadd - target_version "2.3.9" do # 2.4-rc6 - assert_equal 2, r.sadd("foo", ["s1", "s2"]) - assert_equal 1, r.sadd("foo", ["s1", "s2", "s3"]) + assert_equal 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 + 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 true, r.srem("foo", "s1") - assert_equal false, r.srem("foo", "s3") + assert_equal 1, r.srem("foo", "s1") + assert_equal 0, r.srem("foo", "s3") assert_equal ["s2"], r.smembers("foo") end @@ -41,24 +47,34 @@ module Lint r.sadd("foo", "s1") r.sadd("foo", "s2") - assert_equal true, r.srem?("foo", ["s1", "s5"]) - assert_equal false, r.srem?("foo", ["s3", "s4"]) + assert_equal true, r.srem?("foo", "s1") + assert_equal false, r.srem?("foo", "s3") assert_equal ["s2"], r.smembers("foo") end def test_variadic_srem - target_version "2.3.9" do # 2.4-rc6 - r.sadd("foo", "s1") - r.sadd("foo", "s2") - r.sadd("foo", "s3") + 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 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 + 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 @@ -71,18 +87,16 @@ module Lint end def test_spop_with_positive_count - target_version "3.2.0" do - r.sadd "foo", "s1" - r.sadd "foo", "s2" - r.sadd "foo", "s3" - r.sadd "foo", "s4" + r.sadd "foo", "s1" + r.sadd "foo", "s2" + r.sadd "foo", "s3" + r.sadd "foo", "s4" - pops = r.spop("foo", 3) + pops = r.spop("foo", 3) - assert !(["s1", "s2", "s3", "s4"] & pops).empty? - assert_equal 3, pops.size - assert_equal 1, r.scard("foo") - end + assert !(["s1", "s2", "s3", "s4"] & pops).empty? + assert_equal 3, pops.size + assert_equal 1, r.scard("foo") end def test_scard diff --git a/test/lint/sorted_sets.rb b/test/lint/sorted_sets.rb index 4513e61bb7d5a5aaa5c2629a28a80692e6f9edeb..10f8ce5e98fcc6e47fca1709f445ab2e8021b663 100644 --- a/test/lint/sorted_sets.rb +++ b/test/lint/sorted_sets.rb @@ -9,43 +9,43 @@ module Lint assert_equal 1, r.zcard("foo") r.del "foo" - target_version "3.0.2" do - # XX option - assert_equal 0, r.zcard("foo") - assert_equal false, r.zadd("foo", 1, "s1", xx: true) - r.zadd("foo", 1, "s1") - assert_equal false, r.zadd("foo", 2, "s1", xx: true) - assert_equal 2, r.zscore("foo", "s1") - r.del "foo" + # 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" + # 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" + # 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' + # 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 + # 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") @@ -77,84 +77,82 @@ module Lint end def test_variadic_zadd - target_version "2.3.9" do # 2.4-rc6 - # Non-nested array with pairs - assert_equal 0, r.zcard("foo") + # 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 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") + assert_equal 1, r.zadd("foo", [4, "s1", 5, "s2", 6, "s3"]) + assert_equal 3, r.zcard("foo") - r.del "foo" + r.del "foo" - # Nested array with pairs - assert_equal 0, r.zcard("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 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") + assert_equal 1, r.zadd("foo", [[4, "s1"], [5, "s2"], [6, "s3"]]) + assert_equal 3, r.zcard("foo") - r.del "foo" + r.del "foo" - # Empty array - assert_equal 0, r.zcard("foo") + # Empty array + assert_equal 0, r.zcard("foo") - assert_equal 0, r.zadd("foo", []) - assert_equal 0, r.zcard("foo") + assert_equal 0, r.zadd("foo", []) + assert_equal 0, r.zcard("foo") - r.del "foo" + r.del "foo" - # Wrong number of arguments - assert_raises(Redis::CommandError) { r.zadd("foo", ["bar"]) } - assert_raises(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) } - end + # Wrong number of arguments + assert_raises(Redis::CommandError) { r.zadd("foo", ["bar"]) } + assert_raises(Redis::CommandError) { r.zadd("foo", ["bar", "qux", "zap"]) } - target_version "3.0.2" do - # XX option - assert_equal 0, r.zcard("foo") - assert_equal 0, r.zadd("foo", [1, "s1", 2, "s2"], xx: true) - r.zadd("foo", [1, "s1", 2, "s2"]) - assert_equal 0, r.zadd("foo", [2, "s1", 3, "s2", 4, "s3"], xx: true) - assert_equal 2, r.zscore("foo", "s1") - assert_equal 3, r.zscore("foo", "s2") - assert_nil r.zscore("foo", "s3") - assert_equal 2, r.zcard("foo") - r.del "foo" + # 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" + # 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" + # 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' + # 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 + # 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") @@ -189,25 +187,23 @@ module Lint end def test_variadic_zrem - target_version "2.3.9" do # 2.4-rc6 - r.zadd("foo", 1, "s1") - r.zadd("foo", 2, "s2") - r.zadd("foo", 3, "s3") + r.zadd("foo", 1, "s1") + r.zadd("foo", 2, "s2") + r.zadd("foo", 3, "s3") - assert_equal 3, r.zcard("foo") + assert_equal 3, r.zcard("foo") - assert_equal 0, r.zrem("foo", []) - 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 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 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 + assert_equal 1, r.zrem("foo", ["eee", "s3"]) + assert_equal 1, r.zcard("foo") end def test_zincrby @@ -470,20 +466,44 @@ module Lint end def test_zpopmax - target_version('5.0.0') do - r.zadd('foo', %w[0 a 1 b 2 c 3 d]) - assert_equal ['d', 3.0], r.zpopmax('foo') - assert_equal [['c', 2.0], ['b', 1.0]], r.zpopmax('foo', 2) - assert_equal [['a', 0.0]], r.zrange('foo', 0, -1, with_scores: true) - end + 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 - target_version('5.0.0') do - r.zadd('foo', %w[0 a 1 b 2 c 3 d]) - assert_equal ['a', 0.0], r.zpopmin('foo') - assert_equal [['b', 1.0], ['c', 2.0]], r.zpopmin('foo', 2) - assert_equal [['d', 3.0]], r.zrange('foo', 0, -1, with_scores: true) + 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 @@ -493,46 +513,40 @@ module Lint end def test_zlexcount - target_version '2.8.9' do - r.zadd 'foo', 0, 'aaren' - r.zadd 'foo', 0, 'abagael' - r.zadd 'foo', 0, 'abby' - r.zadd 'foo', 0, 'abbygail' - - assert_equal 4, r.zlexcount('foo', '[a', "[a\xff") - assert_equal 4, r.zlexcount('foo', '[aa', "[ab\xff") - assert_equal 3, r.zlexcount('foo', '(aaren', "[ab\xff") - assert_equal 2, r.zlexcount('foo', '[aba', '(abbygail') - assert_equal 1, r.zlexcount('foo', '(aaren', '(abby') - end + 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 - target_version '2.8.9' do - r.zadd 'foo', 0, 'aaren' - r.zadd 'foo', 0, 'abagael' - r.zadd 'foo', 0, 'abby' - r.zadd 'foo', 0, 'abbygail' - - assert_equal %w[aaren abagael abby abbygail], r.zrangebylex('foo', '[a', "[a\xff") - assert_equal %w[aaren abagael], r.zrangebylex('foo', '[a', "[a\xff", limit: [0, 2]) - assert_equal %w[abby abbygail], r.zrangebylex('foo', '(abb', "(abb\xff") - assert_equal %w[abbygail], r.zrangebylex('foo', '(abby', "(abby\xff") - end + 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 - target_version '2.9.9' do - r.zadd 'foo', 0, 'aaren' - r.zadd 'foo', 0, 'abagael' - r.zadd 'foo', 0, 'abby' - r.zadd 'foo', 0, 'abbygail' - - assert_equal %w[abbygail abby abagael aaren], r.zrevrangebylex('foo', "[a\xff", '[a') - assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "[a\xff", '[a', limit: [0, 2]) - assert_equal %w[abbygail abby], r.zrevrangebylex('foo', "(abb\xff", '(abb') - assert_equal %w[abbygail], r.zrevrangebylex('foo', "(abby\xff", '(abby') - end + 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 diff --git a/test/lint/streams.rb b/test/lint/streams.rb index d7060fbb0e2c0e81b5066c30a11d3f420b6ae784..f23a4ae8691331fef6cf3e569c9421d57a45500d 100644 --- a/test/lint/streams.rb +++ b/test/lint/streams.rb @@ -3,7 +3,6 @@ module Lint module Streams MIN_REDIS_VERSION = '4.9.0' - MIN_REDIS_VERSION_XAUTOCLAIM = '6.2.0' ENTRY_ID_FORMAT = /\d+-\d+/.freeze def setup @@ -90,8 +89,18 @@ module Lint 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(Redis::CommandError) { redis.xadd(nil, {}) } + assert_raises(TypeError) { redis.xadd(nil, {}) } assert_raises(Redis::CommandError) { redis.xadd('', {}) } assert_raises(Redis::CommandError) { redis.xadd('s1', {}) } end @@ -112,6 +121,62 @@ module Lint 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 @@ -119,12 +184,10 @@ module Lint def test_xtrim_with_invalid_arguments if version >= '6.2' assert_raises(Redis::CommandError) { redis.xtrim('', '') } - assert_raises(Redis::CommandError) { redis.xtrim(nil, nil) } 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(nil, nil) assert_equal 0, redis.xtrim('s1', 0) assert_equal 0, redis.xtrim('s1', -1, approximate: true) end @@ -146,8 +209,8 @@ module Lint end def test_xdel_with_invalid_arguments - assert_equal 0, redis.xdel(nil, nil) - assert_equal 0, redis.xdel(nil, [nil]) + 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', []) } @@ -222,7 +285,7 @@ module Lint end def test_xrange_with_invalid_arguments - assert_equal([], redis.xrange(nil)) + assert_raises(TypeError) { redis.xrange(nil) } assert_equal([], redis.xrange('')) end @@ -298,7 +361,7 @@ module Lint end def test_xrevrange_with_invalid_arguments - assert_equal([], redis.xrevrange(nil)) + assert_raises(TypeError) { redis.xrevrange(nil) } assert_equal([], redis.xrevrange('')) end @@ -313,7 +376,7 @@ module Lint end def test_xlen_with_invalid_key - assert_equal 0, redis.xlen(nil) + assert_raises(TypeError) { redis.xlen(nil) } assert_equal 0, redis.xlen('') end @@ -357,19 +420,20 @@ module Lint def test_xread_does_not_raise_timeout_error_when_the_block_option_is_zero_msec prepared = false actual = nil - wire = Wire.new do + thread = Thread.new do prepared = true actual = redis.xread('s1', 0, block: 0) end - Wire.pass until prepared - redis.dup.xadd('s1', { f: 'v1' }, id: '0-1') - wire.join + 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(Redis::CommandError) { redis.xread(nil, nil) } + 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([''], ['']) } @@ -481,7 +545,7 @@ module Lint end def test_xreadgroup_with_invalid_arguments - assert_raises(Redis::CommandError) { redis.xreadgroup(nil, nil, nil, nil) } + 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('', '', [''], ['']) } @@ -534,7 +598,7 @@ module Lint end def test_xack_with_invalid_arguments - assert_equal 0, redis.xack(nil, nil, nil) + assert_raises(TypeError) { redis.xack(nil, nil, nil) } assert_equal 0, redis.xack('', '', '') assert_raises(Redis::CommandError) { redis.xack('', '', []) } assert_equal 0, redis.xack('', '', ['']) @@ -641,12 +705,12 @@ module Lint end def test_xclaim_with_invalid_arguments - assert_raises(Redis::CommandError) { redis.xclaim(nil, nil, nil, nil, nil) } + assert_raises(TypeError) { redis.xclaim(nil, nil, nil, nil, nil) } assert_raises(Redis::CommandError) { redis.xclaim('', '', '', '', '') } end def test_xautoclaim - omit_version(MIN_REDIS_VERSION_XAUTOCLAIM) + omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') @@ -663,7 +727,7 @@ module Lint end def test_xautoclaim_with_justid_option - omit_version(MIN_REDIS_VERSION_XAUTOCLAIM) + omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') @@ -679,7 +743,7 @@ module Lint end def test_xautoclaim_with_count_option - omit_version(MIN_REDIS_VERSION_XAUTOCLAIM) + omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') @@ -696,7 +760,7 @@ module Lint end def test_xautoclaim_with_larger_interval - omit_version(MIN_REDIS_VERSION_XAUTOCLAIM) + omit_version('6.2.0') redis.xadd('s1', { f: 'v1' }, id: '0-1') redis.xgroup(:create, 's1', 'g1', '$') @@ -711,6 +775,22 @@ module Lint 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', '$') @@ -752,6 +832,51 @@ module Lint 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', '$') diff --git a/test/lint/strings.rb b/test/lint/strings.rb index 78a483aa9424b85faffe5ae160781c740ea3cc84..ebf10a8f8f85d21b5a952657f87ee69993c83f7b 100644 --- a/test/lint/strings.rb +++ b/test/lint/strings.rb @@ -27,28 +27,22 @@ module Lint end def test_set_and_get_with_ascii_characters - with_external_encoding("ASCII-8BIT") do - (0..255).each do |i| - str = "#{i.chr}---#{i.chr}" - r.set("foo", str) + (0..255).each do |i| + str = "#{i.chr}---#{i.chr}" + r.set("foo", str) - assert_equal str, r.get("foo") - end + assert_equal str, r.get("foo") end end def test_set_with_ex - target_version "2.6.12" do - r.set("foo", "bar", ex: 2) - assert_in_range 0..2, r.ttl("foo") - end + r.set("foo", "bar", ex: 2) + assert_in_range 0..2, r.ttl("foo") end def test_set_with_px - target_version "2.6.12" do - r.set("foo", "bar", px: 2000) - assert_in_range 0..2, r.ttl("foo") - end + r.set("foo", "bar", px: 2000) + assert_in_range 0..2, r.ttl("foo") end def test_set_with_exat @@ -66,26 +60,22 @@ module Lint end def test_set_with_nx - target_version "2.6.12" do - r.set("foo", "qux", nx: true) - assert !r.set("foo", "bar", nx: true) - assert_equal "qux", r.get("foo") + r.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 + r.del("foo") + assert r.set("foo", "bar", nx: true) + assert_equal "bar", r.get("foo") end def test_set_with_xx - target_version "2.6.12" do - r.set("foo", "qux") - assert r.set("foo", "bar", xx: true) - assert_equal "bar", r.get("foo") + r.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 + r.del("foo") + assert !r.set("foo", "bar", xx: true) end def test_set_with_keepttl @@ -124,21 +114,17 @@ module Lint end def test_psetex - target_version "2.5.4" do - assert r.psetex("foo", 1000, "bar") - assert_equal "bar", r.get("foo") - assert [0, 1].include? r.ttl("foo") - end + 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 - target_version "2.5.4" do - value = ["b", "a", "r"] + 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 + 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 @@ -208,11 +194,9 @@ module Lint end def test_incrbyfloat - target_version "2.5.4" do - assert_equal 1.23, r.incrbyfloat("foo", 1.23) - assert_equal 2, r.incrbyfloat("foo", 0.77) - assert_equal 1.9, r.incrbyfloat("foo", -0.1) - end + 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 @@ -259,11 +243,18 @@ module Lint end def test_bitcount - target_version "2.5.10" do + r.set("foo", "abcde") + + assert_equal 10, r.bitcount("foo", 1, 3) + assert_equal 17, r.bitcount("foo", 0, -1) + end + + def test_bitcount_bits_range + target_version "7.0" do r.set("foo", "abcde") - assert_equal 10, r.bitcount("foo", 1, 3) - assert_equal 17, r.bitcount("foo", 0, -1) + assert_equal 10, r.bitcount("foo", 8, 31, scale: :bit) + assert_equal 17, r.bitcount("foo", 0, -1, scale: :byte) end end @@ -299,10 +290,8 @@ module Lint end def test_bitfield - target_version('3.2.0') do - mock(bitfield: ->(*_) { "*2\r\n:1\r\n:0\r\n" }) do |redis| - assert_equal [1, 0], redis.bitfield('foo', 'INCRBY', 'i5', 100, 1, 'GET', 'u4', 0) - end + 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 @@ -312,6 +301,7 @@ module Lint 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 @@ -334,8 +324,8 @@ module Lint r.set('{1}foo', 's1') r.set('{1}bar', 's2') - result = r.pipelined do - r.mapped_mget('{1}foo', '{1}bar') + result = r.pipelined do |pipeline| + pipeline.mapped_mget('{1}foo', '{1}bar') end assert_equal({ '{1}foo' => 's1', '{1}bar' => 's2' }, result[0]) @@ -380,21 +370,21 @@ module Lint end def test_bitop - with_external_encoding('UTF-8') do - target_version '2.5.10' do - r.set('foo{1}', 'a') - r.set('bar{1}', 'b') - - r.bitop(:and, 'foo&bar{1}', 'foo{1}', 'bar{1}') - assert_equal "\x60", r.get('foo&bar{1}') - r.bitop(:or, 'foo|bar{1}', 'foo{1}', 'bar{1}') - assert_equal "\x63", r.get('foo|bar{1}') - r.bitop(:xor, 'foo^bar{1}', 'foo{1}', 'bar{1}') - assert_equal "\x03", r.get('foo^bar{1}') - r.bitop(:not, '~foo{1}', 'foo{1}') - assert_equal "\x9E", r.get('~foo{1}') - end - end + 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 diff --git a/test/lint/value_types.rb b/test/lint/value_types.rb index 0b5d620d484b290a7d57a8a778a0124ffdd6957e..f8fce5b4451d1d1ae74868f4c6ed69e19a1bd914 100644 --- a/test/lint/value_types.rb +++ b/test/lint/value_types.rb @@ -8,18 +8,7 @@ module Lint r.set("foo", "s1") assert_equal 1, r.exists("foo") - end - - def test_exists_integer - previous_exists_returns_integer = Redis.exists_returns_integer - Redis.exists_returns_integer = false - assert_equal false, r.exists("foo") - - r.set("foo", "s1") - - assert_equal true, r.exists("foo") - ensure - Redis.exists_returns_integer = previous_exists_returns_integer + assert_equal 1, r.exists(["foo"]) end def test_variadic_exists @@ -32,6 +21,7 @@ module Lint 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? @@ -40,10 +30,12 @@ module Lint 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 @@ -83,12 +75,12 @@ module Lint end def test_pexpire - target_version "2.5.4" do - r.set("foo", "s1") - assert r.pexpire("foo", 2000) - assert_in_range 0..2, r.ttl("foo") - end + 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) @@ -108,7 +100,9 @@ module Lint 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) @@ -124,13 +118,26 @@ module Lint end end - def test_pexpireat - target_version "2.5.4" do - r.set("foo", "s1") - assert r.pexpireat("foo", (Time.now + 2).to_i * 1_000) - assert_in_range 0..2, r.ttl("foo") + 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) @@ -146,6 +153,19 @@ module Lint 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) @@ -161,39 +181,35 @@ module Lint end def test_pttl - target_version "2.5.4" do - r.set("foo", "s1") - r.expire("foo", 2) - assert_in_range 1..2000, r.pttl("foo") - end + r.set("foo", "s1") + r.expire("foo", 2) + assert_in_range 1..2000, r.pttl("foo") end def test_dump_and_restore - target_version "2.5.7" do - r.set("foo", "a") - v = r.dump("foo") - r.del("foo") - - assert r.restore("foo", 1000, v) - assert_equal "a", r.get("foo") - assert [0, 1].include? r.ttl("foo") - - r.rpush("bar", ["b", "c", "d"]) - w = r.dump("bar") - r.del("bar") - - assert r.restore("bar", 1000, w) - assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1) - assert [0, 1].include? r.ttl("bar") - - r.set("bar", "somethingelse") - assert_raises(Redis::CommandError) { r.restore("bar", 1000, w) } # ensure by default replace is false - assert_raises(Redis::CommandError) { r.restore("bar", 1000, w, replace: false) } - assert_equal "somethingelse", r.get("bar") - assert r.restore("bar", 1000, w, replace: true) - assert_equal ["b", "c", "d"], r.lrange("bar", 0, -1) - assert [0, 1].include? r.ttl("bar") - end + 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 diff --git a/test/redis/bitpos_test.rb b/test/redis/bitpos_test.rb index 48a816e9ce6e7ae9c53d5416e2415fd0f3301130..89304493b6e84948b255ffd300fa26d3e2a025a3 100644 --- a/test/redis/bitpos_test.rb +++ b/test/redis/bitpos_test.rb @@ -6,58 +6,52 @@ class TestBitpos < Minitest::Test include Helper::Client def test_bitpos_empty_zero - target_version "2.9.11" do - r.del "foo" - assert_equal(0, r.bitpos("foo", 0)) - end + r.del "foo" + assert_equal(0, r.bitpos("foo", 0)) end def test_bitpos_empty_one - target_version "2.9.11" do - r.del "foo" - assert_equal(-1, r.bitpos("foo", 1)) - end + r.del "foo" + assert_equal(-1, r.bitpos("foo", 1)) end def test_bitpos_zero - target_version "2.9.11" do - r.set "foo", "\xff\xf0\x00" - assert_equal(12, r.bitpos("foo", 0)) - end + r.set "foo", "\xff\xf0\x00" + assert_equal(12, r.bitpos("foo", 0)) end def test_bitpos_one - target_version "2.9.11" do - r.set "foo", "\x00\x0f\x00" - assert_equal(12, r.bitpos("foo", 1)) - end + r.set "foo", "\x00\x0f\x00" + assert_equal(12, r.bitpos("foo", 1)) end def test_bitpos_zero_end_is_given - target_version "2.9.11" do - r.set "foo", "\xff\xff\xff" - assert_equal(24, r.bitpos("foo", 0)) - assert_equal(24, r.bitpos("foo", 0, 0)) - assert_equal(-1, r.bitpos("foo", 0, 0, -1)) - end + 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 - target_version "2.9.11" do + r.set "foo", "\x00\xff\x00" + assert_equal(8, r.bitpos("foo", 1, 0, -1)) + assert_equal(8, r.bitpos("foo", 1, 1, -1)) + assert_equal(-1, r.bitpos("foo", 1, 2, -1)) + assert_equal(-1, r.bitpos("foo", 1, 2, 200)) + assert_equal(8, r.bitpos("foo", 1, 1, 1)) + end + + 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, 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)) + 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 - target_version "2.9.11" do - assert_raises(ArgumentError) do - r.bitpos("foo", 0, nil, 2) - end + assert_raises(ArgumentError) do + r.bitpos("foo", 0, nil, 2) end end end diff --git a/test/redis/blocking_commands_test.rb b/test/redis/blocking_commands_test.rb index f695765e982742d545d360e4f7f28c0c91cc4a95..bdd64e8ecf354532a73e231ed97effb429534260 100644 --- a/test/redis/blocking_commands_test.rb +++ b/test/redis/blocking_commands_test.rb @@ -15,8 +15,7 @@ class TestBlockingCommands < Minitest::Test yield(r) t2 = Time.now - assert timeout == r._client.timeout - assert delay <= (t2 - t1) + assert_operator delay, :<=, (t2 - t1) end end @@ -47,18 +46,18 @@ class TestBlockingCommands < Minitest::Test end def test_brpoplpush_in_transaction - results = r.multi do - r.brpoplpush('foo', 'bar') - r.brpoplpush('foo', 'bar', timeout: 2) + 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 - r.brpoplpush('foo', 'bar') - r.brpoplpush('foo', 'bar', timeout: 2) + results = r.pipelined do |transaction| + transaction.brpoplpush('foo', 'bar') + transaction.brpoplpush('foo', 'bar', timeout: 2) end assert_equal ['0', '2'], results end diff --git a/test/redis/client_test.rb b/test/redis/client_test.rb index af60040728ca559b947ffaacf96d3cc3857a52e0..bec97431ca21c85d22b1eda75c2fb3199f731d3e 100644 --- a/test/redis/client_test.rb +++ b/test/redis/client_test.rb @@ -26,57 +26,18 @@ class TestClient < Minitest::Test end end - def test_queue_commit - r.queue("SET", "foo", "bar") - r.queue("GET", "foo") - result = r.commit + def test_error_translate_subclasses + error = Class.new(RedisClient::CommandError) + assert_equal Redis::CommandError, Redis::Client.send(:translate_error_class, error) - assert_equal result, ["OK", "bar"] - end - - def test_commit_raise - r.queue("SET", "foo", "bar") - r.queue("INCR") - - assert_raises(Redis::CommandError) do - r.commit - end - end - - def test_queue_after_error - r.queue("SET", "foo", "bar") - r.queue("INCR") - - assert_raises(Redis::CommandError) do - r.commit - end - - r.queue("SET", "foo", "bar") - r.queue("INCR", "baz") - result = r.commit - - assert_equal result, ["OK", 1] - end - - def test_client_with_custom_connector - custom_connector = Class.new(Redis::Client::Connector) do - def resolve - @options[:host] = '127.0.0.5' - @options[:port] = '999' - @options - end - end - - error = assert_raises do - new_redis = _new_client(connector: custom_connector) - new_redis.ping + assert_raises KeyError do + Redis::Client.send(:translate_error_class, StandardError) end - assert_match(/Error connecting to Redis on 127\.0\.0\.5:999 (.+)/, error.message) 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", r.call("GET", "fée") + 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") diff --git a/test/redis/command_map_test.rb b/test/redis/command_map_test.rb deleted file mode 100644 index 42917b5f65738db27af0a933f2b0b270d3d57f93..0000000000000000000000000000000000000000 --- a/test/redis/command_map_test.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -class TestCommandMap < Minitest::Test - include Helper::Client - - def test_override_existing_commands - r.set("counter", 1) - - assert_equal 2, r.incr("counter") - - r._client.command_map[:incr] = :decr - - assert_equal 1, r.incr("counter") - end - - def test_override_non_existing_commands - r.set("key", "value") - - assert_raises Redis::CommandError do - r.idontexist("key") - end - - r._client.command_map[:idontexist] = :get - - assert_equal "value", r.idontexist("key") - end -end diff --git a/test/redis/commands_on_geo_test.rb b/test/redis/commands_on_geo_test.rb index 90171bf1615a1670bc4ca00d5c6ee417904aa744..62ba99c0a2e302056ade2a11c7fb5f07bfc44ef9 100644 --- a/test/redis/commands_on_geo_test.rb +++ b/test/redis/commands_on_geo_test.rb @@ -8,126 +8,96 @@ class TestCommandsGeo < Minitest::Test def setup super - target_version "3.2.0" do - added_items_count = r.geoadd("Sicily", 13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania") - assert_equal 2, added_items_count - end + 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 - target_version "3.2.0" do - added_items_count = r.geoadd("SicilyArray", [13.361389, 38.115556, "Palermo", 15.087269, 37.502669, "Catania"]) - assert_equal 2, added_items_count - end + 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 - target_version "3.2.0" do - r.geoadd("Chad", 15, 15, "Kanem") - nearest_cities = r.georadius("Chad", 15, 15, 15, 'km', sort: 'asc') - assert_equal %w(Kanem), nearest_cities - end + 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 - target_version "3.2.0" do - nearest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'asc') - assert_equal %w(Catania Palermo), nearest_cities + 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 + farthest_cities = r.georadius("Sicily", 15, 37, 200, 'km', sort: 'desc') + assert_equal %w(Palermo Catania), farthest_cities end def test_georadius_with_count - target_version "3.2.0" do - city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1) - assert_equal %w(Catania), city - end + city = r.georadius("Sicily", 15, 37, 200, 'km', count: 1) + assert_equal %w(Catania), city end def test_georadius_with_options_count_sort - target_version "3.2.0" do - city = r.georadius("Sicily", 15, 37, 200, 'km', sort: :desc, options: :WITHDIST, count: 1) - assert_equal [["Palermo", "190.4424"]], city - end + 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 - target_version "3.2.0" do - nearest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'asc') - assert_equal %w(Catania Palermo), nearest_cities + 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 + farthest_cities = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: 'desc') + assert_equal %w(Palermo Catania), farthest_cities end def test_georadiusbymember_with_count - target_version "3.2.0" do - city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1) - assert_equal %w(Catania), city - end + city = r.georadiusbymember("Sicily", "Catania", 200, 'km', count: 1) + assert_equal %w(Catania), city end def test_georadiusbymember_with_options_count_sort - target_version "3.2.0" do - city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1) - assert_equal [["Palermo", "166.2742"]], city - end + city = r.georadiusbymember("Sicily", "Catania", 200, 'km', sort: :desc, options: :WITHDIST, count: 1) + assert_equal [["Palermo", "166.2742"]], city end def test_geopos - target_version "3.2.0" do - location = r.geopos("Sicily", "Catania") - assert_equal [["15.08726745843887329", "37.50266842333162032"]], location + 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 + locations = r.geopos("Sicily", ["Palermo", "Catania"]) + assert_equal [["13.36138933897018433", "38.11555639549629859"], ["15.08726745843887329", "37.50266842333162032"]], locations end def test_geopos_nonexistant_location - target_version "3.2.0" do - location = r.geopos("Sicily", "Rome") - assert_equal [nil], 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 + locations = r.geopos("Sicily", ["Rome", "Catania"]) + assert_equal [nil, ["15.08726745843887329", "37.50266842333162032"]], locations end def test_geodist - target_version "3.2.0" do - distination_in_meters = r.geodist("Sicily", "Palermo", "Catania") - assert_equal "166274.1516", distination_in_meters + distination_in_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 + distination_in_feet = r.geodist("Sicily", "Palermo", "Catania", 'ft') + assert_equal "545518.8700", distination_in_feet end def test_geodist_with_nonexistant_location - target_version "3.2.0" do - distination = r.geodist("Sicily", "Palermo", "Rome") - assert_nil distination - end + distination = r.geodist("Sicily", "Palermo", "Rome") + assert_nil distination end def test_geohash - target_version "3.2.0" do - geohash = r.geohash("Sicily", "Palermo") - assert_equal ["sqc8b49rny0"], geohash + geohash = r.geohash("Sicily", "Palermo") + assert_equal ["sqc8b49rny0"], geohash - geohashes = r.geohash("Sicily", ["Palermo", "Catania"]) - assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes - end + geohashes = r.geohash("Sicily", ["Palermo", "Catania"]) + assert_equal %w(sqc8b49rny0 sqdtr74hyu0), geohashes end def test_geohash_with_nonexistant_location - target_version "3.2.0" do - geohashes = r.geohash("Sicily", ["Palermo", "Rome"]) - assert_equal ["sqc8b49rny0", nil], geohashes - end + geohashes = r.geohash("Sicily", ["Palermo", "Rome"]) + assert_equal ["sqc8b49rny0", nil], geohashes end end diff --git a/test/redis/commands_on_value_types_test.rb b/test/redis/commands_on_value_types_test.rb index 56213af9eed79271bf24c77c835643f339fc5afe..b79d6898bd7d3203856e1c1693f11a9f34dc5ba5 100644 --- a/test/redis/commands_on_value_types_test.rb +++ b/test/redis/commands_on_value_types_test.rb @@ -43,39 +43,35 @@ class TestCommandsOnValueTypes < Minitest::Test end def test_unlink - target_version "4.0.0" do - r.set "foo", "s1" - r.set "bar", "s2" - r.set "baz", "s3" + r.set "foo", "s1" + r.set "bar", "s2" + r.set "baz", "s3" - assert_equal ["bar", "baz", "foo"], r.keys("*").sort + assert_equal ["bar", "baz", "foo"], r.keys("*").sort - assert_equal 1, r.unlink("foo") + assert_equal 1, r.unlink("foo") - assert_equal ["bar", "baz"], r.keys("*").sort + assert_equal ["bar", "baz"], r.keys("*").sort - assert_equal 2, r.unlink("bar", "baz") + assert_equal 2, r.unlink("bar", "baz") - assert_equal [], r.keys("*").sort - end + assert_equal [], r.keys("*").sort end def test_unlink_with_array_argument - target_version "4.0.0" do - r.set "foo", "s1" - r.set "bar", "s2" - r.set "baz", "s3" + r.set "foo", "s1" + r.set "bar", "s2" + r.set "baz", "s3" - assert_equal ["bar", "baz", "foo"], r.keys("*").sort + assert_equal ["bar", "baz", "foo"], r.keys("*").sort - assert_equal 1, r.unlink(["foo"]) + assert_equal 1, r.unlink(["foo"]) - assert_equal ["bar", "baz"], r.keys("*").sort + assert_equal ["bar", "baz"], r.keys("*").sort - assert_equal 2, r.unlink(["bar", "baz"]) + assert_equal 2, r.unlink(["bar", "baz"]) - assert_equal [], r.keys("*").sort - end + assert_equal [], r.keys("*").sort end def test_randomkey @@ -140,19 +136,17 @@ class TestCommandsOnValueTypes < Minitest::Test assert_equal 0, r.dbsize # Test async - target_version "3.9.101" do - r.set("foo", "s1") - r.set("bar", "s2") + r.set("foo", "s1") + r.set("bar", "s2") - assert_equal 2, r.dbsize + assert_equal 2, r.dbsize - r.flushdb(async: true) + r.flushdb(async: true) - assert_equal 0, r.dbsize + assert_equal 0, r.dbsize - redis_mock(flushdb: ->(args) { "+FLUSHDB #{args.upcase}" }) do |redis| - assert_equal "FLUSHDB ASYNC", redis.flushdb(async: true) - end + redis_mock(flushdb: ->(args) { "+FLUSHDB #{args.upcase}" }) do |redis| + assert_equal "FLUSHDB ASYNC", redis.flushdb(async: true) end end @@ -168,10 +162,8 @@ class TestCommandsOnValueTypes < Minitest::Test end # Test async - target_version "3.9.101" do - redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis| - assert_equal "FLUSHALL ASYNC", redis.flushall(async: true) - end + redis_mock(flushall: ->(args) { "+FLUSHALL #{args.upcase}" }) do |redis| + assert_equal "FLUSHALL ASYNC", redis.flushall(async: true) end end diff --git a/test/redis/connection_handling_test.rb b/test/redis/connection_handling_test.rb index ac5ed73a9687adb4c779368aea21195b8bc33b58..04946c8ea061e2af33baa35cacce4ce91268393d 100644 --- a/test/redis/connection_handling_test.rb +++ b/test/redis/connection_handling_test.rb @@ -17,7 +17,7 @@ class TestConnectionHandling < Minitest::Test assert_equal "PONG", redis.ping end - assert_equal ["setname", "client-name"], @name + assert_equal ["SETNAME", "client-name"], @name end def test_ping @@ -30,9 +30,9 @@ class TestConnectionHandling < Minitest::Test r.select 14 assert_nil r.get("foo") - r._client.disconnect + r._client.close - assert_nil r.get("foo") + assert_equal "bar", r.get("foo") end def test_quit @@ -127,91 +127,6 @@ class TestConnectionHandling < Minitest::Test end end - def test_shutdown_from_pipeline - commands = { - shutdown: -> { :exit } - } - - redis_mock(commands) do |redis| - result = redis.pipelined do - redis.shutdown - end - - assert_nil result - assert !redis._client.connected? - end - end - - def test_shutdown_with_error_from_pipeline - connections = 0 - commands = { - select: ->(*_) { connections += 1; "+OK\r\n" }, - connections: -> { ":#{connections}\r\n" }, - shutdown: -> { "-ERR could not shutdown\r\n" } - } - - redis_mock(commands) do |redis| - connections = redis.connections - - # SHUTDOWN replies with an error: test that it gets raised - assert_raises Redis::CommandError do - redis.pipelined do - redis.shutdown - end - end - - # The connection should remain in tact - assert_equal connections, redis.connections - end - end - - def test_shutdown_from_multi_exec - commands = { - multi: -> { "+OK\r\n" }, - shutdown: -> { "+QUEUED\r\n" }, - exec: -> { :exit } - } - - redis_mock(commands) do |redis| - result = redis.multi do - redis.shutdown - end - - assert_nil result - assert !redis._client.connected? - end - end - - def test_shutdown_with_error_from_multi_exec - connections = 0 - commands = { - select: ->(*_) { connections += 1; "+OK\r\n" }, - connections: -> { ":#{connections}\r\n" }, - multi: -> { "+OK\r\n" }, - shutdown: -> { "+QUEUED\r\n" }, - exec: -> { "*1\r\n-ERR could not shutdown\r\n" } - } - - redis_mock(commands) do |redis| - connections = redis.connections - - # SHUTDOWN replies with an error: test that it gets returned - # We should test for Redis::CommandError here, but hiredis doesn't yet do - # custom error classes. - err = nil - - begin - redis.multi { redis.shutdown } - rescue => err - end - - assert err.is_a?(StandardError) - - # The connection should remain intact - assert_equal connections, redis.connections - end - end - def test_slaveof redis_mock(slaveof: ->(host, port) { "+SLAVEOF #{host} #{port}" }) do |redis| assert_equal "SLAVEOF somehost 6381", redis.slaveof("somehost", 6381) @@ -241,25 +156,4 @@ class TestConnectionHandling < Minitest::Test ensure r.config :set, "timeout", 300 end - - driver(:ruby, :hiredis) do - def test_consistency_on_multithreaded_env - t = nil - - commands = { - set: ->(_key, _value) { t.kill; "+OK\r\n" }, - incr: ->(_key) { ":1\r\n" } - } - - redis_mock(commands) do |redis| - t = Thread.new do - redis.set("foo", "bar") - end - - t.join - - assert_equal 1, redis.incr("baz") - end - end - end end diff --git a/test/redis/connection_test.rb b/test/redis/connection_test.rb index d5172db720581ad35ee67789335f626b1fcfc47c..08a45e4ea43c1221f54b67d00f0e92ad73b22c84 100644 --- a/test/redis/connection_test.rb +++ b/test/redis/connection_test.rb @@ -6,7 +6,7 @@ class TestConnection < Minitest::Test include Helper::Client def test_provides_a_meaningful_inspect - assert_equal "#<Redis client v#{Redis::VERSION} for redis://127.0.0.1:#{PORT}/15>", r.inspect + assert_equal "#<Redis client v#{Redis::VERSION} for redis://localhost:#{PORT}/15>", r.inspect end def test_connection_with_user_and_password @@ -27,46 +27,37 @@ class TestConnection < Minitest::Test end end - def test_connection_with_wrong_user_and_password - target_version "6.0" do - with_default_user_password do |_username, password| - redis = Redis.new(OPTIONS.merge(username: "does-not-exist", password: password)) - assert_equal "PONG", redis.ping - end - end - end - def test_connection_information - assert_equal "127.0.0.1", r.connection.fetch(:host) + assert_equal "localhost", r.connection.fetch(:host) assert_equal 6381, r.connection.fetch(:port) assert_equal 15, r.connection.fetch(:db) - assert_equal "127.0.0.1:6381", r.connection.fetch(:location) - assert_equal "redis://127.0.0.1:6381/15", r.connection.fetch(:id) + 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/0", redis.connection.fetch(:id) + 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/0", redis.connection.fetch(:id) + 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, scheme: "foo")) - assert_equal "foo://host:1234/0", redis.connection.fetch(:id) + 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/0", redis.connection.fetch(:id) + 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, scheme: "foo")) - assert_equal "unix:///tmp/redis.sock/0", redis.connection.fetch(:id) + 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 @@ -84,7 +75,7 @@ class TestConnection < Minitest::Test connection_id = redis.connection.fetch(:id) end - assert_equal "redis://127.0.0.1:6381/15", id - assert_equal "redis://127.0.0.1:6381/15", connection_id + assert_equal "redis://localhost:6381/15", id + assert_equal "redis://localhost:6381/15", connection_id end end diff --git a/test/redis/encoding_test.rb b/test/redis/encoding_test.rb index 71520b8bc62fab4aeac4515f35c2c6fabe592848..302bb69bf5ca63bfb25dc6635206c0b9be113880 100644 --- a/test/redis/encoding_test.rb +++ b/test/redis/encoding_test.rb @@ -6,10 +6,14 @@ class TestEncoding < Minitest::Test include Helper::Client def test_returns_properly_encoded_strings - with_external_encoding("UTF-8") do - r.set "foo", "שלו×" + r.set "foo", "שלו×" - assert_equal "Shalom שלו×", "Shalom #{r.get('foo')}" - end + 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 diff --git a/test/redis/error_replies_test.rb b/test/redis/error_replies_test.rb index abf9c41896479974515dd72843c87e554c53fb61..d7df0e862ae79679a02872bd6a34aa38e875c9bc 100644 --- a/test/redis/error_replies_test.rb +++ b/test/redis/error_replies_test.rb @@ -19,40 +19,23 @@ class TestErrorReplies < Minitest::Test def test_error_reply_for_single_command with_reconnection_check do - begin - r.unknown_command - rescue => ex - ensure - assert ex.message =~ /unknown command/i - end + 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 - begin - r.pipelined do - r.set("foo", "s1") - r.incr("foo") # not an integer - r.lpush("foo", "value") # wrong kind of value - end - rescue => ex - ensure - assert ex.message =~ /not an integer/i - end - end - end - - def test_recover_from_raise_in__call_loop - with_reconnection_check do - begin - r._client.call_loop([:invalid_monitor]) do - assert false # Should never be executed - end - rescue => ex - ensure - assert ex.message =~ /unknown command/i + 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 diff --git a/test/redis/fork_safety_test.rb b/test/redis/fork_safety_test.rb index fdbf7ac71507ddb4a566dd0ed6126a4a779c9b19..6a7be285078aab67b7bc9213849273bd263ecbef 100644 --- a/test/redis/fork_safety_test.rb +++ b/test/redis/fork_safety_test.rb @@ -5,57 +5,21 @@ require "helper" class TestForkSafety < Minitest::Test include Helper::Client - driver(:ruby, :hiredis) do - def test_fork_safety - redis = Redis.new(OPTIONS) - redis.set "foo", 1 + def setup + skip("Fork unavailable") unless Process.respond_to?(:fork) + end - child_pid = fork do - begin - # InheritedError triggers a reconnect, - # so we need to disable reconnects to force - # the exception bubble up - redis.without_reconnect do - redis.set "foo", 2 - end - exit! 0 - rescue Redis::InheritedError - exit! 127 - end + def test_fork_safety + redis = Redis.new(OPTIONS) + pid = fork do + 1000.times do + assert_equal "OK", redis.set("key", "foo") end - - _, status = Process.wait2(child_pid) - - assert_equal 127, status.exitstatus - assert_equal "1", redis.get("foo") - rescue NotImplementedError => error - raise unless error.message =~ /fork is not available/ end - - def test_fork_safety_with_enabled_inherited_socket - redis = Redis.new(OPTIONS.merge(inherit_socket: true)) - redis.set "foo", 1 - - child_pid = fork do - begin - # InheritedError triggers a reconnect, - # so we need to disable reconnects to force - # the exception bubble up - redis.without_reconnect do - redis.set "foo", 2 - end - exit! 0 - rescue Redis::InheritedError - exit! 127 - end - end - - _, status = Process.wait2(child_pid) - - assert_equal 0, status.exitstatus - assert_equal "2", redis.get("foo") - rescue NotImplementedError => error - raise unless error.message =~ /fork is not available/ + 1000.times do + assert_equal "PONG", redis.ping end + _, status = Process.wait2(pid) + assert_predicate(status, :success?) end end diff --git a/test/redis/internals_test.rb b/test/redis/internals_test.rb index 4a1fa0b169b1e90e2701675049e31034c374d205..1b718a1fcf18e5a86fed9a665f4a4b6932e4b90c 100644 --- a/test/redis/internals_test.rb +++ b/test/redis/internals_test.rb @@ -5,33 +5,20 @@ require "helper" class TestInternals < Minitest::Test include Helper::Client - def test_logger - r.ping - - assert log.string["[Redis] command=PING"] - assert log.string =~ /\[Redis\] call_time=\d+\.\d+ ms/ - end - def test_large_payload # see: https://github.com/redis/redis-rb/issues/962 # large payloads will trigger write_nonblock to write a portion # of the payload in connection/ruby.rb _write_to_socket + + # 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_logger_with_pipelining - r.pipelined do - r.set "foo", "bar" - r.get "foo" - end - - assert log.string[" command=SET args=\"foo\" \"bar\""] - assert log.string[" command=GET args=\"foo\""] - end - def test_recovers_from_failed_commands # See https://github.com/redis/redis-rb/issues#issue/28 @@ -50,26 +37,6 @@ class TestInternals < Minitest::Test end end - def test_redis_current - assert_equal "127.0.0.1", Redis.current._client.host - assert_equal 6379, Redis.current._client.port - assert_equal 0, Redis.current._client.db - - Redis.current = Redis.new(OPTIONS.merge(port: 6380, db: 1)) - - t = Thread.new do - assert_equal "127.0.0.1", Redis.current._client.host - assert_equal 6380, Redis.current._client.port - assert_equal 1, Redis.current._client.db - end - - t.join - - assert_equal "127.0.0.1", Redis.current._client.host - assert_equal 6380, Redis.current._client.port - assert_equal 1, Redis.current._client.db - end - def test_redis_connected? fresh_client = _new_client assert !fresh_client.connected? @@ -85,33 +52,15 @@ class TestInternals < Minitest::Test Redis.new(OPTIONS.merge(timeout: 0)) end - driver(:ruby) do - def test_tcp_keepalive - keepalive = { time: 20, intvl: 10, probes: 5 } - - redis = Redis.new(OPTIONS.merge(tcp_keepalive: keepalive)) - redis.ping - - connection = redis._client.connection - actual_keepalive = connection.get_tcp_keepalive - - %i[time intvl probes].each do |key| - assert_equal actual_keepalive[key], keepalive[key] if actual_keepalive.key?(key) - end - end - end - def test_time - target_version "2.5.4" do - # Test that the difference between the time that Ruby reports and the time - # that Redis reports is minimal (prevents the test from being racy). - rv = r.time + # 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) + 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 + assert((ruby_usec - redis_usec).abs < 500_000) end def test_connection_timeout @@ -151,24 +100,6 @@ class TestInternals < Minitest::Test end end - def test_retry_when_wrapped_in_with_reconnect_true - close_on_ping([0]) do |redis| - redis.with_reconnect(true) do - assert_equal "1", redis.ping - end - end - end - - def test_dont_retry_when_wrapped_in_with_reconnect_false - close_on_ping([0]) do |redis| - assert_raises Redis::ConnectionError do - redis.with_reconnect(false) do - redis.ping - end - end - end - end - def test_dont_retry_when_wrapped_in_without_reconnect close_on_ping([0]) do |redis| assert_raises Redis::ConnectionError do @@ -206,27 +137,21 @@ class TestInternals < Minitest::Test end def test_retry_with_custom_reconnect_attempts_and_exponential_backoff - close_on_ping([0, 1, 2], reconnect_attempts: 3, - reconnect_delay_max: 0.5, - reconnect_delay: 0.01) do |redis| - Kernel.expects(:sleep).with(0.01).returns(true) - Kernel.expects(:sleep).with(0.02).returns(true) - Kernel.expects(:sleep).with(0.04).returns(true) + 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_don_t_retry_when_second_read_in_pipeline_raises_econnreset - close_on_ping([1]) do |redis| - assert_raises Redis::ConnectionError do - redis.pipelined do - redis.ping - redis.ping # Second #read times out - end + def test_retry_pipeline_first_command + close_on_ping([0]) do |redis| + results = redis.pipelined do |pipeline| + pipeline.ping end - - assert !redis._client.connected? + assert_equal ["1"], results end end @@ -262,25 +187,7 @@ class TestInternals < Minitest::Test def test_retry_on_write_error_by_default close_on_connection([0]) do |redis| - assert_equal "1", redis._client.call(["x" * 128 * 1024]) - end - end - - def test_retry_on_write_error_when_wrapped_in_with_reconnect_true - close_on_connection([0]) do |redis| - redis.with_reconnect(true) do - assert_equal "1", redis._client.call(["x" * 128 * 1024]) - end - end - end - - def test_dont_retry_on_write_error_when_wrapped_in_with_reconnect_false - close_on_connection([0]) do |redis| - assert_raises Redis::ConnectionError do - redis.with_reconnect(false) do - redis._client.call(["x" * 128 * 1024]) - end - end + assert_equal "1", redis._client.call_v(["x" * 128 * 1024]) end end @@ -288,7 +195,7 @@ class TestInternals < Minitest::Test close_on_connection([0]) do |redis| assert_raises Redis::ConnectionError do redis.without_reconnect do - redis._client.call(["x" * 128 * 1024]) + redis._client.call_v(["x" * 128 * 1024]) end end end @@ -298,27 +205,24 @@ class TestInternals < Minitest::Test Redis.new(OPTIONS.merge(path: ENV.fetch("REDIS_SOCKET_PATH"))).ping end - driver(:ruby, :hiredis) do - def test_bubble_timeout_without_retrying - serv = TCPServer.new(6380) + def test_bubble_timeout_without_retrying + serv = TCPServer.new(6380) - redis = Redis.new(port: 6380, timeout: 0.1) + redis = Redis.new(port: 6380, timeout: 0.1) - assert_raises(Redis::TimeoutError) do - redis.ping - end - ensure - serv&.close + 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, scheme: "foo")) + redis = Redis.new(OPTIONS.merge(host: "host", port: 1234, db: 1)) - assert_equal "host", redis._client.options[:host] - assert_equal 1234, redis._client.options[:port] - assert_equal 1, redis._client.options[:db] - assert_equal "foo", redis._client.options[:scheme] + assert_equal "host", redis._client.host + assert_equal 1234, redis._client.port + assert_equal 1, redis._client.db end def test_resolves_localhost @@ -367,19 +271,15 @@ class TestInternals < Minitest::Test redis_mock(commands, host: host, &:ping) end - driver(:ruby) do - af_family_supported(Socket::AF_INET) do - def test_connect_ipv4 - af_test("127.0.0.1") - end + af_family_supported(Socket::AF_INET) do + def test_connect_ipv4 + af_test("127.0.0.1") end end - driver(:ruby) do - af_family_supported(Socket::AF_INET6) do - def test_connect_ipv6 - af_test("::1") - end + af_family_supported(Socket::AF_INET6) do + def test_connect_ipv6 + af_test("::1") end end @@ -391,4 +291,38 @@ class TestInternals < Minitest::Test 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 diff --git a/test/redis/pipelining_commands_test.rb b/test/redis/pipelining_commands_test.rb index 5b80103c29c2a24611ef41cabfecda0fe76773d4..6265921a04c5db342ef778d236ecbad9ac215e89 100644 --- a/test/redis/pipelining_commands_test.rb +++ b/test/redis/pipelining_commands_test.rb @@ -75,8 +75,8 @@ class TestPipeliningCommands < Minitest::Test def test_assignment_of_results_inside_the_block r.pipelined do |p| - @first = p.sadd("foo", 1) - @second = p.sadd("foo", 1) + @first = p.sadd?("foo", 1) + @second = p.sadd?("foo", 1) end assert_equal true, @first.value @@ -89,8 +89,8 @@ class TestPipeliningCommands < Minitest::Test assert_raises(Redis::CommandError) do r.pipelined do |p| p.doesnt_exist - @first = p.sadd("foo", 1) - @second = p.sadd("foo", 1) + @first = p.sadd?("foo", 1) + @second = p.sadd?("foo", 1) end end @@ -98,12 +98,24 @@ class TestPipeliningCommands < Minitest::Test 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) + @first = p.sadd?("foo", 1) - r.pipelined do |p2| - @second = p2.sadd("foo", 1) + p.pipelined do |p2| + @second = p2.sadd?("foo", 1) end end @@ -111,9 +123,33 @@ class TestPipeliningCommands < Minitest::Test 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) + @result = p.sadd?("foo", 1) end assert_raises(NoMethodError) { @result.to_s } @@ -130,20 +166,12 @@ class TestPipeliningCommands < Minitest::Test def test_futures_raise_when_command_errors_and_needs_transformation assert_raises(Redis::CommandError) do r.pipelined do |p| - @result = p.zrange("a", "b", 5, with_scores: true) + p.zadd("set", "1", "one") + @result = p.zincryby("set", "fail", "one") end end end - def test_futures_warn_when_tested_for_equality - r.pipelined do |p| - @result = p.sadd("foo", 1) - end - - Redis.expects(:deprecate!).once - @result == 1 - end - def test_futures_can_be_identified r.pipelined do |p| @result = p.sadd("foo", 1) @@ -191,24 +219,24 @@ class TestPipeliningCommands < Minitest::Test def test_hgetall_in_a_pipeline_returns_hash r.hmset("hash", "field", "value") + future = nil result = r.pipelined do |p| - p.hgetall("hash") + future = p.hgetall("hash") end - assert_equal result.first, { "field" => "value" } + assert_equal([{ "field" => "value" }], result) + assert_equal({ "field" => "value" }, future.value) end def test_zpopmax_in_a_pipeline_produces_future - target_version('5.0.0') do - r.zadd("sortedset", 1.0, "value") - future = nil - result = r.pipelined do - future = r.zpopmax("sortedset") - end - - assert_equal [["value", 1.0]], result - assert_equal ["value", 1.0], future.value + 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_keys_in_a_pipeline @@ -244,30 +272,9 @@ class TestPipeliningCommands < Minitest::Test assert_equal "2", r.get("db") end - def test_pipeline_select_client_db - r.select 1 - r.pipelined do |p2| - p2.select 2 - end - - assert_equal 2, r._client.db - end - - def test_nested_pipeline_select_client_db - r.select 1 - r.pipelined do |p2| - p2.select 2 - p2.pipelined do |p3| - p3.select 3 - end - end - - assert_equal 3, r._client.db - end - def test_pipeline_interrupt_preserves_client original = r._client - Redis::Pipeline.stubs(:new).raises(Interrupt) + Redis::PipelinedConnection.stubs(:new).raises(Interrupt) assert_raises(Interrupt) { r.pipelined {} } assert_equal r._client, original end diff --git a/test/redis/publish_subscribe_test.rb b/test/redis/publish_subscribe_test.rb index e42a8b98d22b4715b3a0cef5403138231767e9d4..4deea52ccb9873096005b388567e0fb6cf4b4c77 100644 --- a/test/redis/publish_subscribe_test.rb +++ b/test/redis/publish_subscribe_test.rb @@ -5,6 +5,26 @@ 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 @@ -12,8 +32,8 @@ class TestPublishSubscribe < Minitest::Test @subscribed = false @unsubscribed = false - wire = Wire.new do - r.subscribe("foo") do |on| + thread = new_thread do |r| + r.subscribe(channel_name) do |on| on.subscribe do |_channel, total| @subscribed = true @t1 = total @@ -34,11 +54,10 @@ class TestPublishSubscribe < Minitest::Test end # Wait until the subscription is active before publishing - Wire.pass until @subscribed - - Redis.new(OPTIONS).publish("foo", "s1") + Thread.pass until @subscribed - wire.join + redis.publish(channel_name, "s1") + thread.join assert @subscribed assert_equal 1, @t1 @@ -51,8 +70,8 @@ class TestPublishSubscribe < Minitest::Test @subscribed = false @unsubscribed = false - wire = Wire.new do - r.psubscribe("f*") do |on| + thread = new_thread do |r| + r.psubscribe("channel:*") do |on| on.psubscribe do |_pattern, total| @subscribed = true @t1 = total @@ -73,11 +92,9 @@ class TestPublishSubscribe < Minitest::Test end # Wait until the subscription is active before publishing - Wire.pass until @subscribed - - Redis.new(OPTIONS).publish("foo", "s1") - - wire.join + Thread.pass until @subscribed + redis.publish(channel_name, "s1") + thread.join assert @subscribed assert_equal 1, @t1 @@ -86,74 +103,49 @@ class TestPublishSubscribe < Minitest::Test assert_equal "s1", @message end - def test_pubsub_with_numpat_subcommand - target_version("2.8.0") do - @subscribed = false - wire = Wire.new do - r.psubscribe("f*") do |on| - on.psubscribe { |_channel, _total| @subscribed = true } - on.pmessage { |_pattern, _channel, _message| r.punsubscribe } - end - end - Wire.pass until @subscribed - redis = Redis.new(OPTIONS) - numpat_result = redis.pubsub(:numpat) - - redis.publish("foo", "s1") - wire.join - - assert_equal redis.pubsub(:numpat), 0 - assert_equal numpat_result, 1 - end - end - def test_pubsub_with_channels_and_numsub_subcommnads - target_version("2.8.0") do - @subscribed = false - wire = Wire.new do - r.subscribe("foo") do |on| - on.subscribe { |_channel, _total| @subscribed = true } - on.message { |_channel, _message| r.unsubscribe } - end + @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 - Wire.pass until @subscribed - redis = Redis.new(OPTIONS) - channels_result = redis.pubsub(:channels) - channels_result.delete('__sentinel__:hello') - numsub_result = redis.pubsub(:numsub, 'foo', 'boo') + 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("foo", "s1") - wire.join + redis.publish(channel_name, "s1") + thread.join - assert_equal channels_result, ['foo'] - assert_equal numsub_result, ['foo', 1, 'boo', 0] - end + 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 - wire = Wire.new do - begin - r.subscribe("foo") do |on| - on.subscribe do |_channel, _total| - @subscribed = true - end + thread = new_thread do |r| + r.subscribe(channel_name) do |on| + on.subscribe do |_channel, _total| + @subscribed = true + end - on.message do |_channel, _message| - raise TestError - end + on.message do |_channel, _message| + r.unsubscribe + raise TestError end - rescue TestError end + rescue TestError end # Wait until the subscription is active before publishing - Wire.pass until @subscribed + Thread.pass until @subscribed - Redis.new(OPTIONS).publish("foo", "s1") + redis.publish(channel_name, "s1") - wire.join + thread.join assert_equal "PONG", r.ping end @@ -161,74 +153,75 @@ class TestPublishSubscribe < Minitest::Test def test_psubscribe_connection_usable_after_raise @subscribed = false - wire = Wire.new do - begin - r.psubscribe("f*") do |on| - on.psubscribe do |_pattern, _total| - @subscribed = true - end + 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 + on.pmessage do |_pattern, _channel, _message| + raise TestError end - rescue TestError end + rescue TestError end # Wait until the subscription is active before publishing - Wire.pass until @subscribed + Thread.pass until @subscribed - Redis.new(OPTIONS).publish("foo", "s1") + redis.publish(channel_name, "s1") - wire.join + thread.join assert_equal "PONG", r.ping end def test_subscribe_within_subscribe - @channels = [] + @channels = Queue.new - wire = Wire.new do - r.subscribe("foo") do |on| + thread = new_thread do |r| + r.subscribe(channel_name) do |on| on.subscribe do |channel, _total| @channels << channel - r.subscribe("bar") if channel == "foo" + r.subscribe("bar") if channel == channel_name r.unsubscribe if channel == "bar" end end end - wire.join + thread.join - assert_equal ["foo", "bar"], @channels + assert_equal [channel_name, "bar"], [@channels.pop, @channels.pop] + assert_empty @channels end def test_other_commands_within_a_subscribe - assert_raises Redis::CommandError do - r.subscribe("foo") do |on| - on.subscribe do |_channel, _total| - r.set("bar", "s2") - end + 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 - assert_raises LocalJumpError do - r.subscribe("foo") + 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 - assert_raises RuntimeError do + error = assert_raises Redis::SubscriptionError do r.unsubscribe end + assert_includes "This client is not subscribed", error.message - assert_raises RuntimeError do + 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 @@ -254,28 +247,112 @@ class TestPublishSubscribe < Minitest::Test def test_subscribe_with_timeout received = false - assert_raises Redis::TimeoutError do - r.subscribe_with_timeout(LOW_TIMEOUT, "foo") do |on| - on.message do |_channel, _message| - received = true - end + r.subscribe_with_timeout(LOW_TIMEOUT, channel_name) do |on| + on.message do |_channel, _message| + received = true end end - assert !received + refute received end def test_psubscribe_with_timeout received = false - assert_raises Redis::TimeoutError do - r.psubscribe_with_timeout(LOW_TIMEOUT, "f*") do |on| - on.message do |_channel, _message| - received = true + 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 - assert !received + 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 diff --git a/test/redis/remote_server_control_commands_test.rb b/test/redis/remote_server_control_commands_test.rb index 785e5b50e5321a5c2a2d191a39e016825e1de189..e778054c299706825f6dd4752a5072627ca5a935 100644 --- a/test/redis/remote_server_control_commands_test.rb +++ b/test/redis/remote_server_control_commands_test.rb @@ -25,54 +25,29 @@ class TestRemoteServerControlCommands < Minitest::Test end def test_info_commandstats - target_version "2.5.7" do - r.config(:resetstat) - r.get("foo") - r.get("bar") + r.config(:resetstat) + r.get("foo") + r.get("bar") - result = r.info(:commandstats) - assert_equal '2', result['get']['calls'] - end + result = r.info(:commandstats) + assert_equal '2', result['get']['calls'] end - def test_monitor_redis_lt_2_5_0 - return unless version < "2.5.0" - + def test_monitor_redis log = [] - wire = Wire.new do - Redis.new(OPTIONS).monitor do |line| - log << line - break if log.size == 3 - end - end - - Wire.pass while log.empty? # Faster than sleep - - r.set "foo", "s1" - - wire.join - - assert log[-1]['(db 15) "set" "foo" "s1"'] - end - - def test_monitor_redis_gte_2_5_0 - return unless version >= "2.5.0" - - log = [] - - wire = Wire.new do + thread = Thread.new do Redis.new(OPTIONS).monitor do |line| log << line break if line =~ /set/ end end - Wire.pass while log.empty? # Faster than sleep + Thread.pass while log.empty? # Faster than sleep r.set "foo", "s1" - wire.join + thread.join assert log[-1] =~ /\b15\b.* "set" "foo" "s1"/ end @@ -100,7 +75,7 @@ class TestRemoteServerControlCommands < Minitest::Test assert_equal 1, r.object(:refcount, "list") encoding = r.object(:encoding, "list") - assert encoding == "ziplist" || encoding == "quicklist", "Wrong encoding for list" + assert encoding == "ziplist" || encoding == "quicklist" || encoding == "listpack", "Wrong encoding for list" assert r.object(:idletime, "list").is_a?(Integer) end @@ -121,8 +96,6 @@ class TestRemoteServerControlCommands < Minitest::Test end def test_client_list - return if version < "2.4.0" - keys = [ "addr", "fd", @@ -153,8 +126,6 @@ class TestRemoteServerControlCommands < Minitest::Test end def test_client_kill - return if version < "2.6.9" - r.client(:setname, 'redis-rb') clients = r.client(:list) i = clients.index { |client| client['name'] == 'redis-rb' } @@ -166,8 +137,6 @@ class TestRemoteServerControlCommands < Minitest::Test end def test_client_getname_and_setname - return if version < "2.6.9" - assert_nil r.client(:getname) r.client(:setname, 'redis-rb') diff --git a/test/redis/scanning_test.rb b/test/redis/scanning_test.rb index 77f97d2597e3121c08f3249f7f3c8c960c08c838..72eac1344d3c635f9610b854bfb011527a3a7425 100644 --- a/test/redis/scanning_test.rb +++ b/test/redis/scanning_test.rb @@ -6,51 +6,45 @@ class TestScanning < Minitest::Test include Helper::Client def test_scan_basic - target_version "2.7.105" do - r.debug :populate, 1000 - - cursor = 0 - all_keys = [] - loop do - cursor, keys = r.scan cursor - all_keys += keys - break if cursor == "0" - end - - assert_equal 1000, all_keys.uniq.size + 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 - target_version "2.7.105" do - r.debug :populate, 1000 - - cursor = 0 - all_keys = [] - loop do - cursor, keys = r.scan cursor, count: 5 - all_keys += keys - break if cursor == "0" - end - - assert_equal 1000, all_keys.uniq.size + 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 - target_version "2.7.105" do - r.debug :populate, 1000 - - cursor = 0 - all_keys = [] - loop do - cursor, keys = r.scan cursor, match: "key:1??" - all_keys += keys - break if cursor == "0" - end - - assert_equal 100, all_keys.uniq.size + 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 @@ -73,28 +67,24 @@ class TestScanning < Minitest::Test end def test_scan_each_enumerator - target_version "2.7.105" do - r.debug :populate, 1000 + r.debug :populate, 1000 - scan_enumerator = r.scan_each - assert_equal true, scan_enumerator.is_a?(::Enumerator) + 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 "*" + keys_from_scan = scan_enumerator.to_a.uniq + all_keys = r.keys "*" - assert all_keys.sort == keys_from_scan.sort - end + assert all_keys.sort == keys_from_scan.sort end def test_scan_each_enumerator_match - target_version "2.7.105" do - r.debug :populate, 1000 + r.debug :populate, 1000 - keys_from_scan = r.scan_each(match: "key:1??").to_a.uniq - all_keys = r.keys "key:1??" + 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 + assert all_keys.sort == keys_from_scan.sort end def test_scan_each_enumerator_type @@ -112,321 +102,261 @@ class TestScanning < Minitest::Test end def test_scan_each_block - target_version "2.7.105" do - r.debug :populate, 100 + r.debug :populate, 100 - keys_from_scan = [] - r.scan_each do |key| - keys_from_scan << key - end + keys_from_scan = [] + r.scan_each do |key| + keys_from_scan << key + end - all_keys = r.keys "*" + all_keys = r.keys "*" - assert all_keys.sort == keys_from_scan.uniq.sort - end + assert all_keys.sort == keys_from_scan.uniq.sort end def test_scan_each_block_match - target_version "2.7.105" do - r.debug :populate, 100 + 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 + keys_from_scan = [] + r.scan_each(match: "key:1?") do |key| + keys_from_scan << key end - end - - def test_sscan_with_encoding - target_version "2.7.105" do - %i[intset hashtable].each do |enc| - r.del "set" - prefix = "" - prefix = "ele:" if enc == :hashtable + all_keys = r.keys "key:1?" - elements = [] - 100.times { |j| elements << "#{prefix}#{j}" } - - r.sadd "set", elements - - assert_equal enc.to_s, r.object("encoding", "set") - - cursor = 0 - all_keys = [] - loop do - cursor, keys = r.sscan "set", cursor - all_keys += keys - break if cursor == "0" - end - - assert_equal 100, all_keys.uniq.size - end - end + assert all_keys.sort == keys_from_scan.uniq.sort end def test_sscan_each_enumerator - target_version "2.7.105" do - elements = [] - 100.times { |j| elements << "ele:#{j}" } - r.sadd "set", elements + 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) + 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") + keys_from_scan = scan_enumerator.to_a.uniq + all_keys = r.smembers("set") - assert all_keys.sort == keys_from_scan.sort - end + assert all_keys.sort == keys_from_scan.sort end def test_sscan_each_enumerator_match - target_version "2.7.105" do - elements = [] - 100.times { |j| elements << "ele:#{j}" } - r.sadd "set", elements + elements = [] + 100.times { |j| elements << "ele:#{j}" } + r.sadd "set", elements - keys_from_scan = r.sscan_each("set", match: "ele:1?").to_a.uniq + keys_from_scan = r.sscan_each("set", match: "ele:1?").to_a.uniq - all_keys = r.smembers("set").grep(/^ele:1.$/) + all_keys = r.smembers("set").grep(/^ele:1.$/) - assert all_keys.sort == keys_from_scan.sort - end + assert all_keys.sort == keys_from_scan.sort end def test_sscan_each_enumerator_block - target_version "2.7.105" do - elements = [] - 100.times { |j| elements << "ele:#{j}" } - r.sadd "set", elements + 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 + keys_from_scan = [] + r.sscan_each("set") do |key| + keys_from_scan << key + end - all_keys = r.smembers("set") + all_keys = r.smembers("set") - assert all_keys.sort == keys_from_scan.uniq.sort - end + assert all_keys.sort == keys_from_scan.uniq.sort end def test_sscan_each_enumerator_block_match - target_version "2.7.105" do - elements = [] - 100.times { |j| elements << "ele:#{j}" } - r.sadd "set", elements + 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 + 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.$/) + all_keys = r.smembers("set").grep(/^ele:1.$/) - assert all_keys.sort == keys_from_scan.uniq.sort - end + assert all_keys.sort == keys_from_scan.uniq.sort end def test_hscan_with_encoding - target_version "2.7.105" do - %i[ziplist hashtable].each do |enc| - r.del "set" - - count = 1000 - count = 30 if enc == :ziplist + %i[ziplist hashtable].each do |enc| + r.del "set" - elements = [] - count.times { |j| elements << "key:#{j}" << j.to_s } + count = 1000 + count = 30 if enc == :ziplist - r.hmset "hash", *elements + elements = [] + count.times { |j| elements << "key:#{j}" << j.to_s } - 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 + r.hmset "hash", *elements - keys2 = [] - all_key_values.each do |k, v| - assert_equal "key:#{v}", k - keys2 << k - end + 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 - assert_equal count, keys2.uniq.size + 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 - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << "key:#{j}" << j.to_s } - r.hmset "hash", *elements + 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) + 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 + keys_from_scan = scan_enumerator.to_a.uniq + all_keys = r.hgetall("hash").to_a - assert all_keys.sort == keys_from_scan.sort - end + assert all_keys.sort == keys_from_scan.sort end def test_hscan_each_enumerator_match - target_version "2.7.105" do - count = 100 - elements = [] - count.times { |j| elements << "key:#{j}" << j.to_s } - r.hmset "hash", *elements + 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.$/ } + 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 + assert all_keys.sort == keys_from_scan.sort end def test_hscan_each_block - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << "key:#{j}" << j.to_s } - r.hmset "hash", *elements + 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 - 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 - assert all_keys.sort == keys_from_scan.uniq.sort + 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_hscan_each_block_match - target_version "2.7.105" do + 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 << "key:#{j}" << j.to_s } - r.hmset "hash", *elements + count.times { |j| elements << j << "key:#{j}" } - 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.$/ } + r.zadd "zset", elements - assert all_keys.sort == keys_from_scan.uniq.sort - end - end + 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 - def test_zscan_with_encoding - target_version "2.7.105" do - %i[ziplist skiplist].each do |enc| - r.del "zset" - - count = 1000 - count = 30 if enc == :ziplist - - elements = [] - count.times { |j| elements << j << "key:#{j}" } - - r.zadd "zset", elements - - 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 + 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 - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << j << "key:#{j}" } - r.zadd "zset", elements + 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) + 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) + 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 + assert member_scores.sort == scores_from_scan.sort end def test_zscan_each_enumerator_match - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << j << "key:#{j}" } - r.zadd "zset", elements + 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..$/ } + 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 + assert filtered_members.sort == scores_from_scan.sort end def test_zscan_each_block - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << j << "key:#{j}" } - r.zadd "zset", elements - - scores_from_scan = [] - r.zscan_each("zset") do |member, score| - scores_from_scan << [member, score] - end - member_scores = r.zrange("zset", 0, -1, with_scores: true) - - assert member_scores.sort == scores_from_scan.sort + 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 - target_version "2.7.105" do - count = 1000 - elements = [] - count.times { |j| elements << j << "key:#{j}" } - r.zadd "zset", elements - - scores_from_scan = [] - r.zscan_each("zset", match: "key:1??") do |member, score| - scores_from_scan << [member, score] - end - member_scores = r.zrange("zset", 0, -1, with_scores: true) - filtered_members = member_scores.select { |k, _s| k =~ /^key:1..$/ } - - assert filtered_members.sort == scores_from_scan.sort + 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 diff --git a/test/redis/scripting_test.rb b/test/redis/scripting_test.rb index 56a8058057c0a047b1d13bede8443db55e6bb3da..f016501229fa5a55af490bf3f7572ce2447be8ef 100644 --- a/test/redis/scripting_test.rb +++ b/test/redis/scripting_test.rb @@ -10,68 +10,54 @@ class TestScripting < Minitest::Test end def test_script_exists - target_version "2.5.9" do # 2.6-rc1 - a = to_sha("return 1") - b = a.succ + 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 + 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 - target_version "2.5.9" do # 2.6-rc1 - sha = to_sha("return 1") - assert r.script(:exists, sha) - assert_equal "OK", r.script(:flush) - assert !r.script(:exists, sha) - end + 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 - target_version "2.5.9" do # 2.6-rc1 - redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| - assert_equal "KILL", redis.script(:kill) - end + redis_mock(script: ->(arg) { "+#{arg.upcase}" }) do |redis| + assert_equal "KILL", redis.script(:kill) end end def test_eval - target_version "2.5.9" do # 2.6-rc1 - assert_equal 0, r.eval("return #KEYS") - assert_equal 0, r.eval("return #ARGV") - assert_equal ["k1", "k2"], r.eval("return KEYS", ["k1", "k2"]) - assert_equal ["a1", "a2"], r.eval("return ARGV", [], ["a1", "a2"]) - end + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_equal 0, r.eval("return #KEYS", {}) - assert_equal 0, r.eval("return #ARGV", {}) - assert_equal ["k1", "k2"], r.eval("return KEYS", { keys: ["k1", "k2"] }) - assert_equal ["a1", "a2"], r.eval("return ARGV", { argv: ["a1", "a2"] }) - end + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_equal 0, r.evalsha(to_sha("return #KEYS")) - assert_equal 0, r.evalsha(to_sha("return #ARGV")) - assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), ["k1", "k2"]) - assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), [], ["a1", "a2"]) - end + 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 - target_version "2.5.9" do # 2.6-rc1 - assert_equal 0, r.evalsha(to_sha("return #KEYS"), {}) - assert_equal 0, r.evalsha(to_sha("return #ARGV"), {}) - assert_equal ["k1", "k2"], r.evalsha(to_sha("return KEYS"), { keys: ["k1", "k2"] }) - assert_equal ["a1", "a2"], r.evalsha(to_sha("return ARGV"), { argv: ["a1", "a2"] }) - end + 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 diff --git a/test/redis/ssl_test.rb b/test/redis/ssl_test.rb index 769bf8a65c5d10d9b60ddb1e79e651584cd28674..0752973208500959225cba8d043b2ece2c2d99f2 100644 --- a/test/redis/ssl_test.rb +++ b/test/redis/ssl_test.rb @@ -5,55 +5,42 @@ require "helper" class SslTest < Minitest::Test include Helper::Client - driver(:ruby) do - def test_connection_to_non_ssl_server - assert_raises(Redis::CannotConnectError) do - redis = Redis.new(OPTIONS.merge(ssl: true, timeout: LOW_TIMEOUT)) - redis.ping - end - end - - def test_verified_ssl_connection - RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port| - redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) - assert_equal redis.ping, "PONG" - end + 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_unverified_ssl_connection - assert_raises(OpenSSL::SSL::SSLError) do - RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| - redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) - redis.ping - end - end + 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_verify_certificates_by_default - assert_raises(OpenSSL::SSL::SSLError) do - RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("untrusted")) do |port| - redis = Redis.new(port: port, ssl: true) - redis.ping - end + 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_ssl_blocking - RedisMock.start({}, ssl_server_opts("trusted")) do |port| - redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) - assert_equal redis.set("boom", "a" * 10_000_000), "OK" + 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 - driver(:hiredis, :synchrony) do - def test_ssl_not_implemented_exception - assert_raises(NotImplementedError) do - RedisMock.start({ ping: proc { "+PONG" } }, ssl_server_opts("trusted")) do |port| - redis = Redis.new(port: port, ssl: true, ssl_params: { ca_file: ssl_ca_file }) - redis.ping - end - end + 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 diff --git a/test/redis/synchrony_driver.rb b/test/redis/synchrony_driver.rb deleted file mode 100644 index 9734c77a33f6c92e358ef3d8ec19a381325b0928..0000000000000000000000000000000000000000 --- a/test/redis/synchrony_driver.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require "em-synchrony" -require "em-synchrony/connection_pool" - -require_relative "../lib/redis" -require_relative "../lib/redis/connection/synchrony" - -require "helper" - -PORT = 6381 -OPTIONS = { port: PORT, db: 15 }.freeze - -# -# if running under Eventmachine + Synchrony (Ruby 1.9+), then -# we can simulate the blocking API while performing the network -# IO via the EM reactor. -# - -EM.synchrony do - r = Redis.new OPTIONS - r.flushdb - - r.rpush "foo", "s1" - r.rpush "foo", "s2" - - assert_equal 2, r.llen("foo") - assert_equal "s2", r.rpop("foo") - - r.set("foo", "bar") - - assert_equal "bar", r.getset("foo", "baz") - assert_equal "baz", r.get("foo") - - r.set("foo", "a") - - assert_equal 1, r.getbit("foo", 1) - assert_equal 1, r.getbit("foo", 2) - assert_equal 0, r.getbit("foo", 3) - assert_equal 0, r.getbit("foo", 4) - assert_equal 0, r.getbit("foo", 5) - assert_equal 0, r.getbit("foo", 6) - assert_equal 1, r.getbit("foo", 7) - - r.flushdb - - # command pipelining - r.pipelined do - r.lpush "foo", "s1" - r.lpush "foo", "s2" - end - - assert_equal 2, r.llen("foo") - assert_equal "s2", r.lpop("foo") - assert_equal "s1", r.lpop("foo") - - assert_equal "OK", r._client.call(:quit) - assert_equal "PONG", r.ping - - rpool = EM::Synchrony::ConnectionPool.new(size: 5) { Redis.new OPTIONS } - - result = rpool.watch 'foo' do |rd| - assert_kind_of Redis, rd - - rd.set "foo", "s1" - rd.multi do |multi| - multi.set "foo", "s2" - end - end - - assert_nil result - assert_equal "s1", rpool.get("foo") - - result = rpool.watch "foo" do |rd| - assert_kind_of Redis, rd - - rd.multi do |multi| - multi.set "foo", "s3" - end - end - - assert_equal ["OK"], result - assert_equal "s3", rpool.get("foo") - - EM.stop -end diff --git a/test/redis/thread_safety_test.rb b/test/redis/thread_safety_test.rb index ced338d270b5fc73640eb98f3305042ea626eeaa..57ec5e9c47a9ff2dd39ccc199a47361080993831 100644 --- a/test/redis/thread_safety_test.rb +++ b/test/redis/thread_safety_test.rb @@ -5,57 +5,25 @@ require "helper" class TestThreadSafety < Minitest::Test include Helper::Client - driver(:ruby, :hiredis) do - def test_thread_safety - redis = Redis.new(OPTIONS) - redis.set "foo", 1 - redis.set "bar", 2 + def test_thread_safety + redis = Redis.new(OPTIONS) + redis.set "foo", 1 + redis.set "bar", 2 - sample = 100 + 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 + t1 = Thread.new do + @foos = Array.new(sample) { redis.get "foo" } end - def test_thread_safety_queue_commit - redis = Redis.new(OPTIONS) - redis.set "foo", 1 - redis.set "bar", 2 - - sample = 100 - - t1 = Thread.new do - sample.times do - r.queue("get", "foo") - end - - @foos = r.commit - end - - t2 = Thread.new do - sample.times do - r.queue("get", "bar") - end - - @bars = r.commit - end + t2 = Thread.new do + @bars = Array.new(sample) { redis.get "bar" } + end - t1.join - t2.join + t1.join + t2.join - assert_equal ["1"], @foos.uniq - assert_equal ["2"], @bars.uniq - end + assert_equal ["1"], @foos.uniq + assert_equal ["2"], @bars.uniq end end diff --git a/test/redis/transactions_test.rb b/test/redis/transactions_test.rb index 6699a9f9d4b53a044f32bbec497c45a78e3b9ec8..026d79cf3521a015b620ae18912e1e27353a0616 100644 --- a/test/redis/transactions_test.rb +++ b/test/redis/transactions_test.rb @@ -6,16 +6,9 @@ class TestTransactions < Minitest::Test include Helper::Client def test_multi_discard - r.multi - - assert_equal "QUEUED", r.set("foo", "1") - assert_equal "QUEUED", r.get("foo") - assert_equal "QUEUED", r.zincrby("bar", 1, "baz") # Floatify - assert_equal "QUEUED", r.hsetnx("plop", "foo", "bar") # Boolify - - r.discard - - assert_nil r.get("foo") + assert_raises(LocalJumpError) do + r.multi + end end def test_multi_exec_with_a_block @@ -39,8 +32,9 @@ class TestTransactions < Minitest::Test def test_multi_in_pipeline foo_future = bar_future = nil + multi_future = nil response = r.pipelined do |pipeline| - pipeline.multi do |multi| + multi_future = pipeline.multi do |multi| multi.set("foo", "s1") foo_future = multi.get("foo") end @@ -51,76 +45,34 @@ class TestTransactions < Minitest::Test end end - assert_equal "s1", foo_future.value - assert_equal "s2", bar_future.value - assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response) - end - def test_multi_in_pipeline_deprecated - foo_future = bar_future = nil - response = r.pipelined do - r.multi do |multi| - multi.set("foo", "s1") - foo_future = multi.get("foo") - end - - r.multi do |multi| - multi.set("bar", "s2") - bar_future = multi.get("bar") - end - end + assert_equal ["OK", "s1"], multi_future.value assert_equal "s1", foo_future.value assert_equal "s2", bar_future.value - - assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response) - end - - def test_multi_in_pipeline_double_deprecated - foo_future = bar_future = nil - response = r.pipelined do - r.multi do - r.set("foo", "s1") - foo_future = r.get("foo") - end - - r.multi do - r.set("bar", "s2") - bar_future = r.get("bar") - end - end - - assert_equal "s1", foo_future.value - assert_equal "s2", bar_future.value - - assert_equal(["OK", "QUEUED", "QUEUED", ["OK", "s1"], "OK", "QUEUED", "QUEUED", ["OK", "s2"]], response) end def test_assignment_inside_multi_exec_block r.multi do |m| - @first = m.sadd("foo", 1) - @second = m.sadd("foo", 1) + @first = m.sadd?("foo", 1) + @second = m.sadd?("foo", 1) end assert_equal true, @first.value assert_equal false, @second.value end - # Although we could support accessing the values in these futures, - # it doesn't make a lot of sense. def test_assignment_inside_multi_exec_block_with_delayed_command_errors assert_raises(Redis::CommandError) do r.multi do |m| @first = m.set("foo", "s1") @second = m.incr("foo") # not an integer - @third = m.lpush("foo", "value") # wrong kind of value end end assert_equal "OK", @first.value - assert_raises(Redis::CommandError) { @second.value } - assert_raises(Redis::FutureNotReady) { @third.value } + assert_raises(Redis::FutureNotReady) { @second.value } end def test_assignment_inside_multi_exec_block_with_immediate_command_errors @@ -149,16 +101,16 @@ class TestTransactions < Minitest::Test end def test_transformed_replies_as_return_values_for_multi_exec_block - info, = r.multi do |_m| - r.info + info, = r.multi do |transaction| + transaction.info end - assert info.is_a?(Hash) + assert_instance_of Hash, info end def test_transformed_replies_inside_multi_exec_block - r.multi do |_m| - @info = r.info + r.multi do |transaction| + @info = transaction.info end assert @info.value.is_a?(Hash) @@ -260,7 +212,7 @@ class TestTransactions < Minitest::Test def test_multi_with_interrupt_preserves_client original = r._client - Redis::Pipeline.stubs(:new).raises(Interrupt) + Redis::MultiConnection.stubs(:new).raises(Interrupt) assert_raises(Interrupt) { r.multi {} } assert_equal r._client, original end diff --git a/test/redis/url_param_test.rb b/test/redis/url_param_test.rb index b2560fe70f68de663fb9dccf27a4a882c8f9127a..e14314ac2ea2941d9f4ea6bcd87a95edb4ba4172 100644 --- a/test/redis/url_param_test.rb +++ b/test/redis/url_param_test.rb @@ -5,10 +5,10 @@ require "helper" class TestUrlParam < Minitest::Test include Helper::Client - def test_url_defaults_to_______________ + def test_url_defaults_to_localhost redis = Redis.new - assert_equal "127.0.0.1", redis._client.host + assert_equal "localhost", redis._client.host assert_equal 6379, redis._client.port assert_equal 0, redis._client.db assert_nil redis._client.password @@ -23,49 +23,20 @@ class TestUrlParam < Minitest::Test assert_equal "secr3t", redis._client.password end - def test_allows_to_pass_in_a_url_with_string_key - redis = Redis.new "url" => "redis://:secr3t@foo.com:999/2" - - assert_equal "foo.com", redis._client.host - assert_equal 999, redis._client.port - assert_equal 2, redis._client.db - assert_equal "secr3t", redis._client.password - end - def test_unescape_password_from_url redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2" assert_equal "secr3t:", redis._client.password end - def test_unescape_password_from_url_with_string_key - redis = Redis.new "url" => "redis://:secr3t%3A@foo.com:999/2" - - assert_equal "secr3t:", redis._client.password - end - def test_does_not_unescape_password_when_explicitly_passed redis = Redis.new url: "redis://:secr3t%3A@foo.com:999/2", password: "secr3t%3A" assert_equal "secr3t%3A", redis._client.password end - def test_does_not_unescape_password_when_explicitly_passed_with_string_key - redis = Redis.new :url => "redis://:secr3t%3A@foo.com:999/2", "password" => "secr3t%3A" - - assert_equal "secr3t%3A", redis._client.password - end - def test_override_url_if_path_option_is_passed - redis = Redis.new url: "redis://:secr3t@foo.com/foo:999/2", path: "/tmp/redis.sock" - - assert_equal "/tmp/redis.sock", redis._client.path - assert_nil redis._client.host - assert_nil redis._client.port - end - - def test_override_url_if_path_option_is_passed_with_string_key - redis = Redis.new :url => "redis://:secr3t@foo.com/foo:999/2", "path" => "/tmp/redis.sock" + 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 @@ -81,15 +52,6 @@ class TestUrlParam < Minitest::Test assert_equal "secr3t", redis._client.password end - def test_overrides_url_if_another_connection_option_is_passed_with_string_key - redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => 1000 - - assert_equal "foo.com", redis._client.host - assert_equal 1000, redis._client.port - assert_equal 2, redis._client.db - assert_equal "secr3t", redis._client.password - end - def test_does_not_overrides_url_if_a_nil_option_is_passed redis = Redis.new url: "redis://:secr3t@foo.com:999/2", port: nil @@ -99,15 +61,6 @@ class TestUrlParam < Minitest::Test assert_equal "secr3t", redis._client.password end - def test_does_not_overrides_url_if_a_nil_option_is_passed_with_string_key - redis = Redis.new :url => "redis://:secr3t@foo.com:999/2", "port" => nil - - assert_equal "foo.com", redis._client.host - assert_equal 999, redis._client.port - assert_equal 2, redis._client.db - assert_equal "secr3t", redis._client.password - end - def test_does_not_modify_the_passed_options options = { url: "redis://:secr3t@foo.com:999/2" } @@ -130,9 +83,9 @@ class TestUrlParam < Minitest::Test end def test_defaults_to_localhost - redis = Redis.new(url: "redis:///") + redis = Redis.new(url: "redis://") - assert_equal "127.0.0.1", redis._client.host + assert_equal "localhost", redis._client.host end def test_ipv6_url @@ -144,7 +97,6 @@ class TestUrlParam < Minitest::Test def test_user_and_password redis = Redis.new(url: 'redis://johndoe:mysecret@foo.com:999/2') - assert_equal('redis', redis._client.scheme) assert_equal('johndoe', redis._client.username) assert_equal('mysecret', redis._client.password) assert_equal('foo.com', redis._client.host) diff --git a/test/sentinel/sentinel_command_test.rb b/test/sentinel/sentinel_command_test.rb index bf6372d89daf77929c27e00687021c8374782769..98124801f906f45c6e51eaa3fce67d750bb5e81a 100644 --- a/test/sentinel/sentinel_command_test.rb +++ b/test/sentinel/sentinel_command_test.rb @@ -7,6 +7,8 @@ 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) @@ -15,6 +17,8 @@ class SentinelCommandsTest < Minitest::Test end def test_sentinel_command_masters + wait_for_quorum + redis = build_sentinel_client result = redis.sentinel('masters') @@ -24,6 +28,8 @@ class SentinelCommandsTest < Minitest::Test end def test_sentinel_command_slaves + wait_for_quorum + redis = build_sentinel_client result = redis.sentinel('slaves', MASTER_NAME) @@ -33,6 +39,8 @@ class SentinelCommandsTest < Minitest::Test end def test_sentinel_command_sentinels + wait_for_quorum + redis = build_sentinel_client result = redis.sentinel('sentinels', MASTER_NAME) @@ -51,9 +59,10 @@ class SentinelCommandsTest < Minitest::Test 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 diff --git a/test/sentinel/sentinel_test.rb b/test/sentinel/sentinel_test.rb index c0d0e3ff8eae3be5f9e19add3abe033884f68014..541ae63b754135656332e4fdb16e0f439ca92ebf 100644 --- a/test/sentinel/sentinel_test.rb +++ b/test/sentinel/sentinel_test.rb @@ -6,13 +6,16 @@ class SentinelTest < Minitest::Test include Helper::Sentinel def test_sentinel_master_role_connection + wait_for_quorum + actual = redis.role assert_equal 'master', actual[0] - assert_equal SLAVE_PORT, actual[2][0][1] end def test_sentinel_slave_role_connection + wait_for_quorum + redis = build_slave_role_client actual = redis.role @@ -20,6 +23,14 @@ class SentinelTest < Minitest::Test 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 |*_| @@ -69,7 +80,14 @@ class SentinelTest < Minitest::Test s2 = { sentinel: lambda do |command, *args| commands[:s2] << [command, *args] - ["127.0.0.1", "6381"] + case command + when "get-master-addr-by-name" + ["127.0.0.1", "6381"] + when "sentinels" + [] + else + raise "Unexpected command #{[command, *args].inspect}" + end end } @@ -84,7 +102,7 @@ class SentinelTest < Minitest::Test end assert_equal commands[:s1], [%w[get-master-addr-by-name master1]] - assert_equal commands[:s2], [%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 @@ -106,7 +124,17 @@ class SentinelTest < Minitest::Test s2 = { sentinel: lambda do |command, *args| commands[:s2] << [command, *args] - ["127.0.0.1", "6381"] + 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 } @@ -124,8 +152,8 @@ class SentinelTest < Minitest::Test end end - assert_equal commands[:s1], [%w[get-master-addr-by-name master1]] - assert_equal commands[:s2], [%w[get-master-addr-by-name master1], %w[get-master-addr-by-name master1]] + 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 @@ -133,8 +161,8 @@ class SentinelTest < Minitest::Test sentinel = lambda do |port| { - auth: lambda do |pass| - commands[:s1] << ['auth', pass] + auth: lambda do |*args| + commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| @@ -143,14 +171,24 @@ class SentinelTest < Minitest::Test end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] - ['127.0.0.1', port.to_s] + 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 |pass| - commands[:m1] << ['auth', pass] + auth: lambda do |*args| + commands[:m1] << ['auth', *args] '+OK' end, role: lambda do @@ -167,7 +205,7 @@ class SentinelTest < Minitest::Test 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[:s1] assert_equal [%w[auth foo], %w[role]], commands[:m1] end @@ -176,8 +214,8 @@ class SentinelTest < Minitest::Test sentinel = lambda do |port| { - auth: lambda do |pass| - commands[:s1] << ['auth', pass] + auth: lambda do |*args| + commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| @@ -186,14 +224,24 @@ class SentinelTest < Minitest::Test end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] - ['127.0.0.1', port.to_s] + 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 |pass| - commands[:m1] << ['auth', pass] + auth: lambda do |*args| + commands[:s1] << ['auth', *args] '-ERR Client sent AUTH, but no password is set' end, role: lambda do @@ -204,13 +252,13 @@ class SentinelTest < Minitest::Test RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| - s = [{ host: '127.0.0.1', port: sen_port, password: 'foo' }] - r = Redis.new(host: 'master1', sentinels: s, role: :master) + 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]], commands[:s1] + assert_equal [%w[auth foo], %w[get-master-addr-by-name master1], ["sentinels", "master1"]], commands[:s1] assert_equal [%w[role]], commands[:m1] end @@ -219,8 +267,8 @@ class SentinelTest < Minitest::Test sentinel = lambda do |port| { - auth: lambda do |pass| - commands[:s1] << ['auth', pass] + auth: lambda do |*args| + commands[:s1] << ['auth', *args] '+OK' end, select: lambda do |db| @@ -229,31 +277,44 @@ class SentinelTest < Minitest::Test end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] - ['127.0.0.1', port.to_s] - end + 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 |pass| - commands[:m1] << ['auth', pass] + auth: lambda do |*args| + commands[:m1] << ['auth', *args] '+OK' end, role: lambda do commands[:m1] << ['role'] ['master'] - end + 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, password: 'foo' }] - r = Redis.new(host: 'master1', sentinels: s, role: :master, password: 'bar') + 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]], commands[:s1] + 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 @@ -272,7 +333,17 @@ class SentinelTest < Minitest::Test end, sentinel: lambda do |command, *args| commands[:s1] << [command, *args] - ['127.0.0.1', port.to_s] + 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 @@ -290,13 +361,13 @@ class SentinelTest < Minitest::Test RedisMock.start(master) do |master_port| RedisMock.start(sentinel.call(master_port)) do |sen_port| - s = [{ host: '127.0.0.1', port: sen_port, username: 'bob', password: 'foo' }] - r = Redis.new(host: 'master1', sentinels: s, role: :master, username: 'alice', password: 'bar') + 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]], commands[:s1] + 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 @@ -305,8 +376,17 @@ class SentinelTest < Minitest::Test sentinel = lambda do |port| { - sentinel: lambda do |_command, *_args| - ["127.0.0.1", port.to_s] + 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 @@ -317,18 +397,18 @@ class SentinelTest < Minitest::Test end } - ex = assert_raises(Redis::ConnectionError) do + 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) + redis = Redis.new(url: "redis://master1", sentinels: sentinels, role: :master, reconnect_attempts: 0) assert redis.ping end end end - assert_match(/Instance role mismatch/, ex.message) + assert_match(/Expected to connect to a master, but the server is a replica/, ex.message) end def test_sentinel_retries @@ -337,15 +417,28 @@ class SentinelTest < Minitest::Test connections = [] + fails = Hash.new(0) + handler = lambda do |id, port| { - sentinel: lambda do |_command, *_args| + sentinel: lambda do |command, *_args| connections << id - if connections.count(id) < 2 + if fails[id] < 2 + fails[id] += 1 :close else - ["127.0.0.1", port.to_s] + 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 } @@ -369,9 +462,10 @@ class SentinelTest < Minitest::Test end end - assert_equal %i[s1 s2 s1], connections + 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| @@ -389,35 +483,4 @@ class SentinelTest < Minitest::Test assert_match(/No sentinels available/, ex.message) end - - def test_sentinel_with_string_option_keys - commands = [] - - master = { - role: lambda do - ['master'] - end - } - - sentinel = lambda do |port| - { - sentinel: lambda do |command, *args| - commands << [command, *args] - ['127.0.0.1', port.to_s] - end - } - end - - RedisMock.start(master) do |master_port| - RedisMock.start(sentinel.call(master_port)) do |sen_port| - sentinels = [{ 'host' => '127.0.0.1', 'port' => sen_port }] - - redis = Redis.new(url: 'redis://master1', 'sentinels' => sentinels, 'role' => :master) - - assert redis.ping - end - end - - assert_equal [%w[get-master-addr-by-name master1]], commands - end end diff --git a/test/support/conf/redis-7.2.conf b/test/support/conf/redis-7.2.conf new file mode 100644 index 0000000000000000000000000000000000000000..261b18686ee750be1d9cacbd0841614f9dbe479e --- /dev/null +++ b/test/support/conf/redis-7.2.conf @@ -0,0 +1,3 @@ +appendonly no +save "" +enable-debug-command yes diff --git a/test/support/connection/hiredis.rb b/test/support/connection/hiredis.rb deleted file mode 100644 index 2545fa7badf7686223d1f44cf124333f4a741d91..0000000000000000000000000000000000000000 --- a/test/support/connection/hiredis.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "../wire/thread" diff --git a/test/support/connection/ruby.rb b/test/support/connection/ruby.rb deleted file mode 100644 index 2545fa7badf7686223d1f44cf124333f4a741d91..0000000000000000000000000000000000000000 --- a/test/support/connection/ruby.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "../wire/thread" diff --git a/test/support/connection/synchrony.rb b/test/support/connection/synchrony.rb deleted file mode 100644 index 9228b499ef73bfc62054b9b03d18afca3956e577..0000000000000000000000000000000000000000 --- a/test/support/connection/synchrony.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require_relative "../wire/synchrony" - -module Helper - def around - rv = nil - - EM.synchrony do - begin - rv = yield - ensure - EM.stop - end - end - - rv - end -end diff --git a/test/support/redis_mock.rb b/test/support/redis_mock.rb index ab8bc059c115138353f1f1e178263d18c26ac658..ac10298332f5724ead6be52fb4416a4f8bfde54d 100644 --- a/test/support/redis_mock.rb +++ b/test/support/redis_mock.rb @@ -8,6 +8,8 @@ module RedisMock 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 @@ -32,14 +34,21 @@ module RedisMock @thread.kill end - def run + def run(&block) loop do - session = @server.accept - - begin - return if yield(session) == :exit - ensure - session.close + 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 @@ -93,7 +102,7 @@ module RedisMock end command = argv.shift - blk = commands[command.to_sym] + blk = commands[command.downcase.to_sym] blk ||= ->(*_) { "+OK" } response = blk.call(*argv) diff --git a/test/support/wire/synchrony.rb b/test/support/wire/synchrony.rb deleted file mode 100644 index b147aae13e61645dc716dbc3faf93a19b631ffbc..0000000000000000000000000000000000000000 --- a/test/support/wire/synchrony.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class Wire < Fiber - # We cannot run this fiber explicitly because EM schedules it. Resuming the - # current fiber on the next tick to let the reactor do work. - def self.pass - f = Fiber.current - EM.next_tick { f.resume } - Fiber.yield - end - - def self.sleep(sec) - EM::Synchrony.sleep(sec) - end - - def initialize(&blk) - super - - # Schedule run in next tick - EM.next_tick { resume } - end - - def join - self.class.pass while alive? - end -end diff --git a/test/support/wire/thread.rb b/test/support/wire/thread.rb deleted file mode 100644 index d91a8f67eab556eab15d6370c4ed2f703788fce2..0000000000000000000000000000000000000000 --- a/test/support/wire/thread.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class Wire < Thread - def self.sleep(sec) - Kernel.sleep(sec) - end -end