Redis client
Talk to Redis from Scarlet, with a typed result for every one of its 275 commands.
A first program
Add the client to your project as a git submodule:
git submodule add https://github.com/scarletindustries/redis_client lib/redis
Keep hyphens out of the directory name, because Scarlet reads an import path as names and a hyphen as a minus sign. With Redis running locally, this stores a greeting and reads it back:
import scarlet/result
import ./lib/redis/redis.{Conn}
import ./lib/redis/strings
fn greet(c Conn) Result(Nil, redis.RedisError) {
_ <- result.then(strings.set(c, 'greeting', 'hello'))
value <- result.then(strings.get(c, 'greeting'))
println(value or '(nil)')
Ok(Nil)
}
pub fn main() {
redis.with_conn('127.0.0.1', 6379, greet)
}
with_conn closes the connection when greet returns. Each
<- line stops the function at the first command that fails.
Errors
Every command returns a Result, and the error says what kind of failure it was:
pub type RedisError {
Net(err NetError) // the connection failed, so open a new one
Server(message String) // Redis refused the command, the connection is fine
Protocol(why String) // the reply made no sense, so drop the connection
Misuse(why String) // the call was wrong before anything was sent
Pool(fault PoolFault) // the pool has ended
Tls(err TlsError)
}
fn read_greeting(c Conn) String {
match strings.get(c, 'greeting') {
Ok(Some(text)) -> text
Ok(None) -> 'nobody has set a greeting yet'
Err(Server(message)) -> 'Redis refused: ${message}'
Err(Net(_)) -> 'the connection is gone, so reconnect'
Err(e) -> redis.show_error(e)
}
}
Connecting
A hosted Redis usually comes as a URL. rediss:// is encrypted with TLS,
redis:// is not, and a path such as /2 picks the database.
redis.with_conn_url('rediss://default:s3cret@cache.example.com:6380/2', greet)
connect, connect_tls, connect_auth and
connect_tls_auth take the parts separately. The client needs Redis 6 or
newer, because it speaks RESP3.
Pools
A plain connection belongs to the process that opened it. When many processes need
Redis, start a pool, which is also a Conn and costs nothing to pass around:
import ./lib/redis/pool
fn handle(p Conn, job String) Nil {
match strings.incr(p, 'processed') {
Ok(n) -> println('${job} done, ${n} so far')
Err(e) -> println(redis.show_error(e))
}
}
fn run(p Conn) Result(Nil, redis.RedisError) {
_ = process.spawn(fn() handle(p, 'job:1'))
_ = process.spawn(fn() handle(p, 'job:2'))
Ok(Nil)
}
pub fn main() {
pool.with_pool('127.0.0.1', 6379, 4, run)
}
Commands on a pool can land on different connections, so wrap a
WATCH or MULTI in pool.with_conn to keep them on
one.
Pipelines
redis.pipeline sends a batch of commands in one write. Against a local
Redis, 500 SETs took 152 ms one at a time and 6 ms as one pipeline.
replies <- result.then(
redis.pipeline(c, [['SET', 'hits', '0'], ['INCR', 'hits'], ['GET', 'hits']]),
)
Each reply is its own Result, in the order the commands were sent:
match replies {
[_, incr, get] -> {
n <- result.then(redis.expect_int(incr))
v <- result.then(redis.expect_bulk(get))
println('${n} increment, value ${v or '(nil)'}')
Ok(Nil)
}
_ -> Err(redis.Protocol('expected three replies'))
}
Pub/sub
Publishing returns how many subscribers got the message:
subscribers <- result.then(pubsub.publish(c, 'news', 'hello'))
A subscribed connection cannot carry ordinary commands, so subscribe takes
it and returns a Subscription. Each next_message returns a new
one along with the event, and the next call must use that.
import ./lib/redis/pubsub.{Message}
fn listen(c Conn) Result(Nil, redis.RedisError) {
sub <- result.then(pubsub.subscribe(c, ['news']))
(rest, event) = pubsub.next_message(sub)
match event {
Ok(Message(channel, payload)) -> println('${channel}: ${payload}')
Ok(_) -> Nil
Err(e) -> println(redis.show_error(e))
}
pubsub.close(rest)
Ok(Nil)
}
Everything else
The commands are grouped into modules the same way Redis groups them:
strings keys hashes lists sets sorted_sets streams pubsub geo
bitmaps hyperloglog arrays transactions scripting connection
server cluster
For a command the client does not wrap, send it yourself:
encoding <- result.then(redis.command(c, ['OBJECT', 'ENCODING', 'user:1']))
stored <- result.then(redis.command_raw(c, [<<'SET'>>, key, jpeg]))