Batch multithreading

October 3, 2010 at 13:57
filed under Dynamics AX
Tagged , ,

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 :-).

Download the XPO file by clicking here.

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.

20 comments

RSS / trackback

  1. casperkamal

    good one… that helps :)

  2. Jeroen Doens

    I didn’t know this was possible. I always create a batch that launches a new batch with several tasks (http://www.doens.be/2010/04/batch-job-performance-boost), but I like your solution more.

  3. Klaas Deforche

    I’m glad you agree Jeroen.
    This really shows how powerful the batch framework in AX 2009 is.

  4. Dilip

    Nice article Klaas and thanks for the detailed steps. We have around 130 batches in our implementation and batch tasks with multi-threading running on an AOS Cluster will help boost up the performance.

  5. Shailesh Kumar

    Nice article and elaborated very well…

  6. kittu

    System.NullReferenceException: Object reference not set to an instance of an object.

    at Dynamics.Ax.Application.SalesLineforUpdateMultithreadBatch.Run() in SalesLineforUpdateMultithreadBatch.run.xpp:line 40

    at Dynamics.Ax.Application.BatchRun.runJobStaticCode(Int64 batchId) in BatchRun.runJobStaticCode.xpp:line 54

    hi ,

    i am creating a multithreadbatch class , got one error , could u help me.

    at Dynamics.Ax.Application.BatchRun.runJobStatic(Int64 batchId) in BatchRun.runJobStatic.xpp:line 13

    at BatchRun::runJobStatic(Object[] )

    at Microsoft.Dynamics.Ax.Xpp.ReflectionCallHelper.MakeStaticCall(Type type, String MethodName, Object[] parameters)

    at BatchIL.taskThreadEntry(Object threadArg)

  7. Klaas Deforche

    Hi Kittu,

    What version of AX are you using?
    Could you post the line that throws this error?
    What object is not set to an instance of an object?

  8. kittu

    I am working on Ax2012 R2. when ever i run my Multitheadbatch status will be going on executing then status Will be change on Error
    the Error Message Is:

    System.NullReferenceException: Object reference not set to an instance of an object.

    at Dynamics.Ax.Application.SalesLineforUpdateMultithreadBatch.Run() in SalesLineforUpdateMultithreadBatch.run.xpp:line 40

    at Dynamics.Ax.Application.BatchRun.runJobStaticCode(Int64 batchId) in BatchRun.runJobStaticCode.xpp:line 54

    at Dynamics.Ax.Application.BatchRun.runJobStatic(Int64 batchId) in BatchRun.runJobStatic.xpp:line 13

    at BatchRun::runJobStatic(Object[] )

    at Microsoft.Dynamics.Ax.Xpp.ReflectionCallHelper.MakeStaticCall(Type type, String MethodName, Object[] parameters)

    at BatchIL.taskThreadEntry(Object threadArg)

  9. Klaas Deforche

    Hi Kittu,

    If you are using AX2012, I would suggest you use runtime tasks in the way described here:
    http://www.artofcreation.be/2012/02/27/adding-runtime-tasks-to-a-business-operation-framework-service/

    I will analyze the stacktrace and get back to you :) (via email).

  10. kittu

    thanks for Replay,

    I am New in ax .could u give how to implement MultitheadingBatchJobs In Ax2012 R2 with an examples like previoes post.

    Thanks u.

  11. Klaas Deforche

    Hi Kittu,

    In AX 2012, the prefered way of creating batch jobs is using services (Sysoperation aka BOF), not runbasebach.

    If you want to know how to create a service, start here:
    http://www.artofcreation.be/2011/08/21/ax2012-sysoperation-introduction/

    When you are familiar with creating services, you can add runtimetasks (aka multi threading) as described here:
    http://www.artofcreation.be/2012/02/27/adding-runtime-tasks-to-a-business-operation-framework-service/

    Also, I have co-authored a book about services an sysoperation where this is explained in detail: Microsft Dynamics AX 2012 Services. There is a chapter that deals with batches an multithreading with examples you can download.
    Check the link to amazon on top :).

  12. Kishore

    Hi,

    Thanks a lot for demo. From the above description i’ve created a batch Job with multi threading capability.

    I’m facing the issue while logging the error in the error log.

    I’m not able to see any errors in the error log.

    Could anyone help me identify the issue.

    Thanks,
    Kishore.

  13. Herman Kanter

    I noticed that in your multi-tasking infolog, that there are no CustIds listed “updated customer”. Does this mean that the CustTable is not getting passed?

    Thank you.

  14. Klaas Deforche

    Hi Herman,

    You are correct, they didn’t get passed when I ran the code that time, but I only noticed it afterwards. I updated the code when I noticed it didn’t work but not the screenshot.

    I changed this line:

    klForUpdateCustomersMultiThreadTask = KlForUpdateCustomersMultiThreadTask::newcustTable(custTable);

    To this:

    klForUpdateCustomersMultiThreadTask = KlForUpdateCustomersMultiThreadTask::newcustTable(custTable.data());

    I’m not exactly sure why, but it (sometimes?) doesn’t work if you just pass the record buffer. It does work if you pass a copy of it using the data() method.

    Passing buffers is a personal preference of mine, because I don’t have to reselect the record in the thread afterwards (as apposed to passing a recid of a natural key). Although I’m always in doubt if it’s the best way. It might be better to pass a recid when constructing the class. That way there is no need to pack the full record in the batch table. I’m unsure what is the best/quickest, it’s something to think about :).

    I hope this helped.

  15. Manikanta

    Hi Klaas Deforche,

    Thanks a lot for the information. It’s great. I’ve one small doubt.
    For suppose, If I want to create batch header for every group of records and then add task for each record in that group.

    At present, I’ve created a query, added data source and grouped by few fields. Now I want to create batch header for each of that group and tasks for each of the record in that group. Can you please help me how to solve this.

    Thanks in advance.

  16. Prasad

    Thank you very much for this document. But a small doubt. What is the maximum number of tasks can we create? Is there any limit? I was able to create about 25,000 tasks without any problems but it took a while to execute.

    Asking out of curiosity. Is there any way to halt the
    KlForUpdateCustomersMultiThreadBatch until all the tasks are completed. (I have added dependencies for the task execution priority.) But I would like to add another functionality at the end in KlForUpdateCustomersMultiThreadBatch class to write to some custom table that all tasks are finished execution with its endtime as an example.

    Presently I am using this method and not sure weather my approach is right or wrong.

    while(!(createdTaskCount – (finishedTaskCount + errorTaskCount)) <= 1)
    {
    finishedTaskCount = this.endedThreadCount(thisBatchJobID);
    errorTaskCount = this.errorThreadCount(thisBatchJobID);
    break;
    }

    I am getting the total count from batch and batchjob table for particular job id to halt the execution untill all the tasks are finished. I know it is really resource intensive, but I do not have any other option. Please advise.

    Any thoughts.

  17. Prasad

    For better understanding of my reqs
    KlForUpdateCustomersMultiThreadBatch.run()
    {
    CustomLogTable logTable;
    —-;
    —-;
    if(batchHeader)
    {
    // save the batchheader with added tasks
    batchHeader.save();
    }
    while(!(createdTaskCount – (finishedTaskCount + errorTaskCount)) <= 1)
    {
    //Get count of tasks finished from batch table
    finishedTaskCount = this.endedThreadCount(thisBatchJobID);
    //Get count of tasks error from batch table
    errorTaskCount = this.errorThreadCount(thisBatchJobID);
    break;
    }
    logTable.notes = strfmt("All Tasks have finished execution @ %1",dateTimeStamp);
    logTable.insert();
    }
    Hope code will help you understand what I am looking for!!!

  18. Mark Kopan

    this does not work.
    batchHeader.addRuntimeTask(klForUpdateCustomersMultiThreadTask, this.parmCurrentBatch().RecId); throws an nondescript error .
    Task is within SalesJournalChangeType.Run(); this should run a KitExplosion routine that is normally fired on the SalesTable Form. SalesType field.

    initiated a batch run, but when it gets to the addRuntimeTask(), there appears to be something missing in the base()

    I did mess with the parameters and got it to run a couple of times, now it will not run. any ideas why the Task i s not initializing or receiving the BatchInfo from the batchHeader?

    thanks

  19. Trackbacks

respond