redis 5.4.1 → 6.0.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 +52 -1
- data/README.md +165 -12
- data/lib/redis/client.rb +42 -10
- data/lib/redis/commands/geo.rb +108 -6
- data/lib/redis/commands/hashes.rb +166 -2
- data/lib/redis/commands/keys.rb +5 -1
- data/lib/redis/commands/lists.rb +93 -0
- data/lib/redis/commands/modules/json.rb +530 -0
- data/lib/redis/commands/modules/search/aggregation.rb +418 -0
- data/lib/redis/commands/modules/search/dialect.rb +14 -0
- data/lib/redis/commands/modules/search/field.rb +306 -0
- data/lib/redis/commands/modules/search/hybrid.rb +359 -0
- data/lib/redis/commands/modules/search/index.rb +351 -0
- data/lib/redis/commands/modules/search/index_definition.rb +114 -0
- data/lib/redis/commands/modules/search/miscellaneous.rb +607 -0
- data/lib/redis/commands/modules/search/query.rb +738 -0
- data/lib/redis/commands/modules/search/result.rb +488 -0
- data/lib/redis/commands/modules/search/schema.rb +211 -0
- data/lib/redis/commands/modules/search.rb +19 -0
- data/lib/redis/commands/sets.rb +41 -0
- data/lib/redis/commands/sorted_sets.rb +0 -1
- data/lib/redis/commands/streams.rb +28 -8
- data/lib/redis/commands.rb +30 -10
- data/lib/redis/distributed.rb +271 -1
- data/lib/redis/lib_identity.rb +105 -0
- data/lib/redis/subscribe.rb +1 -1
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +141 -11
- metadata +22 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10e2b37efa4ce556e0a412006412a027693dc20e5f45df8759d2571dec5008ab
|
|
4
|
+
data.tar.gz: 074126dd5ed435ac137b0b946129db11ef16020afd8a65dcbe94b181beaf9198
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5cc54f6d9f95a0d77c2f824569dc9d09506eabf593c9b9d18483bae897258daf1a1fa8e069b79388ac0f8948d288cfe4af3fcea9d3760a3074bc0c4b55cb2fb
|
|
7
|
+
data.tar.gz: a8137ff6826639d927179a78d0017604aa34f592bd9710a777a58db0618a8061eb71aa4ee402fae928b26dc6e43a19c4248181604db17b1a584760a806fedadf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 6.0.0
|
|
2
|
+
|
|
3
|
+
## Breaking changes
|
|
4
|
+
|
|
5
|
+
- Use RESP3 (`HELLO 3`) by default; pass `protocol: 2` to keep RESP2. `GEOPOS` and
|
|
6
|
+
`GEOSEARCH`/`GEORADIUS` with `WITHCOORD` now return coordinates as `Float` instead of `String`.
|
|
7
|
+
See [specs/migration-resp3.md](specs/migration-resp3.md). (#1351)
|
|
8
|
+
- Require Ruby 3.2+. (#1353, #1365)
|
|
9
|
+
|
|
10
|
+
## New features
|
|
11
|
+
|
|
12
|
+
- Add support for the Redis Query Engine (RediSearch, `FT.*`): `ft_create`, `ft_alter`,
|
|
13
|
+
`ft_dropindex`, `ft_info`, `ft_search`, `ft_aggregate`, vector and hybrid search, suggestions,
|
|
14
|
+
dictionaries, synonyms, aliases, and spellcheck. Not supported by `Redis::Distributed`.
|
|
15
|
+
(#1356, #1359, #1360, #1361)
|
|
16
|
+
- Add support for the JSON module: `json_set`, `json_get`, `json_mset`, `json_mget`, `json_del`,
|
|
17
|
+
`json_forget`, `json_clear`, `json_merge`, `json_arr*`, `json_obj*`, `json_str*`,
|
|
18
|
+
`json_numincrby`, `json_toggle`, `json_type`, `json_debug_memory`. (#1346, #1347, #1348, #1349)
|
|
19
|
+
- Add `lmovem` and `blmovem` (Redis 8.10). (#1363)
|
|
20
|
+
- Add `sunioncard` and `sdiffcard` (Redis 8.10). (#1357)
|
|
21
|
+
- `xread` and `xreadgroup` now accept `max_count:` and `max_size:` (Redis 8.10). (#1358)
|
|
22
|
+
- Add `hexpire`, `hpexpire`, `httl` and `hpttl` (Redis 7.4). (#1324, #1325, #1331)
|
|
23
|
+
- `hscan` and `hscan_each` now accept `novalues: true` (Redis 7.4). (#1327)
|
|
24
|
+
- Add `geosearch` and `geosearchstore` (Redis 6.2); geo commands are now available on
|
|
25
|
+
`Redis::Distributed`. (#1342)
|
|
26
|
+
- `geoadd` now accepts `nx:`, `xx:` and `ch:`; geo radius searches accept `count_any:`; `xadd`
|
|
27
|
+
accepts `limit:` (Redis 6.2). (#1345)
|
|
28
|
+
- `Redis::Distributed` now implements `hscan`, `hscan_each` and `hstrlen`. (#1319)
|
|
29
|
+
- Identify the client to the server via `CLIENT SETINFO` (`lib-name=redis-rb`,
|
|
30
|
+
`lib-ver=<version>`). Extend the reported name with `driver_info:`, or disable with
|
|
31
|
+
`driver_info: false`. See the README "Client identification" section. (#1369)
|
|
32
|
+
|
|
33
|
+
## Experimental
|
|
34
|
+
|
|
35
|
+
- Add `himport_prepare`, `himport_set`, `himport_discard` and `himport_discard_all`
|
|
36
|
+
(Redis 8.10 `HIMPORT`). Lost fieldsets are re-prepared and retried automatically; disable with
|
|
37
|
+
`himport_auto_prepare: false`. The API may change in a future minor release. See the README
|
|
38
|
+
"Bulk hash ingestion (HIMPORT)" section. (#1364)
|
|
39
|
+
|
|
40
|
+
## Bug fixes
|
|
41
|
+
|
|
42
|
+
- Fix `FloatifyPairs` to not re-transform already transformed replies. (#1354)
|
|
43
|
+
- `unlink` now returns `0` for an empty keyset, consistent with `del`. (#1316)
|
|
44
|
+
|
|
45
|
+
## Maintenance
|
|
46
|
+
|
|
47
|
+
- Pin `redis-client` to the exact version `0.30.1` (previously `>= 0.22.0`); includes the
|
|
48
|
+
reply-desynchronization fix from 0.26.4. (#1350, #1352)
|
|
49
|
+
- Maintainership change: `redis-rb` is now maintained by the Redis Ltd company.
|
|
50
|
+
- Run the test suite against prebuilt `redislabs/client-libs-test` Docker images; CI now covers
|
|
51
|
+
MRI 3.2/3.3/3.4/4.0 and TruffleRuby against Redis 7.2–8.10; JRuby removed. (#1317, #1318, #1344)
|
|
52
|
+
- Drop the unused `executables` config from the gemspec. (#1322)
|
|
2
53
|
|
|
3
54
|
# 5.4.1
|
|
4
55
|
|
data/README.md
CHANGED
|
@@ -71,6 +71,97 @@ redis.get("mykey")
|
|
|
71
71
|
All commands, their arguments, and return values are documented and
|
|
72
72
|
available on [RubyDoc.info][rubydoc].
|
|
73
73
|
|
|
74
|
+
## Language and server support
|
|
75
|
+
|
|
76
|
+
redis-rb targets actively supported runtimes on both the language and the server side:
|
|
77
|
+
|
|
78
|
+
- **Ruby:** Ruby 3.2 and newer. See the [Ruby maintenance branches][ruby-branches] page for
|
|
79
|
+
each version's status and dates.
|
|
80
|
+
- **Redis server:** the versions designated for support by Redis. See
|
|
81
|
+
[Supported Redis database versions][redis-versions].
|
|
82
|
+
|
|
83
|
+
## Protocol (RESP3)
|
|
84
|
+
|
|
85
|
+
Starting in 6.0, the client negotiates the [RESP3 protocol][resp3] (`HELLO 3`)
|
|
86
|
+
by default. Command return values are unchanged from 5.x, with one exception:
|
|
87
|
+
`GEOPOS` and `GEOSEARCH`/`GEORADIUS` with `WITHCOORD` now return coordinates as
|
|
88
|
+
`Float` instead of `String`.
|
|
89
|
+
|
|
90
|
+
To keep the previous RESP2 behavior, pass `protocol: 2`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
redis = Redis.new(protocol: 2)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Servers without RESP3 support (Redis < 6.0, or anything replying `NOPROTO`) are
|
|
97
|
+
detected on connect and the client transparently falls back to RESP2, so no
|
|
98
|
+
configuration is needed for older servers.
|
|
99
|
+
|
|
100
|
+
### Why RESP3 is the default
|
|
101
|
+
|
|
102
|
+
RESP3's richer wire types let the parser deliver replies already in their final
|
|
103
|
+
Ruby shape. Under RESP2, structured replies arrive as flat arrays of bulk
|
|
104
|
+
strings and the client re-shapes them in Ruby: `HGETALL` turns a flat
|
|
105
|
+
`[field, value, field, value, ...]` array into a `Hash`, and sorted-set scores
|
|
106
|
+
are converted from `String` to `Float` pair by pair. Under RESP3 the server
|
|
107
|
+
tags these replies as native maps and doubles, so the final `Hash` and `Float`
|
|
108
|
+
values come straight out of the parser and the Ruby-side re-shaping pass
|
|
109
|
+
disappears entirely.
|
|
110
|
+
|
|
111
|
+
How much that saves depends on where parsing happens. In our benchmarks
|
|
112
|
+
([bench/resp_comparison.rb](bench/resp_comparison.rb), Ruby 3.4, 100-element
|
|
113
|
+
replies), hash reads (`HGETALL`) consistently use ~10–25% less client CPU per
|
|
114
|
+
call on both drivers. With the [hiredis driver](#hiredis-binding), where
|
|
115
|
+
parsing runs in C, sorted-set reads with scores gain up to 16% throughput and
|
|
116
|
+
~20% less CPU per call on top of that; with the pure-Ruby driver they are
|
|
117
|
+
unchanged, since the parser then spends in Ruby roughly what the re-shaping
|
|
118
|
+
pass used to cost. Simple string commands and stream commands are unaffected
|
|
119
|
+
either way — their reply shapes are the same in both protocols. In short:
|
|
120
|
+
RESP3 is never slower where it matters, and it pairs best with hiredis — that
|
|
121
|
+
combination moves all reply construction out of Ruby and into C.
|
|
122
|
+
|
|
123
|
+
Beyond performance, RESP3 unlocks protocol capabilities RESP2 simply doesn't
|
|
124
|
+
have. The most important is out-of-band **push messages**: the server can send
|
|
125
|
+
notifications on a connection without the client asking, which is the
|
|
126
|
+
foundation for server-assisted client-side caching (`CLIENT TRACKING`
|
|
127
|
+
invalidation events), pub/sub messages delivered over the regular command
|
|
128
|
+
connection instead of a dedicated one, and other server-initiated
|
|
129
|
+
notifications. Defaulting to RESP3 in 6.0 lays the groundwork for building
|
|
130
|
+
these features in future releases without another protocol migration.
|
|
131
|
+
|
|
132
|
+
See [the RESP3 migration guide](specs/migration-resp3.md) for full details.
|
|
133
|
+
|
|
134
|
+
## Client identification
|
|
135
|
+
|
|
136
|
+
On connect the client identifies itself to the server with `CLIENT SETINFO`, so
|
|
137
|
+
`redis-rb` and its version are visible in `CLIENT LIST` and `CLIENT INFO`:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
lib-name=redis-rb lib-ver=<Redis::VERSION>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Libraries built on top of `redis-rb` can add their own identity with
|
|
144
|
+
`driver_info:`, which is reported alongside it. The recommended suffix format
|
|
145
|
+
is `<name>_v<version>`, following the convention used by the official client
|
|
146
|
+
libraries:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Redis.new(driver_info: "my-gem_v#{MyGem::VERSION}")
|
|
150
|
+
# reported as: lib-name=redis-rb(my-gem_v1.0.0) lib-ver=<Redis::VERSION>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`driver_info:` also accepts an array, joined with `;` (the conventional
|
|
154
|
+
delimiter for multiple suffixes). It extends the reported name rather than
|
|
155
|
+
replacing it, so `redis-rb` stays identifiable either way. Runs of characters
|
|
156
|
+
the server would reject (spaces, non-printable bytes) and of the parentheses
|
|
157
|
+
that delimit the suffix are each replaced with a single `_`, or dropped at the
|
|
158
|
+
edges of the value.
|
|
159
|
+
|
|
160
|
+
Servers older than 7.2 don't support `CLIENT SETINFO`; they reject it, the error
|
|
161
|
+
is ignored, and the connection is used as normal. If a proxy or server can't
|
|
162
|
+
tolerate the command at all, pass `driver_info: false` to disable client
|
|
163
|
+
identification entirely.
|
|
164
|
+
|
|
74
165
|
## Connection Pooling and Thread safety
|
|
75
166
|
|
|
76
167
|
The client does not provide connection pooling. Each `Redis` instance
|
|
@@ -94,7 +185,7 @@ MyApp.redis.incr("some-counter")
|
|
|
94
185
|
## Sentinel support
|
|
95
186
|
|
|
96
187
|
The client is able to perform automatic failover by using [Redis
|
|
97
|
-
Sentinel](http://redis.io/topics/sentinel).
|
|
188
|
+
Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+
|
|
98
189
|
if you want to use this feature.
|
|
99
190
|
|
|
100
191
|
To connect using Sentinel, use:
|
|
@@ -114,13 +205,13 @@ and `slave`. When the role is `slave`, the client will try to connect to a
|
|
|
114
205
|
random slave of the specified master. If a role is not specified, the client
|
|
115
206
|
will connect to the master.
|
|
116
207
|
|
|
117
|
-
* When using
|
|
208
|
+
* When using Sentinel support, you need to specify a list of sentinels to
|
|
118
209
|
connect to. The list does not need to enumerate all your Sentinel instances,
|
|
119
210
|
but a few so that if one is down the client will try the next one. The client
|
|
120
211
|
is able to remember the last Sentinel that was able to reply correctly and will
|
|
121
|
-
use it for the next
|
|
212
|
+
use it for the next request.
|
|
122
213
|
|
|
123
|
-
To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication.
|
|
214
|
+
To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) with Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication.
|
|
124
215
|
|
|
125
216
|
```ruby
|
|
126
217
|
SENTINELS = [{ host: '127.0.0.1', port: 26380},
|
|
@@ -129,7 +220,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380},
|
|
|
129
220
|
redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, sentinel_username: 'appuser', sentinel_password: 'mysecret', role: :master)
|
|
130
221
|
```
|
|
131
222
|
|
|
132
|
-
If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not*
|
|
223
|
+
If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* use those credentials.
|
|
133
224
|
|
|
134
225
|
```ruby
|
|
135
226
|
# Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
|
|
@@ -139,7 +230,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
|
|
|
139
230
|
redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
|
|
140
231
|
```
|
|
141
232
|
|
|
142
|
-
So you have to provide Sentinel
|
|
233
|
+
So you have to provide Sentinel credentials and Redis explicitly even if they are the same.
|
|
143
234
|
|
|
144
235
|
```ruby
|
|
145
236
|
# Use 'mysecret' to authenticate against the mymaster instance and sentinel
|
|
@@ -149,7 +240,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
|
|
|
149
240
|
redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret')
|
|
150
241
|
```
|
|
151
242
|
|
|
152
|
-
Also the `name`, `password`, `username
|
|
243
|
+
Also, the `name`, `password`, `username`, and `db` for the Redis instance can be passed as a URL:
|
|
153
244
|
|
|
154
245
|
```ruby
|
|
155
246
|
redis = Redis.new(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master)
|
|
@@ -251,6 +342,70 @@ incr.value
|
|
|
251
342
|
# => 1
|
|
252
343
|
```
|
|
253
344
|
|
|
345
|
+
## Bulk hash ingestion (HIMPORT)
|
|
346
|
+
|
|
347
|
+
> **Experimental:** HIMPORT support is experimental. The client API (method
|
|
348
|
+
> signatures, reply aggregation on cluster, and the automatic re-prepare
|
|
349
|
+
> behavior) may change in a future minor release without a major version bump.
|
|
350
|
+
|
|
351
|
+
Redis 8.10 adds the `HIMPORT` command family for loading many hashes that share
|
|
352
|
+
the same set of field names: register the field names once with
|
|
353
|
+
`himport_prepare`, then create each hash by sending only its values. Keys
|
|
354
|
+
written this way are regular hashes — every hash command works on them.
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
redis.himport_prepare("users", ["name", "email", "age"])
|
|
358
|
+
redis.himport_set("user:1", "users", ["alice", "alice@example.com", "25"])
|
|
359
|
+
redis.himport_set("user:2", "users", ["bob", "bob@example.com", "30"])
|
|
360
|
+
redis.himport_discard("users") # => 1
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Values pair positionally with the prepared fields. Note that hash enumeration
|
|
364
|
+
order (`HGETALL`, `HKEYS`) is not guaranteed to match the prepare order.
|
|
365
|
+
|
|
366
|
+
### Fieldsets are connection state
|
|
367
|
+
|
|
368
|
+
A prepared fieldset lives in the server-side session of the physical connection
|
|
369
|
+
that prepared it: it is invisible to other connections and destroyed by a
|
|
370
|
+
disconnect or `RESET`. A `himport_set` on a connection without the fieldset
|
|
371
|
+
fails with `ERR no such fieldset`.
|
|
372
|
+
|
|
373
|
+
Because a `Redis` instance transparently replaces a dead connection (see
|
|
374
|
+
[Reconnections](#reconnections)), the client keeps the last schema prepared for
|
|
375
|
+
each fieldset name and, when a `himport_set` reports the fieldset is gone,
|
|
376
|
+
re-prepares it and retries the command once. Explicitly discarded fieldsets are
|
|
377
|
+
never restored. To keep the fieldset lifecycle fully explicit instead, disable
|
|
378
|
+
the recovery:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
redis = Redis.new(himport_auto_prepare: false)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
For the highest ingestion throughput, send the `PREPARE` and its `SET`s as one
|
|
385
|
+
pipeline — a single batch always executes on a single connection:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
redis.pipelined do |pipeline|
|
|
389
|
+
pipeline.himport_prepare("users", ["name", "email", "age"])
|
|
390
|
+
rows.each { |id, row| pipeline.himport_set("user:#{id}", "users", row) }
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
If the `PREPARE` in a batch fails, every `SET` in it fails with
|
|
395
|
+
`no such fieldset` — the `PREPARE` error is the root cause. Note that the
|
|
396
|
+
automatic re-prepare applies to direct calls only, not to commands inside
|
|
397
|
+
`pipelined`/`multi` blocks.
|
|
398
|
+
|
|
399
|
+
When using the `connection_pool` gem, each checkout may hand you a different
|
|
400
|
+
underlying connection: run `himport_prepare` and its `himport_set` calls within
|
|
401
|
+
one checkout (`pool.with { |redis| ... }`), ideally as one pipelined block.
|
|
402
|
+
|
|
403
|
+
With `Redis::Distributed`, `himport_prepare`, `himport_discard` and
|
|
404
|
+
`himport_discard_all` fan out to every ring node and return an array with one
|
|
405
|
+
reply per node; `himport_set` routes by key. With `Redis::Cluster`, the same
|
|
406
|
+
commands fan out to every master node and return a single aggregated reply,
|
|
407
|
+
matching the standalone API.
|
|
408
|
+
|
|
254
409
|
## Error Handling
|
|
255
410
|
|
|
256
411
|
In general, if something goes wrong you'll get an exception. For example, if
|
|
@@ -421,11 +576,6 @@ the client object, specify hiredis:
|
|
|
421
576
|
redis = Redis.new(driver: :hiredis)
|
|
422
577
|
```
|
|
423
578
|
|
|
424
|
-
## Testing
|
|
425
|
-
|
|
426
|
-
This library is tested against recent Ruby and Redis versions.
|
|
427
|
-
Check [Github Actions][gh-actions-link] for the exact versions supported.
|
|
428
|
-
|
|
429
579
|
## See Also
|
|
430
580
|
|
|
431
581
|
- [async-redis](https://github.com/socketry/async-redis) — An [async](https://github.com/socketry/async) compatible Redis client.
|
|
@@ -451,3 +601,6 @@ requests.
|
|
|
451
601
|
[gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
|
|
452
602
|
[gh-actions-link]: https://github.com/redis/redis-rb/actions
|
|
453
603
|
[rubydoc]: https://rubydoc.info/gems/redis
|
|
604
|
+
[resp3]: https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md
|
|
605
|
+
[ruby-branches]: https://www.ruby-lang.org/en/downloads/branches/
|
|
606
|
+
[redis-versions]: https://redis.io/docs/latest/operate/rc/databases/version-management/#supported-database-versions
|
data/lib/redis/client.rb
CHANGED
|
@@ -20,12 +20,15 @@ class Redis
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
class << self
|
|
23
|
-
def config(**kwargs)
|
|
24
|
-
super(protocol:
|
|
23
|
+
def config(protocol: 3, **kwargs)
|
|
24
|
+
super(protocol: protocol, **kwargs,
|
|
25
|
+
driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]))
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def sentinel(**kwargs)
|
|
28
|
-
super(protocol:
|
|
28
|
+
def sentinel(protocol: 3, **kwargs)
|
|
29
|
+
super(protocol: protocol, **kwargs,
|
|
30
|
+
driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]),
|
|
31
|
+
client_implementation: ::RedisClient)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def translate_error!(error, mapping: ERROR_MAPPING)
|
|
@@ -33,6 +36,24 @@ class Redis
|
|
|
33
36
|
raise redis_error, error.message, error.backtrace
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Whether +error+ raised during the connection handshake means the server can't speak RESP3
|
|
40
|
+
# (so we should retry the connection as RESP2). Covers Redis < 6 (no HELLO command, surfaced
|
|
41
|
+
# by redis-client as UnsupportedServer) and any server replying NOPROTO to `HELLO 3`.
|
|
42
|
+
def resp3_unsupported?(error)
|
|
43
|
+
return true if error.is_a?(::RedisClient::UnsupportedServer)
|
|
44
|
+
return true if error.is_a?(::RedisClient::CommandError) && error.message.include?("NOPROTO")
|
|
45
|
+
|
|
46
|
+
# Redis::Cluster discovers its topology by connecting to each startup node. When those nodes
|
|
47
|
+
# don't speak RESP3, redis-cluster-client collects the per-node failures and re-raises them
|
|
48
|
+
# wrapped in an InitialSetupError, discarding the original error classes (see
|
|
49
|
+
# RedisClient::Cluster::InitialSetupError.from_errors). Only the concatenated message
|
|
50
|
+
# survives, so match it to still trigger the RESP2 fallback for pre-6.0 clusters. Guarded by
|
|
51
|
+
# defined? because the cluster error class is only loaded with the redis-clustering gem.
|
|
52
|
+
defined?(::RedisClient::Cluster::InitialSetupError) &&
|
|
53
|
+
error.is_a?(::RedisClient::Cluster::InitialSetupError) &&
|
|
54
|
+
(error.message.include?("NOPROTO") || error.message.include?("HELLO command"))
|
|
55
|
+
end
|
|
56
|
+
|
|
36
57
|
private
|
|
37
58
|
|
|
38
59
|
def translate_error_class(error_class, mapping: ERROR_MAPPING)
|
|
@@ -62,6 +83,10 @@ class Redis
|
|
|
62
83
|
config.db
|
|
63
84
|
end
|
|
64
85
|
|
|
86
|
+
def protocol
|
|
87
|
+
config.protocol
|
|
88
|
+
end
|
|
89
|
+
|
|
65
90
|
def host
|
|
66
91
|
config.host unless config.path
|
|
67
92
|
end
|
|
@@ -87,15 +112,16 @@ class Redis
|
|
|
87
112
|
undef_method :call_once_v
|
|
88
113
|
undef_method :blocking_call
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
# We default to RESP3. Servers that can't speak it reject the `HELLO 3` handshake (Redis < 6.0
|
|
116
|
+
# has no HELLO at all; others answer NOPROTO). Re-raise those errors untranslated so they reach
|
|
117
|
+
# Redis#with_protocol_fallback as RedisClient::Error and trigger a transparent rebuild as RESP2 —
|
|
118
|
+
# the same path the sentinel and cluster clients already use. Everything else is translated to
|
|
119
|
+
# the matching Redis::* error.
|
|
96
120
|
def call_v(command, &block)
|
|
97
121
|
super(command, &block)
|
|
98
122
|
rescue ::RedisClient::Error => error
|
|
123
|
+
raise if Client.resp3_unsupported?(error)
|
|
124
|
+
|
|
99
125
|
Client.translate_error!(error)
|
|
100
126
|
end
|
|
101
127
|
|
|
@@ -109,18 +135,24 @@ class Redis
|
|
|
109
135
|
|
|
110
136
|
super(timeout, command, &block)
|
|
111
137
|
rescue ::RedisClient::Error => error
|
|
138
|
+
raise if Client.resp3_unsupported?(error)
|
|
139
|
+
|
|
112
140
|
Client.translate_error!(error)
|
|
113
141
|
end
|
|
114
142
|
|
|
115
143
|
def pipelined(exception: true)
|
|
116
144
|
super
|
|
117
145
|
rescue ::RedisClient::Error => error
|
|
146
|
+
raise if Client.resp3_unsupported?(error)
|
|
147
|
+
|
|
118
148
|
Client.translate_error!(error)
|
|
119
149
|
end
|
|
120
150
|
|
|
121
151
|
def multi(watch: nil)
|
|
122
152
|
super
|
|
123
153
|
rescue ::RedisClient::Error => error
|
|
154
|
+
raise if Client.resp3_unsupported?(error)
|
|
155
|
+
|
|
124
156
|
Client.translate_error!(error)
|
|
125
157
|
end
|
|
126
158
|
|
data/lib/redis/commands/geo.rb
CHANGED
|
@@ -7,9 +7,19 @@ class Redis
|
|
|
7
7
|
#
|
|
8
8
|
# @param [String] key
|
|
9
9
|
# @param [Array] member arguemnts for member or members: longitude, latitude, name
|
|
10
|
-
# @
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# @param [Boolean] nx don't update already existing elements, always add new ones (since Redis 6.2)
|
|
11
|
+
# @param [Boolean] xx only update elements that already exist, never add new ones (since Redis 6.2)
|
|
12
|
+
# @param [Boolean] ch modify the return value to the number of changed elements (since Redis 6.2)
|
|
13
|
+
# @return [Integer] number of elements added to the sorted set, or changed when `ch` is set
|
|
14
|
+
def geoadd(key, *member, nx: false, xx: false, ch: false)
|
|
15
|
+
raise ArgumentError, "can't supply both nx and xx" if nx && xx
|
|
16
|
+
|
|
17
|
+
args = [:geoadd, key]
|
|
18
|
+
args << "NX" if nx
|
|
19
|
+
args << "XX" if xx
|
|
20
|
+
args << "CH" if ch
|
|
21
|
+
args.concat(member)
|
|
22
|
+
send_command(args)
|
|
13
23
|
end
|
|
14
24
|
|
|
15
25
|
# Returns geohash string representing position for specified members of the specified key.
|
|
@@ -28,6 +38,7 @@ class Redis
|
|
|
28
38
|
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
|
|
29
39
|
# or the farthest to the nearest relative to the center
|
|
30
40
|
# @param [Integer] count limit the results to the first N matching items
|
|
41
|
+
# @param [Boolean] count_any return as soon as enough matches found (only with count, since Redis 6.2)
|
|
31
42
|
# @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
|
|
32
43
|
# @return [Array<String>] may be changed with `options`
|
|
33
44
|
def georadius(*args, **geoptions)
|
|
@@ -43,6 +54,7 @@ class Redis
|
|
|
43
54
|
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
|
|
44
55
|
# to the nearest relative to the center
|
|
45
56
|
# @param [Integer] count limit the results to the first N matching items
|
|
57
|
+
# @param [Boolean] count_any return as soon as enough matches found (only with count, since Redis 6.2)
|
|
46
58
|
# @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
|
|
47
59
|
# @return [Array<String>] may be changed with `options`
|
|
48
60
|
def georadiusbymember(*args, **geoptions)
|
|
@@ -61,6 +73,93 @@ class Redis
|
|
|
61
73
|
send_command([:geopos, key, member])
|
|
62
74
|
end
|
|
63
75
|
|
|
76
|
+
# Return the members of a geospatial sorted set that are within the borders of the
|
|
77
|
+
# area specified by a given shape, either a circle (BYRADIUS) or a rectangle (BYBOX),
|
|
78
|
+
# starting from a center point given either by member (FROMMEMBER) or by longitude and
|
|
79
|
+
# latitude (FROMLONLAT). Available since Redis 6.2.
|
|
80
|
+
#
|
|
81
|
+
# @example Search by radius from longitude/latitude
|
|
82
|
+
# redis.geosearch("Sicily", fromlonlat: [15, 37], byradius: [200, "km"], sort: "asc")
|
|
83
|
+
# # => ["Catania", "Palermo"]
|
|
84
|
+
#
|
|
85
|
+
# @example Search by box from an existing member, with extras
|
|
86
|
+
# redis.geosearch("Sicily", frommember: "Catania", bybox: [400, 400, "km"],
|
|
87
|
+
# sort: "asc", withcoord: true, withdist: true)
|
|
88
|
+
# # => [["Catania", "0.0000", ["15.087...", "37.502..."]], ...]
|
|
89
|
+
#
|
|
90
|
+
# @param [String] key
|
|
91
|
+
# @param [String] frommember use the position of the given existing member as the center
|
|
92
|
+
# @param [Array<Numeric>] fromlonlat a [longitude, latitude] pair used as the center
|
|
93
|
+
# @param [Array] byradius a [radius, unit] pair where unit is one of 'm', 'km', 'ft', 'mi'
|
|
94
|
+
# @param [Array] bybox a [width, height, unit] triple where unit is one of 'm', 'km', 'ft', 'mi'
|
|
95
|
+
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest, or vice versa
|
|
96
|
+
# @param [Integer] count limit the results to the first N matching items
|
|
97
|
+
# @param [Boolean] count_any return as soon as enough matches are found (only with count)
|
|
98
|
+
# @param [Boolean] withcoord also return the longitude and latitude of matching items
|
|
99
|
+
# @param [Boolean] withdist also return the distance from the center point
|
|
100
|
+
# @param [Boolean] withhash also return the raw geohash-encoded sorted set score of the item
|
|
101
|
+
# @return [Array<String>] may be changed with WITH* flags
|
|
102
|
+
def geosearch(key, frommember: nil, fromlonlat: nil, byradius: nil, bybox: nil,
|
|
103
|
+
sort: nil, count: nil, count_any: false,
|
|
104
|
+
withcoord: false, withdist: false, withhash: false)
|
|
105
|
+
args = [key]
|
|
106
|
+
args << "FROMMEMBER" << frommember if frommember
|
|
107
|
+
args << "FROMLONLAT" << fromlonlat[0] << fromlonlat[1] if fromlonlat
|
|
108
|
+
args << "BYRADIUS" << byradius[0] << byradius[1] if byradius
|
|
109
|
+
args << "BYBOX" << bybox[0] << bybox[1] << bybox[2] if bybox
|
|
110
|
+
|
|
111
|
+
options = []
|
|
112
|
+
options << "WITHCOORD" if withcoord
|
|
113
|
+
options << "WITHDIST" if withdist
|
|
114
|
+
options << "WITHHASH" if withhash
|
|
115
|
+
|
|
116
|
+
geoarguments = _geoarguments(*args, sort: sort, count: count, count_any: count_any, options: options)
|
|
117
|
+
|
|
118
|
+
send_command([:geosearch, *geoarguments])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Like GEOSEARCH, but stores the result in a destination key. By default the destination
|
|
122
|
+
# is populated with the matching members and their geospatial scores; when STOREDIST is
|
|
123
|
+
# set, the members are stored with their distance from the center point as the score.
|
|
124
|
+
# Available since Redis 6.2.
|
|
125
|
+
#
|
|
126
|
+
# @example Store the three nearest members
|
|
127
|
+
# redis.geosearchstore("nearest", "Sicily",
|
|
128
|
+
# fromlonlat: [15, 37], bybox: [400, 400, "km"], sort: "asc", count: 3)
|
|
129
|
+
# # => 3
|
|
130
|
+
#
|
|
131
|
+
# @example Store distances as scores
|
|
132
|
+
# redis.geosearchstore("distances", "Sicily",
|
|
133
|
+
# fromlonlat: [15, 37], bybox: [400, 400, "km"], storedist: true)
|
|
134
|
+
# # => 3
|
|
135
|
+
#
|
|
136
|
+
# @param [String] destination key to store the result in
|
|
137
|
+
# @param [String] source geospatial sorted set to search
|
|
138
|
+
# @param [String] frommember use the position of the given existing member as the center
|
|
139
|
+
# @param [Array<Numeric>] fromlonlat a [longitude, latitude] pair used as the center
|
|
140
|
+
# @param [Array] byradius a [radius, unit] pair where unit is one of 'm', 'km', 'ft', 'mi'
|
|
141
|
+
# @param [Array] bybox a [width, height, unit] triple where unit is one of 'm', 'km', 'ft', 'mi'
|
|
142
|
+
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest, or vice versa
|
|
143
|
+
# @param [Integer] count limit the results to the first N matching items
|
|
144
|
+
# @param [Boolean] count_any return as soon as enough matches are found (only with count)
|
|
145
|
+
# @param [Boolean] storedist store the distance from the center point as the score
|
|
146
|
+
# @return [Integer] number of elements stored in the destination key
|
|
147
|
+
def geosearchstore(destination, source, frommember: nil, fromlonlat: nil, byradius: nil, bybox: nil,
|
|
148
|
+
sort: nil, count: nil, count_any: false, storedist: false)
|
|
149
|
+
args = [destination, source]
|
|
150
|
+
args << "FROMMEMBER" << frommember if frommember
|
|
151
|
+
args << "FROMLONLAT" << fromlonlat[0] << fromlonlat[1] if fromlonlat
|
|
152
|
+
args << "BYRADIUS" << byradius[0] << byradius[1] if byradius
|
|
153
|
+
args << "BYBOX" << bybox[0] << bybox[1] << bybox[2] if bybox
|
|
154
|
+
|
|
155
|
+
options = []
|
|
156
|
+
options << "STOREDIST" if storedist
|
|
157
|
+
|
|
158
|
+
geoarguments = _geoarguments(*args, sort: sort, count: count, count_any: count_any, options: options)
|
|
159
|
+
|
|
160
|
+
send_command([:geosearchstore, *geoarguments])
|
|
161
|
+
end
|
|
162
|
+
|
|
64
163
|
# Returns the distance between two members of a geospatial index
|
|
65
164
|
#
|
|
66
165
|
# @param [String ]key
|
|
@@ -73,10 +172,13 @@ class Redis
|
|
|
73
172
|
|
|
74
173
|
private
|
|
75
174
|
|
|
76
|
-
def _geoarguments(*args, options: nil, sort: nil, count: nil)
|
|
175
|
+
def _geoarguments(*args, options: nil, sort: nil, count: nil, count_any: false)
|
|
77
176
|
args << sort if sort
|
|
78
|
-
|
|
79
|
-
|
|
177
|
+
if count
|
|
178
|
+
args << 'COUNT' << Integer(count)
|
|
179
|
+
args << 'ANY' if count_any
|
|
180
|
+
end
|
|
181
|
+
args.concat(Array(options))
|
|
80
182
|
args
|
|
81
183
|
end
|
|
82
184
|
end
|