cubby/README.md
2026-04-29 12:37:14 +00:00

129 lines
3.4 KiB
Markdown

# cubby
A tiny in-memory key-value cache shared between processes via a Unix socket.
Stash a value, fetch it from another process. That's it.
## Run the server
```bash
go run ./cmd/cubby # default: /tmp/cubby.sock, owner-only (0600)
go run ./cmd/cubby -socket /tmp/c.sock # custom path
go run ./cmd/cubby -group cache # share with members of "cache" group (0660)
go run ./cmd/cubby -group 1001 # numeric GID also works
```
## Use it from Go
```go
import "cubby"
c, err := cubby.Dial("/tmp/cubby.sock")
if err != nil { ... }
defer c.Close()
c.Set("hello", "world", 0) // no TTL
c.Set("session", "abc", 5*time.Second) // expires in 5s
v, ok, err := c.Get("hello") // ok=false means missing (not an error)
existed, _ := c.Del("hello")
keys, _ := c.Keys()
```
A `Client` is a single connection and isn't safe for concurrent use. For
goroutines, use a `Pool`:
```go
pool := cubby.NewPool("/tmp/cubby.sock", 8) // up to 8 idle conns
defer pool.Close()
err := pool.Do(func(c *cubby.Client) error {
return c.Set("k", "v", 0)
})
```
See `examples/basic` for a runnable example.
## Sharing with a group
By default the socket is `chmod 0600` and chowned to the user running cubby —
only that user can connect. With `-group <name>`, the socket is created `0660`
and chowned to that group, so any user in the group can connect. Everyone
else is still locked out.
To set this up:
```bash
sudo groupadd cache # create the group (one-time)
sudo usermod -aG cache alice # add each user who should connect
sudo usermod -aG cache bob
# users need to log out and back in for new group membership to take effect
go run server.go -group cache
```
## Talk to it
The protocol is line-based, so you can poke at it with `nc -U`:
```
$ nc -U /tmp/cubby.sock
PING
+PONG
SET hello world
+OK
GET hello
$"world"
SET token abc 5
+OK
DEL hello
#1
KEYS
#1
$"token"
QUIT
+BYE
```
## Protocol
**Commands** (one per line, whitespace-separated, values can be Go-quoted to
include spaces or newlines):
| Command | Meaning |
| --- | --- |
| `SET <key> <value> [ttl_seconds]` | Stash a value, optional TTL |
| `GET <key>` | Fetch a value |
| `DEL <key>` | Remove a key |
| `KEYS` | List all live keys |
| `PING` | Health check |
| `QUIT` | Close connection |
**Replies** (single sigil, terminated by `\n`):
| Sigil | Meaning |
| --- | --- |
| `+text` | OK / simple string |
| `-text` | Error |
| `$"value"` | Go-quoted string value |
| `_` | Nil (key not found) |
| `#n` | Count (integer) |
`KEYS` replies with `#N` followed by N `$"..."` lines.
## Design notes
- **Storage** is a single `map[string]entry` behind a `sync.RWMutex`. Plenty
for a process-local cache. Shard by key hash if you ever need more.
- **Expiry** is checked lazily on read and swept once per second.
- **Quoting** uses Go's `strconv.Quote`/`Unquote`, so values can contain
arbitrary bytes including spaces, tabs, and newlines.
- **One goroutine per connection** — idiomatic Go, no event loop needed.
- **Socket permissions** are set via `umask` before `Listen`, so there's no
race where the socket exists with looser bits.
- **Clean shutdown** on `SIGINT`/`SIGTERM` removes the socket file.
## What it doesn't do
No persistence, pub/sub, transactions, or data types beyond strings. It's a
shared in-memory cache. If you need any of that, reach for something bigger.