github.com/alwitt/goutils@v0.6.4/req_resp.go (about)

     1  package goutils
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"sync"
     8  	"time"
     9  
    10  	"cloud.google.com/go/pubsub"
    11  	"github.com/apex/log"
    12  	"github.com/google/uuid"
    13  )
    14  
    15  // ReqRespMessage message structure passing through a request-response system
    16  type ReqRespMessage struct {
    17  	// RequestID request ID associated with this message
    18  	RequestID string `json:"request" validate:"required"`
    19  	// SenderID message sender ID
    20  	SenderID string `json:"sender" validate:"required"`
    21  	// TargetID message target ID
    22  	TargetID string `json:"target" validate:"required"`
    23  	// IsRequest whether the message is a request message
    24  	IsRequest bool `json:"is_request"`
    25  	// Timestamp message timestamp
    26  	Timestamp time.Time `json:"timestamp"`
    27  	// Metadata message metadata
    28  	Metadata map[string]string `json:"meta,omitempty"`
    29  	// Payload message payload
    30  	Payload []byte `json:"payload,omitempty"`
    31  }
    32  
    33  // ReqRespMessageHandler callback called when request-response message is available for processing
    34  type ReqRespMessageHandler func(ctxt context.Context, msg ReqRespMessage) error
    35  
    36  // ReqRespTimeoutHandler callback called when request timed out waiting for all responses
    37  type ReqRespTimeoutHandler func(ctxt context.Context) error
    38  
    39  // RequestCallParam contains the parameters of a request
    40  type RequestCallParam struct {
    41  	// RespHandler response message handler callback
    42  	RespHandler ReqRespMessageHandler
    43  	// ExpectedResponsesCount the expected number of responses to receive. Once this many responses
    44  	// are received, the request is considered to be complete.
    45  	ExpectedResponsesCount int
    46  	// Blocking whether the request call is blocking
    47  	Blocking bool
    48  	// Timeout the request timeout if it has not received all responses after this duration
    49  	Timeout time.Duration
    50  	// TimeoutHandler request timeout handler callback
    51  	// TODO FIXME: implement request timeout handling
    52  	TimeoutHandler ReqRespTimeoutHandler
    53  }
    54  
    55  // RequestResponseClient is a request-response client built on provided messaging transport
    56  //
    57  // Each client instance will respond to requests for a single request target ID
    58  type RequestResponseClient interface {
    59  	/*
    60  		SetInboundRequestHandler set the inbound request handler
    61  
    62  		 @param ctxt context.Context - execution context
    63  		 @param handler ReqRespMessageHandler - the handler to use
    64  	*/
    65  	SetInboundRequestHandler(ctxt context.Context, handler ReqRespMessageHandler) error
    66  
    67  	/*
    68  		Request make a new request
    69  
    70  		 @param ctxt context.Context - execution context
    71  		 @param targetID string - target ID this request is destined for
    72  		 @param message []byte - request message payload
    73  		 @param metadata map[string]string - request metadata
    74  		 @param callParam RequestCallParam - request call parameters
    75  		 @return request ID
    76  	*/
    77  	Request(
    78  		ctxt context.Context,
    79  		targetID string,
    80  		message []byte,
    81  		metadata map[string]string,
    82  		callParam RequestCallParam,
    83  	) (string, error)
    84  
    85  	/*
    86  		Respond respond to an inbound request
    87  
    88  		 @param ctxt context.Context - execution context
    89  		 @param originalReq ReqRespMessage - original request
    90  		 @param message []byte - response message payload
    91  		 @param metadata map[string]string - request metadata
    92  		 @param blocking bool - whether the call is blocking
    93  	*/
    94  	Respond(
    95  		ctxt context.Context,
    96  		originalReq ReqRespMessage,
    97  		message []byte,
    98  		metadata map[string]string,
    99  		blocking bool,
   100  	) error
   101  
   102  	/*
   103  		Stop stop any support background tasks which were started
   104  
   105  		 @param ctxt context.Context - execution context
   106  	*/
   107  	Stop(ctxt context.Context) error
   108  }
   109  
   110  // requestContextKey associated key for requestContext when storing in context
   111  type requestContextKey struct{}
   112  
   113  // requestContext represents a request made through the system
   114  type requestContext struct {
   115  	requestID         string
   116  	senderID          string
   117  	targetID          string
   118  	createdAt         time.Time
   119  	deadLine          time.Time
   120  	expectedRespCount int
   121  	receivedRespCount int
   122  	completeCallback  func()
   123  	callParam         RequestCallParam
   124  }
   125  
   126  // updateLogTags updates Apex log.Fields map with values the requests's parameters
   127  func (c *requestContext) updateLogTags(tags log.Fields) {
   128  	tags["req_request_id"] = c.requestID
   129  	tags["sender_id"] = c.senderID
   130  	tags["target_id"] = c.targetID
   131  }
   132  
   133  // String toString function
   134  func (c *requestContext) String() string {
   135  	return fmt.Sprintf(
   136  		"(REQ-RESP[%s] (%s ==> %s) DEADLINE %s)",
   137  		c.requestID,
   138  		c.senderID,
   139  		c.targetID,
   140  		c.deadLine.Format(time.RFC3339),
   141  	)
   142  }
   143  
   144  // modifyLogMetadataByRRRequestParam update log metadata with info from requestContext
   145  func modifyLogMetadataByRRRequestParam(ctxt context.Context, theTags log.Fields) {
   146  	if ctxt.Value(requestContextKey{}) != nil {
   147  		v, ok := ctxt.Value(requestContextKey{}).(requestContext)
   148  		if ok {
   149  			v.updateLogTags(theTags)
   150  		}
   151  	}
   152  }
   153  
   154  // pubsubReqRespClient implements RequestResponseClient using PubSub.
   155  type pubsubReqRespClient struct {
   156  	Component
   157  	targetID               string
   158  	psClient               PubSubClient
   159  	inboundProcessor       TaskProcessor
   160  	inboundRequestHandler  ReqRespMessageHandler
   161  	outboundProcessor      TaskProcessor
   162  	outboundRequests       map[string]*requestContext
   163  	parentContext          context.Context
   164  	processorContext       context.Context
   165  	processorContextCancel context.CancelFunc
   166  	checkTimer             IntervalTimer
   167  	wg                     sync.WaitGroup
   168  }
   169  
   170  // PubSubRequestResponseClientParam configuration parameters of PubSub based RequestResponseClient
   171  type PubSubRequestResponseClientParam struct {
   172  	// TargetID the ID to target to send a request (or response) to this client
   173  	TargetID string
   174  	// Name client instance name
   175  	Name string
   176  	// PSClient base pubsub client
   177  	PSClient PubSubClient
   178  	// MsgRetentionTTL PubSub message TTL, after which the message is purged.
   179  	MsgRetentionTTL time.Duration
   180  	// LogTags metadata fields to include in the logs
   181  	LogTags log.Fields
   182  	// CustomLogModifiers additional log metadata modifiers to use
   183  	CustomLogModifiers []LogMetadataModifier
   184  	// SupportWorkerCount number of support workers to spawn to process incoming messages
   185  	SupportWorkerCount int
   186  	// TimeoutEnforceInt interval between request timeout checks
   187  	TimeoutEnforceInt time.Duration
   188  }
   189  
   190  /*
   191  GetNewPubSubRequestResponseClientInstance get PubSub based RequestResponseClient
   192  
   193  	@param parentCtxt context.Context - parent context
   194  	@param params PubSubRequestResponseClientParam - client config parameters
   195  	@return new RequestResponseClient instance
   196  */
   197  func GetNewPubSubRequestResponseClientInstance(
   198  	parentCtxt context.Context,
   199  	params PubSubRequestResponseClientParam,
   200  ) (RequestResponseClient, error) {
   201  	params.LogTags["rr-instance"] = params.Name
   202  	params.LogTags["rr-target-id"] = params.TargetID
   203  
   204  	workerContext, workerCtxtCancel := context.WithCancel(parentCtxt)
   205  
   206  	// -----------------------------------------------------------------------------------------
   207  	// Prepare PubSub topic and subscription for inbound messages
   208  
   209  	// Prepare the topic
   210  	if existingTopic, err := params.PSClient.GetTopic(parentCtxt, params.TargetID); err != nil {
   211  		// Create new topic
   212  		log.WithFields(params.LogTags).Warnf("Topic '%s' is not known. Creating...", params.TargetID)
   213  		if err := params.PSClient.CreateTopic(
   214  			parentCtxt, params.TargetID, &pubsub.TopicConfig{RetentionDuration: params.MsgRetentionTTL},
   215  		); err != nil {
   216  			log.
   217  				WithError(err).
   218  				WithFields(params.LogTags).
   219  				Errorf("Failed when creating PubSub topic '%s'", params.TargetID)
   220  			workerCtxtCancel()
   221  			return nil, err
   222  		}
   223  	} else {
   224  		// Use existing topic
   225  		log.WithFields(params.LogTags).Infof("Reusing existing topic '%s'", params.TargetID)
   226  		updatedTopic := pubsub.TopicConfigToUpdate{
   227  			Labels:               existingTopic.Labels,
   228  			MessageStoragePolicy: &existingTopic.MessageStoragePolicy,
   229  			RetentionDuration:    params.MsgRetentionTTL,
   230  			SchemaSettings:       existingTopic.SchemaSettings,
   231  		}
   232  		if err := params.PSClient.UpdateTopic(parentCtxt, params.TargetID, updatedTopic); err != nil {
   233  			log.
   234  				WithError(err).
   235  				WithFields(params.LogTags).
   236  				Errorf("Failed when updating PubSub topic '%s'", params.TargetID)
   237  			workerCtxtCancel()
   238  			return nil, err
   239  		}
   240  	}
   241  
   242  	// Prepare the subscription
   243  	receiveSubscription := fmt.Sprintf("%s.%s", params.Name, params.TargetID)
   244  	if _, err := params.PSClient.GetSubscription(
   245  		parentCtxt, receiveSubscription,
   246  	); err != nil {
   247  		// Create new subscription
   248  		log.WithFields(params.LogTags).Warnf("Subscription '%s' is not known. Creating...", receiveSubscription)
   249  		if err := params.PSClient.CreateSubscription(
   250  			parentCtxt,
   251  			params.TargetID,
   252  			receiveSubscription,
   253  			pubsub.SubscriptionConfig{
   254  				ExpirationPolicy:  time.Hour * 24,
   255  				RetentionDuration: params.MsgRetentionTTL,
   256  			},
   257  		); err != nil {
   258  			log.
   259  				WithError(err).
   260  				WithFields(params.LogTags).
   261  				Errorf("Failed when creating PubSub subscription '%s'", receiveSubscription)
   262  			workerCtxtCancel()
   263  			return nil, err
   264  		}
   265  	} else {
   266  		// Use existing subscription
   267  		log.WithFields(params.LogTags).Infof("Reusing existing subscription '%s'", receiveSubscription)
   268  		updatedSubscription := pubsub.SubscriptionConfigToUpdate{
   269  			RetentionDuration: params.MsgRetentionTTL,
   270  			ExpirationPolicy:  time.Hour * 24,
   271  		}
   272  		if err := params.PSClient.UpdateSubscription(
   273  			parentCtxt, receiveSubscription, updatedSubscription,
   274  		); err != nil {
   275  			log.
   276  				WithError(err).
   277  				WithFields(params.LogTags).
   278  				Errorf("Failed when updating PubSub subscription '%s'", receiveSubscription)
   279  			workerCtxtCancel()
   280  			return nil, err
   281  		}
   282  	}
   283  
   284  	// -----------------------------------------------------------------------------------------
   285  	// Setup support daemon for processing inbound requests
   286  	procInboundInstanceName := fmt.Sprintf("%s-inbound-process", params.Name)
   287  	procInboundLogTags := log.Fields{}
   288  	for lKey, lVal := range params.LogTags {
   289  		procInboundLogTags[lKey] = lVal
   290  	}
   291  	procInboundLogTags["sub-module"] = procInboundInstanceName
   292  	inboundProcessor, err := GetNewTaskDemuxProcessorInstance(
   293  		workerContext,
   294  		procInboundInstanceName,
   295  		params.SupportWorkerCount*2,
   296  		params.SupportWorkerCount,
   297  		procInboundLogTags,
   298  	)
   299  	if err != nil {
   300  		log.WithError(err).WithFields(params.LogTags).Error("Unable to define inbound request processor")
   301  		workerCtxtCancel()
   302  		return nil, err
   303  	}
   304  
   305  	// Setup support daemon for processing outbound requests
   306  	procOutboundInstanceName := fmt.Sprintf("%s-outbound-process", params.Name)
   307  	procOutboundLogTags := log.Fields{}
   308  	for lKey, lVal := range params.LogTags {
   309  		procOutboundLogTags[lKey] = lVal
   310  	}
   311  	procOutboundLogTags["sub-module"] = procOutboundInstanceName
   312  	outboundProcessor, err := GetNewTaskProcessorInstance(
   313  		workerContext, procOutboundInstanceName, params.SupportWorkerCount*2, procOutboundLogTags,
   314  	)
   315  	if err != nil {
   316  		log.WithError(err).WithFields(params.LogTags).Error("Unable to define outbound request processor")
   317  		workerCtxtCancel()
   318  		return nil, err
   319  	}
   320  
   321  	// -----------------------------------------------------------------------------------------
   322  	// Build the component
   323  	instance := &pubsubReqRespClient{
   324  		Component: Component{
   325  			LogTags:         params.LogTags,
   326  			LogTagModifiers: []LogMetadataModifier{modifyLogMetadataByRRRequestParam},
   327  		},
   328  		targetID:               params.TargetID,
   329  		psClient:               params.PSClient,
   330  		inboundProcessor:       inboundProcessor,
   331  		inboundRequestHandler:  nil,
   332  		outboundProcessor:      outboundProcessor,
   333  		outboundRequests:       make(map[string]*requestContext),
   334  		parentContext:          parentCtxt,
   335  		processorContext:       workerContext,
   336  		processorContextCancel: workerCtxtCancel,
   337  		wg:                     sync.WaitGroup{},
   338  	}
   339  	timerLogTags := log.Fields{}
   340  	for lKey, lVal := range params.LogTags {
   341  		timerLogTags[lKey] = lVal
   342  	}
   343  	timerLogTags["sub-module"] = "timeout-enforce-timer"
   344  	instance.checkTimer, err = GetIntervalTimerInstance(workerContext, &instance.wg, timerLogTags)
   345  	if err != nil {
   346  		log.WithError(err).WithFields(params.LogTags).Error("Unable to define request timeout check timer")
   347  		workerCtxtCancel()
   348  		return nil, err
   349  	}
   350  
   351  	// Add additional log tag modifiers
   352  	instance.LogTagModifiers = append(instance.LogTagModifiers, params.CustomLogModifiers...)
   353  
   354  	// -----------------------------------------------------------------------------------------
   355  	// Define support tasks for processing outgoing requests
   356  
   357  	if err := outboundProcessor.AddToTaskExecutionMap(
   358  		reflect.TypeOf(rrRequestPayload{}), instance.request,
   359  	); err != nil {
   360  		log.WithError(err).WithFields(params.LogTags).Error("Unable to install task definition")
   361  		return nil, err
   362  	}
   363  
   364  	// Incoming responses correspond with outgoing requests
   365  	if err := outboundProcessor.AddToTaskExecutionMap(
   366  		reflect.TypeOf(rrInboundResponsePayload{}), instance.receiveInboundResponseMsg,
   367  	); err != nil {
   368  		log.WithError(err).WithFields(params.LogTags).Error("Unable to install task definition")
   369  		return nil, err
   370  	}
   371  
   372  	// Trigger request timeout enforcement
   373  	if err := outboundProcessor.AddToTaskExecutionMap(
   374  		reflect.TypeOf(rrRequestTimeoutCheckPayload{}), instance.requestTimeoutCheck,
   375  	); err != nil {
   376  		log.WithError(err).WithFields(params.LogTags).Error("Unable to install task definition")
   377  		return nil, err
   378  	}
   379  
   380  	// -----------------------------------------------------------------------------------------
   381  	// Define support tasks for processing incoming requests
   382  
   383  	if err := inboundProcessor.AddToTaskExecutionMap(
   384  		reflect.TypeOf(rrInboundRequestPayload{}), instance.receiveInboundRequestMsg,
   385  	); err != nil {
   386  		log.WithError(err).WithFields(params.LogTags).Error("Unable to install task definition")
   387  		return nil, err
   388  	}
   389  
   390  	// Outgoing responses correspond with incoming requests
   391  	if err := inboundProcessor.AddToTaskExecutionMap(
   392  		reflect.TypeOf(rrResponsePayload{}), instance.respond,
   393  	); err != nil {
   394  		log.WithError(err).WithFields(params.LogTags).Error("Unable to install task definition")
   395  		return nil, err
   396  	}
   397  
   398  	// -----------------------------------------------------------------------------------------
   399  	// Listen for messages on previously created subscription
   400  
   401  	instance.wg.Add(1)
   402  	go func() {
   403  		defer instance.wg.Done()
   404  		err := params.PSClient.Subscribe(workerContext, receiveSubscription, instance.ReceivePubSubMsg)
   405  		logTags := instance.GetLogTagsForContext(workerContext)
   406  		if err != nil {
   407  			log.
   408  				WithError(err).
   409  				WithFields(logTags).
   410  				Errorf("Receive failure on subscription '%s'", receiveSubscription)
   411  		}
   412  	}()
   413  
   414  	// -----------------------------------------------------------------------------------------
   415  	// Start the daemon processors
   416  
   417  	if err := outboundProcessor.StartEventLoop(&instance.wg); err != nil {
   418  		log.WithError(err).WithFields(params.LogTags).Error("Unable to outbound processing task pool")
   419  		return nil, err
   420  	}
   421  	if err := inboundProcessor.StartEventLoop(&instance.wg); err != nil {
   422  		log.WithError(err).WithFields(params.LogTags).Error("Unable to inbound processing task pool")
   423  		return nil, err
   424  	}
   425  
   426  	// -----------------------------------------------------------------------------------------
   427  	// Start the timeout check timer
   428  
   429  	if err := instance.checkTimer.Start(params.TimeoutEnforceInt, func() error {
   430  		return instance.RequestTimeoutCheck(workerContext)
   431  	}, false); err != nil {
   432  		log.WithError(err).WithFields(params.LogTags).Error("Unable to start request timeout check timer")
   433  		return nil, err
   434  	}
   435  
   436  	return instance, nil
   437  }
   438  
   439  /*
   440  SetInboundRequestHandler set the inbound request handler
   441  
   442  	@param ctxt context.Context - execution context
   443  	@param handler ReqRespMessageHandler - the handler to use
   444  */
   445  func (c *pubsubReqRespClient) SetInboundRequestHandler(
   446  	ctxt context.Context, handler ReqRespMessageHandler,
   447  ) error {
   448  	logTags := c.GetLogTagsForContext(ctxt)
   449  	c.inboundRequestHandler = handler
   450  	log.WithFields(logTags).Info("Changed inbound request handler")
   451  	return nil
   452  }
   453  
   454  /*
   455  Stop stop any support background tasks which were started
   456  
   457  	@param ctxt context.Context - execution context
   458  */
   459  func (c *pubsubReqRespClient) Stop(ctxt context.Context) error {
   460  	c.processorContextCancel()
   461  
   462  	if err := c.inboundProcessor.StopEventLoop(); err != nil {
   463  		return err
   464  	}
   465  
   466  	if err := c.outboundProcessor.StopEventLoop(); err != nil {
   467  		return err
   468  	}
   469  
   470  	if err := c.checkTimer.Stop(); err != nil {
   471  		return err
   472  	}
   473  
   474  	return TimeBoundedWaitGroupWait(c.parentContext, &c.wg, time.Second*10)
   475  }
   476  
   477  // ===========================================================================================
   478  // Make Outbound Request
   479  
   480  type rrRequestPayload struct {
   481  	requestID string
   482  	request   requestContext
   483  	message   []byte
   484  	metadata  map[string]string
   485  }
   486  
   487  const (
   488  	rrMsgAttributeNameRequestID = "__request_id__"
   489  	rrMsgAttributeNameSenderID  = "__request_sender_id__"
   490  	rrMsgAttributeNameTargetID  = "__request_target_id__"
   491  	rrMsgAttributeNameIsRequest = "__is_request__"
   492  )
   493  
   494  /*
   495  Request make a new request
   496  
   497  	@param ctxt context.Context - execution context
   498  	@param targetID string - target ID this request is destined for
   499  	@param message []byte - request message payload
   500  	@param metadata map[string]string - request metadata
   501  	@param callParam RequestCallParam - request call parameters
   502  	@return request ID
   503  */
   504  func (c *pubsubReqRespClient) Request(
   505  	ctxt context.Context,
   506  	targetID string,
   507  	message []byte,
   508  	metadata map[string]string,
   509  	callParam RequestCallParam,
   510  ) (string, error) {
   511  	logTags := c.GetLogTagsForContext(ctxt)
   512  
   513  	requestID := uuid.NewString()
   514  	currentTime := time.Now().UTC()
   515  
   516  	// Sanity check the request parameters
   517  	if callParam.ExpectedResponsesCount < 1 {
   518  		return "", fmt.Errorf("request which require no response does not fit supported usage")
   519  	}
   520  	if callParam.RespHandler == nil || callParam.TimeoutHandler == nil {
   521  		return "", fmt.Errorf("request must define all handler callbacks")
   522  	}
   523  
   524  	// Define new outbound request
   525  	requestEntry := requestContext{
   526  		requestID:         requestID,
   527  		senderID:          c.targetID,
   528  		targetID:          targetID,
   529  		createdAt:         currentTime,
   530  		deadLine:          currentTime.Add(callParam.Timeout),
   531  		expectedRespCount: callParam.ExpectedResponsesCount,
   532  		receivedRespCount: 0,
   533  		completeCallback:  nil,
   534  		callParam:         callParam,
   535  	}
   536  
   537  	// Define back channel, if call is blocking
   538  	finishSignal := make(chan bool)
   539  	if callParam.Blocking {
   540  		requestEntry.completeCallback = func() {
   541  			finishSignal <- true
   542  		}
   543  	}
   544  
   545  	log.
   546  		WithFields(logTags).
   547  		WithField("target-id", targetID).
   548  		WithField("request-id", requestID).
   549  		Info("Submitting new outbound REQUEST")
   550  
   551  	// Make the request
   552  	taskParams := rrRequestPayload{
   553  		requestID: requestID,
   554  		request:   requestEntry,
   555  		message:   message,
   556  		metadata:  metadata,
   557  	}
   558  	if err := c.outboundProcessor.Submit(ctxt, taskParams); err != nil {
   559  		log.
   560  			WithError(err).
   561  			WithFields(logTags).
   562  			WithField("target-id", targetID).
   563  			WithField("request-id", requestID).
   564  			Error("Failed to submit 'Request' job")
   565  		return requestID, err
   566  	}
   567  
   568  	// Wait for all responses to be processed, if blocking
   569  	if callParam.Blocking {
   570  		log.
   571  			WithFields(logTags).
   572  			WithField("target-id", targetID).
   573  			WithField("request-id", requestID).
   574  			Info("Awaiting all responses")
   575  
   576  		select {
   577  		case <-ctxt.Done():
   578  			err := fmt.Errorf("request timed out waiting for all responses")
   579  			log.
   580  				WithError(err).
   581  				WithFields(logTags).
   582  				WithField("target-id", targetID).
   583  				WithField("request-id", requestID).
   584  				Error("Outbound request failed")
   585  			return requestID, err
   586  		case <-finishSignal:
   587  			log.
   588  				WithFields(logTags).
   589  				WithField("target-id", targetID).
   590  				WithField("request-id", requestID).
   591  				Info("Received all responses")
   592  		}
   593  	}
   594  	return requestID, nil
   595  }
   596  
   597  func (c *pubsubReqRespClient) request(params interface{}) error {
   598  	// Convert params into expected data type
   599  	if requestParams, ok := params.(rrRequestPayload); ok {
   600  		return c.handleRequest(requestParams)
   601  	}
   602  	err := fmt.Errorf("received unexpected call parameters: %s", reflect.TypeOf(params))
   603  	logTags := c.GetLogTagsForContext(c.processorContext)
   604  	log.WithError(err).WithFields(logTags).Error("'Request' processing failure")
   605  	return err
   606  }
   607  
   608  // handleRequest contains the actual logic for `Request`
   609  func (c *pubsubReqRespClient) handleRequest(params rrRequestPayload) error {
   610  	lclCtxt := context.WithValue(c.processorContext, requestContextKey{}, params.request)
   611  	logTags := c.GetLogTagsForContext(lclCtxt)
   612  
   613  	if params.metadata == nil {
   614  		params.metadata = make(map[string]string)
   615  	}
   616  
   617  	// Add additional information to message metadata
   618  	params.metadata[rrMsgAttributeNameRequestID] = params.requestID
   619  	params.metadata[rrMsgAttributeNameSenderID] = c.targetID
   620  	params.metadata[rrMsgAttributeNameTargetID] = params.request.targetID
   621  	params.metadata[rrMsgAttributeNameIsRequest] = "true"
   622  
   623  	// Publish the message
   624  	log.
   625  		WithFields(logTags).
   626  		Debug("Publishing new request")
   627  	if _, err := c.psClient.Publish(
   628  		c.processorContext, params.request.targetID, params.message, params.metadata, true,
   629  	); err != nil {
   630  		log.
   631  			WithError(err).
   632  			WithFields(logTags).
   633  			Error("PubSub publish failed")
   634  		return err
   635  	}
   636  
   637  	// Record the message context
   638  	c.outboundRequests[params.requestID] = &params.request
   639  
   640  	log.
   641  		WithFields(logTags).
   642  		Debug("Published new request")
   643  
   644  	return nil
   645  }
   646  
   647  // ===========================================================================================
   648  // Send Outbound Response
   649  
   650  type rrResponsePayload struct {
   651  	request  requestContext
   652  	message  []byte
   653  	metadata map[string]string
   654  }
   655  
   656  /*
   657  Respond respond to an inbound request
   658  
   659  	@param ctxt context.Context - execution context
   660  	@param originalReq ReqRespMessage - original request
   661  	@param message []byte - response message payload
   662  	@param metadata map[string]string - request metadata
   663  	@param blocking bool - whether the call is blocking
   664  */
   665  func (c *pubsubReqRespClient) Respond(
   666  	ctxt context.Context,
   667  	originalReq ReqRespMessage,
   668  	message []byte,
   669  	metadata map[string]string,
   670  	blocking bool,
   671  ) error {
   672  	logTags := c.GetLogTagsForContext(ctxt)
   673  
   674  	currentTime := time.Now().UTC()
   675  
   676  	// Define inbound response context
   677  	response := requestContext{
   678  		requestID:        originalReq.RequestID,
   679  		senderID:         originalReq.TargetID,
   680  		targetID:         originalReq.SenderID,
   681  		createdAt:        currentTime,
   682  		completeCallback: nil,
   683  	}
   684  
   685  	// Define back channel, if call is blocking
   686  	finishSignal := make(chan bool)
   687  	if blocking {
   688  		response.completeCallback = func() {
   689  			finishSignal <- true
   690  		}
   691  	}
   692  
   693  	log.
   694  		WithFields(logTags).
   695  		WithField("orig-target-id", originalReq.TargetID).
   696  		WithField("orig-request-id", originalReq.RequestID).
   697  		Info("Submitting new outbound RESPONSE")
   698  
   699  	// Make the request
   700  	taskParams := rrResponsePayload{
   701  		request:  response,
   702  		message:  message,
   703  		metadata: metadata,
   704  	}
   705  	if err := c.inboundProcessor.Submit(ctxt, taskParams); err != nil {
   706  		log.
   707  			WithError(err).
   708  			WithFields(logTags).
   709  			WithField("orig-target-id", originalReq.TargetID).
   710  			WithField("orig-request-id", originalReq.RequestID).
   711  			Error("Failed to submit 'Respond' job")
   712  		return err
   713  	}
   714  
   715  	// Wait for response to be sent
   716  	if blocking {
   717  		log.
   718  			WithFields(logTags).
   719  			WithField("orig-target-id", originalReq.TargetID).
   720  			WithField("orig-request-id", originalReq.RequestID).
   721  			Info("Wait until response sent")
   722  
   723  		select {
   724  		case <-ctxt.Done():
   725  			err := fmt.Errorf("request timed out waiting response transmit")
   726  			log.
   727  				WithError(err).
   728  				WithFields(logTags).
   729  				WithField("orig-target-id", originalReq.TargetID).
   730  				WithField("orig-request-id", originalReq.RequestID).
   731  				Error("Outbound response failed")
   732  			return err
   733  		case <-finishSignal:
   734  			log.
   735  				WithFields(logTags).
   736  				WithField("orig-target-id", originalReq.TargetID).
   737  				WithField("orig-request-id", originalReq.RequestID).
   738  				Info("Transmitted response")
   739  		}
   740  	}
   741  	return nil
   742  }
   743  
   744  func (c *pubsubReqRespClient) respond(params interface{}) error {
   745  	// Convert params into expected data type
   746  	if responseParams, ok := params.(rrResponsePayload); ok {
   747  		return c.handleRespond(responseParams)
   748  	}
   749  	err := fmt.Errorf("received unexpected call parameters: %s", reflect.TypeOf(params))
   750  	logTags := c.GetLogTagsForContext(c.processorContext)
   751  	log.WithError(err).WithFields(logTags).Error("'Respond' processing failure")
   752  	return err
   753  }
   754  
   755  // handleRespond contains the actual logic for `Respond`
   756  func (c *pubsubReqRespClient) handleRespond(params rrResponsePayload) error {
   757  	lclCtxt := context.WithValue(c.processorContext, requestContextKey{}, params.request)
   758  	logTags := c.GetLogTagsForContext(lclCtxt)
   759  
   760  	if params.metadata == nil {
   761  		params.metadata = make(map[string]string)
   762  	}
   763  
   764  	// Add additional information to message metadata
   765  	params.metadata[rrMsgAttributeNameRequestID] = params.request.requestID
   766  	params.metadata[rrMsgAttributeNameSenderID] = c.targetID
   767  	params.metadata[rrMsgAttributeNameTargetID] = params.request.targetID
   768  	params.metadata[rrMsgAttributeNameIsRequest] = "false"
   769  
   770  	// Publish the message
   771  	log.
   772  		WithFields(logTags).
   773  		Debug("Publishing new response")
   774  	if _, err := c.psClient.Publish(
   775  		c.processorContext, params.request.targetID, params.message, params.metadata, true,
   776  	); err != nil {
   777  		log.
   778  			WithError(err).
   779  			WithFields(logTags).
   780  			Error("PubSub publish failed")
   781  		return err
   782  	}
   783  
   784  	log.
   785  		WithFields(logTags).
   786  		Debug("Published new request")
   787  
   788  	// Unblock the original caller
   789  	if params.request.completeCallback != nil {
   790  		params.request.completeCallback()
   791  	}
   792  
   793  	return nil
   794  }
   795  
   796  // ===========================================================================================
   797  // Receive inbound messages
   798  
   799  type rrInboundRequestPayload struct {
   800  	requestID string
   801  	entry     requestContext
   802  	message   []byte
   803  	metadata  map[string]string
   804  }
   805  
   806  type rrInboundResponsePayload struct {
   807  	senderID  string
   808  	requestID string
   809  	timestamp time.Time
   810  	message   []byte
   811  	metadata  map[string]string
   812  }
   813  
   814  /*
   815  ReceivePubSubMsg receive PubSub message on topic `c.targetID`
   816  
   817  	@param ctxt context.Context - execution context
   818  	@param pubTimestamp time.Time - timestamp when the message received by the PubSub system
   819  	@param msg []byte - message payload
   820  	@param metadata map[string]string - message metadata
   821  */
   822  func (c *pubsubReqRespClient) ReceivePubSubMsg(
   823  	ctxt context.Context, pubTimestamp time.Time, msg []byte, metadata map[string]string,
   824  ) error {
   825  	logTags := c.GetLogTagsForContext(ctxt)
   826  
   827  	requestID, ok := metadata[rrMsgAttributeNameRequestID]
   828  	if !ok {
   829  		err := fmt.Errorf("message did not come with request ID")
   830  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound message")
   831  		return err
   832  	}
   833  
   834  	senderID, ok := metadata[rrMsgAttributeNameSenderID]
   835  	if !ok {
   836  		err := fmt.Errorf("message did not come with sender ID")
   837  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound message")
   838  		return err
   839  	}
   840  
   841  	if msgTargetID, ok := metadata[rrMsgAttributeNameTargetID]; !ok {
   842  		err := fmt.Errorf("message did not come with target ID")
   843  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound message")
   844  		return err
   845  	} else if msgTargetID != c.targetID {
   846  		err := fmt.Errorf("message target ID does not match client: %s =/= %s", msgTargetID, c.targetID)
   847  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound message")
   848  		return err
   849  	}
   850  
   851  	// Process based on whether the message is an inbound request or response
   852  	msgType, ok := metadata[rrMsgAttributeNameIsRequest]
   853  	if ok && msgType == "true" {
   854  		// An inbound request
   855  
   856  		// Define inbound request
   857  		requestEntry := requestContext{
   858  			requestID: requestID,
   859  			senderID:  senderID,
   860  			targetID:  c.targetID,
   861  			createdAt: pubTimestamp,
   862  		}
   863  		// Define task param
   864  		taskParams := rrInboundRequestPayload{
   865  			requestID: requestID,
   866  			entry:     requestEntry,
   867  			message:   msg,
   868  			metadata:  metadata,
   869  		}
   870  
   871  		log.
   872  			WithFields(logTags).
   873  			WithField("sender-id", senderID).
   874  			WithField("request-id", requestID).
   875  			Debug("Submitting inbound request for processing")
   876  
   877  		// Make the request
   878  		if err := c.inboundProcessor.Submit(ctxt, taskParams); err != nil {
   879  			log.
   880  				WithError(err).
   881  				WithFields(logTags).
   882  				WithField("sender-id", senderID).
   883  				WithField("request-id", requestID).
   884  				Error("Failed to submit 'ReceivePubSubMsg' job for inbound request")
   885  			return err
   886  		}
   887  	} else {
   888  		// An inbound response
   889  
   890  		// Define task param
   891  		taskParams := rrInboundResponsePayload{
   892  			senderID:  senderID,
   893  			requestID: requestID,
   894  			timestamp: pubTimestamp,
   895  			message:   msg,
   896  			metadata:  metadata,
   897  		}
   898  
   899  		log.
   900  			WithFields(logTags).
   901  			WithField("sender-id", senderID).
   902  			WithField("request-id", requestID).
   903  			Debug("Submitting inbound response for processing")
   904  
   905  		// Make the request
   906  		if err := c.outboundProcessor.Submit(ctxt, taskParams); err != nil {
   907  			log.
   908  				WithError(err).
   909  				WithFields(logTags).
   910  				WithField("sender-id", senderID).
   911  				WithField("request-id", requestID).
   912  				Error("Failed to submit 'ReceivePubSubMsg' job for inbound response")
   913  			return err
   914  		}
   915  	}
   916  
   917  	return nil
   918  }
   919  
   920  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   921  // Receive inbound requests
   922  
   923  func (c *pubsubReqRespClient) receiveInboundRequestMsg(params interface{}) error {
   924  	// Convert params into expected data type
   925  	if inboundReqParams, ok := params.(rrInboundRequestPayload); ok {
   926  		return c.handleInboundRequest(inboundReqParams)
   927  	}
   928  	err := fmt.Errorf("received unexpected call parameters: %s", reflect.TypeOf(params))
   929  	logTags := c.GetLogTagsForContext(c.processorContext)
   930  	log.
   931  		WithError(err).
   932  		WithFields(logTags).
   933  		Error("'ReceivePubSubMsg' inbound request processing failure")
   934  	return err
   935  }
   936  
   937  func (c *pubsubReqRespClient) handleInboundRequest(params rrInboundRequestPayload) error {
   938  	// Build new context for local logging
   939  	lclCtxt := context.WithValue(c.processorContext, requestContextKey{}, params.entry)
   940  	logTags := c.GetLogTagsForContext(lclCtxt)
   941  
   942  	if c.inboundRequestHandler == nil {
   943  		err := fmt.Errorf("no handler installed for inbound requests")
   944  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound request")
   945  		return err
   946  	}
   947  
   948  	// Package the message up for forwarding
   949  	forwardMsg := ReqRespMessage{
   950  		RequestID: params.requestID,
   951  		SenderID:  params.entry.senderID,
   952  		TargetID:  params.entry.targetID,
   953  		IsRequest: true,
   954  		Timestamp: params.entry.createdAt,
   955  		Metadata:  params.metadata,
   956  		Payload:   params.message,
   957  	}
   958  
   959  	log.WithFields(logTags).Debug("Forwarding request for processing")
   960  	// Call the request handler
   961  	if err := c.inboundRequestHandler(lclCtxt, forwardMsg); err != nil {
   962  		// TODO: in the future, sort out how to handle failure in upstream processing.
   963  		// Dead letter queue, retry, return to sender, etc.
   964  		log.WithError(err).WithFields(logTags).Error("Upstream failed to process inbound request")
   965  	}
   966  	log.WithFields(logTags).Debug("Request processed")
   967  
   968  	return nil
   969  }
   970  
   971  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
   972  // Receive inbound responses
   973  
   974  func (c *pubsubReqRespClient) receiveInboundResponseMsg(params interface{}) error {
   975  	// Convert params into expected data type
   976  	if inboundRespParams, ok := params.(rrInboundResponsePayload); ok {
   977  		return c.handleInboundResponse(inboundRespParams)
   978  	}
   979  	err := fmt.Errorf("received unexpected call parameters: %s", reflect.TypeOf(params))
   980  	logTags := c.GetLogTagsForContext(c.processorContext)
   981  	log.
   982  		WithError(err).
   983  		WithFields(logTags).
   984  		Error("'ReceivePubSubMsg' inbound response processing failure")
   985  	return err
   986  }
   987  
   988  func (c *pubsubReqRespClient) handleInboundResponse(params rrInboundResponsePayload) error {
   989  	// Find the associated outbound request
   990  	logTags := c.GetLogTagsForContext(c.processorContext)
   991  
   992  	originalReq, ok := c.outboundRequests[params.requestID]
   993  	if !ok {
   994  		err := fmt.Errorf("request ID '%s' did not originate from this client", params.requestID)
   995  		log.WithError(err).WithFields(logTags).Error("Unable to process inbound response")
   996  		return err
   997  	}
   998  
   999  	// Build new context for local logging
  1000  	lclCtxt := context.WithValue(c.processorContext, requestContextKey{}, originalReq)
  1001  	logTags = c.GetLogTagsForContext(lclCtxt)
  1002  
  1003  	// Package the message up for forwarding
  1004  	forwardMsg := ReqRespMessage{
  1005  		RequestID: params.requestID,
  1006  		SenderID:  params.senderID,
  1007  		TargetID:  originalReq.senderID,
  1008  		IsRequest: false,
  1009  		Timestamp: params.timestamp,
  1010  		Metadata:  params.metadata,
  1011  		Payload:   params.message,
  1012  	}
  1013  
  1014  	log.WithFields(logTags).Debug("Forwarding response for processing")
  1015  	// Forward the response to the original caller
  1016  	if err := originalReq.callParam.RespHandler(lclCtxt, forwardMsg); err != nil {
  1017  		// TODO: in the future, sort out how to handle failure in upstream processing.
  1018  		// Dead letter queue, retry, return to sender, etc.
  1019  		log.WithError(err).WithFields(logTags).Error("Upstream failed to process inbound response")
  1020  	}
  1021  	log.WithFields(logTags).Debug("Response processed")
  1022  
  1023  	// Record one more response collected
  1024  	originalReq.receivedRespCount++
  1025  
  1026  	// Clean up if all responses received
  1027  	if originalReq.receivedRespCount == originalReq.expectedRespCount {
  1028  		log.WithFields(logTags).Debug("All expected responses received")
  1029  
  1030  		// Forget the request entry
  1031  		delete(c.outboundRequests, params.requestID)
  1032  
  1033  		if originalReq.completeCallback != nil {
  1034  			// Notify anyone waiting
  1035  			originalReq.completeCallback()
  1036  		}
  1037  	}
  1038  
  1039  	return nil
  1040  }
  1041  
  1042  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  1043  // Trigger request timeout enforcement
  1044  
  1045  type rrRequestTimeoutCheckPayload struct {
  1046  	timestamp time.Time
  1047  }
  1048  
  1049  // RequestTimeoutCheck execute to enforce request timeout
  1050  func (c *pubsubReqRespClient) RequestTimeoutCheck(ctxt context.Context) error {
  1051  	logTags := c.GetLogTagsForContext(ctxt)
  1052  
  1053  	currentTime := time.Now().UTC()
  1054  
  1055  	log.
  1056  		WithFields(logTags).
  1057  		Debug("Submitting request timeout check trigger")
  1058  
  1059  	if err := c.outboundProcessor.Submit(
  1060  		ctxt, rrRequestTimeoutCheckPayload{timestamp: currentTime},
  1061  	); err != nil {
  1062  		log.
  1063  			WithError(err).
  1064  			WithFields(logTags).
  1065  			Error("Failed to submit 'RequestTimeoutCheck' job")
  1066  		return err
  1067  	}
  1068  
  1069  	return nil
  1070  }
  1071  
  1072  func (c *pubsubReqRespClient) requestTimeoutCheck(params interface{}) error {
  1073  	// Convert params into expected data type
  1074  	if requestParams, ok := params.(rrRequestTimeoutCheckPayload); ok {
  1075  		return c.handleRequestTimeoutCheck(requestParams)
  1076  	}
  1077  	err := fmt.Errorf("received unexpected call parameters: %s", reflect.TypeOf(params))
  1078  	logTags := c.GetLogTagsForContext(c.processorContext)
  1079  	log.WithError(err).WithFields(logTags).Error("'RequestTimeoutCheck' processing failure")
  1080  	return err
  1081  }
  1082  
  1083  // handleRequestTimeoutCheck contains the actual logic for `RequestTimeoutCheck`
  1084  func (c *pubsubReqRespClient) handleRequestTimeoutCheck(params rrRequestTimeoutCheckPayload) error {
  1085  	logTags := c.GetLogTagsForContext(c.parentContext)
  1086  
  1087  	// Go through all the request, and find the ones which have timed out
  1088  	removeRequests := []string{}
  1089  	for requestID, request := range c.outboundRequests {
  1090  		log.WithFields(logTags).Debugf("Checking request %s for timeout", request.String())
  1091  		if request.deadLine.Before(params.timestamp) && request.receivedRespCount < request.expectedRespCount {
  1092  			log.WithFields(logTags).Infof("Request '%s' has timed out", requestID)
  1093  			removeRequests = append(removeRequests, requestID)
  1094  
  1095  			// Notify the original caller that the request timed out
  1096  			if err := request.callParam.TimeoutHandler(c.parentContext); err != nil {
  1097  				log.
  1098  					WithError(err).
  1099  					WithFields(logTags).
  1100  					Errorf("Errored when calling timeout handler of request '%s'", requestID)
  1101  			}
  1102  		}
  1103  	}
  1104  
  1105  	// Remove the request entries
  1106  	for _, requestID := range removeRequests {
  1107  		delete(c.outboundRequests, requestID)
  1108  	}
  1109  
  1110  	return nil
  1111  }