|
Understanding the ADF Business Components State Management Feature
Oracle JDeveloper Tip
Understanding the ADF Business Components State Management Feature Author: Michael Gantman, Oracle Worldwide
Support Contributions from John Smiljanic & Steve Muench, Oracle ADF
Development TeamDate: October 6, 2004
| NOTE: |
This article
contains advanced topics and it is assumed that reader has some experience and
basic understanding of ADF Business Components (ADF BC).
|
Contents Introduction Overview Review of Business Components Terminology Review of HTTP Client to ADF BC Middle Tier Interaction Review of State Management Release Level Setting the Release Level Limitations Implied By the State Management Feature Failover Feature Passivation Storage Options What Gets Passivated? Timeouts Managing Custom User Specific Info Managing Transient ViewObjects Partial Rollback on Middle Tier Configuration Parameters and Interesting Use Cases Testing ADF Business Components
Introduction
State management is a set of features in the
Oracle Application Development Framework (ADF) that preserves the state of
pending changes in your application over a period of time. This is a very
typical task for a large group of applications called stateful web
applications. Stateful web applications preserve logical application state
across several HTTP requests. Web applications that do not preserve this kind
of information are referred to as stateless applications. In stateless
applications that involve database transactions, a transaction gets started and
terminated by commit or rollback within the scope of one request. The reason
for this is the stateless nature of the HTTP protocol itself. If a web
application requires a transaction spanning multiple HTTP requests, it is the
application’s responsibility to preserve the pending changes to be made as part
of the transaction and to keep it consistent between HTTP requests. In this
article, when we say “pending changes” or “pending session state” we mean the
ADF entity objects that have been created, updated, or removed during the
current transaction but which haven't yet been permanently stored to the
database by a post or commit operation. The ADF Business Components features in
ADF let your easily create stateful applications out of the box, with
scalability nearing that of a purely stateless application. It is important for
you to understand what is happening behind the scenes in order to make the most
efficient use of this very productive feature.
Overview Review of Business Components Terminology
An ADF BC middle tier application consists of one or
more root Application Modules (AM). Each root AM correlates to a single
database transaction. Each AM may contain instances of View Objects (VOs)
and/or nested AMs (which always share the parent AM’s transaction). A VO’s
attributes may be based on Entity Object (EO) attributes, be populated from a
SQL-calculated expression (known as “SQL-derived”), or they can be calculated
in Java and not correlate to a database at all (known as “transient”). By
convention, if all attributes of a VO are transient then VO itself is called a
transient VO.
Review of HTTP Client to ADF BC Middle Tier Interaction
Oracle ADF implements a
MVC (Model – View – Controller) architecture. In this architecture the model is
an abstraction level that separates client from middle tier business services.
The client (known as the “View” in MVC) communicates with model layer through
the controller, and the model layer relays the requests from client to the
middle tier business services. The controller also relays the reply from the
middle tier business services back to the model layer to that the View layer
can display it to the client. In this way, the client layer is not dependent on
the implementation details of your business services. Figure 1
shows schematically what is explained above.
 Figure 1: Overview of MVC Architecture
It is important to understand the
interaction between an HTTP client (HttpSession) and the
model layer. The ADFBindingFilter in the
oracle.adf.model.servlet package is a servlet filter that
intercepts all incoming requests from Web-based clients. On the first request
the filter initializes the ADF data binding support classes. Among other
things, during this bootstrap step it creates the
BindingContext object and associates it with the
HttpSession. There is one BindingContext
object per HttpSession.
The BindingContext contains one or more Data Control
and Binding Container objects. As its name implies, the Binding Container
contains bindings, which are helper objects that abstract the details of
“wiring” UI controls to business service data. These bindings interact with
data provided and methods implemented by the Data Control in order to work with
the model layer data. Each Data Control contains one ADF BC
SessionCookie and uses it to checkout and release the
underlying ApplicationModule instance that implements the business service’s
functionality. That SessionCookie is used as an AM state
identifier. An HttpSession can contain references to
multiple Data Controls, but any given Data Control instance is always tied to
one HttpSession. That is, the relationship between
HttpSession and Data Controls is one to many. So in summary,
the BindingContext is related to the
HttpSession, and the Data Controls in the
BindingContext use a SessionCookie to
acquire and release an AM instance when they need to perform work. This
SessionCookie is implemented by the
oracle.jbo.http.HttpSessionCookieImpl class.
Now
that we have the structure in place, we may describe in short how it is
functioning. When the ADFBindingFilter receives an HTTP
request it fires beginRequest notifications on all Data Controls that are
associated with that request's HttpSession (via the
BindingContext). It then passes the request to the next
filter/servlet in the processing chain. Once the processing chain is finished
control is passed back to the ADFBindingFilter. The
ADFBindingFilter then fires endRequest notifications on all
of the Data Controls that are associated with the request's
HttpSession.
The ADF Business Components Data
Control responds to the beginRequest notification by using the
SessionCookie to acquire an ApplicationModule for the
request. The ApplicationModule is therefore "pinned" to the Data Control for
the duration of the request. The ADF BC Data Control responds to the endRequest
notification by using the SessionCookie to release the
request ApplicationModule. This article will discuss in depth the technologies
provided that enable that release while still managing the Data Control state.
Figure 2 illustrates the steps involved. This is of
course a brief very short and incomplete description of how model layer works,
but detailed discussion of this issue is out of scope of this document. We just
need to establish general understanding for the purposes of discussion of
several issues in this document later on.
 Figure 2: Flow of Control During HTTP RequestReview of State Management
The following steps describe
briefly how ADF uses state management to preserve pending changes made in the
current HttpSession across HTTP requests. The simplest case
is as follows:
- The Data Control (associated with
HttpSession) requests an application module instance, and is
given an unreferenced application module from a pool of
instances. By unreferenced, we mean an application module that is not currently
managing the pending state for any other user session)
- At the end of the request, the application module instance is checked
back into the pool. The instance is now referenced by the
Data Control that just used it. It still contains pending transaction state
made by the Data Control (that is, entity object and view object caches;
updates made but not committed; and cursor states), stored in memory. As we’ll
see below, it’s not dedicated to this Data Control, just referenced by
it.
- The same Data Control (identified by its
SessionCookie) makes a second request for an application
module instance, and the pool supplies the same application module instance,
with the state still saved in memory.
Sometimes application module instances need to be shared among different
HttpSessions, because a high number of users are accessing
the application simultaneously. In this case, the application pool must
recycle a currently referenced application module instance
for use by another session, as follows:
- A
Data Control 1(associated with
HttpSession 1) checks an AM
instance (instance 1) back into the application pool, and the application state
is saved in memory.
- Another Data Control (associated with
HttpSession 2)
requests an AM instance, and there are no unreferenced instances available in
the pool. The AM instance then passivates its pending transaction state
referenced by Data Control 1, by saving it to persistent storage, and resets
its transaction state so it can be used by another Data Control. Instance 1 is
now unreferenced, and the pool gives it to Data Control 2.
- Data Control 1 makes a second request for an AM instance. The
pool activates state referenced by Data Control 1 by retrieving it from the
persistent storage, assigning it to an AM instance (instance 2), and giving
instance 2 to Data Control 1.
The process
of passivation, activation, and recycling allows the state referenced by Data
Control to be preserved across requests without requiring a dedicated AM
instance for each Data Control. Both browser users in the above scenario are
carrying on an application transaction that spans multiple HTTP requests, but
the end-users are unaware whether the passivation and activation is occurring
in the background. They just continue to see the pending, but as of yet
uncommitted, changes. Options for persisting passivated state are described
later in this document.
| NOTE: |
While ADF’s architecture allows any
data control to participate in the state management lifecycle, currently only
the ADF Business Components Data Control offers an out-of-the-box
implementation of the state management support.
|
Release Level
Once a Data Control
receives the “endRequest” notification (which means that HTTP request has been
serviced and now is terminated) it releases and checks back the AM instance
into the AM pool (or releases it for garbage collection if AM pool is not
used). After every HTTP request Data Control is responsible for releasing the
AM with the correct release level, one of the following three:
-
Unmanaged Level (formerly known as
Stateless Mode)
This mode implies that no state associated with
this Data Control has to be preserved to survive beyond the current HTTP
request. This level is the most efficient in performance because there is no
overhead related to state management, but it is limited in its use to
applications that require no state management, or to cases when state no longer
needs to be preserved at this point (classic example is releasing AM after
servicing HTTP request from logout page.
-
Managed Level (formerly known
as Stateful Mode)
This is the default level.
This level implies that AM’s state is relevant and has to be preserved for this
Data Control to span over several HTTP requests. Managed level does not
guarantee that for next request this Data Control will receive the same
physical AM instance, but it guarantees that an AM with identical state will be
provided so it is logically the same AM instance each time. It is important to
note that the framework makes the best effort it can to provide the same
instance of AM for the same Data Control if it is available at the moment. This
is done for better performance since the same AM does not need to activate the
previous state which it still has intact after servicing the same Data Control
during previous request. However the Data Control is not guaranteed to receive
the same instance for all its requests and if the AM that serviced that Data
Control during previous is busy or unavailable different AM will activate this
Data Control’s state. For this reason it is not valid to cache references to AM
objects, View Objects, or View Rows across HTTP requests in controller-layer
code.
-
Reserved
Level (formerly known as Reserved Mode)
This level
guarantees that each Data Control will be assigned its own AM during its first
request and for all subsequent requests coming from the
HttpSession associated with this Data Control. This Data
Control will always receive the same physical instance of AM. This mode exists
for legacy compatibility reasons and for very rare special case uses. In
general it is strongly recommended never to use this mode. The reasons are that
with this mode since Data Control to Application Module correlation becomes one
to one, the scalability of the application reduces very sharply and so does
reliability of the application. Reliability suffers because if for whatever
reason the AM is lost, the Data Control will not be able to receive any other
AM in its place from the pool, and so HttpSession gets lost
as well, which is not the case for managed level.
| NOTE: |
One case
when Reserved mode may be used is when PL/SQL stored procedures are used, and
so DB transaction state is created. (Or user must use postChanges method, which
also will create DB transaction state. See further in the article details about
postChanges method). In this case, because some state is stored in DB
transaction the passivation/activation cycle will not be able to make a
different AM instance identical to previous one. Thus, the Data Control has to
use the same AM. However, this is a very special case, and general
recommendation is to never use reserved
level |
Setting the Release Level
Release level can be set in your
Struts action code related to the current request. If you are using an action
that extends DataForwardAction in the
oracle.adf.controller.struts.actions package, you will
override the following method:
protected void handleLifecycle(DataActionContext actionContext) throws Exception
As mentioned above Managed Level is the default level
and if request doesn’t take any action to override the specification of the
release level, the Managed Level will be used. This is expected to be the
common case. However, if the release level for AM has been changed to
“Reserved” it will stay so for all consequent requests until explicitly
changed. Therefore, it is possible to specify managed level explicitly as well.
To do so invoke method:
setReleaseLevel(int releaseLevel)
of class
oracle.adf.model.bc4j.DCJboDataControl or interface
oracle.jbo.ApplicationModule (or override method
getReleaseLevel()). Invoke the setter method with parameter
ApplicationModule.RELEASE_LEVEL_MANAGED.
To set
Unmanaged Level invoke the method resetState() of class
DCDataControl (in the
oracle.adf.model.binding package) at any time during request
processing. This will cause AM not to passivate its state at all once it is
released. (Obviously after this the AM will become unreferenced and reset, and
when it will be used next time it will have its Release Level set to Stateful
by default unless it is changed explicitly). To set Reserved Level use the
setReleaseLevel() method in the class
DCJboDataControl (in the
oracle.adf.model.bc4j package) or on the
oracle.jbo.ApplicationModule interface, passing the value
ApplicationModule.RELEASE_LEVEL_RESERVED as an
argument.
Limitations Implied By the State Management Feature
As explained above, the
entire state management mechanism relies on passivation and activation as a
vehicle to replicate the state of one AM instance to another one, so that those
two instances are indistinguishable from the Data Control’s perspective. This
is possible only if all pending changes are managed by the application module
instance on the middle tier. This insures that each passivated “snapshot” of
the pending changes accurately reflects all the changes the user has made but
not yet committed. The alternative would be to save the pending changes to the
database by posting the appropriate INSERT, UPDATE, and DELETE statements in
the current database transaction – without committing them yet – however this
approach would have two major drawbacks, one on performance and one on
functionality.
Before explaining the details of those drawbacks it
is important to mention that by default AM is configured to use AM pooling and
not to use supplementary connection pooling. What this means is that AM
receives its JDBC connection at the moment it is created and holds this
connection for its entire lifespan in the pool. So by default AM is always tied
to the same JDBC connection. It is recommended to keep such configuration as it
is the best choice for performance. This is so because by holding onto the JDBC
connection that was used to create it’s JDBC PreparedStatement objects, ADF can
reuse those. With this in mind we now will explain what the drawbacks on
performance and functionality are if pending changes are posted to Database but
not committed.
Regarding performance, the AM would be forced to
stay dedicated to its current Data Control to remain in the context of these
pending changes. This would effectively require releasing the application
module with the Reserved Level and would lower application scalability.
Regarding functionality, doing early posting of changes to the database would
force possibly incomplete information to be posted to the database before they
were completely valid, potentially causing integrity violations. The cleanest
and most scalable strategy is to keep pending changes in middle-tier objects
and not tie Data Control to a particular AM instance and its JDBC connection
containing pending database-level transaction state. This allows the highest
leverage of the performance optimizations offered by the Application Module
pool.
So, based on the above the following restrictions apply when
using State persistence spanning over several HTTP requests:
- Method postChanges should not be used because it
posts the changes into the transaction and thus creates transactional state.
Note that there is no mechanism to prevent developers from invoking postChanges
method, but if it is invoked it will compromise the AM state and will lead to
unpredictable and possibly unrecoverable negative results.
- Pessimistic Locking should not be used as it creates locks in the
database, which is also a transactional state. If pessimistic locking is set,
state management will work, but the locking mode will not perform as expected.
Behind the scenes, every time an AM is recycled, a rollback is issued in the
JDBC connection. This releases all the locks that pessimistic locking has
created. We recommend using Optimistic Locking. Note that by default locking
mode is set to “Pessimistic”. To change it to Optimistic open AM configuration
panel (by right clicking on AM and choosing option “Configurations…”) click on
edit button and go to “Properties” tab. There change the value of parameter
jbo.locking.mode to “optimistic”.
If you do want to create a transactional
state in Database in some request by invoking postChanges()
method or by calling PL/SQL stored procedure but do not want to issue commit or
rollback by the end of request, the solution is that from that request on and
until commit or rollback is issued all requests for this Data Control must
release AM with reserved level, guaranteeing that the
HttpSession will use the same AM instance. The
recommendation is that it should be a short period of time between creation of
transactional state in Database and commit or rollback, so reserved level
doesn’t have to be used for long time. Because, as mentioned above, reserved
level has adverse effects on application’s scalability and reliability.
| NOTE: |
Once an application module has been released with Reserved Level,
it will be configured to be released with that level for all subsequent
requests until release level is explicitly changed. So it is the developer’s
responsibility to set Release Level back to Managed Level (Stateful Mode) once
commit or rollback has been issued.
|
The statement above
brings us to another best practice :
The use of
database connection pooling (parameter jbo.doconnectionpooling set to true)
fundamentally incompatible with the practice of calling postChanges() and/or
invoking PL/SQL stored procedures which perform DML operations in the current
transaction —without committing the transaction in the same
request.
As explained above, if you either:
- call the
postChanges()
method, or
- invoke custom PL/SQL stored
procedures which perform DML statements
outside of the normal framework transaction commit processing cycle, then
this requires releasing your application module with the Reserved Level so it
stays dedicated to its current Data Control and its underlying application
module instance in which this pending database transaction state was first
created.
However, if you are using the Disconnect
Application Module Upon Release option in the application module's
configuration in order to share a common pool of database connections across
multiple application module pools, then upon releasing your application module
to the AM pool, its JDBC connection will be disconnected from it and released
back to the database connection pool and a ROLLBACK will be
issued on that connection. This implies that all changes which were posted but
not commited will be lost. On the next request when the AM
is used it will receive a JDBC connection from the pool — which likely won't be
the exact same connection object that was used previously — those changes that
were posted but not commited into DB during previous request are no longer
there.
Failover Feature
The ADF Business Components application module pool makes a best effort
to keep an application module instance “sticky” to the current Data Control
whose pending state it is managing. This is known as maintaining user session
affinity. The best performance is achieved if a Data Control continues to use
exactly the same application module instance on each request, since this avoids
any overhead involved in reactivating the pending “state of affairs” from a
persisted snapshot.
There is a parameter called
jbo.dofailover that can be set in your AM configuration.
This parameter controls when and how often passivation occurs. If the failover
feature is turned off, then application module pending state will only be
passivated just before the pool must hand out a currently-referenced AM
instance to a different Data Control. That is, the AM state is not saved until
it has to be. With failover feature turned on, the application module’s pending
state is passivated every time it is checked in back into AM pool thus saving
its state immediately upon return to the pool.
The idea behind
failover is that if for whatever reason AM instance gets lost (for example one
of OC4J instances to which this application is deployed crashed) its state is
always saved and may be activated by any AM instance at any time. This
capability comes at expense of the additional overhead of performing
passivation eagerly every time AM is checked in (with managed level) back into
the pool. The failover option is a classic case of performance versus
reliability trade off. There is no clear-cut recommendation on the value of
this parameter and each user has to make the decision based on the resources
available and the nature of the application. The default setting of the
failover mode is true; out of the box ADF BC opts to err on the side of
reliability, rather than performance in this area.
| NOTE: |
The
failover option is ignored for AM released with Reserved release level since
the meaning of Reserved level is that Data Control requires the same AM and
substitute by different AM is not acceptable, making the failover option
meaningless in this case. But as soon as release level will be changed from
Reserved back to Stateful the value of the failover parameter will become
relevant.
|
Passivation Storage Options
Passivated information can be
stored in several places. You can control it by configuring an option in AM
configuration or programmatically. The choices are Database, file stored on
local file system and memory:
-
File
This choice may be the fastest
available as access to the file is faster then access to the database. This
choice is good if the entire middle tier (one or multiple Oracle Application
Server installation(s) and all their OC4J instances either installed on the
same machine or have access to commonly shared file system, so passivated
information is accessible to all. Usually, this choice may be good for small
middle tier where one OracleAS is used. In other words this is very suitable
choice for small middle tier such as one Oracle AS with all its components
installed on one physical machine. The location and name of the persistent
snapshot files are determined by jbo.tmpdir property if
specified. It follows usual rules of BC4J property precedence for a
configuration property. If nothing else is specified then the location is
determined by user.dir if specified as a
java.util.Property. This is a default property and OS
specific.
-
Database
This is the
default choice. While performance-wise it may be a little
slower then passivating to file it is by far the most reliable choice. With
passivation to file the common problem might be that it is not accessible to
OracleAS instances that are remotely installed, so in a cluster environment if
one node goes down the other may not be able to access passivated information
and then failover will not work. Another possible problem is that even if file
is accessible to the remote node, the access time for local and remote node may
be very different and performance will be inconsistent. With database access
time should be about the same for all nodes. So database is the most reliable
and robust choice even though somewhat slower then file. Passivated information
is stored as XML snapshot (this is true for all storages), and in case of
Database storage it is written into BLOB column into temporary table within
schema specified in jbo.server.internal_connection
property
-
Memory
This choice is for testing purposes
only and should not be used in production environment. An obvious problem is
that this storage will not be accessible to different processes and
nodes.
To set the value of your choice in
design time set the property jbo.passivationstore to
database or file. Value
null will indicate that default should be used, which is
database. Any values other than "database" and "file" will result default
behaviour. Specifically, a DBSerializer is used by default if the database type
is Oracle or DB2. A FileSerializer is used by default otherwise.
To set the storage programmatically use the method
setStoreForPassiveState() of interface
oracle.jbo.ApplicationModule. The parameter values that you
can pass are:
PASSIVATE_TO_DATABASE
PASSIVATE_TO_FILE
PASSIVATE_TO_MEMORY
What Gets Passivated?
Here
is a partial list of the information passivated during as part of the AM
passivation “snapshot”.
- New, modified, and deleted entities in the entity
caches of the root application module for this user session’s (including
old/new values for modified ones)
-
And for each
active view object (both statically and dynamically created)
- Current row indicator for each rowset (typically
one)
- New rows and their positions (New rows are treated differently
then updated ones. Their index in the VO is traced as well)
- ViewCriteria and all its related parameters such as view
criteria row etc.
- Flag indicating whether or
not a rowset has been executed
- Range start
and Range size
- Access mode
- Fetch mode and fetch size
-
Any VO-level custom data
- SELECT, FROM, WHERE,
and ORDER BY clause if created dynamically or changed from the View
definition
| NOTE: |
This is not a full list of what is passivated but covers the bulk of
what's written out.
|
Here is simple example of a passivation
snapshot from AM that holds VO based on Departments table from HR schema. This
snapshot is holding one new row with department number value 271 and department
name value “TestDept” :
<AM MomVer="0">
<cd/>
<TXN Def="1" New="0" Lok="1">
<EO Name="model.Departments">
<![CDATA[000100000003C20348]]>
<DepartmentsRow PS="0" PK="Y">
<DepartmentId>271</DepartmentId>
<DepartmentName>TestDept</DepartmentName>
</DepartmentsRow>
</EO>
</TXN>
<VO>
<VO It="1" Sz="10" St="17" Ex="1" Def="model.DepartmentsView"
Name="DepartmentsView1" cli="1">
<Key>
<![CDATA[000100000003C20348]]>
</Key>
<cd>
<QC>
<Ke>ACED000577020000</Ke>
<NR Hdl="76" Idx="26">
<Ke>000100000003C20348</Ke>
</NR>
</QC>
</cd>
</VO>
<VO It="1" Sz="10" Ex="1" Def="model.EmployeesView"
Name="EmployeesView3" cli="1"/>
</VO>
</AM>
Timeouts
There are several timeout parameters that can be configured. Two of them
will be discussed in this article. First one is HTTP timeout. This parameter is
not specified in AM configuration. It is specified in your client project for
web application in file web.xml. The default value is 35
minutes. It could be edited manually in text of web.xml file
and finding the following fragment:
<session-config>
<session-timeout>35</session-timeout>
</session-config>
Or better yet, by making a right click on the file and
choosing Properties.... The HttpSession
timeout titled “Session timeout” may be specified in
General section (the first one that appears by
default).
However, the most interesting thing about this parameter
is not how to set it but how it is interpreted by the middle tier J2EE
container. The HttpSession may be treated as physical
resource or as logical user session. Logical user session means a user
interaction session with application. Physical resource means that
HttpSession is treated as some memory-consuming object that
may be removed and reinstantiated according to system needs and its termination
and reinstantiation is completely transparent to the user.
BC4J
uses the SessionCookie stored in Data Control to store a
state identifier for AM state related to Binding Context that correlated to
that HttpSession (the passivation id). Recall that the
HttpSession has a single BindingContext
and all Data Controls in the BindingContext are indirectly
related to the same HttpSession as containees of
BindingContext. When the HttpSession
times out the BindingContext will go out of scope, so that
SessionCookie stored in the Data Control and used as state
identifier will go out of scope and the AM state referenced by this Data
Control may no longer be accessible. At the same time AM holding current state
will become unreferenced and it will be reset (its state cleaned up) and
available to service any HttpSession. If failover is
disabled the HttpSession timeout will represent both the
physical and the logical end of that session because both the physical
HttpSession instance and the state associated with that
session will be inaccessible. (So in this case user attempting to continue
working will not be able to do so until (s)he starts a new
HttpSession. The old HttpSession and
state associated with it are discarded. If failover is enabled, the
HttpSession timeout will represent the physical end of the
session only; the HttpSession instance for that session will
be inaccessible but the state associated with Data Control related to that
session will still be accessible. (Even though
BindingContext with all Data Controls it contains will be
out of scope together with its HttpSession). At this point
the AM also will be reset and made unreferenced but its state is passivated
earlier due to enabled failover feature. The reason that the state still will
be accessible is because when failover is enabled the
HttpSession cookie that is stored in the
HttpSession will also be stored on the browser tier as a
browser cookie. This is performed by the framework to ensure that the
possibility of re-establishing the link between a new
HttpSession and the pending session state in the case of
being routed to a different middle-tier server.
For example,
consider a scenario in which a user opens the browser does some work and lets
say does not commit his changes and leaves for lunch. Assume that failover is
enabled. The HttpSession will time out and will be removed
from the memory and so will BindingContext. Corresponding AM
will become unreferenced and it will be reset, but it will not signify the end
of user’s session from user’s point of view. When user comes back and tries to
continue working, his HttpSession is recreated using browser
cookie that is stored on his local file system,
ADFBindingFilter will perform bootstraping again and will
recreate the BindingContext with its Data Controls with
SessionCookie holding identical state identifier, and Data
Control that checked out AM during first request from this
HttpSession will be able to identify and activate the AM
state referenced by the previous HttpSession through its old
Data Control. So, the user will be able to continue working as if the
HttpSession had never timed out. In other words, from user’s
point of view the session never was terminated. The session is treated only as
physical resource that may be removed for efficiency reasons but the logical
application session lives on, so to speak.
To test this, try the
following: create simple ADF Business Components application and web client to
it. Set the HttpSession timeout to 1 minute, and make sure
that failover is enabled, run your client application, create a new row or
modify some value but do not commit (or rollback). Now wait for more then one
minute to let the HttpSession timeout. Note that if in your
ViewController project you add a Java VM property on the
“Runner” panel like -Djbo.debugoutput=console you will
actually see when your HttpSession times out in the debug
trace information. Now try to continue working. You will see that all your
state has been preserved as if the HttpSession never timed
out. You will be able to see all your changes that have not been committed yet
and you still will be able to commit them as well. Now you may try the same
scenario, but now with failover turned off. You will see that after timeout
your pending state is lost. So you will have to start from the point where you
last committed your changes.
So, if from user’s perspective HTTP
timeout is transparent and unnoticeable what is it really used for? It is used
for resource management. With Internet Explorer it is possible to launch the
browser several times from the (Start) menu, and each gets
run in a separate process and behaves as if it were a separate browser
user.
| NOTE: |
In contrast, other browsers — including IE itself if
you only create a new browser window rather than launching another browser
process — treat all windows as the same browser user.
|
Assuming that user may open many browser windows, it's clearly the case that
they can only perform active work in one window at a time. So you specify
HttpSession timeout so inactive sessions will be removed and
AMs referenced by them become unreferenced and will be reset to save the
resources and they will be restored only when someone will continue to work on
particular window. However, be careful not to set it too short so excessive
removal and recreating of HttpSessions and extra passivation
and activation of AMs do not occur.
One more detail about HttpSession timeout is that it
could be caused programmatically. To do so user needs to get access to instance
of the class javax.servlet.http.HttpSession and to invoke
its method called invalidate(). Note that this will have the
same effect as HTTP timeout, which does not necessarily mean logical session
termination as described above.
In many cases it important that
unauthorized user may not get an access to an existing
HttpSession after an authorized user has finished working on
his/her session. To prevent this, logical user’s session has to be terminated,
and that as we saw does not happen with HttpSession timeout
if failover is enabled. One way to do so is to close the browser window (or
more precisely all windows belonging to the same browser process). This will
terminate the process and effectively will end user’s
HttpSession. But even that could be reconfigured, as it will
be explained below. Using another timeout parameter that we will discuss later,
it is possible to make the logical session survive for a certain time even
after browser process has been terminated. The other way to properly terminate
user’s session is to create a button or link in the application called “Log
off” and upon user clicking on this button AM should be released with stateless
level. This will cause AM not to get passivated at all, so no state for this
session will be saved. (Of course it is recommended that at this moment all
pending transaction changes have been committed or rolled back. As the
developer, you may want to check to make sure this is so, and if not
appropriate action should be taken before log-off occurs).
There is
a very important exception to the rule that the HttpSession
is treated as physical resource only and not the logical termination of user’s
session when failover is enabled. If AM was released with
reserved level the HttpSession timeout will be logical
session termination and user will not be able to continue working after timeout
as if nothing happened. User will have to go through authentication process,
and all unsaved changes will be lost. Recall that reserved release level
ignores the jbo.dofailover value. Of course, if failover is
disabled then HttpSession will be treated as logical
termination of user’s session regardless of release level.
There’s
something important to understand about the HttpSession
object. ADF Business Components session state is identified by an ADF BC
SessionCookie. The ADF Business Components
SessionCookie registers itself with the
HttpSession so that it may be managed between the requests
of an HttpSession. An HttpSession is
identified by an id generated by the servlet container. An application
developer may use a variety of HttpSession tracking
mechanisms, like browser cookies and URL rewriting, to associate the
HttpSession id with a browser and to correlate a set of HTTP
requests from a browser with a particular HttpSession.
The SessionCookie in turn has a browser cookie
stored on the local file system where the browser is located. If web
application is marked as distributed and it is installed on several nodes
within a cluster, at run time the HttpSession and its
SessionCookie is replicated to all instances of OC4J where
application is installed. (This is true only if application is configured to be
in state replicating mode on OracleAS). So if one of the nodes goes down
HttpSession still will be available on other OC4J instances
for seamless failover. If replication fails, the ADF BC
SessionCookie can still be recreated; the generic
HttpSession may not be recreated. This means the logical
user session and their pending work can still can be recovered and get
recreated using the browser cookie.
The second important timeout
parameter that we will discuss is set in AM configuration and it is called
jbo.maxpoolcookieage. This parameter is specified in seconds
and it defines how long browser cookie will survive after the browser process
has been terminated or after the last activity in the browser. The default
value is –1 and for most of the cases it should always remain such. The default
value means that browser cookie will exist as long as browser process is alive.
If you set it to positive value it will mean that browser cookie will be kept
for specified period of time after last activity in the browser, even if the
browser process is terminated. The most important consequence that needs to be
understood here is, that if user opens the browser and creates some AM state
within his application and then closes all browser windows, if the same browser
will be reopened within the timeout period (one that is specified in
jbo.maxpoolcookieage parameter), user’s session will be
restored and all the uncommited changes will be restored with it. That is, the
user may continue working as if the browser’s process had never been
terminated.
| NOTE: |
Due to known bug in JDeveloper version 9.0.5.X
the parameter jbo.maxpoolcookieage is ignored if set in AM
configuration and default is always used. To work around this problem this
parameter could be passed as command line parameter in your client project
properties. (It has to be set in client project as oppose to server one since
it is client project that is executed and it is its properties that are picked
up) In project properties under “Runner” you may specify additional parameter
in java options. To pass the options use –D flag. For instance for this
parameter you will need to add -Djbo.maxpoolcookieage=300.
This will set the value of this parameter to 5 minutes. (There is another
parameter that gets ignored in AM configuration and it is
jbo.ampool.monitorsleepinterval. This is also a known bug.
This parameter is not discussed in this document). Both of those mentioned bugs
are fixed in JDeveloper 10.1.2 and from that version on there will be no need
to set any parameters in this way, although it still will be possible to do
so.
|
I’ll use a simple use case to demonstrate the point
above. Set the value jbo.maxpoolcookieage property to 5
minutes, run your Web application and create some state by updating or
inserting some record. Now close the browser. (Make sure that your browser
process has been terminated by checking that in your task manager browser
process is no longer found). Open the browser again within time frame not
longer then timeout you specified. Notice that your changes are picked up and
you continue working as if browser was never closed.
So going back
to HttpSession timeout interpreted as physical resource and
not logical session termination for stateful release level with failover
enabled: if developer wants to change this behavior and make
HttpSession timeout to be interpreted as logical session
termination, developer may set HttpSession timeout and
jbo.maxpoolcookieage to the same value. In this case both
HttpSession cookie and ADF BC
SessionCookie-related browser cookie will go out of scope
and will be deleted at the same time and there will be nothing left to restore
session from. A drawback is that the ADF BC session state could be restored
after the browser process has been killed if that browser is restarted before
the jbo.maxpoolcookieage has elapsed. For example, if one
user performed some work and closed the browser then a second user could
restart a new browser instance on that machine and gain access to any
AM/VO/Transaction state (where clauses, RowSets, etc.) that was created by the
first user.
| NOTE: |
Most of the users will never need to change the default value for
this property. Don’t change this value if you don’t absolutely need it.
Changing the default to some positive value creates a potential security
vulnerability. If you change this value make sure that all consequences are
anticipated.
|
Managing Custom User Specific Info
It is fairly common practice to add custom user
defined information in the AM. It could be member variables added by user into
AM or some custom information stored in session object. ADF BC framework
provides mechanism to passivate this information as well. This is done using
so-called hooks – API provides some empty methods that are invoked by framework
during passivation and activation. By overriding those methods user gets the
chance to add custom logic executed with each passivation and activation
occurrence. The methods that user may override to take advantage of this
feature could be found in
oracle.jbo.server.ApplicationModuleImpl. Some of the methods
are:
protected void passivateState(Document doc, Element parent)
public void activateState(Element elem)
There are other hooks that could be used for this
purpose as well. Please see API for
oracle.jbo.ApplicationModule and
oracle.jbo.server.ApplicationModuleImpl for more details. VO
and EO also have hook methods. Below is a simple example that shows how to
override methods mentioned above to make sure that custom information stored in
AM is included in passivation/activation cycle.
Assume that you
would like to keep some custom parameter in your AM called
jbo.counter that has session related value that you are
interested to preserve. Each AM has a session object associated with it that
stores its state (please do not confuse with HTTPSession, See
oracle.jbo.Session interface for reference) in the session
there is an object designated for storing user custom information. It is called
“User’s data”. This is where custom info usually stored. So in this case in
your class that extends
oracle.jbo.server.ApplicationModuleImpl you will need to
override two methods mentioned above. It might look like this:
public void passivateState(Document doc, Element parent) {
int counterValue = getCounterValue();
Node node = doc.createElement(COUNTER);
Node cNode = doc.createTextNode(Integer.toString(counterValue));
node.appendChild(cNode);
parent.appendChild(node);
}
public void activateState(Element elem) {
super.activateState(elem);
if (elem != null) {
NodeList nl = elem.getElementsByTagName(COUNTER);
if (nl != null) {
for (int i=0, length = nl.getLength(); i < length; i++) {
Node child = nl.item(i).getFirstChild();
if (child != null) {
setCounterValue(new Integer(child.getNodeValue()).intValue()+1);
break;
}
}
}
}
}
/*
* Helper Methods
*/
private int getCounterValue() {
String counterValue = (String)getSession().getUserData().get(COUNTER);
return counterValue == null ? 0 : Integer.parseInt(counterValue);
}
private void setCounterValue(int i) {
getSession().getUserData().put(COUNTER,Integer.toString(i));
}
private static final String COUNTER = "jbo.counter";
This is a simple example that demonstrates use of some
but not all hooks that allow developer to take care of persisting custom
information.
Managing Transient ViewObjects
For passivation/activation purposes Transient VO
attributes and VO attributes not based on EO ones but derived directly from the
database through SQL-calculated expressions are treated in the same way. So for
simplicity only transient versus EO based attributes are discussed here.
Transient attributes are not passivated by default. This is because due to
their nature they are usually intended to be “read only” and very easily
recreatable. So it often doesn’t make sense to passivate their values as part
of the XML snapshot. However, each transient attribute may be configured to be
passivation-enabled. And every VO can be also configured to be passivation
enabled. This is true for any VO and not only for transient VO. By default all
VOs are marked as passivation-enabled, and all transient attributes are not.
That means that VO that only contains transient attributes is marked to be
passivation enabled, but only passivates its non transactional state.
Recall that information saved by passivation is divided in two parts:
transactional and non-transactional state. Transactional state is the set of
updates made to EO data – performed either directly on entity objects or on
entities through view object rows – that are intended to be saved into DB.
Non-transactional state comprises VO attributes, such as current row index,
“where” clause, “order by” clause etc. Please see the section “What gets
passivated” for details.
It is worth to mention that passivating
transient attributes is more costly resource- and performance- wise, because
transactional functionality is usually managed on EO level. Since transient VOs
are not based on EO this means that all updates are managed in VO cache and not
in EO as usual. Therefore passivating transient VOs or attributes requires
special handling. Usually passivation only saves the values that have been
changed, but with Transient VOs passivation has to save entire row (row will
include only attributes marked for passivation).
Consider the
following example: regular (i.e. non-transient) VO contains 10 rows and each
row has 7 attributes. The update has been made in two attributes in row number
2 and one attribute in row number 5. During the passivation three values will
be passivated (the ones that have been updated). Now consider the same size
transient VO with 4 out of 7 transient attributes marked for passivation.
Assume that the same updates where made as in previous case for regular VO. The
passivation will have to save all 10 rows with 4 attributes each (as we said
that only 4 attributes out of 7 are marked for passivation). This is 40
attributes to passivate as oppose to 3 in case of regular VO. Therefore if you
have any transient VOs or attributes that you want to passivate, consider
making them not transient in the first place.
To configure VO to be
passivation-enabled or disabled, double-click on VO to open the VO Editor and
go to the Tuning panel. There is a checkbox there called “Enable passivation”
that is set on by default. There is additional checkbox there called “For all
transient attributes”, and this box is not checked on by default. Checking this
box will mark all transient attributes to be passivation-enabled. It is also
possible to mark each transient attribute as passivation enabled. In the VO
editor, go to the Attributes node in the left-hand tree and expand it to show
each attribute. For each attribute you can set attribute related configuration
options. Note that checkbox called “Passivate” appears for transient attributes
only.
There is no need for any custom code to passivate transient
attributes. But keep in mind that it is costly operation and use it
sparingly.
Partial Rollback on Middle Tier
Partial rollback is a potentially very powerful
feature. In the database server we have the savepoint feature that allows a
developer to rollback to a certain point within a transaction instead rolling
back the entire transaction. Here we have the same feature but implemented in
middle tier. There are three methods in oracle.jbo.ApplicationModule interface
that allow you to take advantage of this feature. The methods are:
public String passivateStateForUndo(String id,byte[] clientData,int flags)
public byte[] activateStateForUndo(String id,int flags)
public boolean isValidIdForUndo(String id)
These methods allow developer to create a stack of
identifiable snapshots and restore the pending transaction state from them by
name. Keep in mind that those snapshots do not survive past duration of
transaction i.e. events of commit or rollback. This feature could be used to
develop complex capability of application to undo and redo changes. One of
ambitious goals could be implement functionality for back and forward browser
buttons. But more simple uses obviously could be very handy.
Configuration Parameters and Interesting Use Cases
Tuning ADF-BC is not the topic of this paper, but there are
some very important parameters that should be mentioned here, because they have
strong impact on state management. Also some simple testing scenarios that
should help to demonstrate visually some complicated issues explained in this
paper.
The first parameter is jbo.recyclethreshhold. This parameter
controls how many AM instances AM pool tries to preserve to be “sticky” to
their Data Controls before it will take one to recycle instead of creating a
new AM instance. The value of this parameter should be equal to the expected
number of "active" concurrent HTTP sessions. Define an "active" session as one,
which will be expected to send subsequent requests before timing out. If this
parameter is lower then it should be it will cause excessive recycling of AMs
which will have negative impact on performance.
jbo.ampool.maxavailablesize - This is the maximal amount of Application
modules that could be kept idle in the pool waiting for the user to come. If
one available then upon request it will be given instead of creating a new one,
which is a costly performance operation. Creation of new AM involves creating
AM instance itself and all its VOs as well. When each VO is instantiated the
VO’s query gets executed. So each AM creation involves execution of multiple
queries (one pr each VO). Therefore it is best to configure AM pool to have
minimal amount of creations and removals of AMs
jbo.ampool.minavailablesize - This is the minimal amount of Application
modules that will be kept idle in the pool waiting for the user to come.
It is recommended that both of those parameters should be set to equal
value and that value should be expected number of HTTP requests serviced
concurrently. Otherwise, you will note high Application Module
creation/removal. This will also impact performance negatively. Please note
that both those parameters refer to referenced and unreferenced AMs that have
been released
Just to demonstrate how those parameters influence
the pool consider the following situation: Assume that you have a pool which
has been configured for normal workload of 50 concurrent users. Let’s say that
the number of requests serviced concurrently at average is 10. So assume that
jbo.ampool.minavailablesize and jbo.ampool.maxavailablesize are set to 10. So
in a normal situation we will have 50 AMs in the pool that are frequently used
and the pool will make sure that at any given moment there are at least 10 AMs
available in the pool. Assume that there is a peak time and we got 100
concurrent users working on the application. So the pool will have to create
extra 50 AMs to accommodate this situation. But once the pick time is over we
get back to our regular workload. But we have 100 AMs in the pool of which 50
don’t get used, but some if not all of those inactive 50 AMs are referenced
(i.e. have the state of the Data Control request from which they serviced
last). This is true only if this AM has been released with stateful release
level. When the time specified in jbo.ampool.maxinactiveage will elapse for AM
since it last being used it will be marked for removal from AM pool (because at
the moment we have more inactive AMs then specified in
jbo.ampool.maxavailablesize) and so the pool will be “shrinking,” removing
extra AM instances, until it leaves only 10 inactive AMs in the pool. But if
failover is not enabled each such AM holds the state that has not been
passivated (again if it was released with stateful level). So in this case
(failover is not enabled) before AM is removed from the pool it will get
passivated in order to preserve its state and keep it accessible.
Further discussion of ADF-BC tuning is out of scope of this document.
Please refer to
How
to Performance Tune an ADF Business Components
Application.
Testing ADF Business Components
A very interesting test tool could be made by
overriding just few methods in class that extends
oracle.jbo.server.ApplicationModuleImpl. Add two variables
to your class:
static int idProducer = 0;
private int amId;
amId will be variable that uniquely
identifies AM instance. Override the constructor as follows:
public AppModuleImpl() {
amId = ++idProducer;
System.out.println("AM with ID " + amId + " is being created");
}
You may add and expose to your clients the following method
public int getAmId() {
return amId;
}
And add these three methods as well:
protected void afterConnect() {
System.out.println("Application Module with id " +amId +
" just got connected");
super.afterConnect();
}
protected void prepareSession(oracle.jbo.Session session) {
System.out.println("Application Module with id " + amId +
" is about to be used by different session");
super.prepareSession(session);
}
protected void finalize() {
System.out.println("Application Module with id " + amId +
" is getting garbage collected");
super.finalize();
}
Those methods will put out diagnostic printouts that
will help developer to understand what happens behind the scenes and how and
when different events occur. You will see that if AM pooling is on and
Connection pooling is off AM gets connected right after it’s creation and holds
to its connection for entire life duration. It will get disconnected only when
it will be removed from AM pool.
The fact that
prepareSession has occurred means that AM has been recycled,
i.e. its state has been reset and it assumed the state of different Data
Control then the one it serviced before. Of course this method will be invoked
always when AM will service request for the first time in its life.
On your client you may get reference to your AM and use method getAmId to
see which AM is servicing Data Control from Binding Context related to your
current HTTP session. If your AM pool is configured correctly and recycle
threshold is higher then the number of HTTP sessions serviced at the moment you
will see that usually your HTTP session will always be serviced by the same AM
instance.
To get reference to your AM instance on your client side
you may create an action for your JSP page and override the method
handleLifecycle as follows
public class DeptEmpBrowserAction extends DataForwardAction {
protected void handleLifecycle(DataActionContext ctx) throws Exception {
HttpServletRequest request = actionContext.getHttpServletRequest();
BindingContext bc = ctx.getBindingContext();
YourModule am = (YourModule)bc.findDataControl("YourModuleDataControl")
.getDataProvider();
System.out.println("Message from client: The AM with id "+
am.getAmId() + " is in use");
super.handleLifecycle(actionContext);
}
}
For the testing purposes you may want to disable AM
pooling to see that in that case every single request is serviced by different
AM instance. And if you create some state that is carried over between requests
you will know for sure that it is not because your Data Control received the
same AM instance but because your state has been successfully passivated by
previous AM and activated by a different one.
Another testcase is
to enable AM pooling and set recycle threshold to 1 and open two different HTTP
sessions (two different browsers such as Internet Explorer and Mozilla or two
different windows of Internet Explorer that are executed in different
processes.) You will see that with this parameter you will be juggling AM
instances between your sessions.
There are many other cases that
such simple tool could be used to demonstrate on practice the complex features
described in this paper.
|