director

package module
v0.0.0-...-78829fc Latest Latest
Warning

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

Go to latest
Published: Apr 10, 2024 License: BSD-2-Clause Imports: 2 Imported by: 50

README

godoc here Build Status

Director

This package is built to make it easy to write and to test background goroutines. These are the kinds of goroutines that are meant to have a reasonably long lifespan built around a central loop. This is often a for {} loop with no conditions.

The interface allows routines to be dispatched and run for a set number of iterations, or indefinitely. You can also signal them to quit, and block waiting for them to complete.

This is what it looks like:

func RunForever(looper Looper) error {
	looper.Loop(func() error {
		... do some work ...
		if err != nil {
			return err
		}
	})
}

looper := NewFreeLooper(FOREVER, make(chan error))
go RunForever(looper)

err := looper.Wait()
if err != nil {
	... handle it ...
}

The Problem This Solves

Testing functions that are meant to run in background goroutines can be tricky. You might have to wait for a period of time to let it run on a timed loop. You might have to trigger the function from inside the test rather than from where it would naturally run. Then there is the problem of stopping it between tests if you need to reset state. Or knowing that it completed. These are all solvable. Depending on your scenario it might not even be complex. But wouldn't it be nice if you could rely on doing this the same way everywhere? That's what this library attempts to do.

Example Usage

The core interface for the package is the Looper. Two Looper implementations are currently included, a TimedLooper whichs runs the loop on a specified interval, and a FreeLooper which runs the loop as quickly as possible. You get some nice fucntions to control how they behave. Let's take a look at how you might use it.

The Looper interface looks like this:

type Looper interface {
	// To be called when we want to run the inner loop. Used by the
	// dependant code.
	Loop(fn func() error)
	// Called by dependant routine. Block waiting for the loop to end
	Wait() error
	// Signal that the routine is done. Generally used internally
	Done(err error)
	// Externally signal the long-lived goroutine to complete work
	Quit()
}

Here's an example goroutine that could benefit from a FreeLooper:

func RunForever() error {
	for {
		... do some work ...
		if err != nil {
			return err
		}
	}
}

go RunForever()

This works but it kind of stinks, because we can't easily test the code with go test and we can't capture our error. If we start this up, it will never exit, which is what we want it to do in our production code. But we want it to stop after running in test code. To do that, we need to have a way to get the code to quit after iterating. So we can do something like this:

func RunForever(quit chan bool) error {
	for {
		... do some work ...
		if err != nil {
			return err
		}

		select {
		case <-quit:
			return nil
		}
	}
}

quit := make(chan bool, 1)
quit <-true
go RunForever(quit)

Now we can tell it to quit when we want to. We probably wanted that to begin with, so that the main program can tell the goroutine to end. But it also now means we can test it by using a buffered channel, putting a message into the channel, then running the test.

But what about when we want to run it more than once in a pass? Or when we want to have our code wait on its completion somewhere during execution? These are all common patterns and require boilerplate code. If you do that once in your program, fine. But it's often the case that this proliferates all over the code. Particularly for applications which are doing more than one thing in the background. Instead we could use a FreeLooper like this:

func RunForever(looper Looper) error {
	looper.Loop(func() error {
		... do some work ...
		if err != nil {
			return err
		}
	})
}

looper := NewFreeLooper(FOREVER, make(chan error))
go RunForever(looper)

err := looper.Wait()
if err != nil {
	... handle it ...
}

That will run the loop once, and wait for it to complete, handling the resulting error.

Or we, can tell it to run forever, and then stop it when we want to:

looper := NewFreeLooper(FOREVER, make(chan error))
go RunForever(looper)

... do more work ...

looper.Quit()
err := looper.Wait()
if err != nil {
	... handle it ...
}

And if on a later basis we want this to run as a timed loop, such as one iteration per 5 seconds, we can just substitute a TimedLooper:

looper := NewTimedLooper(1, 5 * time.Second, make(chan error))
go RunForever(looper)

Lastly, a timed looper will only execute once the tick interval has been met. To immediately execute the first iteration of the loop, you can instantiate an immediate timed looper via the NewImmediateTimedLooper function.

Or in other words:

looper := NewImmediateTimedLooper(10, 5 * time.Second, make(chan error))
go looper.Loop(func() error { fmt.Println("Immediate execution"); return nil })
time.Sleep(10 * time.Second)

// STDOUT: "Immediate execution" output as soon as looper.Loop() is ran.

Documentation

Overview

The package defines a single interface and a few implementations of the interface. The externalization of the loop flow control makes it easy to test the internal functions of background goroutines by, for instance, only running the loop once while under test.

The design is that any errors which need to be returned from the loop will be passed back on a channel whose implementation is left up to the individual Looper. Calling methods can wait on execution and for any resulting errors by calling the Wait() method on the Looper.

Example

In this example, we are going to run a FreeLooper with 5 iterations. In the course of running, an error is generated, which the parent function captures and outputs. As a result of the error only 3 of the 5 iterations are completed and the output reflects this.

looper := NewFreeLooper(5, make(chan error))

runner := func(looper Looper) {
	x := 0
	looper.Loop(func() error {
		fmt.Println(x)
		x++
		if x == 3 {
			return errors.New("Uh oh")
		}
		return nil
	})
}

go runner(looper)
err := looper.Wait()

if err != nil {
	fmt.Printf("I got an error: %s\n", err.Error())
}
Output:

0
1
2
I got an error: Uh oh

Index

Examples

Constants

View Source
const (
	FOREVER = -1
	ONCE    = 1
)

Variables

This section is empty.

Functions

This section is empty.

Types

type FreeLooper

type FreeLooper struct {
	Count    int
	DoneChan chan error
	// contains filtered or unexported fields
}

A FreeLooper is like a TimedLooper but doesn't wait between iterations.

func NewFreeLooper

func NewFreeLooper(count int, done chan error) *FreeLooper

func (*FreeLooper) Done

func (l *FreeLooper) Done(err error)

This is used internally, but can also be used by controlling routines to signal that a job is completed. The FreeLooper doesn's support its use outside the internals.

func (*FreeLooper) Loop

func (l *FreeLooper) Loop(fn func() error)

func (*FreeLooper) Quit

func (l *FreeLooper) Quit()

Quit() signals to the Looper to not run the next iteration and to call Done() and return as quickly as possible. It is does not intervene between iterations. It is a non-blocking operation.

func (*FreeLooper) Wait

func (l *FreeLooper) Wait() error

func (*FreeLooper) WaitWithoutError

func (l *FreeLooper) WaitWithoutError()

type Looper

type Looper interface {
	Loop(fn func() error)
	Wait() error
	WaitWithoutError()
	Done(err error)
	Quit()
}

A Looper is used in place of a direct call to "for {}" and implements some controls over how the loop will be run. The Loop() function is the main call used by dependant routines. Common patterns like Quit and Done channels are easily implemented in a Looper.

type TimedLooper

type TimedLooper struct {
	Count    int
	Interval time.Duration
	DoneChan chan error

	Immediate bool
	// contains filtered or unexported fields
}

A TimedLooper is a Looper that runs on a timed schedule, using a Timer underneath. It also implements Quit and Done channels to allow external routines to more easily control and synchronize the loop.

If you pass in a DoneChan at creation time, it will send a nil on the channel when the loop has completed successfully or an error if the loop resulted in an error condition.

Example

In this example, we run a really fast TimedLooper for a fixed number of runs.

looper := NewTimedLooper(5, 1*time.Nanosecond, make(chan error))

runner := func(looper Looper) {
	x := 0
	looper.Loop(func() error {
		fmt.Println(x)
		x++
		return nil
	})
}

go runner(looper)
err := looper.Wait()
if err != nil {
	fmt.Printf("I got an error: %s\n", err.Error())
}
Output:

0
1
2
3
4

func NewImmediateTimedLooper

func NewImmediateTimedLooper(count int, interval time.Duration, done chan error) *TimedLooper

Same as a TimedLooper, except it will execute an iteration of the loop immediately after calling on Loop() (as opposed to waiting until the tick)

func NewTimedLooper

func NewTimedLooper(count int, interval time.Duration, done chan error) *TimedLooper

func (*TimedLooper) Done

func (l *TimedLooper) Done(err error)

Signal a dependant routine that we're done with our work

func (*TimedLooper) Loop

func (l *TimedLooper) Loop(fn func() error)

The main method of the Looper. This call takes a function with a single return value, an error. If the error is nil, the Looper will run the next iteration. If it's an error, it will not run the next iteration, will clean up any internals that need to be, and will invoke done().

func (*TimedLooper) Quit

func (l *TimedLooper) Quit()

Quit() signals to the Looper to not run the next iteration and to call done() and return as quickly as possible. It is does not intervene between iterations.

Example

In this example we run a really fast TimedLooper for a fixed number of runs, but we interrupt it with a Quit() call so it only completes one run.

looper := NewTimedLooper(5, 50*time.Millisecond, make(chan error))

runner := func(looper Looper) {
	x := 0
	looper.Loop(func() error {
		fmt.Println(x)
		x++
		return nil
	})
}

go runner(looper)
// Wait for one run to complete
time.Sleep(90 * time.Millisecond)
looper.Quit()
err := looper.Wait()
if err != nil {
	fmt.Printf("I got an error: %s\n", err.Error())
}
Output:

0

func (*TimedLooper) Wait

func (l *TimedLooper) Wait() error

func (*TimedLooper) WaitWithoutError

func (l *TimedLooper) WaitWithoutError()

WaitWithoutError, unlike Wait(), can be waited on by multiple goroutine safely. It does not, however, return the last error.

Jump to

Keyboard shortcuts

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