github.com/decred/politeia@v1.4.0/politeiawww/wsdcrdata/wsdcrdata.go (about) 1 // Copyright (c) 2019-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 // Package wsdcrdata provides a client for managing dcrdata websocket 6 // subscriptions. 7 package wsdcrdata 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "sync" 14 "time" 15 16 "github.com/decred/dcrdata/v6/pubsub/psclient" 17 "github.com/decred/dcrdata/v6/semver" 18 ) 19 20 type StatusT int 21 22 const ( 23 // Websocket statuses 24 StatusInvalid StatusT = 0 // Invalid status 25 StatusOpen StatusT = 1 // Websocket is open 26 StatusReconnecting StatusT = 2 // Websocket is attempting to reconnect 27 StatusShutdown StatusT = 3 // Websocket client has been shutdown 28 29 // eventAddress is used to subscribe to events for a specific dcr 30 // address. The dcr address must be appended onto the eventAddress 31 // string. 32 eventAddress = "address:" 33 34 // eventNewBlock is used to subscribe to new block events. 35 eventNewBlock = "newblock" 36 ) 37 38 var ( 39 // ErrDuplicateSub is emitted when attempting to subscribe to an 40 // event that has already been subscribed to. 41 ErrDuplicateSub = errors.New("duplicate subscription") 42 43 // ErrSubNotFound is emitted when attempting to unsubscribe to an 44 // event that has not yet been subscribed to. 45 ErrSubNotFound = errors.New("subscription not found") 46 47 // ErrReconnecting is emitted when attempting to use the Client 48 // while it is in the process of reconnecting to dcrdata. All 49 // subscribe/unsubscribe actions that are attempted while the 50 // client is reconnecting are recorded and completed once the new 51 // connection has been made. 52 ErrReconnecting = errors.New("reconnecting to dcrdata") 53 54 // ErrShutdown is emitted when attempting to use the Client after 55 // it has already been shut down. 56 ErrShutdown = errors.New("client is shutdown") 57 ) 58 59 const ( 60 // Pending event actions 61 actionSubscribe = "subscribe" 62 actionUnsubscribe = "unsubscribe" 63 ) 64 65 // pendingEvent represents an event action (subscribe/unsubscribe) that is 66 // attempted to be made while the Client is in a StateReconnecting state. The 67 // pending event actions are replayed in the order in which they were received 68 // once a new dcrdata connection has been established. 69 type pendingEvent struct { 70 event string // Websocket event 71 action string // Subscribe/unsubscribe 72 } 73 74 // Client is a dcrdata websocket client for managing dcrdata websocket 75 // subscriptions. 76 type Client struct { 77 sync.Mutex 78 url string 79 status StatusT // Websocket status 80 client *psclient.Client // dcrdata websocket client 81 subscriptions map[string]struct{} // Active subscriptions 82 83 // pending contains events that were attempted to be subscribed to 84 // or unsubscribed from while the client was in a StateReconnecting 85 // state. Once a new connection has been established the pending 86 // events are replayed in the order in which they were received. 87 pending []pendingEvent 88 } 89 90 // statusSet sets the client status. 91 func (c *Client) statusSet(s StatusT) { 92 c.Lock() 93 defer c.Unlock() 94 95 c.status = s 96 } 97 98 // clientSet sets the websocket client. The lock is held for this so that the 99 // golang race detector doesn't complain when the a new client is created and 100 // set on reconnection attempts. 101 func (c *Client) clientSet(psc *psclient.Client) { 102 c.Lock() 103 defer c.Unlock() 104 105 c.client = psc 106 } 107 108 // subAdd adds an event subscription to the subscriptions map. 109 func (c *Client) subAdd(event string) { 110 c.Lock() 111 defer c.Unlock() 112 113 c.subscriptions[event] = struct{}{} 114 } 115 116 // subDel removes an event subscription from the subscriptions map. 117 func (c *Client) subDel(event string) { 118 c.Lock() 119 defer c.Unlock() 120 121 delete(c.subscriptions, event) 122 } 123 124 // subsGet returns a copy of the full subscriptions list. 125 func (c *Client) subsGet() map[string]struct{} { 126 c.Lock() 127 defer c.Unlock() 128 129 s := make(map[string]struct{}, len(c.subscriptions)) 130 for k := range c.subscriptions { 131 s[k] = struct{}{} 132 } 133 134 return s 135 } 136 137 // subsDel removes all of the subscriptions from the subscriptions map. 138 func (c *Client) subsDel() { 139 c.Lock() 140 defer c.Unlock() 141 142 c.subscriptions = make(map[string]struct{}) 143 } 144 145 // isSubscribed returns whether the client is subscribed to the provided event. 146 func (c *Client) isSubscribed(event string) bool { 147 c.Lock() 148 defer c.Unlock() 149 150 _, ok := c.subscriptions[event] 151 return ok 152 } 153 154 // pendingAdd adds a pending event to the list of pending events. 155 func (c *Client) pendingAdd(pe pendingEvent) { 156 c.Lock() 157 defer c.Unlock() 158 159 c.pending = append(c.pending, pe) 160 } 161 162 // pendingDel deletes the full list of pending events. 163 func (c *Client) pendingDel() { 164 c.Lock() 165 defer c.Unlock() 166 167 c.pending = make([]pendingEvent, 0) 168 } 169 170 // pendingGet returns a copy of the pending events list. 171 func (c *Client) pendingGet() []pendingEvent { 172 c.Lock() 173 defer c.Unlock() 174 175 p := make([]pendingEvent, 0, len(c.pending)) 176 p = append(p, c.pending...) 177 178 return p 179 } 180 181 // subscribe subscribes the dcrdata client to an event. 182 func (c *Client) subscribe(event string) error { 183 // Check connection status 184 switch c.Status() { 185 case StatusShutdown: 186 return ErrShutdown 187 case StatusReconnecting: 188 // Add to list of pending events 189 c.pendingAdd(pendingEvent{ 190 event: event, 191 action: actionSubscribe, 192 }) 193 log.Debugf("Pending event added: subscribe %v", event) 194 return ErrReconnecting 195 } 196 197 // Ensure subscription doesn't already exist 198 if c.isSubscribed(event) { 199 return ErrDuplicateSub 200 } 201 202 // Subscribe 203 _, err := c.client.Subscribe(event) 204 if err != nil { 205 return fmt.Errorf("wcDcrdata failed to subscribe to %v: %v", 206 event, err) 207 } 208 209 log.Debugf("Subscribed to %v", event) 210 211 // Update subscriptions list 212 c.subAdd(event) 213 214 return nil 215 } 216 217 // unsubscribe ubsubscribes the dcrdata client from an event. 218 func (c *Client) unsubscribe(event string) error { 219 // Check connection status 220 switch c.Status() { 221 case StatusShutdown: 222 return ErrShutdown 223 case StatusReconnecting: 224 // Add to list of pending events 225 c.pendingAdd(pendingEvent{ 226 event: event, 227 action: actionUnsubscribe, 228 }) 229 log.Debugf("Pending event added: unsubscribe %v", event) 230 return ErrReconnecting 231 } 232 233 // Ensure subscription exists 234 if !c.isSubscribed(event) { 235 return ErrSubNotFound 236 } 237 238 // Unsubscribe 239 _, err := c.client.Unsubscribe(event) 240 if err != nil { 241 return fmt.Errorf("Client failed to unsubscribe from %v: %v", 242 event, err) 243 } 244 245 log.Debugf("Unsubscribed from %v", event) 246 247 // Update subscriptions list 248 c.subDel(event) 249 250 return nil 251 } 252 253 // Status returns the websocket status. 254 func (c *Client) Status() StatusT { 255 log.Tracef("Status") 256 257 c.Lock() 258 defer c.Unlock() 259 260 return c.status 261 } 262 263 // AddressSubscribe subscribes to events for the provided address. 264 func (c *Client) AddressSubscribe(address string) error { 265 log.Tracef("AddressSubscribe: %v", address) 266 267 return c.subscribe(eventAddress + address) 268 } 269 270 // AddressUnsubscribe unsubscribes from events for the provided address. 271 func (c *Client) AddressUnsubscribe(address string) error { 272 log.Tracef("AddressUnsubscribe: %v", address) 273 274 return c.unsubscribe(eventAddress + address) 275 } 276 277 // NewBlockSubscribe subscibes to the new block event. 278 func (c *Client) NewBlockSubscribe() error { 279 log.Tracef("NewBlockSubscribe") 280 281 return c.subscribe(eventNewBlock) 282 } 283 284 // NewBlockUnsubscribe unsubscibes from the new block event. 285 func (c *Client) NewBlockUnsubscribe() error { 286 log.Tracef("NewBlockUnsubscribe") 287 288 return c.unsubscribe(eventNewBlock) 289 } 290 291 // Receive returns a new channel that receives websocket messages from the 292 // dcrdata server. 293 func (c *Client) Receive() <-chan *psclient.ClientMessage { 294 log.Tracef("Receive") 295 296 // Hold the lock to prevent the go race detector from complaining 297 // when the client is switched out on reconnection attempts. 298 c.Lock() 299 defer c.Unlock() 300 301 return c.client.Receive() 302 } 303 304 // Reconnect creates a new websocket client and subscribes to the same 305 // subscriptions as the previous client. If a connection cannot be established, 306 // this function will continue to episodically attempt to reconnect until 307 // either a connection is made or the application is shut down. If any new 308 // subscribe/unsubscribe events are registered during this reconnection 309 // process, they are added to a pending events list and are replayed in the 310 // order in which they are received once a new connection has been established. 311 func (c *Client) Reconnect() { 312 log.Tracef("Reconnect") 313 314 // Update client status 315 c.statusSet(StatusReconnecting) 316 317 // prevSubs is used to track the subscriptions that existed prior 318 // to being disconnected so that we can resubscribe to them after 319 // establishing a new connection. 320 prevSubs := c.subsGet() 321 322 // Clear out disconnected client subscriptions 323 c.subsDel() 324 325 // timeToWait specifies the time to wait in between reconnection 326 // attempts. This limit is increased if reconnection attempts fail. 327 timeToWait := 1 * time.Minute 328 329 // Keep attempting to reconnect until a new connection has been 330 // made and all previous subscriptions have been resubscribed to. 331 var done bool 332 for !done { 333 log.Infof("Attempting to reconnect dcrdata websocket") 334 335 // Reconnect to dcrdata 336 client, err := psclientNew(c.url) 337 if err != nil { 338 log.Errorf("New client failed: %v", err) 339 goto wait 340 } 341 c.clientSet(client) 342 343 // Connection open again. Update status. 344 c.statusSet(StatusOpen) 345 346 // Resubscribe to previous event subscriptions 347 for event := range prevSubs { 348 // Ensure not already subscribed 349 if c.isSubscribed(event) { 350 continue 351 } 352 353 // Subscribe 354 _, err := c.client.Subscribe(event) 355 if err != nil { 356 log.Errorf("Failed to subscribe to %v: %v", event, err) 357 goto wait 358 } 359 360 // Update subscriptions list 361 c.subAdd(event) 362 log.Debugf("Subscribed to %v", event) 363 } 364 365 // Replay any pending event actions that were registered while 366 // the client was attempting to reconnect. 367 for _, v := range c.pendingGet() { 368 switch v.action { 369 case actionSubscribe: 370 // Ensure not already subscribed 371 if c.isSubscribed(v.event) { 372 continue 373 } 374 375 // Subscribe 376 _, err := c.client.Subscribe(v.event) 377 if err != nil { 378 log.Errorf("Failed to subscribe to %v: %v", v.event, err) 379 goto wait 380 } 381 382 // Update subscriptions list 383 c.subAdd(v.event) 384 log.Debugf("Subscribed to %v", v.event) 385 386 case actionUnsubscribe: 387 // Ensure not already unsubscribed 388 if !c.isSubscribed(v.event) { 389 continue 390 } 391 392 // Unsubscribe 393 _, err := c.client.Unsubscribe(v.event) 394 if err != nil { 395 log.Errorf("Failed to unsubscribe to %v: %v", v.event, err) 396 goto wait 397 } 398 399 // Update subscriptions list 400 c.subDel(v.event) 401 log.Debugf("Unsubscribed from %v", v.event) 402 403 default: 404 log.Errorf("unknown pending event action: %v", v.action) 405 } 406 } 407 408 // Clear out pending events list. These have all been replayed. 409 c.pendingDel() 410 411 // We're done! 412 done = true 413 continue 414 415 wait: 416 // Websocket connection is either still closed or closed again 417 // before we were able to re-subscribe to all events. Update 418 // websocket status and retry again after wait time has elapsed. 419 c.statusSet(StatusReconnecting) 420 421 log.Infof("Dcrdata websocket reconnect waiting %v", timeToWait) 422 time.Sleep(timeToWait) 423 424 // Increase the wait time until it reaches 15m and then try to 425 // reconnect every 15m. 426 limit := 15 * time.Minute 427 timeToWait *= 2 428 if timeToWait > limit { 429 timeToWait = limit 430 } 431 } 432 } 433 434 // Close closes the dcrdata websocket client. 435 func (c *Client) Close() error { 436 log.Tracef("Close") 437 438 // Update websocket status 439 c.statusSet(StatusShutdown) 440 441 // Clear out subscriptions list 442 c.subsDel() 443 444 // Close connection 445 return c.client.Close() 446 } 447 448 func psclientNew(url string) (*psclient.Client, error) { 449 opts := psclient.Opts{ 450 ReadTimeout: psclient.DefaultReadTimeout, 451 WriteTimeout: 3 * time.Second, 452 } 453 c, err := psclient.New(url, context.Background(), &opts) 454 if err != nil { 455 return nil, fmt.Errorf("failed to connect to %v: %v", url, err) 456 } 457 458 log.Infof("Dcrdata websocket host: %v", url) 459 460 // Check client and server compatibility 461 v, err := c.ServerVersion() 462 if err != nil { 463 return nil, fmt.Errorf("server version failed: %v", err) 464 } 465 serverSemVer := semver.NewSemver(v.Major, v.Minor, v.Patch) 466 clientSemVer := psclient.Version() 467 if !semver.Compatible(clientSemVer, serverSemVer) { 468 return nil, fmt.Errorf("version mismatch; client %v, server %v", 469 serverSemVer, clientSemVer) 470 } 471 472 log.Infof("Dcrdata pubsub server version: %v, client version %v", 473 serverSemVer, clientSemVer) 474 475 return c, nil 476 } 477 478 // New returns a new Client. 479 func New(dcrdataURL string) (*Client, error) { 480 log.Tracef("New: %v", dcrdataURL) 481 482 // Setup dcrdata connection. If there is an error when connecting 483 // to dcrdata, return both the error and the Client so that the 484 // caller can decide if reconnection attempts should be made. 485 var status StatusT 486 c, err := psclientNew(dcrdataURL) 487 if err == nil { 488 // Connection is good 489 status = StatusOpen 490 } else { 491 // Unable to make a connection 492 c = &psclient.Client{} 493 status = StatusShutdown 494 } 495 496 return &Client{ 497 url: dcrdataURL, 498 status: status, 499 client: c, 500 subscriptions: make(map[string]struct{}), 501 pending: make([]pendingEvent, 0), 502 }, err 503 }