-
Notifications
You must be signed in to change notification settings - Fork 73
/
baseplate.go
428 lines (372 loc) · 12.3 KB
/
baseplate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
package baseplate
import (
"context"
"fmt"
"io"
"os"
"runtime/debug"
"time"
"github.com/reddit/baseplate.go/batchcloser"
"github.com/reddit/baseplate.go/configbp"
"github.com/reddit/baseplate.go/ecinterface"
"github.com/reddit/baseplate.go/log"
"github.com/reddit/baseplate.go/metricsbp"
"github.com/reddit/baseplate.go/prometheusbp"
"github.com/reddit/baseplate.go/runtimebp"
"github.com/reddit/baseplate.go/secrets"
"github.com/reddit/baseplate.go/tracing"
)
const (
// DefaultStopDelay is the default StopDelay to be used in Serve.
DefaultStopDelay = 5 * time.Second
)
// Configer defines the interface that allows you to extend Config with your own
// configurations.
type Configer interface {
GetConfig() Config
}
var (
_ Configer = Config{}
)
// Config is a general purpose config for assembling a Baseplate server.
//
// It implements Configer.
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"`
// Deprecated: No-op for now, will be removed in a future release.
Timeout time.Duration `yaml:"timeout"`
// StopTimeout is the timeout for the Stop command for the service.
//
// If this is not set, then a default value of 30 seconds will be used.
// If this is less than 0, then no timeout will be set on the Stop command.
StopTimeout time.Duration `yaml:"stopTimeout"`
// Delay after receiving termination signal (SIGTERM, etc.) before kicking off
// the graceful shutdown process. This happens before the PreShutdown closers.
//
// By default this is 1s (DefaultStopDelay).
// To disable it, set it to a negative value.
StopDelay time.Duration `yaml:"stopDelay"`
Log log.Config `yaml:"log"`
Runtime runtimebp.Config `yaml:"runtime"`
Secrets secrets.Config `yaml:"secrets"`
Sentry log.SentryConfig `yaml:"sentry"`
Tracing tracing.Config `yaml:"tracing"`
// Deprecated: statsd metrics are deprecated.
Metrics metricsbp.Config `yaml:"metrics"`
}
// GetConfig implements Configer.
func (c Config) GetConfig() Config {
return c
}
// Baseplate is the general purpose object that you build a Server on.
type Baseplate interface {
io.Closer
Configer
EdgeContextImpl() ecinterface.Interface
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
}
// ServeArgs provides a list of arguments to Serve.
type ServeArgs struct {
// Server is the Server that should be run until receiving a shutdown signal.
// This is a required argument and baseplate.Serve will panic if this is nil.
Server Server
// PreShutdown is an optional slice of io.Closers that should be gracefully
// shut down before the server upon receipt of a shutdown signal.
PreShutdown []io.Closer
// PostShutdown is an optional slice of io.Closers that should be gracefully
// shut down after the server upon receipt of a shutdown signal.
PostShutdown []io.Closer
}
// Serve runs the given Server until it is given an external shutdown signal.
//
// It uses runtimebp.HandleShutdown to handle the signal and gracefully shut
// down, in order:
//
// * any provided PreShutdown closers,
//
// * the Server, and
//
// * any provided PostShutdown closers.
//
// Returns the (possibly nil) error returned by "Close", or
// context.DeadlineExceeded if it times out.
//
// If a StopTimeout is configured, 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, args ServeArgs) error {
server := args.Server
// Initialize a channel to return the response from server.Close() as our
// return value.
shutdownChannel := make(chan error)
// Listen for a shutdown command.
//
// This is a blocking call so it is in a separate goroutine. It will exit
// either when it is triggered via a shutdown command or if the context passed
// in is cancelled.
go runtimebp.HandleShutdown(
ctx,
func(signal os.Signal) {
// Check if the server has a StopTimeout configured.
//
// If one is set, we will only wait for that duration for the server to
// stop gracefully and will exit after the deadline is exceeded.
timeout := server.Baseplate().GetConfig().StopTimeout
// Default to 30 seconds if not set.
if timeout == 0 {
timeout = time.Second * 30
}
// If timeout is < 0, we will wait indefinitely for the server to close.
if timeout > 0 {
// Declare cancel in advance so we can just use `=` when calling
// context.WithTimeout. If we used `:=` it would bind `ctx` to the scope
// of this `if` statement rather than updating the value declared before.
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
delay := server.Baseplate().GetConfig().StopDelay
if delay == 0 {
delay = DefaultStopDelay
}
// Initialize a channel to pass the result of server.Close().
//
// It's buffered with size 1 to avoid blocking the goroutine forever.
closeChannel := make(chan error, 1)
// Tell the server and any provided closers to close.
//
// This is a blocking call, so it is called in a separate goroutine.
go func() {
var bc batchcloser.BatchCloser
if delay > 0 {
bc.Add(batchcloser.Wrap(func() error {
time.Sleep(delay)
return nil
}))
}
bc.Add(args.PreShutdown...)
bc.Add(server)
bc.Add(args.PostShutdown...)
closeChannel <- bc.Close()
}()
// Declare the error variable we will use later here so we can set it to
// the result of the switch statement below.
var err error
// Wait for either ctx.Done() to be closed (indicating that the context
// was cancelled or its deadline was exceeded) or server.Close() to return.
select {
case <-ctx.Done():
// The context timed-out or was cancelled so use that error.
err = fmt.Errorf("baseplate: context cancelled while waiting for server.Close(). %w", ctx.Err())
case e := <-closeChannel:
// server.Close() completed and passed its result to closeChannel, so
// use that value.
err = e
}
log.Infow(
"graceful shutdown",
"signal", signal,
"close error", err,
)
// Pass the final, potentially nil, error to shutdownChannel.
shutdownChannel <- err
},
)
// Start the server.
//
// This is a blocking command and will run until the server is closed.
log.Info(server.Serve())
// Return the error passed via shutdownChannel to the caller.
//
// This will block until a value is put on the channel.
return <-shutdownChannel
}
// ParseConfigYAML loads the baseplate config into the structed pointed to by cfgPointer.
//
// The configuration file is located based on the $BASEPLATE_CONFIG_PATH
// environment variable.
//
// To avoid easy mistakes, "strict" mode is enabled while parsing the file,
// which means that all values in the YAML config must have a matching
// struct or value during decoding.
//
// If you don't have any customized configurations to decode from YAML,
// you can just pass in a *pointer* to baseplate.Config:
//
// var cfg baseplate.Config
// if err := baseplate.ParseConfigYAML(&cfg); err != nil {
// log.Fatalf("Parsing config: %s", err)
// }
// ctx, bp, err := baseplate.New(baseplate.NewArgs{
// EdgeContextFactory: edgecontext.Factory(...),
// Config: cfg,
// })
//
// If you do have customized configurations to decode from YAML,
// embed a baseplate.Config with `yaml:",inline"` yaml tags, for example:
//
// type myServiceConfig struct {
// // The yaml tag is required to pass strict parsing.
// baseplate.Config `yaml:",inline"`
//
// // Actual configs
// FancyName string `yaml:"fancy_name"`
// }
// var cfg myServiceCfg
// if err := baseplate.ParseConfigYAML(&cfg); err != nil {
// log.Fatalf("Parsing config: %s", err)
// }
// ctx, bp, err := baseplate.New(baseplate.NewArgs{
// EdgeContextFactory: edgecontext.Factory(...),
// Config: cfg,
// })
//
// Environment variable references (e.g. $FOO and ${FOO}) are substituted into the
// YAML from the process-level environment before parsing the configuration.
func ParseConfigYAML(cfgPointer Configer) error {
if configbp.BaseplateConfigPath == "" {
return fmt.Errorf("no $BASEPLATE_CONFIG_PATH specified, cannot load config")
}
return configbp.ParseStrictFile(configbp.BaseplateConfigPath, cfgPointer)
}
// NewArgs defines the args used in New functino.
type NewArgs struct {
// Required. New will panic if this is nil.
Config Configer
// Required. New will panic if this is nil.
//
// The factory to be used to create edge context implementation.
EdgeContextFactory ecinterface.Factory
}
// New initializes Baseplate libraries with the given config,
// (logging, secrets, tracing, edge context, etc.),
// and returns the "serve" context and a new Baseplate to
// run your service on.
// The returned context will be cancelled when the Baseplate is closed.
func New(ctx context.Context, args NewArgs) (context.Context, Baseplate, error) {
cfg := args.Config.GetConfig()
bp := impl{cfg: cfg, closers: batchcloser.New()}
if info, ok := debug.ReadBuildInfo(); ok {
prometheusbp.RecordModuleVersions(info)
} else {
log.C(ctx).Warn("baseplate.New: unable to read build info to export dependency metrics")
}
runtimebp.InitFromConfig(cfg.Runtime)
ctx, cancel := context.WithCancel(ctx)
bp.closers.Add(batchcloser.WrapCancel(cancel))
log.InitFromConfig(cfg.Log)
closer, err := log.InitSentry(cfg.Sentry)
if err != nil {
bp.Close()
return nil, nil, fmt.Errorf(
"baseplate.New: failed to init sentry: %w (config: %#v)",
err,
cfg.Sentry,
)
}
bp.closers.Add(closer)
bp.secrets, err = secrets.InitFromConfig(ctx, cfg.Secrets)
if err != nil {
bp.Close()
return nil, nil, fmt.Errorf(
"baseplate.New: failed to init secrets: %w (config: %#v)",
err,
cfg.Secrets,
)
}
bp.closers.Add(bp.secrets)
closer, err = tracing.InitFromConfig(cfg.Tracing)
if err != nil {
bp.Close()
return nil, nil, fmt.Errorf(
"baseplate.New: failed to init tracing: %w (config: %#v)",
err,
cfg.Tracing,
)
}
bp.closers.Add(closer)
bp.ecImpl, err = args.EdgeContextFactory(ecinterface.FactoryArgs{
Store: bp.secrets,
})
if err != nil {
bp.Close()
return nil, nil, fmt.Errorf(
"baseplate.New: failed to init edge context: %w",
err,
)
}
return ctx, bp, nil
}
type impl struct {
closers *batchcloser.BatchCloser
cfg Config
ecImpl ecinterface.Interface
secrets *secrets.Store
}
func (bp impl) GetConfig() Config {
return bp.cfg
}
func (bp impl) Secrets() *secrets.Store {
return bp.secrets
}
func (bp impl) EdgeContextImpl() ecinterface.Interface {
return bp.ecImpl
}
func (bp impl) Close() error {
err := bp.closers.Close()
if err != nil {
log.Errorw(
"Error while closing closers",
"err", err,
)
}
return err
}
// NewTestBaseplateArgs defines the args used by NewTestBaseplate.
type NewTestBaseplateArgs struct {
Config Config
Store *secrets.Store
EdgeContextImpl ecinterface.Interface
}
// 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 initialize any of
// the monitoring or logging frameworks.
func NewTestBaseplate(args NewTestBaseplateArgs) Baseplate {
return &impl{
cfg: args.Config,
secrets: args.Store,
ecImpl: args.EdgeContextImpl,
closers: batchcloser.New(),
}
}
var (
_ Baseplate = impl{}
_ Baseplate = (*impl)(nil)
)