redis.
Redis driver for iyi
v0.1.0Get started
iyi get github.com/iyilang/redis
iyi get adds it to iyi.mod and fetches its newest version tag. Import it as github.com/iyilang/redis::*.
README
iyi-redis
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).
Requirements
iyi 0.18.0 or later. Tested against Redis 8.2 (Memurai 8.2 on Windows).
Installation
iyi get github.com/iyilang/redis --as redis
Files 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.
Usage
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.
Errors
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.
Pipelines
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]
Transactions
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.
Pub/Sub
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
URLs
redis://localhost:6379/0
redis://:[email protected]/3
redis://user:[email protected]?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.
TLS
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.
Differences from jgaskins/redis
- 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).
Development
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 examples
iyi 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_test
redis/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.