Skip to content

Commit bb50a02

Browse files
committed
improved interrupt handling
Improved interrupt handling by passing a context through the entire call flow. This also improves user feedback when the call environment terminates the program.
1 parent 92e3ba2 commit bb50a02

12 files changed

Lines changed: 105 additions & 157 deletions

api/exec.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package api
22

3-
import "time"
3+
import (
4+
"context"
5+
"time"
6+
)
47

58
// ExecutionInfo information about an executed command
69
type ExecutionInfo struct {
@@ -16,7 +19,7 @@ type ExecCommandFn = func() (*ExecutionInfo, error)
1619

1720
// CommandExecutor is an abstraction for commands executed as subprocesses.
1821
type CommandExecutor interface {
19-
ExecuteFn(cmd *CommandSpec, defaultWorkingDir string, env map[string]string) ExecCommandFn
22+
ExecuteFn(ctx context.Context, cmd *CommandSpec, defaultWorkingDir string, env map[string]string) ExecCommandFn
2023
}
2124

2225
// ExecutionContext provides access to benchmark global resources

cmd/main.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,18 @@ package main
22

33
import (
44
"fmt"
5-
"os"
65
"runtime"
76
"runtime/debug"
87

98
"github.com/fatih/color"
109
"github.com/sha1n/bert/api"
1110
"github.com/sha1n/bert/internal/cli"
12-
"github.com/sha1n/bert/pkg/osutil"
1311
"github.com/sha1n/gommons/pkg/cmd"
1412
errorhandling "github.com/sha1n/gommons/pkg/error_handling"
15-
"github.com/sha1n/termite"
1613
log "github.com/sirupsen/logrus"
1714
)
1815

1916
func init() {
20-
osutil.RegisterInterruptGuard(func(sig os.Signal) {
21-
termite.NewCursor(os.Stdout).Show()
22-
doExit(1)
23-
})
24-
2517
log.SetFormatter(&log.TextFormatter{
2618
DisableTimestamp: true,
2719
})

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ require (
1313
github.com/sha1n/gommons v0.0.19
1414
github.com/sha1n/termite v0.0.25
1515
github.com/sirupsen/logrus v1.9.3
16-
github.com/spf13/cobra v1.9.1
16+
github.com/spf13/cobra v1.10.1
1717
github.com/stretchr/testify v1.11.1
1818
gopkg.in/yaml.v2 v2.4.0
1919
)
2020

2121
require (
2222
github.com/davecgh/go-spew v1.1.1 // indirect
23-
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
23+
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
2424
github.com/google/go-github/v35 v35.3.0 // indirect
2525
github.com/google/go-querystring v1.1.0 // indirect
2626
github.com/google/uuid v1.6.0 // indirect
@@ -30,10 +30,10 @@ require (
3030
github.com/mattn/go-colorable v0.1.14 // indirect
3131
github.com/mattn/go-isatty v0.0.20 // indirect
3232
github.com/pmezard/go-difflib v1.0.0 // indirect
33-
github.com/spf13/pflag v1.0.6 // indirect
34-
github.com/stretchr/objx v0.5.2 // indirect
33+
github.com/spf13/pflag v1.0.10 // indirect
34+
github.com/stretchr/objx v0.5.3 // indirect
3535
golang.org/x/crypto v0.45.0 // indirect
36-
golang.org/x/mod v0.29.0 // indirect
36+
golang.org/x/mod v0.30.0 // indirect
3737
golang.org/x/sys v0.38.0 // indirect
3838
golang.org/x/text v0.31.0 // indirect
3939
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
66
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
7-
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
8-
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
7+
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
8+
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
99
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
1010
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
1111
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -46,21 +46,22 @@ github.com/sha1n/termite v0.0.25 h1:8HqHIMi5zagv/FjSzLyHiXnM8/EkFRYJaBibjtvOwSg=
4646
github.com/sha1n/termite v0.0.25/go.mod h1:DftpGi9AABpnwXYA2CrJrNs1nHw1FlDIwXlGiBpGbWI=
4747
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
4848
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
49-
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
50-
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
51-
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
52-
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
49+
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
50+
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
51+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
52+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
53+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
5354
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
54-
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
55-
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
55+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
56+
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
5657
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
5758
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
5859
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5960
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6061
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
6162
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
62-
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
63-
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
63+
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
64+
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
6465
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
6566
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
6667
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

internal/cli/main_runner.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package cli
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"os"
8+
"os/signal"
79
"path"
810
"path/filepath"
911
"strings"
12+
"syscall"
1013

1114
"github.com/sha1n/bert/api"
1215
"github.com/sha1n/bert/internal/report"
1316
"github.com/sha1n/bert/pkg/exec"
1417
"github.com/sha1n/bert/pkg/osutil"
18+
1519
"github.com/sha1n/bert/pkg/reporthandlers"
1620
"github.com/sha1n/bert/pkg/specs"
1721
"github.com/sha1n/bert/pkg/ui"
@@ -117,7 +121,12 @@ func runFn(ctx api.IOContext) func(*cobra.Command, []string) {
117121
reportHandler.Subscribe(tracer.Stream())
118122

119123
log.Info("Executing...")
120-
exec.Execute(spec, resolveExecutionContext(cmd, spec, ctx, tracer))
124+
125+
// Create a context that is cancelled on interrupt signal
126+
execCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
127+
defer cancel()
128+
129+
exec.Execute(execCtx, spec, resolveExecutionContext(cmd, spec, ctx, tracer))
121130

122131
log.Info("Finalizing report...")
123132
err = reportHandler.Finalize()

pkg/exec/benchmark_exec.go

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,103 @@
11
package exec
22

33
import (
4+
"context"
5+
46
"github.com/sha1n/bert/api"
57
)
68

79
// Execute executes a benchmark and returns an object that provides access to collected stats.
8-
func Execute(spec api.BenchmarkSpec, ctx api.ExecutionContext) {
9-
ctx.Listener.OnBenchmarkStart()
10-
defer ctx.Listener.OnBenchmarkEnd()
10+
func Execute(ctx context.Context, spec api.BenchmarkSpec, execCtx api.ExecutionContext) {
11+
execCtx.Listener.OnBenchmarkStart()
12+
defer execCtx.Listener.OnBenchmarkEnd()
1113

1214
if spec.Alternate {
13-
executeAlternately(spec, ctx)
15+
executeAlternately(ctx, spec, execCtx)
1416
} else {
15-
executeSequentially(spec, ctx)
17+
executeSequentially(ctx, spec, execCtx)
1618
}
1719
}
1820

19-
func executeAlternately(spec api.BenchmarkSpec, ctx api.ExecutionContext) {
21+
func executeAlternately(ctx context.Context, spec api.BenchmarkSpec, execCtx api.ExecutionContext) {
2022
for i := 1; i <= spec.Executions; i++ {
2123
for si := range spec.Scenarios {
24+
if ctx.Err() != nil {
25+
return
26+
}
27+
2228
scenario := spec.Scenarios[si]
2329

24-
ctx.Listener.OnScenarioStart(scenario.ID())
30+
execCtx.Listener.OnScenarioStart(scenario.ID())
2531
if i == 1 {
26-
executeScenarioSetup(scenario, ctx)
32+
executeScenarioSetup(ctx, scenario, execCtx)
2733
}
28-
executeScenarioCommand(scenario, i, spec.Executions, ctx)
34+
executeScenarioCommand(ctx, scenario, i, spec.Executions, execCtx)
2935
if i == spec.Executions {
30-
executeScenarioTeardown(scenario, ctx)
36+
executeScenarioTeardown(ctx, scenario, execCtx)
3137
}
3238

33-
ctx.Listener.OnScenarioEnd(scenario.ID())
39+
execCtx.Listener.OnScenarioEnd(scenario.ID())
3440
}
3541
}
3642
}
3743

38-
func executeSequentially(spec api.BenchmarkSpec, ctx api.ExecutionContext) {
44+
func executeSequentially(ctx context.Context, spec api.BenchmarkSpec, execCtx api.ExecutionContext) {
3945
for si := range spec.Scenarios {
4046
scenario := spec.Scenarios[si]
4147

4248
for i := 1; i <= spec.Executions; i++ {
43-
ctx.Listener.OnScenarioStart(scenario.ID())
49+
if ctx.Err() != nil {
50+
return
51+
}
52+
53+
execCtx.Listener.OnScenarioStart(scenario.ID())
4454
if i == 1 {
45-
executeScenarioSetup(scenario, ctx)
55+
executeScenarioSetup(ctx, scenario, execCtx)
4656
}
4757

48-
executeScenarioCommand(scenario, i, spec.Executions, ctx)
58+
executeScenarioCommand(ctx, scenario, i, spec.Executions, execCtx)
4959

5060
if i == spec.Executions {
51-
executeScenarioTeardown(scenario, ctx)
61+
executeScenarioTeardown(ctx, scenario, execCtx)
5262
}
53-
ctx.Listener.OnScenarioEnd(scenario.ID())
63+
execCtx.Listener.OnScenarioEnd(scenario.ID())
5464
}
5565
}
5666
}
5767

58-
func executeScenarioSetup(scenario api.ScenarioSpec, ctx api.ExecutionContext) {
68+
func executeScenarioSetup(ctx context.Context, scenario api.ScenarioSpec, execCtx api.ExecutionContext) {
5969
if scenario.BeforeAll != nil {
60-
ctx.Listener.OnMessagef(scenario.ID(), "running 'beforeAll' command %v...", scenario.BeforeAll.Cmd)
61-
reportIfExecError(ctx.Executor.ExecuteFn(scenario.BeforeAll, scenario.WorkingDirectory, scenario.Env), scenario.ID(), ctx)
70+
execCtx.Listener.OnMessagef(scenario.ID(), "running 'beforeAll' command %v...", scenario.BeforeAll.Cmd)
71+
reportIfExecError(execCtx.Executor.ExecuteFn(ctx, scenario.BeforeAll, scenario.WorkingDirectory, scenario.Env), scenario.ID(), execCtx)
6272
}
6373
}
6474

65-
func executeScenarioTeardown(scenario api.ScenarioSpec, ctx api.ExecutionContext) {
75+
func executeScenarioTeardown(ctx context.Context, scenario api.ScenarioSpec, execCtx api.ExecutionContext) {
6676
if scenario.AfterAll != nil {
67-
ctx.Listener.OnMessagef(scenario.ID(), "running 'afterAll' command %v...", scenario.AfterAll.Cmd)
68-
reportIfExecError(ctx.Executor.ExecuteFn(scenario.AfterAll, scenario.WorkingDirectory, scenario.Env), scenario.ID(), ctx)
77+
execCtx.Listener.OnMessagef(scenario.ID(), "running 'afterAll' command %v...", scenario.AfterAll.Cmd)
78+
reportIfExecError(execCtx.Executor.ExecuteFn(ctx, scenario.AfterAll, scenario.WorkingDirectory, scenario.Env), scenario.ID(), execCtx)
6979
}
7080
}
7181

72-
func executeScenarioCommand(scenario api.ScenarioSpec, execIndex int, totalExec int, ctx api.ExecutionContext) {
73-
ctx.Listener.OnMessagef(scenario.ID(), "run %d of %d", execIndex, totalExec)
82+
func executeScenarioCommand(ctx context.Context, scenario api.ScenarioSpec, execIndex int, totalExec int, execCtx api.ExecutionContext) {
83+
execCtx.Listener.OnMessagef(scenario.ID(), "run %d of %d", execIndex, totalExec)
7484
if scenario.BeforeEach != nil {
75-
ctx.Listener.OnMessagef(scenario.ID(), "running 'beforeEach' command %v", scenario.BeforeEach.Cmd)
76-
reportIfExecError(ctx.Executor.ExecuteFn(scenario.BeforeEach, scenario.WorkingDirectory, scenario.Env), scenario.ID(), ctx)
85+
execCtx.Listener.OnMessagef(scenario.ID(), "running 'beforeEach' command %v", scenario.BeforeEach.Cmd)
86+
reportIfExecError(execCtx.Executor.ExecuteFn(ctx, scenario.BeforeEach, scenario.WorkingDirectory, scenario.Env), scenario.ID(), execCtx)
7787
}
7888

79-
ctx.Listener.OnMessagef(scenario.ID(), "running benchmark command %v", scenario.Command.Cmd)
80-
executeFn := ctx.Executor.ExecuteFn(scenario.Command, scenario.WorkingDirectory, scenario.Env)
89+
execCtx.Listener.OnMessagef(scenario.ID(), "running benchmark command %v", scenario.Command.Cmd)
90+
executeFn := execCtx.Executor.ExecuteFn(ctx, scenario.Command, scenario.WorkingDirectory, scenario.Env)
8191

82-
endTrace := ctx.Tracer.Start(scenario)
92+
endTrace := execCtx.Tracer.Start(scenario)
8393
info, err := executeFn()
8494
endTrace(info, err)
8595

86-
reportIfError(err, scenario.ID(), ctx)
96+
reportIfError(err, scenario.ID(), execCtx)
8797

8898
if scenario.AfterEach != nil {
89-
ctx.Listener.OnMessagef(scenario.ID(), "running 'afterEach' command %v", scenario.AfterEach.Cmd)
90-
reportIfExecError(ctx.Executor.ExecuteFn(scenario.AfterEach, scenario.WorkingDirectory, scenario.Env), scenario.ID(), ctx)
99+
execCtx.Listener.OnMessagef(scenario.ID(), "running 'afterEach' command %v", scenario.AfterEach.Cmd)
100+
reportIfExecError(execCtx.Executor.ExecuteFn(ctx, scenario.AfterEach, scenario.WorkingDirectory, scenario.Env), scenario.ID(), execCtx)
91101
}
92102
}
93103

pkg/exec/benchmark_exec_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package exec
22

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

67
"github.com/sha1n/bert/api"
@@ -65,7 +66,7 @@ func TestExecuteBenchmarkWithSetupAndTeardownSpecs(t *testing.T) {
6566
func executeWith(spec api.BenchmarkSpec) *CmdRecordingExecutor {
6667
recordingCtx := recordingExecutionContext()
6768

68-
Execute(spec, recordingCtx)
69+
Execute(context.Background(), spec, recordingCtx)
6970

7071
return recordingCtx.Executor.(*CmdRecordingExecutor)
7172
}

pkg/exec/command_exec.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package exec
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/exec"
@@ -25,17 +26,13 @@ func NewCommandExecutor(pipeStdout bool, pipeStderr bool) api.CommandExecutor {
2526
}
2627

2728
// Executes a single command in a subprocess based on the specified specs.
28-
func (ce *commandExecutor) ExecuteFn(cmdSpec *api.CommandSpec, defaultWorkingDir string, env map[string]string) api.ExecCommandFn {
29+
func (ce *commandExecutor) ExecuteFn(ctx context.Context, cmdSpec *api.CommandSpec, defaultWorkingDir string, env map[string]string) api.ExecCommandFn {
2930
log.Debugf("Going to execute command %v", cmdSpec.Cmd)
3031

31-
execCmd := exec.Command(cmdSpec.Cmd[0], cmdSpec.Cmd[1:]...)
32+
execCmd := exec.CommandContext(ctx, cmdSpec.Cmd[0], cmdSpec.Cmd[1:]...)
3233
ce.configureCommand(cmdSpec, execCmd, defaultWorkingDir, env)
3334

34-
cancel := osutil.RegisterInterruptGuard(onInterruptSignalFn(execCmd))
35-
3635
return func() (execInfo *api.ExecutionInfo, err error) {
37-
defer cancel()
38-
3936
startTime := time.Now()
4037
err = execCmd.Run()
4138
perceivedTime := time.Since(startTime)
@@ -88,14 +85,3 @@ func toEnvVarsArray(env map[string]string) []string {
8885

8986
return arr
9087
}
91-
92-
func onInterruptSignalFn(execCmd *exec.Cmd) func(os.Signal) {
93-
return func(sig os.Signal) {
94-
if sig == os.Interrupt {
95-
log.Debugf("Got %s signal. Forwarding to %s...", sig, execCmd.Args[0])
96-
if err := execCmd.Process.Signal(sig); err != nil {
97-
log.Debug(err)
98-
}
99-
}
100-
}
101-
}

0 commit comments

Comments
 (0)