Strategies
A worker can have several sources bound to it: queues, priority queues and topics. When a slot frees up, it needs a rule for which source gives the next job. That rule is the strategy. Workers and queues introduced it; this page covers all four.
Choosing a strategy
Section titled “Choosing a strategy”Strategy has four values. WithStrategy sets one when you create the worker, and DefaultStrategy changes the default for workers created after the call.
| Strategy | Takes the next job from | Ties |
|---|---|---|
Priority, the default |
the queue with the lowest priority number that has a waiting job | the queue bound first |
RoundRobin |
the next queue in turn that has a waiting job | not applicable: it takes turns |
MaxLen |
the queue with the most waiting jobs | the queue that comes first in the list below |
MinLen |
the queue with the fewest waiting jobs, ignoring empty queues | the queue that comes first in the list below |
worker := varmq.NewWorker(process, 2, varmq.WithStrategy(varmq.RoundRobin))Pick a strategy below and take jobs one at a time. The queues start with 2 alerts, 1 report and 3 emails, the same as the RoundRobin and length programs below. Before each job, the scene says which queue the strategy chooses and why. Picking another strategy starts a new worker, because a worker keeps its strategy.
One worker has three queues: alerts with rank 1 and 2 jobs, reports with rank 2 and 1 job, and emails with no rank and 3 jobs. Pick a strategy and take jobs one at a time to see which queue gives each job. With Priority the order is alert, alert, report, email, email, email. With RoundRobin it is alert, report, email, alert, email, email. With MaxLen it is email, alert, email, alert, report, email. With MinLen it is report, alert, alert, email, email, email.
The worker took, in order0 of 6 jobs
worker := varmq.NewWorker(process, 1) // Priority, the defaultA few rules hold for all four:
- Empty queues are skipped. A strategy only chooses among sources that have a job waiting.
- The strategy is fixed when the worker is created. There is no call to change it later.
- It decides every job. With two or more sources bound, the worker hands out one job at a time and picks the source again each time, reading the queue lengths as they are then. How it works explains why it does not batch in this case. With a single source there is nothing to choose.
- The zero value of
StrategyisRoundRobin, not the default. A worker created withoutWithStrategyusesPriority.
“The list” is every source ordered by priority number, lowest first, and by bind order among equal numbers. Every strategy walks it in that order.
Priority: the default
Section titled “Priority: the default”WithQueuePriority ranks a queue when you bind it. It works on BindQueue and BindPriorityQueue. The programs on this page use a worker with concurrency 1 and WithAutoRun(false): no job starts until every queue is filled, and the worker takes one job at a time, so the order the jobs print in is the order the worker took them.
9 collapsed lines
package main
import ( "fmt"
"github.com/goptics/varmq")
func main() { // Concurrency 1 takes one job at a time, so jobs print in the order they are taken. worker := varmq.NewWorker( func(j varmq.Job[string]) { fmt.Println(j.Data()) }, varmq.WithAutoRun(false), )
alerts := worker.BindQueue(varmq.WithQueuePriority(1)) reports := worker.BindQueue(varmq.WithQueuePriority(2)) emails := worker.BindQueue() // no priority: it ranks last
// Fill the lowest-ranked queue first. emails.Add("email 1") emails.Add("email 2") reports.Add("report 1") alerts.Add("alert 1") alerts.Add("alert 2")
worker.Start() worker.WaitUntilIdle()}Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
The emails were added first but run last. Both alerts go before the report, and the report before the emails.
A lower number goes first. The worker takes from the first queue in the list that has a job waiting. A queue without WithQueuePriority gets the lowest possible rank, so it comes after every queue that has one. Queues with the same rank go in the order they were bound.
Priority does not share. While a higher-ranked queue has jobs waiting, a lower-ranked one gets none. A busy alerts queue can keep emails waiting.
A topic subscription takes part in the same list. WithTopicPriority sets its rank like WithQueuePriority does. See topics.
RoundRobin
Section titled “RoundRobin”The worker goes through the list one queue per job, then starts over. It skips a queue that is empty when its turn comes, so once a queue runs out the others share the turns.
9 collapsed lines
package main
import ( "fmt"
"github.com/goptics/varmq")
func main() { worker := varmq.NewWorker( func(j varmq.Job[string]) { fmt.Println(j.Data()) }, varmq.WithStrategy(varmq.RoundRobin), varmq.WithAutoRun(false), )
alerts := worker.BindQueue() reports := worker.BindQueue() emails := worker.BindQueue()
alerts.Add("alert 1") alerts.Add("alert 2") reports.Add("report 1") emails.Add("email 1") emails.Add("email 2") emails.Add("email 3")
worker.Start() worker.WaitUntilIdle()}Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
One job from each queue in turn. reports runs out after its first job, so the later turns go to alerts and emails.
MaxLen and MinLen
Section titled “MaxLen and MinLen”Both compare how many jobs are waiting in each queue, read again for every job.
MaxLentakes from the longest queue. Each job it takes makes that queue shorter, so while nothing new arrives the turns pass to the next-longest queue, and the longest queues are worked down together.MinLentakes from the shortest queue that is not empty. It runs a short queue to empty, then moves to the next shortest. A long queue waits while any shorter queue still has jobs.
On a tie, both take the queue that comes first in the list.
8 collapsed lines
package main
import ( "fmt"
"github.com/goptics/varmq")
func run(name string, strategy varmq.Strategy) { worker := varmq.NewWorker( func(j varmq.Job[string]) { fmt.Print(" ", j.Data()) }, varmq.WithStrategy(strategy), varmq.WithAutoRun(false), )
alerts := worker.BindQueue() reports := worker.BindQueue() emails := worker.BindQueue()
alerts.Add("alert") alerts.Add("alert") reports.Add("report") emails.Add("email") emails.Add("email") emails.Add("email")
fmt.Print(name, ":") worker.Start() worker.WaitUntilIdle() fmt.Println()}
func main() { run("MaxLen", varmq.MaxLen) run("MinLen", varmq.MinLen)}Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
The queues start with 2 alerts, 1 report and 3 emails. MaxLen takes an email from the longest queue, then breaks the 2-2 tie with alerts, which comes first in the list. When reports and emails tie at 1 each, reports goes first, so an email runs last. MinLen empties reports, then alerts, and leaves the long emails queue for last.
“In order” here means the order the jobs leave the queues. With a concurrency above 1 several of them run at once, and their output can interleave.
What priority does under the other strategies
Section titled “What priority does under the other strategies”RoundRobin, MaxLen and MinLen do not choose by priority. They still walk the list in priority order, so a priority sets the order of the RoundRobin turns and which queue wins a tie in MaxLen and MinLen. If you give no priorities, the list is the bind order.
VarMQ is MIT licensed. Benchmark figures come from github.com/goptics/benchmarks.