buckets

module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: Apache-2.0

README

buckets

A Go library providing generic, in-memory bucketed storage for resources.

Resources are distributed across a fixed number of buckets, each guarded by its own mutex. This reduces lock contention compared to a single global lock while keeping all state in-process.

This library originated from the internal bucketing used by maxcap.

Installation

go get codeberg.org/jrh3k5/buckets/pkg

Usage

The bucket.Buckets interface defines the contract for storing and retrieving resources by key. The interface is generic over both the bucket key type and the bucketed resource type.

Keys

Keys implement the bucket.ListKey interface, which maps a resource to one of the fixed buckets via a 0-based BucketIndex() and identifies the resource within that bucket via ResourceID().

The provided StringListKey hashes a string resource ID (using MD5, where collisions are tolerated for bucketing purposes) into a consistent bucket index for a given bucket count:

key, err := bucket.NewStringListKey("tenant-acme/user-abc", 256)
if err != nil {
    log.Fatal(err)
}

Custom key types are supported by implementing ListKey; this is useful if you already have a natural way to partition your resources.

Single-Value Buckets

BucketsList stores at most one resource per resource ID:

package main

import (
	"context"
	"log"

	bucket "codeberg.org/jrh3k5/buckets/pkg"
)

type Connection struct {
	// ...
}

func (c *Connection) Close() error {
	// ...
	return nil
}

func main() {
	ctx := context.Background()

	bucketsList, err := bucket.NewBucketsList[*bucket.StringListKey, string, *Connection](256)
	if err != nil {
		log.Fatal(err)
	}

	key, err := bucket.NewStringListKey("conn-abc123", 256)
	if err != nil {
		log.Fatal(err)
	}

	// Store a resource; any existing resource for this ID is overwritten.
	err = bucketsList.Add(ctx, key, &Connection{})
	if err != nil {
		log.Fatal(err)
	}

	// Mutate a stored resource in place. The returned boolean reports whether
	// a resource exists for the given key.
	found, err := bucketsList.Apply(ctx, key, func(_ context.Context, conn *Connection) error {
		return conn.Close()
	})
	if err != nil {
		log.Fatal(err)
	}
	_ = found

	// Remove and retrieve a resource.
	conn, found, err := bucketsList.Pop(ctx, key)
	if err != nil {
		log.Fatal(err)
	}
	_ = conn
	_ = found
}

AddIfNotExists provides get-or-create semantics. The creator function is only invoked when no resource exists for the given key, and the pre-existing resource is returned otherwise:

resource, err := bucketsList.AddIfNotExists(ctx, key, func(_ context.Context, _ *bucket.StringListKey) (*Connection, error) {
	return dialConnection(ctx)
})

EvictOlderThan evicts every resource that was added before the given time and returns them, which is useful for reaping abandoned resources:

evicted, err := bucketsList.EvictOlderThan(ctx, time.Now().Add(-5*time.Minute))
if err != nil {
    log.Fatal(err)
}

for _, conn := range evicted {
    _ = conn.Close()
}
Multi-Value Buckets

MultiValueBucketsList extends BucketsList for buckets that hold multiple values, such as all of a client's open sessions. Each bucket must implement the MultiValueBucketResource interface; MultiValueResource is a ready-made implementation that tracks values by ID and preserves insertion order (satisfying OrderedMultiValueBucketResource).

A common pattern is to get-or-create the bucket for a client, add values to it, and then cap the number of retained values:

package main

import (
	"context"
	"log"

	"codeberg.org/jrh3k5/buckets/pkg"
)

func main() {
	ctx := context.Background()

	const maximumSessionsPerClient = 10

	sessions, err := bucket.NewMultiValueBucketsList[
		*bucket.StringListKey,
		string,
		bucket.OrderedMultiValueBucketResource[string, string],
		string,
		string,
	](256)
	if err != nil {
		log.Fatal(err)
	}

	key, err := bucket.NewStringListKey("tenant-acme/user-abc", 256)
	if err != nil {
		log.Fatal(err)
	}

	// Get or create the client's bucket.
	clientSessions, err := sessions.AddIfNotExists(ctx, key, func(_ context.Context, _ *bucket.StringListKey) (bucket.OrderedMultiValueBucketResource[string, string], error) {
		return bucket.NewMultiValueResource[string, string](), nil
	})
	if err != nil {
		log.Fatal(err)
	}

	err = clientSessions.Add(ctx, "session-1", "session-one-value")
	if err != nil {
		log.Fatal(err)
	}

	// Enforce a cap of N most-recent values; older values are returned for cleanup.
	evicted, err := clientSessions.RetainLast(ctx, maximumSessionsPerClient)
	if err != nil {
		log.Fatal(err)
	}
	_ = evicted

	// Pop the first N values in insertion order.
	first, err := clientSessions.PopFirst(ctx, 2)
	if err != nil {
		log.Fatal(err)
	}
	_ = first
}

At the collection level, PopValues pops specific values out of a client's bucket without removing the bucket itself, and EvictValuesOlderThan sweeps all buckets, evicting sufficiently-old values and cleaning up any buckets left empty:

popped, err := sessions.PopValues(ctx, key, []string{"session-1", "session-2"})
if err != nil {
    log.Fatal(err)
}
_ = popped

evicted, err := sessions.EvictValuesOlderThan(ctx, time.Now().Add(-30*time.Minute))
if err != nil {
    log.Fatal(err)
}
_ = evicted

Thread Safety

All implementations are safe for concurrent use. Each bucket is guarded by its own mutex, so operations against different buckets do not contend with each other. Note that eviction times are captured with time.Now() when a resource or value is added.

Logging

This library uses the standard library's log/slog package for structured logging. Configure the default slog logger to control log output and verbosity.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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