Allow websocket conns to be established after New.

ok @davecgh
This commit is contained in:
Josh Rickmar 2014-07-10 13:11:57 -05:00
parent 6b8ff7f52f
commit 160a843171

View file

@ -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
}