RESP clients
Point a RESP2 or RESP3 client at the node's RESP port. Its keys live in a space, its lists, hashes, sets and sorted sets are the store's own, and its streams are topics.
--resp <host:port> makes a node speak RESP, the protocol that key-value
servers and their client libraries use, on port 6379 by convention. A client
that speaks RESP2 or RESP3 connects to that address and works on a TessariDB
space. The space is in the same store and the same
transactions as everything else.
tessaridb ./data --serve 0.0.0.0:9080 \
--resp 0.0.0.0:6379 --resp-space shop.orders.cache
The space a connection works on
A RESP connection has one space. --resp-space <namespace>.<database>.<space>
(or TESSARIDB_RESP_SPACE) names the one every connection starts in. A client
can change it with an extension command that any client's raw-command call can
send:
> TESSARIDB.USE shop orders sessions
OK
The name is looked up when the connection next uses it, so a space that does
not exist is reported then: no table named "sessions". A node started
without --resp-space answers every key command with
ERR no space is chosen … send TESSARIDB.USE <namespace> <database> <space>.
SELECT 0 is accepted and any other index is refused,
DB index is out of range. The database number of the protocol has nothing to
map to, and a space is chosen by name.
A cache server's space
For the data a cache server would hold — sessions, rate counters, hot lookups — declare the space in memory:
DEFINE NAMESPACE IF NOT EXISTS shop;
USE NAMESPACE shop;
DEFINE DATABASE IF NOT EXISTS orders;
USE DATABASE orders;
DEFINE SPACE cache IN MEMORY MAX 100000;
An IN MEMORY space is never written to the log or to disk. It is not in a
backup, it is empty after a restart, and it needs MAX, because a cache with
no bound runs out of memory. A durable space, declared without IN MEMORY, is
served the same way and keeps its keys. The choice belongs to the space, not
to the client.
What a value is stored as
A key is the space's key, so SET session:ada … writes cache:'session:ada'.
A value is stored by what it holds:
| A client sends | It is stored as | So |
|---|---|---|
a canonical decimal integer, 41 | an integer | INCR is the store's own atomic increment, and TessariQL reads a number |
any other UTF-8 text, hello | a string | TessariQL reads a string |
anything else, \xff\x00\x01 | bytes | read back byte for byte |
GET answers exactly the bytes that were sent, whichever row applies. The
same key read from TessariQL:
GET cache:'visits';
42 — after SET visits 41 and INCR visits over RESP, the store holds the
number, not the text.
The commands
These are the families served. The replies are the protocol's, in RESP2 or
RESP3 as the connection asked with HELLO:
| Family | Commands |
|---|---|
| strings and counters | GET SET (with EX PX EXAT PXAT NX XX KEEPTTL GET) SETNX SETEX PSETEX GETSET GETDEL GETEX MGET MSET MSETNX APPEND STRLEN GETRANGE SETRANGE SUBSTR LCS INCR INCRBY INCRBYFLOAT DECR DECRBY |
| keys and expiry | DEL UNLINK EXISTS TYPE KEYS SCAN RANDOMKEY RENAME RENAMENX COPY TOUCH OBJECT EXPIRE PEXPIRE EXPIREAT PEXPIREAT EXPIRETIME PEXPIRETIME TTL PTTL PERSIST DBSIZE |
| hashes | HSET HSETNX HMSET HGET HMGET HGETALL HDEL HEXISTS HLEN HKEYS HVALS HSTRLEN HINCRBY HINCRBYFLOAT HRANDFIELD HSCAN, and a field that expires on its own: HEXPIRE HPEXPIRE HEXPIREAT HPEXPIREAT HTTL HPTTL HEXPIRETIME HPEXPIRETIME HPERSIST |
| lists | LPUSH RPUSH LPUSHX RPUSHX LPOP RPOP LRANGE LINDEX LSET LINSERT LLEN LREM LTRIM LPOS LMOVE RPOPLPUSH LMPOP, and blocking BLPOP BRPOP BLMOVE BRPOPLPUSH BLMPOP |
| sets | SADD SREM SMEMBERS SISMEMBER SMISMEMBER SCARD SPOP SRANDMEMBER SMOVE SSCAN SINTER SUNION SDIFF SINTERCARD SINTERSTORE SUNIONSTORE SDIFFSTORE |
| sorted sets | ZADD ZINCRBY ZREM ZSCORE ZMSCORE ZCARD ZCOUNT ZLEXCOUNT ZRANK ZREVRANK ZRANGE ZRANGEBYSCORE ZRANGEBYLEX ZREVRANGE ZREVRANGEBYSCORE ZREVRANGEBYLEX ZRANGESTORE ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREMRANGEBYLEX ZPOPMIN ZPOPMAX ZMPOP ZRANDMEMBER ZSCAN ZINTER ZUNION ZDIFF ZINTERCARD ZINTERSTORE ZUNIONSTORE ZDIFFSTORE, and blocking BZPOPMIN BZPOPMAX BZMPOP |
| transactions | MULTI EXEC DISCARD WATCH UNWATCH |
| streams | XADD XRANGE XREVRANGE XLEN XREAD XTRIM XINFO XGROUP XREADGROUP XACK XPENDING XCLAIM XAUTOCLAIM |
| pub/sub | SUBSCRIBE UNSUBSCRIBE PSUBSCRIBE PUNSUBSCRIBE SSUBSCRIBE SUNSUBSCRIBE PUBLISH SPUBLISH PUBSUB |
| connection | HELLO AUTH PING ECHO SELECT CLIENT RESET QUIT COMMAND INFO TIME, and TESSARIDB.USE |
A hash, list, set or sorted set is one key in the space whose value is the
container. Each command is one atomic statement on that key, not a read
followed by a write. Asking a key for the wrong kind answers WRONGTYPE, as
the protocol does.
A blocking pop waits on the store's commits rather than by polling. It is woken by any writer: another RESP client, or a TessariQL script.
What is not served
These answer ERR unknown command:
- scripting —
EVAL,FUNCTIONand the rest. TessariQL is the store's language, and a script sent to the wire protocol or HTTP runs in one transaction. - administration —
FLUSHALL,CONFIG,SAVE,REPLICAOF,DEBUG,ACLand the like. Users, backups and replication are TessariQL statements. - bit fields and probabilistic keys —
GETBIT,BITFIELD,PFADD. For a distinct count the store hascount(DISTINCT x), andapprox_distinctin bounded memory. - geo commands — the store's shapes are their own engine, with an index and exact predicates.
- cluster-mode commands — a TessariDB cluster is one store. Every node answers for the same space and carries a write to the node that leads it.
Transactions
MULTI … EXEC runs the queued commands as one store transaction: they
commit together or not at all. WATCH makes EXEC refuse when a watched key
changed first. A command the node does not serve, queued inside MULTI,
discards the transaction at EXEC, as the protocol says it should.
Streams are topics
A stream key names a topic in the space's database,
so it must be a plain identifier like signups, never a key such as
orders:eu. XADD appends a message, which TessariQL reads with READ FROM
and a Kafka consumer fetches.
> XADD signups * kind signup user 42
"1792670367016-1"
An ID is <milliseconds appended>-<position>. Both parts only ever grow, so
IDs sort as the protocol requires, and an ID leads straight to its message.
XRANGE and XTRIM … MINID bounded by a time alone are exact.
A group is a topic group. XREADGROUP hands out
messages and holds them until XACK. XPENDING, XCLAIM and XAUTOCLAIM
read and move what is held, and XINFO describes the stream, its groups and
their consumers.
Where the store's rules differ from the protocol's, the store's win:
| You might expect | What happens |
|---|---|
XADD key 1700000000000-1 … with your own ID | refused: a position is decided when the message commits |
XDEL | refused: a topic never has a hole. XTRIM removes from the front, which readers are told about |
XSETID | refused, for the same reason |
| fields in the order sent | fields in name order: a message is an object |
XREADGROUP … NOACK in one step | the read and its acknowledgement, in one transaction |
XCLAIM's IDLE, TIME and RETRYCOUNT options | accepted and not applied |
XGROUP SETID keeping what is pending | it moves the group and forgets what it held |
| a stream on a topic split into partitions | refused by name: a stream is one order |
Pub/sub
PUBLISH reaches the subscribers connected to this node, in memory and never
stored, as the protocol promises. A channel belongs to the space's
database, so two tenants with a news channel never hear each other.
Shard channels (SSUBSCRIBE, SPUBLISH) are a separate set of names, as in
the protocol.
Each subscription is checked against the user's grants: subscribing needs
read on the space and publishing needs write. The check is repeated while
the connection is subscribed, so a revoked grant ends its subscriptions.
A subscriber that stops reading has a bounded queue. When the queue is full, that subscriber is disconnected rather than silently missing messages, and publishers are not slowed down.
Signing in
AUTH user password, or HELLO 3 AUTH user password, signs in as a store
user. The connection then runs under that user's grants, on every command:
> AUTH reporting a-real-password
OK
> GET visits
"43"
> SET price 10
(error) NOPERM a viewer may not write (at 38..57)
> PUBLISH news hello
(error) NOPERM a viewer may not write (at 45..64)
A wrong password is answered WRONGPASS. A command sent before signing in to
a store that has users is answered NOAUTH this store requires a signed-in user.
A single-argument AUTH password signs in as the user default, if the store
has one. On a store with no users at all, every connection may work without
signing in, as on every other surface.
TLS
A node given --tls-cert and --tls-key serves the RESP port over TLS 1.3
with the same certificate as its other surfaces. Point the client at it with
TLS on and the issuing authority given. --require-client-tls refuses to
start a node whose RESP port would otherwise be in the clear. See
encryption in transit.
On a cluster
A RESP client may connect to any node. A write sent to a node that does not lead the space's range is carried to the node that does, and the reply comes back on the same connection.
An IN MEMORY space is held by the node leading its range. That is one copy
for every client, and it is lost on a failover, as a cache server's memory is
lost on a restart.
Next
- Spaces — the key-value engine underneath, with expiry, counters, conditional writes and limits.
- Kafka clients — the other protocol a node speaks.