Skip to content

Conversation

@tendinginfinity24
Copy link

@tendinginfinity24 tendinginfinity24 commented Sep 5, 2025

This PR implements the KeyValueStore with the following features:

Thread-safe in-memory key-value storage using sync.RWMutex.

Persistent storage with Load() and Save() methods.

Set, Get, and Delete operations with proper error handling.

Added and fixed unit tests to validate functionality (TestLoadAndSave, TestMissingKey).

✅ All tests are passing.

Closes #44
{7374C2C9-7E97-488D-BC14-4A363AA25735}

Summary by CodeRabbit

  • New Features

    • Adds a persistent, thread-safe key-value store with atomic disk-backed load/save.
    • Supports set, get, and delete operations; returns a clear "key not found" error for missing keys.
    • Loading a missing file is a no-op (does not error).
  • Tests

    • Updates tests to validate persistence across instances, delete semantics, and missing-key behavior using the public API and temporary files.

@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

Implements a persistent, thread-safe key-value store with constructor, Load/Save (atomic file writes), Set/Get/Delete with RWMutex, and sentinel ErrKeyNotFound. Tests updated to exercise persistence, missing-key behavior, delete semantics, and loading a missing file.

Changes

Cohort / File(s) Summary
KV store implementation
internal/exercises/templates/35_basic_key_value_store/key_value_store.go
Added full KeyValueStore implementation: mu sync.RWMutex field, ErrKeyNotFound, NewKeyValueStore, Load(), Save() (atomic temp-file + rename), Set(), Get(), Delete(); file I/O and parsing logic; error propagation.
Tests: persistence & error cases
internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go
Replaced/rewrote tests to use public API only: tests for Load/Save persistence across instances, TestMissingKey (ErrKeyNotFound), TestDelete behavior, and TestLoadMissingFileIsNoop; uses t.TempDir() and filepath.Join.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant T as Test
  participant S as KeyValueStore
  participant FS as FileSystem

  Note over T,S: Persisting data
  T->>S: Set(key1, val1)
  T->>S: Set(key2, val2)
  T->>S: Save()
  activate S
  S->>FS: create temp file in same dir
  FS-->>S: file handle / error
  S->>FS: write buffered "key=value\n" lines
  FS-->>S: bytes written / error
  S->>FS: fs.Rename(temp -> target)  /* atomic */
  FS-->>S: success / error
  S-->>T: return err/nil
  deactivate S

  Note over T,S: Loading persisted data (new instance)
  T->>S: NewKeyValueStore(path)
  T->>S: Load()
  activate S
  S->>FS: open file for read
  FS-->>S: file handle / not found
  alt file exists
    S->>S: parse lines -> map under write lock
    S-->>T: return nil
  else file missing
    S-->>T: return nil (no-op)
  end
  deactivate S

  Note over T,S: Lookup / Delete
  T->>S: Get(key1)
  S-->>T: value or ErrKeyNotFound
  T->>S: Delete(key1)
  S-->>T: nil or ErrKeyNotFound
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement code in internal/exercises/templates/35_basic_key_value_store (#44)
All tests pass for exercise (#44) Cannot verify test run status from diff; changes include tests but CI results unknown.
Add/adjust docs or comments if needed (#44) No documentation or comment changes present in the diff.
Cross-link this issue in the PR description (#44) PR description content not included in diff; cannot verify.

Poem

I bury bytes where carrots hide,
I save each hop, I save with pride.
I load them back beneath the moon,
If one goes missing—thump! too soon.
A rabbit store, robust and spry, 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 0

🧹 Nitpick comments (6)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (3)

42-64: Avoid holding the write lock during file I/O; replace state atomically and handle long lines.

Reading from disk while holding s.mu blocks all readers/writers and can stall the app with large files. Also, Scanner’s default 64K token limit risks ErrTooLong. Build a temporary map without locking, then swap under a short write lock; increase the scanner buffer and trim whitespace. This also clears stale keys by replacing the map rather than merging.

Apply this diff:

 func (s *KeyValueStore) Load() error {
-  s.mu.Lock()
-  defer s.mu.Unlock()
-
-  file, err := os.Open(s.filepath)
+  file, err := os.Open(s.filepath)
   if err != nil {
     if os.IsNotExist(err) {
       return nil
     }
     return err
   }
   defer file.Close()
 
-  scanner := bufio.NewScanner(file)
-  for scanner.Scan() {
-    line := scanner.Text()
-    parts := strings.SplitN(line, "=", 2)
-    if len(parts) == 2 {
-      s.data[parts[0]] = parts[1]
-    }
-  }
-
-  return scanner.Err()
+  scanner := bufio.NewScanner(file)
+  scanner.Buffer(make(byte, 0), 1024*1024) // allow long lines (1MB)
+  tmp := make(map[string]string)
+  for scanner.Scan() {
+    line := scanner.Text()
+    if line == "" {
+      continue
+    }
+    parts := strings.SplitN(line, "=", 2)
+    if len(parts) == 2 {
+      k := strings.TrimSpace(parts[0])
+      v := strings.TrimSpace(parts[1])
+      tmp[k] = v
+    }
+  }
+  if err := scanner.Err(); err != nil {
+    return err
+  }
+  s.mu.Lock()
+  s.data = tmp
+  s.mu.Unlock()
+  return nil
 }

66-84: Make Save atomic and reduce lock scope to avoid torn files and long write lock holds.

Current Save holds a write lock across disk I/O and truncates the file in-place; a crash during write can corrupt the store. Snapshot under RLock, write to a temp file, fsync, then rename to the target (atomic on most OSes).

Apply these diffs (also adds a small import):

 import (
   "bufio"
   "fmt"
   "os"
+  "path/filepath"
   "strings"
   "sync"
 )
 func (s *KeyValueStore) Save() error {
-  s.mu.Lock()
-  defer s.mu.Unlock()
-  file, err := os.Create(s.filepath)
-  if err != nil {
-    return err
-  }
-  defer file.Close()
-
-  writer := bufio.NewWriter(file)
-  for k, v := range s.data {
-    _, err := fmt.Fprintf(writer, "%s=%s\n", k, v)
-    if err != nil {
-      return err
-    }
-  }
-  return writer.Flush()
+  // Snapshot under read lock
+  s.mu.RLock()
+  snapshot := make(map[string]string, len(s.data))
+  for k, v := range s.data {
+    snapshot[k] = v
+  }
+  s.mu.RUnlock()
+
+  dir := filepath.Dir(s.filepath)
+  tmp, err := os.CreateTemp(dir, ".kvtmp-*")
+  if err != nil {
+    return err
+  }
+  writer := bufio.NewWriter(tmp)
+  for k, v := range snapshot {
+    if _, err := fmt.Fprintf(writer, "%s=%s\n", k, v); err != nil {
+      tmp.Close()
+      _ = os.Remove(tmp.Name())
+      return err
+    }
+  }
+  if err := writer.Flush(); err != nil {
+    tmp.Close()
+    _ = os.Remove(tmp.Name())
+    return err
+  }
+  if err := tmp.Sync(); err != nil {
+    tmp.Close()
+    _ = os.Remove(tmp.Name())
+    return err
+  }
+  if err := tmp.Close(); err != nil {
+    _ = os.Remove(tmp.Name())
+    return err
+  }
+  return os.Rename(tmp.Name(), s.filepath)
 }

24-30: Use a sentinel error for missing keys to enable errors.Is checks and avoid allocations.

Returning new &StoreError each time makes comparisons clumsy. Use a package-level sentinel and reuse it in Get/Delete.

Apply these diffs:

 type StoreError struct {
   Message string
 }
 
 func (e *StoreError) Error() string {
   return e.Message
 }
+
+// Sentinel error for missing keys.
+var ErrKeyNotFound = &StoreError{Message: "key not found"}
   val, ok := s.data[key]
   if !ok {
-    return "", &StoreError{Message: "key not found"}
+    return "", ErrKeyNotFound
   }
   return val, nil
   if _, ok := s.data[key]; !ok {
-    return &StoreError{Message: "key not found"}
+    return ErrKeyNotFound
   }
   delete(s.data, key)
   return nil

Also applies to: 93-102, 104-113

internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (3)

13-16: Use t.TempDir instead of manual cleanup; drop ad-hoc helper.

Avoids leftover files and enables safe parallel runs.

Apply these diffs (also updates imports):

 import (
-  "os"
+  "errors"
+  "path/filepath"
   "testing"
 )
-// helper to clean up after tests
-func cleanup(filename string) {
-  _ = os.Remove(filename)
-}
+// no-op: using t.TempDir in tests below
 func TestLoadAndSave(t *testing.T) {
-  filename := "test_store.db"
-  defer cleanup(filename)
+  dir := t.TempDir()
+  filename := filepath.Join(dir, "test_store.db")
 func TestMissingKey(t *testing.T) {
-  filename := "test_store_missing.db"
-  defer cleanup(filename)
+  dir := t.TempDir()
+  filename := filepath.Join(dir, "test_store_missing.db")

Also applies to: 55-58


61-65: Assert the precise error using errors.Is and the sentinel.

Stronger assertion and future-proofing.

Apply this diff:

-  _, err := store.Get("notthere")
-  if err == nil {
-    t.Errorf("expected error for missing key, got nil")
-  }
+  _, err := store.Get("notthere")
+  if !errors.Is(err, ErrKeyNotFound) {
+    t.Errorf("expected ErrKeyNotFound, got %v", err)
+  }

1-66: Consider adding Delete and “missing file Load()” tests.

  • Add a Delete test (delete once ok, twice returns ErrKeyNotFound; Get after delete returns ErrKeyNotFound).
  • Add a Load() on non-existent file test (should return nil).

I can open a follow-up PR; or you can paste this test:

func TestDelete(t *testing.T) {
  t.Parallel()
  dir := t.TempDir()
  filename := filepath.Join(dir, "test_store.db")

  store := NewKeyValueStore(filename)
  store.Set("k", "v")

  if err := store.Delete("k"); err != nil {
    t.Fatalf("unexpected delete error: %v", err)
  }
  if _, err := store.Get("k"); !errors.Is(err, ErrKeyNotFound) {
    t.Fatalf("expected ErrKeyNotFound after delete, got %v", err)
  }
  if err := store.Delete("k"); !errors.Is(err, ErrKeyNotFound) {
    t.Fatalf("expected ErrKeyNotFound on second delete, got %v", err)
  }
}

func TestLoadMissingFileIsNoop(t *testing.T) {
  t.Parallel()
  dir := t.TempDir()
  filename := filepath.Join(dir, "does_not_exist.db")

  store := NewKeyValueStore(filename)
  if err := store.Load(); err != nil {
    t.Fatalf("expected nil error on missing file, got %v", err)
  }
}
📜 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 9c561fe and 1eaf792.

📒 Files selected for processing (2)
  • internal/exercises/templates/35_basic_key_value_store/key_value_store.go (2 hunks)
  • internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (1)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (1)
  • NewKeyValueStore (32-38)
🔇 Additional comments (2)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (2)

18-22: Solid, idiomatic state + locking.

Map + RWMutex + filepath fields look good for a basic, thread-safe store.


32-37: Constructor is correct.

Initializes map and records path; no issues.

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: 1

🧹 Nitpick comments (7)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (5)

91-97: Make persisted output deterministic (sorted keys).

This stabilizes diffs and eases debugging.

-writer := bufio.NewWriter(tmp)
-for k, v := range snapshot {
-  if _, err := fmt.Fprintf(writer, "%s=%s\n", k, v); err != nil {
+writer := bufio.NewWriter(tmp)
+keys := make([]string, 0, len(snapshot))
+for k := range snapshot {
+  keys = append(keys, k)
+}
+sort.Strings(keys)
+for _, k := range keys {
+  v := snapshot[k]
+  if _, err := fmt.Fprintf(writer, "%s=%s\n", k, v); err != nil {
     tmp.Close()
     _ = os.Remove(tmp.Name())
     return err
   }
 }

Add import:

 import (
   "bufio"
+  "errors"
   "fmt"
   "os"
   "path/filepath"
+  "sort"
   "strings"
   "sync"
 )

57-63: Preserve value whitespace; only trim keys.

Trimming value loses intentional leading/trailing spaces on round-trip.

- k := strings.TrimSpace(parts[0])
- v := strings.TrimSpace(parts[1])
+ k := strings.TrimSpace(parts[0])
+ v := parts[1] // preserve value as-is
  tmp[k] = v

If preserving value whitespace is undesirable, document it explicitly in the package comment.


57-63: Guard against non-round-trippable keys/values.

Newlines in values and '=' in keys break the parser. Either encode (e.g., base64 or JSON lines) or validate inputs in Set (reject keys containing "\n", "\r", or "=").

Example minimal validation in Set:

 func (s *KeyValueStore) Set(key, value string) {
   s.mu.Lock()
   defer s.mu.Unlock()
+  if strings.ContainsAny(key, "\n\r=") {
+    // alternatively: log/return error by changing signature
+    key = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(key, "\n", " "), "\r", " "), "=", ":")
+  }
   s.data[key] = value
 }

Also applies to: 115-120


19-29: *Return error, not StoreError (idiomatic API).

Using concrete *StoreError in signatures is unusual; prefer error with a sentinel error value.

Imports:

 import (
   "bufio"
+  "errors"
   "fmt"
   "os"
   "path/filepath"
   "strings"
   "sync"
 )

Replace custom type + sentinel:

-type StoreError struct {
-  Message string
-}
-
-func (e *StoreError) Error() string { return e.Message }
-
-// Sentinel error for missing keys.
-var ErrKeyNotFound = &StoreError{Message: "key not found"}
+// Sentinel error for missing keys.
+var ErrKeyNotFound = errors.New("key not found")

Adjust Get/Delete:

-func (s *KeyValueStore) Get(key string) (string, *StoreError) {
+func (s *KeyValueStore) Get(key string) (string, error) {
   s.mu.RLock()
   defer s.mu.RUnlock()
   val, ok := s.data[key]
   if !ok {
     return "", ErrKeyNotFound
   }
   return val, nil
 }

-func (s *KeyValueStore) Delete(key string) *StoreError {
+func (s *KeyValueStore) Delete(key string) error {
   s.mu.Lock()
   defer s.mu.Unlock()
   if _, ok := s.data[key]; !ok {
     return ErrKeyNotFound
   }
   delete(s.data, key)
   return nil
 }

Tests already use errors.Is and should continue to pass.

Also applies to: 122-131, 133-142, 3-10


30-36: Avoid parameter name shadowing the filepath import.

Minor readability nit.

-func NewKeyValueStore(filepath string) *KeyValueStore {
+func NewKeyValueStore(path string) *KeyValueStore {
   return &KeyValueStore{
     data:     make(map[string]string),
-    filepath: filepath,
+    filepath: path,
   }
 }
internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (2)

9-19: Run this test in parallel.

Safe with t.TempDir; speeds up the suite.

-func TestLoadAndSave(t *testing.T) {
+func TestLoadAndSave(t *testing.T) {
+  t.Parallel()

67-76: Nice: Load is a no-op for absent file.

Consider adding a companion test that Save creates missing parent directories (after implementing MkdirAll).

I can add tests for:

  • Save to nested non-existent dir.
  • Round-trip of values with leading/trailing spaces or '='.
    Want me to push those?
📜 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 1eaf792 and 43d7dfb.

📒 Files selected for processing (2)
  • internal/exercises/templates/35_basic_key_value_store/key_value_store.go (2 hunks)
  • internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (1)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (2)
  • NewKeyValueStore (31-36)
  • ErrKeyNotFound (28-28)
🔇 Additional comments (4)
internal/exercises/templates/35_basic_key_value_store/key_value_store.go (2)

68-71: LGTM: atomic map replacement under write lock.

Clear, simple, and avoids partial state during Load.


112-113: Confirm atomic rename semantics on target OS.

os.Rename within the same directory is typically atomic; on some platforms behavior differs when destination exists.

Would Windows support be required? If yes, consider documenting guarantees or switching to a well-known atomic write helper (e.g., rename-on-same-dir with replace semantics).

Also applies to: 84-89

internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go (2)

35-46: LGTM: correct sentinel check for missing key.


48-66: LGTM: delete semantics covered (including idempotent second delete).

Comment on lines +84 to +89
dir := filepath.Dir(s.filepath)
tmp, err := os.CreateTemp(dir, ".kvtmp-*")
if err != nil {
return err
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Create parent directory before atomic write.

Save fails if s.filepath’s directory doesn’t exist. Ensure dir is created.

 dir := filepath.Dir(s.filepath)
+if err := os.MkdirAll(dir, 0o755); err != nil {
+  return err
+}
 tmp, err := os.CreateTemp(dir, ".kvtmp-*")
 if err != nil {
   return err
 }
📝 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
dir := filepath.Dir(s.filepath)
tmp, err := os.CreateTemp(dir, ".kvtmp-*")
if err != nil {
return err
}
dir := filepath.Dir(s.filepath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".kvtmp-*")
if err != nil {
return err
}
🤖 Prompt for AI Agents
In internal/exercises/templates/35_basic_key_value_store/key_value_store.go
around lines 84 to 89, the code attempts to create a temp file in dir but fails
when the parent directory for s.filepath does not exist; call os.MkdirAll(dir,
0o755) (and check/return its error) before os.CreateTemp to ensure the parent
directory exists, then proceed with creating the temp file and the existing
atomic write flow.

@zhravan
Copy link
Owner

zhravan commented Sep 16, 2025

@tendinginfinity24: Thank you so much for contributing to the issue!! Really appreaciate your efforts!

Can you create a folder called solutions, copy these template and solve it there?

Already made those changes you can just move your change to that folder

Copy link
Collaborator

@kaushalyap kaushalyap left a comment

Choose a reason for hiding this comment

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

Haven't updated the catalog.yaml

@zhravan
Copy link
Owner

zhravan commented Oct 12, 2025

Haven't updated the catalog.yaml

@kaushalyap Not just that, exercise has been solved, that is wrong.

This solved should go to solution and exercise should remain as it is, incomplete.

@kaushalyap
Copy link
Collaborator

@zhravan yeah, also this should go under projects part. There are other things to discuss, I'll message you on Discord

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add solution: 35_basic_key_value_store

3 participants