Skip to content

Commit 66f243e

Browse files
committed
Add shutdown method per component
In the current implementation, we provide the user the ability to initialize things by implementing an `Init` method. However, they don't have the ability to do a clean shutdown easily. The user filled multiple github issues (#275 and #765) asking for a `Shutdown` method. This PR adds a `Shutdown` method to each component. Note that there is no guarantee that this method can run - e.g., the app receives a SIGKILL because the machine suddenly crashes. However, a `Shutdown` method can enable the user to achieve graceful shutdown in normal scenarios. In tests, if someone wants to test the `Shutdown` method, they can get a pointer to the implementation of the component and explicitly call the `Shutdown` method.
1 parent 3ba5acf commit 66f243e

File tree

5 files changed

+78
-20
lines changed

5 files changed

+78
-20
lines changed

examples/chat/sqlstore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (cfg *config) Validate() error {
9696
return nil
9797
}
9898

99-
func (s *sqlStore) Init(ctx context.Context) error {
99+
func (s *sqlStore) Init(context.Context) error {
100100
cfg := s.Config()
101101
if cfg.Driver == "" {
102102
return fmt.Errorf("missing database driver in config")

internal/tool/generate/generator.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,12 @@ func extractComponent(opt Options, pkg *packages.Package, file *ast.File, tset *
602602
seenLis[lis] = struct{}{}
603603
}
604604

605-
// Warn the user if the component has a mistyped Init method. Init methods
606-
// are supposed to have type "func(context.Context) error", but it's easy
605+
// Warn the user if the component has a mistyped Init or Shutdown method. These
606+
// methods are supposed to have type "func(context.Context) error", but it's easy
607607
// to forget to add a context.Context argument or error return. Without
608-
// this warning, the component's Init method will be silently ignored. This
609-
// can be very frustrating to debug.
610-
if err := checkMistypedInit(pkg, tset, impl); err != nil {
608+
// this warning, the component's Init or Shutdown method will be silently ignored.
609+
// This can be very frustrating to debug.
610+
if err := checkMistypedInitOrShutdown(pkg, tset, impl); err != nil {
611611
opt.Warn(err)
612612
}
613613

@@ -789,27 +789,27 @@ func validateMethods(pkg *packages.Package, tset *typeSet, intf *types.Named) er
789789
return errors.Join(errs...)
790790
}
791791

792-
// checkMistypedInit returns an error if the provided component implementation
793-
// has an Init method that does not have type "func(context.Context) error".
794-
func checkMistypedInit(pkg *packages.Package, tset *typeSet, impl *types.Named) error {
792+
// checkMistypedInitOrShutdown returns an error if the provided component implementation
793+
// has an Init or a Shutdown method that does not have type "func(context.Context) error".
794+
func checkMistypedInitOrShutdown(pkg *packages.Package, tset *typeSet, impl *types.Named) error {
795795
for i := 0; i < impl.NumMethods(); i++ {
796796
m := impl.Method(i)
797-
if m.Name() != "Init" {
797+
if m.Name() != "Init" && m.Name() != "Shutdown" {
798798
continue
799799
}
800800

801801
// TODO(mwhittaker): Highlight the warning yellow instead of red.
802802
sig := m.Type().(*types.Signature)
803803
err := errorf(pkg.Fset, m.Pos(),
804-
`WARNING: Component %v's Init method has type "%v", not type "func(context.Context) error". It will be ignored. See https://serviceweaver.dev/docs.html#components-implementation for more information.`,
805-
impl.Obj().Name(), sig)
804+
`WARNING: Component %v's %s method has type "%v", not type "func(context.Context) error". It will be ignored. See https://serviceweaver.dev/docs.html#components-implementation for more information.`,
805+
impl.Obj().Name(), m.Name(), sig)
806806

807-
// Check Init's parameters.
807+
// Check parameters.
808808
if sig.Params().Len() != 1 || !isContext(sig.Params().At(0).Type()) {
809809
return err
810810
}
811811

812-
// Check Init's returns.
812+
// Check returns.
813813
if sig.Results().Len() != 1 || sig.Results().At(0).Type().String() != "error" {
814814
return err
815815
}

internal/weaver/remoteweavelet.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import (
2323
"log/slog"
2424
"net"
2525
"os"
26+
"os/signal"
2627
"reflect"
2728
"strings"
2829
"sync"
2930
"sync/atomic"
31+
"syscall"
3032

3133
"github.com/ServiceWeaver/weaver/internal/config"
3234
"github.com/ServiceWeaver/weaver/internal/control"
@@ -286,6 +288,26 @@ func NewRemoteWeavelet(ctx context.Context, regs []*codegen.Registration, bootst
286288
return nil
287289
})
288290

291+
// Start a signal handler to detect when the process is killed. This isn't
292+
// perfect, as we can't catch a SIGKILL, but it's good in the common case.
293+
done := make(chan os.Signal, 1)
294+
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
295+
go func() {
296+
<-done
297+
for _, c := range w.componentsByName {
298+
if !c.implReady.Load() {
299+
continue
300+
}
301+
// Call Shutdown method if available.
302+
if i, ok := c.impl.(interface{ Shutdown(context.Context) error }); ok {
303+
if err := i.Shutdown(ctx); err != nil {
304+
w.syslogger.Error("Component shutdown failed", "component", c.reg.Name, "err", err)
305+
}
306+
}
307+
}
308+
os.Exit(1)
309+
}()
310+
289311
w.syslogger.Debug("🧶 weavelet started", "addr", dialAddr)
290312
return w, nil
291313
}
@@ -537,7 +559,7 @@ func (w *RemoteWeavelet) GetLoad(context.Context, *protos.GetLoadRequest) (*prot
537559
}
538560

539561
// UpdateComponents implements weaver.controller and conn.WeaverHandler interfaces.
540-
func (w *RemoteWeavelet) UpdateComponents(ctx context.Context, req *protos.UpdateComponentsRequest) (*protos.UpdateComponentsReply, error) {
562+
func (w *RemoteWeavelet) UpdateComponents(_ context.Context, req *protos.UpdateComponentsRequest) (*protos.UpdateComponentsReply, error) {
541563
var errs []error
542564
var components []*component
543565
var shortened []string
@@ -578,7 +600,7 @@ func (w *RemoteWeavelet) UpdateComponents(ctx context.Context, req *protos.Updat
578600
}
579601

580602
// UpdateRoutingInfo implements controller.UpdateRoutingInfo.
581-
func (w *RemoteWeavelet) UpdateRoutingInfo(ctx context.Context, req *protos.UpdateRoutingInfoRequest) (reply *protos.UpdateRoutingInfoReply, err error) {
603+
func (w *RemoteWeavelet) UpdateRoutingInfo(_ context.Context, req *protos.UpdateRoutingInfoRequest) (reply *protos.UpdateRoutingInfoReply, err error) {
582604
if req.RoutingInfo == nil {
583605
w.syslogger.Error("Failed to update nil routing info")
584606
return nil, fmt.Errorf("nil RoutingInfo")
@@ -643,7 +665,7 @@ func (w *RemoteWeavelet) UpdateRoutingInfo(ctx context.Context, req *protos.Upda
643665
}
644666

645667
// GetHealth implements controller.GetHealth.
646-
func (w *RemoteWeavelet) GetHealth(ctx context.Context, req *protos.GetHealthRequest) (*protos.GetHealthReply, error) {
668+
func (w *RemoteWeavelet) GetHealth(context.Context, *protos.GetHealthRequest) (*protos.GetHealthReply, error) {
647669
// Get the health status for all components. For now, we consider a component
648670
// healthy iff it has been successfully initialized. In the future, we will
649671
// maintain a real-time health for each component.
@@ -657,7 +679,7 @@ func (w *RemoteWeavelet) GetHealth(ctx context.Context, req *protos.GetHealthReq
657679
}
658680

659681
// GetMetrics implements controller.GetMetrics.
660-
func (w *RemoteWeavelet) GetMetrics(ctx context.Context, req *protos.GetMetricsRequest) (*protos.GetMetricsReply, error) {
682+
func (w *RemoteWeavelet) GetMetrics(context.Context, *protos.GetMetricsRequest) (*protos.GetMetricsReply, error) {
661683
// TODO(sanjay): The protocol is currently brittle; if we ever lose a set of
662684
// updates, they will be lost forever. Fix by versioning the "last" map in
663685
// metrics.Exporter. The reader echoes back the version of the last set of

internal/weaver/singleweavelet.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func NewSingleWeavelet(ctx context.Context, regs []*codegen.Registration, opts S
132132
fmt.Fprint(os.Stderr, reg.Rolodex())
133133
}
134134

135-
return &SingleWeavelet{
135+
w := &SingleWeavelet{
136136
ctx: ctx,
137137
regs: regs,
138138
regsByName: regsByName,
@@ -149,7 +149,29 @@ func NewSingleWeavelet(ctx context.Context, regs []*codegen.Registration, opts S
149149
stats: imetrics.NewStatsProcessor(),
150150
components: map[string]any{},
151151
listeners: map[string]net.Listener{},
152-
}, nil
152+
}
153+
154+
// Start a signal handler to detect when the process is killed. This isn't
155+
// perfect, as we can't catch a SIGKILL, but it's good in the common case.
156+
done := make(chan os.Signal, 1)
157+
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
158+
go func() {
159+
<-done
160+
161+
w.mu.Lock()
162+
defer w.mu.Unlock()
163+
for c, impl := range w.components {
164+
// Call Shutdown method if available.
165+
if i, ok := impl.(interface{ Shutdown(context.Context) error }); ok {
166+
if err := i.Shutdown(ctx); err != nil {
167+
fmt.Printf("Component %s failed to shutdown: %v\n", c, err)
168+
}
169+
}
170+
}
171+
os.Exit(1)
172+
}()
173+
174+
return w, nil
153175
}
154176

155177
// parseSingleConfig parses the "[single]" section of a config file.

website/docs.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,20 @@ func (f *foo) Init(context.Context) error {
636636
}
637637
```
638638

639+
If a component implementation implements an `Shutdown(context.Context) error`
640+
method, it will be called when an instance of the component is destroyed.
641+
642+
```go
643+
func (f *foo) Shutdown(context.Context) error {
644+
// ...
645+
}
646+
```
647+
648+
**Note**: There is no guarantee that the `Shutdown` method will always
649+
be called. `Shutdown` is called **iff** your application receives a
650+
`SIGINT` or a `SIGTERM` signal. However, if the machine where your application runs
651+
crashes unexpectedly or becomes unresponsive, the `Shutdown` method is never called.
652+
639653
## Semantics
640654

641655
When implementing a component, there are a few semantic details to keep in mind:

0 commit comments

Comments
 (0)