Question
· Aug 8, 2023

Issues with Automatic Resending in a Business Operation

Context

I have created a Business Operation (BO) named "Sender" that sends HTTP messages to an endpoint (for testing, I'm using Postman's Mock Servers).

Goal: I want to set up an automatic timeout mechanism so that if I don't receive a response from the server within 18 seconds, an alert is generated and the message is resent. This process should be repeated every 18 seconds, for a maximum of 90 seconds. If no response is received within 90 seconds, I would like to generate an error message. On the other hand, if a response is received, I want to stop the resending process and complete the operation by indicating successful reception.

Current Configuration: Currently, I have the following settings:

  • Adapter: "EnsLib.HTTP.OutboundAdapter" (set via code)
  • Retry = 1 (set via code)
  • Connect Timeout = 5 (set through BO's Connection Settings)
  • Response Timeout = 18 (set through BO's Connection Settings)
  • Reply Code Actions = E=R (set through BO's Additional Settings)
  • Retry Interval = 5
  • Failure Timeout = 90 (set through BO's Additional Settings)

Code

Here's a summary version of the main functions of my code:

/// Method to send a POST message to the selected endpoint
Method SendPostRequest(pRequest As User.SendMessageReq, pResponse As EnsLib.HTTP.GenericMessage) As %Status
{
    Set sc = $$$OK
    Set InputStream = pRequest.Stream     
    
    // Log an alert if no response is received for the first message
    If ..RetryCount '= 1 {
        $$$LOGALERT("object: 'No response was received to the previous message. Attempting to resend the message.', "_$CHAR(10)_"retryCount: '"_..RetryCount_"'")
    }     
    Set ..Retry = 1     
    
    // Find the endpoint associated with the desired laboratory
    Set sc = ..FindApplication(.application, .pResponse)     
    
    // Create an HTTP request to send to the endpoint address associated with the selected application
    Set sc = ..CreateHTTPRequest(application, InputStream, .httpRequest)     
    
    // Find the operation to perform
    If pRequest.MessageType = "New Order" {
        Set operation = "/SendRequestTo_ES"
    } ElseIf pRequest.MessageType = "Response" {
        Set operation = "/ERreceiveMessage"
    } ElseIf pRequest.MessageType = "Notification" {
        Set operation = "/ERreceiveNotification"
    }     
    
    // Send an HTTP POST request
    Set messageStatus = httpRequest.Post(operation)     
    
    // Verify that the message was sent successfully and that the response arrived within the timeout
    Set sc = ..VerifyMessageStatus(httpRequest, messageStatus, .pResponse)     
    
    // If a response was received, check its HTTP status
    Set sc = ..CheckResponseStatus(httpRequest, .pResponse)     
    
    // If everything went well, generate a positive response
    Set pResponse = ..GenerateResponse(sc, httpRequest)     
    
    Return sc
}

Issues Encountered

  • This is not necessarily a problem, but I noticed that the method SendPostRequest is executed again from start to finish for each reply, reinitializing all variables as if it were the first execution, except for the RetryCount variable, which indicates the current resend count.
  • Setting Retry = 0 prevents message resending, even with active Reply Code Actions (e.g., E=R).
  • The timeouts are not respected. Instead, the message is resent every 30 seconds instead of 18 (I also tried other Response Timeout values like 20 or 35 seconds, but nothing changes). The Retry Interval is not respected either; the resending is attempted a number of times equal to FailureTimeout/30 instead of FailureTimeout/RetryInterval, as written in the documentation.
  • After the Failure Timeout expires, an error is generated even if one or more responses have been received. Despite the responses arriving, the BO does not detect them and generates the error: "ERROR 5922: Timed out waiting for response". The message continues to be resent until the Failure Timeout expires, and finally the "ERROR <Ens>ErrFailureTimeout: FailureTimeout of 90 seconds exceeded in lombardia.OMR.BO.Sender; status from last attempt was OK".

Visual Trace

In the provided Visual Trace image with visible I/O messages we can see: 

  • Two alerts ([36], [38]), in yellow, indicating two resends (so, here I sent a total of three messages), generated at a time interval of 30 seconds each. 
  • Three #5922 error ([35], [37], [39]) which are returned after sending the POST request (in the code: Set messageStatus = httpRequest.Post(operation)). These errors are returned only for debugging purposes and are generated at a time interval of 30 seconds each.
  • A final error ([40]): "ERROR <Ens>ErrFailureTimeout", that is generated at the end of the Failure Timeout. It is not clear at which point in the code it appears. According to the Stack details, it seems to be related to the MessageHeaderHandler method, that is not a method defined by me but a system method (I think).
  • As we can see, three messages were received in response ([41], [42], [43]) but the BO doesn't sees them and return the last error [40] and finally a NULL pResponse

Any ideas on how to resolve this matter? Thank you to those who will respond! 😊

Product version: IRIS 2021.1
$ZV: 2021.1.0.215.0
Discussion (7)3
Log in or sign up to continue

Hi Pietro,

Not sure if this will help you but I written a retry mechanisme in the BO with 2 additional settings MaxRetries and RetryWaitSeconds. 

See code below:

Class BO.Rest.PostBodyHeaderAuth Extends EnsLib.REST.Operation
{

Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter";

Parameter INVOCATION = "Queue";

Property HeaderName As %String(MAXLEN = 1000) [ InitialExpression = "Authorization", Required ];

Property HeaderValue As %String(MAXLEN = 10000) [ InitialExpression = "Bearer", Required ];

Property UseSSL As %Integer [ InitialExpression = 1, Required ];

Property MaxRetries As %Integer [ InitialExpression = 3, Required ];

Property RetryWaitSeconds As %Integer [ InitialExpression = 300, Required ];

Property ContentType As %String(MAXLEN = 300) [ InitialExpression = "application/json; charset=utf-8", Required ];

Parameter SETTINGS = "HeaderName:Auth,HeaderValue:Auth,MaxRetries:Retry,RetryWaitSeconds:Retry,UseSSL:Connection,ContentType:ContentType";

Method PostBodyData(pRequest As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthReq, Output pResponse As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp) As %Status
{
      Set tSc	= $$$OK
      
      Set tCounter = 0
      
      While (tCounter <= ..MaxRetries)
      {
        //Create a new %Net.HttpRequest
        Set tHttpRequest	= ##class(%Net.HttpRequest).%New()
            
        //Get the serversetting from the Adapter
        Set tHttpRequest.Server	= ..Adapter.HTTPServer
        Set tHttpRequest.Port		= ..Adapter.HTTPPort
            
        //Set Content-Type
        Set tHttpRequest.ContentType = ..ContentType
      
        //Set Body from Request Object
        Do tHttpRequest.EntityBody.Write(pRequest.Body)
        
        // Set Https Flag
        If (..UseSSL = 1) {
            Set tHttpRequest.Https	= 1
            Set tHttpRequest.SSLConfiguration	= ..Adapter.SSLConfig
        } Else {
            Set tHttpRequest.Https	= 0
        }
          
        //Set the Header value for Authentication
        If ($LENGTH(pRequest.HeaderValue) > 0) {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, pRequest.HeaderValue)
        } Else {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, ..HeaderValue)  
        }
        
        //Set Custom Timeouts from Adapter
        Set tHttpRequest.WriteTimeout = ..Adapter.WriteTimeout
        Set tHttpRequest.Timeout = ..Adapter.ResponseTimeout
        Set tHttpRequest.OpenTimeout  = ..Adapter.ConnectTimeout
            
        If ($LENGTH(pRequest.Url) > 0) { 
            Set tUrl = pRequest.Url
        } Else {
            Set tUrl = ..Adapter.URL
        }
      
        //Create a trace with the complete url
        $$$TRACE(..Adapter.HTTPServer_"/"_tUrl)
            
        //Do the post call
        Set tSc = tHttpRequest.Post(tUrl)
    
        //Create the tHttpResponse object
        #dim tHttpResponse As %Net.HttpResponse
          
        //Set the response object	
        Set tHttpResponse = tHttpRequest.HttpResponse
                  
        //If an technical error has occured
        If ($$$ISERR(tSc)) 
        { 
            $$$TRACE("Error:"_$System.Status.GetErrorText(tSc))
            
        } ElseIf ('$IsObject(tHttpResponse)) 
        {
            $$$TRACE("Invalid HTTP Response Object")
            Set tSc=$$$ERROR($$$EnsErrGeneral, "Invalid HTTP Response Object.")
        } 
        Else 
        {
          ;Build Operation Response
          Set pResponse = ##class(BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp).%New()
          Set pResponse.HTTPStatusCode = tHttpResponse.StatusCode
          Set pResponse.HTTPStatusLine = tHttpResponse.StatusLine
          
          ;Check the HTTPStatusCode
          Set tSuccess = ##class(Util.Functions).IsSuccess(tHttpResponse.StatusCode)
          
          If (tSuccess) {
            Set tCounter = ..MaxRetries + 1
              Do tHttpResponse.Data.Rewind()
              Set pResponse.HTTPResponseData = tHttpResponse.Data
          } Else {
            $$$TRACE("An error HTTP code has been received. HTTPSCode="_tHttpResponse.StatusCode)  
            Try {
                Do tHttpResponse.Data.Rewind()
                Set pResponse.HTTPResponseData = tHttpResponse.Data
            } Catch exception {}
          }
        }
        
        Set tCounter = tCounter+1
        If (tCounter <= ..MaxRetries) {
            $$$TRACE("Retry: "_tCounter_". Wait for "_..RetryWaitSeconds_" seconds")
            HANG ..RetryWaitSeconds	
        }	  
      }
     return tSc
}
}

Sorry if I am a bit late to the discussion.  Let me address your issues in order

  • This is not necessarily a problem, but I noticed that the method SendPostRequest is executed again from start to finish for each reply, reinitializing all variables as if it were the first execution, except for the RetryCount variable, which indicates the current resend count.
    • This is expected behavior.  Messages are handled as a specific unit of work (atomic).  When a message errors and is going to retry it is basically put back on the queue to process.  When the retry occurs it is treated as a new message with the exception, as you noted, of the retry count. 
  • Setting Retry = 0 prevents message resending, even with active Reply Code Actions (e.g., E=R).
    • Also expected.  With this configuration the reply action code may be set to retry , but this setting is indicating that no (0) retries are to be attempted.
  • The timeouts are not respected. Instead, the message is resent every 30 seconds instead of 18 (I also tried other Response Timeout values like 20 or 35 seconds, but nothing changes). The Retry Interval is not respected either; the resending is attempted a number of times equal to FailureTimeout/30 instead of FailureTimeout/RetryInterval, as written in the documentation.
    • This seems to be the main issue.  The ResponseTimeout is a property of the Adapter. The default value of this setting is 30 which is what you are seeing.  It appears that you have a custom Business Operation here that is using the EnsLib.HTTP.OutboundAdapter and that you assign this adapter in code rather than using the ADAPTER parameter.  When you initialize this you should assign the reference to the Adapter property of the Business Operation.  Then when you want to set the response timeout in code you would update the property of the Adapter. I would highly recommend using the ADAPTER parameter unless you need to dynamically change the adapter at runtime.  If done in code at runtime you need to be sure that everything is initialized properly
  • After the Failure Timeout expires, an error is generated even if one or more responses have been received. Despite the responses arriving, the BO does not detect them and generates the error: "ERROR 5922: Timed out waiting for response". The message continues to be resent until the Failure Timeout expires, and finally the "ERROR <Ens>ErrFailureTimeout: FailureTimeout of 90 seconds exceeded in lombardia.OMR.BO.Sender; status from last attempt was OK".
    • What is being reported here is the last known status of the operation.  In this case that is the error indicated.   There is a difference between a reply and a response.  The Visual trace is indicating that the operation receive a  reply.  This reply was an error.   The 'Response' is what is received from the Web Server.  This never arrived hence the error.  Business Operations will always receive a reply of some kind.  This reply may indicate an error as in this case or success

If you continue to have issues I would encourage you to reach out to the Worldwide Response Center (WRC) for support.  Additionally you could contact your assigned Sales Engineer.

Regards,

Rich Taylot

Hi Rich, sorry for the late response but I've worked on other projects for some times.

I believe the issue with my code lies in the fact that the POST request was sent via a %Net.HttpRequest object:

// Send an HTTP POST request
Set messageStatus = httpRequest.Post(operation)

rather than using directly the adapter: 

set tsc = ..Adapter.PostURL(URL,.tResponse,,InputStream)

so the Timeout property of the %Net.HttpRequest object wasn't affected by the change on Response Timeout.

I solved this issue with a little help of @Menno Voerman's suggestion:

Set tHttpRequest.WriteTimeout = ..Adapter.WriteTimeout
Set tHttpRequest.Timeout = ..Adapter.ResponseTimeout
Set tHttpRequest.OpenTimeout = ..Adapter.ConnectTimeout  

In this way is possible to modify the response timeout dinamically too. 

I'm not sure if switching to the Adapter.PostURL method is a better choice compared to using httpRequest.Post. For now, I'm using the latter to minimize changes in my code.

Whether or not using the Adapter over an instantiation of the %Net.HttpRequest is a matter of your needs really.  The adapter seeks to make things "simpler" in some way.  However if you need greater control over the process and response using the HttpRequest directly is also a reasonable direction.  I have done both depending on my needs.

Glad it is working for you in an manner that is maintainable.  That is what is important.