kvs

package module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2025 License: MIT Imports: 9 Imported by: 0

README

kvs

A high-performance, thread-safe key-value store library for Go with sharding, TTL support, batch operations, and persistence.

Features

  • Thread-Safe Operations: Full concurrency support with fine-grained shard-level locking
  • Sharding: Configurable sharding for improved concurrent performance
  • TTL Support: Redis-like key expiration with hybrid active/passive cleanup
  • Batch Operations: Optimized batch get/set/delete operations with per-shard locking
  • Custom Hash Functions: Pluggable hash functions for flexible key distribution
  • Snapshot Persistence: Save and load store state to/from disk with integrity checking
  • Zero External Dependencies: Built entirely on Go standard library
  • Race-Tested: All operations validated with Go's race detector

Installation

go get github.com/bay0/kvs

Quick Start

package main

import (
    "fmt"
    "time"

    "github.com/bay0/kvs"
)

type Person struct {
    Name string
    Age  int
}

func (p Person) Clone() kvs.Value {
    return Person{
        Name: p.Name,
        Age:  p.Age,
    }
}

func main() {
    // Create a new key-value store with 10 shards
    store, err := kvs.NewKeyValueStore(10)
    if err != nil {
        panic(err)
    }
    defer store.Close() // Important: Close to stop background goroutines

    // Basic operations
    person := Person{Name: "Alice", Age: 30}

    // Set a value
    store.Set("alice", person)

    // Check if key exists (no error handling needed)
    if store.Exists("alice") {
        fmt.Println("Alice exists in store")
    }

    // Get the total number of keys (O(1) operation)
    fmt.Printf("Store contains %d keys\n", store.Len())

    // Get a value
    val, err := store.Get("alice")
    if err != nil {
        panic(err)
    }
    p := val.(Person)
    fmt.Printf("%s is %d years old\n", p.Name, p.Age)

    // Delete a key
    store.Delete("alice")
}

Core Operations

Basic Operations
// Set a key-value pair
err := store.Set("key", value)

// Get a value
val, err := store.Get("key")

// Delete a key
err := store.Delete("key")

// Check if a key exists (returns bool, never errors)
exists := store.Exists("key")

// Get the total number of keys (O(1) operation)
count := store.Len()

// Get all keys in the store
keys, err := store.Keys()
TTL Support

Keys can be set with automatic expiration. The store provides both active cleanup (background goroutine every 50ms) and passive cleanup (on access).

// Set a key with 5-minute TTL
err := store.SetWithTTL("session", sessionData, 5*time.Minute)

// Set a key with zero TTL (no expiration)
err := store.SetWithTTL("permanent", data, 0)

// Negative TTL returns ErrInvalidTTL
err := store.SetWithTTL("bad", data, -1*time.Second) // Error!

// Close the store to stop background cleanup goroutine
store.Close()

TTL Behavior:

  • TTL precision: ±50ms (background cleanup runs every 50ms)
  • Zero TTL: Treated as no expiration
  • Negative TTL: Returns ErrInvalidTTL
  • Expired keys: Return ErrNotFound on access (passive cleanup)
  • Background cleanup: Active removal every 50ms
Batch Operations

Batch operations optimize performance by acquiring locks once per shard rather than once per key.

// Batch Get - retrieve multiple keys at once
result, err := store.BatchGet([]string{"key1", "key2", "key3"})
if err != nil {
    panic(err)
}

// Check results
for key, value := range result.Values {
    fmt.Printf("%s: %v\n", key, value)
}

// Check errors for individual keys
for key, err := range result.Errors {
    fmt.Printf("%s failed: %v\n", key, err)
}

// Batch Set - store multiple key-value pairs at once
items := map[string]kvs.Value{
    "key1": value1,
    "key2": value2,
    "key3": value3,
}
result, err := store.BatchSet(items)
fmt.Printf("Successfully set %d keys\n", result.Successful)

// Batch Delete - remove multiple keys at once
result, err := store.BatchDelete([]string{"key1", "key2", "key3"})
fmt.Printf("Successfully deleted %d keys\n", result.Deleted)

Batch Operation Features:

  • Partial Success: One key's error doesn't affect others
  • Per-Shard Locking: Locks acquired once per shard, not per key
  • Complexity: O(N + M) where N=items, M=shards touched
  • Thread-Safe: Concurrent batch operations are safe
Custom Hash Functions

You can provide a custom hash function for key distribution across shards.

// Custom hash function using modulo
customHash := func(key string) int {
    sum := 0
    for _, ch := range key {
        sum += int(ch)
    }
    return sum % 10
}

// Create store with custom hash
store, err := kvs.NewKeyValueStoreWithHash(10, customHash)
if err != nil {
    panic(err)
}
defer store.Close()

// Your hash function MUST:
// - Be deterministic (same key always returns same index)
// - Return values in range [0, numShards)
// - Be thread-safe (may be called concurrently)

Default Hash Function: FNV-1a hashing

Snapshot Persistence

Save and load the entire store state to/from disk with integrity checking.

// Save snapshot to disk
err := store.SaveSnapshot("/path/to/snapshot.kvs")
if err != nil {
    panic(err)
}

// Load snapshot from disk
newStore, _ := kvs.NewKeyValueStore(10)
err = newStore.LoadSnapshot("/path/to/snapshot.kvs")
if err != nil {
    panic(err)
}
defer newStore.Close()

Snapshot Features:

  • Point-in-Time Consistency: Snapshot captures consistent state
  • TTL Preservation: Absolute expiration timestamps are saved
  • Replace Semantics: LoadSnapshot clears existing data
  • Expired Key Filtering: Expired keys are skipped during load
  • Integrity Checking: CRC32 checksums detect corruption
  • Versioned Format: Magic bytes and version validation
  • Thread-Safe: Concurrent with reads, uses per-shard RLock

File Format:

  • Magic bytes: "KVS1"
  • Version: 1
  • Encoding: Gob (Go's binary encoding)
  • Checksum: CRC32 (IEEE)

Error Codes

The library defines the following error codes:

const (
    ErrUnknown              // Unknown error
    ErrNotFound             // Key not found in store
    ErrDuplicate            // Key already exists (currently unused)
    ErrInvalidNumShards     // Invalid number of shards (≤ 0)
    ErrInvalidShardIndex    // Shard index out of bounds
    ErrCorruptedSnapshot    // Snapshot file is corrupted
    ErrUnsupportedVersion   // Snapshot version not supported
    ErrInvalidTTL           // TTL value is invalid (negative)
    ErrStoreClosed          // Operation on closed store
    ErrNilHashFunc          // Nil hash function provided
)

Interfaces

Value Interface

All values stored in the kvs must implement the Value interface:

type Value interface {
    // Clone creates a copy of the value
    Clone() Value
}
Store Interface

The core store interface defines essential operations:

type Store interface {
    Get(key string) (Value, error)
    Set(key string, val Value) error
    Delete(key string) error
    Keys() []string
}

Performance Characteristics

Operation Time Complexity Notes
Set O(1) Hash lookup + map insert
Get O(1) Hash lookup + map access
Delete O(1) Hash lookup + map delete
Exists O(1) Hash lookup + map check
Len O(1) Atomic counter read
Keys O(N) N = total keys
BatchGet O(N + M) N = keys, M = shards touched
BatchSet O(N + M) N = items, M = shards touched
BatchDelete O(N + M) N = keys, M = shards touched
SaveSnapshot O(N × S) N = keys, S = gob encode time
LoadSnapshot O(N × D) N = keys, D = gob decode time

Thread Safety

All operations are thread-safe:

  • Read Operations (Get, Exists, BatchGet): Use RLock (concurrent with other reads)
  • Write Operations (Set, Delete, BatchSet, BatchDelete): Use Lock (exclusive access)
  • Shard-Level Locking: Each shard has its own lock for improved concurrency
  • Atomic Operations: Len() uses atomic operations for lock-free reads
  • Race Tested: All operations validated with -race flag

Best Practices

  1. Always Close the Store: Call store.Close() to stop background TTL cleanup goroutines and prevent leaks

    store, _ := kvs.NewKeyValueStore(10)
    defer store.Close()
    
  2. Choose Appropriate Shard Count: More shards = better concurrency, but diminishing returns after ~10-100 shards depending on workload

  3. Use Batch Operations: When working with multiple keys, batch operations are significantly faster

    // Instead of multiple Get calls:
    result, _ := store.BatchGet([]string{"key1", "key2", "key3"})
    
  4. Check for Store Closed: After closing, all operations return ErrStoreClosed

    if err == kvs.ErrStoreClosed {
        // Handle closed store
    }
    
  5. Handle TTL Cleanup: Background cleanup runs every 50ms, providing ±50ms precision

  6. Implement Clone Correctly: Ensure your Value.Clone() creates a true deep copy

    func (p Person) Clone() kvs.Value {
        return Person{Name: p.Name, Age: p.Age} // Deep copy
    }
    

Examples

See the examples/ directory for complete working examples:

  • Basic Usage: Simple get/set/delete operations
  • Multi-threaded: Concurrent access patterns
  • Sharding with Consistent Hashing: Custom hash function example
  • TTL Management: Working with key expiration
  • Batch Operations: Optimized bulk operations
  • Snapshot Persistence: Save and restore store state

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please ensure:

  • All tests pass: go test -v -race ./...
  • Code is formatted: go fmt ./...
  • Linter passes: golangci-lint run
  • New features include tests and documentation

Documentation

Overview

Package kvs provides an in-memory key-value store implementation that supports sharding, batching, and transactions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BatchDeleteResult added in v0.0.2

type BatchDeleteResult struct {
	Deleted int              // Count of successfully deleted keys
	Errors  map[string]error // Failed deletions: key → error
}

BatchDeleteResult contains the results of a BatchDelete operation. Deleted count + len(Errors) == total input keys.

type BatchGetResult added in v0.0.2

type BatchGetResult struct {
	Values map[string]Value // Successful retrievals: key → value
	Errors map[string]error // Failed retrievals: key → error
}

BatchGetResult contains the results of a BatchGet operation. Keys appear in either Values (success) or Errors (failure), never both.

type BatchSetResult added in v0.0.2

type BatchSetResult struct {
	Successful int              // Count of successfully stored keys
	Errors     map[string]error // Failed operations: key → error
}

BatchSetResult contains the results of a BatchSet operation. Successful count + len(Errors) == total input items.

type ErrCode

type ErrCode int

ErrCode is an enumeration of error codes for the key-value store.

const (
	ErrUnknown ErrCode = iota
	ErrNotFound
	ErrDuplicate
	ErrInvalidNumShards
	ErrInvalidShardIndex
	ErrCorruptedSnapshot
	ErrUnsupportedVersion
	ErrInvalidTTL
	ErrStoreClosed
	ErrNilHashFunc
)

func (ErrCode) Error

func (c ErrCode) Error() string

Error returns the string representation of an error code.

type HashFunc added in v0.0.2

type HashFunc func(key string) int

HashFunc maps a key string to a shard index. Must be deterministic and return values in range [0, numShards).

type KeyValueStore

type KeyValueStore struct {
	// contains filtered or unexported fields
}

KeyValueStore is a type that implements the Store interface using an in-memory map.

func NewKeyValueStore

func NewKeyValueStore(numShards int) (*KeyValueStore, error)

NewKeyValueStore creates a new KeyValueStore instance with a specified number of shards. Uses the default FNV-1a hash function for key distribution. Starts a background goroutine for TTL cleanup - call Close() to stop it.

func NewKeyValueStoreWithHash added in v0.0.2

func NewKeyValueStoreWithHash(numShards int, hashFunc HashFunc) (*KeyValueStore, error)

NewKeyValueStoreWithHash creates a new store with a custom hash function. The hash function maps keys to shard indices and must be deterministic.

Parameters:

  • numShards: Number of shards (must be > 0)
  • hashFunc: Custom hash function (must return [0, numShards))

Returns error if numShards <= 0 or hashFunc is nil. Default behavior: Use NewKeyValueStore() for FNV-1a hash.

IMPORTANT: The hash function MUST:

  • Be deterministic (same key always returns same index)
  • Return values in range [0, numShards)
  • Be thread-safe (may be called concurrently)

func (*KeyValueStore) BatchDelete added in v0.0.2

func (kvs *KeyValueStore) BatchDelete(keys []string) (*BatchDeleteResult, error)

BatchDelete removes multiple keys with optimized locking. Returns count of successful deletions and per-key errors.

Thread-safe: Acquires Lock per shard (not per key) Complexity: O(N + M) where N=keys, M=shards touched Partial success: One key's error doesn't affect others Missing keys: Reported in Errors map with ErrNotFound

func (*KeyValueStore) BatchGet added in v0.0.2

func (kvs *KeyValueStore) BatchGet(keys []string) (*BatchGetResult, error)

BatchGet retrieves multiple keys in a single operation with optimized locking. Returns successful retrievals in Values map and per-key errors in Errors map.

Thread-safe: Acquires RLock per shard (not per key) Complexity: O(N + M) where N=keys, M=shards touched TTL-aware: Expired keys return ErrNotFound Partial success: One key's error doesn't affect others

func (*KeyValueStore) BatchSet added in v0.0.2

func (kvs *KeyValueStore) BatchSet(items map[string]Value) (*BatchSetResult, error)

BatchSet stores multiple key-value pairs with optimized locking. Returns count of successful operations and per-key errors.

Thread-safe: Acquires Lock per shard (not per key) Complexity: O(N + M) where N=items, M=shards touched Partial success: One key's error doesn't affect others Duplicate keys: Last value in input map wins

func (*KeyValueStore) Close added in v0.0.2

func (kvs *KeyValueStore) Close() error

Close gracefully shuts down the store, stopping background goroutines. Must be called to prevent goroutine leaks if TTL features were used.

Thread-safe: Can be called multiple times (idempotent) Blocks until: TTL cleanup goroutine terminates (timeout: 5 seconds) Post-close: All operations return ErrStoreClosed

func (*KeyValueStore) Delete

func (kvs *KeyValueStore) Delete(key string) error

Delete removes the key-value pair associated with the given key from the store. If the key is not found in the store, it returns an error.

func (*KeyValueStore) Exists added in v0.0.2

func (kvs *KeyValueStore) Exists(key string) bool

Exists returns true if the key is present in the store, false otherwise. This method does NOT return an error for missing keys (unlike Get).

Thread-safe: Uses shard-level RLock Complexity: O(1) hash lookup + map access TTL-aware: Returns false for expired keys

func (*KeyValueStore) Get

func (kvs *KeyValueStore) Get(key string) (Value, error)

Get retrieves the value associated with the given key from the store. If the key is not found in the store, it returns an error. TTL-aware: Returns ErrNotFound for expired keys (passive cleanup).

func (*KeyValueStore) Keys

func (kvs *KeyValueStore) Keys() ([]string, error)

Keys returns a slice of all the keys in the store.

func (*KeyValueStore) Len added in v0.0.2

func (kvs *KeyValueStore) Len() int

Len returns the total number of key-value pairs across all shards. This operation is O(1) using an atomic counter maintained by Set/Delete.

Thread-safe: Uses atomic load Complexity: O(1) TTL-aware: Includes keys that haven't been cleaned up yet (eventual consistency)

func (*KeyValueStore) LoadSnapshot added in v0.0.2

func (kvs *KeyValueStore) LoadSnapshot(filepath string) error

LoadSnapshot replaces the current store contents with data from a snapshot file. All existing data is cleared before loading (replace semantics). Expired keys in the snapshot are skipped (not restored).

Thread-safe: Acquires Lock on all shards (exclusive access) Complexity: O(N * D) where N=keys, D=gob decode time Atomicity: All-or-nothing (corruption or error leaves store unchanged) Version check: Rejects unsupported snapshot formats

func (*KeyValueStore) SaveSnapshot added in v0.0.2

func (kvs *KeyValueStore) SaveSnapshot(filepath string) error

SaveSnapshot writes the current store contents to a file with a consistent point-in-time view. The snapshot includes all keys, values, and TTL metadata with CRC32 integrity checking.

Thread-safe: Acquires RLock per shard (concurrent with reads) Complexity: O(N * S) where N=keys per shard, S=gob encode time TTL persistence: Absolute expiration timestamps saved File format: Versioned binary with gob encoding

func (*KeyValueStore) Set

func (kvs *KeyValueStore) Set(key string, val Value) error

Set adds or updates the given key-value pair in the store. If the key already exists, it overwrites the previous value.

func (*KeyValueStore) SetWithTTL added in v0.0.2

func (kvs *KeyValueStore) SetWithTTL(key string, val Value, ttl time.Duration) error

SetWithTTL stores a key-value pair with automatic expiration after the specified duration. The key will be automatically removed when TTL expires (active cleanup). Accessing an expired key returns ErrNotFound (passive cleanup).

Thread-safe: Acquires shard Lock Complexity: O(1) hash lookup + map insert TTL precision: ±50ms (background cleanup runs every 50ms) Zero TTL: Treated as no expiration Negative TTL: Returns ErrInvalidTTL

func (*KeyValueStore) Size

func (kvs *KeyValueStore) Size() string

Size returns the size of the store in human-readable format.

type Store

type Store interface {
	// Get retrieves the value associated with the given key from the store.
	// If the key is not found in the store, it returns an ErrNotFound error.
	Get(key string) (Value, error)

	// Set adds or updates the given key-value pair in the store.
	// If the key already exists, it overwrites the previous value.
	Set(key string, val Value) error

	// Delete removes the key-value pair associated with the given key from the store.
	// If the key is not found in the store, it returns an ErrNotFound error.
	Delete(key string) error

	// Keys returns a slice of all the keys in the store.
	Keys() []string
}

Store is an interface that defines the methods that a key-value store must implement.

type Value

type Value interface {
	// Clone creates a copy of the value.
	Clone() Value
}

Value is an interface that defines the methods that a value in the key-value store must implement.

Directories

Path Synopsis
examples
crawl-and-store command
multi-thread command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL