parent
e741e79acf
commit
e91ef01857
@ -1,130 +1,151 @@ |
||||
// Package m is the glue which holds all the other packages in this project
|
||||
// together. While other packages in this project are intended to be able to be
|
||||
// used separately and largely independently, this package combines them in ways
|
||||
// which I specifically like.
|
||||
// Package m implements functionality specific to how I like my programs to
|
||||
// work. It acts as glue between many of the other packages in this framework,
|
||||
// putting them together in the way I find most useful.
|
||||
package m |
||||
|
||||
import ( |
||||
"context" |
||||
"os" |
||||
"os/signal" |
||||
"time" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mcfg" |
||||
"github.com/mediocregopher/mediocre-go-lib/mcmp" |
||||
"github.com/mediocregopher/mediocre-go-lib/mctx" |
||||
"github.com/mediocregopher/mediocre-go-lib/merr" |
||||
"github.com/mediocregopher/mediocre-go-lib/mlog" |
||||
"github.com/mediocregopher/mediocre-go-lib/mrun" |
||||
) |
||||
|
||||
type ctxKey int |
||||
type cmpKey int |
||||
|
||||
const ( |
||||
ctxKeyCfgSrc ctxKey = iota |
||||
ctxKeyInfoLog |
||||
cmpKeyCfgSrc cmpKey = iota |
||||
cmpKeyInfoLog |
||||
) |
||||
|
||||
func debugLog(msg string, ctxs ...context.Context) { |
||||
fn := mlog.Debug |
||||
func debugLog(cmp *mcmp.Component, msg string, ctxs ...context.Context) { |
||||
level := mlog.DebugLevel |
||||
if len(ctxs) > 0 { |
||||
if ok, _ := ctxs[0].Value(ctxKeyInfoLog).(bool); ok { |
||||
fn = mlog.Info |
||||
if ok, _ := ctxs[0].Value(cmpKeyInfoLog).(bool); ok { |
||||
level = mlog.InfoLevel |
||||
} |
||||
} |
||||
fn(msg, ctxs...) |
||||
|
||||
mlog.From(cmp).Log(mlog.Message{ |
||||
Level: level, |
||||
Description: msg, |
||||
Contexts: ctxs, |
||||
}) |
||||
} |
||||
|
||||
// ProcessContext returns a Context which should be used as the root Context
|
||||
// when implementing most processes.
|
||||
// RootComponent returns a Component which should be used as the root Component
|
||||
// when implementing most programs.
|
||||
//
|
||||
// The returned Context will automatically handle setting up global
|
||||
// configuration parameters like "log-level", as well as an http endpoint where
|
||||
// debug information about the running process can be accessed.
|
||||
func ProcessContext() context.Context { |
||||
ctx := context.Background() |
||||
// The returned Component will automatically handle setting up global
|
||||
// configuration parameters like "log-level", as well as parsing those
|
||||
// and all other parameters when the Init even is triggered on it.
|
||||
func RootComponent() *mcmp.Component { |
||||
cmp := new(mcmp.Component) |
||||
|
||||
// embed confuration source which should be used into the context.
|
||||
ctx = context.WithValue(ctx, ctxKeyCfgSrc, mcfg.Source(new(mcfg.SourceCLI))) |
||||
cmp.SetValue(cmpKeyCfgSrc, mcfg.Source(new(mcfg.SourceCLI))) |
||||
|
||||
// set up log level handling
|
||||
logger := mlog.NewLogger() |
||||
ctx = mlog.WithLogger(ctx, logger) |
||||
ctx, logLevelStr := mcfg.WithString(ctx, "log-level", "info", "Maximum log level which will be printed.") |
||||
ctx = mrun.WithStartHook(ctx, func(context.Context) error { |
||||
mlog.SetLogger(cmp, logger) |
||||
|
||||
// set up parameter parsing
|
||||
mrun.InitHook(cmp, func(context.Context) error { |
||||
src, _ := cmp.Value(cmpKeyCfgSrc).(mcfg.Source) |
||||
if src == nil { |
||||
return merr.New("Component not sourced from m package", cmp.Context()) |
||||
} else if err := mcfg.Populate(cmp, src); err != nil { |
||||
return merr.Wrap(err, cmp.Context()) |
||||
} |
||||
return nil |
||||
}) |
||||
|
||||
logLevelStr := mcfg.String(cmp, "log-level", |
||||
mcfg.ParamDefault("info"), |
||||
mcfg.ParamUsage("Maximum log level which will be printed.")) |
||||
mrun.InitHook(cmp, func(context.Context) error { |
||||
logLevel := mlog.LevelFromString(*logLevelStr) |
||||
if logLevel == nil { |
||||
ctx := mctx.Annotate(ctx, "log-level", *logLevelStr) |
||||
return merr.New("invalid log level", ctx) |
||||
return merr.New("invalid log level", cmp.Context(), |
||||
mctx.Annotated("log-level", *logLevelStr)) |
||||
} |
||||
logger.SetMaxLevel(logLevel) |
||||
return nil |
||||
}) |
||||
|
||||
return ctx |
||||
return cmp |
||||
} |
||||
|
||||
// ServiceContext extends ProcessContext so that it better supports long running
|
||||
// processes which are expected to handle requests from outside clients.
|
||||
// RootServiceComponent extends RootComponent so that it better supports long
|
||||
// running processes which are expected to handle requests from outside clients.
|
||||
//
|
||||
// Additional behavior it adds includes setting up an http endpoint where debug
|
||||
// information about the running process can be accessed.
|
||||
func ServiceContext() context.Context { |
||||
ctx := ProcessContext() |
||||
func RootServiceComponent() *mcmp.Component { |
||||
cmp := RootComponent() |
||||
|
||||
// services expect to use many different configuration sources
|
||||
ctx = context.WithValue(ctx, ctxKeyCfgSrc, mcfg.Source(mcfg.Sources{ |
||||
cmp.SetValue(cmpKeyCfgSrc, mcfg.Source(mcfg.Sources{ |
||||
new(mcfg.SourceEnv), |
||||
new(mcfg.SourceCLI), |
||||
})) |
||||
|
||||
// it's useful to show debug entries (from this package specifically) as
|
||||
// info logs for long-running services.
|
||||
ctx = context.WithValue(ctx, ctxKeyInfoLog, true) |
||||
cmp.SetValue(cmpKeyInfoLog, true) |
||||
|
||||
// TODO set up the debug endpoint.
|
||||
return ctx |
||||
return cmp |
||||
} |
||||
|
||||
// Start performs the work of populating configuration parameters and triggering
|
||||
// the start event. It will return once the Start event has completed running.
|
||||
//
|
||||
// This function returns a Context because there are cases where the Context
|
||||
// will be modified during Start, such as if WithSubCommand was used. If the
|
||||
// Context was not modified then the passed in Context will be returned.
|
||||
func Start(ctx context.Context) context.Context { |
||||
src, _ := ctx.Value(ctxKeyCfgSrc).(mcfg.Source) |
||||
if src == nil { |
||||
mlog.Fatal("ctx not sourced from m package", ctx) |
||||
} |
||||
// MustInit will call mrun.Init on the given Component, which must have been
|
||||
// created in this package, and exit the process if mrun.Init does not complete
|
||||
// successfully.
|
||||
func MustInit(cmp *mcmp.Component) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
||||
defer cancel() |
||||
|
||||
// no logging should happen before populate, primarily because log-level
|
||||
// hasn't been populated yet, but also because it makes help output on cli
|
||||
// look weird.
|
||||
ctx, err := mcfg.Populate(ctx, src) |
||||
if err != nil { |
||||
mlog.Fatal("error populating configuration", ctx, merr.Context(err)) |
||||
} else if err := mrun.Start(ctx); err != nil { |
||||
mlog.Fatal("error triggering start event", ctx, merr.Context(err)) |
||||
debugLog(cmp, "initializing") |
||||
if err := mrun.Init(ctx, cmp); err != nil { |
||||
mlog.From(cmp).Fatal("initialization failed", merr.Context(err)) |
||||
} |
||||
debugLog("start hooks completed", ctx) |
||||
return ctx |
||||
debugLog(cmp, "initialization completed successfully") |
||||
} |
||||
|
||||
// StartWaitStop performs the work of populating configuration parameters,
|
||||
// triggering the start event, waiting for an interrupt, and then triggering the
|
||||
// stop event. Run will block until the stop event is done. If any errors are
|
||||
// encountered a fatal is thrown.
|
||||
func StartWaitStop(ctx context.Context) { |
||||
ctx = Start(ctx) |
||||
// MustShutdown is like MustInit, except that it triggers the Shutdown event on
|
||||
// the Component.
|
||||
func MustShutdown(cmp *mcmp.Component) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
||||
defer cancel() |
||||
|
||||
debugLog(cmp, "shutting down") |
||||
if err := mrun.Shutdown(ctx, cmp); err != nil { |
||||
mlog.From(cmp).Fatal("shutdown failed", merr.Context(err)) |
||||
} |
||||
debugLog(cmp, "shutting down completed successfully") |
||||
} |
||||
|
||||
// Exec calls MustInit on the given Component, then blocks until an interrupt
|
||||
// signal is received, then calls MustShutdown on the Component, until finally
|
||||
// exiting the process.
|
||||
func Exec(cmp *mcmp.Component) { |
||||
MustInit(cmp) |
||||
{ |
||||
ch := make(chan os.Signal, 1) |
||||
signal.Notify(ch, os.Interrupt) |
||||
s := <-ch |
||||
debugLog("signal received, stopping", mctx.Annotate(ctx, "signal", s)) |
||||
debugLog(cmp, "signal received, stopping", mctx.Annotated("signal", s)) |
||||
} |
||||
MustShutdown(cmp) |
||||
|
||||
if err := mrun.Stop(ctx); err != nil { |
||||
mlog.Fatal("error triggering stop event", ctx, merr.Context(err)) |
||||
} |
||||
debugLog("exiting process", ctx) |
||||
debugLog(cmp, "exiting process") |
||||
os.Stdout.Sync() |
||||
os.Stderr.Sync() |
||||
os.Exit(0) |
||||
} |
||||
|
Loading…
Reference in new issue