github.com/supragya/TendermintConnector@v0.0.0-20210619045051-113e32b84fb1/_deprecated_chains/cosmos/libs/pubsub/pubsub.go (about) 1 // Package pubsub implements a pub-sub model with a single publisher (Server) 2 // and multiple subscribers (clients). 3 // 4 // Though you can have multiple publishers by sharing a pointer to a server or 5 // by giving the same channel to each publisher and publishing messages from 6 // that channel (fan-in). 7 // 8 // Clients subscribe for messages, which could be of any type, using a query. 9 // When some message is published, we match it with all queries. If there is a 10 // match, this message will be pushed to all clients, subscribed to that query. 11 // See query subpackage for our implementation. 12 // 13 // Due to the blocking send implementation, a single subscriber can freeze an 14 // entire server by not reading messages before it unsubscribes. To avoid such 15 // scenario, subscribers must either: 16 // 17 // a) make sure they continue to read from the out channel until 18 // Unsubscribe(All) is called 19 // 20 // s.Subscribe(ctx, sub, qry, out) 21 // go func() { 22 // for msg := range out { 23 // // handle msg 24 // // will exit automatically when out is closed by Unsubscribe(All) 25 // } 26 // }() 27 // s.UnsubscribeAll(ctx, sub) 28 // 29 // b) drain the out channel before calling Unsubscribe(All) 30 // 31 // s.Subscribe(ctx, sub, qry, out) 32 // defer func() { 33 // // drain out to make sure we don't block 34 // LOOP: 35 // for { 36 // select { 37 // case <-out: 38 // default: 39 // break LOOP 40 // } 41 // } 42 // s.UnsubscribeAll(ctx, sub) 43 // }() 44 // for msg := range out { 45 // // handle msg 46 // if err != nil { 47 // return err 48 // } 49 // } 50 // 51 package pubsub 52 53 import ( 54 "context" 55 "errors" 56 "sync" 57 58 cmn "github.com/tendermint/tendermint/libs/common" 59 ) 60 61 type operation int 62 63 const ( 64 sub operation = iota 65 pub 66 unsub 67 shutdown 68 ) 69 70 var ( 71 // ErrSubscriptionNotFound is returned when a client tries to unsubscribe 72 // from not existing subscription. 73 ErrSubscriptionNotFound = errors.New("subscription not found") 74 75 // ErrAlreadySubscribed is returned when a client tries to subscribe twice or 76 // more using the same query. 77 ErrAlreadySubscribed = errors.New("already subscribed") 78 ) 79 80 type cmd struct { 81 op operation 82 query Query 83 ch chan<- interface{} 84 clientID string 85 msg interface{} 86 tags TagMap 87 } 88 89 // Query defines an interface for a query to be used for subscribing. 90 type Query interface { 91 Matches(tags TagMap) bool 92 String() string 93 } 94 95 // Server allows clients to subscribe/unsubscribe for messages, publishing 96 // messages with or without tags, and manages internal state. 97 type Server struct { 98 cmn.BaseService 99 100 cmds chan cmd 101 cmdsCap int 102 103 // check if we have subscription before 104 // subscribing or unsubscribing 105 mtx sync.RWMutex 106 subscriptions map[string]map[string]Query // subscriber -> query (string) -> Query 107 } 108 109 // Option sets a parameter for the server. 110 type Option func(*Server) 111 112 // TagMap is used to associate tags to a message. 113 // They can be queried by subscribers to choose messages they will received. 114 type TagMap interface { 115 // Get returns the value for a key, or nil if no value is present. 116 // The ok result indicates whether value was found in the tags. 117 Get(key string) (value string, ok bool) 118 // Len returns the number of tags. 119 Len() int 120 } 121 122 type tagMap map[string]string 123 124 var _ TagMap = (*tagMap)(nil) 125 126 // NewTagMap constructs a new immutable tag set from a map. 127 func NewTagMap(data map[string]string) TagMap { 128 return tagMap(data) 129 } 130 131 // Get returns the value for a key, or nil if no value is present. 132 // The ok result indicates whether value was found in the tags. 133 func (ts tagMap) Get(key string) (value string, ok bool) { 134 value, ok = ts[key] 135 return 136 } 137 138 // Len returns the number of tags. 139 func (ts tagMap) Len() int { 140 return len(ts) 141 } 142 143 // NewServer returns a new server. See the commentary on the Option functions 144 // for a detailed description of how to configure buffering. If no options are 145 // provided, the resulting server's queue is unbuffered. 146 func NewServer(options ...Option) *Server { 147 s := &Server{ 148 subscriptions: make(map[string]map[string]Query), 149 } 150 s.BaseService = *cmn.NewBaseService(nil, "PubSub", s) 151 152 for _, option := range options { 153 option(s) 154 } 155 156 // if BufferCapacity option was not set, the channel is unbuffered 157 s.cmds = make(chan cmd, s.cmdsCap) 158 159 return s 160 } 161 162 // BufferCapacity allows you to specify capacity for the internal server's 163 // queue. Since the server, given Y subscribers, could only process X messages, 164 // this option could be used to survive spikes (e.g. high amount of 165 // transactions during peak hours). 166 func BufferCapacity(cap int) Option { 167 return func(s *Server) { 168 if cap > 0 { 169 s.cmdsCap = cap 170 } 171 } 172 } 173 174 // BufferCapacity returns capacity of the internal server's queue. 175 func (s *Server) BufferCapacity() int { 176 return s.cmdsCap 177 } 178 179 // Subscribe creates a subscription for the given client. It accepts a channel 180 // on which messages matching the given query can be received. An error will be 181 // returned to the caller if the context is canceled or if subscription already 182 // exist for pair clientID and query. 183 func (s *Server) Subscribe(ctx context.Context, clientID string, query Query, out chan<- interface{}) error { 184 s.mtx.RLock() 185 clientSubscriptions, ok := s.subscriptions[clientID] 186 if ok { 187 _, ok = clientSubscriptions[query.String()] 188 } 189 s.mtx.RUnlock() 190 if ok { 191 return ErrAlreadySubscribed 192 } 193 194 select { 195 case s.cmds <- cmd{op: sub, clientID: clientID, query: query, ch: out}: 196 s.mtx.Lock() 197 if _, ok = s.subscriptions[clientID]; !ok { 198 s.subscriptions[clientID] = make(map[string]Query) 199 } 200 // preserve original query 201 // see Unsubscribe 202 s.subscriptions[clientID][query.String()] = query 203 s.mtx.Unlock() 204 return nil 205 case <-ctx.Done(): 206 return ctx.Err() 207 case <-s.Quit(): 208 return nil 209 } 210 } 211 212 // Unsubscribe removes the subscription on the given query. An error will be 213 // returned to the caller if the context is canceled or if subscription does 214 // not exist. 215 func (s *Server) Unsubscribe(ctx context.Context, clientID string, query Query) error { 216 var origQuery Query 217 s.mtx.RLock() 218 clientSubscriptions, ok := s.subscriptions[clientID] 219 if ok { 220 origQuery, ok = clientSubscriptions[query.String()] 221 } 222 s.mtx.RUnlock() 223 if !ok { 224 return ErrSubscriptionNotFound 225 } 226 227 // original query is used here because we're using pointers as map keys 228 select { 229 case s.cmds <- cmd{op: unsub, clientID: clientID, query: origQuery}: 230 s.mtx.Lock() 231 delete(clientSubscriptions, query.String()) 232 s.mtx.Unlock() 233 return nil 234 case <-ctx.Done(): 235 return ctx.Err() 236 case <-s.Quit(): 237 return nil 238 } 239 } 240 241 // UnsubscribeAll removes all client subscriptions. An error will be returned 242 // to the caller if the context is canceled or if subscription does not exist. 243 func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error { 244 s.mtx.RLock() 245 _, ok := s.subscriptions[clientID] 246 s.mtx.RUnlock() 247 if !ok { 248 return ErrSubscriptionNotFound 249 } 250 251 select { 252 case s.cmds <- cmd{op: unsub, clientID: clientID}: 253 s.mtx.Lock() 254 delete(s.subscriptions, clientID) 255 s.mtx.Unlock() 256 return nil 257 case <-ctx.Done(): 258 return ctx.Err() 259 case <-s.Quit(): 260 return nil 261 } 262 } 263 264 // Publish publishes the given message. An error will be returned to the caller 265 // if the context is canceled. 266 func (s *Server) Publish(ctx context.Context, msg interface{}) error { 267 return s.PublishWithTags(ctx, msg, NewTagMap(make(map[string]string))) 268 } 269 270 // PublishWithTags publishes the given message with the set of tags. The set is 271 // matched with clients queries. If there is a match, the message is sent to 272 // the client. 273 func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags TagMap) error { 274 select { 275 case s.cmds <- cmd{op: pub, msg: msg, tags: tags}: 276 return nil 277 case <-ctx.Done(): 278 return ctx.Err() 279 case <-s.Quit(): 280 return nil 281 } 282 } 283 284 // OnStop implements Service.OnStop by shutting down the server. 285 func (s *Server) OnStop() { 286 s.cmds <- cmd{op: shutdown} 287 } 288 289 // NOTE: not goroutine safe 290 type state struct { 291 // query -> client -> ch 292 queries map[Query]map[string]chan<- interface{} 293 // client -> query -> struct{} 294 clients map[string]map[Query]struct{} 295 } 296 297 // OnStart implements Service.OnStart by starting the server. 298 func (s *Server) OnStart() error { 299 go s.loop(state{ 300 queries: make(map[Query]map[string]chan<- interface{}), 301 clients: make(map[string]map[Query]struct{}), 302 }) 303 return nil 304 } 305 306 // OnReset implements Service.OnReset 307 func (s *Server) OnReset() error { 308 return nil 309 } 310 311 func (s *Server) loop(state state) { 312 loop: 313 for cmd := range s.cmds { 314 switch cmd.op { 315 case unsub: 316 if cmd.query != nil { 317 state.remove(cmd.clientID, cmd.query) 318 } else { 319 state.removeAll(cmd.clientID) 320 } 321 case shutdown: 322 for clientID := range state.clients { 323 state.removeAll(clientID) 324 } 325 break loop 326 case sub: 327 state.add(cmd.clientID, cmd.query, cmd.ch) 328 case pub: 329 state.send(cmd.msg, cmd.tags) 330 } 331 } 332 } 333 334 func (state *state) add(clientID string, q Query, ch chan<- interface{}) { 335 336 // initialize clientToChannelMap per query if needed 337 if _, ok := state.queries[q]; !ok { 338 state.queries[q] = make(map[string]chan<- interface{}) 339 } 340 341 // create subscription 342 state.queries[q][clientID] = ch 343 344 // add client if needed 345 if _, ok := state.clients[clientID]; !ok { 346 state.clients[clientID] = make(map[Query]struct{}) 347 } 348 state.clients[clientID][q] = struct{}{} 349 } 350 351 func (state *state) remove(clientID string, q Query) { 352 clientToChannelMap, ok := state.queries[q] 353 if !ok { 354 return 355 } 356 357 ch, ok := clientToChannelMap[clientID] 358 if ok { 359 close(ch) 360 361 delete(state.clients[clientID], q) 362 363 // if it not subscribed to anything else, remove the client 364 if len(state.clients[clientID]) == 0 { 365 delete(state.clients, clientID) 366 } 367 368 delete(state.queries[q], clientID) 369 if len(state.queries[q]) == 0 { 370 delete(state.queries, q) 371 } 372 } 373 } 374 375 func (state *state) removeAll(clientID string) { 376 queryMap, ok := state.clients[clientID] 377 if !ok { 378 return 379 } 380 381 for q := range queryMap { 382 ch := state.queries[q][clientID] 383 close(ch) 384 385 delete(state.queries[q], clientID) 386 if len(state.queries[q]) == 0 { 387 delete(state.queries, q) 388 } 389 } 390 delete(state.clients, clientID) 391 } 392 393 func (state *state) send(msg interface{}, tags TagMap) { 394 for q, clientToChannelMap := range state.queries { 395 if q.Matches(tags) { 396 for _, ch := range clientToChannelMap { 397 ch <- msg 398 } 399 } 400 } 401 }