Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ on:
- "static/**"
- "README.md"
- "CONTRIBUTING.md"
- "docs/RELEASE.md"
pull_request:
branches:
- main
Expand All @@ -24,7 +23,6 @@ on:
- "static/**"
- "README.md"
- "CONTRIBUTING.md"
- "docs/RELEASE.md"
workflow_dispatch:

permissions:
Expand Down
22 changes: 16 additions & 6 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ checksum:
algorithm: sha256
name_template: 'CHECKSUMS'

# Publishes Formula/kvs.rb to skyoo2003/homebrew-tap. The token comes from a
# Publishes Casks/kvs.rb to skyoo2003/homebrew-tap. The token comes from a
# GitHub App installed only on that repository: the job's own GITHUB_TOKEN
# cannot write to another repo.
brews:
#
# A cask rather than a formula because GoReleaser deprecated `brews`: a formula that installs a
# pre-built binary is what a cask is for. `brew install skyoo2003/tap/kvs` keeps working, and the
# tap needs a tap_migrations.json entry so anyone holding the old formula moves over on upgrade
# instead of being told it is gone.
homebrew_casks:
- ids:
- kvs
repository:
Expand All @@ -68,10 +73,15 @@ brews:
homepage: "https://github.com/skyoo2003/kvs"
description: "A key-value store you can run as a server or import as a Go module"
license: "MIT"
install: |
bin.install "kvs"
test: |
assert_match version.to_s, shell_output("#{bin}/kvs -v")
# Nothing signs or notarizes these binaries, so Gatekeeper quarantines what the cask staged and
# the first run dies on a dialog rather than an error. Stripping the attribute is what makes an
# unsigned cask runnable; it is not an optional nicety.
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/kvs"]
end

dockers_v2:
- images:
Expand Down
32 changes: 24 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).

## [v1.0.0](https://github.com/skyoo2003/kvs/releases/tag/v1.0.0) - 2026-08-11
### Added
* Redis/Valkey compatible RESP2 server on 127.0.0.1:6379, sharing one keyspace with the HTTP and gRPC APIs; it steps aside if the port is taken, holds at most 10000 connections, and bounds one transaction's queue at 64MiB so a single client cannot exhaust memory ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Lua scripting with EVAL, EVALSHA, and SCRIPT LOAD/EXISTS/FLUSH; a script runs sandboxed under one write lock, so its redis.call sequence is atomic, and gives up after 5 seconds rather than hold the store ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Keep the keyspace across restarts with `kvs serve --data-dir`, which appends every change to a log and replays it at startup ([#236](https://github.com/skyoo2003/kvs/issues/236))
* The cjson library inside a script, so cjson.encode and cjson.decode work under EVAL instead of failing on a nil global; a JSON null decodes to cjson.null rather than nil, so it does not end the array it sits in ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Run a Raft cluster with `kvs serve --raft-addr` and `--join`; writes pass consensus, and losing the leader triggers an election instead of needing a person ([#236](https://github.com/skyoo2003/kvs/issues/236))
* A compatibility page states what v1 promises not to break, what it leaves out, and the trust boundary kvs assumes; the exported Go surface is pinned in testdata/api-surface.txt and checked by a test, so it cannot widen or narrow unnoticed ([#244](https://github.com/skyoo2003/kvs/issues/244))
* A data directory now carries a format version, and kvs refuses to start on one it does not recognise instead of replaying bytes another version laid out; the refusal names both versions and what to do about it, and it covers the Raft store as well as the append log ([#245](https://github.com/skyoo2003/kvs/issues/245))
### Changed
* Container images now run 'serve' by default as UID 65534 and no longer declare a volume ([#228](https://github.com/skyoo2003/kvs/issues/228))
* RESP SCAN, HSCAN, SSCAN, and ZSCAN now page their walk, WATCH tracks only the keys it was given, lists cost O(1) at both ends, and expired keys are reclaimed by a sampling sweep ([#232](https://github.com/skyoo2003/kvs/issues/232))
* INFO and HELLO now report what a clustered node actually is — a follower answers role:slave and names the leader in master_host and master_port, a leader counts the others in connected_slaves, and cluster_enabled:0 says kvs is not Redis Cluster ([#236](https://github.com/skyoo2003/kvs/issues/236))
* Homebrew installs a cask instead of a formula; `brew install skyoo2003/tap/kvs` is unchanged, and the tap carries a tap_migrations.json entry so an existing formula install moves across on upgrade. GoReleaser deprecated the formula path for pre-built binaries, and the release would have broken on a floating version of it ([#248](https://github.com/skyoo2003/kvs/issues/248))
### Removed
* Drop the unused data structure packages pkg/bitset, pkg/cuckoofilter, pkg/lsm, and pkg/rbt; the storage engine went to an append log and Raft instead, and nothing in the server or the library imported them. pkg/resp stays, since the RESP server is built on it ([#236](https://github.com/skyoo2003/kvs/issues/236))
### Fixed
* Fix multi-arch (linux/arm64) release images and expose the gRPC port 3457 in container images ([#228](https://github.com/skyoo2003/kvs/issues/228))
* An HTTP 405 now carries the same JSON error body as every other HTTP error, instead of an empty response the documented contract did not allow ([#244](https://github.com/skyoo2003/kvs/issues/244))
### Documentation
* The durability and clustering page now carries measured numbers from a four hour run under load with a node stopped every thirty seconds and kept down for ten - 329,631 acknowledged writes, 111,516 of them taken while a node was gone, and none of them lost across 479 restarts - along with 51 bytes of append log per write, the Raft log a node too busy restarting never truncates, and a make soak target that reproduces all of it ([#246](https://github.com/skyoo2003/kvs/issues/246))
* The release page now covers what one tag actually produces, the checks worth running before pushing it, and an upgrade section measured by running v0.1.1 and v1 side by side - the library, the CLI, and gRPC unchanged, an HTTP 405 now carrying a JSON body, a RESP listener appearing on loopback, and no data directory to migrate because v0.1.1 never wrote one ([#248](https://github.com/skyoo2003/kvs/issues/248))
### Misc
* Migrate golangci-lint config to v2 and pin the linter to v2.12.2 in CI ([#229](https://github.com/skyoo2003/kvs/issues/229))
## [v0.1.1](https://github.com/skyoo2003/kvs/releases/tag/v0.1.1) - 2026-04-20

### Changed
Expand All @@ -15,7 +32,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie).

### Fixed
* Fix goreleaser flag: --rm-dist → --clean for v2 compatibility

## [v0.1.0](https://github.com/skyoo2003/kvs/releases/tag/v0.1.0) - 2026-03-14
### Added
* Implement a usable kvs module and release path ([#184](https://github.com/skyoo2003/kvs/issues/184))
Expand Down
2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ KVS follows a **BDFL (Benevolent Dictator for Life)** model:
- Releases follow [Semantic Versioning](https://semver.org)
- The project lead determines release scope and timing
- Changelog is managed with [Changie](https://github.com/miniscruff/changie)
- See [RELEASE.md](docs/RELEASE.md) for the full release workflow
- See [the release process](https://skyoo2003.github.io/kvs/docs/release/) for the full release workflow

## Contributing

Expand Down
5 changes: 0 additions & 5 deletions changes/unreleased/Added-20260730-210000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Added-20260801-120000.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions changes/unreleased/Added-20260801-152145.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Added-20260801-160000.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions changes/unreleased/Added-20260801-161504.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Added-20260809-000000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Added-20260809-120000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Changed-20260730-073249.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Changed-20260730-210001.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Changed-20260730-233000.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions changes/unreleased/Changed-20260802-000000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Documentation-20260809-210000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Fixed-20260730-073249.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Fixed-20260731-001220.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions changes/unreleased/Fixed-20260803-090000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Fixed-20260810-100000.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Misc-20260730-080930.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions changes/unreleased/Removed-20260730-073249.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions changes/unreleased/Removed-20260802-001500.yaml

This file was deleted.

24 changes: 24 additions & 0 deletions changes/v1.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## [v1.0.0](https://github.com/skyoo2003/kvs/releases/tag/v1.0.0) - 2026-08-11
### Added
* Redis/Valkey compatible RESP2 server on 127.0.0.1:6379, sharing one keyspace with the HTTP and gRPC APIs; it steps aside if the port is taken, holds at most 10000 connections, and bounds one transaction's queue at 64MiB so a single client cannot exhaust memory ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Lua scripting with EVAL, EVALSHA, and SCRIPT LOAD/EXISTS/FLUSH; a script runs sandboxed under one write lock, so its redis.call sequence is atomic, and gives up after 5 seconds rather than hold the store ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Keep the keyspace across restarts with `kvs serve --data-dir`, which appends every change to a log and replays it at startup ([#236](https://github.com/skyoo2003/kvs/issues/236))
* The cjson library inside a script, so cjson.encode and cjson.decode work under EVAL instead of failing on a nil global; a JSON null decodes to cjson.null rather than nil, so it does not end the array it sits in ([#232](https://github.com/skyoo2003/kvs/issues/232))
* Run a Raft cluster with `kvs serve --raft-addr` and `--join`; writes pass consensus, and losing the leader triggers an election instead of needing a person ([#236](https://github.com/skyoo2003/kvs/issues/236))
* A compatibility page states what v1 promises not to break, what it leaves out, and the trust boundary kvs assumes; the exported Go surface is pinned in testdata/api-surface.txt and checked by a test, so it cannot widen or narrow unnoticed ([#244](https://github.com/skyoo2003/kvs/issues/244))
* A data directory now carries a format version, and kvs refuses to start on one it does not recognise instead of replaying bytes another version laid out; the refusal names both versions and what to do about it, and it covers the Raft store as well as the append log ([#245](https://github.com/skyoo2003/kvs/issues/245))
### Changed
* Container images now run 'serve' by default as UID 65534 and no longer declare a volume ([#228](https://github.com/skyoo2003/kvs/issues/228))
* RESP SCAN, HSCAN, SSCAN, and ZSCAN now page their walk, WATCH tracks only the keys it was given, lists cost O(1) at both ends, and expired keys are reclaimed by a sampling sweep ([#232](https://github.com/skyoo2003/kvs/issues/232))
* INFO and HELLO now report what a clustered node actually is — a follower answers role:slave and names the leader in master_host and master_port, a leader counts the others in connected_slaves, and cluster_enabled:0 says kvs is not Redis Cluster ([#236](https://github.com/skyoo2003/kvs/issues/236))
* Homebrew installs a cask instead of a formula; `brew install skyoo2003/tap/kvs` is unchanged, and the tap carries a tap_migrations.json entry so an existing formula install moves across on upgrade. GoReleaser deprecated the formula path for pre-built binaries, and the release would have broken on a floating version of it ([#248](https://github.com/skyoo2003/kvs/issues/248))
### Removed
* Drop the unused data structure packages pkg/bitset, pkg/cuckoofilter, pkg/lsm, and pkg/rbt; the storage engine went to an append log and Raft instead, and nothing in the server or the library imported them. pkg/resp stays, since the RESP server is built on it ([#236](https://github.com/skyoo2003/kvs/issues/236))
### Fixed
* Fix multi-arch (linux/arm64) release images and expose the gRPC port 3457 in container images ([#228](https://github.com/skyoo2003/kvs/issues/228))
* An HTTP 405 now carries the same JSON error body as every other HTTP error, instead of an empty response the documented contract did not allow ([#244](https://github.com/skyoo2003/kvs/issues/244))
### Documentation
* The durability and clustering page now carries measured numbers from a four hour run under load with a node stopped every thirty seconds and kept down for ten - 329,631 acknowledged writes, 111,516 of them taken while a node was gone, and none of them lost across 479 restarts - along with 51 bytes of append log per write, the Raft log a node too busy restarting never truncates, and a make soak target that reproduces all of it ([#246](https://github.com/skyoo2003/kvs/issues/246))
* The release page now covers what one tag actually produces, the checks worth running before pushing it, and an upgrade section measured by running v0.1.1 and v1 side by side - the library, the CLI, and gRPC unchanged, an HTTP 405 now carrying a JSON body, a RESP listener appearing on loopback, and no data directory to migrate because v0.1.1 never wrote one ([#248](https://github.com/skyoo2003/kvs/issues/248))
### Misc
* Migrate golangci-lint config to v2 and pin the linter to v2.12.2 in CI ([#229](https://github.com/skyoo2003/kvs/issues/229))
31 changes: 31 additions & 0 deletions cmd/kvs/serve_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package main

import (
"path/filepath"
"strconv"
"strings"
"testing"

"github.com/spf13/viper"

"github.com/skyoo2003/kvs/internal/datadir"
"github.com/skyoo2003/kvs/internal/server"
)

Expand Down Expand Up @@ -163,3 +167,30 @@ func TestResolveServeConfigFlagsOverrideViper(t *testing.T) {
t.Fatalf("resolveServeConfig() = %+v, want flag values", got)
}
}

// Upgrading to a build that reads a different on-disk format is the one upgrade path kvs
// promises anything about, and the promise is a refusal that says what to do. The datadir
// package tests the refusal itself; this tests that it reaches whoever ran the command, which is
// where the sentence is actually read. Nothing binds a port: serve.go opens the store before it
// listens, so the command is over before the addresses matter.
func TestServeRefusesADataDirItCannotRead(t *testing.T) {
dir := t.TempDir()

const foreign = "999"
if err := osWriteFile(filepath.Join(dir, datadir.FormatName), []byte(foreign+"\n")); err != nil {
t.Fatalf("write %s error = %v", datadir.FormatName, err)
}

_, _, err := runCLI(t, "serve", "--data-dir", dir)
if err == nil {
t.Fatal("serve --data-dir on a foreign format = nil, want a refusal")
}

// The operator has to be able to tell which directory, what it holds, and what this build
// would have read, or the message is not an upgrade instruction.
for _, want := range []string{dir, foreign, strconv.Itoa(datadir.Version)} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("serve --data-dir error = %q, want it to mention %q", err, want)
}
}
}
Loading
Loading