github.com/franono/tendermint@v0.32.2-0.20200527150959-749313264ce9/rpc/jsonrpc/client/ws_client.go (about) 1 package client 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net" 8 "net/http" 9 "sync" 10 "time" 11 12 "github.com/gorilla/websocket" 13 metrics "github.com/rcrowley/go-metrics" 14 15 amino "github.com/tendermint/go-amino" 16 17 tmrand "github.com/franono/tendermint/libs/rand" 18 "github.com/franono/tendermint/libs/service" 19 types "github.com/franono/tendermint/rpc/jsonrpc/types" 20 ) 21 22 const ( 23 defaultMaxReconnectAttempts = 25 24 defaultWriteWait = 0 25 defaultReadWait = 0 26 defaultPingPeriod = 0 27 ) 28 29 // WSClient is a JSON-RPC client, which uses WebSocket for communication with 30 // the remote server. 31 // 32 // WSClient is safe for concurrent use by multiple goroutines. 33 type WSClient struct { // nolint: maligned 34 conn *websocket.Conn 35 cdc *amino.Codec 36 37 Address string // IP:PORT or /path/to/socket 38 Endpoint string // /websocket/url/endpoint 39 Dialer func(string, string) (net.Conn, error) 40 41 // Single user facing channel to read RPCResponses from, closed only when the 42 // client is being stopped. 43 ResponsesCh chan types.RPCResponse 44 45 // Callback, which will be called each time after successful reconnect. 46 onReconnect func() 47 48 // internal channels 49 send chan types.RPCRequest // user requests 50 backlog chan types.RPCRequest // stores a single user request received during a conn failure 51 reconnectAfter chan error // reconnect requests 52 readRoutineQuit chan struct{} // a way for readRoutine to close writeRoutine 53 54 // Maximum reconnect attempts (0 or greater; default: 25). 55 maxReconnectAttempts int 56 57 // Support both ws and wss protocols 58 protocol string 59 60 wg sync.WaitGroup 61 62 mtx sync.RWMutex 63 sentLastPingAt time.Time 64 reconnecting bool 65 nextReqID int 66 // sentIDs map[types.JSONRPCIntID]bool // IDs of the requests currently in flight 67 68 // Time allowed to write a message to the server. 0 means block until operation succeeds. 69 writeWait time.Duration 70 71 // Time allowed to read the next message from the server. 0 means block until operation succeeds. 72 readWait time.Duration 73 74 // Send pings to server with this period. Must be less than readWait. If 0, no pings will be sent. 75 pingPeriod time.Duration 76 77 service.BaseService 78 79 // Time between sending a ping and receiving a pong. See 80 // https://godoc.org/github.com/rcrowley/go-metrics#Timer. 81 PingPongLatencyTimer metrics.Timer 82 } 83 84 // NewWS returns a new client. See the commentary on the func(*WSClient) 85 // functions for a detailed description of how to configure ping period and 86 // pong wait time. The endpoint argument must begin with a `/`. 87 // An error is returned on invalid remote. The function panics when remote is nil. 88 func NewWS(remoteAddr, endpoint string, options ...func(*WSClient)) (*WSClient, error) { 89 parsedURL, err := newParsedURL(remoteAddr) 90 if err != nil { 91 return nil, err 92 } 93 // default to ws protocol, unless wss is explicitly specified 94 if parsedURL.Scheme != protoWSS { 95 parsedURL.Scheme = protoWS 96 } 97 98 dialFn, err := makeHTTPDialer(remoteAddr) 99 if err != nil { 100 return nil, err 101 } 102 103 c := &WSClient{ 104 cdc: amino.NewCodec(), 105 Address: parsedURL.GetTrimmedHostWithPath(), 106 Dialer: dialFn, 107 Endpoint: endpoint, 108 PingPongLatencyTimer: metrics.NewTimer(), 109 110 maxReconnectAttempts: defaultMaxReconnectAttempts, 111 readWait: defaultReadWait, 112 writeWait: defaultWriteWait, 113 pingPeriod: defaultPingPeriod, 114 protocol: parsedURL.Scheme, 115 116 // sentIDs: make(map[types.JSONRPCIntID]bool), 117 } 118 c.BaseService = *service.NewBaseService(nil, "WSClient", c) 119 for _, option := range options { 120 option(c) 121 } 122 return c, nil 123 } 124 125 // MaxReconnectAttempts sets the maximum number of reconnect attempts before returning an error. 126 // It should only be used in the constructor and is not Goroutine-safe. 127 func MaxReconnectAttempts(max int) func(*WSClient) { 128 return func(c *WSClient) { 129 c.maxReconnectAttempts = max 130 } 131 } 132 133 // ReadWait sets the amount of time to wait before a websocket read times out. 134 // It should only be used in the constructor and is not Goroutine-safe. 135 func ReadWait(readWait time.Duration) func(*WSClient) { 136 return func(c *WSClient) { 137 c.readWait = readWait 138 } 139 } 140 141 // WriteWait sets the amount of time to wait before a websocket write times out. 142 // It should only be used in the constructor and is not Goroutine-safe. 143 func WriteWait(writeWait time.Duration) func(*WSClient) { 144 return func(c *WSClient) { 145 c.writeWait = writeWait 146 } 147 } 148 149 // PingPeriod sets the duration for sending websocket pings. 150 // It should only be used in the constructor - not Goroutine-safe. 151 func PingPeriod(pingPeriod time.Duration) func(*WSClient) { 152 return func(c *WSClient) { 153 c.pingPeriod = pingPeriod 154 } 155 } 156 157 // OnReconnect sets the callback, which will be called every time after 158 // successful reconnect. 159 func OnReconnect(cb func()) func(*WSClient) { 160 return func(c *WSClient) { 161 c.onReconnect = cb 162 } 163 } 164 165 // String returns WS client full address. 166 func (c *WSClient) String() string { 167 return fmt.Sprintf("WSClient{%s (%s)}", c.Address, c.Endpoint) 168 } 169 170 // OnStart implements service.Service by dialing a server and creating read and 171 // write routines. 172 func (c *WSClient) OnStart() error { 173 err := c.dial() 174 if err != nil { 175 return err 176 } 177 178 c.ResponsesCh = make(chan types.RPCResponse) 179 180 c.send = make(chan types.RPCRequest) 181 // 1 additional error may come from the read/write 182 // goroutine depending on which failed first. 183 c.reconnectAfter = make(chan error, 1) 184 // capacity for 1 request. a user won't be able to send more because the send 185 // channel is unbuffered. 186 c.backlog = make(chan types.RPCRequest, 1) 187 188 c.startReadWriteRoutines() 189 go c.reconnectRoutine() 190 191 return nil 192 } 193 194 // Stop overrides service.Service#Stop. There is no other way to wait until Quit 195 // channel is closed. 196 func (c *WSClient) Stop() error { 197 if err := c.BaseService.Stop(); err != nil { 198 return err 199 } 200 // only close user-facing channels when we can't write to them 201 c.wg.Wait() 202 close(c.ResponsesCh) 203 204 return nil 205 } 206 207 // IsReconnecting returns true if the client is reconnecting right now. 208 func (c *WSClient) IsReconnecting() bool { 209 c.mtx.RLock() 210 defer c.mtx.RUnlock() 211 return c.reconnecting 212 } 213 214 // IsActive returns true if the client is running and not reconnecting. 215 func (c *WSClient) IsActive() bool { 216 return c.IsRunning() && !c.IsReconnecting() 217 } 218 219 // Send the given RPC request to the server. Results will be available on 220 // ResponsesCh, errors, if any, on ErrorsCh. Will block until send succeeds or 221 // ctx.Done is closed. 222 func (c *WSClient) Send(ctx context.Context, request types.RPCRequest) error { 223 select { 224 case c.send <- request: 225 c.Logger.Info("sent a request", "req", request) 226 // c.mtx.Lock() 227 // c.sentIDs[request.ID.(types.JSONRPCIntID)] = true 228 // c.mtx.Unlock() 229 return nil 230 case <-ctx.Done(): 231 return ctx.Err() 232 } 233 } 234 235 // Call enqueues a call request onto the Send queue. Requests are JSON encoded. 236 func (c *WSClient) Call(ctx context.Context, method string, params map[string]interface{}) error { 237 request, err := types.MapToRequest(c.cdc, c.nextRequestID(), method, params) 238 if err != nil { 239 return err 240 } 241 return c.Send(ctx, request) 242 } 243 244 // CallWithArrayParams enqueues a call request onto the Send queue. Params are 245 // in a form of array (e.g. []interface{}{"abcd"}). Requests are JSON encoded. 246 func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, params []interface{}) error { 247 request, err := types.ArrayToRequest(c.cdc, c.nextRequestID(), method, params) 248 if err != nil { 249 return err 250 } 251 return c.Send(ctx, request) 252 } 253 254 func (c *WSClient) Codec() *amino.Codec { return c.cdc } 255 func (c *WSClient) SetCodec(cdc *amino.Codec) { c.cdc = cdc } 256 257 /////////////////////////////////////////////////////////////////////////////// 258 // Private methods 259 260 func (c *WSClient) nextRequestID() types.JSONRPCIntID { 261 c.mtx.Lock() 262 id := c.nextReqID 263 c.nextReqID++ 264 c.mtx.Unlock() 265 return types.JSONRPCIntID(id) 266 } 267 268 func (c *WSClient) dial() error { 269 dialer := &websocket.Dialer{ 270 NetDial: c.Dialer, 271 Proxy: http.ProxyFromEnvironment, 272 } 273 rHeader := http.Header{} 274 conn, _, err := dialer.Dial(c.protocol+"://"+c.Address+c.Endpoint, rHeader) // nolint:bodyclose 275 if err != nil { 276 return err 277 } 278 c.conn = conn 279 return nil 280 } 281 282 // reconnect tries to redial up to maxReconnectAttempts with exponential 283 // backoff. 284 func (c *WSClient) reconnect() error { 285 attempt := 0 286 287 c.mtx.Lock() 288 c.reconnecting = true 289 c.mtx.Unlock() 290 defer func() { 291 c.mtx.Lock() 292 c.reconnecting = false 293 c.mtx.Unlock() 294 }() 295 296 for { 297 jitter := time.Duration(tmrand.Float64() * float64(time.Second)) // 1s == (1e9 ns) 298 backoffDuration := jitter + ((1 << uint(attempt)) * time.Second) 299 300 c.Logger.Info("reconnecting", "attempt", attempt+1, "backoff_duration", backoffDuration) 301 time.Sleep(backoffDuration) 302 303 err := c.dial() 304 if err != nil { 305 c.Logger.Error("failed to redial", "err", err) 306 } else { 307 c.Logger.Info("reconnected") 308 if c.onReconnect != nil { 309 go c.onReconnect() 310 } 311 return nil 312 } 313 314 attempt++ 315 316 if attempt > c.maxReconnectAttempts { 317 return fmt.Errorf("reached maximum reconnect attempts: %w", err) 318 } 319 } 320 } 321 322 func (c *WSClient) startReadWriteRoutines() { 323 c.wg.Add(2) 324 c.readRoutineQuit = make(chan struct{}) 325 go c.readRoutine() 326 go c.writeRoutine() 327 } 328 329 func (c *WSClient) processBacklog() error { 330 select { 331 case request := <-c.backlog: 332 if c.writeWait > 0 { 333 if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { 334 c.Logger.Error("failed to set write deadline", "err", err) 335 } 336 } 337 if err := c.conn.WriteJSON(request); err != nil { 338 c.Logger.Error("failed to resend request", "err", err) 339 c.reconnectAfter <- err 340 // requeue request 341 c.backlog <- request 342 return err 343 } 344 c.Logger.Info("resend a request", "req", request) 345 default: 346 } 347 return nil 348 } 349 350 func (c *WSClient) reconnectRoutine() { 351 for { 352 select { 353 case originalError := <-c.reconnectAfter: 354 // wait until writeRoutine and readRoutine finish 355 c.wg.Wait() 356 if err := c.reconnect(); err != nil { 357 c.Logger.Error("failed to reconnect", "err", err, "original_err", originalError) 358 c.Stop() 359 return 360 } 361 // drain reconnectAfter 362 LOOP: 363 for { 364 select { 365 case <-c.reconnectAfter: 366 default: 367 break LOOP 368 } 369 } 370 err := c.processBacklog() 371 if err == nil { 372 c.startReadWriteRoutines() 373 } 374 375 case <-c.Quit(): 376 return 377 } 378 } 379 } 380 381 // The client ensures that there is at most one writer to a connection by 382 // executing all writes from this goroutine. 383 func (c *WSClient) writeRoutine() { 384 var ticker *time.Ticker 385 if c.pingPeriod > 0 { 386 // ticker with a predefined period 387 ticker = time.NewTicker(c.pingPeriod) 388 } else { 389 // ticker that never fires 390 ticker = &time.Ticker{C: make(<-chan time.Time)} 391 } 392 393 defer func() { 394 ticker.Stop() 395 c.conn.Close() 396 // err != nil { 397 // ignore error; it will trigger in tests 398 // likely because it's closing an already closed connection 399 // } 400 c.wg.Done() 401 }() 402 403 for { 404 select { 405 case request := <-c.send: 406 if c.writeWait > 0 { 407 if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { 408 c.Logger.Error("failed to set write deadline", "err", err) 409 } 410 } 411 if err := c.conn.WriteJSON(request); err != nil { 412 c.Logger.Error("failed to send request", "err", err) 413 c.reconnectAfter <- err 414 // add request to the backlog, so we don't lose it 415 c.backlog <- request 416 return 417 } 418 case <-ticker.C: 419 if c.writeWait > 0 { 420 if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { 421 c.Logger.Error("failed to set write deadline", "err", err) 422 } 423 } 424 if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { 425 c.Logger.Error("failed to write ping", "err", err) 426 c.reconnectAfter <- err 427 return 428 } 429 c.mtx.Lock() 430 c.sentLastPingAt = time.Now() 431 c.mtx.Unlock() 432 c.Logger.Debug("sent ping") 433 case <-c.readRoutineQuit: 434 return 435 case <-c.Quit(): 436 if err := c.conn.WriteMessage( 437 websocket.CloseMessage, 438 websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), 439 ); err != nil { 440 c.Logger.Error("failed to write message", "err", err) 441 } 442 return 443 } 444 } 445 } 446 447 // The client ensures that there is at most one reader to a connection by 448 // executing all reads from this goroutine. 449 func (c *WSClient) readRoutine() { 450 defer func() { 451 c.conn.Close() 452 // err != nil { 453 // ignore error; it will trigger in tests 454 // likely because it's closing an already closed connection 455 // } 456 c.wg.Done() 457 }() 458 459 c.conn.SetPongHandler(func(string) error { 460 // gather latency stats 461 c.mtx.RLock() 462 t := c.sentLastPingAt 463 c.mtx.RUnlock() 464 c.PingPongLatencyTimer.UpdateSince(t) 465 466 c.Logger.Debug("got pong") 467 return nil 468 }) 469 470 for { 471 // reset deadline for every message type (control or data) 472 if c.readWait > 0 { 473 if err := c.conn.SetReadDeadline(time.Now().Add(c.readWait)); err != nil { 474 c.Logger.Error("failed to set read deadline", "err", err) 475 } 476 } 477 _, data, err := c.conn.ReadMessage() 478 if err != nil { 479 if !websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { 480 return 481 } 482 483 c.Logger.Error("failed to read response", "err", err) 484 close(c.readRoutineQuit) 485 c.reconnectAfter <- err 486 return 487 } 488 489 var response types.RPCResponse 490 err = json.Unmarshal(data, &response) 491 if err != nil { 492 c.Logger.Error("failed to parse response", "err", err, "data", string(data)) 493 continue 494 } 495 496 if err = validateResponseID(response.ID); err != nil { 497 c.Logger.Error("error in response ID", "id", response.ID, "err", err) 498 continue 499 } 500 501 // TODO: events resulting from /subscribe do not work with -> 502 // because they are implemented as responses with the subscribe request's 503 // ID. According to the spec, they should be notifications (requests 504 // without IDs). 505 // https://github.com/franono/tendermint/issues/2949 506 // c.mtx.Lock() 507 // if _, ok := c.sentIDs[response.ID.(types.JSONRPCIntID)]; !ok { 508 // c.Logger.Error("unsolicited response ID", "id", response.ID, "expected", c.sentIDs) 509 // c.mtx.Unlock() 510 // continue 511 // } 512 // delete(c.sentIDs, response.ID.(types.JSONRPCIntID)) 513 // c.mtx.Unlock() 514 // Combine a non-blocking read on BaseService.Quit with a non-blocking write on ResponsesCh to avoid blocking 515 // c.wg.Wait() in c.Stop(). Note we rely on Quit being closed so that it sends unlimited Quit signals to stop 516 // both readRoutine and writeRoutine 517 518 c.Logger.Info("got response", "id", response.ID, "result", fmt.Sprintf("%X", response.Result)) 519 520 select { 521 case <-c.Quit(): 522 case c.ResponsesCh <- response: 523 } 524 } 525 } 526 527 /////////////////////////////////////////////////////////////////////////////// 528 // Predefined methods 529 530 // Subscribe to a query. Note the server must have a "subscribe" route 531 // defined. 532 func (c *WSClient) Subscribe(ctx context.Context, query string) error { 533 params := map[string]interface{}{"query": query} 534 return c.Call(ctx, "subscribe", params) 535 } 536 537 // Unsubscribe from a query. Note the server must have a "unsubscribe" route 538 // defined. 539 func (c *WSClient) Unsubscribe(ctx context.Context, query string) error { 540 params := map[string]interface{}{"query": query} 541 return c.Call(ctx, "unsubscribe", params) 542 } 543 544 // UnsubscribeAll from all. Note the server must have a "unsubscribe_all" route 545 // defined. 546 func (c *WSClient) UnsubscribeAll(ctx context.Context) error { 547 params := map[string]interface{}{} 548 return c.Call(ctx, "unsubscribe_all", params) 549 }