fractaltree

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 25, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

FractalTree

fractaltree

A write-optimized Bε-tree (fractal tree) for Go.
Buffer writes, flush in batches, iterate in sorted order.

Go Reference Version Go Version Go Report Card Coverage License


What is this?

fractaltree is a pure-Go, generic, concurrent-safe implementation of the Bε-tree data structure. It is an ordered key-value store that is optimized for write-heavy workloads: instead of propagating every write to a leaf immediately (like a B-tree), it buffers mutations as messages in internal nodes and flushes them downward in batches. This amortizes the cost of random I/O across many writes.

The library works entirely in-memory by default, but ships with a pluggable Flusher interface and Codec system for disk persistence. Zero external runtime dependencies.

Installation

go get github.com/aalhour/fractaltree@latest

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/aalhour/fractaltree"
)

func main() {
    t, err := fractaltree.New[string, int]()
    if err != nil {
        log.Fatal(err)
    }

    t.Put("alice", 100)
    t.Put("bob", 200)
    t.Put("charlie", 300)

    v, ok := t.Get("bob")
    fmt.Println(v, ok) // 200 true

    for k, v := range t.Range("alice", "charlie") {
        fmt.Println(k, v) // alice 100, bob 200
    }
}

Background: Bε-Trees

How It Works

A fractal tree, more formally known as an Bε-tree (pronounced "B-epsilon tree"), is a search tree from the family of write-optimized data structures introduced by Brodal and Fagerberg (2003) and later refined in the context of databases by Bender, Farach-Colton, Fineman, Fogel, Kuszmaul, and Nelson.

Key papers:

The core mechanism is simple:

  1. Buffered writes. Every internal node carries a message buffer. A write (put, delete, upsert) inserts a message into the root's buffer and returns immediately. No leaf is touched.
  2. Batch flush. When a node's buffer fills up, all messages are sorted by key, partitioned by the node's pivot keys, and pushed to the appropriate child. If the child is a leaf, messages are applied directly. If it is internal, they land in that child's buffer.
  3. Reads check buffers. A point query walks from root to leaf, collecting pending messages for the target key at every level. The most recent message wins (a DELETE cancels a prior PUT, an UPSERT transforms the value).
                    ┌──────────────────────────────────┐
         Root       │  pivots: [10, 20, 30]            │
       (internal)   │  buffer:  [PUT(25,"y"), DEL(7)]  │  ← writes land HERE first
                    └──┬──────┬──────┬──────┬──────────┘
                       │      │      │      │
                  <10  │ 10-20│ 20-30│  ≥30 │
                       ▼      ▼      ▼      ▼
                    [leaf]  [leaf] [leaf]  [leaf]
                    1,3,7   12,15  22,28   35,40

The Epsilon Parameter

The parameter ε (epsilon, where 0 < ε ≤ 1) controls how the block size B is divided between fanout and buffer capacity:

ε Fanout (Bε) Buffer (B1-ε) Character
0.3 8 512 Very large buffers, fastest bulk writes, slower reads
0.5 64 64 Balanced (default)
0.7 512 8 Small buffers, behaves closer to a B-tree
1.0 4096 1 Equivalent to a B-tree (no buffering)

Values shown for default B = 4096.

Complexity

Operation B-tree Bε-tree
Point read O(logB N) O(logB N)
Range (k results) O(logB N + k/B) O(logB N + k/B)
Insert / Delete O(logB N) O(logB N / B1-ε) amortized

For ε = 0.5 and B = 4096, writes are ~64x fewer I/Os than a B-tree in the amortized case.

Use Cases

Bε-trees were designed for workloads where writes dominate reads:

  • Databases and storage engines — TokuDB (MySQL), PerconaFT, BetrFS (file system). Any LSM-tree alternative that needs sorted order without compaction storms.
  • Time-series and event logging — high-throughput append of timestamped records with occasional range scans.
  • Write-ahead logs with indexed access — buffer mutations and flush periodically, while still supporting point lookups.
  • Counters and accumulators — the Upsert / Increment pattern lets you bump values without reading first.
  • Rate limiters and leaderboards — sorted structure supports both fast updates and ranked range queries.
  • ETL and bulk ingest pipelines — batch-load millions of records, then scan or export in sorted order.

Features

  • Generic — works with any key type: cmp.Ordered types via New, composite keys via NewWithCompare.
  • Concurrent-safesync.RWMutex with read-many / write-one semantics. Iterators take a snapshot and release the lock.
  • Rich query APIAll, Ascend, Descend, Range, DescendRange as Go 1.23+ iter.Seq2 iterators, plus a stateful Cursor with Seek / Next / Prev.
  • Upsert primitivesUpsert(key, fn), PutIfAbsent, Increment, CompareAndSwap for atomic read-modify-write.
  • Range deletionDeleteRange(lo, hi) removes an entire key range in one call.
  • Disk persistenceDiskBETree with pluggable Flusher interface and Codec system (ships with GobCodec).
  • TunableWithEpsilon and WithBlockSize to control the write/read tradeoff.
  • Zero runtime dependencies — only stretchr/testify and uber/goleak for tests.

API Overview

Constructors

// For cmp.Ordered keys (int, string, float64, ...)
t, err := fractaltree.New[string, int](
    fractaltree.WithEpsilon(0.5),   // default
    fractaltree.WithBlockSize(4096), // default
)

// For composite or custom-ordered keys
t, err := fractaltree.NewWithCompare[MyKey, MyVal](compareFn)

// With disk persistence
t, err := fractaltree.NewDisk[string, int](flusher,
    fractaltree.WithKeyCodec[string, int](myCodec),
    fractaltree.WithValueCodec[string, int](myCodec),
)

Core Operations

t.Put(key, value)               // Insert or overwrite
v, ok := t.Get(key)             // Point lookup
ok := t.Contains(key)           // Existence check
deleted := t.Delete(key)        // Remove single key
n := t.DeleteRange(lo, hi)      // Remove keys in [lo, hi)
t.Clear()                       // Remove all keys
n := t.Len()                    // Key count
t.Close()                       // Release resources

Upsert

// Read-modify-write with a custom function
t.Upsert("counter", func(existing *int, exists bool) int {
    if exists {
        return *existing + 1
    }
    return 1
})

// Built-in helpers
t.Upsert("hits", fractaltree.Increment(1))
t.Upsert("flag", fractaltree.CompareAndSwap(oldVal, newVal))
inserted := t.PutIfAbsent("key", value)

Iteration

All iterators use Go 1.23+ range-over-func (iter.Seq2[K, V]):

for k, v := range t.All()                { ... }  // ascending
for k, v := range t.Ascend()             { ... }  // same as All
for k, v := range t.Descend()            { ... }  // descending
for k, v := range t.Range(lo, hi)        { ... }  // [lo, hi) ascending
for k, v := range t.DescendRange(hi, lo) { ... }  // (lo, hi] descending

Cursor

For stateful traversal with seek support:

c := t.Cursor()
defer c.Close()

c.Seek(42)               // position at first key >= 42
for c.Valid() {
    fmt.Println(c.Key(), c.Value())
    c.Next()
}

Disk Persistence

Implement the Flusher interface to control where nodes are stored:

type Flusher[K, V any] interface {
    WriteNode(id uint64, data []byte) error
    ReadNode(id uint64) ([]byte, error)
    Sync() error
    Close() error
}

Key and value serialization uses the Codec interface:

type Codec[T any] interface {
    Encode(T) ([]byte, error)
    Decode([]byte) (T, error)
}

The default GobCodec works out of the box. See the examples/disktree-* directories for custom binary encodings.


Concurrency Model

BETree uses a single sync.RWMutex:

Operation Lock Blocks writers? Blocks readers?
Get, Contains, Len RLock No No
Put, Delete, Upsert, Clear, DeleteRange Lock Yes Yes
All, Range, Cursor (materialization) RLock No No
Iterator/Cursor use (after materialization) None No No

Iterators and cursors use snapshot semantics: they materialize a consistent view of the data under a read lock, then release the lock immediately. The snapshot is iterated without holding any lock, so writers are not blocked during traversal.


Tuning

Parameter Option Default Effect
Block size WithBlockSize(B) 4096 Controls leaf capacity and derived fanout/buffer size
Epsilon WithEpsilon(e) 0.5 Tradeoff between write throughput and read latency

Rules of thumb:

  • For write-heavy workloads (logging, counters, bulk ingest): lower ε (0.3 – 0.5) gives larger buffers and fewer flushes.
  • For read-heavy workloads (lookups, range scans): higher ε (0.5 – 0.8) gives more fanout and shallower trees.
  • For mixed workloads: the default (ε = 0.5, B = 4096) is a good starting point.
  • Larger block sizes improve throughput but increase memory per node.

Performance

All benchmarks on Apple M2 Max, 32 GB RAM, Go 1.26.2. Full results, Google BTree comparison, and reproduction instructions in docs/PERFORMANCE.md.

Workload Time Allocs Notes
Put/Sequential/1M 114 ms 7,980 Append fast-path
Put/Random/1M 1,344 ms 4,849 Batch merge
Get/Hit (100K) 15.1 ms 100,000 Sorted buffer binary search
Delete (100K) 17.0 ms 100,006 Buffered deletes
Range/10K (100K tree) 383 µs 10,023 Collect-and-merge iterator
Mixed read-heavy (100K) 34.8 ms 80,000 80% reads, 20% writes
Mixed write-heavy (100K) 83.6 ms 20,000 80% writes, 20% reads

vs Google BTree (highlights)

The Bε-tree and B-tree have different design points. FractalTree wins on sequential writes and point reads; Google BTree wins on random in-memory writes and range scans.

Benchmark FractalTree Google BTree Design point
Put/Sequential/100K 8.9 ms 9.7 ms Bε-tree (0.92x)
Put/Random/100K 60.6 ms 23.2 ms B-tree (2.62x)
Get/Hit (100K) 15.5 ms 21.7 ms Neutral (0.71x)
Get/Miss (100K) 5.1 ms 7.4 ms Neutral (0.69x)
Range/10K (100K tree) 412 µs 58 µs B-tree (7.1x)

FractalTree achieves 99.6% fewer allocations on write operations. See docs/PERFORMANCE.md for the full comparison with reclassified design-point analysis.


Examples

The examples/ directory contains 15 runnable programs:

Example Description
basic Put, Get, Delete, Contains, Len, Clear
comparator Custom composite key with NewWithCompare
range Range queries, Descend, Cursor with Seek
concurrent Multi-goroutine reads and writes
upsert Upsert, PutIfAbsent, Increment, CompareAndSwap
disktree-gob DiskBETree with default GobCodec
disktree-binary DiskBETree with encoding/binary codec
disktree-varint DiskBETree with hand-rolled varint codec
leaderboard Ranked leaderboard with top-N and rank lookup
timeseries Time-series ingestion with range queries
ratelimiter Sliding-window rate limiter
eviction LRU-style eviction by timestamp
bulkimport Bulk ingest of 1M records
mergejoin Merge join and anti-join using two cursors
prefixscan Prefix scan using Range with ASCII ordering

Run any example:

go run ./examples/basic

Run all examples:

make examples

Contributing

See CONTRIBUTING.md for the development workflow, testing practices, and benchmark procedures.

Design decisions are documented in docs/ADR.md — read the relevant records before modifying the flush or size-tracking paths.


License

Apache 2.0

Documentation

Overview

Package fractaltree provides an in-memory B-epsilon-tree (fractal tree), a write-optimized ordered key-value data structure.

A B-epsilon-tree amortizes write cost by buffering mutations as messages in internal nodes and flushing them toward leaves in batches. The parameter epsilon controls the trade-off between write throughput and read latency.

Quick Start

For keys that satisfy cmp.Ordered (int, string, float64, ...):

t, err := fractaltree.New[string, int]()
if err != nil {
    log.Fatal(err)
}
t.Put("alice", 42)
v, ok := t.Get("alice") // v=42, ok=true

For composite or custom-ordered keys, supply a comparator:

type TenantKey struct {
    Tenant string
    ID     int64
}

t, err := fractaltree.NewWithCompare[TenantKey, string](func(a, b TenantKey) int {
    if c := cmp.Compare(a.Tenant, b.Tenant); c != 0 {
        return c
    }
    return cmp.Compare(a.ID, b.ID)
})

Concurrency

A BETree is safe for concurrent use by multiple goroutines. Reads acquire a shared lock; writes acquire an exclusive lock. Iterators hold a shared lock for their lifetime, providing snapshot-consistent traversal.

Epsilon Parameter

Epsilon (set via WithEpsilon) must be in (0, 1] and defaults to 0.5. Given a block size B:

  • Fanout (max children per internal node): B^epsilon
  • Buffer capacity (max messages per node): B^(1-epsilon)

Lower epsilon favors write throughput; higher epsilon favors read latency.

Complexity

  • Point query: O(log_B N)
  • Range query: O(log_B N + k/B) for k results
  • Insert/Delete: O(log_B N / B^(1-epsilon)) amortized

Disk Extension

BETree operates entirely in memory. DiskBETree extends it with pluggable persistence via the Flusher interface. Implement Flusher to control how and where nodes are serialized. Key and value serialization is handled by the Codec interface, with GobCodec provided as a default.

Index

Examples

Constants

View Source
const (
	// DefaultEpsilon controls the buffer-vs-fanout trade-off.
	// At 0.5, an internal node has sqrt(B) children and sqrt(B) buffer slots.
	DefaultEpsilon = 0.5

	// DefaultBlockSize is the number of entries per node (leaf capacity).
	// With DefaultEpsilon, this yields fanout=64 and buffer capacity=64.
	DefaultBlockSize = 4096
)

Variables

View Source
var (
	// ErrClosed is returned when an operation is attempted on a closed tree.
	ErrClosed = errors.New("fractaltree: tree is closed")

	// ErrInvalidEpsilon is returned when epsilon is not in the valid range (0, 1].
	ErrInvalidEpsilon = errors.New("fractaltree: epsilon must be in (0, 1]")

	// ErrNilCompare is returned when a nil comparator is passed to NewWithCompare.
	ErrNilCompare = errors.New("fractaltree: compare function must not be nil")

	// ErrInvalidBlockSize is returned when block size is less than 2.
	ErrInvalidBlockSize = errors.New("fractaltree: block size must be at least 2")
)

Sentinel errors returned by the library.

Functions

This section is empty.

Types

type BETree

type BETree[K any, V any] struct {
	// contains filtered or unexported fields
}

BETree is an in-memory B-epsilon-tree. It is safe for concurrent use.

func New

func New[K cmp.Ordered, V any](opts ...Option) (*BETree[K, V], error)

New creates a BETree for keys that satisfy cmp.Ordered. The comparator is derived automatically via cmp.Compare.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, int]()

	t.Put("alice", 42)
	t.Put("bob", 7)

	v, ok := t.Get("alice")
	fmt.Println(v, ok)
	fmt.Println(t.Len())

}
Output:
42 true
2

func NewWithCompare

func NewWithCompare[K any, V any](compare func(K, K) int, opts ...Option) (*BETree[K, V], error)

NewWithCompare creates a BETree with a user-supplied comparator. The comparator must return a negative value when a < b, zero when a == b, and a positive value when a > b.

Example
package main

import (
	"cmp"
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	type Point struct {
		X, Y int
	}

	comparator := func(a, b Point) int {
		if c := cmp.Compare(a.X, b.X); c != 0 {
			return c
		}
		return cmp.Compare(a.Y, b.Y)
	}

	t, _ := fractaltree.NewWithCompare[Point, string](comparator)

	t.Put(Point{1, 2}, "a")
	t.Put(Point{1, 3}, "b")
	t.Put(Point{2, 1}, "c")

	for k, v := range t.All() {
		fmt.Printf("(%d,%d)=%s ", k.X, k.Y, v)
	}
	fmt.Println()

}
Output:
(1,2)=a (1,3)=b (2,1)=c

func (*BETree[K, V]) All

func (t *BETree[K, V]) All() iter.Seq2[K, V]

All returns an iterator over all key-value pairs in ascending order. The iterator operates on a snapshot taken at call time; concurrent mutations after All returns are not visible to the iterator.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(3, "three")
	t.Put(1, "one")
	t.Put(2, "two")

	for k, v := range t.All() {
		fmt.Println(k, v)
	}

}
Output:
1 one
2 two
3 three

func (*BETree[K, V]) Ascend

func (t *BETree[K, V]) Ascend() iter.Seq2[K, V]

Ascend returns an iterator over all key-value pairs in ascending order.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(3, "three")
	t.Put(1, "one")
	t.Put(2, "two")

	for k, v := range t.Ascend() {
		fmt.Println(k, v)
	}

}
Output:
1 one
2 two
3 three

func (*BETree[K, V]) Clear

func (t *BETree[K, V]) Clear()

Clear removes all entries from the tree.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")
	t.Clear()

	fmt.Println(t.Len())
	fmt.Println(t.Contains(1))

}
Output:
0
false

func (*BETree[K, V]) Close

func (t *BETree[K, V]) Close() error

Close marks the tree as closed. Subsequent operations will still work but this method exists to satisfy the Tree interface. For BETree it is effectively a no-op; DiskBETree uses it to flush and close storage.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	err := t.Close()
	fmt.Println(err)

}
Output:
<nil>

func (*BETree[K, V]) Contains

func (t *BETree[K, V]) Contains(key K) bool

Contains reports whether key exists in the tree.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, int]()

	t.Put("alice", 1)

	fmt.Println(t.Contains("alice"))
	fmt.Println(t.Contains("bob"))

}
Output:
true
false

func (*BETree[K, V]) Cursor

func (t *BETree[K, V]) Cursor() *Cursor[K, V]

Cursor returns a cursor for manual bidirectional iteration. The cursor operates on a snapshot taken at call time. It starts in an invalid state; call Next, Prev, or Seek to position it.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(10, "ten")
	t.Put(20, "twenty")
	t.Put(30, "thirty")

	c := t.Cursor()
	defer c.Close()

	c.Seek(15) // positions at first key >= 15
	fmt.Println(c.Key(), c.Value())

	c.Next()
	fmt.Println(c.Key(), c.Value())

}
Output:
20 twenty
30 thirty

func (*BETree[K, V]) Delete

func (t *BETree[K, V]) Delete(key K) bool

Delete removes key from the tree. Returns true if the key existed.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")

	fmt.Println(t.Delete(1))
	fmt.Println(t.Delete(99))
	fmt.Println(t.Len())

}
Output:
true
false
1

func (*BETree[K, V]) DeleteRange

func (t *BETree[K, V]) DeleteRange(lo, hi K) int

DeleteRange removes all keys in [lo, hi). Returns the count removed. Keys in the range are collected via a read-only traversal, then buffered as individual MsgDelete entries at the root. This gives O(K) insertion time (K = keys in range) with the benefit of batched flush to leaves.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	for i := range 10 {
		t.Put(i, "v")
	}

	removed := t.DeleteRange(3, 7) // removes keys 3, 4, 5, 6
	fmt.Println("removed:", removed)
	fmt.Println("len:", t.Len())
	fmt.Println("contains 3:", t.Contains(3))
	fmt.Println("contains 7:", t.Contains(7))

}
Output:
removed: 4
len: 6
contains 3: false
contains 7: true

func (*BETree[K, V]) Descend

func (t *BETree[K, V]) Descend() iter.Seq2[K, V]

Descend returns an iterator over all key-value pairs in descending order.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")
	t.Put(3, "three")

	for k, v := range t.Descend() {
		fmt.Println(k, v)
	}

}
Output:
3 three
2 two
1 one

func (*BETree[K, V]) DescendRange

func (t *BETree[K, V]) DescendRange(hi, lo K) iter.Seq2[K, V]

DescendRange returns an iterator over keys in (lo, hi] in descending order. Note: the first parameter is the high bound, the second is the low bound.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	for i := range 10 {
		t.Put(i, fmt.Sprintf("val%d", i))
	}

	// DescendRange(hi=7, lo=3) yields keys in (3, 7] = {7, 6, 5, 4} descending.
	for k, v := range t.DescendRange(7, 3) {
		fmt.Println(k, v)
	}

}
Output:
7 val7
6 val6
5 val5
4 val4

func (*BETree[K, V]) Get

func (t *BETree[K, V]) Get(key K) (V, bool)

Get returns the value for key and true, or the zero value and false.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")

	v, ok := t.Get(1)
	fmt.Println(v, ok)

	v, ok = t.Get(99)
	fmt.Println(v, ok)

}
Output:
one true
 false

func (*BETree[K, V]) Len

func (t *BETree[K, V]) Len() int

Len returns the number of key-value pairs in the tree.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	fmt.Println(t.Len())
	t.Put(1, "one")
	t.Put(2, "two")
	fmt.Println(t.Len())

}
Output:
0
2

func (*BETree[K, V]) Put

func (t *BETree[K, V]) Put(key K, value V)

Put inserts or overwrites the value for key.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")
	t.Put(1, "ONE") // overwrite

	v, _ := t.Get(1)
	fmt.Println(v)
	fmt.Println(t.Len())

}
Output:
ONE
2

func (*BETree[K, V]) PutIfAbsent

func (t *BETree[K, V]) PutIfAbsent(key K, value V) bool

PutIfAbsent inserts value only if key does not exist. Returns true if inserted.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, string]()

	fmt.Println(t.PutIfAbsent("key", "first"))
	fmt.Println(t.PutIfAbsent("key", "second"))

	v, _ := t.Get("key")
	fmt.Println(v)

}
Output:
true
false
first

func (*BETree[K, V]) Range

func (t *BETree[K, V]) Range(lo, hi K) iter.Seq2[K, V]

Range returns an iterator over keys in [lo, hi) in ascending order.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	for i := range 10 {
		t.Put(i, fmt.Sprintf("val%d", i))
	}

	// Range [3, 6) yields keys 3, 4, 5 in ascending order.
	for k, v := range t.Range(3, 6) {
		fmt.Println(k, v)
	}

}
Output:
3 val3
4 val4
5 val5

func (*BETree[K, V]) Upsert

func (t *BETree[K, V]) Upsert(key K, fn UpsertFn[V])

Upsert applies fn atomically to the value at key. If the key exists, fn receives a pointer to the current value and exists=true. Otherwise fn receives nil and exists=false. The returned value is stored.

The function is buffered as a MsgUpsert message and resolved lazily when the message reaches a leaf during flush, matching the B-epsilon-tree design.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, int]()

	t.Upsert("counter", fractaltree.Increment(1))
	t.Upsert("counter", fractaltree.Increment(1))
	t.Upsert("counter", fractaltree.Increment(1))

	v, _ := t.Get("counter")
	fmt.Println(v)

}
Output:
3

type Codec

type Codec[T any] interface {
	Encode(T) ([]byte, error)
	Decode([]byte) (T, error)
}

Codec defines how values of type T are serialized and deserialized. Implementations must be safe for concurrent use.

type Cursor

type Cursor[K any, V any] struct {
	// contains filtered or unexported fields
}

Cursor provides manual bidirectional iteration over a BETree. It operates on a snapshot taken at creation time; subsequent tree mutations are not visible. A Cursor must be closed when no longer needed.

func (*Cursor[K, V]) Close

func (c *Cursor[K, V]) Close()

Close releases resources held by the cursor. It is safe to call Close multiple times.

func (*Cursor[K, V]) Key

func (c *Cursor[K, V]) Key() K

Key returns the key at the cursor's current position. The caller must check Valid before calling Key.

func (*Cursor[K, V]) Next

func (c *Cursor[K, V]) Next() bool

Next advances the cursor to the next key-value pair. Returns true if the cursor is positioned at a valid entry after advancing.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")
	t.Put(3, "three")

	c := t.Cursor()
	defer c.Close()

	// Next advances forward. First call positions at the beginning.
	for c.Next() {
		fmt.Println(c.Key(), c.Value())
	}

}
Output:
1 one
2 two
3 three

func (*Cursor[K, V]) Prev

func (c *Cursor[K, V]) Prev() bool

Prev moves the cursor to the previous key-value pair. Returns true if the cursor is positioned at a valid entry after moving.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(1, "one")
	t.Put(2, "two")
	t.Put(3, "three")

	c := t.Cursor()
	defer c.Close()

	// Prev moves backward. First call positions at the end.
	for c.Prev() {
		fmt.Println(c.Key(), c.Value())
	}

}
Output:
3 three
2 two
1 one

func (*Cursor[K, V]) Seek

func (c *Cursor[K, V]) Seek(key K) bool

Seek positions the cursor at the first key >= the given key. Returns true if such a key exists.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[int, string]()

	t.Put(10, "ten")
	t.Put(20, "twenty")
	t.Put(30, "thirty")

	c := t.Cursor()
	defer c.Close()

	// Seek positions at the first key >= the target.
	fmt.Println(c.Seek(20))
	fmt.Println(c.Key(), c.Value())

	// Non-existent key: positions at next greater.
	fmt.Println(c.Seek(15))
	fmt.Println(c.Key(), c.Value())

	// Beyond max: returns false.
	fmt.Println(c.Seek(100))

}
Output:
true
20 twenty
true
20 twenty
false

func (*Cursor[K, V]) Valid

func (c *Cursor[K, V]) Valid() bool

Valid reports whether the cursor is positioned at a valid entry.

func (*Cursor[K, V]) Value

func (c *Cursor[K, V]) Value() V

Value returns the value at the cursor's current position. The caller must check Valid before calling Value.

type DiskBETree

type DiskBETree[K any, V any] struct {
	// contains filtered or unexported fields
}

DiskBETree wraps a BETree with a Flusher for disk persistence. All tree operations are delegated to the in-memory BETree. The Flusher is called during Close to sync data. Full lazy-loading and flush-time persistence are extension points for future implementation.

func NewDisk

func NewDisk[K cmp.Ordered, V any](
	f Flusher[K, V],
	opts ...DiskOption[K, V],
) (*DiskBETree[K, V], error)

NewDisk creates a DiskBETree for keys that satisfy cmp.Ordered.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/aalhour/fractaltree"
)

func main() {
	f := &memFlusher[string, int]{nodes: make(map[uint64][]byte)}
	t, _ := fractaltree.NewDisk[string, int](f)

	t.Put("key", 42)
	v, _ := t.Get("key")
	fmt.Println(v)
	fmt.Println(t.Close())

}

type memFlusher[K, V any] struct {
	mu    sync.Mutex
	nodes map[uint64][]byte
}

func (m *memFlusher[K, V]) WriteNode(id uint64, data []byte) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.nodes[id] = append([]byte(nil), data...)
	return nil
}

func (m *memFlusher[K, V]) ReadNode(id uint64) ([]byte, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	data, ok := m.nodes[id]
	if !ok {
		return nil, fmt.Errorf("node %d not found", id)
	}
	return data, nil
}

func (m *memFlusher[K, V]) Sync() error  { return nil }
func (m *memFlusher[K, V]) Close() error { return nil }
Output:
42
<nil>

func NewDiskWithCompare

func NewDiskWithCompare[K any, V any](
	compare func(K, K) int,
	f Flusher[K, V],
	opts ...DiskOption[K, V],
) (*DiskBETree[K, V], error)

NewDiskWithCompare creates a DiskBETree with a user-supplied comparator.

func (*DiskBETree[K, V]) All

func (d *DiskBETree[K, V]) All() iter.Seq2[K, V]

All delegates to the underlying BETree.

func (*DiskBETree[K, V]) Ascend

func (d *DiskBETree[K, V]) Ascend() iter.Seq2[K, V]

Ascend delegates to the underlying BETree.

func (*DiskBETree[K, V]) Clear

func (d *DiskBETree[K, V]) Clear()

Clear delegates to the underlying BETree.

func (*DiskBETree[K, V]) Close

func (d *DiskBETree[K, V]) Close() error

Close syncs and closes the flusher, then marks the tree as closed.

func (*DiskBETree[K, V]) Contains

func (d *DiskBETree[K, V]) Contains(key K) bool

Contains delegates to the underlying BETree.

func (*DiskBETree[K, V]) Cursor

func (d *DiskBETree[K, V]) Cursor() *Cursor[K, V]

Cursor delegates to the underlying BETree.

func (*DiskBETree[K, V]) Delete

func (d *DiskBETree[K, V]) Delete(key K) bool

Delete delegates to the underlying BETree.

func (*DiskBETree[K, V]) DeleteRange

func (d *DiskBETree[K, V]) DeleteRange(lo, hi K) int

DeleteRange delegates to the underlying BETree.

func (*DiskBETree[K, V]) Descend

func (d *DiskBETree[K, V]) Descend() iter.Seq2[K, V]

Descend delegates to the underlying BETree.

func (*DiskBETree[K, V]) DescendRange

func (d *DiskBETree[K, V]) DescendRange(hi, lo K) iter.Seq2[K, V]

DescendRange delegates to the underlying BETree.

func (*DiskBETree[K, V]) Get

func (d *DiskBETree[K, V]) Get(key K) (V, bool)

Get delegates to the underlying BETree.

func (*DiskBETree[K, V]) Len

func (d *DiskBETree[K, V]) Len() int

Len delegates to the underlying BETree.

func (*DiskBETree[K, V]) Put

func (d *DiskBETree[K, V]) Put(key K, value V)

Put delegates to the underlying BETree.

func (*DiskBETree[K, V]) PutIfAbsent

func (d *DiskBETree[K, V]) PutIfAbsent(key K, v V) bool

PutIfAbsent delegates to the underlying BETree.

func (*DiskBETree[K, V]) Range

func (d *DiskBETree[K, V]) Range(lo, hi K) iter.Seq2[K, V]

Range delegates to the underlying BETree.

func (*DiskBETree[K, V]) Upsert

func (d *DiskBETree[K, V]) Upsert(key K, fn UpsertFn[V])

Upsert delegates to the underlying BETree.

type DiskOption

type DiskOption[K, V any] func(*diskOptions[K, V])

DiskOption configures a DiskBETree.

func WithDiskBlockSize

func WithDiskBlockSize[K, V any](size int) DiskOption[K, V]

WithDiskBlockSize sets the block size for the underlying tree.

func WithDiskEpsilon

func WithDiskEpsilon[K, V any](eps float64) DiskOption[K, V]

WithDiskEpsilon sets the epsilon parameter for the underlying tree.

func WithKeyCodec

func WithKeyCodec[K, V any](c Codec[K]) DiskOption[K, V]

WithKeyCodec sets the codec used to serialize keys for disk storage. Defaults to GobCodec if not set.

func WithValueCodec

func WithValueCodec[K, V any](c Codec[V]) DiskOption[K, V]

WithValueCodec sets the codec used to serialize values for disk storage. Defaults to GobCodec if not set.

type Flusher

type Flusher[K, V any] interface {
	// WriteNode persists the serialized node data under the given ID.
	WriteNode(id uint64, data []byte) error

	// ReadNode retrieves the serialized node data for the given ID.
	ReadNode(id uint64) ([]byte, error)

	// Sync ensures all written data is durable.
	Sync() error

	// Close releases any resources held by the flusher.
	Close() error
}

Flusher defines the storage backend for a DiskBETree. Implementations handle persisting and retrieving serialized node data.

type GobCodec

type GobCodec[T any] struct{}

GobCodec is a Codec backed by encoding/gob. It works for any type that gob can handle (primitives, structs with exported fields, etc.).

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	c := fractaltree.GobCodec[string]{}

	data, _ := c.Encode("hello")
	v, _ := c.Decode(data)
	fmt.Println(v)
	fmt.Println(len(data), "bytes")

}
Output:
hello
9 bytes

func (GobCodec[T]) Decode

func (GobCodec[T]) Decode(data []byte) (T, error)

Decode deserializes data produced by Encode back into a value of type T.

func (GobCodec[T]) Encode

func (GobCodec[T]) Encode(v T) ([]byte, error)

Encode serializes v using encoding/gob.

type Message

type Message[K any, V any] struct {
	Kind MsgKind
	Key  K
	// EndKey is the exclusive upper bound for MsgDeleteRange. Unused for other kinds.
	EndKey K
	// Value is the payload for MsgPut. Zero value for other kinds.
	Value V
	// Fn is the upsert function for MsgUpsert. Nil for other kinds.
	Fn UpsertFn[V]
	// contains filtered or unexported fields
}

Message represents a buffered operation in a B-epsilon-tree node. Messages flow from root toward leaves during flushes.

type MsgKind

type MsgKind uint8

MsgKind identifies the type of a buffered message in a B-epsilon-tree node.

const (
	// MsgPut inserts or overwrites a key-value pair.
	MsgPut MsgKind = iota

	// MsgDelete removes a single key.
	MsgDelete

	// MsgDeleteRange removes all keys in [Key, EndKey).
	MsgDeleteRange

	// MsgUpsert applies a read-modify-write function to a key.
	MsgUpsert
)

type Option

type Option func(*options)

Option configures a BETree.

func WithBlockSize

func WithBlockSize(size int) Option

WithBlockSize sets the logical node size measured in number of entries. Must be at least 2. Default is 4096.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	// Smaller block size = more frequent flushes and splits.
	t, _ := fractaltree.New[int, string](fractaltree.WithBlockSize(64))

	t.Put(1, "one")
	fmt.Println(t.Len())

}
Output:
1

func WithEpsilon

func WithEpsilon(eps float64) Option

WithEpsilon sets the epsilon parameter that controls the trade-off between write throughput and read latency. Must be in (0, 1].

Lower epsilon means larger buffers (better write amortization) but higher fanout cost. Higher epsilon means smaller buffers but faster point queries.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	// Lower epsilon = larger buffers, better write amortization.
	t, _ := fractaltree.New[int, string](fractaltree.WithEpsilon(0.3))

	t.Put(1, "one")
	fmt.Println(t.Len())

}
Output:
1

type Tree

type Tree[K any, V any] interface {
	// Put inserts or overwrites the value for key.
	Put(key K, value V)

	// Get returns the value for key and true, or the zero value and false.
	Get(key K) (V, bool)

	// Delete removes key. Returns true if the key existed.
	Delete(key K) bool

	// DeleteRange removes all keys in [lo, hi). Returns the count removed.
	DeleteRange(lo, hi K) int

	// Contains reports whether key exists in the tree.
	Contains(key K) bool

	// Len returns the number of key-value pairs in the tree.
	Len() int

	// Clear removes all entries from the tree.
	Clear()

	// Upsert applies fn atomically to the value at key.
	Upsert(key K, fn UpsertFn[V])

	// PutIfAbsent inserts value only if key does not exist. Returns true if inserted.
	PutIfAbsent(key K, value V) bool

	// All returns an iterator over all key-value pairs in ascending order.
	All() iter.Seq2[K, V]

	// Ascend returns an iterator over all key-value pairs in ascending order.
	Ascend() iter.Seq2[K, V]

	// Descend returns an iterator over all key-value pairs in descending order.
	Descend() iter.Seq2[K, V]

	// Range returns an iterator over keys in [lo, hi) in ascending order.
	Range(lo, hi K) iter.Seq2[K, V]

	// DescendRange returns an iterator over keys in (lo, hi] in descending order.
	DescendRange(hi, lo K) iter.Seq2[K, V]

	// Cursor returns a positioned cursor for manual bidirectional iteration.
	Cursor() *Cursor[K, V]

	// Close releases resources held by the tree.
	Close() error
}

Tree is the interface implemented by both BETree (in-memory) and DiskBETree (with disk flush hooks).

type UpsertFn

type UpsertFn[V any] func(existing *V, exists bool) V

UpsertFn is called during upsert resolution. If the key exists, existing points to the current value and exists is true. Otherwise existing is nil and exists is false. The returned value is stored as the new value.

func CompareAndSwap

func CompareAndSwap[V comparable](expected, newVal V) UpsertFn[V]

CompareAndSwap returns an UpsertFn that replaces the value with newVal only if the current value equals expected. If the key does not exist or the current value differs, the existing value is retained unchanged.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, string]()

	t.Put("status", "pending")

	// CAS only updates if the current value matches expected.
	t.Upsert("status", fractaltree.CompareAndSwap("pending", "active"))
	v, _ := t.Get("status")
	fmt.Println(v)

	// Mismatch: value stays unchanged.
	t.Upsert("status", fractaltree.CompareAndSwap("pending", "closed"))
	v, _ = t.Get("status")
	fmt.Println(v)

}
Output:
active
active

func Increment

func Increment[V interface{ ~int | ~int64 | ~float64 }](delta V) UpsertFn[V]

Increment returns an UpsertFn that adds delta to the existing value. If the key does not exist, delta is used as the initial value.

Example
package main

import (
	"fmt"

	"github.com/aalhour/fractaltree"
)

func main() {
	t, _ := fractaltree.New[string, int]()

	// Increment creates an UpsertFn that adds delta to the value.
	// If the key is absent, delta becomes the initial value.
	t.Upsert("hits", fractaltree.Increment(1))
	t.Upsert("hits", fractaltree.Increment(5))

	v, _ := t.Get("hits")
	fmt.Println(v)

}
Output:
6

Directories

Path Synopsis
examples
basic command
Basic demonstrates Put, Get, Delete, Contains, Len, and Clear.
Basic demonstrates Put, Get, Delete, Contains, Len, and Clear.
bulkimport command
Bulkimport benchmarks inserting 1M keys to demonstrate the write amortization benefit of the B-epsilon-tree's message buffering.
Bulkimport benchmarks inserting 1M keys to demonstrate the write amortization benefit of the B-epsilon-tree's message buffering.
comparator command
Comparator demonstrates NewWithCompare with a composite key struct.
Comparator demonstrates NewWithCompare with a composite key struct.
concurrent command
Concurrent demonstrates safe concurrent reads and writes.
Concurrent demonstrates safe concurrent reads and writes.
disktree-binary command
Disktree-binary demonstrates DiskBETree with a codec backed by encoding/binary for fixed-size types (int32 keys, float64 values).
Disktree-binary demonstrates DiskBETree with a codec backed by encoding/binary for fixed-size types (int32 keys, float64 values).
disktree-gob command
Disktree-gob demonstrates DiskBETree with the default GobCodec.
Disktree-gob demonstrates DiskBETree with the default GobCodec.
disktree-varint command
Disktree-varint demonstrates DiskBETree with a hand-rolled codec that encodes strings as a varint length prefix followed by raw UTF-8 bytes.
Disktree-varint demonstrates DiskBETree with a hand-rolled codec that encodes strings as a varint length prefix followed by raw UTF-8 bytes.
eviction command
Eviction demonstrates LRU-style cache eviction using a composite key that orders by access time, allowing Ascend to find the oldest entries.
Eviction demonstrates LRU-style cache eviction using a composite key that orders by access time, allowing Ascend to find the oldest entries.
leaderboard command
Leaderboard demonstrates using Descend to maintain a live top-K leaderboard.
Leaderboard demonstrates using Descend to maintain a live top-K leaderboard.
mergejoin command
Mergejoin demonstrates using two cursors to perform a merge join between two trees — finding keys that exist in both.
Mergejoin demonstrates using two cursors to perform a merge join between two trees — finding keys that exist in both.
prefixscan command
Prefixscan demonstrates scanning all keys with a common prefix using Range.
Prefixscan demonstrates scanning all keys with a common prefix using Range.
range command
Range demonstrates Range queries, Descend, and Cursor usage.
Range demonstrates Range queries, Descend, and Cursor usage.
ratelimiter command
Ratelimiter demonstrates a sliding-window rate limiter using Increment to count requests per time bucket and DeleteRange to expire old buckets.
Ratelimiter demonstrates a sliding-window rate limiter using Increment to count requests per time bucket and DeleteRange to expire old buckets.
timeseries command
Timeseries demonstrates using Range to query events within a time window.
Timeseries demonstrates using Range to query events within a time window.
upsert command
Upsert demonstrates Upsert, PutIfAbsent, Increment, and CompareAndSwap.
Upsert demonstrates Upsert, PutIfAbsent, Increment, and CompareAndSwap.

Jump to

Keyboard shortcuts

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