github.com/number571/tendermint@v0.34.11-gost/rpc/client/http/ws.go (about) 1 package http 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 tmsync "github.com/number571/tendermint/internal/libs/sync" 11 tmjson "github.com/number571/tendermint/libs/json" 12 tmpubsub "github.com/number571/tendermint/libs/pubsub" 13 "github.com/number571/tendermint/libs/service" 14 rpcclient "github.com/number571/tendermint/rpc/client" 15 ctypes "github.com/number571/tendermint/rpc/core/types" 16 jsonrpcclient "github.com/number571/tendermint/rpc/jsonrpc/client" 17 ) 18 19 var errNotRunning = errors.New("client is not running. Use .Start() method to start") 20 21 // WSOptions for the WS part of the HTTP client. 22 type WSOptions struct { 23 Path string // path (e.g. "/ws") 24 25 jsonrpcclient.WSOptions // WSClient options 26 } 27 28 // DefaultWSOptions returns default WS options. 29 // See jsonrpcclient.DefaultWSOptions. 30 func DefaultWSOptions() WSOptions { 31 return WSOptions{ 32 Path: "/websocket", 33 WSOptions: jsonrpcclient.DefaultWSOptions(), 34 } 35 } 36 37 // Validate performs a basic validation of WSOptions. 38 func (wso WSOptions) Validate() error { 39 if len(wso.Path) <= 1 { 40 return errors.New("empty Path") 41 } 42 if wso.Path[0] != '/' { 43 return errors.New("leading slash is missing in Path") 44 } 45 46 return nil 47 } 48 49 // wsEvents is a wrapper around WSClient, which implements EventsClient. 50 type wsEvents struct { 51 service.BaseService 52 ws *jsonrpcclient.WSClient 53 54 mtx tmsync.RWMutex 55 subscriptions map[string]*wsSubscription 56 } 57 58 type wsSubscription struct { 59 res chan ctypes.ResultEvent 60 id string 61 query string 62 } 63 64 var _ rpcclient.EventsClient = (*wsEvents)(nil) 65 66 func newWsEvents(remote string, wso WSOptions) (*wsEvents, error) { 67 // validate options 68 if err := wso.Validate(); err != nil { 69 return nil, fmt.Errorf("invalid WSOptions: %w", err) 70 } 71 72 // remove the trailing / from the remote else the websocket endpoint 73 // won't parse correctly 74 if remote[len(remote)-1] == '/' { 75 remote = remote[:len(remote)-1] 76 } 77 78 w := &wsEvents{ 79 subscriptions: make(map[string]*wsSubscription), 80 } 81 w.BaseService = *service.NewBaseService(nil, "wsEvents", w) 82 83 var err error 84 w.ws, err = jsonrpcclient.NewWSWithOptions(remote, wso.Path, wso.WSOptions) 85 if err != nil { 86 return nil, fmt.Errorf("can't create WS client: %w", err) 87 } 88 w.ws.OnReconnect(func() { 89 // resubscribe immediately 90 w.redoSubscriptionsAfter(0 * time.Second) 91 }) 92 w.ws.SetLogger(w.Logger) 93 94 return w, nil 95 } 96 97 // OnStart implements service.Service by starting WSClient and event loop. 98 func (w *wsEvents) OnStart() error { 99 if err := w.ws.Start(); err != nil { 100 return err 101 } 102 103 go w.eventListener() 104 105 return nil 106 } 107 108 // OnStop implements service.Service by stopping WSClient. 109 func (w *wsEvents) OnStop() { 110 if err := w.ws.Stop(); err != nil { 111 w.Logger.Error("Can't stop ws client", "err", err) 112 } 113 } 114 115 // Subscribe implements EventsClient by using WSClient to subscribe given 116 // subscriber to query. By default, it returns a channel with cap=1. Error is 117 // returned if it fails to subscribe. 118 // 119 // When reading from the channel, keep in mind there's a single events loop, so 120 // if you don't read events for this subscription fast enough, other 121 // subscriptions will slow down in effect. 122 // 123 // The channel is never closed to prevent clients from seeing an erroneous 124 // event. 125 // 126 // It returns an error if wsEvents is not running. 127 func (w *wsEvents) Subscribe(ctx context.Context, subscriber, query string, 128 outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) { 129 130 if !w.IsRunning() { 131 return nil, errNotRunning 132 } 133 134 if err := w.ws.Subscribe(ctx, query); err != nil { 135 return nil, err 136 } 137 138 outCap := 1 139 if len(outCapacity) > 0 { 140 outCap = outCapacity[0] 141 } 142 143 outc := make(chan ctypes.ResultEvent, outCap) 144 w.mtx.Lock() 145 defer w.mtx.Unlock() 146 // subscriber param is ignored because Tendermint will override it with 147 // remote IP anyway. 148 w.subscriptions[query] = &wsSubscription{res: outc, query: query} 149 150 return outc, nil 151 } 152 153 // Unsubscribe implements EventsClient by using WSClient to unsubscribe given 154 // subscriber from query. 155 // 156 // It returns an error if wsEvents is not running. 157 func (w *wsEvents) Unsubscribe(ctx context.Context, subscriber, query string) error { 158 if !w.IsRunning() { 159 return errNotRunning 160 } 161 162 if err := w.ws.Unsubscribe(ctx, query); err != nil { 163 return err 164 } 165 166 w.mtx.Lock() 167 info, ok := w.subscriptions[query] 168 if ok { 169 if info.id != "" { 170 delete(w.subscriptions, info.id) 171 } 172 delete(w.subscriptions, info.query) 173 } 174 w.mtx.Unlock() 175 176 return nil 177 } 178 179 // UnsubscribeAll implements EventsClient by using WSClient to unsubscribe 180 // given subscriber from all the queries. 181 // 182 // It returns an error if wsEvents is not running. 183 func (w *wsEvents) UnsubscribeAll(ctx context.Context, subscriber string) error { 184 if !w.IsRunning() { 185 return errNotRunning 186 } 187 188 if err := w.ws.UnsubscribeAll(ctx); err != nil { 189 return err 190 } 191 192 w.mtx.Lock() 193 w.subscriptions = make(map[string]*wsSubscription) 194 w.mtx.Unlock() 195 196 return nil 197 } 198 199 // After being reconnected, it is necessary to redo subscription to server 200 // otherwise no data will be automatically received. 201 func (w *wsEvents) redoSubscriptionsAfter(d time.Duration) { 202 time.Sleep(d) 203 204 ctx := context.Background() 205 206 w.mtx.Lock() 207 defer w.mtx.Unlock() 208 209 for q, info := range w.subscriptions { 210 if q != "" && q == info.id { 211 continue 212 } 213 err := w.ws.Subscribe(ctx, q) 214 if err != nil { 215 w.Logger.Error("failed to resubscribe", "query", q, "err", err) 216 delete(w.subscriptions, q) 217 } 218 } 219 } 220 221 func isErrAlreadySubscribed(err error) bool { 222 return strings.Contains(err.Error(), tmpubsub.ErrAlreadySubscribed.Error()) 223 } 224 225 func (w *wsEvents) eventListener() { 226 for { 227 select { 228 case resp, ok := <-w.ws.ResponsesCh: 229 if !ok { 230 return 231 } 232 233 if resp.Error != nil { 234 w.Logger.Error("WS error", "err", resp.Error.Error()) 235 // Error can be ErrAlreadySubscribed or max client (subscriptions per 236 // client) reached or Tendermint exited. 237 // We can ignore ErrAlreadySubscribed, but need to retry in other 238 // cases. 239 if !isErrAlreadySubscribed(resp.Error) { 240 // Resubscribe after 1 second to give Tendermint time to restart (if 241 // crashed). 242 w.redoSubscriptionsAfter(1 * time.Second) 243 } 244 continue 245 } 246 247 result := new(ctypes.ResultEvent) 248 err := tmjson.Unmarshal(resp.Result, result) 249 if err != nil { 250 w.Logger.Error("failed to unmarshal response", "err", err) 251 continue 252 } 253 254 w.mtx.RLock() 255 out, ok := w.subscriptions[result.Query] 256 if ok { 257 if _, idOk := w.subscriptions[result.SubscriptionID]; !idOk { 258 out.id = result.SubscriptionID 259 w.subscriptions[result.SubscriptionID] = out 260 } 261 } 262 263 w.mtx.RUnlock() 264 if ok { 265 select { 266 case out.res <- *result: 267 case <-w.Quit(): 268 return 269 } 270 } 271 case <-w.Quit(): 272 return 273 } 274 } 275 }