Skip to content

Commit

Permalink
Baseplate package 1.0
Browse files Browse the repository at this point in the history
Overhaul the top level `baseplate` package and disperse some of its functionality into different packages.

- The `Config` formats for a particular package along with the `Init` methods that go along with those `Config`s are now in each package rather than being done manually in the `baseplate` package.
- Add a `baseplate.Baseplate` object that in responsible for initializing the monitoring/logging frameworks and forms the base of a `baseplate.Server`.
- Add a `baseplate.Server` implementation to `httpbp`.
- Add a `baseplate.Server` implementation to `thriftbp`.
- Add `baseplate.Serve` to manage starting and shutting down a `baseplate.Server` using `runtimebp.HandleShutdown`.  Accepts a `StopTimeout` configuration value to control the amount of time you wait for a `Server` to stop before exiting.
  • Loading branch information
pacejackson committed May 5, 2020
1 parent b6f22e7 commit 279f081
Show file tree
Hide file tree
Showing 31 changed files with 1,572 additions and 588 deletions.
15 changes: 9 additions & 6 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ gazelle(name = "gazelle")
go_library(
name = "go_default_library",
srcs = [
"baseplate.go",
"doc.go",
"server.go",
"thrift.go",
],
importpath = "github.com/reddit/baseplate.go",
visibility = ["//visibility:public"],
Expand All @@ -19,17 +18,21 @@ go_library(
"//edgecontext:go_default_library",
"//log:go_default_library",
"//metricsbp:go_default_library",
"//runtimebp:go_default_library",
"//secrets:go_default_library",
"//thriftbp:go_default_library",
"//tracing:go_default_library",
"@com_github_apache_thrift//lib/go/thrift:go_default_library",
"@in_gopkg_yaml_v2//:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["server_test.go"],
srcs = ["baseplate_test.go"],
embed = [":go_default_library"],
deps = ["//log:go_default_library"],
deps = [
"//log:go_default_library",
"//metricsbp:go_default_library",
"//secrets:go_default_library",
"//tracing:go_default_library",
],
)
257 changes: 257 additions & 0 deletions baseplate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package baseplate

import (
"context"
"errors"
"fmt"
"io"
"os"
"time"

yaml "gopkg.in/yaml.v2"

"github.com/reddit/baseplate.go/batcherror"
"github.com/reddit/baseplate.go/edgecontext"
"github.com/reddit/baseplate.go/log"
"github.com/reddit/baseplate.go/metricsbp"
"github.com/reddit/baseplate.go/runtimebp"
"github.com/reddit/baseplate.go/secrets"
"github.com/reddit/baseplate.go/tracing"
)

// Config is a general purpose config for assembling a Baseplate server
type Config struct {
// Addr is the local address to run your server on.
//
// It should be in the format "${IP}:${Port}", "localhost:${Port}",
// or simply ":${Port}".
Addr string `yaml:"addr"`

// Timeout is the socket connection timeout for Servers that
// support that.
Timeout time.Duration `yaml:"timeout"`

// StopTimeout is the timeout for the Stop command for the service.
//
// If this is not set, then no timeout will be set on the Stop command.
StopTimeout time.Duration `yaml:"stopTimeout"`

Log log.Config `yaml:"log"`
Metrics metricsbp.Config `yaml:"metrics"`
Secrets secrets.Config `yaml:"secrets"`
Sentry log.SentryConfig `yaml:"setry"`
Tracing tracing.Config `yaml:"tracing"`
}

// Baseplate is the general purpose object that you build a Server on.
type Baseplate interface {
io.Closer

Config() Config
EdgeContextImpl() *edgecontext.Impl
Secrets() *secrets.Store
}

// Server is the primary interface for baseplate servers.
type Server interface {
// Close should stop the server gracefully and only return after the server has
// finished shutting down.
//
// It is recommended that you use baseplate.Serve() rather than calling Close
// directly as baseplate.Serve will manage starting your service as well as
// shutting it down gracefully in response to a shutdown signal.
io.Closer

// Baseplate returns the Baseplate object the server is built on.
Baseplate() Baseplate

// Serve should start the Server on the Addr given by the Config and only
// return once the Server has stopped.
//
// It is recommended that you use baseplate.Serve() rather than calling Serve
// directly as baseplate.Serve will manage starting your service as well as
// shutting it down gracefully.
Serve() error
}

// Serve runs the given Server until it is given an external shutdown signal
// using runtimebp.HandleShutdown to handle the signal and shut down the
// server gracefully. Returns the (possibly nil) error returned by "Close" or
// context.DeadlineExceeded if it times out.
//
// If a StopTimeout is configure, Serve will wait for that duration for the
// server to stop before timing out and returning to force a shutdown.
//
// This is the recommended way to run a Baseplate Server rather than calling
// server.Start/Stop directly.
func Serve(ctx context.Context, server Server) error {
shutdownChannel := make(chan error)
go runtimebp.HandleShutdown(
ctx,
func(signal os.Signal) {
timeout := server.Baseplate().Config().StopTimeout
ctx := context.Background()
if timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}

var err error
closeChannel := make(chan error)
go func() {
closeChannel <- server.Close()
}()

// Wait for either the context to timeout or Stop() to finish.
select {
case <-ctx.Done():
err = ctx.Err()
case e := <-closeChannel:
err = e
}

log.Infow(
"graceful shutdown",
"signal", signal,
"close error", err,
)
shutdownChannel <- err
},
)
log.Info(server.Serve())
return <-shutdownChannel
}

// ParseConfig returns a new Config parsed from the YAML file at the given path.
func ParseConfig(path string) (Config, error) {
cfg := Config{}
if path == "" {
return cfg, errors.New("baseplate.ParseConfig: no config path given")
}

f, err := os.Open(path)
if err != nil {
return cfg, err
}
defer f.Close()

return DecodeConfigYAML(f)
}

// DecodeConfigYAML returns a new Config built from decoding the YAML read from
// the given Reader.
func DecodeConfigYAML(reader io.Reader) (Config, error) {
cfg := Config{}
if err := yaml.NewDecoder(reader).Decode(&cfg); err != nil {
return cfg, err
}

log.Debugf("%#v", cfg)
return cfg, nil
}

type cancelCloser struct {
cancel context.CancelFunc
}

func (c cancelCloser) Close() error {
c.cancel()
return nil
}

// New parses the config file at the given path, initializes the monitoring and
// logging frameworks, and returns the "serve" context and a new Baseplate to
// run your service on.
func New(ctx context.Context, path string) (Baseplate, error) {
cfg, err := ParseConfig(path)
if err != nil {
return nil, err
}
bp := impl{cfg: cfg}

ctx, cancel := context.WithCancel(ctx)
bp.closers = append(bp.closers, cancelCloser{cancel})

log.InitFromConfig(cfg.Log)
bp.closers = append(bp.closers, metricsbp.InitFromConfig(ctx, cfg.Metrics))

closer, err := log.InitSentry(cfg.Sentry)
if err != nil {
bp.Close()
return nil, err
}
bp.closers = append(bp.closers, closer)

bp.secrets, err = secrets.InitFromConfig(ctx, cfg.Secrets)
if err != nil {
bp.Close()
return nil, err
}
bp.closers = append(bp.closers, bp.secrets)

closer, err = tracing.InitFromConfig(cfg.Tracing)
if err != nil {
bp.Close()
return nil, err
}
bp.closers = append(bp.closers, closer)

bp.ecImpl = edgecontext.Init(edgecontext.Config{
Store: bp.secrets,
Logger: log.ErrorWithSentryWrapper(),
})
return bp, nil
}

type impl struct {
cfg Config
closers []io.Closer
ecImpl *edgecontext.Impl
secrets *secrets.Store
}

func (bp impl) Config() Config {
return bp.cfg
}

func (bp impl) Secrets() *secrets.Store {
return bp.secrets
}

func (bp impl) EdgeContextImpl() *edgecontext.Impl {
return bp.ecImpl
}

func (bp impl) Close() error {
batch := &batcherror.BatchError{}
for _, c := range bp.closers {
if err := c.Close(); err != nil {
log.Errorw(
"Failed to close closer",
"err", err,
"closer", fmt.Sprintf("%#v", c),
)
batch.Add(err)
}
}
return batch.Compile()
}

// NewTestBaseplate returns a new Baseplate using the given Config and secrets
// Store that can be used in testing.
//
// NewTestBaseplate only returns a Baseplate, it does not initialialize any of
// the monitoring or logging frameworks.
func NewTestBaseplate(cfg Config, store *secrets.Store) Baseplate {
return &impl{
cfg: cfg,
secrets: store,
ecImpl: edgecontext.Init(edgecontext.Config{Store: store}),
}
}

var (
_ Baseplate = impl{}
_ Baseplate = (*impl)(nil)
)
Loading

0 comments on commit 279f081

Please sign in to comment.