github.com/alwitt/goutils@v0.6.4/bus.go (about) 1 package goutils 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/apex/log" 10 ) 11 12 // MessageBus an application scoped local message bus 13 type MessageBus interface { 14 /* 15 CreateTopic create new message topic 16 17 @param ctxt context.Context - execution context 18 @param topicName string - topic name 19 @param topicLogTags log.Fields - metadata fields to include in the logs of the topic entity 20 @return new MessageTopic instance 21 */ 22 CreateTopic( 23 ctxt context.Context, topicName string, topicLogTags log.Fields, 24 ) (MessageTopic, error) 25 26 /* 27 GetTopic fetch a message topic 28 29 @param ctxt context.Context - execution context 30 @param topicName string - topic name 31 @returns MessageTopic instance 32 */ 33 GetTopic(ctxt context.Context, topicName string) (MessageTopic, error) 34 35 /* 36 DeleteTopic delete a message topic 37 38 @param ctxt context.Context - execution context 39 @param topicName string - topic name 40 */ 41 DeleteTopic(ctxt context.Context, topicName string) error 42 } 43 44 // messageBusImpl implements MessageBus 45 type messageBusImpl struct { 46 Component 47 48 // manage access to subscriptions 49 lock sync.RWMutex 50 51 // collection of topic 52 topics map[string]MessageTopic 53 54 operationContext context.Context 55 contextCancel context.CancelFunc 56 } 57 58 /* 59 GetNewMessageBusInstance get message bus instance 60 61 @param parentCtxt context.Context - parent execution context 62 @param logTags log.Fields - metadata fields to include in the logs 63 @return new MessageBus instance 64 */ 65 func GetNewMessageBusInstance(parentCtxt context.Context, logTags log.Fields) (MessageBus, error) { 66 optCtxt, cancel := context.WithCancel(parentCtxt) 67 68 instance := &messageBusImpl{ 69 Component: Component{LogTags: logTags}, 70 lock: sync.RWMutex{}, 71 topics: make(map[string]MessageTopic), 72 operationContext: optCtxt, 73 contextCancel: cancel, 74 } 75 76 return instance, nil 77 } 78 79 /* 80 CreateTopic create new message topic 81 82 @param ctxt context.Context - execution context 83 @param topicName string - topic name 84 @param topicLogTags log.Fields - metadata fields to include in the logs of the topic entity 85 @return new MessageTopic instance 86 */ 87 func (b *messageBusImpl) CreateTopic( 88 ctxt context.Context, topicName string, topicLogTags log.Fields, 89 ) (MessageTopic, error) { 90 logTags := b.GetLogTagsForContext(ctxt) 91 92 b.lock.Lock() 93 defer b.lock.Unlock() 94 95 if existing, ok := b.topics[topicName]; ok { 96 log.WithFields(logTags).WithField("topic", topicName).Debug("Topic already defined") 97 return existing, nil 98 } 99 100 topic, err := getNewMessageTopicInstance(b.operationContext, topicName, topicLogTags) 101 102 if err != nil { 103 return nil, err 104 } 105 106 b.topics[topicName] = topic 107 108 log.WithFields(logTags).WithField("topic", topicName).Info("Defined new topic") 109 110 return topic, nil 111 } 112 113 /* 114 GetTopic fetch a message topic 115 116 @param ctxt context.Context - execution context 117 @param topicName string - topic name 118 @returns MessageTopic instance 119 */ 120 func (b *messageBusImpl) GetTopic(ctxt context.Context, topicName string) (MessageTopic, error) { 121 b.lock.RLock() 122 defer b.lock.RUnlock() 123 124 if existing, ok := b.topics[topicName]; ok { 125 return existing, nil 126 } 127 128 return nil, fmt.Errorf("topic is unknown") 129 } 130 131 /* 132 DeleteTopic delete a message topic 133 134 @param ctxt context.Context - execution context 135 @param topicName string - topic name 136 */ 137 func (b *messageBusImpl) DeleteTopic(ctxt context.Context, topicName string) error { 138 logTags := b.GetLogTagsForContext(ctxt) 139 140 b.lock.RLock() 141 defer b.lock.RUnlock() 142 143 if _, ok := b.topics[topicName]; !ok { 144 return fmt.Errorf("topic is unknown") 145 } 146 147 delete(b.topics, topicName) 148 149 log.WithFields(logTags).WithField("topic", topicName).Info("Deleted topic") 150 return nil 151 } 152 153 // ====================================================================================== 154 155 // MessageTopic a message bus topic, responsible for managing its child subscriptions. 156 type MessageTopic interface { 157 /* 158 Publish publish a message on the topic in parallel. 159 160 @param ctxt context.Context - execution context 161 @param message interface{} - the message to send 162 @param blockFor time.Duration - how long to block for the publish to complete. If >0, 163 this is a non-blocking call; blocking call otherwise. 164 */ 165 Publish(ctxt context.Context, message interface{}, blockFor time.Duration) error 166 167 /* 168 CreateSubscription create a new topic subscription 169 170 @param ctxt context.Context - execution context 171 @param subscriber string - name of the subscription 172 @param bufferLen int - length of message buffer 173 @returns the channel to receive messages on 174 */ 175 CreateSubscription( 176 ctxt context.Context, subscriber string, bufferLen int, 177 ) (chan interface{}, error) 178 179 /* 180 DeleteSubscription delete an existing topic subscription 181 182 @param ctxt context.Context - execution context 183 @param subscriber string - subscription to delete 184 */ 185 DeleteSubscription(ctxt context.Context, subscriber string) error 186 } 187 188 // messageTopicImpl implements MessageTopic 189 type messageTopicImpl struct { 190 Component 191 topic string 192 193 // manage access to subscriptions 194 lock sync.RWMutex 195 196 // collection of subscription of this topic 197 subscriptions map[string]chan interface{} 198 199 operationContext context.Context 200 contextCancel context.CancelFunc 201 } 202 203 /* 204 getNewMessageTopicInstance get message topic instance 205 206 @param parentCtxt context.Context - parent execution context 207 @param topic string - topic name 208 @param logTags log.Fields - metadata fields to include in the logs 209 @return new MessageTopic instance 210 */ 211 func getNewMessageTopicInstance( 212 parentCtxt context.Context, topic string, logTags log.Fields, 213 ) (MessageTopic, error) { 214 optCtxt, cancel := context.WithCancel(parentCtxt) 215 216 instance := &messageTopicImpl{ 217 Component: Component{LogTags: logTags}, 218 topic: topic, 219 lock: sync.RWMutex{}, 220 subscriptions: make(map[string]chan interface{}), 221 operationContext: optCtxt, 222 contextCancel: cancel, 223 } 224 225 return instance, nil 226 } 227 228 /* 229 Publish publish a message on the topic in parallel. 230 231 @param ctxt context.Context - execution context 232 @param message interface{} - the message to send 233 @param blockFor time.Duration - how long to block for the publish to complete. If >0, 234 this is a non-blocking call; blocking call otherwise. 235 */ 236 func (t *messageTopicImpl) Publish( 237 ctxt context.Context, message interface{}, blockFor time.Duration, 238 ) error { 239 logTags := t.GetLogTagsForContext(ctxt) 240 241 blocking := blockFor <= 0 242 243 var lclCtxt context.Context 244 var lclCancel context.CancelFunc 245 246 if blocking { 247 lclCtxt, lclCancel = context.WithCancel(ctxt) 248 } else { 249 lclCtxt, lclCancel = context.WithTimeout(ctxt, blockFor) 250 } 251 252 defer lclCancel() 253 254 // Duplicate the message to all subscribers in parallel 255 wg := sync.WaitGroup{} 256 257 // Function to write the message to the subscriber 258 tx := func(subscriber string, buffer chan interface{}) { 259 wg.Done() 260 // Write message 261 select { 262 case buffer <- message: 263 break 264 case <-lclCtxt.Done(): 265 // write timed out 266 log. 267 WithFields(logTags). 268 WithField("subscriber", subscriber). 269 Error("Timed out sending message to subscriber") 270 } 271 272 log. 273 WithFields(logTags). 274 WithField("subscriber", subscriber). 275 Debug("Published message to subscriber") 276 } 277 278 t.lock.RLock() 279 defer t.lock.RUnlock() 280 281 // Start all the message sending helpers 282 wg.Add(len(t.subscriptions)) 283 for subscriber, channel := range t.subscriptions { 284 go tx(subscriber, channel) 285 } 286 287 if blocking { 288 wg.Wait() 289 return nil 290 } 291 return TimeBoundedWaitGroupWait(lclCtxt, &wg, blockFor) 292 } 293 294 /* 295 CreateSubscription create a new topic subscription 296 297 @param ctxt context.Context - execution context 298 @param subscriber string - name of the subscription 299 @param bufferLen int - length of message buffer 300 @returns the channel to receive messages on 301 */ 302 func (t *messageTopicImpl) CreateSubscription( 303 ctxt context.Context, subscriber string, bufferLen int, 304 ) (chan interface{}, error) { 305 logTags := t.GetLogTagsForContext(ctxt) 306 307 t.lock.Lock() 308 defer t.lock.Unlock() 309 310 if _, ok := t.subscriptions[subscriber]; ok { 311 return nil, fmt.Errorf("subscription '%s' already exist", subscriber) 312 } 313 314 msgBuffer := make(chan interface{}, bufferLen) 315 316 t.subscriptions[subscriber] = msgBuffer 317 318 log. 319 WithFields(logTags). 320 WithField("buffer-len", bufferLen). 321 Infof("Created new subscription '%s'", subscriber) 322 323 return msgBuffer, nil 324 } 325 326 /* 327 DeleteSubscription delete an existing topic subscription 328 329 @param ctxt context.Context - execution context 330 @param subscriber string - subscription to delete 331 */ 332 func (t *messageTopicImpl) DeleteSubscription(ctxt context.Context, subscriber string) error { 333 logTags := t.GetLogTagsForContext(ctxt) 334 335 t.lock.Lock() 336 defer t.lock.Unlock() 337 338 existing, ok := t.subscriptions[subscriber] 339 if !ok { 340 return fmt.Errorf("unknown subscription '%s'", subscriber) 341 } 342 log. 343 WithFields(logTags). 344 Debugf("Closing subscription '%s' channel", subscriber) 345 close(existing) 346 log. 347 WithFields(logTags). 348 Infof("Closed subscription '%s' channel", subscriber) 349 delete(t.subscriptions, subscriber) 350 351 log. 352 WithFields(logTags). 353 Infof("Deleted subscription '%s'", subscriber) 354 355 return nil 356 }