redis-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +190 -0
  3. data/CHANGELOG.md +3 -0
  4. data/Gemfile +23 -0
  5. data/Gemfile.lock +67 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +347 -0
  8. data/Rakefile +86 -0
  9. data/ext/redis_client/hiredis/extconf.rb +54 -0
  10. data/ext/redis_client/hiredis/hiredis_connection.c +696 -0
  11. data/ext/redis_client/hiredis/vendor/.gitignore +9 -0
  12. data/ext/redis_client/hiredis/vendor/.travis.yml +131 -0
  13. data/ext/redis_client/hiredis/vendor/CHANGELOG.md +364 -0
  14. data/ext/redis_client/hiredis/vendor/CMakeLists.txt +165 -0
  15. data/ext/redis_client/hiredis/vendor/COPYING +29 -0
  16. data/ext/redis_client/hiredis/vendor/Makefile +308 -0
  17. data/ext/redis_client/hiredis/vendor/README.md +664 -0
  18. data/ext/redis_client/hiredis/vendor/adapters/ae.h +130 -0
  19. data/ext/redis_client/hiredis/vendor/adapters/glib.h +156 -0
  20. data/ext/redis_client/hiredis/vendor/adapters/ivykis.h +84 -0
  21. data/ext/redis_client/hiredis/vendor/adapters/libev.h +179 -0
  22. data/ext/redis_client/hiredis/vendor/adapters/libevent.h +175 -0
  23. data/ext/redis_client/hiredis/vendor/adapters/libuv.h +117 -0
  24. data/ext/redis_client/hiredis/vendor/adapters/macosx.h +115 -0
  25. data/ext/redis_client/hiredis/vendor/adapters/qt.h +135 -0
  26. data/ext/redis_client/hiredis/vendor/alloc.c +86 -0
  27. data/ext/redis_client/hiredis/vendor/alloc.h +91 -0
  28. data/ext/redis_client/hiredis/vendor/appveyor.yml +24 -0
  29. data/ext/redis_client/hiredis/vendor/async.c +887 -0
  30. data/ext/redis_client/hiredis/vendor/async.h +147 -0
  31. data/ext/redis_client/hiredis/vendor/async_private.h +75 -0
  32. data/ext/redis_client/hiredis/vendor/dict.c +352 -0
  33. data/ext/redis_client/hiredis/vendor/dict.h +126 -0
  34. data/ext/redis_client/hiredis/vendor/fmacros.h +12 -0
  35. data/ext/redis_client/hiredis/vendor/hiredis-config.cmake.in +13 -0
  36. data/ext/redis_client/hiredis/vendor/hiredis.c +1174 -0
  37. data/ext/redis_client/hiredis/vendor/hiredis.h +336 -0
  38. data/ext/redis_client/hiredis/vendor/hiredis.pc.in +12 -0
  39. data/ext/redis_client/hiredis/vendor/hiredis_ssl-config.cmake.in +13 -0
  40. data/ext/redis_client/hiredis/vendor/hiredis_ssl.h +157 -0
  41. data/ext/redis_client/hiredis/vendor/hiredis_ssl.pc.in +12 -0
  42. data/ext/redis_client/hiredis/vendor/net.c +612 -0
  43. data/ext/redis_client/hiredis/vendor/net.h +56 -0
  44. data/ext/redis_client/hiredis/vendor/read.c +739 -0
  45. data/ext/redis_client/hiredis/vendor/read.h +129 -0
  46. data/ext/redis_client/hiredis/vendor/sds.c +1289 -0
  47. data/ext/redis_client/hiredis/vendor/sds.h +278 -0
  48. data/ext/redis_client/hiredis/vendor/sdsalloc.h +44 -0
  49. data/ext/redis_client/hiredis/vendor/sockcompat.c +248 -0
  50. data/ext/redis_client/hiredis/vendor/sockcompat.h +92 -0
  51. data/ext/redis_client/hiredis/vendor/ssl.c +544 -0
  52. data/ext/redis_client/hiredis/vendor/test.c +1401 -0
  53. data/ext/redis_client/hiredis/vendor/test.sh +78 -0
  54. data/ext/redis_client/hiredis/vendor/win32.h +56 -0
  55. data/lib/redis-client.rb +3 -0
  56. data/lib/redis_client/buffered_io.rb +149 -0
  57. data/lib/redis_client/config.rb +174 -0
  58. data/lib/redis_client/connection.rb +86 -0
  59. data/lib/redis_client/hiredis_connection.rb +78 -0
  60. data/lib/redis_client/pooled.rb +86 -0
  61. data/lib/redis_client/resp3.rb +225 -0
  62. data/lib/redis_client/sentinel_config.rb +134 -0
  63. data/lib/redis_client/version.rb +5 -0
  64. data/lib/redis_client.rb +438 -0
  65. data/redis-client.gemspec +34 -0
  66. metadata +125 -0
@@ -0,0 +1,78 @@
1
+ #!/bin/sh -ue
2
+
3
+ REDIS_SERVER=${REDIS_SERVER:-redis-server}
4
+ REDIS_PORT=${REDIS_PORT:-56379}
5
+ REDIS_SSL_PORT=${REDIS_SSL_PORT:-56443}
6
+ TEST_SSL=${TEST_SSL:-0}
7
+ SKIPS_AS_FAILS=${SKIPS_AS_FAILS-:0}
8
+ SSL_TEST_ARGS=
9
+ SKIPS_ARG=
10
+
11
+ tmpdir=$(mktemp -d)
12
+ PID_FILE=${tmpdir}/hiredis-test-redis.pid
13
+ SOCK_FILE=${tmpdir}/hiredis-test-redis.sock
14
+
15
+ if [ "$TEST_SSL" = "1" ]; then
16
+ SSL_CA_CERT=${tmpdir}/ca.crt
17
+ SSL_CA_KEY=${tmpdir}/ca.key
18
+ SSL_CERT=${tmpdir}/redis.crt
19
+ SSL_KEY=${tmpdir}/redis.key
20
+
21
+ openssl genrsa -out ${tmpdir}/ca.key 4096
22
+ openssl req \
23
+ -x509 -new -nodes -sha256 \
24
+ -key ${SSL_CA_KEY} \
25
+ -days 3650 \
26
+ -subj '/CN=Hiredis Test CA' \
27
+ -out ${SSL_CA_CERT}
28
+ openssl genrsa -out ${SSL_KEY} 2048
29
+ openssl req \
30
+ -new -sha256 \
31
+ -key ${SSL_KEY} \
32
+ -subj '/CN=Hiredis Test Cert' | \
33
+ openssl x509 \
34
+ -req -sha256 \
35
+ -CA ${SSL_CA_CERT} \
36
+ -CAkey ${SSL_CA_KEY} \
37
+ -CAserial ${tmpdir}/ca.txt \
38
+ -CAcreateserial \
39
+ -days 365 \
40
+ -out ${SSL_CERT}
41
+
42
+ 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}"
43
+ fi
44
+
45
+ cleanup() {
46
+ set +e
47
+ kill $(cat ${PID_FILE})
48
+ rm -rf ${tmpdir}
49
+ }
50
+ trap cleanup INT TERM EXIT
51
+
52
+ cat > ${tmpdir}/redis.conf <<EOF
53
+ daemonize yes
54
+ pidfile ${PID_FILE}
55
+ port ${REDIS_PORT}
56
+ bind 127.0.0.1
57
+ unixsocket ${SOCK_FILE}
58
+ EOF
59
+
60
+ if [ "$TEST_SSL" = "1" ]; then
61
+ cat >> ${tmpdir}/redis.conf <<EOF
62
+ tls-port ${REDIS_SSL_PORT}
63
+ tls-ca-cert-file ${SSL_CA_CERT}
64
+ tls-cert-file ${SSL_CERT}
65
+ tls-key-file ${SSL_KEY}
66
+ EOF
67
+ fi
68
+
69
+ cat ${tmpdir}/redis.conf
70
+ ${REDIS_SERVER} ${tmpdir}/redis.conf
71
+
72
+ # Wait until we detect the unix socket
73
+ while [ ! -S "${SOCK_FILE}" ]; do sleep 1; done
74
+
75
+ # Treat skips as failures if directed
76
+ [ "$SKIPS_AS_FAILS" = 1 ] && SKIPS_ARG="--skips-as-fails"
77
+
78
+ ${TEST_PREFIX:-} ./hiredis-test -h 127.0.0.1 -p ${REDIS_PORT} -s ${SOCK_FILE} ${SSL_TEST_ARGS} ${SKIPS_ARG}
@@ -0,0 +1,56 @@
1
+ #ifndef _WIN32_HELPER_INCLUDE
2
+ #define _WIN32_HELPER_INCLUDE
3
+ #ifdef _MSC_VER
4
+
5
+ #include <winsock2.h> /* for struct timeval */
6
+
7
+ #ifndef inline
8
+ #define inline __inline
9
+ #endif
10
+
11
+ #ifndef strcasecmp
12
+ #define strcasecmp stricmp
13
+ #endif
14
+
15
+ #ifndef strncasecmp
16
+ #define strncasecmp strnicmp
17
+ #endif
18
+
19
+ #ifndef va_copy
20
+ #define va_copy(d,s) ((d) = (s))
21
+ #endif
22
+
23
+ #ifndef snprintf
24
+ #define snprintf c99_snprintf
25
+
26
+ __inline int c99_vsnprintf(char* str, size_t size, const char* format, va_list ap)
27
+ {
28
+ int count = -1;
29
+
30
+ if (size != 0)
31
+ count = _vsnprintf_s(str, size, _TRUNCATE, format, ap);
32
+ if (count == -1)
33
+ count = _vscprintf(format, ap);
34
+
35
+ return count;
36
+ }
37
+
38
+ __inline int c99_snprintf(char* str, size_t size, const char* format, ...)
39
+ {
40
+ int count;
41
+ va_list ap;
42
+
43
+ va_start(ap, format);
44
+ count = c99_vsnprintf(str, size, format, ap);
45
+ va_end(ap);
46
+
47
+ return count;
48
+ }
49
+ #endif
50
+ #endif /* _MSC_VER */
51
+
52
+ #ifdef _WIN32
53
+ #define strerror_r(errno,buf,len) strerror_s(buf,len,errno)
54
+ #endif /* _WIN32 */
55
+
56
+ #endif /* _WIN32_HELPER_INCLUDE */
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_client"
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/wait" unless IO.method_defined?(:wait_readable) && IO.method_defined?(:wait_writable)
4
+
5
+ class RedisClient
6
+ class BufferedIO
7
+ EOL = "\r\n".b.freeze
8
+ EOL_SIZE = EOL.bytesize
9
+
10
+ def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
11
+ @io = io
12
+ @buffer = "".b
13
+ @offset = 0
14
+ @chunk_size = chunk_size
15
+ @read_timeout = read_timeout
16
+ @write_timeout = write_timeout
17
+ @blocking_reads = false
18
+ end
19
+
20
+ def close
21
+ @io.to_io.close
22
+ end
23
+
24
+ def closed?
25
+ @io.to_io.closed?
26
+ end
27
+
28
+ def eof?
29
+ @offset >= @buffer.bytesize && @io.eof?
30
+ end
31
+
32
+ def with_timeout(new_timeout)
33
+ new_timeout = false if new_timeout == 0
34
+
35
+ previous_read_timeout = @read_timeout
36
+ previous_blocking_reads = @blocking_reads
37
+
38
+ if new_timeout
39
+ @read_timeout = new_timeout
40
+ else
41
+ @blocking_reads = true
42
+ end
43
+
44
+ begin
45
+ yield
46
+ ensure
47
+ @read_timeout = previous_read_timeout
48
+ @blocking_reads = previous_blocking_reads
49
+ end
50
+ end
51
+
52
+ def skip(offset)
53
+ ensure_remaining(offset)
54
+ @offset += offset
55
+ nil
56
+ end
57
+
58
+ def write(string)
59
+ total = remaining = string.bytesize
60
+ loop do
61
+ case bytes_written = @io.write_nonblock(string, exception: false)
62
+ when Integer
63
+ remaining -= bytes_written
64
+ if remaining > 0
65
+ string = string.byteslice(bytes_written..-1)
66
+ else
67
+ return total
68
+ end
69
+ when :wait_readable
70
+ @io.to_io.wait_readable(@read_timeout) or raise ReadTimeoutError
71
+ when :wait_writable
72
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
73
+ when nil
74
+ raise Errno::ECONNRESET
75
+ else
76
+ raise "Unexpected `write_nonblock` return: #{bytes.inspect}"
77
+ end
78
+ end
79
+ end
80
+
81
+ def getbyte
82
+ ensure_remaining(1)
83
+ byte = @buffer.getbyte(@offset)
84
+ @offset += 1
85
+ byte
86
+ end
87
+
88
+ def gets_chomp
89
+ fill_buffer(false) if @offset >= @buffer.bytesize
90
+ until eol_index = @buffer.index(EOL, @offset)
91
+ fill_buffer(false)
92
+ end
93
+
94
+ line = @buffer.byteslice(@offset, eol_index - @offset)
95
+ @offset = eol_index + EOL_SIZE
96
+ line
97
+ end
98
+
99
+ def read_chomp(bytes)
100
+ ensure_remaining(bytes + EOL_SIZE)
101
+ str = @buffer.byteslice(@offset, bytes)
102
+ @offset += bytes + EOL_SIZE
103
+ str
104
+ end
105
+
106
+ private
107
+
108
+ def ensure_remaining(bytes)
109
+ needed = bytes - (@buffer.bytesize - @offset)
110
+ if needed > 0
111
+ fill_buffer(true, needed)
112
+ end
113
+ end
114
+
115
+ def fill_buffer(strict, size = @chunk_size)
116
+ remaining = size
117
+ empty_buffer = @offset >= @buffer.bytesize
118
+
119
+ loop do
120
+ bytes = if empty_buffer
121
+ @io.read_nonblock([remaining, @chunk_size].max, @buffer, exception: false)
122
+ else
123
+ @io.read_nonblock([remaining, @chunk_size].max, exception: false)
124
+ end
125
+ case bytes
126
+ when String
127
+ if empty_buffer
128
+ @offset = 0
129
+ empty_buffer = false
130
+ else
131
+ @buffer << bytes
132
+ end
133
+ remaining -= bytes.bytesize
134
+ return if !strict || remaining <= 0
135
+ when :wait_readable
136
+ unless @io.to_io.wait_readable(@read_timeout)
137
+ raise ReadTimeoutError unless @blocking_reads
138
+ end
139
+ when :wait_writable
140
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
141
+ when nil
142
+ raise Errno::ECONNRESET
143
+ else
144
+ raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ class RedisClient
6
+ class Config
7
+ DEFAULT_TIMEOUT = 1.0
8
+ DEFAULT_HOST = "localhost"
9
+ DEFAULT_PORT = 6379
10
+ DEFAULT_USERNAME = "default"
11
+ DEFAULT_DB = 0
12
+
13
+ module Common
14
+ attr_reader :db, :username, :password, :id, :ssl, :ssl_params,
15
+ :connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude
16
+
17
+ alias_method :ssl?, :ssl
18
+
19
+ def initialize(
20
+ username: nil,
21
+ password: nil,
22
+ db: nil,
23
+ id: nil,
24
+ timeout: DEFAULT_TIMEOUT,
25
+ read_timeout: timeout,
26
+ write_timeout: timeout,
27
+ connect_timeout: timeout,
28
+ ssl: nil,
29
+ ssl_params: nil,
30
+ driver: :ruby,
31
+ reconnect_attempts: false
32
+ )
33
+ @username = username || DEFAULT_USERNAME
34
+ @password = password
35
+ @db = db || DEFAULT_DB
36
+ @id = id
37
+ @ssl = ssl || false
38
+
39
+ @ssl_params = ssl_params
40
+ @connect_timeout = connect_timeout
41
+ @read_timeout = read_timeout
42
+ @write_timeout = write_timeout
43
+
44
+ @driver = case driver
45
+ when :ruby
46
+ Connection
47
+ when :hiredis
48
+ unless defined?(RedisClient::HiredisConnection)
49
+ require "redis_client/hiredis_connection"
50
+ end
51
+ HiredisConnection
52
+ else
53
+ raise ArgumentError, "Unknown driver #{driver.inspect}, expected one of: `:ruby`, `:hiredis`"
54
+ end
55
+
56
+ reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer)
57
+ @reconnect_attempts = reconnect_attempts
58
+
59
+ @connection_prelude = build_connection_prelude
60
+ end
61
+
62
+ def sentinel?
63
+ false
64
+ end
65
+
66
+ def new_pool(**kwargs)
67
+ kwargs[:timeout] ||= DEFAULT_TIMEOUT
68
+ Pooled.new(self, **kwargs)
69
+ end
70
+
71
+ def new_client(**kwargs)
72
+ RedisClient.new(self, **kwargs)
73
+ end
74
+
75
+ def retry_connecting?(attempt, _error)
76
+ if @reconnect_attempts
77
+ if (pause = @reconnect_attempts[attempt])
78
+ if pause > 0
79
+ sleep(pause)
80
+ end
81
+ return true
82
+ end
83
+ end
84
+ false
85
+ end
86
+
87
+ def hiredis_ssl_context
88
+ @hiredis_ssl_context ||= HiredisConnection::SSLContext.new(
89
+ ca_file: @ssl_params[:ca_file],
90
+ ca_path: @ssl_params[:ca_path],
91
+ cert: @ssl_params[:cert],
92
+ key: @ssl_params[:key],
93
+ hostname: @ssl_params[:hostname],
94
+ )
95
+ end
96
+
97
+ def openssl_context
98
+ @openssl_context ||= begin
99
+ params = @ssl_params.dup || {}
100
+
101
+ cert = params[:cert]
102
+ if cert.is_a?(String)
103
+ cert = File.read(cert) if File.exist?(cert)
104
+ params[:cert] = OpenSSL::X509::Certificate.new(cert)
105
+ end
106
+
107
+ key = params[:key]
108
+ if key.is_a?(String)
109
+ key = File.read(key) if File.exist?(key)
110
+ params[:key] = OpenSSL::PKey.read(key)
111
+ end
112
+
113
+ context = OpenSSL::SSL::SSLContext.new
114
+ context.set_params(params)
115
+ if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
116
+ if context.respond_to?(:verify_hostname) # Missing on JRuby
117
+ context.verify_hostname
118
+ end
119
+ end
120
+ context
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def build_connection_prelude
127
+ prelude = []
128
+ prelude << if @password
129
+ ["HELLO", "3", "AUTH", @username, @password]
130
+ else
131
+ ["HELLO", "3"]
132
+ end
133
+
134
+ if @db && @db != 0
135
+ prelude << ["SELECT", @db.to_s]
136
+ end
137
+ prelude.freeze
138
+ end
139
+ end
140
+
141
+ include Common
142
+
143
+ attr_reader :host, :port, :path
144
+
145
+ def initialize(
146
+ url: nil,
147
+ host: nil,
148
+ port: nil,
149
+ path: nil,
150
+ **kwargs
151
+ )
152
+ uri = url && URI.parse(url)
153
+ if uri
154
+ kwargs[:ssl] = uri.scheme == "rediss" unless kwargs.key?(:ssl)
155
+
156
+ kwargs[:username] ||= uri.user && uri.password
157
+
158
+ kwargs[:password] ||= if uri.user && !uri.password
159
+ URI.decode_www_form_component(uri.user)
160
+ elsif uri&.user && uri&.password
161
+ URI.decode_www_form_component(uri.password)
162
+ end
163
+
164
+ kwargs[:db] ||= Integer(uri.path.delete_prefix("/")) if uri.path && !uri.path.empty?
165
+ end
166
+
167
+ super(**kwargs)
168
+
169
+ @host = host || uri&.host || DEFAULT_HOST
170
+ @port = port || uri&.port || DEFAULT_PORT
171
+ @path = path
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "openssl"
5
+ require "redis_client/buffered_io"
6
+
7
+ class RedisClient
8
+ class Connection
9
+ def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
10
+ socket = if config.path
11
+ UNIXSocket.new(config.path)
12
+ else
13
+ sock = Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
14
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
15
+ sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
16
+ sock
17
+ end
18
+
19
+ if config.ssl
20
+ socket = OpenSSL::SSL::SSLSocket.new(socket, config.openssl_context)
21
+ socket.hostname = config.host
22
+ loop do
23
+ case status = socket.connect_nonblock(exception: false)
24
+ when :wait_readable
25
+ socket.to_io.wait_readable(connect_timeout) or raise ReadTimeoutError
26
+ when :wait_writable
27
+ socket.to_io.wait_writable(connect_timeout) or raise WriteTimeoutError
28
+ when socket
29
+ break
30
+ else
31
+ raise "Unexpected `connect_nonblock` return: #{status.inspect}"
32
+ end
33
+ end
34
+ end
35
+
36
+ @io = BufferedIO.new(
37
+ socket,
38
+ read_timeout: read_timeout,
39
+ write_timeout: write_timeout,
40
+ )
41
+ rescue Errno::ETIMEDOUT => error
42
+ raise ConnectTimeoutError, error.message
43
+ rescue SystemCallError, OpenSSL::SSL::SSLError => error
44
+ raise ConnectionError, error.message
45
+ end
46
+
47
+ def connected?
48
+ !@io.closed?
49
+ end
50
+
51
+ def close
52
+ @io.close
53
+ end
54
+
55
+ def write(command)
56
+ buffer = RESP3.dump(command)
57
+ begin
58
+ @io.write(buffer)
59
+ rescue SystemCallError, IOError => error
60
+ raise ConnectionError, error.message
61
+ end
62
+ end
63
+
64
+ def write_multi(commands)
65
+ buffer = nil
66
+ commands.each do |command|
67
+ buffer = RESP3.dump(command, buffer)
68
+ end
69
+ begin
70
+ @io.write(buffer)
71
+ rescue SystemCallError, IOError => error
72
+ raise ConnectionError, error.message
73
+ end
74
+ end
75
+
76
+ def read(timeout = nil)
77
+ if timeout.nil?
78
+ RESP3.load(@io)
79
+ else
80
+ @io.with_timeout(timeout) { RESP3.load(@io) }
81
+ end
82
+ rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
83
+ raise ConnectionError, error.message
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_client/hiredis_connection.so"
4
+
5
+ class RedisClient
6
+ class HiredisConnection
7
+ class SSLContext
8
+ def initialize(ca_file: nil, ca_path: nil, cert: nil, key: nil, hostname: nil)
9
+ if (error = init(ca_file, ca_path, cert, key, hostname))
10
+ raise error
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
16
+ self.connect_timeout = connect_timeout
17
+ self.read_timeout = read_timeout
18
+ self.write_timeout = write_timeout
19
+
20
+ if config.path
21
+ connect_unix(config.path)
22
+ else
23
+ connect_tcp(config.host, config.port)
24
+ end
25
+
26
+ if config.ssl
27
+ init_ssl(config.hiredis_ssl_context)
28
+ end
29
+ end
30
+
31
+ def connect_timeout=(timeout)
32
+ self.connect_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0
33
+ @connect_timeout = timeout
34
+ end
35
+
36
+ def read_timeout=(timeout)
37
+ self.read_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0
38
+ @read_timeout = timeout
39
+ end
40
+
41
+ def write_timeout=(timeout)
42
+ self.write_timeout_us = timeout ? (timeout * 1_000_000).to_i : 0
43
+ @write_timeout = timeout
44
+ end
45
+
46
+ def read(timeout = nil)
47
+ if timeout.nil?
48
+ _read
49
+ else
50
+ previous_timeout = @read_timeout
51
+ self.read_timeout = timeout
52
+ begin
53
+ _read
54
+ ensure
55
+ self.read_timeout = previous_timeout
56
+ end
57
+ end
58
+ rescue SystemCallError, IOError => error
59
+ raise ConnectionError, error.message
60
+ end
61
+
62
+ def write(command)
63
+ _write(command)
64
+ flush
65
+ rescue SystemCallError, IOError => error
66
+ raise ConnectionError, error.message
67
+ end
68
+
69
+ def write_multi(commands)
70
+ commands.each do |command|
71
+ _write(command)
72
+ end
73
+ flush
74
+ rescue SystemCallError, IOError => error
75
+ raise ConnectionError, error.message
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "connection_pool"
4
+
5
+ class RedisClient
6
+ class Pooled
7
+ EMPTY_HASH = {}.freeze
8
+
9
+ attr_reader :config
10
+
11
+ def initialize(config, **kwargs)
12
+ @config = config
13
+ @pool_kwargs = kwargs
14
+ @pool = new_pool
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def with(options = EMPTY_HASH, &block)
19
+ pool.with(options, &block)
20
+ rescue ConnectionPool::TimeoutError => error
21
+ raise CheckoutTimeoutError, "Couldn't checkout a connection in time: #{error.message}"
22
+ end
23
+ alias_method :then, :with
24
+
25
+ def close
26
+ if @pool
27
+ @mutex.synchronize do
28
+ pool = @pool
29
+ @pool = nil
30
+ pool&.shutdown(&:close)
31
+ end
32
+ end
33
+ nil
34
+ end
35
+
36
+ def size
37
+ pool.size
38
+ end
39
+
40
+ %w(pipelined).each do |method|
41
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
42
+ def #{method}(&block)
43
+ with { |r| r.#{method}(&block) }
44
+ end
45
+ RUBY
46
+ end
47
+
48
+ %w(multi).each do |method|
49
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
50
+ def #{method}(**kwargs, &block)
51
+ with { |r| r.#{method}(**kwargs, &block) }
52
+ end
53
+ RUBY
54
+ end
55
+
56
+ %w(call call_once blocking_call pubsub).each do |method|
57
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
58
+ def #{method}(*args)
59
+ with { |r| r.#{method}(*args) }
60
+ end
61
+ RUBY
62
+ end
63
+
64
+ %w(scan sscan hscan zscan).each do |method|
65
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
66
+ def #{method}(*args, &block)
67
+ unless block_given?
68
+ return to_enum(__callee__, *args)
69
+ end
70
+
71
+ with { |r| r.#{method}(*args, &block) }
72
+ end
73
+ RUBY
74
+ end
75
+
76
+ private
77
+
78
+ def pool
79
+ @pool ||= @mutex.synchronize { new_pool }
80
+ end
81
+
82
+ def new_pool
83
+ ConnectionPool.new(**@pool_kwargs) { @config.new_client }
84
+ end
85
+ end
86
+ end