Skip to content

Commit 8951c0d

Browse files
committed
clisqlshell: handle interactive query cancellation
Release note (cli change): The interactive SQL shell (`cockroach sql`, `cockroach demo`) now supports interrupting a currently running query with Ctrl+C, without losing access to the shell.
1 parent a38b5e4 commit 8951c0d

File tree

3 files changed

+105
-8
lines changed

3 files changed

+105
-8
lines changed

pkg/cli/clisqlshell/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ go_library(
2626
"//pkg/sql/scanner",
2727
"//pkg/sql/sqlfsm",
2828
"//pkg/util/envutil",
29+
"//pkg/util/syncutil",
2930
"@com_github_cockroachdb_errors//:errors",
3031
"@com_github_knz_go_libedit//:go-libedit",
3132
],

pkg/cli/clisqlshell/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
democlusterapi "github.com/cockroachdb/cockroach/pkg/cli/democluster/api"
18+
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
1819
)
1920

2021
// Context represents the external configuration of the interactive
@@ -71,4 +72,11 @@ type internalContext struct {
7172

7273
// current database name, if known. This is maintained on a best-effort basis.
7374
dbName string
75+
76+
// state about the current query.
77+
mu struct {
78+
syncutil.Mutex
79+
cancelFn func()
80+
doneCh chan struct{}
81+
}
7482
}

pkg/cli/clisqlshell/sql.go

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"net/url"
2020
"os"
2121
"os/exec"
22+
"os/signal"
2223
"path/filepath"
2324
"regexp"
2425
"sort"
@@ -1607,10 +1608,11 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum {
16071608
}
16081609

16091610
// Now run the statement/query.
1610-
c.exitErr = c.sqlExecCtx.RunQueryAndFormatResults(
1611-
context.Background(),
1612-
c.conn, c.iCtx.stdout, c.iCtx.stderr,
1613-
clisqlclient.MakeQuery(c.concatLines))
1611+
c.exitErr = c.runWithInterruptableCtx(func(ctx context.Context) error {
1612+
return c.sqlExecCtx.RunQueryAndFormatResults(ctx,
1613+
c.conn, c.iCtx.stdout, c.iCtx.stderr,
1614+
clisqlclient.MakeQuery(c.concatLines))
1615+
})
16141616
if c.exitErr != nil {
16151617
if !c.singleStatement {
16161618
clierror.OutputError(c.iCtx.stderr, c.exitErr, true /*showSeverity*/, false /*verbose*/)
@@ -1640,10 +1642,11 @@ func (c *cliState) doRunStatements(nextState cliStateEnum) cliStateEnum {
16401642
if strings.Contains(c.iCtx.autoTrace, "kv") {
16411643
traceType = "kv"
16421644
}
1643-
if err := c.sqlExecCtx.RunQueryAndFormatResults(
1644-
context.Background(),
1645-
c.conn, c.iCtx.stdout, c.iCtx.stderr,
1646-
clisqlclient.MakeQuery(fmt.Sprintf("SHOW %s TRACE FOR SESSION", traceType))); err != nil {
1645+
if err := c.runWithInterruptableCtx(func(ctx context.Context) error {
1646+
return c.sqlExecCtx.RunQueryAndFormatResults(ctx,
1647+
c.conn, c.iCtx.stdout, c.iCtx.stderr,
1648+
clisqlclient.MakeQuery(fmt.Sprintf("SHOW %s TRACE FOR SESSION", traceType)))
1649+
}); err != nil {
16471650
clierror.OutputError(c.iCtx.stderr, err, true /*showSeverity*/, false /*verbose*/)
16481651
if c.exitErr == nil {
16491652
// Both the query and SET tracing had encountered no error
@@ -1705,6 +1708,9 @@ func NewShell(
17051708

17061709
// RunInteractive implements the Shell interface.
17071710
func (c *cliState) RunInteractive(cmdIn, cmdOut, cmdErr *os.File) (exitErr error) {
1711+
finalFn := c.maybeHandleInterrupt()
1712+
defer finalFn()
1713+
17081714
return c.doRunShell(cliStart, cmdIn, cmdOut, cmdErr)
17091715
}
17101716

@@ -1986,3 +1992,85 @@ func (c *cliState) serverSideParse(sql string) (helpText string, err error) {
19861992
}
19871993
return "", nil
19881994
}
1995+
1996+
func (c *cliState) maybeHandleInterrupt() func() {
1997+
if !c.cliCtx.IsInteractive {
1998+
return func() {}
1999+
}
2000+
intCh := make(chan os.Signal, 1)
2001+
signal.Notify(intCh, os.Interrupt)
2002+
ctx, cancel := context.WithCancel(context.Background())
2003+
go func() {
2004+
for {
2005+
select {
2006+
case <-intCh:
2007+
c.iCtx.mu.Lock()
2008+
cancelFn, doneCh := c.iCtx.mu.cancelFn, c.iCtx.mu.doneCh
2009+
c.iCtx.mu.Unlock()
2010+
if cancelFn == nil {
2011+
// No query currently executing; nothing to do.
2012+
continue
2013+
}
2014+
2015+
fmt.Fprintf(c.iCtx.stderr, "\nattempting to cancel query...\n")
2016+
// Cancel the query's context, which should make the driver
2017+
// send a cancellation message.
2018+
cancelFn()
2019+
2020+
// Now wait for the shell to process the cancellation.
2021+
//
2022+
// If it takes too long (e.g. server has become unresponsive,
2023+
// or we're connected to a pre-22.1 server which does not
2024+
// support cancellation), fall back to the previous behavior
2025+
// which is to interrupt the shell altogether.
2026+
tooLongTimer := time.After(3 * time.Second)
2027+
wait:
2028+
for {
2029+
select {
2030+
case <-doneCh:
2031+
break wait
2032+
case <-tooLongTimer:
2033+
fmt.Fprintln(c.iCtx.stderr, "server does not respond to query cancellation; a second interrupt will stop the shell.")
2034+
signal.Reset(os.Interrupt)
2035+
}
2036+
}
2037+
// Re-arm the signal handler.
2038+
signal.Notify(intCh, os.Interrupt)
2039+
2040+
case <-ctx.Done():
2041+
// Shell is terminating.
2042+
return
2043+
}
2044+
}
2045+
}()
2046+
return cancel
2047+
}
2048+
2049+
func (c *cliState) runWithInterruptableCtx(fn func(ctx context.Context) error) error {
2050+
if !c.cliCtx.IsInteractive {
2051+
return fn(context.Background())
2052+
}
2053+
// The cancellation function can be used by the Ctrl+C handler
2054+
// to cancel this query.
2055+
ctx, cancel := context.WithCancel(context.Background())
2056+
// doneCh will be used on the return path to teach the Ctrl+C
2057+
// handler that the query has been cancelled successfully.
2058+
doneCh := make(chan struct{})
2059+
defer func() { close(doneCh) }()
2060+
2061+
// Inform the Ctrl+C handler that this query is executing.
2062+
c.iCtx.mu.Lock()
2063+
c.iCtx.mu.cancelFn = cancel
2064+
c.iCtx.mu.doneCh = doneCh
2065+
c.iCtx.mu.Unlock()
2066+
defer func() {
2067+
c.iCtx.mu.Lock()
2068+
c.iCtx.mu.cancelFn = nil
2069+
c.iCtx.mu.doneCh = nil
2070+
c.iCtx.mu.Unlock()
2071+
}()
2072+
2073+
// Now run the query.
2074+
err := fn(ctx)
2075+
return err
2076+
}

0 commit comments

Comments
 (0)