pax_global_header00006660000000000000000000000064151565401510014515gustar00rootroot0000000000000052 comment=f4808dec166f53ffe25939c30fb53123bff275bb redis-rb-redis-client-367ca25/000077500000000000000000000000001515654015100161405ustar00rootroot00000000000000redis-rb-redis-client-367ca25/.github/000077500000000000000000000000001515654015100175005ustar00rootroot00000000000000redis-rb-redis-client-367ca25/.github/dependabot.yml000066400000000000000000000005761515654015100223400ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "bundler" directory: "/" schedule: interval: "weekly" ignore: # redis is used for benchmarking, since redis 5.0 use # redis-client under the hood, there's no point comparing it. - dependency-name: "redis" redis-rb-redis-client-367ca25/.github/workflows/000077500000000000000000000000001515654015100215355ustar00rootroot00000000000000redis-rb-redis-client-367ca25/.github/workflows/ci.yml000066400000000000000000000065671515654015100226710ustar00rootroot00000000000000--- name: Test on: [push, pull_request] jobs: lint: name: Rubocop env: BUNDLE_WITHOUT: "benchmark" timeout-minutes: 15 strategy: fail-fast: false runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3.3" bundler-cache: true - name: Lint run: bundle exec rubocop ruby: name: Ruby ${{ matrix.ruby }} env: BUNDLE_WITHOUT: "linter:benchmark" timeout-minutes: 15 strategy: fail-fast: false matrix: os: ["ubuntu-latest"] redis: ["6.2"] ruby: ["ruby-head", "4.0", "3.4", "3.3", "3.2", "3.1", "3.0", "2.7", "2.6", "jruby", "truffleruby"] runs-on: ${{ matrix.os }} steps: - name: Check out code uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Cache redis build uses: actions/cache@v5 with: path: tmp/cache key: "local-tmp-cache-${{ matrix.redis }}-on-${{ matrix.os }}" - name: Lower system timeout run: sudo sysctl -w net.ipv4.tcp_syn_retries=2 - name: Test run: | bundle exec rake ci env: EXT_PEDANTIC: "1" REDIS: ${{ matrix.redis }} redis: name: Redis ${{ matrix.redis }} env: BUNDLE_WITHOUT: "linter:benchmark" timeout-minutes: 15 strategy: fail-fast: false matrix: os: ["ubuntu-latest"] redis: ["7.0", "7.2"] ruby: ["3.2"] runs-on: ${{ matrix.os }} steps: - name: Check out code uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Cache redis build uses: actions/cache@v5 with: path: tmp/cache key: "local-tmp-cache-${{ matrix.redis }}-on-${{ matrix.os }}" - name: Lower system timeout run: sudo sysctl -w net.ipv4.tcp_syn_retries=2 - name: Test run: | bundle exec rake ci env: EXT_PEDANTIC: "1" REDIS: ${{ matrix.redis }} # Redis sentinel is super slow to setup nad very flaky # So we run them independently against a single set of versions # so that they're easier to retry and less likely to flake. sentinel: name: Sentinel env: BUNDLE_WITHOUT: "linter:benchmark" timeout-minutes: 15 strategy: fail-fast: false matrix: os: ["ubuntu-latest"] redis: ["6.2"] ruby: ["3.1"] runs-on: ${{ matrix.os }} steps: - name: Check out code uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Cache redis build uses: actions/cache@v5 with: path: tmp/cache key: "local-tmp-cache-${{ matrix.redis }}-on-${{ matrix.os }}" - name: Lower system timeout run: sudo sysctl -w net.ipv4.tcp_syn_retries=2 - name: Test run: | bundle exec rake test:sentinel env: EXT_PEDANTIC: "1" REDIS: ${{ matrix.redis }} redis-rb-redis-client-367ca25/.gitignore000066400000000000000000000002521515654015100201270ustar00rootroot00000000000000/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /hiredis-client/pkg/ /spec/reports/ /tmp/ /log/ /hiredis-client/tmp/ /bin/toxiproxy-server *.so *.bundle *.dll *.rdb redis-rb-redis-client-367ca25/.rubocop.yml000066400000000000000000000065551515654015100204250ustar00rootroot00000000000000AllCops: TargetRubyVersion: 2.6 NewCops: enable SuggestExtensions: false Bundler/OrderedGems: Enabled: false Layout/LineLength: Max: 120 Exclude: - 'test/**/*' - 'benchmark/**/*' Layout/CaseIndentation: EnforcedStyle: end Layout/LineEndStringConcatenationIndentation: EnforcedStyle: indented Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation Lint/RescueException: Enabled: false Lint/SuppressedException: Enabled: false Lint/AssignmentInCondition: Enabled: false Lint/UnifiedInteger: Enabled: false Lint/UnderscorePrefixedVariableName: Enabled: false Lint/EmptyBlock: Enabled: false Lint/ShadowedException: Enabled: false Lint/DuplicateBranch: Enabled: false Lint/MissingSuper: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/AbcSize: Enabled: false Metrics/BlockLength: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/ParameterLists: Enabled: false Metrics/PerceivedComplexity: Enabled: false Style/InfiniteLoop: Enabled: false Style/WhileUntilModifier: Enabled: false Style/Alias: EnforcedStyle: prefer_alias_method Style/PercentLiteralDelimiters: Enabled: false Style/ParallelAssignment: Enabled: false Style/NumericPredicate: Enabled: false Style/NumericLiterals: Enabled: false Style/IfUnlessModifier: Enabled: false Style/SignalException: Exclude: - 'lib/redis/connection/synchrony.rb' Style/StringLiterals: Enabled: false Style/DoubleNegation: Enabled: false Style/MultipleComparison: Enabled: false Style/GuardClause: Enabled: false Style/Semicolon: Enabled: false Style/Documentation: Enabled: false Style/FormatStringToken: Enabled: false Style/FormatString: Enabled: false Style/RescueStandardError: Enabled: false Style/WordArray: Enabled: false Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrailingCommaInArguments: EnforcedStyleForMultiline: consistent_comma Lint/NonLocalExitFromIterator: Enabled: false Layout/FirstArrayElementIndentation: EnforcedStyle: consistent Layout/FirstHashElementIndentation: EnforcedStyle: consistent Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/ElseAlignment: Enabled: false Layout/RescueEnsureAlignment: Enabled: false Naming/HeredocDelimiterNaming: Enabled: false Naming/FileName: Enabled: false Naming/RescuedExceptionsVariableName: Enabled: false Naming/AccessorMethodName: Exclude: - lib/redis/connection/ruby.rb Naming/MethodParameterName: Enabled: false Metrics/BlockNesting: Enabled: false Style/HashTransformValues: Enabled: false Style/FetchEnvVar: Enabled: false Style/SymbolProc: Exclude: - 'test/**/*' Style/SoleNestedConditional: Enabled: false Style/GlobalVars: Exclude: - '**/extconf.rb' - bin/console Gemspec/RequireMFA: Enabled: false Style/StderrPuts: Enabled: false Style/ModuleFunction: Enabled: false Style/CombinableLoops: Enabled: false Style/DocumentDynamicEvalDefinition: Enabled: false Style/CaseLikeIf: Enabled: false Style/EmptyMethod: Enabled: false Style/AccessModifierDeclarations: Enabled: false Style/RedundantFreeze: Enabled: false # Has some false positives redis-rb-redis-client-367ca25/CHANGELOG.md000066400000000000000000000217461515654015100177630ustar00rootroot00000000000000# Unreleased # 0.28.0 - Added `RedisClient::HashRing` for horizontal sharing (compatible with `Redis::Distributed` from `redis-rb`). # 0.27.0 - Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to 30 seconds. - Added `driver_info` configuration, to issue `CLIENT SETINFO` during connection prelude. # 0.26.4 - Further improve `rediss://` URLs used with Redis sentinel. Now avoid override explictly set `ssl:` parameter. - Fix compatibility with `redis-rb` in sentinel mode. # 0.26.3 - Fix `rediss://` (ssl) URLs used with Redis sentinel. - Handle Ruby 4.0 connection timeout raising an `IO::Timeout` instead of `Errno::ETIMEDOUT`. - Entirely close the connection on authentication failures. # 0.26.2 - Fix compatibility with `connection_pool` version 3+. # 0.26.1 - Fix a few corner cases where `RedisClient::Error#final?` was innacurate. - hiredis-client: Properly reconnect to the new leader after a sentinel failover. # 0.26.0 - Add `RedisClient::Error#final?` and `#retriable?` to allow middleware to filter out non-final errors. - Fix precedence of `db: nil` initialization parameter. ```ruby Redis.new(url: "redis://localhost:6379/3", db: nil).db ``` Before: `0` After: `3` # 0.25.3 - Fix `hiredis-client` compilation with `clang 21`. # 0.25.2 - Fix circuit breakers to respect the `error_threshold_timeout` config is provided. - Fix circuit breakers to clear errors when closing back. # 0.25.1 - Fix Ruby driver TCP keep alive TTL. It was intended to be 120 seconds but was mistakenly set to 15 seconds. # 0.25.0 - Fix `hiredis-client` compilation with GCC 15. - Fix `hiredis-client` from a work directory with spaces. - Add `CommandError#code`. - Add `RedisClient::NoScriptError` for `EVALSHA`. # 0.24.0 - Allow `sentinel_password` to be provided as a `Proc`. - Ensure `Config#inspect` and `Config#to_s` do not display stored passwords. # 0.23.2 - Fix retry logic not to attempt to retry on an open circuit breaker. Fix #227. # 0.23.1 - Fix a potential crash in `hiredis-client` when using subcriptions (`next_event`). See #221. # 0.23.0 - Allow `password` to be a callable. Makes it easy to implement short lived password authentication strategies. - Fix a thread safety issue in `hiredis-client` when using the `pubsub` client concurrently. # 0.22.2 - Fix the sentinel client to properly extend timeout for blocking commands. - Fix IPv6 support in `RedisClient::Config#server_url`. # 0.22.1 - Fix `ProtocolError: Unknown sigil type` errors when using SSL connection. See #190. # 0.22.0 - Made various performance optimizations to the Ruby driver. See #184. - Always assume UTF-8 encoding instead of relying on `Encoding.default_external`. - Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`. See #187. # 0.21.1 - Handle unresolved Sentinel master/replica error when displaying server URL in exceptions. See #182. # 0.21.0 - Include redis server URL in most error messages. See #178. - Close Redis Sentinel connection after resolving role. See #176. # 0.20.0 - Accept `unix://` schemes as well as simple paths in the `url:` config parameter. #170. - Make basic usage Ractor compatible. # 0.19.1 - Fixed a bug in `hiredis-client` that could cause a crash if interrupted by `Timeout.timeout` or other `Thread#raise` based mecanism. - Fixed a GC bug that could cause crashes in `hiredis-client`. # 0.19.0 - Revalidate connection in `RedisClient#connected?` - Eagerly fail if `db:` isn't an Integer. #151. # 0.18.0 - Expose more connection details such as `host`, `db`, etc on `RedisClient`. # 0.17.1 - Add support for `NaN` in RESP3 protocol doubles. This was initially missing from the spec and added about a year ago. # 0.17.0 - Adds `sentinel_username` and `sentinel_password` options for `RedisClient#sentinel` # 0.16.0 - Add `RedisClient#disable_reconnection`. - Reverted the special discard of connection. A regular `close(2)` should be enough. # 0.15.0 - Discard sockets rather than explictly close them when a fork is detected. #126. - Allow to configure sentinel client via url. #117. - Fix sentinel to preverse the auth/password when refreshing the sentinel list. #107. - Added `RedisClient#measure_round_trip_delay` method. #113. # 0.14.1 - Include the timeout value in TimeoutError messages. - Fix connection keep-alive on FreeBSD. #102. # 0.14.0 - Implement Sentinels list automatic refresh. - hiredis binding now implement GC compaction and write barriers. - hiredis binding now properly release the GVL around `connect(2)`. - hiredis the client memory is now re-used on reconnection when possible to reduce allocation churn. # 0.13.0 - Enable TCP keepalive on redis sockets. It sends a keep alive probe every 15 seconds for 2 minutes. #94. # 0.12.2 - Cache calls to `Process.pid` on Ruby 3.1+. #91. # 0.12.1 - Improve compatibility with `uri 0.12.0` (default in Ruby 3.2.0). # 0.12.0 - hiredis: fix a compilation issue on macOS and Ruby 3.2.0. See: #79 - Close connection on MASTERDOWN errors. Similar to READONLY. - Add a `circuit_breaker` configuration option for cache servers and other disposable Redis servers. See #55 / #70 # 0.11.2 - Close connection on READONLY errors. Fix: #64 - Handle Redis 6+ servers with a missing HELLO command. See: #67 - Validate `url` parameters a bit more strictly. Fix #61 # 0.11.1 - hiredis: Workaround a compilation bug with Xcode 14.0. Fix: #58 - Accept `URI` instances as `uri` parameter. # 0.11.0 - hiredis: do not eagerly close the connection on read timeout, let the caller decide if a timeout is final. - Add `Config#custom` to store configuration metadata. It can be used for per server middleware configuration. # 0.10.0 - Added instance scoped middlewares. See: #53 - Allow subclasses of accepted types as command arguments. Fix: #51 - Improve hiredis driver error messages. # 0.9.0 - Automatically reconnect if the process was forked. # 0.8.1 - Make the client resilient to `Timeout.timeout` or `Thread#kill` use (it still is very much discouraged to use either). Use of async interrupts could cause responses to be interleaved. - hiredis: handle commands returning a top-level `false` (no command does this today, but some extensions might). - Workaround a bug in Ruby 2.6 causing a crash if the `debug` gem is enabled when `redis-client` is being required. Fix: #48 # 0.8.0 - Add a `connect` interface to the instrumentation API. # 0.7.4 - Properly parse script errors on pre 7.0 redis server. # 0.7.3 - Fix a bug in `url` parsing conflicting with the `path` option. # 0.7.2 - Raise a distinct `RedisClient::OutOfMemoryError`, for Redis `OOM` errors. - Fix the instrumentation API to be called even for authentication commands. - Fix `url:` configuration to accept a trailing slash. # 0.7.1 - Fix `#pubsub` being called when reconnection is disabled (redis-rb compatibility fix). # 0.7.0 - Sentinel config now accept a list of URLs: `RedisClient.sentinel(sentinels: %w(redis://example.com:7000 redis://example.com:7001 ..))` # 0.6.2 - Fix sentinel to not connected to s_down or o_down replicas. # 0.6.1 - Fix `REDIS_REPLY_SET` parsing in `hiredis`. # 0.6.0 - Added `protocol: 2` options to talk with Redis 5 and older servers. - Added `_v` versions of `call` methods to make it easier to pass commands as arrays without splating. - Fix calling `blocking_call` with a block in a pipeline. - `blocking_call` now raise `ReadTimeoutError` if the command didn't complete in time. - Fix `blocking_call` to not respect `reconnect_attempts` on timeout. - Stop parsing RESP3 sets as Ruby Set instances. - Fix `SystemStackError` when parsing very large hashes. Fix: #30 - `hiredis` now more properly release the GVL when doing IOs. # 0.5.1 - Fix a regression in the `scan` familly of methods, they would raise with `ArgumentError: can't issue an empty redis command`. Fix: #24 # 0.5.0 - Fix handling of connection URLs with empty passwords (`redis://:pass@example.com`). - Handle URLs with IPv6 hosts. - Add `RedisClient::Config#server_url` as a quick way to identify which server the client is pointing to. - Add `CommandError#command` to expose the command that caused the error. - Raise a more explicit error when connecting to older redises without RESP3 support (5.0 and older). - Properly reject empty commands early. # 0.4.0 - The `hiredis` driver have been moved to the `hiredis-client` gem. # 0.3.0 - `hiredis` is now the default driver when available. - Add `RedisClient.default_driver=`. - `#call` now takes an optional block to cast the return value. - Treat `#call` keyword arguments as Redis flags. - Fix `RedisClient#multi` returning some errors as values instead of raising them. # 0.2.1 - Use a more robust way to detect the current compiler. # 0.2.0 - Added `RedisClient.register` as a public instrumentation API. - Fix `read_timeout=` and `write_timeout=` to apply even when the client or pool is already connected. - Properly convert DNS resolution errors into `RedisClient::ConnectionError`. Previously it would raise `SocketError` # 0.1.0 - Initial Release redis-rb-redis-client-367ca25/Gemfile000066400000000000000000000010121515654015100174250ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" # Specify your gem's dependencies in redis-client.gemspec gemspec name: "redis-client" gem "megatest" gem "rake", "~> 13.3" gem "rake-compiler" gem "toxiproxy" gem "benchmark" group :linter do gem "rubocop" gem "base64" # For rubocop gem "ostruct" # For rubocop gem "rubocop-minitest" end group :benchmark do gem "benchmark-ips" gem "hiredis" gem "redis", "~> 4.6" gem "cgi", ">= 0.5.0" # For Redis 4.x gem "stackprof", platform: :mri end redis-rb-redis-client-367ca25/Gemfile.lock000066400000000000000000000027151515654015100203670ustar00rootroot00000000000000PATH remote: . specs: redis-client (0.28.0) connection_pool GEM remote: https://rubygems.org/ specs: ast (2.4.2) base64 (0.3.0) benchmark (0.5.0) benchmark-ips (2.14.0) cgi (0.5.1) cgi (0.5.1-java) connection_pool (2.5.5) hiredis (0.6.3) hiredis (0.6.3-java) json (2.7.6) json (2.7.6-java) megatest (0.6.0) ostruct (0.6.3) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) racc racc (1.7.3) racc (1.7.3-java) rainbow (3.1.1) rake (13.3.1) rake-compiler (1.3.1) rake redis (4.6.0) regexp_parser (2.9.0) rexml (3.3.9) rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) parser (>= 3.2.1.0) rubocop-minitest (0.30.0) rubocop (>= 1.39, < 2.0) ruby-progressbar (1.13.0) stackprof (0.2.28) toxiproxy (2.0.2) unicode-display_width (2.5.0) PLATFORMS ruby universal-java-18 x86_64-darwin-20 x86_64-linux DEPENDENCIES base64 benchmark benchmark-ips cgi (>= 0.5.0) hiredis megatest ostruct rake (~> 13.3) rake-compiler redis (~> 4.6) redis-client! rubocop rubocop-minitest stackprof toxiproxy BUNDLED WITH 2.4.22 redis-rb-redis-client-367ca25/LICENSE.md000066400000000000000000000020621515654015100175440ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2022 Shopify Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. redis-rb-redis-client-367ca25/README.md000066400000000000000000000516311515654015100174250ustar00rootroot00000000000000# RedisClient `redis-client` is a simple, low-level, client for [Redis](https://redis.io/) 6+, [Valkey](https://valkey.io/) 7+, [KeyDB](https://docs.keydb.dev/), and several other databases that implement the same `RESP3` protocol. Contrary to the `redis` gem, `redis-client` doesn't try to map all Redis commands to Ruby constructs, it merely is a thin wrapper on top of the `RESP3` protocol. ## Installation Add this line to your application's Gemfile: ```ruby gem 'redis-client' ``` And then execute: $ bundle install Or install it yourself as: $ gem install redis-client ## Usage To use `RedisClient` you first define a connection configuration, from which you can create a connection pool: ```ruby redis_config = RedisClient.config(host: "10.0.1.1", port: 6380, db: 15) redis = redis_config.new_pool(timeout: 0.5, size: Integer(ENV.fetch("RAILS_MAX_THREADS", 5))) redis.call("PING") # => "PONG" ``` If you are issuing multiple commands in a raw, but can't pipeline them, it's best to use `#with` to avoid going through the connection checkout several times: ```ruby redis.with do |r| r.call("SET", "mykey", "hello world") # => "OK" r.call("GET", "mykey") # => "hello world" end ``` If you are working in a single-threaded environment, or wish to use your own connection pooling mechanism, you can obtain a raw client with `#new_client` ```ruby redis_config = RedisClient.config(host: "10.0.1.1", port: 6380, db: 15) redis = redis_config.new_client redis.call("PING") # => "PONG" ``` NOTE: Raw `RedisClient` instances must not be shared between threads. Make sure to read the section on [thread safety](#thread-safety). For simple use cases where only a single connection is needed, you can use the `RedisClient.new` shortcut: ```ruby redis = RedisClient.new redis.call("GET", "mykey") ``` ### Configuration - `url`: A Redis connection URL, e.g. `redis://example.com:6379/5` - a `rediss://` scheme enables SSL, and the path is interpreted as a database number. To connect to UNIX domain sockets, the `url` can also just be a path, and the database specified as query parameter: `/run/redis/foo.sock?db=5`, or optionally have a `unix://` scheme: `unix:///run/redis/foo.sock?db=5` Note that all other configurations take precedence, e.g. `RedisClient.config(url: "redis://localhost:3000", port: 6380)` will connect on port `6380`. - `host`: The server hostname or IP address. Defaults to `"localhost"`. - `port`: The server port. Defaults to `6379`. - `path`: The path to a UNIX socket, if set `url`, `host` and `port` are ignored. - `ssl`: Whether to connect using SSL or not. - `ssl_params`: A configuration Hash passed to [`OpenSSL::SSL::SSLContext#set_params`](https://www.rubydoc.info/stdlib/openssl/OpenSSL%2FSSL%2FSSLContext:set_params), notable options include: - `cert`: The path to the client certificate (e.g. `client.crt`). - `key`: The path to the client key (e.g. `client.key`). - `ca_file`: The certificate authority to use, useful for self-signed certificates (e.g. `ca.crt`), - `db`: The database to select after connecting, defaults to `0`. - `id` ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`. - `username` Username to authenticate against server, defaults to `"default"`. - `password` Password to authenticate against server. Can either be a String or a callable that recieve `username` as argument and return a passowrd as a String. - `timeout`: The general timeout in seconds, default to `1.0`. - `connect_timeout`: The connection timeout, takes precedence over the general timeout when connecting to the server. - `read_timeout`: The read timeout, takes precedence over the general timeout when reading responses from the server. - `write_timeout`: The write timeout, takes precedence over the general timeout when sending commands to the server. - `idle_timeout`: Amount of time after which an idle connection has to be revalidated with a PING command. Defaults to `30` seconds. - `reconnect_attempts`: Specify how many times the client should retry to send queries. Defaults to `0`. Makes sure to read the [reconnection section](#reconnection) before enabling it. - `circuit_breaker`: A Hash with circuit breaker configuration. Defaults to `nil`. See the [circuit breaker section](#circuit-breaker) for details. - `protocol:` The version of the RESP protocol to use. Default to `3`. - `custom`: A user-owned value ignored by `redis-client` but available as `Config#custom`. This can be used to hold middleware configurations and other user-specific metadata. ### Sentinel support The client is able to perform automatic failover by using [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/). To connect using Sentinel, use: ```ruby redis_config = RedisClient.sentinel( name: "mymaster", sentinels: [ { host: "127.0.0.1", port: 26380 }, { host: "127.0.0.1", port: 26381 }, ], role: :master, ) ``` or: ```ruby redis_config = RedisClient.sentinel( name: "mymaster", sentinels: [ "redis://127.0.0.1:26380", "redis://127.0.0.1:26381", ], role: :master, ) ``` * The name identifies a group of Redis instances composed of a master and one or more replicas (`mymaster` in the example). * It is possible to optionally provide a role. The allowed roles are `:master` and `:replica`. When the role is `:replica`, the client will try to connect to a random replica of the specified master. If a role is not specified, the client will connect to the master. * When using the Sentinel support you need to specify a list of sentinels to connect to. The list does not need to enumerate all your Sentinel instances, but a few so that if one is down the client will try the next one. The client is able to remember the last Sentinel that was able to reply correctly and will use it for the next requests. To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) Sentinel itself, you can specify the `sentinel_username` and `sentinel_password` options per instance. Exclude the `sentinel_username` option if you're using password-only authentication. ```ruby SENTINELS = [{ host: '127.0.0.1', port: 26380}, { host: '127.0.0.1', port: 26381}] redis_config = RedisClient.sentinel(name: 'mymaster', sentinel_username: 'appuser', sentinel_password: 'mysecret', sentinels: SENTINELS, role: :master) ``` If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials ```ruby ### Consistent Hashing To horizontally shard keys across multiple servers without relying on clustering, a `RedisClient::HashRing` class is provided: ```ruby ring = RedisClient.ring( RedisClient.config(host: "10.0.1.1", port: 6380).new_pool(timeout: 0.5, size: 3), RedisClient.config(host: "10.0.1.2", port: 6380).new_pool(timeout: 0.5, size: 3), RedisClient.config(host: "10.0.1.3", port: 6380).new_pool(timeout: 0.5, size: 3), ) ring.node_for("cache_key").call("GET", "cache_key") # => "value" ring.nodes_for("key1", "key2", "key3").each do |node, keys| node.call("DEL", *keys) end ring.nodes.each do |node| node.close end ``` Note that regular clients do respond to `node_for`, `nodes_for` and `nodes`, so that code that support `RedisClient.ring` is also usable with a single server. # Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels: SENTINELS = [{ host: '127.0.0.1', port: 26380 }, { host: '127.0.0.1', port: 26381 }] redis_config = RedisClient.sentinel(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret') ``` So you have to provide Sentinel credential and Redis explicitly even they are the same ```ruby # Use 'mysecret' to authenticate against the mymaster instance and sentinel SENTINELS = [{ host: '127.0.0.1', port: 26380 }, { host: '127.0.0.1', port: 26381 }] redis_config = RedisClient.sentinel(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret') ``` Also the `name`, `password`, `username` and `db` for Redis instance can be passed as an url: ```ruby redis_config = RedisClient.sentinel(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master) ``` ### Type support Only a select few Ruby types are supported as arguments beside strings. Integer and Float are supported: ```ruby redis.call("SET", "mykey", 42) redis.call("SET", "mykey", 1.23) ``` is equivalent to: ```ruby redis.call("SET", "mykey", 42.to_s) redis.call("SET", "mykey", 1.23.to_s) ``` Arrays are flattened as arguments: ```ruby redis.call("LPUSH", "list", [1, 2, 3], 4) ``` is equivalent to: ```ruby redis.call("LPUSH", "list", "1", "2", "3", "4") ``` Hashes are flattened as well: ```ruby redis.call("HMSET", "hash", { "foo" => "1", "bar" => "2" }) ``` is equivalent to: ```ruby redis.call("HMSET", "hash", "foo", "1", "bar", "2") ``` Any other type requires the caller to explicitly cast the argument as a string. Keywords arguments are treated as Redis command flags: ```ruby redis.call("SET", "mykey", "value", nx: true, ex: 60) redis.call("SET", "mykey", "value", nx: false, ex: nil) ``` is equivalent to: ```ruby redis.call("SET", "mykey", "value", "nx", "ex", "60") redis.call("SET", "mykey", "value") ``` If flags are built dynamically, you'll have to explicitly pass them as keyword arguments with `**`: ```ruby flags = {} flags[:nx] = true if something? redis.call("SET", "mykey", "value", **flags) ``` **Important Note**: because of the keyword argument semantic change between Ruby 2 and Ruby 3, unclosed hash literals with string keys may be interpreted differently: ```ruby redis.call("HMSET", "hash", "foo" => "bar") ``` On Ruby 2 `"foo" => "bar"` will be passed as a positional argument, but on Ruby 3 it will be interpreted as keyword arguments. To avoid such problem, make sure to enclose hash literals: ```ruby redis.call("HMSET", "hash", { "foo" => "bar" }) ``` ### Commands return values Contrary to the `redis` gem, `redis-client` doesn't do any type casting on the return value of commands. If you wish to cast the return value, you can pass a block to the `#call` family of methods: ```ruby redis.call("INCR", "counter") # => 1 redis.call("GET", "counter") # => "1" redis.call("GET", "counter", &:to_i) # => 1 redis.call("EXISTS", "counter") # => 1 redis.call("EXISTS", "counter") { |c| c > 0 } # => true ``` ### `*_v` methods In some it's more convenient to pass commands as arrays, for that `_v` versions of `call` methods are available. ```ruby redis.call_v(["MGET"] + keys) redis.blocking_call_v(1, ["MGET"] + keys) redis.call_once_v(1, ["MGET"] + keys) ``` ### Blocking commands For blocking commands such as `BRPOP`, a custom timeout duration can be passed as first argument of the `#blocking_call` method: ``` redis.blocking_call(timeout, "BRPOP", "key", 0) ``` If `timeout` is reached, `#blocking_call` raises `RedisClient::ReadTimeoutError` and doesn't retry regardless of the `reconnect_attempts` configuration. `timeout` is expressed in seconds, you can pass `false` or `0` to mean no timeout. ### Scan commands For easier use of the [`SCAN` family of commands](https://redis.io/commands/scan), `#scan`, `#sscan`, `#hscan` and `#zscan` methods are provided ```ruby redis.scan("MATCH", "pattern:*") do |key| ... end ``` ```ruby redis.sscan("myset", "MATCH", "pattern:*") do |key| ... end ``` For `HSCAN` and `ZSCAN`, pairs are yielded ```ruby redis.hscan("myhash", "MATCH", "pattern:*") do |key, value| ... end ``` ```ruby redis.zscan("myzset") do |element, score| ... end ``` In all cases the `cursor` parameter must be omitted and starts at `0`. ### Pipelining When multiple commands are executed sequentially, but are not dependent, the calls can be pipelined. This means that the client doesn't wait for reply of the first command before sending the next command. The advantage is that multiple commands are sent at once, resulting in faster overall execution. The client can be instructed to pipeline commands by using the `#pipelined method`. After the block is executed, the client sends all commands to Redis and gathers their replies. These replies are returned by the `#pipelined` method. ```ruby redis.pipelined do |pipeline| pipeline.call("SET", "foo", "bar") # => nil pipeline.call("INCR", "baz") # => nil end # => ["OK", 1] ``` #### Exception management The `exception` flag in the `#pipelined` method of `RedisClient` 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.call("SET", "foo", "bar") # => nil pipeline.call("DOESNOTEXIST", 12) # => nil pipeline.call("INCR", "baz") # => nil end # results => ["OK", #, 2] results.each do |result| if result.is_a?(RedisClient::CommandError) # Do something with the failed result end end ``` ### Transactions You can use [`MULTI/EXEC` to run a number of commands in an atomic fashion](https://redis.io/topics/transactions). This is similar to executing a pipeline, but the commands are preceded by a call to `MULTI`, and followed by a call to `EXEC`. Like the regular pipeline, the replies to the commands are returned by the `#multi` method. ```ruby redis.multi do |transaction| transaction.call("SET", "foo", "bar") # => nil transaction.call("INCR", "baz") # => nil end # => ["OK", 1] ``` For optimistic locking, the watched keys can be passed to the `#multi` method: ```ruby redis.multi(watch: ["title"]) do |transaction| title = redis.call("GET", "title") transaction.call("SET", "title", title.upcase) end # => ["OK"] / nil ``` If the transaction wasn't successful, `#multi` will return `nil`. Note that transactions using optimistic locking aren't automatically retried upon connection errors. ### Publish / Subscribe Pub/Sub related commands must be called on a dedicated `PubSub` object: ```ruby redis = RedisClient.new pubsub = redis.pubsub pubsub.call("SUBSCRIBE", "channel-1", "channel-2") loop do if message = pubsub.next_event(timeout) message # => ["subscribe", "channel-1", 1] else # no new message was received in the allocated timeout end end ``` *Note*: pubsub connections are stateful, as such they won't ever reconnect automatically. The caller is responsible for reconnecting if the connection is lost and to resubscribe to all channels. ## Production ### Instrumentation and Middlewares `redis-client` offers a public middleware API to aid in monitoring and library extension. Middleware can be registered either globally or on a given configuration instance. ```ruby module MyGlobalRedisInstrumentation def connect(redis_config) MyMonitoringService.instrument("redis.connect") { super } end def call(command, redis_config) MyMonitoringService.instrument("redis.query") { super } end def call_pipelined(commands, redis_config) MyMonitoringService.instrument("redis.pipeline") { super } end end RedisClient.register(MyGlobalRedisInstrumentation) ``` Note that `RedisClient.register` is global and apply to all `RedisClient` instances. To add middlewares to only a single client, you can provide them when creating the config: ```ruby redis_config = RedisClient.config(middlewares: [AnotherRedisInstrumentation]) redis_config.new_client ``` If middlewares need a client-specific configuration, `Config#custom` can be used ```ruby module MyGlobalRedisInstrumentation def connect(redis_config) MyMonitoringService.instrument("redis.connect", tags: redis_config.custom[:tags]) { super } end def call(command, redis_config) MyMonitoringService.instrument("redis.query", tags: redis_config.custom[:tags]) { super } end def call_pipelined(commands, redis_config) MyMonitoringService.instrument("redis.pipeline", tags: redis_config.custom[:tags]) { super } end end RedisClient.register(MyGlobalRedisInstrumentation) redis_config = RedisClient.config(custom: { tags: { "environment": Rails.env }}) ``` ### Instrumenting Errors It is important to note that when `reconnect_attempts` is enabled, all network errors are reported to the middlewares, even the ones that will be retried. In many cases you may want to ignore retriable errors, or report them differently: ```ruby module MyGlobalRedisInstrumentation def call(command, redis_config) super rescue RedisClient::Error => error if error.final? # Error won't be retried. else # Error will be retried. end raise end end ``` ### Timeouts The client allows you to configure connect, read, and write timeouts. Passing a single `timeout` option will set all three values: ```ruby RedisClient.config(timeout: 1).new ``` But you can use specific values for each of them: ```ruby RedisClient.config( connect_timeout: 0.2, read_timeout: 1.0, write_timeout: 0.5, ).new ``` All timeout values are specified in seconds. ### Reconnection `redis-client` support automatic reconnection after network errors via the `reconnect_attempts:` configuration option. It can be set as a number of retries: ```ruby redis_config = RedisClient.config(reconnect_attempts: 1) ``` **Important Note**: Retrying may cause commands to be issued more than once to the server, so in the case of non-idempotent commands such as `LPUSH` or `INCR`, it may cause consistency issues. To selectively disable automatic retries, you can use the `#call_once` method: ```ruby redis_config = RedisClient.config(reconnect_attempts: 3) redis = redis_config.new_client redis.call("GET", "counter") # Will be retried up to 3 times. redis.call_once("INCR", "counter") # Won't be retried. ``` **Note**: automatic reconnection doesn't apply to pubsub clients as their connection is stateful. ### Exponential backoff Alternatively, `reconnect_attempts` accepts a list of sleep durations for implementing exponential backoff: ```ruby redis_config = RedisClient.config(reconnect_attempts: [0, 0.05, 0.1]) ``` This configuration is generally used when the Redis server is expected to failover or recover relatively quickly and that it's not really possible to continue without issuing the command. When the Redis server is used as an ephemeral cache, circuit breakers are generally preferred. ### Circuit Breaker When Redis is used as a cache and a connection error happens, you may not want to retry as it might take longer than to recompute the value. Instead it's likely preferable to mark the server as unavailable and let it recover for a while. [Circuit breakers are a pattern that does exactly that](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern). Configuration options: - `error_threshold`. The amount of errors to encounter within `error_threshold_timeout` amount of time before opening the circuit, that is to start rejecting requests instantly. - `error_threshold_timeout`. The amount of time in seconds that `error_threshold` errors must occur to open the circuit. Defaults to `error_timeout` seconds if not set. - `error_timeout`. The amount of time in seconds until trying to query the resource again. - `success_threshold`. The amount of successes on the circuit until closing it again, that is to start accepting all requests to the circuit. ```ruby RedisClient.config( circuit_breaker: { # Stop querying the server after 3 errors happened in a 2 seconds window error_threshold: 3, error_threshold_timeout: 2, # Try querying again after 1 second error_timeout: 1, # Stay in half-open state until 3 queries succeeded. success_threshold: 3, } ) ``` ### Drivers `redis-client` ships with a pure Ruby socket implementation. For increased performance, you can enable the `hiredis` binding by adding `hiredis-client` to your Gemfile: ```ruby gem "hiredis-client" ``` The hiredis binding is only available on Linux, macOS and other POSIX platforms. You can install the gem on other platforms, but it won't have any effect. The default driver can be set through `RedisClient.default_driver=`: ## Notable differences with the `redis` gem ### Thread Safety Contrary to the `redis` gem, `redis-client` doesn't protect against concurrent access. To use `redis-client` in concurrent environments, you MUST use a connection pool, or have one client per Thread or Fiber. ## Development After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/redis-rb/redis-client. redis-rb-redis-client-367ca25/Rakefile000066400000000000000000000075351515654015100176170ustar00rootroot00000000000000# frozen_string_literal: true require "rake/extensiontask" begin require 'rubocop/rake_task' RuboCop::RakeTask.new rescue LoadError task :rubocop do # noop end end require "rake/clean" CLOBBER.include "pkg" require "bundler/gem_helper" Bundler::GemHelper.install_tasks(name: "redis-client") Bundler::GemHelper.install_tasks(dir: "hiredis-client", name: "hiredis-client") gemspec = Gem::Specification.load("redis-client.gemspec") Rake::ExtensionTask.new do |ext| ext.name = "hiredis_connection" ext.ext_dir = "hiredis-client/ext/redis_client/hiredis" ext.lib_dir = "hiredis-client/lib/redis_client" ext.gem_spec = gemspec CLEAN.add("#{ext.ext_dir}/vendor/*.{a,o}") end require "megatest/test_task" namespace :test do jobs = ENV["JOBS"] || "4" jobs = jobs && Process.respond_to?(:fork) ? ["-j", jobs] : [] extra_args = ["--max-retries", "1"] + jobs Megatest::TestTask.create(:ruby) do |t| t.tests = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb") t.extra_args = extra_args end Megatest::TestTask.create(:hiredis) do |t| t.libs << "test/hiredis" t.libs << "hiredis-client/lib" t.tests = FileList["test/hiredis/test_helper.rb"] + FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb") t.extra_args = extra_args t.deps << :compile end Megatest::TestTask.create(:sentinel) do |t| t.libs << "test/sentinel" t.tests = ["test/sentinel"] t.extra_args = extra_args end end hiredis_supported = RUBY_ENGINE == "ruby" && !RUBY_PLATFORM.match?(/mswin/) if hiredis_supported task test: %i[test:ruby test:hiredis test:sentinel] else task test: %i[test:ruby test:sentinel] end namespace :hiredis do task :download do version = "1.0.2" archive_path = "tmp/hiredis-#{version}.tar.gz" url = "https://github.com/redis/hiredis/archive/refs/tags/v#{version}.tar.gz" system("curl", "-L", url, out: archive_path) or raise "Downloading of #{url} failed" system("rm", "-rf", "hiredis-client/ext/redis_client/hiredis/vendor/") system("mkdir", "-p", "hiredis-client/ext/redis_client/hiredis/vendor/") system( "tar", "xvzf", archive_path, "-C", "hiredis-client/ext/redis_client/hiredis/vendor", "--strip-components", "1", ) system("rm", "-rf", "hiredis-client/ext/redis_client/hiredis/vendor/examples") end end benchmark_suites = %w(single pipelined drivers) benchmark_modes = %i[ruby yjit hiredis] namespace :benchmark do benchmark_suites.each do |suite| benchmark_modes.each do |mode| next if suite == "drivers" && mode == :hiredis name = "#{suite}_#{mode}" desc name task name do output_path = "benchmark/#{name}.md" sh "rm", "-f", output_path File.open(output_path, "w+") do |output| output.puts("ruby: `#{RUBY_DESCRIPTION}`\n\n") output.puts("redis-server: `#{`redis-server -v`.strip}`\n\n") output.puts output.flush env = {} args = [] args << "--yjit" if mode == :yjit env["DRIVER"] = mode == :hiredis ? "hiredis" : "ruby" system(env, RbConfig.ruby, *args, "benchmark/#{suite}.rb", out: output) end skipping = false output = File.readlines(output_path).reject do |line| if skipping if line == "Comparison:\n" skipping = false true else skipping end else skipping = true if line.start_with?("Warming up ---") skipping end end File.write(output_path, output.join) end end end task all: benchmark_suites.flat_map { |s| benchmark_modes.flat_map { |m| "#{s}_#{m}" } } end if hiredis_supported task default: %i[compile test rubocop] task ci: %i[compile test:ruby test:hiredis] else task default: %i[test rubocop] task ci: %i[test:ruby] end redis-rb-redis-client-367ca25/benchmark/000077500000000000000000000000001515654015100200725ustar00rootroot00000000000000redis-rb-redis-client-367ca25/benchmark/drivers.rb000066400000000000000000000031441515654015100220770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" ruby = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :ruby) hiredis = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: :hiredis) ruby.call("SET", "key", "value") ruby.call("SET", "large", "value" * 10_000) ruby.call("LPUSH", "list", *5.times.to_a) ruby.call("LPUSH", "large-list", *1000.times.to_a) ruby.call("HMSET", "hash", *8.times.to_a) ruby.call("HMSET", "large-hash", *1000.times.to_a) benchmark("small string x 100") do |x| x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("GET", "key") } } } x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("GET", "key") } } } end benchmark("large string") do |x| x.report("hiredis") { hiredis.call("GET", "large") } x.report("ruby") { ruby.call("GET", "large") } end benchmark("small list x 100") do |x| x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } } x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } } end benchmark("large list") do |x| x.report("hiredis") { hiredis.call("LRANGE", "large-list", 0, -1) } x.report("ruby") { ruby.call("LRANGE", "large-list", 0, -1) } end benchmark("small hash x 100") do |x| x.report("hiredis") { hiredis.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } } x.report("ruby") { ruby.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } } end benchmark("large hash") do |x| x.report("hiredis") { ruby.call("HGETALL", "large-hash") } x.report("ruby") { ruby.call("HGETALL", "large-hash") } end redis-rb-redis-client-367ca25/benchmark/drivers_ruby.md000066400000000000000000000025451515654015100231410ustar00rootroot00000000000000ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 5239.0 i/s ruby: 2957.4 i/s - 1.77x slower ``` ### large string ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 12784.9 i/s ruby: 14948.2 i/s - same-ish: difference falls within error ``` ### small list x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 2599.3 i/s ruby: 1300.0 i/s - 2.00x slower ``` ### large list ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 6836.0 i/s ruby: 1867.6 i/s - 3.66x slower ``` ### small hash x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 3392.5 i/s ruby: 1408.9 i/s - 2.41x slower ``` ### large hash ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23] hiredis: 1786.2 i/s ruby: 1811.9 i/s - same-ish: difference falls within error ``` redis-rb-redis-client-367ca25/benchmark/drivers_yjit.md000066400000000000000000000026111515654015100231310ustar00rootroot00000000000000ruby: `ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 7148.9 i/s ruby: 5758.6 i/s - 1.24x slower ``` ### large string ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 13023.5 i/s ruby: 20246.4 i/s - 1.55x faster ``` ### small list x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 3973.6 i/s ruby: 2668.7 i/s - 1.49x slower ``` ### large list ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 6706.8 i/s ruby: 6529.3 i/s - same-ish: difference falls within error ``` ### small hash x 100 ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 4001.6 i/s ruby: 3482.9 i/s - 1.15x slower ``` ### large hash ``` ruby 3.4.0dev (2024-03-19T14:18:56Z master 5c2937733c) +YJIT [arm64-darwin23] hiredis: 5511.9 i/s ruby: 5555.7 i/s - same-ish: difference falls within error ``` redis-rb-redis-client-367ca25/benchmark/pipelined.rb000066400000000000000000000037261515654015100224000ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" driver = ENV.fetch("DRIVER", "ruby").to_sym redis_client = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis = Redis.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis_client.call("SET", "key", "value") redis_client.call("SET", "large", "value" * 10_000) redis_client.call("LPUSH", "list", *5.times.to_a) redis_client.call("LPUSH", "large-list", *1000.times.to_a) redis_client.call("HMSET", "hash", *8.times.to_a) redis_client.call("HMSET", "large-hash", *1000.times.to_a) benchmark("small string") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.get("key") } } } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("GET", "key") } } } end benchmark("large string") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.get("large") } }.each(&:valid_encoding?) } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("GET", "large") } }.each(&:valid_encoding?) } end benchmark("small list") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.lrange("list", 0, -1) } } } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("LRANGE", "list", 0, -1) } } } end benchmark("large list") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.lrange("large-list", 0, -1) } } } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("LRANGE", "large-list", 0, -1) } } } end benchmark("small hash") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.hgetall("hash") } } } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("HGETALL", "hash") } } } end benchmark("large hash") do |x| x.report("redis-rb") { redis.pipelined { |p| 100.times { p.hgetall("large-hash") } } } x.report("redis-client") { redis_client.pipelined { |p| 100.times { p.call("HGETALL", "large-hash") } } } end redis-rb-redis-client-367ca25/benchmark/pipelined_hiredis.md000066400000000000000000000023541515654015100241000ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 5438.6 i/s redis-client: 5552.9 i/s - same-ish: difference falls within error ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 354.4 i/s redis-client: 310.9 i/s - 1.14x slower ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 3081.4 i/s redis-client: 2733.0 i/s - 1.13x slower ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 82.3 i/s redis-client: 65.0 i/s - 1.26x slower ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 2249.0 i/s redis-client: 3117.0 i/s - 1.39x faster ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 46.9 i/s redis-client: 67.5 i/s - 1.44x faster ``` redis-rb-redis-client-367ca25/benchmark/pipelined_ruby.md000066400000000000000000000023221515654015100234250ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 1212.6 i/s redis-client: 3007.1 i/s - 2.48x faster ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 238.9 i/s redis-client: 306.1 i/s - 1.28x faster ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 604.3 i/s redis-client: 1190.9 i/s - 1.97x faster ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 2.3 i/s redis-client: 14.2 i/s - 6.16x faster ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 401.8 i/s redis-client: 1123.9 i/s - 2.80x faster ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 2.3 i/s redis-client: 14.5 i/s - 6.41x faster ``` redis-rb-redis-client-367ca25/benchmark/pipelined_yjit.md000066400000000000000000000023701515654015100234260ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 1460.6 i/s redis-client: 5345.6 i/s - 3.66x faster ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 272.9 i/s redis-client: 338.7 i/s - 1.24x faster ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 732.6 i/s redis-client: 1985.5 i/s - 2.71x faster ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 2.6 i/s redis-client: 31.7 i/s - 12.33x faster ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 471.1 i/s redis-client: 2190.7 i/s - 4.65x faster ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 2.5 i/s redis-client: 30.9 i/s - 12.19x faster ``` redis-rb-redis-client-367ca25/benchmark/profile_large_list.rb000066400000000000000000000006351515654015100242700ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" require "stackprof" driver = ENV.fetch("DRIVER", "ruby").to_sym redis_client = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis_client.call("LPUSH", "list", *1000.times.to_a) StackProf.run(out: "tmp/stackprof-large-list.dump", raw: true) do 1_000.times do redis_client.call("LRANGE", "list", 0, -1) end end redis-rb-redis-client-367ca25/benchmark/profile_large_string.rb000066400000000000000000000006251515654015100246220ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" require "stackprof" driver = ENV.fetch("DRIVER", "ruby").to_sym redis_client = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis_client.call("SET", "large", "value" * 10_000) StackProf.run(out: "tmp/stackprof-large-string.dump", raw: true) do 1_000.times do redis_client.call("GET", "large") end end redis-rb-redis-client-367ca25/benchmark/profile_pipeline.rb000066400000000000000000000007401515654015100237450ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" require "stackprof" driver = ENV.fetch("DRIVER", "ruby").to_sym redis_client = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) StackProf.run(out: "tmp/stackprof-pipeline.dump", raw: true) do 10_000.times do redis_client.pipelined do |pipeline| pipeline.call("SET", "foo", "bar") pipeline.call("GET", "foo") pipeline.call("INCRBY", "counter", 2) end end end redis-rb-redis-client-367ca25/benchmark/setup.rb000066400000000000000000000014131515654015100215560ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) $LOAD_PATH.unshift(File.expand_path("../hiredis-client/lib", __dir__)) $LOAD_PATH.unshift(File.expand_path("../test/support", __dir__)) require "redis" require "redis-client" require "hiredis-client" require "servers" require "benchmark/ips" Servers::BENCHMARK.prepare at_exit { Servers::BENCHMARK.shutdown } class RedisBenchmark def initialize(x) @x = x end def report(name, &block) @x.report(name, &block) end end def benchmark(name) if $stdout.tty? puts "=== #{name} ===" else puts "### #{name}\n\n```" end Benchmark.ips do |x| yield RedisBenchmark.new(x) x.compare!(order: :baseline) end unless $stdout.tty? puts "```\n\n" end end redis-rb-redis-client-367ca25/benchmark/single.rb000066400000000000000000000030561515654015100217040ustar00rootroot00000000000000# frozen_string_literal: true require_relative "setup" driver = ENV.fetch("DRIVER", "ruby").to_sym redis_client = RedisClient.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis = Redis.new(host: "localhost", port: Servers::REDIS.real_port, driver: driver) redis_client.call("SET", "key", "value") redis_client.call("SET", "large", "value" * 10_000) redis_client.call("LPUSH", "list", *5.times.to_a) redis_client.call("LPUSH", "large-list", *1000.times.to_a) redis_client.call("HMSET", "hash", *8.times.to_a) redis_client.call("HMSET", "large-hash", *1000.times.to_a) benchmark("small string") do |x| x.report("redis-rb") { redis.get("key") } x.report("redis-client") { redis_client.call("GET", "key") } end benchmark("large string") do |x| x.report("redis-rb") { redis.get("large").valid_encoding? } x.report("redis-client") { redis_client.call("GET", "large").valid_encoding? } end benchmark("small list") do |x| x.report("redis-rb") { redis.lrange("list", 0, -1) } x.report("redis-client") { redis_client.call("LRANGE", "list", 0, -1) } end benchmark("large list") do |x| x.report("redis-rb") { redis.lrange("large-list", 0, -1) } x.report("redis-client") { redis_client.call("LRANGE", "large-list", 0, -1) } end benchmark("small hash") do |x| x.report("redis-rb") { redis.hgetall("hash") } x.report("redis-client") { redis_client.call("HGETALL", "hash") } end benchmark("large hash") do |x| x.report("redis-rb") { redis.hgetall("large-hash") } x.report("redis-client") { redis_client.call("HGETALL", "large-hash") } end redis-rb-redis-client-367ca25/benchmark/single_hiredis.md000066400000000000000000000023541515654015100234100ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 47841.9 i/s redis-client: 25336.0 i/s - 1.89x slower ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 21223.9 i/s redis-client: 12986.1 i/s - 1.63x slower ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 43794.1 i/s redis-client: 24659.6 i/s - 1.78x slower ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 7014.5 i/s redis-client: 6820.4 i/s - same-ish: difference falls within error ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 41932.1 i/s redis-client: 23808.4 i/s - 1.76x slower ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 4544.7 i/s redis-client: 5388.2 i/s - 1.19x faster ``` redis-rb-redis-client-367ca25/benchmark/single_ruby.md000066400000000000000000000024061515654015100227400ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 33612.8 i/s redis-client: 34726.1 i/s - same-ish: difference falls within error ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 16680.5 i/s redis-client: 18644.2 i/s - same-ish: difference falls within error ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 25772.9 i/s redis-client: 29299.7 i/s - 1.14x faster ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 356.9 i/s redis-client: 1342.9 i/s - 3.76x faster ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 22903.5 i/s redis-client: 29003.8 i/s - 1.27x faster ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] redis-rb: 343.3 i/s redis-client: 1284.8 i/s - 3.74x faster ``` redis-rb-redis-client-367ca25/benchmark/single_yjit.md000066400000000000000000000024521515654015100227370ustar00rootroot00000000000000ruby: `ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]` redis-server: `Redis server v=7.0.12 sha=00000000:0 malloc=libc bits=64 build=a11d0151eabf466c` ### small string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 36024.0 i/s redis-client: 38624.3 i/s - same-ish: difference falls within error ``` ### large string ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 18402.4 i/s redis-client: 21330.5 i/s - same-ish: difference falls within error ``` ### small list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 28625.5 i/s redis-client: 34434.7 i/s - 1.20x faster ``` ### large list ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 404.9 i/s redis-client: 2856.7 i/s - 7.05x faster ``` ### small hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 25868.4 i/s redis-client: 34166.3 i/s - 1.32x faster ``` ### large hash ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +YJIT [arm64-darwin23] redis-rb: 378.0 i/s redis-client: 2432.8 i/s - 6.44x faster ``` redis-rb-redis-client-367ca25/bin/000077500000000000000000000000001515654015100167105ustar00rootroot00000000000000redis-rb-redis-client-367ca25/bin/console000077500000000000000000000004731515654015100203040ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" $LOAD_PATH.unshift(File.expand_path("../hiredis-client/lib", __dir__)) require "redis-client" require "hiredis-client" $redis = RedisClient.new(driver: :ruby) $hiredis = RedisClient.new(driver: :hiredis) require "irb" IRB.start(__FILE__) redis-rb-redis-client-367ca25/bin/install-toxiproxy000077500000000000000000000011131515654015100223630ustar00rootroot00000000000000#!/bin/bash -e VERSION='v2.4.0' OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) DOWNLOAD_TYPE="${OS}-${ARCH}" if [[ "${ARCH}" = "aarch64" ]]; then DOWNLOAD_TYPE="${OS}-arm64" fi if [[ "${ARCH}" = "x86_64" ]]; then DOWNLOAD_TYPE="${OS}-amd64" fi CACHE_DIR="./tmp/cache/${ARCH}-${OS}/" echo "[download toxiproxy for $DOWNLOAD_TYPE into ${CACHE_DIR}]" mkdir -p "${CACHE_DIR}" curl --silent -L "https://github.com/Shopify/toxiproxy/releases/download/${VERSION}/toxiproxy-server-${DOWNLOAD_TYPE}" -o "${CACHE_DIR}/toxiproxy-server" chmod +x "${CACHE_DIR}/toxiproxy-server" redis-rb-redis-client-367ca25/bin/megatest000077500000000000000000000013531515654015100204510ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'megatest' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("megatest", "megatest") redis-rb-redis-client-367ca25/bin/setup000077500000000000000000000002031515654015100177710ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here redis-rb-redis-client-367ca25/hiredis-client/000077500000000000000000000000001515654015100210435ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/README.md000066400000000000000000000002651515654015100223250ustar00rootroot00000000000000# HiredisClient `hiredis-client` provides a `hiredis` binding for the native `hiredis` client library. See [`redis-client`](https://github.com/redis-rb/redis-client) for details. redis-rb-redis-client-367ca25/hiredis-client/ext/000077500000000000000000000000001515654015100216435ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/000077500000000000000000000000001515654015100243075ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/000077500000000000000000000000001515654015100257365ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/extconf.rb000066400000000000000000000052621515654015100277360ustar00rootroot00000000000000# frozen_string_literal: true require "mkmf" class HiredisConnectionExtconf def initialize(debug) @debug = debug end def configure if RUBY_ENGINE == "ruby" && !Gem.win_platform? configure_extension create_makefile("redis_client/hiredis_connection") else File.write("Makefile", dummy_makefile($srcdir).join) end end def configure_extension build_hiredis have_func("rb_hash_new_capa", "ruby.h") append_cflags(["-I #{Shellwords.escape(hiredis_dir)}", "-std=c99", "-fvisibility=hidden"]) $CFLAGS = if @debug concat_flags($CFLAGS, "-Werror", "-g", RbConfig::CONFIG["debugflags"]) else concat_flags($CFLAGS, "-O3") end if @debug $CPPFLAGS = concat_flags($CPPFLAGS, "-DRUBY_DEBUG=1") end append_cflags("-Wno-declaration-after-statement") # Older compilers append_cflags("-Wno-compound-token-split-by-macro") # Older rubies on macos end def build_hiredis env = { "USE_SSL" => 1, "CFLAGS" => concat_flags(ENV["CFLAGS"], "-fvisibility=hidden"), } env["OPTIMIZATION"] = "-g" if @debug env = configure_openssl(env) env_args = env.map { |k, v| "#{k}=#{v}" } Dir.chdir(hiredis_dir) do unless system(*Shellwords.split(make_program), "static", *env_args) raise "Building hiredis failed" end end $LDFLAGS = concat_flags($LDFLAGS, "-lssl", "-lcrypto") $libs = concat_flags($libs, "#{Shellwords.escape(hiredis_dir)}/libhiredis.a", "#{Shellwords.escape(hiredis_dir)}/libhiredis_ssl.a",) end def configure_openssl(original_env) original_env.dup.tap do |env| config = dir_config("openssl") if config.none? config = dir_config("opt").map { |c| detect_openssl_dir(c) } end unless have_header("openssl/ssl.h") message = "ERROR: OpenSSL library could not be found." if config.none? message += "\nUse --with-openssl-dir= option to specify the prefix where OpenSSL is installed." end abort message end if config.any? env["CFLAGS"] = concat_flags(env["CFLAGS"], "-I#{config.first}") env["SSL_LDFLAGS"] = "-L#{config.last}" end end end private def detect_openssl_dir(paths) paths &.split(File::PATH_SEPARATOR) &.detect { |dir| dir.include?("openssl") } end def concat_flags(*args) args.compact.join(" ") end def hiredis_dir File.expand_path('vendor', __dir__) end def make_program with_config("make-prog", ENV["MAKE"]) || case RUBY_PLATFORM when /(bsd|solaris)/ 'gmake' else 'make' end end end HiredisConnectionExtconf.new(ENV["EXT_PEDANTIC"]).configure redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/hiredis_connection.c000066400000000000000000000754441515654015100317660ustar00rootroot00000000000000// Extensive parts of this code is taken from `hiredis-rb`, so we're keeping the // initial copyright: // // Copyright (c) 2010-2012, Pieter Noordhuis // // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * Neither the name of Redis nor the names of its contributors may be used to // endorse or promote products derived from this software without specific prior // written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "ruby.h" #include "ruby/thread.h" #include "ruby/encoding.h" #include #include #include #include "vendor/hiredis.h" #include "vendor/net.h" #include "vendor/hiredis_ssl.h" #include #if !defined(RUBY_ASSERT) # define RUBY_ASSERT(condition) ((void)0) #endif #if !defined(HAVE_RB_HASH_NEW_CAPA) static inline VALUE rb_hash_new_capa(long capa) { return rb_hash_new(); } #endif static void redis_set_error(redisContext *c, int type, const char *str) { size_t len; c->err = type; len = strlen(str); len = len < (sizeof(c->errstr) - 1) ? len : (sizeof(c->errstr) - 1); memcpy(c->errstr, str, len); c->errstr[len] = '\0'; } static VALUE rb_eRedisClientCommandError, rb_eRedisClientConnectionError, rb_eRedisClientCannotConnectError, rb_eRedisClientProtocolError; static VALUE rb_eRedisClientReadTimeoutError, rb_eRedisClientWriteTimeoutError; static VALUE Redis_Qfalse; static ID id_parse; typedef struct { redisSSLContext *context; } hiredis_ssl_context_t; #define ENSURE_CONNECTED(connection) if (!connection->context) rb_raise(rb_eRedisClientConnectionError, "Not connected"); #define SSL_CONTEXT(from, name) \ hiredis_ssl_context_t *name = NULL; \ TypedData_Get_Struct(from, hiredis_ssl_context_t, &hiredis_ssl_context_data_type, name); \ if (name == NULL) { \ rb_raise(rb_eArgError, "NULL found for " # name " when shouldn't be."); \ } static void hiredis_ssl_context_free(void *ptr) { hiredis_ssl_context_t *ssl_context = (hiredis_ssl_context_t *)ptr; if (ssl_context->context) { redisFreeSSLContext(ssl_context->context); ssl_context = NULL; } } static size_t hiredis_ssl_context_memsize(const void *ptr) { size_t size = sizeof(hiredis_ssl_context_t); // Note: I couldn't find a way to measure the SSLContext size. return size; } static const rb_data_type_t hiredis_ssl_context_data_type = { .wrap_struct_name = "redis-client:hiredis_ssl_context", .function = { .dmark = NULL, .dfree = hiredis_ssl_context_free, .dsize = hiredis_ssl_context_memsize, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static VALUE hiredis_ssl_context_alloc(VALUE klass) { hiredis_ssl_context_t *ssl_context; return TypedData_Make_Struct(klass, hiredis_ssl_context_t, &hiredis_ssl_context_data_type, ssl_context); } static VALUE hiredis_ssl_context_init(VALUE self, VALUE ca_file, VALUE ca_path, VALUE cert, VALUE key, VALUE hostname) { redisSSLContextError ssl_error = 0; SSL_CONTEXT(self, ssl_context); ssl_context->context = redisCreateSSLContext( RTEST(ca_file) ? StringValueCStr(ca_file) : NULL, RTEST(ca_path) ? StringValueCStr(ca_path) : NULL, RTEST(cert) ? StringValueCStr(cert) : NULL, RTEST(key) ? StringValueCStr(key) : NULL, RTEST(hostname) ? StringValueCStr(hostname) : NULL, &ssl_error ); if (ssl_error) { return rb_str_new_cstr(redisSSLContextGetError(ssl_error)); } if (!ssl_context->context) { return rb_str_new_cstr("Unknown error while creating SSLContext"); } return Qnil; } typedef struct { VALUE stack; int *task_index; } hiredis_reader_state_t; static void *reply_append(const redisReadTask *task, VALUE value) { hiredis_reader_state_t *state = (hiredis_reader_state_t *)task->privdata; int task_index = *state->task_index; if (task->parent) { RUBY_ASSERT(task_index > 0); VALUE parent = rb_ary_entry(state->stack, task_index - 1); switch (task->parent->type) { case REDIS_REPLY_ARRAY: case REDIS_REPLY_SET: case REDIS_REPLY_PUSH: rb_ary_store(parent, task->idx, value); break; case REDIS_REPLY_MAP: if (task->idx % 2) { VALUE key = rb_ary_pop(state->stack); rb_hash_aset(parent, key, value); RB_GC_GUARD(key); } else { rb_ary_push(state->stack, value); } break; default: rb_bug("[hiredis] Unexpected task parent type %d", task->parent->type); break; } RB_GC_GUARD(parent); } rb_ary_store(state->stack, task_index, value); RB_GC_GUARD(value); return (void*)value; } static void *reply_create_string(const redisReadTask *task, char *cstr, size_t len) { if (len >= 4 && task->type == REDIS_REPLY_VERB) { // Skip 4 bytes of verbatim type header. cstr += 4; len -= 4; } VALUE string = rb_utf8_str_new(cstr, len); if (rb_enc_str_coderange(string) == ENC_CODERANGE_BROKEN) { rb_enc_associate(string, rb_ascii8bit_encoding()); } if (task->type == REDIS_REPLY_STATUS) { rb_str_freeze(string); } if (task->type == REDIS_REPLY_ERROR) { string = rb_funcall(rb_eRedisClientCommandError, id_parse, 1, string); } return reply_append(task, string); } static void *reply_create_array(const redisReadTask *task, size_t elements) { VALUE value = Qnil; switch (task->type) { case REDIS_REPLY_PUSH: case REDIS_REPLY_ARRAY: case REDIS_REPLY_SET: value = rb_ary_new_capa(elements); break; case REDIS_REPLY_MAP: value = rb_hash_new_capa(elements / 2); break; default: rb_bug("[hiredis] Unexpected create array type %d", task->parent->type); break; } return reply_append(task, value); } static void *reply_create_integer(const redisReadTask *task, long long value) { return reply_append(task, LL2NUM(value)); } static void *reply_create_double(const redisReadTask *task, double value, char *str, size_t len) { return reply_append(task, DBL2NUM(value)); } static void *reply_create_nil(const redisReadTask *task) { return reply_append(task, Qnil); } static void *reply_create_bool(const redisReadTask *task, int bval) { reply_append(task, bval ? Qtrue : Qfalse); // Qfalse == NULL, so we can't return Qfalse as it would be interpreted as out of memory error. // So we return a token value instead and the caller is responsible for turning it into Qfalse. return (void*)(bval ? Qtrue : Redis_Qfalse); } static void reply_free(void *ptr) { // we let GC handle it. } /* Default set of functions to build the reply. Keep in mind that such a * function returning NULL is interpreted as OOM. */ static redisReplyObjectFunctions reply_functions = { reply_create_string, reply_create_array, reply_create_integer, reply_create_double, reply_create_nil, reply_create_bool, reply_free, }; typedef struct { redisContext *context; int return_value; } hiredis_buffer_read_args_t; static void *hiredis_buffer_read_safe(void *_args) { hiredis_buffer_read_args_t *args = _args; args->return_value = redisBufferRead(args->context); return NULL; } static int hiredis_buffer_read_nogvl(redisContext *context) { hiredis_buffer_read_args_t args = { .context = context, }; rb_thread_call_without_gvl(hiredis_buffer_read_safe, &args, RUBY_UBF_IO, 0); return args.return_value; } typedef struct { redisContext *context; int *done; int return_value; } hiredis_buffer_write_args_t; static void *hiredis_buffer_write_safe(void *_args) { hiredis_buffer_write_args_t *args = _args; args->return_value = redisBufferWrite(args->context, args->done); return NULL; } static int hiredis_buffer_write_nogvl(redisContext *context, int *done) { hiredis_buffer_write_args_t args = { .context = context, .done = done, }; rb_thread_call_without_gvl(hiredis_buffer_write_safe, &args, RUBY_UBF_IO, 0); return args.return_value; } #define CONNECTION(from, name) \ hiredis_connection_t *name = NULL; \ TypedData_Get_Struct(from, hiredis_connection_t, &hiredis_connection_data_type, name); \ if (name == NULL) { \ rb_raise(rb_eArgError, "NULL found for " # name " when shouldn't be."); \ } typedef struct { redisContext *context; int return_value; } hiredis_reconnect_args_t; static void *hiredis_reconnect_safe(void *_args) { hiredis_reconnect_args_t *args = _args; args->return_value = redisReconnect(args->context); return NULL; } static int hiredis_reconnect_nogvl(redisContext *context) { hiredis_reconnect_args_t args = { .context = context, .return_value = 0 }; rb_thread_call_without_gvl(hiredis_reconnect_safe, &args, RUBY_UBF_IO, 0); return args.return_value; } static void *hiredis_connect_with_options_safe(void *options) { return (void *)redisConnectWithOptions(options); } static redisContext *hiredis_connect_with_options_nogvl(redisOptions *options) { return (redisContext *)rb_thread_call_without_gvl(hiredis_connect_with_options_safe, options, RUBY_UBF_IO, 0); } typedef struct { redisContext *context; struct timeval connect_timeout; struct timeval read_timeout; struct timeval write_timeout; hiredis_reader_state_t reader_state; } hiredis_connection_t; static void hiredis_connection_free(void *ptr) { hiredis_connection_t *connection = ptr; if (connection) { if (connection->context) { // redisFree may calls close() if we're still connected, but // it shoudln't be considered a "blocking" call given we don't // use any socket option that may make it a blocking operation. redisFree(connection->context); } xfree(connection); } } static size_t hiredis_connection_memsize(const void *ptr) { hiredis_connection_t *connection = (hiredis_connection_t *)ptr; size_t size = sizeof(hiredis_connection_t); if (connection->context) { size += sizeof(redisContext); if (connection->context->reader) { redisReader *reader = connection->context->reader; size += sizeof(redisReader); size += reader->maxbuf; size += reader->tasks * sizeof(redisReadTask); } } return size; } static const rb_data_type_t hiredis_connection_data_type = { .wrap_struct_name = "redis-client:hiredis_connection", .function = { .dmark = NULL, .dfree = hiredis_connection_free, .dsize = hiredis_connection_memsize, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static VALUE hiredis_alloc(VALUE klass) { hiredis_connection_t *connection; return TypedData_Make_Struct(klass, hiredis_connection_t, &hiredis_connection_data_type, connection); } static void redis_set_io_error(redisContext *context, int err) { if (err) { errno = err; } context->err = REDIS_ERR_IO; (void)!strerror_r(errno, context->errstr, sizeof(context->errstr)); } static inline void redis_raise_error_and_disconnect(redisContext *context, VALUE timeout_error) { if (!context) return; int err = context->err; char errstr[128]; if (context->err) { strncpy(errstr, context->errstr, 128); } redisNetClose(context); if (!err) { rb_raise(timeout_error, "Unknown Error (redis_raise_error_and_disconnect)"); } // OpenSSL bug: The SSL_ERROR_SYSCALL with errno value of 0 indicates unexpected EOF from the peer. if (errno == EAGAIN || (err == REDIS_ERR_IO && errno == 0)) { errno = 0; rb_raise(timeout_error, "Resource temporarily unavailable"); } switch(err) { case REDIS_ERR_IO: rb_sys_fail(0); break; case REDIS_ERR_PROTOCOL: rb_raise(rb_eRedisClientProtocolError, "%s", errstr); default: /* Raise something else */ rb_raise(rb_eRedisClientConnectionError, "%s", errstr); } } static inline void hiredis_raise_error_and_disconnect(hiredis_connection_t *connection, VALUE timeout_error) { redisContext *context = connection->context; if (!context) return; connection->context = NULL; redis_raise_error_and_disconnect(context, timeout_error); } static VALUE hiredis_set_connect_timeout(VALUE self, VALUE timeout_us) { CONNECTION(self, connection); connection->connect_timeout.tv_sec = NUM2INT(timeout_us) / 1000000; connection->connect_timeout.tv_usec = NUM2INT(timeout_us) % 1000000; return timeout_us; } static VALUE hiredis_set_read_timeout(VALUE self, VALUE timeout_us) { CONNECTION(self, connection); connection->read_timeout.tv_sec = NUM2INT(timeout_us) / 1000000; connection->read_timeout.tv_usec = NUM2INT(timeout_us) % 1000000; return timeout_us; } static VALUE hiredis_set_write_timeout(VALUE self, VALUE timeout_us) { CONNECTION(self, connection); connection->write_timeout.tv_sec = NUM2INT(timeout_us) / 1000000; connection->write_timeout.tv_usec = NUM2INT(timeout_us) % 1000000; return timeout_us; } static int hiredis_wait_readable(int fd, const struct timeval *timeout, int *isset) { struct timeval to; struct timeval *toptr = NULL; rb_fdset_t fds; /* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure * must be paired with a call to rb_fd_term to free it. */ rb_fd_init(&fds); rb_fd_set(fd, &fds); /* rb_thread_{fd_,}select modifies the passed timeval, so we pass a copy */ if (timeout != NULL && (timeout->tv_sec || timeout->tv_usec)) { memcpy(&to, timeout, sizeof(to)); toptr = &to; } if (rb_thread_fd_select(fd + 1, &fds, NULL, NULL, toptr) < 0) { rb_fd_term(&fds); return -1; } if (rb_fd_isset(fd, &fds) && isset) { *isset = 1; } rb_fd_term(&fds); return 0; } struct fd_select { rb_fdset_t *write_fds; struct timeval *timeout; int max; int return_value; }; static VALUE protected_writable_select(VALUE _args) { struct fd_select *args = (struct fd_select *)_args; args->return_value = rb_thread_fd_select(args->max, NULL, args->write_fds, NULL, args->timeout); return Qnil; } static int hiredis_wait_writable(int fd, const struct timeval *timeout, int *isset) { struct timeval to; struct timeval *toptr = NULL; /* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure * must be paired with a call to rb_fd_term to free it. */ rb_fdset_t fds; rb_fd_init(&fds); rb_fd_set(fd, &fds); /* rb_thread_{fd_,}select modifies the passed timeval, so we pass a copy */ if (timeout != NULL && (timeout->tv_sec || timeout->tv_usec)) { memcpy(&to, timeout, sizeof(to)); toptr = &to; } struct fd_select args = { .max = fd + 1, .write_fds = &fds, .timeout = toptr, }; int status; (void)rb_protect(protected_writable_select, (VALUE)&args, &status); // Error in case an exception arrives in rb_thread_fd_select() // (e.g. from Thread.raise) if (status || args.return_value < 0) { rb_fd_term(&fds); return -1; } if (rb_fd_isset(fd, &fds) && isset) { *isset = 1; } rb_fd_term(&fds); return 0; } static VALUE hiredis_connect_finish(hiredis_connection_t *connection, redisContext *context) { if (connection->context && connection->context != context) { redisFree(context); rb_raise(rb_eRuntimeError, "HiredisConnection is already connected, must be a bug"); } if (context->err) { redis_raise_error_and_disconnect(context, rb_eRedisClientCannotConnectError); } int writable = 0; int optval = 0; errno = 0; socklen_t optlen = sizeof(optval); /* Wait for socket to become writable */ if (hiredis_wait_writable(context->fd, &connection->connect_timeout, &writable) < 0) { redis_set_io_error(context, ETIMEDOUT); redis_raise_error_and_disconnect(context, rb_eRedisClientCannotConnectError); } if (!writable) { redis_set_io_error(context, ETIMEDOUT); redis_raise_error_and_disconnect(context, rb_eRedisClientCannotConnectError); } /* Check for socket error */ if (getsockopt(context->fd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0) { redis_set_io_error(context, 0); redis_raise_error_and_disconnect(context, rb_eRedisClientCannotConnectError); } if (optval) { redis_set_io_error(context, optval); redis_raise_error_and_disconnect(context, rb_eRedisClientCannotConnectError); } context->reader->fn = &reply_functions; redisSetPushCallback(context, NULL); connection->context = context; return Qtrue; } static void hiredis_init_ssl(hiredis_connection_t *connection, VALUE ssl_param) { SSL_CONTEXT(ssl_param, ssl_context) if (redisInitiateSSLWithContext(connection->context, ssl_context->context) != REDIS_OK) { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientCannotConnectError); } redisSSL *redis_ssl = redisGetSSLSocket(connection->context); if (redis_ssl->wantRead) { int readable = 0; if (hiredis_wait_readable(connection->context->fd, &connection->connect_timeout, &readable) < 0) { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientCannotConnectError); } if (!readable) { errno = EAGAIN; redis_set_error(connection->context, REDIS_ERR_IO, "SSL Connection Timeout"); hiredis_raise_error_and_disconnect(connection, rb_eRedisClientReadTimeoutError); } if (redisInitiateSSLContinue(connection->context) != REDIS_OK) { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientCannotConnectError); }; } } static VALUE hiredis_connect(VALUE self, VALUE path, VALUE host, VALUE port, VALUE ssl_param) { CONNECTION(self, connection); if (connection->context) { rb_raise(rb_eRuntimeError, "HiredisConnection is already connected, must be a bug"); } redisOptions options = { .connect_timeout = &connection->connect_timeout, .options = REDIS_OPT_NONBLOCK }; if (RTEST(path)) { REDIS_OPTIONS_SET_UNIX(&options, StringValuePtr(path)); } else { REDIS_OPTIONS_SET_TCP(&options, StringValuePtr(host), NUM2INT(port)); } redisContext *context = hiredis_connect_with_options_nogvl(&options); if (context && !RTEST(path)) { redisEnableKeepAlive(context); } VALUE success = hiredis_connect_finish(connection, context); if (RTEST(success) && !NIL_P(ssl_param)) { hiredis_init_ssl(connection, ssl_param); } return success; } static VALUE hiredis_reconnect(VALUE self, VALUE is_unix, VALUE ssl_param) { CONNECTION(self, connection); if (!connection->context) { return Qfalse; } // Clear context on the connection, since the nogvl call can raise an // exception after the redisReconnect() call succeeds and leave the // connection in a half-initialized state. redisContext *context = connection->context; connection->context = NULL; hiredis_reconnect_nogvl(context); VALUE success = hiredis_connect_finish(connection, context); if (RTEST(success)) { if (!RTEST(is_unix)) { redisEnableKeepAlive(connection->context); } if (RTEST(ssl_param)) { hiredis_init_ssl(connection, ssl_param); } } return success; } static VALUE hiredis_connected_p(VALUE self) { CONNECTION(self, connection); if (!connection->context) { return Qfalse; } if (connection->context->fd == REDIS_INVALID_FD) { return Qfalse; } return Qtrue; } static VALUE hiredis_write(VALUE self, VALUE command) { Check_Type(command, T_ARRAY); CONNECTION(self, connection); ENSURE_CONNECTED(connection); int size = (int)RARRAY_LEN(command); VALUE _argv_handle; char **argv = RB_ALLOCV_N(char *, _argv_handle, size); VALUE _argv_len_handle; size_t *argv_len = RB_ALLOCV_N(size_t, _argv_len_handle, size); for (int index = 0; index < size; index++) { VALUE arg = rb_ary_entry(command, index); Check_Type(arg, T_STRING); argv[index] = RSTRING_PTR(arg); argv_len[index] = RSTRING_LEN(arg); } redisAppendCommandArgv(connection->context, size, (const char **)argv, argv_len); return Qnil; } static VALUE hiredis_flush(VALUE self) { CONNECTION(self, connection); ENSURE_CONNECTED(connection); int wdone = 0; while (!wdone) { errno = 0; if (hiredis_buffer_write_nogvl(connection->context, &wdone) == REDIS_ERR) { if (errno == EAGAIN) { int writable = 0; if (hiredis_wait_writable(connection->context->fd, &connection->write_timeout, &writable) < 0) { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientWriteTimeoutError); } if (!writable) { errno = EAGAIN; hiredis_raise_error_and_disconnect(connection, rb_eRedisClientWriteTimeoutError); } } else { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientWriteTimeoutError); } } } return Qtrue; } #define HIREDIS_FATAL_CONNECTION_ERROR -1 #define HIREDIS_CLIENT_TIMEOUT -2 static int hiredis_read_internal(hiredis_connection_t *connection, VALUE *reply) { void *redis_reply = NULL; // This array being on the stack, the GC won't move nor collect it. // We use that to avoid having to have a `mark` function with write barriers. // Not that it would be too hard, but if we mark the response objects, we'll likely end up // promoting them to the old generation which isn't desirable. VALUE stack = rb_ary_new(); connection->reader_state.stack = stack; connection->reader_state.task_index = &connection->context->reader->ridx; connection->context->reader->privdata = &connection->reader_state; /* Try to read pending replies */ if (redisGetReplyFromReader(connection->context, &redis_reply) == REDIS_ERR) { return HIREDIS_FATAL_CONNECTION_ERROR; // Protocol error } /* Read until there is a full reply */ while (redis_reply == NULL) { errno = 0; if (hiredis_buffer_read_nogvl(connection->context) == REDIS_ERR) { return HIREDIS_FATAL_CONNECTION_ERROR; // Socket error } if (errno == EAGAIN) { int readable = 0; if (hiredis_wait_readable(connection->context->fd, &connection->read_timeout, &readable) < 0) { return HIREDIS_CLIENT_TIMEOUT; } if (!readable) { errno = EAGAIN; return HIREDIS_CLIENT_TIMEOUT; } /* Retry */ continue; } if (redisGetReplyFromReader(connection->context, &redis_reply) == REDIS_ERR) { return HIREDIS_FATAL_CONNECTION_ERROR; // Protocol error } } /* Set reply object */ if (reply != NULL) { *reply = rb_ary_entry(stack, 0); } RB_GC_GUARD(stack); return 0; } static VALUE hiredis_read(VALUE self) { CONNECTION(self, connection); ENSURE_CONNECTED(connection); VALUE reply = Qnil; switch (hiredis_read_internal(connection, &reply)) { case HIREDIS_FATAL_CONNECTION_ERROR: // The error is unrecoverable, we eagerly close the connection to ensure // it won't be re-used. hiredis_raise_error_and_disconnect(connection, rb_eRedisClientReadTimeoutError); break; case HIREDIS_CLIENT_TIMEOUT: // The timeout might have been expected (e.g. `PubSub#next_event`). // we let the caller decide if the connection should be closed. rb_raise(rb_eRedisClientReadTimeoutError, "Unknown Error (hiredis_read)"); break; } if (reply == Redis_Qfalse) { // See reply_create_bool reply = Qfalse; } RB_GC_GUARD(self); return reply; } static VALUE hiredis_close(VALUE self) { CONNECTION(self, connection); if (connection->context) { redisNetClose(connection->context); } return Qnil; } static inline double diff_timespec_ms(const struct timespec *time1, const struct timespec *time0) { return ((time1->tv_sec - time0->tv_sec) * 1000.0) + (time1->tv_nsec - time0->tv_nsec) / 1000000.0; } static inline int timeval_to_msec(struct timeval duration) { return (int)(duration.tv_sec * 1000 + duration.tv_usec / 1000); } typedef struct { hiredis_connection_t *connection; struct timespec start; struct timespec end; int return_value; } hiredis_measure_round_trip_delay_args_t; static const size_t pong_length = 7; static void *hiredis_measure_round_trip_delay_safe(void *_args) { hiredis_measure_round_trip_delay_args_t *args = _args; hiredis_connection_t *connection = args->connection; redisReader *reader = connection->context->reader; if (reader->len - reader->pos != 0) { args->return_value = REDIS_ERR; return NULL; } redisAppendFormattedCommand(connection->context, "PING\r\n", 6); clock_gettime(CLOCK_MONOTONIC, &args->start); int wdone = 0; do { if (redisBufferWrite(connection->context, &wdone) == REDIS_ERR) { args->return_value = REDIS_ERR; return NULL; } } while (!wdone); struct pollfd wfd[1]; wfd[0].fd = connection->context->fd; wfd[0].events = POLLIN; int retval = poll(wfd, 1, timeval_to_msec(connection->read_timeout)); if (retval == -1) { args->return_value = REDIS_ERR_IO; return NULL; } else if (!retval) { args->return_value = REDIS_ERR_IO; return NULL; } redisBufferRead(connection->context); if (reader->len - reader->pos != pong_length) { args->return_value = REDIS_ERR; return NULL; } if (strncmp(reader->buf + reader->pos, "+PONG\r\n", pong_length) != 0) { args->return_value = REDIS_ERR; return NULL; } reader->pos += pong_length; clock_gettime(CLOCK_MONOTONIC, &args->end); args->return_value = REDIS_OK; return NULL; } static VALUE hiredis_measure_round_trip_delay(VALUE self) { CONNECTION(self, connection); ENSURE_CONNECTED(connection); hiredis_measure_round_trip_delay_args_t args = { .connection = connection, }; rb_thread_call_without_gvl(hiredis_measure_round_trip_delay_safe, &args, RUBY_UBF_IO, 0); if (args.return_value != REDIS_OK) { hiredis_raise_error_and_disconnect(connection, rb_eRedisClientReadTimeoutError); return Qnil; // unreachable; } return DBL2NUM(diff_timespec_ms(&args.end, &args.start)); } RUBY_FUNC_EXPORTED void Init_hiredis_connection(void) { // Qfalse == NULL, so we can't return Qfalse in `reply_create_bool()` RUBY_ASSERT((void *)Qfalse == NULL); RUBY_ASSERT((void *)Qnil != NULL); redisInitOpenSSL(); id_parse = rb_intern("parse"); rb_global_variable(&Redis_Qfalse); Redis_Qfalse = rb_obj_alloc(rb_cObject); VALUE rb_cRedisClient = rb_const_get(rb_cObject, rb_intern("RedisClient")); rb_eRedisClientCommandError = rb_const_get(rb_cRedisClient, rb_intern("CommandError")); rb_global_variable(&rb_eRedisClientCommandError); rb_eRedisClientConnectionError = rb_const_get(rb_cRedisClient, rb_intern("ConnectionError")); rb_global_variable(&rb_eRedisClientConnectionError); rb_eRedisClientCannotConnectError = rb_const_get(rb_cRedisClient, rb_intern("CannotConnectError")); rb_global_variable(&rb_eRedisClientCannotConnectError); rb_eRedisClientProtocolError = rb_const_get(rb_cRedisClient, rb_intern("ProtocolError")); rb_global_variable(&rb_eRedisClientProtocolError); rb_eRedisClientReadTimeoutError = rb_const_get(rb_cRedisClient, rb_intern("ReadTimeoutError")); rb_global_variable(&rb_eRedisClientReadTimeoutError); rb_eRedisClientWriteTimeoutError = rb_const_get(rb_cRedisClient, rb_intern("WriteTimeoutError")); rb_global_variable(&rb_eRedisClientWriteTimeoutError); VALUE rb_cHiredisConnection = rb_define_class_under(rb_cRedisClient, "HiredisConnection", rb_cObject); rb_define_alloc_func(rb_cHiredisConnection, hiredis_alloc); rb_define_private_method(rb_cHiredisConnection, "connect_timeout_us=", hiredis_set_connect_timeout, 1); rb_define_private_method(rb_cHiredisConnection, "read_timeout_us=", hiredis_set_read_timeout, 1); rb_define_private_method(rb_cHiredisConnection, "write_timeout_us=", hiredis_set_write_timeout, 1); rb_define_private_method(rb_cHiredisConnection, "_connect", hiredis_connect, 4); rb_define_private_method(rb_cHiredisConnection, "_reconnect", hiredis_reconnect, 2); rb_define_method(rb_cHiredisConnection, "connected?", hiredis_connected_p, 0); rb_define_private_method(rb_cHiredisConnection, "_write", hiredis_write, 1); rb_define_private_method(rb_cHiredisConnection, "_read", hiredis_read, 0); rb_define_private_method(rb_cHiredisConnection, "flush", hiredis_flush, 0); rb_define_private_method(rb_cHiredisConnection, "_close", hiredis_close, 0); rb_define_method(rb_cHiredisConnection, "measure_round_trip_delay", hiredis_measure_round_trip_delay, 0); VALUE rb_cHiredisSSLContext = rb_define_class_under(rb_cHiredisConnection, "SSLContext", rb_cObject); rb_define_alloc_func(rb_cHiredisSSLContext, hiredis_ssl_context_alloc); rb_define_private_method(rb_cHiredisSSLContext, "init", hiredis_ssl_context_init, 5); } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/000077500000000000000000000000001515654015100272335ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/.gitignore000066400000000000000000000001241515654015100312200ustar00rootroot00000000000000/hiredis-test /examples/hiredis-example* /*.o /*.so /*.dylib /*.a /*.pc *.dSYM tags redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/.travis.yml000066400000000000000000000064701515654015100313530ustar00rootroot00000000000000language: c compiler: - gcc - clang os: - linux - osx dist: bionic branches: only: - staging - trying - master - /^release\/.*$/ install: - if [ "$BITS" == "64" ]; then wget https://github.com/redis/redis/archive/6.0.6.tar.gz; tar -xzvf 6.0.6.tar.gz; pushd redis-6.0.6 && BUILD_TLS=yes make && export PATH=$PWD/src:$PATH && popd; fi before_script: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then curl -O https://distfiles.macports.org/MacPorts/MacPorts-2.6.2-10.13-HighSierra.pkg; sudo installer -pkg MacPorts-2.6.2-10.13-HighSierra.pkg -target /; export PATH=$PATH:/opt/local/bin && sudo port -v selfupdate; sudo port -N install openssl redis; fi; addons: apt: sources: - sourceline: 'ppa:chris-lea/redis-server' packages: - libc6-dbg - libc6-dev - libc6:i386 - libc6-dev-i386 - libc6-dbg:i386 - gcc-multilib - g++-multilib - libssl-dev - libssl-dev:i386 - valgrind - redis env: - BITS="32" - BITS="64" script: - EXTRA_CMAKE_OPTS="-DENABLE_EXAMPLES:BOOL=ON -DENABLE_SSL:BOOL=ON"; if [ "$BITS" == "64" ]; then EXTRA_CMAKE_OPTS="$EXTRA_CMAKE_OPTS -DENABLE_SSL_TESTS:BOOL=ON"; fi; if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$BITS" == "32" ]; then CFLAGS="-m32 -Werror"; CXXFLAGS="-m32 -Werror"; LDFLAGS="-m32"; EXTRA_CMAKE_OPTS=; else CFLAGS="-Werror"; CXXFLAGS="-Werror"; fi; else TEST_PREFIX="valgrind --track-origins=yes --leak-check=full"; if [ "$BITS" == "32" ]; then CFLAGS="-m32 -Werror"; CXXFLAGS="-m32 -Werror"; LDFLAGS="-m32"; EXTRA_CMAKE_OPTS=; else CFLAGS="-Werror"; CXXFLAGS="-Werror"; fi; fi; export CFLAGS CXXFLAGS LDFLAGS TEST_PREFIX EXTRA_CMAKE_OPTS - make && make clean; if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$BITS" == "64" ]; then OPENSSL_PREFIX="$(ls -d /usr/local/Cellar/openssl@1.1/*)" USE_SSL=1 make; fi; else USE_SSL=1 make; fi; - mkdir build/ && cd build/ - cmake .. ${EXTRA_CMAKE_OPTS} - make VERBOSE=1 - if [ "$BITS" == "64" ]; then TEST_SSL=1 SKIPS_AS_FAILS=1 ctest -V; else SKIPS_AS_FAILS=1 ctest -V; fi; jobs: include: # Windows MinGW cross compile on Linux - os: linux dist: xenial compiler: mingw addons: apt: packages: - ninja-build - gcc-mingw-w64-x86-64 - g++-mingw-w64-x86-64 script: - mkdir build && cd build - CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_BUILD_WITH_INSTALL_RPATH=on - ninja -v # Windows MSVC 2017 - os: windows compiler: msvc env: - MATRIX_EVAL="CC=cl.exe && CXX=cl.exe" before_install: - eval "${MATRIX_EVAL}" install: - choco install ninja - choco install -y memurai-developer script: - mkdir build && cd build - cmd.exe //C 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvarsall.bat' amd64 '&&' cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_EXAMPLES=ON '&&' ninja -v - ./hiredis-test.exe redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/CHANGELOG.md000066400000000000000000000577331515654015100310630ustar00rootroot00000000000000## [1.0.2](https://github.com/redis/hiredis/tree/v1.0.2) - (2021-10-07) Announcing Hiredis v1.0.2, which fixes CVE-2021-32765 but returns the SONAME to the correct value of `1.0.0`. - [Revert SONAME bump](https://github.com/redis/hiredis/commit/d4e6f109a064690cde64765c654e679fea1d3548) ([Michael Grunder](https://github.com/michael-grunder)) ## [1.0.1](https://github.com/redis/hiredis/tree/v1.0.1) - (2021-10-04) This release erroneously bumped the SONAME, please use [1.0.2](https://github.com/redis/hiredis/tree/v1.0.2) Announcing Hiredis v1.0.1, a security release fixing CVE-2021-32765 - Fix for [CVE-2021-32765](https://github.com/redis/hiredis/security/advisories/GHSA-hfm9-39pp-55p2) [commit](https://github.com/redis/hiredis/commit/76a7b10005c70babee357a7d0f2becf28ec7ed1e) ([Yossi Gottlieb](https://github.com/yossigo)) _Thanks to [Yossi Gottlieb](https://github.com/yossigo) for the security fix and to [Microsoft Security Vulnerability Research](https://www.microsoft.com/en-us/msrc/msvr) for finding the bug._ :sparkling_heart: ## [1.0.0](https://github.com/redis/hiredis/tree/v1.0.0) - (2020-08-03) Announcing Hiredis v1.0.0, which adds support for RESP3, SSL connections, allocator injection, and better Windows support! :tada: _A big thanks to everyone who helped with this release. The following list includes everyone who contributed at least five lines, sorted by lines contributed._ :sparkling_heart: [Michael Grunder](https://github.com/michael-grunder), [Yossi Gottlieb](https://github.com/yossigo), [Mark Nunberg](https://github.com/mnunberg), [Marcus Geelnard](https://github.com/mbitsnbites), [Justin Brewer](https://github.com/justinbrewer), [Valentino Geron](https://github.com/valentinogeron), [Minun Dragonation](https://github.com/dragonation), [Omri Steiner](https://github.com/OmriSteiner), [Sangmoon Yi](https://github.com/jman-krafton), [Jinjiazh](https://github.com/jinjiazhang), [Odin Hultgren Van Der Horst](https://github.com/Miniwoffer), [Muhammad Zahalqa](https://github.com/tryfinally), [Nick Rivera](https://github.com/heronr), [Qi Yang](https://github.com/movebean), [kevin1018](https://github.com/kevin1018) [Full Changelog](https://github.com/redis/hiredis/compare/v0.14.1...v1.0.0) **BREAKING CHANGES**: * `redisOptions` now has two timeout fields. One for connecting, and one for commands. If you're presently using `options->timeout` you will need to change it to use `options->connect_timeout`. (See [example](https://github.com/redis/hiredis/commit/38b5ae543f5c99eb4ccabbe277770fc6bc81226f#diff-86ba39d37aa829c8c82624cce4f049fbL36)) * Bulk and multi-bulk lengths less than -1 or greater than `LLONG_MAX` are now protocol errors. This is consistent with the RESP specification. On 32-bit platforms, the upper bound is lowered to `SIZE_MAX`. * `redisReplyObjectFunctions.createArray` now takes `size_t` for its length parameter. **New features:** - Support for RESP3 [\#697](https://github.com/redis/hiredis/pull/697), [\#805](https://github.com/redis/hiredis/pull/805), [\#819](https://github.com/redis/hiredis/pull/819), [\#841](https://github.com/redis/hiredis/pull/841) ([Yossi Gottlieb](https://github.com/yossigo), [Michael Grunder](https://github.com/michael-grunder)) - Support for SSL connections [\#645](https://github.com/redis/hiredis/pull/645), [\#699](https://github.com/redis/hiredis/pull/699), [\#702](https://github.com/redis/hiredis/pull/702), [\#708](https://github.com/redis/hiredis/pull/708), [\#711](https://github.com/redis/hiredis/pull/711), [\#821](https://github.com/redis/hiredis/pull/821), [more](https://github.com/redis/hiredis/pulls?q=is%3Apr+is%3Amerged+SSL) ([Mark Nunberg](https://github.com/mnunberg), [Yossi Gottlieb](https://github.com/yossigo)) - Run-time allocator injection [\#800](https://github.com/redis/hiredis/pull/800) ([Michael Grunder](https://github.com/michael-grunder)) - Improved Windows support (including MinGW and Windows CI) [\#652](https://github.com/redis/hiredis/pull/652), [\#663](https://github.com/redis/hiredis/pull/663) ([Marcus Geelnard](https://www.bitsnbites.eu/author/m/)) - Adds support for distinct connect and command timeouts [\#839](https://github.com/redis/hiredis/pull/839), [\#829](https://github.com/redis/hiredis/pull/829) ([Valentino Geron](https://github.com/valentinogeron)) - Add generic pointer and destructor to `redisContext` that users can use for context. [\#855](https://github.com/redis/hiredis/pull/855) ([Michael Grunder](https://github.com/michael-grunder)) **Closed issues (that involved code changes):** - Makefile does not install TLS libraries [\#809](https://github.com/redis/hiredis/issues/809) - redisConnectWithOptions should not set command timeout [\#722](https://github.com/redis/hiredis/issues/722), [\#829](https://github.com/redis/hiredis/pull/829) ([valentinogeron](https://github.com/valentinogeron)) - Fix integer overflow in `sdsrange` [\#827](https://github.com/redis/hiredis/issues/827) - INFO & CLUSTER commands failed when using RESP3 [\#802](https://github.com/redis/hiredis/issues/802) - Windows compatibility patches [\#687](https://github.com/redis/hiredis/issues/687), [\#838](https://github.com/redis/hiredis/issues/838), [\#842](https://github.com/redis/hiredis/issues/842) - RESP3 PUSH messages incorrectly use pending callback [\#825](https://github.com/redis/hiredis/issues/825) - Asynchronous PSUBSCRIBE command fails when using RESP3 [\#815](https://github.com/redis/hiredis/issues/815) - New SSL API [\#804](https://github.com/redis/hiredis/issues/804), [\#813](https://github.com/redis/hiredis/issues/813) - Hard-coded limit of nested reply depth [\#794](https://github.com/redis/hiredis/issues/794) - Fix TCP_NODELAY in Windows/OSX [\#679](https://github.com/redis/hiredis/issues/679), [\#690](https://github.com/redis/hiredis/issues/690), [\#779](https://github.com/redis/hiredis/issues/779), [\#785](https://github.com/redis/hiredis/issues/785), - Added timers to libev adapter. [\#778](https://github.com/redis/hiredis/issues/778), [\#795](https://github.com/redis/hiredis/pull/795) - Initialization discards const qualifier [\#777](https://github.com/redis/hiredis/issues/777) - \[BUG\]\[MinGW64\] Error setting socket timeout [\#775](https://github.com/redis/hiredis/issues/775) - undefined reference to hi_malloc [\#769](https://github.com/redis/hiredis/issues/769) - hiredis pkg-config file incorrectly ignores multiarch libdir spec'n [\#767](https://github.com/redis/hiredis/issues/767) - Don't use -G to build shared object on Solaris [\#757](https://github.com/redis/hiredis/issues/757) - error when make USE\_SSL=1 [\#748](https://github.com/redis/hiredis/issues/748) - Allow to change SSL Mode [\#646](https://github.com/redis/hiredis/issues/646) - hiredis/adapters/libevent.h memleak [\#618](https://github.com/redis/hiredis/issues/618) - redisLibuvPoll crash when server closes the connetion [\#545](https://github.com/redis/hiredis/issues/545) - about redisAsyncDisconnect question [\#518](https://github.com/redis/hiredis/issues/518) - hiredis adapters libuv error for help [\#508](https://github.com/redis/hiredis/issues/508) - API/ABI changes analysis [\#506](https://github.com/redis/hiredis/issues/506) - Memory leak patch in Redis [\#502](https://github.com/redis/hiredis/issues/502) - Remove the depth limitation [\#421](https://github.com/redis/hiredis/issues/421) **Merged pull requests:** - Move SSL management to a distinct private pointer [\#855](https://github.com/redis/hiredis/pull/855) ([michael-grunder](https://github.com/michael-grunder)) - Move include to sockcompat.h to maintain style [\#850](https://github.com/redis/hiredis/pull/850) ([michael-grunder](https://github.com/michael-grunder)) - Remove erroneous tag and add license to push example [\#849](https://github.com/redis/hiredis/pull/849) ([michael-grunder](https://github.com/michael-grunder)) - fix windows compiling with mingw [\#848](https://github.com/redis/hiredis/pull/848) ([rmalizia44](https://github.com/rmalizia44)) - Some Windows quality of life improvements. [\#846](https://github.com/redis/hiredis/pull/846) ([michael-grunder](https://github.com/michael-grunder)) - Use \_WIN32 define instead of WIN32 [\#845](https://github.com/redis/hiredis/pull/845) ([michael-grunder](https://github.com/michael-grunder)) - Non Linux CI fixes [\#844](https://github.com/redis/hiredis/pull/844) ([michael-grunder](https://github.com/michael-grunder)) - Resp3 oob push support [\#841](https://github.com/redis/hiredis/pull/841) ([michael-grunder](https://github.com/michael-grunder)) - fix \#785: defer TCP\_NODELAY in async tcp connections [\#836](https://github.com/redis/hiredis/pull/836) ([OmriSteiner](https://github.com/OmriSteiner)) - sdsrange overflow fix [\#830](https://github.com/redis/hiredis/pull/830) ([michael-grunder](https://github.com/michael-grunder)) - Use explicit pointer casting for c++ compatibility [\#826](https://github.com/redis/hiredis/pull/826) ([aureus1](https://github.com/aureus1)) - Document allocator injection and completeness fix in test.c [\#824](https://github.com/redis/hiredis/pull/824) ([michael-grunder](https://github.com/michael-grunder)) - Use unique names for allocator struct members [\#823](https://github.com/redis/hiredis/pull/823) ([michael-grunder](https://github.com/michael-grunder)) - New SSL API to replace redisSecureConnection\(\). [\#821](https://github.com/redis/hiredis/pull/821) ([yossigo](https://github.com/yossigo)) - Add logic to handle RESP3 push messages [\#819](https://github.com/redis/hiredis/pull/819) ([michael-grunder](https://github.com/michael-grunder)) - Use standrad isxdigit instead of custom helper function. [\#814](https://github.com/redis/hiredis/pull/814) ([tryfinally](https://github.com/tryfinally)) - Fix missing SSL build/install options. [\#812](https://github.com/redis/hiredis/pull/812) ([yossigo](https://github.com/yossigo)) - Add link to ABI tracker [\#808](https://github.com/redis/hiredis/pull/808) ([michael-grunder](https://github.com/michael-grunder)) - Resp3 verbatim string support [\#805](https://github.com/redis/hiredis/pull/805) ([michael-grunder](https://github.com/michael-grunder)) - Allow users to replace allocator and handle OOM everywhere. [\#800](https://github.com/redis/hiredis/pull/800) ([michael-grunder](https://github.com/michael-grunder)) - Remove nested depth limitation. [\#797](https://github.com/redis/hiredis/pull/797) ([michael-grunder](https://github.com/michael-grunder)) - Attempt to fix compilation on Solaris [\#796](https://github.com/redis/hiredis/pull/796) ([michael-grunder](https://github.com/michael-grunder)) - Support timeouts in libev adapater [\#795](https://github.com/redis/hiredis/pull/795) ([michael-grunder](https://github.com/michael-grunder)) - Fix pkgconfig when installing to a custom lib dir [\#793](https://github.com/redis/hiredis/pull/793) ([michael-grunder](https://github.com/michael-grunder)) - Fix USE\_SSL=1 make/cmake on OSX and CMake tests [\#789](https://github.com/redis/hiredis/pull/789) ([michael-grunder](https://github.com/michael-grunder)) - Use correct libuv call on Windows [\#784](https://github.com/redis/hiredis/pull/784) ([michael-grunder](https://github.com/michael-grunder)) - Added CMake package config and fixed hiredis\_ssl on Windows [\#783](https://github.com/redis/hiredis/pull/783) ([michael-grunder](https://github.com/michael-grunder)) - CMake: Set hiredis\_ssl shared object version. [\#780](https://github.com/redis/hiredis/pull/780) ([yossigo](https://github.com/yossigo)) - Win32 tests and timeout fix [\#776](https://github.com/redis/hiredis/pull/776) ([michael-grunder](https://github.com/michael-grunder)) - Provides an optional cleanup callback for async data. [\#768](https://github.com/redis/hiredis/pull/768) ([heronr](https://github.com/heronr)) - Housekeeping fixes [\#764](https://github.com/redis/hiredis/pull/764) ([michael-grunder](https://github.com/michael-grunder)) - install alloc.h [\#756](https://github.com/redis/hiredis/pull/756) ([ch1aki](https://github.com/ch1aki)) - fix spelling mistakes [\#746](https://github.com/redis/hiredis/pull/746) ([ShooterIT](https://github.com/ShooterIT)) - Free the reply in redisGetReply when passed NULL [\#741](https://github.com/redis/hiredis/pull/741) ([michael-grunder](https://github.com/michael-grunder)) - Fix dead code in sslLogCallback relating to should\_log variable. [\#737](https://github.com/redis/hiredis/pull/737) ([natoscott](https://github.com/natoscott)) - Fix typo in dict.c. [\#731](https://github.com/redis/hiredis/pull/731) ([Kevin-Xi](https://github.com/Kevin-Xi)) - Adding an option to DISABLE\_TESTS [\#727](https://github.com/redis/hiredis/pull/727) ([pbotros](https://github.com/pbotros)) - Update README with SSL support. [\#720](https://github.com/redis/hiredis/pull/720) ([yossigo](https://github.com/yossigo)) - Fixes leaks in unit tests [\#715](https://github.com/redis/hiredis/pull/715) ([michael-grunder](https://github.com/michael-grunder)) - SSL Tests [\#711](https://github.com/redis/hiredis/pull/711) ([yossigo](https://github.com/yossigo)) - SSL Reorganization [\#708](https://github.com/redis/hiredis/pull/708) ([yossigo](https://github.com/yossigo)) - Fix MSVC build. [\#706](https://github.com/redis/hiredis/pull/706) ([yossigo](https://github.com/yossigo)) - SSL: Properly report SSL\_connect\(\) errors. [\#702](https://github.com/redis/hiredis/pull/702) ([yossigo](https://github.com/yossigo)) - Silent SSL trace to stdout by default. [\#699](https://github.com/redis/hiredis/pull/699) ([yossigo](https://github.com/yossigo)) - Port RESP3 support from Redis. [\#697](https://github.com/redis/hiredis/pull/697) ([yossigo](https://github.com/yossigo)) - Removed whitespace before newline [\#691](https://github.com/redis/hiredis/pull/691) ([Miniwoffer](https://github.com/Miniwoffer)) - Add install adapters header files [\#688](https://github.com/redis/hiredis/pull/688) ([kevin1018](https://github.com/kevin1018)) - Remove unnecessary null check before free [\#684](https://github.com/redis/hiredis/pull/684) ([qlyoung](https://github.com/qlyoung)) - redisReaderGetReply leak memory [\#671](https://github.com/redis/hiredis/pull/671) ([movebean](https://github.com/movebean)) - fix timeout code in windows [\#670](https://github.com/redis/hiredis/pull/670) ([jman-krafton](https://github.com/jman-krafton)) - test: fix errstr matching for musl libc [\#665](https://github.com/redis/hiredis/pull/665) ([ghost](https://github.com/ghost)) - Windows: MinGW fixes and Windows Travis builders [\#663](https://github.com/redis/hiredis/pull/663) ([mbitsnbites](https://github.com/mbitsnbites)) - The setsockopt and getsockopt API diffs from BSD socket and WSA one [\#662](https://github.com/redis/hiredis/pull/662) ([dragonation](https://github.com/dragonation)) - Fix Compile Error On Windows \(Visual Studio\) [\#658](https://github.com/redis/hiredis/pull/658) ([jinjiazhang](https://github.com/jinjiazhang)) - Fix NXDOMAIN test case [\#653](https://github.com/redis/hiredis/pull/653) ([michael-grunder](https://github.com/michael-grunder)) - Add MinGW support [\#652](https://github.com/redis/hiredis/pull/652) ([mbitsnbites](https://github.com/mbitsnbites)) - SSL Support [\#645](https://github.com/redis/hiredis/pull/645) ([mnunberg](https://github.com/mnunberg)) - Fix Invalid argument after redisAsyncConnectUnix [\#644](https://github.com/redis/hiredis/pull/644) ([codehz](https://github.com/codehz)) - Makefile: use predefined AR [\#632](https://github.com/redis/hiredis/pull/632) ([Mic92](https://github.com/Mic92)) - FreeBSD build fix [\#628](https://github.com/redis/hiredis/pull/628) ([devnexen](https://github.com/devnexen)) - Fix errors not propagating properly with libuv.h. [\#624](https://github.com/redis/hiredis/pull/624) ([yossigo](https://github.com/yossigo)) - Update README.md [\#621](https://github.com/redis/hiredis/pull/621) ([Crunsher](https://github.com/Crunsher)) - Fix redisBufferRead documentation [\#620](https://github.com/redis/hiredis/pull/620) ([hacst](https://github.com/hacst)) - Add CPPFLAGS to REAL\_CFLAGS [\#614](https://github.com/redis/hiredis/pull/614) ([thomaslee](https://github.com/thomaslee)) - Update createArray to take size\_t [\#597](https://github.com/redis/hiredis/pull/597) ([justinbrewer](https://github.com/justinbrewer)) - fix common realloc mistake and add null check more [\#580](https://github.com/redis/hiredis/pull/580) ([charsyam](https://github.com/charsyam)) - Proper error reporting for connect failures [\#578](https://github.com/redis/hiredis/pull/578) ([mnunberg](https://github.com/mnunberg)) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* ## [1.0.0-rc1](https://github.com/redis/hiredis/tree/v1.0.0-rc1) - (2020-07-29) _Note: There were no changes to code between v1.0.0-rc1 and v1.0.0 so see v1.0.0 for changelog_ ### 0.14.1 (2020-03-13) * Adds safe allocation wrappers (CVE-2020-7105, #747, #752) (Michael Grunder) ### 0.14.0 (2018-09-25) **BREAKING CHANGES**: * Change `redisReply.len` to `size_t`, as it denotes the the size of a string User code should compare this to `size_t` values as well. If it was used to compare to other values, casting might be necessary or can be removed, if casting was applied before. * Make string2ll static to fix conflict with Redis (Tom Lee [c3188b]) * Use -dynamiclib instead of -shared for OSX (Ryan Schmidt [a65537]) * Use string2ll from Redis w/added tests (Michael Grunder [7bef04, 60f622]) * Makefile - OSX compilation fixes (Ryan Schmidt [881fcb, 0e9af8]) * Remove redundant NULL checks (Justin Brewer [54acc8, 58e6b8]) * Fix bulk and multi-bulk length truncation (Justin Brewer [109197]) * Fix SIGSEGV in OpenBSD by checking for NULL before calling freeaddrinfo (Justin Brewer [546d94]) * Several POSIX compatibility fixes (Justin Brewer [bbeab8, 49bbaa, d1c1b6]) * Makefile - Compatibility fixes (Dimitri Vorobiev [3238cf, 12a9d1]) * Makefile - Fix make install on FreeBSD (Zach Shipko [a2ef2b]) * Makefile - don't assume $(INSTALL) is cp (Igor Gnatenko [725a96]) * Separate side-effect causing function from assert and small cleanup (amallia [b46413, 3c3234]) * Don't send negative values to `__redisAsyncCommand` (Frederik Deweerdt [706129]) * Fix leak if setsockopt fails (Frederik Deweerdt [e21c9c]) * Fix libevent leak (zfz [515228]) * Clean up GCC warning (Ichito Nagata [2ec774]) * Keep track of errno in `__redisSetErrorFromErrno()` as snprintf may use it (Jin Qing [25cd88]) * Solaris compilation fix (Donald Whyte [41b07d]) * Reorder linker arguments when building examples (Tustfarm-heart [06eedd]) * Keep track of subscriptions in case of rapid subscribe/unsubscribe (Hyungjin Kim [073dc8, be76c5, d46999]) * libuv use after free fix (Paul Scott [cbb956]) * Properly close socket fd on reconnect attempt (WSL [64d1ec]) * Skip valgrind in OSX tests (Jan-Erik Rediger [9deb78]) * Various updates for Travis testing OSX (Ted Nyman [fa3774, 16a459, bc0ea5]) * Update libevent (Chris Xin [386802]) * Change sds.h for building in C++ projects (Ali Volkan ATLI [f5b32e]) * Use proper format specifier in redisFormatSdsCommandArgv (Paulino Huerta, Jan-Erik Rediger [360a06, 8655a6]) * Better handling of NULL reply in example code (Jan-Erik Rediger [1b8ed3]) * Prevent overflow when formatting an error (Jan-Erik Rediger [0335cb]) * Compatibility fix for strerror_r (Tom Lee [bb1747]) * Properly detect integer parse/overflow errors (Justin Brewer [93421f]) * Adds CI for Windows and cygwin fixes (owent, [6c53d6, 6c3e40]) * Catch a buffer overflow when formatting the error message * Import latest upstream sds. This breaks applications that are linked against the old hiredis v0.13 * Fix warnings, when compiled with -Wshadow * Make hiredis compile in Cygwin on Windows, now CI-tested * Bulk and multi-bulk lengths less than -1 or greater than `LLONG_MAX` are now protocol errors. This is consistent with the RESP specification. On 32-bit platforms, the upper bound is lowered to `SIZE_MAX`. * Remove backwards compatibility macro's This removes the following old function aliases, use the new name now: | Old | New | | --------------------------- | ---------------------- | | redisReplyReaderCreate | redisReaderCreate | | redisReplyReaderCreate | redisReaderCreate | | redisReplyReaderFree | redisReaderFree | | redisReplyReaderFeed | redisReaderFeed | | redisReplyReaderGetReply | redisReaderGetReply | | redisReplyReaderSetPrivdata | redisReaderSetPrivdata | | redisReplyReaderGetObject | redisReaderGetObject | | redisReplyReaderGetError | redisReaderGetError | * The `DEBUG` variable in the Makefile was renamed to `DEBUG_FLAGS` Previously it broke some builds for people that had `DEBUG` set to some arbitrary value, due to debugging other software. By renaming we avoid unintentional name clashes. Simply rename `DEBUG` to `DEBUG_FLAGS` in your environment to make it working again. ### 0.13.3 (2015-09-16) * Revert "Clear `REDIS_CONNECTED` flag when connection is closed". * Make tests pass on FreeBSD (Thanks, Giacomo Olgeni) If the `REDIS_CONNECTED` flag is cleared, the async onDisconnect callback function will never be called. This causes problems as the disconnect is never reported back to the user. ### 0.13.2 (2015-08-25) * Prevent crash on pending replies in async code (Thanks, @switch-st) * Clear `REDIS_CONNECTED` flag when connection is closed (Thanks, Jerry Jacobs) * Add MacOS X addapter (Thanks, @dizzus) * Add Qt adapter (Thanks, Pietro Cerutti) * Add Ivykis adapter (Thanks, Gergely Nagy) All adapters are provided as is and are only tested where possible. ### 0.13.1 (2015-05-03) This is a bug fix release. The new `reconnect` method introduced new struct members, which clashed with pre-defined names in pre-C99 code. Another commit forced C99 compilation just to make it work, but of course this is not desirable for outside projects. Other non-C99 code can now use hiredis as usual again. Sorry for the inconvenience. * Fix memory leak in async reply handling (Salvatore Sanfilippo) * Rename struct member to avoid name clash with pre-c99 code (Alex Balashov, ncopa) ### 0.13.0 (2015-04-16) This release adds a minimal Windows compatibility layer. The parser, standalone since v0.12.0, can now be compiled on Windows (and thus used in other client libraries as well) * Windows compatibility layer for parser code (tzickel) * Properly escape data printed to PKGCONF file (Dan Skorupski) * Fix tests when assert() undefined (Keith Bennett, Matt Stancliff) * Implement a reconnect method for the client context, this changes the structure of `redisContext` (Aaron Bedra) ### 0.12.1 (2015-01-26) * Fix `make install`: DESTDIR support, install all required files, install PKGCONF in proper location * Fix `make test` as 32 bit build on 64 bit platform ### 0.12.0 (2015-01-22) * Add optional KeepAlive support * Try again on EINTR errors * Add libuv adapter * Add IPv6 support * Remove possibility of multiple close on same fd * Add ability to bind source address on connect * Add redisConnectFd() and redisFreeKeepFd() * Fix getaddrinfo() memory leak * Free string if it is unused (fixes memory leak) * Improve redisAppendCommandArgv performance 2.5x * Add support for SO_REUSEADDR * Fix redisvFormatCommand format parsing * Add GLib 2.0 adapter * Refactor reading code into read.c * Fix errno error buffers to not clobber errors * Generate pkgconf during build * Silence _BSD_SOURCE warnings * Improve digit counting for multibulk creation ### 0.11.0 * Increase the maximum multi-bulk reply depth to 7. * Increase the read buffer size from 2k to 16k. * Use poll(2) instead of select(2) to support large fds (>= 1024). ### 0.10.1 * Makefile overhaul. Important to check out if you override one or more variables using environment variables or via arguments to the "make" tool. * Issue #45: Fix potential memory leak for a multi bulk reply with 0 elements being created by the default reply object functions. * Issue #43: Don't crash in an asynchronous context when Redis returns an error reply after the connection has been made (this happens when the maximum number of connections is reached). ### 0.10.0 * See commit log. redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/CMakeLists.txt000066400000000000000000000121711515654015100317750ustar00rootroot00000000000000CMAKE_MINIMUM_REQUIRED(VERSION 3.4.0) INCLUDE(GNUInstallDirs) PROJECT(hiredis) OPTION(ENABLE_SSL "Build hiredis_ssl for SSL support" OFF) OPTION(DISABLE_TESTS "If tests should be compiled or not" OFF) OPTION(ENABLE_SSL_TESTS, "Should we test SSL connections" OFF) MACRO(getVersionBit name) SET(VERSION_REGEX "^#define ${name} (.+)$") FILE(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/hiredis.h" VERSION_BIT REGEX ${VERSION_REGEX}) STRING(REGEX REPLACE ${VERSION_REGEX} "\\1" ${name} "${VERSION_BIT}") ENDMACRO(getVersionBit) getVersionBit(HIREDIS_MAJOR) getVersionBit(HIREDIS_MINOR) getVersionBit(HIREDIS_PATCH) getVersionBit(HIREDIS_SONAME) SET(VERSION "${HIREDIS_MAJOR}.${HIREDIS_MINOR}.${HIREDIS_PATCH}") MESSAGE("Detected version: ${VERSION}") PROJECT(hiredis VERSION "${VERSION}") SET(ENABLE_EXAMPLES OFF CACHE BOOL "Enable building hiredis examples") SET(hiredis_sources alloc.c async.c dict.c hiredis.c net.c read.c sds.c sockcompat.c) SET(hiredis_sources ${hiredis_sources}) IF(WIN32) ADD_COMPILE_DEFINITIONS(_CRT_SECURE_NO_WARNINGS WIN32_LEAN_AND_MEAN) ENDIF() ADD_LIBRARY(hiredis SHARED ${hiredis_sources}) SET_TARGET_PROPERTIES(hiredis PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE VERSION "${HIREDIS_SONAME}") IF(WIN32 OR MINGW) TARGET_LINK_LIBRARIES(hiredis PRIVATE ws2_32) ENDIF() TARGET_INCLUDE_DIRECTORIES(hiredis PUBLIC $ $) CONFIGURE_FILE(hiredis.pc.in hiredis.pc @ONLY) INSTALL(TARGETS hiredis EXPORT hiredis-targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) INSTALL(FILES hiredis.h read.h sds.h async.h alloc.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) INSTALL(DIRECTORY adapters DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) export(EXPORT hiredis-targets FILE "${CMAKE_CURRENT_BINARY_DIR}/hiredis-targets.cmake" NAMESPACE hiredis::) SET(CMAKE_CONF_INSTALL_DIR share/hiredis) SET(INCLUDE_INSTALL_DIR include) include(CMakePackageConfigHelpers) configure_package_config_file(hiredis-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/hiredis-config.cmake INSTALL_DESTINATION ${CMAKE_CONF_INSTALL_DIR} PATH_VARS INCLUDE_INSTALL_DIR) INSTALL(EXPORT hiredis-targets FILE hiredis-targets.cmake NAMESPACE hiredis:: DESTINATION ${CMAKE_CONF_INSTALL_DIR}) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis-config.cmake DESTINATION ${CMAKE_CONF_INSTALL_DIR}) IF(ENABLE_SSL) IF (NOT OPENSSL_ROOT_DIR) IF (APPLE) SET(OPENSSL_ROOT_DIR "/usr/local/opt/openssl") ENDIF() ENDIF() FIND_PACKAGE(OpenSSL REQUIRED) SET(hiredis_ssl_sources ssl.c) ADD_LIBRARY(hiredis_ssl SHARED ${hiredis_ssl_sources}) IF (APPLE) SET_PROPERTY(TARGET hiredis_ssl PROPERTY LINK_FLAGS "-Wl,-undefined -Wl,dynamic_lookup") ENDIF() SET_TARGET_PROPERTIES(hiredis_ssl PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE VERSION "${HIREDIS_SONAME}") TARGET_INCLUDE_DIRECTORIES(hiredis_ssl PRIVATE "${OPENSSL_INCLUDE_DIR}") TARGET_LINK_LIBRARIES(hiredis_ssl PRIVATE ${OPENSSL_LIBRARIES}) IF (WIN32 OR MINGW) TARGET_LINK_LIBRARIES(hiredis_ssl PRIVATE hiredis) ENDIF() CONFIGURE_FILE(hiredis_ssl.pc.in hiredis_ssl.pc @ONLY) INSTALL(TARGETS hiredis_ssl EXPORT hiredis_ssl-targets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) INSTALL(FILES hiredis_ssl.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_ssl.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) export(EXPORT hiredis_ssl-targets FILE "${CMAKE_CURRENT_BINARY_DIR}/hiredis_ssl-targets.cmake" NAMESPACE hiredis::) SET(CMAKE_CONF_INSTALL_DIR share/hiredis_ssl) configure_package_config_file(hiredis_ssl-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/hiredis_ssl-config.cmake INSTALL_DESTINATION ${CMAKE_CONF_INSTALL_DIR} PATH_VARS INCLUDE_INSTALL_DIR) INSTALL(EXPORT hiredis_ssl-targets FILE hiredis_ssl-targets.cmake NAMESPACE hiredis:: DESTINATION ${CMAKE_CONF_INSTALL_DIR}) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_ssl-config.cmake DESTINATION ${CMAKE_CONF_INSTALL_DIR}) ENDIF() IF(NOT DISABLE_TESTS) ENABLE_TESTING() ADD_EXECUTABLE(hiredis-test test.c) IF(ENABLE_SSL_TESTS) ADD_DEFINITIONS(-DHIREDIS_TEST_SSL=1) TARGET_LINK_LIBRARIES(hiredis-test hiredis hiredis_ssl) ELSE() TARGET_LINK_LIBRARIES(hiredis-test hiredis) ENDIF() ADD_TEST(NAME hiredis-test COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test.sh) ENDIF() # Add examples IF(ENABLE_EXAMPLES) ADD_SUBDIRECTORY(examples) ENDIF(ENABLE_EXAMPLES) redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/COPYING000066400000000000000000000030641515654015100302710ustar00rootroot00000000000000Copyright (c) 2009-2011, Salvatore Sanfilippo Copyright (c) 2010-2011, Pieter Noordhuis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Redis nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/Makefile000066400000000000000000000265241515654015100307040ustar00rootroot00000000000000# Hiredis Makefile # Copyright (C) 2010-2011 Salvatore Sanfilippo # Copyright (C) 2010-2011 Pieter Noordhuis # This file is released under the BSD license, see the COPYING file OBJ=alloc.o net.o hiredis.o sds.o async.o read.o sockcompat.o SSL_OBJ=ssl.o EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev hiredis-example-glib hiredis-example-push ifeq ($(USE_SSL),1) EXAMPLES+=hiredis-example-ssl hiredis-example-libevent-ssl endif TESTS=hiredis-test LIBNAME=libhiredis PKGCONFNAME=hiredis.pc SSL_LIBNAME=libhiredis_ssl SSL_PKGCONFNAME=hiredis_ssl.pc HIREDIS_MAJOR=$(shell grep HIREDIS_MAJOR hiredis.h | awk '{print $$3}') HIREDIS_MINOR=$(shell grep HIREDIS_MINOR hiredis.h | awk '{print $$3}') HIREDIS_PATCH=$(shell grep HIREDIS_PATCH hiredis.h | awk '{print $$3}') HIREDIS_SONAME=$(shell grep HIREDIS_SONAME hiredis.h | awk '{print $$3}') # Installation related variables and target PREFIX?=/usr/local INCLUDE_PATH?=include/hiredis LIBRARY_PATH?=lib PKGCONF_PATH?=pkgconfig INSTALL_INCLUDE_PATH= $(DESTDIR)$(PREFIX)/$(INCLUDE_PATH) INSTALL_LIBRARY_PATH= $(DESTDIR)$(PREFIX)/$(LIBRARY_PATH) INSTALL_PKGCONF_PATH= $(INSTALL_LIBRARY_PATH)/$(PKGCONF_PATH) # redis-server configuration used for testing REDIS_PORT=56379 REDIS_SERVER=redis-server define REDIS_TEST_CONFIG daemonize yes pidfile /tmp/hiredis-test-redis.pid port $(REDIS_PORT) bind 127.0.0.1 unixsocket /tmp/hiredis-test-redis.sock endef export REDIS_TEST_CONFIG # Fallback to gcc when $CC is not in $PATH. CC:=$(shell sh -c 'type $${CC%% *} >/dev/null 2>/dev/null && echo $(CC) || echo gcc') CXX:=$(shell sh -c 'type $${CXX%% *} >/dev/null 2>/dev/null && echo $(CXX) || echo g++') OPTIMIZATION?=-O3 WARNINGS=-Wall -W -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers DEBUG_FLAGS?= -g -ggdb REAL_CFLAGS=$(OPTIMIZATION) -fPIC $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(DEBUG_FLAGS) REAL_LDFLAGS=$(LDFLAGS) DYLIBSUFFIX=so STLIBSUFFIX=a DYLIB_MINOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_SONAME) DYLIB_MAJOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR) DYLIBNAME=$(LIBNAME).$(DYLIBSUFFIX) DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) STLIBNAME=$(LIBNAME).$(STLIBSUFFIX) STLIB_MAKE_CMD=$(AR) rcs SSL_DYLIB_MINOR_NAME=$(SSL_LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_SONAME) SSL_DYLIB_MAJOR_NAME=$(SSL_LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR) SSL_DYLIBNAME=$(SSL_LIBNAME).$(DYLIBSUFFIX) SSL_STLIBNAME=$(SSL_LIBNAME).$(STLIBSUFFIX) SSL_DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(SSL_DYLIB_MINOR_NAME) # Platform-specific overrides uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') USE_SSL?=0 # This is required for test.c only ifeq ($(USE_SSL),1) CFLAGS+=-DHIREDIS_TEST_SSL endif ifeq ($(uname_S),Linux) SSL_LDFLAGS=-lssl -lcrypto else OPENSSL_PREFIX?=/usr/local/opt/openssl CFLAGS+=-I$(OPENSSL_PREFIX)/include SSL_LDFLAGS+=-L$(OPENSSL_PREFIX)/lib -lssl -lcrypto endif ifeq ($(uname_S),SunOS) IS_SUN_CC=$(shell sh -c '$(CC) -V 2>&1 |egrep -i -c "sun|studio"') ifeq ($(IS_SUN_CC),1) SUN_SHARED_FLAG=-G else SUN_SHARED_FLAG=-shared endif REAL_LDFLAGS+= -ldl -lnsl -lsocket DYLIB_MAKE_CMD=$(CC) $(SUN_SHARED_FLAG) -o $(DYLIBNAME) -h $(DYLIB_MINOR_NAME) $(LDFLAGS) SSL_DYLIB_MAKE_CMD=$(CC) $(SUN_SHARED_FLAG) -o $(SSL_DYLIBNAME) -h $(SSL_DYLIB_MINOR_NAME) $(LDFLAGS) $(SSL_LDFLAGS) endif ifeq ($(uname_S),Darwin) DYLIBSUFFIX=dylib DYLIB_MINOR_NAME=$(LIBNAME).$(HIREDIS_SONAME).$(DYLIBSUFFIX) DYLIB_MAKE_CMD=$(CC) -dynamiclib -Wl,-install_name,$(PREFIX)/$(LIBRARY_PATH)/$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(LDFLAGS) SSL_DYLIB_MAKE_CMD=$(CC) -dynamiclib -Wl,-install_name,$(PREFIX)/$(LIBRARY_PATH)/$(SSL_DYLIB_MINOR_NAME) -o $(SSL_DYLIBNAME) $(LDFLAGS) $(SSL_LDFLAGS) DYLIB_PLUGIN=-Wl,-undefined -Wl,dynamic_lookup endif all: $(DYLIBNAME) $(STLIBNAME) hiredis-test $(PKGCONFNAME) ifeq ($(USE_SSL),1) all: $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(SSL_PKGCONFNAME) endif # Deps (use make dep to generate this) alloc.o: alloc.c fmacros.h alloc.h async.o: async.c fmacros.h alloc.h async.h hiredis.h read.h sds.h net.h dict.c dict.h win32.h async_private.h dict.o: dict.c fmacros.h alloc.h dict.h hiredis.o: hiredis.c fmacros.h hiredis.h read.h sds.h alloc.h net.h async.h win32.h net.o: net.c fmacros.h net.h hiredis.h read.h sds.h alloc.h sockcompat.h win32.h read.o: read.c fmacros.h alloc.h read.h sds.h win32.h sds.o: sds.c sds.h sdsalloc.h alloc.h sockcompat.o: sockcompat.c sockcompat.h ssl.o: ssl.c hiredis.h read.h sds.h alloc.h async.h win32.h async_private.h test.o: test.c fmacros.h hiredis.h read.h sds.h alloc.h net.h sockcompat.h win32.h $(DYLIBNAME): $(OBJ) $(DYLIB_MAKE_CMD) -o $(DYLIBNAME) $(OBJ) $(REAL_LDFLAGS) $(STLIBNAME): $(OBJ) $(STLIB_MAKE_CMD) $(STLIBNAME) $(OBJ) $(SSL_DYLIBNAME): $(SSL_OBJ) $(SSL_DYLIB_MAKE_CMD) $(DYLIB_PLUGIN) -o $(SSL_DYLIBNAME) $(SSL_OBJ) $(REAL_LDFLAGS) $(LDFLAGS) $(SSL_LDFLAGS) $(SSL_STLIBNAME): $(SSL_OBJ) $(STLIB_MAKE_CMD) $(SSL_STLIBNAME) $(SSL_OBJ) dynamic: $(DYLIBNAME) static: $(STLIBNAME) ifeq ($(USE_SSL),1) dynamic: $(SSL_DYLIBNAME) static: $(SSL_STLIBNAME) endif # Binaries: hiredis-example-libevent: examples/example-libevent.c adapters/libevent.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -levent $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-libevent-ssl: examples/example-libevent-ssl.c adapters/libevent.h $(STLIBNAME) $(SSL_STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -levent $(STLIBNAME) $(SSL_STLIBNAME) $(REAL_LDFLAGS) $(SSL_LDFLAGS) hiredis-example-libev: examples/example-libev.c adapters/libev.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -lev $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-glib: examples/example-glib.c adapters/glib.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(shell pkg-config --cflags --libs glib-2.0) $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-ivykis: examples/example-ivykis.c adapters/ivykis.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -livykis $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-macosx: examples/example-macosx.c adapters/macosx.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -framework CoreFoundation $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-ssl: examples/example-ssl.c $(STLIBNAME) $(SSL_STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(STLIBNAME) $(SSL_STLIBNAME) $(REAL_LDFLAGS) $(SSL_LDFLAGS) ifndef AE_DIR hiredis-example-ae: @echo "Please specify AE_DIR (e.g. /src)" @false else hiredis-example-ae: examples/example-ae.c adapters/ae.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(AE_DIR) $< $(AE_DIR)/ae.o $(AE_DIR)/zmalloc.o $(AE_DIR)/../deps/jemalloc/lib/libjemalloc.a -pthread $(STLIBNAME) endif ifndef LIBUV_DIR hiredis-example-libuv: @echo "Please specify LIBUV_DIR (e.g. ../libuv/)" @false else hiredis-example-libuv: examples/example-libuv.c adapters/libuv.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. -I$(LIBUV_DIR)/include $< $(LIBUV_DIR)/.libs/libuv.a -lpthread -lrt $(STLIBNAME) $(REAL_LDFLAGS) endif ifeq ($(and $(QT_MOC),$(QT_INCLUDE_DIR),$(QT_LIBRARY_DIR)),) hiredis-example-qt: @echo "Please specify QT_MOC, QT_INCLUDE_DIR AND QT_LIBRARY_DIR" @false else hiredis-example-qt: examples/example-qt.cpp adapters/qt.h $(STLIBNAME) $(QT_MOC) adapters/qt.h -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore | \ $(CXX) -x c++ -o qt-adapter-moc.o -c - $(REAL_CFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore $(QT_MOC) examples/example-qt.h -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore | \ $(CXX) -x c++ -o qt-example-moc.o -c - $(REAL_CFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore $(CXX) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore -L$(QT_LIBRARY_DIR) qt-adapter-moc.o qt-example-moc.o $< -pthread $(STLIBNAME) -lQtCore endif hiredis-example: examples/example.c $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-push: examples/example-push.c $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(STLIBNAME) $(REAL_LDFLAGS) examples: $(EXAMPLES) TEST_LIBS = $(STLIBNAME) ifeq ($(USE_SSL),1) TEST_LIBS += $(SSL_STLIBNAME) TEST_LDFLAGS = $(SSL_LDFLAGS) -lssl -lcrypto -lpthread endif hiredis-test: test.o $(TEST_LIBS) $(CC) -o $@ $(REAL_CFLAGS) -I. $^ $(REAL_LDFLAGS) $(TEST_LDFLAGS) hiredis-%: %.o $(STLIBNAME) $(CC) $(REAL_CFLAGS) -o $@ $< $(TEST_LIBS) $(REAL_LDFLAGS) test: hiredis-test ./hiredis-test check: hiredis-test TEST_SSL=$(USE_SSL) ./test.sh .c.o: $(CC) -std=c99 -pedantic -c $(REAL_CFLAGS) $< clean: rm -rf $(DYLIBNAME) $(STLIBNAME) $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(TESTS) $(PKGCONFNAME) examples/hiredis-example* *.o *.gcda *.gcno *.gcov dep: $(CC) $(CPPFLAGS) $(CFLAGS) -MM *.c INSTALL?= cp -pPR $(PKGCONFNAME): hiredis.h @echo "Generating $@ for pkgconfig..." @echo prefix=$(PREFIX) > $@ @echo exec_prefix=\$${prefix} >> $@ @echo libdir=$(PREFIX)/$(LIBRARY_PATH) >> $@ @echo includedir=$(PREFIX)/$(INCLUDE_PATH) >> $@ @echo >> $@ @echo Name: hiredis >> $@ @echo Description: Minimalistic C client library for Redis. >> $@ @echo Version: $(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(HIREDIS_PATCH) >> $@ @echo Libs: -L\$${libdir} -lhiredis >> $@ @echo Cflags: -I\$${includedir} -D_FILE_OFFSET_BITS=64 >> $@ $(SSL_PKGCONFNAME): hiredis_ssl.h @echo "Generating $@ for pkgconfig..." @echo prefix=$(PREFIX) > $@ @echo exec_prefix=\$${prefix} >> $@ @echo libdir=$(PREFIX)/$(LIBRARY_PATH) >> $@ @echo includedir=$(PREFIX)/$(INCLUDE_PATH) >> $@ @echo >> $@ @echo Name: hiredis_ssl >> $@ @echo Description: SSL Support for hiredis. >> $@ @echo Version: $(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(HIREDIS_PATCH) >> $@ @echo Requires: hiredis >> $@ @echo Libs: -L\$${libdir} -lhiredis_ssl >> $@ @echo Libs.private: -lssl -lcrypto >> $@ install: $(DYLIBNAME) $(STLIBNAME) $(PKGCONFNAME) mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_INCLUDE_PATH)/adapters $(INSTALL_LIBRARY_PATH) $(INSTALL) hiredis.h async.h read.h sds.h alloc.h $(INSTALL_INCLUDE_PATH) $(INSTALL) adapters/*.h $(INSTALL_INCLUDE_PATH)/adapters $(INSTALL) $(DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(DYLIB_MINOR_NAME) cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MINOR_NAME) $(DYLIBNAME) $(INSTALL) $(STLIBNAME) $(INSTALL_LIBRARY_PATH) mkdir -p $(INSTALL_PKGCONF_PATH) $(INSTALL) $(PKGCONFNAME) $(INSTALL_PKGCONF_PATH) ifeq ($(USE_SSL),1) install: install-ssl install-ssl: $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(SSL_PKGCONFNAME) mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_LIBRARY_PATH) $(INSTALL) hiredis_ssl.h $(INSTALL_INCLUDE_PATH) $(INSTALL) $(SSL_DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(SSL_DYLIB_MINOR_NAME) cd $(INSTALL_LIBRARY_PATH) && ln -sf $(SSL_DYLIB_MINOR_NAME) $(SSL_DYLIBNAME) $(INSTALL) $(SSL_STLIBNAME) $(INSTALL_LIBRARY_PATH) mkdir -p $(INSTALL_PKGCONF_PATH) $(INSTALL) $(SSL_PKGCONFNAME) $(INSTALL_PKGCONF_PATH) endif 32bit: @echo "" @echo "WARNING: if this fails under Linux you probably need to install libc6-dev-i386" @echo "" $(MAKE) CFLAGS="-m32" LDFLAGS="-m32" 32bit-vars: $(eval CFLAGS=-m32) $(eval LDFLAGS=-m32) gprof: $(MAKE) CFLAGS="-pg" LDFLAGS="-pg" gcov: $(MAKE) CFLAGS="-fprofile-arcs -ftest-coverage" LDFLAGS="-fprofile-arcs" coverage: gcov make check mkdir -p tmp/lcov lcov -d . -c -o tmp/lcov/hiredis.info genhtml --legend -o tmp/lcov/report tmp/lcov/hiredis.info noopt: $(MAKE) OPTIMIZATION="" .PHONY: all test check clean dep install 32bit 32bit-vars gprof gcov noopt redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/README.md000066400000000000000000000672451515654015100305300ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/redis/hiredis.png)](https://travis-ci.org/redis/hiredis) **This Readme reflects the latest changed in the master branch. See [v1.0.0](https://github.com/redis/hiredis/tree/v1.0.0) for the Readme and documentation for the latest release ([API/ABI history](https://abi-laboratory.pro/?view=timeline&l=hiredis)).** # HIREDIS Hiredis is a minimalistic C client library for the [Redis](http://redis.io/) database. It is minimalistic because it just adds minimal support for the protocol, but at the same time it uses a high level printf-alike API in order to make it much higher level than otherwise suggested by its minimal code base and the lack of explicit bindings for every Redis command. Apart from supporting sending commands and receiving replies, it comes with a reply parser that is decoupled from the I/O layer. It is a stream parser designed for easy reusability, which can for instance be used in higher level language bindings for efficient reply parsing. Hiredis only supports the binary-safe Redis protocol, so you can use it with any Redis version >= 1.2.0. The library comes with multiple APIs. There is the *synchronous API*, the *asynchronous API* and the *reply parsing API*. ## Upgrading to `1.0.2` NOTE: v1.0.1 erroneously bumped SONAME, which is why it is skipped here. Version 1.0.2 is simply 1.0.0 with a fix for [CVE-2021-32765](https://github.com/redis/hiredis/security/advisories/GHSA-hfm9-39pp-55p2). They are otherwise identical. ## Upgrading to `1.0.0` Version 1.0.0 marks the first stable release of Hiredis. It includes some minor breaking changes, mostly to make the exposed API more uniform and self-explanatory. It also bundles the updated `sds` library, to sync up with upstream and Redis. For code changes see the [Changelog](CHANGELOG.md). _Note: As described below, a few member names have been changed but most applications should be able to upgrade with minor code changes and recompiling._ ## IMPORTANT: Breaking changes from `0.14.1` -> `1.0.0` * `redisContext` has two additional members (`free_privdata`, and `privctx`). * `redisOptions.timeout` has been renamed to `redisOptions.connect_timeout`, and we've added `redisOptions.command_timeout`. * `redisReplyObjectFunctions.createArray` now takes `size_t` instead of `int` for its length parameter. ## IMPORTANT: Breaking changes when upgrading from 0.13.x -> 0.14.x Bulk and multi-bulk lengths less than -1 or greater than `LLONG_MAX` are now protocol errors. This is consistent with the RESP specification. On 32-bit platforms, the upper bound is lowered to `SIZE_MAX`. Change `redisReply.len` to `size_t`, as it denotes the the size of a string User code should compare this to `size_t` values as well. If it was used to compare to other values, casting might be necessary or can be removed, if casting was applied before. ## Upgrading from `<0.9.0` Version 0.9.0 is a major overhaul of hiredis in every aspect. However, upgrading existing code using hiredis should not be a big pain. The key thing to keep in mind when upgrading is that hiredis >= 0.9.0 uses a `redisContext*` to keep state, in contrast to the stateless 0.0.1 that only has a file descriptor to work with. ## Synchronous API To consume the synchronous API, there are only a few function calls that need to be introduced: ```c redisContext *redisConnect(const char *ip, int port); void *redisCommand(redisContext *c, const char *format, ...); void freeReplyObject(void *reply); ``` ### Connecting The function `redisConnect` is used to create a so-called `redisContext`. The context is where Hiredis holds state for a connection. The `redisContext` struct has an integer `err` field that is non-zero when the connection is in an error state. The field `errstr` will contain a string with a description of the error. More information on errors can be found in the **Errors** section. After trying to connect to Redis using `redisConnect` you should check the `err` field to see if establishing the connection was successful: ```c redisContext *c = redisConnect("127.0.0.1", 6379); if (c == NULL || c->err) { if (c) { printf("Error: %s\n", c->errstr); // handle error } else { printf("Can't allocate redis context\n"); } } ``` *Note: A `redisContext` is not thread-safe.* ### Sending commands There are several ways to issue commands to Redis. The first that will be introduced is `redisCommand`. This function takes a format similar to printf. In the simplest form, it is used like this: ```c reply = redisCommand(context, "SET foo bar"); ``` The specifier `%s` interpolates a string in the command, and uses `strlen` to determine the length of the string: ```c reply = redisCommand(context, "SET foo %s", value); ``` When you need to pass binary safe strings in a command, the `%b` specifier can be used. Together with a pointer to the string, it requires a `size_t` length argument of the string: ```c reply = redisCommand(context, "SET foo %b", value, (size_t) valuelen); ``` Internally, Hiredis splits the command in different arguments and will convert it to the protocol used to communicate with Redis. One or more spaces separates arguments, so you can use the specifiers anywhere in an argument: ```c reply = redisCommand(context, "SET key:%s %s", myid, value); ``` ### Using replies The return value of `redisCommand` holds a reply when the command was successfully executed. When an error occurs, the return value is `NULL` and the `err` field in the context will be set (see section on **Errors**). Once an error is returned the context cannot be reused and you should set up a new connection. The standard replies that `redisCommand` are of the type `redisReply`. The `type` field in the `redisReply` should be used to test what kind of reply was received: ### RESP2 * **`REDIS_REPLY_STATUS`**: * The command replied with a status reply. The status string can be accessed using `reply->str`. The length of this string can be accessed using `reply->len`. * **`REDIS_REPLY_ERROR`**: * The command replied with an error. The error string can be accessed identical to `REDIS_REPLY_STATUS`. * **`REDIS_REPLY_INTEGER`**: * The command replied with an integer. The integer value can be accessed using the `reply->integer` field of type `long long`. * **`REDIS_REPLY_NIL`**: * The command replied with a **nil** object. There is no data to access. * **`REDIS_REPLY_STRING`**: * A bulk (string) reply. The value of the reply can be accessed using `reply->str`. The length of this string can be accessed using `reply->len`. * **`REDIS_REPLY_ARRAY`**: * A multi bulk reply. The number of elements in the multi bulk reply is stored in `reply->elements`. Every element in the multi bulk reply is a `redisReply` object as well and can be accessed via `reply->element[..index..]`. Redis may reply with nested arrays but this is fully supported. ### RESP3 Hiredis also supports every new `RESP3` data type which are as follows. For more information about the protocol see the `RESP3` [specification.](https://github.com/antirez/RESP3/blob/master/spec.md) * **`REDIS_REPLY_DOUBLE`**: * The command replied with a double-precision floating point number. The value is stored as a string in the `str` member, and can be converted with `strtod` or similar. * **`REDIS_REPLY_BOOL`**: * A boolean true/false reply. The value is stored in the `integer` member and will be either `0` or `1`. * **`REDIS_REPLY_MAP`**: * An array with the added invariant that there will always be an even number of elements. The MAP is functionally equivelant to `REDIS_REPLY_ARRAY` except for the previously mentioned invariant. * **`REDIS_REPLY_SET`**: * An array response where each entry is unique. Like the MAP type, the data is identical to an array response except there are no duplicate values. * **`REDIS_REPLY_PUSH`**: * An array that can be generated spontaneously by Redis. This array response will always contain at least two subelements. The first contains the type of `PUSH` message (e.g. `message`, or `invalidate`), and the second being a sub-array with the `PUSH` payload itself. * **`REDIS_REPLY_ATTR`**: * An array structurally identical to a `MAP` but intended as meta-data about a reply. _As of Redis 6.0.6 this reply type is not used in Redis_ * **`REDIS_REPLY_BIGNUM`**: * A string representing an arbitrarily large signed or unsigned integer value. The number will be encoded as a string in the `str` member of `redisReply`. * **`REDIS_REPLY_VERB`**: * A verbatim string, intended to be presented to the user without modification. The string payload is stored in the `str` memeber, and type data is stored in the `vtype` member (e.g. `txt` for raw text or `md` for markdown). Replies should be freed using the `freeReplyObject()` function. Note that this function will take care of freeing sub-reply objects contained in arrays and nested arrays, so there is no need for the user to free the sub replies (it is actually harmful and will corrupt the memory). **Important:** the current version of hiredis (1.0.0) frees replies when the asynchronous API is used. This means you should not call `freeReplyObject` when you use this API. The reply is cleaned up by hiredis _after_ the callback returns. We may introduce a flag to make this configurable in future versions of the library. ### Cleaning up To disconnect and free the context the following function can be used: ```c void redisFree(redisContext *c); ``` This function immediately closes the socket and then frees the allocations done in creating the context. ### Sending commands (cont'd) Together with `redisCommand`, the function `redisCommandArgv` can be used to issue commands. It has the following prototype: ```c void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); ``` It takes the number of arguments `argc`, an array of strings `argv` and the lengths of the arguments `argvlen`. For convenience, `argvlen` may be set to `NULL` and the function will use `strlen(3)` on every argument to determine its length. Obviously, when any of the arguments need to be binary safe, the entire array of lengths `argvlen` should be provided. The return value has the same semantic as `redisCommand`. ### Pipelining To explain how Hiredis supports pipelining in a blocking connection, there needs to be understanding of the internal execution flow. When any of the functions in the `redisCommand` family is called, Hiredis first formats the command according to the Redis protocol. The formatted command is then put in the output buffer of the context. This output buffer is dynamic, so it can hold any number of commands. After the command is put in the output buffer, `redisGetReply` is called. This function has the following two execution paths: 1. The input buffer is non-empty: * Try to parse a single reply from the input buffer and return it * If no reply could be parsed, continue at *2* 2. The input buffer is empty: * Write the **entire** output buffer to the socket * Read from the socket until a single reply could be parsed The function `redisGetReply` is exported as part of the Hiredis API and can be used when a reply is expected on the socket. To pipeline commands, the only things that needs to be done is filling up the output buffer. For this cause, two commands can be used that are identical to the `redisCommand` family, apart from not returning a reply: ```c void redisAppendCommand(redisContext *c, const char *format, ...); void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); ``` After calling either function one or more times, `redisGetReply` can be used to receive the subsequent replies. The return value for this function is either `REDIS_OK` or `REDIS_ERR`, where the latter means an error occurred while reading a reply. Just as with the other commands, the `err` field in the context can be used to find out what the cause of this error is. The following examples shows a simple pipeline (resulting in only a single call to `write(2)` and a single call to `read(2)`): ```c redisReply *reply; redisAppendCommand(context,"SET foo bar"); redisAppendCommand(context,"GET foo"); redisGetReply(context,(void *)&reply); // reply for SET freeReplyObject(reply); redisGetReply(context,(void *)&reply); // reply for GET freeReplyObject(reply); ``` This API can also be used to implement a blocking subscriber: ```c reply = redisCommand(context,"SUBSCRIBE foo"); freeReplyObject(reply); while(redisGetReply(context,(void *)&reply) == REDIS_OK) { // consume message freeReplyObject(reply); } ``` ### Errors When a function call is not successful, depending on the function either `NULL` or `REDIS_ERR` is returned. The `err` field inside the context will be non-zero and set to one of the following constants: * **`REDIS_ERR_IO`**: There was an I/O error while creating the connection, trying to write to the socket or read from the socket. If you included `errno.h` in your application, you can use the global `errno` variable to find out what is wrong. * **`REDIS_ERR_EOF`**: The server closed the connection which resulted in an empty read. * **`REDIS_ERR_PROTOCOL`**: There was an error while parsing the protocol. * **`REDIS_ERR_OTHER`**: Any other error. Currently, it is only used when a specified hostname to connect to cannot be resolved. In every case, the `errstr` field in the context will be set to hold a string representation of the error. ## Asynchronous API Hiredis comes with an asynchronous API that works easily with any event library. Examples are bundled that show using Hiredis with [libev](http://software.schmorp.de/pkg/libev.html) and [libevent](http://monkey.org/~provos/libevent/). ### Connecting The function `redisAsyncConnect` can be used to establish a non-blocking connection to Redis. It returns a pointer to the newly created `redisAsyncContext` struct. The `err` field should be checked after creation to see if there were errors creating the connection. Because the connection that will be created is non-blocking, the kernel is not able to instantly return if the specified host and port is able to accept a connection. *Note: A `redisAsyncContext` is not thread-safe.* ```c redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); if (c->err) { printf("Error: %s\n", c->errstr); // handle error } ``` The asynchronous context can hold a disconnect callback function that is called when the connection is disconnected (either because of an error or per user request). This function should have the following prototype: ```c void(const redisAsyncContext *c, int status); ``` On a disconnect, the `status` argument is set to `REDIS_OK` when disconnection was initiated by the user, or `REDIS_ERR` when the disconnection was caused by an error. When it is `REDIS_ERR`, the `err` field in the context can be accessed to find out the cause of the error. The context object is always freed after the disconnect callback fired. When a reconnect is needed, the disconnect callback is a good point to do so. Setting the disconnect callback can only be done once per context. For subsequent calls it will return `REDIS_ERR`. The function to set the disconnect callback has the following prototype: ```c int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); ``` `ac->data` may be used to pass user data to this callback, the same can be done for redisConnectCallback. ### Sending commands and their callbacks In an asynchronous context, commands are automatically pipelined due to the nature of an event loop. Therefore, unlike the synchronous API, there is only a single way to send commands. Because commands are sent to Redis asynchronously, issuing a command requires a callback function that is called when the reply is received. Reply callbacks should have the following prototype: ```c void(redisAsyncContext *c, void *reply, void *privdata); ``` The `privdata` argument can be used to curry arbitrary data to the callback from the point where the command is initially queued for execution. The functions that can be used to issue commands in an asynchronous context are: ```c int redisAsyncCommand( redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...); int redisAsyncCommandArgv( redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen); ``` Both functions work like their blocking counterparts. The return value is `REDIS_OK` when the command was successfully added to the output buffer and `REDIS_ERR` otherwise. Example: when the connection is being disconnected per user-request, no new commands may be added to the output buffer and `REDIS_ERR` is returned on calls to the `redisAsyncCommand` family. If the reply for a command with a `NULL` callback is read, it is immediately freed. When the callback for a command is non-`NULL`, the memory is freed immediately following the callback: the reply is only valid for the duration of the callback. All pending callbacks are called with a `NULL` reply when the context encountered an error. ### Disconnecting An asynchronous connection can be terminated using: ```c void redisAsyncDisconnect(redisAsyncContext *ac); ``` When this function is called, the connection is **not** immediately terminated. Instead, new commands are no longer accepted and the connection is only terminated when all pending commands have been written to the socket, their respective replies have been read and their respective callbacks have been executed. After this, the disconnection callback is executed with the `REDIS_OK` status and the context object is freed. ### Hooking it up to event library *X* There are a few hooks that need to be set on the context object after it is created. See the `adapters/` directory for bindings to *libev* and *libevent*. ## Reply parsing API Hiredis comes with a reply parsing API that makes it easy for writing higher level language bindings. The reply parsing API consists of the following functions: ```c redisReader *redisReaderCreate(void); void redisReaderFree(redisReader *reader); int redisReaderFeed(redisReader *reader, const char *buf, size_t len); int redisReaderGetReply(redisReader *reader, void **reply); ``` The same set of functions are used internally by hiredis when creating a normal Redis context, the above API just exposes it to the user for a direct usage. ### Usage The function `redisReaderCreate` creates a `redisReader` structure that holds a buffer with unparsed data and state for the protocol parser. Incoming data -- most likely from a socket -- can be placed in the internal buffer of the `redisReader` using `redisReaderFeed`. This function will make a copy of the buffer pointed to by `buf` for `len` bytes. This data is parsed when `redisReaderGetReply` is called. This function returns an integer status and a reply object (as described above) via `void **reply`. The returned status can be either `REDIS_OK` or `REDIS_ERR`, where the latter means something went wrong (either a protocol error, or an out of memory error). The parser limits the level of nesting for multi bulk payloads to 7. If the multi bulk nesting level is higher than this, the parser returns an error. ### Customizing replies The function `redisReaderGetReply` creates `redisReply` and makes the function argument `reply` point to the created `redisReply` variable. For instance, if the response of type `REDIS_REPLY_STATUS` then the `str` field of `redisReply` will hold the status as a vanilla C string. However, the functions that are responsible for creating instances of the `redisReply` can be customized by setting the `fn` field on the `redisReader` struct. This should be done immediately after creating the `redisReader`. For example, [hiredis-rb](https://github.com/pietern/hiredis-rb/blob/master/ext/hiredis_ext/reader.c) uses customized reply object functions to create Ruby objects. ### Reader max buffer Both when using the Reader API directly or when using it indirectly via a normal Redis context, the redisReader structure uses a buffer in order to accumulate data from the server. Usually this buffer is destroyed when it is empty and is larger than 16 KiB in order to avoid wasting memory in unused buffers However when working with very big payloads destroying the buffer may slow down performances considerably, so it is possible to modify the max size of an idle buffer changing the value of the `maxbuf` field of the reader structure to the desired value. The special value of 0 means that there is no maximum value for an idle buffer, so the buffer will never get freed. For instance if you have a normal Redis context you can set the maximum idle buffer to zero (unlimited) just with: ```c context->reader->maxbuf = 0; ``` This should be done only in order to maximize performances when working with large payloads. The context should be set back to `REDIS_READER_MAX_BUF` again as soon as possible in order to prevent allocation of useless memory. ### Reader max array elements By default the hiredis reply parser sets the maximum number of multi-bulk elements to 2^32 - 1 or 4,294,967,295 entries. If you need to process multi-bulk replies with more than this many elements you can set the value higher or to zero, meaning unlimited with: ```c context->reader->maxelements = 0; ``` ## SSL/TLS Support ### Building SSL/TLS support is not built by default and requires an explicit flag: make USE_SSL=1 This requires OpenSSL development package (e.g. including header files to be available. When enabled, SSL/TLS support is built into extra `libhiredis_ssl.a` and `libhiredis_ssl.so` static/dynamic libraries. This leaves the original libraries unaffected so no additional dependencies are introduced. ### Using it First, you'll need to make sure you include the SSL header file: ```c #include "hiredis.h" #include "hiredis_ssl.h" ``` You will also need to link against `libhiredis_ssl`, **in addition** to `libhiredis` and add `-lssl -lcrypto` to satisfy its dependencies. Hiredis implements SSL/TLS on top of its normal `redisContext` or `redisAsyncContext`, so you will need to establish a connection first and then initiate an SSL/TLS handshake. #### Hiredis OpenSSL Wrappers Before Hiredis can negotiate an SSL/TLS connection, it is necessary to initialize OpenSSL and create a context. You can do that in two ways: 1. Work directly with the OpenSSL API to initialize the library's global context and create `SSL_CTX *` and `SSL *` contexts. With an `SSL *` object you can call `redisInitiateSSL()`. 2. Work with a set of Hiredis-provided wrappers around OpenSSL, create a `redisSSLContext` object to hold configuration and use `redisInitiateSSLWithContext()` to initiate the SSL/TLS handshake. ```c /* An Hiredis SSL context. It holds SSL configuration and can be reused across * many contexts. */ redisSSLContext *ssl; /* An error variable to indicate what went wrong, if the context fails to * initialize. */ redisSSLContextError ssl_error; /* Initialize global OpenSSL state. * * You should call this only once when your app initializes, and only if * you don't explicitly or implicitly initialize OpenSSL it elsewhere. */ redisInitOpenSSL(); /* Create SSL context */ ssl = redisCreateSSLContext( "cacertbundle.crt", /* File name of trusted CA/ca bundle file, optional */ "/path/to/certs", /* Path of trusted certificates, optional */ "client_cert.pem", /* File name of client certificate file, optional */ "client_key.pem", /* File name of client private key, optional */ "redis.mydomain.com", /* Server name to request (SNI), optional */ &ssl_error ) != REDIS_OK) { printf("SSL error: %s\n", redisSSLContextGetError(ssl_error); /* Abort... */ } /* Create Redis context and establish connection */ c = redisConnect("localhost", 6443); if (c == NULL || c->err) { /* Handle error and abort... */ } /* Negotiate SSL/TLS */ if (redisInitiateSSLWithContext(c, ssl) != REDIS_OK) { /* Handle error, in c->err / c->errstr */ } ``` ## RESP3 PUSH replies Redis 6.0 introduced PUSH replies with the reply-type `>`. These messages are generated spontaneously and can arrive at any time, so must be handled using callbacks. ### Default behavior Hiredis installs handlers on `redisContext` and `redisAsyncContext` by default, which will intercept and free any PUSH replies detected. This means existing code will work as-is after upgrading to Redis 6 and switching to `RESP3`. ### Custom PUSH handler prototypes The callback prototypes differ between `redisContext` and `redisAsyncContext`. #### redisContext ```c void my_push_handler(void *privdata, void *reply) { /* Handle the reply */ /* Note: We need to free the reply in our custom handler for blocking contexts. This lets us keep the reply if we want. */ freeReplyObject(reply); } ``` #### redisAsyncContext ```c void my_async_push_handler(redisAsyncContext *ac, void *reply) { /* Handle the reply */ /* Note: Because async hiredis always frees replies, you should not call freeReplyObject in an async push callback. */ } ``` ### Installing a custom handler There are two ways to set your own PUSH handlers. 1. Set `push_cb` or `async_push_cb` in the `redisOptions` struct and connect with `redisConnectWithOptions` or `redisAsyncConnectWithOptions`. ```c redisOptions = {0}; REDIS_OPTIONS_SET_TCP(&options, "127.0.0.1", 6379); options->push_cb = my_push_handler; redisContext *context = redisConnectWithOptions(&options); ``` 2. Call `redisSetPushCallback` or `redisAsyncSetPushCallback` on a connected context. ```c redisContext *context = redisConnect("127.0.0.1", 6379); redisSetPushCallback(context, my_push_handler); ``` _Note `redisSetPushCallback` and `redisAsyncSetPushCallback` both return any currently configured handler, making it easy to override and then return to the old value._ ### Specifying no handler If you have a unique use-case where you don't want hiredis to automatically intercept and free PUSH replies, you will want to configure no handler at all. This can be done in two ways. 1. Set the `REDIS_OPT_NO_PUSH_AUTOFREE` flag in `redisOptions` and leave the callback function pointer `NULL`. ```c redisOptions = {0}; REDIS_OPTIONS_SET_TCP(&options, "127.0.0.1", 6379); options->options |= REDIS_OPT_NO_PUSH_AUTOFREE; redisContext *context = redisConnectWithOptions(&options); ``` 3. Call `redisSetPushCallback` with `NULL` once connected. ```c redisContext *context = redisConnect("127.0.0.1", 6379); redisSetPushCallback(context, NULL); ``` _Note: With no handler configured, calls to `redisCommand` may generate more than one reply, so this strategy is only applicable when there's some kind of blocking`redisGetReply()` loop (e.g. `MONITOR` or `SUBSCRIBE` workloads)._ ## Allocator injection Hiredis uses a pass-thru structure of function pointers defined in [alloc.h](https://github.com/redis/hiredis/blob/f5d25850/alloc.h#L41) that contain the currently configured allocation and deallocation functions. By default they just point to libc (`malloc`, `calloc`, `realloc`, etc). ### Overriding One can override the allocators like so: ```c hiredisAllocFuncs myfuncs = { .mallocFn = my_malloc, .callocFn = my_calloc, .reallocFn = my_realloc, .strdupFn = my_strdup, .freeFn = my_free, }; // Override allocators (function returns current allocators if needed) hiredisAllocFuncs orig = hiredisSetAllocators(&myfuncs); ``` To reset the allocators to their default libc function simply call: ```c hiredisResetAllocators(); ``` ## AUTHORS Salvatore Sanfilippo (antirez at gmail),\ Pieter Noordhuis (pcnoordhuis at gmail)\ Michael Grunder (michael dot grunder at gmail) _Hiredis is released under the BSD license._ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/000077500000000000000000000000001515654015100310365ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/ae.h000066400000000000000000000102571515654015100316010ustar00rootroot00000000000000/* * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_AE_H__ #define __HIREDIS_AE_H__ #include #include #include "../hiredis.h" #include "../async.h" typedef struct redisAeEvents { redisAsyncContext *context; aeEventLoop *loop; int fd; int reading, writing; } redisAeEvents; static void redisAeReadEvent(aeEventLoop *el, int fd, void *privdata, int mask) { ((void)el); ((void)fd); ((void)mask); redisAeEvents *e = (redisAeEvents*)privdata; redisAsyncHandleRead(e->context); } static void redisAeWriteEvent(aeEventLoop *el, int fd, void *privdata, int mask) { ((void)el); ((void)fd); ((void)mask); redisAeEvents *e = (redisAeEvents*)privdata; redisAsyncHandleWrite(e->context); } static void redisAeAddRead(void *privdata) { redisAeEvents *e = (redisAeEvents*)privdata; aeEventLoop *loop = e->loop; if (!e->reading) { e->reading = 1; aeCreateFileEvent(loop,e->fd,AE_READABLE,redisAeReadEvent,e); } } static void redisAeDelRead(void *privdata) { redisAeEvents *e = (redisAeEvents*)privdata; aeEventLoop *loop = e->loop; if (e->reading) { e->reading = 0; aeDeleteFileEvent(loop,e->fd,AE_READABLE); } } static void redisAeAddWrite(void *privdata) { redisAeEvents *e = (redisAeEvents*)privdata; aeEventLoop *loop = e->loop; if (!e->writing) { e->writing = 1; aeCreateFileEvent(loop,e->fd,AE_WRITABLE,redisAeWriteEvent,e); } } static void redisAeDelWrite(void *privdata) { redisAeEvents *e = (redisAeEvents*)privdata; aeEventLoop *loop = e->loop; if (e->writing) { e->writing = 0; aeDeleteFileEvent(loop,e->fd,AE_WRITABLE); } } static void redisAeCleanup(void *privdata) { redisAeEvents *e = (redisAeEvents*)privdata; redisAeDelRead(privdata); redisAeDelWrite(privdata); hi_free(e); } static int redisAeAttach(aeEventLoop *loop, redisAsyncContext *ac) { redisContext *c = &(ac->c); redisAeEvents *e; /* Nothing should be attached when something is already attached */ if (ac->ev.data != NULL) return REDIS_ERR; /* Create container for context and r/w events */ e = (redisAeEvents*)hi_malloc(sizeof(*e)); if (e == NULL) return REDIS_ERR; e->context = ac; e->loop = loop; e->fd = c->fd; e->reading = e->writing = 0; /* Register functions to start/stop listening for events */ ac->ev.addRead = redisAeAddRead; ac->ev.delRead = redisAeDelRead; ac->ev.addWrite = redisAeAddWrite; ac->ev.delWrite = redisAeDelWrite; ac->ev.cleanup = redisAeCleanup; ac->ev.data = e; return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/glib.h000066400000000000000000000074161515654015100321340ustar00rootroot00000000000000#ifndef __HIREDIS_GLIB_H__ #define __HIREDIS_GLIB_H__ #include #include "../hiredis.h" #include "../async.h" typedef struct { GSource source; redisAsyncContext *ac; GPollFD poll_fd; } RedisSource; static void redis_source_add_read (gpointer data) { RedisSource *source = (RedisSource *)data; g_return_if_fail(source); source->poll_fd.events |= G_IO_IN; g_main_context_wakeup(g_source_get_context((GSource *)data)); } static void redis_source_del_read (gpointer data) { RedisSource *source = (RedisSource *)data; g_return_if_fail(source); source->poll_fd.events &= ~G_IO_IN; g_main_context_wakeup(g_source_get_context((GSource *)data)); } static void redis_source_add_write (gpointer data) { RedisSource *source = (RedisSource *)data; g_return_if_fail(source); source->poll_fd.events |= G_IO_OUT; g_main_context_wakeup(g_source_get_context((GSource *)data)); } static void redis_source_del_write (gpointer data) { RedisSource *source = (RedisSource *)data; g_return_if_fail(source); source->poll_fd.events &= ~G_IO_OUT; g_main_context_wakeup(g_source_get_context((GSource *)data)); } static void redis_source_cleanup (gpointer data) { RedisSource *source = (RedisSource *)data; g_return_if_fail(source); redis_source_del_read(source); redis_source_del_write(source); /* * It is not our responsibility to remove ourself from the * current main loop. However, we will remove the GPollFD. */ if (source->poll_fd.fd >= 0) { g_source_remove_poll((GSource *)data, &source->poll_fd); source->poll_fd.fd = -1; } } static gboolean redis_source_prepare (GSource *source, gint *timeout_) { RedisSource *redis = (RedisSource *)source; *timeout_ = -1; return !!(redis->poll_fd.events & redis->poll_fd.revents); } static gboolean redis_source_check (GSource *source) { RedisSource *redis = (RedisSource *)source; return !!(redis->poll_fd.events & redis->poll_fd.revents); } static gboolean redis_source_dispatch (GSource *source, GSourceFunc callback, gpointer user_data) { RedisSource *redis = (RedisSource *)source; if ((redis->poll_fd.revents & G_IO_OUT)) { redisAsyncHandleWrite(redis->ac); redis->poll_fd.revents &= ~G_IO_OUT; } if ((redis->poll_fd.revents & G_IO_IN)) { redisAsyncHandleRead(redis->ac); redis->poll_fd.revents &= ~G_IO_IN; } if (callback) { return callback(user_data); } return TRUE; } static void redis_source_finalize (GSource *source) { RedisSource *redis = (RedisSource *)source; if (redis->poll_fd.fd >= 0) { g_source_remove_poll(source, &redis->poll_fd); redis->poll_fd.fd = -1; } } static GSource * redis_source_new (redisAsyncContext *ac) { static GSourceFuncs source_funcs = { .prepare = redis_source_prepare, .check = redis_source_check, .dispatch = redis_source_dispatch, .finalize = redis_source_finalize, }; redisContext *c = &ac->c; RedisSource *source; g_return_val_if_fail(ac != NULL, NULL); source = (RedisSource *)g_source_new(&source_funcs, sizeof *source); if (source == NULL) return NULL; source->ac = ac; source->poll_fd.fd = c->fd; source->poll_fd.events = 0; source->poll_fd.revents = 0; g_source_add_poll((GSource *)source, &source->poll_fd); ac->ev.addRead = redis_source_add_read; ac->ev.delRead = redis_source_del_read; ac->ev.addWrite = redis_source_add_write; ac->ev.delWrite = redis_source_del_write; ac->ev.cleanup = redis_source_cleanup; ac->ev.data = source; return (GSource *)source; } #endif /* __HIREDIS_GLIB_H__ */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/ivykis.h000066400000000000000000000044271515654015100325340ustar00rootroot00000000000000#ifndef __HIREDIS_IVYKIS_H__ #define __HIREDIS_IVYKIS_H__ #include #include "../hiredis.h" #include "../async.h" typedef struct redisIvykisEvents { redisAsyncContext *context; struct iv_fd fd; } redisIvykisEvents; static void redisIvykisReadEvent(void *arg) { redisAsyncContext *context = (redisAsyncContext *)arg; redisAsyncHandleRead(context); } static void redisIvykisWriteEvent(void *arg) { redisAsyncContext *context = (redisAsyncContext *)arg; redisAsyncHandleWrite(context); } static void redisIvykisAddRead(void *privdata) { redisIvykisEvents *e = (redisIvykisEvents*)privdata; iv_fd_set_handler_in(&e->fd, redisIvykisReadEvent); } static void redisIvykisDelRead(void *privdata) { redisIvykisEvents *e = (redisIvykisEvents*)privdata; iv_fd_set_handler_in(&e->fd, NULL); } static void redisIvykisAddWrite(void *privdata) { redisIvykisEvents *e = (redisIvykisEvents*)privdata; iv_fd_set_handler_out(&e->fd, redisIvykisWriteEvent); } static void redisIvykisDelWrite(void *privdata) { redisIvykisEvents *e = (redisIvykisEvents*)privdata; iv_fd_set_handler_out(&e->fd, NULL); } static void redisIvykisCleanup(void *privdata) { redisIvykisEvents *e = (redisIvykisEvents*)privdata; iv_fd_unregister(&e->fd); hi_free(e); } static int redisIvykisAttach(redisAsyncContext *ac) { redisContext *c = &(ac->c); redisIvykisEvents *e; /* Nothing should be attached when something is already attached */ if (ac->ev.data != NULL) return REDIS_ERR; /* Create container for context and r/w events */ e = (redisIvykisEvents*)hi_malloc(sizeof(*e)); if (e == NULL) return REDIS_ERR; e->context = ac; /* Register functions to start/stop listening for events */ ac->ev.addRead = redisIvykisAddRead; ac->ev.delRead = redisIvykisDelRead; ac->ev.addWrite = redisIvykisAddWrite; ac->ev.delWrite = redisIvykisDelWrite; ac->ev.cleanup = redisIvykisCleanup; ac->ev.data = e; /* Initialize and install read/write events */ IV_FD_INIT(&e->fd); e->fd.fd = c->fd; e->fd.handler_in = redisIvykisReadEvent; e->fd.handler_out = redisIvykisWriteEvent; e->fd.handler_err = NULL; e->fd.cookie = e->context; iv_fd_register(&e->fd); return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/libev.h000066400000000000000000000125721515654015100323170ustar00rootroot00000000000000/* * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_LIBEV_H__ #define __HIREDIS_LIBEV_H__ #include #include #include #include "../hiredis.h" #include "../async.h" typedef struct redisLibevEvents { redisAsyncContext *context; struct ev_loop *loop; int reading, writing; ev_io rev, wev; ev_timer timer; } redisLibevEvents; static void redisLibevReadEvent(EV_P_ ev_io *watcher, int revents) { #if EV_MULTIPLICITY ((void)loop); #endif ((void)revents); redisLibevEvents *e = (redisLibevEvents*)watcher->data; redisAsyncHandleRead(e->context); } static void redisLibevWriteEvent(EV_P_ ev_io *watcher, int revents) { #if EV_MULTIPLICITY ((void)loop); #endif ((void)revents); redisLibevEvents *e = (redisLibevEvents*)watcher->data; redisAsyncHandleWrite(e->context); } static void redisLibevAddRead(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); if (!e->reading) { e->reading = 1; ev_io_start(EV_A_ &e->rev); } } static void redisLibevDelRead(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); if (e->reading) { e->reading = 0; ev_io_stop(EV_A_ &e->rev); } } static void redisLibevAddWrite(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); if (!e->writing) { e->writing = 1; ev_io_start(EV_A_ &e->wev); } } static void redisLibevDelWrite(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); if (e->writing) { e->writing = 0; ev_io_stop(EV_A_ &e->wev); } } static void redisLibevStopTimer(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); ev_timer_stop(EV_A_ &e->timer); } static void redisLibevCleanup(void *privdata) { redisLibevEvents *e = (redisLibevEvents*)privdata; redisLibevDelRead(privdata); redisLibevDelWrite(privdata); redisLibevStopTimer(privdata); hi_free(e); } static void redisLibevTimeout(EV_P_ ev_timer *timer, int revents) { ((void)revents); redisLibevEvents *e = (redisLibevEvents*)timer->data; redisAsyncHandleTimeout(e->context); } static void redisLibevSetTimeout(void *privdata, struct timeval tv) { redisLibevEvents *e = (redisLibevEvents*)privdata; struct ev_loop *loop = e->loop; ((void)loop); if (!ev_is_active(&e->timer)) { ev_init(&e->timer, redisLibevTimeout); e->timer.data = e; } e->timer.repeat = tv.tv_sec + tv.tv_usec / 1000000.00; ev_timer_again(EV_A_ &e->timer); } static int redisLibevAttach(EV_P_ redisAsyncContext *ac) { redisContext *c = &(ac->c); redisLibevEvents *e; /* Nothing should be attached when something is already attached */ if (ac->ev.data != NULL) return REDIS_ERR; /* Create container for context and r/w events */ e = (redisLibevEvents*)hi_calloc(1, sizeof(*e)); if (e == NULL) return REDIS_ERR; e->context = ac; #if EV_MULTIPLICITY e->loop = loop; #else e->loop = NULL; #endif e->rev.data = e; e->wev.data = e; /* Register functions to start/stop listening for events */ ac->ev.addRead = redisLibevAddRead; ac->ev.delRead = redisLibevDelRead; ac->ev.addWrite = redisLibevAddWrite; ac->ev.delWrite = redisLibevDelWrite; ac->ev.cleanup = redisLibevCleanup; ac->ev.scheduleTimer = redisLibevSetTimeout; ac->ev.data = e; /* Initialize read/write events */ ev_io_init(&e->rev,redisLibevReadEvent,c->fd,EV_READ); ev_io_init(&e->wev,redisLibevWriteEvent,c->fd,EV_WRITE); return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/libevent.h000066400000000000000000000127411515654015100330240ustar00rootroot00000000000000/* * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_LIBEVENT_H__ #define __HIREDIS_LIBEVENT_H__ #include #include "../hiredis.h" #include "../async.h" #define REDIS_LIBEVENT_DELETED 0x01 #define REDIS_LIBEVENT_ENTERED 0x02 typedef struct redisLibeventEvents { redisAsyncContext *context; struct event *ev; struct event_base *base; struct timeval tv; short flags; short state; } redisLibeventEvents; static void redisLibeventDestroy(redisLibeventEvents *e) { hi_free(e); } static void redisLibeventHandler(int fd, short event, void *arg) { ((void)fd); redisLibeventEvents *e = (redisLibeventEvents*)arg; e->state |= REDIS_LIBEVENT_ENTERED; #define CHECK_DELETED() if (e->state & REDIS_LIBEVENT_DELETED) {\ redisLibeventDestroy(e);\ return; \ } if ((event & EV_TIMEOUT) && (e->state & REDIS_LIBEVENT_DELETED) == 0) { redisAsyncHandleTimeout(e->context); CHECK_DELETED(); } if ((event & EV_READ) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) { redisAsyncHandleRead(e->context); CHECK_DELETED(); } if ((event & EV_WRITE) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) { redisAsyncHandleWrite(e->context); CHECK_DELETED(); } e->state &= ~REDIS_LIBEVENT_ENTERED; #undef CHECK_DELETED } static void redisLibeventUpdate(void *privdata, short flag, int isRemove) { redisLibeventEvents *e = (redisLibeventEvents *)privdata; const struct timeval *tv = e->tv.tv_sec || e->tv.tv_usec ? &e->tv : NULL; if (isRemove) { if ((e->flags & flag) == 0) { return; } else { e->flags &= ~flag; } } else { if (e->flags & flag) { return; } else { e->flags |= flag; } } event_del(e->ev); event_assign(e->ev, e->base, e->context->c.fd, e->flags | EV_PERSIST, redisLibeventHandler, privdata); event_add(e->ev, tv); } static void redisLibeventAddRead(void *privdata) { redisLibeventUpdate(privdata, EV_READ, 0); } static void redisLibeventDelRead(void *privdata) { redisLibeventUpdate(privdata, EV_READ, 1); } static void redisLibeventAddWrite(void *privdata) { redisLibeventUpdate(privdata, EV_WRITE, 0); } static void redisLibeventDelWrite(void *privdata) { redisLibeventUpdate(privdata, EV_WRITE, 1); } static void redisLibeventCleanup(void *privdata) { redisLibeventEvents *e = (redisLibeventEvents*)privdata; if (!e) { return; } event_del(e->ev); event_free(e->ev); e->ev = NULL; if (e->state & REDIS_LIBEVENT_ENTERED) { e->state |= REDIS_LIBEVENT_DELETED; } else { redisLibeventDestroy(e); } } static void redisLibeventSetTimeout(void *privdata, struct timeval tv) { redisLibeventEvents *e = (redisLibeventEvents *)privdata; short flags = e->flags; e->flags = 0; e->tv = tv; redisLibeventUpdate(e, flags, 0); } static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) { redisContext *c = &(ac->c); redisLibeventEvents *e; /* Nothing should be attached when something is already attached */ if (ac->ev.data != NULL) return REDIS_ERR; /* Create container for context and r/w events */ e = (redisLibeventEvents*)hi_calloc(1, sizeof(*e)); if (e == NULL) return REDIS_ERR; e->context = ac; /* Register functions to start/stop listening for events */ ac->ev.addRead = redisLibeventAddRead; ac->ev.delRead = redisLibeventDelRead; ac->ev.addWrite = redisLibeventAddWrite; ac->ev.delWrite = redisLibeventDelWrite; ac->ev.cleanup = redisLibeventCleanup; ac->ev.scheduleTimer = redisLibeventSetTimeout; ac->ev.data = e; /* Initialize and install read/write events */ e->ev = event_new(base, c->fd, EV_READ | EV_WRITE, redisLibeventHandler, e); e->base = base; return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/libuv.h000066400000000000000000000050241515654015100323310ustar00rootroot00000000000000#ifndef __HIREDIS_LIBUV_H__ #define __HIREDIS_LIBUV_H__ #include #include #include "../hiredis.h" #include "../async.h" #include typedef struct redisLibuvEvents { redisAsyncContext* context; uv_poll_t handle; int events; } redisLibuvEvents; static void redisLibuvPoll(uv_poll_t* handle, int status, int events) { redisLibuvEvents* p = (redisLibuvEvents*)handle->data; int ev = (status ? p->events : events); if (p->context != NULL && (ev & UV_READABLE)) { redisAsyncHandleRead(p->context); } if (p->context != NULL && (ev & UV_WRITABLE)) { redisAsyncHandleWrite(p->context); } } static void redisLibuvAddRead(void *privdata) { redisLibuvEvents* p = (redisLibuvEvents*)privdata; p->events |= UV_READABLE; uv_poll_start(&p->handle, p->events, redisLibuvPoll); } static void redisLibuvDelRead(void *privdata) { redisLibuvEvents* p = (redisLibuvEvents*)privdata; p->events &= ~UV_READABLE; if (p->events) { uv_poll_start(&p->handle, p->events, redisLibuvPoll); } else { uv_poll_stop(&p->handle); } } static void redisLibuvAddWrite(void *privdata) { redisLibuvEvents* p = (redisLibuvEvents*)privdata; p->events |= UV_WRITABLE; uv_poll_start(&p->handle, p->events, redisLibuvPoll); } static void redisLibuvDelWrite(void *privdata) { redisLibuvEvents* p = (redisLibuvEvents*)privdata; p->events &= ~UV_WRITABLE; if (p->events) { uv_poll_start(&p->handle, p->events, redisLibuvPoll); } else { uv_poll_stop(&p->handle); } } static void on_close(uv_handle_t* handle) { redisLibuvEvents* p = (redisLibuvEvents*)handle->data; hi_free(p); } static void redisLibuvCleanup(void *privdata) { redisLibuvEvents* p = (redisLibuvEvents*)privdata; p->context = NULL; // indicate that context might no longer exist uv_close((uv_handle_t*)&p->handle, on_close); } static int redisLibuvAttach(redisAsyncContext* ac, uv_loop_t* loop) { redisContext *c = &(ac->c); if (ac->ev.data != NULL) { return REDIS_ERR; } ac->ev.addRead = redisLibuvAddRead; ac->ev.delRead = redisLibuvDelRead; ac->ev.addWrite = redisLibuvAddWrite; ac->ev.delWrite = redisLibuvDelWrite; ac->ev.cleanup = redisLibuvCleanup; redisLibuvEvents* p = (redisLibuvEvents*)hi_malloc(sizeof(*p)); if (p == NULL) return REDIS_ERR; memset(p, 0, sizeof(*p)); if (uv_poll_init_socket(loop, &p->handle, c->fd) != 0) { return REDIS_ERR; } ac->ev.data = p; p->handle.data = p; p->context = ac; return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/macosx.h000066400000000000000000000074551515654015100325140ustar00rootroot00000000000000// // Created by Дмитрий Бахвалов on 13.07.15. // Copyright (c) 2015 Dmitry Bakhvalov. All rights reserved. // #ifndef __HIREDIS_MACOSX_H__ #define __HIREDIS_MACOSX_H__ #include #include "../hiredis.h" #include "../async.h" typedef struct { redisAsyncContext *context; CFSocketRef socketRef; CFRunLoopSourceRef sourceRef; } RedisRunLoop; static int freeRedisRunLoop(RedisRunLoop* redisRunLoop) { if( redisRunLoop != NULL ) { if( redisRunLoop->sourceRef != NULL ) { CFRunLoopSourceInvalidate(redisRunLoop->sourceRef); CFRelease(redisRunLoop->sourceRef); } if( redisRunLoop->socketRef != NULL ) { CFSocketInvalidate(redisRunLoop->socketRef); CFRelease(redisRunLoop->socketRef); } hi_free(redisRunLoop); } return REDIS_ERR; } static void redisMacOSAddRead(void *privdata) { RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata; CFSocketEnableCallBacks(redisRunLoop->socketRef, kCFSocketReadCallBack); } static void redisMacOSDelRead(void *privdata) { RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata; CFSocketDisableCallBacks(redisRunLoop->socketRef, kCFSocketReadCallBack); } static void redisMacOSAddWrite(void *privdata) { RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata; CFSocketEnableCallBacks(redisRunLoop->socketRef, kCFSocketWriteCallBack); } static void redisMacOSDelWrite(void *privdata) { RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata; CFSocketDisableCallBacks(redisRunLoop->socketRef, kCFSocketWriteCallBack); } static void redisMacOSCleanup(void *privdata) { RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata; freeRedisRunLoop(redisRunLoop); } static void redisMacOSAsyncCallback(CFSocketRef __unused s, CFSocketCallBackType callbackType, CFDataRef __unused address, const void __unused *data, void *info) { redisAsyncContext* context = (redisAsyncContext*) info; switch (callbackType) { case kCFSocketReadCallBack: redisAsyncHandleRead(context); break; case kCFSocketWriteCallBack: redisAsyncHandleWrite(context); break; default: break; } } static int redisMacOSAttach(redisAsyncContext *redisAsyncCtx, CFRunLoopRef runLoop) { redisContext *redisCtx = &(redisAsyncCtx->c); /* Nothing should be attached when something is already attached */ if( redisAsyncCtx->ev.data != NULL ) return REDIS_ERR; RedisRunLoop* redisRunLoop = (RedisRunLoop*) hi_calloc(1, sizeof(RedisRunLoop)); if (redisRunLoop == NULL) return REDIS_ERR; /* Setup redis stuff */ redisRunLoop->context = redisAsyncCtx; redisAsyncCtx->ev.addRead = redisMacOSAddRead; redisAsyncCtx->ev.delRead = redisMacOSDelRead; redisAsyncCtx->ev.addWrite = redisMacOSAddWrite; redisAsyncCtx->ev.delWrite = redisMacOSDelWrite; redisAsyncCtx->ev.cleanup = redisMacOSCleanup; redisAsyncCtx->ev.data = redisRunLoop; /* Initialize and install read/write events */ CFSocketContext socketCtx = { 0, redisAsyncCtx, NULL, NULL, NULL }; redisRunLoop->socketRef = CFSocketCreateWithNative(NULL, redisCtx->fd, kCFSocketReadCallBack | kCFSocketWriteCallBack, redisMacOSAsyncCallback, &socketCtx); if( !redisRunLoop->socketRef ) return freeRedisRunLoop(redisRunLoop); redisRunLoop->sourceRef = CFSocketCreateRunLoopSource(NULL, redisRunLoop->socketRef, 0); if( !redisRunLoop->sourceRef ) return freeRedisRunLoop(redisRunLoop); CFRunLoopAddSource(runLoop, redisRunLoop->sourceRef, kCFRunLoopDefaultMode); return REDIS_OK; } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/adapters/qt.h000066400000000000000000000102111515654015100316260ustar00rootroot00000000000000/*- * Copyright (C) 2014 Pietro Cerutti * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #ifndef __HIREDIS_QT_H__ #define __HIREDIS_QT_H__ #include #include "../async.h" static void RedisQtAddRead(void *); static void RedisQtDelRead(void *); static void RedisQtAddWrite(void *); static void RedisQtDelWrite(void *); static void RedisQtCleanup(void *); class RedisQtAdapter : public QObject { Q_OBJECT friend void RedisQtAddRead(void * adapter) { RedisQtAdapter * a = static_cast(adapter); a->addRead(); } friend void RedisQtDelRead(void * adapter) { RedisQtAdapter * a = static_cast(adapter); a->delRead(); } friend void RedisQtAddWrite(void * adapter) { RedisQtAdapter * a = static_cast(adapter); a->addWrite(); } friend void RedisQtDelWrite(void * adapter) { RedisQtAdapter * a = static_cast(adapter); a->delWrite(); } friend void RedisQtCleanup(void * adapter) { RedisQtAdapter * a = static_cast(adapter); a->cleanup(); } public: RedisQtAdapter(QObject * parent = 0) : QObject(parent), m_ctx(0), m_read(0), m_write(0) { } ~RedisQtAdapter() { if (m_ctx != 0) { m_ctx->ev.data = NULL; } } int setContext(redisAsyncContext * ac) { if (ac->ev.data != NULL) { return REDIS_ERR; } m_ctx = ac; m_ctx->ev.data = this; m_ctx->ev.addRead = RedisQtAddRead; m_ctx->ev.delRead = RedisQtDelRead; m_ctx->ev.addWrite = RedisQtAddWrite; m_ctx->ev.delWrite = RedisQtDelWrite; m_ctx->ev.cleanup = RedisQtCleanup; return REDIS_OK; } private: void addRead() { if (m_read) return; m_read = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Read, 0); connect(m_read, SIGNAL(activated(int)), this, SLOT(read())); } void delRead() { if (!m_read) return; delete m_read; m_read = 0; } void addWrite() { if (m_write) return; m_write = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Write, 0); connect(m_write, SIGNAL(activated(int)), this, SLOT(write())); } void delWrite() { if (!m_write) return; delete m_write; m_write = 0; } void cleanup() { delRead(); delWrite(); } private slots: void read() { redisAsyncHandleRead(m_ctx); } void write() { redisAsyncHandleWrite(m_ctx); } private: redisAsyncContext * m_ctx; QSocketNotifier * m_read; QSocketNotifier * m_write; }; #endif /* !__HIREDIS_QT_H__ */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/alloc.c000066400000000000000000000053601515654015100304750ustar00rootroot00000000000000/* * Copyright (c) 2020, Michael Grunder * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include "alloc.h" #include #include hiredisAllocFuncs hiredisAllocFns = { .mallocFn = malloc, .callocFn = calloc, .reallocFn = realloc, .strdupFn = strdup, .freeFn = free, }; /* Override hiredis' allocators with ones supplied by the user */ hiredisAllocFuncs hiredisSetAllocators(hiredisAllocFuncs *override) { hiredisAllocFuncs orig = hiredisAllocFns; hiredisAllocFns = *override; return orig; } /* Reset allocators to use libc defaults */ void hiredisResetAllocators(void) { hiredisAllocFns = (hiredisAllocFuncs) { .mallocFn = malloc, .callocFn = calloc, .reallocFn = realloc, .strdupFn = strdup, .freeFn = free, }; } #ifdef _WIN32 void *hi_malloc(size_t size) { return hiredisAllocFns.mallocFn(size); } void *hi_calloc(size_t nmemb, size_t size) { return hiredisAllocFns.callocFn(nmemb, size); } void *hi_realloc(void *ptr, size_t size) { return hiredisAllocFns.reallocFn(ptr, size); } char *hi_strdup(const char *str) { return hiredisAllocFns.strdupFn(str); } void hi_free(void *ptr) { hiredisAllocFns.freeFn(ptr); } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/alloc.h000066400000000000000000000056461515654015100305110ustar00rootroot00000000000000/* * Copyright (c) 2020, Michael Grunder * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef HIREDIS_ALLOC_H #define HIREDIS_ALLOC_H #include /* for size_t */ #ifdef __cplusplus extern "C" { #endif /* Structure pointing to our actually configured allocators */ typedef struct hiredisAllocFuncs { void *(*mallocFn)(size_t); void *(*callocFn)(size_t,size_t); void *(*reallocFn)(void*,size_t); char *(*strdupFn)(const char*); void (*freeFn)(void*); } hiredisAllocFuncs; hiredisAllocFuncs hiredisSetAllocators(hiredisAllocFuncs *ha); void hiredisResetAllocators(void); #ifndef _WIN32 /* Hiredis' configured allocator function pointer struct */ extern hiredisAllocFuncs hiredisAllocFns; static inline void *hi_malloc(size_t size) { return hiredisAllocFns.mallocFn(size); } static inline void *hi_calloc(size_t nmemb, size_t size) { return hiredisAllocFns.callocFn(nmemb, size); } static inline void *hi_realloc(void *ptr, size_t size) { return hiredisAllocFns.reallocFn(ptr, size); } static inline char *hi_strdup(const char *str) { return hiredisAllocFns.strdupFn(str); } static inline void hi_free(void *ptr) { hiredisAllocFns.freeFn(ptr); } #else void *hi_malloc(size_t size); void *hi_calloc(size_t nmemb, size_t size); void *hi_realloc(void *ptr, size_t size); char *hi_strdup(const char *str); void hi_free(void *ptr); #endif #ifdef __cplusplus } #endif #endif /* HIREDIS_ALLOC_H */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/appveyor.yml000066400000000000000000000013471515654015100316300ustar00rootroot00000000000000# Appveyor configuration file for CI build of hiredis on Windows (under Cygwin) environment: matrix: - CYG_BASH: C:\cygwin64\bin\bash CC: gcc - CYG_BASH: C:\cygwin\bin\bash CC: gcc CFLAGS: -m32 CXXFLAGS: -m32 LDFLAGS: -m32 clone_depth: 1 # Attempt to ensure we don't try to convert line endings to Win32 CRLF as this will cause build to fail init: - git config --global core.autocrlf input # Install needed build dependencies install: - '%CYG_BASH% -lc "cygcheck -dc cygwin"' build_script: - 'echo building...' - '%CYG_BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0 * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include "alloc.h" #include #include #ifndef _MSC_VER #include #endif #include #include #include #include "async.h" #include "net.h" #include "dict.c" #include "sds.h" #include "win32.h" #include "async_private.h" /* Forward declarations of hiredis.c functions */ int __redisAppendCommand(redisContext *c, const char *cmd, size_t len); void __redisSetError(redisContext *c, int type, const char *str); /* Functions managing dictionary of callbacks for pub/sub. */ static unsigned int callbackHash(const void *key) { return dictGenHashFunction((const unsigned char *)key, sdslen((const sds)key)); } static void *callbackValDup(void *privdata, const void *src) { ((void) privdata); redisCallback *dup; dup = hi_malloc(sizeof(*dup)); if (dup == NULL) return NULL; memcpy(dup,src,sizeof(*dup)); return dup; } static int callbackKeyCompare(void *privdata, const void *key1, const void *key2) { int l1, l2; ((void) privdata); l1 = sdslen((const sds)key1); l2 = sdslen((const sds)key2); if (l1 != l2) return 0; return memcmp(key1,key2,l1) == 0; } static void callbackKeyDestructor(void *privdata, void *key) { ((void) privdata); sdsfree((sds)key); } static void callbackValDestructor(void *privdata, void *val) { ((void) privdata); hi_free(val); } static dictType callbackDict = { callbackHash, NULL, callbackValDup, callbackKeyCompare, callbackKeyDestructor, callbackValDestructor }; static redisAsyncContext *redisAsyncInitialize(redisContext *c) { redisAsyncContext *ac; dict *channels = NULL, *patterns = NULL; channels = dictCreate(&callbackDict,NULL); if (channels == NULL) goto oom; patterns = dictCreate(&callbackDict,NULL); if (patterns == NULL) goto oom; ac = hi_realloc(c,sizeof(redisAsyncContext)); if (ac == NULL) goto oom; c = &(ac->c); /* The regular connect functions will always set the flag REDIS_CONNECTED. * For the async API, we want to wait until the first write event is * received up before setting this flag, so reset it here. */ c->flags &= ~REDIS_CONNECTED; ac->err = 0; ac->errstr = NULL; ac->data = NULL; ac->dataCleanup = NULL; ac->ev.data = NULL; ac->ev.addRead = NULL; ac->ev.delRead = NULL; ac->ev.addWrite = NULL; ac->ev.delWrite = NULL; ac->ev.cleanup = NULL; ac->ev.scheduleTimer = NULL; ac->onConnect = NULL; ac->onDisconnect = NULL; ac->replies.head = NULL; ac->replies.tail = NULL; ac->sub.invalid.head = NULL; ac->sub.invalid.tail = NULL; ac->sub.channels = channels; ac->sub.patterns = patterns; return ac; oom: if (channels) dictRelease(channels); if (patterns) dictRelease(patterns); return NULL; } /* We want the error field to be accessible directly instead of requiring * an indirection to the redisContext struct. */ static void __redisAsyncCopyError(redisAsyncContext *ac) { if (!ac) return; redisContext *c = &(ac->c); ac->err = c->err; ac->errstr = c->errstr; } redisAsyncContext *redisAsyncConnectWithOptions(const redisOptions *options) { redisOptions myOptions = *options; redisContext *c; redisAsyncContext *ac; /* Clear any erroneously set sync callback and flag that we don't want to * use freeReplyObject by default. */ myOptions.push_cb = NULL; myOptions.options |= REDIS_OPT_NO_PUSH_AUTOFREE; myOptions.options |= REDIS_OPT_NONBLOCK; c = redisConnectWithOptions(&myOptions); if (c == NULL) { return NULL; } ac = redisAsyncInitialize(c); if (ac == NULL) { redisFree(c); return NULL; } /* Set any configured async push handler */ redisAsyncSetPushCallback(ac, myOptions.async_push_cb); __redisAsyncCopyError(ac); return ac; } redisAsyncContext *redisAsyncConnect(const char *ip, int port) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); return redisAsyncConnectWithOptions(&options); } redisAsyncContext *redisAsyncConnectBind(const char *ip, int port, const char *source_addr) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.endpoint.tcp.source_addr = source_addr; return redisAsyncConnectWithOptions(&options); } redisAsyncContext *redisAsyncConnectBindWithReuse(const char *ip, int port, const char *source_addr) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.options |= REDIS_OPT_REUSEADDR; options.endpoint.tcp.source_addr = source_addr; return redisAsyncConnectWithOptions(&options); } redisAsyncContext *redisAsyncConnectUnix(const char *path) { redisOptions options = {0}; REDIS_OPTIONS_SET_UNIX(&options, path); return redisAsyncConnectWithOptions(&options); } int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) { if (ac->onConnect == NULL) { ac->onConnect = fn; /* The common way to detect an established connection is to wait for * the first write event to be fired. This assumes the related event * library functions are already set. */ _EL_ADD_WRITE(ac); return REDIS_OK; } return REDIS_ERR; } int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn) { if (ac->onDisconnect == NULL) { ac->onDisconnect = fn; return REDIS_OK; } return REDIS_ERR; } /* Helper functions to push/shift callbacks */ static int __redisPushCallback(redisCallbackList *list, redisCallback *source) { redisCallback *cb; /* Copy callback from stack to heap */ cb = hi_malloc(sizeof(*cb)); if (cb == NULL) return REDIS_ERR_OOM; if (source != NULL) { memcpy(cb,source,sizeof(*cb)); cb->next = NULL; } /* Store callback in list */ if (list->head == NULL) list->head = cb; if (list->tail != NULL) list->tail->next = cb; list->tail = cb; return REDIS_OK; } static int __redisShiftCallback(redisCallbackList *list, redisCallback *target) { redisCallback *cb = list->head; if (cb != NULL) { list->head = cb->next; if (cb == list->tail) list->tail = NULL; /* Copy callback from heap to stack */ if (target != NULL) memcpy(target,cb,sizeof(*cb)); hi_free(cb); return REDIS_OK; } return REDIS_ERR; } static void __redisRunCallback(redisAsyncContext *ac, redisCallback *cb, redisReply *reply) { redisContext *c = &(ac->c); if (cb->fn != NULL) { c->flags |= REDIS_IN_CALLBACK; cb->fn(ac,reply,cb->privdata); c->flags &= ~REDIS_IN_CALLBACK; } } static void __redisRunPushCallback(redisAsyncContext *ac, redisReply *reply) { if (ac->push_cb != NULL) { ac->c.flags |= REDIS_IN_CALLBACK; ac->push_cb(ac, reply); ac->c.flags &= ~REDIS_IN_CALLBACK; } } /* Helper function to free the context. */ static void __redisAsyncFree(redisAsyncContext *ac) { redisContext *c = &(ac->c); redisCallback cb; dictIterator *it; dictEntry *de; /* Execute pending callbacks with NULL reply. */ while (__redisShiftCallback(&ac->replies,&cb) == REDIS_OK) __redisRunCallback(ac,&cb,NULL); /* Execute callbacks for invalid commands */ while (__redisShiftCallback(&ac->sub.invalid,&cb) == REDIS_OK) __redisRunCallback(ac,&cb,NULL); /* Run subscription callbacks with NULL reply */ if (ac->sub.channels) { it = dictGetIterator(ac->sub.channels); if (it != NULL) { while ((de = dictNext(it)) != NULL) __redisRunCallback(ac,dictGetEntryVal(de),NULL); dictReleaseIterator(it); } dictRelease(ac->sub.channels); } if (ac->sub.patterns) { it = dictGetIterator(ac->sub.patterns); if (it != NULL) { while ((de = dictNext(it)) != NULL) __redisRunCallback(ac,dictGetEntryVal(de),NULL); dictReleaseIterator(it); } dictRelease(ac->sub.patterns); } /* Signal event lib to clean up */ _EL_CLEANUP(ac); /* Execute disconnect callback. When redisAsyncFree() initiated destroying * this context, the status will always be REDIS_OK. */ if (ac->onDisconnect && (c->flags & REDIS_CONNECTED)) { if (c->flags & REDIS_FREEING) { ac->onDisconnect(ac,REDIS_OK); } else { ac->onDisconnect(ac,(ac->err == 0) ? REDIS_OK : REDIS_ERR); } } if (ac->dataCleanup) { ac->dataCleanup(ac->data); } /* Cleanup self */ redisFree(c); } /* Free the async context. When this function is called from a callback, * control needs to be returned to redisProcessCallbacks() before actual * free'ing. To do so, a flag is set on the context which is picked up by * redisProcessCallbacks(). Otherwise, the context is immediately free'd. */ void redisAsyncFree(redisAsyncContext *ac) { redisContext *c = &(ac->c); c->flags |= REDIS_FREEING; if (!(c->flags & REDIS_IN_CALLBACK)) __redisAsyncFree(ac); } /* Helper function to make the disconnect happen and clean up. */ void __redisAsyncDisconnect(redisAsyncContext *ac) { redisContext *c = &(ac->c); /* Make sure error is accessible if there is any */ __redisAsyncCopyError(ac); if (ac->err == 0) { /* For clean disconnects, there should be no pending callbacks. */ int ret = __redisShiftCallback(&ac->replies,NULL); assert(ret == REDIS_ERR); } else { /* Disconnection is caused by an error, make sure that pending * callbacks cannot call new commands. */ c->flags |= REDIS_DISCONNECTING; } /* cleanup event library on disconnect. * this is safe to call multiple times */ _EL_CLEANUP(ac); /* For non-clean disconnects, __redisAsyncFree() will execute pending * callbacks with a NULL-reply. */ if (!(c->flags & REDIS_NO_AUTO_FREE)) { __redisAsyncFree(ac); } } /* Tries to do a clean disconnect from Redis, meaning it stops new commands * from being issued, but tries to flush the output buffer and execute * callbacks for all remaining replies. When this function is called from a * callback, there might be more replies and we can safely defer disconnecting * to redisProcessCallbacks(). Otherwise, we can only disconnect immediately * when there are no pending callbacks. */ void redisAsyncDisconnect(redisAsyncContext *ac) { redisContext *c = &(ac->c); c->flags |= REDIS_DISCONNECTING; /** unset the auto-free flag here, because disconnect undoes this */ c->flags &= ~REDIS_NO_AUTO_FREE; if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL) __redisAsyncDisconnect(ac); } static int __redisGetSubscribeCallback(redisAsyncContext *ac, redisReply *reply, redisCallback *dstcb) { redisContext *c = &(ac->c); dict *callbacks; redisCallback *cb; dictEntry *de; int pvariant; char *stype; sds sname; /* Custom reply functions are not supported for pub/sub. This will fail * very hard when they are used... */ if (reply->type == REDIS_REPLY_ARRAY || reply->type == REDIS_REPLY_PUSH) { assert(reply->elements >= 2); assert(reply->element[0]->type == REDIS_REPLY_STRING); stype = reply->element[0]->str; pvariant = (tolower(stype[0]) == 'p') ? 1 : 0; if (pvariant) callbacks = ac->sub.patterns; else callbacks = ac->sub.channels; /* Locate the right callback */ assert(reply->element[1]->type == REDIS_REPLY_STRING); sname = sdsnewlen(reply->element[1]->str,reply->element[1]->len); if (sname == NULL) goto oom; de = dictFind(callbacks,sname); if (de != NULL) { cb = dictGetEntryVal(de); /* If this is an subscribe reply decrease pending counter. */ if (strcasecmp(stype+pvariant,"subscribe") == 0) { cb->pending_subs -= 1; } memcpy(dstcb,cb,sizeof(*dstcb)); /* If this is an unsubscribe message, remove it. */ if (strcasecmp(stype+pvariant,"unsubscribe") == 0) { if (cb->pending_subs == 0) dictDelete(callbacks,sname); /* If this was the last unsubscribe message, revert to * non-subscribe mode. */ assert(reply->element[2]->type == REDIS_REPLY_INTEGER); /* Unset subscribed flag only when no pipelined pending subscribe. */ if (reply->element[2]->integer == 0 && dictSize(ac->sub.channels) == 0 && dictSize(ac->sub.patterns) == 0) c->flags &= ~REDIS_SUBSCRIBED; } } sdsfree(sname); } else { /* Shift callback for invalid commands. */ __redisShiftCallback(&ac->sub.invalid,dstcb); } return REDIS_OK; oom: __redisSetError(&(ac->c), REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } #define redisIsSpontaneousPushReply(r) \ (redisIsPushReply(r) && !redisIsSubscribeReply(r)) static int redisIsSubscribeReply(redisReply *reply) { char *str; size_t len, off; /* We will always have at least one string with the subscribe/message type */ if (reply->elements < 1 || reply->element[0]->type != REDIS_REPLY_STRING || reply->element[0]->len < sizeof("message") - 1) { return 0; } /* Get the string/len moving past 'p' if needed */ off = tolower(reply->element[0]->str[0]) == 'p'; str = reply->element[0]->str + off; len = reply->element[0]->len - off; return !strncasecmp(str, "subscribe", len) || !strncasecmp(str, "message", len); } void redisProcessCallbacks(redisAsyncContext *ac) { redisContext *c = &(ac->c); redisCallback cb = {NULL, NULL, 0, NULL}; void *reply = NULL; int status; while((status = redisGetReply(c,&reply)) == REDIS_OK) { if (reply == NULL) { /* When the connection is being disconnected and there are * no more replies, this is the cue to really disconnect. */ if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0 && ac->replies.head == NULL) { __redisAsyncDisconnect(ac); return; } /* If monitor mode, repush callback */ if(c->flags & REDIS_MONITORING) { __redisPushCallback(&ac->replies,&cb); } /* When the connection is not being disconnected, simply stop * trying to get replies and wait for the next loop tick. */ break; } /* Send any non-subscribe related PUSH messages to our PUSH handler * while allowing subscribe related PUSH messages to pass through. * This allows existing code to be backward compatible and work in * either RESP2 or RESP3 mode. */ if (redisIsSpontaneousPushReply(reply)) { __redisRunPushCallback(ac, reply); c->reader->fn->freeObject(reply); continue; } /* Even if the context is subscribed, pending regular * callbacks will get a reply before pub/sub messages arrive. */ if (__redisShiftCallback(&ac->replies,&cb) != REDIS_OK) { /* * A spontaneous reply in a not-subscribed context can be the error * reply that is sent when a new connection exceeds the maximum * number of allowed connections on the server side. * * This is seen as an error instead of a regular reply because the * server closes the connection after sending it. * * To prevent the error from being overwritten by an EOF error the * connection is closed here. See issue #43. * * Another possibility is that the server is loading its dataset. * In this case we also want to close the connection, and have the * user wait until the server is ready to take our request. */ if (((redisReply*)reply)->type == REDIS_REPLY_ERROR) { c->err = REDIS_ERR_OTHER; snprintf(c->errstr,sizeof(c->errstr),"%s",((redisReply*)reply)->str); c->reader->fn->freeObject(reply); __redisAsyncDisconnect(ac); return; } /* No more regular callbacks and no errors, the context *must* be subscribed or monitoring. */ assert((c->flags & REDIS_SUBSCRIBED || c->flags & REDIS_MONITORING)); if(c->flags & REDIS_SUBSCRIBED) __redisGetSubscribeCallback(ac,reply,&cb); } if (cb.fn != NULL) { __redisRunCallback(ac,&cb,reply); c->reader->fn->freeObject(reply); /* Proceed with free'ing when redisAsyncFree() was called. */ if (c->flags & REDIS_FREEING) { __redisAsyncFree(ac); return; } } else { /* No callback for this reply. This can either be a NULL callback, * or there were no callbacks to begin with. Either way, don't * abort with an error, but simply ignore it because the client * doesn't know what the server will spit out over the wire. */ c->reader->fn->freeObject(reply); } } /* Disconnect when there was an error reading the reply */ if (status != REDIS_OK) __redisAsyncDisconnect(ac); } static void __redisAsyncHandleConnectFailure(redisAsyncContext *ac) { if (ac->onConnect) ac->onConnect(ac, REDIS_ERR); __redisAsyncDisconnect(ac); } /* Internal helper function to detect socket status the first time a read or * write event fires. When connecting was not successful, the connect callback * is called with a REDIS_ERR status and the context is free'd. */ static int __redisAsyncHandleConnect(redisAsyncContext *ac) { int completed = 0; redisContext *c = &(ac->c); if (redisCheckConnectDone(c, &completed) == REDIS_ERR) { /* Error! */ redisCheckSocketError(c); __redisAsyncHandleConnectFailure(ac); return REDIS_ERR; } else if (completed == 1) { /* connected! */ if (c->connection_type == REDIS_CONN_TCP && redisSetTcpNoDelay(c) == REDIS_ERR) { __redisAsyncHandleConnectFailure(ac); return REDIS_ERR; } if (ac->onConnect) ac->onConnect(ac, REDIS_OK); c->flags |= REDIS_CONNECTED; return REDIS_OK; } else { return REDIS_OK; } } void redisAsyncRead(redisAsyncContext *ac) { redisContext *c = &(ac->c); if (redisBufferRead(c) == REDIS_ERR) { __redisAsyncDisconnect(ac); } else { /* Always re-schedule reads */ _EL_ADD_READ(ac); redisProcessCallbacks(ac); } } /* This function should be called when the socket is readable. * It processes all replies that can be read and executes their callbacks. */ void redisAsyncHandleRead(redisAsyncContext *ac) { redisContext *c = &(ac->c); if (!(c->flags & REDIS_CONNECTED)) { /* Abort connect was not successful. */ if (__redisAsyncHandleConnect(ac) != REDIS_OK) return; /* Try again later when the context is still not connected. */ if (!(c->flags & REDIS_CONNECTED)) return; } c->funcs->async_read(ac); } void redisAsyncWrite(redisAsyncContext *ac) { redisContext *c = &(ac->c); int done = 0; if (redisBufferWrite(c,&done) == REDIS_ERR) { __redisAsyncDisconnect(ac); } else { /* Continue writing when not done, stop writing otherwise */ if (!done) _EL_ADD_WRITE(ac); else _EL_DEL_WRITE(ac); /* Always schedule reads after writes */ _EL_ADD_READ(ac); } } void redisAsyncHandleWrite(redisAsyncContext *ac) { redisContext *c = &(ac->c); if (!(c->flags & REDIS_CONNECTED)) { /* Abort connect was not successful. */ if (__redisAsyncHandleConnect(ac) != REDIS_OK) return; /* Try again later when the context is still not connected. */ if (!(c->flags & REDIS_CONNECTED)) return; } c->funcs->async_write(ac); } void redisAsyncHandleTimeout(redisAsyncContext *ac) { redisContext *c = &(ac->c); redisCallback cb; if ((c->flags & REDIS_CONNECTED) && ac->replies.head == NULL) { /* Nothing to do - just an idle timeout */ return; } if (!c->err) { __redisSetError(c, REDIS_ERR_TIMEOUT, "Timeout"); } if (!(c->flags & REDIS_CONNECTED) && ac->onConnect) { ac->onConnect(ac, REDIS_ERR); } while (__redisShiftCallback(&ac->replies, &cb) == REDIS_OK) { __redisRunCallback(ac, &cb, NULL); } /** * TODO: Don't automatically sever the connection, * rather, allow to ignore responses before the queue is clear */ __redisAsyncDisconnect(ac); } /* Sets a pointer to the first argument and its length starting at p. Returns * the number of bytes to skip to get to the following argument. */ static const char *nextArgument(const char *start, const char **str, size_t *len) { const char *p = start; if (p[0] != '$') { p = strchr(p,'$'); if (p == NULL) return NULL; } *len = (int)strtol(p+1,NULL,10); p = strchr(p,'\r'); assert(p); *str = p+2; return p+2+(*len)+2; } /* Helper function for the redisAsyncCommand* family of functions. Writes a * formatted command to the output buffer and registers the provided callback * function with the context. */ static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len) { redisContext *c = &(ac->c); redisCallback cb; struct dict *cbdict; dictEntry *de; redisCallback *existcb; int pvariant, hasnext; const char *cstr, *astr; size_t clen, alen; const char *p; sds sname; int ret; /* Don't accept new commands when the connection is about to be closed. */ if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR; /* Setup callback */ cb.fn = fn; cb.privdata = privdata; cb.pending_subs = 1; /* Find out which command will be appended. */ p = nextArgument(cmd,&cstr,&clen); assert(p != NULL); hasnext = (p[0] == '$'); pvariant = (tolower(cstr[0]) == 'p') ? 1 : 0; cstr += pvariant; clen -= pvariant; if (hasnext && strncasecmp(cstr,"subscribe\r\n",11) == 0) { c->flags |= REDIS_SUBSCRIBED; /* Add every channel/pattern to the list of subscription callbacks. */ while ((p = nextArgument(p,&astr,&alen)) != NULL) { sname = sdsnewlen(astr,alen); if (sname == NULL) goto oom; if (pvariant) cbdict = ac->sub.patterns; else cbdict = ac->sub.channels; de = dictFind(cbdict,sname); if (de != NULL) { existcb = dictGetEntryVal(de); cb.pending_subs = existcb->pending_subs + 1; } ret = dictReplace(cbdict,sname,&cb); if (ret == 0) sdsfree(sname); } } else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) { /* It is only useful to call (P)UNSUBSCRIBE when the context is * subscribed to one or more channels or patterns. */ if (!(c->flags & REDIS_SUBSCRIBED)) return REDIS_ERR; /* (P)UNSUBSCRIBE does not have its own response: every channel or * pattern that is unsubscribed will receive a message. This means we * should not append a callback function for this command. */ } else if(strncasecmp(cstr,"monitor\r\n",9) == 0) { /* Set monitor flag and push callback */ c->flags |= REDIS_MONITORING; __redisPushCallback(&ac->replies,&cb); } else { if (c->flags & REDIS_SUBSCRIBED) /* This will likely result in an error reply, but it needs to be * received and passed to the callback. */ __redisPushCallback(&ac->sub.invalid,&cb); else __redisPushCallback(&ac->replies,&cb); } __redisAppendCommand(c,cmd,len); /* Always schedule a write when the write buffer is non-empty */ _EL_ADD_WRITE(ac); return REDIS_OK; oom: __redisSetError(&(ac->c), REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) { char *cmd; int len; int status; len = redisvFormatCommand(&cmd,format,ap); /* We don't want to pass -1 or -2 to future functions as a length. */ if (len < 0) return REDIS_ERR; status = __redisAsyncCommand(ac,fn,privdata,cmd,len); hi_free(cmd); return status; } int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) { va_list ap; int status; va_start(ap,format); status = redisvAsyncCommand(ac,fn,privdata,format,ap); va_end(ap); return status; } int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) { sds cmd; int len; int status; len = redisFormatSdsCommandArgv(&cmd,argc,argv,argvlen); if (len < 0) return REDIS_ERR; status = __redisAsyncCommand(ac,fn,privdata,cmd,len); sdsfree(cmd); return status; } int redisAsyncFormattedCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len) { int status = __redisAsyncCommand(ac,fn,privdata,cmd,len); return status; } redisAsyncPushFn *redisAsyncSetPushCallback(redisAsyncContext *ac, redisAsyncPushFn *fn) { redisAsyncPushFn *old = ac->push_cb; ac->push_cb = fn; return old; } int redisAsyncSetTimeout(redisAsyncContext *ac, struct timeval tv) { if (!ac->c.command_timeout) { ac->c.command_timeout = hi_calloc(1, sizeof(tv)); if (ac->c.command_timeout == NULL) { __redisSetError(&ac->c, REDIS_ERR_OOM, "Out of memory"); __redisAsyncCopyError(ac); return REDIS_ERR; } } if (tv.tv_sec != ac->c.command_timeout->tv_sec || tv.tv_usec != ac->c.command_timeout->tv_usec) { *ac->c.command_timeout = tv; } return REDIS_OK; } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/async.h000066400000000000000000000136131515654015100305250ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_ASYNC_H #define __HIREDIS_ASYNC_H #include "hiredis.h" #ifdef __cplusplus extern "C" { #endif struct redisAsyncContext; /* need forward declaration of redisAsyncContext */ struct dict; /* dictionary header is included in async.c */ /* Reply callback prototype and container */ typedef void (redisCallbackFn)(struct redisAsyncContext*, void*, void*); typedef struct redisCallback { struct redisCallback *next; /* simple singly linked list */ redisCallbackFn *fn; int pending_subs; void *privdata; } redisCallback; /* List of callbacks for either regular replies or pub/sub */ typedef struct redisCallbackList { redisCallback *head, *tail; } redisCallbackList; /* Connection callback prototypes */ typedef void (redisDisconnectCallback)(const struct redisAsyncContext*, int status); typedef void (redisConnectCallback)(const struct redisAsyncContext*, int status); typedef void(redisTimerCallback)(void *timer, void *privdata); /* Context for an async connection to Redis */ typedef struct redisAsyncContext { /* Hold the regular context, so it can be realloc'ed. */ redisContext c; /* Setup error flags so they can be used directly. */ int err; char *errstr; /* Not used by hiredis */ void *data; void (*dataCleanup)(void *privdata); /* Event library data and hooks */ struct { void *data; /* Hooks that are called when the library expects to start * reading/writing. These functions should be idempotent. */ void (*addRead)(void *privdata); void (*delRead)(void *privdata); void (*addWrite)(void *privdata); void (*delWrite)(void *privdata); void (*cleanup)(void *privdata); void (*scheduleTimer)(void *privdata, struct timeval tv); } ev; /* Called when either the connection is terminated due to an error or per * user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */ redisDisconnectCallback *onDisconnect; /* Called when the first write event was received. */ redisConnectCallback *onConnect; /* Regular command callbacks */ redisCallbackList replies; /* Address used for connect() */ struct sockaddr *saddr; size_t addrlen; /* Subscription callbacks */ struct { redisCallbackList invalid; struct dict *channels; struct dict *patterns; } sub; /* Any configured RESP3 PUSH handler */ redisAsyncPushFn *push_cb; } redisAsyncContext; /* Functions that proxy to hiredis */ redisAsyncContext *redisAsyncConnectWithOptions(const redisOptions *options); redisAsyncContext *redisAsyncConnect(const char *ip, int port); redisAsyncContext *redisAsyncConnectBind(const char *ip, int port, const char *source_addr); redisAsyncContext *redisAsyncConnectBindWithReuse(const char *ip, int port, const char *source_addr); redisAsyncContext *redisAsyncConnectUnix(const char *path); int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn); int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); redisAsyncPushFn *redisAsyncSetPushCallback(redisAsyncContext *ac, redisAsyncPushFn *fn); int redisAsyncSetTimeout(redisAsyncContext *ac, struct timeval tv); void redisAsyncDisconnect(redisAsyncContext *ac); void redisAsyncFree(redisAsyncContext *ac); /* Handle read/write events */ void redisAsyncHandleRead(redisAsyncContext *ac); void redisAsyncHandleWrite(redisAsyncContext *ac); void redisAsyncHandleTimeout(redisAsyncContext *ac); void redisAsyncRead(redisAsyncContext *ac); void redisAsyncWrite(redisAsyncContext *ac); /* Command functions for an async context. Write the command to the * output buffer and register the provided callback. */ int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap); int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...); int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen); int redisAsyncFormattedCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len); #ifdef __cplusplus } #endif #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/async_private.h000066400000000000000000000064501515654015100322600ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_ASYNC_PRIVATE_H #define __HIREDIS_ASYNC_PRIVATE_H #define _EL_ADD_READ(ctx) \ do { \ refreshTimeout(ctx); \ if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \ } while (0) #define _EL_DEL_READ(ctx) do { \ if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \ } while(0) #define _EL_ADD_WRITE(ctx) \ do { \ refreshTimeout(ctx); \ if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \ } while (0) #define _EL_DEL_WRITE(ctx) do { \ if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \ } while(0) #define _EL_CLEANUP(ctx) do { \ if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \ ctx->ev.cleanup = NULL; \ } while(0); static inline void refreshTimeout(redisAsyncContext *ctx) { #define REDIS_TIMER_ISSET(tvp) \ (tvp && ((tvp)->tv_sec || (tvp)->tv_usec)) #define REDIS_EL_TIMER(ac, tvp) \ if ((ac)->ev.scheduleTimer && REDIS_TIMER_ISSET(tvp)) { \ (ac)->ev.scheduleTimer((ac)->ev.data, *(tvp)); \ } if (ctx->c.flags & REDIS_CONNECTED) { REDIS_EL_TIMER(ctx, ctx->c.command_timeout); } else { REDIS_EL_TIMER(ctx, ctx->c.connect_timeout); } } void __redisAsyncDisconnect(redisAsyncContext *ac); void redisProcessCallbacks(redisAsyncContext *ac); #endif /* __HIREDIS_ASYNC_PRIVATE_H */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/dict.c000066400000000000000000000251121515654015100303230ustar00rootroot00000000000000/* Hash table implementation. * * This file implements in memory hash tables with insert/del/replace/find/ * get-random-element operations. Hash tables will auto resize if needed * tables of power of two in size are used, collisions are handled by * chaining. See the source code for more information... :) * * Copyright (c) 2006-2010, Salvatore Sanfilippo * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include "alloc.h" #include #include #include #include "dict.h" /* -------------------------- private prototypes ---------------------------- */ static int _dictExpandIfNeeded(dict *ht); static unsigned long _dictNextPower(unsigned long size); static int _dictKeyIndex(dict *ht, const void *key); static int _dictInit(dict *ht, dictType *type, void *privDataPtr); /* -------------------------- hash functions -------------------------------- */ /* Generic hash function (a popular one from Bernstein). * I tested a few and this was the best. */ static unsigned int dictGenHashFunction(const unsigned char *buf, int len) { unsigned int hash = 5381; while (len--) hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ return hash; } /* ----------------------------- API implementation ------------------------- */ /* Reset an hashtable already initialized with ht_init(). * NOTE: This function should only called by ht_destroy(). */ static void _dictReset(dict *ht) { ht->table = NULL; ht->size = 0; ht->sizemask = 0; ht->used = 0; } /* Create a new hash table */ static dict *dictCreate(dictType *type, void *privDataPtr) { dict *ht = hi_malloc(sizeof(*ht)); if (ht == NULL) return NULL; _dictInit(ht,type,privDataPtr); return ht; } /* Initialize the hash table */ static int _dictInit(dict *ht, dictType *type, void *privDataPtr) { _dictReset(ht); ht->type = type; ht->privdata = privDataPtr; return DICT_OK; } /* Expand or create the hashtable */ static int dictExpand(dict *ht, unsigned long size) { dict n; /* the new hashtable */ unsigned long realsize = _dictNextPower(size), i; /* the size is invalid if it is smaller than the number of * elements already inside the hashtable */ if (ht->used > size) return DICT_ERR; _dictInit(&n, ht->type, ht->privdata); n.size = realsize; n.sizemask = realsize-1; n.table = hi_calloc(realsize,sizeof(dictEntry*)); if (n.table == NULL) return DICT_ERR; /* Copy all the elements from the old to the new table: * note that if the old hash table is empty ht->size is zero, * so dictExpand just creates an hash table. */ n.used = ht->used; for (i = 0; i < ht->size && ht->used > 0; i++) { dictEntry *he, *nextHe; if (ht->table[i] == NULL) continue; /* For each hash entry on this slot... */ he = ht->table[i]; while(he) { unsigned int h; nextHe = he->next; /* Get the new element index */ h = dictHashKey(ht, he->key) & n.sizemask; he->next = n.table[h]; n.table[h] = he; ht->used--; /* Pass to the next element */ he = nextHe; } } assert(ht->used == 0); hi_free(ht->table); /* Remap the new hashtable in the old */ *ht = n; return DICT_OK; } /* Add an element to the target hash table */ static int dictAdd(dict *ht, void *key, void *val) { int index; dictEntry *entry; /* Get the index of the new element, or -1 if * the element already exists. */ if ((index = _dictKeyIndex(ht, key)) == -1) return DICT_ERR; /* Allocates the memory and stores key */ entry = hi_malloc(sizeof(*entry)); if (entry == NULL) return DICT_ERR; entry->next = ht->table[index]; ht->table[index] = entry; /* Set the hash entry fields. */ dictSetHashKey(ht, entry, key); dictSetHashVal(ht, entry, val); ht->used++; return DICT_OK; } /* Add an element, discarding the old if the key already exists. * Return 1 if the key was added from scratch, 0 if there was already an * element with such key and dictReplace() just performed a value update * operation. */ static int dictReplace(dict *ht, void *key, void *val) { dictEntry *entry, auxentry; /* Try to add the element. If the key * does not exists dictAdd will succeed. */ if (dictAdd(ht, key, val) == DICT_OK) return 1; /* It already exists, get the entry */ entry = dictFind(ht, key); if (entry == NULL) return 0; /* Free the old value and set the new one */ /* Set the new value and free the old one. Note that it is important * to do that in this order, as the value may just be exactly the same * as the previous one. In this context, think to reference counting, * you want to increment (set), and then decrement (free), and not the * reverse. */ auxentry = *entry; dictSetHashVal(ht, entry, val); dictFreeEntryVal(ht, &auxentry); return 0; } /* Search and remove an element */ static int dictDelete(dict *ht, const void *key) { unsigned int h; dictEntry *de, *prevde; if (ht->size == 0) return DICT_ERR; h = dictHashKey(ht, key) & ht->sizemask; de = ht->table[h]; prevde = NULL; while(de) { if (dictCompareHashKeys(ht,key,de->key)) { /* Unlink the element from the list */ if (prevde) prevde->next = de->next; else ht->table[h] = de->next; dictFreeEntryKey(ht,de); dictFreeEntryVal(ht,de); hi_free(de); ht->used--; return DICT_OK; } prevde = de; de = de->next; } return DICT_ERR; /* not found */ } /* Destroy an entire hash table */ static int _dictClear(dict *ht) { unsigned long i; /* Free all the elements */ for (i = 0; i < ht->size && ht->used > 0; i++) { dictEntry *he, *nextHe; if ((he = ht->table[i]) == NULL) continue; while(he) { nextHe = he->next; dictFreeEntryKey(ht, he); dictFreeEntryVal(ht, he); hi_free(he); ht->used--; he = nextHe; } } /* Free the table and the allocated cache structure */ hi_free(ht->table); /* Re-initialize the table */ _dictReset(ht); return DICT_OK; /* never fails */ } /* Clear & Release the hash table */ static void dictRelease(dict *ht) { _dictClear(ht); hi_free(ht); } static dictEntry *dictFind(dict *ht, const void *key) { dictEntry *he; unsigned int h; if (ht->size == 0) return NULL; h = dictHashKey(ht, key) & ht->sizemask; he = ht->table[h]; while(he) { if (dictCompareHashKeys(ht, key, he->key)) return he; he = he->next; } return NULL; } static dictIterator *dictGetIterator(dict *ht) { dictIterator *iter = hi_malloc(sizeof(*iter)); if (iter == NULL) return NULL; iter->ht = ht; iter->index = -1; iter->entry = NULL; iter->nextEntry = NULL; return iter; } static dictEntry *dictNext(dictIterator *iter) { while (1) { if (iter->entry == NULL) { iter->index++; if (iter->index >= (signed)iter->ht->size) break; iter->entry = iter->ht->table[iter->index]; } else { iter->entry = iter->nextEntry; } if (iter->entry) { /* We need to save the 'next' here, the iterator user * may delete the entry we are returning. */ iter->nextEntry = iter->entry->next; return iter->entry; } } return NULL; } static void dictReleaseIterator(dictIterator *iter) { hi_free(iter); } /* ------------------------- private functions ------------------------------ */ /* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *ht) { /* If the hash table is empty expand it to the initial size, * if the table is "full" double its size. */ if (ht->size == 0) return dictExpand(ht, DICT_HT_INITIAL_SIZE); if (ht->used == ht->size) return dictExpand(ht, ht->size*2); return DICT_OK; } /* Our hash table capability is a power of two */ static unsigned long _dictNextPower(unsigned long size) { unsigned long i = DICT_HT_INITIAL_SIZE; if (size >= LONG_MAX) return LONG_MAX; while(1) { if (i >= size) return i; i *= 2; } } /* Returns the index of a free slot that can be populated with * an hash entry for the given 'key'. * If the key already exists, -1 is returned. */ static int _dictKeyIndex(dict *ht, const void *key) { unsigned int h; dictEntry *he; /* Expand the hashtable if needed */ if (_dictExpandIfNeeded(ht) == DICT_ERR) return -1; /* Compute the key hash value */ h = dictHashKey(ht, key) & ht->sizemask; /* Search if this slot does not already contain the given key */ he = ht->table[h]; while(he) { if (dictCompareHashKeys(ht, key, he->key)) return -1; he = he->next; } return h; } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/dict.h000066400000000000000000000111231515654015100303250ustar00rootroot00000000000000/* Hash table implementation. * * This file implements in memory hash tables with insert/del/replace/find/ * get-random-element operations. Hash tables will auto resize if needed * tables of power of two in size are used, collisions are handled by * chaining. See the source code for more information... :) * * Copyright (c) 2006-2010, Salvatore Sanfilippo * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __DICT_H #define __DICT_H #define DICT_OK 0 #define DICT_ERR 1 /* Unused arguments generate annoying warnings... */ #define DICT_NOTUSED(V) ((void) V) typedef struct dictEntry { void *key; void *val; struct dictEntry *next; } dictEntry; typedef struct dictType { unsigned int (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); } dictType; typedef struct dict { dictEntry **table; dictType *type; unsigned long size; unsigned long sizemask; unsigned long used; void *privdata; } dict; typedef struct dictIterator { dict *ht; int index; dictEntry *entry, *nextEntry; } dictIterator; /* This is the initial size of every hash table */ #define DICT_HT_INITIAL_SIZE 4 /* ------------------------------- Macros ------------------------------------*/ #define dictFreeEntryVal(ht, entry) \ if ((ht)->type->valDestructor) \ (ht)->type->valDestructor((ht)->privdata, (entry)->val) #define dictSetHashVal(ht, entry, _val_) do { \ if ((ht)->type->valDup) \ entry->val = (ht)->type->valDup((ht)->privdata, _val_); \ else \ entry->val = (_val_); \ } while(0) #define dictFreeEntryKey(ht, entry) \ if ((ht)->type->keyDestructor) \ (ht)->type->keyDestructor((ht)->privdata, (entry)->key) #define dictSetHashKey(ht, entry, _key_) do { \ if ((ht)->type->keyDup) \ entry->key = (ht)->type->keyDup((ht)->privdata, _key_); \ else \ entry->key = (_key_); \ } while(0) #define dictCompareHashKeys(ht, key1, key2) \ (((ht)->type->keyCompare) ? \ (ht)->type->keyCompare((ht)->privdata, key1, key2) : \ (key1) == (key2)) #define dictHashKey(ht, key) (ht)->type->hashFunction(key) #define dictGetEntryKey(he) ((he)->key) #define dictGetEntryVal(he) ((he)->val) #define dictSlots(ht) ((ht)->size) #define dictSize(ht) ((ht)->used) /* API */ static unsigned int dictGenHashFunction(const unsigned char *buf, int len); static dict *dictCreate(dictType *type, void *privDataPtr); static int dictExpand(dict *ht, unsigned long size); static int dictAdd(dict *ht, void *key, void *val); static int dictReplace(dict *ht, void *key, void *val); static int dictDelete(dict *ht, const void *key); static void dictRelease(dict *ht); static dictEntry * dictFind(dict *ht, const void *key); static dictIterator *dictGetIterator(dict *ht); static dictEntry *dictNext(dictIterator *iter); static void dictReleaseIterator(dictIterator *iter); #endif /* __DICT_H */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/fmacros.h000066400000000000000000000003411515654015100310340ustar00rootroot00000000000000#ifndef __HIREDIS_FMACRO_H #define __HIREDIS_FMACRO_H #define _XOPEN_SOURCE 600 #define _POSIX_C_SOURCE 200112L #if defined(__APPLE__) && defined(__MACH__) /* Enable TCP_KEEPALIVE */ #define _DARWIN_C_SOURCE #endif #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis-config.cmake.in000066400000000000000000000004641515654015100335400ustar00rootroot00000000000000@PACKAGE_INIT@ set_and_check(hiredis_INCLUDEDIR "@PACKAGE_INCLUDE_INSTALL_DIR@") IF (NOT TARGET hiredis::hiredis) INCLUDE(${CMAKE_CURRENT_LIST_DIR}/hiredis-targets.cmake) ENDIF() SET(hiredis_LIBRARIES hiredis::hiredis) SET(hiredis_INCLUDE_DIRS ${hiredis_INCLUDEDIR}) check_required_components(hiredis) redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis.c000066400000000000000000001053031515654015100310300ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2014, Pieter Noordhuis * Copyright (c) 2015, Matt Stancliff , * Jan-Erik Rediger * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include #include #include #include #include #include "hiredis.h" #include "net.h" #include "sds.h" #include "async.h" #include "win32.h" extern int redisContextUpdateConnectTimeout(redisContext *c, const struct timeval *timeout); extern int redisContextUpdateCommandTimeout(redisContext *c, const struct timeval *timeout); static redisContextFuncs redisContextDefaultFuncs = { .free_privctx = NULL, .async_read = redisAsyncRead, .async_write = redisAsyncWrite, .read = redisNetRead, .write = redisNetWrite }; static redisReply *createReplyObject(int type); static void *createStringObject(const redisReadTask *task, char *str, size_t len); static void *createArrayObject(const redisReadTask *task, size_t elements); static void *createIntegerObject(const redisReadTask *task, long long value); static void *createDoubleObject(const redisReadTask *task, double value, char *str, size_t len); static void *createNilObject(const redisReadTask *task); static void *createBoolObject(const redisReadTask *task, int bval); /* Default set of functions to build the reply. Keep in mind that such a * function returning NULL is interpreted as OOM. */ static redisReplyObjectFunctions defaultFunctions = { createStringObject, createArrayObject, createIntegerObject, createDoubleObject, createNilObject, createBoolObject, freeReplyObject }; /* Create a reply object */ static redisReply *createReplyObject(int type) { redisReply *r = hi_calloc(1,sizeof(*r)); if (r == NULL) return NULL; r->type = type; return r; } /* Free a reply object */ void freeReplyObject(void *reply) { redisReply *r = reply; size_t j; if (r == NULL) return; switch(r->type) { case REDIS_REPLY_INTEGER: break; /* Nothing to free */ case REDIS_REPLY_ARRAY: case REDIS_REPLY_MAP: case REDIS_REPLY_SET: case REDIS_REPLY_PUSH: if (r->element != NULL) { for (j = 0; j < r->elements; j++) freeReplyObject(r->element[j]); hi_free(r->element); } break; case REDIS_REPLY_ERROR: case REDIS_REPLY_STATUS: case REDIS_REPLY_STRING: case REDIS_REPLY_DOUBLE: case REDIS_REPLY_VERB: hi_free(r->str); break; } hi_free(r); } static void *createStringObject(const redisReadTask *task, char *str, size_t len) { redisReply *r, *parent; char *buf; r = createReplyObject(task->type); if (r == NULL) return NULL; assert(task->type == REDIS_REPLY_ERROR || task->type == REDIS_REPLY_STATUS || task->type == REDIS_REPLY_STRING || task->type == REDIS_REPLY_VERB); /* Copy string value */ if (task->type == REDIS_REPLY_VERB) { buf = hi_malloc(len-4+1); /* Skip 4 bytes of verbatim type header. */ if (buf == NULL) goto oom; memcpy(r->vtype,str,3); r->vtype[3] = '\0'; memcpy(buf,str+4,len-4); buf[len-4] = '\0'; r->len = len - 4; } else { buf = hi_malloc(len+1); if (buf == NULL) goto oom; memcpy(buf,str,len); buf[len] = '\0'; r->len = len; } r->str = buf; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET || parent->type == REDIS_REPLY_PUSH); parent->element[task->idx] = r; } return r; oom: freeReplyObject(r); return NULL; } static void *createArrayObject(const redisReadTask *task, size_t elements) { redisReply *r, *parent; r = createReplyObject(task->type); if (r == NULL) return NULL; if (elements > 0) { if (SIZE_MAX / sizeof(redisReply*) < elements) return NULL; /* Don't overflow */ r->element = hi_calloc(elements,sizeof(redisReply*)); if (r->element == NULL) { freeReplyObject(r); return NULL; } } r->elements = elements; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET || parent->type == REDIS_REPLY_PUSH); parent->element[task->idx] = r; } return r; } static void *createIntegerObject(const redisReadTask *task, long long value) { redisReply *r, *parent; r = createReplyObject(REDIS_REPLY_INTEGER); if (r == NULL) return NULL; r->integer = value; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET || parent->type == REDIS_REPLY_PUSH); parent->element[task->idx] = r; } return r; } static void *createDoubleObject(const redisReadTask *task, double value, char *str, size_t len) { redisReply *r, *parent; r = createReplyObject(REDIS_REPLY_DOUBLE); if (r == NULL) return NULL; r->dval = value; r->str = hi_malloc(len+1); if (r->str == NULL) { freeReplyObject(r); return NULL; } /* The double reply also has the original protocol string representing a * double as a null terminated string. This way the caller does not need * to format back for string conversion, especially since Redis does efforts * to make the string more human readable avoiding the calssical double * decimal string conversion artifacts. */ memcpy(r->str, str, len); r->str[len] = '\0'; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET); parent->element[task->idx] = r; } return r; } static void *createNilObject(const redisReadTask *task) { redisReply *r, *parent; r = createReplyObject(REDIS_REPLY_NIL); if (r == NULL) return NULL; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET); parent->element[task->idx] = r; } return r; } static void *createBoolObject(const redisReadTask *task, int bval) { redisReply *r, *parent; r = createReplyObject(REDIS_REPLY_BOOL); if (r == NULL) return NULL; r->integer = bval != 0; if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY || parent->type == REDIS_REPLY_MAP || parent->type == REDIS_REPLY_SET); parent->element[task->idx] = r; } return r; } /* Return the number of digits of 'v' when converted to string in radix 10. * Implementation borrowed from link in redis/src/util.c:string2ll(). */ static uint32_t countDigits(uint64_t v) { uint32_t result = 1; for (;;) { if (v < 10) return result; if (v < 100) return result + 1; if (v < 1000) return result + 2; if (v < 10000) return result + 3; v /= 10000U; result += 4; } } /* Helper that calculates the bulk length given a certain string length. */ static size_t bulklen(size_t len) { return 1+countDigits(len)+2+len+2; } int redisvFormatCommand(char **target, const char *format, va_list ap) { const char *c = format; char *cmd = NULL; /* final command */ int pos; /* position in final command */ sds curarg, newarg; /* current argument */ int touched = 0; /* was the current argument touched? */ char **curargv = NULL, **newargv = NULL; int argc = 0; int totlen = 0; int error_type = 0; /* 0 = no error; -1 = memory error; -2 = format error */ int j; /* Abort if there is not target to set */ if (target == NULL) return -1; /* Build the command string accordingly to protocol */ curarg = sdsempty(); if (curarg == NULL) return -1; while(*c != '\0') { if (*c != '%' || c[1] == '\0') { if (*c == ' ') { if (touched) { newargv = hi_realloc(curargv,sizeof(char*)*(argc+1)); if (newargv == NULL) goto memory_err; curargv = newargv; curargv[argc++] = curarg; totlen += bulklen(sdslen(curarg)); /* curarg is put in argv so it can be overwritten. */ curarg = sdsempty(); if (curarg == NULL) goto memory_err; touched = 0; } } else { newarg = sdscatlen(curarg,c,1); if (newarg == NULL) goto memory_err; curarg = newarg; touched = 1; } } else { char *arg; size_t size; /* Set newarg so it can be checked even if it is not touched. */ newarg = curarg; switch(c[1]) { case 's': arg = va_arg(ap,char*); size = strlen(arg); if (size > 0) newarg = sdscatlen(curarg,arg,size); break; case 'b': arg = va_arg(ap,char*); size = va_arg(ap,size_t); if (size > 0) newarg = sdscatlen(curarg,arg,size); break; case '%': newarg = sdscat(curarg,"%"); break; default: /* Try to detect printf format */ { static const char intfmts[] = "diouxX"; static const char flags[] = "#0-+ "; char _format[16]; const char *_p = c+1; size_t _l = 0; va_list _cpy; /* Flags */ while (*_p != '\0' && strchr(flags,*_p) != NULL) _p++; /* Field width */ while (*_p != '\0' && isdigit(*_p)) _p++; /* Precision */ if (*_p == '.') { _p++; while (*_p != '\0' && isdigit(*_p)) _p++; } /* Copy va_list before consuming with va_arg */ va_copy(_cpy,ap); /* Integer conversion (without modifiers) */ if (strchr(intfmts,*_p) != NULL) { va_arg(ap,int); goto fmt_valid; } /* Double conversion (without modifiers) */ if (strchr("eEfFgGaA",*_p) != NULL) { va_arg(ap,double); goto fmt_valid; } /* Size: char */ if (_p[0] == 'h' && _p[1] == 'h') { _p += 2; if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { va_arg(ap,int); /* char gets promoted to int */ goto fmt_valid; } goto fmt_invalid; } /* Size: short */ if (_p[0] == 'h') { _p += 1; if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { va_arg(ap,int); /* short gets promoted to int */ goto fmt_valid; } goto fmt_invalid; } /* Size: long long */ if (_p[0] == 'l' && _p[1] == 'l') { _p += 2; if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { va_arg(ap,long long); goto fmt_valid; } goto fmt_invalid; } /* Size: long */ if (_p[0] == 'l') { _p += 1; if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { va_arg(ap,long); goto fmt_valid; } goto fmt_invalid; } fmt_invalid: va_end(_cpy); goto format_err; fmt_valid: _l = (_p+1)-c; if (_l < sizeof(_format)-2) { memcpy(_format,c,_l); _format[_l] = '\0'; newarg = sdscatvprintf(curarg,_format,_cpy); /* Update current position (note: outer blocks * increment c twice so compensate here) */ c = _p-1; } va_end(_cpy); break; } } if (newarg == NULL) goto memory_err; curarg = newarg; touched = 1; c++; } c++; } /* Add the last argument if needed */ if (touched) { newargv = hi_realloc(curargv,sizeof(char*)*(argc+1)); if (newargv == NULL) goto memory_err; curargv = newargv; curargv[argc++] = curarg; totlen += bulklen(sdslen(curarg)); } else { sdsfree(curarg); } /* Clear curarg because it was put in curargv or was free'd. */ curarg = NULL; /* Add bytes needed to hold multi bulk count */ totlen += 1+countDigits(argc)+2; /* Build the command at protocol level */ cmd = hi_malloc(totlen+1); if (cmd == NULL) goto memory_err; pos = sprintf(cmd,"*%d\r\n",argc); for (j = 0; j < argc; j++) { pos += sprintf(cmd+pos,"$%zu\r\n",sdslen(curargv[j])); memcpy(cmd+pos,curargv[j],sdslen(curargv[j])); pos += sdslen(curargv[j]); sdsfree(curargv[j]); cmd[pos++] = '\r'; cmd[pos++] = '\n'; } assert(pos == totlen); cmd[pos] = '\0'; hi_free(curargv); *target = cmd; return totlen; format_err: error_type = -2; goto cleanup; memory_err: error_type = -1; goto cleanup; cleanup: if (curargv) { while(argc--) sdsfree(curargv[argc]); hi_free(curargv); } sdsfree(curarg); hi_free(cmd); return error_type; } /* Format a command according to the Redis protocol. This function * takes a format similar to printf: * * %s represents a C null terminated string you want to interpolate * %b represents a binary safe string * * When using %b you need to provide both the pointer to the string * and the length in bytes as a size_t. Examples: * * len = redisFormatCommand(target, "GET %s", mykey); * len = redisFormatCommand(target, "SET %s %b", mykey, myval, myvallen); */ int redisFormatCommand(char **target, const char *format, ...) { va_list ap; int len; va_start(ap,format); len = redisvFormatCommand(target,format,ap); va_end(ap); /* The API says "-1" means bad result, but we now also return "-2" in some * cases. Force the return value to always be -1. */ if (len < 0) len = -1; return len; } /* Format a command according to the Redis protocol using an sds string and * sdscatfmt for the processing of arguments. This function takes the * number of arguments, an array with arguments and an array with their * lengths. If the latter is set to NULL, strlen will be used to compute the * argument lengths. */ int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv, const size_t *argvlen) { sds cmd, aux; unsigned long long totlen; int j; size_t len; /* Abort on a NULL target */ if (target == NULL) return -1; /* Calculate our total size */ totlen = 1+countDigits(argc)+2; for (j = 0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); totlen += bulklen(len); } /* Use an SDS string for command construction */ cmd = sdsempty(); if (cmd == NULL) return -1; /* We already know how much storage we need */ aux = sdsMakeRoomFor(cmd, totlen); if (aux == NULL) { sdsfree(cmd); return -1; } cmd = aux; /* Construct command */ cmd = sdscatfmt(cmd, "*%i\r\n", argc); for (j=0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); cmd = sdscatfmt(cmd, "$%u\r\n", len); cmd = sdscatlen(cmd, argv[j], len); cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1); } assert(sdslen(cmd)==totlen); *target = cmd; return totlen; } void redisFreeSdsCommand(sds cmd) { sdsfree(cmd); } /* Format a command according to the Redis protocol. This function takes the * number of arguments, an array with arguments and an array with their * lengths. If the latter is set to NULL, strlen will be used to compute the * argument lengths. */ int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) { char *cmd = NULL; /* final command */ int pos; /* position in final command */ size_t len; int totlen, j; /* Abort on a NULL target */ if (target == NULL) return -1; /* Calculate number of bytes needed for the command */ totlen = 1+countDigits(argc)+2; for (j = 0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); totlen += bulklen(len); } /* Build the command at protocol level */ cmd = hi_malloc(totlen+1); if (cmd == NULL) return -1; pos = sprintf(cmd,"*%d\r\n",argc); for (j = 0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); pos += sprintf(cmd+pos,"$%zu\r\n",len); memcpy(cmd+pos,argv[j],len); pos += len; cmd[pos++] = '\r'; cmd[pos++] = '\n'; } assert(pos == totlen); cmd[pos] = '\0'; *target = cmd; return totlen; } void redisFreeCommand(char *cmd) { hi_free(cmd); } void __redisSetError(redisContext *c, int type, const char *str) { size_t len; c->err = type; if (str != NULL) { len = strlen(str); len = len < (sizeof(c->errstr)-1) ? len : (sizeof(c->errstr)-1); memcpy(c->errstr,str,len); c->errstr[len] = '\0'; } else { /* Only REDIS_ERR_IO may lack a description! */ assert(type == REDIS_ERR_IO); strerror_r(errno, c->errstr, sizeof(c->errstr)); } } redisReader *redisReaderCreate(void) { return redisReaderCreateWithFunctions(&defaultFunctions); } static void redisPushAutoFree(void *privdata, void *reply) { (void)privdata; freeReplyObject(reply); } static redisContext *redisContextInit(void) { redisContext *c; c = hi_calloc(1, sizeof(*c)); if (c == NULL) return NULL; c->funcs = &redisContextDefaultFuncs; c->obuf = sdsempty(); c->reader = redisReaderCreate(); c->fd = REDIS_INVALID_FD; if (c->obuf == NULL || c->reader == NULL) { redisFree(c); return NULL; } return c; } void redisFree(redisContext *c) { if (c == NULL) return; redisNetClose(c); sdsfree(c->obuf); redisReaderFree(c->reader); hi_free(c->tcp.host); hi_free(c->tcp.source_addr); hi_free(c->unix_sock.path); hi_free(c->connect_timeout); hi_free(c->command_timeout); hi_free(c->saddr); if (c->privdata && c->free_privdata) c->free_privdata(c->privdata); if (c->funcs->free_privctx) c->funcs->free_privctx(c->privctx); memset(c, 0xff, sizeof(*c)); hi_free(c); } redisFD redisFreeKeepFd(redisContext *c) { redisFD fd = c->fd; c->fd = REDIS_INVALID_FD; redisFree(c); return fd; } int redisReconnect(redisContext *c) { c->err = 0; memset(c->errstr, '\0', strlen(c->errstr)); if (c->privctx && c->funcs->free_privctx) { c->funcs->free_privctx(c->privctx); c->privctx = NULL; } redisNetClose(c); sdsfree(c->obuf); redisReaderFree(c->reader); c->obuf = sdsempty(); c->reader = redisReaderCreate(); if (c->obuf == NULL || c->reader == NULL) { __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } int ret = REDIS_ERR; if (c->connection_type == REDIS_CONN_TCP) { ret = redisContextConnectBindTcp(c, c->tcp.host, c->tcp.port, c->connect_timeout, c->tcp.source_addr); } else if (c->connection_type == REDIS_CONN_UNIX) { ret = redisContextConnectUnix(c, c->unix_sock.path, c->connect_timeout); } else { /* Something bad happened here and shouldn't have. There isn't enough information in the context to reconnect. */ __redisSetError(c,REDIS_ERR_OTHER,"Not enough information to reconnect"); ret = REDIS_ERR; } if (c->command_timeout != NULL && (c->flags & REDIS_BLOCK) && c->fd != REDIS_INVALID_FD) { redisContextSetTimeout(c, *c->command_timeout); } return ret; } redisContext *redisConnectWithOptions(const redisOptions *options) { redisContext *c = redisContextInit(); if (c == NULL) { return NULL; } if (!(options->options & REDIS_OPT_NONBLOCK)) { c->flags |= REDIS_BLOCK; } if (options->options & REDIS_OPT_REUSEADDR) { c->flags |= REDIS_REUSEADDR; } if (options->options & REDIS_OPT_NOAUTOFREE) { c->flags |= REDIS_NO_AUTO_FREE; } /* Set any user supplied RESP3 PUSH handler or use freeReplyObject * as a default unless specifically flagged that we don't want one. */ if (options->push_cb != NULL) redisSetPushCallback(c, options->push_cb); else if (!(options->options & REDIS_OPT_NO_PUSH_AUTOFREE)) redisSetPushCallback(c, redisPushAutoFree); c->privdata = options->privdata; c->free_privdata = options->free_privdata; if (redisContextUpdateConnectTimeout(c, options->connect_timeout) != REDIS_OK || redisContextUpdateCommandTimeout(c, options->command_timeout) != REDIS_OK) { __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return c; } if (options->type == REDIS_CONN_TCP) { redisContextConnectBindTcp(c, options->endpoint.tcp.ip, options->endpoint.tcp.port, options->connect_timeout, options->endpoint.tcp.source_addr); } else if (options->type == REDIS_CONN_UNIX) { redisContextConnectUnix(c, options->endpoint.unix_socket, options->connect_timeout); } else if (options->type == REDIS_CONN_USERFD) { c->fd = options->endpoint.fd; c->flags |= REDIS_CONNECTED; } else { // Unknown type - FIXME - FREE return NULL; } if (options->command_timeout != NULL && (c->flags & REDIS_BLOCK) && c->fd != REDIS_INVALID_FD) { redisContextSetTimeout(c, *options->command_timeout); } return c; } /* Connect to a Redis instance. On error the field error in the returned * context will be set to the return value of the error function. * When no set of reply functions is given, the default set will be used. */ redisContext *redisConnect(const char *ip, int port) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); return redisConnectWithOptions(&options); } redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.connect_timeout = &tv; return redisConnectWithOptions(&options); } redisContext *redisConnectNonBlock(const char *ip, int port) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.options |= REDIS_OPT_NONBLOCK; return redisConnectWithOptions(&options); } redisContext *redisConnectBindNonBlock(const char *ip, int port, const char *source_addr) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.endpoint.tcp.source_addr = source_addr; options.options |= REDIS_OPT_NONBLOCK; return redisConnectWithOptions(&options); } redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port, const char *source_addr) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, ip, port); options.endpoint.tcp.source_addr = source_addr; options.options |= REDIS_OPT_NONBLOCK|REDIS_OPT_REUSEADDR; return redisConnectWithOptions(&options); } redisContext *redisConnectUnix(const char *path) { redisOptions options = {0}; REDIS_OPTIONS_SET_UNIX(&options, path); return redisConnectWithOptions(&options); } redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv) { redisOptions options = {0}; REDIS_OPTIONS_SET_UNIX(&options, path); options.connect_timeout = &tv; return redisConnectWithOptions(&options); } redisContext *redisConnectUnixNonBlock(const char *path) { redisOptions options = {0}; REDIS_OPTIONS_SET_UNIX(&options, path); options.options |= REDIS_OPT_NONBLOCK; return redisConnectWithOptions(&options); } redisContext *redisConnectFd(redisFD fd) { redisOptions options = {0}; options.type = REDIS_CONN_USERFD; options.endpoint.fd = fd; return redisConnectWithOptions(&options); } /* Set read/write timeout on a blocking socket. */ int redisSetTimeout(redisContext *c, const struct timeval tv) { if (c->flags & REDIS_BLOCK) return redisContextSetTimeout(c,tv); return REDIS_ERR; } /* Enable connection KeepAlive. */ int redisEnableKeepAlive(redisContext *c) { if (redisKeepAlive(c, REDIS_KEEPALIVE_TTL, REDIS_KEEPALIVE_INTERVAL) != REDIS_OK) return REDIS_ERR; return REDIS_OK; } /* Set a user provided RESP3 PUSH handler and return any old one set. */ redisPushFn *redisSetPushCallback(redisContext *c, redisPushFn *fn) { redisPushFn *old = c->push_cb; c->push_cb = fn; return old; } /* Use this function to handle a read event on the descriptor. It will try * and read some bytes from the socket and feed them to the reply parser. * * After this function is called, you may use redisGetReplyFromReader to * see if there is a reply available. */ int redisBufferRead(redisContext *c) { char buf[1024*16]; int nread; /* Return early when the context has seen an error. */ if (c->err) { return REDIS_ERR; } nread = c->funcs->read(c, buf, sizeof(buf)); if (nread > 0) { if (redisReaderFeed(c->reader, buf, nread) != REDIS_OK) { __redisSetError(c, c->reader->err, c->reader->errstr); return REDIS_ERR; } else { } } else if (nread < 0) { return REDIS_ERR; } return REDIS_OK; } /* Write the output buffer to the socket. * * Returns REDIS_OK when the buffer is empty, or (a part of) the buffer was * successfully written to the socket. When the buffer is empty after the * write operation, "done" is set to 1 (if given). * * Returns REDIS_ERR if an error occurred trying to write and sets * c->errstr to hold the appropriate error string. */ int redisBufferWrite(redisContext *c, int *done) { /* Return early when the context has seen an error. */ if (c->err) return REDIS_ERR; if (sdslen(c->obuf) > 0) { ssize_t nwritten = c->funcs->write(c); if (nwritten < 0) { return REDIS_ERR; } else if (nwritten > 0) { if (nwritten == (ssize_t)sdslen(c->obuf)) { sdsfree(c->obuf); c->obuf = sdsempty(); if (c->obuf == NULL) goto oom; } else { if (sdsrange(c->obuf,nwritten,-1) < 0) goto oom; } } } if (done != NULL) *done = (sdslen(c->obuf) == 0); return REDIS_OK; oom: __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } /* Internal helper function to try and get a reply from the reader, * or set an error in the context otherwise. */ int redisGetReplyFromReader(redisContext *c, void **reply) { if (redisReaderGetReply(c->reader,reply) == REDIS_ERR) { __redisSetError(c,c->reader->err,c->reader->errstr); return REDIS_ERR; } return REDIS_OK; } /* Internal helper that returns 1 if the reply was a RESP3 PUSH * message and we handled it with a user-provided callback. */ static int redisHandledPushReply(redisContext *c, void *reply) { if (reply && c->push_cb && redisIsPushReply(reply)) { c->push_cb(c->privdata, reply); return 1; } return 0; } int redisGetReply(redisContext *c, void **reply) { int wdone = 0; void *aux = NULL; /* Try to read pending replies */ if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) return REDIS_ERR; /* For the blocking context, flush output buffer and read reply */ if (aux == NULL && c->flags & REDIS_BLOCK) { /* Write until done */ do { if (redisBufferWrite(c,&wdone) == REDIS_ERR) return REDIS_ERR; } while (!wdone); /* Read until there is a reply */ do { if (redisBufferRead(c) == REDIS_ERR) return REDIS_ERR; /* We loop here in case the user has specified a RESP3 * PUSH handler (e.g. for client tracking). */ do { if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) return REDIS_ERR; } while (redisHandledPushReply(c, aux)); } while (aux == NULL); } /* Set reply or free it if we were passed NULL */ if (reply != NULL) { *reply = aux; } else { freeReplyObject(aux); } return REDIS_OK; } /* Helper function for the redisAppendCommand* family of functions. * * Write a formatted command to the output buffer. When this family * is used, you need to call redisGetReply yourself to retrieve * the reply (or replies in pub/sub). */ int __redisAppendCommand(redisContext *c, const char *cmd, size_t len) { sds newbuf; newbuf = sdscatlen(c->obuf,cmd,len); if (newbuf == NULL) { __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); return REDIS_ERR; } c->obuf = newbuf; return REDIS_OK; } int redisAppendFormattedCommand(redisContext *c, const char *cmd, size_t len) { if (__redisAppendCommand(c, cmd, len) != REDIS_OK) { return REDIS_ERR; } return REDIS_OK; } int redisvAppendCommand(redisContext *c, const char *format, va_list ap) { char *cmd; int len; len = redisvFormatCommand(&cmd,format,ap); if (len == -1) { __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); return REDIS_ERR; } else if (len == -2) { __redisSetError(c,REDIS_ERR_OTHER,"Invalid format string"); return REDIS_ERR; } if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { hi_free(cmd); return REDIS_ERR; } hi_free(cmd); return REDIS_OK; } int redisAppendCommand(redisContext *c, const char *format, ...) { va_list ap; int ret; va_start(ap,format); ret = redisvAppendCommand(c,format,ap); va_end(ap); return ret; } int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { sds cmd; int len; len = redisFormatSdsCommandArgv(&cmd,argc,argv,argvlen); if (len == -1) { __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); return REDIS_ERR; } if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { sdsfree(cmd); return REDIS_ERR; } sdsfree(cmd); return REDIS_OK; } /* Helper function for the redisCommand* family of functions. * * Write a formatted command to the output buffer. If the given context is * blocking, immediately read the reply into the "reply" pointer. When the * context is non-blocking, the "reply" pointer will not be used and the * command is simply appended to the write buffer. * * Returns the reply when a reply was successfully retrieved. Returns NULL * otherwise. When NULL is returned in a blocking context, the error field * in the context will be set. */ static void *__redisBlockForReply(redisContext *c) { void *reply; if (c->flags & REDIS_BLOCK) { if (redisGetReply(c,&reply) != REDIS_OK) return NULL; return reply; } return NULL; } void *redisvCommand(redisContext *c, const char *format, va_list ap) { if (redisvAppendCommand(c,format,ap) != REDIS_OK) return NULL; return __redisBlockForReply(c); } void *redisCommand(redisContext *c, const char *format, ...) { va_list ap; va_start(ap,format); void *reply = redisvCommand(c,format,ap); va_end(ap); return reply; } void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { if (redisAppendCommandArgv(c,argc,argv,argvlen) != REDIS_OK) return NULL; return __redisBlockForReply(c); } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis.h000066400000000000000000000307711515654015100310430ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2014, Pieter Noordhuis * Copyright (c) 2015, Matt Stancliff , * Jan-Erik Rediger * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_H #define __HIREDIS_H #include "read.h" #include /* for va_list */ #ifndef _MSC_VER #include /* for struct timeval */ #else struct timeval; /* forward declaration */ typedef long long ssize_t; #endif #include /* uintXX_t, etc */ #include "sds.h" /* for sds */ #include "alloc.h" /* for allocation wrappers */ #define HIREDIS_MAJOR 1 #define HIREDIS_MINOR 0 #define HIREDIS_PATCH 2 #define HIREDIS_SONAME 1.0.0 /* Connection type can be blocking or non-blocking and is set in the * least significant bit of the flags field in redisContext. */ #define REDIS_BLOCK 0x1 /* Connection may be disconnected before being free'd. The second bit * in the flags field is set when the context is connected. */ #define REDIS_CONNECTED 0x2 /* The async API might try to disconnect cleanly and flush the output * buffer and read all subsequent replies before disconnecting. * This flag means no new commands can come in and the connection * should be terminated once all replies have been read. */ #define REDIS_DISCONNECTING 0x4 /* Flag specific to the async API which means that the context should be clean * up as soon as possible. */ #define REDIS_FREEING 0x8 /* Flag that is set when an async callback is executed. */ #define REDIS_IN_CALLBACK 0x10 /* Flag that is set when the async context has one or more subscriptions. */ #define REDIS_SUBSCRIBED 0x20 /* Flag that is set when monitor mode is active */ #define REDIS_MONITORING 0x40 /* Flag that is set when we should set SO_REUSEADDR before calling bind() */ #define REDIS_REUSEADDR 0x80 /** * Flag that indicates the user does not want the context to * be automatically freed upon error */ #define REDIS_NO_AUTO_FREE 0x200 #define REDIS_KEEPALIVE_INTERVAL 15 /* seconds */ #define REDIS_KEEPALIVE_TTL 120 /* seconds */ /* number of times we retry to connect in the case of EADDRNOTAVAIL and * SO_REUSEADDR is being used. */ #define REDIS_CONNECT_RETRIES 10 /* Forward declarations for structs defined elsewhere */ struct redisAsyncContext; struct redisContext; /* RESP3 push helpers and callback prototypes */ #define redisIsPushReply(r) (((redisReply*)(r))->type == REDIS_REPLY_PUSH) typedef void (redisPushFn)(void *, void *); typedef void (redisAsyncPushFn)(struct redisAsyncContext *, void *); #ifdef __cplusplus extern "C" { #endif /* This is the reply object returned by redisCommand() */ typedef struct redisReply { int type; /* REDIS_REPLY_* */ long long integer; /* The integer when type is REDIS_REPLY_INTEGER */ double dval; /* The double when type is REDIS_REPLY_DOUBLE */ size_t len; /* Length of string */ char *str; /* Used for REDIS_REPLY_ERROR, REDIS_REPLY_STRING REDIS_REPLY_VERB, and REDIS_REPLY_DOUBLE (in additional to dval). */ char vtype[4]; /* Used for REDIS_REPLY_VERB, contains the null terminated 3 character content type, such as "txt". */ size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */ struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */ } redisReply; redisReader *redisReaderCreate(void); /* Function to free the reply objects hiredis returns by default. */ void freeReplyObject(void *reply); /* Functions to format a command according to the protocol. */ int redisvFormatCommand(char **target, const char *format, va_list ap); int redisFormatCommand(char **target, const char *format, ...); int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen); int redisFormatSdsCommandArgv(sds *target, int argc, const char ** argv, const size_t *argvlen); void redisFreeCommand(char *cmd); void redisFreeSdsCommand(sds cmd); enum redisConnectionType { REDIS_CONN_TCP, REDIS_CONN_UNIX, REDIS_CONN_USERFD }; struct redisSsl; #define REDIS_OPT_NONBLOCK 0x01 #define REDIS_OPT_REUSEADDR 0x02 /** * Don't automatically free the async object on a connection failure, * or other implicit conditions. Only free on an explicit call to disconnect() or free() */ #define REDIS_OPT_NOAUTOFREE 0x04 /* Don't automatically intercept and free RESP3 PUSH replies. */ #define REDIS_OPT_NO_PUSH_AUTOFREE 0x08 /* In Unix systems a file descriptor is a regular signed int, with -1 * representing an invalid descriptor. In Windows it is a SOCKET * (32- or 64-bit unsigned integer depending on the architecture), where * all bits set (~0) is INVALID_SOCKET. */ #ifndef _WIN32 typedef int redisFD; #define REDIS_INVALID_FD -1 #else #ifdef _WIN64 typedef unsigned long long redisFD; /* SOCKET = 64-bit UINT_PTR */ #else typedef unsigned long redisFD; /* SOCKET = 32-bit UINT_PTR */ #endif #define REDIS_INVALID_FD ((redisFD)(~0)) /* INVALID_SOCKET */ #endif typedef struct { /* * the type of connection to use. This also indicates which * `endpoint` member field to use */ int type; /* bit field of REDIS_OPT_xxx */ int options; /* timeout value for connect operation. If NULL, no timeout is used */ const struct timeval *connect_timeout; /* timeout value for commands. If NULL, no timeout is used. This can be * updated at runtime with redisSetTimeout/redisAsyncSetTimeout. */ const struct timeval *command_timeout; union { /** use this field for tcp/ip connections */ struct { const char *source_addr; const char *ip; int port; } tcp; /** use this field for unix domain sockets */ const char *unix_socket; /** * use this field to have hiredis operate an already-open * file descriptor */ redisFD fd; } endpoint; /* Optional user defined data/destructor */ void *privdata; void (*free_privdata)(void *); /* A user defined PUSH message callback */ redisPushFn *push_cb; redisAsyncPushFn *async_push_cb; } redisOptions; /** * Helper macros to initialize options to their specified fields. */ #define REDIS_OPTIONS_SET_TCP(opts, ip_, port_) \ (opts)->type = REDIS_CONN_TCP; \ (opts)->endpoint.tcp.ip = ip_; \ (opts)->endpoint.tcp.port = port_; #define REDIS_OPTIONS_SET_UNIX(opts, path) \ (opts)->type = REDIS_CONN_UNIX; \ (opts)->endpoint.unix_socket = path; #define REDIS_OPTIONS_SET_PRIVDATA(opts, data, dtor) \ (opts)->privdata = data; \ (opts)->free_privdata = dtor; \ typedef struct redisContextFuncs { void (*free_privctx)(void *); void (*async_read)(struct redisAsyncContext *); void (*async_write)(struct redisAsyncContext *); ssize_t (*read)(struct redisContext *, char *, size_t); ssize_t (*write)(struct redisContext *); } redisContextFuncs; /* Context for a connection to Redis */ typedef struct redisContext { const redisContextFuncs *funcs; /* Function table */ int err; /* Error flags, 0 when there is no error */ char errstr[128]; /* String representation of error when applicable */ redisFD fd; int flags; char *obuf; /* Write buffer */ redisReader *reader; /* Protocol reader */ enum redisConnectionType connection_type; struct timeval *connect_timeout; struct timeval *command_timeout; struct { char *host; char *source_addr; int port; } tcp; struct { char *path; } unix_sock; /* For non-blocking connect */ struct sockadr *saddr; size_t addrlen; /* Optional data and corresponding destructor users can use to provide * context to a given redisContext. Not used by hiredis. */ void *privdata; void (*free_privdata)(void *); /* Internal context pointer presently used by hiredis to manage * SSL connections. */ void *privctx; /* An optional RESP3 PUSH handler */ redisPushFn *push_cb; } redisContext; redisContext *redisConnectWithOptions(const redisOptions *options); redisContext *redisConnect(const char *ip, int port); redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv); redisContext *redisConnectNonBlock(const char *ip, int port); redisContext *redisConnectBindNonBlock(const char *ip, int port, const char *source_addr); redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port, const char *source_addr); redisContext *redisConnectUnix(const char *path); redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv); redisContext *redisConnectUnixNonBlock(const char *path); redisContext *redisConnectFd(redisFD fd); /** * Reconnect the given context using the saved information. * * This re-uses the exact same connect options as in the initial connection. * host, ip (or path), timeout and bind address are reused, * flags are used unmodified from the existing context. * * Returns REDIS_OK on successful connect or REDIS_ERR otherwise. */ int redisReconnect(redisContext *c); redisPushFn *redisSetPushCallback(redisContext *c, redisPushFn *fn); int redisSetTimeout(redisContext *c, const struct timeval tv); int redisEnableKeepAlive(redisContext *c); void redisFree(redisContext *c); redisFD redisFreeKeepFd(redisContext *c); int redisBufferRead(redisContext *c); int redisBufferWrite(redisContext *c, int *done); /* In a blocking context, this function first checks if there are unconsumed * replies to return and returns one if so. Otherwise, it flushes the output * buffer to the socket and reads until it has a reply. In a non-blocking * context, it will return unconsumed replies until there are no more. */ int redisGetReply(redisContext *c, void **reply); int redisGetReplyFromReader(redisContext *c, void **reply); /* Write a formatted command to the output buffer. Use these functions in blocking mode * to get a pipeline of commands. */ int redisAppendFormattedCommand(redisContext *c, const char *cmd, size_t len); /* Write a command to the output buffer. Use these functions in blocking mode * to get a pipeline of commands. */ int redisvAppendCommand(redisContext *c, const char *format, va_list ap); int redisAppendCommand(redisContext *c, const char *format, ...); int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); /* Issue a command to Redis. In a blocking context, it is identical to calling * redisAppendCommand, followed by redisGetReply. The function will return * NULL if there was an error in performing the request, otherwise it will * return the reply. In a non-blocking context, it is identical to calling * only redisAppendCommand and will always return NULL. */ void *redisvCommand(redisContext *c, const char *format, va_list ap); void *redisCommand(redisContext *c, const char *format, ...); void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); #ifdef __cplusplus } #endif #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis.pc.in000066400000000000000000000005611515654015100316150ustar00rootroot00000000000000prefix=@CMAKE_INSTALL_PREFIX@ install_libdir=@CMAKE_INSTALL_LIBDIR@ exec_prefix=${prefix} libdir=${exec_prefix}/${install_libdir} includedir=${prefix}/include pkgincludedir=${includedir}/hiredis Name: hiredis Description: Minimalistic C client library for Redis. Version: @PROJECT_VERSION@ Libs: -L${libdir} -lhiredis Cflags: -I${pkgincludedir} -D_FILE_OFFSET_BITS=64 hiredis_ssl-config.cmake.in000066400000000000000000000005241515654015100343370ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor@PACKAGE_INIT@ set_and_check(hiredis_ssl_INCLUDEDIR "@PACKAGE_INCLUDE_INSTALL_DIR@") IF (NOT TARGET hiredis::hiredis_ssl) INCLUDE(${CMAKE_CURRENT_LIST_DIR}/hiredis_ssl-targets.cmake) ENDIF() SET(hiredis_ssl_LIBRARIES hiredis::hiredis_ssl) SET(hiredis_ssl_INCLUDE_DIRS ${hiredis_ssl_INCLUDEDIR}) check_required_components(hiredis_ssl) redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis_ssl.h000066400000000000000000000124001515654015100317110ustar00rootroot00000000000000 /* * Copyright (c) 2019, Redis Labs * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_SSL_H #define __HIREDIS_SSL_H #include #ifdef __cplusplus extern "C" { #endif /* This is the underlying struct for SSL in ssl.h, which is not included to * keep build dependencies short here. */ struct ssl_st; /* A wrapper around OpenSSL SSL_CTX to allow easy SSL use without directly * calling OpenSSL. */ typedef struct redisSSLContext redisSSLContext; /* The SSL connection context is attached to SSL/TLS connections as a privdata. */ typedef struct redisSSL { /** * OpenSSL SSL object. */ SSL *ssl; /** * SSL_write() requires to be called again with the same arguments it was * previously called with in the event of an SSL_read/SSL_write situation */ size_t lastLen; /** Whether the SSL layer requires read (possibly before a write) */ int wantRead; /** * Whether a write was requested prior to a read. If set, the write() * should resume whenever a read takes place, if possible */ int pendingWrite; } redisSSL; /** * Initialization errors that redisCreateSSLContext() may return. */ typedef enum { REDIS_SSL_CTX_NONE = 0, /* No Error */ REDIS_SSL_CTX_CREATE_FAILED, /* Failed to create OpenSSL SSL_CTX */ REDIS_SSL_CTX_CERT_KEY_REQUIRED, /* Client cert and key must both be specified or skipped */ REDIS_SSL_CTX_CA_CERT_LOAD_FAILED, /* Failed to load CA Certificate or CA Path */ REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED, /* Failed to load client certificate */ REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED /* Failed to load private key */ } redisSSLContextError; /** * Return the error message corresponding with the specified error code. */ const char *redisSSLContextGetError(redisSSLContextError error); /** * Helper function to initialize the OpenSSL library. * * OpenSSL requires one-time initialization before it can be used. Callers should * call this function only once, and only if OpenSSL is not directly initialized * elsewhere. */ int redisInitOpenSSL(void); /** * Helper function to initialize an OpenSSL context that can be used * to initiate SSL connections. * * cacert_filename is an optional name of a CA certificate/bundle file to load * and use for validation. * * capath is an optional directory path where trusted CA certificate files are * stored in an OpenSSL-compatible structure. * * cert_filename and private_key_filename are optional names of a client side * certificate and private key files to use for authentication. They need to * be both specified or omitted. * * server_name is an optional and will be used as a server name indication * (SNI) TLS extension. * * If error is non-null, it will be populated in case the context creation fails * (returning a NULL). */ redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *capath, const char *cert_filename, const char *private_key_filename, const char *server_name, redisSSLContextError *error); /** * Free a previously created OpenSSL context. */ void redisFreeSSLContext(redisSSLContext *redis_ssl_ctx); /** * Initiate SSL on an existing redisContext. * * This is similar to redisInitiateSSL() but does not require the caller * to directly interact with OpenSSL, and instead uses a redisSSLContext * previously created using redisCreateSSLContext(). */ int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx); int redisInitiateSSLContinue(redisContext *c); /** * Initiate SSL/TLS negotiation on a provided OpenSSL SSL object. */ int redisInitiateSSL(redisContext *c, struct ssl_st *ssl); redisSSL *redisGetSSLSocket(redisContext *c); #ifdef __cplusplus } #endif #endif /* __HIREDIS_SSL_H */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/hiredis_ssl.pc.in000066400000000000000000000004621515654015100324760ustar00rootroot00000000000000prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include pkgincludedir=${includedir}/hiredis Name: hiredis_ssl Description: SSL Support for hiredis. Version: @PROJECT_VERSION@ Requires: hiredis Libs: -L${libdir} -lhiredis_ssl Libs.private: -lssl -lcrypto redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/net.c000066400000000000000000000453521515654015100301760ustar00rootroot00000000000000/* Extracted from anet.c to work properly with Hiredis error reporting. * * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2014, Pieter Noordhuis * Copyright (c) 2015, Matt Stancliff , * Jan-Erik Rediger * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include #include #include #include #include #include #include #include #include "net.h" #include "sds.h" #include "sockcompat.h" #include "win32.h" /* Defined in hiredis.c */ void __redisSetError(redisContext *c, int type, const char *str); void redisNetClose(redisContext *c) { if (c && c->fd != REDIS_INVALID_FD) { close(c->fd); c->fd = REDIS_INVALID_FD; } } ssize_t redisNetRead(redisContext *c, char *buf, size_t bufcap) { ssize_t nread = recv(c->fd, buf, bufcap, 0); if (nread == -1) { if ((errno == EWOULDBLOCK && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { /* Try again later */ return 0; } else if(errno == ETIMEDOUT && (c->flags & REDIS_BLOCK)) { /* especially in windows */ __redisSetError(c, REDIS_ERR_TIMEOUT, "recv timeout"); return -1; } else { __redisSetError(c, REDIS_ERR_IO, NULL); return -1; } } else if (nread == 0) { __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); return -1; } else { return nread; } } ssize_t redisNetWrite(redisContext *c) { ssize_t nwritten = send(c->fd, c->obuf, sdslen(c->obuf), 0); if (nwritten < 0) { if ((errno == EWOULDBLOCK && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { /* Try again later */ } else { __redisSetError(c, REDIS_ERR_IO, NULL); return -1; } } return nwritten; } static void __redisSetErrorFromErrno(redisContext *c, int type, const char *prefix) { int errorno = errno; /* snprintf() may change errno */ char buf[128] = { 0 }; size_t len = 0; if (prefix != NULL) len = snprintf(buf,sizeof(buf),"%s: ",prefix); strerror_r(errorno, (char *)(buf + len), sizeof(buf) - len); __redisSetError(c,type,buf); } static int redisSetReuseAddr(redisContext *c) { int on = 1; if (setsockopt(c->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); redisNetClose(c); return REDIS_ERR; } return REDIS_OK; } static int redisCreateSocket(redisContext *c, int type) { redisFD s; if ((s = socket(type, SOCK_STREAM, 0)) == REDIS_INVALID_FD) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); return REDIS_ERR; } c->fd = s; if (type == AF_INET) { if (redisSetReuseAddr(c) == REDIS_ERR) { return REDIS_ERR; } } return REDIS_OK; } static int redisSetBlocking(redisContext *c, int blocking) { #ifndef _WIN32 int flags; /* Set the socket nonblocking. * Note that fcntl(2) for F_GETFL and F_SETFL can't be * interrupted by a signal. */ if ((flags = fcntl(c->fd, F_GETFL)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_GETFL)"); redisNetClose(c); return REDIS_ERR; } if (blocking) flags &= ~O_NONBLOCK; else flags |= O_NONBLOCK; if (fcntl(c->fd, F_SETFL, flags) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_SETFL)"); redisNetClose(c); return REDIS_ERR; } #else u_long mode = blocking ? 0 : 1; if (ioctl(c->fd, FIONBIO, &mode) == -1) { __redisSetErrorFromErrno(c, REDIS_ERR_IO, "ioctl(FIONBIO)"); redisNetClose(c); return REDIS_ERR; } #endif /* _WIN32 */ return REDIS_OK; } int redisKeepAlive(redisContext *c, int ttl, int interval) { int val = 1; redisFD fd = c->fd; if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1){ __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); return REDIS_ERR; } #if defined(__APPLE__) && defined(__MACH__) val = ttl; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &val, sizeof(val)) < 0) { __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); return REDIS_ERR; } #else #if defined(__GLIBC__) && !defined(__FreeBSD_kernel__) val = interval; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) { __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); return REDIS_ERR; } val = interval; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &val, sizeof(val)) < 0) { __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); return REDIS_ERR; } val = (ttl / interval) - 1; if (val <= 0) val = 1; if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) < 0) { __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); return REDIS_ERR; } #endif #endif return REDIS_OK; } int redisSetTcpNoDelay(redisContext *c) { int yes = 1; if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(TCP_NODELAY)"); redisNetClose(c); return REDIS_ERR; } return REDIS_OK; } #define __MAX_MSEC (((LONG_MAX) - 999) / 1000) static int redisContextTimeoutMsec(redisContext *c, long *result) { const struct timeval *timeout = c->connect_timeout; long msec = -1; /* Only use timeout when not NULL. */ if (timeout != NULL) { if (timeout->tv_usec > 1000000 || timeout->tv_sec > __MAX_MSEC) { *result = msec; return REDIS_ERR; } msec = (timeout->tv_sec * 1000) + ((timeout->tv_usec + 999) / 1000); if (msec < 0 || msec > INT_MAX) { msec = INT_MAX; } } *result = msec; return REDIS_OK; } static int redisContextWaitReady(redisContext *c, long msec) { struct pollfd wfd[1]; wfd[0].fd = c->fd; wfd[0].events = POLLOUT; if (errno == EINPROGRESS) { int res; if ((res = poll(wfd, 1, msec)) == -1) { __redisSetErrorFromErrno(c, REDIS_ERR_IO, "poll(2)"); redisNetClose(c); return REDIS_ERR; } else if (res == 0) { errno = ETIMEDOUT; __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); redisNetClose(c); return REDIS_ERR; } if (redisCheckConnectDone(c, &res) != REDIS_OK || res == 0) { redisCheckSocketError(c); return REDIS_ERR; } return REDIS_OK; } __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); redisNetClose(c); return REDIS_ERR; } int redisCheckConnectDone(redisContext *c, int *completed) { int rc = connect(c->fd, (const struct sockaddr *)c->saddr, c->addrlen); if (rc == 0) { *completed = 1; return REDIS_OK; } switch (errno) { case EISCONN: *completed = 1; return REDIS_OK; case EALREADY: case EINPROGRESS: case EWOULDBLOCK: *completed = 0; return REDIS_OK; default: return REDIS_ERR; } } int redisCheckSocketError(redisContext *c) { int err = 0, errno_saved = errno; socklen_t errlen = sizeof(err); if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"getsockopt(SO_ERROR)"); return REDIS_ERR; } if (err == 0) { err = errno_saved; } if (err) { errno = err; __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); return REDIS_ERR; } return REDIS_OK; } int redisContextSetTimeout(redisContext *c, const struct timeval tv) { const void *to_ptr = &tv; size_t to_sz = sizeof(tv); if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,to_ptr,to_sz) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_RCVTIMEO)"); return REDIS_ERR; } if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,to_ptr,to_sz) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_SNDTIMEO)"); return REDIS_ERR; } return REDIS_OK; } int redisContextUpdateConnectTimeout(redisContext *c, const struct timeval *timeout) { /* Same timeval struct, short circuit */ if (c->connect_timeout == timeout) return REDIS_OK; /* Allocate context timeval if we need to */ if (c->connect_timeout == NULL) { c->connect_timeout = hi_malloc(sizeof(*c->connect_timeout)); if (c->connect_timeout == NULL) return REDIS_ERR; } memcpy(c->connect_timeout, timeout, sizeof(*c->connect_timeout)); return REDIS_OK; } int redisContextUpdateCommandTimeout(redisContext *c, const struct timeval *timeout) { /* Same timeval struct, short circuit */ if (c->command_timeout == timeout) return REDIS_OK; /* Allocate context timeval if we need to */ if (c->command_timeout == NULL) { c->command_timeout = hi_malloc(sizeof(*c->command_timeout)); if (c->command_timeout == NULL) return REDIS_ERR; } memcpy(c->command_timeout, timeout, sizeof(*c->command_timeout)); return REDIS_OK; } static int _redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout, const char *source_addr) { redisFD s; int rv, n; char _port[6]; /* strlen("65535"); */ struct addrinfo hints, *servinfo, *bservinfo, *p, *b; int blocking = (c->flags & REDIS_BLOCK); int reuseaddr = (c->flags & REDIS_REUSEADDR); int reuses = 0; long timeout_msec = -1; servinfo = NULL; c->connection_type = REDIS_CONN_TCP; c->tcp.port = port; /* We need to take possession of the passed parameters * to make them reusable for a reconnect. * We also carefully check we don't free data we already own, * as in the case of the reconnect method. * * This is a bit ugly, but atleast it works and doesn't leak memory. **/ if (c->tcp.host != addr) { hi_free(c->tcp.host); c->tcp.host = hi_strdup(addr); if (c->tcp.host == NULL) goto oom; } if (timeout) { if (redisContextUpdateConnectTimeout(c, timeout) == REDIS_ERR) goto oom; } else { hi_free(c->connect_timeout); c->connect_timeout = NULL; } if (redisContextTimeoutMsec(c, &timeout_msec) != REDIS_OK) { __redisSetError(c, REDIS_ERR_IO, "Invalid timeout specified"); goto error; } if (source_addr == NULL) { hi_free(c->tcp.source_addr); c->tcp.source_addr = NULL; } else if (c->tcp.source_addr != source_addr) { hi_free(c->tcp.source_addr); c->tcp.source_addr = hi_strdup(source_addr); } snprintf(_port, 6, "%d", port); memset(&hints,0,sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; /* Try with IPv6 if no IPv4 address was found. We do it in this order since * in a Redis client you can't afford to test if you have IPv6 connectivity * as this would add latency to every connect. Otherwise a more sensible * route could be: Use IPv6 if both addresses are available and there is IPv6 * connectivity. */ if ((rv = getaddrinfo(c->tcp.host,_port,&hints,&servinfo)) != 0) { hints.ai_family = AF_INET6; if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) { __redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv)); return REDIS_ERR; } } for (p = servinfo; p != NULL; p = p->ai_next) { addrretry: if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == REDIS_INVALID_FD) continue; c->fd = s; if (redisSetBlocking(c,0) != REDIS_OK) goto error; if (c->tcp.source_addr) { int bound = 0; /* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */ if ((rv = getaddrinfo(c->tcp.source_addr, NULL, &hints, &bservinfo)) != 0) { char buf[128]; snprintf(buf,sizeof(buf),"Can't get addr: %s",gai_strerror(rv)); __redisSetError(c,REDIS_ERR_OTHER,buf); goto error; } if (reuseaddr) { n = 1; if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*) &n, sizeof(n)) < 0) { freeaddrinfo(bservinfo); goto error; } } for (b = bservinfo; b != NULL; b = b->ai_next) { if (bind(s,b->ai_addr,b->ai_addrlen) != -1) { bound = 1; break; } } freeaddrinfo(bservinfo); if (!bound) { char buf[128]; snprintf(buf,sizeof(buf),"Can't bind socket: %s",strerror(errno)); __redisSetError(c,REDIS_ERR_OTHER,buf); goto error; } } /* For repeat connection */ hi_free(c->saddr); c->saddr = hi_malloc(p->ai_addrlen); if (c->saddr == NULL) goto oom; memcpy(c->saddr, p->ai_addr, p->ai_addrlen); c->addrlen = p->ai_addrlen; if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { if (errno == EHOSTUNREACH) { redisNetClose(c); continue; } else if (errno == EINPROGRESS) { if (blocking) { goto wait_for_ready; } /* This is ok. * Note that even when it's in blocking mode, we unset blocking * for `connect()` */ } else if (errno == EADDRNOTAVAIL && reuseaddr) { if (++reuses >= REDIS_CONNECT_RETRIES) { goto error; } else { redisNetClose(c); goto addrretry; } } else { wait_for_ready: if (redisContextWaitReady(c,timeout_msec) != REDIS_OK) goto error; if (redisSetTcpNoDelay(c) != REDIS_OK) goto error; } } if (blocking && redisSetBlocking(c,1) != REDIS_OK) goto error; c->flags |= REDIS_CONNECTED; rv = REDIS_OK; goto end; } if (p == NULL) { char buf[128]; snprintf(buf,sizeof(buf),"Can't create socket: %s",strerror(errno)); __redisSetError(c,REDIS_ERR_OTHER,buf); goto error; } oom: __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); error: rv = REDIS_ERR; end: if(servinfo) { freeaddrinfo(servinfo); } return rv; // Need to return REDIS_OK if alright } int redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout) { return _redisContextConnectTcp(c, addr, port, timeout, NULL); } int redisContextConnectBindTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout, const char *source_addr) { return _redisContextConnectTcp(c, addr, port, timeout, source_addr); } int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout) { #ifndef _WIN32 int blocking = (c->flags & REDIS_BLOCK); struct sockaddr_un *sa; long timeout_msec = -1; if (redisCreateSocket(c,AF_UNIX) < 0) return REDIS_ERR; if (redisSetBlocking(c,0) != REDIS_OK) return REDIS_ERR; c->connection_type = REDIS_CONN_UNIX; if (c->unix_sock.path != path) { hi_free(c->unix_sock.path); c->unix_sock.path = hi_strdup(path); if (c->unix_sock.path == NULL) goto oom; } if (timeout) { if (redisContextUpdateConnectTimeout(c, timeout) == REDIS_ERR) goto oom; } else { hi_free(c->connect_timeout); c->connect_timeout = NULL; } if (redisContextTimeoutMsec(c,&timeout_msec) != REDIS_OK) return REDIS_ERR; /* Don't leak sockaddr if we're reconnecting */ if (c->saddr) hi_free(c->saddr); sa = (struct sockaddr_un*)(c->saddr = hi_malloc(sizeof(struct sockaddr_un))); if (sa == NULL) goto oom; c->addrlen = sizeof(struct sockaddr_un); sa->sun_family = AF_UNIX; strncpy(sa->sun_path, path, sizeof(sa->sun_path) - 1); if (connect(c->fd, (struct sockaddr*)sa, sizeof(*sa)) == -1) { if (errno == EINPROGRESS && !blocking) { /* This is ok. */ } else { if (redisContextWaitReady(c,timeout_msec) != REDIS_OK) return REDIS_ERR; } } /* Reset socket to be blocking after connect(2). */ if (blocking && redisSetBlocking(c,1) != REDIS_OK) return REDIS_ERR; c->flags |= REDIS_CONNECTED; return REDIS_OK; #else /* We currently do not support Unix sockets for Windows. */ /* TODO(m): https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ */ errno = EPROTONOSUPPORT; return REDIS_ERR; #endif /* _WIN32 */ oom: __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/net.h000066400000000000000000000053231515654015100301750ustar00rootroot00000000000000/* Extracted from anet.c to work properly with Hiredis error reporting. * * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2014, Pieter Noordhuis * Copyright (c) 2015, Matt Stancliff , * Jan-Erik Rediger * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __NET_H #define __NET_H #include "hiredis.h" void redisNetClose(redisContext *c); ssize_t redisNetRead(redisContext *c, char *buf, size_t bufcap); ssize_t redisNetWrite(redisContext *c); int redisCheckSocketError(redisContext *c); int redisContextSetTimeout(redisContext *c, const struct timeval tv); int redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout); int redisContextConnectBindTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout, const char *source_addr); int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout); int redisKeepAlive(redisContext *c, int ttl, int interval); int redisCheckConnectDone(redisContext *c, int *completed); int redisSetTcpNoDelay(redisContext *c); #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/read.c000066400000000000000000000530271515654015100303210ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include #include #ifndef _MSC_VER #include #include #endif #include #include #include #include #include #include "alloc.h" #include "read.h" #include "sds.h" #include "win32.h" /* Initial size of our nested reply stack and how much we grow it when needd */ #define REDIS_READER_STACK_SIZE 9 static void __redisReaderSetError(redisReader *r, int type, const char *str) { size_t len; if (r->reply != NULL && r->fn && r->fn->freeObject) { r->fn->freeObject(r->reply); r->reply = NULL; } /* Clear input buffer on errors. */ sdsfree(r->buf); r->buf = NULL; r->pos = r->len = 0; /* Reset task stack. */ r->ridx = -1; /* Set error. */ r->err = type; len = strlen(str); len = len < (sizeof(r->errstr)-1) ? len : (sizeof(r->errstr)-1); memcpy(r->errstr,str,len); r->errstr[len] = '\0'; } static size_t chrtos(char *buf, size_t size, char byte) { size_t len = 0; switch(byte) { case '\\': case '"': len = snprintf(buf,size,"\"\\%c\"",byte); break; case '\n': len = snprintf(buf,size,"\"\\n\""); break; case '\r': len = snprintf(buf,size,"\"\\r\""); break; case '\t': len = snprintf(buf,size,"\"\\t\""); break; case '\a': len = snprintf(buf,size,"\"\\a\""); break; case '\b': len = snprintf(buf,size,"\"\\b\""); break; default: if (isprint(byte)) len = snprintf(buf,size,"\"%c\"",byte); else len = snprintf(buf,size,"\"\\x%02x\"",(unsigned char)byte); break; } return len; } static void __redisReaderSetErrorProtocolByte(redisReader *r, char byte) { char cbuf[8], sbuf[128]; chrtos(cbuf,sizeof(cbuf),byte); snprintf(sbuf,sizeof(sbuf), "Protocol error, got %s as reply type byte", cbuf); __redisReaderSetError(r,REDIS_ERR_PROTOCOL,sbuf); } static void __redisReaderSetErrorOOM(redisReader *r) { __redisReaderSetError(r,REDIS_ERR_OOM,"Out of memory"); } static char *readBytes(redisReader *r, unsigned int bytes) { char *p; if (r->len-r->pos >= bytes) { p = r->buf+r->pos; r->pos += bytes; return p; } return NULL; } /* Find pointer to \r\n. */ static char *seekNewline(char *s, size_t len) { int pos = 0; int _len = len-1; /* Position should be < len-1 because the character at "pos" should be * followed by a \n. Note that strchr cannot be used because it doesn't * allow to search a limited length and the buffer that is being searched * might not have a trailing NULL character. */ while (pos < _len) { while(pos < _len && s[pos] != '\r') pos++; if (pos==_len) { /* Not found. */ return NULL; } else { if (s[pos+1] == '\n') { /* Found. */ return s+pos; } else { /* Continue searching. */ pos++; } } } return NULL; } /* Convert a string into a long long. Returns REDIS_OK if the string could be * parsed into a (non-overflowing) long long, REDIS_ERR otherwise. The value * will be set to the parsed value when appropriate. * * Note that this function demands that the string strictly represents * a long long: no spaces or other characters before or after the string * representing the number are accepted, nor zeroes at the start if not * for the string "0" representing the zero number. * * Because of its strictness, it is safe to use this function to check if * you can convert a string into a long long, and obtain back the string * from the number without any loss in the string representation. */ static int string2ll(const char *s, size_t slen, long long *value) { const char *p = s; size_t plen = 0; int negative = 0; unsigned long long v; if (plen == slen) return REDIS_ERR; /* Special case: first and only digit is 0. */ if (slen == 1 && p[0] == '0') { if (value != NULL) *value = 0; return REDIS_OK; } if (p[0] == '-') { negative = 1; p++; plen++; /* Abort on only a negative sign. */ if (plen == slen) return REDIS_ERR; } /* First digit should be 1-9, otherwise the string should just be 0. */ if (p[0] >= '1' && p[0] <= '9') { v = p[0]-'0'; p++; plen++; } else if (p[0] == '0' && slen == 1) { *value = 0; return REDIS_OK; } else { return REDIS_ERR; } while (plen < slen && p[0] >= '0' && p[0] <= '9') { if (v > (ULLONG_MAX / 10)) /* Overflow. */ return REDIS_ERR; v *= 10; if (v > (ULLONG_MAX - (p[0]-'0'))) /* Overflow. */ return REDIS_ERR; v += p[0]-'0'; p++; plen++; } /* Return if not all bytes were used. */ if (plen < slen) return REDIS_ERR; if (negative) { if (v > ((unsigned long long)(-(LLONG_MIN+1))+1)) /* Overflow. */ return REDIS_ERR; if (value != NULL) *value = -v; } else { if (v > LLONG_MAX) /* Overflow. */ return REDIS_ERR; if (value != NULL) *value = v; } return REDIS_OK; } static char *readLine(redisReader *r, int *_len) { char *p, *s; int len; p = r->buf+r->pos; s = seekNewline(p,(r->len-r->pos)); if (s != NULL) { len = s-(r->buf+r->pos); r->pos += len+2; /* skip \r\n */ if (_len) *_len = len; return p; } return NULL; } static void moveToNextTask(redisReader *r) { redisReadTask *cur, *prv; while (r->ridx >= 0) { /* Return a.s.a.p. when the stack is now empty. */ if (r->ridx == 0) { r->ridx--; return; } cur = r->task[r->ridx]; prv = r->task[r->ridx-1]; assert(prv->type == REDIS_REPLY_ARRAY || prv->type == REDIS_REPLY_MAP || prv->type == REDIS_REPLY_SET || prv->type == REDIS_REPLY_PUSH); if (cur->idx == prv->elements-1) { r->ridx--; } else { /* Reset the type because the next item can be anything */ assert(cur->idx < prv->elements); cur->type = -1; cur->elements = -1; cur->idx++; return; } } } static int processLineItem(redisReader *r) { redisReadTask *cur = r->task[r->ridx]; void *obj; char *p; int len; if ((p = readLine(r,&len)) != NULL) { if (cur->type == REDIS_REPLY_INTEGER) { if (r->fn && r->fn->createInteger) { long long v; if (string2ll(p, len, &v) == REDIS_ERR) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Bad integer value"); return REDIS_ERR; } obj = r->fn->createInteger(cur,v); } else { obj = (void*)REDIS_REPLY_INTEGER; } } else if (cur->type == REDIS_REPLY_DOUBLE) { if (r->fn && r->fn->createDouble) { char buf[326], *eptr; double d; if ((size_t)len >= sizeof(buf)) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Double value is too large"); return REDIS_ERR; } memcpy(buf,p,len); buf[len] = '\0'; if (strcasecmp(buf,",inf") == 0) { d = INFINITY; /* Positive infinite. */ } else if (strcasecmp(buf,",-inf") == 0) { d = -INFINITY; /* Negative infinite. */ } else if ((len == 3 && strcasecmp(buf,"nan") == 0) || (len == 4 && strcasecmp(buf, "-nan") == 0)) { d = NAN; /* nan. */ } else { d = strtod((char*)buf,&eptr); if (buf[0] == '\0' || eptr[0] != '\0' || isnan(d)) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Bad double value"); return REDIS_ERR; } } obj = r->fn->createDouble(cur,d,buf,len); } else { obj = (void*)REDIS_REPLY_DOUBLE; } } else if (cur->type == REDIS_REPLY_NIL) { if (r->fn && r->fn->createNil) obj = r->fn->createNil(cur); else obj = (void*)REDIS_REPLY_NIL; } else if (cur->type == REDIS_REPLY_BOOL) { int bval = p[0] == 't' || p[0] == 'T'; if (r->fn && r->fn->createBool) obj = r->fn->createBool(cur,bval); else obj = (void*)REDIS_REPLY_BOOL; } else { /* Type will be error or status. */ if (r->fn && r->fn->createString) obj = r->fn->createString(cur,p,len); else obj = (void*)(size_t)(cur->type); } if (obj == NULL) { __redisReaderSetErrorOOM(r); return REDIS_ERR; } /* Set reply if this is the root object. */ if (r->ridx == 0) r->reply = obj; moveToNextTask(r); return REDIS_OK; } return REDIS_ERR; } static int processBulkItem(redisReader *r) { redisReadTask *cur = r->task[r->ridx]; void *obj = NULL; char *p, *s; long long len; unsigned long bytelen; int success = 0; p = r->buf+r->pos; s = seekNewline(p,r->len-r->pos); if (s != NULL) { p = r->buf+r->pos; bytelen = s-(r->buf+r->pos)+2; /* include \r\n */ if (string2ll(p, bytelen - 2, &len) == REDIS_ERR) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Bad bulk string length"); return REDIS_ERR; } if (len < -1 || (LLONG_MAX > SIZE_MAX && len > (long long)SIZE_MAX)) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Bulk string length out of range"); return REDIS_ERR; } if (len == -1) { /* The nil object can always be created. */ if (r->fn && r->fn->createNil) obj = r->fn->createNil(cur); else obj = (void*)REDIS_REPLY_NIL; success = 1; } else { /* Only continue when the buffer contains the entire bulk item. */ bytelen += len+2; /* include \r\n */ if (r->pos+bytelen <= r->len) { if ((cur->type == REDIS_REPLY_VERB && len < 4) || (cur->type == REDIS_REPLY_VERB && s[5] != ':')) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Verbatim string 4 bytes of content type are " "missing or incorrectly encoded."); return REDIS_ERR; } if (r->fn && r->fn->createString) obj = r->fn->createString(cur,s+2,len); else obj = (void*)(long)cur->type; success = 1; } } /* Proceed when obj was created. */ if (success) { if (obj == NULL) { __redisReaderSetErrorOOM(r); return REDIS_ERR; } r->pos += bytelen; /* Set reply if this is the root object. */ if (r->ridx == 0) r->reply = obj; moveToNextTask(r); return REDIS_OK; } } return REDIS_ERR; } static int redisReaderGrow(redisReader *r) { redisReadTask **aux; int newlen; /* Grow our stack size */ newlen = r->tasks + REDIS_READER_STACK_SIZE; aux = hi_realloc(r->task, sizeof(*r->task) * newlen); if (aux == NULL) goto oom; r->task = aux; /* Allocate new tasks */ for (; r->tasks < newlen; r->tasks++) { r->task[r->tasks] = hi_calloc(1, sizeof(**r->task)); if (r->task[r->tasks] == NULL) goto oom; } return REDIS_OK; oom: __redisReaderSetErrorOOM(r); return REDIS_ERR; } /* Process the array, map and set types. */ static int processAggregateItem(redisReader *r) { redisReadTask *cur = r->task[r->ridx]; void *obj; char *p; long long elements; int root = 0, len; /* Set error for nested multi bulks with depth > 7 */ if (r->ridx == r->tasks - 1) { if (redisReaderGrow(r) == REDIS_ERR) return REDIS_ERR; } if ((p = readLine(r,&len)) != NULL) { if (string2ll(p, len, &elements) == REDIS_ERR) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Bad multi-bulk length"); return REDIS_ERR; } root = (r->ridx == 0); if (elements < -1 || (LLONG_MAX > SIZE_MAX && elements > SIZE_MAX) || (r->maxelements > 0 && elements > r->maxelements)) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Multi-bulk length out of range"); return REDIS_ERR; } if (elements == -1) { if (r->fn && r->fn->createNil) obj = r->fn->createNil(cur); else obj = (void*)REDIS_REPLY_NIL; if (obj == NULL) { __redisReaderSetErrorOOM(r); return REDIS_ERR; } moveToNextTask(r); } else { if (cur->type == REDIS_REPLY_MAP) elements *= 2; if (r->fn && r->fn->createArray) obj = r->fn->createArray(cur,elements); else obj = (void*)(long)cur->type; if (obj == NULL) { __redisReaderSetErrorOOM(r); return REDIS_ERR; } /* Modify task stack when there are more than 0 elements. */ if (elements > 0) { cur->elements = elements; cur->obj = obj; r->ridx++; r->task[r->ridx]->type = -1; r->task[r->ridx]->elements = -1; r->task[r->ridx]->idx = 0; r->task[r->ridx]->obj = NULL; r->task[r->ridx]->parent = cur; r->task[r->ridx]->privdata = r->privdata; } else { moveToNextTask(r); } } /* Set reply if this is the root object. */ if (root) r->reply = obj; return REDIS_OK; } return REDIS_ERR; } static int processItem(redisReader *r) { redisReadTask *cur = r->task[r->ridx]; char *p; /* check if we need to read type */ if (cur->type < 0) { if ((p = readBytes(r,1)) != NULL) { switch (p[0]) { case '-': cur->type = REDIS_REPLY_ERROR; break; case '+': cur->type = REDIS_REPLY_STATUS; break; case ':': cur->type = REDIS_REPLY_INTEGER; break; case ',': cur->type = REDIS_REPLY_DOUBLE; break; case '_': cur->type = REDIS_REPLY_NIL; break; case '$': cur->type = REDIS_REPLY_STRING; break; case '*': cur->type = REDIS_REPLY_ARRAY; break; case '%': cur->type = REDIS_REPLY_MAP; break; case '~': cur->type = REDIS_REPLY_SET; break; case '#': cur->type = REDIS_REPLY_BOOL; break; case '=': cur->type = REDIS_REPLY_VERB; break; case '>': cur->type = REDIS_REPLY_PUSH; break; default: __redisReaderSetErrorProtocolByte(r,*p); return REDIS_ERR; } } else { /* could not consume 1 byte */ return REDIS_ERR; } } /* process typed item */ switch(cur->type) { case REDIS_REPLY_ERROR: case REDIS_REPLY_STATUS: case REDIS_REPLY_INTEGER: case REDIS_REPLY_DOUBLE: case REDIS_REPLY_NIL: case REDIS_REPLY_BOOL: return processLineItem(r); case REDIS_REPLY_STRING: case REDIS_REPLY_VERB: return processBulkItem(r); case REDIS_REPLY_ARRAY: case REDIS_REPLY_MAP: case REDIS_REPLY_SET: case REDIS_REPLY_PUSH: return processAggregateItem(r); default: assert(NULL); return REDIS_ERR; /* Avoid warning. */ } } redisReader *redisReaderCreateWithFunctions(redisReplyObjectFunctions *fn) { redisReader *r; r = hi_calloc(1,sizeof(redisReader)); if (r == NULL) return NULL; r->buf = sdsempty(); if (r->buf == NULL) goto oom; r->task = hi_calloc(REDIS_READER_STACK_SIZE, sizeof(*r->task)); if (r->task == NULL) goto oom; for (; r->tasks < REDIS_READER_STACK_SIZE; r->tasks++) { r->task[r->tasks] = hi_calloc(1, sizeof(**r->task)); if (r->task[r->tasks] == NULL) goto oom; } r->fn = fn; r->maxbuf = REDIS_READER_MAX_BUF; r->maxelements = REDIS_READER_MAX_ARRAY_ELEMENTS; r->ridx = -1; return r; oom: redisReaderFree(r); return NULL; } void redisReaderFree(redisReader *r) { if (r == NULL) return; if (r->reply != NULL && r->fn && r->fn->freeObject) r->fn->freeObject(r->reply); if (r->task) { /* We know r->task[i] is allocated if i < r->tasks */ for (int i = 0; i < r->tasks; i++) { hi_free(r->task[i]); } hi_free(r->task); } sdsfree(r->buf); hi_free(r); } int redisReaderFeed(redisReader *r, const char *buf, size_t len) { sds newbuf; /* Return early when this reader is in an erroneous state. */ if (r->err) return REDIS_ERR; /* Copy the provided buffer. */ if (buf != NULL && len >= 1) { /* Destroy internal buffer when it is empty and is quite large. */ if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) { sdsfree(r->buf); r->buf = sdsempty(); if (r->buf == 0) goto oom; r->pos = 0; } newbuf = sdscatlen(r->buf,buf,len); if (newbuf == NULL) goto oom; r->buf = newbuf; r->len = sdslen(r->buf); } return REDIS_OK; oom: __redisReaderSetErrorOOM(r); return REDIS_ERR; } int redisReaderGetReply(redisReader *r, void **reply) { /* Default target pointer to NULL. */ if (reply != NULL) *reply = NULL; /* Return early when this reader is in an erroneous state. */ if (r->err) return REDIS_ERR; /* When the buffer is empty, there will never be a reply. */ if (r->len == 0) return REDIS_OK; /* Set first item to process when the stack is empty. */ if (r->ridx == -1) { r->task[0]->type = -1; r->task[0]->elements = -1; r->task[0]->idx = -1; r->task[0]->obj = NULL; r->task[0]->parent = NULL; r->task[0]->privdata = r->privdata; r->ridx = 0; } /* Process items in reply. */ while (r->ridx >= 0) if (processItem(r) != REDIS_OK) break; /* Return ASAP when an error occurred. */ if (r->err) return REDIS_ERR; /* Discard part of the buffer when we've consumed at least 1k, to avoid * doing unnecessary calls to memmove() in sds.c. */ if (r->pos >= 1024) { if (sdsrange(r->buf,r->pos,-1) < 0) return REDIS_ERR; r->pos = 0; r->len = sdslen(r->buf); } /* Emit a reply when there is one. */ if (r->ridx == -1) { if (reply != NULL) { *reply = r->reply; } else if (r->reply != NULL && r->fn && r->fn->freeObject) { r->fn->freeObject(r->reply); } r->reply = NULL; } return REDIS_OK; } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/read.h000066400000000000000000000114661515654015100303270ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2011, Pieter Noordhuis * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __HIREDIS_READ_H #define __HIREDIS_READ_H #include /* for size_t */ #define REDIS_ERR -1 #define REDIS_OK 0 /* When an error occurs, the err flag in a context is set to hold the type of * error that occurred. REDIS_ERR_IO means there was an I/O error and you * should use the "errno" variable to find out what is wrong. * For other values, the "errstr" field will hold a description. */ #define REDIS_ERR_IO 1 /* Error in read or write */ #define REDIS_ERR_EOF 3 /* End of file */ #define REDIS_ERR_PROTOCOL 4 /* Protocol error */ #define REDIS_ERR_OOM 5 /* Out of memory */ #define REDIS_ERR_TIMEOUT 6 /* Timed out */ #define REDIS_ERR_OTHER 2 /* Everything else... */ #define REDIS_REPLY_STRING 1 #define REDIS_REPLY_ARRAY 2 #define REDIS_REPLY_INTEGER 3 #define REDIS_REPLY_NIL 4 #define REDIS_REPLY_STATUS 5 #define REDIS_REPLY_ERROR 6 #define REDIS_REPLY_DOUBLE 7 #define REDIS_REPLY_BOOL 8 #define REDIS_REPLY_MAP 9 #define REDIS_REPLY_SET 10 #define REDIS_REPLY_ATTR 11 #define REDIS_REPLY_PUSH 12 #define REDIS_REPLY_BIGNUM 13 #define REDIS_REPLY_VERB 14 /* Default max unused reader buffer. */ #define REDIS_READER_MAX_BUF (1024*16) /* Default multi-bulk element limit */ #define REDIS_READER_MAX_ARRAY_ELEMENTS ((1LL<<32) - 1) #ifdef __cplusplus extern "C" { #endif typedef struct redisReadTask { int type; long long elements; /* number of elements in multibulk container */ int idx; /* index in parent (array) object */ void *obj; /* holds user-generated value for a read task */ struct redisReadTask *parent; /* parent task */ void *privdata; /* user-settable arbitrary field */ } redisReadTask; typedef struct redisReplyObjectFunctions { void *(*createString)(const redisReadTask*, char*, size_t); void *(*createArray)(const redisReadTask*, size_t); void *(*createInteger)(const redisReadTask*, long long); void *(*createDouble)(const redisReadTask*, double, char*, size_t); void *(*createNil)(const redisReadTask*); void *(*createBool)(const redisReadTask*, int); void (*freeObject)(void*); } redisReplyObjectFunctions; typedef struct redisReader { int err; /* Error flags, 0 when there is no error */ char errstr[128]; /* String representation of error when applicable */ char *buf; /* Read buffer */ size_t pos; /* Buffer cursor */ size_t len; /* Buffer length */ size_t maxbuf; /* Max length of unused buffer */ long long maxelements; /* Max multi-bulk elements */ redisReadTask **task; int tasks; int ridx; /* Index of current read task */ void *reply; /* Temporary reply pointer */ redisReplyObjectFunctions *fn; void *privdata; } redisReader; /* Public API for the protocol parser. */ redisReader *redisReaderCreateWithFunctions(redisReplyObjectFunctions *fn); void redisReaderFree(redisReader *r); int redisReaderFeed(redisReader *r, const char *buf, size_t len); int redisReaderGetReply(redisReader *r, void **reply); #define redisReaderSetPrivdata(_r, _p) (int)(((redisReader*)(_r))->privdata = (_p)) #define redisReaderGetObject(_r) (((redisReader*)(_r))->reply) #define redisReaderGetError(_r) (((redisReader*)(_r))->errstr) #ifdef __cplusplus } #endif #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/sds.c000066400000000000000000001165161515654015100302020ustar00rootroot00000000000000/* SDSLib 2.0 -- A C dynamic strings library * * Copyright (c) 2006-2015, Salvatore Sanfilippo * Copyright (c) 2015, Oran Agra * Copyright (c) 2015, Redis Labs, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "fmacros.h" #include #include #include #include #include #include #include "sds.h" #include "sdsalloc.h" static inline int sdsHdrSize(char type) { switch(type&SDS_TYPE_MASK) { case SDS_TYPE_5: return sizeof(struct sdshdr5); case SDS_TYPE_8: return sizeof(struct sdshdr8); case SDS_TYPE_16: return sizeof(struct sdshdr16); case SDS_TYPE_32: return sizeof(struct sdshdr32); case SDS_TYPE_64: return sizeof(struct sdshdr64); } return 0; } static inline char sdsReqType(size_t string_size) { if (string_size < 32) return SDS_TYPE_5; if (string_size < 0xff) return SDS_TYPE_8; if (string_size < 0xffff) return SDS_TYPE_16; if (string_size < 0xffffffff) return SDS_TYPE_32; return SDS_TYPE_64; } /* Create a new sds string with the content specified by the 'init' pointer * and 'initlen'. * If NULL is used for 'init' the string is initialized with zero bytes. * * The string is always null-termined (all the sds strings are, always) so * even if you create an sds string with: * * mystring = sdsnewlen("abc",3); * * You can print the string with printf() as there is an implicit \0 at the * end of the string. However the string is binary safe and can contain * \0 characters in the middle, as the length is stored in the sds header. */ sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; char type = sdsReqType(initlen); /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */ if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; int hdrlen = sdsHdrSize(type); unsigned char *fp; /* flags pointer. */ sh = s_malloc(hdrlen+initlen+1); if (sh == NULL) return NULL; if (!init) memset(sh, 0, hdrlen+initlen+1); s = (char*)sh+hdrlen; fp = ((unsigned char*)s)-1; switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS); break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } } if (initlen && init) memcpy(s, init, initlen); s[initlen] = '\0'; return s; } /* Create an empty (zero length) sds string. Even in this case the string * always has an implicit null term. */ sds sdsempty(void) { return sdsnewlen("",0); } /* Create a new sds string starting from a null terminated C string. */ sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); } /* Duplicate an sds string. */ sds sdsdup(const sds s) { return sdsnewlen(s, sdslen(s)); } /* Free an sds string. No operation is performed if 's' is NULL. */ void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); } /* Set the sds string length to the length as obtained with strlen(), so * considering as content only up to the first null term character. * * This function is useful when the sds string is hacked manually in some * way, like in the following example: * * s = sdsnew("foobar"); * s[2] = '\0'; * sdsupdatelen(s); * printf("%d\n", sdslen(s)); * * The output will be "2", but if we comment out the call to sdsupdatelen() * the output will be "6" as the string was modified but the logical length * remains 6 bytes. */ void sdsupdatelen(sds s) { int reallen = strlen(s); sdssetlen(s, reallen); } /* Modify an sds string in-place to make it empty (zero length). * However all the existing buffer is not discarded but set as free space * so that next append operations will not require allocations up to the * number of bytes previously available. */ void sdsclear(sds s) { sdssetlen(s, 0); s[0] = '\0'; } /* Enlarge the free space at the end of the sds string so that the caller * is sure that after calling this function can overwrite up to addlen * bytes after the end of the string, plus one more byte for nul term. * * Note: this does not change the *length* of the sds string as returned * by sdslen(), but only the free buffer space we have. */ sds sdsMakeRoomFor(sds s, size_t addlen) { void *sh, *newsh; size_t avail = sdsavail(s); size_t len, newlen; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; /* Return ASAP if there is enough space left. */ if (avail >= addlen) return s; len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; type = sdsReqType(newlen); /* Don't use type 5: the user is appending to the string and type 5 is * not able to remember empty space, so sdsMakeRoomFor() must be called * at every appending operation. */ if (type == SDS_TYPE_5) type = SDS_TYPE_8; hdrlen = sdsHdrSize(type); if (oldtype==type) { newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { /* Since the header size changes, need to move the string forward, * and can't use realloc */ newsh = s_malloc(hdrlen+newlen+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; sdssetlen(s, len); } sdssetalloc(s, newlen); return s; } /* Reallocate the sds string so that it has no free space at the end. The * contained string remains not altered, but next concatenation operations * will require a reallocation. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdsRemoveFreeSpace(sds s) { void *sh, *newsh; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; size_t len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); type = sdsReqType(len); hdrlen = sdsHdrSize(type); if (oldtype==type) { newsh = s_realloc(sh, hdrlen+len+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { newsh = s_malloc(hdrlen+len+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; sdssetlen(s, len); } sdssetalloc(s, len); return s; } /* Return the total size of the allocation of the specifed sds string, * including: * 1) The sds header before the pointer. * 2) The string. * 3) The free buffer at the end if any. * 4) The implicit null term. */ size_t sdsAllocSize(sds s) { size_t alloc = sdsalloc(s); return sdsHdrSize(s[-1])+alloc+1; } /* Return the pointer of the actual SDS allocation (normally SDS strings * are referenced by the start of the string buffer). */ void *sdsAllocPtr(sds s) { return (void*) (s-sdsHdrSize(s[-1])); } /* Increment the sds length and decrements the left free space at the * end of the string according to 'incr'. Also set the null term * in the new end of the string. * * This function is used in order to fix the string length after the * user calls sdsMakeRoomFor(), writes something after the end of * the current string, and finally needs to set the new length. * * Note: it is possible to use a negative increment in order to * right-trim the string. * * Usage example: * * Using sdsIncrLen() and sdsMakeRoomFor() it is possible to mount the * following schema, to cat bytes coming from the kernel to the end of an * sds string without copying into an intermediate buffer: * * oldlen = sdslen(s); * s = sdsMakeRoomFor(s, BUFFER_SIZE); * nread = read(fd, s+oldlen, BUFFER_SIZE); * ... check for nread <= 0 and handle it ... * sdsIncrLen(s, nread); */ void sdsIncrLen(sds s, int incr) { unsigned char flags = s[-1]; size_t len; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; unsigned char oldlen = SDS_TYPE_5_LEN(flags); assert((incr > 0 && oldlen+incr < 32) || (incr < 0 && oldlen >= (unsigned int)(-incr))); *fp = SDS_TYPE_5 | ((oldlen+incr) << SDS_TYPE_BITS); len = oldlen+incr; break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr))); len = (sh->len += incr); break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr))); len = (sh->len += incr); break; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); assert((incr >= 0 && sh->alloc-sh->len >= (unsigned int)incr) || (incr < 0 && sh->len >= (unsigned int)(-incr))); len = (sh->len += incr); break; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); assert((incr >= 0 && sh->alloc-sh->len >= (uint64_t)incr) || (incr < 0 && sh->len >= (uint64_t)(-incr))); len = (sh->len += incr); break; } default: len = 0; /* Just to avoid compilation warnings. */ } s[len] = '\0'; } /* Grow the sds to have the specified length. Bytes that were not part of * the original length of the sds will be set to zero. * * if the specified length is smaller than the current length, no operation * is performed. */ sds sdsgrowzero(sds s, size_t len) { size_t curlen = sdslen(s); if (len <= curlen) return s; s = sdsMakeRoomFor(s,len-curlen); if (s == NULL) return NULL; /* Make sure added region doesn't contain garbage */ memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */ sdssetlen(s, len); return s; } /* Append the specified binary-safe string pointed by 't' of 'len' bytes to the * end of the specified sds string 's'. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s); s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; memcpy(s+curlen, t, len); sdssetlen(s, curlen+len); s[curlen+len] = '\0'; return s; } /* Append the specified null termianted C string to the sds string 's'. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); } /* Append the specified sds 't' to the existing sds 's'. * * After the call, the modified sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatsds(sds s, const sds t) { return sdscatlen(s, t, sdslen(t)); } /* Destructively modify the sds string 's' to hold the specified binary * safe string pointed by 't' of length 'len' bytes. */ sds sdscpylen(sds s, const char *t, size_t len) { if (sdsalloc(s) < len) { s = sdsMakeRoomFor(s,len-sdslen(s)); if (s == NULL) return NULL; } memcpy(s, t, len); s[len] = '\0'; sdssetlen(s, len); return s; } /* Like sdscpylen() but 't' must be a null-termined string so that the length * of the string is obtained with strlen(). */ sds sdscpy(sds s, const char *t) { return sdscpylen(s, t, strlen(t)); } /* Helper for sdscatlonglong() doing the actual number -> string * conversion. 's' must point to a string with room for at least * SDS_LLSTR_SIZE bytes. * * The function returns the length of the null-terminated string * representation stored at 's'. */ #define SDS_LLSTR_SIZE 21 int sdsll2str(char *s, long long value) { char *p, aux; unsigned long long v; size_t l; /* Generate the string representation, this method produces * an reversed string. */ v = (value < 0) ? -value : value; p = s; do { *p++ = '0'+(v%10); v /= 10; } while(v); if (value < 0) *p++ = '-'; /* Compute length and add null term. */ l = p-s; *p = '\0'; /* Reverse the string. */ p--; while(s < p) { aux = *s; *s = *p; *p = aux; s++; p--; } return l; } /* Identical sdsll2str(), but for unsigned long long type. */ int sdsull2str(char *s, unsigned long long v) { char *p, aux; size_t l; /* Generate the string representation, this method produces * an reversed string. */ p = s; do { *p++ = '0'+(v%10); v /= 10; } while(v); /* Compute length and add null term. */ l = p-s; *p = '\0'; /* Reverse the string. */ p--; while(s < p) { aux = *s; *s = *p; *p = aux; s++; p--; } return l; } /* Create an sds string from a long long value. It is much faster than: * * sdscatprintf(sdsempty(),"%lld\n", value); */ sds sdsfromlonglong(long long value) { char buf[SDS_LLSTR_SIZE]; int len = sdsll2str(buf,value); return sdsnewlen(buf,len); } /* Like sdscatprintf() but gets va_list instead of being variadic. */ sds sdscatvprintf(sds s, const char *fmt, va_list ap) { va_list cpy; char staticbuf[1024], *buf = staticbuf, *t; size_t buflen = strlen(fmt)*2; /* We try to start using a static buffer for speed. * If not possible we revert to heap allocation. */ if (buflen > sizeof(staticbuf)) { buf = s_malloc(buflen); if (buf == NULL) return NULL; } else { buflen = sizeof(staticbuf); } /* Try with buffers two times bigger every time we fail to * fit the string in the current buffer size. */ while(1) { buf[buflen-2] = '\0'; va_copy(cpy,ap); vsnprintf(buf, buflen, fmt, cpy); va_end(cpy); if (buf[buflen-2] != '\0') { if (buf != staticbuf) s_free(buf); buflen *= 2; buf = s_malloc(buflen); if (buf == NULL) return NULL; continue; } break; } /* Finally concat the obtained string to the SDS string and return it. */ t = sdscat(s, buf); if (buf != staticbuf) s_free(buf); return t; } /* Append to the sds string 's' a string obtained using printf-alike format * specifier. * * After the call, the modified sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. * * Example: * * s = sdsnew("Sum is: "); * s = sdscatprintf(s,"%d+%d = %d",a,b,a+b). * * Often you need to create a string from scratch with the printf-alike * format. When this is the need, just use sdsempty() as the target string: * * s = sdscatprintf(sdsempty(), "... your format ...", args); */ sds sdscatprintf(sds s, const char *fmt, ...) { va_list ap; char *t; va_start(ap, fmt); t = sdscatvprintf(s,fmt,ap); va_end(ap); return t; } /* This function is similar to sdscatprintf, but much faster as it does * not rely on sprintf() family functions implemented by the libc that * are often very slow. Moreover directly handling the sds string as * new data is concatenated provides a performance improvement. * * However this function only handles an incompatible subset of printf-alike * format specifiers: * * %s - C String * %S - SDS string * %i - signed int * %I - 64 bit signed integer (long long, int64_t) * %u - unsigned int * %U - 64 bit unsigned integer (unsigned long long, uint64_t) * %% - Verbatim "%" character. */ sds sdscatfmt(sds s, char const *fmt, ...) { const char *f = fmt; int i; va_list ap; va_start(ap,fmt); i = sdslen(s); /* Position of the next byte to write to dest str. */ while(*f) { char next, *str; size_t l; long long num; unsigned long long unum; /* Make sure there is always space for at least 1 char. */ if (sdsavail(s)==0) { s = sdsMakeRoomFor(s,1); if (s == NULL) goto fmt_error; } switch(*f) { case '%': next = *(f+1); f++; switch(next) { case 's': case 'S': str = va_arg(ap,char*); l = (next == 's') ? strlen(str) : sdslen(str); if (sdsavail(s) < l) { s = sdsMakeRoomFor(s,l); if (s == NULL) goto fmt_error; } memcpy(s+i,str,l); sdsinclen(s,l); i += l; break; case 'i': case 'I': if (next == 'i') num = va_arg(ap,int); else num = va_arg(ap,long long); { char buf[SDS_LLSTR_SIZE]; l = sdsll2str(buf,num); if (sdsavail(s) < l) { s = sdsMakeRoomFor(s,l); if (s == NULL) goto fmt_error; } memcpy(s+i,buf,l); sdsinclen(s,l); i += l; } break; case 'u': case 'U': if (next == 'u') unum = va_arg(ap,unsigned int); else unum = va_arg(ap,unsigned long long); { char buf[SDS_LLSTR_SIZE]; l = sdsull2str(buf,unum); if (sdsavail(s) < l) { s = sdsMakeRoomFor(s,l); if (s == NULL) goto fmt_error; } memcpy(s+i,buf,l); sdsinclen(s,l); i += l; } break; default: /* Handle %% and generally %. */ s[i++] = next; sdsinclen(s,1); break; } break; default: s[i++] = *f; sdsinclen(s,1); break; } f++; } va_end(ap); /* Add null-term */ s[i] = '\0'; return s; fmt_error: va_end(ap); return NULL; } /* Remove the part of the string from left and from right composed just of * contiguous characters found in 'cset', that is a null terminted C string. * * After the call, the modified sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. * * Example: * * s = sdsnew("AA...AA.a.aa.aHelloWorld :::"); * s = sdstrim(s,"Aa. :"); * printf("%s\n", s); * * Output will be just "Hello World". */ sds sdstrim(sds s, const char *cset) { char *start, *end, *sp, *ep; size_t len; sp = start = s; ep = end = s+sdslen(s)-1; while(sp <= end && strchr(cset, *sp)) sp++; while(ep > sp && strchr(cset, *ep)) ep--; len = (sp > ep) ? 0 : ((ep-sp)+1); if (s != sp) memmove(s, sp, len); s[len] = '\0'; sdssetlen(s,len); return s; } /* Turn the string into a smaller (or equal) string containing only the * substring specified by the 'start' and 'end' indexes. * * start and end can be negative, where -1 means the last character of the * string, -2 the penultimate character, and so forth. * * The interval is inclusive, so the start and end characters will be part * of the resulting string. * * The string is modified in-place. * * Return value: * -1 (error) if sdslen(s) is larger than maximum positive ssize_t value. * 0 on success. * * Example: * * s = sdsnew("Hello World"); * sdsrange(s,1,-1); => "ello World" */ int sdsrange(sds s, ssize_t start, ssize_t end) { size_t newlen, len = sdslen(s); if (len > SSIZE_MAX) return -1; if (len == 0) return 0; if (start < 0) { start = len+start; if (start < 0) start = 0; } if (end < 0) { end = len+end; if (end < 0) end = 0; } newlen = (start > end) ? 0 : (end-start)+1; if (newlen != 0) { if (start >= (ssize_t)len) { newlen = 0; } else if (end >= (ssize_t)len) { end = len-1; newlen = (start > end) ? 0 : (end-start)+1; } } else { start = 0; } if (start && newlen) memmove(s, s+start, newlen); s[newlen] = 0; sdssetlen(s,newlen); return 0; } /* Apply tolower() to every character of the sds string 's'. */ void sdstolower(sds s) { int len = sdslen(s), j; for (j = 0; j < len; j++) s[j] = tolower(s[j]); } /* Apply toupper() to every character of the sds string 's'. */ void sdstoupper(sds s) { int len = sdslen(s), j; for (j = 0; j < len; j++) s[j] = toupper(s[j]); } /* Compare two sds strings s1 and s2 with memcmp(). * * Return value: * * positive if s1 > s2. * negative if s1 < s2. * 0 if s1 and s2 are exactly the same binary string. * * If two strings share exactly the same prefix, but one of the two has * additional characters, the longer string is considered to be greater than * the smaller one. */ int sdscmp(const sds s1, const sds s2) { size_t l1, l2, minlen; int cmp; l1 = sdslen(s1); l2 = sdslen(s2); minlen = (l1 < l2) ? l1 : l2; cmp = memcmp(s1,s2,minlen); if (cmp == 0) return l1-l2; return cmp; } /* Split 's' with separator in 'sep'. An array * of sds strings is returned. *count will be set * by reference to the number of tokens returned. * * On out of memory, zero length string, zero length * separator, NULL is returned. * * Note that 'sep' is able to split a string using * a multi-character separator. For example * sdssplit("foo_-_bar","_-_"); will return two * elements "foo" and "bar". * * This version of the function is binary-safe but * requires length arguments. sdssplit() is just the * same function but for zero-terminated strings. */ sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) { int elements = 0, slots = 5, start = 0, j; sds *tokens; if (seplen < 1 || len < 0) return NULL; tokens = s_malloc(sizeof(sds)*slots); if (tokens == NULL) return NULL; if (len == 0) { *count = 0; return tokens; } for (j = 0; j < (len-(seplen-1)); j++) { /* make sure there is room for the next element and the final one */ if (slots < elements+2) { sds *newtokens; slots *= 2; newtokens = s_realloc(tokens,sizeof(sds)*slots); if (newtokens == NULL) goto cleanup; tokens = newtokens; } /* search the separator */ if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) { tokens[elements] = sdsnewlen(s+start,j-start); if (tokens[elements] == NULL) goto cleanup; elements++; start = j+seplen; j = j+seplen-1; /* skip the separator */ } } /* Add the final element. We are sure there is room in the tokens array. */ tokens[elements] = sdsnewlen(s+start,len-start); if (tokens[elements] == NULL) goto cleanup; elements++; *count = elements; return tokens; cleanup: { int i; for (i = 0; i < elements; i++) sdsfree(tokens[i]); s_free(tokens); *count = 0; return NULL; } } /* Free the result returned by sdssplitlen(), or do nothing if 'tokens' is NULL. */ void sdsfreesplitres(sds *tokens, int count) { if (!tokens) return; while(count--) sdsfree(tokens[count]); s_free(tokens); } /* Append to the sds string "s" an escaped string representation where * all the non-printable characters (tested with isprint()) are turned into * escapes in the form "\n\r\a...." or "\x". * * After the call, the modified sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatrepr(sds s, const char *p, size_t len) { s = sdscatlen(s,"\"",1); while(len--) { switch(*p) { case '\\': case '"': s = sdscatprintf(s,"\\%c",*p); break; case '\n': s = sdscatlen(s,"\\n",2); break; case '\r': s = sdscatlen(s,"\\r",2); break; case '\t': s = sdscatlen(s,"\\t",2); break; case '\a': s = sdscatlen(s,"\\a",2); break; case '\b': s = sdscatlen(s,"\\b",2); break; default: if (isprint(*p)) s = sdscatprintf(s,"%c",*p); else s = sdscatprintf(s,"\\x%02x",(unsigned char)*p); break; } p++; } return sdscatlen(s,"\"",1); } /* Helper function for sdssplitargs() that converts a hex digit into an * integer from 0 to 15 */ int hex_digit_to_int(char c) { switch(c) { case '0': return 0; case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; case '6': return 6; case '7': return 7; case '8': return 8; case '9': return 9; case 'a': case 'A': return 10; case 'b': case 'B': return 11; case 'c': case 'C': return 12; case 'd': case 'D': return 13; case 'e': case 'E': return 14; case 'f': case 'F': return 15; default: return 0; } } /* Split a line into arguments, where every argument can be in the * following programming-language REPL-alike form: * * foo bar "newline are supported\n" and "\xff\x00otherstuff" * * The number of arguments is stored into *argc, and an array * of sds is returned. * * The caller should free the resulting array of sds strings with * sdsfreesplitres(). * * Note that sdscatrepr() is able to convert back a string into * a quoted string in the same format sdssplitargs() is able to parse. * * The function returns the allocated tokens on success, even when the * input string is empty, or NULL if the input contains unbalanced * quotes or closed quotes followed by non space characters * as in: "foo"bar or "foo' */ sds *sdssplitargs(const char *line, int *argc) { const char *p = line; char *current = NULL; char **vector = NULL; *argc = 0; while(1) { /* skip blanks */ while(*p && isspace(*p)) p++; if (*p) { /* get a token */ int inq=0; /* set to 1 if we are in "quotes" */ int insq=0; /* set to 1 if we are in 'single quotes' */ int done=0; if (current == NULL) current = sdsempty(); while(!done) { if (inq) { if (*p == '\\' && *(p+1) == 'x' && isxdigit(*(p+2)) && isxdigit(*(p+3))) { unsigned char byte; byte = (hex_digit_to_int(*(p+2))*16)+ hex_digit_to_int(*(p+3)); current = sdscatlen(current,(char*)&byte,1); p += 3; } else if (*p == '\\' && *(p+1)) { char c; p++; switch(*p) { case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'b': c = '\b'; break; case 'a': c = '\a'; break; default: c = *p; break; } current = sdscatlen(current,&c,1); } else if (*p == '"') { /* closing quote must be followed by a space or * nothing at all. */ if (*(p+1) && !isspace(*(p+1))) goto err; done=1; } else if (!*p) { /* unterminated quotes */ goto err; } else { current = sdscatlen(current,p,1); } } else if (insq) { if (*p == '\\' && *(p+1) == '\'') { p++; current = sdscatlen(current,"'",1); } else if (*p == '\'') { /* closing quote must be followed by a space or * nothing at all. */ if (*(p+1) && !isspace(*(p+1))) goto err; done=1; } else if (!*p) { /* unterminated quotes */ goto err; } else { current = sdscatlen(current,p,1); } } else { switch(*p) { case ' ': case '\n': case '\r': case '\t': case '\0': done=1; break; case '"': inq=1; break; case '\'': insq=1; break; default: current = sdscatlen(current,p,1); break; } } if (*p) p++; } /* add the token to the vector */ { char **new_vector = s_realloc(vector,((*argc)+1)*sizeof(char*)); if (new_vector == NULL) { s_free(vector); return NULL; } vector = new_vector; vector[*argc] = current; (*argc)++; current = NULL; } } else { /* Even on empty input string return something not NULL. */ if (vector == NULL) vector = s_malloc(sizeof(void*)); return vector; } } err: while((*argc)--) sdsfree(vector[*argc]); s_free(vector); if (current) sdsfree(current); *argc = 0; return NULL; } /* Modify the string substituting all the occurrences of the set of * characters specified in the 'from' string to the corresponding character * in the 'to' array. * * For instance: sdsmapchars(mystring, "ho", "01", 2) * will have the effect of turning the string "hello" into "0ell1". * * The function returns the sds string pointer, that is always the same * as the input pointer since no resize is needed. */ sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen) { size_t j, i, l = sdslen(s); for (j = 0; j < l; j++) { for (i = 0; i < setlen; i++) { if (s[j] == from[i]) { s[j] = to[i]; break; } } } return s; } /* Join an array of C strings using the specified separator (also a C string). * Returns the result as an sds string. */ sds sdsjoin(char **argv, int argc, char *sep) { sds join = sdsempty(); int j; for (j = 0; j < argc; j++) { join = sdscat(join, argv[j]); if (j != argc-1) join = sdscat(join,sep); } return join; } /* Like sdsjoin, but joins an array of SDS strings. */ sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) { sds join = sdsempty(); int j; for (j = 0; j < argc; j++) { join = sdscatsds(join, argv[j]); if (j != argc-1) join = sdscatlen(join,sep,seplen); } return join; } /* Wrappers to the allocators used by SDS. Note that SDS will actually * just use the macros defined into sdsalloc.h in order to avoid to pay * the overhead of function calls. Here we define these wrappers only for * the programs SDS is linked to, if they want to touch the SDS internals * even if they use a different allocator. */ void *sds_malloc(size_t size) { return s_malloc(size); } void *sds_realloc(void *ptr, size_t size) { return s_realloc(ptr,size); } void sds_free(void *ptr) { s_free(ptr); } #if defined(SDS_TEST_MAIN) #include #include "testhelp.h" #include "limits.h" #define UNUSED(x) (void)(x) int sdsTest(void) { { sds x = sdsnew("foo"), y; test_cond("Create a string and obtain the length", sdslen(x) == 3 && memcmp(x,"foo\0",4) == 0) sdsfree(x); x = sdsnewlen("foo",2); test_cond("Create a string with specified length", sdslen(x) == 2 && memcmp(x,"fo\0",3) == 0) x = sdscat(x,"bar"); test_cond("Strings concatenation", sdslen(x) == 5 && memcmp(x,"fobar\0",6) == 0); x = sdscpy(x,"a"); test_cond("sdscpy() against an originally longer string", sdslen(x) == 1 && memcmp(x,"a\0",2) == 0) x = sdscpy(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk"); test_cond("sdscpy() against an originally shorter string", sdslen(x) == 33 && memcmp(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk\0",33) == 0) sdsfree(x); x = sdscatprintf(sdsempty(),"%d",123); test_cond("sdscatprintf() seems working in the base case", sdslen(x) == 3 && memcmp(x,"123\0",4) == 0) sdsfree(x); x = sdsnew("--"); x = sdscatfmt(x, "Hello %s World %I,%I--", "Hi!", LLONG_MIN,LLONG_MAX); test_cond("sdscatfmt() seems working in the base case", sdslen(x) == 60 && memcmp(x,"--Hello Hi! World -9223372036854775808," "9223372036854775807--",60) == 0) printf("[%s]\n",x); sdsfree(x); x = sdsnew("--"); x = sdscatfmt(x, "%u,%U--", UINT_MAX, ULLONG_MAX); test_cond("sdscatfmt() seems working with unsigned numbers", sdslen(x) == 35 && memcmp(x,"--4294967295,18446744073709551615--",35) == 0) sdsfree(x); x = sdsnew(" x "); sdstrim(x," x"); test_cond("sdstrim() works when all chars match", sdslen(x) == 0) sdsfree(x); x = sdsnew(" x "); sdstrim(x," "); test_cond("sdstrim() works when a single char remains", sdslen(x) == 1 && x[0] == 'x') sdsfree(x); x = sdsnew("xxciaoyyy"); sdstrim(x,"xy"); test_cond("sdstrim() correctly trims characters", sdslen(x) == 4 && memcmp(x,"ciao\0",5) == 0) y = sdsdup(x); sdsrange(y,1,1); test_cond("sdsrange(...,1,1)", sdslen(y) == 1 && memcmp(y,"i\0",2) == 0) sdsfree(y); y = sdsdup(x); sdsrange(y,1,-1); test_cond("sdsrange(...,1,-1)", sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) sdsfree(y); y = sdsdup(x); sdsrange(y,-2,-1); test_cond("sdsrange(...,-2,-1)", sdslen(y) == 2 && memcmp(y,"ao\0",3) == 0) sdsfree(y); y = sdsdup(x); sdsrange(y,2,1); test_cond("sdsrange(...,2,1)", sdslen(y) == 0 && memcmp(y,"\0",1) == 0) sdsfree(y); y = sdsdup(x); sdsrange(y,1,100); test_cond("sdsrange(...,1,100)", sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) sdsfree(y); y = sdsdup(x); sdsrange(y,100,100); test_cond("sdsrange(...,100,100)", sdslen(y) == 0 && memcmp(y,"\0",1) == 0) sdsfree(y); sdsfree(x); x = sdsnew("foo"); y = sdsnew("foa"); test_cond("sdscmp(foo,foa)", sdscmp(x,y) > 0) sdsfree(y); sdsfree(x); x = sdsnew("bar"); y = sdsnew("bar"); test_cond("sdscmp(bar,bar)", sdscmp(x,y) == 0) sdsfree(y); sdsfree(x); x = sdsnew("aar"); y = sdsnew("bar"); test_cond("sdscmp(bar,bar)", sdscmp(x,y) < 0) sdsfree(y); sdsfree(x); x = sdsnewlen("\a\n\0foo\r",7); y = sdscatrepr(sdsempty(),x,sdslen(x)); test_cond("sdscatrepr(...data...)", memcmp(y,"\"\\a\\n\\x00foo\\r\"",15) == 0) { unsigned int oldfree; char *p; int step = 10, j, i; sdsfree(x); sdsfree(y); x = sdsnew("0"); test_cond("sdsnew() free/len buffers", sdslen(x) == 1 && sdsavail(x) == 0); /* Run the test a few times in order to hit the first two * SDS header types. */ for (i = 0; i < 10; i++) { int oldlen = sdslen(x); x = sdsMakeRoomFor(x,step); int type = x[-1]&SDS_TYPE_MASK; test_cond("sdsMakeRoomFor() len", sdslen(x) == oldlen); if (type != SDS_TYPE_5) { test_cond("sdsMakeRoomFor() free", sdsavail(x) >= step); oldfree = sdsavail(x); } p = x+oldlen; for (j = 0; j < step; j++) { p[j] = 'A'+j; } sdsIncrLen(x,step); } test_cond("sdsMakeRoomFor() content", memcmp("0ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ",x,101) == 0); test_cond("sdsMakeRoomFor() final length",sdslen(x)==101); sdsfree(x); } } test_report() return 0; } #endif #ifdef SDS_TEST_MAIN int main(void) { return sdsTest(); } #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/sds.h000066400000000000000000000217751515654015100302110ustar00rootroot00000000000000/* SDSLib 2.0 -- A C dynamic strings library * * Copyright (c) 2006-2015, Salvatore Sanfilippo * Copyright (c) 2015, Oran Agra * Copyright (c) 2015, Redis Labs, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __SDS_H #define __SDS_H #define SDS_MAX_PREALLOC (1024*1024) #ifdef _MSC_VER #define __attribute__(x) typedef long long ssize_t; #define SSIZE_MAX (LLONG_MAX >> 1) #endif #include #include #include typedef char *sds; /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))); #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) static inline size_t sdslen(const sds s) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: return SDS_TYPE_5_LEN(flags); case SDS_TYPE_8: return SDS_HDR(8,s)->len; case SDS_TYPE_16: return SDS_HDR(16,s)->len; case SDS_TYPE_32: return SDS_HDR(32,s)->len; case SDS_TYPE_64: return SDS_HDR(64,s)->len; } return 0; } static inline size_t sdsavail(const sds s) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: { return 0; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); return sh->alloc - sh->len; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); return sh->alloc - sh->len; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); return sh->alloc - sh->len; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); return sh->alloc - sh->len; } } return 0; } static inline void sdssetlen(sds s, size_t newlen) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; *fp = (unsigned char)(SDS_TYPE_5 | (newlen << SDS_TYPE_BITS)); } break; case SDS_TYPE_8: SDS_HDR(8,s)->len = (uint8_t)newlen; break; case SDS_TYPE_16: SDS_HDR(16,s)->len = (uint16_t)newlen; break; case SDS_TYPE_32: SDS_HDR(32,s)->len = (uint32_t)newlen; break; case SDS_TYPE_64: SDS_HDR(64,s)->len = (uint64_t)newlen; break; } } static inline void sdsinclen(sds s, size_t inc) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; unsigned char newlen = SDS_TYPE_5_LEN(flags)+(unsigned char)inc; *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); } break; case SDS_TYPE_8: SDS_HDR(8,s)->len += (uint8_t)inc; break; case SDS_TYPE_16: SDS_HDR(16,s)->len += (uint16_t)inc; break; case SDS_TYPE_32: SDS_HDR(32,s)->len += (uint32_t)inc; break; case SDS_TYPE_64: SDS_HDR(64,s)->len += (uint64_t)inc; break; } } /* sdsalloc() = sdsavail() + sdslen() */ static inline size_t sdsalloc(const sds s) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: return SDS_TYPE_5_LEN(flags); case SDS_TYPE_8: return SDS_HDR(8,s)->alloc; case SDS_TYPE_16: return SDS_HDR(16,s)->alloc; case SDS_TYPE_32: return SDS_HDR(32,s)->alloc; case SDS_TYPE_64: return SDS_HDR(64,s)->alloc; } return 0; } static inline void sdssetalloc(sds s, size_t newlen) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: /* Nothing to do, this type has no total allocation info. */ break; case SDS_TYPE_8: SDS_HDR(8,s)->alloc = (uint8_t)newlen; break; case SDS_TYPE_16: SDS_HDR(16,s)->alloc = (uint16_t)newlen; break; case SDS_TYPE_32: SDS_HDR(32,s)->alloc = (uint32_t)newlen; break; case SDS_TYPE_64: SDS_HDR(64,s)->alloc = (uint64_t)newlen; break; } } sds sdsnewlen(const void *init, size_t initlen); sds sdsnew(const char *init); sds sdsempty(void); sds sdsdup(const sds s); void sdsfree(sds s); sds sdsgrowzero(sds s, size_t len); sds sdscatlen(sds s, const void *t, size_t len); sds sdscat(sds s, const char *t); sds sdscatsds(sds s, const sds t); sds sdscpylen(sds s, const char *t, size_t len); sds sdscpy(sds s, const char *t); sds sdscatvprintf(sds s, const char *fmt, va_list ap); #ifdef __GNUC__ sds sdscatprintf(sds s, const char *fmt, ...) __attribute__((format(printf, 2, 3))); #else sds sdscatprintf(sds s, const char *fmt, ...); #endif sds sdscatfmt(sds s, char const *fmt, ...); sds sdstrim(sds s, const char *cset); int sdsrange(sds s, ssize_t start, ssize_t end); void sdsupdatelen(sds s); void sdsclear(sds s); int sdscmp(const sds s1, const sds s2); sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count); void sdsfreesplitres(sds *tokens, int count); void sdstolower(sds s); void sdstoupper(sds s); sds sdsfromlonglong(long long value); sds sdscatrepr(sds s, const char *p, size_t len); sds *sdssplitargs(const char *line, int *argc); sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen); sds sdsjoin(char **argv, int argc, char *sep); sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen); /* Low level functions exposed to the user API */ sds sdsMakeRoomFor(sds s, size_t addlen); void sdsIncrLen(sds s, int incr); sds sdsRemoveFreeSpace(sds s); size_t sdsAllocSize(sds s); void *sdsAllocPtr(sds s); /* Export the allocator used by SDS to the program using SDS. * Sometimes the program SDS is linked to, may use a different set of * allocators, but may want to allocate or free things that SDS will * respectively free or allocate. */ void *sds_malloc(size_t size); void *sds_realloc(void *ptr, size_t size); void sds_free(void *ptr); #ifdef REDIS_TEST int sdsTest(int argc, char *argv[]); #endif #endif redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/sdsalloc.h000066400000000000000000000041111515654015100312050ustar00rootroot00000000000000/* SDSLib 2.0 -- A C dynamic strings library * * Copyright (c) 2006-2015, Salvatore Sanfilippo * Copyright (c) 2015, Oran Agra * Copyright (c) 2015, Redis Labs, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* SDS allocator selection. * * This file is used in order to change the SDS allocator at compile time. * Just define the following defines to what you want to use. Also add * the include of your alternate allocator if needed (not needed in order * to use the default libc allocator). */ #include "alloc.h" #define s_malloc hi_malloc #define s_realloc hi_realloc #define s_free hi_free redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/sockcompat.c000066400000000000000000000214141515654015100315440ustar00rootroot00000000000000/* * Copyright (c) 2019, Marcus Geelnard * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #define REDIS_SOCKCOMPAT_IMPLEMENTATION #include "sockcompat.h" #ifdef _WIN32 static int _wsaErrorToErrno(int err) { switch (err) { case WSAEWOULDBLOCK: return EWOULDBLOCK; case WSAEINPROGRESS: return EINPROGRESS; case WSAEALREADY: return EALREADY; case WSAENOTSOCK: return ENOTSOCK; case WSAEDESTADDRREQ: return EDESTADDRREQ; case WSAEMSGSIZE: return EMSGSIZE; case WSAEPROTOTYPE: return EPROTOTYPE; case WSAENOPROTOOPT: return ENOPROTOOPT; case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; case WSAEOPNOTSUPP: return EOPNOTSUPP; case WSAEAFNOSUPPORT: return EAFNOSUPPORT; case WSAEADDRINUSE: return EADDRINUSE; case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL; case WSAENETDOWN: return ENETDOWN; case WSAENETUNREACH: return ENETUNREACH; case WSAENETRESET: return ENETRESET; case WSAECONNABORTED: return ECONNABORTED; case WSAECONNRESET: return ECONNRESET; case WSAENOBUFS: return ENOBUFS; case WSAEISCONN: return EISCONN; case WSAENOTCONN: return ENOTCONN; case WSAETIMEDOUT: return ETIMEDOUT; case WSAECONNREFUSED: return ECONNREFUSED; case WSAELOOP: return ELOOP; case WSAENAMETOOLONG: return ENAMETOOLONG; case WSAEHOSTUNREACH: return EHOSTUNREACH; case WSAENOTEMPTY: return ENOTEMPTY; default: /* We just return a generic I/O error if we could not find a relevant error. */ return EIO; } } static void _updateErrno(int success) { errno = success ? 0 : _wsaErrorToErrno(WSAGetLastError()); } static int _initWinsock() { static int s_initialized = 0; if (!s_initialized) { static WSADATA wsadata; int err = WSAStartup(MAKEWORD(2,2), &wsadata); if (err != 0) { errno = _wsaErrorToErrno(err); return 0; } s_initialized = 1; } return 1; } int win32_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) { /* Note: This function is likely to be called before other functions, so run init here. */ if (!_initWinsock()) { return EAI_FAIL; } switch (getaddrinfo(node, service, hints, res)) { case 0: return 0; case WSATRY_AGAIN: return EAI_AGAIN; case WSAEINVAL: return EAI_BADFLAGS; case WSAEAFNOSUPPORT: return EAI_FAMILY; case WSA_NOT_ENOUGH_MEMORY: return EAI_MEMORY; case WSAHOST_NOT_FOUND: return EAI_NONAME; case WSATYPE_NOT_FOUND: return EAI_SERVICE; case WSAESOCKTNOSUPPORT: return EAI_SOCKTYPE; default: return EAI_FAIL; /* Including WSANO_RECOVERY */ } } const char *win32_gai_strerror(int errcode) { switch (errcode) { case 0: errcode = 0; break; case EAI_AGAIN: errcode = WSATRY_AGAIN; break; case EAI_BADFLAGS: errcode = WSAEINVAL; break; case EAI_FAMILY: errcode = WSAEAFNOSUPPORT; break; case EAI_MEMORY: errcode = WSA_NOT_ENOUGH_MEMORY; break; case EAI_NONAME: errcode = WSAHOST_NOT_FOUND; break; case EAI_SERVICE: errcode = WSATYPE_NOT_FOUND; break; case EAI_SOCKTYPE: errcode = WSAESOCKTNOSUPPORT; break; default: errcode = WSANO_RECOVERY; break; /* Including EAI_FAIL */ } return gai_strerror(errcode); } void win32_freeaddrinfo(struct addrinfo *res) { freeaddrinfo(res); } SOCKET win32_socket(int domain, int type, int protocol) { SOCKET s; /* Note: This function is likely to be called before other functions, so run init here. */ if (!_initWinsock()) { return INVALID_SOCKET; } _updateErrno((s = socket(domain, type, protocol)) != INVALID_SOCKET); return s; } int win32_ioctl(SOCKET fd, unsigned long request, unsigned long *argp) { int ret = ioctlsocket(fd, (long)request, argp); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } int win32_bind(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen) { int ret = bind(sockfd, addr, addrlen); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } int win32_connect(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen) { int ret = connect(sockfd, addr, addrlen); _updateErrno(ret != SOCKET_ERROR); /* For Winsock connect(), the WSAEWOULDBLOCK error means the same thing as * EINPROGRESS for POSIX connect(), so we do that translation to keep POSIX * logic consistent. */ if (errno == EWOULDBLOCK) { errno = EINPROGRESS; } return ret != SOCKET_ERROR ? ret : -1; } int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, socklen_t *optlen) { int ret = 0; if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) { if (*optlen >= sizeof (struct timeval)) { struct timeval *tv = optval; DWORD timeout = 0; socklen_t dwlen = 0; ret = getsockopt(sockfd, level, optname, (char *)&timeout, &dwlen); tv->tv_sec = timeout / 1000; tv->tv_usec = (timeout * 1000) % 1000000; } else { ret = WSAEFAULT; } *optlen = sizeof (struct timeval); } else { ret = getsockopt(sockfd, level, optname, (char*)optval, optlen); } _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen) { int ret = 0; if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) { const struct timeval *tv = optval; DWORD timeout = tv->tv_sec * 1000 + tv->tv_usec / 1000; ret = setsockopt(sockfd, level, optname, (const char*)&timeout, sizeof(DWORD)); } else { ret = setsockopt(sockfd, level, optname, (const char*)optval, optlen); } _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } int win32_close(SOCKET fd) { int ret = closesocket(fd); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } ssize_t win32_recv(SOCKET sockfd, void *buf, size_t len, int flags) { int ret = recv(sockfd, (char*)buf, (int)len, flags); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } ssize_t win32_send(SOCKET sockfd, const void *buf, size_t len, int flags) { int ret = send(sockfd, (const char*)buf, (int)len, flags); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } int win32_poll(struct pollfd *fds, nfds_t nfds, int timeout) { int ret = WSAPoll(fds, nfds, timeout); _updateErrno(ret != SOCKET_ERROR); return ret != SOCKET_ERROR ? ret : -1; } #endif /* _WIN32 */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/sockcompat.h000066400000000000000000000103511515654015100315470ustar00rootroot00000000000000/* * Copyright (c) 2019, Marcus Geelnard * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __SOCKCOMPAT_H #define __SOCKCOMPAT_H #ifndef _WIN32 /* For POSIX systems we use the standard BSD socket API. */ #include #include #include #include #include #include #include #include #include #else /* For Windows we use winsock. */ #undef _WIN32_WINNT #define _WIN32_WINNT 0x0600 /* To get WSAPoll etc. */ #include #include #include #include #ifdef _MSC_VER typedef long long ssize_t; #endif /* Emulate the parts of the BSD socket API that we need (override the winsock signatures). */ int win32_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); const char *win32_gai_strerror(int errcode); void win32_freeaddrinfo(struct addrinfo *res); SOCKET win32_socket(int domain, int type, int protocol); int win32_ioctl(SOCKET fd, unsigned long request, unsigned long *argp); int win32_bind(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen); int win32_connect(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen); int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, socklen_t *optlen); int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen); int win32_close(SOCKET fd); ssize_t win32_recv(SOCKET sockfd, void *buf, size_t len, int flags); ssize_t win32_send(SOCKET sockfd, const void *buf, size_t len, int flags); typedef ULONG nfds_t; int win32_poll(struct pollfd *fds, nfds_t nfds, int timeout); #ifndef REDIS_SOCKCOMPAT_IMPLEMENTATION #define getaddrinfo(node, service, hints, res) win32_getaddrinfo(node, service, hints, res) #undef gai_strerror #define gai_strerror(errcode) win32_gai_strerror(errcode) #define freeaddrinfo(res) win32_freeaddrinfo(res) #define socket(domain, type, protocol) win32_socket(domain, type, protocol) #define ioctl(fd, request, argp) win32_ioctl(fd, request, argp) #define bind(sockfd, addr, addrlen) win32_bind(sockfd, addr, addrlen) #define connect(sockfd, addr, addrlen) win32_connect(sockfd, addr, addrlen) #define getsockopt(sockfd, level, optname, optval, optlen) win32_getsockopt(sockfd, level, optname, optval, optlen) #define setsockopt(sockfd, level, optname, optval, optlen) win32_setsockopt(sockfd, level, optname, optval, optlen) #define close(fd) win32_close(fd) #define recv(sockfd, buf, len, flags) win32_recv(sockfd, buf, len, flags) #define send(sockfd, buf, len, flags) win32_send(sockfd, buf, len, flags) #define poll(fds, nfds, timeout) win32_poll(fds, nfds, timeout) #endif /* REDIS_SOCKCOMPAT_IMPLEMENTATION */ #endif /* _WIN32 */ #endif /* __SOCKCOMPAT_H */ redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/ssl.c000066400000000000000000000350251515654015100302050ustar00rootroot00000000000000/* * Copyright (c) 2009-2011, Salvatore Sanfilippo * Copyright (c) 2010-2011, Pieter Noordhuis * Copyright (c) 2019, Redis Labs * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Redis nor the names of its contributors may be used * to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "hiredis.h" #include "async.h" #include #include #include #ifdef _WIN32 #include #else #include #endif #include #include #include "win32.h" #include "async_private.h" #include "hiredis_ssl.h" void __redisSetError(redisContext *c, int type, const char *str); struct redisSSLContext { /* Associated OpenSSL SSL_CTX as created by redisCreateSSLContext() */ SSL_CTX *ssl_ctx; /* Requested SNI, or NULL */ char *server_name; }; /* Forward declaration */ redisContextFuncs redisContextSSLFuncs; /** * OpenSSL global initialization and locking handling callbacks. * Note that this is only required for OpenSSL < 1.1.0. */ #if OPENSSL_VERSION_NUMBER < 0x10100000L #define HIREDIS_USE_CRYPTO_LOCKS #endif #ifdef HIREDIS_USE_CRYPTO_LOCKS #ifdef _WIN32 typedef CRITICAL_SECTION sslLockType; static void sslLockInit(sslLockType* l) { InitializeCriticalSection(l); } static void sslLockAcquire(sslLockType* l) { EnterCriticalSection(l); } static void sslLockRelease(sslLockType* l) { LeaveCriticalSection(l); } #else typedef pthread_mutex_t sslLockType; static void sslLockInit(sslLockType *l) { pthread_mutex_init(l, NULL); } static void sslLockAcquire(sslLockType *l) { pthread_mutex_lock(l); } static void sslLockRelease(sslLockType *l) { pthread_mutex_unlock(l); } #endif static sslLockType* ossl_locks; static void opensslDoLock(int mode, int lkid, const char *f, int line) { sslLockType *l = ossl_locks + lkid; if (mode & CRYPTO_LOCK) { sslLockAcquire(l); } else { sslLockRelease(l); } (void)f; (void)line; } static int initOpensslLocks(void) { unsigned ii, nlocks; if (CRYPTO_get_locking_callback() != NULL) { /* Someone already set the callback before us. Don't destroy it! */ return REDIS_OK; } nlocks = CRYPTO_num_locks(); ossl_locks = hi_malloc(sizeof(*ossl_locks) * nlocks); if (ossl_locks == NULL) return REDIS_ERR; for (ii = 0; ii < nlocks; ii++) { sslLockInit(ossl_locks + ii); } CRYPTO_set_locking_callback(opensslDoLock); return REDIS_OK; } #endif /* HIREDIS_USE_CRYPTO_LOCKS */ int redisInitOpenSSL(void) { SSL_library_init(); #ifdef HIREDIS_USE_CRYPTO_LOCKS initOpensslLocks(); #endif return REDIS_OK; } static int maybeCheckWant(redisSSL *rssl, int rv) { /** * If the error is WANT_READ or WANT_WRITE, the appropriate flags are set * and true is returned. False is returned otherwise */ if (rv == SSL_ERROR_WANT_READ) { rssl->wantRead = 1; return 1; } else if (rv == SSL_ERROR_WANT_WRITE) { rssl->pendingWrite = 1; return 1; } else { return 0; } } /** * redisSSLContext helper context destruction. */ const char *redisSSLContextGetError(redisSSLContextError error) { switch (error) { case REDIS_SSL_CTX_NONE: return "No Error"; case REDIS_SSL_CTX_CREATE_FAILED: return "Failed to create OpenSSL SSL_CTX"; case REDIS_SSL_CTX_CERT_KEY_REQUIRED: return "Client cert and key must both be specified or skipped"; case REDIS_SSL_CTX_CA_CERT_LOAD_FAILED: return "Failed to load CA Certificate or CA Path"; case REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED: return "Failed to load client certificate"; case REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED: return "Failed to load private key"; default: return "Unknown error code"; } } void redisFreeSSLContext(redisSSLContext *ctx) { if (!ctx) return; if (ctx->server_name) { hi_free(ctx->server_name); ctx->server_name = NULL; } if (ctx->ssl_ctx) { SSL_CTX_free(ctx->ssl_ctx); ctx->ssl_ctx = NULL; } hi_free(ctx); } /** * redisSSLContext helper context initialization. */ redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *capath, const char *cert_filename, const char *private_key_filename, const char *server_name, redisSSLContextError *error) { redisSSLContext *ctx = hi_calloc(1, sizeof(redisSSLContext)); if (ctx == NULL) goto error; ctx->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); if (!ctx->ssl_ctx) { if (error) *error = REDIS_SSL_CTX_CREATE_FAILED; goto error; } SSL_CTX_set_options(ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); SSL_CTX_set_verify(ctx->ssl_ctx, SSL_VERIFY_PEER, NULL); if ((cert_filename != NULL && private_key_filename == NULL) || (private_key_filename != NULL && cert_filename == NULL)) { if (error) *error = REDIS_SSL_CTX_CERT_KEY_REQUIRED; goto error; } if (capath || cacert_filename) { if (!SSL_CTX_load_verify_locations(ctx->ssl_ctx, cacert_filename, capath)) { if (error) *error = REDIS_SSL_CTX_CA_CERT_LOAD_FAILED; goto error; } } if (cert_filename) { if (!SSL_CTX_use_certificate_chain_file(ctx->ssl_ctx, cert_filename)) { if (error) *error = REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED; goto error; } if (!SSL_CTX_use_PrivateKey_file(ctx->ssl_ctx, private_key_filename, SSL_FILETYPE_PEM)) { if (error) *error = REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED; goto error; } } if (server_name) ctx->server_name = hi_strdup(server_name); return ctx; error: redisFreeSSLContext(ctx); return NULL; } int redisInitiateSSLContinue(redisContext *c) { if (!c->privctx) { __redisSetError(c, REDIS_ERR_OTHER, "redisContext is not associated"); return REDIS_ERR; } redisSSL *rssl = (redisSSL *)c->privctx; ERR_clear_error(); int rv = SSL_connect(rssl->ssl); if (rv == 1) { c->privctx = rssl; return REDIS_OK; } rv = SSL_get_error(rssl->ssl, rv); if (((c->flags & REDIS_BLOCK) == 0) && (rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) { maybeCheckWant(rssl, rv); c->privctx = rssl; return REDIS_OK; } if (c->err == 0) { char err[512]; if (rv == SSL_ERROR_SYSCALL) snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",strerror(errno)); else { unsigned long e = ERR_peek_last_error(); snprintf(err,sizeof(err)-1,"SSL_connect failed: %s", ERR_reason_error_string(e)); } __redisSetError(c, REDIS_ERR_IO, err); } return REDIS_ERR; } /** * SSL Connection initialization. */ static int redisSSLConnect(redisContext *c, SSL *ssl) { if (c->privctx) { __redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated"); return REDIS_ERR; } redisSSL *rssl = hi_calloc(1, sizeof(redisSSL)); if (rssl == NULL) { __redisSetError(c, REDIS_ERR_OOM, "Out of memory"); return REDIS_ERR; } c->funcs = &redisContextSSLFuncs; rssl->ssl = ssl; SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); SSL_set_fd(rssl->ssl, c->fd); SSL_set_connect_state(rssl->ssl); ERR_clear_error(); int rv = SSL_connect(rssl->ssl); if (rv == 1) { c->privctx = rssl; return REDIS_OK; } rv = SSL_get_error(rssl->ssl, rv); if (((c->flags & REDIS_BLOCK) == 0) && (rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) { maybeCheckWant(rssl, rv); c->privctx = rssl; return REDIS_OK; } if (c->err == 0) { char err[512]; if (rv == SSL_ERROR_SYSCALL) snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",strerror(errno)); else { unsigned long e = ERR_peek_last_error(); snprintf(err,sizeof(err)-1,"SSL_connect failed: %s", ERR_reason_error_string(e)); } __redisSetError(c, REDIS_ERR_IO, err); } hi_free(rssl); return REDIS_ERR; } redisSSL *redisGetSSLSocket(redisContext *c) { return c->privctx; } /** * A wrapper around redisSSLConnect() for users who manage their own context and * create their own SSL object. */ int redisInitiateSSL(redisContext *c, SSL *ssl) { return redisSSLConnect(c, ssl); } /** * A wrapper around redisSSLConnect() for users who use redisSSLContext and don't * manage their own SSL objects. */ int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx) { if (!c || !redis_ssl_ctx) return REDIS_ERR; /* We want to verify that redisSSLConnect() won't fail on this, as it will * not own the SSL object in that case and we'll end up leaking. */ if (c->privctx) return REDIS_ERR; SSL *ssl = SSL_new(redis_ssl_ctx->ssl_ctx); if (!ssl) { __redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance"); goto error; } if (redis_ssl_ctx->server_name) { if (!SSL_set_tlsext_host_name(ssl, redis_ssl_ctx->server_name)) { __redisSetError(c, REDIS_ERR_OTHER, "Failed to set server_name/SNI"); goto error; } } return redisSSLConnect(c, ssl); error: if (ssl) SSL_free(ssl); return REDIS_ERR; } /** * Implementation of redisContextFuncs for SSL connections. */ static void redisSSLFree(void *privctx){ redisSSL *rsc = privctx; if (!rsc) return; if (rsc->ssl) { SSL_free(rsc->ssl); rsc->ssl = NULL; } hi_free(rsc); } static ssize_t redisSSLRead(redisContext *c, char *buf, size_t bufcap) { redisSSL *rssl = c->privctx; int nread = SSL_read(rssl->ssl, buf, bufcap); if (nread > 0) { return nread; } else if (nread == 0) { __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); return -1; } else { int err = SSL_get_error(rssl->ssl, nread); if (c->flags & REDIS_BLOCK) { /** * In blocking mode, we should never end up in a situation where * we get an error without it being an actual error, except * in the case of EINTR, which can be spuriously received from * debuggers or whatever. */ if (errno == EINTR) { return 0; } else { const char *msg = NULL; if (errno == EAGAIN) { msg = "Resource temporarily unavailable"; } __redisSetError(c, REDIS_ERR_IO, msg); return -1; } } /** * We can very well get an EWOULDBLOCK/EAGAIN, however */ if (maybeCheckWant(rssl, err)) { return 0; } else { __redisSetError(c, REDIS_ERR_IO, NULL); return -1; } } } static ssize_t redisSSLWrite(redisContext *c) { redisSSL *rssl = c->privctx; size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf); int rv = SSL_write(rssl->ssl, c->obuf, len); if (rv > 0) { rssl->lastLen = 0; } else if (rv < 0) { rssl->lastLen = len; int err = SSL_get_error(rssl->ssl, rv); if ((c->flags & REDIS_BLOCK) == 0 && maybeCheckWant(rssl, err)) { return 0; } else { __redisSetError(c, REDIS_ERR_IO, NULL); return -1; } } return rv; } static void redisSSLAsyncRead(redisAsyncContext *ac) { int rv; redisSSL *rssl = ac->c.privctx; redisContext *c = &ac->c; rssl->wantRead = 0; if (rssl->pendingWrite) { int done; /* This is probably just a write event */ rssl->pendingWrite = 0; rv = redisBufferWrite(c, &done); if (rv == REDIS_ERR) { __redisAsyncDisconnect(ac); return; } else if (!done) { _EL_ADD_WRITE(ac); } } rv = redisBufferRead(c); if (rv == REDIS_ERR) { __redisAsyncDisconnect(ac); } else { _EL_ADD_READ(ac); redisProcessCallbacks(ac); } } static void redisSSLAsyncWrite(redisAsyncContext *ac) { int rv, done = 0; redisSSL *rssl = ac->c.privctx; redisContext *c = &ac->c; rssl->pendingWrite = 0; rv = redisBufferWrite(c, &done); if (rv == REDIS_ERR) { __redisAsyncDisconnect(ac); return; } if (!done) { if (rssl->wantRead) { /* Need to read-before-write */ rssl->pendingWrite = 1; _EL_DEL_WRITE(ac); } else { /* No extra reads needed, just need to write more */ _EL_ADD_WRITE(ac); } } else { /* Already done! */ _EL_DEL_WRITE(ac); } /* Always reschedule a read */ _EL_ADD_READ(ac); } redisContextFuncs redisContextSSLFuncs = { .free_privctx = redisSSLFree, .async_read = redisSSLAsyncRead, .async_write = redisSSLAsyncWrite, .read = redisSSLRead, .write = redisSSLWrite }; redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/test.c000066400000000000000000001355531515654015100303720ustar00rootroot00000000000000#include "fmacros.h" #include "sockcompat.h" #include #include #include #ifndef _WIN32 #include #include #endif #include #include #include #include #include "hiredis.h" #include "async.h" #ifdef HIREDIS_TEST_SSL #include "hiredis_ssl.h" #endif #include "net.h" #include "win32.h" enum connection_type { CONN_TCP, CONN_UNIX, CONN_FD, CONN_SSL }; struct config { enum connection_type type; struct { const char *host; int port; struct timeval timeout; } tcp; struct { const char *path; } unix_sock; struct { const char *host; int port; const char *ca_cert; const char *cert; const char *key; } ssl; }; struct privdata { int dtor_counter; }; #ifdef HIREDIS_TEST_SSL redisSSLContext *_ssl_ctx = NULL; #endif /* The following lines make up our testing "framework" :) */ static int tests = 0, fails = 0, skips = 0; #define test(_s) { printf("#%02d ", ++tests); printf(_s); } #define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;} #define test_skipped() { printf("\033[01;33mSKIPPED\033[0;0m\n"); skips++; } static long long usec(void) { #ifndef _MSC_VER struct timeval tv; gettimeofday(&tv,NULL); return (((long long)tv.tv_sec)*1000000)+tv.tv_usec; #else FILETIME ft; GetSystemTimeAsFileTime(&ft); return (((long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime) / 10; #endif } /* The assert() calls below have side effects, so we need assert() * even if we are compiling without asserts (-DNDEBUG). */ #ifdef NDEBUG #undef assert #define assert(e) (void)(e) #endif /* Helper to extract Redis version information. Aborts on any failure. */ #define REDIS_VERSION_FIELD "redis_version:" void get_redis_version(redisContext *c, int *majorptr, int *minorptr) { redisReply *reply; char *eptr, *s, *e; int major, minor; reply = redisCommand(c, "INFO"); if (reply == NULL || c->err || reply->type != REDIS_REPLY_STRING) goto abort; if ((s = strstr(reply->str, REDIS_VERSION_FIELD)) == NULL) goto abort; s += strlen(REDIS_VERSION_FIELD); /* We need a field terminator and at least 'x.y.z' (5) bytes of data */ if ((e = strstr(s, "\r\n")) == NULL || (e - s) < 5) goto abort; /* Extract version info */ major = strtol(s, &eptr, 10); if (*eptr != '.') goto abort; minor = strtol(eptr+1, NULL, 10); /* Push info the caller wants */ if (majorptr) *majorptr = major; if (minorptr) *minorptr = minor; freeReplyObject(reply); return; abort: freeReplyObject(reply); fprintf(stderr, "Error: Cannot determine Redis version, aborting\n"); exit(1); } static redisContext *select_database(redisContext *c) { redisReply *reply; /* Switch to DB 9 for testing, now that we know we can chat. */ reply = redisCommand(c,"SELECT 9"); assert(reply != NULL); freeReplyObject(reply); /* Make sure the DB is emtpy */ reply = redisCommand(c,"DBSIZE"); assert(reply != NULL); if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 0) { /* Awesome, DB 9 is empty and we can continue. */ freeReplyObject(reply); } else { printf("Database #9 is not empty, test can not continue\n"); exit(1); } return c; } /* Switch protocol */ static void send_hello(redisContext *c, int version) { redisReply *reply; int expected; reply = redisCommand(c, "HELLO %d", version); expected = version == 3 ? REDIS_REPLY_MAP : REDIS_REPLY_ARRAY; assert(reply != NULL && reply->type == expected); freeReplyObject(reply); } /* Togggle client tracking */ static void send_client_tracking(redisContext *c, const char *str) { redisReply *reply; reply = redisCommand(c, "CLIENT TRACKING %s", str); assert(reply != NULL && reply->type == REDIS_REPLY_STATUS); freeReplyObject(reply); } static int disconnect(redisContext *c, int keep_fd) { redisReply *reply; /* Make sure we're on DB 9. */ reply = redisCommand(c,"SELECT 9"); assert(reply != NULL); freeReplyObject(reply); reply = redisCommand(c,"FLUSHDB"); assert(reply != NULL); freeReplyObject(reply); /* Free the context as well, but keep the fd if requested. */ if (keep_fd) return redisFreeKeepFd(c); redisFree(c); return -1; } static void do_ssl_handshake(redisContext *c) { #ifdef HIREDIS_TEST_SSL redisInitiateSSLWithContext(c, _ssl_ctx); if (c->err) { printf("SSL error: %s\n", c->errstr); redisFree(c); exit(1); } #else (void) c; #endif } static redisContext *do_connect(struct config config) { redisContext *c = NULL; if (config.type == CONN_TCP) { c = redisConnect(config.tcp.host, config.tcp.port); } else if (config.type == CONN_SSL) { c = redisConnect(config.ssl.host, config.ssl.port); } else if (config.type == CONN_UNIX) { c = redisConnectUnix(config.unix_sock.path); } else if (config.type == CONN_FD) { /* Create a dummy connection just to get an fd to inherit */ redisContext *dummy_ctx = redisConnectUnix(config.unix_sock.path); if (dummy_ctx) { int fd = disconnect(dummy_ctx, 1); printf("Connecting to inherited fd %d\n", fd); c = redisConnectFd(fd); } } else { assert(NULL); } if (c == NULL) { printf("Connection error: can't allocate redis context\n"); exit(1); } else if (c->err) { printf("Connection error: %s\n", c->errstr); redisFree(c); exit(1); } if (config.type == CONN_SSL) { do_ssl_handshake(c); } return select_database(c); } static void do_reconnect(redisContext *c, struct config config) { redisReconnect(c); if (config.type == CONN_SSL) { do_ssl_handshake(c); } } static void test_format_commands(void) { char *cmd; int len; test("Format command without interpolation: "); len = redisFormatCommand(&cmd,"SET foo bar"); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); hi_free(cmd); test("Format command with %%s string interpolation: "); len = redisFormatCommand(&cmd,"SET %s %s","foo","bar"); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); hi_free(cmd); test("Format command with %%s and an empty string: "); len = redisFormatCommand(&cmd,"SET %s %s","foo",""); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(0+2)); hi_free(cmd); test("Format command with an empty string in between proper interpolations: "); len = redisFormatCommand(&cmd,"SET %s %s","","foo"); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 && len == 4+4+(3+2)+4+(0+2)+4+(3+2)); hi_free(cmd); test("Format command with %%b string interpolation: "); len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"b\0r",(size_t)3); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); hi_free(cmd); test("Format command with %%b and an empty string: "); len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"",(size_t)0); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(0+2)); hi_free(cmd); test("Format command with literal %%: "); len = redisFormatCommand(&cmd,"SET %% %%"); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 && len == 4+4+(3+2)+4+(1+2)+4+(1+2)); hi_free(cmd); /* Vararg width depends on the type. These tests make sure that the * width is correctly determined using the format and subsequent varargs * can correctly be interpolated. */ #define INTEGER_WIDTH_TEST(fmt, type) do { \ type value = 123; \ test("Format command with printf-delegation (" #type "): "); \ len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \ test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \ len == 4+5+(12+2)+4+(9+2)); \ hi_free(cmd); \ } while(0) #define FLOAT_WIDTH_TEST(type) do { \ type value = 123.0; \ test("Format command with printf-delegation (" #type "): "); \ len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \ test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \ len == 4+5+(12+2)+4+(9+2)); \ hi_free(cmd); \ } while(0) INTEGER_WIDTH_TEST("d", int); INTEGER_WIDTH_TEST("hhd", char); INTEGER_WIDTH_TEST("hd", short); INTEGER_WIDTH_TEST("ld", long); INTEGER_WIDTH_TEST("lld", long long); INTEGER_WIDTH_TEST("u", unsigned int); INTEGER_WIDTH_TEST("hhu", unsigned char); INTEGER_WIDTH_TEST("hu", unsigned short); INTEGER_WIDTH_TEST("lu", unsigned long); INTEGER_WIDTH_TEST("llu", unsigned long long); FLOAT_WIDTH_TEST(float); FLOAT_WIDTH_TEST(double); test("Format command with invalid printf format: "); len = redisFormatCommand(&cmd,"key:%08p %b",(void*)1234,"foo",(size_t)3); test_cond(len == -1); const char *argv[3]; argv[0] = "SET"; argv[1] = "foo\0xxx"; argv[2] = "bar"; size_t lens[3] = { 3, 7, 3 }; int argc = 3; test("Format command by passing argc/argv without lengths: "); len = redisFormatCommandArgv(&cmd,argc,argv,NULL); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); hi_free(cmd); test("Format command by passing argc/argv with lengths: "); len = redisFormatCommandArgv(&cmd,argc,argv,lens); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(7+2)+4+(3+2)); hi_free(cmd); sds sds_cmd; sds_cmd = NULL; test("Format command into sds by passing argc/argv without lengths: "); len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,NULL); test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); sdsfree(sds_cmd); sds_cmd = NULL; test("Format command into sds by passing argc/argv with lengths: "); len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,lens); test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && len == 4+4+(3+2)+4+(7+2)+4+(3+2)); sdsfree(sds_cmd); } static void test_append_formatted_commands(struct config config) { redisContext *c; redisReply *reply; char *cmd; int len; c = do_connect(config); test("Append format command: "); len = redisFormatCommand(&cmd, "SET foo bar"); test_cond(redisAppendFormattedCommand(c, cmd, len) == REDIS_OK); assert(redisGetReply(c, (void*)&reply) == REDIS_OK); hi_free(cmd); freeReplyObject(reply); disconnect(c, 0); } static void test_reply_reader(void) { redisReader *reader; void *reply, *root; int ret; int i; test("Error handling in reply parser: "); reader = redisReaderCreate(); redisReaderFeed(reader,(char*)"@foo\r\n",6); ret = redisReaderGetReply(reader,NULL); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); redisReaderFree(reader); /* when the reply already contains multiple items, they must be free'd * on an error. valgrind will bark when this doesn't happen. */ test("Memory cleanup in reply parser: "); reader = redisReaderCreate(); redisReaderFeed(reader,(char*)"*2\r\n",4); redisReaderFeed(reader,(char*)"$5\r\nhello\r\n",11); redisReaderFeed(reader,(char*)"@foo\r\n",6); ret = redisReaderGetReply(reader,NULL); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); redisReaderFree(reader); reader = redisReaderCreate(); test("Can handle arbitrarily nested multi-bulks: "); for (i = 0; i < 128; i++) { redisReaderFeed(reader,(char*)"*1\r\n", 4); } redisReaderFeed(reader,(char*)"$6\r\nLOLWUT\r\n",12); ret = redisReaderGetReply(reader,&reply); root = reply; /* Keep track of the root reply */ test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_ARRAY && ((redisReply*)reply)->elements == 1); test("Can parse arbitrarily nested multi-bulks correctly: "); while(i--) { assert(reply != NULL && ((redisReply*)reply)->type == REDIS_REPLY_ARRAY); reply = ((redisReply*)reply)->element[0]; } test_cond(((redisReply*)reply)->type == REDIS_REPLY_STRING && !memcmp(((redisReply*)reply)->str, "LOLWUT", 6)); freeReplyObject(root); redisReaderFree(reader); test("Correctly parses LLONG_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, ":9223372036854775807\r\n",22); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_INTEGER && ((redisReply*)reply)->integer == LLONG_MAX); freeReplyObject(reply); redisReaderFree(reader); test("Set error when > LLONG_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, ":9223372036854775808\r\n",22); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Bad integer value") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Correctly parses LLONG_MIN: "); reader = redisReaderCreate(); redisReaderFeed(reader, ":-9223372036854775808\r\n",23); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_INTEGER && ((redisReply*)reply)->integer == LLONG_MIN); freeReplyObject(reply); redisReaderFree(reader); test("Set error when < LLONG_MIN: "); reader = redisReaderCreate(); redisReaderFeed(reader, ":-9223372036854775809\r\n",23); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Bad integer value") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Set error when array < -1: "); reader = redisReaderCreate(); redisReaderFeed(reader, "*-2\r\n+asdf\r\n",12); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Multi-bulk length out of range") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Set error when bulk < -1: "); reader = redisReaderCreate(); redisReaderFeed(reader, "$-2\r\nasdf\r\n",11); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Bulk string length out of range") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Can configure maximum multi-bulk elements: "); reader = redisReaderCreate(); reader->maxelements = 1024; redisReaderFeed(reader, "*1025\r\n", 7); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr, "Multi-bulk length out of range") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Multi-bulk never overflows regardless of maxelements: "); size_t bad_mbulk_len = (SIZE_MAX / sizeof(void *)) + 3; char bad_mbulk_reply[100]; snprintf(bad_mbulk_reply, sizeof(bad_mbulk_reply), "*%llu\r\n+asdf\r\n", (unsigned long long) bad_mbulk_len); reader = redisReaderCreate(); reader->maxelements = 0; /* Don't rely on default limit */ redisReaderFeed(reader, bad_mbulk_reply, strlen(bad_mbulk_reply)); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr, "Out of memory") == 0); freeReplyObject(reply); redisReaderFree(reader); #if LLONG_MAX > SIZE_MAX test("Set error when array > SIZE_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, "*9223372036854775807\r\n+asdf\r\n",29); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Multi-bulk length out of range") == 0); freeReplyObject(reply); redisReaderFree(reader); test("Set error when bulk > SIZE_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, "$9223372036854775807\r\nasdf\r\n",28); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && strcasecmp(reader->errstr,"Bulk string length out of range") == 0); freeReplyObject(reply); redisReaderFree(reader); #endif test("Works with NULL functions for reply: "); reader = redisReaderCreate(); reader->fn = NULL; redisReaderFeed(reader,(char*)"+OK\r\n",5); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); redisReaderFree(reader); test("Works when a single newline (\\r\\n) covers two calls to feed: "); reader = redisReaderCreate(); reader->fn = NULL; redisReaderFeed(reader,(char*)"+OK\r",4); ret = redisReaderGetReply(reader,&reply); assert(ret == REDIS_OK && reply == NULL); redisReaderFeed(reader,(char*)"\n",1); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); redisReaderFree(reader); test("Don't reset state after protocol error: "); reader = redisReaderCreate(); reader->fn = NULL; redisReaderFeed(reader,(char*)"x",1); ret = redisReaderGetReply(reader,&reply); assert(ret == REDIS_ERR); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_ERR && reply == NULL); redisReaderFree(reader); /* Regression test for issue #45 on GitHub. */ test("Don't do empty allocation for empty multi bulk: "); reader = redisReaderCreate(); redisReaderFeed(reader,(char*)"*0\r\n",4); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_ARRAY && ((redisReply*)reply)->elements == 0); freeReplyObject(reply); redisReaderFree(reader); /* RESP3 verbatim strings (GitHub issue #802) */ test("Can parse RESP3 verbatim strings: "); reader = redisReaderCreate(); redisReaderFeed(reader,(char*)"=10\r\ntxt:LOLWUT\r\n",17); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_VERB && !memcmp(((redisReply*)reply)->str,"LOLWUT", 6)); freeReplyObject(reply); redisReaderFree(reader); /* RESP3 push messages (Github issue #815) */ test("Can parse RESP3 push messages: "); reader = redisReaderCreate(); redisReaderFeed(reader,(char*)">2\r\n$6\r\nLOLWUT\r\n:42\r\n",21); ret = redisReaderGetReply(reader,&reply); test_cond(ret == REDIS_OK && ((redisReply*)reply)->type == REDIS_REPLY_PUSH && ((redisReply*)reply)->elements == 2 && ((redisReply*)reply)->element[0]->type == REDIS_REPLY_STRING && !memcmp(((redisReply*)reply)->element[0]->str,"LOLWUT",6) && ((redisReply*)reply)->element[1]->type == REDIS_REPLY_INTEGER && ((redisReply*)reply)->element[1]->integer == 42); freeReplyObject(reply); redisReaderFree(reader); } static void test_free_null(void) { void *redisCtx = NULL; void *reply = NULL; test("Don't fail when redisFree is passed a NULL value: "); redisFree(redisCtx); test_cond(redisCtx == NULL); test("Don't fail when freeReplyObject is passed a NULL value: "); freeReplyObject(reply); test_cond(reply == NULL); } static void *hi_malloc_fail(size_t size) { (void)size; return NULL; } static void *hi_calloc_fail(size_t nmemb, size_t size) { (void)nmemb; (void)size; return NULL; } static void *hi_realloc_fail(void *ptr, size_t size) { (void)ptr; (void)size; return NULL; } static void test_allocator_injection(void) { hiredisAllocFuncs ha = { .mallocFn = hi_malloc_fail, .callocFn = hi_calloc_fail, .reallocFn = hi_realloc_fail, .strdupFn = strdup, .freeFn = free, }; // Override hiredis allocators hiredisSetAllocators(&ha); test("redisContext uses injected allocators: "); redisContext *c = redisConnect("localhost", 6379); test_cond(c == NULL); test("redisReader uses injected allocators: "); redisReader *reader = redisReaderCreate(); test_cond(reader == NULL); // Return allocators to default hiredisResetAllocators(); } #define HIREDIS_BAD_DOMAIN "idontexist-noreally.com" static void test_blocking_connection_errors(void) { redisContext *c; struct addrinfo hints = {.ai_family = AF_INET}; struct addrinfo *ai_tmp = NULL; int rv = getaddrinfo(HIREDIS_BAD_DOMAIN, "6379", &hints, &ai_tmp); if (rv != 0) { // Address does *not* exist test("Returns error when host cannot be resolved: "); // First see if this domain name *actually* resolves to NXDOMAIN c = redisConnect(HIREDIS_BAD_DOMAIN, 6379); test_cond( c->err == REDIS_ERR_OTHER && (strcmp(c->errstr, "Name or service not known") == 0 || strcmp(c->errstr, "Can't resolve: " HIREDIS_BAD_DOMAIN) == 0 || strcmp(c->errstr, "Name does not resolve") == 0 || strcmp(c->errstr, "nodename nor servname provided, or not known") == 0 || strcmp(c->errstr, "No address associated with hostname") == 0 || strcmp(c->errstr, "Temporary failure in name resolution") == 0 || strcmp(c->errstr, "hostname nor servname provided, or not known") == 0 || strcmp(c->errstr, "no address associated with name") == 0 || strcmp(c->errstr, "No such host is known. ") == 0)); redisFree(c); } else { printf("Skipping NXDOMAIN test. Found evil ISP!\n"); freeaddrinfo(ai_tmp); } #ifndef _WIN32 test("Returns error when the port is not open: "); c = redisConnect((char*)"localhost", 1); test_cond(c->err == REDIS_ERR_IO && strcmp(c->errstr,"Connection refused") == 0); redisFree(c); test("Returns error when the unix_sock socket path doesn't accept connections: "); c = redisConnectUnix((char*)"/tmp/idontexist.sock"); test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */ redisFree(c); #endif } /* Dummy push handler */ void push_handler(void *privdata, void *reply) { int *counter = privdata; freeReplyObject(reply); *counter += 1; } /* Dummy function just to test setting a callback with redisOptions */ void push_handler_async(redisAsyncContext *ac, void *reply) { (void)ac; (void)reply; } static void test_resp3_push_handler(redisContext *c) { redisPushFn *old = NULL; redisReply *reply; void *privdata; int n = 0; /* Switch to RESP3 and turn on client tracking */ send_hello(c, 3); send_client_tracking(c, "ON"); privdata = c->privdata; c->privdata = &n; reply = redisCommand(c, "GET key:0"); assert(reply != NULL); freeReplyObject(reply); test("RESP3 PUSH messages are handled out of band by default: "); reply = redisCommand(c, "SET key:0 val:0"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS); freeReplyObject(reply); assert((reply = redisCommand(c, "GET key:0")) != NULL); freeReplyObject(reply); old = redisSetPushCallback(c, push_handler); test("We can set a custom RESP3 PUSH handler: "); reply = redisCommand(c, "SET key:0 val:0"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && n == 1); freeReplyObject(reply); /* Unset the push callback and generate an invalidate message making * sure it is not handled out of band. */ test("With no handler, PUSH replies come in-band: "); redisSetPushCallback(c, NULL); assert((reply = redisCommand(c, "GET key:0")) != NULL); freeReplyObject(reply); assert((reply = redisCommand(c, "SET key:0 invalid")) != NULL); test_cond(reply->type == REDIS_REPLY_PUSH); freeReplyObject(reply); test("With no PUSH handler, no replies are lost: "); assert(redisGetReply(c, (void**)&reply) == REDIS_OK); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS); freeReplyObject(reply); /* Return to the originally set PUSH handler */ assert(old != NULL); redisSetPushCallback(c, old); /* Switch back to RESP2 and disable tracking */ c->privdata = privdata; send_client_tracking(c, "OFF"); send_hello(c, 2); } redisOptions get_redis_tcp_options(struct config config) { redisOptions options = {0}; REDIS_OPTIONS_SET_TCP(&options, config.tcp.host, config.tcp.port); return options; } static void test_resp3_push_options(struct config config) { redisAsyncContext *ac; redisContext *c; redisOptions options; test("We set a default RESP3 handler for redisContext: "); options = get_redis_tcp_options(config); assert((c = redisConnectWithOptions(&options)) != NULL); test_cond(c->push_cb != NULL); redisFree(c); test("We don't set a default RESP3 push handler for redisAsyncContext: "); options = get_redis_tcp_options(config); assert((ac = redisAsyncConnectWithOptions(&options)) != NULL); test_cond(ac->c.push_cb == NULL); redisAsyncFree(ac); test("Our REDIS_OPT_NO_PUSH_AUTOFREE flag works: "); options = get_redis_tcp_options(config); options.options |= REDIS_OPT_NO_PUSH_AUTOFREE; assert((c = redisConnectWithOptions(&options)) != NULL); test_cond(c->push_cb == NULL); redisFree(c); test("We can use redisOptions to set a custom PUSH handler for redisContext: "); options = get_redis_tcp_options(config); options.push_cb = push_handler; assert((c = redisConnectWithOptions(&options)) != NULL); test_cond(c->push_cb == push_handler); redisFree(c); test("We can use redisOptions to set a custom PUSH handler for redisAsyncContext: "); options = get_redis_tcp_options(config); options.async_push_cb = push_handler_async; assert((ac = redisAsyncConnectWithOptions(&options)) != NULL); test_cond(ac->push_cb == push_handler_async); redisAsyncFree(ac); } void free_privdata(void *privdata) { struct privdata *data = privdata; data->dtor_counter++; } static void test_privdata_hooks(struct config config) { struct privdata data = {0}; redisOptions options; redisContext *c; test("We can use redisOptions to set privdata: "); options = get_redis_tcp_options(config); REDIS_OPTIONS_SET_PRIVDATA(&options, &data, free_privdata); assert((c = redisConnectWithOptions(&options)) != NULL); test_cond(c->privdata == &data); test("Our privdata destructor fires when we free the context: "); redisFree(c); test_cond(data.dtor_counter == 1); } static void test_blocking_connection(struct config config) { redisContext *c; redisReply *reply; int major; c = do_connect(config); test("Is able to deliver commands: "); reply = redisCommand(c,"PING"); test_cond(reply->type == REDIS_REPLY_STATUS && strcasecmp(reply->str,"pong") == 0) freeReplyObject(reply); test("Is a able to send commands verbatim: "); reply = redisCommand(c,"SET foo bar"); test_cond (reply->type == REDIS_REPLY_STATUS && strcasecmp(reply->str,"ok") == 0) freeReplyObject(reply); test("%%s String interpolation works: "); reply = redisCommand(c,"SET %s %s","foo","hello world"); freeReplyObject(reply); reply = redisCommand(c,"GET foo"); test_cond(reply->type == REDIS_REPLY_STRING && strcmp(reply->str,"hello world") == 0); freeReplyObject(reply); test("%%b String interpolation works: "); reply = redisCommand(c,"SET %b %b","foo",(size_t)3,"hello\x00world",(size_t)11); freeReplyObject(reply); reply = redisCommand(c,"GET foo"); test_cond(reply->type == REDIS_REPLY_STRING && memcmp(reply->str,"hello\x00world",11) == 0) test("Binary reply length is correct: "); test_cond(reply->len == 11) freeReplyObject(reply); test("Can parse nil replies: "); reply = redisCommand(c,"GET nokey"); test_cond(reply->type == REDIS_REPLY_NIL) freeReplyObject(reply); /* test 7 */ test("Can parse integer replies: "); reply = redisCommand(c,"INCR mycounter"); test_cond(reply->type == REDIS_REPLY_INTEGER && reply->integer == 1) freeReplyObject(reply); test("Can parse multi bulk replies: "); freeReplyObject(redisCommand(c,"LPUSH mylist foo")); freeReplyObject(redisCommand(c,"LPUSH mylist bar")); reply = redisCommand(c,"LRANGE mylist 0 -1"); test_cond(reply->type == REDIS_REPLY_ARRAY && reply->elements == 2 && !memcmp(reply->element[0]->str,"bar",3) && !memcmp(reply->element[1]->str,"foo",3)) freeReplyObject(reply); /* m/e with multi bulk reply *before* other reply. * specifically test ordering of reply items to parse. */ test("Can handle nested multi bulk replies: "); freeReplyObject(redisCommand(c,"MULTI")); freeReplyObject(redisCommand(c,"LRANGE mylist 0 -1")); freeReplyObject(redisCommand(c,"PING")); reply = (redisCommand(c,"EXEC")); test_cond(reply->type == REDIS_REPLY_ARRAY && reply->elements == 2 && reply->element[0]->type == REDIS_REPLY_ARRAY && reply->element[0]->elements == 2 && !memcmp(reply->element[0]->element[0]->str,"bar",3) && !memcmp(reply->element[0]->element[1]->str,"foo",3) && reply->element[1]->type == REDIS_REPLY_STATUS && strcasecmp(reply->element[1]->str,"pong") == 0); freeReplyObject(reply); /* Make sure passing NULL to redisGetReply is safe */ test("Can pass NULL to redisGetReply: "); assert(redisAppendCommand(c, "PING") == REDIS_OK); test_cond(redisGetReply(c, NULL) == REDIS_OK); get_redis_version(c, &major, NULL); if (major >= 6) test_resp3_push_handler(c); test_resp3_push_options(config); test_privdata_hooks(config); disconnect(c, 0); } /* Send DEBUG SLEEP 0 to detect if we have this command */ static int detect_debug_sleep(redisContext *c) { int detected; redisReply *reply = redisCommand(c, "DEBUG SLEEP 0\r\n"); if (reply == NULL || c->err) { const char *cause = c->err ? c->errstr : "(none)"; fprintf(stderr, "Error testing for DEBUG SLEEP (Redis error: %s), exiting\n", cause); exit(-1); } detected = reply->type == REDIS_REPLY_STATUS; freeReplyObject(reply); return detected; } static void test_blocking_connection_timeouts(struct config config) { redisContext *c; redisReply *reply; ssize_t s; const char *sleep_cmd = "DEBUG SLEEP 3\r\n"; struct timeval tv; c = do_connect(config); test("Successfully completes a command when the timeout is not exceeded: "); reply = redisCommand(c,"SET foo fast"); freeReplyObject(reply); tv.tv_sec = 0; tv.tv_usec = 10000; redisSetTimeout(c, tv); reply = redisCommand(c, "GET foo"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STRING && memcmp(reply->str, "fast", 4) == 0); freeReplyObject(reply); disconnect(c, 0); c = do_connect(config); test("Does not return a reply when the command times out: "); if (detect_debug_sleep(c)) { redisAppendFormattedCommand(c, sleep_cmd, strlen(sleep_cmd)); s = c->funcs->write(c); tv.tv_sec = 0; tv.tv_usec = 10000; redisSetTimeout(c, tv); reply = redisCommand(c, "GET foo"); #ifndef _WIN32 test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO && strcmp(c->errstr, "Resource temporarily unavailable") == 0); #else test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_TIMEOUT && strcmp(c->errstr, "recv timeout") == 0); #endif freeReplyObject(reply); } else { test_skipped(); } test("Reconnect properly reconnects after a timeout: "); do_reconnect(c, config); reply = redisCommand(c, "PING"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0); freeReplyObject(reply); test("Reconnect properly uses owned parameters: "); config.tcp.host = "foo"; config.unix_sock.path = "foo"; do_reconnect(c, config); reply = redisCommand(c, "PING"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0); freeReplyObject(reply); disconnect(c, 0); } static void test_blocking_io_errors(struct config config) { redisContext *c; redisReply *reply; void *_reply; int major, minor; /* Connect to target given by config. */ c = do_connect(config); get_redis_version(c, &major, &minor); test("Returns I/O error when the connection is lost: "); reply = redisCommand(c,"QUIT"); if (major > 2 || (major == 2 && minor > 0)) { /* > 2.0 returns OK on QUIT and read() should be issued once more * to know the descriptor is at EOF. */ test_cond(strcasecmp(reply->str,"OK") == 0 && redisGetReply(c,&_reply) == REDIS_ERR); freeReplyObject(reply); } else { test_cond(reply == NULL); } #ifndef _WIN32 /* On 2.0, QUIT will cause the connection to be closed immediately and * the read(2) for the reply on QUIT will set the error to EOF. * On >2.0, QUIT will return with OK and another read(2) needed to be * issued to find out the socket was closed by the server. In both * conditions, the error will be set to EOF. */ assert(c->err == REDIS_ERR_EOF && strcmp(c->errstr,"Server closed the connection") == 0); #endif redisFree(c); c = do_connect(config); test("Returns I/O error on socket timeout: "); struct timeval tv = { 0, 1000 }; assert(redisSetTimeout(c,tv) == REDIS_OK); int respcode = redisGetReply(c,&_reply); #ifndef _WIN32 test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_IO && errno == EAGAIN); #else test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_TIMEOUT); #endif redisFree(c); } static void test_invalid_timeout_errors(struct config config) { redisContext *c; test("Set error when an invalid timeout usec value is given to redisConnectWithTimeout: "); config.tcp.timeout.tv_sec = 0; config.tcp.timeout.tv_usec = 10000001; c = redisConnectWithTimeout(config.tcp.host, config.tcp.port, config.tcp.timeout); test_cond(c->err == REDIS_ERR_IO && strcmp(c->errstr, "Invalid timeout specified") == 0); redisFree(c); test("Set error when an invalid timeout sec value is given to redisConnectWithTimeout: "); config.tcp.timeout.tv_sec = (((LONG_MAX) - 999) / 1000) + 1; config.tcp.timeout.tv_usec = 0; c = redisConnectWithTimeout(config.tcp.host, config.tcp.port, config.tcp.timeout); test_cond(c->err == REDIS_ERR_IO && strcmp(c->errstr, "Invalid timeout specified") == 0); redisFree(c); } /* Wrap malloc to abort on failure so OOM checks don't make the test logic * harder to follow. */ void *hi_malloc_safe(size_t size) { void *ptr = hi_malloc(size); if (ptr == NULL) { fprintf(stderr, "Error: Out of memory\n"); exit(-1); } return ptr; } static void test_throughput(struct config config) { redisContext *c = do_connect(config); redisReply **replies; int i, num; long long t1, t2; test("Throughput:\n"); for (i = 0; i < 500; i++) freeReplyObject(redisCommand(c,"LPUSH mylist foo")); num = 1000; replies = hi_malloc_safe(sizeof(redisReply*)*num); t1 = usec(); for (i = 0; i < num; i++) { replies[i] = redisCommand(c,"PING"); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0); replies = hi_malloc_safe(sizeof(redisReply*)*num); t1 = usec(); for (i = 0; i < num; i++) { replies[i] = redisCommand(c,"LRANGE mylist 0 499"); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); assert(replies[i] != NULL && replies[i]->elements == 500); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0); replies = hi_malloc_safe(sizeof(redisReply*)*num); t1 = usec(); for (i = 0; i < num; i++) { replies[i] = redisCommand(c, "INCRBY incrkey %d", 1000000); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_INTEGER); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx INCRBY: %.3fs)\n", num, (t2-t1)/1000000.0); num = 10000; replies = hi_malloc_safe(sizeof(redisReply*)*num); for (i = 0; i < num; i++) redisAppendCommand(c,"PING"); t1 = usec(); for (i = 0; i < num; i++) { assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); replies = hi_malloc_safe(sizeof(redisReply*)*num); for (i = 0; i < num; i++) redisAppendCommand(c,"LRANGE mylist 0 499"); t1 = usec(); for (i = 0; i < num; i++) { assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); assert(replies[i] != NULL && replies[i]->elements == 500); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); replies = hi_malloc_safe(sizeof(redisReply*)*num); for (i = 0; i < num; i++) redisAppendCommand(c,"INCRBY incrkey %d", 1000000); t1 = usec(); for (i = 0; i < num; i++) { assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_INTEGER); } t2 = usec(); for (i = 0; i < num; i++) freeReplyObject(replies[i]); hi_free(replies); printf("\t(%dx INCRBY (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); disconnect(c, 0); } // static long __test_callback_flags = 0; // static void __test_callback(redisContext *c, void *privdata) { // ((void)c); // /* Shift to detect execution order */ // __test_callback_flags <<= 8; // __test_callback_flags |= (long)privdata; // } // // static void __test_reply_callback(redisContext *c, redisReply *reply, void *privdata) { // ((void)c); // /* Shift to detect execution order */ // __test_callback_flags <<= 8; // __test_callback_flags |= (long)privdata; // if (reply) freeReplyObject(reply); // } // // static redisContext *__connect_nonblock() { // /* Reset callback flags */ // __test_callback_flags = 0; // return redisConnectNonBlock("127.0.0.1", port, NULL); // } // // static void test_nonblocking_connection() { // redisContext *c; // int wdone = 0; // // test("Calls command callback when command is issued: "); // c = __connect_nonblock(); // redisSetCommandCallback(c,__test_callback,(void*)1); // redisCommand(c,"PING"); // test_cond(__test_callback_flags == 1); // redisFree(c); // // test("Calls disconnect callback on redisDisconnect: "); // c = __connect_nonblock(); // redisSetDisconnectCallback(c,__test_callback,(void*)2); // redisDisconnect(c); // test_cond(__test_callback_flags == 2); // redisFree(c); // // test("Calls disconnect callback and free callback on redisFree: "); // c = __connect_nonblock(); // redisSetDisconnectCallback(c,__test_callback,(void*)2); // redisSetFreeCallback(c,__test_callback,(void*)4); // redisFree(c); // test_cond(__test_callback_flags == ((2 << 8) | 4)); // // test("redisBufferWrite against empty write buffer: "); // c = __connect_nonblock(); // test_cond(redisBufferWrite(c,&wdone) == REDIS_OK && wdone == 1); // redisFree(c); // // test("redisBufferWrite against not yet connected fd: "); // c = __connect_nonblock(); // redisCommand(c,"PING"); // test_cond(redisBufferWrite(c,NULL) == REDIS_ERR && // strncmp(c->error,"write:",6) == 0); // redisFree(c); // // test("redisBufferWrite against closed fd: "); // c = __connect_nonblock(); // redisCommand(c,"PING"); // redisDisconnect(c); // test_cond(redisBufferWrite(c,NULL) == REDIS_ERR && // strncmp(c->error,"write:",6) == 0); // redisFree(c); // // test("Process callbacks in the right sequence: "); // c = __connect_nonblock(); // redisCommandWithCallback(c,__test_reply_callback,(void*)1,"PING"); // redisCommandWithCallback(c,__test_reply_callback,(void*)2,"PING"); // redisCommandWithCallback(c,__test_reply_callback,(void*)3,"PING"); // // /* Write output buffer */ // wdone = 0; // while(!wdone) { // usleep(500); // redisBufferWrite(c,&wdone); // } // // /* Read until at least one callback is executed (the 3 replies will // * arrive in a single packet, causing all callbacks to be executed in // * a single pass). */ // while(__test_callback_flags == 0) { // assert(redisBufferRead(c) == REDIS_OK); // redisProcessCallbacks(c); // } // test_cond(__test_callback_flags == 0x010203); // redisFree(c); // // test("redisDisconnect executes pending callbacks with NULL reply: "); // c = __connect_nonblock(); // redisSetDisconnectCallback(c,__test_callback,(void*)1); // redisCommandWithCallback(c,__test_reply_callback,(void*)2,"PING"); // redisDisconnect(c); // test_cond(__test_callback_flags == 0x0201); // redisFree(c); // } int main(int argc, char **argv) { struct config cfg = { .tcp = { .host = "127.0.0.1", .port = 6379 }, .unix_sock = { .path = "/tmp/redis.sock" } }; int throughput = 1; int test_inherit_fd = 1; int skips_as_fails = 0; int test_unix_socket; /* Parse command line options. */ argv++; argc--; while (argc) { if (argc >= 2 && !strcmp(argv[0],"-h")) { argv++; argc--; cfg.tcp.host = argv[0]; } else if (argc >= 2 && !strcmp(argv[0],"-p")) { argv++; argc--; cfg.tcp.port = atoi(argv[0]); } else if (argc >= 2 && !strcmp(argv[0],"-s")) { argv++; argc--; cfg.unix_sock.path = argv[0]; } else if (argc >= 1 && !strcmp(argv[0],"--skip-throughput")) { throughput = 0; } else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) { test_inherit_fd = 0; } else if (argc >= 1 && !strcmp(argv[0],"--skips-as-fails")) { skips_as_fails = 1; #ifdef HIREDIS_TEST_SSL } else if (argc >= 2 && !strcmp(argv[0],"--ssl-port")) { argv++; argc--; cfg.ssl.port = atoi(argv[0]); } else if (argc >= 2 && !strcmp(argv[0],"--ssl-host")) { argv++; argc--; cfg.ssl.host = argv[0]; } else if (argc >= 2 && !strcmp(argv[0],"--ssl-ca-cert")) { argv++; argc--; cfg.ssl.ca_cert = argv[0]; } else if (argc >= 2 && !strcmp(argv[0],"--ssl-cert")) { argv++; argc--; cfg.ssl.cert = argv[0]; } else if (argc >= 2 && !strcmp(argv[0],"--ssl-key")) { argv++; argc--; cfg.ssl.key = argv[0]; #endif } else { fprintf(stderr, "Invalid argument: %s\n", argv[0]); exit(1); } argv++; argc--; } #ifndef _WIN32 /* Ignore broken pipe signal (for I/O error tests). */ signal(SIGPIPE, SIG_IGN); test_unix_socket = access(cfg.unix_sock.path, F_OK) == 0; #else /* Unix sockets don't exist in Windows */ test_unix_socket = 0; #endif test_allocator_injection(); test_format_commands(); test_reply_reader(); test_blocking_connection_errors(); test_free_null(); printf("\nTesting against TCP connection (%s:%d):\n", cfg.tcp.host, cfg.tcp.port); cfg.type = CONN_TCP; test_blocking_connection(cfg); test_blocking_connection_timeouts(cfg); test_blocking_io_errors(cfg); test_invalid_timeout_errors(cfg); test_append_formatted_commands(cfg); if (throughput) test_throughput(cfg); printf("\nTesting against Unix socket connection (%s): ", cfg.unix_sock.path); if (test_unix_socket) { printf("\n"); cfg.type = CONN_UNIX; test_blocking_connection(cfg); test_blocking_connection_timeouts(cfg); test_blocking_io_errors(cfg); if (throughput) test_throughput(cfg); } else { test_skipped(); } #ifdef HIREDIS_TEST_SSL if (cfg.ssl.port && cfg.ssl.host) { redisInitOpenSSL(); _ssl_ctx = redisCreateSSLContext(cfg.ssl.ca_cert, NULL, cfg.ssl.cert, cfg.ssl.key, NULL, NULL); assert(_ssl_ctx != NULL); printf("\nTesting against SSL connection (%s:%d):\n", cfg.ssl.host, cfg.ssl.port); cfg.type = CONN_SSL; test_blocking_connection(cfg); test_blocking_connection_timeouts(cfg); test_blocking_io_errors(cfg); test_invalid_timeout_errors(cfg); test_append_formatted_commands(cfg); if (throughput) test_throughput(cfg); redisFreeSSLContext(_ssl_ctx); _ssl_ctx = NULL; } #endif if (test_inherit_fd) { printf("\nTesting against inherited fd (%s): ", cfg.unix_sock.path); if (test_unix_socket) { printf("\n"); cfg.type = CONN_FD; test_blocking_connection(cfg); } else { test_skipped(); } } if (fails || (skips_as_fails && skips)) { printf("*** %d TESTS FAILED ***\n", fails); if (skips) { printf("*** %d TESTS SKIPPED ***\n", skips); } return 1; } printf("ALL TESTS PASSED (%d skipped)\n", skips); return 0; } redis-rb-redis-client-367ca25/hiredis-client/ext/redis_client/hiredis/vendor/test.sh000077500000000000000000000037311515654015100305550ustar00rootroot00000000000000#!/bin/sh -ue REDIS_SERVER=${REDIS_SERVER:-redis-server} REDIS_PORT=${REDIS_PORT:-56379} REDIS_SSL_PORT=${REDIS_SSL_PORT:-56443} TEST_SSL=${TEST_SSL:-0} SKIPS_AS_FAILS=${SKIPS_AS_FAILS-:0} SSL_TEST_ARGS= SKIPS_ARG= tmpdir=$(mktemp -d) PID_FILE=${tmpdir}/hiredis-test-redis.pid SOCK_FILE=${tmpdir}/hiredis-test-redis.sock if [ "$TEST_SSL" = "1" ]; then SSL_CA_CERT=${tmpdir}/ca.crt SSL_CA_KEY=${tmpdir}/ca.key SSL_CERT=${tmpdir}/redis.crt SSL_KEY=${tmpdir}/redis.key openssl genrsa -out ${tmpdir}/ca.key 4096 openssl req \ -x509 -new -nodes -sha256 \ -key ${SSL_CA_KEY} \ -days 3650 \ -subj '/CN=Hiredis Test CA' \ -out ${SSL_CA_CERT} openssl genrsa -out ${SSL_KEY} 2048 openssl req \ -new -sha256 \ -key ${SSL_KEY} \ -subj '/CN=Hiredis Test Cert' | \ openssl x509 \ -req -sha256 \ -CA ${SSL_CA_CERT} \ -CAkey ${SSL_CA_KEY} \ -CAserial ${tmpdir}/ca.txt \ -CAcreateserial \ -days 365 \ -out ${SSL_CERT} SSL_TEST_ARGS="--ssl-host 127.0.0.1 --ssl-port ${REDIS_SSL_PORT} --ssl-ca-cert ${SSL_CA_CERT} --ssl-cert ${SSL_CERT} --ssl-key ${SSL_KEY}" fi cleanup() { set +e kill $(cat ${PID_FILE}) rm -rf ${tmpdir} } trap cleanup INT TERM EXIT cat > ${tmpdir}/redis.conf <> ${tmpdir}/redis.conf < /* for struct timeval */ #ifndef inline #define inline __inline #endif #ifndef strcasecmp #define strcasecmp stricmp #endif #ifndef strncasecmp #define strncasecmp strnicmp #endif #ifndef va_copy #define va_copy(d,s) ((d) = (s)) #endif #ifndef snprintf #define snprintf c99_snprintf __inline int c99_vsnprintf(char* str, size_t size, const char* format, va_list ap) { int count = -1; if (size != 0) count = _vsnprintf_s(str, size, _TRUNCATE, format, ap); if (count == -1) count = _vscprintf(format, ap); return count; } __inline int c99_snprintf(char* str, size_t size, const char* format, ...) { int count; va_list ap; va_start(ap, format); count = c99_vsnprintf(str, size, format, ap); va_end(ap); return count; } #endif #endif /* _MSC_VER */ #ifdef _WIN32 #define strerror_r(errno,buf,len) strerror_s(buf,len,errno) #endif /* _WIN32 */ #endif /* _WIN32_HELPER_INCLUDE */ redis-rb-redis-client-367ca25/hiredis-client/hiredis-client.gemspec000066400000000000000000000023061515654015100253140ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/redis_client/version" Gem::Specification.new do |spec| spec.name = "hiredis-client" spec.version = RedisClient::VERSION spec.authors = ["Jean Boussier"] spec.email = ["jean.boussier@gmail.com"] spec.summary = "Hiredis binding for redis-client" spec.homepage = "https://github.com/redis-rb/redis-client" spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = File.join(spec.homepage, "blob/master/CHANGELOG.md") # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. gemspec = File.basename(__FILE__) spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject do |f| (f == gemspec) || f.start_with?(*%w[bin/ test/]) end end spec.require_paths = ["lib"] spec.extensions = ["ext/redis_client/hiredis/extconf.rb"] spec.add_runtime_dependency "redis-client", RedisClient::VERSION end redis-rb-redis-client-367ca25/hiredis-client/lib/000077500000000000000000000000001515654015100216115ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/lib/hiredis-client.rb000066400000000000000000000003661515654015100250460ustar00rootroot00000000000000# frozen_string_literal: true require "redis-client" begin require "redis_client/hiredis_connection" rescue LoadError else RedisClient.register_driver(:hiredis) { RedisClient::HiredisConnection } RedisClient.default_driver = :hiredis end redis-rb-redis-client-367ca25/hiredis-client/lib/redis_client/000077500000000000000000000000001515654015100242555ustar00rootroot00000000000000redis-rb-redis-client-367ca25/hiredis-client/lib/redis_client/hiredis_connection.rb000066400000000000000000000074401515654015100304550ustar00rootroot00000000000000# frozen_string_literal: true require "openssl" require "redis_client/hiredis_connection.so" require "redis_client/connection_mixin" class RedisClient class HiredisConnection include ConnectionMixin class << self def ssl_context(ssl_params) unless ssl_params[:ca_file] || ssl_params[:ca_path] default_ca_file = OpenSSL::X509::DEFAULT_CERT_FILE default_ca_path = OpenSSL::X509::DEFAULT_CERT_DIR if File.readable? default_ca_file ssl_params[:ca_file] = default_ca_file elsif File.directory? default_ca_path ssl_params[:ca_path] = default_ca_path end end HiredisConnection::SSLContext.new( ca_file: ssl_params[:ca_file], ca_path: ssl_params[:ca_path], cert: ssl_params[:cert], key: ssl_params[:key], hostname: ssl_params[:hostname], ) end end class SSLContext def initialize(ca_file: nil, ca_path: nil, cert: nil, key: nil, hostname: nil) if (error = init(ca_file, ca_path, cert, key, hostname)) raise error end end end def initialize(config, connect_timeout:, read_timeout:, write_timeout:) super(config) self.connect_timeout = connect_timeout self.read_timeout = read_timeout self.write_timeout = write_timeout connect end def close _close super end def reconnect reconnected = begin _reconnect(@config.path, @config.ssl_context) rescue SystemCallError => error host = @config.path || "#{@config.host}:#{@config.port}" error_code = error.class.name.split("::").last raise CannotConnectError, "Failed to reconnect to #{host} (#{error_code})" end if reconnected true else # Reusing the hiredis connection didn't work let's create a fresh one super end end def connect_timeout=(timeout) self.connect_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0 @connect_timeout = timeout end def read_timeout=(timeout) self.read_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0 @read_timeout = timeout end def write_timeout=(timeout) self.write_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0 @write_timeout = timeout end def read(timeout = nil) if timeout.nil? _read else previous_timeout = @read_timeout self.read_timeout = timeout begin _read ensure self.read_timeout = previous_timeout end end rescue SystemCallError, IOError => error raise connection_error(error.message) rescue Error => error error._set_config(config) error._set_retry_attempt(@retry_attempt) raise error end def write(command) _write(command) flush rescue SystemCallError, IOError => error raise connection_error(error.message) rescue Error => error error._set_config(config) error._set_retry_attempt(@retry_attempt) raise error end def write_multi(commands) commands.each do |command| _write(command) end flush rescue SystemCallError, IOError => error raise connection_error(error.message) rescue Error => error error._set_config(config) error._set_retry_attempt(@retry_attempt) raise error end private def connect @server_key = @config.server_key _connect(@config.path, @config.host, @config.port, @config.ssl_context) rescue SystemCallError => error host = @config.path || "#{@config.host}:#{@config.port}" error_code = error.class.name.split("::").last raise CannotConnectError, "Failed to connect to #{host} (#{error_code})" end end end redis-rb-redis-client-367ca25/lib/000077500000000000000000000000001515654015100167065ustar00rootroot00000000000000redis-rb-redis-client-367ca25/lib/redis-client.rb000066400000000000000000000000661515654015100216170ustar00rootroot00000000000000# frozen_string_literal: true require "redis_client" redis-rb-redis-client-367ca25/lib/redis_client.rb000066400000000000000000000473011515654015100217040ustar00rootroot00000000000000# frozen_string_literal: true require "redis_client/version" require "redis_client/command_builder" require "redis_client/url_config" require "redis_client/config" require "redis_client/pid_cache" require "redis_client/sentinel_config" require "redis_client/middlewares" class RedisClient @driver_definitions = {} @drivers = {} @default_driver = nil class << self def register_driver(name, &block) @driver_definitions[name] = block end def driver(name) return name if name.is_a?(Class) name = name.to_sym unless @driver_definitions.key?(name) raise ArgumentError, "Unknown driver #{name.inspect}, expected one of: `#{@driver_definitions.keys.inspect}`" end @drivers[name] ||= @driver_definitions[name]&.call end def default_driver unless @default_driver @driver_definitions.each_key do |name| if @default_driver = driver(name) break end rescue LoadError end end @default_driver end def default_driver=(name) @default_driver = driver(name) end def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end def now_ms Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) end end register_driver :ruby do require "redis_client/ruby_connection" RubyConnection end module Common attr_reader :config, :id, :nodes attr_accessor :connect_timeout, :read_timeout, :write_timeout def initialize( config, id: config.id, connect_timeout: config.connect_timeout, read_timeout: config.read_timeout, write_timeout: config.write_timeout ) @config = config @id = id&.to_s @connect_timeout = connect_timeout @read_timeout = read_timeout @write_timeout = write_timeout @command_builder = config.command_builder @pid = PIDCache.pid @nodes = [self].freeze end def timeout=(timeout) @connect_timeout = @read_timeout = @write_timeout = timeout end def node_for(_key) self end def nodes_for(*keys) keys.flatten! { self => keys } end end module HasConfig attr_reader :config def _set_config(config) @config = config end def message return super unless config&.resolved? "#{super} (#{config.server_url})" end end module Retriable def _set_retry_attempt(retry_attempt) @retry_attempt = retry_attempt end def retry_attempt @retry_attempt || 0 end def retriable? !!@retry_attempt end def final? !@retry_attempt end end module Final def _set_retry_attempt(_retry_attempt) end def retry_attempt 0 end def retriable? false end def final? true end end class Error < StandardError include HasConfig include Retriable def self.with_config(message, config = nil) error = new(message) error._set_config(config) error end end ProtocolError = Class.new(Error) UnsupportedServer = Class.new(Error) ConnectionError = Class.new(Error) CannotConnectError = Class.new(ConnectionError) FailoverError = Class.new(ConnectionError) TimeoutError = Class.new(ConnectionError) ReadTimeoutError = Class.new(TimeoutError) WriteTimeoutError = Class.new(TimeoutError) CheckoutTimeoutError = Class.new(TimeoutError) module HasCommand attr_reader :command def _set_command(command) @command = command end end module HasCode attr_reader :code def initialize(message = nil, code = nil) super(message) @code = code end end class CommandError < Error include HasCommand include HasCode include Final class << self def parse(error_message) code = if error_message.start_with?("ERR Error running script") # On older redis servers script errors are nested. # So we need to parse some more. if (match = error_message.match(/:\s-([A-Z]+) /)) match[1] end end code ||= error_message.split(' ', 2).first klass = ERRORS.fetch(code, self) klass.new(error_message.strip, code.freeze) end end end AuthenticationError = Class.new(CommandError) PermissionError = Class.new(CommandError) WrongTypeError = Class.new(CommandError) OutOfMemoryError = Class.new(CommandError) NoScriptError = Class.new(CommandError) ReadOnlyError = Class.new(ConnectionError) ReadOnlyError.include(HasCommand) ReadOnlyError.include(HasCode) MasterDownError = Class.new(ConnectionError) MasterDownError.include(HasCommand) MasterDownError.include(HasCode) CommandError::ERRORS = { "WRONGPASS" => AuthenticationError, "NOPERM" => PermissionError, "READONLY" => ReadOnlyError, "MASTERDOWN" => MasterDownError, "WRONGTYPE" => WrongTypeError, "OOM" => OutOfMemoryError, "NOSCRIPT" => NoScriptError, }.freeze class << self def config(**kwargs) Config.new(client_implementation: self, **kwargs) end def sentinel(**kwargs) SentinelConfig.new(client_implementation: self, **kwargs) end def ring(*clients, **options) clients.flatten! require "redis_client/hash_ring" unless defined?(HashRing) HashRing.new(clients, **options) end def new(arg = nil, **kwargs) if arg.is_a?(Config::Common) super else super(config(**(arg || {}), **kwargs)) end end def register(middleware) Middlewares.include(middleware) end end include Common def initialize(config, **) super @middlewares = config.middlewares_stack.new(self) @raw_connection = nil @disable_reconnection = false @retry_attempt = nil end def inspect id_string = " id=#{id}" if id "#<#{self.class.name} #{config.server_url}#{id_string}>" end def server_url config.server_url end def id config.id end def timeout config.read_timeout end def idle_timeout config.idle_timeout end def db config.db end def host config.host unless config.path end def port config.port unless config.path end def path config.path end def username config.username end def password config.password end def size 1 end def with(_options = nil) yield self end alias_method :then, :with def timeout=(timeout) super @raw_connection&.read_timeout = timeout @raw_connection&.write_timeout = timeout end def read_timeout=(timeout) super @raw_connection&.read_timeout = timeout end def write_timeout=(timeout) super @raw_connection&.write_timeout = timeout end def pubsub sub = PubSub.new(ensure_connected, @command_builder) @raw_connection = nil sub end def measure_round_trip_delay ensure_connected do |connection| @middlewares.call(["PING"], config) do connection.measure_round_trip_delay end end end def call(*command, **kwargs) command = @command_builder.generate(command, kwargs) result = ensure_connected do |connection| @middlewares.call(command, config) do connection.call(command, nil) end end if block_given? yield result else result end end def call_v(command) command = @command_builder.generate(command) result = ensure_connected do |connection| @middlewares.call(command, config) do connection.call(command, nil) end end if block_given? yield result else result end end def call_once(*command, **kwargs) command = @command_builder.generate(command, kwargs) result = ensure_connected(retryable: false) do |connection| @middlewares.call(command, config) do connection.call(command, nil) end end if block_given? yield result else result end end def call_once_v(command) command = @command_builder.generate(command) result = ensure_connected(retryable: false) do |connection| @middlewares.call(command, config) do connection.call(command, nil) end end if block_given? yield result else result end end def blocking_call(timeout, *command, **kwargs) command = @command_builder.generate(command, kwargs) error = nil result = ensure_connected do |connection| @middlewares.call(command, config) do connection.call(command, timeout) end rescue ReadTimeoutError => error break end if error raise error elsif block_given? yield result else result end end def blocking_call_v(timeout, command) command = @command_builder.generate(command) error = nil result = ensure_connected do |connection| @middlewares.call(command, config) do connection.call(command, timeout) end rescue ReadTimeoutError => error break end if error raise error elsif block_given? yield result else result end end def scan(*args, **kwargs, &block) unless block_given? return to_enum(__callee__, *args, **kwargs) end args = @command_builder.generate(["SCAN", 0] + args, kwargs) scan_list(1, args, &block) end def sscan(key, *args, **kwargs, &block) unless block_given? return to_enum(__callee__, key, *args, **kwargs) end args = @command_builder.generate(["SSCAN", key, 0] + args, kwargs) scan_list(2, args, &block) end def hscan(key, *args, **kwargs, &block) unless block_given? return to_enum(__callee__, key, *args, **kwargs) end args = @command_builder.generate(["HSCAN", key, 0] + args, kwargs) scan_pairs(2, args, &block) end def zscan(key, *args, **kwargs, &block) unless block_given? return to_enum(__callee__, key, *args, **kwargs) end args = @command_builder.generate(["ZSCAN", key, 0] + args, kwargs) scan_pairs(2, args, &block) end def connected? @raw_connection&.revalidate end def close @raw_connection&.close self end def disable_reconnection(&block) ensure_connected(retryable: false, &block) end def pipelined(exception: true) pipeline = Pipeline.new(@command_builder) yield pipeline if pipeline._size == 0 [] else results = ensure_connected(retryable: pipeline._retryable?) do |connection| commands = pipeline._commands @middlewares.call_pipelined(commands, config) do connection.call_pipelined(commands, pipeline._timeouts, exception: exception) end end pipeline._coerce!(results) end end def multi(watch: nil, &block) transaction = nil results = if watch # WATCH is stateful, so we can't reconnect if it's used, the whole transaction # has to be redone. ensure_connected(retryable: false) do |connection| call("WATCH", *watch) begin if transaction = build_transaction(&block) commands = transaction._commands results = @middlewares.call_pipelined(commands, config) do connection.call_pipelined(commands, nil) end.last else call("UNWATCH") [] end rescue call("UNWATCH") if connected? && watch raise end end else transaction = build_transaction(&block) if transaction._empty? [] else ensure_connected(retryable: transaction._retryable?) do |connection| commands = transaction._commands @middlewares.call_pipelined(commands, config) do connection.call_pipelined(commands, nil) end.last end end end if transaction transaction._coerce!(results) else results end end class PubSub def initialize(raw_connection, command_builder) @raw_connection = raw_connection @command_builder = command_builder end def call(*command, **kwargs) raw_connection.write(@command_builder.generate(command, kwargs)) nil end def call_v(command) raw_connection.write(@command_builder.generate(command)) nil end def close @raw_connection&.close @raw_connection = nil # PubSub can't just reconnect self end def next_event(timeout = nil) unless raw_connection raise ConnectionError, "Connection was closed or lost" end raw_connection.read(timeout) rescue ReadTimeoutError nil end private attr_reader :raw_connection end class Multi def initialize(command_builder) @command_builder = command_builder @size = 0 @commands = [] @blocks = nil @retryable = true end def call(*command, **kwargs, &block) command = @command_builder.generate(command, kwargs) (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def call_v(command, &block) command = @command_builder.generate(command) (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def call_once(*command, **kwargs, &block) command = @command_builder.generate(command, kwargs) @retryable = false (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def call_once_v(command, &block) command = @command_builder.generate(command) @retryable = false (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def _commands @commands end def _blocks @blocks end def _size @commands.size end def _empty? @commands.size <= 2 end def _timeouts nil end def _retryable? @retryable end def _coerce!(results) results&.each_with_index do |result, index| if result.is_a?(CommandError) result._set_command(@commands[index + 1]) raise result end if @blocks && block = @blocks[index + 1] results[index] = block.call(result) end end results end end class Pipeline < Multi def initialize(_command_builder) super @timeouts = nil end def blocking_call(timeout, *command, **kwargs, &block) command = @command_builder.generate(command, kwargs) @timeouts ||= [] @timeouts[@commands.size] = timeout (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def blocking_call_v(timeout, command, &block) command = @command_builder.generate(command) @timeouts ||= [] @timeouts[@commands.size] = timeout (@blocks ||= [])[@commands.size] = block if block_given? @commands << command nil end def _timeouts @timeouts end def _empty? @commands.empty? end def _coerce!(results) return results unless results @blocks&.each_with_index do |block, index| if block results[index] = block.call(results[index]) end end results end end private def build_transaction transaction = Multi.new(@command_builder) transaction.call("MULTI") yield transaction transaction.call("EXEC") transaction end def scan_list(cursor_index, command, &block) cursor = 0 while cursor != "0" command[cursor_index] = cursor cursor, elements = call(*command) elements.each(&block) end nil end def scan_pairs(cursor_index, command) cursor = 0 while cursor != "0" command[cursor_index] = cursor cursor, elements = call(*command) index = 0 size = elements.size while index < size yield elements[index], elements[index + 1] index += 2 end end nil end def ensure_connected(retryable: true) close if !config.inherit_socket && @pid != PIDCache.pid if @disable_reconnection @raw_connection.retry_attempt = nil if block_given? yield @raw_connection else @raw_connection end elsif retryable tries = 0 connection = nil preferred_error = nil begin @retry_attempt = config.retriable?(tries) ? tries : nil connection = raw_connection if block_given? result = yield connection @last_used_at = RedisClient.now result else connection end rescue ConnectionError, ProtocolError => error preferred_error ||= error close if error.is_a?(CircuitBreaker::OpenCircuitError) raise preferred_error else preferred_error = error end if !@disable_reconnection && config.retry_connecting?(tries, error) tries += 1 retry else raise preferred_error end end else previous_disable_reconnection = @disable_reconnection connection = ensure_connected begin @disable_reconnection = true @raw_connection.retry_attempt = nil result = yield connection @last_used_at = RedisClient.now result rescue ConnectionError, ProtocolError close raise ensure @disable_reconnection = previous_disable_reconnection end end end def raw_connection if @raw_connection.nil? || !@raw_connection.revalidate connect end if config.idle_timeout && (@last_used_at + config.idle_timeout) < RedisClient.now @middlewares.call(["PING"], config) do @raw_connection.call(["PING"], nil) rescue ConnectionError, ProtocolError close connect end end @raw_connection.retry_attempt = @retry_attempt @raw_connection end def connect @pid = PIDCache.pid if @raw_connection&.revalidate @middlewares.connect(config) do @raw_connection.reconnect end else @raw_connection = @middlewares.connect(config) do config.driver.new( config, connect_timeout: connect_timeout, read_timeout: read_timeout, write_timeout: write_timeout, ) end end @last_used_at = RedisClient.now @raw_connection.retry_attempt = @retry_attempt prelude = config.connection_prelude.dup if id prelude << ["CLIENT", "SETNAME", id] end # The connection prelude is deliberately not sent to Middlewares if config.sentinel? prelude << ["ROLE"] role, = @middlewares.call_pipelined(prelude, config) do @raw_connection.call_pipelined(prelude, nil).last end config.check_role!(role) else unless prelude.empty? @middlewares.call_pipelined(prelude, config) do @raw_connection.call_pipelined(prelude, nil) end end end rescue FailoverError, CannotConnectError => error @raw_connection&.close error._set_config(config) raise error rescue ConnectionError => error @raw_connection&.close connect_error = CannotConnectError.with_config(error.message, config) connect_error.set_backtrace(error.backtrace) raise connect_error rescue CommandError => error @raw_connection&.close if error.message.match?(/ERR unknown command ['`]HELLO['`]/) raise UnsupportedServer, "redis-client requires Redis 6+ with HELLO command available (#{config.server_url})" # Ignore CLIENT SETINFO errors (Redis < 7.2 doesn't support it) elsif error.message.match?(/unknown subcommand.*setinfo/i) # Silently ignore - CLIENT SETINFO is not supported on Redis < 7.2 else raise end end end require "redis_client/pooled" require "redis_client/circuit_breaker" RedisClient.default_driver redis-rb-redis-client-367ca25/lib/redis_client/000077500000000000000000000000001515654015100213525ustar00rootroot00000000000000redis-rb-redis-client-367ca25/lib/redis_client/circuit_breaker.rb000066400000000000000000000051021515654015100250320ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient class CircuitBreaker module Middleware def connect(config) config.circuit_breaker.protect { super } end def call(_command, config) config.circuit_breaker.protect { super } end def call_pipelined(_commands, config) config.circuit_breaker.protect { super } end end OpenCircuitError = Class.new(CannotConnectError) attr_reader :error_timeout, :error_threshold, :error_threshold_timeout, :success_threshold def initialize(error_threshold:, error_timeout:, error_threshold_timeout: error_timeout, success_threshold: 0) @error_threshold = Integer(error_threshold) @error_threshold_timeout = Float(error_threshold_timeout) @error_timeout = Float(error_timeout) @success_threshold = Integer(success_threshold) @errors = [] @successes = 0 @state = :closed @lock = Mutex.new end def protect if @state == :open refresh_state end case @state when :open raise OpenCircuitError, "Too many connection errors happened recently" when :closed begin yield rescue ConnectionError record_error raise end when :half_open begin result = yield record_success result rescue ConnectionError record_error raise end else raise "[BUG] RedisClient::CircuitBreaker unexpected @state (#{@state.inspect}})" end end private def refresh_state now = RedisClient.now @lock.synchronize do if @errors.last < (now - @error_timeout) if @success_threshold > 0 @state = :half_open @successes = 0 else @errors.clear @state = :closed end end end end def record_error now = RedisClient.now expiry = now - @error_threshold_timeout @lock.synchronize do if @state == :closed @errors.reject! { |t| t < expiry } end @errors << now @successes = 0 if @state == :half_open || (@state == :closed && @errors.size >= @error_threshold) @state = :open end end end def record_success return unless @state == :half_open @lock.synchronize do return unless @state == :half_open @successes += 1 if @successes >= @success_threshold @errors.clear @state = :closed end end end end end redis-rb-redis-client-367ca25/lib/redis_client/command_builder.rb000066400000000000000000000035671515654015100250360ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient module CommandBuilder extend self if Symbol.method_defined?(:name) def generate(args, kwargs = nil) command = args.flat_map do |element| case element when Hash element.flatten else element end end kwargs&.each do |key, value| if value if value == true command << key.name else command << key.name << value end end end command.map! do |element| case element when String element when Symbol element.name when Integer, Float element.to_s else raise TypeError, "Unsupported command argument type: #{element.class}" end end if command.empty? raise ArgumentError, "can't issue an empty redis command" end command end else def generate(args, kwargs = nil) command = args.flat_map do |element| case element when Hash element.flatten else element end end kwargs&.each do |key, value| if value if value == true command << key.to_s else command << key.to_s << value end end end command.map! do |element| case element when String element when Integer, Float, Symbol element.to_s else raise TypeError, "Unsupported command argument type: #{element.class}" end end if command.empty? raise ArgumentError, "can't issue an empty redis command" end command end end end end redis-rb-redis-client-367ca25/lib/redis_client/config.rb000066400000000000000000000153571515654015100231570ustar00rootroot00000000000000# frozen_string_literal: true require "openssl" require "uri" class RedisClient class Config DEFAULT_TIMEOUT = 1.0 DEFAULT_HOST = "localhost" DEFAULT_PORT = 6379 DEFAULT_USERNAME = "default" DEFAULT_DB = 0 DEFAULT_IDLE_TIMEOUT = 30.0 module Common attr_reader :db, :id, :ssl, :ssl_params, :command_builder, :inherit_socket, :connect_timeout, :read_timeout, :write_timeout, :driver, :protocol, :middlewares_stack, :custom, :circuit_breaker, :driver_info, :idle_timeout alias_method :ssl?, :ssl def initialize( username: nil, password: nil, db: nil, id: nil, timeout: DEFAULT_TIMEOUT, read_timeout: timeout, write_timeout: timeout, connect_timeout: timeout, idle_timeout: DEFAULT_IDLE_TIMEOUT, ssl: nil, custom: {}, ssl_params: nil, driver: nil, protocol: 3, client_implementation: RedisClient, command_builder: CommandBuilder, inherit_socket: false, reconnect_attempts: false, middlewares: false, circuit_breaker: nil, driver_info: nil ) @username = username @password = password && !password.respond_to?(:call) ? ->(_) { password } : password @db = begin Integer(db || DEFAULT_DB) rescue ArgumentError raise ArgumentError, "db: must be an Integer, got: #{db.inspect}" end @id = id @ssl = ssl || false @ssl_params = ssl_params @connect_timeout = connect_timeout @read_timeout = read_timeout @write_timeout = write_timeout @idle_timeout = idle_timeout @driver = driver ? RedisClient.driver(driver) : RedisClient.default_driver @custom = custom @client_implementation = client_implementation @protocol = protocol unless protocol == 2 || protocol == 3 raise ArgumentError, "Unknown protocol version #{protocol.inspect}, expected 2 or 3" end @command_builder = command_builder @inherit_socket = inherit_socket reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer) @reconnect_attempts = reconnect_attempts circuit_breaker = CircuitBreaker.new(**circuit_breaker) if circuit_breaker.is_a?(Hash) if @circuit_breaker = circuit_breaker middlewares = [CircuitBreaker::Middleware] + (middlewares || []) end middlewares_stack = Middlewares if middlewares && !middlewares.empty? middlewares_stack = Class.new(Middlewares) middlewares.each do |mod| middlewares_stack.include(mod) end end @middlewares_stack = middlewares_stack @driver_info = driver_info end def connection_prelude prelude = [] pass = password if protocol == 3 prelude << if pass ["HELLO", "3", "AUTH", username, pass] else ["HELLO", "3"] end elsif pass prelude << if @username && !@username.empty? ["AUTH", @username, pass] else ["AUTH", pass] end end if @db && @db != 0 prelude << ["SELECT", @db.to_s] end # Add CLIENT SETINFO commands for lib-name and lib-ver # These commands are supported in Redis 7.2+ if @driver_info prelude << ["CLIENT", "SETINFO", "LIB-NAME", build_lib_name] prelude << ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION] end # Deep freeze all the strings and commands prelude.map! do |commands| commands = commands.map { |str| str.frozen? ? str : str.dup.freeze } commands.freeze end prelude.freeze end def password @password&.call(username) end def username @username || DEFAULT_USERNAME end # Build the library name for CLIENT SETINFO LIB-NAME. # # @param driver_info [String, Array, nil] Upstream driver info # @return [String] Library name with optional upstream driver info # @raise [ArgumentError] if driver_info is not a String or Array def build_lib_name return "redis-client" if @driver_info.nil? info = case @driver_info when String then @driver_info when Array then @driver_info.join(";") else raise ArgumentError, "driver_info must be a String or Array of Strings" end return "redis-client" if info.empty? "redis-client(#{info})" end def resolved? true end def sentinel? false end def new_pool(**kwargs) kwargs[:timeout] ||= DEFAULT_TIMEOUT Pooled.new(self, **kwargs) end def new_client(**kwargs) @client_implementation.new(self, **kwargs) end def retriable?(attempt) @reconnect_attempts && @reconnect_attempts[attempt] end def retry_connecting?(attempt, _error) if @reconnect_attempts if (pause = @reconnect_attempts[attempt]) if pause > 0 sleep(pause) end return true end end false end def ssl_context if ssl @ssl_context ||= @driver.ssl_context(@ssl_params || {}) end end def server_url if path url = "unix://#{path}" if db != 0 url = "#{url}?db=#{db}" end else # add brackets to IPv6 address redis_host = if host.count(":") >= 2 "[#{host}]" else host end url = "redis#{'s' if ssl?}://#{redis_host}:#{port}" if db != 0 url = "#{url}/#{db}" end end url end end include Common attr_reader :host, :port, :path, :server_key def initialize( url: nil, host: nil, port: nil, path: nil, username: nil, password: nil, db: nil, **kwargs ) if url url_config = URLConfig.new(url) kwargs = { ssl: url_config.ssl?, }.compact.merge(kwargs) db ||= url_config.db host ||= url_config.host port ||= url_config.port path ||= url_config.path username ||= url_config.username password ||= url_config.password end super(username: username, password: password, db: db, **kwargs) if @path = path @host = nil @port = nil else @host = host || DEFAULT_HOST @port = Integer(port || DEFAULT_PORT) end @server_key = [@path, @host, @port].freeze end end end redis-rb-redis-client-367ca25/lib/redis_client/connection_mixin.rb000066400000000000000000000050271515654015100252460ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient module ConnectionMixin attr_accessor :retry_attempt attr_reader :config def initialize(config) @pending_reads = 0 @retry_attempt = nil @config = config @server_key = nil end def reconnect close connect end def close @pending_reads = 0 nil end def revalidate if @pending_reads > 0 || @server_key != @config.server_key close false else connected? end end def call(command, timeout) @pending_reads += 1 write(command) result = read(connection_timeout(timeout)) @pending_reads -= 1 if result.is_a?(Error) result._set_command(command) result._set_config(config) result._set_retry_attempt(@retry_attempt) raise result else result end end def call_pipelined(commands, timeouts, exception: true) first_exception = nil size = commands.size results = Array.new(commands.size) @pending_reads += size write_multi(commands) size.times do |index| timeout = timeouts && timeouts[index] result = read(connection_timeout(timeout)) @pending_reads -= 1 # A multi/exec command can return an array of results. # An error from a multi/exec command is handled in Multi#_coerce!. if result.is_a?(Array) result.each do |res| res._set_config(config) if res.is_a?(Error) end elsif result.is_a?(Error) result._set_command(commands[index]) result._set_config(config) result._set_retry_attempt(@retry_attempt) first_exception ||= result end results[index] = result end if first_exception && exception raise first_exception else results end end def connection_timeout(timeout) return timeout unless 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 def protocol_error(message) error = ProtocolError.with_config(message, config) error._set_retry_attempt(@retry_attempt) error end def connection_error(message) error = ConnectionError.with_config(message, config) error._set_retry_attempt(@retry_attempt) error end end end redis-rb-redis-client-367ca25/lib/redis_client/decorator.rb000066400000000000000000000043251515654015100236650ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient module Decorator class << self def create(commands_mixin) client_decorator = Class.new(Client) client_decorator.include(commands_mixin) pipeline_decorator = Class.new(Pipeline) pipeline_decorator.include(commands_mixin) client_decorator.const_set(:Pipeline, pipeline_decorator) client_decorator end end module CommandsMixin def initialize(client) @client = client end %i(call call_v call_once call_once_v blocking_call blocking_call_v).each do |method| class_eval(<<~RUBY, __FILE__, __LINE__ + 1) def #{method}(*args, &block) @client.#{method}(*args, &block) end ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true) RUBY end end class Pipeline include CommandsMixin end class Client include CommandsMixin def initialize(_client) super @_pipeline_class = self.class::Pipeline end def with(*args) @client.with(*args) { |c| yield self.class.new(c) } end ruby2_keywords :with if respond_to?(:ruby2_keywords, true) def pipelined(exception: true) @client.pipelined(exception: exception) { |p| yield @_pipeline_class.new(p) } end def multi(**kwargs) @client.multi(**kwargs) { |p| yield @_pipeline_class.new(p) } end %i(close scan hscan sscan zscan).each do |method| class_eval(<<~RUBY, __FILE__, __LINE__ + 1) def #{method}(*args, &block) @client.#{method}(*args, &block) end ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true) RUBY end %i(id config size connect_timeout read_timeout write_timeout pubsub).each do |reader| class_eval(<<~RUBY, __FILE__, __LINE__ + 1) def #{reader} @client.#{reader} end RUBY end %i(timeout connect_timeout read_timeout write_timeout).each do |writer| class_eval(<<~RUBY, __FILE__, __LINE__ + 1) def #{writer}=(value) @client.#{writer} = value end RUBY end end end end redis-rb-redis-client-367ca25/lib/redis_client/hash_ring.rb000066400000000000000000000033031515654015100236400ustar00rootroot00000000000000# frozen_string_literal: true require 'zlib' class RedisClient class HashRing POINTS_PER_SERVER = 160 class << self attr_writer :digest def digest @digest ||= begin require 'digest/md5' Digest::MD5 end end end attr_reader :nodes def initialize(nodes = [], replicas: POINTS_PER_SERVER, digest: self.class.digest) @replicas = replicas @ring = {} @digest = digest ids = {} @nodes = nodes.dup.freeze nodes.each do |node| id = node.id || node.config.server_url if ids[id] raise ArgumentError, "duplicate node id: #{id.inspect}" end ids[id] = true replicas.times do |i| @ring[server_hash_for("#{id}:#{i}".freeze)] = node end end @sorted_keys = @ring.keys @sorted_keys.sort! end # get the node in the hash ring for this key def node_for(key) hash = hash_for(key) idx = binary_search(@sorted_keys, hash) @ring[@sorted_keys[idx]] end def nodes_for(*keys) keys.flatten! mapping = {} keys.each do |key| (mapping[node_for(key)] ||= []) << key end mapping end private def hash_for(key) Zlib.crc32(key) end def server_hash_for(key) @digest.digest(key).unpack1("L>") end # Find the closest index in HashRing with value <= the given value def binary_search(ary, value) upper = ary.size lower = 0 while lower < upper mid = (lower + upper) / 2 if ary[mid] > value upper = mid else lower = mid + 1 end end upper - 1 end end end redis-rb-redis-client-367ca25/lib/redis_client/middlewares.rb000066400000000000000000000005451515654015100242030ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient class BasicMiddleware attr_reader :client def initialize(client) @client = client end def connect(_config) yield end def call(command, _config) yield command end alias_method :call_pipelined, :call end class Middlewares < BasicMiddleware end end redis-rb-redis-client-367ca25/lib/redis_client/pid_cache.rb000066400000000000000000000012741515654015100236020ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient module PIDCache if !Process.respond_to?(:fork) # JRuby or TruffleRuby @pid = Process.pid singleton_class.attr_reader(:pid) elsif Process.respond_to?(:_fork) # Ruby 3.1+ class << self attr_reader :pid def update! @pid = Process.pid end end update! module CoreExt def _fork child_pid = super PIDCache.update! if child_pid == 0 child_pid end end Process.singleton_class.prepend(CoreExt) else # Ruby 3.0 or older class << self def pid Process.pid end end end end end redis-rb-redis-client-367ca25/lib/redis_client/pooled.rb000066400000000000000000000041661515654015100231700ustar00rootroot00000000000000# frozen_string_literal: true require "connection_pool" class RedisClient class Pooled EMPTY_HASH = {}.freeze include Common def initialize( config, id: config.id, connect_timeout: config.connect_timeout, read_timeout: config.read_timeout, write_timeout: config.write_timeout, **kwargs ) super(config, id: id, connect_timeout: connect_timeout, read_timeout: read_timeout, write_timeout: write_timeout) @pool_kwargs = kwargs @pool = new_pool @mutex = Mutex.new end def with(options = EMPTY_HASH) pool.with(**options) do |client| client.connect_timeout = connect_timeout client.read_timeout = read_timeout client.write_timeout = write_timeout yield client end rescue ConnectionPool::TimeoutError => error raise CheckoutTimeoutError, "Couldn't checkout a connection in time: #{error.message}" end alias_method :then, :with def close if @pool @mutex.synchronize do pool = @pool @pool = nil pool&.shutdown(&:close) end end nil end def size pool.size end methods = %w(pipelined multi pubsub call call_v call_once call_once_v blocking_call blocking_call_v) iterable_methods = %w(scan sscan hscan zscan) methods.each do |method| class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) with { |r| r.#{method}(*args, &block) } end ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true) RUBY end iterable_methods.each do |method| class_eval <<~RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) unless block_given? return to_enum(__callee__, *args) end with { |r| r.#{method}(*args, &block) } end ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true) RUBY end private def pool @pool ||= @mutex.synchronize { new_pool } end def new_pool ConnectionPool.new(**@pool_kwargs) { @config.new_client } end end end redis-rb-redis-client-367ca25/lib/redis_client/ruby_connection.rb000066400000000000000000000137621515654015100251100ustar00rootroot00000000000000# frozen_string_literal: true require "socket" require "openssl" require "redis_client/connection_mixin" require "redis_client/ruby_connection/buffered_io" require "redis_client/ruby_connection/resp3" class RedisClient class RubyConnection include ConnectionMixin class << self def ssl_context(ssl_params) params = ssl_params.dup || {} cert = params[:cert] if cert.is_a?(String) cert = File.read(cert) if File.exist?(cert) params[:cert] = OpenSSL::X509::Certificate.new(cert) end key = params[:key] if key.is_a?(String) key = File.read(key) if File.exist?(key) params[:key] = OpenSSL::PKey.read(key) end context = OpenSSL::SSL::SSLContext.new context.set_params(params) if context.verify_mode != OpenSSL::SSL::VERIFY_NONE if context.respond_to?(:verify_hostname) # Missing on JRuby context.verify_hostname end end context end end SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout } def initialize(config, connect_timeout:, read_timeout:, write_timeout:) super(config) @connect_timeout = connect_timeout @read_timeout = read_timeout @write_timeout = write_timeout connect end def connected? !@io.closed? end def close @io.close super end def read_timeout=(timeout) @read_timeout = timeout @io.read_timeout = timeout if @io end def write_timeout=(timeout) @write_timeout = timeout @io.write_timeout = timeout if @io end def write(command) buffer = RESP3.dump(command) begin @io.write(buffer) rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error raise connection_error(error.message) rescue Error => error error._set_config(config) error._set_retry_attempt(@retry_attempt) raise error end end def write_multi(commands) buffer = nil commands.each do |command| buffer = RESP3.dump(command, buffer) end begin @io.write(buffer) rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error raise connection_error(error.message) end end def read(timeout = nil) if timeout.nil? RESP3.load(@io) else @io.with_timeout(timeout) { RESP3.load(@io) } end rescue RedisClient::RESP3::Error => error raise protocol_error(error.message) rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error raise connection_error(error.message) end def measure_round_trip_delay start = RedisClient.now_ms call(["PING"], @read_timeout) RedisClient.now_ms - start end private def connect @server_key = @config.server_key socket = if @config.path UNIXSocket.new(@config.path) else sock = if SUPPORTS_RESOLV_TIMEOUT begin Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout) rescue Errno::ETIMEDOUT => timeout_error timeout_error.message << ": #{@connect_timeout}s" raise end else Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout) end # disables Nagle's Algorithm, prevents multiple round trips with MULTI sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) enable_socket_keep_alive(sock) sock end if @config.ssl socket = OpenSSL::SSL::SSLSocket.new(socket, @config.ssl_context) socket.hostname = @config.host loop do case status = socket.connect_nonblock(exception: false) when :wait_readable socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError.with_config("", config) when :wait_writable socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError.with_config("", config) when socket break else raise "Unexpected `connect_nonblock` return: #{status.inspect}" end end end @io = BufferedIO.new( socket, read_timeout: @read_timeout, write_timeout: @write_timeout, ) true rescue SystemCallError, IOError, OpenSSL::SSL::SSLError, SocketError => error socket&.close raise CannotConnectError, error.message, error.backtrace end KEEP_ALIVE_INTERVAL = 15 # Same as hiredis defaults KEEP_ALIVE_TTL = 120 # Longer than hiredis defaults KEEP_ALIVE_PROBES = (KEEP_ALIVE_TTL / KEEP_ALIVE_INTERVAL) - 1 private_constant :KEEP_ALIVE_INTERVAL private_constant :KEEP_ALIVE_TTL private_constant :KEEP_ALIVE_PROBES if %i[SOL_TCP SOL_SOCKET TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } # Linux def enable_socket_keep_alive(socket) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEP_ALIVE_TTL) socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEP_ALIVE_INTERVAL) socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEP_ALIVE_PROBES) end elsif %i[IPPROTO_TCP TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } # macOS def enable_socket_keep_alive(socket) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, KEEP_ALIVE_INTERVAL) socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, KEEP_ALIVE_PROBES) end elsif %i[SOL_SOCKET SO_KEEPALIVE].all? { |c| Socket.const_defined? c } # unknown POSIX def enable_socket_keep_alive(socket) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) end else # unknown def enable_socket_keep_alive(_socket) end end end end redis-rb-redis-client-367ca25/lib/redis_client/ruby_connection/000077500000000000000000000000001515654015100245525ustar00rootroot00000000000000redis-rb-redis-client-367ca25/lib/redis_client/ruby_connection/buffered_io.rb000066400000000000000000000152661515654015100273620ustar00rootroot00000000000000# frozen_string_literal: true require "io/wait" unless IO.method_defined?(:wait_readable) && IO.method_defined?(:wait_writable) class RedisClient class RubyConnection class BufferedIO EOL = "\r\n".b.freeze EOL_SIZE = EOL.bytesize attr_accessor :read_timeout, :write_timeout if String.method_defined?(:byteindex) # Ruby 3.2+ ENCODING = Encoding::UTF_8 def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096) @io = io @buffer = +"" @offset = 0 @chunk_size = chunk_size @read_timeout = read_timeout @write_timeout = write_timeout @blocking_reads = false end def gets_chomp fill_buffer(false) if @offset >= @buffer.bytesize until eol_index = @buffer.byteindex(EOL, @offset) fill_buffer(false) end line = @buffer.byteslice(@offset, eol_index - @offset) @offset = eol_index + EOL_SIZE line end def read_chomp(bytes) ensure_remaining(bytes + EOL_SIZE) str = @buffer.byteslice(@offset, bytes) @offset += bytes + EOL_SIZE str end private def ensure_line fill_buffer(false) if @offset >= @buffer.bytesize until @buffer.byteindex(EOL, @offset) fill_buffer(false) end end else ENCODING = Encoding::BINARY def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096) @io = io @buffer = "".b @offset = 0 @chunk_size = chunk_size @read_timeout = read_timeout @write_timeout = write_timeout @blocking_reads = false end def gets_chomp fill_buffer(false) if @offset >= @buffer.bytesize until eol_index = @buffer.index(EOL, @offset) fill_buffer(false) end line = @buffer.byteslice(@offset, eol_index - @offset) @offset = eol_index + EOL_SIZE line end def read_chomp(bytes) ensure_remaining(bytes + EOL_SIZE) str = @buffer.byteslice(@offset, bytes) @offset += bytes + EOL_SIZE str.force_encoding(Encoding::UTF_8) end private def ensure_line fill_buffer(false) if @offset >= @buffer.bytesize until @buffer.index(EOL, @offset) fill_buffer(false) end end end def close @io.to_io.close end def closed? @io.to_io.closed? end def eof? @offset >= @buffer.bytesize && @io.eof? end def with_timeout(new_timeout) new_timeout = false if new_timeout == 0 previous_read_timeout = @read_timeout previous_blocking_reads = @blocking_reads if new_timeout @read_timeout = new_timeout else @blocking_reads = true end begin yield ensure @read_timeout = previous_read_timeout @blocking_reads = previous_blocking_reads end end def skip(offset) ensure_remaining(offset) @offset += offset nil end def write(string) total = remaining = string.bytesize loop do case bytes_written = @io.write_nonblock(string, exception: false) when Integer remaining -= bytes_written if remaining > 0 string = string.byteslice(bytes_written..-1) else return total end when :wait_readable @io.to_io.wait_readable(@read_timeout) or raise(ReadTimeoutError, "Waited #{@read_timeout} seconds") when :wait_writable @io.to_io.wait_writable(@write_timeout) or raise(WriteTimeoutError, "Waited #{@write_timeout} seconds") when nil raise Errno::ECONNRESET else raise "Unexpected `write_nonblock` return: #{bytes.inspect}" end end end def getbyte unless byte = @buffer.getbyte(@offset) ensure_remaining(1) byte = @buffer.getbyte(@offset) end @offset += 1 byte end def gets_integer int = 0 offset = @offset while true chr = @buffer.getbyte(offset) if chr if chr == 13 # "\r".ord @offset = offset + 2 break else int = (int * 10) + chr - 48 end offset += 1 else ensure_line return gets_integer end end int end private def ensure_remaining(bytes) needed = bytes - (@buffer.bytesize - @offset) if needed > 0 fill_buffer(true, needed) end end def fill_buffer(strict, size = @chunk_size) remaining = size buffer_size = @buffer.bytesize start = @offset - buffer_size empty_buffer = start >= 0 loop do bytes = if empty_buffer @io.read_nonblock([remaining, @chunk_size].max, @buffer, exception: false) else @io.read_nonblock([remaining, @chunk_size].max, exception: false) end case bytes when :wait_readable # Ref: https://github.com/redis-rb/redis-client/issues/190 # SSLSocket always clear the provided buffer, even when it didn't # read anything. So we need to reset the offset accordingly. if empty_buffer && @buffer.empty? @offset -= buffer_size end unless @io.to_io.wait_readable(@read_timeout) raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads end when :wait_writable # Ref: https://github.com/redis-rb/redis-client/issues/190 # SSLSocket always clear the provided buffer, even when it didn't # read anything. So we need to reset the offset accordingly. if empty_buffer && @buffer.empty? @offset -= buffer_size end @io.to_io.wait_writable(@write_timeout) or raise(WriteTimeoutError, "Waited #{@write_timeout} seconds") when nil raise EOFError else if empty_buffer @offset = start empty_buffer = false @buffer.force_encoding(ENCODING) unless @buffer.encoding == ENCODING else @buffer << bytes.force_encoding(ENCODING) end remaining -= bytes.bytesize return if !strict || remaining <= 0 end end end end end end redis-rb-redis-client-367ca25/lib/redis_client/ruby_connection/resp3.rb000066400000000000000000000122131515654015100261320ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient module RESP3 module_function Error = Class.new(RedisClient::Error) UnknownType = Class.new(Error) SyntaxError = Class.new(Error) EOL = "\r\n".b.freeze EOL_SIZE = EOL.bytesize DUMP_TYPES = { String => :dump_string, Symbol => :dump_symbol, Integer => :dump_numeric, Float => :dump_numeric, }.freeze PARSER_TYPES = { '#' => :parse_boolean, '$' => :parse_blob, '+' => :parse_string, '=' => :parse_verbatim_string, '-' => :parse_error, ':' => :parse_integer, '(' => :parse_integer, ',' => :parse_double, '_' => :parse_null, '*' => :parse_array, '%' => :parse_map, '~' => :parse_set, '>' => :parse_array, }.transform_keys(&:ord).freeze INTEGER_RANGE = ((((2**64) / 2) * -1)..(((2**64) / 2) - 1)).freeze def dump(command, buffer = nil) buffer ||= new_buffer command = command.flat_map do |element| case element when Hash element.flatten else element end end dump_array(command, buffer) end def load(io) parse(io) end def new_buffer String.new(encoding: Encoding::BINARY, capacity: 127) end def dump_any(object, buffer) method = DUMP_TYPES.fetch(object.class) do |unexpected_class| if superclass = DUMP_TYPES.keys.find { |t| t > unexpected_class } DUMP_TYPES[superclass] else raise TypeError, "Unsupported command argument type: #{unexpected_class}" end end send(method, object, buffer) end def dump_array(array, buffer) buffer << '*' << array.size.to_s << EOL array.each do |item| dump_any(item, buffer) end buffer end def dump_set(set, buffer) buffer << '~' << set.size.to_s << EOL set.each do |item| dump_any(item, buffer) end buffer end def dump_hash(hash, buffer) buffer << '%' << hash.size.to_s << EOL hash.each_pair do |key, value| dump_any(key, buffer) dump_any(value, buffer) end buffer end def dump_numeric(numeric, buffer) dump_string(numeric.to_s, buffer) end def dump_string(string, buffer) string = string.b unless string.ascii_only? buffer << '$' << string.bytesize.to_s << EOL << string << EOL end if Symbol.method_defined?(:name) def dump_symbol(symbol, buffer) dump_string(symbol.name, buffer) end else def dump_symbol(symbol, buffer) dump_string(symbol.to_s, buffer) end end def parse(io) type = io.getbyte if type == 35 # '#'.ord parse_boolean(io) elsif type == 36 # '$'.ord parse_blob(io) elsif type == 43 # '+'.ord parse_string(io) elsif type == 61 # '='.ord parse_verbatim_string(io) elsif type == 45 # '-'.ord parse_error(io) elsif type == 58 # ':'.ord parse_integer(io) elsif type == 40 # '('.ord parse_integer(io) elsif type == 44 # ','.ord parse_double(io) elsif type == 95 # '_'.ord parse_null(io) elsif type == 42 # '*'.ord parse_array(io) elsif type == 37 # '%'.ord parse_map(io) elsif type == 126 # '~'.ord parse_set(io) elsif type == 62 # '>'.ord parse_array(io) else raise UnknownType, "Unknown sigil type: #{type.chr.inspect}" end end def parse_string(io) str = io.gets_chomp str.force_encoding(Encoding::BINARY) unless str.valid_encoding? str.freeze end def parse_error(io) CommandError.parse(parse_string(io)) end def parse_boolean(io) case value = io.gets_chomp when "t" true when "f" false else raise SyntaxError, "Expected `t` or `f` after `#`, got: #{value}" end end def parse_array(io) parse_sequence(io, io.gets_integer) end def parse_set(io) parse_sequence(io, io.gets_integer) end def parse_map(io) hash = {} io.gets_integer.times do hash[parse(io).freeze] = parse(io) end hash end def parse_push(io) parse_array(io) end def parse_sequence(io, size) return if size < 0 # RESP2 nil array = Array.new(size) size.times do |index| array[index] = parse(io) end array end def parse_integer(io) Integer(io.gets_chomp) end def parse_double(io) case value = io.gets_chomp when "nan" Float::NAN when "inf" Float::INFINITY when "-inf" -Float::INFINITY else Float(value) end end def parse_null(io) io.skip(EOL_SIZE) nil end def parse_blob(io) bytesize = io.gets_integer return if bytesize < 0 # RESP2 nil type str = io.read_chomp(bytesize) str.force_encoding(Encoding::BINARY) unless str.valid_encoding? str end def parse_verbatim_string(io) blob = parse_blob(io) blob.byteslice(4..-1) end end end redis-rb-redis-client-367ca25/lib/redis_client/sentinel_config.rb000066400000000000000000000145051515654015100250520ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient class SentinelConfig include Config::Common SENTINEL_DELAY = 0.25 DEFAULT_RECONNECT_ATTEMPTS = 2 attr_reader :name def initialize( sentinels:, sentinel_password: nil, sentinel_username: nil, role: :master, name: nil, url: nil, **client_config ) unless %i(master replica slave).include?(role.to_sym) raise ArgumentError, "Expected role to be either :master or :replica, got: #{role.inspect}" end # Track whether SSL was explicitly provided by user ssl_explicitly_set = client_config.key?(:ssl) if url url_config = URLConfig.new(url) client_config = { username: url_config.username, password: url_config.password, db: url_config.db, ssl: url_config.ssl?, }.compact.merge(client_config) name ||= url_config.host end @name = name unless @name raise ArgumentError, "RedisClient::SentinelConfig requires either a name or an url with a host" end @to_list_of_hash = @to_hash = nil password = if sentinel_password && !sentinel_password.respond_to?(:call) ->(_) { sentinel_password } else sentinel_password end @extra_config = { username: sentinel_username, password: password, db: nil, } if client_config[:protocol] == 2 @extra_config[:protocol] = client_config[:protocol] @to_list_of_hash = lambda do |may_be_a_list| if may_be_a_list.is_a?(Array) may_be_a_list.map { |l| l.each_slice(2).to_h } else may_be_a_list end end end @sentinels = {}.compare_by_identity @role = role.to_sym @mutex = Mutex.new @config = nil client_config[:reconnect_attempts] ||= DEFAULT_RECONNECT_ATTEMPTS @client_config = client_config || {} @sentinel_client_config = @client_config.dup # Only remove SSL from sentinel config if it was derived from URL, # not if explicitly set by user. @sentinel_client_config.delete(:ssl) unless ssl_explicitly_set super(**client_config) @sentinel_configs = sentinels_to_configs(sentinels) end def sentinels @mutex.synchronize do @sentinel_configs.dup end end def reset @mutex.synchronize do @config = nil end end def server_key config.server_key end def host config.host end def port config.port end def path nil end def retry_connecting?(attempt, error) reset unless error.is_a?(TimeoutError) super end def sentinel? true end def check_role!(role) if @role == :master unless role == "master" sleep SENTINEL_DELAY raise FailoverError, "Expected to connect to a master, but the server is a replica" end else unless role == "slave" sleep SENTINEL_DELAY raise FailoverError, "Expected to connect to a replica, but the server is a master" end end end def resolved? @mutex.synchronize do !!@config end end private def sentinels_to_configs(sentinels) sentinels.map do |sentinel| case sentinel when String Config.new(**@sentinel_client_config, **@extra_config, url: sentinel) else Config.new(**@sentinel_client_config, **@extra_config, **sentinel) end end end def config @mutex.synchronize do @config ||= if @role == :master resolve_master else resolve_replica end end end def resolve_master each_sentinel do |sentinel_client| host, port = sentinel_client.call_v(["SENTINEL", "get-master-addr-by-name", @name]) next unless host && port refresh_sentinels(sentinel_client) return Config.new(host: host, port: Integer(port), **@client_config) end rescue ConnectionError raise ConnectionError, "No sentinels available" else raise ConnectionError, "Couldn't locate a replica for role: #{@name}" end def sentinel_client(sentinel_config) @sentinels[sentinel_config] ||= sentinel_config.new_client end def resolve_replica each_sentinel do |sentinel_client| replicas = sentinel_client.call_v(["SENTINEL", "replicas", @name], &@to_list_of_hash) replicas.reject! do |r| flags = r["flags"].to_s.split(",") flags.include?("s_down") || flags.include?("o_down") end next if replicas.empty? replica = replicas.sample return Config.new(host: replica["ip"], port: Integer(replica["port"]), **@client_config) end rescue ConnectionError raise ConnectionError, "No sentinels available" else raise ConnectionError, "Couldn't locate a replica for role: #{@name}" end def each_sentinel last_error = nil @sentinel_configs.dup.each do |sentinel_config| sentinel_client = sentinel_client(sentinel_config) success = true begin yield sentinel_client rescue RedisClient::Error => error last_error = error success = false sleep SENTINEL_DELAY ensure if success @sentinel_configs.unshift(@sentinel_configs.delete(sentinel_config)) end # Redis Sentinels may be configured to have a lower maxclients setting than # the Redis nodes. Close the connection to the Sentinel node to avoid using # a connection. sentinel_client.close end end raise last_error if last_error end def refresh_sentinels(sentinel_client) sentinel_response = sentinel_client.call_v(["SENTINEL", "sentinels", @name], &@to_list_of_hash) sentinels = sentinel_response.map do |sentinel| { host: sentinel.fetch("ip"), port: Integer(sentinel.fetch("port")) } end new_sentinels = sentinels.select do |sentinel| @sentinel_configs.none? do |sentinel_config| sentinel_config.host == sentinel.fetch(:host) && sentinel_config.port == sentinel.fetch(:port) end end @sentinel_configs.concat sentinels_to_configs(new_sentinels) end end end redis-rb-redis-client-367ca25/lib/redis_client/url_config.rb000066400000000000000000000026521515654015100240330ustar00rootroot00000000000000# frozen_string_literal: true require "uri" class RedisClient class URLConfig attr_reader :url, :uri def initialize(url) @url = url @uri = URI(url) @unix = false @ssl = false case uri.scheme when "redis" # expected when "rediss" @ssl = true when "unix", nil @unix = true else raise ArgumentError, "Unknown URL scheme: #{url.inspect}" end end def ssl? @ssl end def db unless @unix db_path = uri.path&.delete_prefix("/") return Integer(db_path) if db_path && !db_path.empty? end unless uri.query.nil? || uri.query.empty? _, db_query = URI.decode_www_form(uri.query).find do |key, _| key == "db" end return Integer(db_query) if db_query && !db_query.empty? end end def username uri.user if uri.password && !uri.user.empty? end def password if uri.user && !uri.password URI.decode_www_form_component(uri.user) elsif uri.user && uri.password URI.decode_www_form_component(uri.password) end end def host return if uri.host.nil? || uri.host.empty? uri.host.sub(/\A\[(.*)\]\z/, '\1') end def path if @unix File.join(*[uri.host, uri.path].compact) end end def port return unless uri.port Integer(uri.port) end end end redis-rb-redis-client-367ca25/lib/redis_client/version.rb000066400000000000000000000001121515654015100233560ustar00rootroot00000000000000# frozen_string_literal: true class RedisClient VERSION = "0.28.0" end redis-rb-redis-client-367ca25/redis-client.gemspec000066400000000000000000000022601515654015100220670ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/redis_client/version" Gem::Specification.new do |spec| spec.name = "redis-client" spec.version = RedisClient::VERSION spec.authors = ["Jean Boussier"] spec.email = ["jean.boussier@gmail.com"] spec.summary = "Simple low-level client for Redis 6+" spec.homepage = "https://github.com/redis-rb/redis-client" spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = File.join(spec.homepage, "blob/master/CHANGELOG.md") # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. gemspec = File.basename(__FILE__) spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject do |f| (f == gemspec) || f.start_with?(*%w[bin/ hiredis-client/ test/ benchmark/ .git .rubocop Gemfile Rakefile]) end end spec.require_paths = ["lib"] spec.add_runtime_dependency "connection_pool" end redis-rb-redis-client-367ca25/test/000077500000000000000000000000001515654015100171175ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/env.rb000066400000000000000000000006131515654015100202340ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift File.expand_path("../lib", __dir__) $LOAD_PATH.unshift File.expand_path("../hiredis-client/lib", __dir__) require "redis-client" require "redis_client/decorator" require "toxiproxy" require "stringio" Dir[File.join(__dir__, "support/**/*.rb")].sort.each { |f| require f } Dir[File.join(__dir__, "shared/**/*.rb")].sort.each { |f| require f } redis-rb-redis-client-367ca25/test/fixtures/000077500000000000000000000000001515654015100207705ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/fixtures/certs/000077500000000000000000000000001515654015100221105ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/fixtures/certs/ca.crt000066400000000000000000000035471515654015100232160ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFSzCCAzOgAwIBAgIUeyNhqaA/DqUivAK1BILqZO8HDWcwDQYJKoZIhvcNAQEL BQAwNTETMBEGA1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUg QXV0aG9yaXR5MB4XDTI1MDQyMTIzNDYyMFoXDTM1MDQxOTIzNDYyMFowNTETMBEG A1UECgwKUmVkaXMgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5 MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr8kvD8PpZ0jR6YcVQ1BO qIL8/x1rpchaXroFrborZUPJhoaPhNCHZEbz+rku699XeF1M52F8Gx6UGpA7sz24 4Fw3EUgLb1Dqg+/cZJ+RX88gdHwdAOK5rVHkv1td1VVxdk48lHpJobbk6oZvLLHV wrEJqGRfz4d8MSd8yKas0967mDhVfGR+ul0Ty330WoLyS3qwni35BEEGDzL2Kk+8 IdAEmLQJvIb9beTDvEqykFJzrzm7HdgKpj7QFrgMg838OJOJPtrsJj0oYMOL9ImF DubCAhIkiMX6e1ONRFVF9Pj95dzsG2TwUdktg/vZyzSic168EeURkiZhN+d5vdMY 56Xm+QONh6V/ncPEyXdMbi8bQI681OJj9IarO/9OXZ5sb/aZRqwzLrtt/ua4Ex5u uHOtmnTjh4ahA9AtCV7g908EmRltUClN2D8HVnxTUWAkdVDghMAIAGvW6Ryay1aE Nb6cd8tqusZu5VW1I/8oTzFsxBvUoofOQJYlh44ijjNPp3Or8lH38X5Xm3oPeugd 9gbbH3EYE/UyfTtaaZ2RZD50dWUeHbr0WP9JwZrSePUGGlURnywgOvuyjswes+RW xQhPoNOV1pI0llspmDkRbFARgmgjrXd25WhvlPkZB1/mN1APWg7Uyfh8YU8gRoPj GoKBLEGXN2ZvYaOJVcdJeBECAwEAAaNTMFEwHQYDVR0OBBYEFFuraL4Ktd4MW4Xk eyLhrO1u6PoEMB8GA1UdIwQYMBaAFFuraL4Ktd4MW4XkeyLhrO1u6PoEMA8GA1Ud EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAJtMsTigQK0MI4EwFQTCioHX vQSfr+ddd5JmFMqCGW9D+nd+NFVxATsZyocawbHGx9oFWySjZ/PPNpRnnVC4o/+c Zyywj/RtxayMH+8irdIqBmuwd4D2grI00huL+AZfiM3zW4WYxklosav6i63MNtOs 6u+wfaBJqllYql6EAEO4zIirPvmj/xuZGfzlqPwAXHed2Zkv95WYL9A3BeufPxHO l/800cOtO3l8Exmc2JfsingLEMZJ3kSt7mdf93iPEl/drlmfjbuvL4zpzoU1MzrT oWo7/ft4rNQ50KM80wmhjkV9+dyDLYxL1hdQDQuEnEt11lUXc2xzi/IROtS2sxXe lUbKpL45SeQQxQh0Qf7v65WET3h0ekOj6BRrPik90xK1bWMoolyva6EVwNI3EEAg zqo/x8ZwtLPTjE8AfYLM0in4/kI+vQTv3iZPcUkCMpi5l1N6F16UFnDurU5w4dd+ vfgGq61MZGmRQhV0csvEu8fWv/wPdEPtVtcUlIQBytb0/m9+vP+6+FEYzmPXOvRq t8sYwNHtvSmdta/Cz/udNK5D268luh7uFGf9kU4j0uH0+Q9HADpcAGc8P4DRxlAU hZDkNpR+Kfov+15L6O2S32p2RzegIRxRsvB8XGREW+JpsxDX9Q322BCdsKQGGMQN eWXLxteYx0NZkz9iSWnB -----END CERTIFICATE----- redis-rb-redis-client-367ca25/test/fixtures/certs/ca.key000066400000000000000000000062531515654015100232130ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAr8kvD8PpZ0jR6YcVQ1BOqIL8/x1rpchaXroFrborZUPJhoaP hNCHZEbz+rku699XeF1M52F8Gx6UGpA7sz244Fw3EUgLb1Dqg+/cZJ+RX88gdHwd AOK5rVHkv1td1VVxdk48lHpJobbk6oZvLLHVwrEJqGRfz4d8MSd8yKas0967mDhV fGR+ul0Ty330WoLyS3qwni35BEEGDzL2Kk+8IdAEmLQJvIb9beTDvEqykFJzrzm7 HdgKpj7QFrgMg838OJOJPtrsJj0oYMOL9ImFDubCAhIkiMX6e1ONRFVF9Pj95dzs G2TwUdktg/vZyzSic168EeURkiZhN+d5vdMY56Xm+QONh6V/ncPEyXdMbi8bQI68 1OJj9IarO/9OXZ5sb/aZRqwzLrtt/ua4Ex5uuHOtmnTjh4ahA9AtCV7g908EmRlt UClN2D8HVnxTUWAkdVDghMAIAGvW6Ryay1aENb6cd8tqusZu5VW1I/8oTzFsxBvU oofOQJYlh44ijjNPp3Or8lH38X5Xm3oPeugd9gbbH3EYE/UyfTtaaZ2RZD50dWUe Hbr0WP9JwZrSePUGGlURnywgOvuyjswes+RWxQhPoNOV1pI0llspmDkRbFARgmgj rXd25WhvlPkZB1/mN1APWg7Uyfh8YU8gRoPjGoKBLEGXN2ZvYaOJVcdJeBECAwEA AQKCAgANre5Hn8tOClCrh6OT9W/plSfzAmsaH5lIvdkrR82Qt9G68kXA5CllGFBs NnT8TgkUiM4vQ1rREXQdDRRYQnlcnFB8u8qIAxf85HGWMwSxHAE+j1oCc6JXZoQS kB2hOGD3/+ae91U7jGwMBCIqrDwiRnyl6gm6sKRtftErHC1e33phwiCE6Z0jC5M6 xrZ5RK9uSEHuTU2Pky/Rhvm5GTNevj1dVMVdMnQOVTsWMAntST1PaYKyM9nATisL WY8/wovaK0EG7yppX2EBBrahdQIxwqteVeMZ8a4oYrwfkTM8eRPpC2QkTZqWA/yS xMqSEaqCp+Ci09ymLu5p102WBBNv8mSSVGhzpnbI8p7qucSOez5kIckTUssBlMYG UHHaFrODQVmdabLxseE6wTEP7KI31aFyqcGbCU+x9y4oOWHRITXlB0u2qUbocqD+ tjAWforqj6t/zNbKprJBt5S86PGExWtumTcy11UdYdzUNhfhHAfBCsouoxByzK3B s5FLy/I7pNmq4E2cj7Rmm69JnGg3t4s+/buUj/oo2YBA401BFeMCv+ZX8sI8sf4B 0ypf0ngUT6TrVMBCe9UHYE0FP4p+j7mfOPMQVXvXn0F1XKWYs8JAWok+/T4jdfNF EebVr1RXFEc+ihFcOS4c30v7Zg4Qf2qx7Ui8xHHv6EpFXab9MQKCAQEA2wZswDq9 ZJlc+3+yuy33WUa46tA+IW9YeWPxkf48FSLGLUCAg71BzMPHuWl8aJIsHvtQOZwR Aey8nvr0jyfvU2kTM/1Wf01qX63fGOgU7mqj8bRnlPb/bTe75vyW0ZK0HzRT1JVp IhaT/g2YKGYRyWFXHLfpsrA/+q2gNp+/FmPDMcv1KPLOgu5Q/DHDJdt48WSZAAq0 9+7+QocXLxvFZVyiiFYex9hEMNSnQUHGwaMDQLdc83AEO0f/8Nh440ouYEWkhMia 2ccppsXePda8Vb8FHz5Is3YMTngqOOucDHTfFyoXt10K74jnA5O2EpN6WrxMhFNb DiN1ybEiBGghrwKCAQEAzXYYj+IlqjBfOyATsE8Kn1+0aAejnEu7LzsWs5RFP4T/ Rv57s5YU2NVaMVT1MdazsTGSjcm7cRdNEkTIvL8hITQBuGyqGpAaq6zs42D2PQyv tgItNshYdj5guAEqzBmTvx7acdLoGmiLnwXPjqwxkI9Jh+KfrACYJJN+EHvQQVzA w7dxt41tAWLzWNOdEeEaG/Cs8WzURQH/sRcRMut2IcWrw6DZv8zMgzp7q+B1Uhyt FEpyujb6gwAunTZKb7G6fEJCDTsy28Gy0trCtc9mn57II0Tj8B6HYLTOR+mTLGOS BF/MA4uCRUUYCnO7gk3kJZnWtH9kBAg9QfE3lHsyPwKCAQEAmYq0fEHxeW+F3o1T x6JerwhEI+CeXaQH+vlUZQs8JXj+QsTgEvp/AUQSZGmNnGU0Zve48tn0lkvWowC8 pwrQ3MFhg+XKWG3172MdbgFsgwLhMVVN9AD/aRpUMIbMV9inSuTNC88+J3Z6gvQW weNj/q+teOV5ABpMj7heA00TkWeYc/VORUmJ+gGFZnagHo7wBxGFrKDU4qZ5Ojwn xY+LXxaEnlz5MRHsI+s/4SBybFaRtjGVCNdzL/e2danbfUMIpdbMkYVsANV83nwB 44oA93904MUyBBTyZaQZvVN2Tskzh7Krc5DXVLq9cCWB0x3t/WPZpD4nLA8xyJXT ihFR4wKCAQBm86EVH+VtpPVjBAy5kLGq8GLOqd1CqPPvk7UpYMdeL79WjJfhgfeK O0YJaB/AzGuYA1YDNC13Woyk9dB8O21XXN7r0Y9e5gxnL6w3t1NLffrhwa90PumM vm7qZLNUOBC/eK9Oz7a73NzxXxEE1aW0YQggTd9iaZ3S3hESI2hUCC0TJO2XYwdW 5YU4YjjXR6s0iuGty7GFrp231+4nTLMR8yHBUe0qXW2w5/ImSr+e2H2lqDRauMfI MqQo5JZh53WhY/YC+UHfuexoGXPtdDJhE0gH3DI3FKUTQSYIBLNZT79P78yjjhlF qnyEaD9x6KPEb5SVNywflR1U2JDYFu8zAoIBAGM1qd05fz15r+FcjJ5vIyAdf9Sn t6bY77jpUfB+JE2v/ZU+GbtntpijYQgHexuPhegvd1neAoY9lLd0yIqNP6BtZ8IT aBUTiC1MPL9SSv6f2tTfgG9ZqVPmAk4nnGH8AyXItWzGLOdebKrM44ZyHfzR5YbF Fwr1MAsakzUuxeXLOfYhRugAl6p9PblrvMWZsmBpqeyhq0dSWz8t/8jp/l4Oo9bv JgrK/dolVU+sBIudgCl1ouzl8QzQpoo21yiVvfdsHFfDcIiayqTWfiAFWwypKil+ eFpXdXYRqoKjzNo0S2bSmzCaCmHxo7Kf39Rh6cOwfFwgRIlgO3Odyi6DZ0Y= -----END RSA PRIVATE KEY----- redis-rb-redis-client-367ca25/test/fixtures/certs/ca.txt000066400000000000000000000000211515654015100232250ustar00rootroot00000000000000E8E8B1291EDD3341 redis-rb-redis-client-367ca25/test/fixtures/certs/client.crt000066400000000000000000000030011515654015100240720ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEQzCCAiugAwIBAgIJAOjosSke3TNAMA0GCSqGSIb3DQEBCwUAMDUxEzARBgNV BAoMClJlZGlzIFRlc3QxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTAe Fw0yNTA0MjEyMzQ2MjBaFw0yNjA0MjEyMzQ2MjBaMCkxEzARBgNVBAoMClJlZGlz IFRlc3QxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAKXpCD+S8fpwUIoPqucA1HNhH0lbtbwxFskD+aY1q155k/skIs5l 56E0kjPukM54vaDzG7RLiZUsgGk1u7GuwTO3L8LDyE/yFbjdslUdCzSHgf3cuvMB 4mhZNwFTxmWnlhQ0NJbDX6g060M0zzgY6rjuyfAfGZ3rcvaoRgYIzQCjdni0IQBU Z0hVPj9GLVSNMiXR5OyTe/VZoBJQkeb7NwAAUPme/c3BmDfHkeSlitCsdltoIoI4 a72u6ShgOqLqT76kvPXIqoxhF83B2Sjk204GMLg3D56Sm6aU4O+hrZku4ngSCiL1 dkrWMi8nVFIDATSgk/ClwmW4URahus0QbjsCAwEAAaNiMGAwCwYDVR0PBAQDAgWg MBEGCWCGSAGG+EIBAQQEAwIHgDAdBgNVHQ4EFgQU+Ew8a/ddSCqaqmMZPwtv7enK 8QEwHwYDVR0jBBgwFoAUW6tovgq13gxbheR7IuGs7W7o+gQwDQYJKoZIhvcNAQEL BQADggIBABcFkuloL/JFgfHMWI30e/3vf+IVGvdurMKvdXn0UwVdavVYUAF+dgfI srYx6V809psuI1HR0NyU8HsQ0AXGm90SmHIKYcms5x6KmZRdmkw8CzIOwS3C9sCk DjowvfRaH59tZ5cKn019J2kWJCiKEih5LcyWEBYQxsUhqssq6MxsAKzgQr83XhLT YRLK4fat23IoGiyuPiIkiGRcB215VZinXN75Ulc7wCnGdFpQ/m8E9v8RQeZSJ9EY 7kozMmyAeoWv8fNcyK2kgQqSxRS7EElQ8Jwr0AmzqYkU4CIxmgwFS/1jYsmfO/H3 W/Gc5u1uu3HwR6dm7bxjPY8GcUn+FnU0u3Ph2/uGFveyg9I4ja2QrlF02mxaEkr7 jDINUu7p2VU0rWlE0meQD+OLdtDxV8LU89y3OEkqQYlAtgukPRriKf3i1qw9C68x +eiWO12hPJYmlX1IhqpKSin3zTQoIj7YPX/LmLdhsu/azrUZ2h34rBEsLBK8RTbw qFaKRUj5J/mI2UXLeHsCGqwgpewvFGjQU+BqwP1p4CEz5TH7oKWpDZTMZiMdcr8M xGWuoP6GdzZiUrrIzZUnfFrpWqjMLm+mFGhwlVNIpDshgZU9T7QUYAjIo1YGQMVz BBJcm3PLdUX4UNuxZuSVWP8MLc6hfPRIrIG4JE82bkfij6+RKpR3 -----END CERTIFICATE----- redis-rb-redis-client-367ca25/test/fixtures/certs/client.key000066400000000000000000000032131515654015100240770ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEApekIP5Lx+nBQig+q5wDUc2EfSVu1vDEWyQP5pjWrXnmT+yQi zmXnoTSSM+6Qzni9oPMbtEuJlSyAaTW7sa7BM7cvwsPIT/IVuN2yVR0LNIeB/dy6 8wHiaFk3AVPGZaeWFDQ0lsNfqDTrQzTPOBjquO7J8B8Znety9qhGBgjNAKN2eLQh AFRnSFU+P0YtVI0yJdHk7JN79VmgElCR5vs3AABQ+Z79zcGYN8eR5KWK0Kx2W2gi gjhrva7pKGA6oupPvqS89ciqjGEXzcHZKOTbTgYwuDcPnpKbppTg76GtmS7ieBIK IvV2StYyLydUUgMBNKCT8KXCZbhRFqG6zRBuOwIDAQABAoIBAHGZ0GYHbdy3Ts5Z 0AGAVffyxoNqYlPLoPhe2m/uS7rSsHrD0XlV2XZOEtWwQkK99cng7FVVa41S/VIM 0snlCLEqe292sw/aiPkeA9+3lVaQeneizfdakPY2MC2eeThduat325JnkHYSVgyc ek7E8ONTzb227clt0DgIHHpBSG1oZRhMLeRyHZACdoTVzdskBaIyyrNLRFwo0qCm YxPiuDSZLYgcuQ6U3m4N6oo8Sgy5eHjmYhbq7CYJ9InpANveZPZXUdz4b2LMpTMF uRGtCNSQ+VSG7nDX9TbkSWq2IbsAJh+KnX6vKBjbecpPu7p0DEa6iprSkxhVVUSX tsDCV+ECgYEA2s1ixwodPaGHTheXHAxEG/XQ5S8ADT8MlVYZJ5pixjj+skBjE5jr 2XaLxWQsljsydAcukau/YAOfljIVGhB88FO89ePAdVdaFHnof7iTAiIZ/wHPixVS PNDSFZ0CnJjJueEJ/3lwKNPT8a/pxCdu2WbLHuWfpoJJWBH7opdMxlkCgYEAwh20 VGbUVOTLGBTmz71Q1Fc6ZWxrMT1ipcAxUvEi4rgBjQIhG1RH00C5UFchXH91ZIqz kHcaxceaRLZMhrfg0q+UM0F7wIkg2Dk2pWtmyy9+t+2Ot0yeLk36A/pNHobqn4Pq eR+tFXdDIqaBCj/TLbuP6zHDukXln+5zh8hz7rMCgYAM51nw9Ra+YL1TDK8bt1l4 8KlOKtRs84/xaq93F3LFz+ytZICzUixumcAqdvruLTWPhDDp5GAX8H7D49Y8wEYu AjI9qh3ajblBReNBTQhWct5nnJq50BsWfRY2shjKVXRoIu5tA6NqtPtl4IL/z3eJ GLfX7aDZuAtNR1o4v4WGmQKBgAdZOodrcSRZmPqrZ+V7ZED1oGdQiGpPyZk+wl9C c7CjiKN+7iPrt+BedeV9tuyagqYwvgV9DM1p9gQd5p2+/krbjL+3/ehXCKBG4jO2 8ihE/wYVfy6fPum/1/QomJzMPLuXMdwt/85tOmRoa0ApFGSJ0jP0KVW26a95RnRg eUsTAoGBAKaP9LrD9Cxu4naBW15K14sy/eRyqgvP7T0ScHjBBAHVYv6h9F8O7RhH VLm2OzPxIUdwuEnBUP5yQuI0VMi7EFgGX3Kg37aoDaZBNm7SCzlViBcEYHllOe90 eJX7XqqFIBbXmB2Tjc7B1QPdP3Psl92D0w6C/q/QMDi/lqL1L9zv -----END RSA PRIVATE KEY----- redis-rb-redis-client-367ca25/test/fixtures/certs/openssl.cnf000066400000000000000000000002421515654015100242610ustar00rootroot00000000000000[ server_cert ] keyUsage = digitalSignature, keyEncipherment nsCertType = server [ client_cert ] keyUsage = digitalSignature, keyEncipherment nsCertType = client redis-rb-redis-client-367ca25/test/fixtures/certs/redis.crt000066400000000000000000000027311515654015100237330ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEIzCCAgugAwIBAgIJAOjosSke3TNBMA0GCSqGSIb3DQEBCwUAMDUxEzARBgNV BAoMClJlZGlzIFRlc3QxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTAe Fw0yNTA0MjEyMzQ2MjBaFw0yNjA0MjEyMzQ2MjBaMCkxEzARBgNVBAoMClJlZGlz IFRlc3QxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAMN9gRk08ats5u753XNmwkDzyqvhBHJ7GvkyKE5BIA6jFlNR+miz ZhiKxgmfuSa139ojRfgqea3vUr03KdGXCn6uxucIJxzoJdMF3n6pnoE3znIO/SDn VbRiF773bIP1ssMnowYFXQWNqVLoQwEmJE+PVk1t3cXQ8bsiHEmpn4sAEO2hlztT ihRdRyDkeese5H/tGdUjNmfbyuSqoiqSHmWajiNSEELG3tzwEsY7mx6VQ8kdC+I5 LHxwoRN/tB0lnlszgVs1CzsK64aPI/UzwLEdT3soytAbb/kwgq29719AS3cz9dgK Es0/FAbnfE1cbRtA1wkXcGDE594QiT+bNUUCAwEAAaNCMEAwHQYDVR0OBBYEFL9k kxWhI3Bu7TLZx4QLLzxb+DUwMB8GA1UdIwQYMBaAFFuraL4Ktd4MW4XkeyLhrO1u 6PoEMA0GCSqGSIb3DQEBCwUAA4ICAQBbp/46IIRwtyiWs3+KLLgpuVC8NRCJicUQ 7vb8MOusHkj7/bEhSDDiVzQc+FaqHOhAKA54/PUAXZlDV++H7yP+GMRKT59vP6F1 XSG66zMZRC3nNKBZOc7aHLXJl5rrv7jHWowjQy10uhBM5xKVhb2mIKEUmdXCtN7+ AGw4g1UYe1SMM/PZtW83nq4GUmna2dN1+dTGMYXOn5/fHhxxRZy6TFq7VfuitOps V3uZ+V6z9IxIZEWxfvuNQbByK+BNaOSIALaWn++1sdfTPphH51c7sGcnonaXZUfn xiMsfEl6ZUUA54DHKGp9dtB4PySyACm4XzcdcaNV8ax2BcXr8ZQZkYG0o+4pAnvD Bltlw3YqrSDI2edcD2M1SpMEVqeDpbw5ZmsQjV73lAllVQXnvto7dgyJvp0f3ZqI xh9q4aDL1tV9MqKAZrd3CtRbjr6sPsrLhu1vPGjEiJho6nMdOef0E0rI4mgUpwQD Nx/izHwDYHhP6RvClMJ26+tnOE7qfPmbAnvCkVepCufr7zuRlq73LEEHzjGYjrcM CK2HSk9U4i/pnF77WK7IDL/UeT5b9upoqmDrO05QdTFS51+91cdXfzbLaSfqgaFV JuLE39nPjyhCnBQk4vyNsXpgxxYL4yUHGMctS+UPHADZW1xj5Zh55d6jg9+ixtpE oYYePoIHPw== -----END CERTIFICATE----- redis-rb-redis-client-367ca25/test/fixtures/certs/redis.dh000066400000000000000000000006501515654015100235340ustar00rootroot00000000000000-----BEGIN DH PARAMETERS----- MIIBCAKCAQEA+oW/vZAKdLFzHVqOuGMU8K3wEMbyG7EtC77s1lwpCO+SCXDoC9d1 E/VHEXWk9IkHetUpYRQRNBMfJntBxh4v2TDYhdqwL9FkHrXsik1dz5jbILAminxV OJQ5H+qA1y8syQUyXs/DeeVwop3s5J+VeWV7dgNZ4Wi7RoN4rDZTsymIwvzyby9j TrM+CP+6Mo7OEM5PuDr6G7jEkDvzkbHf2MXImF+OQOiu4S+dfeiYPDqH7rc4X+i9 F8XAsZEUCmIUeapcyuHtTTeXQd+ib/vHilB4WjRzPp7Qo/wjehoIq89J2jatH71K dHUqy115NDMkYdEM8j60+xQVeuRL5xETSwIBAg== -----END DH PARAMETERS----- redis-rb-redis-client-367ca25/test/fixtures/certs/redis.key000066400000000000000000000032131515654015100237270ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAw32BGTTxq2zm7vndc2bCQPPKq+EEcnsa+TIoTkEgDqMWU1H6 aLNmGIrGCZ+5JrXf2iNF+Cp5re9SvTcp0ZcKfq7G5wgnHOgl0wXefqmegTfOcg79 IOdVtGIXvvdsg/WywyejBgVdBY2pUuhDASYkT49WTW3dxdDxuyIcSamfiwAQ7aGX O1OKFF1HIOR56x7kf+0Z1SM2Z9vK5KqiKpIeZZqOI1IQQsbe3PASxjubHpVDyR0L 4jksfHChE3+0HSWeWzOBWzULOwrrho8j9TPAsR1PeyjK0Btv+TCCrb3vX0BLdzP1 2AoSzT8UBud8TVxtG0DXCRdwYMTn3hCJP5s1RQIDAQABAoIBAB3eJgRQ53+Wgu4O NPx1vcYouVVrar+G+YcLV8clAh3aYwXV55lpl3a2dS1xPtugPBRbAUy6SJ7/ireo HvaLyimy0GbqAvfSrDzCj8zwY2xAt4ULrzcAwUJvHkuqB+Vde7N/cdPwq9a2XyFw pRQe3LtfHgN0fsbDdrttqb7DcMHOuwR2QehOBVbJpmPKOJMsd2nNJfz/hR/sW0Ci UGfk5FsW/SNoG2wq8wptWF1F37pYweYqohnhwuzOBx0yyxVwckf3h18ErMeF0/xX pgUUImWYk/adWZBQzTyPgR2vvPcLis8ycCHozrbISE08RHfaP4OOFVrSTxFMvfGL 8GRctL0CgYEA+VReNsEFcByCzyhmGaM3ZVjAkjrWJwqcwVXQfDF0GU/zVZnxIfzI IelM0EySsr4BWtvPyyR7BSFelsDYerBu0Gb1/BKPjQ/gLFJuIuhnx3GGiG+RdAws yaR8tQC12ILtm98dxHLeTW6AsyCejtzH1gGHbR/uW6nkBjWbunTCkZMCgYEAyLhl f8HRMPuwTfQHiE7vIEshzAqNOr1dboV678q0usMxC6GcEjAMnZJYqUNtNZVPQb9y a41mSSP5rWHD+s5DZz3nLEwl3mWY2ZWMMj9WnCqtDrQ4tjbyn8A+J0hT7eH11MZb MHC6i6ARHEPeyCkNSBQirKR4nzgzM0J/wH4rRMcCgYEA6q6+C32915wOiF03VVRr FQroH/wfjRoRGG3k0rFd3WGC4oUHEn20By9o7PvWbUYpUlNqkISjAt45AV89pKYj eCghy4XQ9u8Fi9J+9n6ZCILUJeIWIAxBr/8SnvCvOb9rVfc6NqoEkw+7NmAyvrgT pV1FErMmkcMk7a9SCLxUU98CgYBD1fYPsGxHtrhGEDQ/gBXW/y1j7Sj/8iHSiXAb /JEKEY/Q04SQrQaGdoBabDxLgLOxj8dWzAoGrA7k5wa6C93B1az8Tpv5xrJazuz7 ymY2D0I/lu8XvghPr0QSOKKM4fIYQBVvkJmrOKSvvcxcL2uasZtqZ4eQoAjFyTKt 1rY+3wKBgAqJ3AwQVSSn/2oyNicJMKiozwtO+rO0rl5+Tbq70RhRQ0CI/7fnsINl xzYtE4NmNMda6PBNh5YmGfhGiBKWxu5mKrglEA6kCLhd0CbItVCXzTmnTk74+wxh R6vvajHDXKQKw/50wGjqNZxP1pP6eMh4Cjo50s5mb6EsLxCGcl+P -----END RSA PRIVATE KEY----- redis-rb-redis-client-367ca25/test/fixtures/certs/server.crt000066400000000000000000000030011515654015100241220ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEQzCCAiugAwIBAgIJAOjosSke3TM/MA0GCSqGSIb3DQEBCwUAMDUxEzARBgNV BAoMClJlZGlzIFRlc3QxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTAe Fw0yNTA0MjEyMzQ2MjBaFw0yNjA0MjEyMzQ2MjBaMCkxEzARBgNVBAoMClJlZGlz IFRlc3QxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAMH782VCzecCYzNf1lYFynMcuO6hH9Y2O7pGU9XnKESUz2STyMCI cUmSEqVGBjGurUrwt5f9ocMci4b6xl7SCw19UTU8wPVOB+CVxMRrH+haWO1Rf3Po D1L768KM4IAZNrehWmzWkiw36ZFQxykui/B0imF0+S5LFFjv+GnZdnXYKumL8TnU 6fwFiyfyDpPBiwBBV8aWf2t6n/KXwdM6aTgxgKwQYKmAuYw20N8UrwS+9LzTsKLM 7NJ3zSf3mbR5B4iZTCksYlwpYmpmGBODCbWcbqCt/4GDFsRiWl519RBTzAa/0OmH OHBEr2EjktYFp0W5OsP26YtviI4mH1BZrZcCAwEAAaNiMGAwCwYDVR0PBAQDAgWg MBEGCWCGSAGG+EIBAQQEAwIGQDAdBgNVHQ4EFgQUD6xb1kjJDasoygpvRipcixOs CXcwHwYDVR0jBBgwFoAUW6tovgq13gxbheR7IuGs7W7o+gQwDQYJKoZIhvcNAQEL BQADggIBABUjuxDrEy+o+3ozxOaJCy0GRWcdejQg9SBvXZAOCjTN5J5zUc5mRNVK AUOF9l337c92VRFEj784O/2C21usx5yYHk8ERdNwIhOY20KdhKFlEa9fY/HJ4fbI 05nZWmWMhatlTYdRlKOFBtMe6GPdHAIAPQE7cEYLibMQyWILhQdvBAVUqeJzQzXN khYtPx8MDWyGfM5vSx7GT10Zjd8HEAcn3IXG9X4tht2kLL5yB+JK/I+YZzkmTGyh Jfgqq3nYGBtBFUVv9mhjLfGMz8B9irMUUcqikPIqwoPxHddCPnmzYruozne+Rt/B v6UODkvjqckc8tKcaBoZFzfGqf4auflz0Gs0IZzwavJR7Pt8i25JA9inVZdjuPHM JFQ3a98XC6Re2al72RyDL2RkDvFdT7mlYTSyy93Fc40XkQcp/p20CoWTL9HwDlZu 3Nw2HcSrMG5EPfKiFu5vRvlnuehdZg5I/xqHhliAndaO20YUF/yYKkoWaombDfnT Owm+OIdfC3CuIN/0fM45+Qoe/3TwB0aPE+NsPJgAG3JxClTtgcQlYyACcitgKWTe YlNSSf94nF55o80Dk6Yx2fISZFnuj7bLlPytFcsXGYrad8MDDMEwjMwXdrNFnRCe 1WnYTqaMRiytnfS9Yw2AbSZhhdLsvh47HWgN3QFQex2Gb7cKMaoJ -----END CERTIFICATE----- redis-rb-redis-client-367ca25/test/fixtures/certs/server.key000066400000000000000000000032131515654015100241270ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAwfvzZULN5wJjM1/WVgXKcxy47qEf1jY7ukZT1ecoRJTPZJPI wIhxSZISpUYGMa6tSvC3l/2hwxyLhvrGXtILDX1RNTzA9U4H4JXExGsf6FpY7VF/ c+gPUvvrwozggBk2t6FabNaSLDfpkVDHKS6L8HSKYXT5LksUWO/4adl2ddgq6Yvx OdTp/AWLJ/IOk8GLAEFXxpZ/a3qf8pfB0zppODGArBBgqYC5jDbQ3xSvBL70vNOw oszs0nfNJ/eZtHkHiJlMKSxiXCliamYYE4MJtZxuoK3/gYMWxGJaXnX1EFPMBr/Q 6Yc4cESvYSOS1gWnRbk6w/bpi2+IjiYfUFmtlwIDAQABAoIBAB+8equcbFdY+qXT DhvwwphmoJLZ5X2ETe1ByEF8mgfuWKfZzcRCDla9ATPs6uKB83QJQeAp7KchKmqg 6Idm0cwZLooJMIBxjbRejFyeMhAvh9D7vmBWHPu0n3Oq3KfYeC0+xq57xFpbo2jU 0GCabuaeCm27V3ENc3zBdeDLZSgONgoaj5PVFi+MxxNV+oXXtQZdBplFfOuy24zk w+6KHkVoa3uZDQAK87ca1zrpL7qkLuqzGkckpxr2HOXPXYazhXMXBDkbuV+PlQhI TXVMxd2PMFVj64cNWVkJbO2C6CtGZUq48+SmrRMCF0OBEI85zohU3FAEqKGj9mo/ AtDcPgECgYEA/9bHN9XMe/f7dyE//UW0JNsctRqcJ7BN1XhuUEZyOUI2Iex5LRLm 6W6+LuoQOPBO+tfURbbEl4/RMqu14jfoCo/gMoSsshyQaKyCh2VywGTUpScjbgiT 0wr4G7yajzKIhWzmQ+jee84qDELQRte75EODFcqsFzABcK3BXP9NGBcCgYEAwhs0 znCr4qjPO4O7z/vptRPzebF3o94iisgWCVLDAsAyQ5Oxi+dmGl5acfjT4cPt3Jnu QFIzQaS0dHn1VDU/R8jpcR8s+FLd3jEhjcbLNXFSgox+LbHVnK11Xy1MMkohaEmS h0ghdhAY9ZiorSmK/0TK7NgIkuMgu7WrQ4mQBoECgYEAlNJwgrdYwwhm/E6YNZGV kBbxpRv8mE3DiRkMOqAwE8TDToqLlr+3GTU1Zn77vtNzbhGcxozh4TRkwfAG1rgk v/gft+NbviRFkM5BA9fsn6RH2mZhAsH0k8B+wUu+MOx5Y/wMGpbczPIJnaZEF+Go x8jJ+SQzZS2kuNIqeBl+1DMCgYBg0hVLDCSQ0Mdd1l3uZqeyrRr7jqwwzvLH6voi +GdRjfEEiD09ndTuPjY7N3To3kRdj2KqLtZmXfOtTdAzisPf2LWouXZC/4Kv/C3S fGCMbdRMTiv6OwRkPJmZOg0R4Kw9SsWOOUqHi4wHpXgtt9Ufc38NGM1eB3EicIHX FF0FAQKBgC2R5qQ+4Exktz8+TUFDYoMFr6tFYAVA7+vq6IJcxkb9bq4bvjKOJX54 VDMuzaQ0rgyS7V8eSMBWjzq/up8JT9sfZPasChdU6tcOOXTpbcaDc/K+mgvKbF03 cO/mKPFFcqI5leJOgyPjLW2hySptzzUBNfbQS/Jnse2WyFlXbYjD -----END RSA PRIVATE KEY----- redis-rb-redis-client-367ca25/test/fixtures/generate-certs.sh000077500000000000000000000037121515654015100242420ustar00rootroot00000000000000#!/bin/bash # Generate some test certificates which are used by the regression test suite: # # test/fixtures/certs/ca.{crt,key} Self signed CA certificate. # test/fixtures/certs/redis.{crt,key} A certificate with no key usage/policy restrictions. # test/fixtures/certs/client.{crt,key} A certificate restricted for SSL client usage. # test/fixtures/certs/server.{crt,key} A certificate restricted for SSL server usage. # test/fixtures/certs/redis.dh DH Params file. generate_cert() { local name=$1 local cn="$2" local opts="$3" local keyfile=test/fixtures/certs/${name}.key local certfile=test/fixtures/certs/${name}.crt [ -f $keyfile ] || openssl genrsa -out $keyfile 2048 openssl req \ -new -sha256 \ -subj "/O=Redis Test/CN=$cn" \ -key $keyfile | \ openssl x509 \ -req -sha256 \ -CA test/fixtures/certs/ca.crt \ -CAkey test/fixtures/certs/ca.key \ -CAserial test/fixtures/certs/ca.txt \ -CAcreateserial \ -days 365 \ $opts \ -out $certfile } mkdir -p tests/tls [ -f test/fixtures/certs/ca.key ] || openssl genrsa -out test/fixtures/certs/ca.key 4096 openssl req \ -x509 -new -nodes -sha256 \ -key test/fixtures/certs/ca.key \ -days 3650 \ -subj '/O=Redis Test/CN=Certificate Authority' \ -out test/fixtures/certs/ca.crt cat > test/fixtures/certs/openssl.cnf <<_END_ [ server_cert ] keyUsage = digitalSignature, keyEncipherment nsCertType = server [ client_cert ] keyUsage = digitalSignature, keyEncipherment nsCertType = client _END_ generate_cert server "127.0.0.1" "-extfile test/fixtures/certs/openssl.cnf -extensions server_cert" generate_cert client "127.0.0.1" "-extfile test/fixtures/certs/openssl.cnf -extensions client_cert" generate_cert redis "127.0.0.1" [ -f test/fixtures/certs/redis.dh ] || openssl dhparam -out test/fixtures/certs/redis.dh 2048redis-rb-redis-client-367ca25/test/hiredis/000077500000000000000000000000001515654015100205465ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/hiredis/test_helper.rb000066400000000000000000000010701515654015100234070ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../test_helper" require "hiredis-client" unless RedisClient.default_driver == RedisClient::HiredisConnection abort("Hiredis not defined as default driver") end begin # This method was added in Ruby 3.0.0. Calling it this way asks the GC to # move objects around, helping to find object movement bugs. if RUBY_VERSION >= "3.2.0" GC.verify_compaction_references(expand_heap: true, toward: :empty) else GC.verify_compaction_references(double_heap: true, toward: :empty) end rescue NoMethodError end redis-rb-redis-client-367ca25/test/redis_client/000077500000000000000000000000001515654015100215635ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/redis_client/circuit_breaker_test.rb000066400000000000000000000045071515654015100263120ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class CircuitBreakerTest < RedisClientTestCase include ClientTestHelper def setup super @circuit_breaker = CircuitBreaker.new( error_threshold: 3, error_threshold_timeout: 2, success_threshold: 2, error_timeout: 1, ) end def test_open_circuit_after_consecutive_errors open_circuit @circuit_breaker assert_open @circuit_breaker end def test_track_errors_during_error_threshold_window assert_closed @circuit_breaker (@circuit_breaker.error_threshold - 1).times do record_error @circuit_breaker end travel(@circuit_breaker.error_threshold_timeout - 0.01) do record_error @circuit_breaker assert_open @circuit_breaker end end def test_allow_use_after_the_errors_timedout open_circuit @circuit_breaker assert_open @circuit_breaker travel(@circuit_breaker.error_threshold_timeout) do assert_closed(@circuit_breaker) end end def test_reopen_immediately_when_half_open open_circuit @circuit_breaker assert_open @circuit_breaker travel(@circuit_breaker.error_timeout) do record_error(@circuit_breaker) assert_open(@circuit_breaker) end end def test_close_fully_after_success_threshold_is_reached open_circuit @circuit_breaker assert_open @circuit_breaker travel(@circuit_breaker.error_timeout) do @circuit_breaker.success_threshold.times do assert_closed(@circuit_breaker) end record_error(@circuit_breaker) assert_closed(@circuit_breaker) end end private def assert_open(circuit_breaker) assert_raises CircuitBreaker::OpenCircuitError do circuit_breaker.protect do # noop end end end def assert_closed(circuit_breaker) assert_equal(:result, circuit_breaker.protect { :result }) end def open_circuit(circuit_breaker) circuit_breaker.error_threshold.times do record_error(circuit_breaker) end end def record_error(circuit_breaker) assert_raises CannotConnectError do circuit_breaker.protect do raise CannotConnectError, "Oh no!" end end end end end redis-rb-redis-client-367ca25/test/redis_client/command_builder_test.rb000066400000000000000000000017041515654015100262750ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class CommandBuilderTest < RedisClientTestCase def test_positional assert_equal ["a", "b", "c"], call("a", "b", "c") end def test_array assert_equal ["a", "b", "c"], call("a", ["b", "c"]) end def test_hash assert_equal ["a", "b", "c"], call("a", { "b" => "c" }) end def test_symbol assert_equal ["a", "b", "c", "d"], call(:a, { b: :c }, :d) end def test_numeric assert_equal ["1", "2.3"], call(1, 2.3) end def test_kwargs_boolean assert_equal ["withscores"], call(ttl: nil, ex: false, withscores: true) end def test_kwargs_values assert_equal ["ttl", "42"], call(ttl: 42) end def test_nil_kwargs assert_equal ["a", "b", "c"], CommandBuilder.generate(%i(a b c)) end private def call(*args, **kwargs) CommandBuilder.generate(args, kwargs) end end end redis-rb-redis-client-367ca25/test/redis_client/config_test.rb000066400000000000000000000262121515654015100244170ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class ConfigTest < RedisClientTestCase def test_simple_uri config = Config.new(url: "redis://example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "default", config.username assert_nil config.password assert_equal 0, config.db refute_predicate config, :ssl? end def test_unix_uri config = Config.new(url: "/run/redis/test.sock?db=1") assert_equal "/run/redis/test.sock", config.path assert_nil config.host assert_nil config.port assert_equal 1, config.db config = Config.new(url: "run/redis/test.sock?db=1") assert_equal "run/redis/test.sock", config.path assert_nil config.host assert_nil config.port assert_equal 1, config.db config = Config.new(url: "unix:///run/redis/test.sock?db=1") assert_equal "/run/redis/test.sock", config.path assert_nil config.host assert_nil config.port assert_equal 1, config.db config = Config.new(url: "unix://run/redis/test.sock?db=1") assert_equal "run/redis/test.sock", config.path assert_nil config.host assert_nil config.port assert_equal 1, config.db end def test_uri_instance config = Config.new(url: URI.parse("redis://example.com")) assert_equal "example.com", config.host end def test_invalid_url error = assert_raises ArgumentError do Config.new(url: "http://example.com") end assert_includes error.message, "Unknown URL scheme" assert_includes error.message, "example.com" end def test_defaults_to_localhost config = Config.new(url: "redis://") assert_equal "localhost", config.host end def test_ipv6_uri config = Config.new(url: "redis://[::1]") assert_equal "::1", config.host end def test_resp2_user_password_uri config = Config.new(protocol: 2, url: "redis://username:password@example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "username", config.username assert_equal "password", config.password assert_equal 0, config.db refute_predicate config, :ssl? assert_equal [%w[AUTH username password]], config.connection_prelude end def test_resp3_user_password_uri config = Config.new(url: "redis://username:password@example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "username", config.username assert_equal "password", config.password assert_equal 0, config.db refute_predicate config, :ssl? assert_equal [%w[HELLO 3 AUTH username password]], config.connection_prelude end def test_hide_password config = Config.new(password: "PASSWORD") refute_match "PASSWORD", config.inspect refute_match "PASSWORD", config.to_s end def test_hide_password_in_uri config = Config.new(url: "redis://username:PASSWORD@example.com") refute_match "PASSWORD", config.inspect refute_match "PASSWORD", config.to_s end def test_resp2_frozen_prelude config = Config.new(protocol: 2, url: "redis://username:password@example.com") prelude = config.connection_prelude assert_equal [%w[AUTH username password]], prelude assert_equal true, prelude.frozen? assert_equal true, (prelude.all? { |commands| commands.frozen? }) prelude.each do |commands| assert_equal true, (commands.all? { |arg| arg.frozen? }) end end def test_resp3_frozen_prelude config = Config.new(url: "redis://username:password@example.com") prelude = config.connection_prelude assert_equal [%w[HELLO 3 AUTH username password]], prelude assert_equal true, prelude.frozen? assert_equal true, (prelude.all? { |commands| commands.frozen? }) prelude.each do |commands| assert_equal true, (commands.all? { |arg| arg.frozen? }) end end def test_resp2_simple_password_uri config = Config.new(protocol: 2, url: "redis://password@example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "default", config.username assert_equal "password", config.password assert_equal 0, config.db refute_predicate config, :ssl? assert_equal [%w[AUTH password]], config.connection_prelude end def test_resp3_simple_password_uri config = Config.new(url: "redis://password@example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "default", config.username assert_equal "password", config.password assert_equal 0, config.db refute_predicate config, :ssl? assert_equal [%w[HELLO 3 AUTH default password]], config.connection_prelude end def test_simple_password_uri_empty_user config = Config.new(url: "redis://:password@example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "default", config.username assert_equal "password", config.password assert_equal 0, config.db refute_predicate config, :ssl? assert_equal [%w[HELLO 3 AUTH default password]], config.connection_prelude end def test_percent_encoded_password_uri # from https://redis.io/topics/rediscli#host-port-password-and-database config = Config.new(url: "redis://p%40ssw0rd@redis-16379.hosted.com:16379/12") assert_equal "redis-16379.hosted.com", config.host assert_equal 16379, config.port assert_equal "default", config.username assert_equal "p@ssw0rd", config.password assert_equal 12, config.db refute_predicate config, :ssl? assert_equal [%w[HELLO 3 AUTH default p@ssw0rd], %w[SELECT 12]], config.connection_prelude end def test_rediss_url config = Config.new(url: "rediss://example.com") assert_equal "example.com", config.host assert_equal 6379, config.port assert_equal "default", config.username assert_nil config.password assert_equal 0, config.db assert_predicate config, :ssl? assert_equal [%w[HELLO 3]], config.connection_prelude end def test_trailing_slash_url config = Config.new(url: "redis://example.com/") assert_equal 0, config.db assert_equal [%w[HELLO 3]], config.connection_prelude config = Config.new(url: "redis://[::1]/") assert_equal 0, config.db assert_equal [%w[HELLO 3]], config.connection_prelude end def test_overriding config = Config.new( url: "redis://p%40ssw0rd@redis-16379.hosted.com:16379/12", ssl: true, username: "george", password: "hunter2", host: "example.com", port: 12, db: 5, ) assert_equal "example.com", config.host assert_equal 12, config.port assert_equal "george", config.username assert_equal "hunter2", config.password assert_equal 5, config.db assert_predicate config, :ssl? assert_equal [%w[HELLO 3 AUTH george hunter2], %w[SELECT 5]], config.connection_prelude end def test_overriding_nil config = Config.new( url: "redis://p%40ssw0rd@redis-16379.hosted.com:16379/12", username: nil, password: nil, host: nil, port: nil, db: nil, ) assert_equal "redis-16379.hosted.com", config.host assert_equal 16379, config.port assert_equal "default", config.username assert_equal "p@ssw0rd", config.password assert_equal 12, config.db end def test_server_url assert_equal "redis://localhost:6379", Config.new.server_url assert_equal "redis://localhost:6379", Config.new(username: "george", password: "hunter2").server_url assert_equal "redis://localhost:6379/5", Config.new(db: 5).server_url assert_equal "redis://192.168.0.1:6379", Config.new(host: "192.168.0.1", port: 6379).server_url assert_equal "redis://192.168.0.1:6379/5", Config.new(host: "192.168.0.1", port: 6379, db: 5).server_url assert_equal "redis://example.com:8080", Config.new(host: "example.com", port: 8080).server_url assert_equal "rediss://localhost:6379", Config.new(ssl: true).server_url assert_equal "redis://[::1]:6379", Config.new(host: "::1", port: 6379).server_url assert_equal "redis://[::1]:6379/2", Config.new(host: "::1", port: 6379, db: 2).server_url assert_equal "redis://[::1]:6379/2", Config.new(url: "redis://[::1]:6379/2").server_url assert_equal "redis://[ffff:aaaa:1111::fcf]:6379", Config.new(host: "ffff:aaaa:1111::fcf", port: 6379).server_url assert_equal "redis://[ffff:aaaa:1111::fcf]:6379/2", Config.new(host: "ffff:aaaa:1111::fcf", port: 6379, db: 2).server_url assert_equal "unix:///var/redis/redis.sock?db=5", Config.new(path: "/var/redis/redis.sock", db: 5).server_url end def test_custom_field config = Config.new(custom: { foo: "bar" }) assert_equal({ foo: "bar" }, config.custom) end def test_callable_password called = 0 argument = nil password_callable = lambda do |username| called += 1 argument = username username.upcase.reverse end config = Config.new( username: "george", password: password_callable, ) assert_equal [%w(HELLO 3 AUTH george EGROEG)], config.connection_prelude assert_equal 1, called assert_equal "george", argument end def test_connection_prelude_with_driver_info config = Config.new(driver_info: "rails_v7.0.0") prelude = config.connection_prelude assert_equal [ %w[HELLO 3], %w[CLIENT SETINFO LIB-NAME redis-client(rails_v7.0.0)], ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION], ], prelude end def test_connection_prelude_with_multiple_upstream_drivers_string config = Config.new(driver_info: "rails_v7.0.0;sidekiq_v7.1.0") prelude = config.connection_prelude assert_equal [ %w[HELLO 3], ["CLIENT", "SETINFO", "LIB-NAME", "redis-client(rails_v7.0.0;sidekiq_v7.1.0)"], ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION], ], prelude end def test_connection_prelude_with_driver_info_array config = Config.new(driver_info: ["rails_v7.0.0", "sidekiq_v7.1.0"]) prelude = config.connection_prelude assert_equal [ %w[HELLO 3], ["CLIENT", "SETINFO", "LIB-NAME", "redis-client(rails_v7.0.0;sidekiq_v7.1.0)"], ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION], ], prelude end def test_driver_info_attribute config = Config.new(driver_info: "sidekiq_v7.1.0") assert_equal "sidekiq_v7.1.0", config.driver_info assert_equal "redis-client(sidekiq_v7.1.0)", config.build_lib_name end def test_build_lib_name_with_empty_string config = Config.new(driver_info: "") assert_equal "redis-client", config.build_lib_name end def test_build_lib_name_with_invalid_type config = Config.new(driver_info: 123) assert_raises ArgumentError do config.build_lib_name end end end end redis-rb-redis-client-367ca25/test/redis_client/connection_test.rb000066400000000000000000000353361515654015100253200ustar00rootroot00000000000000# frozen_string_literal: true require "benchmark" require "test_helper" class RedisClient module ConnectionTests def test_connection_working assert_equal "PONG", @redis.call("PING") end def test_connected? client = new_client refute_predicate client, :connected? client.call("PING") assert_predicate @redis, :connected? @redis.close refute_predicate @redis, :connected? @redis.call("PING") @redis.instance_variable_get(:@raw_connection).close refute_predicate @redis, :connected? end def test_connect_failure client = new_client(host: "example.com") error = assert_raises RedisClient::ConnectionError do client.call("PING") end assert_match(%r{ \(rediss?://example.com:.*\)$}, error.message) end def test_redis_down_after_connect @redis.call("PING") # force connect Toxiproxy[/redis/].down do error = assert_raises RedisClient::ConnectionError do @redis.call("PING") end assert_match(%r{ \(rediss?://127.0.0.1:.*\)$}, error.message) end end def test_redis_down_before_connect @redis.close Toxiproxy[/redis/].down do error = assert_raises RedisClient::ConnectionError do @redis.call("PING") end assert_match(%r{ \(rediss?://127.0.0.1:.*\)$}, error.message) end end def test_tcp_connect_upstream_timeout @redis.close Toxiproxy[/redis/].upstream(:timeout, timeout: 2_000).apply do assert_timeout RedisClient::ConnectionError do @redis.call("PING") end end end def test_tcp_connect_downstream_timeout @redis.close Toxiproxy[/redis/].upstream(:timeout, timeout: 2_000).apply do assert_timeout RedisClient::ConnectionError do @redis.call("PING") end end end def test_tcp_upstream_timeout @redis.call("PING") # pre-connect Toxiproxy[/redis/].upstream(:timeout, timeout: 2_000).apply do assert_timeout RedisClient::TimeoutError do @redis.call("PING") end end end def test_tcp_upstream_latency @redis.call("PING") # pre-connect Toxiproxy[/redis/].upstream(:latency, latency: 2_000).apply do assert_timeout RedisClient::TimeoutError do @redis.call("PING") end end end def test_tcp_downstream_timeout @redis.call("PING") # pre-connect Toxiproxy[/redis/].downstream(:timeout, timeout: 2_000).apply do assert_timeout RedisClient::TimeoutError do @redis.call("PING") end end end def test_tcp_downstream_latency @redis.call("PING") # pre-connect Toxiproxy[/redis/].downstream(:latency, latency: 2_000).apply do assert_timeout RedisClient::TimeoutError do @redis.call("PING") end end end def test_reconnect_on_next_try value = "a" * 75 @redis.call("SET", "foo", value) Toxiproxy[/redis/].downstream(:limit_data, bytes: 120).apply do assert_equal value, @redis.call("GET", "foo") assert_raises RedisClient::ConnectionError do @redis.call("GET", "foo") end end refute_predicate @redis, :connected? assert_equal value, @redis.call("GET", "foo") end def test_reconnect_attempts_disabled client = new_client(reconnect_attempts: false) simulate_network_errors(client, ["PING"]) do assert_raises ConnectionError do client.call("PING") end end end def test_reconnect_attempts_enabled client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_equal "PONG", client.call("PING") end end def test_reconnect_attempts_enabled_pipelines client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_equal(["PONG"], client.pipelined { |p| p.call("PING") }) end end def test_reconnect_attempts_enabled_transactions client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_equal(["PONG"], client.multi { |p| p.call("PING") }) end end def test_reconnect_attempts_enabled_watching_transactions client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_raises ConnectionError do client.multi(watch: ["foo"]) { |p| p.call("PING") } end end end def test_reconnect_attempts_enabled_inside_watching_transactions client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["GET"]) do assert_raises ConnectionError do client.multi(watch: ["foo"]) do |transaction| # Since we called WATCH, the connection becomes stateful, so we can't # simply reconnect on failure. assert_raises ConnectionError do client.call("GET", "foo") end transaction.call("SET", "foo", "2") end end end end def test_reconnect_with_non_idempotent_commands client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["INCR"]) do # The INCR command is retried, causing the counter to be incremented twice assert_equal 2, client.call("INCR", "counter") end assert_equal "2", client.call("GET", "counter") end def test_reconnect_with_call_once client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["INCR"]) do assert_raises ConnectionError do client.call_once("INCR", "counter") end end assert_equal "1", client.call("GET", "counter") end def test_reconnect_reuse 5.times do @redis.close assert_equal "PONG", @redis.call("PING") end end def test_reconnect_config_change assert_equal "PONG", @redis.call("PING") @redis.close @redis.instance_variable_set(:@config, RedisClient.config(**tcp_config, port: 1)) assert_raises RedisClient::CannotConnectError do @redis.call("PING") end end def test_circuit_breaker circuit_breaker = CircuitBreaker.new(error_threshold: 3, success_threshold: 2, error_timeout: 1) @redis = new_client(circuit_breaker: circuit_breaker) assert_equal "PONG", @redis.call("PING") Toxiproxy[/redis/].down do circuit_breaker.error_threshold.times do assert_raises ConnectionError do @redis.call("PING") end end end assert_equal "PONG", new_client.call("PING") assert_raises CircuitBreaker::OpenCircuitError do @redis.call("PING") end assert_raises CircuitBreaker::OpenCircuitError do @redis.config.new_client.call("PING") end travel(circuit_breaker.error_timeout) do assert_equal "PONG", @redis.call("PING") end end def test_circuit_breaker_initial_connection_exception circuit_breaker = CircuitBreaker.new(error_threshold: 1, success_threshold: 1, error_timeout: 1) @redis = new_client(circuit_breaker: circuit_breaker, reconnect_attempts: 2) Toxiproxy[/redis/].down do error = assert_raises CannotConnectError do @redis.call("PING") end refute_instance_of CircuitBreaker::OpenCircuitError, error end end def test_killed_connection client = new_client(reconnect_attempts: 1, id: "background") thread = Thread.new do client.blocking_call(false, "BLPOP", "list", 0) end thread.join(0.1) assert_predicate thread, :alive? second_client = new_client id = second_client.call("CLIENT", "LIST").lines.grep(/name=background/)[0].match(/^id=(\d+)/)[1] assert_equal 1, second_client.call("CLIENT", "KILL", "ID", id) second_client.call("LPUSH", "list", "hello") assert_equal ["list", "hello"], thread.join.value end def test_oom_errors config_client = new_client old_max_memory = config_client.call("config", "get", "maxmemory").fetch("maxmemory") begin config_client.call("config", "set", "maxmemory", "1") # 1 byte client = new_client assert_raises RedisClient::OutOfMemoryError do client.call("SET", "foo", "a" * 1000) end assert_raises RedisClient::OutOfMemoryError do client.call("EVAL", "redis.call('SET', 'foo', '#{'a' * 1000}')", 0) end ensure config_client.call("config", "set", "maxmemory", old_max_memory) end end def test_script_errors error = assert_raises RedisClient::NoScriptError do @redis.call("EVALSHA", "invalid", 0) end assert_equal "NOSCRIPT", error.code end def test_idle_timeout client = new_client(idle_timeout: 5) id = client.call("CLIENT", "ID") assert_equal 1, @redis.call("CLIENT", "KILL", "ID", id) travel(client.idle_timeout + 1) do assert_equal "PONG", client.call("PING") end end def test_no_idle_timeout client = new_client(idle_timeout: 5) id = client.call("CLIENT", "ID") assert_equal 1, @redis.call("CLIENT", "KILL", "ID", id) assert_raises ConnectionError do client.call("PING") end end private def assert_timeout(error, faster_than = ClientTestHelper::DEFAULT_TIMEOUT + 0.2, &block) realtime = Benchmark.realtime do assert_raises(error, &block) end assert realtime < faster_than, "Took longer than #{faster_than}s to timeout (#{realtime})" end end class TCPConnectionTest < RedisClientTestCase include ClientTestHelper include ConnectionTests def test_connecting_to_a_ssl_server client = new_client(**ssl_config, ssl: false) error = assert_raises CannotConnectError do client.call("PING") end assert_match(%r{ \(rediss?://.*:.*\)$}, error.message) end def test_protocol_error 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 session.write("invalid") session.close end error = assert_raises RedisClient::ProtocolError do new_client(host: "127.0.0.1", port: port).call("PING") end assert_match(%r{ \(rediss?://127.0.0.1:#{port}\)$}, error.message) ensure server_thread&.kill end def test_boolean_replies # The RESP3 protocol allows for boolean replies, but I'm not aware of # any commands doing so. So to test for potential future commands returning # a boolean, we use a fake server. 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 = RubyConnection::BufferedIO.new(session, read_timeout: 1, write_timeout: 1) while command = RESP3.load(io) case command.first when "HELLO" session.write("_\r\n") when "ECHO" session.write(command[1]) else session.write("-ERR Unknown command #{command.first}\r\n") end end session.close end client = new_client(host: "127.0.0.1", port: port) assert_equal true, client.call("ECHO", "#t\r\n") assert_equal false, client.call("ECHO", "#f\r\n") assert_nil client.call("ECHO", "_\r\n") ensure server_thread&.kill end def test_reconnect_on_readonly 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 = RubyConnection::BufferedIO.new(session, read_timeout: 1, write_timeout: 1) while command = RESP3.load(io) case command.first when "HELLO" session.write("_\r\n") when "PING" session.write("+PING\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 client = new_client(host: "127.0.0.1", port: port) client.call("PING") error = assert_raises RedisClient::ReadOnlyError do client.call("SET", "foo", "bar") end refute_predicate client, :connected? assert_match(%r{ \(rediss?://127.0.0.1:#{port}\)$}, error.message) ensure server_thread&.kill end def test_reconnect_on_masterdown 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 = RubyConnection::BufferedIO.new(session, read_timeout: 1, write_timeout: 1) while command = RESP3.load(io) case command.first when "HELLO" session.write("_\r\n") when "PING" session.write("+PING\r\n") when "SET" session.write("-MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'\r\n") else session.write("-ERR Unknown command #{command.first}\r\n") end end session.close end client = new_client(host: "127.0.0.1", port: port) client.call("PING") assert_raises RedisClient::MasterDownError do client.call("SET", "foo", "bar") end refute_predicate client, :connected? ensure server_thread&.kill end private def new_client(**overrides) RedisClient.new(**tcp_config, **overrides) end end class SSLConnectionTest < RedisClientTestCase include ClientTestHelper include ConnectionTests def test_connecting_to_a_raw_tcp_server client = new_client(**tcp_config, ssl: true) assert_raises CannotConnectError do client.call("PING") end end private def new_client(**overrides) RedisClient.new(**ssl_config, **overrides) end end class UnixConnectionTest < RedisClientTestCase include ClientTestHelper def test_connection_working assert_equal "PONG", @redis.call("PING") end def test_missing_socket assert_raises RedisClient::CannotConnectError do new_client(path: "/tmp/does-not-exist").call("PING") end end private def new_client(**overrides) RedisClient.new(**unix_config, **overrides) end end end redis-rb-redis-client-367ca25/test/redis_client/decorator_test.rb000066400000000000000000000026471515654015100251420ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient module DecoratorTests include ClientTestHelper include RedisClientTests module Commands def exists?(key) call("EXISTS", key) { |c| c > 0 } end end MyDecorator = Decorator.create(Commands) class MyDecorator def test? true end end def test_custom_command_helpers @redis.call("SET", "key", "hello") assert_equal 1, @redis.call("EXISTS", "key") assert_equal true, @redis.exists?("key") assert_equal([true], @redis.pipelined { |p| p.exists?("key") }) assert_equal([true], @redis.multi { |p| p.exists?("key") }) end def test_client_methods_not_available_on_pipelines assert_equal true, @redis.test? @redis.pipelined do |pipeline| assert_equal false, pipeline.respond_to?(:test?) end @redis.multi do |pipeline| assert_equal false, pipeline.respond_to?(:test?) end end end class DecoratorTest < RedisClientTestCase include DecoratorTests private def new_client(**overrides) MyDecorator.new(RedisClient.config(**tcp_config.merge(overrides)).new_client) end end class PooledDecoratorTest < RedisClientTestCase include DecoratorTests private def new_client(**overrides) MyDecorator.new(RedisClient.config(**tcp_config.merge(overrides)).new_pool) end end end redis-rb-redis-client-367ca25/test/redis_client/hash_ring_test.rb000066400000000000000000000031541515654015100251140ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "redis_client/hash_ring" require "digest/sha1" class RedisClient class HashRingTest < RedisClientTestCase include ClientTestHelper setup do @nodes = [ new_client(db: 1, id: "cache-1"), new_client(db: 2, id: "cache-2"), new_client(db: 3, id: "cache-3"), ] @ring = RedisClient.ring(@nodes) end def test_node_for assert_equal @nodes[2], @ring.node_for("foo") assert_equal @nodes[0], @ring.node_for("bar") assert_equal @nodes[1], @ring.node_for("baz") assert_equal @nodes[0], @nodes[0].node_for("baz") end def test_custom_digest @ring = RedisClient.ring(@nodes, digest: Digest::SHA1) assert_equal @nodes[0], @ring.node_for("foo") assert_equal @nodes[2], @ring.node_for("bar") assert_equal @nodes[1], @ring.node_for("baz999") end def test_nodes_for mapping = @ring.nodes_for("foo", "bar", "baz", "egg", "spam", "plop") assert_equal 3, mapping.size assert_equal ["baz", "spam", "plop"], mapping[@nodes[1]] assert_equal ["bar", "egg"], mapping[@nodes[0]] assert_equal ["foo"], mapping[@nodes[2]] assert_equal mapping, @ring.nodes_for(["foo", "bar", "baz", "egg", "spam", "plop"]) assert_equal({ @nodes[0] => ["foo", "bar"] }, @nodes[0].nodes_for("foo", "bar")) end def test_nodes assert_equal @nodes, @ring.nodes assert_equal [@nodes[0]], @nodes[0].nodes end private def new_client(**overrides) RedisClient.config(**tcp_config.merge(overrides)).new_pool end end end redis-rb-redis-client-367ca25/test/redis_client/middlewares_test.rb000066400000000000000000000167361515654015100254640ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class MiddlewaresTest < RedisClientTestCase include ClientTestHelper def setup @original_module = RedisClient::Middlewares new_module = @original_module.dup RedisClient.send(:remove_const, :Middlewares) RedisClient.const_set(:Middlewares, new_module) RedisClient.register(TestMiddleware) super TestMiddleware.calls.clear end def teardown if @original_module RedisClient.send(:remove_const, :Middlewares) RedisClient.const_set(:Middlewares, @original_module) end TestMiddleware.calls.clear super end def test_call_instrumentation @redis.call("PING") assert_call [:call, :success, ["PING"], "PONG", @redis.config] end def test_failing_call_instrumentation assert_raises CommandError do @redis.call("PONG") end call = TestMiddleware.calls.first assert_equal [:call, :error, ["PONG"]], call.first(3) assert_instance_of CommandError, call[3] end def test_call_once_instrumentation @redis.call_once("PING") assert_call [:call, :success, ["PING"], "PONG", @redis.config] end def test_blocking_call_instrumentation @redis.blocking_call(nil, "PING") assert_call [:call, :success, ["PING"], "PONG", @redis.config] end def test_pipeline_instrumentation @redis.pipelined do |pipeline| pipeline.call("PING") end assert_call [:pipeline, :success, [["PING"]], ["PONG"], @redis.config] end def test_multi_instrumentation @redis.multi do |transaction| transaction.call("PING") end assert_call [ :pipeline, :success, [["MULTI"], ["PING"], ["EXEC"]], ["OK", "QUEUED", ["PONG"]], @redis.config, ] end def test_final_errors client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_equal("PONG", client.call("PING")) end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 2, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] refute_predicate call[3], :final? call = calls[1] assert_equal :success, call[1] assert_equal ["PING"], call[2] TestMiddleware.calls.clear client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING", "PING"]) do assert_raises ConnectionError do client.call("PING") end end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 2, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] refute_predicate call[3], :final? call = calls[1] assert_equal :error, call[1] assert_equal ["PING"], call[2] assert_predicate call[3], :final? TestMiddleware.calls.clear client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING"]) do assert_raises ConnectionError do client.call_once("PING") end end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 1, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] assert_predicate call[3], :final? TestMiddleware.calls.clear end def test_final_errors_during_reconnect client = new_client(reconnect_attempts: 1) simulate_network_errors(client, ["PING", "HELLO"]) do assert_raises ConnectionError do client.call("PING") end end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 1, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] refute_predicate call[3], :final? pipeline_calls = TestMiddleware.calls.select { |type, _| type == :pipeline } assert_equal 2, pipeline_calls.size failing_pipeline = pipeline_calls[1] assert_equal :error, failing_pipeline[1] assert_equal [["HELLO", "3"]], failing_pipeline[2] assert_predicate failing_pipeline[3], :final? end def test_command_error_final 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 session.write("-Whoops\r\n") session.close end assert_raises CommandError do new_client(host: "127.0.0.1", port: port, reconnect_attempts: 1, protocol: 2).call("PING") end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 1, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] assert_predicate call[3], :final? ensure server_thread&.kill end def test_protocol_error 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 2.times do session = tcp_server.accept session.write("garbage\r\n") session.flush session.close end end assert_raises ProtocolError do new_client(host: "127.0.0.1", port: port, reconnect_attempts: 1, protocol: 2).call("PING") end calls = TestMiddleware.calls.select { |type, _| type == :call } assert_equal 2, calls.size call = calls[0] assert_equal :error, call[1] assert_equal ["PING"], call[2] refute_predicate call[3], :final? call = calls[1] assert_equal :error, call[1] assert_equal ["PING"], call[2] assert_predicate call[3], :final? ensure server_thread&.kill end module DummyMiddleware def call(command, _config, &_) command end def call_pipelined(commands, _config, &_) commands end end def test_instance_middleware second_client = new_client(middlewares: [DummyMiddleware]) assert_equal ["GET", "2"], second_client.call("GET", 2) assert_equal([["GET", "2"]], second_client.pipelined { |p| p.call("GET", 2) }) end private def assert_call(call) assert_equal call, TestMiddleware.calls.first assert_equal 1, TestMiddleware.calls.size end def assert_calls(calls) assert_equal calls, TestMiddleware.calls end module TestMiddleware class << self attr_accessor :calls end @calls = [] def connect(config) result = super TestMiddleware.calls << [:connect, :success, result, config] result rescue => error TestMiddleware.calls << [:connect, :error, error, config] raise end def call(command, config) result = super TestMiddleware.calls << [:call, :success, command, result, config] result rescue => error TestMiddleware.calls << [:call, :error, command, error, config] raise end def call_pipelined(commands, config) result = super TestMiddleware.calls << [:pipeline, :success, commands, result, config] result rescue => error TestMiddleware.calls << [:pipeline, :error, commands, error, config] raise end end end end redis-rb-redis-client-367ca25/test/redis_client/pooled_test.rb000066400000000000000000000011701515654015100244300ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisPooledClientTest < RedisClientTestCase include ClientTestHelper include RedisClientTests def test_checkout_timeout pool = RedisClient.config(**tcp_config).new_pool(size: 1, timeout: 0.01) Thread.new { pool.instance_variable_get(:@pool).checkout }.join error = assert_raises RedisClient::ConnectionError do pool.with {} end assert_includes error.message, "Couldn't checkout a connection in time: Waited 0.01 sec" end private def new_client(**overrides) RedisClient.config(**tcp_config.merge(overrides)).new_pool end end redis-rb-redis-client-367ca25/test/redis_client/ractor_test.rb000066400000000000000000000033571515654015100244510ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RactorTest < RedisClientTestCase tag isolated: true def setup skip("Ractors are not supported on this Ruby version") unless defined?(::Ractor) skip("Hiredis is not Ractor safe") if RedisClient.default_driver.name == "RedisClient::HiredisConnection" begin ractor_value(Ractor.new { RedisClient.default_driver.name }) rescue Ractor::RemoteError skip("Ractor implementation is too limited (MRI 3.0?)") end super end def test_get_and_set_within_ractor ractor = Ractor.new do config = Ractor.receive within_ractor_redis = RedisClient.new(**config) within_ractor_redis.call("SET", "foo", "bar") within_ractor_redis.call("GET", "foo") end ractor.send(ClientTestHelper.tcp_config.freeze) assert_equal("bar", ractor_value(ractor)) end def test_multiple_ractors ractor1 = Ractor.new do config = Ractor.receive within_ractor_redis = RedisClient.new(**config) within_ractor_redis.call("SET", "foo", "bar") within_ractor_redis.call("GET", "foo") end ractor1.send(ClientTestHelper.tcp_config.freeze) ractor_value(ractor1) # We do this to ensure that the SET has been processed ractor2 = Ractor.new do config = Ractor.receive within_ractor_redis = RedisClient.new(**config) key = Ractor.receive within_ractor_redis.call("GET", key) end ractor2.send(ClientTestHelper.tcp_config.freeze) ractor2.send("foo") assert_equal("bar", ractor_value(ractor2)) end if defined?(Ractor) && Ractor.method_defined?(:value) # Ruby 3.5+ def ractor_value(ractor) ractor.value end else def ractor_value(ractor) ractor.take end end end redis-rb-redis-client-367ca25/test/redis_client/resp3_test.rb000066400000000000000000000104531515654015100242060ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" require "redis_client/ruby_connection/buffered_io" require "redis_client/ruby_connection/resp3" class RedisClient class RESP3Test < RedisClientTestCase class StringIO < ::StringIO def skip(offset) seek(offset, IO::SEEK_CUR) nil end end def test_dump_mixed_encoding assert_dumps ["SET", "fée", "\xC6bIJ"], "*3\r\n$3\r\nSET\r\n$4\r\nfée\r\n$4\r\n\xC6bIJ\r\n" end def test_dump_string assert_dumps ["Hello World!"], "*1\r\n$12\r\nHello World!\r\n" assert_dumps ["Hello\r\nWorld!"], "*1\r\n$13\r\nHello\r\nWorld!\r\n" end def test_dump_integer assert_dumps [42], "*1\r\n$2\r\n42\r\n" end def test_dump_true assert_raises TypeError do RESP3.dump([true]) end end def test_dump_false assert_raises TypeError do RESP3.dump([false]) end end def test_dump_nil assert_raises TypeError do RESP3.dump([nil]) end end def test_dump_big_integer assert_dumps [1_000_000_000_000_000_000_000], "*1\r\n$22\r\n1000000000000000000000\r\n" end def test_dump_float assert_dumps [42.42], "*1\r\n$5\r\n42.42\r\n" # TODO: What about NaN, Infinity, -Infinity end def test_dump_array assert_dumps ["PRINT", [1, 2, 3]], "*4\r\n$5\r\nPRINT\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n" end def test_dump_hash assert_dumps(["PRINT", { 'first' => 1, 'second' => 2 }], "*5\r\n$5\r\nPRINT\r\n$5\r\nfirst\r\n$1\r\n1\r\n$6\r\nsecond\r\n$1\r\n2\r\n") end def test_dump_subclasses my_string_class = Class.new(String) assert_dumps [my_string_class.new("Hello")], "*1\r\n$5\r\nHello\r\n" end def test_load_blob_string assert_parses "Hello World!", "$12\r\nHello World!\r\n" end def test_load_simple_string assert_parses "Hello World!", "+Hello World!\r\n" end def test_load_error assert_parses CommandError.parse("SOMEERROR"), "-SOMEERROR\r\n" end def test_load_integer assert_parses 42, ":42\r\n" assert_parses(-42, ":-42\r\n") assert_parses 3_492_890_328_409_238_509_324_850_943_850_943_825_024_385, "(3492890328409238509324850943850943825024385\r\n" end def test_load_double assert_parses 42.42, ",42.42\r\n" assert_parses(-42.42, ",-42.42\r\n") assert_parses Float::INFINITY, ",inf\r\n" assert_parses(-Float::INFINITY, ",-inf\r\n") assert_parses(nil, ",nan\r\n") do |actual| assert_predicate actual, :nan? end end def test_load_null assert_parses nil, "_\r\n" end def test_load_boolean assert_parses true, "#t\r\n" assert_parses false, "#f\r\n" end def test_load_array assert_parses [1, 2, 3], "*3\r\n:1\r\n:2\r\n:3\r\n" end def test_load_multibyte_chars # Check that the buffer is properly operating over bytes and not characters assert_parses ["€™€™", 2], "*2\r\n$12\r\n€™€™\r\n:2\r\n" end def test_load_set assert_parses ['orange', 'apple', true, 100, 999], "~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n" end def test_load_map assert_parses({ 'first' => 1, 'second' => 2 }, "%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n") end def test_load_large_map entries = 100_000 payload = +"%#{entries}\r\n" entries.times do |i| payload << "+#{i}\r\n:#{i}\r\n" end expected = entries.times.each_with_object({}) { |i, h| h[i.to_s] = i } assert_parses(expected, payload) end def test_load_verbatim_string assert_parses "Some string", "=15\r\ntxt:Some string\r\n" end private def assert_parses(expected, payload) raw_io = StringIO.new(payload.b) io = RedisClient::RubyConnection::BufferedIO.new(raw_io, read_timeout: 1, write_timeout: 1) actual = RESP3.load(io) if block_given? yield actual elsif expected.nil? assert_nil actual else assert_equal(expected, actual) end assert io.eof?, "Expected IO to be fully consumed: #{raw_io.read.inspect}" end def assert_dumps(payload, expected) buffer = RESP3.dump(payload) assert_equal expected.b, buffer assert_equal Encoding::BINARY, buffer.encoding end end end redis-rb-redis-client-367ca25/test/redis_client/subscriptions_test.rb000066400000000000000000000055731515654015100260700ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class SubscriptionsTest < RedisClientTestCase include ClientTestHelper def setup super @subscription = @redis.pubsub end def test_subscribe assert_nil @subscription.call("SUBSCRIBE", "mychannel") @redis.pipelined do |pipeline| 3.times do |i| pipeline.call("PUBLISH", "mychannel", "event-#{i}") end end events = [] while event = @subscription.next_event events << event end assert_equal [ ["subscribe", "mychannel", 1], ["message", "mychannel", "event-0"], ["message", "mychannel", "event-1"], ["message", "mychannel", "event-2"], ], events end def test_psubscribe assert_nil @subscription.call("PSUBSCRIBE", "my*") @redis.pipelined do |pipeline| 3.times do |i| pipeline.call("PUBLISH", "mychannel", "event-#{i}") end end events = [] while event = @subscription.next_event events << event end assert_equal [ ["psubscribe", "my*", 1], ["pmessage", "my*", "mychannel", "event-0"], ["pmessage", "my*", "mychannel", "event-1"], ["pmessage", "my*", "mychannel", "event-2"], ], events end def test_connection_lost assert_nil @subscription.call("SUBSCRIBE", "mychannel") @redis.call("PUBLISH", "mychannel", "event-0") assert_equal ["subscribe", "mychannel", 1], @subscription.next_event assert_equal ["message", "mychannel", "event-0"], @subscription.next_event assert_nil @subscription.next_event(0.2) assert_nil @subscription.next_event(0.2) end def test_close assert_nil @subscription.call("SUBSCRIBE", "mychannel") @redis.pipelined do |pipeline| 3.times do |i| pipeline.call("PUBLISH", "mychannel", "event-#{i}") end end assert_equal ["subscribe", "mychannel", 1], @subscription.next_event assert_equal @subscription, @subscription.close assert_raises ConnectionError do @subscription.next_event end end def test_next_event_timeout assert_nil @subscription.next_event(0.01) end def test_pubsub_with_disabled_reconnection @redis.send(:ensure_connected, retryable: false) do refute_nil @redis.pubsub end end def test_pubsub_timeout_retry assert_nil @subscription.call("SUBSCRIBE", "mychannel") refute_nil @subscription.next_event # subscribed event assert_nil @subscription.next_event(0.01) @redis.call("PUBLISH", "mychannel", "test") 1.times.each do # rubocop:disable Lint/UselessTimes # We use 1.times.each to change the stack depth. # See https://github.com/redis-rb/redis-client/issues/221 refute_nil @subscription.next_event end end end end redis-rb-redis-client-367ca25/test/redis_client_test.rb000066400000000000000000000121601515654015100231470ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClientTest < RedisClientTestCase include ClientTestHelper include RedisClientTests def test_preselect_database client = new_client(db: 5) assert_includes client.call("CLIENT", "INFO"), " db=5 " client.call("SELECT", 6) assert_includes client.call("CLIENT", "INFO"), " db=6 " client.close assert_includes client.call("CLIENT", "INFO"), " db=5 " end def test_set_client_id client = new_client(id: "peter") assert_includes client.call("CLIENT", "INFO"), " name=peter " client.call("CLIENT", "SETNAME", "steven") assert_includes client.call("CLIENT", "INFO"), " name=steven " client.close assert_includes client.call("CLIENT", "INFO"), " name=peter " end def test_encoding @redis.call("SET", "str", "fée") str = @redis.call("GET", "str") assert_equal Encoding::UTF_8, str.encoding assert_predicate str, :valid_encoding? bytes = "\xFF\00" refute_predicate bytes, :valid_encoding? @redis.call("SET", "str", bytes.b) str = @redis.call("GET", "str") assert_equal Encoding::BINARY, str.encoding assert_predicate str, :valid_encoding? end def test_dns_resolution_failure client = RedisClient.new(host: "does-not-exist.example.com") error = assert_raises RedisClient::ConnectionError do client.call("PING") end assert_match(%r{ \(redis://does-not-exist.example.com:.*\)$}, error.message) end def test_older_server fake_redis5_driver = Class.new(RedisClient::RubyConnection) do def call_pipelined(commands, *, &_) if commands.any? { |c| c == ["HELLO", "3"] } raise RedisClient::CommandError, "ERR unknown command `HELLO`, with args beginning with: `3`" else super end end end client = new_client(driver: fake_redis5_driver) error = assert_raises RedisClient::UnsupportedServer do client.call("PING") end assert_includes error.message, "redis-client requires Redis 6+ with HELLO command available" assert_includes error.message, "(redis://" end def test_redis_6_server_with_missing_hello_command fake_redis6_driver = Class.new(RedisClient::RubyConnection) do def call_pipelined(commands, *, &_) if commands.any? { |c| c == ["HELLO", "3"] } raise RedisClient::CommandError, "ERR unknown command 'HELLO'" else super end end end client = new_client(driver: fake_redis6_driver) error = assert_raises RedisClient::UnsupportedServer do client.call("PING") end assert_includes error.message, "redis-client requires Redis 6+ with HELLO command available" assert_includes error.message, "(redis://" end def test_handle_async_raise 10.times do |i| thread = Thread.new do loop do assert_equal "OK", @redis.call("SET", "key#{i}", i) end rescue RuntimeError end thread.join(rand(0.01..0.2)) thread.raise("Timeout Error") refute_predicate thread.join, :alive? assert_equal i.to_s, @redis.call("GET", "key#{i}") end end def test_handle_async_thread_kill 10.times do |i| thread = Thread.new do loop do assert_equal "OK", @redis.call("SET", "key#{i}", i) end rescue RuntimeError end thread.join(rand(0.01..0.2)) thread.kill refute_predicate thread.join, :alive? assert_equal i.to_s, @redis.call("GET", "key#{i}") end end def test_measure_round_trip_delay assert_equal "OK", @redis.call("SET", "foo", "bar") assert_instance_of Float, @redis.measure_round_trip_delay assert_equal "OK", @redis.call("SET", "foo", "bar") @redis.close assert_instance_of Float, @redis.measure_round_trip_delay end def test_server_url assert_equal "redis://#{Servers::HOST}:#{Servers::REDIS.port}", @redis.server_url end def test_timeout assert_equal ClientTestHelper::DEFAULT_TIMEOUT, @redis.timeout end def test_db assert_equal 0, @redis.db end def test_id assert_nil @redis.id end def test_host assert_equal Servers::HOST, @redis.host end def test_port assert_equal Servers::REDIS.port, @redis.port end def test_path client = new_client(**unix_config) assert_equal Servers::REDIS.socket_file.to_s, client.path end def test_username username = "test" client = new_client(username: username) assert_equal username, client.username end def test_password password = "test" client = new_client(password: password) assert_equal password, client.password end if GC.respond_to?(:auto_compact) def test_gc_safety gc_stress_was = GC.stress gc_auto_compact_was = GC.auto_compact GC.stress = true GC.auto_compact = true client = new_client client.call("PING") list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(&:to_s) client.call("LPUSH", "list", list) 3.times do assert_equal list, client.call("LRANGE", "list", 0, -1).reverse end ensure GC.stress = gc_stress_was GC.auto_compact = gc_auto_compact_was end end end redis-rb-redis-client-367ca25/test/sentinel/000077500000000000000000000000001515654015100207405ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/sentinel/sentinel_test.rb000066400000000000000000000347311515654015100241550ustar00rootroot00000000000000# frozen_string_literal: true require "test_helper" class RedisClient class SentinelTest < RedisClientTestCase include ClientTestHelper def setup @config = new_config super @config = new_config end def check_server retried = false begin @redis = @config.new_client @redis.call("FLUSHDB") rescue if retried raise else retried = true Servers::SENTINEL_TESTS.reset retry end end end def teardown Servers::SENTINEL_TESTS.reset super end def test_new_client assert_equal "PONG", @config.new_client.call("PING") end def test_url_list sentinel_config = new_config(sentinels: Servers::SENTINELS.map { |s| "redis://#{s.host}:#{s.port}" }) assert_equal "PONG", sentinel_config.new_client.call("PING") end def test_sentinel_config assert_equal [Servers::REDIS.host, Servers::REDIS.port], [@config.host, @config.port] end def test_sentinel_down assert_equal [Servers::REDIS.host, Servers::REDIS.port], [@config.host, @config.port] original_order = Servers::SENTINELS.map(&:port) assert_equal original_order, @config.sentinels.map(&:port) Toxiproxy[/sentinel_(1|3)/].down do @config.reset assert_equal [Servers::REDIS.host, Servers::REDIS.port], [@config.host, @config.port] expected_order = [Servers::SENTINELS[1].port, Servers::SENTINELS[0].port, Servers::SENTINELS[2].port] assert_equal expected_order, @config.sentinels.map(&:port) end end def test_sentinel_all_down Toxiproxy[/sentinel_.*/].down do assert_raises RedisClient::ConnectionError do @config.new_client.call("PING") end end end class SentinelClientMock attr_reader :close_count def initialize(responses) @responses = responses @close_count = 0 end def call(*args, &block) call_v(args, &block) end def call_v(command) expected_command, response = @responses.shift if expected_command == command if block_given? yield response else response end else raise "Expected #{expected_command.inspect}, got: #{command.inspect}" end end def close @close_count += 1 end end # Mock that only has call_v, simulating Redis::Client which undefs call class CallVOnlySentinelClientMock < SentinelClientMock undef_method :call end def test_unknown_master @config = new_config(name: "does-not-exist") client = @config.new_client assert_raises ConnectionError do client.call("PING") end end def test_unresolved_config client = @config.new_client stub(@config, :server_url, -> { raise ConnectionError.with_config('this should not be called', @config) }) do stub(@config, :resolved?, false) do stub(client, :call, ->(_) { raise ConnectionError.with_config('call error', @config) }) do error = assert_raises ConnectionError do client.call('PING') end assert_equal 'call error', error.message end end end end def test_master_failover_not_ready sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS_REPLICA.host, Servers::REDIS_REPLICA.port.to_s]], sentinel_refresh_command_mock, [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS_REPLICA.host, Servers::REDIS_REPLICA.port.to_s]], sentinel_refresh_command_mock, [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS_REPLICA.host, Servers::REDIS_REPLICA.port.to_s]], sentinel_refresh_command_mock, ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_raises FailoverError do client.call("PING") end end end def test_master_failover_ready sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock, [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS_REPLICA.host, Servers::REDIS_REPLICA.port.to_s]], sentinel_refresh_command_mock, ]) replica = RedisClient.new(host: Servers::REDIS_REPLICA.host, port: Servers::REDIS_REPLICA.port) assert_equal "OK", replica.call("REPLICAOF", "NO", "ONE") stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_equal "PONG", client.call("PING") initial_server_key = @config.server_key Toxiproxy[Servers::REDIS.name].down do assert_equal "PONG", client.call("PING") refute_equal initial_server_key, @config.server_key end end ensure replica = RedisClient.new(host: Servers::REDIS_REPLICA.host, port: Servers::REDIS_REPLICA.port) assert_equal "OK", replica.call("REPLICAOF", Servers::REDIS.host, Servers::REDIS.port) end def test_no_replicas @config = new_config(role: :replica) tries = Servers::SENTINELS.size * (SentinelConfig::DEFAULT_RECONNECT_ATTEMPTS + 1) sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "replicas", "cache"], []], ] * tries) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do assert_raises ConnectionError do @config.new_client.call("PING") end end end def test_replica_failover_not_ready @config = new_config(role: :replica) sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS_REPLICA.host, "port" => Servers::REDIS_REPLICA.port.to_s, "flags" => "slave")]], [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS.host, "port" => Servers::REDIS.port.to_s, "flags" => "slave")]], [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS.host, "port" => Servers::REDIS.port.to_s, "flags" => "slave")]], ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_equal "PONG", client.call("PING") Toxiproxy[Servers::REDIS_REPLICA.name].down do assert_raises FailoverError do client.call("PING") end end end end def test_replica_failover_ready @config = new_config(role: :replica) sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS.host, "port" => Servers::REDIS.port.to_s, "flags" => "slave")]], [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS_REPLICA.host, "port" => Servers::REDIS_REPLICA.port.to_s, "flags" => "slave")]], [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS_REPLICA.host, "port" => Servers::REDIS_REPLICA.port.to_s, "flags" => "slave")]], [["SENTINEL", "replicas", "cache"], [response_hash("ip" => Servers::REDIS_REPLICA.host, "port" => Servers::REDIS_REPLICA.port.to_s, "flags" => "slave")]], ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_equal "PONG", client.call("PING") end end def test_successful_connection_refreshes_sentinels_list assert_equal Servers::SENTINELS.length, @config.sentinels.length new_sentinel_ip = "10.0.0.1" new_sentinel_port = 1234 # Trigger sentinel refresh to make the client aware of a new sentinel sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock( additional_sentinels: [response_hash("ip" => new_sentinel_ip, "port" => new_sentinel_port.to_s)], ), ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_equal "PONG", client.call("PING") end assert_equal Servers::SENTINELS.length + 1, @config.sentinels.length assert_equal new_sentinel_ip, @config.sentinels.last.host assert_equal new_sentinel_port, @config.sentinels.last.port assert_equal 1, sentinel_client_mock.close_count end def test_sentinel_refresh_password @config = new_config(sentinel_password: "hunter2") assert_equal Servers::SENTINELS.length, @config.sentinels.length new_sentinel_ip = "10.0.0.1" new_sentinel_port = 1234 # Trigger sentinel refresh to make the client aware of a new sentinel sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock( additional_sentinels: [response_hash("ip" => new_sentinel_ip, "port" => new_sentinel_port.to_s)], ), ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do client = @config.new_client assert_equal "PONG", client.call("PING") end assert_equal Servers::SENTINELS.length + 1, @config.sentinels.length @config.sentinels.each do |sentinel| refute_nil sentinel.password end assert_equal 1, sentinel_client_mock.close_count end def test_hide_sentinel_password config = new_config(sentinel_password: "PASSWORD") refute_match "PASSWORD", config.inspect refute_match "PASSWORD", config.to_s end def test_sentinel_client_without_call_method # Simulates Redis::Client which undefs call method sentinel_client_mock = CallVOnlySentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock, ]) stub(@config, :sentinel_client, ->(_config) { sentinel_client_mock }) do assert_equal [Servers::REDIS.host, Servers::REDIS.port], [@config.host, @config.port] end end def test_config_user_password_from_url_for_redis_master_replica_only config = new_config(url: "redis://george:hunter2@cache/10", name: nil) assert_equal "hunter2", config.password assert_equal "george", config.username assert_equal 10, config.db assert_equal [Servers::REDIS.host, Servers::REDIS.port], [config.host, config.port] config.sentinels.each do |sentinel| assert_nil sentinel.password end end def test_config_ssl_from_rediss_url sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock, ]) config = new_config(url: "rediss://george:hunter2@cache/10", name: nil) stub(config, :sentinel_client, ->(_config) { sentinel_client_mock }) do assert_equal "hunter2", config.password assert_equal "george", config.username assert_equal 10, config.db assert_predicate config, :ssl? assert_equal [Servers::REDIS.host, Servers::REDIS.port], [config.host, config.port] config.sentinels.each do |sentinel| assert_nil sentinel.password refute_predicate sentinel, :ssl? end end end def test_explicit_ssl_option_overrides_url sentinel_client_mock = SentinelClientMock.new([ [["SENTINEL", "get-master-addr-by-name", "cache"], [Servers::REDIS.host, Servers::REDIS.port.to_s]], sentinel_refresh_command_mock, ]) # Passing ssl: true should enable SSL for both Redis and Sentinel configs config = new_config(ssl: true) stub(config, :sentinel_client, ->(_config) { sentinel_client_mock }) do assert_predicate config, :ssl?, "Expected Redis config to use SSL when explicitly passed" config.sentinels.each do |sentinel| assert_predicate sentinel, :ssl?, "Expected Sentinel config to use SSL when explicitly passed" end end end def test_sentinel_shared_username_password config = new_config(sentinel_username: "alice", sentinel_password: "superpassword") config.sentinels.each do |sentinel| assert_equal "alice", sentinel.username assert_equal "superpassword", sentinel.password end end def test_sentinel_explicit_username_password sentinels = Servers::SENTINELS.map do |sentinel| { host: sentinel.host, port: sentinel.port, username: "alice", password: "superpassword" } end config = new_config(sentinels: sentinels) config.sentinels.each do |sentinel| assert_equal "alice", sentinel.username assert_equal "superpassword", sentinel.password end end def test_sentinel_config_from_url sentinels = Servers::SENTINELS.map do |sentinel| "redis://alice:superpassword@#{sentinel.host}:#{sentinel.port}" end config = new_config(sentinels: sentinels) assert_equal Servers::SENTINELS.length, config.sentinels.length config.sentinels.each do |sentinel| assert_equal "alice", sentinel.username assert_equal "superpassword", sentinel.password end end private def response_hash(hash) hash end def new_config(**kwargs) RedisClient.sentinel( name: Servers::SENTINEL_NAME, sentinels: Servers::SENTINELS.map do |sentinel| { host: sentinel.host, port: sentinel.port } end, timeout: 0.1, driver: ENV.fetch("DRIVER", "ruby").to_sym, **kwargs, ) end def sentinel_refresh_command_mock(additional_sentinels: []) sentinels = Servers::SENTINELS.map do |sentinel| response_hash("ip" => sentinel.host, "port" => sentinel.port.to_s) end [["SENTINEL", "sentinels", "cache"], sentinels.concat(additional_sentinels)] end end class RESP2SentinelTest < SentinelTest def test_sentinel_refresh_password skip("On RESP2, AUTH require the redis server to have a default password") end private def new_config(**kwargs) super(**kwargs, protocol: 2) end def response_hash(hash) # In RESP2 hashes are returned as flat arrays hash.to_a.flatten(1) end end end redis-rb-redis-client-367ca25/test/sentinel/test_helper.rb000066400000000000000000000002661515654015100236070ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../env" require_relative "../test_helper" Servers.build_redis Servers::SENTINEL_TESTS.prepare Servers.all = Servers::SENTINEL_TESTS redis-rb-redis-client-367ca25/test/shared/000077500000000000000000000000001515654015100203655ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/shared/redis_client_tests.rb000066400000000000000000000507221515654015100246060ustar00rootroot00000000000000# frozen_string_literal: true module RedisClientTests def test_has_version assert_instance_of String, RedisClient::VERSION end def test_config assert_instance_of RedisClient::Config, @redis.config redis_config = RedisClient.config(**tcp_config) redis = redis_config.new_client assert_equal "PONG", redis.call("PING") end def test_id assert_nil @redis.id end def test_ping assert_equal "PONG", @redis.call("PING") end def test_empty_call assert_raises ArgumentError do @redis.call end assert_raises ArgumentError do @redis.blocking_call(false) end assert_raises ArgumentError do @redis.call([], []) end @redis.pipelined do |pipeline| assert_raises ArgumentError do pipeline.call end end @redis.multi do |transaction| assert_raises ArgumentError do transaction.call end end end def test_call_v assert_equal "OK", @redis.call(["SET", "str", 42]) assert_equal "OK", @redis.blocking_call_v(1, ["SET", "str", 42]) assert_equal "OK", @redis.call_once_v(["SET", "str", 42]) results = @redis.pipelined do |pipeline| assert_nil pipeline.call(["SET", "str", 42]) assert_nil pipeline.blocking_call_v(1, ["SET", "str", 42]) assert_nil pipeline.call_once_v(["SET", "str", 42]) end assert_equal %w(OK OK OK), results results = @redis.multi do |transaction| assert_nil transaction.call(["SET", "str", 42]) assert_nil transaction.call_once_v(["SET", "str", 42]) end assert_equal %w(OK OK), results assert_nil @redis.pubsub.call_v(["PING"]) end def test_acts_as_pool assert_equal("PONG", @redis.with { |c| c.call("PING") }) assert_instance_of Integer, @redis.size end def test_status_strings_are_frozen assert_predicate @redis.call("SET", "str", "42"), :frozen? assert_predicate @redis.call("PING"), :frozen? end def test_argument_casting_numeric assert_equal "OK", @redis.call("SET", "str", 42) assert_equal "42", @redis.call("GET", "str") assert_equal "OK", @redis.call("SET", "str", 4.2) assert_equal "4.2", @redis.call("GET", "str") end def test_argument_casting_symbol assert_equal "OK", @redis.call("SET", :str, 42) assert_equal "42", @redis.call(:GET, "str") end def test_argument_casting_unsupported assert_raises TypeError do @redis.call("SET", "str", nil) # Could be casted as empty string, but would likely hide errors end assert_raises TypeError do @redis.call("SET", "str", true) # Unclear how it should be casted end assert_raises TypeError do @redis.call("SET", "str", false) # Unclear how it should be casted end end def test_argument_casting_arrays assert_equal 3, @redis.call("LPUSH", "list", [1, 2, 3]) assert_equal ["3", "2", "1"], @redis.call("LRANGE", "list", 0, 3) error = assert_raises TypeError do @redis.call("LPUSH", "list", [1, [2, 3]]) end assert_includes error.message, "Unsupported command argument type: Array" error = assert_raises TypeError do @redis.call("LPUSH", "list", [1, { 2 => 3 }]) end assert_includes error.message, "Unsupported command argument type: Hash" end def test_argument_casting_hashes assert_equal "OK", @redis.call("HMSET", "hash", { "bar" => 1, "baz" => 2 }) assert_equal({ "bar" => "1", "baz" => "2" }, @redis.call("HGETALL", "hash")) error = assert_raises TypeError do @redis.call("HMSET", "hash", { "bar" => [1, 2] }) end assert_includes error.message, "Unsupported command argument type: Array" error = assert_raises TypeError do @redis.call("HMSET", "hash", { "bar" => { 1 => 2 } }) end assert_includes error.message, "Unsupported command argument type: Hash" end def test_keyword_arguments_casting assert_equal "OK", @redis.call("SET", "key", "val", ex: 5) assert_equal 5, @redis.call("TTL", "key") end def test_pipeline_argument_casting_numeric assert_equal(["OK"], @redis.pipelined { |p| p.call("SET", "str", 42) }) assert_equal "42", @redis.call("GET", "str") assert_equal(["OK"], @redis.pipelined { |p| p.call("SET", "str", 4.2) }) assert_equal "4.2", @redis.call("GET", "str") end def test_pipeline_argument_casting_symbol assert_equal(["OK"], @redis.pipelined { |p| p.call("SET", :str, 42) }) assert_equal "42", @redis.call(:GET, "str") end def test_pipeline_argument_casting_arrays assert_equal([3], @redis.pipelined { |p| p.call("LPUSH", "list", [1, 2, 3]) }) assert_equal ["3", "2", "1"], @redis.call("LRANGE", "list", 0, 3) error = assert_raises TypeError do @redis.pipelined { |p| p.call("LPUSH", "list", [1, [2, 3]]) } end assert_includes error.message, "Unsupported command argument type: Array" error = assert_raises TypeError do @redis.pipelined { |p| p.call("LPUSH", "list", [1, { 2 => 3 }]) } end assert_includes error.message, "Unsupported command argument type: Hash" end def test_pipeline_argument_casting_hashes assert_equal(["OK"], @redis.pipelined { |p| p.call("HMSET", "hash", { "bar" => 1, "baz" => 2 }) }) assert_equal({ "bar" => "1", "baz" => "2" }, @redis.call("HGETALL", "hash")) error = assert_raises TypeError do @redis.pipelined { |p| p.call("HMSET", "hash", { "bar" => [1, 2] }) } end assert_includes error.message, "Unsupported command argument type: Array" error = assert_raises TypeError do @redis.pipelined { |p| p.call("HMSET", "hash", { "bar" => { 1 => 2 } }) } end assert_includes error.message, "Unsupported command argument type: Hash" end def test_empty_pipeline assert_equal([], @redis.pipelined { |_p| }) end def test_large_read_pipelines assert_equal 1000, @redis.call("LPUSH", "list", *1000.times.to_a) results = @redis.pipelined do |pipeline| 100.times do pipeline.call("LRANGE", "list", 0, -1) end end assert_equal 100, results.size end def test_large_write_pipelines results = @redis.pipelined do |pipeline| 10.times do |i| pipeline.call("SET", i, i.to_s * 10485760) end end assert_equal ["OK"] * 10, results end def test_get_set string = "a" * 15_000 assert_equal "OK", @redis.call("SET", "foo", string) assert_equal string, @redis.call("GET", "foo") end def test_hashes @redis.call("HMSET", "foo", "bar", "1", "baz", "2") assert_equal({ "bar" => "1", "baz" => "2" }, @redis.call("HGETALL", "foo")) end def test_call_once assert_equal 1, @redis.call_once("INCR", "counter") result = @redis.pipelined do |pipeline| pipeline.call_once("INCR", "counter") end assert_equal [2], result result = @redis.multi do |transaction| transaction.call_once("INCR", "counter") end assert_equal [3], result end def test_pipelining result = @redis.pipelined do |pipeline| assert_nil pipeline.call("SET", "foo", "42") assert_equal "OK", @redis.call("SET", "foo", "21") # Not pipelined assert_nil pipeline.call("EXPIRE", "foo", "100") end assert_equal ["OK", 1], result end def test_pipelining_error error = assert_raises RedisClient::CommandError do @redis.pipelined do |pipeline| pipeline.call("DOESNOTEXIST", 12) pipeline.call("SET", "foo", "42") end end assert_equal ["DOESNOTEXIST", "12"], error.command assert_equal "ERR", error.code assert_equal "42", @redis.call("GET", "foo") end def test_pipelining_error_with_explicit_raising_exception error = assert_raises RedisClient::CommandError do @redis.pipelined(exception: true) do |pipeline| pipeline.call("DOESNOTEXIST", 12) pipeline.call("SET", "foo", "42") end end assert_equal ["DOESNOTEXIST", "12"], error.command assert_equal "42", @redis.call("GET", "foo") end def test_pipelining_error_without_raising_exception result = @redis.pipelined(exception: false) do |pipeline| pipeline.call("DOESNOTEXIST", 12) pipeline.call("SET", "foo", "42") end assert result[0].is_a?(RedisClient::CommandError) assert_equal ["DOESNOTEXIST", "12"], result[0].command assert_equal "OK", result[1] assert_equal "42", @redis.call("GET", "foo") end def test_multi_error error = assert_raises RedisClient::CommandError do @redis.multi do |pipeline| pipeline.call("DOESNOTEXIST", 12) pipeline.call("SET", "foo", "42") end end assert_equal ["DOESNOTEXIST", "12"], error.command assert_nil @redis.call("GET", "foo") end def test_wrong_type @redis.call("SET", "str", "hello") error = assert_raises RedisClient::CommandError do @redis.call("SISMEMBER", "str", "member") end assert_equal ["SISMEMBER", "str", "member"], error.command assert_match(/WRONGTYPE Operation against a key holding the wrong kind of value (.*:.*)/, error.message) error = assert_raises RedisClient::CommandError do @redis.pipelined do |pipeline| pipeline.call("SISMEMBER", "str", "member") end end assert_equal ["SISMEMBER", "str", "member"], error.command assert_match(/WRONGTYPE Operation against a key holding the wrong kind of value (.*:.*)/, error.message) error = assert_raises RedisClient::CommandError do @redis.multi do |transaction| transaction.call("SISMEMBER", "str", "member") end end assert_equal ["SISMEMBER", "str", "member"], error.command assert_match(/WRONGTYPE Operation against a key holding the wrong kind of value (.*:.*)/, error.message) end def test_command_missing error = assert_raises RedisClient::CommandError do @redis.call("DOESNOTEXIST", "foo") end assert_match(/ERR unknown command .DOESNOTEXIST.*\(.*:.*\)/, error.message) end def test_authentication @redis.call("ACL", "DELUSER", "AzureDiamond") @redis.call("ACL", "SETUSER", "AzureDiamond", ">hunter2", "on", "+PING", "+CLIENT") @redis.call("ACL", "DELUSER", "backup_admin") @redis.call("ACL", "SETUSER", "backup_admin", ">hunter2", "on", "~*", "&*", "+@all") backup = new_client(username: "backup_admin", password: "hunter2") backup.call("ACL", "SETUSER", "default", "off") client = new_client(username: "AzureDiamond", password: "hunter2") assert_equal "PONG", client.call("PING") assert_raises RedisClient::PermissionError do client.call("GET", "foo") end # Wrong password client = new_client(username: "AzureDiamond", password: "trolilol") error = assert_raises RedisClient::AuthenticationError do client.call("PING") end assert_match(/WRONGPASS invalid username-password pair/, error.message) # The same error is raised, this shows that the client retried AUTH and didn't fall back to the default user error = assert_raises RedisClient::AuthenticationError do client.call("PING") end assert_match(/WRONGPASS invalid username-password pair/, error.message) # Correct password, but user disabled @redis.call("ACL", "DELUSER", "AnotherUser") backup.call("ACL", "SETUSER", "AnotherUser", ">boom", "off", "+PING", "+CLIENT") client = new_client(username: "AnotherUser", password: "boom") error = assert_raises RedisClient::AuthenticationError do client.call_once("PING") end assert_match(/WRONGPASS invalid username-password pair/, error.message) # Correct password, user enabled backup.call("ACL", "SETUSER", "AnotherUser", "on") assert_equal "PONG", client.call_once("PING") assert_match(/user=AnotherUser/, client.call("CLIENT", "INFO")) # Wrong username client = new_client(username: "GreenOpal", password: "boom") error = assert_raises RedisClient::AuthenticationError do client.call("PING") end assert_match(/WRONGPASS invalid username-password pair/, error.message) ensure backup.call("ACL", "SETUSER", "default", "on") end def test_prelude_failure client = new_client(db: 100) error = assert_raises RedisClient::CommandError do client.call("PING") end assert_match(/ERR DB index is out of range/, error.message) error = assert_raises RedisClient::CommandError do client.call("PING") end assert_match(/ERR DB index is out of range/, error.message) end def test_noauth @redis.call("ACL", "DELUSER", "AzureDiamond") @redis.call("ACL", "SETUSER", "AzureDiamond", ">hunter2", "on", "~*", "&*", "+@all") backup = new_client(username: "AzureDiamond", password: "hunter2") backup.call("ACL", "SETUSER", "default", "off") client = new_client(protocol: 2) error = assert_raises RedisClient::CommandError do client.call("PING") end assert_match(/NOAUTH Authentication required/, error.message) backup.call("ACL", "SETUSER", "default", "on") client.call("PING") ensure backup.call("ACL", "SETUSER", "default", "on") end def test_transaction result = @redis.multi do |transaction| transaction.call("SET", "foo", "1") transaction.call("EXPIRE", "foo", "42") end assert_equal ["OK", 1], result end def test_empty_transaction result = @redis.multi do |_transaction| end assert_equal [], result end def test_transaction_abort other_client = new_client @redis.call("SET", "foo", "1") result = @redis.multi(watch: ["foo"]) do |transaction| counter = Integer(@redis.call("GET", "foo")) other_client.call("SET", "foo", "2") transaction.call("SET", "foo", (counter + 1).to_s) transaction.call("EXPIRE", "foo", "42") end assert_nil result end def test_transaction_watch_reset other_client = new_client assert_raises RuntimeError do @redis.multi(watch: ["foo"]) do |_transaction| raise "oops" end end result = @redis.multi do |transaction| other_client.call("SET", "foo", "2") transaction.call("SET", "foo", "3") end assert_equal ["OK"], result assert_equal "3", @redis.call("GET", "foo") end def test_empty_transaction_watch_reset other_client = new_client @redis.multi(watch: ["foo"]) { |_t| } result = @redis.multi do |transaction| other_client.call("SET", "foo", "2") transaction.call("SET", "foo", "3") end assert_equal ["OK"], result assert_equal "3", @redis.call("GET", "foo") end def test_call_timeout_false thr = Thread.start do client = new_client client.blocking_call(false, "BRPOP", "list", "0") end assert_nil thr.join(0.3) # still blocking @redis.call("LPUSH", "list", "token") refute_nil thr.join(0.3) assert_equal ["list", "token"], thr.value end def test_call_timeout_zero thr = Thread.start do client = new_client client.blocking_call(0, "BRPOP", "list", "0") end assert_nil thr.join(0.3) # still blocking @redis.call("LPUSH", "list", "token") refute_nil thr.join(0.3) assert_equal ["list", "token"], thr.value end def test_pipelined_call_timeout thr = Thread.start do client = new_client client.pipelined do |pipeline| pipeline.blocking_call(false, "BRPOP", "list", "0") { |r| r.map(&:upcase) } end end assert_nil thr.join(0.3) # still blocking @redis.call("LPUSH", "list", "token") refute_nil thr.join(0.3) assert_equal [["LIST", "TOKEN"]], thr.value end def test_multi_call_timeout assert_raises NoMethodError do @redis.multi do |transaction| transaction.blocking_call(false, "BRPOP", "list", "0") end end end def test_blocking_call_timeout assert_nil @redis.blocking_call(0.5, "BRPOP", "list", "0.1") assert_equal "OK", @redis.call("SET", "foo", "bar") end def test_blocking_call_long_timeout client = new_client(timeout: 0.5) assert_nil client.blocking_call(0.5, "BRPOP", "list", "0.5") assert_nil client.blocking_call_v(0.5, ["BRPOP", "list", "0.5"]) result = client.pipelined do |pipeline| pipeline.blocking_call(0.5, "BRPOP", "list", "0.5") pipeline.blocking_call_v(0.5, ["BRPOP", "list", "0.5"]) end assert_equal([nil, nil], result) result = client.multi do |pipeline| pipeline.call("BRPOP", "list", "0.5") pipeline.call_v(["BRPOP", "list", "0.5"]) end assert_equal([nil, nil], result) end def test_blocking_call_timeout_retries redis = new_client(timeout: 0.5, reconnect_attempts: [3.0]) start = RedisClient.now assert_nil redis.blocking_call(0.1, "BRPOP", "list", "0.1") duration = RedisClient.now - start assert duration < 0.5 # if we retried we'd have waited much long end def test_scan @redis.call("MSET", *100.times.to_a) expected_keys = 100.times.select(&:even?).map(&:to_s).sort keys = [] @redis.scan do |key| keys << key end assert_equal expected_keys, keys.sort keys = [] @redis.scan("COUNT", "10") do |key| keys << key end assert_equal expected_keys, keys.sort end def test_scan_iterator @redis.call("MSET", *100.times.to_a) expected_keys = 100.times.select(&:even?).map(&:to_s).sort keys = @redis.scan.to_a assert_equal expected_keys, keys.sort keys = @redis.scan(count: 10).to_a assert_equal expected_keys, keys.sort end def test_sscan @redis.call("SADD", "large-set", *100.times.to_a) expected_elements = *100.times.map(&:to_s).sort elements = [] @redis.sscan("large-set") do |element| elements << element end assert_equal expected_elements, elements.sort elements = @redis.sscan("large-set").to_a assert_equal expected_elements, elements.sort elements = [] @redis.sscan("large-set", "COUNT", "10") do |element| elements << element end assert_equal expected_elements, elements.sort end def test_zscan @redis.call("ZADD", "large-set", *100.times.to_a) expected_elements = Hash[*100.times.map(&:to_s)].invert elements = {} @redis.zscan("large-set") do |element, score| elements[element] = score end assert_equal expected_elements, elements elements = @redis.zscan("large-set").to_a.to_h assert_equal expected_elements, elements elements = {} @redis.zscan("large-set", "COUNT", "10") do |element, score| elements[element] = score end assert_equal expected_elements, elements end def test_hscan @redis.call("HMSET", "large-hash", *100.times.to_a) expected_pairs = Hash[*100.times.map(&:to_s)].to_a pairs = [] @redis.hscan("large-hash") do |key, value| pairs << [key, value] end assert_equal expected_pairs, pairs pairs = @redis.hscan("large-hash").to_a assert_equal expected_pairs, pairs pairs = [] @redis.hscan("large-hash", "COUNT", "10") do |key, value| pairs << [key, value] end assert_equal expected_pairs, pairs end def test_timeouts_are_adjustable_on_the_client @redis.close @redis.connect_timeout = 1 @redis.read_timeout = 1 @redis.write_timeout = 1 assert_equal "PONG", @redis.call("PING") @redis.connect_timeout = 2 @redis.read_timeout = 2 @redis.write_timeout = 2 end def test_custom_result_casting assert_equal(0, @redis.call("SISMEMBER", "set", "unknown")) assert_equal(false, @redis.call("SISMEMBER", "set", "unknown") { |m| m > 0 }) assert_equal([false], @redis.pipelined { |p| p.call("SISMEMBER", "set", "unknown") { |m| m > 0 } }) assert_equal([false], @redis.multi { |p| p.call("SISMEMBER", "set", "unknown") { |m| m > 0 } }) end def test_verbatim_string_reply assert_equal("# Server", @redis.call("INFO")[0..7]) end def test_resp2_nil_string @redis = new_client(protocol: 2) @redis.call("SET", "foo", "bar") assert_equal "bar", @redis.call("GETDEL", "foo") assert_nil @redis.call("GET", "foo") end def test_resp2_limited_type_casting @redis = new_client(protocol: 2) assert_equal 1, @redis.call("INCR", "foo") @redis.call("HMSET", "hash", "foo", "bar") assert_equal ["foo", "bar"], @redis.call("HGETALL", "hash") end if Process.respond_to?(:fork) def test_handle_fork pid = fork do 1000.times do assert_equal "OK", @redis.call("SET", "key", "foo") end end 1000.times do assert_equal "PONG", @redis.call("PING") end _, status = Process.wait2(pid) assert_predicate(status, :success?) end def test_closing_in_child_doesnt_impact_parent pid = fork do @redis.close exit(0) end _, status = Process.wait2(pid) assert_predicate(status, :success?) assert_equal "PONG", @redis.call("PING") end end end redis-rb-redis-client-367ca25/test/support/000077500000000000000000000000001515654015100206335ustar00rootroot00000000000000redis-rb-redis-client-367ca25/test/support/client_test_helper.rb000066400000000000000000000061601515654015100250370ustar00rootroot00000000000000# frozen_string_literal: true module ClientTestHelper module FlakyDriver def self.included(base) base.extend(ClassMethods) end module ClassMethods attr_accessor :failures end def write(command) if self.class.failures.first == command.first self.class.failures.shift @fail_now = true end super end def read(*) @fail_now ||= false if @fail_now raise connection_error("simulated failure") end super end def reconnect @fail_now = false super end def write_multi(commands) commands.each { |c| write(c) } nil end end def setup super @redis = new_client check_server end private def check_server retried = false begin @redis.call("flushdb", "async") rescue if retried raise else retried = true Servers.reset retry end end end def travel(seconds) original_now = RedisClient.singleton_class.instance_method(:now) original_now_ms = RedisClient.singleton_class.instance_method(:now_ms) begin RedisClient.singleton_class.alias_method(:now, :now) RedisClient.define_singleton_method(:now) do original_now.bind(RedisClient).call + seconds end RedisClient.singleton_class.alias_method(:now_ms, :now_ms) RedisClient.define_singleton_method(:now_ms) do original_now_ms.bind(RedisClient).call + (seconds * 1000.0) end yield ensure RedisClient.singleton_class.alias_method(:now, :now) RedisClient.define_singleton_method(:now, original_now) RedisClient.singleton_class.alias_method(:now_ms, :now_ms) RedisClient.define_singleton_method(:now_ms, original_now_ms) end end def simulate_network_errors(client, failures) client.close client.instance_variable_set(:@raw_connection, nil) original_config = client.config flaky_driver = Class.new(original_config.driver) flaky_driver.include(FlakyDriver) flaky_driver.failures = failures flaky_config = original_config.dup flaky_config.instance_variable_set(:@driver, flaky_driver) begin client.instance_variable_set(:@config, flaky_config) yield ensure client.instance_variable_set(:@config, original_config) client.close client.instance_variable_set(:@raw_connection, nil) end end module_function DEFAULT_TIMEOUT = 0.1 def tcp_config { host: Servers::HOST, port: Servers::REDIS.port, timeout: DEFAULT_TIMEOUT, } end def ssl_config { host: Servers::HOST, port: Servers::REDIS.tls_port, timeout: DEFAULT_TIMEOUT, ssl: true, ssl_params: { cert: Servers::CERTS_PATH.join("client.crt").to_s, key: Servers::CERTS_PATH.join("client.key").to_s, ca_file: Servers::CERTS_PATH.join("ca.crt").to_s, }, } end def unix_config { path: Servers::REDIS.socket_file.to_s, timeout: DEFAULT_TIMEOUT, } end def new_client(**overrides) RedisClient.new(**tcp_config.merge(overrides)) end end redis-rb-redis-client-367ca25/test/support/driver.rb000066400000000000000000000000361515654015100224520ustar00rootroot00000000000000# frozen_string_literal: true redis-rb-redis-client-367ca25/test/support/raise_warnings.rb000066400000000000000000000004471515654015100242000ustar00rootroot00000000000000# frozen_string_literal: true $VERBOSE = true module RaiseWarnings def warn(message, *) return if message.match?(/Ractor.*is experimental/) super raise message end ruby2_keywords :warn if respond_to?(:ruby2_keywords, true) end Warning.singleton_class.prepend(RaiseWarnings) redis-rb-redis-client-367ca25/test/support/redis_builder.rb000077500000000000000000000037021515654015100240010ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'digest/sha1' require 'English' require 'fileutils' require 'net/http' class RedisBuilder TARBALL_CACHE_EXPIRATION = 60 * 60 * 30 def initialize(redis_branch, tmp_dir) @redis_branch = redis_branch @tmp_dir = tmp_dir @build_dir = Servers::CACHE_DIR.join("redis-#{redis_branch}").to_s end def bin_path File.join(@build_dir, "src/redis-server") end def install download_tarball_if_needed if old_checkum != checksum build update_checksum end end private def download_tarball_if_needed return if File.exist?(tarball_path) && File.mtime(tarball_path) > Time.now - TARBALL_CACHE_EXPIRATION FileUtils.mkdir_p(@tmp_dir) download(tarball_url, tarball_path) end def download(url, path) response = Net::HTTP.get_response(URI(url)) case Integer(response.code) when 300..399 download(response['Location'], path) when 200 File.binwrite(tarball_path, response.body) else raise "Unexpected HTTP response #{response.code} #{url}" end end def tarball_path File.join(@tmp_dir, "redis-#{@redis_branch}.tar.gz") end def tarball_url "https://github.com/redis/redis/archive/#{@redis_branch}.tar.gz" end def build FileUtils.rm_rf(@build_dir) FileUtils.mkdir_p(@build_dir) command!('tar', 'xf', tarball_path, '-C', File.expand_path('../', @build_dir)) Dir.chdir(@build_dir) do command!('make', 'BUILD_TLS=yes') end end def update_checksum File.write(checksum_path, checksum) end def old_checkum File.read(checksum_path) rescue Errno::ENOENT nil end def checksum_path File.join(@build_dir, 'build.checksum') end def checksum @checksum ||= Digest::SHA1.file(tarball_path).hexdigest end def command!(*args) puts "$ #{args.join(' ')}" raise "Command failed with status #{$CHILD_STATUS.exitstatus}" unless system(*args) end end redis-rb-redis-client-367ca25/test/support/server_manager.rb000066400000000000000000000061221515654015100241610ustar00rootroot00000000000000# frozen_string_literal: true require "pathname" class ServerManager ROOT = Pathname.new(File.expand_path("../../", __dir__)) class << self def kill_all Dir[ROOT.join("tmp/**/*.pid").to_s].each do |pid_file| pid = begin Integer(File.read(pid_file)) rescue ArgumentError nil end if pid begin Process.kill(:KILL, pid) rescue Errno::ESRCH, Errno::ECHILD nil # It's fine end end File.unlink(pid_file) end end end @worker_index = nil singleton_class.attr_accessor :worker_index module NullIO extend self def puts(_str) nil end def print(_str) nil end end attr_reader :name, :host, :command attr_accessor :out def initialize(name, port:, command: nil, real_port: port, host: "127.0.0.1") @name = name @host = host @port = port @real_port = real_port @command = command @out = $stderr end def worker_index ServerManager.worker_index end def port_offset worker_index.to_i * 200 end def port @port + port_offset end def real_port @real_port + port_offset end def spawn shutdown pid_file.parent.mkpath pid = Process.spawn(*command.map(&:to_s), out: log_file.to_s, err: log_file.to_s) pid_file.write(pid.to_s) @out.puts "started #{name}-#{worker_index.to_i} with pid=#{pid}" end def wait(timeout: 5) unless wait_until_ready(timeout: 1) @out.puts "Waiting for #{name}-#{worker_index.to_i} (port #{real_port})..." end if wait_until_ready(timeout: timeout - 1) @out.puts "#{name}-#{worker_index.to_i} ready." true else @out.puts "#{name}-#{worker_index.to_i} timedout." false end end def health_check TCPSocket.new(host, real_port) true rescue Errno::ECONNREFUSED false end def on_ready nil end def wait_until_ready(timeout: 5) (timeout * 100).times do if health_check on_ready return true else sleep 0.01 end end false end def shutdown if alive? pid = self.pid Process.kill("INT", pid) Process.wait(pid) end true rescue Errno::ESRCH, Errno::ECHILD true end def pid Integer(pid_file.read) rescue Errno::ENOENT, ArgumentError nil end def alive? pid = self.pid return false unless pid pid && Process.kill(0, pid) true rescue Errno::ESRCH false end private def dir ROOT.join("tmp/#{name}-#{worker_index.to_i}").tap(&:mkpath) end def pid_file dir.join("#{name}.pid") end def log_file dir.join("#{name}.log") end end class ServerList def initialize(*servers) @servers = servers end def silence @servers.each { |s| s.out = ServerManager::NullIO } yield ensure @servers.each { |s| s.out = $stderr } end def prepare shutdown @servers.each(&:spawn) @servers.all?(&:wait) end def reset silence { prepare } end def shutdown @servers.reverse_each(&:shutdown) end end redis-rb-redis-client-367ca25/test/support/servers.rb000066400000000000000000000126251515654015100226570ustar00rootroot00000000000000# frozen_string_literal: true require_relative "redis_builder" require_relative "server_manager" module Servers ROOT = Pathname.new(File.expand_path("../../", __dir__)) platform = `echo $(uname -m)-$(uname -s | tr '[:upper:]' '[:lower:]')`.strip CACHE_DIR = ROOT.join("tmp/cache", platform) HOST = "127.0.0.1" CERTS_PATH = ServerManager::ROOT.join("test/fixtures/certs") SENTINEL_CONF_PATH = ServerManager::ROOT.join("tmp/sentinel.conf") SENTINEL_NAME = "cache" DEFAULT_REDIS_VERSION = "7.0" class << self def build_redis redis_builder.install end def redis_server_bin redis_builder.bin_path end def redis_builder @redis_builder ||= RedisBuilder.new(ENV.fetch("REDIS", DEFAULT_REDIS_VERSION), ROOT.join("tmp").to_s) end attr_accessor :all def reset all.reset end end class RedisManager < ServerManager def spawn begin dir.join("dump.rdb").to_s rescue Errno::ENOENT end super end def socket_file dir.join("redis.sock").to_s end def command ROOT.join("tmp/redis-#{worker_index.to_i}").mkpath [ Servers.redis_server_bin, "--unixsocket", socket_file, "--unixsocketperm", "700", "--port", real_port.to_s, "--tls-port", real_tls_port.to_s, "--tls-cert-file", CERTS_PATH.join("redis.crt").to_s, "--tls-key-file", CERTS_PATH.join("redis.key").to_s, "--tls-ca-cert-file", CERTS_PATH.join("ca.crt").to_s, "--save", "", "--appendonly", "no", "--dir", dir, ] end def dir ROOT.join("tmp/redis-#{worker_index.to_i}") end def tls_port port + 10_000 end def real_tls_port real_port + 10_000 end end REDIS = RedisManager.new( "redis", port: 16379, real_port: 16380, ) class RedisReplicaManager < ServerManager def command [ Servers.redis_server_bin, "--port", real_port.to_s, "--save", "", "--appendonly", "no", "--dir", "tmp/", "--replicaof", REDIS.host, REDIS.real_port.to_s, ] end end REDIS_REPLICA = RedisReplicaManager.new( "redis_replica", port: 16381, real_port: 16382, ) REDIS_REPLICA_2 = RedisReplicaManager.new( "redis_replica_2", port: 16383, real_port: 16384, ) class SentinelManager < ServerManager def generate_conf conf_file.write(<<~EOS) sentinel monitor #{SENTINEL_NAME} #{REDIS.host} #{REDIS.port} 2 sentinel down-after-milliseconds #{SENTINEL_NAME} 10 sentinel failover-timeout #{SENTINEL_NAME} 2000 sentinel parallel-syncs #{SENTINEL_NAME} 1 user alice on allcommands allkeys >superpassword EOS end def conf_file ROOT.join("tmp/#{name}-#{worker_index.to_i}.conf") end def command [ Servers.redis_server_bin, conf_file.to_s, "--port", real_port.to_s, "--sentinel", ] end def spawn generate_conf super end end SENTINELS = [ SentinelManager.new( "redis_sentinel_1", port: 26_300, real_port: 26_301, command: [], ), SentinelManager.new( "redis_sentinel_2", port: 26_302, real_port: 26_303, command: [], ), SentinelManager.new( "redis_sentinel_3", port: 26_304, real_port: 26_305, command: [], ), ].freeze class ToxiproxyManager < ServerManager BIN = CACHE_DIR.join("toxiproxy-server") def spawn system(ROOT.join("bin/install-toxiproxy").to_s) unless BIN.exist? super end def command [ ToxiproxyManager::BIN.to_s, "-port", port.to_s, ] end def on_ready Toxiproxy.host = "http://#{host}:#{port}" retries = 3 begin Toxiproxy.populate(proxies) rescue SystemCallError, Net::HTTPError, Net::ProtocolError retries -= 1 if retries > 0 sleep 0.5 retry else raise end end end def proxies [ { name: "redis", upstream: "localhost:#{REDIS.real_port}", listen: ":#{REDIS.port}", }, { name: "redis_tls", upstream: "localhost:#{REDIS.real_tls_port}", listen: ":#{REDIS.tls_port}", }, { name: "redis_replica", upstream: "localhost:#{REDIS_REPLICA.real_port}", listen: ":#{REDIS_REPLICA.port}", }, { name: "redis_replica_2", upstream: "localhost:#{REDIS_REPLICA_2.real_port}", listen: ":#{REDIS_REPLICA_2.port}", }, ] end end class ToxiproxySentinelManager < ToxiproxyManager def proxies sentinels = SENTINELS.map do |sentinel| { name: sentinel.name, upstream: "localhost:#{sentinel.real_port}", listen: ":#{sentinel.port}", } end super + sentinels end end TOXIPROXY = ToxiproxyManager.new( "toxiproxy", port: 8475, command: [], ) TOXIPROXY_SENTINELS = ToxiproxySentinelManager.new( "toxiproxy", port: 8475, command: [], ) TESTS = ServerList.new( TOXIPROXY, REDIS, ) SENTINEL_TESTS = ServerList.new( TOXIPROXY_SENTINELS, REDIS, REDIS_REPLICA, REDIS_REPLICA_2, *SENTINELS, ) BENCHMARK = ServerList.new(REDIS) end redis-rb-redis-client-367ca25/test/test_config.rb000066400000000000000000000011501515654015100217450ustar00rootroot00000000000000# frozen_string_literal: true Megatest.config do |c| c.global_setup do ServerManager.kill_all $stderr.puts "Running test suite with driver: #{RedisClient.default_driver}" end c.job_setup do |_, index| Servers::TESTS.shutdown Servers::SENTINEL_TESTS.shutdown ServerManager.worker_index = index Toxiproxy.host = "http://#{Servers::HOST}:#{Servers::TOXIPROXY.port}" unless Servers.all.prepare puts "worker #{index} failed setup" exit(1) end end c.job_teardown do unless ENV["REDIS_CLIENT_RESTART_SERVER"] == "0" Servers.all.shutdown end end end redis-rb-redis-client-367ca25/test/test_helper.rb000066400000000000000000000002711515654015100217620ustar00rootroot00000000000000# frozen_string_literal: true require_relative "env" Servers.build_redis Servers::SENTINEL_TESTS.shutdown Servers.all = Servers::TESTS class RedisClientTestCase < Megatest::Test end