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  }