2020-08-06 20:58:47 +03:00
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-12-18 07:31:06 +03:00
|
|
|
"errors"
|
2023-04-30 17:18:56 +03:00
|
|
|
"fmt"
|
2020-08-06 20:58:47 +03:00
|
|
|
"time"
|
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
"github.com/neilotoole/sq/cli/run"
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/options"
|
|
|
|
|
2023-04-19 08:28:09 +03:00
|
|
|
"github.com/neilotoole/sq/cli/flag"
|
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
"github.com/samber/lo"
|
|
|
|
|
2023-04-02 22:49:45 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
|
|
|
|
|
|
|
"github.com/neilotoole/sq/libsq/core/lg"
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-04-02 22:49:45 +03:00
|
|
|
"github.com/spf13/cobra"
|
2020-08-06 20:58:47 +03:00
|
|
|
|
|
|
|
"github.com/neilotoole/sq/cli/output"
|
2020-08-23 13:42:15 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/errz"
|
2020-08-06 20:58:47 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/driver"
|
|
|
|
"github.com/neilotoole/sq/libsq/source"
|
|
|
|
)
|
|
|
|
|
2023-04-26 18:16:42 +03:00
|
|
|
// OptPingTimeout controls ping timeout.
|
|
|
|
var OptPingTimeout = options.NewDuration(
|
|
|
|
"ping.timeout",
|
2023-05-07 05:36:34 +03:00
|
|
|
0,
|
2023-04-26 18:16:42 +03:00
|
|
|
time.Second*10,
|
2023-05-07 05:36:34 +03:00
|
|
|
"ping timeout duration",
|
|
|
|
"How long to wait before ping timeout occurs. For example: 500ms or 2m10s.",
|
2023-04-26 18:16:42 +03:00
|
|
|
)
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
func newPingCmd() *cobra.Command {
|
2020-08-06 20:58:47 +03:00
|
|
|
cmd := &cobra.Command{
|
2023-04-16 01:28:51 +03:00
|
|
|
Use: "ping [@HANDLE|GROUP]*",
|
2021-02-22 10:37:00 +03:00
|
|
|
RunE: execPing,
|
2023-04-16 01:28:51 +03:00
|
|
|
ValidArgsFunction: completeHandleOrGroup,
|
2021-02-22 10:37:00 +03:00
|
|
|
|
|
|
|
Short: "Ping data sources",
|
2023-04-16 01:28:51 +03:00
|
|
|
Long: `Ping data sources (or groups of sources) to check connection health.
|
|
|
|
If no arguments provided, the active data source is pinged. Otherwise, ping
|
|
|
|
the specified sources or groups.
|
2021-02-22 10:37:00 +03:00
|
|
|
|
|
|
|
The exit code is 1 if ping fails for any of the sources.`,
|
2023-04-16 01:28:51 +03:00
|
|
|
Example: ` # Ping active data source.
|
2021-02-22 10:37:00 +03:00
|
|
|
$ sq ping
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
# Ping @my1 and @pg1.
|
2021-02-22 10:37:00 +03:00
|
|
|
$ sq ping @my1 @pg1
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
# Ping sources in the root group (i.e. all sources).
|
|
|
|
$ sq ping /
|
|
|
|
|
|
|
|
# Ping sources in the "prod" and "staging" groups.
|
|
|
|
$ sq ping prod staging
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
# Ping @my1 with 2s timeout.
|
|
|
|
$ sq ping @my1 --timeout 2s
|
|
|
|
|
|
|
|
# Output in TSV format.
|
2021-02-22 10:37:00 +03:00
|
|
|
$ sq ping --tsv @my1`,
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
2023-05-03 15:36:10 +03:00
|
|
|
cmd.Flags().BoolP(flag.JSON, flag.JSONShort, false, flag.JSONUsage)
|
2023-04-19 08:28:09 +03:00
|
|
|
cmd.Flags().BoolP(flag.CSV, flag.CSVShort, false, flag.CSVUsage)
|
|
|
|
cmd.Flags().BoolP(flag.TSV, flag.TSVShort, false, flag.TSVUsage)
|
2023-05-05 20:41:22 +03:00
|
|
|
cmd.Flags().BoolP(flag.Compact, flag.CompactShort, false, flag.CompactUsage)
|
2023-05-05 17:32:50 +03:00
|
|
|
|
2023-04-19 08:28:09 +03:00
|
|
|
cmd.Flags().Duration(flag.PingTimeout, time.Second*10, flag.PingTimeoutUsage)
|
2021-02-22 10:37:00 +03:00
|
|
|
return cmd
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
2021-02-22 10:37:00 +03:00
|
|
|
func execPing(cmd *cobra.Command, args []string) error {
|
2023-05-19 17:24:18 +03:00
|
|
|
ru := run.FromContext(cmd.Context())
|
|
|
|
cfg, coll := ru.Config, ru.Config.Collection
|
2020-08-06 20:58:47 +03:00
|
|
|
var srcs []*source.Source
|
|
|
|
|
|
|
|
// args can be:
|
|
|
|
// [empty] : ping active source
|
|
|
|
// @handle1 @handleN: ping multiple sources
|
2023-04-16 01:28:51 +03:00
|
|
|
// @handle1 group1: ping sources, or those in groups.
|
2021-02-27 17:44:08 +03:00
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
args = lo.Uniq(args)
|
|
|
|
if len(args) == 0 {
|
2023-04-19 08:28:09 +03:00
|
|
|
src := cfg.Collection.Active()
|
2020-08-06 20:58:47 +03:00
|
|
|
if src == nil {
|
|
|
|
return errz.New(msgNoActiveSrc)
|
|
|
|
}
|
|
|
|
srcs = []*source.Source{src}
|
2023-04-16 01:28:51 +03:00
|
|
|
} else {
|
2021-02-27 17:44:08 +03:00
|
|
|
for _, arg := range args {
|
2023-04-16 01:28:51 +03:00
|
|
|
switch {
|
|
|
|
case source.IsValidHandle(arg):
|
2023-04-19 08:28:09 +03:00
|
|
|
src, err := coll.Get(arg)
|
2023-04-16 01:28:51 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
srcs = append(srcs, src)
|
|
|
|
case source.IsValidGroup(arg):
|
2023-04-19 08:28:09 +03:00
|
|
|
groupSrcs, err := coll.SourcesInGroup(arg)
|
2023-04-16 01:28:51 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
srcs = append(srcs, groupSrcs...)
|
|
|
|
default:
|
|
|
|
return errz.Errorf("invalid arg: %s", arg)
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-16 01:28:51 +03:00
|
|
|
srcs = lo.Uniq(srcs)
|
|
|
|
|
2023-05-03 15:36:10 +03:00
|
|
|
cmdOpts, err := getOptionsFromCmd(cmd)
|
2023-04-26 18:16:42 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
2023-04-26 18:16:42 +03:00
|
|
|
timeout := OptPingTimeout.Get(cmdOpts)
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-04-30 17:18:56 +03:00
|
|
|
logFrom(cmd).Debug("Using ping timeout", lga.Val, fmt.Sprintf("%v", timeout))
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-05-19 17:24:18 +03:00
|
|
|
err = pingSources(cmd.Context(), ru.DriverRegistry, srcs, ru.Writers.Ping, timeout)
|
2023-01-01 09:00:07 +03:00
|
|
|
if errors.Is(err, context.Canceled) {
|
|
|
|
// It's common to cancel "sq ping". We don't want to print the cancel message.
|
|
|
|
return errNoMsg
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// pingSources pings each of the sources in srcs, and prints results
|
|
|
|
// to w. If any error occurs pinging any of srcs, that error is printed
|
|
|
|
// inline as part of the ping results, and an errNoMsg is returned.
|
2021-02-22 10:37:00 +03:00
|
|
|
//
|
2023-01-01 09:00:07 +03:00
|
|
|
// NOTE: This ping code has an ancient lineage, in that it was
|
|
|
|
// originally laid down before context.Context was a thing. Thus,
|
|
|
|
// the entire thing could probably be rewritten for simplicity.
|
2023-04-02 22:49:45 +03:00
|
|
|
func pingSources(ctx context.Context, dp driver.Provider, srcs []*source.Source,
|
|
|
|
w output.PingWriter, timeout time.Duration,
|
2022-12-18 11:35:59 +03:00
|
|
|
) error {
|
2023-01-01 09:00:07 +03:00
|
|
|
if err := w.Open(srcs); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-04-02 22:49:45 +03:00
|
|
|
|
2023-05-03 15:36:10 +03:00
|
|
|
log := lg.FromContext(ctx)
|
2023-04-02 22:49:45 +03:00
|
|
|
defer lg.WarnIfFuncError(log, "Close ping writer", w.Close)
|
2020-08-06 20:58:47 +03:00
|
|
|
|
|
|
|
resultCh := make(chan pingResult, len(srcs))
|
|
|
|
|
|
|
|
// pingErrExists is set to true if there was an error for
|
|
|
|
// any of the pings. This later determines if an error
|
|
|
|
// is returned from this func.
|
|
|
|
var pingErrExists bool
|
|
|
|
|
|
|
|
for _, src := range srcs {
|
|
|
|
go pingSource(ctx, dp, src, timeout, resultCh)
|
|
|
|
}
|
|
|
|
|
|
|
|
// This func doesn't check for context.Canceled itself; instead
|
|
|
|
// it checks if any of the goroutines return that value on
|
|
|
|
// resultCh.
|
|
|
|
for i := 0; i < len(srcs); i++ {
|
|
|
|
result := <-resultCh
|
|
|
|
|
|
|
|
switch {
|
2022-12-18 07:31:06 +03:00
|
|
|
case errors.Is(result.err, context.Canceled):
|
2020-08-06 20:58:47 +03:00
|
|
|
// If any one of the goroutines have received context.Canceled,
|
|
|
|
// then we'll bubble that up and ignore the remaining goroutines.
|
|
|
|
return context.Canceled
|
|
|
|
|
2022-12-18 07:31:06 +03:00
|
|
|
case errors.Is(result.err, context.DeadlineExceeded):
|
2020-08-06 20:58:47 +03:00
|
|
|
// If timeout occurred, set the duration to timeout.
|
|
|
|
result.duration = timeout
|
|
|
|
pingErrExists = true
|
|
|
|
|
|
|
|
case result.err != nil:
|
|
|
|
pingErrExists = true
|
|
|
|
}
|
|
|
|
|
2023-04-02 22:49:45 +03:00
|
|
|
err := w.Result(result.src, result.duration, result.err)
|
|
|
|
lg.WarnIfError(log, "Print ping result", err)
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// If there's at least one error, we return the
|
|
|
|
// sentinel errNoMsg so that sq can os.Exit(1) without printing
|
|
|
|
// an additional error message (as the error message will already have
|
|
|
|
// been printed by PingWriter).
|
|
|
|
if pingErrExists {
|
|
|
|
return errNoMsg
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// pingSource pings an individual driver.Source. It always returns a
|
|
|
|
// result on resultCh, even when ctx is done.
|
2022-12-18 05:43:53 +03:00
|
|
|
func pingSource(ctx context.Context, dp driver.Provider, src *source.Source, timeout time.Duration,
|
2022-12-18 11:35:59 +03:00
|
|
|
resultCh chan<- pingResult,
|
|
|
|
) {
|
2020-08-06 20:58:47 +03:00
|
|
|
drvr, err := dp.DriverFor(src.Type)
|
|
|
|
if err != nil {
|
|
|
|
resultCh <- pingResult{src: src, err: err}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if timeout > 0 {
|
|
|
|
var cancelFn context.CancelFunc
|
|
|
|
ctx, cancelFn = context.WithTimeout(ctx, timeout)
|
|
|
|
defer cancelFn()
|
|
|
|
}
|
|
|
|
|
|
|
|
doneCh := make(chan pingResult)
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
err = drvr.Ping(ctx, src)
|
|
|
|
doneCh <- pingResult{src: src, duration: time.Since(start), err: err}
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
resultCh <- pingResult{src: src, err: ctx.Err()}
|
|
|
|
case result := <-doneCh:
|
|
|
|
resultCh <- result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type pingResult struct {
|
|
|
|
src *source.Source
|
|
|
|
duration time.Duration
|
|
|
|
err error
|
|
|
|
}
|