github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/event/pubsub/pubsub.go (about) 1 // This package was extracted from Tendermint 2 // 3 // Package pubsub implements a pub-sub model with a single publisher (Server) 4 // and multiple subscribers (clients). 5 // 6 // Though you can have multiple publishers by sharing a pointer to a server or 7 // by giving the same channel to each publisher and publishing messages from 8 // that channel (fan-in). 9 // 10 // Clients subscribe for messages, which could be of any type, using a query. 11 // When some message is published, we match it with all queries. If there is a 12 // match, this message will be pushed to all clients, subscribed to that query. 13 // See query subpackage for our implementation. 14 package pubsub 15 16 import ( 17 "context" 18 "errors" 19 "sync" 20 21 "github.com/hyperledger/burrow/event/query" 22 "github.com/hyperledger/burrow/logging" 23 "github.com/hyperledger/burrow/logging/structure" 24 "github.com/tendermint/tendermint/libs/service" 25 ) 26 27 type operation int 28 29 const ( 30 sub operation = iota 31 pub 32 unsub 33 shutdown 34 ) 35 36 var ( 37 // ErrSubscriptionNotFound is returned when a client tries to unsubscribe 38 // from not existing subscription. 39 ErrSubscriptionNotFound = errors.New("subscription not found") 40 41 // ErrAlreadySubscribed is returned when a client tries to subscribe twice or 42 // more using the same query. 43 ErrAlreadySubscribed = errors.New("already subscribed") 44 ) 45 46 type cmd struct { 47 op operation 48 query query.Query 49 ch chan interface{} 50 clientID string 51 msg interface{} 52 tags query.Tagged 53 } 54 55 // Server allows clients to subscribe/unsubscribe for messages, publishing 56 // messages with or without tags, and manages internal state. 57 type Server struct { 58 service.BaseService 59 60 cmds chan cmd 61 cmdsCap int 62 63 mtx sync.RWMutex 64 subscriptions map[string]map[string]query.Query // subscriber -> query (string) -> query.Query 65 logger *logging.Logger 66 } 67 68 // Option sets a parameter for the server. 69 type Option func(*Server) 70 71 // NewServer returns a new server. See the commentary on the Option functions 72 // for a detailed description of how to configure buffering. If no options are 73 // provided, the resulting server's queue is unbuffered. 74 func NewServer(options ...Option) *Server { 75 s := &Server{ 76 subscriptions: make(map[string]map[string]query.Query), 77 logger: logging.NewNoopLogger(), 78 } 79 s.BaseService = *service.NewBaseService(nil, "PubSub", s) 80 81 for _, option := range options { 82 option(s) 83 } 84 85 // if BufferCapacity option was not set, the channel is unbuffered 86 s.cmds = make(chan cmd, s.cmdsCap) 87 88 return s 89 } 90 91 // BufferCapacity allows you to specify capacity for the internal server's 92 // queue. Since the server, given Y subscribers, could only process X messages, 93 // this option could be used to survive spikes (e.g. high amount of 94 // transactions during peak hours). 95 func BufferCapacity(cap int) Option { 96 return func(s *Server) { 97 if cap > 0 { 98 s.cmdsCap = cap 99 } 100 } 101 } 102 103 func WithLogger(logger *logging.Logger) Option { 104 return func(s *Server) { 105 s.logger = logger.WithScope("PubSub") 106 } 107 } 108 109 // BufferCapacity returns capacity of the internal server's queue. 110 func (s *Server) BufferCapacity() int { 111 return s.cmdsCap 112 } 113 114 // Subscribe creates a subscription for the given client. It accepts a channel 115 // on which messages matching the given query can be received. An error will be 116 // returned to the caller if the context is canceled or if subscription already 117 // exist for pair clientID and query. 118 func (s *Server) Subscribe(ctx context.Context, clientID string, qry query.Query, outBuffer int) (<-chan interface{}, error) { 119 s.mtx.RLock() 120 clientSubscriptions, ok := s.subscriptions[clientID] 121 if ok { 122 _, ok = clientSubscriptions[qry.String()] 123 } 124 s.mtx.RUnlock() 125 if ok { 126 return nil, ErrAlreadySubscribed 127 } 128 // We are responsible for closing this channel so we create it 129 out := make(chan interface{}, outBuffer) 130 select { 131 case s.cmds <- cmd{op: sub, clientID: clientID, query: qry, ch: out}: 132 s.mtx.Lock() 133 if _, ok = s.subscriptions[clientID]; !ok { 134 s.subscriptions[clientID] = make(map[string]query.Query) 135 } 136 // preserve original query 137 // see Unsubscribe 138 s.subscriptions[clientID][qry.String()] = qry 139 s.mtx.Unlock() 140 return out, nil 141 case <-ctx.Done(): 142 return nil, ctx.Err() 143 } 144 } 145 146 // Unsubscribe removes the subscription on the given query. An error will be 147 // returned to the caller if the context is canceled or if subscription does 148 // not exist. 149 func (s *Server) Unsubscribe(ctx context.Context, clientID string, qry query.Query) error { 150 var origQuery query.Query 151 s.mtx.RLock() 152 clientSubscriptions, ok := s.subscriptions[clientID] 153 if ok { 154 origQuery, ok = clientSubscriptions[qry.String()] 155 } 156 s.mtx.RUnlock() 157 if !ok { 158 return ErrSubscriptionNotFound 159 } 160 161 // original query is used here because we're using pointers as map keys 162 select { 163 case s.cmds <- cmd{op: unsub, clientID: clientID, query: origQuery}: 164 s.mtx.Lock() 165 delete(clientSubscriptions, qry.String()) 166 s.mtx.Unlock() 167 return nil 168 case <-ctx.Done(): 169 return ctx.Err() 170 } 171 } 172 173 // UnsubscribeAll removes all client subscriptions. An error will be returned 174 // to the caller if the context is canceled or if subscription does not exist. 175 func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error { 176 s.mtx.RLock() 177 _, ok := s.subscriptions[clientID] 178 s.mtx.RUnlock() 179 if !ok { 180 return ErrSubscriptionNotFound 181 } 182 183 select { 184 case s.cmds <- cmd{op: unsub, clientID: clientID}: 185 s.mtx.Lock() 186 delete(s.subscriptions, clientID) 187 s.mtx.Unlock() 188 return nil 189 case <-ctx.Done(): 190 return ctx.Err() 191 } 192 } 193 194 // Publish publishes the given message. An error will be returned to the caller 195 // if the context is canceled. 196 func (s *Server) Publish(ctx context.Context, msg interface{}) error { 197 return s.PublishWithTags(ctx, msg, query.TagMap(make(map[string]interface{}))) 198 } 199 200 // PublishWithTags publishes the given message with the set of tags. The set is 201 // matched with clients queries. If there is a match, the message is sent to 202 // the client. 203 func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags query.Tagged) error { 204 select { 205 case s.cmds <- cmd{op: pub, msg: msg, tags: tags}: 206 return nil 207 case <-ctx.Done(): 208 return ctx.Err() 209 } 210 } 211 212 // OnStop implements Service.OnStop by shutting down the server. 213 func (s *Server) OnStop() { 214 s.cmds <- cmd{op: shutdown} 215 } 216 217 // NOTE: not goroutine safe 218 type state struct { 219 // query -> client -> ch 220 queries map[query.Query]map[string]chan interface{} 221 // client -> query -> struct{} 222 clients map[string]map[query.Query]struct{} 223 logger *logging.Logger 224 } 225 226 // OnStart implements Service.OnStart by starting the server. 227 func (s *Server) OnStart() error { 228 go s.loop(state{ 229 queries: make(map[query.Query]map[string]chan interface{}), 230 clients: make(map[string]map[query.Query]struct{}), 231 logger: s.logger, 232 }) 233 return nil 234 } 235 236 // OnReset implements Service.OnReset 237 func (s *Server) OnReset() error { 238 return nil 239 } 240 241 func (s *Server) loop(state state) { 242 loop: 243 for cmd := range s.cmds { 244 switch cmd.op { 245 case unsub: 246 if cmd.query != nil { 247 state.remove(cmd.clientID, cmd.query) 248 } else { 249 state.removeAll(cmd.clientID) 250 } 251 case shutdown: 252 for clientID := range state.clients { 253 state.removeAll(clientID) 254 } 255 break loop 256 case sub: 257 state.add(cmd.clientID, cmd.query, cmd.ch) 258 case pub: 259 state.send(cmd.msg, cmd.tags) 260 } 261 } 262 } 263 264 func (state *state) add(clientID string, q query.Query, ch chan interface{}) { 265 // add query if needed 266 if _, ok := state.queries[q]; !ok { 267 state.queries[q] = make(map[string]chan interface{}) 268 } 269 270 // create subscription 271 state.queries[q][clientID] = ch 272 273 // add client if needed 274 if _, ok := state.clients[clientID]; !ok { 275 state.clients[clientID] = make(map[query.Query]struct{}) 276 } 277 state.clients[clientID][q] = struct{}{} 278 } 279 280 func (state *state) remove(clientID string, q query.Query) { 281 clientToChannelMap, ok := state.queries[q] 282 if !ok { 283 return 284 } 285 286 ch, ok := clientToChannelMap[clientID] 287 if ok { 288 closeAndDrain(ch) 289 290 delete(state.clients[clientID], q) 291 292 // if it not subscribed to anything else, remove the client 293 if len(state.clients[clientID]) == 0 { 294 delete(state.clients, clientID) 295 } 296 297 delete(state.queries[q], clientID) 298 if len(state.queries[q]) == 0 { 299 delete(state.queries, q) 300 } 301 } 302 } 303 304 func (state *state) removeAll(clientID string) { 305 queryMap, ok := state.clients[clientID] 306 if !ok { 307 return 308 } 309 310 for q := range queryMap { 311 ch := state.queries[q][clientID] 312 closeAndDrain(ch) 313 314 delete(state.queries[q], clientID) 315 if len(state.queries[q]) == 0 { 316 delete(state.queries, q) 317 } 318 } 319 delete(state.clients, clientID) 320 } 321 322 func closeAndDrain(ch chan interface{}) { 323 close(ch) 324 for range ch { 325 } 326 } 327 328 func (state *state) send(msg interface{}, tags query.Tagged) { 329 for q, clientToChannelMap := range state.queries { 330 if q.Matches(tags) { 331 for _, ch := range clientToChannelMap { 332 select { 333 case ch <- msg: 334 default: 335 // It's difficult to do anything sensible here with retries/times outs since we may reorder a client's 336 // view of events by sending a later message before an earlier message we retry. If per-client order 337 // matters then we need a queue per client. Possible for us it does not... 338 } 339 } 340 } 341 err := q.MatchError() 342 if err != nil { 343 state.logger.InfoMsg("pubsub Server could not execute query", structure.ErrorKey, err) 344 } 345 } 346 }