How VarMQ Handles Job Queuing and Distribution Balancing: A Deep Dive into the Source Code

VarMQ decouples queue selection strategies (round-robin, priority, length-based) from a dynamic goroutine worker pool, using a pluggable queueManager to route jobs and auto-scaling workers to balance CPU utilization across multiple queues.

The goptics/varmq repository implements a flexible job queue system where job storage and job execution are isolated concerns. Understanding how VarMQ handles job queuing and distribution balancing requires examining the interplay between the strategy-driven queue selection logic and the elastic worker pool architecture.

Queue Selection Strategies: The queueManager Core

VarMQ’s distribution logic lives in queue_manager.go, where a configurable strategy determines which queue supplies the next job to workers.

Strategy Types and Configuration

The Strategy enum defines four selection algorithms:

type Strategy uint8

const (
    RoundRobin Strategy = iota // select queues in a round‑robin fashion
    MaxLen                     // select the queue with the most items
    MinLen                     // select the queue with the fewest items
    Priority                   // select by priority (high priority first)
)

The queueManager struct embeds a generic helpers.Manager[IBaseQueue] and stores the active strategy:

type queueManager struct {
    *helpers.Manager[IBaseQueue]
    strategy Strategy
}

How the next() Method Dispatches Jobs

The next() method in queue_manager.go implements the dispatch logic by delegating to helper methods based on the configured strategy:

func (qm *queueManager) next() (IBaseQueue, error) {
    switch qm.strategy {
    case Priority:   return qm.GetPriorityItem()
    case RoundRobin: return qm.GetRoundRobinItem()
    case MaxLen:     return qm.GetMaxLenItem()
    case MinLen:     return qm.GetMinLenItem()
    default:         return nil, errInvalidStrategyType
    }
}

Each helper method (GetPriorityItem, GetRoundRobinItem, etc.) resides in helpers/manager.go and traverses the internal slice of registered queues to find the optimal match. This abstraction allows workers to consume from multiple queues while the manager balances load according to queue length, priority weight, or fair rotation.

Internal Queue Architecture

VarMQ separates the external queue interface from internal storage implementations, enabling both in-memory and distributed backends.

Generic Queue Wrapper

The queue[T] struct in queue.go composes an externalBaseQueue (tracking pending items and lifecycle methods) with an IQueue implementation:

type queue[T any] struct {
    *externalBaseQueue
    internalQueue IQueue
}

When jobs arrive via Add or AddAll, they enqueue into the internalQueue. The worker dequeues through the manager’s next() selection, ensuring the strategy applies uniformly regardless of the underlying storage.

FIFO and Priority Implementations

Internal queue logic resides under internal/queues/:

These implementations allow binding multiple queue types (FIFO, priority, or distributed) to a single worker, with the manager handling distribution balancing across the heterogeneous set.

Dynamic Worker Pool and Load Balancing

VarMQ achieves horizontal scaling within a single process through an elastic goroutine pool defined in worker.go and internal/pool/pool.go.

The Event Loop and Job Pulling

The goEventLoop method listens on a non-blocking signal channel. When idle workers signal availability via notifyToPullNextJobs(), the loop executes:

func (w *worker[T, JobType]) goEventLoop() {
    go func(signal <-chan struct{}) {
        for range signal {
            for w.IsRunning() && w.curProcessing.Load() < w.concurrency.Load() && w.queues.Len() > 0 {
                if err := w.processNextJob(); err != nil {
                    w.sendError(err)
                }
            }
        }
    }(w.eventLoopSignal)
}

The processNextJob method requests the next queue from the queueManager, dequeues the job (handling []byte unmarshaling when necessary), and dispatches it via sendToNextChannel to an available pool node.

Auto-Scaling and Idle Worker Management

The worker pool implements several balancing mechanisms:

  • Dynamic Pool Creation – When w.pool (a lock-free structure) exhausts available nodes, initPoolNode spawns new goroutines to serve jobs
  • Idle Worker Removal – If idleWorkerExpiryDuration is configured, goRemoveIdleWorkers prunes nodes exceeding the timeout while respecting numMinIdleWorkers() (calculated from minIdleWorkerRatio)
  • Pool TuningTunePool(concurrency int) adjusts target concurrency; manual trimming occurs when shrinking the pool if idle expiry is disabled, otherwise the background remover handles contraction

These features ensure distribution balancing occurs at two levels: the queue manager balances across job sources, while the worker pool balances CPU utilization by matching goroutine count to workload.

Distributed Queue Support

For external brokers, distributed.go provides a DistributedQueue[T] wrapper implementing IDistributedQueue:

type distributedQueue[T any] struct {
    IDistributedQueue
}

Jobs serialize to JSON via Enqueue, while the worker pool and queue manager logic remain unchanged—only the internal queue implementation differs, communicating with Redis, SQLite, or other backends. Example implementations appear in examples/redis-distributed/consumer/main.go, demonstrating how the same balancing strategies apply to distributed deployments.

Summary

  • Strategy-driven selection – The queueManager in queue_manager.go uses configurable strategies (RoundRobin, MaxLen, MinLen, Priority) to determine which queue feeds the next job
  • Pluggable storage – Generic queue[T] wrappers in queue.go allow mixing FIFO, priority, and distributed queues (distributed.go) under one worker
  • Elastic worker poolworker.go implements auto-scaling goroutine pools with idle worker expiration and manual tuning via TunePool()
  • Dual-layer balancing – VarMQ balances load across queues (via manager strategies) and across CPU resources (via dynamic pool sizing)

Frequently Asked Questions

What queue selection strategies does VarMQ support?

VarMQ supports four strategies defined in queue_manager.go: RoundRobin (fair rotation), MaxLen (longest queue first), MinLen (shortest queue first), and Priority (high-priority queues first). The next() method dispatches to the appropriate selection helper based on the configured Strategy constant.

How does VarMQ automatically balance worker concurrency?

According to the source code in worker.go, VarMQ uses a lock-free pool (internal/pool/pool.go) that creates new worker goroutines when demand exceeds available nodes. The goRemoveIdleWorkers goroutine prunes excess workers based on idleWorkerExpiryDuration while maintaining minIdleWorkerRatio. Users can also manually rebalance via TunePool(concurrency).

Can VarMQ distribute jobs across multiple queue types simultaneously?

Yes. The queueManager accepts any IBaseQueue implementation, allowing workers to bind both in-memory FIFO queues (internal/queues/queue.go) and priority queues (internal/queues/priority.go) simultaneously. The strategy determines consumption order across this heterogeneous set, enabling complex routing patterns like priority escalation with fallback FIFO processing.

How does VarMQ handle external message brokers like Redis?

VarMQ wraps external stores using DistributedQueue[T] in distributed.go, which implements IDistributedQueue and serializes jobs to JSON. The same queueManager selection strategies and worker pool balancing apply—only the enqueue/dequeue operations delegate to the external broker (e.g., Redis) rather than local heap structures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →