Introduction To The SysOperation Framework
Introduction To The SysOperation Framework
Introduction To The SysOperation Framework
Table of Contents
Introduction ................................................................................................ 3 Loading the sample code............................................................................. 4 Sample 1: Comparison of the SysOperation and RunBase frameworks ....... 5
RunBase sample: SysOpSampleBasicRunbaseBatch................................................................... 6 SysOperation sample: SysOpSampleBasicController .................................................................13
Sample 2: Demonstration of commonly implemented features in SysOperation and RunBase ....................................................................... 19 Sample 3: Introduction to SysOperation execution modes ........................ 22
Execution modes overview ....................................................................................................22 Sample overview .............................................................................................................. 24 Architecture and code........................................................................................................ 26
Sample 4: How to build asynchronous operations with the SysOperation framework ................................................................................................ 29
Sample overview .............................................................................................................. 30 Scaling out to multiple processors by using batch tasks ......................................................... 30 Cleaning up the results table periodically ............................................................................. 32 Detecting errors in asynchronous operations ........................................................................ 35 Using the alerts framework for notifications ......................................................................... 38 Architecture and code........................................................................................................ 43
Introduction
SysOperation is a framework in Microsoft Dynamics AX 2012 that allows application logic to be written in a way that supports running operations interactively or via the Microsoft Dynamics AX batch server. The framework provides capabilities that are very similar to the RunBase framework that came before it. The batch framework has very specific requirements for defining operations: The operation must support parameter serialization so that its parameters can be saved to the batch table. The operation must have a way to display a user interface that can be launched from the batch job configuration user interface. The operation must implement the interfaces needed for integration with the batch server runtime.
The RunBase framework defines coding patterns that implement these requirements. The SysOperation framework provides base implementations for many of the patterns defined by the RunBase framework. The purpose of this white paper is to outline how the SysOperation framework can be used to build operations that can run asynchronously and make use of the full processing power available on the server. Five samples are presented: the first two demonstrate the basic concepts, the second two demonstrate how to build asynchronous scalable operations and the fifth sample shows how to call asynchronous operations from .NET clients. The following figure shows the X++ project containing the sample code for the first four samples. The fifth sample is a Microsoft Visual Studio solution.
Sample 1 Comparison of the SysOperation and RunBase frameworks Sample 2 Demonstration of commonly implemented features in SysOperation and RunBase Sample 3 Introduction to SysOperation execution modes Sample 4 How to build asynchronous operations with the SysOperation framework Sample 5 How to call asynchronous operations from .NET clients
3. Compile the project, and make sure that there are no errors. (There are two warnings which will not the sample execution).
4. Generate CIL incrementally from the main toolbar in the developer workspace window. For more information about this topic, see the following article on MSDN: http://msdn.microsoft.com/en-us/library/gg839855.aspx.
5. Deploy the service group SysOpFindPrimesServiceGroup, which is located under the CommonItems\ServiceGroups group in the project. Deploying a service group generates a .NET assembly from X++ code and starts a service host that provides a service endpoint for all the service interfaces listed in the service group. For more information about service groups, see the following article on MSDN: http://msdn.microsoft.com/en-us/library/gg731906.aspx. Note that the services deployed in this step are required only for the .NET client sample and are not used in any of the other samples.
6. Open the .NET console application sample, and compile it to make sure that it has no errors.
The console application is meant to work on the same machine as the Microsoft Dynamics AX server. If it is not on the same machine, open the app.config file in the solution, and update the <client> section to point to the Microsoft Dynamics AX server machine. After these steps are completed, all the actions outlined in this paper can be duplicated by using the sample code.
SysOperationController +prompt()
RunBaseBatch +dialogMake()
Currently in Microsoft Dynamics AX, all operations that must run via the batch server must derive from either the SysOperationController or the RunBaseBatch base class. The following two samples illustrate the basic capabilities provided by the two frameworks: SysOpSampleBasicRunbaseBatch SysOpSampleBasicController
These can be found in the sample X++ project under the Sample_1_SysOperation_RunBase_Comparision group.
Override
classDeclaration
Description
Derives from RunBaseBatch. Declares variables for operation input parameters. Declares variables for dialog box controls. Declares a macro defining a list of variables that need to be serialized.
Sample code
class SysOpSampleBasicRunbaseBatch extends RunBaseBatch { str text; int number; DialogRunbase dialog;
dialog
protected Object dialog() Populates the dialog box created by the base class with { controls needed to get user dialog = super(); input. The initial values from the class member variables textField = are used to initialize the dialog.addFieldValue(IdentifierStr(Description255), controls. The type of each control is determined by the text, EDT identifier name. 'Text Property', 'Type some text here'); numberField = dialog.addFieldValue(IdentifierStr(Counter), number, 'Number Property', 'Type some number here'); return dialog; }
getFromDialog
putToDialog
pack
Override
unpack
Description
Deserializes operation input parameters.
Sample code
public boolean unpack(container packedClass) { Integer version = conPeek(packedClass,1); switch (version) { case #CurrentVersion: [version,#CurrentList] = packedClass; break; default: return false; } return true; }
run
Runs the operation. This sample prints the input parameters via the Infolog. It also prints the tier that the operation is running on and the runtime that is used for execution.
public void run() { if (xSession::isCLRSession()) { info('Running in a CLR session.'); } else { info('Running in an interpreter session.'); if (isRunningOnServer()) { info('Running on the AOS.'); } else { info('Running on the Client.'); } } info(strFmt('SysOpSampleBasicRunbaseBatch: %1, %2', this.parmNumber(), this.parmText())); }
description
A static description for the operation. This description is used as the default value for the caption shown in batch and the operation user interface. The main interaction code for the operation. This code prompts the user for input, and then runs the operation or adds it to the batch queue.
main
public static void main(Args args) { SysOpSampleBasicRunbaseBatch operation; operation = new SysOpSampleBasicRunbaseBatch(); if (operation.prompt()) { operation.run(); } }
Override
parmNumber
Description
Optional. It is a Microsoft Dynamics AX best practice to expose operation parameters with the property pattern for better testability and for access to class member variables outside the class. Optional. It is a best practice to expose operation parameters with the property pattern.
Sample code
public int parmNumber(int _number = number) { number = _number; return number; } public str parmText(str _text = text) { text = _text; return text; }
parmText
After it is implemented, the operation can be run by using the Go button on the code editor toolbar.
If an X++ class implements the main operation, it is automatically called by the code editor. The samples main operation will prompt the user for input for the operation when operation.prompt() is called. If the prompt returns true, main calls operation.run() directly. If the prompt returns false, the user either canceled the operation or scheduled it via batch.
To run the operation interactively, enter data on the General tab of the operation user interface.
Make sure that the Batch processing check box is cleared on the Batch tab.
Clicking OK will run the operation and print the following output to the Infolog window.
The Infolog messages show that the operation ran on the server, because the sample class is marked to run on the server. The operation ran via the X++ interpreter, which is the default for X++ code. If you repeat the previous steps but select the Batch processing check box on the Batch tab, the operation will to run via the batch server. When the Batch processing check box is selected, the following Infolog message is shown, indicating that the operation has been added to the batch queue.
The operation may take up to a minute to get scheduled. After waiting for about a minute, open the Batch job form from the Application Object Tree (AOT).
Repeatedly update the form by pressing the F5 key, until the job entry shows that the job has ended. Sorting by the Scheduled start date/time column may help you find the operation if there are many job entries in the grid. After you find the correct job, select it, and then click Log on the toolbar.
Clicking Log opens an Infolog window indicating that the operation ran in a CLR session, which is the batch server execution environment.
In summary, this sample shows the minimum overrides needed to create an operation that can run either interactively or via the batch server by using the RunBaseBatch base class.
SysOpSampleBasicController classDeclaration class SysOpSampleBasicController extends Derives from the framework base class SysOpSampleBaseController. SysOpSampleBaseController Normally the operation should derive { } from the SysOperationServiceController class. The sample base class provides a few fixes for issues in that class. 13 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
Override
new
Description
Identifies the class and method for the operation. In the sample, this points to a method on the controller class; however, in general, it can be any class method. The framework will reflect on this class/method to automatically provide the user interface and parameter serialization.
Sample code
void new() { super(); this.parmClassName( classStr(SysOpSampleBasicController)); this.parmMethodName( methodStr(SysOpSampleBasicController, showTextInInfolog)); this.parmDialogCaption( 'Basic SysOperation Sample'); }
Implemented by the base framework. Handles marshaling execution to a CLR session. Prints the input parameters via the Infolog. Also prints the tier that the operation is running on and the runtime that is used for execution. public void showTextInInfolog(SysOpSampleBasicDataCont ract data) { if (xSession::isCLRSession()) { info('Running in a CLR session.'); } else { info('Running in an interpreter session.'); if (isRunningOnServer()) { info('Running on the AOS.'); } else { info('Running on the Client.'); } }
info(strFmt('SysOpSampleBasicController: %1, %2', data.parmNumber(), data.parmText())); } caption A description for the operation. This description is used as the default value for the caption shown in batch and the operation user interface. public ClassDescription caption() { return 'Basic SysOperation Sample'; }
Override
main
Description
The main interaction code for the operation. This code prompts the user for input, and then runs the operation or adds it to the batch queue.
Sample code
public static void main(Args args) { SysOpSampleBasicController operation; operation = new SysOpSampleBasicController(); operation.startOperation(); }
SysOpSampleBasicDataContract classDeclaration The data contract attribute is used by [DataContractAttribute] the base framework to reflect on the class SysOpSampleBasicDataContract operation. { str text; int number; } parmNumber The data member attribute identifies this property method as part of the data contract. The label, help text, and display order attributes provide hints for user interface creation. [DataMemberAttribute, SysOperationLabelAttribute('Number Property'), SysOperationHelpTextAttribute('Type some number >= 0'), SysOperationDisplayOrderAttribute('2')] public int parmNumber(int _number = number) { number = _number; return number; } parmText The data member attribute identifies this property method as part of the data contract. The label, help text, and display order attributes provide hints for user interface creation. [DataMemberAttribute, SysOperationLabelAttribute('Text Property'), SysOperationHelpTextAttribute('Type some text'), SysOperationDisplayOrderAttribute('1')] public Description255 parmText(str _text = text) { text = _text; return text; }
As in the RunBase sample, the operation can be run by using the Go button on the code editor toolbar.
The main class calls operation.startOperation(), which handles running the operation synchronously or adding it to the batch queue. Although operation.run() can also be called, this should be done only if the data contract has been programmatically filled out. The startOperation method invokes the user interface for the operation, and then calls run. To run the operation interactively, enter data on the General tab of the operation user interface. The user interface created by the framework is very similar to the one created in the RunBase sample.
Make sure that the Batch processing check box is cleared on the Batch tab.
Clicking OK will run the operation and print the following output to the Infolog window.
The Infolog messages show that, unlike in the RunBase sample, the operation ran in a CLR session on the server.
If you repeat the previous steps but select the Batch processing check box on the Batch tab, the operation will run via batch, just as in the RunBase sample.
The operation may take up to a minute to get scheduled. After waiting for about a minute, open the Batch job form from the AOT, as in the RunBase sample.
Repeatedly update the form by pressing the F5 key, until the job entry shows that the job has ended. Sorting by the Scheduled start date/time column may help you find the operation if there are many jobs entries in the grid. After you find the correct job, select it, and then click Log on the toolbar.
Clicking Log opens an Infolog window indicating that the operation ran in a CLR session, which is the batch server execution environment.
This sample showed that the SysOperation framework can provide the same basic functionality as the RunBase framework. In addition, it provides implementation for common RunBase overrides, such as parameter serialization and user interface creation. It also provides the capability to route synchronous operations to a CLR session.
The SysOperation sample, SysOpSampleSimpleController, is factored differently from the first sample, SysOpSampleBasicController. In this sample, there are four classes: the controller, the service operation, the data contract, and the user interface builder. The following figure outlines the classes in relation to the base framework classes.
SysOperationUIBuilder * SysOpSampleSimpleDataContract -text -number * 1 * SysOperationController 1 SysOpSampleSimpleService +showTextInInfolog() +doBatch() +pack() +unpack() +prompt() +startOperation() 1 Dialog (Form) 1 1 *
SysOperationDataContractInfo
1 * SysOperationGroupInfo
SysOperationDataMemberInfo
Operation
Base Framework
Metadata
The service and data contract classes define the operation. The derived controller class provides the main entry point and overrides the new() method to associate the operation classes with the controller. The base controller reflects on the operation and constructs metadata classes that define the operation. The base class SysOperationAutomaticUIBuilder uses the metadata derived from the operation to create the user interface. In the sample, there is a derived user interface builder called SysOpSampleSimpleUserInterfaceBuilder. This overrides the postBuild() and postRun() overrides on the base builder to subscribe to form control events related to validation and lookup. The system uses SysOperationContractProcessingAttribute to associate the custom user interface builder with the data contract.
[DataContractAttribute, SysOperationContractProcessingAttribute(classStr(SysOpSampleSimpleUserInterfaceBuilder))] class SysOpSampleSimpleDataContract { str text; int number; }
If this attribute is not present, the default builder, SysOperationAutomaticUIBuilder, is used. As an experiment, comment out the attribute in the preceding code, and then run the operation to see the differences. The postBuild() override in the custom user interface builder is where the form control metadata needs to be modified before the controls are instantiated. The framework maintains an association between controls and data contracts in a map that can be accessed via the this.bindInfo() method. The map is keyed by the name of the property in the data contract.
public void postBuild() { super(); // get references to dialog controls after creation numberField = this.bindInfo().getDialogField(this.dataContractObject(), 20 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
methodStr(SysOpSampleSimpleDataContract, parmNumber)); textField = this.bindInfo().getDialogField(this.dataContractObject(), methodStr(SysOpSampleSimpleDataContract, parmText)); // change text field metadata to add lookup textField.lookupButton(#lookupAlways); }
The postRun() override in the custom user interface builder is where the form control events are subscribed to. The subscriptions must be added to the controls after they have been instantiated.
public void postRun() { super(); // register overrides for form control events numberField.registerOverrideMethod(methodstr(FormIntControl, validate), methodstr(SysOpSampleSimpleUserInterfaceBuilder, numberFieldValidate), this); textField.registerOverrideMethod(methodstr(FormStringControl, lookup), methodstr(SysOpSampleSimpleUserInterfaceBuilder, textFieldLookup), this); }
The registerOverRideMethod method on the controls is a run-time equivalent to the control overrides used in normal forms. If you use an override method in a standard Microsoft MorphX form, you can use the same method override in a dynamic form by using this mechanism. Note that both the RunBase and SysOperation frameworks allow the use of modeled forms as the operation user interface. The SysOperation framework provides the override SysOperationController.templateForm() for that purpose, however, this topic is outside the scope of this white paper. The samples in this section show how the user interface for the operation can use many of the same features that are available in the normal form programming model. Control overrides fire run-time events that can be subscribed to. The SysOperation version of the sample shows how the different aspects of the operation can be factored into separate classes. To show that everything is possible with code, the RunBase sample is modified so that it marshals its interactive execution into a CLR session, in the same way that the SysOperation framework does. This illustrates the design principle that drove the SysOperation framework: move as much of the boilerplate code as possible into the base classes.
private static server void showTextInInfolog(container packedRunBase) { SysOpSampleSimpleRunbaseBatch thisClass; // If not in a CLR session then marshal over. If already in a CLR session // then execute the logic for the operation if (!xSession::isCLRSession()) { new XppILExecutePermission().assert(); SysDictClass::invokeStaticMethodIL(classStr(SysOpSampleSimpleRunbaseBatch), staticMethodStr(SysOpSampleSimpleRunbaseBatch, showTextInInfolog), packedRunBase); // exit call executed in CLR session. return; } thisClass = new SysOpSampleSimpleRunbaseBatch(); if (!thisClass.unpack(packedRunBase)) { throw AifFault::fault('SysOpSampleSimpleRunbaseBatch unpack error', 'unpackError'); 21 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
} if (xSession::isCLRSession()) { info('Running in a CLR session.'); } else { info('Running in an interpreter session.'); if (isRunningOnServer()) { info('Running on the AOS.'); } else { info('Running on the Client.'); } } info(strFmt('SysOpSampleSimpleRunbaseBatch: %1, %2', thisClass.parmNumber(), thisClass.parmText())); }
The following table lists the four execution modes that are defined by the SysOperation framework enum SysOperationExecutionMode and describes their operations. Execution Mode
Reliable asynchronous
Description
Reliable asynchronous operations use the batch servers session for execution. The call is queued to the empty batch queue by default, but the appropriate queue can be specified by using a property on the operation. It is possible to track the execution of reliable asynchronous calls by tracking the associated batch execution history. It is also possible to make the initiation of the reliable asynchronous call part of a business process transaction. Reliable asynchronous calls can make use of the batch servers parallel execution mechanism. IMPORTANT: For these reasons, reliable asynchronous execution is the recommended mechanism for executing lengthy calls.
Use
Running operations in this mode is equivalent to running them on the batch server, with the additional behavior that the jobs are automatically deleted after they are completed, whether they were successfully completed or not. However, the job history is persisted in the system. This pattern is provided to facilitate building operations that use the batch server runtime, but that do not rely on the batch server administration features. These jobs only temporarily show up in the Batch job form and can be filtered out completely by setting a filter in the BatchJob.RuntimeJob field. To implement this behavior, the batch header exposes a property named parmRuntimeJob, which is set to True by the SysOperation framework to select this behavior. For more information about batch jobs and tasks, see the following article on MSDN: http://technet.microsoft.com/enus/library/dd309586.aspx.
Scheduled batch
Scheduled batch mode uses the traditional batch server execution mechanism. This execution mode differs from the reliable asynchronous mode only in the way batch jobs persist in the system.
You use this mode to run batch jobs in the asynchronous, server-based, batch processing environment. Unlike reliable asynchronous calls, scheduled batch jobs are persisted in the system until a user manually deletes them. You should use the synchronous execution method when the operation is not lengthy or when it is initiated from a batch session. Results may be obtained by using the operations result parameter.
Synchronous
Synchronous calls are always initiated in the callers session. The calls are always marshaled to the server and executed in intermediate language (IL). (There is a mechanism to opt out of having the calls always marshaled to the server. For more information, see http://msdn.microsoft.com/enus/library/sysoperationservicecontroller.ex ecuteoperationwithrunas.aspx). If the class being called is registered as a service in the AxClient service group, then a Windows Communication Foundation (WCF) service proxy is used to marshal the synchronous call to the server. If the class being called is not registered as a service in the AxClient service group then the call is marshaled to the server using the operations pack/unpack mechanism and is executed in IL using the runAs mechanism. If the call is initiated on the server, then it is marshaled to IL, if needed, before the
service method is called. Asynchronous Asynchronous operations are very similar to synchronous operations except that they are executed by using the WCF asynchronous service call mechanism. Asynchronous calls only run asynchronously if they are initiated from the desktop client session and if the called service is registered in the AxClient service group. In all other cases, the calls are executed synchronously. In all cases the caller can get results by using the SysOperationServiceController.operationRe turnValue Method in the afterOperation override method. For more information, see http://msdn.microsoft.com/enus/library/sysoperationservicecontroller.aft eroperation(v=ax.60).aspx and http://msdn.microsoft.com/enus/library/sysoperationservicecontroller.op erationreturnvalue.aspx. NOTE: Adding a service class that is used as part of a SysOperation to the AxClient service group is not usually required. The SysOperation framework always marshals the data contract to the server by value, whether or not the service class is published as a service. The Asynchronous execution mode is the only mode that explicitly requires AxClient server group registration. Asynchronous calls are useful for running lengthy operations from the desktop client, where durability is not important. The caller must guarantee that the call is initiated from the desktop client to get the benefit of asynchronous execution.
Sample overview
1. Open the form, and make sure that the Service operation execution mode field is set to Reliable asynchronous (which is the default value).
2. Click Find Prime Numbers, and then enter the operation input parameters. Enter the range [1,1000000].
3. Click OK to start the operation. The form will poll for changes by using the element.settimeout() mechanism. The operation should be completed in a few seconds. Notice that the client is not frozen while the operation is running.
If you have multiple processors in your machine, note that the full power of all the processors is not used in the computation. There is only one thread.
4. Try the operation again with larger ranges, to see how the application behaves. 5. Change the Service operation execution mode field to Synchronous, and try the operation again. If a large range is entered, the client will freeze while the operation is running.
6. Change the Service operation execution mode field to Scheduled Batch, and try the operation again. In the Batch job form, in the AOT Forms node, view the difference between Reliable Asynchronous transient jobs and Scheduled Batch normal jobs. Transient or runtime jobs are automatically deleted on completion.
Forms
-getRanges SysOpFindPrimesForm -read -read * -startOperation SysOpFindPrimesController 1 +findCountOfPrimeNumbersInRange() +startOperation() +prompt() +new() +unpack() +pack() * BatchJob -RecId RecId BatchJob * -Write results -run +addBatchJobToQueue() +scheduleBatchJob() +addRuntimeTask() BatchServer
Classes
Tables
Sequence diagram
SysOpFindPrimesForm
SysOpFindPrimesController
BatchServer
SysOpFindPrimesTrackingTable
startOperation Prompt User pack() addBatchJobToQueue new() unpack() findCountOfPrimeNumbersInRange insert read data
As the preceding figures show, SysOpFindPrimesForm creates an instance of SysOpFindPrimesController and calls startOperation on it. The controller displays the user interface needed to get operation parameters and starts the operation. The operation updates SysOpFindPrimesTrackingTable, which SysOpFindPrimesForm polls to get the results. The form starts the operation. SysOpFindPrimesForm
void clicked() { guid id; SysOperationStartResult result; Args args; elapsedSeconds.value(0); // Pass the execution mode to the controller from the combo box args = new args(); args.parmEnumType(enumNum(SysOperationExecutionMode)); args.parmEnum(operationExecutionMode.selection()); [result, id] = SysOpFindPrimesController::main(args); // controller's main function passes the operation id back // the form uses this to poll for results. if (result == SysOperationStartResult::AddedToBatchQueue || result == SysOperationStartResult::Started) { callId.value(id); initialTicks = WinAPI::getTickCount(); element.recurringRefresh(); } if (result == SysOperationStartResult::AddedToBatchQueue) { // Run the batch server ping operation in a CLR session // the controller will marshall it over. This will cause // the batch server to poll right away if it is idle. new SysOpSampleBatchServerPingController().run(); } }
Note the use of SysOperationBatchServerPingController in the preceding code. This notifies the batch server that there is additional work to do. The API simply sets a flag that is normally set by the batch server to process new tasks created during job execution. Setting the flag is simply an indication of pending work, it adds no overhead to the batch server runtime. When the controller is run in Reliable Asynchronous mode, it adds itself as a batch job. When the batch server schedules the job, it creates the controller, hydrates it by calling unpack(), and calls the operation logic. The operation logic updates the result table. SysOpFindPrimesController
public void findCountOfPrimeNumbersInRange(SysOpFindPrimesDataContract range) { int64 i, primes; SysOpFindPrimesTrackingTable logTable; RefRecId batchJobId; 27 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
int ticks; if (this.isInBatch()) { batchJobId = BatchHeader::getCurrentBatchHeader().parmBatchHeaderId(); } ttsBegin; logTable.clear(); logTable.BatchJob = batchJobId; logTable.Status = 0; logTable.callId = range.parmCallId(); logTable.Result = strFmt('Finding primes in range[%1, %2]', range.parmStartOfRange(), range.parmEndOfRange()); logTable.insert(); ttsCommit; ticks = WinAPIServer::getTickCount(); primes = 0; for (i = range.parmStartOfRange(); i <= range.parmEndOfRange(); i++) { if (SysOpFindPrimesController::isPrime(i)) { primes++; } } ticks = WinAPIServer::getTickCount() - ticks; ttsBegin; logTable.Status = 1; logTable.callId = range.parmCallId(); logTable.Result = strFmt('Found %3 primes in range[%1, %2] in %4 seconds', range.parmStartOfRange(), range.parmEndOfRange(), primes, ticks / 1000); logTable.update(); ttsCommit; }
Note that, in this sample, the SysOpFindPrimesService class is not used. It will be used later, from the .NET client, to invoke the controller discussed here.
Sample overview
In this sample, the user interface is enhanced. It contains a progress bar that is updated as the operation runs and a Result text box that shows output from the operation logic. Note that, in this sample, the operation runs only via the batch server, so the Batch Job Status field from the BatchJobHistory table is prominent in the UI.
2. Click Find Prime Numbers, and then enter the operation input parameters. As in the previous sample, enter the range [1,1000000].
3. Make sure that the Simulate Error check box is cleared. Also make sure that the Batch processing check box is cleared on the Batch tab.
If you have multiple processors in your machine, note that the full power of all the processors is used in the computation. For large ranges, compare the execution time with the previous sample. The time difference is not great for small ranges because of the overhead of coordinating multiple tasks.
As configured, this batch job will clean up all operations that are more than 10 minutes old if they are not in an error state. Operations that were not completed successfully are left in the table, to alert the administrator of potential problems. 3. To configure the parameters for this recurring task, click Change status on the Functions menu in the Batch job form when the job is in the Waiting status. Change the status to Withhold.
4. After the job is in the Withhold status, click View tasks on the toolbar in the Batch job form to open the Batch tasks form.
5. Click Parameters on the toolbar. This will instantiate the parameters dialog box for the cleanup operation. Note that this dialog box is provided by the SysOperation framework.
6. Modify the cleanup interval as desired, and then click OK. Close the Batch tasks form, and change the status of the parent batch job from Withhold to Waiting. 7. Run some operations.
8. Validate that all the operations exist in the tracking table by clicking the Selected Operation Id lookup.
9. Wait for the cleanup interval that you configured, and then click the Selected Operation Id lookup again to verify that the successful operations have been purged.
error. This step cannot be done in the code executing the task, because an unhandled exception can cause the task code to exit. SysOpFindPrimeNumbersCleanupController is used to propagate the error information from the BatchJobHistory table to the tracking table. Make sure that the cleanup controller is installed by using the steps in the previous section, Cleaning up the results table periodically, before completing the following steps. 1. Run an operation, and select the Simulate Error check box in the operation input parameters form.
2. Wait for the operation to finish running. Notice that the Batch Job Status field shows an error.
3. Click Batch Job History to open the Batch job history form.
5. Wait a minute or two for the cleanup job to run. It is set to recur every minute. Click Refresh in the sample form. The Status field should be updated to -1, and the operation should be marked as Done! in the Description field.
The -1 status on the operation record signifies that the operation did not end correctly. The caller can request more information as needed from the batch job history. Having the error status updated by a periodic job takes care of all exceptional scenarios. The task of catching all exceptions is assigned to the batch server. Application logic can handle the exceptions that it understands and is free to throw exceptions.
It is important to note that alerts are not generated for transient batch jobs created by the SysOperation Reliable Asynchronous execution mode. Transient batch jobs are batch jobs that have the property parmRuntimeJob set. Alerts can, however, be generated in application code as needed. To see alerts in action by using the default batch server functionality, use the form SysOpFindPrimesWithTasksForm. 1. Open the form.
2. Click Find Prime Numbers, and then enter operation input parameters as before.
3. Select the Batch processing check box to run the operation via a non-transient job.
5. The job runs as before, but its entry persists in the batch job table. Navigate to the Batch job form to verify that the job is not automatically deleted upon completion. Look at its alerts configuration: it was configured in code and the settings differ from the default settings.
6. Click the Notifications (bell) button in the status bar at the bottom of the sample form. The notification count may not be updated immediately, but clicking the button will update it and open the Notification list form.
The standard batch server alerts may not be desirable for asynchronous operations where the use of the batch server is an implementation detail. The sample generates alerts in the asynchronous error propagation scenario. Make sure that the cleanup controller is installed by using the steps in the Cleaning up the results table periodically section before completing the following steps.
1. Run an operation with the Simulated Error check box selected and the Batch processing check box cleared.
3. Wait a minute, and then click Refresh until the Status field shows -1 and the Description field shows Done!
4. Click the Notifications (bell) button in the status bar at the bottom of the form. The notification count may not be updated immediately, but clicking the button will update it and open the Notification list form. A custom notification with the subject Errors finding primes should be in the list.
Forms
-startOperation -run SysOperationFindPrimesWithTasksForm
SysOpFindPrimeNumbersCleanupDataContract -cleanupIntervalInMinutes
-Read
Classes
BatchJob -RecId RecId
Tables
BatchJob
Sequence diagram
Form startOperation
JobController
TaskController
BatchServer
TrackingTable
new unpack createTasksForFindingPrimeNumbers new setRanges() pack() addRuntimeTask insert() new unpack findCountOfPrimeNumbersInRange update
read data
// Get the header of the currently executing job batchHeader = BatchHeader::getCurrentBatchHeader(); batchJobId = batchHeader.parmBatchHeaderId(); // the delete on success flag (runtimeJob) is lost when altering batch header. // Restoring it. This parameter is set when the job is created to execute in reliable // async mode select RuntimeJob from currentJob where currentJob.RecId == batchJobId; batchHeader.parmRuntimeJob(currentJob.RuntimeJob); // break up to range to create tasks. The number of tasks is hardcoded in a macro start = range.parmStartOfRange(); end = range.parmEndOfRange(); blockSize = (end - start + 1) / #BatchTaskCount; remainder = (end - start + 1) - (blockSize * #BatchTaskCount); for (i = 0; i < #BatchTaskCount; i++) { end = start + blockSize; if (i == #BatchTaskCount - 1) { end += remainder; } // Create a controller for each sub task and add it // to the current job taskController = new SysOpFindPrimeNumbersTaskController(); subRange = taskController.getDataContractObject(); subRange.parmCallId(range.parmCallId()); subRange.parmStartOfRange(start); subRange.parmEndOfRange(end - 1); subRange.parmSimulateError(range.parmSimulateError()); batchHeader.addRuntimeTask(taskController, 0); start = end; } ttsBegin; // Save the current batch header along with all the newly // created tasks batchHeader.save(); // Insert a record in the operation tracking table. All the tasks will // update this record. logTable.clear(); logTable.BatchJob = batchJobId; logTable.Status = 0; logTable.callId = range.parmCallId(); logTable.Result = strFmt('Finding primes in range[%1, %2]', range.parmStartOfRange(), range.parmEndOfRange()); logTable.insert(); ttsCommit; }
range.parmStartOfRange(), range.parmEndOfRange(), primes, BatchHeader::getCurrentBatchTask().RecId, ticks / 1000); logTable.update(); // Since all the tasks update the same record, there is significant // conflict on committing the transaction. There are better ways to do // this; each task could have its own tracking record ttsCommit; } catch (Exception::Deadlock) { retry; } catch (Exception::UpdateConflict) { // This will not be used with pessimistic locking // use ForUpdate instead of PessimisticLock in the select // to see this in action retry; } }
batchInfo.parmCaption(#JobName); recurrenceData = SysRecurrence::defaultRecurrence(); // start in x minutes from now so that the job can be inspected via the batchjob // form before it starts. recurrenceData = SysRecurrence::setRecurrenceStartDateTime(recurrenceData, DateTimeUtil::addMinutes(DateTimeUtil::utcNow(), 1)); recurrenceData = SysRecurrence::setRecurrenceNoEnd(recurrenceData); // Set the minimum recurrence interval of 1 minute recurrenceData = SysRecurrence::setRecurrenceUnit(recurrenceData, SysRecurrenceUnit::Minute, 1); batchInfo.parmRecurrenceData(recurrenceData); // This will add the job to the batch table cleanupController.parmExecutionMode(SysOperationExecutionMode::ScheduledBatch); cleanupController.doBatch(); } } }
} else if (executionMode == SysOperationExecutionMode::ReliableAsynchronous) { // Alerts don't fire in reliable async mode so clear them batchHeader = this.batchInfo().parmBatchHeader(); batchHeader.clearAllAlerts(); batchHeader.addUserAlerts(curUserId(), NoYes::No, NoYes::No, NoYes::No, NoYes::No, NoYes::No); } return super(); 48 INTRODUCTION TO THE SYSOPERATION FRAMEWORK // alert user who created the job // completed // error // canceled // popup or toast in desktop client // email
where jobHistory.Status == BatchStatus::Error && jobHistory.BatchJobId == operationTable.BatchJob; errorCount = operationTable.RecId; update_recordSet operationTable setting Status = -1 where operationTable.Status == 0 exists join jobHistory where jobHistory.Status == BatchStatus::Error && jobHistory.BatchJobId == operationTable.BatchJob; ttsCommit; // Generate alert for erroneous operations if (errorCount) { ttsBegin; alerts = EventNotification::construct(EventNotificationSource::Batch); alerts.newInfo(curUserId(), // user 'Errors finding primes', null, null, true, false, '', curext(), // subject // menu item // record // popup // email // email address // company
DateTimeUtil::utcNow(), // date strfmt('%1 errors propagated', errorCount)); // message alerts.create(); alerts.insertDatabase(); ttsCommit; } info(this.caption()); info(strFmt('Cleaned up %1 operation records. %2 errors detected.', operationTableCount, errorCount)); }
Note that, in this sample, the class SysOpFindPrimesWithTasksService is not used. It will be used in the next section, from the .NET client, to invoke the controller discussed here.
Deploying this group creates a WCF endpoint out of the Microsoft Dynamics AX server. The endpoint exposes two service interfaces. Four artifacts in the sample project define the service interfaces exposed by the endpoint.
The first service, SysOpFindPrimesService, runs the controller that computes primes by using a single batch task. The second service, SysOpFindPrimesWithTasks, computes primes by using multiple batch tasks. The .NET client calls both service interfaces in the common endpoint and provides a comparison between the two designs.
2. Check the app.config file to make sure that the service endpoints point to localhost. It is assumed that the sample will be run on the same machine as the Microsoft Dynamics AX server. 3. Run the sample. It will run in three steps: 1. Run with a single batch task 2. Run with multiple batch tasks This should run faster on a multiprocessor machine. 3. Run with multiple batch tasks and simulate an error in one of the tasks
The error is propagated from the batch history to the .NET client.
WcfServiceProxies
AX Server
-Fill
-Fill
-Invoke
-run SysOpFindPrimeNumbersJobController +caption() +createTasksForFindingPrimeNumbers() +doBatch() +new() +main() +startOperation() +promptUser() +pack() +unpack() -addBatchJobToQueue BatchServer +addBatchJobToQueue() +scheduleBatchJob() +addRuntimeTask() -addBatchJobToQueue
-run
Sequence diagram
DotNet Program
FindPrimesWithTasksService
JobController
TaskController
BatchServer
TrackingTable
update
getOperationStatus data
read() data
Issue 1: The controller should not be unpacked from the SysLastValue table when running via batch.
Class SysOpSampleBaseController
protected void loadFromSysLastValue() { if (!dataContractsInitialized) { // This is a bug in the SysOperationController class // never load from syslastvalue table when executing in batch // it is never a valid scenario if (!this.isInBatch()) 56 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
Issue 2: The default value for property parmRegisterCallbackForReliableAsyncCall should be false to avoid unnecessary polling of the batch server.
public void new() { super(); // defaulting parameters common to all scenarios // If using reliable async mechanism do not wait for the batch to // complete. This is better done at the application level since // the batch completion state transition is not predictable this.parmRegisterCallbackForReliableAsyncCall(false); code removed for clarity }
Issue 3: The default value for property parmExecutionMode should be Synchronous to avoid issues when creating run-time tasks.
Class SysOpSampleBaseController
public void new() { code removed for clarity // default for controllers in these samples is synchronous execution // batch execution will be explicitly specified. The default for // SysOperationServiceController is ReliableAsynchronous execution this.parmExecutionMode(SysOperationExecutionMode::Synchronous); }
Issue 4: The value of the column runTimeJob in the BatchJob table is overwritten when runtime tasks are added to a batch job.
SysOpFindPrimeNumbersJobController
public void createTasksForFindingPrimeNumbers(SysOpFindPrimesWithTasksDataContract range) { code removed for clarity // Get the header of the currently executing job batchHeader = BatchHeader::getCurrentBatchHeader(); batchJobId = batchHeader.parmBatchHeaderId(); // the delete on success flag (runtimeJob) is lost when altering batch header. // Restoring it. This parameter is set when the job is created to execute in reliable // async mode 57 INTRODUCTION TO THE SYSOPERATION FRAMEWORK
select RuntimeJob from currentJob where currentJob.RecId == batchJobId; batchHeader.parmRuntimeJob(currentJob.RuntimeJob); code removed for clarity }
Change
Initial publication Sample 3: Introduction to SysOperation execution modes was added to provide descriptions of the execution modes for the SysOperation framework that are designed to provide different options for managing the single-threaded constraint that is associated with Microsoft Dynamics AX sessions.
Microsoft Dynamics is a line of integrated, adaptable business management solutions that enables you and your people to make business decisions with greater confidence. Microsoft Dynamics works like and with familiar Microsoft software, automating and streamlining financial, customer relationship and supply chain processes in a way that helps you drive business success. U.S. and Canada Toll Free 1-888-477-7989 Worldwide +1-701-281-6500 www.microsoft.com/dynamics
This document is provided as -is. Information and views expressed in this document, including URL and other Internet Web site references, may change without notice. You bear the risk of using it. Some examples depicted herein are provided for illustration only and are fictitious. No real association or connection is intended or should be inferred. This document does not provide you with any legal rights to any intellectual property in any Microsoft product. You may copy and use this document for your internal, reference purposes. You may modify this document for your internal, reference purposes. 2013 Microsoft Corporation. All rights reserved.