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.
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 -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
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)
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)))
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.
| Key | What 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
| Family | Commands |
|---|---|
| Keys | DEL, UNLINK, EXISTS, TYPE, EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT, TTL, PTTL, EXPIRETIME, PEXPIRETIME, PERSIST, KEYS, SCAN, RANDOMKEY, RENAME, RENAMENX, COPY, MOVE, TOUCH, OBJECT, DBSIZE, FLUSHDB, FLUSHALL |
| Strings | GET, SET, SETNX, SETEX, PSETEX, GETSET, GETDEL, GETEX, MGET, MSET, MSETNX, APPEND, STRLEN, GETRANGE, SETRANGE, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT |
| Hashes | HSET, HMSET, HSETNX, HGET, HMGET, HGETALL, HDEL, HEXISTS, HLEN, HSTRLEN, HKEYS, HVALS, HINCRBY, HINCRBYFLOAT, HRANDFIELD, HSCAN |
| Lists | LPUSH, RPUSH, LPUSHX, RPUSHX, LPOP, RPOP, LLEN, LRANGE, LINDEX, LSET, LREM, LTRIM, LINSERT, LPOS, LMOVE, RPOPLPUSH, BLPOP, BRPOP, BLMOVE, BRPOPLPUSH |
| Sets | SADD, SREM, SMEMBERS, SISMEMBER, SMISMEMBER, SCARD, SPOP, SRANDMEMBER, SINTER, SUNION, SDIFF, SINTERSTORE, SUNIONSTORE, SDIFFSTORE, SINTERCARD, SMOVE, SSCAN |
| Sorted sets | ZADD, ZREM, ZSCORE, ZMSCORE, ZINCRBY, ZCARD, ZCOUNT, ZLEXCOUNT, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX, ZREVRANGEBYLEX, ZRANK, ZREVRANK, ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX, ZREMRANGEBYSCORE, ZREMRANGEBYRANK, ZREMRANGEBYLEX, ZRANDMEMBER, ZSCAN |
| Streams | XADD, XLEN, XRANGE, XREVRANGE, XDEL, XTRIM, XREAD, XREADGROUP, XGROUP, XACK, XPENDING, XCLAIM, XAUTOCLAIM, XINFO |
| Pub/sub | SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB |
| Transactions | MULTI, EXEC, DISCARD, WATCH, UNWATCH |
| Connection and server | HELLO, 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
| User | May |
|---|---|
| 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.