sondahub

sondahub / Redis test server

Redis test server

A server that answers Redis’s commands the way Redis does, already full: the hub’s live activity as streams and pub/sub channels, hashes that follow every device and currency pair, trending hashtags in a sorted set, and an orders stream the hub reads with its own consumer group. Strings, hashes, lists, sets, sorted sets, streams, transactions, RESP2 and RESP3 — on localhost through a small bridge.

Free, nothing to install but the bridge. Each run of the bridge is a server of its own — run several side by side.

Connect

From a Redis client
through the bridge: node bridge.mjs redis [port], then redis://localhost:6479
WebSocket
wss://api.sondahub.com/redis
Protocol
RESP2, and RESP3 after HELLO 3; databases 0–15
Users
default (no password), sonda/sondahub, viewer/viewer
Memory
maxmemory 32 MB, noeviction

The bridge listens on redis://localhost:6479 — off the usual 6379, so a Redis you already run keeps it — or the next free port, and carries every connection to the hub over one WebSocket. One bridge, one server: what one client writes, another reads. The bridge prints who connects, signs in and subscribes, and every ten seconds what each client ran.

Run the bridge
curl -O https://sondahub.com/industrial/bridge.mjs
node bridge.mjs redis

Redis on localhost:6479 → wss://api.sondahub.com/redis — redis://localhost:6479
every run of the bridge is a world of its own
the hub is a Redis server (RESP2 and RESP3) — streams store:*, fleet:*, bank:*… with pub/sub channels of the same names, keys that follow them, and orders:incoming, read by the consumer group sondahub
client 1 connected from 127.0.0.1:50122
client 1 — redis-py
client 1: 41 commands in the last 10 s (XREADGROUP 20, XACK 20, HELLO 1)

In a Redis client

redis-cli
redis-cli -p 6479
> XREAD COUNT 2 BLOCK 5000 STREAMS fleet:telemetry $
> HGETALL bank:fx:EUR/USD
> ZREVRANGE social:trending 0 4 WITHSCORES
> XADD orders:incoming * customer_id 7 total 42.5
> HGETALL store:order:900001
Python (redis-py, a consumer group)
import redis

r = redis.Redis(port=6479, decode_responses=True)
r.xgroup_create('store:orders', 'mine', id='0')
for stream, entries in r.xreadgroup('mine', 'me', {'store:orders': '>'}, count=10, block=5000):
    for id, fields in entries:
        print(id, fields)
        r.xack('store:orders', 'mine', id)
Node (ioredis, pub/sub)
import Redis from 'ioredis'

const sub = new Redis(6479)
await sub.psubscribe('fleet:*')
sub.on('pmessage', (pattern, channel, message) => console.log(channel, JSON.parse(message)))
Go (go-redis)
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6479"})
res, _ := rdb.XRead(ctx, &redis.XReadArgs{Streams: []string{"bank:fx", "$"}, Count: 5, Block: 5 * time.Second}).Result()
fmt.Println(res)

In LockFlare Sonda: a Redis item pointed at localhost:6479 with the bridge running. Or, with no bridge, a WebSocket request to wss://api.sondahub.com/redis, then commands as text messages, one each — PING, HGETALL bank:fx:EUR/USD, XRANGE store:orders - + COUNT 3 — answered in RESP.

What is in it

Database 0 starts full and keeps moving. The feeds are streams — store:orders, store:inventory, fleet:telemetry, fleet:devices, fleet:alerts, bank:fx, bank:transactions, social:posts, social:likes, social:comments, helpdesk:tickets, helpdesk:messages, flights:board, flights:bookings, identity:logins, identity:directory — each entry the event’s fields (nested ones as JSON), capped near 1,000 entries, and the same events go out as JSON on pub/sub channels of the same names.

KeyWhat it holds
<api>:<feed>
stream
Each feed as a stream — store:orders, fleet:telemetry, bank:fx… — the last minute there from the start, capped near 1,000 entries; each entry the event’s fields.
fleet:device:<serial>
hash
The latest reading of each device: type, site, quality and every metric, rewritten as telemetry comes in.
bank:fx:<pair>
hash
The latest rate, bid, ask and change of each currency pair (bank:fx:EUR/USD).
store:product:<id>
hash
Products from the store’s catalog.
store:products:by_price
zset
Those products by price: ZRANGE store:products:by_price 0 50 BYSCORE WITHSCORES.
social:trending
zset
Hashtags by how often they are posted, counting up as posts come in.
flights:board
hash
Flight number to status on the departures board.
store:order:<id>
hash
The orders you add to orders:incoming, with their status as it changes.
orders:incoming
stream
Add an order — XADD orders:incoming * customer_id 7 total 42.5 — and the hub’s consumer group sondahub reads it and acknowledges it: paid in 2 s, shipped in 5, delivered in 9, on store:orders.
orders:dead
stream
What orders:incoming could not take, with the fields as they came and an error field saying why.

Orders

orders:incoming is a stream with a consumer group, sondahub, that the hub reads. Add an order — XADD orders:incoming * customer_id 7 total 42.5, or one field order holding JSON — and the hub reads it, acknowledges it (XINFO GROUPS shows the group caught up), and keeps store:order:<id> up to date as it is paid at 2 s, shipped at 5 and delivered at 9, with each change on the store:orders stream and channel. Over 10,000 it is cancelled. What is not an order goes to orders:dead with an error field.

Commands

FamilyCommands
KeysDEL, UNLINK, EXISTS, TYPE, EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT, TTL, PTTL, EXPIRETIME, PEXPIRETIME, PERSIST, KEYS, SCAN, RANDOMKEY, RENAME, RENAMENX, COPY, MOVE, TOUCH, OBJECT, DBSIZE, FLUSHDB, FLUSHALL
StringsGET, SET, SETNX, SETEX, PSETEX, GETSET, GETDEL, GETEX, MGET, MSET, MSETNX, APPEND, STRLEN, GETRANGE, SETRANGE, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT
HashesHSET, HMSET, HSETNX, HGET, HMGET, HGETALL, HDEL, HEXISTS, HLEN, HSTRLEN, HKEYS, HVALS, HINCRBY, HINCRBYFLOAT, HRANDFIELD, HSCAN
ListsLPUSH, RPUSH, LPUSHX, RPUSHX, LPOP, RPOP, LLEN, LRANGE, LINDEX, LSET, LREM, LTRIM, LINSERT, LPOS, LMOVE, RPOPLPUSH, BLPOP, BRPOP, BLMOVE, BRPOPLPUSH
SetsSADD, SREM, SMEMBERS, SISMEMBER, SMISMEMBER, SCARD, SPOP, SRANDMEMBER, SINTER, SUNION, SDIFF, SINTERSTORE, SUNIONSTORE, SDIFFSTORE, SINTERCARD, SMOVE, SSCAN
Sorted setsZADD, ZREM, ZSCORE, ZMSCORE, ZINCRBY, ZCARD, ZCOUNT, ZLEXCOUNT, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX, ZREVRANGEBYLEX, ZRANK, ZREVRANK, ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX, ZREMRANGEBYSCORE, ZREMRANGEBYRANK, ZREMRANGEBYLEX, ZRANDMEMBER, ZSCAN
StreamsXADD, XLEN, XRANGE, XREVRANGE, XDEL, XTRIM, XREAD, XREADGROUP, XGROUP, XACK, XPENDING, XCLAIM, XAUTOCLAIM, XINFO
Pub/subSUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB
TransactionsMULTI, EXEC, DISCARD, WATCH, UNWATCH
Connection and serverHELLO, AUTH, PING, ECHO, SELECT, CLIENT, RESET, QUIT, COMMAND, INFO, CONFIG, TIME, MEMORY, WAIT, LASTSAVE, SLOWLOG

Replies are Redis’s in both protocols — maps, sets, doubles and pushes in RESP3 — and so are the errors: WRONGTYPE Operation against a key holding the wrong kind of value, ERR wrong number of arguments for 'get' command, BUSYGROUP Consumer Group name already exists, NOGROUP …, EXECABORT …. Blocking commands (BLPOP, BLMOVE, BZPOPMIN, XREAD BLOCK, XREADGROUP BLOCK) wait for another client’s write; WATCH sees every write, the hub’s included.

Users

UserMay
default
no password
Everything — the user a client is until it signs in.
sonda
password sondahub
Everything.
viewer
password viewer
Reads, subscribes, and watches; a write is NOPERM User viewer has no permissions to run the 'set' command.
Anyone else
or a wrong password
WRONGPASS invalid username-password pair or user is disabled.

Questions

Why does Redis need the bridge?

Redis clients open TCP connections, and only HTTP reaches sondahub. So the server runs on the hub and node bridge.mjs redis lends it redis://localhost:6479, carrying every connection over one WebSocket: to redis-cli and every client library it is a Redis on localhost. For trying commands without it, wss://api.sondahub.com/redis takes RESP, or commands typed as text, straight over WebSocket.

Why port 6479?

It is Redis’s 6379 plus 100, so a Redis you already run keeps its port. Run the bridge again and it takes the next free port with a server of its own; give it one — node bridge.mjs redis 6379 — and it takes exactly that or stops.

Do my keys reach other people?

No. The clients of one bridge share one server and its sixteen databases; nobody else sees them, and they are gone when the bridge’s connection is. Over WebSocket each connection is a server of its own.

Is it Redis?

No: it speaks RESP2 and RESP3 and answers Redis 7.2.4’s commands with Redis’s replies and error texts, and says that version to clients that ask. Lua scripts and functions, modules, cluster, replication, persistence, client-side caching, keyspace notifications and MONITOR are not offered, nor geo, HyperLogLog, bitmaps, SORT, the ZUNION/ZINTER/ZDIFF family, LMPOP/ZMPOP and sharded pub/sub.