Contract API Optimization

Today, I want to re-tell about Contract API Optimization. Materials get from conference of Acumatica and introduction by Joshua van Hoesen.

Three point of ineterst

  • Utilisation of ReturnBehavior

  • Avoid graph logic by creating Generic Inquiry for data retrieval

  • Multi-Threading

Introduction

With the continued advancement of integrated software solutions, there has also been a need to address legacy business processes and how they may be implemented in a modern framework. These business processes tend to rely heavily on collecting large amounts of raw data from disparate sources and aggregating it together for further processing.

Some companies whose business process required the aggregation of millions of data records into invoices to then be sent off for clients in a week span but relatively low system usage at all other times. This presented the challenge of aggregation processes exceeding typical processing requirements and disrupting the user experience.

In one of mine articles I've described how Acumatica can be used for multithreaded usage. 

One more way of improving Acumatica can be usage of nginx as load balancer and scaling of Acumatica horizontally


Solution

To overcome this challenge, we developed an intermediary service that would take an initial request from an Acumatica user page and split its data aggregation over multiple threads each one making requests to the instance via the Contract API in a separate user session.

Multi-Threading, Why before the do


• Large external data store to import.

• Large number of records fitting separate criteria to retrieve.

• Large numbers of consecutive records to be processed.

• Large number of records need to be summarized and created.

• Greater utilization of server resources

The implementation of multi-threading allowed for full utilization of system resources during data aggregation showing a dramatic increase in performance. To demonstrate this we created a series of 500 journal entries within an instance utilizing a series of thread counts.

 watch.Start();
threads.ForEach(x => x.Start());
threads.ForEach(x => x.Join());
watch.Stop();
Console.Writeline("Records have beeen created in" + watch.Elapsed.TotalSecond.ToString() + "seconds");
watch.Reset();
threads.Clear();

Console.Writeline("Creating 500 Journal Entries with 5 threads");

for (int i = 0; i < 5; i++)     {            var t = new Thread(() =>              {                 using (AcumaticaProcessor processor = new AcumaticaProcessor())                  {                       processor.Login();                       for (int k = 0; k < 100; k++) processor.CreateJournalEntry();                  }               });               threads
.Add(t);     }

watch.Start();
threads.ForEach(x => x.Start());
threads.ForEach(x => x.Join());
watch.Stop();
Console.Writeline("Records have beeen created in" + watch.Elapsed.TotalSecond.ToString() + "seconds");
watch.Reset();
threads.Clear();

The results of our small scale test cases can be seen below.

Records to Create

Number Of Threads

Time to Completion

500

1

113 Seconds

500

5

37 Seconds

500

10

35 Seconds

500

50

47 Seconds

There are a number of items to note from the results, as can be seen splitting the task at hand into multiple concurrent processes. As expected, this increased performance but it’s an inherent nonlinear gain in creating or reusing threads. The number of threads should be limited in relation to the processors available to the instance reserving additional threads for session management and system processing.    

(X cores * 2 threads per core) – 2 ‘Reserved threads’ = Total Processing Threads Available

 

Configuration Notes

When developing a system that interacts with Acumatica in a multi-threaded manner, via the Contract API, there are certain configurations that must be taken into account.

  1. System.Net.ServicePointManager.DefaultconnectionLimit

  2. ClientBase<Acumatica.DefaultSoap>.CacheSetting

  3. Proper Implementation of IDisposable

By default the maximum number of connections that can be made by a ServicePoint object is 2  and additional requests will languish on the queue until previous requests have completed. Therefore, this value should be set to the maximum number of threads determined for processing to allow maximum throughput of Contract API requests. 

System.Net.ServicePointManager.DefaultConnectionLimit = X

The ConnectionLimit property sets the maximum number of connections that the ServicePoint object can make to an Internet resource.

[https://docs.microsoft.com/en-us/dotnet/api/system.net.servicepoint.connectionlimit?redirectedfrom=MSDN&view=netframework-4.7.2#System_Net_ServicePoint_ConnectionLimit]

If your service is written as a WCF Application be mindful that ChannelFactory<TChannel> incurs overhead during creation of communication channels and has the following behavior by default.

To help minimize this overhead, WCF can cache channel factories when you are using a WCF client proxy. 

[https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/channel-factory-and-caching]

In practical terms, this means that by default, there are requests from separate threads.  However, the threads within the same application pool will share a established session to the Acumatica instance, this behavior can be modified to allow for a true creation of independent sessions with the following code.

ClientBase<Acumatica.DefaultSoap>.CacheSetting = CacheSetting.AlwaysOff;

When utilizing the Acumatica Contract API, the default SOAP client that is generated should be implemented within a class that manages your API specific methods. This class should implement the IDisposable method making sure to log out of the current user session before object disposal. If this is not implemented correctly, additional requests performed in other threads may exceed the concurrent user sessions available as configured within the Acumatica instance causing integration failure on future requests.

public class AcumaticaProcessor : IDisposable
{
       protected DefaultSoapClient client = new DefaultSoapClient();
 
       /// <summary> Login to the Acumatica instance asynchronously
       public async Task<bool> LoginAsync()...
       /// <summary> Logout to the Acumatica instance asynchronously
       public async Task<bool> LogoutAsync()...
       
       public void Dispose()
       {
           Dispose(true);
           GC.SuppressFinalize(this);
       }
 
       protected virtual void Dispose(bool disposing)
       {
           if (disposing)
           {
               try
               {
                   client.Logout();
               }
               catch (Exception e)
               {
 
               }
               finally
               {
                   client.Close();
               }
           }
       }
}

Efficiency Increase – ‘ReturnBehavior’

• ReturnBehavior.All - Returns all data points and child records on a request.

• ReturnBehavior.OnlySpecified — Returns only those data points specified, by default no child records.

• ReturnBehavior.OnlySystem — Returns only system data points such as ID and no child records.

• ReturnBehavior.None — Returns no data points and no child records.

When making multiple requests to Acumatica via the Contract API, limiting the data returned to only that which is absolutely necessary can greatly increase overall system performance. The records returned can be configured with the ‘ReturnBehavior’ Property on each requests.

[From the Acumatica ERP User Guide, 6.10.0956, ReturnBehavior Property pageid=7b6ecfb4-faf6-4125-a4ae-a23a57662f3f]

To demonstrate the time difference these return behaviors made, we created 267 SOOrder records within an instance and then made calls to the system with each of the specified behaviors, limiting fields returned to 7 for the appropriate calls.

/// <summary> Retrieve SOOrders with all fields and no details
public void RetreviewSOorders1()
{
SalesOrder order = new SalesOrder()
{
ReturnBehavior = ReturnBehavior.All,
Details = new SalesOrderDetail[] { new SalesOrderDetail() { ReturnBehavior = ReturnBehavior.None }}
};

var result = client.GetList(order);
}

/// <summary> Retrieve SOOrders with all specified fields and related details
public void RetreviewSOorders2()
{
SalesOrder order = new SalesOrder()
{                OrderNbr = new StringReturn(),                OrderType = new StringReturn(), 
Status = new StringReturn(), 
Date = new DateTimeReturn(), 
CustomerID = new StringReturn(), 
OrderTotal = new StringReturn(), 
Currency = new StringReturn(), 
Details = new SalesOrderDetail[] { new SalesOrderDetail() { ReturnBehavior = ReturnBehavior.All }}
ReturnBehavior =  ReturnBehavior.OnlySpecified
};

var result = client.GetList(order); }

The results of our small scale test cases can be seen below

Number of Records

ReturnBehavior

Details Included

Seconds

267

All

Yes

219.5

267

All

No

172.2

267

OnlySpecified [7]

Yes

3.2

267

OnlySpecified [7]

No

.56

267

OnlySystem

No

.43

267

None

No

.38

The largest decreases in time can be seen by limiting the return of unnecessary child records. Though more modest gains were seen by limiting fields retrieved such decreases make a large difference when scale is taken into account as seen in the following table.

Number of Records

ReturnBehavior

Details Included

Minutes

5000

All

Yes

60

5000

OnlySpecified [7]

Yes

1

1,000,000

All

Yes

12k (200 Hrs)

1,000,000

OnlySpecified [7]

Yes

200 (3.3 Hrs)

Conclusion

With the need in the industry to process increasingly large amounts of data in a short amount of time, Acumatica has shown the capabilities available through its Contract API and supporting technologies to handle a wide range of client use cases.

No Comments

Add a Comment