129 lines
3.4 KiB
Markdown
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.
|