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