Skip to content

Commit b58f6b4

Browse files
committed
op-service: interrupt handling improvement
1 parent 3be62ed commit b58f6b4

File tree

6 files changed

+103
-31
lines changed

6 files changed

+103
-31
lines changed

indexer/cmd/indexer/main.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"context"
55
"os"
66

7+
"github.com/ethereum/go-ethereum/log"
8+
79
oplog "github.com/ethereum-optimism/optimism/op-service/log"
810
"github.com/ethereum-optimism/optimism/op-service/opio"
9-
"github.com/ethereum/go-ethereum/log"
1011
)
1112

1213
var (
@@ -15,16 +16,10 @@ var (
1516
)
1617

1718
func main() {
18-
// This is the most root context, used to propagate
19-
// cancellations to all spawned application-level goroutines
20-
ctx, cancel := context.WithCancel(context.Background())
21-
go func() {
22-
opio.BlockOnInterrupts()
23-
cancel()
24-
}()
25-
2619
oplog.SetupDefaults()
2720
app := newCli(GitCommit, GitDate)
21+
// sub-commands set up their individual interrupt lifecycles, which can block on the given interrupt as needed.
22+
ctx := opio.WithInterruptBlocker(context.Background())
2823
if err := app.RunContext(ctx, os.Args); err != nil {
2924
log.Error("application failed", "err", err)
3025
os.Exit(1)

op-batcher/cmd/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package main
22

33
import (
4+
"context"
45
"os"
56

6-
opservice "github.com/ethereum-optimism/optimism/op-service"
77
"github.com/urfave/cli/v2"
88

99
"github.com/ethereum-optimism/optimism/op-batcher/batcher"
1010
"github.com/ethereum-optimism/optimism/op-batcher/flags"
1111
"github.com/ethereum-optimism/optimism/op-batcher/metrics"
12+
opservice "github.com/ethereum-optimism/optimism/op-service"
1213
"github.com/ethereum-optimism/optimism/op-service/cliapp"
1314
oplog "github.com/ethereum-optimism/optimism/op-service/log"
1415
"github.com/ethereum-optimism/optimism/op-service/metrics/doc"
16+
"github.com/ethereum-optimism/optimism/op-service/opio"
1517
"github.com/ethereum/go-ethereum/log"
1618
)
1719

@@ -38,7 +40,8 @@ func main() {
3840
},
3941
}
4042

41-
err := app.Run(os.Args)
43+
ctx := opio.WithInterruptBlocker(context.Background())
44+
err := app.RunContext(ctx, os.Args)
4245
if err != nil {
4346
log.Crit("Application failed", "message", err)
4447
}

op-node/cmd/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/ethereum-optimism/optimism/op-service/cliapp"
2222
oplog "github.com/ethereum-optimism/optimism/op-service/log"
2323
"github.com/ethereum-optimism/optimism/op-service/metrics/doc"
24+
"github.com/ethereum-optimism/optimism/op-service/opio"
2425
)
2526

2627
var (
@@ -58,7 +59,8 @@ func main() {
5859
},
5960
}
6061

61-
err := app.Run(os.Args)
62+
ctx := opio.WithInterruptBlocker(context.Background())
63+
err := app.RunContext(ctx, os.Args)
6264
if err != nil {
6365
log.Crit("Application failed", "message", err)
6466
}

op-service/cliapp/lifecycle.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"os"
87

98
"github.com/urfave/cli/v2"
109

@@ -30,21 +29,22 @@ type Lifecycle interface {
3029
// a shutdown when the Stop context is not expired.
3130
type LifecycleAction func(ctx *cli.Context, close context.CancelCauseFunc) (Lifecycle, error)
3231

32+
var interruptErr = errors.New("interrupt signal")
33+
3334
// LifecycleCmd turns a LifecycleAction into an CLI action,
3435
// by instrumenting it with CLI context and signal based termination.
36+
// The signals are catched with the opio.BlockFn attached to the context, if any.
37+
// If no block function is provided, it adds default interrupt handling.
3538
// The app may continue to run post-processing until fully shutting down.
3639
// The user can force an early shut-down during post-processing by sending a second interruption signal.
3740
func LifecycleCmd(fn LifecycleAction) cli.ActionFunc {
38-
return lifecycleCmd(fn, opio.BlockOnInterruptsContext)
39-
}
40-
41-
type waitSignalFn func(ctx context.Context, signals ...os.Signal)
42-
43-
var interruptErr = errors.New("interrupt signal")
44-
45-
func lifecycleCmd(fn LifecycleAction, blockOnInterrupt waitSignalFn) cli.ActionFunc {
4641
return func(ctx *cli.Context) error {
4742
hostCtx := ctx.Context
43+
blockOnInterrupt := opio.BlockerFromContext(hostCtx)
44+
if blockOnInterrupt == nil { // add default interrupt blocker to context if none is set.
45+
hostCtx = opio.WithInterruptBlocker(hostCtx)
46+
blockOnInterrupt = opio.BlockerFromContext(hostCtx)
47+
}
4848
appCtx, appCancel := context.WithCancelCause(hostCtx)
4949
ctx.Context = appCtx
5050

op-service/cliapp/lifecycle_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package cliapp
33
import (
44
"context"
55
"errors"
6-
"os"
76
"testing"
87
"time"
98

109
"github.com/stretchr/testify/require"
1110
"github.com/urfave/cli/v2"
11+
12+
"github.com/ethereum-optimism/optimism/op-service/opio"
1213
)
1314

1415
type fakeLifecycle struct {
@@ -77,19 +78,19 @@ func TestLifecycleCmd(t *testing.T) {
7778
return app, nil
7879
}
7980

80-
// puppeteer a system signal waiter with a test signal channel
81-
fakeSignalWaiter := func(ctx context.Context, signals ...os.Signal) {
82-
select {
83-
case <-ctx.Done():
84-
case <-signalCh:
85-
}
86-
}
87-
8881
// turn our mock app and system signal into a lifecycle-managed command
89-
actionFn := lifecycleCmd(mockAppFn, fakeSignalWaiter)
82+
actionFn := LifecycleCmd(mockAppFn)
9083

9184
// try to shut the test down after being locked more than a minute
9285
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
86+
87+
// puppeteer system signal interrupts by hooking up the test signal channel as "blocker" for the app to use.
88+
ctx = opio.WithBlocker(ctx, func(ctx context.Context) {
89+
select {
90+
case <-ctx.Done():
91+
case <-signalCh:
92+
}
93+
})
9394
t.Cleanup(cancel)
9495

9596
// create a fake CLI context to run our command with

op-service/opio/interrupts.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,74 @@ func BlockOnInterruptsContext(ctx context.Context, signals ...os.Signal) {
4141
signal.Stop(interruptChannel)
4242
}
4343
}
44+
45+
type interruptContextKeyType struct{}
46+
47+
var blockerContextKey = interruptContextKeyType{}
48+
49+
type interruptCatcher struct {
50+
incoming chan os.Signal
51+
}
52+
53+
// Block blocks until either an interrupt signal is received, or the context is cancelled.
54+
// No error is returned on interrupt.
55+
func (c *interruptCatcher) Block(ctx context.Context) {
56+
select {
57+
case <-c.incoming:
58+
case <-ctx.Done():
59+
}
60+
}
61+
62+
// WithInterruptBlocker attaches an interrupt handler to the context,
63+
// which continues to receive signals after every block.
64+
// This helps functions block on individual consecutive interrupts.
65+
func WithInterruptBlocker(ctx context.Context) context.Context {
66+
if ctx.Value(blockerContextKey) != nil { // already has an interrupt handler
67+
return ctx
68+
}
69+
catcher := &interruptCatcher{
70+
incoming: make(chan os.Signal, 10),
71+
}
72+
signal.Notify(catcher.incoming, DefaultInterruptSignals...)
73+
74+
return context.WithValue(ctx, blockerContextKey, BlockFn(catcher.Block))
75+
}
76+
77+
// WithBlocker overrides the interrupt blocker value,
78+
// e.g. to insert a block-function for testing CLI shutdown without actual process signals.
79+
func WithBlocker(ctx context.Context, fn BlockFn) context.Context {
80+
return context.WithValue(ctx, blockerContextKey, fn)
81+
}
82+
83+
// BlockFn simply blocks until the implementation of the blocker interrupts it, or till the given context is cancelled.
84+
type BlockFn func(ctx context.Context)
85+
86+
// BlockerFromContext returns a BlockFn that blocks on interrupts when called.
87+
func BlockerFromContext(ctx context.Context) BlockFn {
88+
v := ctx.Value(blockerContextKey)
89+
if v == nil {
90+
return nil
91+
}
92+
return v.(BlockFn)
93+
}
94+
95+
// CancelOnInterrupt cancels the given context on interrupt.
96+
// If a BlockFn is attached to the context, this is used as interrupt-blocking.
97+
// If not, then the context blocks on a manually handled interrupt signal.
98+
func CancelOnInterrupt(ctx context.Context) context.Context {
99+
inner, cancel := context.WithCancel(ctx)
100+
101+
blockOnInterrupt := BlockerFromContext(ctx)
102+
if blockOnInterrupt == nil {
103+
blockOnInterrupt = func(ctx context.Context) {
104+
BlockOnInterruptsContext(ctx) // default signals
105+
}
106+
}
107+
108+
go func() {
109+
blockOnInterrupt(ctx)
110+
cancel()
111+
}()
112+
113+
return inner
114+
}

0 commit comments

Comments
 (0)