appetizer

package module
v0.0.0-...-8aa6e6d Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2024 License: MIT Imports: 11 Imported by: 0

README

appetizer

TL;DR

Appetizer is simple yet effective way to create applications with background services.

Why?

My requirements have been always simple: I need a tool to run multiple instances of something in background, while being able to restart them if needed or to fail fast otherwise.

Features

  • Declarative approach to define your application
  • Consistent logging for each service, by injecting zerolog.Logger instance on service initialization
  • Any service could be configured as restartable thanks to awesome cenkalti/backoff library.
  • Integrated HTTP servicer with pprof

Examples

Simple time printer
package main

import (
    "context"
    "errors"
    "time"

    "github.com/homier/appetizer"
    "github.com/homier/appetizer/log"
)

type TimePrinter struct {
    TickDuration time.Duration

    log log.Logger
}

func (tp *TimePrinter) Init(log log.Logger) error {
    tp.log = log

    if tp.TickDuration == time.Duration(0) {
        return errors.New("TickDuration must be defined")
    }

    return nil
}

func (tp *TimePrinter) Run(ctx context.Context) error {
    tp.log.Msg("Started")
    defer tp.log.Msg("Stopped")

    ticker := time.NewTicker(tp.TickDuration)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return nil
        case tick := <-ticker.C:
            tp.log.Info().Msgf("Ticked at: %s", tick.Format(time.RFC822Z))
        }
    }
}

func main() {
    app := &appetizer.App{
        Name: "SimpleApplication"
        Services: []appetizer.Service{{
            Name: "TimePrinter",
            Servicer: &TimePrinter{TickDuration: time.Second},
        }},
    }

    ctx, cancel := appetizer.NotifyContext()
    defer cancel()

    if err := app.Run(ctx); err != nil {
        panic(err)
    }
}
HTTP servicer
package main

import (
	"io"
	"net/http"

	"github.com/homier/appetizer"
	"github.com/homier/appetizer/services"
)

func main() {
	app := appetizer.App{
		Name:  "WebServer",
		Debug: true,
		Services: []appetizer.Service{{
			Name: "httpServer",
			Servicer: &services.HTTPServer{
				Handlers: []services.Handler{{
					Path: "GET /hello",
					Handler: func(w http.ResponseWriter, _ *http.Request) {
						if _, err := io.WriteString(w, "world\n"); err != nil {
							panic(err)
						}
					},
				}},
				PprofEnabled: true,
			},
		}},
	}

	ctx, cancel := appetizer.NotifyContext()
	defer cancel()

	runCh := app.RunCh(ctx)

	<-app.WaitCh()

	select {
	case <-ctx.Done():
		return
	case err := <-runCh:
		if err != nil {
			app.Log().Fatal().Err(err).Msg("App crashed")
		}
	}
}

Documentation

Overview

Appetizer is simple yet effective way to create applications with background services. See README.md for more information.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrStarted = errors.New("application is already started")
)
View Source
var SignalsDefault = []os.Signal{
	syscall.SIGINT,
	syscall.SIGTERM,
}

A list of default os signals for NotifyContext.

Functions

func NotifyContext

func NotifyContext(signals ...os.Signal) (context.Context, context.CancelFunc)

Returns a `context.Context` with its cancel function, that will be cancelled on catching specified signals. If no signals are provided, the `SignalsDefault` will be used.

Types

type App

type App struct {
	// Application name. This name will be used as the "app" field value in logger
	Name string

	// A list of services to run. If empty, app will exit immediately.
	Services []Service

	// Configure app to run in debug mode. Will set logger level to `zerolog.DebugLevel`.
	Debug bool
	// contains filtered or unexported fields
}
Example
package main

import (
	"context"
	"fmt"
	"sort"

	"github.com/pkg/errors"

	"github.com/homier/appetizer"
	"github.com/homier/appetizer/log"
)

type QueueFiller struct {
	Queue  chan<- int
	logger log.Logger
}

var _ appetizer.Servicer = (*QueueFiller)(nil)

// Init implements appetizer.Servicer.
func (q *QueueFiller) Init(log log.Logger) error {
	q.logger = log

	if q.Queue == nil {
		return errors.New("queue has not been provided")
	}

	return nil
}

// Run implements appetizer.Servicer.
func (q *QueueFiller) Run(ctx context.Context) error {
	q.logger.Info().Msg("Started")

	for i := range 10 {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			q.Queue <- i
		}
	}

	q.logger.Info().Msg("Finished")
	close(q.Queue)
	return nil
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	queue := make(chan int, 10)
	app := &appetizer.App{
		Name: "simple",
		Services: []appetizer.Service{
			{
				Name: "Queue filler",
				Servicer: &QueueFiller{
					Queue: queue,
				},
			},
		},
	}

	doneCh := make(chan struct{}, 1)
	go func() {
		defer close(doneCh)

		numbers := make([]int, 0, 10)
		for i := range queue {
			numbers = append(numbers, i)
		}

		sort.Slice(numbers, func(i, j int) bool {
			return numbers[i] < numbers[j]
		})

		fmt.Println(numbers)
	}()

	select {
	case err := <-app.RunCh(ctx):
		if err != nil {
			panic(err)
		}
	case <-ctx.Done():
	}

	<-doneCh
}
Output:

[0 1 2 3 4 5 6 7 8 9]

func (*App) Log

func (a *App) Log() *log.Logger

Returns a copy of the application logger.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run application, blocking until error or nil is returned.

func (*App) RunCh

func (a *App) RunCh(ctx context.Context) <-chan error

Run application in background, returning an error channel. Application is considered stopped when that channel is closed or has an error within.

func (*App) Wait

func (a *App) Wait(ctx context.Context) error

Blocks until the app is started, or the provided context is either cancelled or timed out. If context is cancelled/timed out, a context error is returning, otherwise no error is returning.

func (*App) WaitCh

func (a *App) WaitCh() <-chan struct{}

Returns a channel, that will be closed when application is started.

type Service

type Service struct {
	// Service name. This name will be used as the "service" field value in logger.
	Name string

	// Servicer value. Actual logic for the service.
	Servicer Servicer

	// Whether to restart failed service or not.
	RestartEnabled bool

	// If `RestartEnabled` is `true`, this must be defined to describe
	// a restart policy you need.
	// NOTE: `RestartOpts.Opts` must be defined, otherwise service won't be restarted.
	RestartOpts retry.Opts
}

Service descriptor. Here you describe your service and specify the target `Servicer`.

type Servicer

type Servicer interface {
	// An initial stage for every service lifecycle.
	// It could be possible called more than once, so
	// you need to decide by yourself, whether you'll support this or not.
	Init(log log.Logger) error

	// Run your logic here.
	// If this method returns `nil`, a service is considered stopped,
	// it won't be restarted event if the `Service.RestartEnabled` is true.
	// If this method returns some kind of error, a service is considered failed,
	// and it'll be restarted depending on the `Service.RestartEnabled` and
	// `Service.RestartOpts` policy.
	Run(ctx context.Context) error
}

Service logic. No explicit `Stop` method is required, so if you want to gracefully stop your service, consider handling context cancellation.

type Waiter

type Waiter struct {
	// contains filtered or unexported fields
}

Waiter is a special struct that provides a group of methods to wait the internal condition to become true and to manage that condition.

func (*Waiter) Is

func (w *Waiter) Is(ready bool) bool

Checks that the internal condition is equal to the provided boolean value.

func (*Waiter) Set

func (w *Waiter) Set(ready bool)

Sets the internal condition to the provided ready value. If the provided value is true, all of the wait channels will be closed, signalling the readiness of the waiter.

func (*Waiter) Wait

func (w *Waiter) Wait(ctx context.Context) error

Blocks until the internal condition is set to true, or the provided context is either cancelled or timed out. If context is cancelled/timed out, a context error is returning, otherwise no error is returning.

func (*Waiter) WaitCh

func (w *Waiter) WaitCh() <-chan struct{}

Returns a channel that will be closed when the internal condition is set to true.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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