Help us shape the future of Go! Take the 2026 Go Developer Survey

wait

package module
v0.15.6 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 5 Imported by: 1

README

wait

CI GoDoc

Package wait controls access to scarce things like connections, device handles, worker capacity, and API quota, when you need both a hard limit and fair service. You set the limit by how many items you add; the list never creates more. Callers wait their turn in the order they arrived, and nobody keeps racing for whatever comes back next.

An item that comes back goes straight to the caller that has waited longest, never to one that arrives a moment later, so no caller is passed over while items keep coming back.

wait.List is a pool of the items you add with Add. Waiting callers get items in arrival order. Idle items sit in a LIFO stack, so the next caller gets the most recently used one, which is the likeliest to still be warm. MaxWaiters caps how many callers can wait, and a context lets a caller give up or wait only until a deadline.

Take puts you in line and hands you a Ticket. The ticket's Value waits your turn, and its Done gives the item back. Calling Done twice, or on a ticket that never got in, does nothing. To keep the item instead, or replace a broken one with Add, call Leave before the deferred Done.

t := conns.Take(ctx)
defer t.Done()
c, err := t.Value()
if err != nil {
	return err
}

A list never creates items. To dial ahead or replace a broken connection, add connections that redial themselves: one dials in the background when it's made and again when it breaks, so whoever takes it next usually finds it connected. The package's redial example shows how.

A List holding a single item is a fair lock: whoever holds the item has the turn. The package's demand example uses two such lists to share a link's connections and bandwidth among downloads of different sizes, strictly in arrival order, in one short function.

Unlike a buffered channel, a list hands out the most recently returned item instead of the one idle longest, holds your place in line without blocking so you can get ready while you wait, and takes an item back only once no matter how many times you call Done. A channel is simpler when none of that matters. sync.Pool is for reusing temporary allocations; it doesn't limit how many resources exist, and it doesn't order callers.

API details and examples are in the package documentation.

Documentation

Overview

Package wait provides a first-come line for reusable items.

A List pools items added with List.Add. Queued callers get items in arrival order, so waiting is fair, and unused items wait in a LIFO stack, so the next caller gets the most recently used item, whose connection or cache is likeliest to still be warm.

Take returns a Ticket holding the caller's place in line. Its Value waits for admission, and its Done gives the item back:

t := conns.Take(ctx)
defer t.Done()
c, err := t.Value()
if err != nil {
	return err
}

To keep the item instead, or replace a broken one with List.Add, call Ticket.Leave before the deferred Done.

A List of one item is a fair lock: holding the item is your turn. The package's demand example shares a link's connections and bandwidth among downloads with two such Lists, serving callers strictly in arrival order.

Unlike a buffered channel, a List hands out the most recently returned item rather than the one idle longest, holds a caller's place in line without blocking, and takes each item back only once. Use sync.Pool for temporary allocation reuse, not for a bounded resource pool.

Example (Batch)

Seven callers share GPU batches of 16 floats, in arrival order. A batch is a slice whose length is the part reserved so far. The open batch is the only item in turn, so holding it is your turn to reserve. A caller that doesn't fit takes a place in next before it passes the turn on: smaller callers behind it fill the batch, and it goes first in the next one. Each caller's view is a subslice of its batch, not a copy.

package main

import (
	"context"
	"fmt"
	"log"
	"sync"

	"blake.io/wait"
)

func main() {
	ctx := context.Background()
	var turn, next wait.List[[]float32]
	turn.Add(make([]float32, 0, 16))

	views := make([][]float32, 7)
	var wg sync.WaitGroup
	submit := func(i, n int) {
		t := turn.Take(ctx) // Take never blocks, so the call order is the arrival order
		wg.Go(func() {
			l := &turn
			b, err := t.Value()
			for err == nil && n > cap(b)-len(b) {
				l = &next
				aside := l.Take(ctx) // keep a place before passing the turn on
				t.Done()
				t = aside
				b, err = t.Value()
			}
			if err != nil {
				log.Fatal(err)
			}
			views[i] = b[len(b) : len(b)+n]
			l.Add(b[:len(b)+n]) // pass the turn on, n more reserved
		})
	}

	// run waits its turn, so everyone ahead of it has reserved or stepped
	// aside, fills the batch, and opens the next one: first to those who
	// stepped aside, then to everyone else.
	run := func(k int) {
		b, err := turn.Take(ctx).Value()
		if err != nil {
			log.Fatal(err)
		}
		mine := next.Take(ctx) // behind everyone who stepped aside
		fmt.Printf("batch %d runs %d floats\n", k, len(b))
		for i := range b {
			b[i] = float32(100*k + i)
		}
		next.Add(make([]float32, 0, 16))
		if b, err = mine.Value(); err != nil {
			log.Fatal(err)
		}
		turn.Add(b)
	}

	for i, n := range []int{6, 8, 12, 2, 5, 3} {
		submit(i, n)
	}
	run(1)
	submit(6, 4) // arrives after batch 1
	run(2)
	run(3)
	wg.Wait()
	for i, v := range views {
		fmt.Printf("caller %d: %v\n", i+1, v)
	}

}
Output:
batch 1 runs 16 floats
batch 2 runs 15 floats
batch 3 runs 9 floats
caller 1: [100 101 102 103 104 105]
caller 2: [106 107 108 109 110 111 112 113]
caller 3: [200 201 202 203 204 205 206 207 208 209 210 211]
caller 4: [114 115]
caller 5: [300 301 302 303 304]
caller 6: [212 213 214]
caller 7: [305 306 307 308]
Example (Demand)

Downloads share a link's 8 connections and 100 Mbps, strictly in the order they ask. The second download's connections fit beside the first's, but its bandwidth doesn't, so it waits for the first to finish.

The ctx here is never done, so Value never fails. Code whose ctx can end must pass the turn on, with what came back, when it gives up.

package main

import (
	"context"
	"fmt"

	"blake.io/wait"
)

func main() {
	type demand struct{ conns, mbps int }
	ctx := context.Background()

	link := demand{8, 100}

	// The link's free capacity is the only item in free, so holding it is
	// your turn.
	var free, returned wait.List[demand]
	free.Add(link)

	// acquire reports false for more than the whole link, which would hold
	// the turn forever, and everyone behind it with it.
	acquire := func(d demand) bool {
		if d.conns > link.conns || d.mbps > link.mbps {
			return false
		}
		f, _ := free.Take(ctx).Value() // wait your turn
		for d.conns > f.conns || d.mbps > f.mbps {
			r, _ := returned.Take(ctx).Value() // hold the turn until enough is back
			f.conns, f.mbps = f.conns+r.conns, f.mbps+r.mbps
		}
		free.Add(demand{f.conns - d.conns, f.mbps - d.mbps}) // pass the turn on
		return true
	}

	if !acquire(demand{16, 10}) {
		fmt.Println("16 connections is more than the link has")
	}
	for _, d := range []demand{{4, 60}, {2, 50}} {
		acquire(d)
		go func() {
			defer returned.Add(d)
			fmt.Printf("downloading on %d connections at %d Mbps\n", d.conns, d.mbps)
		}()
	}
	acquire(link) // the whole link: waits for both downloads

}
Output:
16 connections is more than the link has
downloading on 4 connections at 60 Mbps
downloading on 2 connections at 50 Mbps
Example (LoadOnDemand)

This example loads copies of a model only when callers need them, up to max. A caller that finds no idle copy starts a load if there is room for another copy, then waits in line. Each copy, whether newly loaded or given back, goes to the caller that has waited longest.

Calling TryTake before Take is fair, because TryTake cannot take a copy that a waiting caller is owed. If a copy is given back between TryTake and Take, the caller may start a load it didn't need, but there are still at most max copies.

A load can fail for reasons the process doesn't control, such as other processes filling the GPU's memory. The goroutine that started the load tries again rather than giving up its room, because callers already in line are waiting for that copy.

package main

import (
	"context"
	"errors"
	"fmt"
	"sync/atomic"
	"time"

	"blake.io/wait"
)

func main() {
	const max = 2
	ctx := context.Background()
	var copies wait.List[string]

	// load stands in for reading the model into GPU memory. Every other
	// load finds the memory full.
	var loads atomic.Int64
	load := func() error {
		if loads.Add(1)%2 == 0 {
			return errors.New("out of GPU memory")
		}
		return nil
	}

	var misses atomic.Int64 // each of the first max misses starts a load
	get := func() wait.Ticket[string] {
		if t, ok := copies.TryTake(); ok {
			return t // an idle copy
		}
		if n := misses.Add(1); n <= max {
			go func() {
				for err := load(); err != nil; err = load() {
					fmt.Printf("loading copy %d: %v; retrying\n", n, err)
					time.Sleep(100 * time.Millisecond)
				}
				copies.Add(fmt.Sprintf("copy %d", n))
			}()
		}
		return copies.Take(ctx)
	}
	use := func(caller string, t wait.Ticket[string]) {
		name, _ := t.Value() // ctx is never done, so Value never fails
		fmt.Println(caller, "running on", name)
	}

	a := get() // loads copy 1
	use("a", a)
	b := get() // copy 1 is in use: loads copy 2
	use("b", b)
	c := get() // at max: waits in line
	a.Done()   // copy 1 goes to c
	use("c", c)
	b.Done()
	c.Done()

}
Output:
a running on copy 1
loading copy 2: out of GPU memory; retrying
b running on copy 2
c running on copy 1
Example (Redial)

A caller that finds its connection broken redials it before the deferred Done gives it back, so the next caller gets a fresh one. Dropping the conn instead would shrink the List for good, and once every conn is dropped, callers wait forever.

package main

import (
	"context"
	"fmt"
	"log"
	"sync"

	"blake.io/wait"
)

// A conn is a pooled connection that redials itself. It dials in the
// background when it is made and again after redial, so whoever takes it
// from a List usually finds it connected.
type conn struct {
	addr   string
	dials  int
	dialed func() (string, error) // waits for the dial in flight
}

func newConn(addr string) *conn {
	c := &conn{addr: addr}
	c.redial()
	return c
}

func (c *conn) redial() {
	c.dials++
	session := fmt.Sprintf("%s#%d", c.addr, c.dials) // stands in for a real dial
	c.dialed = sync.OnceValues(func() (string, error) { return session, nil })
	go c.dialed()
}

// A caller that finds its connection broken redials it before the deferred
// Done gives it back, so the next caller gets a fresh one. Dropping the
// conn instead would shrink the List for good, and once every conn is
// dropped, callers wait forever.
func main() {
	var conns wait.List[*conn]
	conns.Add(newConn("db"))

	use := func(broken bool) {
		t := conns.Take(context.Background())
		defer t.Done()
		c, err := t.Value()
		if err != nil {
			log.Fatal(err)
		}
		session, err := c.dialed()
		if err != nil {
			c.redial()
			return
		}
		fmt.Println("using", session)
		if broken {
			c.redial()
		}
	}

	use(true) // db#1 breaks while in use
	use(false)

}
Output:
using db#1
using db#2

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMaxWaiters is returned by [Ticket.Value] when [List.Take] found
	// MaxWaiters Tickets already waiting.
	ErrMaxWaiters = errors.New("too many waiters")

	// ErrClosed is returned by [Ticket.Value] when the List is closed
	// before the Ticket is admitted.
	ErrClosed = errors.New("closed")

	// ErrDone is returned by [Ticket.Value] after [Ticket.Done], after
	// [Ticket.Leave] takes a waiting Ticket out of the line, and for the
	// zero Ticket.
	ErrDone = errors.New("ticket done")
)

Functions

This section is empty.

Types

type List

type List[Item any] struct {
	// MaxWaiters is the maximum number of Tickets waiting in line. A Take
	// that would exceed it returns a Ticket whose Value reports
	// ErrMaxWaiters. A Ticket whose ctx is done stops counting when its
	// Value returns or it reaches the front of the line. Zero means no
	// limit. Set MaxWaiters before using the List, and do not change it
	// afterward.
	MaxWaiters int
	// contains filtered or unexported fields
}

List pools reusable items of type Item.

It hands items added with List.Add to callers of List.Take in arrival order: a returned item goes straight to the caller that has waited longest, never to one that arrives later. Unused items wait in a LIFO stack, so the next caller gets the most recently used, warmest item. A caller gives an item back with Ticket.Done.

A List never creates items, so an item that is dropped rather than given back or replaced with Add is gone for good; drop them all, and every Ticket's Value waits until its ctx is done. To dial ahead, or replace a broken connection, add items that do it themselves, as the package's redial example shows.

The zero List is empty and ready to use. List is safe for concurrent use.

Example

A List of two connections. A returned connection is the next one taken, while it is still warm.

package main

import (
	"context"
	"fmt"
	"log"

	"blake.io/wait"
)

func main() {
	var conns wait.List[string]
	conns.Add("conn-a")
	conns.Add("conn-b")

	use := func() {
		t := conns.Take(context.Background())
		defer t.Done()
		c, err := t.Value()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("using", c)
	}
	use()
	use()

}
Output:
using conn-b
using conn-b

func (*List[T]) Add added in v0.5.0

func (l *List[T]) Add(v T) bool

Add adds v to the List as a new item. It hands v to the longest-waiting Ticket, or stores it for later. Add returns false after List.Close. To give back an item taken from the List, use Ticket.Done instead.

func (*List[T]) Close

func (l *List[T]) Close()

Close fails every waiting Ticket with ErrClosed and makes later List.Add calls return false. After Close, List.Take returns the remaining ready items, including any given back later, then fails with ErrClosed. Close is idempotent.

Example

At shutdown, Close the List and drain its ready items. After Close, Take returns each ready item, then fails with ErrClosed.

package main

import (
	"context"
	"fmt"

	"blake.io/wait"
)

func main() {
	var conns wait.List[string]
	conns.Add("conn-a")
	conns.Add("conn-b")

	conns.Close()
	for {
		c, err := conns.Take(context.Background()).Value()
		if err != nil {
			fmt.Println(err)
			break
		}
		fmt.Println("closing", c) // not given back: it is ours to close
	}

}
Output:
closing conn-b
closing conn-a
closed

func (*List[T]) Take

func (l *List[T]) Take(ctx context.Context) Ticket[T]

Take returns a Ticket for an item. If ctx is done, the Ticket fails with ctx's cause, even if an item is ready or the List is closed. Otherwise, if an item is ready, the Ticket holds it already; if the List is closed, the Ticket fails with ErrClosed; and if neither, the Ticket waits in line. Take never blocks; the Ticket's Value waits. To take only a ready item, use List.TryTake.

func (*List[T]) TryTake

func (l *List[T]) TryTake() (Ticket[T], bool)

TryTake returns a Ticket holding the most recently used ready item, and true, or the zero Ticket and false if no item is ready. Unlike Take, it never joins the line. A List never leaves an item ready while a Ticket waits in line, so an item TryTake finds is owed to no one: TryTake cannot cut the line.

Example

TryTake takes an idle item if there is one, and never joins the line.

package main

import (
	"fmt"

	"blake.io/wait"
)

func main() {
	var conns wait.List[string]
	conns.Add("conn-a")

	if t, ok := conns.TryTake(); ok {
		defer t.Done()
		c, _ := t.Value()
		fmt.Println("took", c)
	}
	if _, ok := conns.TryTake(); !ok {
		fmt.Println("no conn ready")
	}

}
Output:
took conn-a
no conn ready

type Ticket added in v0.5.0

type Ticket[T any] struct {
	// contains filtered or unexported fields
}

A Ticket is a place in a List's line and, once admitted, the item it was admitted with.

Value waits for admission. Done ends the Ticket, giving its item back to the List; call Done on every Ticket, with defer. To keep the item, or replace it with List.Add, call Leave first: Done then gives nothing back. The first Done ends the Ticket and every copy of it: after that, Value returns ErrDone, and Leave and Done do nothing. A Ticket that failed in Take, without joining the line, has nothing to give back; its Value keeps returning the same error. The zero Ticket is ended.

Once Value returns an item, the holder of a Ticket that is never done owns the item outright. A Ticket still waiting keeps its place until its ctx is done or it leaves.

A Ticket is for use by one goroutine at a time.

func (Ticket[T]) Done added in v0.11.0

func (t Ticket[T]) Done()

Done ends the Ticket and every copy of it. If the Ticket is still waiting, Done takes it out of the line; if it holds an item and has not left, Done gives the item back to the List.

func (Ticket[T]) Leave added in v0.10.0

func (t Ticket[T]) Leave() bool

Leave takes the Ticket out of the line if it is still waiting, and reports whether this is the first Leave of the Ticket or its copies. An admitted Ticket has left the line already; Leave then keeps its item for its holder, and Done no longer gives it back. Once Leave returns true, Value never waits: it returns the item, or, if Leave took the Ticket out of the line, the cause of its ctx if done, else ErrDone. Leave returns false after Done, for a Ticket that failed in Take, and for the zero Ticket.

Example

A caller that finds its connection broken replaces it. Leave keeps the broken one from going back to the List, and Add puts a new one in its place. The deferred Done still ends the Ticket, but gives nothing back.

package main

import (
	"context"
	"fmt"
	"log"

	"blake.io/wait"
)

func main() {
	var conns wait.List[string]
	dials := 1
	conns.Add("conn-1")

	use := func(broken bool) {
		t := conns.Take(context.Background())
		defer t.Done()
		c, err := t.Value()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("using", c)
		if broken && t.Leave() {
			dials++
			conns.Add(fmt.Sprintf("conn-%d", dials))
		}
	}
	use(true) // conn-1 breaks
	use(false)

}
Output:
using conn-1
using conn-2

func (Ticket[T]) Value added in v0.5.0

func (t Ticket[T]) Value() (T, error)

Value returns the item the Ticket was admitted with, waiting for admission if necessary. If the Ticket failed, Value returns the error: ErrClosed, ErrMaxWaiters, or the cause of the context passed to Take. Cancellation wins over admission: if the context is done before Value returns the item, even an item admitted first, Value gives the item back to the List and fails the Ticket with the context's cause. Once Value returns an item, it returns that item until the Ticket is done.

Directories

Path Synopsis
Package batch fills fixed-size batches from concurrent callers in arrival order, for work that is cheaper done many elements at a time, like GPU inference.
Package batch fills fixed-size batches from concurrent callers in arrival order, for work that is cheaper done many elements at a time, like GPU inference.
Package queue provides FIFO queues and LIFO stacks.
Package queue provides FIFO queues and LIFO stacks.

Jump to

Keyboard shortcuts

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