slabbis

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: Apache-2.0 Imports: 12 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]
MGET key [key ...]
MSET key value [key value ...]
SETNX key value
GETDEL key
DEL key [key ...]
EXISTS key [key ...]
KEYS pattern
RENAME from to
DBSIZE
TYPE key
TTL key
PTTL key
FLUSH
PING [message]
QUIT

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()

// 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

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

Default address: 127.0.0.1:6379.

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
  bench/
    main.go           Comparative benchmark: in-process vs slabbis-RESP vs Redis
  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.2"

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

	// 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.

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]
MGET key [key ...]
MSET key value [key value ...]
SETNX key value
GETDEL key
DEL key [key ...]
EXISTS key [key ...]
KEYS pattern
RENAME from to
DBSIZE
TYPE key
TTL key
PTTL 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.

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.

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