redis-client 0.18.0 → 0.24.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0903a6d92200235e71b687f16de1ec058ca88979a42cc14d2a783ba5d5605061'
4
- data.tar.gz: 1e772676e34700120320659108e1be493cd4d2067a286056cb8ac87f1c3b51e7
3
+ metadata.gz: 7c3cb528ecb15130b80ce97d00a5e0e45c31edfc6147068516e1a52295a94045
4
+ data.tar.gz: b4517fbd99d0f3997e510b6d0d56c345c1fceb1ad44fcac59cceb28faf4326b5
5
5
  SHA512:
6
- metadata.gz: 66e2d650fe13befeb44b06514e0523d571dfa749f0f787bf7c95126b16d7e54a6b7cb3aa7ed99375f761a302892c2de50c14ad2c011bd42266f4a441a6c9b24b
7
- data.tar.gz: 14b28ce893f07569688acb16a404c2f98bb8cdcd3eaca6d68264a2f1c790cb52c483cd252cc243e8f3f8b4f6e858daf34204d78c7689dcfb2146fc960d45bf3a
6
+ metadata.gz: dbc7fcacc084fcd9d765337ad48a2a8ee413eecfec48f1a166fbd58510299cf92b3e058ce08620055deadeca86464c2c25fe6cddcccab532ab3d2cd741f9cef0
7
+ data.tar.gz: '09164c4ea9f7021d58df97690f78e7940821a44eb7e12cc4c25324bd17e0e784f447443accedbe2299e6d8c0a0a6f15391491465c4c32baaa56286501b77b693'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.24.0
4
+
5
+ - Allow `sentinel_password` to be provided as a `Proc`.
6
+ - Ensure `Config#inspect` and `Config#to_s` do not display stored passwords.
7
+
8
+ # 0.23.2
9
+
10
+ - Fix retry logic not to attempt to retry on an open circuit breaker. Fix #227.
11
+
12
+ # 0.23.1
13
+
14
+ - Fix a potential crash in `hiredis-client` when using subcriptions (`next_event`). See #221.
15
+
16
+ # 0.23.0
17
+
18
+ - Allow `password` to be a callable. Makes it easy to implement short lived password authentication strategies.
19
+ - Fix a thread safety issue in `hiredis-client` when using the `pubsub` client concurrently.
20
+
21
+ # 0.22.2
22
+
23
+ - Fix the sentinel client to properly extend timeout for blocking commands.
24
+ - Fix IPv6 support in `RedisClient::Config#server_url`.
25
+
26
+ # 0.22.1
27
+
28
+ - Fix `ProtocolError: Unknown sigil type` errors when using SSL connection. See #190.
29
+
30
+ # 0.22.0
31
+
32
+ - Made various performance optimizations to the Ruby driver. See #184.
33
+ - Always assume UTF-8 encoding instead of relying on `Encoding.default_external`.
34
+ - Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`. See #187.
35
+
36
+ # 0.21.1
37
+
38
+ - Handle unresolved Sentinel master/replica error when displaying server URL in exceptions. See #182.
39
+
40
+ # 0.21.0
41
+
42
+ - Include redis server URL in most error messages. See #178.
43
+ - Close Redis Sentinel connection after resolving role. See #176.
44
+
45
+ # 0.20.0
46
+
47
+ - Accept `unix://` schemes as well as simple paths in the `url:` config parameter. #170.
48
+ - Make basic usage Ractor compatible.
49
+
50
+ # 0.19.1
51
+
52
+ - Fixed a bug in `hiredis-client` that could cause a crash if interrupted by `Timeout.timeout` or other `Thread#raise` based mecanism.
53
+ - Fixed a GC bug that could cause crashes in `hiredis-client`.
54
+
55
+ # 0.19.0
56
+
57
+ - Revalidate connection in `RedisClient#connected?`
58
+ - Eagerly fail if `db:` isn't an Integer. #151.
59
+
3
60
  # 0.18.0
4
61
 
5
62
  - Expose more connection details such as `host`, `db`, etc on `RedisClient`.
@@ -23,6 +80,7 @@
23
80
  - Discard sockets rather than explictly close them when a fork is detected. #126.
24
81
  - Allow to configure sentinel client via url. #117.
25
82
  - Fix sentinel to preverse the auth/password when refreshing the sentinel list. #107.
83
+ - Added `RedisClient#measure_round_trip_delay` method. #113.
26
84
 
27
85
  # 0.14.1
28
86
 
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # RedisClient
2
2
 
3
- `redis-client` is a simple, low-level, client for Redis 6+.
3
+ `redis-client` is a simple, low-level, client for [Redis](https://redis.io/) 6+, [Valkey](https://valkey.io/) 7+, [KeyDB](https://docs.keydb.dev/),
4
+ and several other databases that implement the same `RESP3` protocol.
4
5
 
5
6
  Contrary to the `redis` gem, `redis-client` doesn't try to map all Redis commands to Ruby constructs,
6
- it merely is a thin wrapper on top of the RESP3 protocol.
7
+ it merely is a thin wrapper on top of the `RESP3` protocol.
7
8
 
8
9
  ## Installation
9
10
 
@@ -62,7 +63,9 @@ redis.call("GET", "mykey")
62
63
 
63
64
  ### Configuration
64
65
 
65
- - `url`: A Redis connection URL, e.g. `redis://example.com:6379/5`, a `rediss://` scheme enable SSL, and the path is interpreted as a database number.
66
+ - `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.
67
+ 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
68
+ have a `unix://` scheme: `unix:///run/redis/foo.sock?db=5`
66
69
  Note that all other configurations take precedence, e.g. `RedisClient.config(url: "redis://localhost:3000", port: 6380)` will connect on port `6380`.
67
70
  - `host`: The server hostname or IP address. Defaults to `"localhost"`.
68
71
  - `port`: The server port. Defaults to `6379`.
@@ -75,7 +78,7 @@ redis.call("GET", "mykey")
75
78
  - `db`: The database to select after connecting, defaults to `0`.
76
79
  - `id` ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`.
77
80
  - `username` Username to authenticate against server, defaults to `"default"`.
78
- - `password` Password to authenticate against server.
81
+ - `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.
79
82
  - `timeout`: The general timeout in seconds, default to `1.0`.
80
83
  - `connect_timeout`: The connection timeout, takes precedence over the general timeout when connecting to the server.
81
84
  - `read_timeout`: The read timeout, takes precedence over the general timeout when reading responses from the server.
@@ -87,7 +90,7 @@ redis.call("GET", "mykey")
87
90
 
88
91
  ### Sentinel support
89
92
 
90
- The client is able to perform automatic failover by using [Redis Sentinel](https://redis.io/docs/manual/sentinel/).
93
+ The client is able to perform automatic failover by using [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/).
91
94
 
92
95
  To connect using Sentinel, use:
93
96
 
@@ -147,7 +150,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
147
150
  redis_config = RedisClient.sentinel(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
148
151
  ```
149
152
 
150
- So you have to provide Sentinel credential and Redis explictly even they are the same
153
+ So you have to provide Sentinel credential and Redis explicitly even they are the same
151
154
 
152
155
  ```ruby
153
156
  # Use 'mysecret' to authenticate against the mymaster instance and sentinel
@@ -330,6 +333,27 @@ end
330
333
  # => ["OK", 1]
331
334
  ```
332
335
 
336
+ #### Exception management
337
+
338
+ The `exception` flag in the `#pipelined` method of `RedisClient` is a feature that modifies the pipeline execution
339
+ behavior. When set to `false`, it doesn't raise an exception when a command error occurs. Instead, it allows the
340
+ pipeline to execute all commands, and any failed command will be available in the returned array. (Defaults to `true`)
341
+
342
+ ```ruby
343
+ results = redis.pipelined(exception: false) do |pipeline|
344
+ pipeline.call("SET", "foo", "bar") # => nil
345
+ pipeline.call("DOESNOTEXIST", 12) # => nil
346
+ pipeline.call("INCR", "baz") # => nil
347
+ end
348
+ # results => ["OK", #<RedisClient::CommandError: ERR unknown command 'DOESNOTEXIST', with args beginning with: '12'>, 2]
349
+
350
+ results.each do |result|
351
+ if result.is_a?(RedisClient::CommandError)
352
+ # Do something with the failed result
353
+ end
354
+ end
355
+ ```
356
+
333
357
  ### Transactions
334
358
 
335
359
  You can use [`MULTI/EXEC` to run a number of commands in an atomic fashion](https://redis.io/topics/transactions).
@@ -501,7 +525,7 @@ recover for a while.
501
525
 
502
526
  [Circuit breakers are a pattern that does exactly that](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern).
503
527
 
504
- Configuation options:
528
+ Configuration options:
505
529
 
506
530
  - `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.
507
531
  - `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.
@@ -63,7 +63,7 @@ class RedisClient
63
63
  private
64
64
 
65
65
  def refresh_state
66
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ now = RedisClient.now
67
67
  @lock.synchronize do
68
68
  if @errors.last < (now - @error_timeout)
69
69
  if @success_threshold > 0
@@ -78,7 +78,7 @@ class RedisClient
78
78
  end
79
79
 
80
80
  def record_error
81
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ now = RedisClient.now
82
82
  expiry = now - @error_timeout
83
83
  @lock.synchronize do
84
84
  if @state == :closed
@@ -12,8 +12,8 @@ class RedisClient
12
12
  DEFAULT_DB = 0
13
13
 
14
14
  module Common
15
- attr_reader :db, :password, :id, :ssl, :ssl_params, :command_builder, :inherit_socket,
16
- :connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude, :protocol,
15
+ attr_reader :db, :id, :ssl, :ssl_params, :command_builder, :inherit_socket,
16
+ :connect_timeout, :read_timeout, :write_timeout, :driver, :protocol,
17
17
  :middlewares_stack, :custom, :circuit_breaker
18
18
 
19
19
  alias_method :ssl?, :ssl
@@ -40,8 +40,13 @@ class RedisClient
40
40
  circuit_breaker: nil
41
41
  )
42
42
  @username = username
43
- @password = password
44
- @db = db || DEFAULT_DB
43
+ @password = password && !password.respond_to?(:call) ? ->(_) { password } : password
44
+ @db = begin
45
+ Integer(db || DEFAULT_DB)
46
+ rescue ArgumentError
47
+ raise ArgumentError, "db: must be an Integer, got: #{db.inspect}"
48
+ end
49
+
45
50
  @id = id
46
51
  @ssl = ssl || false
47
52
 
@@ -65,7 +70,6 @@ class RedisClient
65
70
 
66
71
  reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer)
67
72
  @reconnect_attempts = reconnect_attempts
68
- @connection_prelude = build_connection_prelude
69
73
 
70
74
  circuit_breaker = CircuitBreaker.new(**circuit_breaker) if circuit_breaker.is_a?(Hash)
71
75
  if @circuit_breaker = circuit_breaker
@@ -82,10 +86,47 @@ class RedisClient
82
86
  @middlewares_stack = middlewares_stack
83
87
  end
84
88
 
89
+ def connection_prelude
90
+ prelude = []
91
+ pass = password
92
+ if protocol == 3
93
+ prelude << if pass
94
+ ["HELLO", "3", "AUTH", username, pass]
95
+ else
96
+ ["HELLO", "3"]
97
+ end
98
+ elsif pass
99
+ prelude << if @username && !@username.empty?
100
+ ["AUTH", @username, pass]
101
+ else
102
+ ["AUTH", pass]
103
+ end
104
+ end
105
+
106
+ if @db && @db != 0
107
+ prelude << ["SELECT", @db.to_s]
108
+ end
109
+
110
+ # Deep freeze all the strings and commands
111
+ prelude.map! do |commands|
112
+ commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
113
+ commands.freeze
114
+ end
115
+ prelude.freeze
116
+ end
117
+
118
+ def password
119
+ @password&.call(username)
120
+ end
121
+
85
122
  def username
86
123
  @username || DEFAULT_USERNAME
87
124
  end
88
125
 
126
+ def resolved?
127
+ true
128
+ end
129
+
89
130
  def sentinel?
90
131
  false
91
132
  end
@@ -119,34 +160,23 @@ class RedisClient
119
160
 
120
161
  def server_url
121
162
  if path
122
- "#{path}/#{db}"
163
+ url = "unix://#{path}"
164
+ if db != 0
165
+ url = "#{url}?db=#{db}"
166
+ end
123
167
  else
124
- "redis#{'s' if ssl?}://#{host}:#{port}/#{db}"
125
- end
126
- end
127
-
128
- private
129
-
130
- def build_connection_prelude
131
- prelude = []
132
- if protocol == 3
133
- prelude << if @password
134
- ["HELLO", "3", "AUTH", @username || DEFAULT_USERNAME, @password]
168
+ # add brackets to IPv6 address
169
+ redis_host = if host.count(":") >= 2
170
+ "[#{host}]"
135
171
  else
136
- ["HELLO", "3"]
172
+ host
137
173
  end
138
- elsif @password
139
- prelude << if @username && !@username.empty?
140
- ["AUTH", @username, @password]
141
- else
142
- ["AUTH", @password]
174
+ url = "redis#{'s' if ssl?}://#{redis_host}:#{port}"
175
+ if db != 0
176
+ url = "#{url}/#{db}"
143
177
  end
144
178
  end
145
-
146
- if @db && @db != 0
147
- prelude << ["SELECT", @db.to_s]
148
- end
149
- prelude.freeze
179
+ url
150
180
  end
151
181
  end
152
182
 
@@ -171,15 +201,20 @@ class RedisClient
171
201
  }.compact.merge(kwargs)
172
202
  host ||= url_config.host
173
203
  port ||= url_config.port
204
+ path ||= url_config.path
174
205
  username ||= url_config.username
175
206
  password ||= url_config.password
176
207
  end
177
208
 
178
209
  super(username: username, password: password, **kwargs)
179
210
 
180
- @host = host || DEFAULT_HOST
181
- @port = Integer(port || DEFAULT_PORT)
182
- @path = path
211
+ if @path = path
212
+ @host = nil
213
+ @port = nil
214
+ else
215
+ @host = host || DEFAULT_HOST
216
+ @port = Integer(port || DEFAULT_PORT)
217
+ end
183
218
  end
184
219
  end
185
220
  end
@@ -28,18 +28,19 @@ class RedisClient
28
28
  def call(command, timeout)
29
29
  @pending_reads += 1
30
30
  write(command)
31
- result = read(timeout)
31
+ result = read(connection_timeout(timeout))
32
32
  @pending_reads -= 1
33
33
  if result.is_a?(Error)
34
34
  result._set_command(command)
35
+ result._set_config(config)
35
36
  raise result
36
37
  else
37
38
  result
38
39
  end
39
40
  end
40
41
 
41
- def call_pipelined(commands, timeouts)
42
- exception = nil
42
+ def call_pipelined(commands, timeouts, exception: true)
43
+ first_exception = nil
43
44
 
44
45
  size = commands.size
45
46
  results = Array.new(commands.size)
@@ -48,20 +49,38 @@ class RedisClient
48
49
 
49
50
  size.times do |index|
50
51
  timeout = timeouts && timeouts[index]
51
- result = read(timeout)
52
+ result = read(connection_timeout(timeout))
52
53
  @pending_reads -= 1
53
- if result.is_a?(Error)
54
+
55
+ # A multi/exec command can return an array of results.
56
+ # An error from a multi/exec command is handled in Multi#_coerce!.
57
+ if result.is_a?(Array)
58
+ result.each do |res|
59
+ res._set_config(config) if res.is_a?(Error)
60
+ end
61
+ elsif result.is_a?(Error)
54
62
  result._set_command(commands[index])
55
- exception ||= result
63
+ result._set_config(config)
64
+ first_exception ||= result
56
65
  end
66
+
57
67
  results[index] = result
58
68
  end
59
69
 
60
- if exception
61
- raise exception
70
+ if first_exception && exception
71
+ raise first_exception
62
72
  else
63
73
  results
64
74
  end
65
75
  end
76
+
77
+ def connection_timeout(timeout)
78
+ return timeout unless timeout && timeout > 0
79
+
80
+ # Can't use the command timeout argument as the connection timeout
81
+ # otherwise it would be very racy. So we add the regular read_timeout on top
82
+ # to account for the network delay.
83
+ timeout + config.read_timeout
84
+ end
66
85
  end
67
86
  end
@@ -47,8 +47,8 @@ class RedisClient
47
47
  end
48
48
  ruby2_keywords :with if respond_to?(:ruby2_keywords, true)
49
49
 
50
- def pipelined
51
- @client.pipelined { |p| yield @_pipeline_class.new(p) }
50
+ def pipelined(exception: true)
51
+ @client.pipelined(exception: exception) { |p| yield @_pipeline_class.new(p) }
52
52
  end
53
53
 
54
54
  def multi(**kwargs)
@@ -10,14 +10,80 @@ class RedisClient
10
10
 
11
11
  attr_accessor :read_timeout, :write_timeout
12
12
 
13
- def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
14
- @io = io
15
- @buffer = "".b
16
- @offset = 0
17
- @chunk_size = chunk_size
18
- @read_timeout = read_timeout
19
- @write_timeout = write_timeout
20
- @blocking_reads = false
13
+ if String.method_defined?(:byteindex) # Ruby 3.2+
14
+ ENCODING = Encoding::UTF_8
15
+
16
+ def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
17
+ @io = io
18
+ @buffer = +""
19
+ @offset = 0
20
+ @chunk_size = chunk_size
21
+ @read_timeout = read_timeout
22
+ @write_timeout = write_timeout
23
+ @blocking_reads = false
24
+ end
25
+
26
+ def gets_chomp
27
+ fill_buffer(false) if @offset >= @buffer.bytesize
28
+ until eol_index = @buffer.byteindex(EOL, @offset)
29
+ fill_buffer(false)
30
+ end
31
+
32
+ line = @buffer.byteslice(@offset, eol_index - @offset)
33
+ @offset = eol_index + EOL_SIZE
34
+ line
35
+ end
36
+
37
+ def read_chomp(bytes)
38
+ ensure_remaining(bytes + EOL_SIZE)
39
+ str = @buffer.byteslice(@offset, bytes)
40
+ @offset += bytes + EOL_SIZE
41
+ str
42
+ end
43
+
44
+ private def ensure_line
45
+ fill_buffer(false) if @offset >= @buffer.bytesize
46
+ until @buffer.byteindex(EOL, @offset)
47
+ fill_buffer(false)
48
+ end
49
+ end
50
+ else
51
+ ENCODING = Encoding::BINARY
52
+
53
+ def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
54
+ @io = io
55
+ @buffer = "".b
56
+ @offset = 0
57
+ @chunk_size = chunk_size
58
+ @read_timeout = read_timeout
59
+ @write_timeout = write_timeout
60
+ @blocking_reads = false
61
+ end
62
+
63
+ def gets_chomp
64
+ fill_buffer(false) if @offset >= @buffer.bytesize
65
+ until eol_index = @buffer.index(EOL, @offset)
66
+ fill_buffer(false)
67
+ end
68
+
69
+ line = @buffer.byteslice(@offset, eol_index - @offset)
70
+ @offset = eol_index + EOL_SIZE
71
+ line
72
+ end
73
+
74
+ def read_chomp(bytes)
75
+ ensure_remaining(bytes + EOL_SIZE)
76
+ str = @buffer.byteslice(@offset, bytes)
77
+ @offset += bytes + EOL_SIZE
78
+ str.force_encoding(Encoding::UTF_8)
79
+ end
80
+
81
+ private def ensure_line
82
+ fill_buffer(false) if @offset >= @buffer.bytesize
83
+ until @buffer.index(EOL, @offset)
84
+ fill_buffer(false)
85
+ end
86
+ end
21
87
  end
22
88
 
23
89
  def close
@@ -82,28 +148,35 @@ class RedisClient
82
148
  end
83
149
 
84
150
  def getbyte
85
- ensure_remaining(1)
86
- byte = @buffer.getbyte(@offset)
151
+ unless byte = @buffer.getbyte(@offset)
152
+ ensure_remaining(1)
153
+ byte = @buffer.getbyte(@offset)
154
+ end
87
155
  @offset += 1
88
156
  byte
89
157
  end
90
158
 
91
- def gets_chomp
92
- fill_buffer(false) if @offset >= @buffer.bytesize
93
- until eol_index = @buffer.index(EOL, @offset)
94
- fill_buffer(false)
95
- end
159
+ def gets_integer
160
+ int = 0
161
+ offset = @offset
162
+ while true
163
+ chr = @buffer.getbyte(offset)
96
164
 
97
- line = @buffer.byteslice(@offset, eol_index - @offset)
98
- @offset = eol_index + EOL_SIZE
99
- line
100
- end
165
+ if chr
166
+ if chr == 13 # "\r".ord
167
+ @offset = offset + 2
168
+ break
169
+ else
170
+ int = (int * 10) + chr - 48
171
+ end
172
+ offset += 1
173
+ else
174
+ ensure_line
175
+ return gets_integer
176
+ end
177
+ end
101
178
 
102
- def read_chomp(bytes)
103
- ensure_remaining(bytes + EOL_SIZE)
104
- str = @buffer.byteslice(@offset, bytes)
105
- @offset += bytes + EOL_SIZE
106
- str
179
+ int
107
180
  end
108
181
 
109
182
  private
@@ -117,7 +190,9 @@ class RedisClient
117
190
 
118
191
  def fill_buffer(strict, size = @chunk_size)
119
192
  remaining = size
120
- empty_buffer = @offset >= @buffer.bytesize
193
+ buffer_size = @buffer.bytesize
194
+ start = @offset - buffer_size
195
+ empty_buffer = start >= 0
121
196
 
122
197
  loop do
123
198
  bytes = if empty_buffer
@@ -125,26 +200,40 @@ class RedisClient
125
200
  else
126
201
  @io.read_nonblock([remaining, @chunk_size].max, exception: false)
127
202
  end
203
+
128
204
  case bytes
129
- when String
130
- if empty_buffer
131
- @offset = 0
132
- empty_buffer = false
133
- else
134
- @buffer << bytes
135
- end
136
- remaining -= bytes.bytesize
137
- return if !strict || remaining <= 0
138
205
  when :wait_readable
206
+ # Ref: https://github.com/redis-rb/redis-client/issues/190
207
+ # SSLSocket always clear the provided buffer, even when it didn't
208
+ # read anything. So we need to reset the offset accordingly.
209
+ if empty_buffer && @buffer.empty?
210
+ @offset -= buffer_size
211
+ end
212
+
139
213
  unless @io.to_io.wait_readable(@read_timeout)
140
214
  raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
141
215
  end
142
216
  when :wait_writable
217
+ # Ref: https://github.com/redis-rb/redis-client/issues/190
218
+ # SSLSocket always clear the provided buffer, even when it didn't
219
+ # read anything. So we need to reset the offset accordingly.
220
+ if empty_buffer && @buffer.empty?
221
+ @offset -= buffer_size
222
+ end
223
+
143
224
  @io.to_io.wait_writable(@write_timeout) or raise(WriteTimeoutError, "Waited #{@write_timeout} seconds")
144
225
  when nil
145
226
  raise EOFError
146
227
  else
147
- raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
228
+ if empty_buffer
229
+ @offset = start
230
+ empty_buffer = false
231
+ @buffer.force_encoding(ENCODING) unless @buffer.encoding == ENCODING
232
+ else
233
+ @buffer << bytes.force_encoding(ENCODING)
234
+ end
235
+ remaining -= bytes.bytesize
236
+ return if !strict || remaining <= 0
148
237
  end
149
238
  end
150
239
  end