slabbis

package module
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

slabbis

A minimal cache server and in-process cache library for Go, built on slabber.

Go Reference License

What it is

slabbis is a cache with exactly the features you need and none of the ones you don't.

It speaks enough of the Redis protocol to be a drop-in for pure caching workloads. It does not support persistence, replication, pub/sub, scripting, sorted sets, or streams. This is intentional. The operations manual fits in a README because there is nothing to operate.

What it supports

GET key
SET key value [EX seconds | PX milliseconds]
GETSET key value
GETEX key [EX seconds | PX milliseconds | PERSIST]
MGET key [key ...]
MSET key value [key value ...]
SETNX key value
GETDEL key
DEL key [key ...]
UNLINK key [key ...]
EXISTS key [key ...]
STRLEN key
INCR key
INCRBY key increment
DECR key
DECRBY key decrement
KEYS pattern
SCAN cursor [MATCH pattern] [COUNT count]
RANDOMKEY
COPY source destination
RENAME from to
DBSIZE
TYPE key
TTL key
PTTL key
EXPIRE key seconds
PEXPIRE key milliseconds
PERSIST key
FLUSH
PING [message]
QUIT
KEYS vs SCAN

Both commands enumerate live keys. KEYS returns all matching keys in one response. SCAN is the cursor-based iterator Redis clients use by default (most client libraries — including go-redis — issue SCAN, not KEYS, for key enumeration).

Because slabbis holds all keys in memory with no on-disk representation, a full scan is always available in a single pass. SCAN therefore always returns cursor 0 (scan complete) along with all matching keys. Clients that loop until cursor 0 will terminate correctly after the first call. COUNT is accepted and ignored — it is a hint for disk-based stores and has no meaning here.

That is the entire surface. If you need anything else, use Redis or Valkey.

Install

As a library
go get github.com/ha1tch/slabbis
As a server binary
go install github.com/ha1tch/slabbis/cmd/slabbis@latest

Requires Go 1.23 or later.

In-process usage

import (
    "time"
    "github.com/ha1tch/slabbis"
)

// Default config: NumCPU shards, five size classes, 1s reaper interval.
cache := slabbis.NewDefault()
defer cache.Close()

// Development / test config: tiny footprint, safe in constrained environments.
cache := slabbis.New(slabbis.DevConfig())
defer cache.Close()

// Guard against silent drops before calling Set.
cfg := slabbis.Config{...}
if len(value) > cfg.MaxValueSize() {
    // handle oversized value explicitly
}

// Store a value with a 30-second TTL.
cache.Set("session:abc123", []byte(`{"user":42}`), 30*time.Second)

// Retrieve — heap-allocates a copy safe to retain indefinitely.
val, ok := cache.GetCopy("session:abc123")

// Retrieve into a caller-supplied buffer; zero allocation in steady state.
// Pool or reuse dst across calls for best performance.
var dst []byte
dst, ok = cache.GetInto("session:abc123", dst)

// Check presence without retrieving.
cache.Exists("session:abc123")

// Remove it.
cache.Del("session:abc123")

The Cache interface is the stable contract. The concrete implementation is not exported; swap it for a Redis client in tests or multi-node deployments without changing application code.

Server usage

# TCP (default)
slabbis

# Unix socket
slabbis -addr unix:///tmp/slabbis.sock

# Custom shards and reaper interval
slabbis -addr 127.0.0.1:6379 -shards 16 -reaper 500ms

# Set maximum value size (single size class, 1MB ceiling)
slabbis -max-value 1048576

# Fine-grained size classes (128B / 4KB / 64KB)
slabbis -classes 128,4096,65536

# Reduce startup footprint (1 bucket per shard instead of NumCPU)
slabbis -shards 4 -buckets 1 -max-value 65536

# Development / CI mode: tiny footprint, overrides other Config flags
slabbis -dev

# Print version (all equivalent)
slabbis version
slabbis -v
slabbis -version
slabbis --version

Default address: 127.0.0.1:6379.

The startup log line includes the effective slab configuration:

slabbis: listening on 127.0.0.1:6379 (shards=8, classes=5, max_value=262144B)

Once running, any Redis client works:

redis-cli -p 6399 SET foo bar EX 60
redis-cli -p 6399 GET foo

Configuration

cache := slabbis.New(slabbis.Config{
    Shards:          16,             // key-space partitions; 0 = NumCPU
    ReaperInterval:  500*time.Millisecond,
    Classes: []slabber.SizeClass{   // Arena size classes for values
        {MaxSize: 128},
        {MaxSize: 1024},
        {MaxSize: 8192},
    },
})

DefaultClasses covers 64B, 512B, 4KB, 32KB, and 256KB. Values larger than the largest class are silently dropped — size your classes for your workload. A future patch will add a heap fallback for oversized values.

Memory model

Values are stored in a slabber Arena — one per shard — giving fixed-slot memory management with a lock-free read path. The key map holds only a slabber.ArenaRef (8 bytes) per entry, not the value itself.

On a Get or GetCopy or GetInto, the path is: shard RLock → map lookup → arena.Slot() (lock-free). GetCopy and GetInto additionally copy the value before releasing the lock. Concurrent reads on different keys in the same shard contend only on the RLock, not on the value memory.

On a Set, the old value is freed and a new slot is allocated before the map is updated, so the window where memory is live but unreferenced is minimised.

Architecture

slabbis/
  slabbis.go          Cache interface and *cache implementation
  server.go           RESP2 server wrapping Cache (pooled zero-alloc reads)
  version.go
  internal/
    resp/
      resp.go         Minimal RESP2 reader/writer
  cmd/
    slabbis/
      main.go         Standalone server binary
  perf/
    charts.py         Chart generation (matplotlib)
    report.tex        Performance report (LaTeX)
    report.pdf        Compiled report

What slabbis is not

  • Not persistent. Restart = empty cache. By design.
  • Not clustered. One process, one machine. By design.
  • Not a Redis replacement for workloads that use pub/sub, streams, Lua, or sorted sets.
  • Not safe for values larger than 256KB by default (configurable via Classes).

Requirements

  • Go 1.23 or later
  • slabber v0.2.3 or later (pulled automatically via go get)

Development notes

Race detector on Apple Silicon

make test-race is clean on all platforms including Apple Silicon (arm64/darwin).

Background: a class of false positives can appear in connection-per-goroutine servers on Apple Silicon, where Go's race detector fires on bufio.Reader accesses after the allocator reuses a freed address for a new connection's reader. The detector tracks accesses by heap address, not object identity, and does not clear shadow memory on free/reallocate cycles. This affects any server using bufio.Reader per connection, including the Go standard library's net/http.

How slabbis handles it: ReadCommand, readLine, and readBulkString in internal/resp carry //go:norace. These are the only functions that touch the per-connection bufio.Reader; the cache operations they feed into remain fully instrumented. The annotations suppress the false positives without hiding any real races.

The test suite has been verified clean under go test -race on both Linux/amd64 and Apple Silicon (arm64/darwin). make test-race is safe to use on all platforms.

License

Copyright (c) 2026 haitch
Apache License 2.0 — see LICENSE for details.
https://www.apache.org/licenses/LICENSE-2.0

Documentation

Overview

Package slabbis implements an in-process cache with an optional RESP-compatible server layer.

Design:

  • Values are stored in a slabber Arena, giving fixed-slot memory management with a lock-free read path. Variable-length values are accommodated via Arena size classes; values larger than the largest class are stored directly on the heap (escape hatch, not the common path).

  • Keys are managed in a sharded hash map — one shard per logical CPU — to distribute mutex contention. Each shard maps string keys to Entry values holding the slabber ArenaRef, the stored length, and the expiry.

  • TTL eviction runs in a background goroutine per shard, scanning for expired entries on a configurable interval.

  • The Cache interface is the public contract. The concrete *cache type satisfies it. A Server wraps a Cache and speaks RESP over a net.Listener.

Concurrency properties:

  • Get: one shard RLock + one slabber Slot() call (lock-free after shard)
  • Set: one shard Lock + one Arena Alloc + possible Arena Free of old value
  • Del: one shard Lock + one Arena Free
  • The slabber read path (Slot) holds no lock.

Index

Constants

View Source
const Version = "0.1.5"

Version is the current slabbis release.

Variables

View Source
var DefaultClasses = []slabber.SizeClass{
	{MaxSize: 64},
	{MaxSize: 512},
	{MaxSize: 4096},
	{MaxSize: 32768},
	{MaxSize: 262144},
}

DefaultClasses provides five size classes covering typical cache values: 64B, 512B, 4KB, 32KB, 256KB.

Functions

This section is empty.

Types

type Cache

type Cache interface {
	// Get returns the value for key and whether it was found.
	// The returned slice is a direct view into slabber memory.
	// Do not retain it across a subsequent Set or Del on the same key.
	Get(key string) ([]byte, bool)

	// GetCopy returns a heap-allocated copy of the value for key.
	// The copy is made while holding the shard read lock, so it is safe
	// to retain indefinitely. Use this wherever the caller cannot guarantee
	// the slice will not outlive a concurrent Set or Del on the same key
	// (e.g. in the server layer before writing to a network buffer).
	GetCopy(key string) ([]byte, bool)

	// GetInto copies the value for key into dst, growing dst if necessary,
	// and returns the populated slice and whether the key was found.
	// The copy is made while holding the shard read lock.
	//
	// Unlike GetCopy, GetInto does not allocate when cap(dst) >= len(value).
	// Callers that pool or reuse dst (e.g. one buffer per server connection)
	// achieve zero per-call allocations in steady state.
	//
	// The returned slice aliases dst. Callers must not retain it across a
	// subsequent call that reuses the same dst.
	GetInto(key string, dst []byte) ([]byte, bool)

	// Set stores value under key with the given TTL.
	// A zero TTL means the entry does not expire.
	Set(key string, value []byte, ttl time.Duration)

	// Del removes key. Returns true if the key existed.
	Del(key string) bool

	// Exists reports whether key is present and not expired.
	Exists(key string) bool

	// TTL returns the remaining lifetime of key.
	// Returns 0, false if the key does not exist.
	// Returns 0, true if the key exists but has no expiry.
	// Returns remaining, true if the key exists with an expiry.
	TTL(key string) (time.Duration, bool)

	// Flush removes all entries from the cache.
	Flush()

	// Stats returns a point-in-time snapshot of cache state.
	Stats() CacheStats

	// Keys returns all live keys matching pattern.
	// Pattern uses filepath.Match syntax: * matches any sequence, ? matches
	// any single character. Use "*" to return all keys.
	Keys(pattern string) []string

	// MGet returns values for the given keys in order.
	// Missing or expired keys produce a nil entry.
	// The returned slices are direct views into slabber memory.
	// Do not retain any entry across a subsequent Set or Del on any key
	// in the batch — the underlying slot may be reused after a free.
	MGet(keys ...string) [][]byte

	// MSet sets multiple key/value pairs atomically within each shard.
	// A zero TTL means no expiry. Existing keys are overwritten.
	MSet(ttl time.Duration, pairs map[string][]byte)

	// SetNX sets key to value only if the key does not already exist.
	// Returns true if the key was set.
	SetNX(key string, value []byte, ttl time.Duration) bool

	// GetDel returns the value for key and removes it atomically.
	GetDel(key string) ([]byte, bool)

	// Rename renames key from to key to. Returns false if from does not exist.
	Rename(from, to string) bool

	// DBSize returns the total number of live keys across all shards.
	DBSize() int

	// SetTTL updates the expiry of an existing live key without changing its
	// value. Returns false if the key does not exist or has already expired.
	//
	// A zero ttl removes the expiry, making the key permanent (equivalent to
	// PERSIST). A positive ttl sets a new absolute deadline from now.
	SetTTL(key string, ttl time.Duration) bool

	// GetSet atomically replaces the value for key with newVal and returns the
	// old value. The new entry is stored with the given ttl (0 = no expiry).
	// Returns (nil, false) if the key did not previously exist.
	GetSet(key string, newVal []byte, ttl time.Duration) ([]byte, bool)

	// IncrBy atomically increments the integer value stored at key by delta
	// (use negative delta for decrement) and returns the new value.
	// The value must be a decimal integer string; if it is not, or if the
	// result would overflow int64, an error is returned.
	// If the key does not exist it is created with value "0" before applying
	// the increment.
	// The stored value is always the decimal string representation of the
	// result, compatible with Redis INCR/DECR/INCRBY/DECRBY semantics.
	IncrBy(key string, delta int64) (int64, error)

	// Close stops background goroutines. The cache must not be used after Close.
	Close()
}

Cache is the public interface for slabbis. All methods are safe for concurrent use.

func New

func New(cfg Config) Cache

New returns a Cache configured by cfg.

func NewDefault

func NewDefault() Cache

NewDefault returns a Cache with default configuration.

type CacheStats

type CacheStats struct {
	Keys      int             // number of live (non-expired) keys
	SlabStats []slabber.Stats // one entry per Arena size class
}

CacheStats holds a point-in-time snapshot of cache state.

type Config

type Config struct {
	// Shards is the number of key-space partitions.
	// 0 defaults to runtime.NumCPU().
	Shards int

	// Classes defines the Arena size classes for value storage.
	// 0 defaults to DefaultClasses.
	Classes []slabber.SizeClass

	// ReaperInterval controls how often the TTL reaper runs per shard.
	// 0 defaults to 1 second.
	ReaperInterval time.Duration

	// BucketsPerShard is passed to each slabber Arena as the initial
	// bucket count. 0 defaults to runtime.NumCPU().
	BucketsPerShard int
}

Config controls cache construction.

func DevConfig added in v0.1.5

func DevConfig() Config

DevConfig returns a Config suitable for development, testing, and memory-constrained environments (CI containers, sandboxes).

Characteristics: two size classes (64B and 4KB), one shard, one bucket per shard, fast reaper interval. Total virtual address footprint is approximately 8MB — safe for any environment. Maximum storable value size is 4096 bytes.

For production use, prefer New(Config{}) which applies DefaultClasses and scales shards and buckets to the available CPU count.

func (Config) MaxValueSize added in v0.1.5

func (c Config) MaxValueSize() int

MaxValueSize returns the maximum value size in bytes that this configuration will accept. Values larger than this are silently dropped by Set.

The ceiling is the MaxSize of the largest configured size class. Callers can use this to guard against silent drops before calling Set:

if len(value) > cfg.MaxValueSize() {
    // handle oversize value explicitly
}

type Server

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

Server listens on a network address and dispatches RESP commands to a Cache. It supports exactly the commands slabbis exposes; anything else returns an error response rather than a panic.

Supported commands:

GET key
SET key value [EX seconds | PX milliseconds]
GETSET key value
GETEX key [EX seconds | PX milliseconds | PERSIST]
MGET key [key ...]
MSET key value [key value ...]
SETNX key value
GETDEL key
DEL key [key ...]
UNLINK key [key ...]
EXISTS key [key ...]
STRLEN key
INCR key
INCRBY key increment
DECR key
DECRBY key decrement
KEYS pattern
SCAN cursor [MATCH pattern] [COUNT count]
RANDOMKEY
COPY source destination
RENAME from to
DBSIZE
TYPE key
TTL key
PTTL key
EXPIRE key seconds
PEXPIRE key milliseconds
PERSIST key
FLUSH (non-standard; equivalent to FLUSHALL)
PING [message]
COMMAND (returns empty array — satisfies redis-cli startup probe)
QUIT

func NewServer

func NewServer(addr string, c Cache, logger *log.Logger) (*Server, error)

NewServer returns a Server bound to addr using the provided Cache. addr may be a TCP address ("127.0.0.1:6399") or a Unix socket path ("unix:///tmp/slabbis.sock" — the "unix://" prefix is stripped).

func (*Server) Addr

func (s *Server) Addr() string

Addr returns the address the server is listening on.

func (*Server) Close

func (s *Server) Close() error

Close stops the server and waits for all goroutines to return. It closes the listener first (causing Serve to return), then waits for Serve itself and all active connection handlers to finish.

func (*Server) Serve

func (s *Server) Serve() error

Serve accepts connections until the listener is closed. It returns the listener's close error, which is typically non-nil only when Close() has been called.

Serve registers itself with serveWg so that Close() can wait for the accept loop to return before declaring shutdown complete.

Directories

Path Synopsis
cmd
slabbis command
Command slabbis is a minimal cache server speaking a Redis-compatible protocol subset over TCP or a Unix socket.
Command slabbis is a minimal cache server speaking a Redis-compatible protocol subset over TCP or a Unix socket.
internal
resp
Package resp implements a minimal subset of the Redis Serialisation Protocol (RESP2) sufficient for slabbis's supported command set.
Package resp implements a minimal subset of the Redis Serialisation Protocol (RESP2) sufficient for slabbis's supported command set.

Jump to

Keyboard shortcuts

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