Allow websocket conns to be established after New.
ok @davecgh
This commit is contained in:
parent
6b8ff7f52f
commit
160a843171
1 changed files with 135 additions and 28 deletions
|
@ -37,6 +37,12 @@ var (
|
||||||
ErrInvalidEndpoint = errors.New("the endpoint either does not support " +
|
ErrInvalidEndpoint = errors.New("the endpoint either does not support " +
|
||||||
"websockets or does not exist")
|
"websockets or does not exist")
|
||||||
|
|
||||||
|
// ErrClientNotConnected is an error to describe the condition where a
|
||||||
|
// websocket client has been created, but the connection was never
|
||||||
|
// established. This condition differs from ErrClientDisconnect, which
|
||||||
|
// represents an established connection that was lost.
|
||||||
|
ErrClientNotConnected = errors.New("the client was never connected")
|
||||||
|
|
||||||
// ErrClientDisconnect is an error to describe the condition where the
|
// ErrClientDisconnect is an error to describe the condition where the
|
||||||
// client has been disconnected from the RPC server. When the
|
// client has been disconnected from the RPC server. When the
|
||||||
// DisableAutoReconnect option is not set, any outstanding futures
|
// DisableAutoReconnect option is not set, any outstanding futures
|
||||||
|
@ -49,6 +55,18 @@ var (
|
||||||
// down. Any outstanding futures when a client shutdown occurs will
|
// down. Any outstanding futures when a client shutdown occurs will
|
||||||
// return this error as will any new requests.
|
// return this error as will any new requests.
|
||||||
ErrClientShutdown = errors.New("the client has been shutdown")
|
ErrClientShutdown = errors.New("the client has been shutdown")
|
||||||
|
|
||||||
|
// ErrNotWebsocketClient is an error to describe the condition of
|
||||||
|
// calling a Client method intended for a websocket client when the
|
||||||
|
// client has been configured to run in HTTP POST mode instead.
|
||||||
|
ErrNotWebsocketClient = errors.New("client is not configured for " +
|
||||||
|
"websockets")
|
||||||
|
|
||||||
|
// ErrClientAlreadyConnected is an error to describe the condition where
|
||||||
|
// a new client connection cannot be established due to a websocket
|
||||||
|
// client having already connected to the RPC server.
|
||||||
|
ErrClientAlreadyConnected = errors.New("websocket client has already " +
|
||||||
|
"connected")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -127,11 +145,12 @@ type Client struct {
|
||||||
ntfnState *notificationState
|
ntfnState *notificationState
|
||||||
|
|
||||||
// Networking infrastructure.
|
// Networking infrastructure.
|
||||||
sendChan chan []byte
|
sendChan chan []byte
|
||||||
sendPostChan chan *sendPostDetails
|
sendPostChan chan *sendPostDetails
|
||||||
disconnect chan struct{}
|
connEstablished chan struct{}
|
||||||
shutdown chan struct{}
|
disconnect chan struct{}
|
||||||
wg sync.WaitGroup
|
shutdown chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextID returns the next id to be used when sending a JSON-RPC message. This
|
// NextID returns the next id to be used when sending a JSON-RPC message. This
|
||||||
|
@ -792,6 +811,15 @@ func (c *Client) sendCmd(cmd btcjson.Cmd) chan *response {
|
||||||
return responseChan
|
return responseChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check whether the websocket connection has never been established,
|
||||||
|
// in which case the handler goroutines are not running.
|
||||||
|
select {
|
||||||
|
case <-c.connEstablished:
|
||||||
|
default:
|
||||||
|
responseChan <- &response{err: ErrClientNotConnected}
|
||||||
|
return responseChan
|
||||||
|
}
|
||||||
|
|
||||||
err := c.addRequest(cmd.Id().(uint64), &jsonRequest{
|
err := c.addRequest(cmd.Id().(uint64), &jsonRequest{
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
responseChan: responseChan,
|
responseChan: responseChan,
|
||||||
|
@ -813,12 +841,18 @@ func (c *Client) sendCmdAndWait(cmd btcjson.Cmd) (interface{}, error) {
|
||||||
return receiveFuture(c.sendCmd(cmd))
|
return receiveFuture(c.sendCmd(cmd))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnected returns whether or not the server is disconnected.
|
// Disconnected returns whether or not the server is disconnected. If a
|
||||||
|
// websocket client was created but never connected, this also returns false.
|
||||||
func (c *Client) Disconnected() bool {
|
func (c *Client) Disconnected() bool {
|
||||||
c.mtx.Lock()
|
c.mtx.Lock()
|
||||||
defer c.mtx.Unlock()
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
return c.disconnected
|
select {
|
||||||
|
case <-c.connEstablished:
|
||||||
|
return c.disconnected
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doDisconnect disconnects the websocket associated with the client if it
|
// doDisconnect disconnects the websocket associated with the client if it
|
||||||
|
@ -841,7 +875,9 @@ func (c *Client) doDisconnect() bool {
|
||||||
|
|
||||||
log.Tracef("Disconnecting RPC client %s", c.config.Host)
|
log.Tracef("Disconnecting RPC client %s", c.config.Host)
|
||||||
close(c.disconnect)
|
close(c.disconnect)
|
||||||
c.wsConn.Close()
|
if c.wsConn != nil {
|
||||||
|
c.wsConn.Close()
|
||||||
|
}
|
||||||
c.disconnected = true
|
c.disconnected = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -922,7 +958,7 @@ func (c *Client) Shutdown() {
|
||||||
c.doDisconnect()
|
c.doDisconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins processing input and output messages.
|
// start begins processing input and output messages.
|
||||||
func (c *Client) start() {
|
func (c *Client) start() {
|
||||||
log.Tracef("Starting RPC client %s", c.config.Host)
|
log.Tracef("Starting RPC client %s", c.config.Host)
|
||||||
|
|
||||||
|
@ -998,6 +1034,12 @@ type ConnConfig struct {
|
||||||
// try to reconnect to the server when it has been disconnected.
|
// try to reconnect to the server when it has been disconnected.
|
||||||
DisableAutoReconnect bool
|
DisableAutoReconnect bool
|
||||||
|
|
||||||
|
// DisableConnectOnNew specifies that a websocket client connection
|
||||||
|
// should not be tried when creating the client with New. Instead, the
|
||||||
|
// client is created and returned unconnected, and Connect must be
|
||||||
|
// called manually.
|
||||||
|
DisableConnectOnNew bool
|
||||||
|
|
||||||
// HttpPostMode instructs the client to run using multiple independent
|
// HttpPostMode instructs the client to run using multiple independent
|
||||||
// connections issuing HTTP POST requests instead of using the default
|
// connections issuing HTTP POST requests instead of using the default
|
||||||
// of websockets. Websockets are generally preferred as some of the
|
// of websockets. Websockets are generally preferred as some of the
|
||||||
|
@ -1119,8 +1161,11 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
|
||||||
// when running in HTTP POST mode.
|
// when running in HTTP POST mode.
|
||||||
var wsConn *websocket.Conn
|
var wsConn *websocket.Conn
|
||||||
var httpClient *http.Client
|
var httpClient *http.Client
|
||||||
|
connEstablished := make(chan struct{})
|
||||||
|
var start bool
|
||||||
if config.HttpPostMode {
|
if config.HttpPostMode {
|
||||||
ntfnHandlers = nil
|
ntfnHandlers = nil
|
||||||
|
start = true
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
httpClient, err = newHTTPClient(config)
|
httpClient, err = newHTTPClient(config)
|
||||||
|
@ -1128,34 +1173,96 @@ func New(config *ConnConfig, ntfnHandlers *NotificationHandlers) (*Client, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
if !config.DisableConnectOnNew {
|
||||||
wsConn, err = dial(config)
|
var err error
|
||||||
if err != nil {
|
wsConn, err = dial(config)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
start = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Infof("Established connection to RPC server %s",
|
log.Infof("Established connection to RPC server %s",
|
||||||
config.Host)
|
config.Host)
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
config: config,
|
config: config,
|
||||||
wsConn: wsConn,
|
wsConn: wsConn,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
requestMap: make(map[uint64]*list.Element),
|
requestMap: make(map[uint64]*list.Element),
|
||||||
requestList: list.New(),
|
requestList: list.New(),
|
||||||
ntfnHandlers: ntfnHandlers,
|
ntfnHandlers: ntfnHandlers,
|
||||||
ntfnState: newNotificationState(),
|
ntfnState: newNotificationState(),
|
||||||
sendChan: make(chan []byte, sendBufferSize),
|
sendChan: make(chan []byte, sendBufferSize),
|
||||||
sendPostChan: make(chan *sendPostDetails, sendPostBufferSize),
|
sendPostChan: make(chan *sendPostDetails, sendPostBufferSize),
|
||||||
disconnect: make(chan struct{}),
|
connEstablished: connEstablished,
|
||||||
shutdown: make(chan struct{}),
|
disconnect: make(chan struct{}),
|
||||||
|
shutdown: make(chan struct{}),
|
||||||
}
|
}
|
||||||
client.start()
|
|
||||||
|
|
||||||
if !client.config.HttpPostMode && !client.config.DisableAutoReconnect {
|
if start {
|
||||||
client.wg.Add(1)
|
close(connEstablished)
|
||||||
go client.wsReconnectHandler()
|
client.start()
|
||||||
|
if !client.config.HttpPostMode && !client.config.DisableAutoReconnect {
|
||||||
|
client.wg.Add(1)
|
||||||
|
go client.wsReconnectHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connect establishes the initial websocket connection. This is necessary when
|
||||||
|
// a client was created after setting the DisableConnectOnNew field of the
|
||||||
|
// Config struct.
|
||||||
|
//
|
||||||
|
// Up to tries number of connections (each after an increasing backoff) will
|
||||||
|
// be tried if the connection can not be established. The special value of 0
|
||||||
|
// indicates an unlimited number of connection attempts.
|
||||||
|
//
|
||||||
|
// This method will error if the client is not configured for websockets, if the
|
||||||
|
// connection has already been established, or if none of the connection
|
||||||
|
// attempts were successful.
|
||||||
|
func (c *Client) Connect(tries int) error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if c.config.HttpPostMode {
|
||||||
|
return ErrNotWebsocketClient
|
||||||
|
}
|
||||||
|
if c.wsConn != nil {
|
||||||
|
return ErrClientAlreadyConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin connection attempts. Increase the backoff after each failed
|
||||||
|
// attempt, up to a maximum of one minute.
|
||||||
|
var err error
|
||||||
|
var backoff time.Duration
|
||||||
|
for i := 0; tries == 0 || i < tries; i++ {
|
||||||
|
var wsConn *websocket.Conn
|
||||||
|
wsConn, err = dial(c.config)
|
||||||
|
if err != nil {
|
||||||
|
backoff = connectionRetryInterval * time.Duration(i+1)
|
||||||
|
if backoff > time.Minute {
|
||||||
|
backoff = time.Minute
|
||||||
|
}
|
||||||
|
time.Sleep(backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection was established. Set the websocket connection
|
||||||
|
// member of the client and start the goroutines necessary
|
||||||
|
// to run the client.
|
||||||
|
c.wsConn = wsConn
|
||||||
|
close(c.connEstablished)
|
||||||
|
c.start()
|
||||||
|
if !c.config.DisableAutoReconnect {
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.wsReconnectHandler()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All connection attempts failed, so return the last error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue