Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ That is the entire plugin. `pluginsdk.Main`:
3. configures logging and runs the optional `Setup` hook,
4. adopts the `--api_fd` socket,
5. builds the gin engine with default middleware and calls `Register`,
6. serves until `SIGINT`/`SIGTERM`, then drains and shuts down.
6. serves until `SIGINT`/`SIGTERM`, then drains, runs the optional `Shutdown`
hook, and returns.

Run it locally with `--api-addr` instead of `--api_fd`:

Expand Down Expand Up @@ -286,6 +287,9 @@ type Plugin[C any] struct {
// Setup is an optional pre-serve hook (see SetupFunc).
Setup SetupFunc[C]

// Shutdown is an optional post-drain hook (see ShutdownFunc).
Shutdown ShutdownFunc[C]

// Middleware is appended to the default stack (unless disabled below).
Middleware []gin.HandlerFunc

Expand All @@ -295,17 +299,27 @@ type Plugin[C any] struct {
}
```

### `type RegisterFunc[C any]` and `type SetupFunc[C any]`
### `type RegisterFunc[C any]`, `type SetupFunc[C any]` and `type ShutdownFunc[C any]`

```go
type RegisterFunc[C any] func(ctx context.Context, cfg *C, engine *gin.Engine) error
type SetupFunc[C any] func(ctx context.Context, cfg *C) error
type ShutdownFunc[C any] func(ctx context.Context, cfg *C) error
```

`Register` attaches routes; returning a non-nil error aborts startup. `Setup` is
an optional hook that runs once after configuration is resolved but before the
server starts accepting requests — use it for one-time work such as fetching
source code.
source code. `Shutdown` is an optional hook that runs once after the server has
stopped accepting requests and drained the ones in flight, but before `Main`
returns — use it to release what `Register` started and the platform cannot see
for you: a session with another service, a lease, a node registration. It runs
on what is left of the `ShutdownTimeout` budget after the drain, and `ctx` is
bound accordingly; a returned error is logged, not fatal.

Do not tear down from a goroutine watching the base context instead: it races
the return from `Main`, and a server that was idle when the signal arrived
drains instantly, so that race is usually lost.

### Options (for `Serve`)

Expand Down
30 changes: 30 additions & 0 deletions go/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ type RegisterFunc[C any] func(
// code). Returning an error aborts startup.
type SetupFunc[C any] func(ctx context.Context, cfg *C) error

// ShutdownFunc is an optional hook run once, after the server has stopped
// accepting requests and drained the ones in flight, but before Main returns
// and the process ends. Use it to release what Register started and the
// platform cannot see: a session with another service, a lease, a node
// registration. It shares the ShutdownTimeout budget with the drain, and ctx
// is bound accordingly. A returned error is logged, not fatal.
type ShutdownFunc[C any] func(ctx context.Context, cfg *C) error

// Plugin declares a plugin for the one-call Main entrypoint.
type Plugin[C any] struct {
// Name is the plugin name, used in logs and diagnostics. It does not affect
Expand All @@ -84,6 +92,9 @@ type Plugin[C any] struct {
// Setup is an optional pre-serve hook (see SetupFunc).
Setup SetupFunc[C]

// Shutdown is an optional post-drain hook (see ShutdownFunc).
Shutdown ShutdownFunc[C]

// Middleware is appended to the default stack (unless disabled below).
Middleware []gin.HandlerFunc

Expand Down Expand Up @@ -151,6 +162,12 @@ func run[C any](ctx context.Context, p *Plugin[C]) error {
opts = append(opts, WithoutDefaultMiddleware())
}

if p.Shutdown != nil {
opts = append(opts, WithShutdown(func(ctx context.Context) error {
return p.Shutdown(ctx, &root.Config)
}))
}

s := newSettings(name, p.Version, opts)

ctx = configureLogging(ctx, s)
Expand Down Expand Up @@ -280,6 +297,19 @@ func serve[C any](
return fmt.Errorf("shutting down server: %w", err)
}

// The hook runs after the drain, on what is left of the same budget.
// Without it a plugin has no safe place for its own teardown: a goroutine
// watching ctx races the return from here, and a server that was idle
// when the signal arrived drains instantly, so that race is usually lost.
if s.shutdown != nil {
if err := s.shutdown(shutdownCtx); err != nil {
log.G(base).
Warn().
Err(err).
Msg("running the plugin shutdown hook")
}
}

return nil
}

Expand Down
10 changes: 10 additions & 0 deletions go/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package pluginsdk

import (
"context"
"net"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -54,6 +55,8 @@ type settings struct {
responder func(*gin.Context, int, any)

rawConfig []byte

shutdown func(context.Context) error
}

// Option customises Serve. Options are applied in order, after any Options
Expand Down Expand Up @@ -104,6 +107,13 @@ func WithResponder(fn func(*gin.Context, int, any)) Option {
return func(s *settings) { s.responder = fn }
}

// WithShutdown runs fn once the server has stopped and drained, before Serve
// returns. It is the programmatic form of Plugin.Shutdown; see ShutdownFunc
// for the contract.
func WithShutdown(fn func(context.Context) error) Option {
return func(s *settings) { s.shutdown = fn }
}

// WithRawConfig supplies the raw platform config bytes (as delivered on STDIN)
// so they are retrievable through RawConfig. Main sets this for you; custom
// command grammars pass it explicitly.
Expand Down