redis-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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