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