Skip to content

Commit

Permalink
Merge pull request #7 from athos/feature/retry-connection
Browse files Browse the repository at this point in the history
Retry on connection
  • Loading branch information
athos authored Sep 6, 2021
2 parents 3f3e465 + fb11db0 commit 52da7bc
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 78 deletions.
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -62,19 +63,21 @@ Trenchman does not have `readline` support at this time. If you want to use feat
usage: trench [<flags>] [<args>...]
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:
[<args>] Arguments to pass to -main. These will be ignored unless -m is specified.
Expand Down Expand Up @@ -138,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:
Expand Down
6 changes: 3 additions & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type (
Request interface{}
Response interface{}

Conn interface {
Transport interface {
io.Closer
Send(req Request) error
Recv() (Response, error)
Expand Down Expand Up @@ -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:
Expand Down
48 changes: 48 additions & 0 deletions client/conn_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package client

import (
"fmt"
"net"
"time"
)

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()
}

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
}
}
})
}
24 changes: 14 additions & 10 deletions cmd/trench/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +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{
Host: host,
Port: port,
ConnBuilder: connBuilder,
InitNS: initNS,
OutputHandler: outHandler,
ErrorHandler: h.errHandler,
Expand All @@ -58,11 +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{
Host: host,
Port: port,
ConnBuilder: connBuilder,
InitNS: initNS,
OutputHandler: outHandler,
ErrorHandler: h.errHandler,
Expand All @@ -74,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 {
Expand Down Expand Up @@ -127,5 +127,9 @@ func (h setupHelper) arbitrateServerInfo(args *cmdArgs) (protocol string, host s
}
port = p
}
connBuilder = &client.TCPConnBuilder{Host: host, Port: port}
if *args.retryTimeout > 0 {
connBuilder = client.NewRetryConnBuilder(connBuilder, *args.retryTimeout, *args.retryInterval)
}
return
}
53 changes: 29 additions & 24 deletions cmd/trench/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/athos/trenchman/client"
"github.com/athos/trenchman/repl"
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -95,7 +100,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)
Expand All @@ -105,7 +110,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 != "" {
Expand Down
7 changes: 2 additions & 5 deletions nrepl/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package nrepl

import (
"fmt"
"net"
"path/filepath"
"strings"
"sync"
Expand All @@ -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
}
)
Expand All @@ -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
}
Expand Down
13 changes: 3 additions & 10 deletions nrepl/nrepl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions nrepl/nrepl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
})
}
Expand Down
13 changes: 3 additions & 10 deletions prepl/prepl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 52da7bc

Please sign in to comment.