Skip to content

Conversation

lnikon
Copy link
Owner

@lnikon lnikon commented Aug 21, 2025

Summary by CodeRabbit

  • New Features

    • Added kvtest Go CLI with a pluggable TinyKVPP adapter: supports Get/Put, health checks, defaults, and YAML-driven scenarios. Introduced gRPC/proto definitions for TinyKVPP and Raft.
  • Chores

    • Updated standalone deployment: service renamed to tkvpp-node-standalone, default port moved to 9891 with mapping, persistent data at /var/tkvpp, config mounted, and seccomp relaxed. Added Ninja to the build environment.
  • Style

    • Improved readability of the configuration file error message when the file cannot be opened.

@lnikon lnikon self-assigned this Aug 21, 2025
@lnikon lnikon moved this to In progress in @lnikon's tinykvpp Aug 21, 2025
Copy link

coderabbitai bot commented Aug 21, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces Ninja to Docker build. Updates standalone configs and Docker Compose to use absolute data path, service rename, and port 9891. Adjusts a C++ error message format. Adds a Go-based kvtest CLI scaffold, a core testing framework, a TinyKVPP gRPC adapter, and new proto definitions for TinyKVPP and Raft services.

Changes

Cohort / File(s) Summary
Docker build environment
Dockerfile
Adds package installation for ninja-build; no other build steps changed.
Infra: standalone config and compose
infra/assets/tkvpp_config_standalone.json, infra/docker-compose.standalone.yaml
Config uses absolute DB path /var/tkvpp; server port and peer updated to 9891. Compose service renamed to tkvpp-node-standalone, sets container_name, security_opt, maps 9891:9891, mounts config and named volume, declares top-level volume.
C++ config error formatting
src/config.cpp
Replaces printf-style %s with {} in fmt::format for the "cannot open config" error message; logic unchanged.
Go CLI scaffold
tests/go/kvtest/cmd/root.go, tests/go/kvtest/cmd/kvtest/main.go, tests/go/kvtest/main.go
Adds Cobra-based root command and Execute(); introduces a no-op cmd/kvtest main; top-level main delegates to cmd.Execute(), removing prior inline RPC setup.
Go core test framework
tests/go/kvtest/pkg/core/config.go, tests/go/kvtest/pkg/core/context.go, tests/go/kvtest/pkg/core/interface.go
Adds YAML-backed test config types with defaults, a TestContext constructor, a KVStoreInterface abstraction, KVError type, and preset error variables.
Go adapter: TinyKVPP
tests/go/kvtest/pkg/adapters/tinykvpp.go
Implements TinyKVPP gRPC adapter: config, connect/close, Get/Put with timeouts and error mapping, HealthCheck, Name; asserts interface conformance.
Protobuf APIs
tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto, tests/go/kvtest/proto/raft/v1/raft_service.proto
Adds TinyKVPP v1 service (Put/Get) and DatabaseOperation message; adds Raft v1 service with AppendEntries, RequestVote, Replicate, related enums and messages.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as kvtest CLI
  participant CMD as Cobra rootCmd
  participant AD as TinyKVPPAdapter
  participant GRPC as TinyKVPPService (gRPC)
  Note over CLI,GRPC: New CLI + adapter flow
  User->>CLI: Run kvtest
  CLI->>CMD: Execute()
  CMD->>AD: Connect(config)
  alt Put
    CMD->>AD: Put(key,value)
    AD->>GRPC: Put(PutRequest)
    GRPC-->>AD: PutResponse
    AD-->>CMD: error/success
  else Get
    CMD->>AD: Get(key)
    AD->>GRPC: Get(GetRequest)
    GRPC-->>AD: GetResponse
    AD-->>CMD: value/ErrKeyNotFound
  end
  CMD-->>CLI: Exit code
  CLI-->>User: Output
Loading
sequenceDiagram
  autonumber
  participant Leader
  participant Follower as RaftService (Follower)
  rect rgba(230,240,255,0.5)
  Note over Leader,Follower: Raft AppendEntries RPC (new proto)
  Leader->>Follower: AppendEntries(req{term,prevLog,entries,leaderCommit})
  Follower-->>Leader: AppendEntriesResponse{term,success,match_index}
  end
  Note over Leader,Follower: RequestVote and Replicate follow similar patterns
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature/grpc server #9 — Also updates TinyKVPP gRPC API and server/config integration, overlapping with new TinyKVPP proto and infra changes here.

Poem

In a burrow of bytes I thump with glee,
New ports burrowed: 9891, keys set free.
I nibble on protos, Raft logs in tow,
A Ninja builds swift where containers grow.
With a hop to the CLI and back I scoot—
Put, Get, squeak! The tests are cute. 🥕

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/raft

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Owner Author

@lnikon lnikon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai full review

- Framework core components in-progress
- KVStore interface & implementation added
- Config for the entire framework added
@lnikon lnikon changed the base branch from master to feature/raft-in-progress August 24, 2025 21:11
@lnikon
Copy link
Owner Author

lnikon commented Aug 24, 2025

@coderabbitai full review

Copy link

coderabbitai bot commented Aug 24, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Dockerfile (1)

64-64: ENTRYPOINT conflicts with docker-compose command

ENTRYPOINT is "/app/tkvpp" but docker-compose also runs "/app/tkvpp" in command, resulting in duplicate binary in argv. Fix compose to only pass flags.

See compose diff under infra/docker-compose.standalone.yaml.

src/config.cpp (1)

177-200: Fix fmt placeholder in logging error; prefer robust parsing

The else branch uses fmt::format with "%s" which won’t substitute loggingLevel. Use {} instead. Also consider spdlog::level::from_str to avoid string literal comparisons.

Apply:

-        throw std::runtime_error(fmt::format("Unknown logging level: %s", loggingLevel));
+        throw std::runtime_error(fmt::format("Unknown logging level: {}", loggingLevel));

Optional parsing refactor:

 void configureLogging(const std::string &loggingLevel)
 {
-    if (loggingLevel == SPDLOG_LEVEL_NAME_INFO) {
-        spdlog::set_level(spdlog::level::info);
-    } else if (loggingLevel == SPDLOG_LEVEL_NAME_DEBUG) {
-        spdlog::set_level(spdlog::level::debug);
-    } else if (loggingLevel == SPDLOG_LEVEL_NAME_TRACE) {
-        spdlog::set_level(spdlog::level::trace);
-    } else if (loggingLevel == SPDLOG_LEVEL_NAME_OFF) {
-        spdlog::set_level(spdlog::level::off);
-    } else {
-        throw std::runtime_error(fmt::format("Unknown logging level: {}", loggingLevel));
-    }
+    auto lvl = spdlog::level::from_str(loggingLevel);
+    if (lvl == spdlog::level::off && loggingLevel != SPDLOG_LEVEL_NAME_OFF) {
+        // from_str returns off on failure; double-check to differentiate real "off"
+        throw std::runtime_error(fmt::format("Unknown logging level: {}", loggingLevel));
+    }
+    spdlog::set_level(lvl);
 }
🧹 Nitpick comments (21)
tests/go/kvtest/cmd/root.go (4)

1-3: Replace placeholder copyright.

Update “NAME HERE ” with actual author or project copyright/license.


12-25: Make root command user-friendly: show help by default and silence Cobra noise.

Set a meaningful description, show help when no subcommands are provided, and silence usage/errors to avoid duplicate output.

Apply:

 var rootCmd = &cobra.Command{
-	Use:   "kvtest",
-	Short: "A brief description of your application",
-	Long: `A longer description that spans multiple lines and likely contains
-examples and usage of using your application. For example:
-
-Cobra is a CLI library for Go that empowers applications.
-This application is a tool to generate the needed files
-to quickly create a Cobra application.`,
-	// Uncomment the following line if your bare application
-	// has an action associated with it:
-	// Run: func(cmd *cobra.Command, args []string) { },
+	Use:           "kvtest",
+	Short:         "CLI harness for TinyKVPP integration and load tests",
+	Long:          "kvtest exercises TinyKVPP and Raft via gRPC. Use subcommands to run scenarios, load tests, and assertions.",
+	SilenceUsage:  true,
+	SilenceErrors: true,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// Show help when no subcommands are specified
+		return cmd.Help()
+	},
 }

6-10: Print error before exiting.

Currently Execute() exits with status 1 but doesn’t print the error, which hinders debugging in CI.

Apply:

 import (
+	"fmt"
 	"os"

 	"github.com/spf13/cobra"
 )
 func Execute() {
 	err := rootCmd.Execute()
 	if err != nil {
-		os.Exit(1)
+		_, _ = fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
 	}
 }

36-46: Replace placeholder flag with a meaningful endpoint flag aligned with defaults.

A generic “toggle” flag isn’t actionable. Given the test harness context, an endpoint flag is more useful and matches the repo’s default port 9891.

Apply:

 func init() {
@@
-	// Cobra also supports local flags, which will only run
-	// when this action is called directly.
-	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+	// Common endpoint for TinyKVPP gRPC server
+	rootCmd.PersistentFlags().StringP("endpoint", "e", "127.0.0.1:9891", "TinyKVPP gRPC endpoint (host:port)")
 }
tests/go/kvtest/main.go (1)

1-5: Replace placeholder copyright.

Update “NAME HERE ” with real project metadata.

Dockerfile (1)

9-16: Trim image size and harden apt usage

Consider adding --no-install-recommends and noninteractive settings to reduce image size and avoid tzdata prompts in some bases.

Apply:

-RUN apt-get update && \
-    apt-get -y install \
+ARG DEBIAN_FRONTEND=noninteractive
+RUN apt-get update && \
+    apt-get -y install --no-install-recommends \
     cmake \
     ninja-build \
     python3 \
     python3-pip \
     python3-virtualenv && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
infra/assets/tkvpp_config_standalone.json (1)

28-36: Optional: align schema with config fields

The config carries "mode": "standalone" under database, but the JSON schema in src/config.cpp doesn’t define database.mode. Consider adding it (e.g., enum ["standalone","raft"]) to catch typos early.

src/config.cpp (2)

348-394: Wrong error message for missing peers

The else branch throws an error about "id" when "peers" is missing. Fix the message.

Apply:

-        throw std::runtime_error("\"id\" is not specified in the config");
+        throw std::runtime_error("\"peers\" is not specified in the config");

107-115: Tighten peers validation (host:port format, disallow 0.0.0.0)

Currently peers are plain strings with no constraints. Consider a pattern like "host:port" and rejecting 0.0.0.0 via pattern or custom validation to prevent misconfiguration.

Example schema tweak:

 "peers": {
   "type": "array",
-  "description": "Array of IPv4 addresses of peers",
-  "items": { "type": "string" }
+  "description": "Peer endpoints as host:port; do not use 0.0.0.0",
+  "items": {
+    "type": "string",
+    "pattern": "^(?!0\\.0\\.0\\.0:)[^:\\s]+:[0-9]{2,5}$"
+  }
 }
infra/docker-compose.standalone.yaml (1)

5-7: Unconfined seccomp reduces container isolation

Unless you rely on syscalls blocked by the default profile, keep default seccomp. If needed, provide a minimal custom profile instead of unconfined.

Would you like a minimal seccomp profile template that permits only the required syscalls?

tests/go/kvtest/pkg/core/config.go (2)

23-30: Nit: comment wording

"AdapterConfig specified..." → "AdapterConfig specifies...".

Apply:

-// AdapterConfig specified which KV store adapter to use
+// AdapterConfig specifies which KV store adapter to use

79-87: Consider ramp-up and weights defaults

Providing non-zero RampUpTime and operation Weights helps produce smoother load profiles and reproducible mixes.

Apply:

 		Load: LoadConfig{
 			Enabled:     true,
 			Algorithm:   "constant",
 			StartUsers:  1,
 			TargetUsers: 10,
-			Duration:    60 * time.Second,
-			ThinkTime:   100 * time.Microsecond,
+			Duration:    60 * time.Second,
+			RampUpTime:  5 * time.Second,
+			ThinkTime:   100 * time.Microsecond,
+			Weights:     map[string]int{"read": 70, "write": 30},
 		},
tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto (2)

17-20: Prefer gRPC status for errors; if keeping fields, rename for clarity

Returning both success and error string is redundant and non-idiomatic in gRPC-Go. Prefer returning only the response payloads and convey failures via gRPC status codes. If you want to keep fields for this harness, consider renaming error -> message to avoid implying transport-level failure.

Example proto tweak (optional):

-message PutResponse {
-  bool success = 1;
-  string error = 2;
-}
+message PutResponse {
+  bool success = 1;    // true if write succeeded
+  string message = 2;  // optional context; not a transport error
+}

26-29: Consider bytes for values to allow binary payloads

If keys/values may contain arbitrary bytes, strings can corrupt data. Using bytes makes the API future-proof even for non-UTF8 data.

-message GetResponse {
-  bool found = 1;
-  string value = 2;
-}
+message GetResponse {
+  bool found = 1;
+  bytes value = 2;
+}

(If you do this, update PutRequest.value to bytes too, and adjust the adapter accordingly.)

tests/go/kvtest/proto/raft/v1/raft_service.proto (2)

24-28: Field numbering gap in LogEntry; either renumber now or reserve 3

New file: safest is to use contiguous numbering. If 3 is intentionally removed, reserve it explicitly to avoid future collisions.

Option A — renumber now (preferred if no wire compatibility constraints yet):

 message LogEntry {
   uint32 term = 1;
   uint32 index = 2;
-  bytes payload = 4;
+  bytes payload = 3;
 }

Option B — keep as-is but reserve 3:

 message LogEntry {
   uint32 term = 1;
   uint32 index = 2;
+  reserved 3;
   bytes payload = 4;
 }

61-69: Replicate RPC: acceptable as a test hook

Given this is test harness-only, the shape is fine. Consider bytes for payloads if you foresee binary.

tests/go/kvtest/pkg/core/context.go (2)

7-11: Private ctx field is unused; either expose it or remove it

Right now ctx is dead weight. Expose it via a getter/setter or drop it to keep TestContext minimal.

Option A — provide accessors:

 type TestContext struct {
   KVStore KVStoreInterface

-  ctx context.Context
+  ctx context.Context
 }

+// Context returns the base context used by tests.
+func (t TestContext) Context() context.Context { return t.ctx }
+
+// WithContext replaces the base context.
+func (t *TestContext) WithContext(ctx context.Context) { t.ctx = ctx }

Option B — remove ctx entirely if callers always pass their own contexts.


13-18: Constructor returns by value; consider pointer if the context will mutate

If you plan to mutate internal state (e.g., swap ctx), returning a pointer avoids copies. If it stays immutable, returning by value is fine.

-func NewTestContext(adapter KVStoreInterface) TestContext {
-	return TestContext{
+func NewTestContext(adapter KVStoreInterface) *TestContext {
+	return &TestContext{
 		KVStore: adapter,
 		ctx:     context.Background(),
 	}
 }
tests/go/kvtest/pkg/core/interface.go (2)

10-22: Interface shape is clear; consider adding Delete or dropping ErrDeleteFailed

You define ErrDeleteFailed but the interface has no Delete method. Either add Delete to the interface or remove the error to avoid dead code.

Option A — add Delete:

 type KVStoreInterface interface {
   // Connection management
   Connect(config Config) error
   Close() error

   // CRUD operations
   Get(ctx context.Context, key []byte) ([]byte, error)
   Put(ctx context.Context, key, value []byte) error
+  Delete(ctx context.Context, key []byte) error

   // Testing utilities
   HealthCheck(ctx context.Context) error
   Name() string
 }

(Adapters/tests will need minor updates.)

Option B — remove ErrDeleteFailed if delete is out of scope for now.


34-39: Predefined errors: consider simpler sentinels

Using context.DeadlineExceeded/Canceled as inner causes may confuse callers. Consider basic sentinels (errors.New) and wrap transport errors at call sites.

If you agree, I can generate a follow-up patch replacing these with simple sentinels and updating adapter checks with errors.Is.

tests/go/kvtest/pkg/adapters/tinykvpp.go (1)

15-21: Config has MaxRetries but no retry logic; either implement or drop

If you want basic resilience, add a bounded, jittered retry on Unavailable/DeadlineExceeded for Get/Put. Otherwise, remove MaxRetries to avoid confusion.

I can add a simple retry helper with exponential backoff guarded by context deadlines; say the word and I’ll draft it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b61c853 and 4aa8fee.

⛔ Files ignored due to path filters (3)
  • tests/go/kvtest/go.sum is excluded by !**/*.sum
  • tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.pb.go is excluded by !**/*.pb.go
  • tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service_grpc.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (13)
  • Dockerfile (1 hunks)
  • infra/assets/tkvpp_config_standalone.json (2 hunks)
  • infra/docker-compose.standalone.yaml (1 hunks)
  • src/config.cpp (1 hunks)
  • tests/go/kvtest/cmd/kvtest/main.go (1 hunks)
  • tests/go/kvtest/cmd/root.go (1 hunks)
  • tests/go/kvtest/main.go (1 hunks)
  • tests/go/kvtest/pkg/adapters/tinykvpp.go (1 hunks)
  • tests/go/kvtest/pkg/core/config.go (1 hunks)
  • tests/go/kvtest/pkg/core/context.go (1 hunks)
  • tests/go/kvtest/pkg/core/interface.go (1 hunks)
  • tests/go/kvtest/proto/raft/v1/raft_service.proto (1 hunks)
  • tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto (1 hunks)
🔇 Additional comments (15)
tests/go/kvtest/main.go (1)

9-11: LGTM: main delegates to Cobra entrypoint.

Clean handoff to cmd.Execute(); once the duplicate main is removed and root command wired, this is good to go.

Dockerfile (2)

11-11: Ninja addition aligns with faster CMake builds

Adding ninja-build is a good move for faster incremental builds. Assuming your CMakePresets select the Ninja generator, this will reduce build times.


43-45: Verify Ninja generator in CMake presets

I couldn’t locate a CMakePresets.json in the repository to confirm that the conan-release and conan-debug presets set generator = "Ninja". Please manually verify that these presets include:

{
  "name": "conan-release", 
  …,
  "generator": "Ninja"
}

(and similarly for conan-debug). If they don’t, add -G Ninja explicitly to the cmake --preset invocation in your Dockerfile (Dockerfile lines 43–45):

RUN cp -f ./build/CMakePresets.json . && \
    cmake --preset conan-${BUILD_TYPE} && \
    cmake --build ./build -t Main
infra/assets/tkvpp_config_standalone.json (1)

6-6: Absolute data path matches container volume

Switching database.path to /var/tkvpp is consistent with the Docker volume mount and the Dockerfile’s mkdir/chmod.

src/config.cpp (2)

158-159: LGTM: fmt placeholder fixed

Swapping %s for {} in the "Unable to open config file" error message correctly uses fmt formatting.


63-84: Verify C++ mapping for maximumLevels before updating schema

I searched for a corresponding C++ field or config‐struct for “maximumLevels” (e.g. LSMTreeConfig, any from_json/to_json overloads, or direct references to "maximumLevels" in src/) but didn’t find one. Before adding the new property to the JSON schema, please:

• Confirm that your LSM configuration struct (whether named LSMConfig, LSMTreeConfig, etc.) actually declares a maximumLevels member (and that from_json/loader code reads it).
• If it doesn’t exist yet, add the field and mapping in the appropriate header/loader.
• Once the C++ side supports it, extend the schema in src/config.cpp as follows:

       "properties": {
         "flushThreshold": {
           "type": "integer",
           "description": "The threshold of bytes at which the memtable should be flushed",
           "minimum": 1
         },
+        "maximumLevels": {
+          "type": "integer",
+          "description": "Maximum number of LSM levels",
+          "minimum": 1
+        },
         "levelZeroCompaction": {
           "$ref": "#/$defs/compaction"
         },

This ensures the schema’s required list aligns with both your examples (infra/assets/...) and the actual C++ config handling.

infra/docker-compose.standalone.yaml (2)

4-4: Image name vs Dockerfile build target

Compose references image: tinykvpp-clang:latest, while Dockerfile defaults to TARGET=gcc. Ensure your CI publishes a clang-based image at this name or add a build section to compose with appropriate ARGs.

Suggested compose tweak if building locally:

-  tkvpp-node-standalone:
-    image: tinykvpp-clang:latest
+  tkvpp-node-standalone:
+    build:
+      context: ../..    # adjust to repo root containing Dockerfile
+      dockerfile: Dockerfile
+      args:
+        TARGET: clang
+    image: tinykvpp-clang:latest

9-13: Ports and volumes wiring looks consistent

9891 maps correctly and the config/volume mounts align with the absolute data path /var/tkvpp.

tests/go/kvtest/pkg/core/config.go (2)

65-71: Defaults align with infra (port 9891) and are sensible

Type "tinykvpp" and Address "localhost:9891" mirror the standalone setup. Solid starting point.


88-99: Scenario parameters are clear; keep ratios in sync with Weights

If you adopt Weights above, ensure these ratios align with the same intended mix to avoid surprise in downstream load generators.

tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto (1)

7-10: Service surface LGTM

Simple Put/Get for the test adapter is clear and minimal.

tests/go/kvtest/proto/raft/v1/raft_service.proto (2)

11-16: Command enum LGTM


17-22: NodeState enum LGTM

tests/go/kvtest/pkg/adapters/tinykvpp.go (2)

120-122: Name() LGTM


34-52: Confirm grpc.NewClient() availability and consider switching to DialContext

Our investigation shows:

  • There is no go.mod in the repo root; however, tests/go/kvtest/go.sum pins google.golang.org/grpc v1.74.2 (lines 53–54).
  • The adapter at tests/go/kvtest/pkg/adapters/tinykvpp.go:42 is calling grpc.NewClient(...), and there is no existing use of grpc.DialContext in the codebase.

Please verify the following:

• That the gRPC-Go version you’re actually using (e.g. v1.74.2) exports grpc.NewClient as a public, stable API. You can check this locally by running:

go doc google.golang.org/grpc NewClient

or

godoc -http=:6060  # then open pkg/google.golang.org/grpc/ and search for NewClient

• If NewClient isn’t part of the stable API (or was introduced in a very recent release you’re not on), refactor the connection code to use grpc.DialContext with a timeout and optional WithBlock(), for example:

 if a.config.Address == "" {
   return fmt.Errorf("missing address")
 }
 ctx, cancel := context.WithTimeout(context.Background(), a.config.ConnectionTimeout)
 defer cancel()
-  conn, err := grpc.NewClient(
-    a.config.Address,
-    grpc.WithTransportCredentials(insecure.NewCredentials()),
-  )
+  conn, err := grpc.DialContext(
+    ctx,
+    a.config.Address,
+    grpc.WithTransportCredentials(insecure.NewCredentials()),
+    grpc.WithBlock(),
+  )
 if err != nil {
   return fmt.Errorf("failed to connect to %s: %w", a.config.Address, err)
 }

Comment on lines +31 to 35
"port": 9891,
"id": 1,
"peers": [
"0.0.0.0:8081"
"0.0.0.0:9891"
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Peers should not use 0.0.0.0; make empty for standalone or use real endpoints

"0.0.0.0" is valid for binding but invalid as a remote peer address. In standalone mode, use an empty list; in clustered mode, list concrete host:port values for other nodes.

Apply:

-        "peers": [
-            "0.0.0.0:9891"
-        ]
+        "peers": []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"port": 9891,
"id": 1,
"peers": [
"0.0.0.0:8081"
"0.0.0.0:9891"
]
"port": 9891,
"id": 1,
"peers": []
🤖 Prompt for AI Agents
In infra/assets/tkvpp_config_standalone.json around lines 31 to 35, the peers
array contains "0.0.0.0:9891" which is incorrect for remote peer addresses;
update the peers field to be an empty array for standalone mode (i.e., "peers":
[]) or replace with concrete host:port entries when configuring a cluster, and
ensure the bind address remains the correct separate setting if needed.

security_opt:
- seccomp:unconfined
container_name: tkvpp-node-standalone
command: ["/app/tkvpp", "-c", "/app/config.json"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t repeat the binary when image has ENTRYPOINT

With ENTRYPOINT ["/app/tkvpp"] in the image, your current command repeats the binary, leading to "/app/tkvpp /app/tkvpp -c ...". Pass only flags.

Apply:

-    command: ["/app/tkvpp", "-c", "/app/config.json"]
+    command: ["-c", "/app/config.json"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
command: ["/app/tkvpp", "-c", "/app/config.json"]
command: ["-c", "/app/config.json"]
🤖 Prompt for AI Agents
In infra/docker-compose.standalone.yaml around line 8 the docker-compose service
command redundantly repeats the image ENTRYPOINT ("/app/tkvpp"), resulting in
"/app/tkvpp /app/tkvpp -c ..."; change the command to pass only the
flags/arguments (e.g. "-c", "/app/config.json") so Docker uses the image
ENTRYPOINT with these flags rather than invoking the binary twice.

Comment on lines 1 to 5
package main

import ()

func main() {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Avoid duplicate entrypoints; keep only one main.

You now have two mains:

  • tests/go/kvtest/main.go (calls cmd.Execute())
  • tests/go/kvtest/cmd/kvtest/main.go (this file)

Pick a single layout:

  • Option A (simplest): Keep tests/go/kvtest/main.go and delete tests/go/kvtest/cmd/kvtest/main.go.
  • Option B (conventional multi-command): Move root.go under tests/go/kvtest/cmd/kvtest (package cmd), keep this main to call cmd.Execute(), and remove tests/go/kvtest/main.go.

Having both will confuse consumers and CI; with the current syntax error, the module cannot be built.

Run the following to enumerate all main packages under kvtest and confirm you have only one after the change:


🏁 Script executed:

#!/bin/bash
# List all Go files that declare package main under tests/go/kvtest
rg -n '^package main\b' -g 'tests/go/kvtest/**/*.go' -S -C1

Length of output: 247


Remove the duplicate main entrypoint

The verification script still finds two package main declarations under tests/go/kvtest:

  • tests/go/kvtest/main.go
  • tests/go/kvtest/cmd/kvtest/main.go

You must choose one layout and delete the other to ensure there’s only a single executable entrypoint:

  • Option A (simplest):
    • Keep tests/go/kvtest/main.go (which calls cmd.Execute()).
    • Delete tests/go/kvtest/cmd/kvtest/main.go.

  • Option B (conventional multi-command):
    • Move root.go under tests/go/kvtest/cmd/kvtest (package cmd).
    • Keep tests/go/kvtest/cmd/kvtest/main.go to call cmd.Execute().
    • Delete tests/go/kvtest/main.go.

Maintaining both prevents the module from building and may confuse CI and downstream consumers. Please remove the unwanted main.go.

🤖 Prompt for AI Agents
In tests/go/kvtest/cmd/kvtest/main.go (lines 1-5) there's a duplicate package
main entrypoint; remove this file so only tests/go/kvtest/main.go remains as the
single executable entrypoint (Option A). After deletion, ensure
tests/go/kvtest/main.go calls cmd.Execute() and no other package/main files
exist under tests/go/kvtest; run `go build` or the test verifier to confirm the
module builds.

@@ -0,0 +1,5 @@
package main

import ()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix compile error: empty import block is invalid.

import () is a syntax error in Go (“expected import path, found ')'”). This file will not compile as-is.

If you intend to keep this entrypoint, wire it to Cobra and remove the empty import:

-package main
-
-import ()
-
-func main() {}
+package main
+
+import "github.com/lnikon/tinykvpp/tests/go/kvtest/cmd"
+
+func main() { cmd.Execute() }

Alternatively (recommended), delete this file entirely to avoid duplicate entrypoints (see next comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import ()
package main
import "github.com/lnikon/tinykvpp/tests/go/kvtest/cmd"
func main() { cmd.Execute() }
🤖 Prompt for AI Agents
In tests/go/kvtest/cmd/kvtest/main.go around line 3 there is an empty import
block (`import ()`) which causes a Go compile error; either delete this file
entirely (preferred to avoid duplicate entrypoints) or replace the empty import
with the actual imports and wire the file into Cobra by adding the needed
imports and invoking the root command (e.g., import the cobra/root command
package and call its Execute in main) so the file becomes a valid entrypoint.

Comment on lines 11 to 13
"github.com/lnikon/tinykvpp/tests/go/kvtest/pkg/core"
pb "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Import path for pb must match proto go_package

Once you fix go_package in tinykvpp_service.proto, ensure this import matches it. See the proto review for two consistent options.

Run this quick check in the repo to catch mismatches:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Proto go_package values:"
rg -nP 'option\s+go_package\s*=\s*"([^"]+)"' tests/go/kvtest/proto -n
echo
echo "pb imports in adapters:"
rg -nP '"github\.com/lnikon/tinykvpp/[^"]+/proto/tinykvpp/v1"' tests/go/kvtest/pkg -n

Length of output: 542


Adjust the pb import to match the go_package in your proto

The go_package in tinykvpp_service.proto is set to:

option go_package = "github.com/lnikon/tinykvpp/protos/tinykvpp/v1";

—but in tests/go/kvtest/pkg/adapters/tinykvpp.go you’re importing:

pb "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1"

These must be identical. Update the import to:

pb "github.com/lnikon/tinykvpp/protos/tinykvpp/v1"

so that it matches the go_package option exactly.

Locations to update:

  • tests/go/kvtest/pkg/adapters/tinykvpp.go: line 12

Diff example:

-   pb "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1"
+   pb "github.com/lnikon/tinykvpp/protos/tinykvpp/v1"
🤖 Prompt for AI Agents
In tests/go/kvtest/pkg/adapters/tinykvpp.go around lines 11 to 13, the protobuf
import alias `pb` points to the wrong module path (tests/go/kvtest/proto/...)
which does not match the `go_package` declared in the proto; update the `pb`
import to the exact go_package path
"github.com/lnikon/tinykvpp/protos/tinykvpp/v1" so the import matches the proto
option exactly and resolves package mismatches.

Comment on lines +1 to +5
syntax = "proto3";

package raft.v1;

service RaftService {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Add go_package for Go codegen; without it, imports will be fragile

The Go plugin strongly recommends specifying go_package. Add one that matches where you’ll import the generated code.

 syntax = "proto3";

 package raft.v1;

+option go_package = "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/raft/v1";

If you plan to place generated files under a different path, tell me the final location so I can align imports across the test harness.


Add a go_package option for reliable Go imports

To ensure generated Go code imports consistently (and avoid fragile relative paths), specify the go_package option in your .proto file. For example, if your module path in go.mod is github.com/lnikon/tinykvpp and you’re placing the generated files under tests/go/kvtest/proto/raft/v1, add:

File: tests/go/kvtest/proto/raft/v1/raft_service.proto (lines 1–5)

 syntax = "proto3";

 package raft.v1;

+option go_package = "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/raft/v1";
 
 service RaftService {

• Verify that this import path matches exactly where you’ll generate the Go code.
• If you intend to generate into a different location, please share the final import path so we can align the go_package value with your test harness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
syntax = "proto3";
package raft.v1;
service RaftService {
syntax = "proto3";
package raft.v1;
option go_package = "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/raft/v1";
service RaftService {
🤖 Prompt for AI Agents
tests/go/kvtest/proto/raft/v1/raft_service.proto lines 1-5: add a go_package
option to the .proto so generated Go imports are stable; set it to your module
path plus the generated path (for example use
github.com/lnikon/tinykvpp/tests/go/kvtest/proto/raft/v1 and append a short
package name after a semicolon, e.g. ;raftv1), ensuring the import path exactly
matches where you will generate the Go code or update the value to match the
final generation target.

Comment on lines +30 to +38
message AppendEntriesRequest {
uint32 term = 1;
string leader_id = 2;
uint32 prev_log_index = 3;
uint32 prev_log_term = 4;
repeated LogEntry entries = 5;
uint32 leader_commit = 6;
uint32 sender_id = 7;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

ID types are inconsistent; unify to a single type across requests/responses

leader_id is string here while sender_id is uint32; responses use responder_id uint32. Pick one type (string or uint32/uint64) and use it consistently across all messages. For Raft, numeric IDs (uint64) are common.

Example (numeric IDs):

 message AppendEntriesRequest {
   uint32 term = 1;
-  string leader_id = 2;
+  uint32 leader_id = 2;
   uint32 prev_log_index = 3;
   uint32 prev_log_term = 4;
   repeated LogEntry entries = 5;
   uint32 leader_commit = 6;
   uint32 sender_id = 7;
 }

Apply the same choice to RequestVoteRequest and all responses.

Do you prefer uint64 for term/index/node IDs to avoid future overflow in long-running tests?


🏁 Script executed:

#!/bin/bash
set -e

proto_file="tests/go/kvtest/proto/raft/v1/raft_service.proto"

if [ ! -f "$proto_file" ]; then
  echo "Error: $proto_file not found" >&2
  exit 1
fi

echo "--- AppendEntriesRequest ---"
grep -n -C2 "message AppendEntriesRequest" "$proto_file" || true
echo

echo "--- RequestVoteRequest ---"
grep -n -C2 "message RequestVoteRequest" "$proto_file" || true
echo

echo "--- RequestVoteResponse ---"
grep -n -C2 "message RequestVoteResponse" "$proto_file" || true
echo

echo "--- AppendEntriesResponse ---"
grep -n -C2 "message AppendEntriesResponse" "$proto_file" || true
echo

echo "--- Occurrences of responder_id ---"
grep -n "responder_id" "$proto_file" || true

Length of output: 1387


Unify all node ID fields to a single numeric type

Across the four Raft RPC messages, the ID fields use mixed types— leader_id is a string, while sender_id, candidate_id, and both responder_id fields are uint32. We should pick one numeric type and apply it consistently. In Raft implementations it’s common to use 64-bit unsigned integers for node IDs (to avoid overflow in long-running clusters and tests).

Please update:

  • AppendEntriesRequest:
    • change string leader_id = 2;uint64 leader_id = 2;
    • change uint32 sender_id = 7;uint64 sender_id = 7;
  • RequestVoteRequest:
    • change uint32 candidate_id = 2;uint64 candidate_id = 2;
  • AppendEntriesResponse:
    • change uint32 responder_id = 4;uint64 responder_id = 4;
  • RequestVoteResponse:
    • change uint32 responder_id = 3;uint64 responder_id = 3;

Example diff for AppendEntriesRequest:

 message AppendEntriesRequest {
-  uint32 term = 1;
-  string leader_id = 2;
+  uint32 term = 1;          // term/index fields left as uint32 for now
+  uint64 leader_id = 2;     // unify node IDs to uint64
   uint32 prev_log_index = 3;
   uint32 prev_log_term = 4;
   repeated LogEntry entries = 5;
   uint32 leader_commit = 6;
-  uint32 sender_id = 7;
+  uint64 sender_id = 7;     // unified to uint64
 }

Question: should we also bump term, prev_log_index, prev_log_term, and leader_commit from uint32 to uint64 now to fully future-proof against very large log sizes and term numbers?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
message AppendEntriesRequest {
uint32 term = 1;
string leader_id = 2;
uint32 prev_log_index = 3;
uint32 prev_log_term = 4;
repeated LogEntry entries = 5;
uint32 leader_commit = 6;
uint32 sender_id = 7;
}
message AppendEntriesRequest {
uint32 term = 1; // term/index fields left as uint32 for now
uint64 leader_id = 2; // unify node IDs to uint64
uint32 prev_log_index = 3;
uint32 prev_log_term = 4;
repeated LogEntry entries = 5;
uint32 leader_commit = 6;
uint64 sender_id = 7; // unified to uint64
}

Comment on lines +55 to +59
message RequestVoteResponse {
uint32 term = 1;
uint32 vote_granted = 2;
uint32 responder_id = 3;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

vote_granted should be a bool, not uint32

This is a correctness bug; consumers will expect a boolean.

 message RequestVoteResponse {
   uint32 term = 1;
-  uint32 vote_granted = 2;
+  bool vote_granted = 2;
   uint32 responder_id = 3;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
message RequestVoteResponse {
uint32 term = 1;
uint32 vote_granted = 2;
uint32 responder_id = 3;
}
message RequestVoteResponse {
uint32 term = 1;
bool vote_granted = 2;
uint32 responder_id = 3;
}
🤖 Prompt for AI Agents
In tests/go/kvtest/proto/raft/v1/raft_service.proto around lines 55-59, the
RequestVoteResponse.message defines vote_granted as uint32 but it should be a
bool; change the field type to "bool vote_granted = 2;" (keep the field number 2
unchanged), then regenerate all protobuf artifacts (Go, any other language
bindings) and update any tests or call sites that set or read vote_granted to
use boolean values instead of integers to ensure consumers see the correct type.


package tinykvpp.v1;

option go_package = "github.com/lnikon/tinykvpp/protos/tinykvpp/v1";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align go_package with actual import path to avoid Go import mismatches

Adapter imports pb from tests/go/kvtest/proto/tinykvpp/v1, but go_package points to github.com/lnikon/tinykvpp/protos/tinykvpp/v1. This will generate code under a different module path than the one you import, leading to build errors.

Apply one of the following diffs (pick one and keep things consistent across repo):

Option A — keep current adapter import path:

-option go_package = "github.com/lnikon/tinykvpp/protos/tinykvpp/v1";
+option go_package = "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1";

Option B — move generated code to match go_package and update adapter import:

-	pb "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1"
+	pb "github.com/lnikon/tinykvpp/protos/tinykvpp/v1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
option go_package = "github.com/lnikon/tinykvpp/protos/tinykvpp/v1";
++ b/tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto
@@ Lines 5-5
option go_package = "github.com/lnikon/tinykvpp/tests/go/kvtest/proto/tinykvpp/v1";

Comment on lines +31 to +45
// For internal database use
message DatabaseOperation {
enum Type {
TYPE_UNSPECIFIED = 0;
TYPE_PUT = 1;
TYPE_DELETE = 2;
TYPE_BATCH = 3;
}

Type type = 1;
string key = 2;
string value = 3;
// Reserve 4 for Batch operations
uint64 request_id = 5;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Reserve field numbers explicitly and sketch batch extensibility

You skipped field 4 with a comment, but did not formally reserve it. Explicit reservations prevent accidental reuse and wire incompatibilities.

 message DatabaseOperation {
   enum Type {
     TYPE_UNSPECIFIED = 0;
     TYPE_PUT = 1;
     TYPE_DELETE = 2;
     TYPE_BATCH = 3;
   }

   Type type = 1;
   string key = 2;
   string value = 3;
-  // Reserve 4 for Batch operations
+  // Reserved for future batch payload
+  reserved 4;
   uint64 request_id = 5;
 }

Optional: If batch is imminent, define it now to avoid another wire change:

message BatchOperation {
  repeated DatabaseOperation ops = 1;
}

Would you like me to propagate the corresponding Go changes (adapter + tests) if you switch to bytes and/or add BatchOperation?


Add explicit reserved 4; and sketch BatchOperation for forward‐compatibility

To avoid accidental reuse of tag 4 and ensure wire compatibility, please formally reserve it rather than just commenting it out:

• File: tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto
• Location: inside message DatabaseOperation, between fields 3 and 5

Proposed diff:

 message DatabaseOperation {
   enum Type {
     TYPE_UNSPECIFIED = 0;
     TYPE_PUT         = 1;
     TYPE_DELETE      = 2;
     TYPE_BATCH       = 3;
   }

   Type    type       = 1;
   string  key        = 2;
   string  value      = 3;
-  // Reserve 4 for Batch operations
+  // Reserved for future batch payload
+  reserved 4;
   uint64  request_id = 5;
 }

Optional enhancement: if you intend to implement batching soon, add a BatchOperation now to avoid another wire‐breaking change:

message BatchOperation {
  repeated DatabaseOperation ops = 1;
}
🤖 Prompt for AI Agents
In tests/go/kvtest/proto/tinykvpp/v1/tinykvpp_service.proto around lines 31-45,
the comment "Reserve 4 for Batch operations" should be implemented by formally
reserving tag 4 inside message DatabaseOperation to prevent reuse (add the
reserved 4; declaration between fields 3 and 5), and optionally add a new
message BatchOperation (containing a repeated DatabaseOperation field) after
DatabaseOperation so future batching uses a defined type instead of reclaiming
tag 4; ensure the .proto compiles and update any generated stubs if present.

lnikon and others added 9 commits August 26, 2025 01:12
- Add interface for scenarios
- Implement CRUD scenario
- Add integration test executor
- Add test orchestrator
- Implement simple CRUD testing via kvtest
- Fix memtables not being flushed
- Leftover cleanup
- Ignore binaries
- ReadYourWrites scenario in-progress
- ReadYourWrites scenario in-progress
- Implement basic memtable stress test
- General refactoring
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

1 participant