mvr

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Mar 12, 2019 License: BSD-3-Clause Imports: 8 Imported by: 0

README

Minimal Viable Runtime (MVR)

GoDoc Go report

Motivation

When starting a new project there always is a certain amount of low-level code that has to be written in order to provide for some basic runtime functionality, like top-level context, signal handlers, etc. Often this kind of code is either written from scratch, or brought in with an external library. Programming the same functionality from scratch tends to be tedious and error-prone, while external libraries may sometimes be just too heavy for the intended use, introduce significant overhead, or impose an uncomfortable programming model. This project is an attempt to bring a number of frequently used runtime functions into one place without introducing another fat API or adding many external dependencies.

The package adds the following functionality:

  • Top-level context with signal handlers to cancel the context when a signal is delivered;
  • Graceful shutdown to make sure all goroutines have completed before the application terminates;
  • A simple way of running a bunch of tasks on a pool of goroutines;
  • Asynchronous logging, where the actual writing to the log file is done in the background to make sure performance-critical code is not exposed to the i/o latency of writing to the log.

Application entry point

The application entry point should be a function of type func() int, returning an integer error code that will be passed down to os.Exit() when the application terminates. Typically, the entry point is invoked like:

func main() {
	mvr.Run(appMain)
}

func appMain() int { ... }

The mvr.Run() function never returns.

Top-level context

The top-level context gets initialised (along with the rest of the package) when the application invokes mvr.Run() function. The context is accessible via mvr.Context() function, with the shortcuts mvr.Done() and mvr.Err() both giving access to the corresponding methods of the top context. The context is cancelled when any of SIGHUP, SIGINT, or SIGTERM is delivered, or when mvr.Cancel() function is called. A termination handler can be implemented either as a goroutine waiting on mvr.Done() channel, or via the provided convenience function mvr.OnCancel(), for example:

srv := &http.Server{ ... }

// termination handler
mvr.OnCancel(10 * time.Second, func(ctx context.Context) {
	if err := srv.Shutdown(ctx); err != nil {
		log.Println(err)
	}
})

// serve
return srv.ListenAndServe()

Goroutine invocation

In order to ensure graceful shutdown the package keeps track of all goroutines invoked (directly or indirectly) through its API. The simplest way to start a goroutine is mvr.Go() function that provides functionality similar to the go keyword. Another way of running a function in a separate goroutine is mvr.Async(), which takes a function to launch, of type func() error, and returns a channel to which the error (if any) will be delivered upon the function completion. Typical usage scenario:

// start a function
errch := mvr.Async(func() error {
	// ...
	return err
})

// do other things here...

// wait for completion and check the error
if err := <-errch; err != nil {
	// handle the error
}

// another option: simply wait for completion and return (aka Await)
return <-errch

Goroutine pool

As simple example of executing tasks on a pool of goroutines consider the case where a number of files need to be compressed in parallel:

// define a function that compresses one file
func compressFile(name string) error { ... }

// a list of files to compress (fixed list for this example)
files := []string{"aaa.json", "bbb.json", "ccc.json", "ddd.json"}

// start parallel compression using 2 goroutines
errch, cancel := mvr.Parallel(2, mvr.ForEachString(files, compressFile))

defer cancel()	// to clean the associated resources afterwards

// do other things...

// retrieve errors (the error channel is closed when the processing is done)
for err := range errch {
	// process the error
}

// another option: wait to get the first error (if any) and stop further processing.
// if there is no error, then the pool runs to completion and the channel gets closed, returning nil
return <-errch

The second parameter to mvr.Parallel() is a channel of tasks, so in a more advanced scenario there may be a separate goroutine continuously supplying tasks to the pool, like in the following example adapted from mvr_test.go:

func TestParallelFeed(t *testing.T) {
	const N = 10	// number of tasks

	var res int32

	// input task channel (in practice should probably have some non-zero size)
	inch := make(chan func() error)

	// start feeder
	mvr.Go(func() {
		defer close(inch) // don't forget this!

		for i := 0; i < N; i++ {
			inch <- func() error {
				atomic.AddInt32(&res, 1) // just for this example
				return nil
			}
		}
	})

	// launch tasks
	errs, cancel := mvr.Parallel(0, inch) // pool of runtime.NumCPU() goroutines

	defer cancel()

	// check errors
	for err := range errs {
		t.Error(err)
		return
	}

	// etc.
}

There is another function, mvr.ParallelCtx(), that takes a context.Context as its first parameter to allow for a user-managed context to control the goroutine pool.

Logging

The package does not replace the logger from the standard library, and it provides no additional API. Instead, the library replaces the target io.Writer to which the logger writes. This should have no effect on any other logging layer built on top of the standard log package. The log writes to os.Stderr.

Testing

For unit-testing of an application utilising this package the correct initialisation of the runtime can be ensured by defining TestMain function from which all the tests are invoked, typically:

func TestMain(m *testing.M) {
	mvr.Run(m.Run)
}

Limitations

  • The package has no way of intercepting calls to terminating functions like log.Fatal() or os.Exit(), and no guarantees can be given if any of those functions is invoked.
  • The package replaces the io.Writer used by the standard logger, so the writer should not be replaced again by the application;
  • Only goroutines started via the package API are waited on before termination;
  • The package does not handle panics, although certain effort has been made to make sure resources are released when a panic is triggered.
Project status

Tested on Linux Mint 18.3 using Go version 1.10.3.

Documentation

Overview

Package mvr is a minimal viable runtime that provides a top-level context cancelled by Unix signals, graceful shutdown by waiting for all registered goroutines to terminate before exitting the application, simple goroutine pool, and asynchronous logger.

Index

Constants

View Source
const DefaultCancellationTimeout = 10 * time.Second

DefaultCancellationTimeout is the default time given to a cancellation function to complete.

Variables

This section is empty.

Functions

func Async

func Async(fn func() error) <-chan error

Async starts the given function in a separate goroutine, and returns a channel where the returned error will be placed when the given function completes.

func Cancel

func Cancel()

Cancel cancels the top-level context.

func Context

func Context() context.Context

Context returns the top-level context.

func Done

func Done() <-chan struct{}

Done returns the top-level context's cancellation channel.

func Err

func Err() error

Err returns the top-level context error.

func ForEachString

func ForEachString(args []string, fn func(string) error) (jobs chan func() error)

ForEachString creates a channel of jobs, where each job is an application of the given function to a string from the given list. The result is suitable for Parallel() function.

func Go

func Go(fn func())

Go starts the given function in a new goroutine registered with the runtime.

func OnCancel

func OnCancel(timeout time.Duration, fn func(context.Context))

OnCancel registers a function to be called when the top-level context gets cancelled. The timeout parameter specifies the time for the function to complete its task; if set to 0 the timeout will be DefaultCancellationTimeout. The supplied function will be called with a context that is cancelled when the timeout expires.

func Parallel

func Parallel(numThreads int, jobs <-chan func() error) (<-chan error, context.CancelFunc)

Parallel invokes job functions from the given queue, sending all non-nil errors to the returned channel. The error channel gets closed when all the jobs from the input queue are either complete or cancelled. The returned cancellation function should always be called at some point to free the associated resources.

func ParallelCtx

func ParallelCtx(parent context.Context, numThreads int, jobs <-chan func() error) (<-chan error, context.CancelFunc)

ParallelCtx invokes job functions from the given queue, sending all non-nil errors to the returned channel. The error channel gets closed when all the jobs from the input queue are either complete or cancelled. The returned cancellation function should always be called at some point to free the associated resources. The function also terminates when the given context is cancelled.

func Run

func Run(main func() int)

Run takes the application entry point function as a parameter, and executes it in a separate goroutine. The return value of the main function will be passed over to os.Exit(). The main function cancels the top-level context upon exit. The Run() function itself never returns.

Types

This section is empty.

Jump to

Keyboard shortcuts

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