Recently we had an issue with the use of the Workday Report as a Service (RaaS) API when consuming reports from Logic Apps. The RaaS API is quite handy for Workday integration scenarios where you can declare a report in Workday and it will pull together a bunch of data meeting the criteria you specify then you can download the report in various formats such as json, xml and csv.

Although Workday has an operations type API for downloading different data, the place RaaS is often used is to create a dataset which makes sense to the business scenario rather than downloading lots of different entities and merging them together outside of Workday to meet the integration needs. RaaS is an out of the box function which is quite populate but one of the issues with it is that sometimes a report can be inefficient and when ran on demand can take a while to run.

Ive used Workday a few times with BizTalk where these long running calls arent really that much of a problem but in the cloud you had a few different challenges around long running API calls. The default approach is you want to avoid them where you can but sometimes a vendor product has a constraint your integration platform just needs to work with and in this case our Workday report had gone from executing within a couple of minutes to taking closer to 6-10 mins to run.

With Logic Apps your timeout ranges from 2 mins on multi-tenant to 4 minutes on ISE. With the Workday report taking longer to run we need to design around this and workout how to not break the Logic App timeout. There are a few different ways we can implement this but really what it boiled down to is:

  • Where can we run the long running call to Workday
  • How can we get the data back to Logic Apps when done to continue downstream processing

In my previous post I talked about the sync to async approach we implemented with Azure API Management and in this post we can use that pattern to handle the workday issue by changing the Logic App to call via API Management rather than using the HTTP action directly.

Below is a sequence diagram showing how we did this.

Workday Proxy API

I decided that in API Management I would implement an API with 2 operations as shown below

The API would have the Workday RaaS service as the backend url but for the Async operation I will use a one way call to the Sync operation and pass in a call back url. This means that the client who calls the sync operation will pass in a call back url and the data will be returned to the caller.

Sync Operation

In the sync operation I would have an operation with the /reports path and it will accept the headers shown below.

  • TrackingId is just to flow a correlation id from the client for tracing
  • Accept header allows the client to inform the API which content type it wants the data returned in.

The request message for this operation looks like the below:

{
    "CallBackUrl":"[The call back url to the Logic App]",
    "ReportPath":"[The path to the workday report]",
    "ReportQueryString":"[The query string for the workday report]",
    "ReportUsername":"[The username for running the report]",
    "ReportPassword":"[The password for running the report]"
}

The sync operation will then use the below policy:

<policies>
    <inbound>
        <set-variable name="requestMessage" value="@{ 
            JObject inMsg = context.Request.Body.As<JObject>();
            return inMsg;
        }" />
        <set-variable name="callBackUrl" value="@{ 
            JObject inMsg = (JObject)context.Variables["requestMessage"];            
            var val = inMsg["CallBackUrl"].ToString();             
            return val;
        }" />
        <set-variable name="reportPath" value="@{ 
            JObject inMsg = (JObject)context.Variables["requestMessage"];            
            var val = inMsg["ReportPath"].ToString();             
            return val;
        }" />
        <set-variable name="reportQueryString" value="@{             
            JObject inMsg = (JObject)context.Variables["requestMessage"];            
            var val = inMsg["ReportQueryString"].ToString();             
            return val;
        }" />
        <set-variable name="reportUsername" value="@{             
            JObject inMsg = (JObject)context.Variables["requestMessage"];            
            var val = inMsg["ReportUsername"].ToString();             
            return val;
        }" />
        <set-variable name="reportPassword" value="@{             
            JObject inMsg = (JObject)context.Variables["requestMessage"];            
            var val = inMsg["ReportPassword"].ToString();             
            return val;
        }" />
        <set-variable name="trackingId" value="@(context.Request.Headers["TrackingId"].FirstOrDefault())" />
        <set-variable name="acceptHeader" value="@(context.Request.Headers["Accept"].FirstOrDefault())" />
        <rewrite-uri template="@{
            return context.Variables.GetValueOrDefault<string>("reportPath") + "?" + context.Variables.GetValueOrDefault<string>("reportQueryString");
        }" />
        <!-- Note we need to change these to read from secrets -->
        <authentication-basic username="@(context.Variables.GetValueOrDefault<string>("reportUsername"))" password="@(context.Variables.GetValueOrDefault<string>("reportPassword"))" />
        <set-method>GET</set-method>
        <base />
    </inbound>
    <backend>
        <forward-request />
    </backend>
    <outbound>
        <choose>
            <when condition="@(((IResponse)context.Response).StatusCode == 200)">
                <!-- Call back to Logic App webhook with call backurl -->
                <send-request ignore-error="false" timeout="600" response-variable-name="logicAppCallBackResponse" mode="new">
                    <set-url>@(context.Variables.GetValueOrDefault<string>("callBackUrl"))</set-url>
                    <set-method>POST</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>@((string)context.Variables["acceptHeader"])</value>
                    </set-header>
                    <set-body template="none">@(context.Response.Body.As<string>())</set-body>
                </send-request>
                <!-- Return Response to API -->
                <return-response>
                    <set-status code="200" reason="OK" />
                    <set-body template="none">@(context.Response.Body.As<string>())</set-body>
                </return-response>
            </when>
            <otherwise>
                <!-- Send error to Logic App -->
                <send-request ignore-error="false" timeout="600" response-variable-name="logicAppCallBackResponse" mode="new">
                    <set-url>@(context.Variables.GetValueOrDefault<string>("callBackUrl"))</set-url>
                    <set-method>POST</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>@((string)context.Variables["acceptHeader"])</value>
                    </set-header>
                    <set-body template="none">@(context.Response.Body.As<string>())</set-body>
                </send-request>
                <!-- Return Response to API -->
                <return-response>
                    <set-status code="@(context.Response.StatusCode)" reason="Error" />
                    <set-body template="none">@(context.Response.Body.As<string>())</set-body>
                </return-response>
            </otherwise>
        </choose>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

What this policy does is:

  • Inbound
    • Extract some variables from the request message
    • Create the uri redirect path so that we get the right path with the report path and query parameters which were in the request message
    • Sets the basic authentication header
    • Changes the HTTP method to a POST for workday
  • Backend
    • Forwards the request to workday. Note we are using the REST endpoint for RaaS. I think there is also a SOAP one
  • Outbound
    • If the response is a 200 then we send the data back to the logic app with a 200 code via a send-request to the call back url and return a good response from this API call
    • If the response from workday is not a 200 then we send an error back to the Logic App via the call back url that was passed in.

Async Operation

The Async version of this API is the same as the sync one in terms of the input. We accept the same headers and request message.

Where it differs is the behaviour of the API because of the different policy which is shown below.

<policies>
    <inbound>
        <set-variable name="request" value="@(context.Request.Body.As<string>())" />
        <set-variable name="trackingId" value="@(context.Request.Headers["TrackingId"].FirstOrDefault())" />
        <set-variable name="acceptHeader" value="@(context.Request.Headers["Accept"].FirstOrDefault())" />
        <set-variable name="Ocp-Apim-Subscription-Key" value="@(context.Request.Headers["Ocp-Apim-Subscription-Key"].FirstOrDefault())" />
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
        <send-one-way-request mode="new">
            <set-url>https://[Your APIM Host].azure-api.net/apps/workday/raas/reports</set-url>
            <set-method>POST</set-method>
            <set-header name="TrackingId" exists-action="override">
                <value>@((string)context.Variables["trackingId"])</value>
            </set-header>
            <set-header name="Ocp-Apim-Subscription-Key" exists-action="override">
                <value>@((string)context.Variables["Ocp-Apim-Subscription-Key"])</value>
            </set-header>
            <set-header name="Accept" exists-action="override">
                <value>@((string)context.Variables["acceptHeader"])</value>
            </set-header>
            <set-body>@((string)context.Variables["request"])</set-body>
        </send-one-way-request>
        <return-response>
            <set-status code="202" reason="Accepted" />
            <set-body>Thanks</set-body>
        </return-response>
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

In this policy we do the following:

  • Inbound
    • Extract the variables from the headers like in the sync operation
    • We also extract the API Subscription key so we can use it when calling the sync API from here
  • Outbound
    • We call the sync API operation using the send-one-way-request policy which allows us to do a fire and forget call. We pass over the message that came into this API and also pass over the headers too.
    • We respond with a 202 so the Logic App knows we have accepted the message

Logic App

In the Logic App we can change the action from the HTTP to the HTTP Webhook action so that we can fire a call to the async operation and then the Logic App will dehydrate until a call back comes back into the Logic Apps platform on the call back url which will rehydrate the Logic App instance and give it the report data from Workday and then the Logic App will continue processing. You can see below we use the webhook action to call the async operation on our Workday RaaS API Proxy in APIM.

When the API runs it will execute the report we specify in the input json and will return a json response as per our Accept header.

Points to Note

  • In the APIM limites (https://github.com/MicrosoftDocs/azure-docs/blob/master/includes/api-management-service-limits.md) it indicates that there is a 30 sec timeout for the consumption tier but no limit for the request duration for the other price tiers
  • In APIM the Forward Request indicates that when forwarding to a backend service a timeout over 240 secs may not be honoured. In practice we found that it seems to work ok but APIM inherits the same networking limitations as other systems so if the connection is not idle and is running you seem to be fine and we didnt have any problems in our scenario
  • You want to be careful about not abusing this pattern. I would not recommend using this all of the time but for those occasional long running calls that you cant do a lot about then this will work but id suggest also using the throttling policy for incoming calls and concurrency limits on backend calls in your API policy to avoid consumers running too many of these long running calls.

Its also worth noting that there are other ways you can implement these long running processes with a call back. Jeff Holland wrote a really cool post a few years ago about using a similar approach but with functions instead which have a longer timeout than Logic Apps. In our case we felt that APIM just didnt need those additional Azure Resources and in some cases our Report may go beyond the timeout of the function so rather than moving the problem for a while APIM was more likely to solve this specific issue.

https://medium.com/@jeffhollan/calling-long-running-functions-from-logic-apps-6d7ba5044701

 

Buy Me A Coffee