queue

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package queue provides task queue implementations for the Vigolium HTTP server. It supports both disk-based (LevelDB) and Redis-based queues with a unified interface.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrQueueClosed is returned when attempting to enqueue to a closed queue.
	ErrQueueClosed = errors.New("queue is closed")

	// ErrQueueFull is returned when the queue capacity is reached (for bounded queues).
	ErrQueueFull = errors.New("queue is full")

	// ErrInvalidTask is returned when a task is missing required fields.
	ErrInvalidTask = errors.New("invalid task: must have either URL or RawRequest")

	// ErrTaskNotFound is returned when a task ID is not found.
	ErrTaskNotFound = errors.New("task not found")

	// ErrSegmentSealed is returned when attempting to write to a sealed segment.
	ErrSegmentSealed = errors.New("segment is sealed")

	// ErrNoActiveSegment is returned when there is no active segment for writing.
	ErrNoActiveSegment = errors.New("no active segment")
)

Functions

This section is empty.

Types

type Config

type Config struct {
	// Type is the queue type: "disk", "redis", or "hybrid".
	Type QueueType

	// Disk queue options
	DiskDir       string
	MaxPerSegment int

	// Hybrid queue options
	// MemBufferSize is the in-memory channel capacity for hybrid mode.
	// Tasks beyond this spill to the disk/redis backend. Default: 10000.
	MemBufferSize int

	// Redis queue options
	RedisAddr         string
	RedisPassword     string
	RedisDB           int
	RedisStreamName   string
	RedisGroupName    string
	RedisConsumer     string
	RedisBlockTimeout time.Duration
}

Config holds configuration for creating a queue.

type DiskQueue

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

DiskQueue implements Queue using LevelDB with segment rotation. Tasks are persisted to disk and survive server restarts.

func NewDiskQueue

func NewDiskQueue(cfg DiskQueueConfig) (*DiskQueue, error)

NewDiskQueue creates a new disk-based queue.

func (*DiskQueue) Ack

func (q *DiskQueue) Ack(taskID string) error

Ack marks a task as completed. It first tries the segment recorded for this task at dequeue (O(1)); if that mapping is missing or stale, it falls back to scanning every segment.

func (*DiskQueue) Close

func (q *DiskQueue) Close() error

Close releases resources.

func (*DiskQueue) Dequeue

func (q *DiskQueue) Dequeue(ctx context.Context) (*ScanTask, error)

Dequeue retrieves the next pending task. Blocks until a task is available or context is cancelled. Uses event-driven wakeup from Enqueue with a fallback ticker for recovery (e.g., pre-existing tasks on disk after restart).

func (*DiskQueue) Enqueue

func (q *DiskQueue) Enqueue(ctx context.Context, task *ScanTask) error

Enqueue adds a task to the queue.

func (*DiskQueue) EnqueueBatch

func (q *DiskQueue) EnqueueBatch(ctx context.Context, tasks []*ScanTask) error

EnqueueBatch enqueues multiple tasks efficiently using batch writes.

func (*DiskQueue) Metrics

func (q *DiskQueue) Metrics() *QueueMetrics

Metrics returns current queue statistics.

func (*DiskQueue) SaveMetadata

func (q *DiskQueue) SaveMetadata() error

SaveMetadata saves queue metadata to disk for recovery.

type DiskQueueConfig

type DiskQueueConfig struct {
	// BaseDir is the root directory for queue storage.
	// Default: os.TempDir()/vigolium-queue
	BaseDir string

	// MaxRecordsPerSegment is the maximum tasks per segment before rotation.
	// Default: 10000
	MaxRecordsPerSegment int
}

DiskQueueConfig holds configuration for DiskQueue.

func DefaultDiskQueueConfig

func DefaultDiskQueueConfig() DiskQueueConfig

DefaultDiskQueueConfig returns sensible defaults.

type HybridQueue

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

HybridQueue combines an in-memory buffered channel with a persistent backend (disk/redis). Enqueue writes to the channel first (zero disk I/O); when the channel is full, tasks spill to the underlying queue. A background goroutine drains spillover back into the channel so consumers always read from one place.

func NewHybridQueue

func NewHybridQueue(cfg HybridQueueConfig) (*HybridQueue, error)

NewHybridQueue creates a hybrid queue wrapping the given backend.

func (*HybridQueue) Ack

func (q *HybridQueue) Ack(taskID string) error

Ack marks a task as completed. Tries the backend first (for spillover tasks), then treats as in-memory no-op.

func (*HybridQueue) Close

func (q *HybridQueue) Close() error

Close releases resources. Stops the drainer, closes the backend, and drains the memory channel.

func (*HybridQueue) Dequeue

func (q *HybridQueue) Dequeue(ctx context.Context) (*ScanTask, error)

Dequeue retrieves the next pending task. Reads from the unified memory channel, which is fed by both direct enqueue and the background drainer (for spillover tasks).

func (*HybridQueue) Enqueue

func (q *HybridQueue) Enqueue(ctx context.Context, task *ScanTask) error

Enqueue adds a task to the queue. Fast path: writes to the in-memory channel with no disk I/O. Slow path: spills to the backend when the channel is full.

func (*HybridQueue) Metrics

func (q *HybridQueue) Metrics() *QueueMetrics

Metrics returns current queue statistics.

type HybridQueueConfig

type HybridQueueConfig struct {
	// MemBufferSize is the capacity of the in-memory channel.
	// Tasks beyond this count spill to the backend queue.
	// Default: 10000
	MemBufferSize int

	// Backend is the persistent queue used for spillover.
	// Typically a *DiskQueue.
	Backend Queue
}

HybridQueueConfig holds configuration for HybridQueue.

type MemoryQueue

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

MemoryQueue is a pure in-memory FIFO queue with no disk or network backend.

It exists so a server that does not need durable task spillover (the common case: any server not running scan-on-receive) can still satisfy the Queue interface for diagnostics/metrics without opening a LevelDB directory, spawning drainer/cleanup goroutines, or taking the on-disk exclusive lock that would otherwise block a second server instance on the same host.

func NewMemoryQueue

func NewMemoryQueue(buffer int) *MemoryQueue

NewMemoryQueue creates an empty in-memory queue. buffer <= 0 uses the default.

func (*MemoryQueue) Ack

func (q *MemoryQueue) Ack(_ string) error

Ack marks a task completed. In-memory delivery is at-most-once, so this only updates metrics.

func (*MemoryQueue) Close

func (q *MemoryQueue) Close() error

Close prevents new Enqueue calls; Dequeue drains remaining items then returns io.EOF. Safe to call multiple times.

func (*MemoryQueue) Dequeue

func (q *MemoryQueue) Dequeue(ctx context.Context) (*ScanTask, error)

Dequeue returns the next task, blocking until one is available, the context is cancelled, or the queue is closed and drained (io.EOF).

func (*MemoryQueue) Enqueue

func (q *MemoryQueue) Enqueue(ctx context.Context, task *ScanTask) error

Enqueue adds a task, blocking while the buffer is full until space frees up, the context is cancelled, or the queue is closed.

func (*MemoryQueue) Metrics

func (q *MemoryQueue) Metrics() *QueueMetrics

Metrics reports the in-memory queue counters. Disk fields stay zero.

type Queue

type Queue interface {
	// Enqueue adds a task to the queue.
	// Returns ctx.Err() if context is cancelled while waiting.
	Enqueue(ctx context.Context, task *ScanTask) error

	// Dequeue retrieves the next pending task from the queue.
	// Blocks until a task is available or context is cancelled.
	// Returns (nil, io.EOF) when queue is closed and drained.
	// Returns (nil, ctx.Err()) if context is cancelled.
	Dequeue(ctx context.Context) (*ScanTask, error)

	// Ack marks a task as completed.
	// For disk-based queue, this triggers segment cleanup check.
	// For Redis queue, this is typically a no-op (ack on dequeue).
	Ack(taskID string) error

	// Close releases resources and prevents new Enqueue calls.
	// Dequeue continues until queue is drained, then returns io.EOF.
	Close() error

	// Metrics returns current queue statistics.
	// May return nil if metrics are not available.
	Metrics() *QueueMetrics
}

Queue is the abstraction for task queue operations. Implementations must be safe for concurrent access from multiple goroutines.

func NewQueue

func NewQueue(cfg Config) (Queue, error)

NewQueue creates a queue based on the configuration.

type QueueInputSource

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

QueueInputSource adapts Queue to the InputSource interface. This allows the existing Executor to consume tasks from queue without modification.

func NewQueueInputSource

func NewQueueInputSource(queue Queue) *QueueInputSource

NewQueueInputSource creates a new QueueInputSource.

func (*QueueInputSource) Close

func (q *QueueInputSource) Close() error

Close closes the source.

func (*QueueInputSource) Next

Next returns the next WorkItem from the queue with ack callback. Implements source.InputSource interface.

func (*QueueInputSource) Queue

func (q *QueueInputSource) Queue() Queue

Queue returns the underlying queue.

type QueueMetrics

type QueueMetrics struct {
	// Depth is the current number of pending tasks.
	// May be -1 if not available (e.g., for Redis without XPENDING call).
	Depth int64

	// TotalEnqueued is the total number of tasks added since start.
	TotalEnqueued int64

	// TotalDequeued is the total number of tasks retrieved since start.
	TotalDequeued int64

	// TotalCompleted is the total number of tasks acknowledged since start.
	TotalCompleted int64

	// EnqueueErrors is the number of failed enqueue operations.
	EnqueueErrors int64

	// DequeueErrors is the number of failed dequeue operations.
	DequeueErrors int64

	// SegmentCount is the number of active disk segments.
	// Only set for disk-based and hybrid queues.
	SegmentCount int

	// DiskUsageBytes is the approximate total disk usage across all segments.
	// Only set for disk-based and hybrid queues; 0 for Redis or in-memory.
	DiskUsageBytes int64
}

QueueMetrics holds statistics about queue operations.

type QueueType

type QueueType string

QueueType represents the type of queue implementation.

const (
	// QueueTypeDisk is a disk-based queue using LevelDB with segment rotation.
	QueueTypeDisk QueueType = "disk"

	// QueueTypeRedis is a Redis-based queue using Redis Streams.
	QueueTypeRedis QueueType = "redis"

	// QueueTypeHybrid is an in-memory buffered queue that spills over to a disk/redis backend.
	// Fast path avoids disk I/O on enqueue; a background goroutine drains spillover back to memory.
	QueueTypeHybrid QueueType = "hybrid"

	// QueueTypeMemory is a pure in-memory queue with no disk/network backend and no
	// background goroutines — used where durable spillover isn't needed so startup
	// avoids a LevelDB directory and its exclusive on-disk lock.
	QueueTypeMemory QueueType = "memory"
)

type RedisQueue

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

RedisQueue implements Queue using Redis Streams.

func NewRedisQueue

func NewRedisQueue(cfg RedisQueueConfig) (*RedisQueue, error)

NewRedisQueue creates a new Redis-based queue.

func (*RedisQueue) Ack

func (q *RedisQueue) Ack(taskID string) error

Ack marks a task as completed and XACKs its Redis Stream message so it leaves the consumer group's pending-entries list. The message ID is resolved from the taskID→messageID map populated at Dequeue.

func (*RedisQueue) AckWithMessageID

func (q *RedisQueue) AckWithMessageID(ctx context.Context, messageID string) error

AckWithMessageID acknowledges a specific message by its Redis Stream ID.

func (*RedisQueue) ClaimPendingTasks

func (q *RedisQueue) ClaimPendingTasks(ctx context.Context, minIdleTime time.Duration) ([]*ScanTask, error)

ClaimPendingTasks claims and returns tasks that have been pending for too long. This is useful for handling crashed workers.

func (*RedisQueue) Close

func (q *RedisQueue) Close() error

Close closes the Redis connection.

func (*RedisQueue) Dequeue

func (q *RedisQueue) Dequeue(ctx context.Context) (*ScanTask, error)

Dequeue retrieves the next pending task from the Redis Stream.

func (*RedisQueue) Enqueue

func (q *RedisQueue) Enqueue(ctx context.Context, task *ScanTask) error

Enqueue adds a task to the Redis Stream.

func (*RedisQueue) Metrics

func (q *RedisQueue) Metrics() *QueueMetrics

Metrics returns current queue statistics.

type RedisQueueConfig

type RedisQueueConfig struct {
	// Addr is the Redis server address (e.g., "localhost:6379").
	Addr string

	// Password is the Redis password (empty for no auth).
	Password string

	// DB is the Redis database number.
	DB int

	// StreamName is the Redis Stream name for tasks.
	// Default: "vigolium:tasks"
	StreamName string

	// ConsumerGroup is the consumer group name.
	// Default: "vigolium-workers"
	ConsumerGroup string

	// ConsumerName is the unique name for this consumer.
	// Default: auto-generated UUID
	ConsumerName string

	// BlockTimeout is how long to wait for new messages.
	// Default: 5s
	BlockTimeout time.Duration

	// MaxRetries is the number of times to retry failed tasks.
	// Default: 3
	MaxRetries int
}

RedisQueueConfig holds configuration for the Redis-based queue.

type ScanTask

type ScanTask struct {
	// ID is the unique identifier for this task.
	ID string `json:"id"`

	// URL is the target URL to scan.
	// Required if RawRequest is empty.
	URL string `json:"url,omitempty"`

	// RawRequest is the raw HTTP request string.
	// Optional - if empty, a GET request will be generated from URL.
	RawRequest string `json:"raw_request,omitempty"`

	// EnableModules is the list of module IDs to run.
	// Empty means use all default modules.
	EnableModules []string `json:"enable_modules,omitempty"`

	// WebhookURL is the URL to send results to.
	// Optional - if empty, results go to default output only.
	WebhookURL string `json:"webhook_url,omitempty"`

	// Metadata contains arbitrary key-value pairs for tracking.
	Metadata map[string]string `json:"metadata,omitempty"`

	// Status is the current state of the task.
	Status TaskStatus `json:"status"`

	// CreatedAt is when the task was enqueued.
	CreatedAt time.Time `json:"created_at"`

	// UpdatedAt is when the task was last updated.
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

ScanTask represents a scan request in the queue.

func NewScanTask

func NewScanTask(url, rawRequest string, enableModules []string) *ScanTask

NewScanTask creates a new ScanTask with a generated UUID and pending status.

func NewScanTaskFromRequest

func NewScanTaskFromRequest(url, rawRequest, webhookURL string, enableModules []string, metadata map[string]string) *ScanTask

NewScanTaskFromRequest creates a ScanTask from an API request.

func (*ScanTask) Clone

func (t *ScanTask) Clone() *ScanTask

Clone creates a deep copy of the task.

func (*ScanTask) IsValid

func (t *ScanTask) IsValid() bool

IsValid checks if the task has required fields.

func (*ScanTask) MarkCompleted

func (t *ScanTask) MarkCompleted()

MarkCompleted updates the task status to completed.

func (*ScanTask) MarkProcessing

func (t *ScanTask) MarkProcessing()

MarkProcessing updates the task status to processing.

func (*ScanTask) MarshalJSON

func (t *ScanTask) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (*ScanTask) UnmarshalJSON

func (t *ScanTask) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

type Segment

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

func NewSegment

func NewSegment(cfg SegmentConfig) (*Segment, error)

NewSegment creates a new segment with a LevelDB database.

func OpenSegment

func OpenSegment(segPath string, id uint64) (*Segment, error)

OpenSegment opens an existing segment from disk and recovers state.

func (*Segment) AckTask

func (s *Segment) AckTask(taskID string) error

AckTask marks a task as completed by deleting its record.

The previous implementation rewrote the task with status=completed, leaving the (potentially large, RawRequest-bearing) record on disk until the entire 10k-task segment became deletable — so a segment with one stuck task pinned thousands of completed records. A completed record is never read again (GetNextPending consults only the pending index, cleared at dequeue; the Iterator method is unused), so we delete it outright and let LevelDB reclaim the space incrementally. recoverState rebuilds totalTasks from the surviving records, so deletion stays consistent with CanDelete (completed >= total) and PendingTasks (total - completed) across restarts.

func (*Segment) CachedDiskSize

func (s *Segment) CachedDiskSize() int64

CachedDiskSize returns the approximate disk usage without filesystem access.

func (*Segment) CanDelete

func (s *Segment) CanDelete() bool

CanDelete returns true if all tasks are completed and segment can be deleted.

func (*Segment) Close

func (s *Segment) Close() error

Close closes the segment database without deleting files.

func (*Segment) CompletedTasks

func (s *Segment) CompletedTasks() int64

CompletedTasks returns the number of completed tasks.

func (*Segment) Delete

func (s *Segment) Delete() error

Delete closes and removes the segment from disk.

func (*Segment) DiskSize

func (s *Segment) DiskSize() int64

DiskSize returns the approximate disk usage of this segment in bytes. Walks the segment directory and sums file sizes.

func (*Segment) GetNextPending

func (s *Segment) GetNextPending() (*ScanTask, error)

GetNextPending retrieves and marks the next pending task as processing. Returns nil if no pending tasks exist.

func (*Segment) ID

func (s *Segment) ID() uint64

ID returns the segment ID.

func (*Segment) IsClosed

func (s *Segment) IsClosed() bool

IsClosed returns true if the segment database has been closed.

func (*Segment) IsSealed

func (s *Segment) IsSealed() bool

IsSealed returns true if the segment is sealed for writes.

func (*Segment) Iterator

func (s *Segment) Iterator() iterator.Iterator

Iterator returns a LevelDB iterator for task keys.

func (*Segment) Path

func (s *Segment) Path() string

Path returns the segment directory path.

func (*Segment) PendingTasks

func (s *Segment) PendingTasks() int64

PendingTasks returns the number of pending tasks.

func (*Segment) Seal

func (s *Segment) Seal()

Seal marks the segment as read-only and persists that state so it survives a restart (see keySealed).

func (*Segment) TotalTasks

func (s *Segment) TotalTasks() int64

TotalTasks returns the total number of tasks in this segment.

func (*Segment) WriteTask

func (s *Segment) WriteTask(task *ScanTask) error

WriteTask writes a task to the segment.

func (*Segment) WriteTasks

func (s *Segment) WriteTasks(tasks []*ScanTask) error

WriteTasks writes multiple tasks in a single LevelDB batch, amortizing the WAL flush across all tasks. This is significantly faster than individual WriteTask calls for bulk enqueue operations.

type SegmentConfig

type SegmentConfig struct {
	// BaseDir is the root directory for all segments.
	BaseDir string

	// ID is the unique segment identifier.
	ID uint64
}

SegmentConfig holds configuration for segment creation.

type TaskStatus

type TaskStatus string

TaskStatus represents the current state of a scan task.

const (
	// TaskStatusPending indicates the task is waiting to be processed.
	TaskStatusPending TaskStatus = "pending"

	// TaskStatusProcessing indicates the task is currently being processed.
	TaskStatusProcessing TaskStatus = "processing"

	// TaskStatusCompleted indicates the task has been completed.
	TaskStatusCompleted TaskStatus = "completed"
)

Jump to

Keyboard shortcuts

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