Thursday 22 April 2010

Configuring Apache JMeter specifically for Oracle's ADF 11g

Sometime back I blogged about Stress & load testing web applications (even ADF & Apex) using Apache JMeter. That post dealt with the generic setup of recording a web session and then replaying under load via JMeter.

When considering using JMeter for JDeveloper 11g ADF applications there are additional JMeter configuration steps required beyond that detailed in the original post. As per the original post ADF passes around cookies to store the JSESSIONID, and you can use JMeter's HTTP Cookie Manager to correctly handle the cookie. However ADF also makes use of a number of parameters passed as HTTP POST parameters and URL parameters. On running an ADF application you'll often see URL parameters like _adf.ctrl-state, _afrLoop, and embedded within the HTTP bodies variables such as javax.faces.ViewState, used by ADF to coordinate state with the browser session. In order for JMeter to simulate multiple user sessions it needs to be correctly configured to handle these too, capturing and carrying the unique values for each session in the load test.

The following post lists the additional configurations required for JMeter beyond those described in the original post. As noted in the original post the easiest way to setup your JMeter Test Plan is to record an existing ADF session through JMeter's HTTP Proxy Server and I haven't bothered to reiterate those instructions here. In turn I've taken no effort to explain the JMeter features we're utilizing to do this; if you want to learn about the relevant JMeter features consult the JMeter User Manual.

Caveat for ADF 10g users: You'll note above I specifically stated JDeveloper 11g ADF, not 10g. I haven't tested any of the following settings for JDev 10g and I don't believe they'll work, as if memory serves me correctly the POST and URL parameters mechanisms changed between versions. However the following notes may give you enough information to dig around in a running 10g ADF application, looking at the HTTP Requests to work out what parameters to work with.

JMeter ADF Required Configurations

The following lists 5 additional objects and their settings. You need only create one of each for each Thread Group. The end result will look something like this:


a) HTTP Cookie Manager

Create via Test Plan -> Thread Group -> Right Click -> Add -> Config Element -> HTTP Cookie Manager

Name:HTTP Cookie Manager
Clear cookies each iteration:Selected

b) User Defined Variables

Create via Test Plan -> Thread Group -> Right Click -> Add -> Config Element -> User Defined Variables

Name:ADF Variables
Variable Name:afrLoop
Variable Name:afrWindowId
Variable Name:jsessionId
Variable Name:adf.ctrl-state
Variable Name:javax.faces.ViewState

No initial values.
c) Regular Expression Extractor #1

Create via Test Plan -> Thread Group -> Right Click -> Add -> Post Processors -> Regular Expression Extractor

Name:adf.ctrl-state Extractor
Response Field to Check:Body
Reference Name:adf.ctrl-state
Regular Expression:_adf.ctrl-state=([-_0-9A-Za-z!]{10,13})
Template:$1$
Match No.:1
Default Value:n/a

Take notice that the variable name does not include an underscore in it's name unlike the actual parameter _adf.ctrl-state.

d) Regular Expression Extractor #2

Create via Test Plan -> Thread Group -> Right Click -> Add -> Post Processors -> Regular Expression Extractor

Name:afrLoop Extractor
Response Field to Check:Body
Reference Name:afrLoop
Regular Expression:_afrLoop=([-_0-9A-Za-z]{13,16})
Template:$1$
Match No.:1
Default Value:n/a

Take notice that the variable name does not include an underscore in it's name unlike the actual parameter _afrLoop.

d) Regular Expression Extractor #3

Create via Test Plan -> Thread Group -> Right Click -> Add -> Post Processors -> Regular Expression Extractor

Name:afrWindowId Extractor
Response Field to Check:Body
Reference Name:afrWindowId
Regular Expression:window.name='([-_0-9A-Za-z!]{10,13})'
Template:$1$
Match No.:1
Default Value:n/a

e) Regular Expression Extractor #4

Create via Test Plan -> Thread Group -> Right Click -> Add -> Post Processors -> Regular Expression Extractor

Name:javax.faces.ViewState Extractor
Response Field to Check:Body
Reference Name:javax.faces.ViewState
Regular Expression:<input type="hidden" name="javax\.faces\.ViewState" value="!(.+?)">
Template:$1$
Match No.:1
Default Value:n/a

f) Regular Expression Extractor #5

Create via Test Plan -> Thread Group -> Right Click -> Add -> Post Processors -> Regular Expression Extractor

Name:jsesssionId Extractor
Response Field to Check:Body
Reference Name:jsessionId
Regular Expression:;jsessionid=([-_0-9A-Za-z!]{63})
Template:$1$
Match No.:1
Default Value:n/a

Recorded HTTP Request Changes

After recording an ADF session in JMeter, and setting up the variables above, you'll note in the recorded HTTP Requests numerous references to the same named variables as HTTP parameters. Yet the recorded HTTP Requests include the parameters' values for the recorded session, which simply won't work when we replay the session as the values will have changed.

It is therefore necessary to identify all locations where the parameters exist in the recorded session HTTP Requests and substitute references to your variables.

a) HTTP Request – Path – any path that makes reference to jsessionId must be changed.

ie: /SageApp/faces/ViewEvents;jsessionId=${jsessionId}


b) HTTP Request – Path – ditto any path that makes reference to _adf.ctrl-state must be changed.

ie. /SageApp/faces/ViewEvents?_adf.ctrl-state=${adf.ctrl-state}


You can also change this in the name if you want but it wont make a different to the overall result, the name is for show only.

c) HTTP Request – Parameters – for any of the following parameters:

_adf.ctrl-state
_afrLoop
_afrWindowId (* see exception at the end of this step)

...for their values substitute the relevant variable. The following example shows the values for the afrLoop and adf.ctrl-state replaced:


However there is one exception with _afrWindowId. In one of the initial requests its value will be the literal string "null" (see the picture in step (a) for an example). Do not replace this, leave it as null.

d) HTTP Request – Parameters – for the "javax.faces.ViewState" parameter, ditto replace its values. Note it is necessary to put a not (negate) symbol in front of it:

ie. !${javax.faces.ViewState}

In addition by default the "Encode?" parameter will be on for each javax.faces.ViewState. Instead turn it off for each:


e) HTTP Request – Parameters – for the "unique" parameter, occurring in the Path and request parameters replace it with the following notation:

${__javaScript(new Date().getTime(),DUMMY)}

Note there is no JMeter user variable for this, we just write it straight into the unique value.

f) HTTP Header Manager Referer – if the HTTP session has been recorded via JMeter's HTTP Proxy, each HTTP Request will include a sub-element HTTP Header Manager that includes an element called Referer. You'll note it will have hardcoded many of the parameters discussed above. However I don't think ADF requires the Referer element and you can just ignore it or delete it from every HTTP Header Manager.

Assertions

Once you've completed the above setup and want to run your load test via JMeter, it is important to see that the test is working correctly. One way to do this is to create a View Results Tree (Test Plan -> Thread Group -> Right Click -> Add -> Listener -> View Results Tree) that allows you to inspect the response of each HTTP Request. If you then see HTTP Request responses in red this indicates a HTTP error code response (!= 200). However it's also necessary to inspect each response even if they are green to see if there's a valid response returned carrying an error message. There is definitely a small knack to differentiating between the different type of failures in an ADF application, which you'll become familiar with on some experience.

In order to save you having to look for the same problems for each test-run, you can create a Response Assertion (Test Plan -> Thread Group -> Add -> Assertions -> Response Assertion) to look for specific errors in the responses. From my own ADF experience you want to setup a Response Assertion with the following values:

Which Samples to Test:Main Sample Only
Response Field to Test:Text Response
Pattern Matching Rules:Contains
Not:Selected
Patterns to Test #1:The following exception occurred
Patterns to Test #2:Server Exception during PPR
Patterns to Test #3:ADFC-12000
Patterns to Test #4:java.net.SocketException
Patterns to Test #5:java.io.IOException

This isn't a definitive list, and in turn there may be specific errors that you may want to pick up for your application.

Sample JMeter file

Readers can download a sample JMeter JMX file for ADF here with the configurations already setup.

Finishing Comment

I haven't had the chance to test this in every ADF scenario or application. It's possible in your application, dependent on the components you're using or some other trick of ADF, that I've missed one of the state values it wants the browser to carry. With the post above hopefully you'll get an idea of the majority and be able to identify more. I'd appreciate it if anybody finds more that we need to cater for please post a comment with your findings to assist other readers.

Wednesday 7 April 2010

ADF BC 11g Groovy expressions implicit type conversions

Under JDeveloper 11g ADF Business Components introduced the support for Groovy expressions. Among other benefits this allows JDeveloper programmers to skip many trivial Java programming tasks with a shorthand Groovy expression. Grant Ronald's Introduction to Groovy Support in JDeveloper and Oracle ADF 11g provides a good compliment to the Fusion Guide on Groovy expressions.

The Groovy support includes the ability to reference attributes of Entity Objects and View Objects. For instance you may create a transient attribute TotalCost in an InvoiceLines Entity Object based on the Groovy expression "Cost * Quantity" referencing the same named Entity Object attributes:


As you become more familiar with the use of Groovy expressions you'll realize you can also call the functions of the underlying attribute type. For instance a String attribute FullName would support the Groovy expression "FullName.toLowerCase()".

However there is a catch. When calling functions of a specific attribute, say calling the oracle.jbo.domain.Number add() function on the Quantity Number attribute, at runtime you might discover a runtime error such as:

(oracle.jbo.JboException) JBO-29000: Unexpected exception caught: groovy.lang.MissingMethodException, msg=No signature of method: java.lang.Long.add() is applicable for argument types: (java.lang.Integer) values: [1]

This comes as a surprise as the Fusion Guide tells us the Number attribute type is backed by the oracle.jbo.domain.Number class supplied by Oracle.

What's happening is that the Groovy expression evaluator is undertaking some implicit datatype conversions/casts on our behalf with the underlying attributes. In order to make this easier to understand I've documented the type conversions for you. The following table gives you the Type Name as selected in the EO/VO attribute editor, the Java Type you'll see in the resulting EntityImpl and ViewRowImpl classes, and then the Groovy Type they'll be cast to during a Groovy expression evaluation.

You'll note with the Groovy Types sometimes they are in fact the oracle.jbo.domain class, but in others they're the parent class. And for some types there is no oracle.jbo.domain equivalent, just a java.lang type. It's all a bit inconsistent but hopefully the table gives you the appropriate type for your attribute:

Type NameJava TypeGroovy Type
Arrayoracle.jbo.domain.Array-
BFileDomainoracle.jbo.domain.BFileDomain-
BigDecimaljava.math.BigDecimaljava.math.BigDecimal
BlobDomainoracle.jbo.domain.BlobDomainoracle.jbo.domain.BlobDomain
Booleanjava.lang.Booleanjava.lang.Boolean
Bytejava.lang.Bytejava.lang.Byte
Charoracle.jbo.domain.Charjava.lang.String
Characterjava.lang.Characterjava.lang.Character
ClobDomainoracle.jbo.domain.ClobDomainoracle.jbo.domain.ClobDomain
DBSequenceoracle.jbo.domain.DBSequencejava.lang.Long
Dateoracle.jbo.domain.Datejava.sql.Date
Doublejava.lang.Doublejava.lang.Double
Floatjava.lang.Floatjava.lang.Float
Integerjava.lang.Integerjava.lang.Integer
Longjava.lang.Longjava.lang.Long
NClobDomainoracle.jbo.domain.NClobDomainoracle.jbo.domain.NClobDomain
Numberoracle.jbo.domain.Numberjava.lang.Long
Objectjava.lang.Objectjava.lang.Object
OrdAudioDomainoracle.ord.im.OrdAudioDomainoracle.ord.im.OrdAudioDomain
OrdDocDomainoracle.ord.im.OrdDomainoracle.ord.im.OrdDomain
OrdImageDomainoracle.ord.im.OrdImageDomainoracle.ord.im.OrdImageDomain
OrdImageSignatureDomainoracle.ord.im.OrdImageSignatureDomainoracle.ord.im.OrdImageSignatureDomain
OrdVideoDomainoracle.ord.im.OrdVideoDomainoracle.ord.im.OrdVideoDomain
REForacle.sql.REF-
Raworacle.jbo.domain.Raw-
RowIDoracle.jbo.domain.RowIDjava.lang.String
Shortjava.lang.Shortjava.lang.Short
Stringjava.lang.Stringjava.lang.String
Timestamporacle.jbo.domain.Timestampjava.sql.Timestamp
TimestampLTZoracle.jbo.domain.TimestampLTZjava.sql.Timestamp
TimestampTZoracle.jbo.domain.TimestampTZjava.sql.Timestamp

You'll note I've missed a few Groovy Types, I simply couldn't determine the equivalent type.

This post was specifically written against JDeveloper 11g 11.1.1.2.0 build 5536. It's quite possibly in the future Oracle will tweak the data types to make it more consistent so be careful to check my facts.