idem

package module
v0.20.5 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2025 License: MIT Imports: 13 Imported by: 19

README

idem: idempotently closable channels for halting goroutines

Package idem supports a common pattern for halting goroutines in Go.

The implementation in halter.go is fairly self explanatory.

The test file halter_test.go has lots of examples.

The summary is: Halter is used for shutting down goroutines, and waiting until they have exited. This requires cooperation from the code the goroutine is running of course, so you'll need to be writing both. Halter has two idempotently closable channels: ReqStop to request that the goroutine stop, and Done for it to acknowledge it is just about to exit.

It is really simple actually.


Copyright (c) 2025 by Jason E. Aten, Ph.D.

Licence: 3-clause BSD, same as Go. See the LICENSE file.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyClosed = fmt.Errorf("Chan already closed")
View Source
var ErrBailed = fmt.Errorf("bail chan closed before getting the lock")
View Source
var ErrChanAlreadyHasParent = fmt.Errorf("error: IdemCloseChan.AddChild(): error, child already has a parent")
View Source
var ErrChanCycle = fmt.Errorf("error: IdemCloseChan.AddChild would create cycle.")
View Source
var ErrGiveUp = fmt.Errorf("giveup channel was closed.")

ErrGiveUp is returned by IdemCloseChan.WaitTilDone if the giveup channel close was the reason that WaitTilDone returned.

View Source
var ErrHalterAlreadyHasParent = fmt.Errorf("error: Halter.AddChild(): error, child already has a parent")
View Source
var ErrHalterCycle = fmt.Errorf("error: Halter.AddChild would create cycle.")
View Source
var ErrNotClosed = fmt.Errorf("tree is not closed")

ErrNotClosed is returned by FirstTreeReason if it is called on a tree that is not completely closed.

View Source
var ErrTasksAllDone = fmt.Errorf("tasks all done")

ErrTasksAllDone is the reason when the task count reached 0.

Functions

func GoroNumber added in v0.9.3

func GoroNumber() int

GoroNumber returns the calling goroutine's number.

func IsNil added in v0.9.3

func IsNil(face interface{}) bool

IsNil uses reflect to to return true iff the face contains a nil pointer, map, array, slice, or channel.

Types

type Halter

type Halter struct {
	// The owning goutine should call Done.Close() as its last
	// action once it has received the ReqStop() signal.
	Done *IdemCloseChan

	// Other goroutines call ReqStop.Close() in order
	// to request that the owning goroutine stop immediately.
	// The owning goroutine should select on ReqStop.Chan
	// in order to recognize shutdown requests.
	ReqStop *IdemCloseChan
	// contains filtered or unexported fields
}

Halter helps shutdown a goroutine

func NewHalter

func NewHalter() (h *Halter)

func NewHalterNamed added in v0.20.4

func NewHalterNamed(name string) (h *Halter)

func (*Halter) AddChild added in v0.0.5

func (h *Halter) AddChild(child *Halter)

AddChild adds child to h's children; and adds child.ReqStop to the children of h.ReqStop.

Note that if h.ReqStop is already closed, then child.ReqStop will also be ClosedWithReason with the same reason as h, if any is available.

func (*Halter) IsDone

func (h *Halter) IsDone() bool

IsDone returns true iff h.Done has been Closed().

func (*Halter) IsStopRequested

func (h *Halter) IsStopRequested() bool

IsStopRequested returns true iff h.ReqStop has been Closed().

func (*Halter) MarkDone

func (h *Halter) MarkDone()

MarkDone closes the h.Done channel if it has not already done so. Safe for multiple goroutine access.

func (*Halter) RemoveChild added in v0.0.5

func (h *Halter) RemoveChild(child *Halter)

RemoveChild removes child from the set of h.children. It does not undo any close operation that AddChild did on addition.

func (*Halter) RequestStop

func (h *Halter) RequestStop()

RequestStop closes the h.ReqStop channel if it has not already done so. Safe for multiple goroutine access.

func (*Halter) RootHalter added in v0.20.2

func (h *Halter) RootHalter() (root *Halter)

func (*Halter) RootReqStopClose added in v0.20.3

func (h *Halter) RootReqStopClose(why error) error

func (*Halter) RootString added in v0.20.4

func (h *Halter) RootString() string

func (*Halter) StopTreeAndWaitTilDone added in v0.0.5

func (h *Halter) StopTreeAndWaitTilDone(atMost time.Duration, giveup <-chan struct{}, why error)

StopTreeAndWaitTilDone first calls ReqStop.CloseWithReason(why) on h (which will recursively call ReqStop.CloseWithReason(why) on all non-closed children in the ReqStop parallel, tree, efficiently stopping at the first already-closed node). Then it waits up to atMost time for all tree members to Close their Done.Chan. It may return much more quickly than atMost, but will never wait longer. An atMost duration <= 0 will wait indefinitely.

type IdemCloseChan

type IdemCloseChan struct {
	Chan chan struct{}
	// contains filtered or unexported fields
}

IdemCloseChan can have Close() called on it multiple times, and it will only close Chan once.

func NewIdemCloseChan

func NewIdemCloseChan() *IdemCloseChan

NewIdemCloseChan makes a new IdemCloseChan.

func (*IdemCloseChan) AddChild

func (c *IdemCloseChan) AddChild(child *IdemCloseChan)

AddChild adds a child IdemCloseChan that will be closed when its parent is Close()-ed. If c is already closed, we close child immediately.

func (*IdemCloseChan) Close

func (c *IdemCloseChan) Close() error

Close returns ErrAlreadyClosed if it has been called before. It never closes IdemClose.Chan more than once, so it is safe to ignore the returned error value. Close() is safe for concurrent access by multiple goroutines. Close returns nil on the initial call, and ErrAlreadyClosed on any subsequent call.

When we actually close the underlying c.Chan, we also call Close on any children, while holding our mutex. This does not happen if the underlying channel is already closed, so the recursion stops at the depth of the first already-closed node in the tree.

func (*IdemCloseChan) CloseWithReason added in v0.0.7

func (c *IdemCloseChan) CloseWithReason(why error) error

CloseWithReason is a no-op if c is already closed. Otherwise, we record the why and close c. Use Reason() to get the why back. Like Close(), we recursive call CloseWithReason() on any children. Naturally decendents will only set why if they were still open when the recursive CloseWithReason gets to them, and if any child is already closed the recursion stops.

func (*IdemCloseChan) FirstTreeReason added in v0.4.0

func (c *IdemCloseChan) FirstTreeReason() (err error)

FirstTreeReason scans the whole tree root at c for the first non-nil close reason, and returns it. If the tree is not completely closed at any node, it returns ErrNotClosed.

func (*IdemCloseChan) IsClosed

func (c *IdemCloseChan) IsClosed() bool

IsClosed tells you if Chan is already closed or not.

func (*IdemCloseChan) Reason added in v0.0.7

func (c *IdemCloseChan) Reason() (why error, isClosed bool)

Reason gets the why set with CloseWithReason(). It also returns the closed state of c. If c is still open, why will be nil, as why is only set during the initial CloseWithReason if c was still open. The returned why will also be nil if c was closed without a reason, by Close() for example.

Callers should be prepared to handle a nil why no matter what the state of isClosed.

func (*IdemCloseChan) Reason1 added in v0.8.1

func (c *IdemCloseChan) Reason1() (why error)

Reason1 is the same as Reason but without the isClosed. This is easier to use in logging.

func (*IdemCloseChan) RemoveChild

func (c *IdemCloseChan) RemoveChild(child *IdemCloseChan)

func (*IdemCloseChan) TaskAdd added in v0.6.0

func (c *IdemCloseChan) TaskAdd(delta int) (newval int)

TaskAdd adds delta to the taskCount. When the taskCount reaches zero (or below), this channel will be closed.

TaskAdd, TaskDone, TaskWait are like sync.WaitGroup but with sane integration with other channels; use the giveup channel in TaskWait, or just integreate c.Chan into your own select loop.

func (*IdemCloseChan) TaskDone added in v0.6.0

func (c *IdemCloseChan) TaskDone() int

TaskDone subtracts one from the task count.

func (*IdemCloseChan) TaskWait added in v0.6.0

func (c *IdemCloseChan) TaskWait(giveup <-chan struct{}) (err error)

TaskWait waits to return until either c or giveup is closed, whever happens first.

TaskAdd, TaskDone, TaskWait are like sync.WaitGroup but with sane integration with other channels.

You can readily integrate c.Chan into your own select statement.

func (*IdemCloseChan) WaitTilChildrenClosed added in v0.6.0

func (c *IdemCloseChan) WaitTilChildrenClosed(giveup <-chan struct{}) (err error)

WaitTilChildrenClosed is just like WaitTilClosed, but does not require (or check) if we ourselves, the root of our tree, is closed.

func (*IdemCloseChan) WaitTilClosed added in v0.6.0

func (c *IdemCloseChan) WaitTilClosed(giveup <-chan struct{}) (err error)

WaitTilClosed does not return until either a) we and all our children have closed our channels; or b) the supplied giveup channel has been closed.

We recursively call WaitTilClosed on all of our children, and then wait for our own close.

Note we cannot use a time.After channel for giveup, since that only fires once--we have possibly many children to wait for. Hence giveup must be _closed_, not just sent on, to have them all stop waiting.

If the giveup channel was closed, the returned error will be ErrGiveUp. Otherwise it will be the first Reason() supplied by any CloseWithReason() calls in the tree. The first non-nil 'why' error we encounter is sticky. If err is returned nil, you know that either CloseWithReason was not used, or that there were only nil reason closes.

Jump to

Keyboard shortcuts

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