A Redis client for iyi, after jgaskins/redis: RESP over TCP, TLS or a Unix socket, a connection pool, typed commands, pipelines, transactions and Pub/Sub. Written in iyi, with no dependency outside iyi's own library.
Errors are values (SPEC.md III.1): every command answers its result or a
Failure, which is the server's error reply (CommandError) or the
connection's (ConnectionError).
iyi 0.18.0 or later. Tested against Redis 8.2 (Memurai 8.2 on Windows).
iyi get github.com/iyilang/redis --as redisFiles then import redis for the root module (Redis.connect,
Redis.from_env, Redis::VERSION) and its other modules as
redis/redis/...: the short name, then the module's path in the package.
module app
import redis
import redis/redis/errors::{Failure}
def run : Nil | Failure
r = Redis.connect("redis://localhost:6379/0")!
r.set("greeting", "hello", ex: 60)!
puts r.get("greeting")! # => hello
puts r.incr("visits")! # => 1
r.rpush("queue", "a", "b")!
puts r.lrange("queue", 0, -1)! # => ["a", "b"]
nil
end
case result = run
in Nil then puts "done"
in Failure then puts "failed: #{result.message}"
end
Redis.connect and Redis.from_env (the URL in REDIS_URL,
redis://localhost:6379 without one) answer a Client: a pool any number
of tasks can share. Redis::Connection::Connection.connect(url) is a single
connection, for one task.
Every command has a method (redis/commands.iyi lists them, each linked to
its Redis documentation) answering the type its reply has: get a
String?, incr an Int64, expire a Bool, hgetall a
Hash(String, String), zrange_with_scores an
Array(Tuple(String, Float64)), xread a
Hash(String, Array(StreamEntry)). Any other command goes through run,
which answers the raw reply, a Value:
r.run(["OBJECT", "ENCODING", "queue"])! # => "listpack"
scan_each, hscan_each, sscan_each and zscan_each walk a keyspace or
a collection a page at a time without blocking the server.
case r.lpush("greeting", "x")
in Int64 then puts "pushed"
in CommandError then puts it.kind # => WRONGTYPE
in ConnectionError then puts it.message # the connection failed
end
A CommandError leaves the connection usable. A ConnectionError closes
it; the pool opens another for the next command, and a command that fails
on a connection taken from the idle ones (the server closed it meanwhile)
is sent again on another.
Commands queued in the block are sent in one write when it ends, and their
replies answered in order: one round trip for all of them. A command's
error reply is a CommandError in its place.
replies = r.pipeline do |pipe|
pipe.incr("visits")
pipe.set("last", "now")
pipe.lpush("log", "visited")
end! # => [2, "OK", 1]
multi runs the block's commands between MULTI and EXEC. It answers
their replies, or nil when nothing ran: the block called txn.discard, or
left early (a return, a !), which sends nothing at all, or a key the
connection watched changed first.
r.with_connection do |conn|
conn.watch("balance")!
balance = (conn.get("balance")! || "0").to_i
conn.multi { |txn| txn.set("balance", (balance - 10).to_s) }
end! # => ["OK"], or nil when "balance" changed meanwhile
with_connection keeps one connection of the pool for the block, which
WATCH needs. A connection given back still watching (the block ended
before multi) is sent UNWATCH, and one given back on another database
than the URL's (select_db) is closed.
r.subscribe(["news"]) do |message, subscription|
puts "#{message.channel}: #{message.payload}"
subscription.unsubscribe if message.payload == "bye"
end!
The block runs for each message until every channel and pattern is
unsubscribed. psubscribe takes patterns, and message.pattern is the one
a message matched. Client#subscribe opens a connection of its own for the
subscription and closes it after. On a Connection, subscription answers
a Subscription to drive by hand, which another task may unsubscribe while
each reads:
sub = conn.subscription
sub.subscribe("a", "b")!
group do |g|
g.spawn do
sleep(1000)
sub.unsubscribe.or_panic
0
end
sub.each { |message| puts message.payload }!
end
redis://localhost:6379/0
redis://:password@example.com/3
redis://user:password@example.com?connect_timeout=2&max_pool_size=50
rediss://example.com/0?ca_file=/etc/ssl/redis-ca.pem
unix:///var/run/redis.sock?db=2
The path is the database. A user and a password are sent with AUTH. The
query takes:
| Name | Default | |
|---|---|---|
connect_timeout |
5 | seconds to connect, TLS handshake included; 0 waits as long as the system does |
read_timeout |
none | seconds a reply may take; a blocking command waits that long at most too |
write_timeout |
none | seconds a command may take to send |
client_name |
none | sent with CLIENT SETNAME |
ca_file |
none | PEM roots a rediss server's chain must reach, instead of the system's |
db |
0 | the database, for a Unix socket |
max_pool_size |
25 | connections a Client opens at most |
max_idle_pool_size |
25 | connections a Client keeps open when idle |
checkout_timeout |
5 | seconds a Client waits for a free connection; 0 waits as long as it takes |
A name not in the table is refused, so a typo is not silently a default.
rediss:// speaks TLS through iyi's own std/tls. The server's
certificate is always checked: its chain to the system's roots or to those
in ca_file, and its name to the URL's host.
In a debug build of iyi 0.18.0, a TLS handshake made inside a task can
overrun the task's stack and corrupt memory silently
(iyilang/iyi#130); a Client
opens connections in the task that needs one. Build with --release, or
open the connections on the main fiber, until that is fixed.
- Commands answer
T | Failurerather than raising. - Inside
pipelineandmultia command answers nothing (there are no futures); the block's replies are what the call answers. - A transaction's block left early sends nothing, where jgaskins/redis sent
DISCARD. - RESP2 is spoken; RESP3 replies (after a
HELLO 3sent throughrun) are read as the RESP2 shapes nearest them. - Not ported: Cluster, Sentinel, RedisJSON, RediSearch, RedisTimeSeries, RedisGraph, Bloom filters, geo and vector commands, TCP keepalive (iyi's sockets have no keepalive option).
The tests need a server: REDIS_TEST_URL, or database 15 on localhost,
which they empty.
iyi test redis
iyi fmt --check redis.iyi redis tools examplesiyi test runs each named case as a task, so over rediss:// a debug
build meets iyilang/iyi#130 (above). Run the command tests over TLS from a
release build instead:
iyi build --release redis/commands_test.iyi -o commands_test
REDIS_TEST_URL="rediss://localhost:6380/15?ca_file=ca.pem" ./commands_testredis/deferred.iyi, the commands of a pipeline and a transaction, is
written from redis/commands.iyi: after changing a command, run
iyi run tools/write_deferred.iyi. redis/deferred_test.iyi fails while
the two disagree.