Batch multithreading

Do you have batches that run for hours? After reading this post (and doing some coding), they might just run in minutes.

How? By creating multiple tasks in your batch job at runtime. Read on :-).

Introduction

Normally, when you schedule your RunBaseBatch class as a batch job, it will create one task.
You can check this under Basic – Periodic – Batch Job when you have scheduled a batch job.
In a diagram, it looks like this:

This batch task will execute everything on one batch session on one batch server.
But in AX 2009, your batch group can be added to multiple AOS server, and an AOS server can process multiple tasks simultaneously. So how can we use this to speed up our batch job?
We do it by splitting up our task in multiple smaller ones, which are then distributed over multiple batch sessions (threads) and over multiple AOS servers.
Like this:

Example

In this example, I will loop all my customers, and simulate a process that runs for 3 seconds by using the sleep() function.
Note: download the .xpo file below.

Single thread


First, let’s create a batch job like we normally would.
For this, I create 2 classes:
  1. KlForUpdateCustomers: A class that contains my business logic I want to execute
  2. KlForUpdateCustomersSingleTheadBatch: A class that extends RunBaseBatch and makes calls to KlForUpdateCustomers
KlForUpdateCustomers
This class contains this in its run method (see xpo file for other methods):
public void run()
{
    ;
    // simulate a process that takes 3 seconds
    sleep(3000);
    info(strfmt('updated customer %1', this.parmCustTable().AccountNum));
}
KlForUpdateCustomersSingleTheadBatch
This class is our batch class so it extends RunBaseBatch. It has a queryRun, and has this run method:
public void run()
{
    CustTable custTable;
    ;

    while(queryRun.next())
    {
        custTable = queryRun.get(tablenum(CustTable));

        KlForUpdateCustomers::newcustTable(custTable).run();
    }
}
It loops all customers, and makes a call to the class we created earlier for each customer. To keep it simple, I did not add exception handling to this example.
When we run this in batch, a single batch task is created in the batch job that runs for about 3 minutes (according to the batch job screen).

Multithread


To rewrite this batch job to use multiple tasks, we will need 3 classes:
  1. KlForUpdateCustomers: the business logic class we created earlier and we can reuse
  2. KlForUpdateCustomersMultiThreadBatch: The class that represents the batch job and will create batch tasks
  3. KlForUpdateCustomersMultiThreadTask: The class that represents one batch task and makes calls to KlForUpdateCustomers
KlForUpdateCustomers
This class is the same as before and processes one customer (CustTable record).
KlForUpdateCustomersMultiThreadTask
This class extends RunBaseBatch and represents one of the many batch tasks we will be creating. In this example it processes one CustTable record, so a task will be created for each CustTable record.
The run method looks like this:
public void run()
{
    ;
    KlForUpdateCustomers::newcustTable(this.parmCustTable()).run();
}
KlForUpdateCustomersMultiThreadBatch
This is our batch job class, and this is where the real magic happens. This class will create multiple instances of KlForUpdateCustomersMultiThreadTask and add them as a task to our job at run time.
This is how the run method looks:
public void run()
{
    BatchHeader                         batchHeader;
    KlForUpdateCustomersMultiThreadTask klForUpdateCustomersMultiThreadTask;
    CustTable                           custTable;
    ;

    while(queryRun.next())
    {
        custTable = queryRun.get(tablenum(CustTable));

        if(this.isInBatch())
        {
            // when in batch
            // create multiple tasks
            if(!batchHeader)
            {
                batchHeader = BatchHeader::construct(this.parmCurrentBatch().BatchJobId);
            }

            // create a new instance of the batch task class
            klForUpdateCustomersMultiThreadTask = KlForUpdateCustomersMultiThreadTask::newcustTable(custTable.data());

            // add tasks to the batch header
            batchHeader.addRuntimeTask(klForUpdateCustomersMultiThreadTask, this.parmCurrentBatch().RecId);
        }
        else
        {
            // when not in batch
            KlForUpdateCustomers::newcustTable(custTable).run();
        }
    }

    if(batchHeader)
    {
        // save the batchheader with added tasks
        batchHeader.save();
    }
}
As you can see, for each custTable record, we create a new task. When the batch job doesn’t run in batch, we process it as we otherwise would using one session.
In the batch tasks screen (Basic – Inquiries – Batch Job – (select you batch job) – View Tasks), you can clearly see what happens.
First, the status is waiting, and one task has been created.

Then, when the batch job is executing, we can see that multiple tasks have been added to our batch job.

We can also see that it processes 8 tasks at the same time! This is bacause the maximum number of batch threads is set to 8 on the batch server schedule tab of the Server configuration screen (Administration – Setup – Server configuration).
Finally, we can see that the job has ended, and that all runtime tasks have been moved to the batch job history:

You can view the history and the log of this batch job:
Notice how the executing time has gone from 3 minutes to 1 minute according to the batch job screen.
Alternatively, you could use a queryrun in you batch tasks class too. You could for example create a batch group per customer group if that makes more sense to you. The ‘per record’ approach is just an example, just do what makes the most sense in your situation. Sometimes it is better if the runtime tasks have a bit more to do, to counter the overload of creating the tasks.

Conclusion


This method has helped me a lot while dealing with performance issues. Be aware though, you could have problems with concurrency and heavy load (that I haven’t discussed here). But if you do it right, it will result in a huge performance boost.
Spread the word :-).
Update 2011/02/11:
There were two errors in the example:
This line:
batchHeader = BatchHeader::construct(this.parmCurrentBatch().RecId);
Has been replaced with:
batchHeader = BatchHeader::construct(this.parmCurrentBatch().BatchJobId);
And this line:
klForUpdateCustomersMultiThreadTask = KlForUpdateCustomersMultiThreadTask::newcustTable(custTable);
Has been replaced with:
klForUpdateCustomersMultiThreadTask = KlForUpdateCustomersMultiThreadTask::newcustTable(custTable.data());
Todo on my part: update the XPO.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.