conductor

package module
v0.0.0-...-c509061 Latest Latest
Warning

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

Go to latest
Published: Mar 11, 2024 License: GPL-3.0 Imports: 10 Imported by: 0

README

Conductor

A generalization of the Context

Motivation

The standard library Context is a great concept. It allows to propagate values and allows to coordinate across API and goroutine boundaries. As much as I like it, when dealing with application that show a lot of moving pieces, I want something more in order to coordinate them. I want a Conductor. In the same spirit as the Done method of a Context, a Conductor Cmd method may be used to listen for commands. These are represented by the ubiquitous generic parameter T across this library, and may be everything, from simple strings to interfaces carrying a complicated logic with them.

How to use

Please, read also the performance considerations.

There are two different Conductors implemented right now, a Simple and a Tagged one.

The first is straightforward to use (we take T to be string, for the sake of simplicity):

simple := Simple[string]()

// we listen for commands

for {
	select {
	case cmd := <-simple.Cmd():
			// react to the commands
	case <-simple.Done():
			// a Conductor is also a Context, so we can use it the same way
			// for example, for controlling a clean exit
	}
}

// ...in another part of the control flow we can call Send to send a commands
// to all the listeners created with Cmd

Send[string](simple)("doit")

// This will fire in the above select statement, delivering "doit" to the first case

The second one might sound a bit more involved, but it's hopefully just a matter of getting used to the syntax:

tagged := Tagged[string]()

// we listen on many different possible tags

for {
	select {
	case cmd := <-WithTag[string](tagged, "tag1").Cmd():
		// React to a command in the "tag1" branch
	case cmd := <-WithTag[string](tagged, "tag2").Cmd():
		// React to a command in the "tag2" branch
	case <-tagged.Done():
		// As for the Simple, also the Tagged is a Context
	}
}


// We may selectively send a command, again using the Send function

Send[string](tagged, "tag1")("doitnow")

// We may also send a broadcast command

Send[string](tagged)("allhands")
Performance

In the examples above and in those in the examples/ folder, you can notice that the typical usage of a conductor boils down to receiving from a channed produced by the Cmd function in a case statement inside a select. This is done to echo the syntax of context.Context.Done. To achieve this, the code tries to keep track where it is called from, unwinding the stack using runtime.Callers until we exit this module and reach the calling site. This, coupled with the semantics of case that if provided with a function calls it every time another statement fires, may require the library to walk the stack very frequently. The following excerpt is taken from the performance example.

func run(c conductor.Conductor[int], inst int) {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-c.Cmd():
			fmt.Println(inst, "received")
		case <-ticker.C:
			fmt.Println(inst, "tick")
		}
	}
}

func main() {
	c := conductor.Simple[int]()

	for i := 0; i < 5; i++ {
		go run(c, i)
	}

	time.Sleep(10 * time.Second)
	conductor.Send[int](c)(0)
	time.Sleep(500 * time.Millisecond)
}

Fortunately, this is easily avoidable, explicitly assigning the result of Cmd before using it in a case:

	lis := c.Cmd()

	for {
		select {
		case <-lis:
			fmt.Println(inst, "received")
		case <-ticker.C:
			fmt.Println(inst, "tick")
		}
	}

This is true both for Simple and Tagged conductors.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetLogFile

func SetLogFile(path string)

SetLogFile explicitly sets the file where to log the inner workings of the library. Panics if called more than once.

Types

type Conductor

type Conductor[T any] interface {
	// Cmd returns a channel where to listen to for commands. Is the analogous of
	// [context.Context.Done].
	Cmd() <-chan T
	// WithContext assigns the given [context.Context] to the [Conductor], replacing
	// the one currently assigned to it.
	WithContext(context.Context) Conductor[T]
	// WithContextPolicy attaches a [Policy] to the given [Conductor].
	WithContextPolicy(Policy[T]) Conductor[T]
	context.Context
}

Conductor is a type useful to convey commands (represented by the generic type T) across API and goroutine boundaries. It can be thought as a generalization of as context.Context and, as such, it also implements the context.Context interface.

func NewConductorWithCtx

func NewConductorWithCtx[T any](conductor Conductor[T], ctx context.Context) Conductor[T]

NewConductorWithCtx creates a new conductor that hinerits the features of the given one, but replaces the inner context.Context. NOTE: the conductor must be non-nil, or the function will panic.

func Simple

func Simple[T any]() Conductor[T]

Simple creates a Conductor with a single type of listener.

func SimpleFromContext

func SimpleFromContext[T any](parent context.Context) Conductor[T]

SimpleFromContext creates a Simple Conductor from a given context.Context.

func Tagged

func Tagged[T any]() Conductor[T]

Tagged creates a Conductor that supports tagged listeners.

func TaggedFromContext

func TaggedFromContext[T any](parent context.Context) Conductor[T]

TaggedFromContext creates a Tagged Conductor from a given context.Context.

func TaggedFromSimple

func TaggedFromSimple[T any](s Conductor[T]) Conductor[T]

TaggedFromSimple transforms a Simple Conductor in a Tagged one.

func WithCancel

func WithCancel[T any](conductor Conductor[T]) (Conductor[T], context.CancelFunc)

WithCancel mimics what context.WithCancel does, but for a Conductor. It returns a *copy* of the given conductor, that behaves as a context subject to a cancel function returned as second value of the output.

func WithDeadline

func WithDeadline[T any](conductor Conductor[T], deadline time.Time) (Conductor[T], context.CancelFunc)

WithDeadline mimics what context.WithCancel does, but for a Conductor. It returns a *copy* of the given conductor, that behaves as a context subject to a cancel function returned as second value of the output, and that will be cancelled at the given deadline.

func WithTag

func WithTag[T any](conductor Conductor[T], tag string, discriminator ...any) Conductor[T]

WithTag loads a tagged listener in a Tagged Conductor.

func WithTimeout

func WithTimeout[T any](conductor Conductor[T], interval time.Duration) (Conductor[T], context.CancelFunc)

WithTimeout mimics what context.WithCancel does, but for a Conductor. It returns a *copy* of the given conductor, that behaves as a context subject to a cancel function returned as second value of the output, and that will be cancelled after the given interval.

type Notifier

type Notifier[T any] func(cmd T, signals ...os.Signal)

Notifier is the return type of the Notify function.

func Notify

func Notify[T any](conductor Conductor[T], args ...any) Notifier[T]

Notify may be used on a Conductor to create a function to register it to an os.Signal, in the same spirit as os/signal.Notify. The optional variadic args may be used to configure this mechanism, depending on the specific instance of the provided Conductor.

type Policy

type Policy[T any] interface {
	// Decide gets invoked when the [Conductor] is canceled.
	Decide(args ...any) (T, bool)
}

Policy is invoked when the Conductor gets cancelled, in the sense of context (i.e. being it a context.Context, it can be canceled in the usual way). When this happens, a Policy is a way to decide what to do with all the listeners loaded in a Conductor.

func ConstantPolicy

func ConstantPolicy[T any](cmd T) Policy[T]

ConstantPolicy creates a Policy that fires the given command to all the listeners.

Example
simple, cancel := WithCancel(Simple[string]())
simple.WithContextPolicy(ConstantPolicy("ciao"))

lis := simple.Cmd()

go cancel()

cmd := <-lis
fmt.Println(cmd)
Output:

ciao

func SetPolicy

func SetPolicy[T any](mapping map[any]T) Policy[T]

SetPolicy creates a Policy that uses the provided map to conditionally fire the command found at the associated identifier in the map. It is to be used only with a Tagged Conductor. If attached to a Simple, it is a noop.

Example
tagged, cancel := WithCancel(Tagged[string]())
tagged.WithContextPolicy(SetPolicy(map[any]string{
	"first":  "ciao",
	"second": "miao",
	"third":  "bau",
}))

lisFirst := WithTag(tagged, "first").Cmd()
lisSecond := WithTag(tagged, "second").Cmd()
lisThird := WithTag(tagged, "third").Cmd()

go cancel()

loop:
for {
	select {
	case cmd := <-lisFirst:
		fmt.Println(cmd)
	case cmd := <-lisSecond:
		fmt.Println(cmd)
	case cmd := <-lisThird:
		fmt.Println(cmd)
	case <-time.After(successTimeout):
		break loop
	}
}
Output:

ciao
miao
bau

type Sender

type Sender[T any] func(cmd T)

Sender is the return type of the Send function.

func Send

func Send[T any](conductor Conductor[T], args ...any) Sender[T]

Send may be used on a Conductor to create a function to send a command to the interested listeners. It accepts a variadic amount of arguments to accommodate custom behavior, depending on the specific instance of a Conductor it acts on.

Example (Simple)
simple := SimpleFromContext[string](context.Background())

lis := simple.Cmd()

go Send(simple)("ciao")

cmd := <-lis
fmt.Println(cmd)
Output:

ciao
Example (Tagged)
tagged := TaggedFromContext[string](context.Background())

lisFirst := WithTag(tagged, "first").Cmd()
lisSecond := WithTag(tagged, "second").Cmd()
lisThird := WithTag(tagged, "third").Cmd()

tagged, cancel := WithCancel(tagged)
feedback := make(chan struct{}, 3)

go func() {
	Send(tagged, "first")("ciao")
	Send(tagged, "second")("miao")
	Send(tagged, "third")("bau")
	<-feedback
	<-feedback
	<-feedback
	cancel()
}()

loop:
for {
	select {
	case cmd := <-lisFirst:
		fmt.Println(cmd)
		feedback <- struct{}{}
	case cmd := <-lisSecond:
		fmt.Println(cmd)
		feedback <- struct{}{}
	case cmd := <-lisThird:
		fmt.Println(cmd)
		feedback <- struct{}{}
	case <-tagged.Done():
		break loop
	}
}
Output:

ciao
miao
bau

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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