-
Notifications
You must be signed in to change notification settings - Fork 0
Task Service
The Task Service is a foundation of the Hemi Framework in that it was one reason the MDI toolkit was discontinued in favor of the M3 design in 2002 (which became Engine, and now Hemi). Development of the Task Service resulted in the separate Transaction Service and the implementation of the Task Service as a transaction participant.
Tasks are sequences of synchronous or asynchronous activities, where an individual activity may define an optional action, an optional handler, and stipulate zero or more dependencies.
- Transaction Service
- Task Service API
Tasks may be created as XML declarations or assembled via script. The task workflow is to name an activity which becomes a virtualized Task List. The Task List exists as a collection of tasks constructed of dependent tasks and named dependencies derived from a principal root task.
Every task has five key properties, defined as XML attributes or API parameter values:
- id : The task identifier, unique to the Task Service instance.
- rid : A reference identifier used with a child task to create a dependency on another task.
- auto-execute : Bit (auto-execute = "1") indicating that the task should be automatically executed when it is loaded.
-
action-type : The type of action to perform for the task. Supported types are:
- none : Any value other than named values indicates to do nothing.
- component : The value of the action attribute is an object constructor. For use in combination with the Task parameters property.
- xml : The value of the action attribute is a path to an XML file to load.
- script : The value of the action attribute may be #cdata for XML, or inline script. The script is executed as a Module, and, if defined, will receive an invocation to RunTaskScript with the Task object as a parameter. If defined as a function pointer, the function is executed in the scope of the Task.
- function : The value of the action attribute is an anonymous function statement (similar to an inline event handler).
- event : The value of the action attribute is a message name to be published by the Message Service.
- task : The value of the action attribute is a task identifier to be executed.
- action: The action to execute, based on the action-type.
-
handler-type : The type of handler action to perform for the task. Supported types are:
- none : Any value other than named values indicates to do nothing.
- script : The value of the handler attribute may be #cdata for XML, or inline script. The script is executed as a Module, and, if defined, will receive an invocation to RunTaskScript with the Task object as a parameter. If defined as a function pointer, the function is executed in the scope of the Task.
- function : The value of the handler attribute is an anonymous function statement (similar to an inline event handler).
- event : The value of the handler attribute is a message name to be published by the Message Service.
- task : The value of the handler attribute is a task identifier to be executed.
- import-task : The value of the handler task is a task identifier, and the expectation is the action-type is xml, such that the Task Object's data property contains the loaded document.
- handler: The handler action to execute, based on the handler-type.
Task dependencies are created by linking another task by its identifier, or a named dependency as a string value that is unique to the Task Service.
Task lists can be described in XML for ease of consumption and reuse. The schema is straight forward:
<task
id = "task_id"
action-type = "..."
action = "..."
handler-type = "..."
handler = "..."
auto-execute = "0|1"
/>
Dependent tasks can be created with a child task element and the rid attribute:
<task>
<task rid = "..." />
</task>
}}}
Named dependencies can be created with the depends element and the rid attribute:
<task>
<depends rid = "..." />
</task>
The rid attribute is a string representing the dependency or a task id.
Declarative Tasks are loaded with the import-task action-type. Given some task list saved to a file named /MyTasks/Tasks.xml, and a primary task identified as MyTaskId, the following statement would load and execute that task list.
/// MyTaskLoader is the id of a task created to load the external task list
Hemi.task.service.executeTaskLoader("MyTaskLoader", "xml", "/MyTasks/Tasks.xml", "import-task", "MyTaskId");
Tasks can be scripted through the Task Service, and use the same types of values as the XML declarations.
The following example demonstrates how to create a task with no action or handler, and a named dependency.
var oTask = Hemi.task.service.addTask("demotask");
Hemi.task.service.addTaskDependency(oTask, "demodependency");
Hemi.task.service.executeTask(oTask);
/// Task is still ACTIVE because of dependency, even though it has no action or handler
///
Hemi.task.service.returnDependency("demodependency");
/// Task is now in COMPLETE state
While the previous example has no input or output, it demonstrates the fundamental Task Hierarchy: Given some task and dependency, execute the action, execute child dependencies, handle child dependencies, execute the handler.
The following example builds on the previous example with specified action and handlers to load an XML document, run some script, and raise a message when it's complete.
/// Create a task to raise an event
///
var oTask1 = Hemi.task.service.addTask("demotask",0, 0,"event","demotaskevent");
/// Subscribe to some named event notification
///
Hemi.message.service.subscribe("demotaskevent",function(sName, vData){
var oService = vData.service;
var oTask = vData.task;
});
/// Create a task to load some XML and invoke a function
///
var oTask2 = Hemi.task.service.addTask("demotask2","xml","/path/to/file.xml","function",function(sTaskName, oService){
var oTask = oService.getTaskByName(sTaskName);
/// The task service stores the xml response on the task _data_ property
///
var oXml = oTask.data;
return true;
});
/// Make Task1 dependent on Task2
///
Hemi.task.service.addTaskDependency(oTask1, oTask2);
/// Task1 will raise the event after the XML document loads and if the function returns true
///
Hemi.task.service.executeTask(oTask1);
The Task state tracks where the task is between initialization and completion. Task states are:
- 0 : Uninitialized
- 1 : Initialized
- 2 : Busy
- 3 : Active
- 4 : Handled
- 5 : Complete
- 99 : Destroyed
Tasks move between states during evaluation of the task, the current task state, and relativity of the state to the action and handler.
- Uninitialized -> Initialized : When the underlying Task transaction begins.
- Initialized -> Busy : When the Task is executed.
- Busy -> Active and Active -> Handled
- Automatically, all the time: default, component, event, import-task, xml.
- Automatically, dependent: function, if the return value is true.
- Semi-Automatically, Manually: script, if the script defines the RunTaskScript function and the return value is true.
- Handled -> Complete : When all dependencies are complete
Although the Task Service handles asynchronous transitions between task states, it may be desirable to spawn asynchronous actions within the action or handler without breaking it apart into separate tasks. If a script task does not return true or return the task dependency, or define the RunTaskScript function and return true, or if the function does not return true, the task enters into a stuck state. Even if the dependency is returned, the task state will not change until the transaction is served.
The following example demonstrates the stuck behavior.
var oTask = Hemi.task.service.addTask("sticky_task1","script","/// do something","function",function(){return true;});
Hemi.task.service.executeTask(oTask);
In the previous example, the script defined in the action does not indicate it completed by returning true, therefore, even if returnTaskDependency or closeTask are used, the handler is never executed.
The following example demonstrates a correction to the first problem, and introduces a second.
function DoSomeAsyncStuff(){
///
}
var oTask = Hemi.task.service.addTask("sticky_task1",
"script",
"function RunTaskScript(oTask){ return true;}",
"function",function(){
DoSomeAsyncStuff();
}
);
Hemi.task.service.executeTask(oTask);
In the previous example, the stuck action was corrected by using the RunTaskScript virtual method and returning true. However, the handler performs some asynchronous operation of its own, and needs to communicate back to complete the task.
This can be handled with a combination of a closure and using the advance method.
function DoSomeAsyncStuff(){
///
}
var oTask = Hemi.task.service.addTask("sticky_task1",
"script",
"function RunTaskScript(oTask){ return true;}",
"function",
function(){
var task = oTask;
AsyncHandler = function(){
Hemi.task.service.advance(task, 1, 1);
};
DoSomeAsyncStuff();
}
);
Hemi.task.service.executeTask(oTask);
In the previous example, the task handler stuck on an asynchronous operation advances by either keeping a pointer to the task object, or looking it up by name, and advancing the task when the operation is completed. This permits task dependencies and parent task handlers to be resolved.
One example of how the Task Service would be applied is to consider Web sites that require a particular loading sequence. Such a sequence might be:
- Load a component
- Load data
- Process (2) with (1)
- Send notification of success or failure.
In addition, there may be other dependencies such as (5) wait for window to load, and (6) load configuration data to identify network URL for (2).
One of the very first responses this example may inspire would be to add a window load handler, create an instance of a component, load data from within the component and use a readystatechange or load handler, and then either fall silent while waiting for UI-events or invoke some callback mechanism. The issue should be obvious: The management of the process sequence is left to individual components or a containing script. And, what was just an unfortunate fact of life in the above example becomes an architectural problem when developers result to polling to complete the sequence. Or, worse, writing convoluted routines specific to that particular component and data.
Let's revisit the previous example in terms of the Task Service.
- Create a component. (1)
- Load configuration data. (6)
- Load component data based on value stored in (6). (2)
- Wait for the window to load. (5)
- Process data. (3)
- Send notification when finished. (4)
It seems simple enough. We know (4) should be done last. However, there are several interwoven dependencies that are easy to overlook. For instance, (5), waiting for the window to load. This isn't a problem if the process was started after the window loaded, but doing so only delays (if ever so slightly) rendering the content for the user. Also, the data cannot be loaded (2) until the location is discovered from (6), therefore preventing (3) from being completed. Granted, any configuration could simply be included on the page and make the process linear. However, it is that same mentality that leads to convoluted processes because apparent simplicity is purchased at one location, but at the cost of wildly complex solutions in another.
As a task, this process can be declaratively managed. The following task configuration outlines the above process, where ExampleProcess is the entry point, where no action is defined, and where the handler is an invocation of the global function allDone.
<task id="ExampleProcess"
action-type="default"
action="[nothing]"
handler-type="function"
handler="allDone"
>
<task rid = "setup_object" auto-execute = "1" />
</task>
<task id="setup_object"
action-type="default"
action="[nothing]"
handler-type="script"
handler="#cdata"
>
<task rid = "create_object" auto-execute = "1"/>
<!-- dom_event_window_load is returned automatically by the Task Service -->
<depends rid="dom_event_window_load" />
<![CDATA[
function RunTaskScript(oTask){
// setup object here
return true;
}
]]>
</task>
<task id = "create_object"
action-type="default"
action="[nothing]"
handler-type="script"
handler="#cdata"
>
<task rid = "load_config" auto-execute = "1" />
<![CDATA[
function RunTaskScript(oTask){
// make the object here
return true;
}
]]>
</task>
<task id="load_config"
action-type="xml"
action="config.xml"
handler-type="default"
handler="[nothing]"
/>
The script to implement the above XML-based configuration is not too complex. However, it illustrates a basic tenet of the framework architecture design: Why saturate a Web page or Web application with structural code that could be better managed by a background service? In both the previous example and following example, the objectives are the same. The difference is that in the previous example the objective is more artfully stated (and would be even cleaner with a JSON mockup) well apart from the Web page or application. The only code developers needs to concern themselves with is starting the task that loads the XML data, and the allDone function. In the following example, which is the script translation of the previous XML, the developer must be more knowledgeable of the framework API. Why should developers be concerned with the details of the process (as long as it works) instead of just receiving notification that the process was finished (or, depending on the implementation, didn't finish)?
function setupExampleObject(){
// setup example object
// return true to automatically return the dependency
return 1;
}
function createMyObject(){
// create example object
// return true to automatically return the dependency
return 1;
}
var task1 = Hemi.task.service.addTask(
"ExampleProcess",
"default",
"[nothing]",
"function",
"allDone"
);
Hemi.task.service.addTaskDependency(task1,"setup_object");
Hemi.task.service.executeTask(task1);
/* can't use #cdata since there's no XML-based tasks */
var task2 = Hemi.task.service.addTask(
"setup_object",
"default",
"[nothing]",
"function",
"setupExampleObject"
);
Hemi.task.service.addTaskDependency(task2,"create_object");
Hemi.task.service.addTaskDependency(task2,"dom_event_window_load");
Hemi.task.service.executeTask(task2);
var task3 = Hemi.task.service.addTask(
"create_object",
"default",
"[nothing]",
"function",
"createMyObject"
);
Hemi.task.service.addTaskDependency(task3,"load_config");
Hemi.task.service.executeTask(task3);
var task4 = Hemi.task.service.addTask(
"load_config",
"xml",
"config.xml",
"nothing",
"[default]"
);
Hemi.task.service.executeTask(task4);