From c3d441a51116eded3aba00f6c7141c97f9cd2c72 Mon Sep 17 00:00:00 2001 From: Mateo Presa Castro Date: Sat, 15 Nov 2025 17:28:11 +0100 Subject: [PATCH 1/2] feat: bench - Add benchmark test. - Replace std lib log for zerolog. - Makefile --- .github/workflows/test.yml | 18 +------ Makefile | 31 ++--------- cmd/mokv.go | 16 ++++-- go.mod | 1 + go.sum | 9 ++++ internal/bench_test.go | 99 ++++++++++++++++++++++++++++++++++ internal/discovery/picker.go | 26 ++++++++- internal/discovery/resolver.go | 7 ++- internal/mokv.go | 11 ++-- internal/server/server.go | 27 +++++++--- 10 files changed, 183 insertions(+), 62 deletions(-) create mode 100644 internal/bench_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43a9e0a..20c86c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/Makefile b/Makefile index f00a040..6a28fbb 100644 --- a/Makefile +++ b/Makefile @@ -9,39 +9,16 @@ compile: .PHONY: test test: - go test -cover ./... + go test -cover -v -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 \ No newline at end of file diff --git a/cmd/mokv.go b/cmd/mokv.go index e82a8ba..ac3cc60 100644 --- a/cmd/mokv.go +++ b/cmd/mokv.go @@ -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" ) @@ -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") } } @@ -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 } diff --git a/go.mod b/go.mod index 3d9537b..a1d1ddb 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hashicorp/raft-boltdb v0.0.0-20251103221153-05f9dd7a5148 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 diff --git a/go.sum b/go.sum index b12ee4d..a76dd39 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -180,10 +182,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= @@ -243,6 +248,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= @@ -397,6 +405,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= diff --git a/internal/bench_test.go b/internal/bench_test.go new file mode 100644 index 0000000..5ef983c --- /dev/null +++ b/internal/bench_test.go @@ -0,0 +1,99 @@ +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" +) + +var client api.KVClient + +func init() { + conn, err := grpc.NewClient( + "mokv://127.0.0.1:8400", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"mokv": {}}]}`), + ) + if err != nil { + panic(fmt.Sprintf("Failed to connect: %v", err)) + } + 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) +} + +// Benchmark single-threaded writes (measures raw latency) +func BenchmarkSet(b *testing.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) { + 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) { + 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) { + 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) + } + } + }) +} diff --git a/internal/discovery/picker.go b/internal/discovery/picker.go index eba6efe..6a260bb 100644 --- a/internal/discovery/picker.go +++ b/internal/discovery/picker.go @@ -1,6 +1,7 @@ package discovery import ( + "log/slog" "strings" "sync/atomic" @@ -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 @@ -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 } diff --git a/internal/discovery/resolver.go b/internal/discovery/resolver.go index a411346..f271c64 100644 --- a/internal/discovery/resolver.go +++ b/internal/discovery/resolver.go @@ -36,8 +36,9 @@ func (r *Resolver) Build( cc resolver.ClientConn, opts resolver.BuildOptions, ) (resolver.Resolver, error) { - slog.Info("building resolver", "target", target.Endpoint()) + slog.Info("building resolver", "target", target.URL.Host) r.clientConn = cc + var dialOpts []grpc.DialOption if opts.DialCreds != nil { dialOpts = append( @@ -49,7 +50,7 @@ func (r *Resolver) Build( fmt.Sprintf(`{"loadBalancingConfig":[{"%s":{}}]}`, Name), ) var err error - r.resolverConn, err = grpc.NewClient(target.Endpoint(), dialOpts...) + r.resolverConn, err = grpc.NewClient(target.URL.Host, dialOpts...) if err != nil { return nil, err } @@ -73,6 +74,7 @@ func (r *Resolver) ResolveNow(resolver.ResolveNowOptions) { res, err := client.GetServers(ctx, &emptypb.Empty{}) if err != nil { slog.Error("failed to resolve server", "err", err) + r.clientConn.ReportError(err) return } var addrs []resolver.Address @@ -86,6 +88,7 @@ func (r *Resolver) ResolveNow(resolver.ResolveNowOptions) { ), }) } + r.clientConn.UpdateState(resolver.State{ Addresses: addrs, ServiceConfig: r.serviceConfig, diff --git a/internal/mokv.go b/internal/mokv.go index bf0c271..4ee33b9 100644 --- a/internal/mokv.go +++ b/internal/mokv.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "log/slog" "net" "net/http" "os/signal" @@ -14,6 +13,8 @@ import ( "syscall" "time" + "github.com/rs/zerolog/log" + "github.com/dynamic-calm/mokv/internal/discovery" "github.com/dynamic-calm/mokv/internal/kv" "github.com/dynamic-calm/mokv/internal/server" @@ -158,7 +159,7 @@ func (m *MOKV) Listen(ctx context.Context) error { // Start metrics server g.Go(func() error { - slog.Info("metrics server listening...", "addr", m.metricsServer.Addr) + log.Info().Str("addr", m.metricsServer.Addr).Msg("metrics server listening...") if err := m.metricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("metrics server failed: %w", err) } @@ -167,7 +168,7 @@ func (m *MOKV) Listen(ctx context.Context) error { // Start gRPC server though gprcLn g.Go(func() error { - slog.Info("gRPC server listening...", "addr", m.grpcLn.Addr()) + log.Info().Str("addr", m.grpcLn.Addr().String()).Msg("gRPC server listening...") if err := m.grpcServer.Serve(m.grpcLn); err != nil { return fmt.Errorf("gRPC server error: %w", err) } @@ -176,7 +177,7 @@ func (m *MOKV) Listen(ctx context.Context) error { // Start Multiplexer g.Go(func() error { - slog.Info("multiplexer (RAFT, gRPC) listening...") + log.Info().Msg("multiplexer (RAFT, gRPC) listening...") if err := m.cmux.Serve(); err != nil { return fmt.Errorf("multiplexer server error: %w", err) } @@ -190,7 +191,7 @@ func (m *MOKV) Listen(ctx context.Context) error { defer cancel() if err := m.close(shutdownCtx); err != nil { - slog.Error("shutdown error", "error", err) + log.Error().Err(err).Msg("shutdown error") } }() diff --git a/internal/server/server.go b/internal/server/server.go index d09f69b..e1e9527 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,13 +3,13 @@ package server import ( "context" "fmt" - "log/slog" "os" "github.com/dynamic-calm/mokv/internal/api" "github.com/dynamic-calm/mokv/internal/kv" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + "github.com/rs/zerolog" "google.golang.org/grpc" "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -20,6 +20,7 @@ type kvServer struct { api.KVServer KV kv.KVI serverGetter kv.ServerProvider + logger zerolog.Logger } func NewServerGetter(kv kv.KVI) kv.ServerProvider { @@ -38,9 +39,10 @@ func (kg *kvServerGetter) GetServers() ([]*api.Server, error) { } func New(KV kv.KVI, opts ...grpc.ServerOption) *grpc.Server { - logger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) + logger := zerolog.New(os.Stderr).With().Timestamp().Logger() logOpts := []logging.Option{ - logging.WithLogOnEvents(logging.StartCall, logging.FinishCall), + logging.WithLogOnEvents(logging.FinishCall), + logging.WithLevels(logging.DefaultServerCodeToLevel), } // Middleware for streaming and unary requests @@ -73,7 +75,7 @@ func (s *kvServer) Get(ctx context.Context, req *api.GetRequest) (*api.GetRespon func (s *kvServer) Set(ctx context.Context, req *api.SetRequest) (*api.SetResponse, error) { err := s.KV.Set(req.Key, req.Value) if err != nil { - slog.Error("set operation failed", "key", req.Key, "error", err) + s.logger.Error().Err(err).Str("key", req.Key).Msg("set operation failed") return &api.SetResponse{Ok: false}, status.Errorf(codes.Internal, "failed to set key: %v", err) } return &api.SetResponse{Ok: true}, nil @@ -114,8 +116,21 @@ func (s *kvServer) notFoundMsg(key string) string { return fmt.Sprintf("no value for key: %s", key) } -func interceptorLogger(l *slog.Logger) logging.Logger { +func interceptorLogger(l zerolog.Logger) logging.Logger { return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { - l.Log(ctx, slog.Level(lvl), msg, fields...) + var event *zerolog.Event + switch lvl { + case logging.LevelDebug: + event = l.Debug() + case logging.LevelInfo: + event = l.Info() + case logging.LevelWarn: + event = l.Warn() + case logging.LevelError: + event = l.Error() + default: + event = l.Info() + } + event.Fields(fields).Msg(msg) }) } From 584934a8ca99724fc63c35c607810cab6b2f92f1 Mon Sep 17 00:00:00 2001 From: Mateo Presa Castro Date: Sat, 15 Nov 2025 18:21:12 +0100 Subject: [PATCH 2/2] chore: bump boltdb to void race conditions --- Makefile | 2 +- go.mod | 5 +++-- go.sum | 5 +++++ internal/bench_test.go | 19 ++++++++++++++----- internal/discovery/membership_test.go | 2 +- internal/kv/kv.go | 2 +- internal/kv/kv_test.go | 13 ++++--------- internal/mokv_test.go | 6 ++---- internal/server/server.go | 1 + 9 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 6a28fbb..0fbc658 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ compile: .PHONY: test test: - go test -cover -v -race ./... + go test -cover -race ./... .PHONY: start start: diff --git a/go.mod b/go.mod index a1d1ddb..d98bcdc 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ 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 @@ -36,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 @@ -62,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 diff --git a/go.sum b/go.sum index a76dd39..a7382b3 100644 --- a/go.sum +++ b/go.sum @@ -147,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= @@ -298,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= @@ -390,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= diff --git a/internal/bench_test.go b/internal/bench_test.go index 5ef983c..0f6b434 100644 --- a/internal/bench_test.go +++ b/internal/bench_test.go @@ -12,18 +12,22 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -var client api.KVClient - -func init() { +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 { - panic(fmt.Sprintf("Failed to connect: %v", err)) + b.Fatalf("failed to connect: %v", err) } - client = api.NewKVClient(conn) + + b.Cleanup(func() { + conn.Close() + }) + + client := api.NewKVClient(conn) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() @@ -36,10 +40,12 @@ func init() { } 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++ { @@ -55,6 +61,7 @@ func BenchmarkSet(b *testing.B) { // Benchmark parallel writes (measures throughput) func BenchmarkSetParallel(b *testing.B) { + client := setupClient(b) ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { @@ -74,6 +81,7 @@ func BenchmarkSetParallel(b *testing.B) { // Benchmark single-threaded reads (measures raw latency) func BenchmarkGet(b *testing.B) { + client := setupClient(b) ctx := context.Background() b.ResetTimer() for b.Loop() { @@ -86,6 +94,7 @@ func BenchmarkGet(b *testing.B) { // Benchmark parallel reads (measures throughput) func BenchmarkGetParallel(b *testing.B) { + client := setupClient(b) ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { diff --git a/internal/discovery/membership_test.go b/internal/discovery/membership_test.go index 576855c..b3a0f71 100644 --- a/internal/discovery/membership_test.go +++ b/internal/discovery/membership_test.go @@ -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): diff --git a/internal/kv/kv.go b/internal/kv/kv.go index 582f8e8..c03c22b 100644 --- a/internal/kv/kv.go +++ b/internal/kv/kv.go @@ -15,7 +15,7 @@ import ( "github.com/dynamic-calm/mokv/internal/api" "github.com/dynamic-calm/mokv/internal/store" "github.com/hashicorp/raft" - raftboltdb "github.com/hashicorp/raft-boltdb" + raftboltdb "github.com/hashicorp/raft-boltdb/v2" "google.golang.org/protobuf/proto" ) diff --git a/internal/kv/kv_test.go b/internal/kv/kv_test.go index 9a3c787..d4077c2 100644 --- a/internal/kv/kv_test.go +++ b/internal/kv/kv_test.go @@ -3,7 +3,6 @@ package kv_test import ( "net" "os" - "path/filepath" "testing" "time" @@ -13,15 +12,11 @@ import ( ) func TestDistributedKVReplication(t *testing.T) { - dir1 := filepath.Join(os.TempDir(), "node-1") - dir2 := filepath.Join(os.TempDir(), "node-2") - os.MkdirAll(dir1, 0755) - os.MkdirAll(dir2, 0755) - defer os.RemoveAll(dir1) - defer os.RemoveAll(dir2) + dir1 := t.TempDir() + dir2 := t.TempDir() store1 := store.New() - ln1, err := net.Listen("tcp", "127.0.0.1:3001") + ln1, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to create listener for node 1: %v", err) } @@ -132,7 +127,7 @@ func TestDistributedKVReplication(t *testing.T) { } // Setup third node - dir3 := filepath.Join(os.TempDir(), "node-3") + dir3 := t.TempDir() os.MkdirAll(dir3, 0755) defer os.RemoveAll(dir3) diff --git a/internal/mokv_test.go b/internal/mokv_test.go index 8e0afd5..798d0d7 100644 --- a/internal/mokv_test.go +++ b/internal/mokv_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "os" - "path" "strconv" "testing" "time" @@ -22,8 +21,7 @@ func TestRunE2E(t *testing.T) { ctx := context.Background() // Setup test data directory - testDir := path.Join(os.TempDir(), fmt.Sprintf("mokv-test-%d", time.Now().UnixNano())) - defer os.RemoveAll(testDir) + testDir := t.TempDir() hostname, err := os.Hostname() if err != nil { @@ -53,7 +51,7 @@ func TestRunE2E(t *testing.T) { // Setup client connection rpcAddr := "127.0.0.1:" + strconv.Itoa(cfg.RPCPort) conn, err := grpc.NewClient( - fmt.Sprintf("mokv:///%s", rpcAddr), + fmt.Sprintf("mokv://%s", rpcAddr), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index e1e9527..175377f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -39,6 +39,7 @@ func (kg *kvServerGetter) GetServers() ([]*api.Server, error) { } func New(KV kv.KVI, opts ...grpc.ServerOption) *grpc.Server { + zerolog.SetGlobalLevel(zerolog.WarnLevel) logger := zerolog.New(os.Stderr).With().Timestamp().Logger() logOpts := []logging.Option{ logging.WithLogOnEvents(logging.FinishCall),