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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +31 -7
- data/lib/redis_client/circuit_breaker.rb +2 -2
- data/lib/redis_client/config.rb +66 -31
- data/lib/redis_client/connection_mixin.rb +27 -8
- data/lib/redis_client/decorator.rb +2 -2
- data/lib/redis_client/ruby_connection/buffered_io.rb +124 -35
- data/lib/redis_client/ruby_connection/resp3.rb +35 -12
- data/lib/redis_client/ruby_connection.rb +19 -11
- data/lib/redis_client/sentinel_config.rb +16 -1
- data/lib/redis_client/url_config.rb +29 -8
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +54 -12
- metadata +4 -12
- data/Gemfile +0 -22
- data/Gemfile.lock +0 -66
- data/Rakefile +0 -123
- data/redis-client.gemspec +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c3cb528ecb15130b80ce97d00a5e0e45c31edfc6147068516e1a52295a94045
|
|
4
|
+
data.tar.gz: b4517fbd99d0f3997e510b6d0d56c345c1fceb1ad44fcac59cceb28faf4326b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
81
|
+
now = RedisClient.now
|
|
82
82
|
expiry = now - @error_timeout
|
|
83
83
|
@lock.synchronize do
|
|
84
84
|
if @state == :closed
|
data/lib/redis_client/config.rb
CHANGED
|
@@ -12,8 +12,8 @@ class RedisClient
|
|
|
12
12
|
DEFAULT_DB = 0
|
|
13
13
|
|
|
14
14
|
module Common
|
|
15
|
-
attr_reader :db, :
|
|
16
|
-
:connect_timeout, :read_timeout, :write_timeout, :driver, :
|
|
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 =
|
|
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
|
-
"
|
|
163
|
+
url = "unix://#{path}"
|
|
164
|
+
if db != 0
|
|
165
|
+
url = "#{url}?db=#{db}"
|
|
166
|
+
end
|
|
123
167
|
else
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
172
|
+
host
|
|
137
173
|
end
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
@
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
159
|
+
def gets_integer
|
|
160
|
+
int = 0
|
|
161
|
+
offset = @offset
|
|
162
|
+
while true
|
|
163
|
+
chr = @buffer.getbyte(offset)
|
|
96
164
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|