- Go 100%
| cmd/cubby | ||
| contrib/systemd | ||
| .gitignore | ||
| client.go | ||
| client_test.go | ||
| go.mod | ||
| LICENSE | ||
| README.md | ||
| testhelpers_test.go | ||
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
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
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:
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:
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]entrybehind async.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
umaskbeforeListen, so there's no race where the socket exists with looser bits. - Clean shutdown on
SIGINT/SIGTERMremoves 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.