dque

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT, MIT Imports: 15 Imported by: 0

README

dque - a fast embedded durable queue for Go

GoDoc

dque is:

  • persistent -- survives program restarts
  • scalable -- not limited by your RAM, but by your disk space
  • FIFO -- First In First Out
  • embedded -- compiled into your Golang program
  • synchronized -- safe for concurrent usage
  • fast or safe, you choose -- turbo mode lets the OS decide when to write to disk
  • has a liberal license -- allows any use, commercial or personal

I love tools that do one thing well. Hopefully this fits that category.

I am indebted to Gabor Cselle who, years ago, inspired me with an example of an in-memory persistent queue written in Java. I was intrigued by the simplicity of his approach, which became the foundation of the "segment" part of this queue which holds the head and the tail of the queue in memory as well as storing the segment files in between.

performance

There are two performance modes: safe and turbo

safe mode
  • safe mode is the default
  • forces an fsync to disk every time you enqueue or dequeue an item.
  • while this is the safest way to use dque with little risk of data loss, it is also the slowest.
turbo mode
  • can be enabled/disabled with a call to DQue.TurboOn() or DQue.TurboOff()
  • lets the OS batch up your changes to disk, which makes it a lot faster.
  • also allows you to flush changes to disk at opportune times. See DQue.TurboSync()
  • comes with a risk that a power failure could lose changes. By turning on Turbo mode you accept that risk.
  • run the benchmark to see the difference on your hardware.
  • there is a todo item to force flush changes to disk after a configurable amount of time to limit risk.
implementation
  • The queue is held in segments of a configurable size.
  • The queue is protected against re-opening from other processes.
  • Each in-memory segment corresponds with a file on disk. Think of the segment files as a bit like rolling log files. The oldest segment files are eventually deleted, not based on time, but whenever their items have all been dequeued.
  • Segment files are only appended to until they fill up. At which point a new segment is created. They are never modified (other than being appended to and deleted when each of their items has been dequeued).
  • If there is more than one segment, new items are enqueued to the last segment while dequeued items are taken from the first segment.
  • Because the encoding/gob package is used to store the struct to disk:
    • Only structs can be stored in the queue.
    • Only one type of struct can be stored in each queue.
    • Only public fields in a struct will be stored.
    • A function is required that returns a pointer to a new struct of the type stored in the queue. This function is used when loading segments into memory from disk. As of v2.1, this is no longer necessary — the queue uses Go generics and new(T) internally.
  • Queue segment implementation:
    • For nice visuals, see Gabor Cselle's documentation here. Note that Gabor's implementation kept the entire queue in memory as well as disk. dque keeps only the head and tail segments in memory.
    • Enqueueing an item adds it both to the end of the last segment file and to the in-memory item slice for that segment.
    • When a segment reaches its maximum size a new segment is created.
    • Dequeueing an item removes it from the beginning of the in-memory slice and appends a 4-byte "delete" marker to the end of the segment file. This allows the item to be left in the file until the number of delete markers matches the number of items, at which point the entire file is deleted.
    • When a segment is reconstituted from disk, each "delete" marker found in the file causes a removal of the first element of the in-memory slice.
    • When each item in the segment has been dequeued, the segment file is deleted and the next segment is loaded into memory.
example

See the full example code here

Or a shortened version here:

package dque_test

import (
    "log"

    "github.com/lbe/sfpg-go"
)

// Item is what we'll be storing in the queue.  It can be any struct
// as long as the fields you want stored are public.
type Item struct {
    Name string
    Id   int
}

func main() {
    ExampleDQue_main()
}

// ExampleQueue_main() show how the queue works
func ExampleDQue_main() {
    qName := "item-queue"
    qDir := "/tmp"
    segmentSize := 50

    // Create a new queue with segment size of 50
    q, err := dque.New[*Item](qName, qDir, segmentSize)
    ...

    // Add an item to the queue
    err := q.Enqueue(&Item{"Joe", 1})
    ...

    // Properly close a queue
    q.Close()

    // You can reconsitute the queue from disk at any time
    q, err = dque.Open[*Item](qName, qDir, segmentSize)
    ...

    // Peek at the next item in the queue
    item, err := q.Peek()
    if err != nil {
        if err != dque.ErrEmpty {
            log.Fatal("Error peeking at item ", err)
        }
    }

    // Dequeue the next item in the queue
    item, err = q.Dequeue()
    if err != nil {
        if err != dque.ErrEmpty {
            log.Fatal("Error dequeuing item ", err)
        }
    }

    // Dequeue the next item in the queue and block until one is available
    if item, err = q.DequeueBlock(); err != nil {
        log.Fatal("Error dequeuing item ", err)
    }

    doSomething(item)
}

func doSomething(item *Item) {
    log.Println("Dequeued", item)
}
v2 migration notes
  • Go 1.26.0 is now required.
  • The public Config type is now unexported (config).
  • DQUE_EMPTY has been replaced by the sentinel error ErrEmpty.
  • The internal error helpers now capture stack traces. Wrapped errors include the stack trace in Error(), so avoid exact-string comparisons and use errors.Is / errors.As instead.
  • The external dependencies github.com/pkg/errors and github.com/gofrs/flock have been removed and replaced with internal packages.
v2.1 migration notes
  • Generics: DQue is now DQue[T any]. Constructors New, Open, and NewOrOpen accept a type parameter instead of a builder function. Remove the builder func() interface{} argument from all constructor calls.
  • Type-safe API: Enqueue accepts T directly. Dequeue, Peek, DequeueBlock, and PeekBlock return (T, error) instead of (interface{}, error). Remove all manual type assertions on return values.
contributors
todo? (feel free to submit pull requests)
  • add option to enable turbo with a timeout that would ensure you would never lose more than n seconds of changes.
  • add Lock() and Unlock() methods so you can peek at the first item and then conditionally dequeue it without worrying that another goroutine has grabbed it out from under you. The use case is when you don't want to actually remove it from the queue until you know you were able to successfully handle it.
  • store the segment size in a config file inside the queue. Then it only needs to be specified on dque.New(...)
alternative tools
  • CurlyQ is a bit heavier (requires Redis) but has more background processing features.

Documentation

Overview

Package dque is a fast, embedded, type-safe durable queue for Go. DQue[T any] provides a generic FIFO queue backed by disk segments, eliminating the need for manual type assertions on Dequeue/Peek return values.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (

	// ErrEmpty is returned when attempting to dequeue from an empty queue.
	ErrEmpty = errors.New("dque is empty")
)
View Source
var ErrQueueClosed = errors.New("queue is closed")

ErrQueueClosed is the error returned when a queue is closed.

Functions

This section is empty.

Types

type DQue

type DQue[T any] struct {
	Name    string
	DirPath string
	// contains filtered or unexported fields
}

DQue is the in-memory representation of a type-safe queue on disk. You must never have two *active* DQue instances pointing at the same path on disk. It is acceptable to reconstitute a new instance from disk, but make sure the old instance is never enqueued to (or dequeued from) again.

Example

ExampleDQue shows how the queue works

package main

//
// Example usage
// Run with: go test -v example_test.go
//

import (
	"errors"
	"fmt"
	"log"
	"os"

	"github.com/lbe/sfpg-go/internal/dque"
)

// Item is what we'll be storing in the queue.  It can be any struct
// as long as the fields you want stored are public.
type Item struct {
	Name string
	Id   int
}

// ExampleDQue shows how the queue works
func main() {
	qName := "item-queue"
	qDir := os.TempDir()
	segmentSize := 50

	// Create a new queue with segment size of 50
	q, err := dque.NewOrOpen[Item](qName, qDir, segmentSize)
	if err != nil {
		log.Fatal("Error creating new dque ", err)
	}

	// Add an item to the queue
	if err = q.Enqueue(&Item{"Joe", 1}); err != nil {
		log.Fatal("Error enqueueing item ", err)
	}
	log.Println("Size should be 1:", q.Size())

	// Properly close a queue
	if err = q.Close(); err != nil {
		log.Fatal("Error closing dque ", err)
	}

	// You can reconsitute the queue from disk at any time
	q, err = dque.Open[Item](qName, qDir, segmentSize)
	if err != nil {
		log.Fatal("Error opening existing dque ", err)
	}

	// Peek at the next item in the queue
	item, err := q.Peek()
	if err != nil {
		if !errors.Is(err, dque.ErrEmpty) {
			log.Fatal("Error peeking at item", err)
		}
	}
	log.Println("Peeked at:", item)

	// Dequeue the next item in the queue
	item, err = q.Dequeue()
	if err != nil && !errors.Is(err, dque.ErrEmpty) {
		log.Fatal("Error dequeuing item:", err)
	}
	log.Println("Dequeued an item:", item)
	log.Println("Size should be zero:", q.Size())

	go func() {
		if enqErr := q.Enqueue(&Item{"Joe", 1}); enqErr != nil {
			log.Fatal("Error enqueueing item", enqErr)
		}
	}()

	// Dequeue the next item in the queue and block until one is available
	item, err = q.DequeueBlock()
	if err != nil {
		log.Fatal("Error dequeuing item ", err)
	}

	doSomething(item)
}

func doSomething(item *Item) {
	fmt.Println("Dequeued:", item)
}
Output:
Dequeued: &{Joe 1}

func New

func New[T any](name string, dirPath string, itemsPerSegment int) (*DQue[T], error)

New creates a new durable queue for items of type T.

func NewOrOpen

func NewOrOpen[T any](name string, dirPath string, itemsPerSegment int) (*DQue[T], error)

NewOrOpen either creates a new queue for items of type T, or opens an existing durable queue.

func Open

func Open[T any](name string, dirPath string, itemsPerSegment int) (*DQue[T], error)

Open opens an existing durable queue for items of type T.

func (*DQue[T]) Close

func (q *DQue[T]) Close() error

Close releases the lock on the queue rendering it unusable for further usage by this instance. Close will return an error if it has already been called.

func (*DQue[T]) Dequeue

func (q *DQue[T]) Dequeue() (*T, error)

Dequeue removes and returns the first item in the queue. When the queue is empty, nil and dque.ErrEmpty are returned.

On error, the returned object may still be non-nil and valid — it was successfully dequeued but subsequent cleanup (segment deletion or creation) failed. Callers should process the returned object even when err != nil.

func (*DQue[T]) DequeueBlock

func (q *DQue[T]) DequeueBlock() (*T, error)

DequeueBlock behaves similar to Dequeue, but is a blocking call until an item is available.

func (*DQue[T]) DiskBytes added in v0.9.0

func (q *DQue[T]) DiskBytes() int64

DiskBytes returns an estimate of disk usage for the queue in bytes by summing the sizes of all segment files in the queue directory. Returns 0 when the queue is closed or on any filesystem error.

func (*DQue[T]) Enqueue

func (q *DQue[T]) Enqueue(obj *T) error

Enqueue adds an item to the end of the queue

func (*DQue[T]) Peek

func (q *DQue[T]) Peek() (*T, error)

Peek returns the first item in the queue without dequeueing it. When the queue is empty, nil and dque.ErrEmpty are returned. Do not use this method with multiple dequeueing threads or you may regret it.

func (*DQue[T]) PeekBlock

func (q *DQue[T]) PeekBlock() (*T, error)

PeekBlock behaves similar to Peek, but is a blocking call until an item is available.

func (*DQue[T]) SegmentNumbers

func (q *DQue[T]) SegmentNumbers() (int, int)

SegmentNumbers returns the number of both the first and last segment. There is likely no use for this information other than testing.

func (*DQue[T]) Size

func (q *DQue[T]) Size() int

Size locks things up while calculating so you are guaranteed an accurate size... unless you have changed the itemsPerSegment value since the queue was last empty. Then it could be wildly inaccurate.

func (*DQue[T]) SizeUnsafe

func (q *DQue[T]) SizeUnsafe() int

SizeUnsafe returns the approximate number of items in the queue. Use Size() if having the exact size is important to your use-case.

The return value could be wildly inaccurate if the itemsPerSegment value has changed since the queue was last empty. Also, because the value is taken under lock, the size may change after returning from this method.

func (*DQue[T]) Turbo

func (q *DQue[T]) Turbo() bool

Turbo returns true if the turbo flag is on. Having turbo on speeds things up significantly.

func (*DQue[T]) TurboOff

func (q *DQue[T]) TurboOff() error

TurboOff re-enables the "safety" mode that syncs every file change to disk as they happen. If turbo is already off an error is returned

func (*DQue[T]) TurboOn

func (q *DQue[T]) TurboOn() error

TurboOn allows the filesystem to decide when to sync file changes to disk. Throughput is greatly increased by turning turbo on, however there is some risk of losing data if a power-loss occurs. If turbo is already on an error is returned

func (*DQue[T]) TurboSync

func (q *DQue[T]) TurboSync() error

TurboSync allows you to fsync changes to disk, but only if turbo is on. If turbo is off an error is returned

type ErrCorruptedSegment

type ErrCorruptedSegment struct {
	Path string
	Err  error
}

ErrCorruptedSegment is returned when a segment file cannot be opened due to inconsistent formatting. Recovery may be possible by clearing or deleting the file, then reloading using dque.New().

func (ErrCorruptedSegment) Error

func (e ErrCorruptedSegment) Error() string

Error returns a string describing ErrCorruptedSegment

func (ErrCorruptedSegment) Unwrap

func (e ErrCorruptedSegment) Unwrap() error

Unwrap returns the wrapped error

type ErrUnableToDecode

type ErrUnableToDecode struct {
	Path string
	Err  error
}

ErrUnableToDecode is returned when an object cannot be decoded.

func (ErrUnableToDecode) Error

func (e ErrUnableToDecode) Error() string

Error returns a string describing ErrUnableToDecode error

func (ErrUnableToDecode) Unwrap

func (e ErrUnableToDecode) Unwrap() error

Unwrap returns the wrapped error

Jump to

Keyboard shortcuts

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