From ffa4f045cf7f0a739cf431534cd150362b7c55a8 Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 15:42:55 +0900 Subject: [PATCH 1/7] Rename client.Conn to Transport --- client/client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 98e1faf..c72953d 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,7 @@ type ( Request interface{} Response interface{} - Conn interface { + Transport interface { io.Closer Send(req Request) error Recv() (Response, error) @@ -60,9 +60,9 @@ func (e *RuntimeError) Error() string { return e.err } -func StartLoop(conn Conn, handler Handler, done chan struct{}) { +func StartLoop(transport Transport, handler Handler, done chan struct{}) { for { - resp, err := conn.Recv() + resp, err := transport.Recv() if err != nil { select { case <-done: From 8c79c0281ced422761281d8e3af887ae6335820b Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 16:30:13 +0900 Subject: [PATCH 2/7] Expose conn builder as public I/F --- client/conn_builder.go | 25 +++++++++++++++++++++++++ cmd/trench/helper.go | 6 ++---- nrepl/client.go | 7 ++----- nrepl/nrepl.go | 13 +++---------- nrepl/nrepl_test.go | 4 ++-- prepl/prepl.go | 13 +++---------- prepl/prepl_test.go | 4 ++-- 7 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 client/conn_builder.go diff --git a/client/conn_builder.go b/client/conn_builder.go new file mode 100644 index 0000000..fdcd7ca --- /dev/null +++ b/client/conn_builder.go @@ -0,0 +1,25 @@ +package client + +import ( + "fmt" + "net" +) + +type ConnBuilder interface { + Connect() (net.Conn, error) +} + +type TCPConnBuilder struct { + Host string + Port int +} + +func (conn *TCPConnBuilder) Connect() (net.Conn, error) { + return net.Dial("tcp", fmt.Sprintf("%s:%d", conn.Host, conn.Port)) +} + +type ConnBuilderFunc func() (net.Conn, error) + +func (f ConnBuilderFunc) Connect() (net.Conn, error) { + return f() +} diff --git a/cmd/trench/helper.go b/cmd/trench/helper.go index 91562b2..5854cfb 100644 --- a/cmd/trench/helper.go +++ b/cmd/trench/helper.go @@ -45,8 +45,7 @@ func readPortFromFile(protocol, portFile string) (int, bool, error) { func (h setupHelper) nReplFactory(host string, port int, initNS string) func(client.OutputHandler) client.Client { return func(outHandler client.OutputHandler) client.Client { c, err := nrepl.NewClient(&nrepl.Opts{ - Host: host, - Port: port, + ConnBuilder: &client.TCPConnBuilder{Host: host, Port: port}, InitNS: initNS, OutputHandler: outHandler, ErrorHandler: h.errHandler, @@ -61,8 +60,7 @@ func (h setupHelper) nReplFactory(host string, port int, initNS string) func(cli func (h setupHelper) pReplFactory(host string, port int, initNS string) func(client.OutputHandler) client.Client { return func(outHandler client.OutputHandler) client.Client { c, err := prepl.NewClient(&prepl.Opts{ - Host: host, - Port: port, + ConnBuilder: &client.TCPConnBuilder{Host: host, Port: port}, InitNS: initNS, OutputHandler: outHandler, ErrorHandler: h.errHandler, diff --git a/nrepl/client.go b/nrepl/client.go index 6e4fcdb..450d9ff 100644 --- a/nrepl/client.go +++ b/nrepl/client.go @@ -2,7 +2,6 @@ package nrepl import ( "fmt" - "net" "path/filepath" "strings" "sync" @@ -28,13 +27,11 @@ type ( } Opts struct { - Host string - Port int InitNS string Oneshot bool OutputHandler client.OutputHandler ErrorHandler client.ErrorHandler - connBuilder func(host string, port int) (net.Conn, error) + ConnBuilder client.ConnBuilder idGenerator func() string } ) @@ -52,7 +49,7 @@ func NewClient(opts *Opts) (*Client, error) { pending: map[string]chan client.EvalResult{}, idGenerator: opts.idGenerator, } - conn, err := Connect(&ConnOpts{opts.Host, opts.Port, opts.connBuilder}) + conn, err := Connect(&ConnOpts{opts.ConnBuilder}) if err != nil { return nil, err } diff --git a/nrepl/nrepl.go b/nrepl/nrepl.go index 34b61d5..e7eda33 100644 --- a/nrepl/nrepl.go +++ b/nrepl/nrepl.go @@ -24,9 +24,7 @@ type ( } ConnOpts struct { - Host string - Port int - connBuilder func(host string, port int) (net.Conn, error) + ConnBuilder client.ConnBuilder } SessionInfo struct { @@ -36,13 +34,8 @@ type ( ) func Connect(opts *ConnOpts) (conn *Conn, err error) { - connBuilder := opts.connBuilder - if connBuilder == nil { - connBuilder = func(host string, port int) (net.Conn, error) { - return net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) - } - } - socket, err := connBuilder(opts.Host, opts.Port) + connBuilder := opts.ConnBuilder + socket, err := connBuilder.Connect() if err != nil { return } diff --git a/nrepl/nrepl_test.go b/nrepl/nrepl_test.go index bee5c8b..9860daa 100644 --- a/nrepl/nrepl_test.go +++ b/nrepl/nrepl_test.go @@ -79,9 +79,9 @@ func setupClient(mock *client.MockServer) (*Client, error) { return NewClient(&Opts{ OutputHandler: mock, ErrorHandler: mock, - connBuilder: func(_ string, _ int) (net.Conn, error) { + ConnBuilder: client.ConnBuilderFunc(func() (net.Conn, error) { return mock, nil - }, + }), idGenerator: func() string { return EXEC_ID }, }) } diff --git a/prepl/prepl.go b/prepl/prepl.go index 86dc017..57cd65e 100644 --- a/prepl/prepl.go +++ b/prepl/prepl.go @@ -34,23 +34,16 @@ type ( } Opts struct { - Host string - Port int InitNS string OutputHandler client.OutputHandler ErrorHandler client.ErrorHandler - connBuilder func(host string, port int) (net.Conn, error) + ConnBuilder client.ConnBuilder } ) func NewClient(opts *Opts) (*Client, error) { - connBuilder := opts.connBuilder - if connBuilder == nil { - connBuilder = func(host string, port int) (net.Conn, error) { - return net.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) - } - } - socket, err := connBuilder(opts.Host, opts.Port) + connBuilder := opts.ConnBuilder + socket, err := connBuilder.Connect() if err != nil { return nil, err } diff --git a/prepl/prepl_test.go b/prepl/prepl_test.go index 13f3260..90b0fe4 100644 --- a/prepl/prepl_test.go +++ b/prepl/prepl_test.go @@ -20,9 +20,9 @@ func setupMock(steps []client.Step) *client.MockServer { func setupClient(mock *client.MockServer) (*Client, error) { return NewClient(&Opts{ - connBuilder: func(_ string, _ int) (net.Conn, error) { + ConnBuilder: client.ConnBuilderFunc(func() (net.Conn, error) { return mock, nil - }, + }), OutputHandler: mock, ErrorHandler: mock, }) From 4a52708af9eb657e86a43443bcf8b125bec021d0 Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 16:57:19 +0900 Subject: [PATCH 3/7] Refactor code with conn builder --- cmd/trench/helper.go | 19 +++++++++++-------- cmd/trench/main.go | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cmd/trench/helper.go b/cmd/trench/helper.go index 5854cfb..3ef3e52 100644 --- a/cmd/trench/helper.go +++ b/cmd/trench/helper.go @@ -42,10 +42,10 @@ func readPortFromFile(protocol, portFile string) (int, bool, error) { return port, false, nil } -func (h setupHelper) nReplFactory(host string, port int, initNS string) func(client.OutputHandler) client.Client { +func (h setupHelper) nReplFactory(connBuilder client.ConnBuilder, initNS string) func(client.OutputHandler) client.Client { return func(outHandler client.OutputHandler) client.Client { c, err := nrepl.NewClient(&nrepl.Opts{ - ConnBuilder: &client.TCPConnBuilder{Host: host, Port: port}, + ConnBuilder: connBuilder, InitNS: initNS, OutputHandler: outHandler, ErrorHandler: h.errHandler, @@ -57,10 +57,10 @@ func (h setupHelper) nReplFactory(host string, port int, initNS string) func(cli } } -func (h setupHelper) pReplFactory(host string, port int, initNS string) func(client.OutputHandler) client.Client { +func (h setupHelper) pReplFactory(connBuilder client.ConnBuilder, initNS string) func(client.OutputHandler) client.Client { return func(outHandler client.OutputHandler) client.Client { c, err := prepl.NewClient(&prepl.Opts{ - ConnBuilder: &client.TCPConnBuilder{Host: host, Port: port}, + ConnBuilder: connBuilder, InitNS: initNS, OutputHandler: outHandler, ErrorHandler: h.errHandler, @@ -72,22 +72,24 @@ func (h setupHelper) pReplFactory(host string, port int, initNS string) func(cli } } -func (h setupHelper) setupRepl(protocol string, host string, port int, initNS string, opts *repl.Opts) *repl.Repl { +func (h setupHelper) setupRepl(protocol string, connBuilder client.ConnBuilder, initNS string, opts *repl.Opts) *repl.Repl { opts.In = os.Stdin opts.Out = os.Stdout opts.Err = os.Stderr opts.ErrHandler = h.errHandler var factory func(client.OutputHandler) client.Client if protocol == "nrepl" { - factory = h.nReplFactory(host, port, initNS) + factory = h.nReplFactory(connBuilder, initNS) } else { - factory = h.pReplFactory(host, port, initNS) + factory = h.pReplFactory(connBuilder, initNS) } return repl.NewRepl(opts, factory) } -func (h setupHelper) arbitrateServerInfo(args *cmdArgs) (protocol string, host string, port int) { +func (h setupHelper) resolveConnection(args *cmdArgs) (protocol string, connBuilder client.ConnBuilder) { server := *args.server + var host string + var port int if server != "" { match := urlRegex.FindStringSubmatch(server) if match == nil { @@ -125,5 +127,6 @@ func (h setupHelper) arbitrateServerInfo(args *cmdArgs) (protocol string, host s } port = p } + connBuilder = &client.TCPConnBuilder{Host: host, Port: port} return } diff --git a/cmd/trench/main.go b/cmd/trench/main.go index 58caddb..6aa64ef 100644 --- a/cmd/trench/main.go +++ b/cmd/trench/main.go @@ -95,7 +95,7 @@ func main() { printer := repl.NewPrinter(colorized(*args.colorOption)) errHandler := errorHandler{printer} helper := setupHelper{errHandler} - protocol, host, port := helper.arbitrateServerInfo(&args) + protocol, connBuilder := helper.resolveConnection(&args) initFile := strings.TrimSpace(*args.init) filename := strings.TrimSpace(*args.file) initNS := strings.TrimSpace(*args.initNS) @@ -105,7 +105,7 @@ func main() { Printer: printer, HidesNil: filename != "" || mainNS != "" || code != "", } - repl := helper.setupRepl(protocol, host, port, initNS, opts) + repl := helper.setupRepl(protocol, connBuilder, initNS, opts) defer repl.Close() if initFile != "" { From 6828785f6903bd06492624b54f6a22e0f684a8dc Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 19:43:53 +0900 Subject: [PATCH 4/7] Implement conn builder with retry --- client/conn_builder.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/client/conn_builder.go b/client/conn_builder.go index fdcd7ca..3121ac9 100644 --- a/client/conn_builder.go +++ b/client/conn_builder.go @@ -3,6 +3,7 @@ package client import ( "fmt" "net" + "time" ) type ConnBuilder interface { @@ -23,3 +24,25 @@ type ConnBuilderFunc func() (net.Conn, error) func (f ConnBuilderFunc) Connect() (net.Conn, error) { return f() } + +func NewRetryConnBuilder(connBuilder ConnBuilder, timeout time.Duration, interval time.Duration) ConnBuilder { + if interval > timeout { + interval = timeout + } + return ConnBuilderFunc(func() (conn net.Conn, err error) { + end := time.Now().Add(timeout) + for { + conn, err = connBuilder.Connect() + if err == nil { + return conn, nil + } + if time.Now().After(end) { + return + } + time.Sleep(interval) + if time.Now().After(end) { + return + } + } + }) +} From 0c178c45e77cb71b55c2614a9f3a1c11d910ac1d Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 19:44:35 +0900 Subject: [PATCH 5/7] Add --retry-timeout/--retry-interval options to enable retry connection --- cmd/trench/helper.go | 3 +++ cmd/trench/main.go | 49 ++++++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/cmd/trench/helper.go b/cmd/trench/helper.go index 3ef3e52..3659d0f 100644 --- a/cmd/trench/helper.go +++ b/cmd/trench/helper.go @@ -128,5 +128,8 @@ func (h setupHelper) resolveConnection(args *cmdArgs) (protocol string, connBuil port = p } connBuilder = &client.TCPConnBuilder{Host: host, Port: port} + if *args.retryTimeout > 0 { + connBuilder = client.NewRetryConnBuilder(connBuilder, *args.retryTimeout, *args.retryInterval) + } return } diff --git a/cmd/trench/main.go b/cmd/trench/main.go index 6aa64ef..7121c47 100644 --- a/cmd/trench/main.go +++ b/cmd/trench/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/athos/trenchman/client" "github.com/athos/trenchman/repl" @@ -21,17 +22,19 @@ const ( ) type cmdArgs struct { - port *int - portfile *string - protocol *string - server *string - init *string - eval *string - file *string - mainNS *string - initNS *string - colorOption *string - args *[]string + port *int + portfile *string + protocol *string + server *string + retryTimeout *time.Duration + retryInterval *time.Duration + init *string + eval *string + file *string + mainNS *string + initNS *string + colorOption *string + args *[]string } type errorHandler struct { @@ -51,17 +54,19 @@ func (h errorHandler) HandleErr(err error) { } var args = cmdArgs{ - port: kingpin.Flag("port", "Connect to the specified port.").Short('p').Int(), - portfile: kingpin.Flag("port-file", "Specify port file that specifies port to connect to. Defaults to .nrepl-port.").PlaceHolder("FILE").String(), - protocol: kingpin.Flag("protocol", "Use the specified protocol. Possible values: n[repl], p[repl]. Defaults to nrepl.").Default("nrepl").Short('P').Enum("n", "nrepl", "p", "prepl"), - server: kingpin.Flag("server", "Connect to the specified URL (e.g. prepl://127.0.0.1:5555).").Default("127.0.0.1").Short('s').PlaceHolder("[(nrepl|prepl)://]host[:port]").String(), - init: kingpin.Flag("init", "Load a file before execution.").Short('i').PlaceHolder("FILE").String(), - eval: kingpin.Flag("eval", "Evaluate an expression.").Short('e').PlaceHolder("EXPR").String(), - file: kingpin.Flag("file", "Evaluate a file.").Short('f').String(), - mainNS: kingpin.Flag("main", "Call the -main function for a namespace.").Short('m').PlaceHolder("NAMESPACE").String(), - initNS: kingpin.Flag("init-ns", "Initialize REPL with the specified namespace. Defaults to \"user\".").PlaceHolder("NAMESPACE").String(), - colorOption: kingpin.Flag("color", "When to use colors. Possible values: always, auto, none. Defaults to auto.").Default(COLOR_AUTO).Short('C').Enum(COLOR_NONE, COLOR_AUTO, COLOR_ALWAYS), - args: kingpin.Arg("args", "Arguments to pass to -main. These will be ignored unless -m is specified.").Strings(), + port: kingpin.Flag("port", "Connect to the specified port.").Short('p').Int(), + portfile: kingpin.Flag("port-file", "Specify port file that specifies port to connect to. Defaults to .nrepl-port.").PlaceHolder("FILE").String(), + protocol: kingpin.Flag("protocol", "Use the specified protocol. Possible values: n[repl], p[repl]. Defaults to nrepl.").Default("nrepl").Short('P').Enum("n", "nrepl", "p", "prepl"), + server: kingpin.Flag("server", "Connect to the specified URL (e.g. prepl://127.0.0.1:5555).").Default("127.0.0.1").Short('s').PlaceHolder("[(nrepl|prepl)://]host[:port]").String(), + retryTimeout: kingpin.Flag("retry-timeout", "Timeout after which retries are aborted. By default, Trenchman never retries connection.").PlaceHolder("DURATION").Duration(), + retryInterval: kingpin.Flag("retry-interval", "Interval between retries when connecting to the server.").Default("1s").Duration(), + init: kingpin.Flag("init", "Load a file before execution.").Short('i').PlaceHolder("FILE").String(), + eval: kingpin.Flag("eval", "Evaluate an expression.").Short('e').PlaceHolder("EXPR").String(), + file: kingpin.Flag("file", "Evaluate a file.").Short('f').String(), + mainNS: kingpin.Flag("main", "Call the -main function for a namespace.").Short('m').PlaceHolder("NAMESPACE").String(), + initNS: kingpin.Flag("init-ns", "Initialize REPL with the specified namespace. Defaults to \"user\".").PlaceHolder("NAMESPACE").String(), + colorOption: kingpin.Flag("color", "When to use colors. Possible values: always, auto, none. Defaults to auto.").Default(COLOR_AUTO).Short('C').Enum(COLOR_NONE, COLOR_AUTO, COLOR_ALWAYS), + args: kingpin.Arg("args", "Arguments to pass to -main. These will be ignored unless -m is specified.").Strings(), } func colorized(colorOption string) bool { From 477686afa4f95d5d1309d153c3c8e82944fa2702 Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 20:59:29 +0900 Subject: [PATCH 6/7] Add --retry-timeout/--retry-interval to options list on document --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 82782ed..c390754 100644 --- a/README.md +++ b/README.md @@ -62,19 +62,21 @@ Trenchman does not have `readline` support at this time. If you want to use feat usage: trench [] [...] Flags: - --help Show context-sensitive help (also try --help-long and --help-man). - -p, --port=PORT Connect to the specified port. - --port-file=FILE Specify port file that specifies port to connect to. Defaults to .nrepl-port. - -P, --protocol=nrepl Use the specified protocol. Possible values: n[repl], p[repl]. Defaults to nrepl. + --help Show context-sensitive help (also try --help-long and --help-man). + -p, --port=PORT Connect to the specified port. + --port-file=FILE Specify port file that specifies port to connect to. Defaults to .nrepl-port. + -P, --protocol=nrepl Use the specified protocol. Possible values: n[repl], p[repl]. Defaults to nrepl. -s, --server=[(nrepl|prepl)://]host[:port] - Connect to the specified URL (e.g. prepl://127.0.0.1:5555). - -i, --init=FILE Load a file before execution. - -e, --eval=EXPR Evaluate an expression. - -f, --file=FILE Evaluate a file. - -m, --main=NAMESPACE Call the -main function for a namespace. - --init-ns=NAMESPACE Initialize REPL with the specified namespace. Defaults to "user". - -C, --color=auto When to use colors. Possible values: always, auto, none. Defaults to auto. - --version Show application version. + Connect to the specified URL (e.g. prepl://127.0.0.1:5555). + --retry-timeout=DURATION Timeout after which retries are aborted. By default, Trenchman never retries connection. + --retry-interval=1s Interval between retries when connecting to the server. + -i, --init=FILE Load a file before execution. + -e, --eval=EXPR Evaluate an expression. + -f, --file=FILE Evaluate a file. + -m, --main=NAMESPACE Call the -main function for a namespace. + --init-ns=NAMESPACE Initialize REPL with the specified namespace. Defaults to "user". + -C, --color=auto When to use colors. Possible values: always, auto, none. Defaults to auto. + --version Show application version. Args: [] Arguments to pass to -main. These will be ignored unless -m is specified. From fb11db05fb83c6c0b9d16dd009a31eb3859485ab Mon Sep 17 00:00:00 2001 From: Shogo Ohta Date: Sun, 5 Sep 2021 21:51:33 +0900 Subject: [PATCH 7/7] Document connection retry --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index c390754..38ca294 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Unlike ordinary Clojure REPLs, it starts up instantly as it just connects to a r - [Usage](#usage) - [Connecting to a server](#connecting-to-a-server) - [Port file](#port-file) + - [Retry on connection](#retry-on-connection) - [Evaluation](#evaluation) - [Evaluating an expression (`-e`)](#evaluating-an-expression--e) - [Evaluating a file (`-f`)](#evaluating-a-file--f) @@ -140,6 +141,23 @@ $ cat my-port-file $ trench --port-file my-port-file ``` +#### Retry on connection + +When connecting to a server that is starting up, it's useful to be able to automatically retry the connection if it fails. + +The `--retry-timeout` and `--retry-interval` options control connection retries. +`--retry-timeout DURATION` specifies the amount of time before connection retries are aborted and `--retry-interval DURATION` specifies the time interval between each retry (`DURATION` can be specified in the format accepted by [Go's duration parser](https://pkg.go.dev/time#ParseDuration), like `500ms`, `10s` or `1m`). + +For example, the following command will retry the connection every 5 seconds for up to 30 seconds: + +```sh +$ trench --retry-timeout 30s --retry-interval 5s +``` + +If the connection fails after retrying the connection until the timeout, Trenchman will print the error and exit. + +If `--retry-timeout` is not specified, Trenchman will not retry the connection. + ### Evaluation By default, Trenchman starts a new REPL session after the connection is established: