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 :-).
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.
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):
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));
}
{
;
// 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:
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();
}
}
{
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).
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.
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();
}
{
;
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 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();
}
}
{
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).
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:
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:
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());
No comments:
Post a Comment