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 ¶
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
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 ¶
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 ¶
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
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
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. |