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] = ¶ms.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 }