Hey, welcome. We appreciate you taking the time to look at this.
Vole is still young and there's plenty of room to shape it. If you've found a bug, have an idea, or just want to clean something up -- jump in.
You'll need Go 1.22 or later. That's the only requirement. Seriously.
# Clone the repo
git clone https://github.com/your-org/vole.git
cd vole
# Build both binaries
go build -o vole ./cmd/vole
go build -o vole-cli ./cmd/vole-cli
# Run the tests
go test ./... -race
# Run the benchmarks
go test -bench=. -benchmem ./internal/store/That's it. No make, no Docker, no dependency managers. If go build works, you're ready.
The normal test suite includes local RESP compatibility coverage. When you need to validate against a real Redis baseline, start Redis separately and point the opt-in harness at it:
# Example with Docker
docker run --rm -p 127.0.0.1:6379:6379 redis:7-alpine
# In another terminal
VOLE_REDIS_COMPAT_ADDR=127.0.0.1:6379 go test ./internal/server -run TestRedisBaselineCompatibility -count=1The harness starts a local Vole server, sends the same command scenarios to Vole and Redis, and compares RESP replies for strings, selected databases, collections, streams, Lua scripts, functions, and command introspection. It uses unique key and function names and does not flush Redis databases.
For deeper compatibility work, you can run Redis's upstream Tcl test harness against a throwaway Vole process:
./scripts/run-redis-tests.shBy default the script checks out redis/redis into /tmp/redis-tests-src,
builds local vole and vole-cli binaries, starts Vole on 127.0.0.1:17379,
and runs Redis ./runtest --host 127.0.0.1 --port 17379. To focus on a
specific upstream slice while triaging failures:
REDIS_TEST_ARGS="--single unit/type/string" ./scripts/run-redis-tests.shSet REDIS_REPO, REDIS_REF, VOLE_REDIS_TEST_PORT, or REDIS_TEST_ARGS to
reuse a local Redis checkout, pin a Redis branch/tag, avoid a busy port, or
limit the test selection.
cmd/
vole/ Server entry point and CLI flags
vole-cli/ Interactive client and single-command runner
internal/
resp/ RESP protocol reader/writer
store/ In-memory data structures (the core)
store.go Main store with all data type operations
deque.go Ring-buffer deque for lists
sortedset.go Maintained-order sorted set
hyperloglog.go Probabilistic cardinality estimator
jsonstore.go JSON document type with path-based access
timeseries.go Time-series data type
queue.go Reliable message queue
geo.go Geospatial encoding and search
bench_test.go Benchmarks
store_test.go Store tests
server/
server.go Command dispatch, connection handling
aof.go Append-only file persistence
snapshot.go Point-in-time snapshot persistence
cluster.go Cluster coordination and health
pubsub.go Pub/sub messaging
http.go HTTP/JSON API
replication.go Leader-follower replication
namespace.go Named namespace isolation
scripting.go EVAL/EVALSHA script execution
cron.go Scheduled task runner
audit.go Mutation audit log
schema.go Hash field validation
webhooks.go Expiry and mutation webhooks
clients.go Client connection tracking
slowlog.go Slow query log
metrics.go Prometheus metrics
Open an issue. Tell us what you did, what you expected, and what actually happened. If you can give us the exact sequence of commands to reproduce it, that saves everyone a lot of back-and-forth.
Open an issue. Tell us about the problem first, not just the solution. Sometimes there's a simpler way to get there that neither of us has thought of yet.
- Fork the repo, make a branch off
main. - Write the code. Look at how the files around yours are written and try to match the style.
- Add tests. New commands should have both a store-level test (in
store_test.go) and an integration test (incommands_test.go). - Run
go test ./... -race. Everything should pass. go vet ./...too, while you're at it.- Open a PR. Tell us what it does and why you made the choices you made.
We'll take a look, ask questions if something's not clear, and get it merged.
Keep it small. One fix or one feature. If you find yourself writing "also while I was in here I..." that's probably two PRs.
Tests matter. If you're adding a command, test it. If you're fixing a bug, add a test that would have caught it. If you're not sure what to test, ask in the issue.
Don't reformat code you didn't touch, and don't add comments that just repeat what the code says. We'd rather read clean code than clean comments.
Try not to break existing commands. If you need to change how something works, flag it in the PR description so we can talk about it.
We don't have a style guide. We have gofmt and a few conventions:
- Run
gofmtbefore you push. Non-negotiable. - No external dependencies. The
go.modis empty on purpose. We'd like to keep it that way. - Error messages are lowercase. e.g.,
"stream ID must be greater than previous ID". - Store methods handle their own locking. Callers shouldn't need to think about it.
- Reads take
RLock, writes takeLock. Getting this wrong causes real bugs.
Say you're adding a command called FROBNICATE. Here's the path through the codebase:
-
Store method in
internal/store/store.go. This is where the actual logic lives. PickRLockfor reads,Lockfor writes. If it modifies data, calltouchKeyLocked()at the end. -
Handler in
internal/server/server.go. Addcase "FROBNICATE":to theexecswitch. Parse the arguments, call your store method, write the response. -
Persistence in
internal/server/aof.go. If it's a write, append it to the AOF and add a replay case so the data comes back after a restart. -
Tests. One in
store_test.gofor the logic, one incommands_test.gofor the end-to-end RESP flow. -
Housekeeping. Add it to
isWriteCommand()if it's a write (replication and audit use this). If it takes a key argument, add it to the prefix map innamespace.go.
# Start Vole with everything turned on
./vole --addr :7379 --http-addr :8080 --appendonly --snapshot vole.snap
# In another terminal
./vole-cli
127.0.0.1:7379> PING
PONGWe're building this in the open because we think it's more fun that way. Be decent to each other. There's a real person behind every GitHub handle.
See CODE_OF_CONDUCT.md for the full version of that sentiment.
Contributions are licensed under the same BSD 3-Clause License as the rest of the project.