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
18 changes: 1 addition & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,5 @@ jobs:
with:
go-version: "1.25.4"

- name: Install Protoc
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

- name: Install cfssl
run: |
go install github.com/cloudflare/cfssl/cmd/cfssl@latest
go install github.com/cloudflare/cfssl/cmd/cfssljson@latest

- name: Create config directory
run: |
mkdir -p ${HOME}/.mokv

- name: Run tests
run: make cicd
run: make test
31 changes: 4 additions & 27 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,16 @@ compile:

.PHONY: test
test:
go test -cover ./...
go test -cover -race ./...

.PHONY: start
start:
go run .

.PHONY: init
init:
mkdir -p ${CONFIG_PATH}

.PHONY: cicd
cicd: compile test

.PHONY: build
build:
go build -o bin/mokv ./cmd/mokv.go

.PHONY: perf-set
perf-set:
ghz --proto ./internal/api/kv.proto \
--insecure \
--call api.KV.Set \
-d '{"key":"test-{{.RequestNumber}}","value":"dGVzdC12YWx1ZQ=="}' \
-n 10000 \
-c 10 \
localhost:8400

.PHONY: perf-get
perf-get:
ghz --proto ./internal/api/kv.proto \
--insecure \
--call api.KV.Get \
-d '{"key":"test-key"}' \
-n 100000 \
-c 10 \
localhost:8400
.PHONY: perf
perf:
-go test -bench=. -benchtime=5s ./internal/ -run=^# -benchmem
16 changes: 13 additions & 3 deletions cmd/mokv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package main

import (
"context"
"log"
"net/http"
"os"
"path"

_ "net/http/pprof"

mokv "github.com/dynamic-calm/mokv/internal"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -28,11 +31,11 @@ func main() {
}

if err := setupFlags(cmd); err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("error setting up flags")
}

if err := cmd.Execute(); err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("error executing command")
}
}

Expand Down Expand Up @@ -88,6 +91,13 @@ func (cli *cli) run(cmd *cobra.Command, args []string) error {
return err
}

go func() {
log.Info().Str("addr", "loclahost:6060").Msg("starting pprof server")
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
log.Error().Err(err).Msg("pprof server failed")
}
}()

if err := mokv.Listen(ctx); err != nil {
return err
}
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
github.com/hashicorp/memberlist v0.5.3
github.com/hashicorp/raft v1.7.3
github.com/hashicorp/raft-boltdb v0.0.0-20251103221153-05f9dd7a5148
github.com/hashicorp/raft-boltdb/v2 v2.3.1
github.com/hashicorp/serf v0.10.2
github.com/prometheus/client_golang v1.20.5
github.com/rs/zerolog v1.34.0
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
Expand All @@ -35,12 +36,12 @@ require (
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-msgpack v0.5.5 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/raft-boltdb v0.0.0-20251103221153-05f9dd7a5148 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/magiconair/properties v1.8.10 // indirect
Expand All @@ -61,6 +62,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -71,6 +72,7 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down Expand Up @@ -145,6 +147,8 @@ github.com/hashicorp/raft v1.7.3 h1:DxpEqZJysHN0wK+fviai5mFcSYsCkNpFUl1xpAW8Rbo=
github.com/hashicorp/raft v1.7.3/go.mod h1:DfvCGFxpAUPE0L4Uc8JLlTPtc3GzSbdH0MTJCLgnmJQ=
github.com/hashicorp/raft-boltdb v0.0.0-20251103221153-05f9dd7a5148 h1:tjaIHlfKX22DCCPTx2mK+6N/kTP9DV7B3bxEUyQtjKA=
github.com/hashicorp/raft-boltdb v0.0.0-20251103221153-05f9dd7a5148/go.mod h1:sgCxzMuvQ3huVxgmeDdj73YIMmezWZ40HQu2IPmjJWk=
github.com/hashicorp/raft-boltdb/v2 v2.3.1 h1:ackhdCNPKblmOhjEU9+4lHSJYFkJd6Jqyvj6eW9pwkc=
github.com/hashicorp/raft-boltdb/v2 v2.3.1/go.mod h1:n4S+g43dXF1tqDT+yzcXHhXM6y7MrlUd3TTwGRcUvQE=
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down Expand Up @@ -180,10 +184,13 @@ github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
Expand Down Expand Up @@ -243,6 +250,9 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
Expand Down Expand Up @@ -290,6 +300,8 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
Expand Down Expand Up @@ -382,6 +394,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -397,6 +410,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
108 changes: 108 additions & 0 deletions internal/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package mokv_test

import (
"context"
"fmt"
"testing"
"time"

"github.com/dynamic-calm/mokv/internal/api"
_ "github.com/dynamic-calm/mokv/internal/discovery"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func setupClient(b *testing.B) api.KVClient {
b.Helper()
conn, err := grpc.NewClient(
"mokv://127.0.0.1:8400",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"mokv": {}}]}`),
)
if err != nil {
b.Fatalf("failed to connect: %v", err)
}

b.Cleanup(func() {
conn.Close()
})

client := api.NewKVClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err = client.Set(ctx, &api.SetRequest{
Key: "bench-key",
Value: []byte("bench-value"),
})
if err != nil {
panic(fmt.Sprintf("Failed to pre-populate bench-key: %v", err))
}

time.Sleep(500 * time.Millisecond)
return client
}

// Benchmark single-threaded writes (measures raw latency)
func BenchmarkSet(b *testing.B) {
client := setupClient(b)
ctx := context.Background()
b.ResetTimer()
for i := 0; b.Loop(); i++ {
_, err := client.Set(ctx, &api.SetRequest{
Key: fmt.Sprintf("key-%d", i),
Value: []byte("benchmark-value"),
})
if err != nil {
b.Fatal(err)
}
}
}

// Benchmark parallel writes (measures throughput)
func BenchmarkSetParallel(b *testing.B) {
client := setupClient(b)
ctx := context.Background()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
_, err := client.Set(ctx, &api.SetRequest{
Key: fmt.Sprintf("key-%d", i),
Value: []byte("benchmark-value"),
})
if err != nil {
b.Error(err)
}
i++
}
})
}

// Benchmark single-threaded reads (measures raw latency)
func BenchmarkGet(b *testing.B) {
client := setupClient(b)
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
_, err := client.Get(ctx, &api.GetRequest{Key: "bench-key"})
if err != nil {
b.Fatal(err)
}
}
}

// Benchmark parallel reads (measures throughput)
func BenchmarkGetParallel(b *testing.B) {
client := setupClient(b)
ctx := context.Background()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := client.Get(ctx, &api.GetRequest{Key: "bench-key"})
if err != nil {
b.Error(err)
}
}
})
}
2 changes: 1 addition & 1 deletion internal/discovery/membership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestMembership(t *testing.T) {
members, _ = setupMember(t, members, 8001)
members, _ = setupMember(t, members, 8002)

for i := 0; i < 2; i++ {
for range 2 {
select {
case <-handler.joins:
case <-time.After(3 * time.Second):
Expand Down
26 changes: 24 additions & 2 deletions internal/discovery/picker.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package discovery

import (
"log/slog"
"strings"
"sync/atomic"

Expand All @@ -19,6 +20,7 @@ func init() {
var _ base.PickerBuilder = (*Builder)(nil)

func (b *Builder) Build(info base.PickerBuildInfo) balancer.Picker {
slog.Info("building picker", "ready_count", len(info.ReadySCs))
var leader balancer.SubConn
var followers []balancer.SubConn

Expand Down Expand Up @@ -51,22 +53,42 @@ func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
return result, balancer.ErrNoSubConnAvailable
}

if strings.Contains(info.FullMethodName, "Set") ||
strings.Contains(info.FullMethodName, "Delete") {
methodName := info.FullMethodName
isWrite := strings.Contains(methodName, "Set") ||
strings.Contains(methodName, "Delete")

// No available connections
if p.leader == nil && len(p.followers) == 0 {
slog.Debug("pick failed: no available subconns",
"method", methodName)
return result, balancer.ErrNoSubConnAvailable
}

// Write operations must go to leader
if isWrite {
if p.leader == nil {
slog.Debug("pick failed: write operation but no leader",
"method", methodName)
return result, balancer.ErrNoSubConnAvailable
}
result.SubConn = p.leader
slog.Debug("picked leader for write", "method", methodName)
return result, nil
}

// Read operations prefer followers for load distribution
if len(p.followers) > 0 {
result.SubConn = p.nextFollower()
slog.Debug("picked follower for read",
"method", methodName,
"follower_index", p.current%uint64(len(p.followers)))
return result, nil
}

// Fall back to leader for reads if no followers available
if p.leader != nil {
result.SubConn = p.leader
slog.Debug("picked leader for read (no followers)", "method", methodName)
return result, nil
}

Expand Down
Loading