Defining what happens when matching events are found
In a monitor, when the correlator detects a matching event, it triggers the action defined by the listener for that event. This section discusses what you can specify in the triggered actions.
In a monitor, when the correlator detects a matching event, it triggers the action defined by the listener for that event. This section discusses what you can specify in the triggered actions.
EPL supports the use of variables in monitors. Depending on where in the monitor you declare a variable, that variable is global or local:
A variable can be of any of the primitive or reference types that are listed under Types in the EPL Reference.
Information about variables is presented in the topics below.
See also Using action type variables.
Variables in monitor scope are global variables; you can access a global variable throughout the monitor. You can define global variables anywhere inside a monitor except in actions and event definitions. For example:
monitor SimpleShareSearch {
// A monitor scope variable to store the stock received:
//
StockTick newTick;
This declares a global variable, newTick
, that can be used anywhere within the SimpleShareSearch
monitor including within any of its actions.
The order does not matter. In the following example, f
is a global variable:
monitor Test {
action onload() {
print getZ().toString();
}
action getZ() returns integer {
return f.z;
}
Foo f;
event Foo{
integer z;
}
}
If you do not explicitly initialize the value of a global variable, the correlator automatically assigns a value to that global variable. See also Default values for types.
A variable that you declare inside an action is a local variable. You must declare a local variable (specifying its type) and initialize that variable before you can use it.
Although the correlator automatically initializes global variables that were not explicitly assigned a value, the correlator does not do this for local variables. For local variables, you must explicitly assign a value before you can use the variable.
If you try to inject an EPL file that declares a local variable and you have not initialized the value of that local variable before you try to use it, the correlator terminates injection of that file and generates a message such as the following: Local variable 'var2' might not have been initialized
. EPL requires explicit assignment of values to local variables as a way of achieving the best performance.
When you declare a variable in an action, you can use that variable only in that action. You can declare a variable anywhere in an action, but you can use it only after you declare it and initialize it.
For example,
action anAction(integer a) returns integer {
integer i;
integer j;
i := 10;
j := a;
return j + i;
}
You can use the local action variables, i
and j
in the action, anAction()
, after you initialize them. The following generates an error:
action anAction2(integer a) returns integer {
i := 10; // error, reference to undeclared variable i
j := a; // error, reference to undeclared variable j
integer i;
integer j;
i := 2;
j := 5;
return j + i;
}
Suppose that an action scope variable has the same name as a monitor scope variable. Within that action, after declaration of the action scope variable, any references to the variable resolve to the action scope variable. In other words, a local action variable always hides a global variable of the same name.
Consider again the definition for anAction2()
in the previous code fragment, but with i
and j
variables declared in the monitor scope. The first use of i
and j
resolves successfully to the values of the i
and j
monitor scope variables. The second use occurs after the local declaration and initialization of i
and j
. That use resolves to the local (within the action) occurrence. This results in the following values:
i
is set to 10
.i
is set to 2
.j
is set to the value of a
.j
is set to 5
.Since you must explicitly initialize local variables before you can use them, the following example is invalid because j
and i
are not initialized to any value before they are used.
action anAction3(integer a) returns integer {
integer i;
integer j;
return j + i; // error, i and j were not initialised
}
It is possible to initialize a variable on the same line as its declaration, as follows:
action anAction4(integer a) returns integer {
integer i := 10;
integer j := a;
return j + i;
}
It is also possible to initialize a local variable by coassigning to it in an event listener. For example, the following is correct:
action onload() {
on all Event() as e {
log e.toString();
}
}
You can also initialize a local variable by coassigning to it from a stream. For example:
action onload() {
from x in all X() select x.f as f {
log f.toString();
}
}
Suppose you use a local variable in a listener action, as in the following example:
monitor MyMonitor {
integer x;
action onload() {
integer y := 10;
on all StockTick(*,*) {
log x.toString();
log y.toString();
}
y := 5;
}
}
In this example, x
is a global variable, and y
is a local variable. There are references to both variables in the listener action.
A reference to a global variable in a listener action is the same as a reference to a global variable anywhere else in the monitor. However, a reference to a local variable in a listener action causes the correlator to retain a copy of the local variable for use when the event listener triggers. The value held by this copy is the value that the local variable has when the correlator instantiates the event listener.
When the event listener triggers the correlator executes the listener action. This will be at some point in the future, and after the rest of the body of the enclosing action has been executed. Since the action has already been executed, any of the original local variables no longer exist. This is why the correlator retains a copy of the local variable to make available to the listener action when it is executed.
In the example above, when the event listener triggers and the correlator executes the listener action
x
has a value of 0
, which is the value that the correlator automatically assignsy
has a value of 10
, which is the value it was set to when the event listener was instantiatedThe value of y
that the correlator retained when it instantiated the event listener is not affected by the subsequent statement (after the on
statement) that sets the value of y
to 5.
In a monitor or in an event type definition, you can specify a named boolean
, decimal
, float
, integer
, or string
value as constant. The format for doing this is as follows:
constant type name := literal;
Element | Description |
---|---|
type | Specify boolean , decimal , float , integer , or string . This is the type of the constant value. |
name | Specify an identifier for the constant. This name must be unique within its scope — monitor, event, or action. |
literal | Specify the value of the constant. The type of the value must be the type that you specify for the constant. |
Benefits of using constants include:
42
, is not.You can refer to a declared constant in any code in the event or monitor being defined. When you define a constant in an event you can refer to it from outside the event by qualifying the name of the constant with the event name, for example, MyEvent.myConstant
.
Following is an example of specifying and using a constant:
event Paper {
constant float GOLDEN := 1.61803398874;
float width;
action getLength() {
return GOLDEN * width;
}
action getWidth() {
return width;
}
}
You cannot declare a constant in an action.
Actions are similar to procedures.
A monitor can define any number of actions. Finding an event, or pattern of events, of interest can trigger an action.
You can also trigger an action by invoking it from inside another action. You can also declare an action as part of an event type definition, and then call that action on an instance of that event.
The topics below provide information about defining actions.
The format for defining an action that takes no parameters and returns no value is as follows:
action actionName() {
// do something
}
Optionally, an action can do either one or both of the following:
The format for defining an action that accepts parameters and returns a value is as follows:
action actionName(type1 param1, type2 param2,...) returns type3 {
// do something
return type3_instance;
}
For example:
action complexAction(integer i, float f) returns string {
// do something
return "Hello";
}
An action that accepts input parameters specifies a list of parameter types and corresponding names in parentheses after the action name. Parentheses always follow the action name, in declarations and calls, whether or not there are any parameters. Parameters can be of any valid EPL type. The correlator passes primitive types by value and passes complex types by reference. EPL types and their properties are described in the API reference for EPL (ApamaDoc).
When an action returns a value, it must specify the returns
keyword followed by the type of value to be returned. In the body of the action, there must be a return
statement that specifies a value of the type to be returned. This can be a literal or any variable of the same type as declared in the action definition.
An action can have any name that is not a reserved keyword. Actions with the names onload()
, onunload()
and ondie()
can only appear once and are treated specially as already described in About monitor contents. It is an EPL convention to specify action names with an initial lowercase letter, and a capital for each subsequent word in the action name.
Actions and global variables must not have the same names. See Using action type variables. If you have any code that uses the same identifier for an action and a global variable, you must change it.
To invoke an action from another action, specify the action name followed by parentheses. If the action takes one or more input parameters, specify values for the parameters inside the parentheses. For example:
// First action:
action myAction1() {
myAction2();
}
// Second action that is called by the first action:
action myAction2() {
//...
}
In the example above, myAction1()
calls myAction2()
from inside the myAction1()
declaration block. myAction2()
takes no parameters and does not return a value.
When an action returns a value, you can invoke that action only from within an expression. You cannot specify a standalone statement that invokes an action that returns a value. Discarding the return value is illegal in EPL. For example:
action myAction3() returns string {
return "Hello";
}
action myAction4() {
string response;
response := myAction3(); // Valid
myAction3(); // Invalid
}
Consider this extended example:
// First action:
//
action myAction1() {
myAction2();
}
// Second action that is called by the first action:
//
action myAction2() {
string answer1, answer2;
myAction5(5, 10.5);
on anEvent() myAction5(5, 10.5);
answer1 := myAction6(256, 1423.2);
answer2 := myAction7();
}
// Action that is called by myAction2:
//
action myAction5 (integer i, float f) {
...
}
// Another action that is called by myAction2:
//
action myAction6 (integer i, float f) returns string {
return "Hello";
}
// Yet another action that is called by myAction2:
//
action myAction7() returns string {
return "Hello again";
}
myAction2()
takes no parameters and does not return a value.
myAction5()
accepts input parameters. You can invoke it from a standalone statement:
myAction5(5, 10.5);
You can also invoke it as a listener action:
on anEvent() myAction5(5, 10.5);
myAction6()
accepts input parameters and returns a value. You can invoke myAction6()
only from within an expression:
answer1 := myAction6(256, 1423.2);
myAction7()
returns a value but does not take any parameters. You can invoke it only from within an expression:
answer2 := myAction7();
You can specify an action in an event type definition. This lets you call that action on an instance of the event, just as you would call a built-in method on some other type, such as calling the toString()
method on the integer
type.
When you define an action
in an event, it behaves almost the same way as an action
in a monitor. For example, an action in an event can
In a monitor, an action in an event has an implicit self
argument that refers to the event instance that the action was called on. The self
argument behaves in the same way as the this
argument in C++ or Java.
For example, consider the following event type definition:
event Circle {
action area() returns float {
return 3.14159 * radius * radius;
}
action circumference() returns float {
return 2.0 * 3.14159 * self.radius;
}
float radius;
}
The specifications here of radius
and self.radius
are equivalent.
You can then write code that looks like this:
Circle c := Circle(4.0);
print "Circle area = " + c.area().toString();
print "Circle circumference = " + c.circumference().toString();
Of course, the output is as follows:
Circle area = 50.26544
Circle circumference = 25.13272
The correlator never executes actions in events automatically. In an event, if you define an onload()
action, the correlator does not treat it specially as it does when you define the onload()
action in a monitor.
When you call an action in an event, the correlator executes the action in the monitor instance in which the call was made. In a monitor, if the action sets up any listeners, these listeners are in the context of this monitor instance. If this monitor instance dies, the listeners also die.
You can use plug-ins from within event actions. In the event definition, specify the import
statement to give the plug-in an alias within the event. Specify the import
statement in the same way that you specify it for a monitor. You use the plug-in alias to call functions on the plug-in in the same way as you use it for a monitor.
When you define an event, there are no ordering restrictions for the definition of fields, imports, or actions. You can define them in any order.
From an action within an event, you can spawn to an action in the same event. The correlator spawns a monitor instance and executes the specified action
on the event instance in the new monitor instance.
It is not possible to spawn from outside a particular event to an action that is a member of that particular event. Instead, spawn to an action that calls the action that is the event member. For example:
event E {
action spawntotarget() {
spawn target(); // legal
}
action target() {
log "Spawned "+self.toString();
}
}
monitor m {
action onload() {
E e;
spawn e.target(); // not legal
spawn calltarget(e); // legal
e.spawntotarget();
}
action calltarget(E e) {
e.target();
}
}
Be sure to follow the spawn
keyword with an action
name identifier. Actions spawned to must have no return value, as before. See also Utilities for operating on monitors.
To summarize, when you define an action in an event, the following restrictions apply:
on
statement, you can coassign a matching event only to local variables. You cannot coassign a matching event to the event’s fields nor to items outside the event or in the monitor.self
parameter, any more than you can assign to this
in Java.In addition to defining an action, you can define a variable whose type is action
. This lets you assign an action to an action
variable of the same action
type. An action is of the same type as an action
variable if they have the same argument list (the same types in the same order) and return type (if any).
The format for defining an action
type variable is as follows:
action<[type1[, type2]...]>[returns type3]name;
Specify the keyword, action
.
Follow the action
keyword with zero, one or more parameter types enclosed in angle brackets and separated by commas. The angle brackets are required even when the action takes no arguments.
Optionally, follow the parameter list with a returns
clause. Specify the returns
keyword followed by the type of the returned value.
Finally, specify the name of the variable. For example:
action<string> a;
action<integer, integer> returns string b;
You can use an action
variable anywhere that you can use a sequence
or dictionary
variable. For example, you can
event
field, sequence
, or dictionary
.You cannot route, emit, enqueue or send an event that contains an action
variable field.
You must initialize an action
variable before you try to invoke it.
When an action variable is a member of an event the behavior of the action depends on the instance of the event that the action is called on. Consequently, it can be handy to bind an action variable member with a particular event instance. See Creating closures.
Built-in methods are treated exactly the same as user-defined actions. This means you can assign a built-in method to an action
variable. For example:
action<float> returns string f := float.toString;
The only operation that you can perform on an action
variable is to call it. You do this in the normal way by passing a set of parameters in parentheses after an expression that evaluates to the action
variable. For example:
monitor Test{
integer i;
action<string> x; // Uninitialized global action variable.
action onload() {
// Invoke the runMe action. The first argument to runMe is an
// action variable for an action having a single argument of
// type integer and no return value.
// Since the printInteger action conforms to the argument
// expected by runMe, you can pass printInteger to runMe.
runMe(printInteger, 10);
// Declare a local action variable, g. This action takes one
// integer argument and does not return a result.
// The printInteger action conforms to this so
// assign printInteger to g.
action<integer> g := printInteger;
// Invoke the runMe action again.
// Pass g instead of explicitly passing printInteger.
runMe(g, 20);
// Declare a local dictionary that contains action variables.
// Each action variable takes a single integer argument and
// and does not return a result.
// Add printInteger to the dictionary.
// Invoke printInteger and pass 30 as the argument.
dictionary<string, action<integer>> do := {};
do["printIt"] := printInteger;
do["printIt"] (30);
// Invoke x. Since this global variable was never
// initialized, the monitor instance terminates.
x("hello!");
}
action runMe(action<integer> f, integer i) {
f(i);
}
action printInteger(integer i) {
print i.toString();
}
}
After injection, this monitor prints
10
20
30
and then terminates upon invocation of x
because x
was never initialized.
Calling an uninitialized, local action
variable causes an error that prevents the correlator from injecting the monitor. While the correlator injects code that contains an uninitialized, global action
variable, trying to call the uninitialized variable causes a runtime error and the monitor instance terminates.
When you define an action as a member field in an event, that action has an implicit self
argument as the first argument (see Specifying actions in event definitions). You must include this implicit argument when determining whether an action definition conforms to an action
variable declaration. For example, the following is illegal:
event A {
action foo(float f) returns string {
return "Hello";
}
action bar() {
action<float> returns string f := A.foo;
}
}
In the previous code, you cannot assign the A.foo
action to f
because f
takes a single float
argument whereas A.foo
has two arguments — the implicit A
argument and then the float
argument. To correct this example, specify A
as the first action
argument in the body of the bar
action.
event A {
action foo(float f) returns string {
return "Hello";
}
action bar() {
action<A, float> returns string f := A.foo;
}
}
In some situations, you might find it more efficient to use action
type variables instead of routing events. For example, suppose you implement a service that takes an action
variable as one of its parameters. Now suppose that the service needs a response from an adapter or some other service before it can send a response. When ready, the service can respond with a routed event, but that means you have to set up an event listener for that event. Routing events and setting up event listeners is more expensive than invoking actions. So instead of routing and listening, the service can respond by invoking the action on the event that initiated the service request. For example:
The following sample code uses a routed event. Following this code there is a sample that uses an action on an event.
event ServiceResponse {
string requestId;
...
}
event Service {
action doRequest( string requestId,... ) {
...
// when asynchronous 'service actions' are complete
route ServiceResponse( requestId,... );
}
...
}
monitor Client {
Service service;
action onload() {
...
string id :=...;
on ServiceResponse( requestId=id )as r {
...
}
service.doRequest( id,... );
}
}
The following sample code uses an action on a Client
monitor:
event Service {
action doRequest( action<... > callback,... ) {
...
// when asynchronous 'service actions' are complete
callback(... );
}
...
}
monitor Client {
Service service;
action onload() {
...
string id :=...;
service.doRequest( onServiceResponse,... );
}
action onServiceResponse(...) {
...
}
}
When an action is a member of an event the behavior of the action depends on the instance of the event that the action is called on. Consequently, you might want to bind an action member with a particular event instance. When you bind an action member to an event instance you are creating a closure. The advantages of creating a closure are:
Consider the following event definition:
event E {
integer i;
action foo() { print "Foo "+i.toString(); }
action times(integer j) returns integer { return i*j; }
}
With this definition, E(1).foo()
would print “Foo 1”, while E(42).foo()
prints “Foo 42”. The action E.foo
always has a specific instance of E
to work with. You can achieve this by specifying the action’s implicit self argument when you call the action, as described earlier in this topic. When you use this technique you identify the event instance when you call the action variable.
Alternatively, you can create a closure that binds an action member with an event instance. You store the closure in an action variable. The action variable and the action member must be of the same action type. That is, they must take the same argument(s), if any, and return the same type, if any.
When you use this technique you identify the event instance when you assign the event’s action member to the action variable.
The following code shows an example of binding an event instance to an action member by storing the closure in an action variable.
monitor m {
action <> a;
action onload() {
E e := E(42);
a := e.foo;
a(); // Prints "Foo 42"
}
}
In this example, e.foo
denotes E.foo
called on e
. That is, when you assign the action e.foo
to the a
action variable you are identifying which instance of E
to use when you call the a
action. This closure binds a reference to E to the E.foo
action and stores it in the a
action variable. After you create a closure, you can call an action on an event as though it is a simple action. This gives you considerable flexibility in what you can assign to an action variable.
EPL performs its own garbage collection. Consequently, you do not need to consider how long a bound object must last. This is handled automatically.
A closure binds by reference. Consider the following example, which uses the same event E
as above:
monitor m {
action <integer> returns integer a;
action onload() {
E e := E(3);
a := e.times;
print a(2).toString(); // Prints "6"
e.i := 5;
print a(2).toString(); // Prints "10"
}
}
In a portion of code, you can define multiple action variables that contain closures for the same object. For example:
event Counter {
integer i;
action increment() { i := i+1; }
action output() { print i.toString(); }
}
event Increment {}
event Finish {}
monitor m {
action <> incrementAction;
action <> outputAction;
action onload() {
Counter counter := new Counter;
incrementAction := counter.increment;
outputAction := counter.output;
on all Increment() and not Finish() { incrementAction(); }
on all Finish() { outputAction(); }
}
}
In an event type, when an action member refers to another action member in the same event type a closure happens implicitly. For example:
event E {
action <integer> returns integer a;
}
event Plus {
integer i;
action f(integer j) returns integer { return i+j; }
action setA(E e) { e.a := f; }
}
Here, the f
in e.a := f
is equivalent to self.f
, just as it would be if setA
had called f
instead of assigning it to an action variable. This creates a closure. After setA
is called on some instance of Plus
, e.a
will call f
on that same instance.
You can create a closure using any value and any action on that value. Thus, it is possible to:
For example:
// Print "E(42)"
E e := E(42);
action <> printE42 := e.toString;
// Print "Foo 12345"
action <> printFoo12345 := E(12345).foo;
// Take a floating-point number and return e to that power:
action <float> returns float eToTheX := 2.718282.pow;
// Return a random integer from 0 to 9 inclusive.
// (The brackets around 10 are needed so that "10." is not treated as a
// floating-point number.)
action <> returns integer randomDigit := (10).rand;
// Return the strings in a sequence, separated by colons.
action <sequence<string>> returns string j := ":".join;
You cannot route, enqueue, emit or send an event that contains an action
variable field. It is okay to route, enqueue, emit or send an event that contains an action definition.
An action
variable cannot be a key in a dictionary. An event that contains an action
field cannot be a key in a dictionary.
In contrast to the regular actions that are described in Defining actions, static actions do not apply to specific instances of an event. They are not as powerful as regular actions, but they are helpful in situations where it makes more sense to use an action that is related to the event type, and where the action is not called on an instance of that event.
Static actions can only be declared inside an event type. They are defined in just the same way as regular actions (see Format for defining actions). The only differences are that they start with static
, cannot reference self
, and cannot reference members of the event. For example:
static action staticActionName() {
// do something
}
The following example contrasts a regular action with a static action.
event MyEventType {
integer i;
action someAction() {
print i.toString(); // Valid
}
static action someStaticAction() {
print i.toString(); // Not valid
someAction(); // Not valid
}
}
MyEventType e := new MyEventType;
e.someAction();
MyEventType.someStaticAction();
A static action can be used, for example, if you have a factory action which constructs a new instance of a particular event type and initializes its members to values that make sense for that type. Although it is possible to have such code in any place, in terms of program readability, it is more helpful to “associate” the static action with the event type that it is creating. For example:
event MyEventType {
string s;
integer i;
static action initialise() returns MyEventType {
MyEventType ret := new MyEventType;
ret.s := "Default";
ret.i := 100;
return ret;
}
...
}
MyEventType e := MyEventType.initialise();
With the above definition, the static action can then be called using a single line of code anywhere in your program.
In the correlator, the current time is the time indicated by the most recent clock tick. However, there are some exceptions to this:
-Xclock
option when you start the correlator, the correlator does not generate clock ticks. Instead, you must send time events (&TIME
) to the correlator. The current time is the time indicated by the most recent received, externally generated, time event. See Externally generating events that keep time (&TIME events).The information in the remainder of this topic assumes that the current time is the time indicated by the most recent clock tick.
Use the currentTime
variable to obtain the current time, which is represented as seconds since the epoch, January 1st, 1970 in UTC. The currentTime
variable is similar to a global read-only constant of type float
. However, the value of the currentTime
variable is always changing to reflect the correlator’s current time.
In the correlator, the current time is never the same as the current system time. In most circumstances it is a few milliseconds behind the system time. This difference increases when the input queues of public contexts grow.
When a listener executes an action, it executes the entire action before the correlator starts to process another event. Consequently, while the listener is executing an action, time and the value of the currentTime
variable do not change. Consider the following code snippet,
float a;
action checkTime() {
a := currentTime;
}
//... Lots of additional code
// A listener calls the following action some time later
action logTime() {
log a.toString(); // The time when checkTime was called
log currentTime.toString(); // The time now
}
In this code, an event listener sets float
variable a
to the value of currentTime
, which is the time indicated by the most recent clock tick. Some time later, a different event listener logs the value of a
and the value of currentTime
. The values logged might not be the same. This is because the first use of currentTime
might return a value that is different from the second use of currentTime
. If the two event listeners have processed the same event, the logged values are the same. If the two event listeners have processed different events, the logged values are different.
As discussed previously, actions can perform calculations and log messages. In addition, actions can dynamically generate events. The topics below discuss this.
The route
statement generates a new event that goes to the front of the input queue of the current context.
Any active listeners seeking that event then receive it. There is only one difference between an externally sourced event (passed in through a live message feed) and an event that was generated internally through a route
statement. The difference is that internally routed events are placed at the front of the context’s input queue in the same order as they are routed within an action, and after any previously internally routed events where multiple listener actions have been triggered by an event. The correlator processes the routed events on the input queue before it processes the next non-routed event on the input queue. See Event processing order for monitors.
For example:
action simulateCrash() {
route StockTick(currentStock.name, 50.0);
route StockTick(currentStock.name, 30.0);
route StockTick(currentStock.name, 20.0);
route StockTick(currentStock.name, 10.0);
route StockTick(currentStock.name, 5.0);
route StockTick(currentStock.name, 1.0);
}
The simulateCrash()
action shown above routes six StockTick
events for the monitor’s specific stock name, with drastically reducing prices. Other monitors (or the same monitor) may receive these events and process them accordingly.
The route
statement can operate on any
values as well as events, provided that the any
value is of a routable event type.
You cannot route an event if the event (or one of its members) contains a field of an unroutable type (action
, chunk
, listener
, stream
). There is a runtime check if the event (or one of its members) can contain an any
field; an exception is thrown if the any
field contains an object of type action
, chunk
, listener
, or stream
.
Note that you can route an event whose type is defined in a monitor.
The send
statement sends an event to a channel, a context, a sequence of contexts, or a com.apama.Channel
object.
When you send an event to a channel, the correlator delivers it to all contexts and external receivers that are subscribed to that channel. To send an event, use the following format:
send event_expression to expression;
The result type of event_expression must be an event. It cannot be a string representation of an event. The send
statement can operate on any
values as well as events, provided that the any
value is of a routable event type.
To send an event to a channel, the expression must resolve to a string or a com.apama.Channel
object that contains a string. If there are no contexts and no external receivers that are subscribed to the specified channel, then the event is discarded. See Subscribing to channels.
The only exception to this is the default channel, which is the empty string. Events sent to the default channel go to all public contexts.
To send an event to a context, the expression must resolve to a context, a sequence of contexts, or a com.apama.Channel
object that contains a context. You must create a context before you send an event to the context. You cannot send an event to a context that you have declared but not created. For example, the following code causes the correlator to terminate the monitor instance:
monitor m {
context c;
action onload()
{
send A() to c;
}
}
If you send an event to a sequence of contexts and one of the contexts has not been created first, then the correlator terminates the monitor instance. Sending an event to a sequence of contexts is non-deterministic. You cannot send an event to a sequence of com.apama.Channel
objects. For details, see Sending an event to a sequence of contexts.
All routable event types can be sent to contexts, including event types defined in monitors. There is a runtime check if the event (or one of its members) can contain an any
field; an exception is thrown if the any
field contains an object of type action
, chunk
, listener
, or stream
.
If a correlator is configured to connect to Universal Messaging, then a channel might have a corresponding Universal Messaging channel. If there is a corresponding Universal Messaging channel, then Universal Messaging is used to send the event to that Universal Messaging channel.
See Choosing when to use Universal Messaging channels and when to use Apama channels.
A com.apama.Channel
object is particularly useful when writing services that can be used in both distributed and local systems. For example, by using a Channel
object to represent the source of a request, you could write a service monitor so that the same code sends a response to a service request. You would not need to have code for sending responses to channels and separate code for sending responses to contexts.
Consider the following Request
event and Service
monitor definitions:
event Request {
...
Channel source;
}
monitor Service {
action onload() {
monitor.subscribe("Requests");
on all Request() as req {
Response rep := Response(...);
send rep to req.source;
}
}
}
EPL code in a context in the same correlator as the Service
monitor could send a Request
event with the source
field set to context.current()
and would receive the Response
event that the Service
monitor sends. For example:
monitor LocalRequester {
action onload() {
Request req := Request(...);
req.source := Channel(context.current());
send req to "Requests";
on all Response() as rep {
...
}
}
}
Now consider a monitor that is in a correlator that is connected to the Service
monitor host correlator. For example, the correlators can be connected by means of engine_connect
. The remote monitor could send a Request
event with the source
field set to a Channel
object that contains the name of a channel that the remote monitor is subscribed to. For example:
monitor RemoteRequester {
action onload() {
monitor.subscribe("Responses");
Request req := new Request;
req.source := Channel("Responses");
send req to "Requests";
on all Response() as rep {
//...
}
}
}
In this example, if the correlators are connected by means of engine_connect
then the connections would need to be subscribed to the Requests
channel and the Responses
channel. As you can see, the service monitor does not require different code according to whether the request is coming from a local or remote context. The service monitor simply sends the response back to the source and it does not matter whether the source is a context or a channel.
You can send a Channel
object from one Apama component to another Apama component only when the Channel
object contains a string. You cannot send a Channel
object outside a correlator when it contains a context.
To enqueue an event to a particular context, use the enqueue...to
statement:
enqueue event_expression to context_expression;
enqueue...to
statement is superseded by the send...to
statement. The enqueue...to
statement will be deprecated in a future release. Use the send...to
statement instead. See Generating events with the send statement.The result type of event_expression must be an event. It cannot be a string representation of an event. The result type of context_expression must be a context or a variable of type context
. It cannot be a com.apama.Channel
object that contains a context. The enqueue...to
statement can operate on any
values as well as events, provided that the any
value is of a routable event type.
The enqueue...to
statement sends the event to the context’s input queue. Even if you have a single context, a call to enqueue x to context.current()
is meaningful and useful.
You must create the context before you enqueue an event to the context. You cannot enqueue an event to a context that you have declared but not created. For example, the following code causes the correlator to terminate the monitor instance:
monitor m {
context c;
action onload()
{
enqueue A() to c;
}
}
If you enqueue an event to a sequence of contexts and one of the contexts has not been created first then the correlator terminates the monitor instance. For details, see Sending an event to a particular context.
Sending an event to a sequence of contexts is non-deterministic.
All routable event types can be enqueued to contexts, including event types defined in monitors. There is a runtime check if the event (or one of its members) can contain an any
field; an exception is thrown if the any
field contains an object of type action
, chunk
, listener
, or stream
.
The emit
statement dispatches events to external registered event receivers, which means that the events leave the correlator. Active listeners do not receive emitted events.
emit
statement is superseded by the send
statement. See Generating events with the send statement. The emit
statement will be deprecated in a future release. Use send
rather than emit
.There are two formats available for using emit
. You can directly emit an event, as the example below does first, or else place the event in a string and emit that. If you use this latter format, you must ensure that you define the string to represent a valid event. The correlator does not check whether the string you specify represents an event that is compliant with any event type that has been injected. In fact, you can use this mechanism to emit an event of a type that has not been defined in EPL anywhere else.
For example, consider a revised version of an earlier example. The result, instead of being printed as a message on the screen, is now being sent out as an event message:
event StockTickPriceChange {
string owner;
string name;
float price;
}
// A new processTicks action that dispatches an output event
// to external applications instead of logging
action processTicks() {
// The following emit format sends the event itself.
emit StockTickPriceChange(currentStock.owner,
newTick.name, newTick.price) to
"com.apamax.pricechanges";
// Or, use the following emit format, which sends a string that
// contains the event.
emit "StockTickPriceChange(\""+currentStock.owner+
"\",\""+newTick.name+"\", "+newTick.price.toString()+")" to
"com.apamax.pricechanges";
Events are emitted onto named channels. In the above code the StockTickPriceChange
event is being published on the com.apamax.pricechanges
channel. For an application to receive events from Apama it must register itself as an event receiver and subscribe to one or more channels. Then if events are emitted to those channels they will be forwarded to it.
Channels effectively allow both point-to-point message delivery as well as through publish-subscribe. As in the above example, channels can be set up to represent topics. External applications can then subscribe to event messages of the relevant topics. Otherwise a channel can be set up purely to indicate a destination and have only one application connected to it.
The emit
statement can operate on any
values as well as events, provided that the any
value is of a routable event type.
You cannot emit the following events:
any
field; an exception is thrown if the any
field contains an object of type action
, chunk
, listener
, or stream
.If a correlator is configured to connect to Universal Messaging, then a channel might have a corresponding Universal Messaging channel. If there is a corresponding Universal Messaging channel, then Universal Messaging is used to emit the event to that Universal Messaging channel.
See Choosing when to use Universal Messaging channels and when to use Apama channels.
EPL supports an any
type that can hold a value of a concrete EPL type (that is, a type other than the any
type). See the API reference for EPL (ApamaDoc) for full details of the any
type.
The switch
statement is the preferred way of handling any
values unless the type is known or not important. See Handling any values of different types with the switch statement for details on the switch
statement.
An any
value may be empty and not contain a value, or it can contain a value which has a type associated with it. The type of the value can be obtained using the getTypeName()
method on the any
type.
A variable of a concrete type can be used where an any
value is expected in:
assignments and initialization, for example:
any anyVariable := "string value";
return values, for example:
action a() returns any { return new sequence<integer>; }
passing a parameter to an action, for example:
actionWithAnyParameter("string value");
the index of a dictionary
with an any
key type.
In these cases, the concrete type is automatically converted to the any
type. This is always safe and valid, and will not throw an exception.
Reflection allows EPL to act on values of any type in a generic way, including altering the behavior to adapt to what fields or actions a type has. This can be values passed as an any
parameter value to some common code, matching an any()
listener (see Listening for events of all types), or created via the any.newInstance
method.
Fields or entries from an any
value can be accessed using the following methods:
For event types, the key
should be a string containing the field name. For sequences, key
is the index and should have an integer value.
The actions (including methods) and constants can be obtained with the following methods of the any
type:
Actions may be cast to the correct action type and then invoked directly.
For actions, a list of the action’s parameter names, a dictionary mapping from the parameter name to the parameter type, and the name of the return type can be obtained with the following methods of the any
type:
getActionParameterNames() returns sequence<string>
getActionParameters() returns dictionary<string,string>
getActionReturnTypeName() returns string
For actions, it is also possible to use a “generic” form to call the action via the following method of the any
type, even if the signature type is not known at compile time:
getGenericAction() returns action<sequence<any>> returns any
A sequence<any>
of the parameter values of the correct count and types must be supplied.
For detailed information on the above methods, see the any
type in the API reference for EPL (ApamaDoc).
EPL supports casting of the any
type to a concrete target type and vice versa.
Casting to the any
type
Casting a concrete type is allowed to any
only. The cast is redundant in this case. Example:
integer i := 10;
any a := <any> i; //redundant cast
any a2 := i; //valid
Example of a cast that is not redundant:
sequence<any> entries := (<any> evt).getEntries();
Casting to a concrete type
targetType tgtValue := <targetType> anyValue;
Casting an any
type with an empty value throws Exception
with type set to CastException
. See also the Exception
type in the API reference for EPL (ApamaDoc).
If the anyValue
does not contain an object of targetType
, it throws Exception
with type set to CastException
, and with the message mentioning the actual type contained by anyValue
and targetType
. Exceptions to this rule are the following casts, which are valid:
any(integer)
to float
any(integer)
to decimal
any(float)
to decimal
any(decimal)
to float
Examples:any a := 10;
integer i := <integer> a; // Valid
// Will inject but throws a CastException during runtime.
string s := <string> a;
Casting to the optional
type
optional<targetType> opt := <optional<targetType>> anyValue;
Casting to optional<targetType>
will never throw. If the any
value cannot be converted, then an empty optional<targetType>
is returned instead.
If the anyValue
is empty, the cast returns an empty optional<targetType>
.
If anyValue
contains object of optional<targetType>
type, the cast returns that object of type optional<targetType>
.
If the anyValue
contains an object of targetType
, the cast returns object of type optional<targetType>
containing targetType
.
If the anyValue
does not contain an object of targetType
or optional<targetType>
, the cast returns an empty optional<targetType>
.
Examples:
any a := 10;
// Returns optional <integer> containing the value 10.
optional<integer> opti := <optional<integer>> a;
// Returns an empty optional <string>.
optional<string> opts := <optional<string>> a;
The switch
statement is used to conditionally execute a block of code. Unlike the if
and if... else
statements, the switch
statement can have a number of possible execution paths.
The switch
statement operates on an expression of the any
type (see also Handling the any type). At runtime, the type of the value is examined, and the case
clause for that type is executed if there is one, otherwise the default
clause is executed. If the any
value is empty, the default
clause is always executed.
If the default
clause is not present and none of the case
clauses match the type of the value passed to the switch
statement, then the switch
statement throws Exception
with type set to UnmatchedTypeException
. See also the description of the Exception
type in the API reference for EPL (ApamaDoc).
The switch
statement names the expression as an identifier with the as
keyword followed by an identifier to name the value. In each case
clause block, the identifier has the same type as the case
clause.
If the expression is a simple identifier (that is, it is referring to a variable or parameter), then the as Identifier
part can be omitted. The new local retains the same name.
The following code example shows the usage of the switch
statement in an action which returns a string, where the case
clauses use return
statements to return from the action. The identifier value in each clause has the same type as the case
clause.
Example:
action getStringOrBlank(any value) returns string {
switch(value) {
// value will be of type float in this block
case float : { return "float "+ value.toString(); }
// value will be of type string in this block
case string: { return value; }
// value will be of type decimal in this block
case decimal: { return "decimal "+value.toString(); }
// value will be of type integer in this block
case integer: { return "integer: "+ value.toString(); }
// value will be of type sequence<string> in this block
case sequence<string> : { return " ".join(value); }
// value will be of any type in this block
default: { return ""; }
}
}
See also The switch statement.
Valid examples of an assignment statement are:
integerVariable := 5;
floatVariable := 6.0;
stringVariable := "ACME";
stringVariable2 := stringVariable;
Assignments are only valid if the type of the literal or variable on the right hand side corresponds to the type of the variable on the left hand side, or can be implicitly converted. Implicit conversions are allowed when assigning to an any
type, or to an optional
(provided the contained type of the optional
matches the value being assigned).
When doing an assignment from a variable to another variable, the behavior of EPL depends on the type of the variable.
any
type, setting to a primitive type creates a new object automatically to hold the primitive value. This is transparent to EPL, but is significantly more expensive than a simple primitive to primitive assignment.EPL supports conditional if
and if... else
statements.
An if
statement is followed by a boolean expression followed by an optional then
keyword followed by a block. A block consists of one or more statements enclosed in curly braces, { }
. If the boolean expression is true
, the contents of the block are executed.
The boolean expression must evaluate to the boolean values true
or false
.
The if
statement can be optionally followed by an else
keyword and a second block. This second block is executed if the boolean expression is false
. Instead of the else
block, a single if
statement, not enclosed in braces, may be used.
EPL example:
if floatVariable > 5.0 {
integerVariable := 1;
} else if floatVariable < -5.0 {
integerVariable := -1;
} else {
integerVariable := 0;
}
The ifpresent
statement is used to check if one or more values are empty (that is, whether they have a value or not). It unpacks the values into new local variables and conditionally executes a block of code.
The ifpresent
statement is followed by one or more expressions with an optional name of a target variable followed by a block of code. If each expression is not empty, then the value of the expression is placed in a new local variable whose name is supplied after the keyword as
. If the expression is a simple identifier (that is, it is referring to a variable or parameter), then the as identifier
part can be omitted; a new local variable (which shadows the original variable) is created with the same name. Multiple expressions can be used in a single ifpresent
statement, separated by commas.
If all the expressions have non-empty values, then the first block is executed. A block consists of one or more statements enclosed in curly braces, { }
. The new local variables are only available in the first block of the ifpresent
statement, where they are guaranteed to have non-empty values.
ifpresent
can be optionally followed by an else
keyword and block, which is executed if any of the supplied expressions do not have a value.
For optional
types, the new local variable is of the unpacked type (the contained type of the optional), for example:
optional<integer> possibleNumber := 42;
ifpresent possibleNumber {
// in this block, possibleNumber is of type integer,
// so we can perform arithmetic on it:
nextNumber := possibleNumber + 1;
} else {
// in practice, won't be executed as possibleNumber
// has been initialized with a value.
}
Thus, ifpresent
is the recommended way of handling optional variable types. Usually, there is no need to call the getOrThrow
method of the optional
type. ifpresent
combines the check of whether the value is empty with extracting the value and control flow, and thus reduces the amount of code that could throw an exception.
As an alternative to the ifpresent
statement, you can use the getOr
method of the optional
type, which is less verbose than using ifpresent
. This is helpful if you want to treat a missing (empty) value as having a value. For example, possibleNumber.getOr(0)
will give you the number in possibleNumber
, or 0
if it is empty.
ifpresent
operates on expressions of the following types:
optional
chunk
stream
listener
context
action
any
See the API reference for EPL (ApamaDoc) for more information on these types.
See also The ifpresent statement.
EPL supports two loop structures, while
and for
.
An EPL example for while
is:
integerVariable := 20;
while integerVariable > 10 {
integerVariable := integerVariable – 1;
on StockTick("ACME", integerVariable) doAction();
}
The for
looping structure allows looping over the contents of a sequence
. The counter must be an assignable variable of the same type as the type of elements of the sequence
. For example:
sequence<integer> s;
integer i;
s.append(0);
s.append(1);
s.append(2);
s.append(3);
for i in s {
print i.toString();
}
The loop will iterate through all the indices in the sequence
, checking whether there are any more indices to cover each time. In the example above, i
will be set to s[0]
, then s[1]
, and so on up to s[3]
. The counter continues incrementing by one each time, and is checked to verify whether it is less than s.size()
before a further iteration is carried out. Looping only terminates when the next index would be beyond the last element of the sequence
, or equal to size()
(since indices are counted from 0).
When the correlator executes a for
loop, it operates on a reference to the sequence
. Consequently, if the code in the for
loop assigns some other sequence
to the sequence
expression specified in the for
statement this has no effect on the iteration. However, if the code in the for
loop changes the contents of the sequence
specified in the for
statement, this can affect the iteration. For example:
sequence <string> tmp := ["X", "Y", "Z"];
sequence <string> seq := ["A", "B", "C", "D", "E"];
string s;
for s in seq {
seq := tmp;
print s;
}
The for
loop steps through whatever seq
referred to when the loop began. Therefore, assigning tmp
to seq
inside the loop does not affect the behavior of the loop. This code prints A
, B
, C
, D
, and E
on separate lines.
In the following example, the code in the for
loop changes the contents of the sequence
specified in the for
statement and this affects the behavior of the loop.
sequence<string> seq := ["A", "B", "C", "D", "E"];
string s;
for s in seq {
seq[2] := "c";
print s;
}
This code prints A
, B
, c
, D
, and E
on separate lines.
In the following code, the changes to the contents of the specified sequence
would prevent the for
loop from terminating.
sequence<string> seq := ["x"];
string s;
for s in seq {
seq.append(s);
}
EPL provides the following statements for manipulating while
and for
loops. Usage is intuitive and as per other programming language conventions:
break
exits the innermost loop. You can use a break
statement only inside a loop.continue
moves to the next iteration of the innermost loop. You can use a continue
statement only inside a loop.return
terminates both the loop and the action that contains it.EPL supports the try... catch
exception handling structure. The statements in each block must be enclosed in curly braces. For example:
using com.apama.exceptions.Exception;
...
action getExchangeRate(
dictionary<string, string> prices, string fxPair) returns float {
try {
return float.parse(prices[fxPair]);
} catch(Exception e) {
return 1.0;
}
}
Exceptions are a mechanism for handling runtime errors. Exceptions can be caused by any of the following, though this is not an exhaustive list:
integer
by zero, or trying to access a non-existent entry in a dictionary
or sequence
ondie()
or onunload()
action, or sending an event to a context and specifying a variable that has not been assigned a valid context objectthrow
statement. See The throw statement for more information.An exception that occurs in try block1 causes execution of catch block2. An exception in try block1 can be caused by:
Note that the die
statement always terminates the monitor, regardless of try... catch
statements.
The variable specified in the catch
clause must be of the type com.apama.exceptions.Exception
. Typically, you specify using com.apama.exceptions.Exception
to simplify specification of exception variables in your code. The Exception
variable describes the exception that occurred.
The com.apama.exceptions
namespace also contains the StackTraceElement
built-in type. The Exception
and StackTraceElement
types are always available; you do not need to inject them and you cannot delete them with the engine_delete
utility.
An Exception
type has methods for accessing:
ArithmeticException
and ParseException
. For a list of exception types, see the description of the Exception
type in the API reference for EPL (ApamaDoc).sequence
of StackTraceElement
objects that describe where the exception was thrown. The first StackTraceElement
points to the place in the code that immediately caused the exception, for example, an attempt to divide by zero or access a dictionary key that does not exist. The second StackTraceElement
points to the place in the code that called the action that contains the immediate cause. The third StackTraceElement
element points to the code that called that action, and so on. Each StackTraceElement
object has methods for accessing:
Information in an Exception
object is available by calling these built-in methods:
Exception.getMessage()
Exception.getType()
Exception.getStackTrace()
StackTraceElement.getFilename()
StackTraceElement.getLineNumber()
StackTraceElement.getActionName()
StackTraceElement.getTypeName()
In the catch
block, you can specify corrective steps, such as returning a default value or logging an error. By default, execution continues after the catch
block. However, you can specify the catch
block so that it returns, dies or causes an exception.
You can nest try... catch
statements in a single action. For example:
action NestedTryCatch() {
try {
print "outer";
try {
print "inner";
integer i:=0/0;
} catch(Exception e) {
// inner catch
}
} catch(Exception e) {
// outer catch
}
}
The block in a try
clause can specify multiple actions and each one can contain a try... catch
statement or nested try... catch
statements. An exception is caught by the innermost enclosing try... catch
statement, either in the action where the exception occurs, or walking up the call stack. If an exception occurs and there is no enclosing try... catch
statement then the correlator logs the stack trace of the exception. If the throw is from an onload()
or spawned action, from within a stream query or there is an ondie()
defined in the monitor, then the monitor instance is terminated.
See About executing ondie() actions for information about how ondie()
can optionally receive exception information if an instance dies due to an uncaught exception.
The following operations are provided for debugging and textual output:
The print
statement outputs its text to standard output, which is normally the active display or some file where such output has been piped. See also Strings in print and log statements.
The log
statement sends the specified string to a particular log file depending on the applicable log level. For details, see Setting EPL log files and log levels dynamically.
The topics below provide information for using the log
statement.
The format of a log
statement is as follows:
log string [at identifier]
Syntax Element | Description |
---|---|
string | Specify an expression that evaluates to a string. |
identifier | Optionally, specify the desired log level. Specify one of the following values: CRIT , FATAL , ERROR , WARN , INFO , DEBUG or TRACE . If you do not specify an identifier, the default is INFO . |
It is recommended that you do not use the FATAL
or CRIT
log levels, which are present only for historical reasons. It is better to use ERROR
for all error conditions regardless of how fatal they are, and INFO
for informational messages.
For each encountered log
statement, the correlator compares the specified identifier with the applicable log level to determine whether to send the specified string to a log file. If the string is to be sent to a log file, the correlator determines the appropriate log file to send it to.
The correlator uses the tree structure of EPL code to identify the applicable log level and the appropriate log file. See Setting EPL log files and log levels dynamically.
The correlator supports the following log levels:
|
|
No entries go to log files. |
|
|
Least amount of entries go to log files. |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
Greatest amount of entries go to log files. |
Suppose that a string expression in a log
statement executes an action or has side effects. In this situation, the correlator executes the log
statement so that side effects always take place. However, if the log level in effect is lower than the log level in the log
statement the correlator still does not send the string to the log file.
Here are some examples where the log level in effect is WARN
:
log "foo bar" at CRIT; // Sends "foo bar" to the log file.
log "foo bar" at INFO; // Does not send anything to the log file.
log "foo" + "bar" + 12345.toString() at INFO;
// Does not send anything to the log file.
// The expression in the log statement is not evaluated as
// the log level is too low to send output to the log file,
// and the expression does not have side effects.
log "foo" + bar() + 12345.toString() at INFO;
// Does not send anything to the log file.
// Calls bar() since that action might have side effects,
// for example, the action could send an event.
Actions on events or monitors are assumed to have side effects. The com.apama.epl.SideEffectFree
annotation (see Adding predefined annotations) can be added to an action definition to mark it as side effect free. Note that with this annotation, actions will only be called from log statements if the log statement would write to the log file. This is more compact than checking the log level before executing the log statement. If the action does in fact have side effects, then changing the log level can change the behavior of your program. It is recommended to only add the SideEffectFree
annotation on an action if a profile shows that a lot of time is spent in calling that action (premature optimizations add to program complexity for no benefit). Actions called via an action variable are always assumed to have side effects, as the EPL runtime does not know which action is invoked.
For more information on the profile, see Profiling EPL Applications.
To determine the log level in effect, the correlator checks whether you set a log level for the following in the order specified below:
log
statement.log
statement. The correlator starts with the immediate parent and works its way up the tree as needed.The log level in effect is the first log level that the correlator finds in the tree structure. See Setting EPL log files and log levels dynamically. If the correlator does not find a log level, the correlator uses the correlator’s log level. If you did not explicitly set the correlator’s log level, the default is INFO
.
After the correlator identifies the applicable log level, the log level itself determines whether the correlator sends the log
statement output to the appropriate log file as follows:
Log level in effect | For log statements with these identifiers, the correlator sends the log statement output to the appropriate log file | For log statements with these identifiers, the correlator ignores log statement output |
---|---|---|
OFF |
None | CRIT, FATAL, ERROR, WARN, INFO, DEBUG, TRACE |
CRIT |
CRIT |
FATAL, ERROR, WARN, INFO, DEBUG, TRACE |
FATAL |
CRIT, FATAL |
ERROR, WARN, INFO, DEBUG, TRACE |
ERROR |
CRIT, FATAL, ERROR |
WARN, INFO, DEBUG, TRACE |
WARN |
CRIT, FATAL, ERROR, WARN |
INFO, DEBUG, TRACE |
INFO |
CRIT, FATAL, ERROR, WARN, INFO |
DEBUG, TRACE |
DEBUG |
CRIT, FATAL, ERROR, WARN, INFO, DEBUG |
TRACE |
TRACE |
CRIT, FATAL, ERROR, WARN, INFO, DEBUG, TRACE |
None |
An advantage of this framework is that there is no performance penalty for having log
statements that do not specify actions in your application. You control the overhead of executing such log
statements by specifying the appropriate log level.
When the correlator needs to send the log
statement output to a log file, the correlator checks whether you set a log file for the following in the order specified below:
log
statement.log
statement. The correlator starts with the immediate parent and works its way up the tree as needed.The log file that receives the log
statement output is the first log file that the correlator finds. If the correlator does not find a log file, the default is that the correlator sends the string and identifier to stdout
.
Suppose you insert DEBUG log
statements without actions in a monitor. You specify ERROR
as the log level for that monitor. The correlator ignores log
statement output of log
statements with identifiers of INFO
or DEBUG
. But then there are some problems. You use the engine_management
correlator utility to change the log level to DEBUG
. Now the correlator sends output from all log
statements to the appropriate log file.
Following is another example:
log "Log statement number " + logNo() at DEBUG;
action logNo() {
logNumber := logNumber + 1;
return logNumber.toString();
}
In this example, the correlator always executes the log
statement because it calls an action. However, the log level in effect must be DEBUG
for the correlator to send the string to the log file. If the log level is anything else, the correlator discards the string because the log level in effect is lower than the log level in the log
statement.
In both print
and log
statements, the string can be any one of the following:
Literal, for example: print "Hello";
Variable, for example:
string welcomeMessage;
...
log welcomeMessage;
Combination of both, for example:
string welcomeMessage;
...
print "Hello " + welcomeMessage + " Bye";
Internally, the correlator encodes all textual information as UTF-8. When the correlator outputs a string to a console or stdout
because of a print
statement, or sends a string to the log, the correlator translates the string from UTF-8 to the current machine’s (where the correlator is running) local character set. However, if you redirect stdout
to a file, the correlator does not translate to the local character set. This ensures that the correlator preserves as much information as possible.
This section describes a complete financial example, using the monitor techniques discussed earlier in this chapter.
This example enables users to register interest, for notification, when a given stock changes in price (positive and negative) by a specified percentage.
Users register their interest by generating an event, here termed Limit
, of the following format:
Limit(userID, stockName, percentageChange)
For example:
Limit(1, "ACME", 5.0)
This specifies that a user (with the user ID 1
) wants to be notified if ACME
’s stock price changes by 5%. Any number of users can register their interests, many users can monitor the same stock (with different price change range), and a single user can monitor many stocks.
In EPL, the complete application is defined as:
event StockTick {
string name;
float price;
}
event Limit {
integer userID;
string name;
float limit;
}
monitor SharePriceTracking {
// store the user's specified attributes
Limit limit;
// store the initial price (this may be the opening price)
StockTick initialPrice;
// store the latest price – to give to the user
StockTick latestPrice;
// when a limit event is received spawn; creating a new
// monitor instance for each user's request
action onload() {
on all Limit(*,*,>0.0):limit spawn setupNewLimitMonitor();
}
// If an identical request from a user is discovered
// stop this monitor and die
// If a StockTick event is received for the stock the
// user specified, store the price and call setPrice
action setupNewLimitMonitor() {
on Limit(limit.userID, limit.name, *) die;
on StockTick(limit.name, *):initialPrice setPrice();
}
// Search for StockTick events of the specified stock name
// whose price is both greater and less than the value
// specified – also converting the value to percentile format
action setPrice() {
on StockTick(limit.name, > initialPrice.price * (1.0 +
(limit.limit/100.0))):latestPrice notifyUser();
on StockTick(limit.name, < initialPrice.price * (1.0 -
(limit.limit/100.0))):latestPrice notifyUser();
}
// display results to user
action notifyUser() {
log "Limit alert. User=" +
limit.userID.toString() +
" Stock=" + limit.name +
" Last Price=" + latestPrice.price.toString() +
" Limit=" + limit.limit.toString();
die;
}
}
The important elements of this example lie in the life-cycle of different monitor states. Firstly a monitor instance is spawned on every incoming Limit
event where the limit is greater than zero. Within setupNewLimitMonitor
, the first on
statement listens for other Limit
events from the same user, upon detection of which the monitor instance is killed. This effectively ensures that there is a unique monitor instance per user per stock. This scheme also allows a user to send in a Limit
event with a zero limit to indicate that they actually no longer want to monitor a particular stock. While this will not be caught by the original monitor instance’s event listener and will not cause spawning, it will trigger the event listener in the monitor instance of that user for that stock and cause it to die.
Then a single on
statement (without anall
) sets up an event listener to look for all StockTick
events for that stock type for that user. Once a relevant StockTick
is detected, new event listeners start seeking a specific price difference for that user. If such a price change is detected it is logged. Note that the log
statement exploits data from variables used before and after the spawn
statement (that is, limit
and latestPrice
, respectively).
This example also demonstrates how mathematical operations may be used within event expressions. Here, two on
statements create event listeners that look for StockTicks
with prices above and below the calculated price. The calculated price in this case is based on the initial price multiplied by the percentage specified by the user. The first event listener is looking for an increase in the share price to 105% of its original value, while the second is looking for a decrease to 95% of its original value.