11package massivews
22
33import (
4+ "context"
45 "encoding/json"
56 "errors"
67 "fmt"
@@ -18,10 +19,11 @@ import (
1819)
1920
2021const (
21- writeWait = 5 * time .Second
22- pongWait = 30 * time .Second
23- pingPeriod = pongWait - 5 * time .Second // send ping 5 seconds before deadline
24- maxMessageSize = 1_000_000 // 1MB
22+ writeWait = 5 * time .Second
23+ pongWait = 30 * time .Second
24+ pingPeriod = pongWait - 5 * time .Second // send ping 5 seconds before deadline
25+ maxMessageSize = 1_000_000 // 1MB
26+ clientMaxBackoffInterval = 60 * time .Second
2527)
2628
2729// Client defines a client to the Massive WebSocket API.
@@ -31,8 +33,10 @@ type Client struct {
3133 market Market
3234 url string
3335
34- shouldClose bool
36+ closeCtx context.Context
37+ closeCtxFn context.CancelFunc
3538 backoff backoff.BackOff
39+ connectTime time.Time
3640
3741 mtx sync.Mutex
3842 rwtomb tomb.Tomb
@@ -57,12 +61,15 @@ func New(config Config) (*Client, error) {
5761 if err := config .validate (); err != nil {
5862 return nil , fmt .Errorf ("invalid client options: %w" , err )
5963 }
60-
64+ expBackoff := backoff .NewExponentialBackOff ()
65+ expBackoff .MaxInterval = clientMaxBackoffInterval
66+ // The client should give up if it is in a reconnect loop for too long.
67+ expBackoff .MaxElapsedTime = time .Hour
6168 c := & Client {
6269 apiKey : config .APIKey ,
6370 feed : config .Feed ,
6471 market : config .Market ,
65- backoff : backoff . NewExponentialBackOff () ,
72+ backoff : expBackoff ,
6673 rQueue : make (chan json.RawMessage , 10000 ),
6774 wQueue : make (chan json.RawMessage , 1000 ),
6875 subs : make (subscriptions ),
@@ -98,11 +105,12 @@ func (c *Client) Connect() error {
98105 if c .conn != nil {
99106 return nil
100107 }
108+ c .closeCtx , c .closeCtxFn = context .WithCancel (context .Background ())
101109
102- notify := func (err error , _ time. Duration ) {
110+ notify := func (err error ) {
103111 c .log .Errorf (err .Error ())
104112 }
105- if err := backoff . RetryNotify (c .connect (false ), c . backoff , notify ); err != nil {
113+ if err := c . backoffRetry (c .connect (false ), notify ); err != nil {
106114 return err
107115 }
108116
@@ -172,11 +180,48 @@ func (c *Client) Error() <-chan error {
172180
173181// Close attempts to gracefully close the connection to the server.
174182func (c * Client ) Close () {
183+ if c .closeCtxFn != nil {
184+ // Close the context so that any pending backoff-retries instantly cancel.
185+ c .closeCtxFn ()
186+ }
175187 c .mtx .Lock ()
176188 defer c .mtx .Unlock ()
177189 c .close (false )
178190}
179191
192+ func (c * Client ) backoffRetry (fn func () error , notify func (error )) error {
193+ var err error
194+ for {
195+ if time .Since (c .connectTime ) > 2 * clientMaxBackoffInterval {
196+ // Reset the backoff timer only if the connection has been stable for double the max interval.
197+ c .backoff .Reset ()
198+ err = nil
199+ }
200+ // Skip backoffs only for the very first connection a client performs.
201+ if ! c .connectTime .IsZero () {
202+ // Backoff regardless of prior error status in this func, since it's possible for
203+ // another goroutine other than the `fn` to have failed and triggered a reconnect.
204+ wait := c .backoff .NextBackOff ()
205+ if wait == backoff .Stop {
206+ return err // Return the last-known recent error.
207+ }
208+ select {
209+ case <- time .After (wait ):
210+ case <- c .closeCtx .Done ():
211+ return c .closeCtx .Err ()
212+ }
213+ }
214+ c .connectTime = time .Now ()
215+ err = fn ()
216+ if err == nil {
217+ return nil
218+ }
219+ if notify != nil {
220+ notify (err )
221+ }
222+ }
223+ }
224+
180225func newConn (uri string ) (* websocket.Conn , error ) {
181226 conn , res , err := websocket .DefaultDialer .Dial (uri , nil )
182227 if err != nil {
@@ -239,20 +284,21 @@ func (c *Client) reconnect() {
239284 c .mtx .Lock ()
240285 defer c .mtx .Unlock ()
241286
242- if c .shouldClose {
287+ if c .closeCtx . Err () != nil {
243288 return
244289 }
245290
246291 c .log .Debugf ("unexpected disconnect: reconnecting" )
247292 c .close (true )
248293
249- notify := func (err error , _ time. Duration ) {
294+ notify := func (err error ) {
250295 c .log .Errorf (err .Error ())
251296 if c .reconnectCallback != nil {
252297 c .reconnectCallback (err )
253298 }
254299 }
255- err := backoff .RetryNotify (c .connect (true ), c .backoff , notify )
300+
301+ err := c .backoffRetry (c .connect (true ), notify )
256302 if err != nil {
257303 err = fmt .Errorf ("error reconnecting: %w: closing connection" , err )
258304 c .log .Errorf (err .Error ())
@@ -286,7 +332,7 @@ func (c *Client) close(reconnect bool) {
286332 if err := c .ptomb .Wait (); err != nil {
287333 c .log .Errorf ("process thread closed: %v" , err )
288334 }
289- c .shouldClose = true
335+ c .closeCtxFn ()
290336 c .closeOutput ()
291337 }
292338
0 commit comments