Execution Policies
From JPPF 6.2 Documentation
|
Main Page > Development guide > Execution Policies |
An execution policy is an object that determines whether a particular set of JPPF tasks can be executed on a JPPF node (for the server-side SLA) or if it can be sent via a communication channel (for the client-side). It does so by applying the set of rules (or tests) it is made of, against a set of properties associated with the node or channel.
These properties include:
- JPPF configuration properties
- System properties (including -D*=* properties specified on the JVM command line)
- Environment variables (e.g. PATH, JAVA_HOME, etc.)
- Networking: list of ipv4 and ipv6 addresses with corresponding host name when it can be resolved
- Runtime information such as maximum heap memory, number of available processors, etc...
- Disk space and storage information
- A special boolean property named “jppf.channel.local” which indicates whether a node (server-side) or communication channel (client-side) is local to the JPPF server or client, respectively
- Operating system information, such as OS name, version and architecture, physical RAM, swap space, CPU load
The kind of tests that can be performed apply to the value of a property, and include:
- Binary comparison operators: ==, <, <=, >, >= ; for instance: property_value <= 15
- Range operators (intervals): property_value in [a,b] , [a,b[ , ]a,b] , ]a,b[
- "One of" operator (discrete sets): property_value in { a1, ... , aN }
- "Contains string" operator: property_value contains "substring"
- Regular expressions: property_value matches 'regexp'
- Custom, user-defined tests
The tests can also be combined into complex expressions using the boolean operators NOT, AND, OR and XOR.
Using this mechanism, it is possible to write execution policies such as:
- "Execute on a node only if the node has at least 256 MB of memory and at least 2 CPUs available"
- “Execute the job only in the client's local executor”
In the context of a server-side SLA, an execution policy is sent along with the tasks to the JPPF driver, and evaluated by the driver. They do not need to be sent to the nodes.
For a detailed and complete description of all policy elements, operators and available properties, please refer to the section Execution policy reference.
1 Creating and using an execution policy
An execution policy is an object whose type is a subclass of ExecutionPolicy. It can be built in 2 ways:
By API, using the classes in the org.jppf.node.policy package.
Example:
// define a policy allowing only nodes with 2 processing threads or more ExecutionPolicy atLeast2ThreadsPolicy = new AtLeast("jppf.processing.threads", 2); // define a policy allowing only nodes that are part of the "mydomain.com" // internet domain (case ignored) ExecutionPolicy myDomainPolicy = new Contains("ipv4.addresses", true, "mydomain.com"); // define a policy that requires both of the above to be satisfied ExecutionPolicy myPolicy = atLeast2ThreadsPolicy.and(myDomainPolicy);
Alternatively, this could be written in a single statement:
// define the same policy in one statement ExecutionPolicy myPolicy = new AtLeast("jppf.processing.threads", 2).and( new Contains("ipv4.addresses", true, "mydomain.com"));
Using an XML policy document:
Example XML policy:
<ExecutionPolicy> <!-- define a policy that requires both rules to be satisfied --> <AND> <!-- define a policy allowing only nodes with 2 processing threads or more --> <AtLeast> <Property>jppf.processing.threads</Property> <Value>2</Value> </AtLeast> <!-- allow only nodes in the "mydomain.com" internet domain (case ignored) --> <Contains ignoreCase="true"> <Property>ipv4.addresses</Property> <Value>mydomain.com</Value> </Contains> </AND> </ExecutionPolicy>
As you can see, this is the exact equivalent of the policy we constructed programmatically before.
To transform this XML policy into an ExecutionPolicy object, we will have to parse it using the PolicyParser API, by the means of one of the following methods:
static ExecutionPolicy parsePolicy(String) // parse from a string static ExecutionPolicy parsePolicyFile(String) // parse from a file static ExecutionPolicy parsePolicy(File) // parse from a file static ExecutionPolicy parsePolicy(Reader) // parse from a Reader static ExecutionPolicy parsePolicy(InputStream) // parse from an InputStream
Example use:
// parse the specified XML file into an ExecutionPolicy object ExecutionPolicy myPolicy = PolicyParser.parsePolicyFile("../policies/MyPolicy.xml");
It is also possible to validate an XML execution policy against the JPPF Execution Policy schema using one of the validatePolicy() methods of PolicyParser:
static ExecutionPolicy validatePolicy(String) // validate from a string static ExecutionPolicy validatePolicyFile(String) // validate from a file static ExecutionPolicy validatePolicy(File) // validate from a file static ExecutionPolicy validatePolicy(Reader) // validate from a Reader static ExecutionPolicy validatePolicy(InputStream) // validate from an InputStream
To enable validation, the document's namespace must be specified in the root element:
<jppf:ExecutionPolicy xmlns:jppf="{{SERVER}}/schemas/ExecutionPolicy.xsd"> ... </jppf:ExecutionPolicy>
Example use:
public ExecutionPolicy createPolicy(String policyPath) { try { // validate the specified XML file PolicyParser.validatePolicyFile(policyPath); } catch(Exception e) { // the validation and parsing errors are in the exception message System.err.println("The execution policy " + policyPath + " is not valid: " + e.getMessage()); return null; } // the policy is valid, we can parse it safely return PolicyParser.parsePolicyFile(policyPath); }
2 Scripted policies
As we have seen earlier, execution policies are objects whose class extends ExecutionPolicy. The evaluation of an execution policy is performed by calling its accepts() method, which returns either true or false. A scripted policy is a special type of policy which can execute a script written in a script language. The result of the evaluation of this script, which must be a boolean, will be the value returned by its accept() method.
By default, JPPF provides engines for Groovy and JavaScript with the Rhino engine, however additional script languages can be added via the service provider interface (SPI).
2.1 Creating scripted policies
At runtime, a scripted policy is an instance of ScriptedPolicy, which defines the following constructors:
public class ScriptedPolicy extends ExecutionPolicy { // create with a script read from a string public ScriptedPolicy(String language, String script) // create with a script read from a reader public ScriptedPolicy(String language, Reader scriptReader) throws IOException // create with a script read from a file public ScriptedPolicy(String language, File scriptFile) throws IOException }
The equivalent XML is as follows:
<Script language="_language_">simple script</Script> <Script language="_language_"><![CDATA[ a more complex script here ]]></Script>
As for any other execution policy predicate, scripted policies can be combined with other predicates, using the logical operators AND, OR, XOR and NOT, for instance:
In Java:
ExecutionPolicy policy = new AtLeast("processing.threads", 2).and( new ScriptedPolicy("groovy", "return true"));
XML equivalent:
<And> <Equal valueType="numeric"> <Property>processing.threads</Property> <Value>2</Value> </Equal> <Script language="groovy">return true</Script> </And>
The script must either be an expression which resolves to a boolean value, or return a boolean value. For instance, the Groovy statement “return true” and the Groovy expression “true” will work seamlessly.
2.2 Predefined variable bindings
6 pre-defined variables are made available to the scripts:
- jppfSystemInfo: the parameter passed to the policy's accepts() method, its type is JPPFSystemInformation
- jppfSla: the server-side SLA of the job being matched, if available, of type JobSLA
- jppfClientSla: the client-side SLA of the job being matched, if available, of type JobClientSLA
- jppfMetadata: the metadata of the job being matched, if available, of type JobMetadata
- jppfDispatches: the number of nodes the job is already disatched to, of type int
- jppfStats: the server statistics, of type JPPFStatistics
For example, let's look at the following JavaScript script, which determines the node participation based on a jobs priority: if the priority is 1 or less, the job can use no more than 10% of the total number of nodes, if the job priority is 2 then if can use no more than 20%, … up to 90% if the priority is 9 or more:
function accepts() { // total nodes in the grid from the server statistics var totalNodes = jppfStats.getSnapshot("nodes").getLatest(); // the job priority var prio = jppfSla.getPriority(); // determine max allowed nodes for the job, as % of total nodes var maxPct = (prio <= 1) ? 0.1 : (prio >= 9 ? 0.9 : prio / 10.0); // return true if current nodes for the job is less than max % return jppfDispatches < totalNodes * maxPct; } // returns a boolean value accepts();
Let's say this script is stored in a file located at ./policies/NodesFromPriority.js, we could then create an execution policy out of it, with the following code:
ScriptedPolicy policy = new ScriptedPolicy("javascript", new File("policies/NodesFromPriority.js"));
2.3 Adding available languages
The JPPF scripting APIs rely entirely on the JSR 223 specification, which is implemented in the JDK's javax.script package. This means that JPPF will be able to use any script language made available to the JVM, including the default JavaScript engine (i.e. Rhino in JDK 7 and Nashorn in JDK 8).
Thus to add a new language, all that is needed is to add the proper jar files, which declare a JSR-223 compliant script engine via the documented SPI discovery mechanism. For example, you can add the Groovy language by simply adding groovy-all-x.y.z.jar to the classpath, because it implements the JSR 223 specification (the jar file is located in the JPPF source distribution at JPPF/lib/Groovy/groovy-all-1.6.5.jar).
3 Execution policy context
Each execution policy has access to a set of contextual information during its evaluation. This information pertains to the job against which the policy is evaluated, along with a snapshot of the server statistics (for server-side policies) at the time of the evaluation. This context is available with the method ExecutrionPolicy.getContext() and returns a PolicyContext object, defined as follows:
public class PolicyContext { // Get the job server side SLA, set at runtime by the server public JobSLA getSLA() // Get the job client side SLA, set at runtime by the server public JobClientSLA getClientSLA() // Get the job metadata, set at runtime by the server public JobMetadata getMetadata() // Get the number of nodes the job is already dispatched to public int getJobDispatches() // Get the server statistics public JPPFStatistics getStats() }
Note that, depending on where the execution policy is evaluated, some parts of the context may not be available:
- when the policy is evaluated in a client local executor, the server statistics are not available
- when the policy is evaluated on by server side scheduler, the client-side SLA is not available
- when the policy is evaluated via a call to any method of JPPFDriverAdminMBean which takes a NodeSelector or ExecutionPolicy parameter, only the server statistics are available.
The context is available to any execution policy, however it will be especially useful for custom policies. For scripted policies, the elements of information it provides are already split into separate variable bindings. Furtherrmore, keep in mind that the policy context is only valid in the scope of the policy's accepts() method.
4 Creating custom policies
It is possible to apply user-defined policies. When you do so, a number of constraints must be respected:
- the custom policy class must extend CustomPolicy
- the custom policy class must be deployed in the JPPF server classpath as well as the client's
Here is a sample custom policy code:
package mypackage; import org.jppf.utils.PropertiesCollection; import org.jppf.node.policy.CustomPolicy; // define a policy allowing only nodes with 2 processing threads or more public class MyCustomPolicy extends CustomPolicy { @Override public boolean accepts(PropertiesCollection info) { // get the value of the "processing.threads" property String s = this.getProperty(info, "processing.threads"); int n = -1; try { n = Integer.valueOf(s); } catch(NumberFormatException e) { // process the exception } } // node is accepted only if number of threads >= 2 return n >= 2; }
Now, let's imagine that we want our policy to be more generic, and to accept nodes with at least a parametrized number of threads given as argument to the policy.
Our policy becomes then:
public class MyCustomPolicy extends CustomPolicy { public MyCustomPolicy(String...args) { super(args); } @Override public boolean accepts(PropertiesCollection info) { // get the value to compare with, passed as the first argument to this policy String s1 = <b>getArgs()[0]</b>; int param = -1; try { param = Integer.valueOf(s1); } catch(NumberFormatException e) { } String s2 = getProperty(info, "processing.thread"); int n = -1; try { n = Integer.valueOf(s2); } catch(NumberFormatException e) { } // node is accepted only if number of threads >= param return n >= param; } }
Here we use the getArgs() method which returns an array of strings, corresponding to the arguments passed in the XML representation of the policy.
To illustrate how to use a custom policy in an XML policy document, here is an example XML representation of the custom policy we created above:
<CustomRule class="mypackage.MyCustomPolicy"> <Arg>3</Arg> </CustomRule>
The "class" attribute is the fully qualified name of the custom policy class. There can be any number of <Arg> elements, these are the parameters that will then be accessible through CustomPolicy.getArgs().
When the XML descriptor is parsed, an execution policy object will be created exactly as in this code snippet:
MyCustomPolicy policy = new MyCustomPolicy(); policy.setArgs( "3" );
Finally, to enable the use of this custom policy, you will need to add the corresponding class(es) to both the server's and the client's classpath, within either a jar file or a class folder.
5 Server global policies
JPPF 5.2 introduced a new type of execution policy which applies globally to all the nodes connected to a given server. It allows expressing rules such as "execute a job only if there are more than 4 nodes, each with at least 2 processors and at least 4 GB of memory".
This type of execution policy is represented by the class NodesMatching, defined as follows:
public class NodesMatching extends ExecutionPolicy { // Initialize this execution policy public NodesMatching(Operator operator, long expectedNodes, ExecutionPolicy nodePolicy) // Evaluate this policy against the nodes public boolean accepts(PropertiesCollection info) }
The interesting part is in the constructor, which takes the following parameters:
- operator: one of the possible comparison operators defined in the Operator enum, that is, one of EQUAL, NOT_EQUAL, LESS_THAN, MORE_THAN, AT_LEAST, AT_MOST.
- expectedNodes: this is the number of nodes expected to match the comparison with the actual number of nodes that satisfy the nodePolicy parameter
- nodePolicy: an execution policy that will be evaluated against all the nodes. The number of nodes matching this policy will be compared to the expectedNodes parameter using the operator parameter as a comparison operator.
As an example, we would write the policy expressed above as follows:
// 1 GB = 1,073,741,824 bytes long GB = 1024*1024*1024; // node with at least 2 processors and at least 4 GB of heap ExecutionPolicy nodePolicy = new AtLeast("availableProcessors", 2) .and(new AtLeast("maxMemory", 4*GB)); // more than 4 nodes satisfying the node policy ExecutionPolicy globalPolicy = new NodesMatching(Operator.MORE_THAN, 4, nodePolicy);
Alternatively, it can also be written as an XML document:
<!-- more than 4 nodes such that: --> <NodesMatching operator="MORE_THAN" expected="4"> <AND> <!-- each node has at least 2 processors --> <AtLeast> <Property>availableProcessors</Property> <Value>2</Value> </AtLeast> <!-- each node has at least 4 GB of heap --> <AtLeast> <Property>maxMemory</Property> <Value>4294967296</Value> </AtLeast> </AND> </NodesMatching>
6 Execution policy arguments as expressions
Since JPPF 6.0, the arguments of most execution policies can also be expressed as the values of JPPF properties including property substitutions and scripted expressions.
As an example, let's imagine the configuration of a node contains the following properties:
int.1 = 1 int.2 = 2 int.3 = 3
Now let's say we define an execution policy where jobs only execute on nodes that have at least 4 processing threads:
ExecutionPolicy policy = new AtLeast("jppf.processing.threads", 4);
Further, let's sauy that the expected number of processing threads is not static, and actually depends on the node's configuration. We could then rewrite the policy as in this example:
ExecutionPolicy policy = new AtLeast("jppf.processing.threads", "$script{ ${int.1} + ${int.3} }$");
Given the node configuration above, we can see that the value of the expression will resolve to 4 for this node.
We distinguish two cases, depending on the semantics of the argument:
1) Property name argument: this argument is always the first in an execution policy's constructor. It can now be either:
- a literal which represents the name of a property. This preserves the behavior from before JPPF 6.0
- or an expression that resolves to a value of the type handled by the execution policy
The limitation here is that this argument can never be a literal value, since any literal is interpreted as the name of a property.
If a literal value is required, it may be specified instead in one of the right-side arguuments (if any), or as a scripted expression, for instance: "$script{4}$".
In the example above, we used a property name expressed as the literal "jppf.processing.threads". We could also write it as a completely unrelated expression, for instance:
ExecutionPolicy policy = new AtLeast("$script{ 2 * ${int.3} }$", "$script{ ${int.1} + ${int.3} }$");
For the node we considered, this policy would resolve to the comparison "6 >= 4".
2) Right-side argument(s): as seen in the example above, the arguments on the right side can be expressions that must resolve to the expected type of the argument, or to a literal of the expected type.
For example, the polices below resolve to the same comparison "valueOf("jppf.processing.threads") >= 4" when evaluated against our node:
ExecutionPolicy p1 = new AtLeast("jppf.processing.threads", "$script{ 2 * ${int.2} }$"); ExecutionPolicy p2 = new AtLeast("jppf.processing.threads", "4"); ExecutionPolicy p3 = new AtLeast("jppf.processing.threads", 4);
Important: all scripted expressions have access to a predefined variable named "jppfSystemInfo" which references an object of type JPPFSystemInformation.
Note 1: support for arguments as expressions is clearly documented in each execution policy's javadoc.
Note 2: remember that in an expression, property substitutions are evaluated first, and scripted expressions are evaluated after that.
Main Page > Development guide > Execution Policies |