github.com/alwitt/goutils@v0.6.4/event_processing.go (about) 1 package goutils 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "sync" 8 9 "github.com/apex/log" 10 ) 11 12 // TaskProcessorSupportHandler is the function signature of callback used to process an user task 13 type TaskProcessorSupportHandler func(taskParam interface{}) error 14 15 // TaskProcessor implements an event loop where tasks are processed by a daemon thread 16 type TaskProcessor interface { 17 /* 18 Submit submits a new task parameter to be processed by a handler 19 20 @param ctx context.Context - calling context 21 @param newTaskParam interface{} - task-parameter 22 @return whether successful 23 */ 24 Submit(ctx context.Context, newTaskParam interface{}) error 25 26 /* 27 SetTaskExecutionMap update the mapping between task-parameter object and its associated 28 handler function. 29 30 The task-parameter object contains information need to execute a particular task. When 31 a user wants to execute a task, the user is submitting a task-parameter object via Submit. 32 The module finds the associated handler function and calls it with the task-parameter object. 33 34 @param newMap map[reflect.Type]TaskHandler - map of task handlers to various task-parameter 35 object types 36 @return whether successful 37 */ 38 SetTaskExecutionMap(newMap map[reflect.Type]TaskProcessorSupportHandler) error 39 40 /* 41 AddToTaskExecutionMap add new (task-parameter, handler function) mapping to the existing set. 42 43 @param parameterType reflect.Type - task-parameter object type 44 @param handler TaskHandler - task handler 45 @return whether successful 46 */ 47 AddToTaskExecutionMap(parameterType reflect.Type, handler TaskProcessorSupportHandler) error 48 49 /* 50 StartEventLoop starts the daemon thread for processing the submitted task-parameters 51 52 @param wg *sync.WaitGroup - wait group 53 @return whether successful 54 */ 55 StartEventLoop(wg *sync.WaitGroup) error 56 57 /* 58 StopEventLoop stops the daemon thread 59 60 @return whether successful 61 */ 62 StopEventLoop() error 63 } 64 65 // taskProcessorImpl implements TaskProcessor which uses only one daemon thread 66 type taskProcessorImpl struct { 67 Component 68 name string 69 operationContext context.Context 70 contextCancel context.CancelFunc 71 newTasks chan interface{} 72 executionMap map[reflect.Type]TaskProcessorSupportHandler 73 } 74 75 /* 76 GetNewTaskProcessorInstance get single threaded implementation of TaskProcessor 77 78 @param ctxt context.Context - parent context 79 @param instanceName string - instance name 80 @param taskBufferLen int - number of task-parameters to buffer 81 @param logTags log.Fields - metadata fields to include in the logs 82 @return new TaskProcessor instance 83 */ 84 func GetNewTaskProcessorInstance( 85 ctxt context.Context, instanceName string, taskBufferLen int, logTags log.Fields, 86 ) (TaskProcessor, error) { 87 optCtxt, cancel := context.WithCancel(ctxt) 88 return &taskProcessorImpl{ 89 Component: Component{LogTags: logTags}, 90 name: instanceName, 91 operationContext: optCtxt, 92 contextCancel: cancel, 93 newTasks: make(chan interface{}, taskBufferLen), 94 executionMap: make(map[reflect.Type]TaskProcessorSupportHandler), 95 }, nil 96 } 97 98 /* 99 Submit submits a new task parameter to be processed by a handler 100 101 @param ctx context.Context - calling context 102 @param newTaskParam interface{} - task-parameter 103 @return whether successful 104 */ 105 func (p *taskProcessorImpl) Submit(ctx context.Context, newTaskParam interface{}) error { 106 select { 107 case p.newTasks <- newTaskParam: 108 return nil 109 case <-ctx.Done(): 110 return ctx.Err() 111 case <-p.operationContext.Done(): 112 return p.operationContext.Err() 113 } 114 } 115 116 /* 117 SetTaskExecutionMap update the mapping between task-parameter object and its associated 118 handler function. 119 120 The task-parameter object contains information need to execute a particular task. When 121 a user wants to execute a task, the user is submitting a task-parameter object via Submit. 122 The module finds the associated handler function and calls it with the task-parameter object. 123 124 @param newMap map[reflect.Type]TaskHandler - map of task handlers to various task-parameter 125 object types 126 @return whether successful 127 */ 128 func (p *taskProcessorImpl) SetTaskExecutionMap( 129 newMap map[reflect.Type]TaskProcessorSupportHandler, 130 ) error { 131 log.WithFields(p.LogTags).Debug("Changing task execution mapping") 132 p.executionMap = newMap 133 return nil 134 } 135 136 /* 137 AddToTaskExecutionMap add new (task-parameter, handler function) mapping to the existing set. 138 139 @param parameterType reflect.Type - task-parameter object type 140 @param handler TaskHandler - task handler 141 @return whether successful 142 */ 143 func (p *taskProcessorImpl) AddToTaskExecutionMap(theType reflect.Type, handler TaskProcessorSupportHandler) error { 144 log.WithFields(p.LogTags).Debugf("Appending to task execution mapping for %s", theType) 145 p.executionMap[theType] = handler 146 return nil 147 } 148 149 // StartEventLoop starts the daemon thread for processing the submitted task-parameters 150 func (p *taskProcessorImpl) processNewTaskParam(newTaskParam interface{}) error { 151 if p.executionMap != nil && len(p.executionMap) > 0 { 152 log.WithFields(p.LogTags).Debugf("Processing new %s", reflect.TypeOf(newTaskParam)) 153 // Process task based on the parameter type 154 if theHandler, ok := p.executionMap[reflect.TypeOf(newTaskParam)]; ok { 155 return theHandler(newTaskParam) 156 } 157 return fmt.Errorf( 158 "[TP %s] No matching handler found for %s", p.name, reflect.TypeOf(newTaskParam), 159 ) 160 } 161 return fmt.Errorf("[TP %s] No task execution mapping set", p.name) 162 } 163 164 /* 165 StartEventLoop starts the daemon thread for processing the submitted task-parameters 166 167 @param wg *sync.WaitGroup - wait group 168 @return whether successful 169 */ 170 func (p *taskProcessorImpl) StartEventLoop(wg *sync.WaitGroup) error { 171 log.WithFields(p.LogTags).Info("Starting event loop") 172 wg.Add(1) 173 go func() { 174 defer wg.Done() 175 defer log.WithFields(p.LogTags).Info("Event loop exited") 176 finished := false 177 for !finished { 178 select { 179 case <-p.operationContext.Done(): 180 finished = true 181 case newTaskParam, ok := <-p.newTasks: 182 if !ok { 183 log.WithFields(p.LogTags).Error( 184 "Event loop terminating. Failed to read new task param", 185 ) 186 return 187 } 188 if err := p.processNewTaskParam(newTaskParam); err != nil { 189 log.WithError(err).WithFields(p.LogTags).Error("Failed to process new task param") 190 } 191 } 192 } 193 }() 194 return nil 195 } 196 197 /* 198 StopEventLoop stops the daemon thread 199 200 @return whether successful 201 */ 202 func (p *taskProcessorImpl) StopEventLoop() error { 203 p.contextCancel() 204 return nil 205 } 206 207 // ============================================================================== 208 209 // taskDemuxProcessorImpl implement TaskProcessor but support multiple parallel workers 210 type taskDemuxProcessorImpl struct { 211 Component 212 name string 213 input TaskProcessor 214 workers []TaskProcessor 215 routeIdx int 216 operationContext context.Context 217 contextCancel context.CancelFunc 218 } 219 220 /* 221 GetNewTaskDemuxProcessorInstance get multi-threaded implementation of TaskProcessor 222 223 @param ctxt context.Context - parent context 224 @param instanceName string - instance name 225 @param taskBufferLen int - number of task-parameters to buffer 226 @param workerNum int - number of supporting worker threads 227 @param logTags log.Fields - metadata fields to include in the logs 228 @return new TaskProcessor instance 229 */ 230 func GetNewTaskDemuxProcessorInstance( 231 ctxt context.Context, 232 instanceName string, 233 taskBufferLen int, 234 workerNum int, 235 logTags log.Fields, 236 ) (TaskProcessor, error) { 237 if workerNum < 2 { 238 return nil, fmt.Errorf("won't create multi-threaded TaskProcessor with less than two workers") 239 } 240 inputTP, err := GetNewTaskProcessorInstance( 241 ctxt, fmt.Sprintf("%s.input", instanceName), taskBufferLen, logTags, 242 ) 243 if err != nil { 244 return nil, err 245 } 246 optCtxt, cancel := context.WithCancel(ctxt) 247 workers := make([]TaskProcessor, workerNum) 248 for itr := 0; itr < workerNum; itr++ { 249 perWorkerLogTags := log.Fields{} 250 for field, value := range logTags { 251 perWorkerLogTags[field] = value 252 } 253 perWorkerLogTags["worker"] = itr 254 workerTP, err := GetNewTaskProcessorInstance( 255 optCtxt, fmt.Sprintf("%s.worker.%d", instanceName, itr), taskBufferLen, perWorkerLogTags, 256 ) 257 if err != nil { 258 cancel() 259 return nil, err 260 } 261 workers[itr] = workerTP 262 } 263 return &taskDemuxProcessorImpl{ 264 name: instanceName, 265 input: inputTP, 266 workers: workers, 267 routeIdx: 0, 268 operationContext: optCtxt, 269 contextCancel: cancel, 270 Component: Component{LogTags: logTags}, 271 }, nil 272 } 273 274 /* 275 Submit submits a new task parameter to be processed by a handler 276 277 @param ctx context.Context - calling context 278 @param newTaskParam interface{} - task-parameter 279 @return whether successful 280 */ 281 func (p *taskDemuxProcessorImpl) Submit(ctx context.Context, newTaskParam interface{}) error { 282 return p.input.Submit(ctx, newTaskParam) 283 } 284 285 // processNewTaskParam execute a submitted task-parameter 286 func (p *taskDemuxProcessorImpl) processNewTaskParam(newTaskParam interface{}) error { 287 if p.workers != nil && len(p.workers) > 0 { 288 log.WithFields(p.LogTags).Debugf("Processing new %s", reflect.TypeOf(newTaskParam)) 289 defer func() { p.routeIdx = (p.routeIdx + 1) % len(p.workers) }() 290 return p.workers[p.routeIdx].Submit(p.operationContext, newTaskParam) 291 } 292 return fmt.Errorf("[TDP %s] No workers defined", p.name) 293 } 294 295 /* 296 SetTaskExecutionMap update the mapping between task-parameter object and its associated 297 handler function. 298 299 The task-parameter object contains information need to execute a particular task. When 300 a user wants to execute a task, the user is submitting a task-parameter object via Submit. 301 The module finds the associated handler function and calls it with the task-parameter object. 302 303 @param newMap map[reflect.Type]TaskHandler - map of task handlers to various task-parameter 304 object types 305 @return whether successful 306 */ 307 func (p *taskDemuxProcessorImpl) SetTaskExecutionMap(newMap map[reflect.Type]TaskProcessorSupportHandler) error { 308 for _, worker := range p.workers { 309 _ = worker.SetTaskExecutionMap(newMap) 310 } 311 // Create a different version of the input to route to worker 312 inputMap := map[reflect.Type]TaskProcessorSupportHandler{} 313 for msgType := range newMap { 314 inputMap[msgType] = p.processNewTaskParam 315 } 316 return p.input.SetTaskExecutionMap(inputMap) 317 } 318 319 /* 320 AddToTaskExecutionMap add new (task-parameter, handler function) mapping to the existing set. 321 322 @param parameterType reflect.Type - task-parameter object type 323 @param handler TaskHandler - task handler 324 @return whether successful 325 */ 326 func (p *taskDemuxProcessorImpl) AddToTaskExecutionMap( 327 theType reflect.Type, handler TaskProcessorSupportHandler, 328 ) error { 329 for _, worker := range p.workers { 330 _ = worker.AddToTaskExecutionMap(theType, handler) 331 } 332 // Do the same for input 333 return p.input.AddToTaskExecutionMap(theType, p.processNewTaskParam) 334 } 335 336 /* 337 StartEventLoop starts the daemon thread for processing the submitted task-parameters 338 339 @param wg *sync.WaitGroup - wait group 340 @return whether successful 341 */ 342 func (p *taskDemuxProcessorImpl) StartEventLoop(wg *sync.WaitGroup) error { 343 log.WithFields(p.LogTags).Info("Starting event loops") 344 // Start the worker loops first 345 for _, worker := range p.workers { 346 _ = worker.StartEventLoop(wg) 347 } 348 // Start the input loop 349 return p.input.StartEventLoop(wg) 350 } 351 352 /* 353 StopEventLoop stops the daemon thread 354 355 @return whether successful 356 */ 357 func (p *taskDemuxProcessorImpl) StopEventLoop() error { 358 p.contextCancel() 359 return nil 360 }