ColdFusion
Locking Best Practices
by Jim Schley
Principal Technical Support Engineer
Spectra Product Support
Allaire Corp. Introduction
Locking shared scope variables within ColdFusion
templates is an often overlooked process that
has severe consequences when best practices are
not followed. This document will explain why the
process of locking shared scope variables is important
and the corresponding best practices.
Developers should be advised that these practices
should not be considered optional under any circumstances.
Most cases of ColdFusion site instability can
be traced back to inproper use or complete lack
of locking. A few extra lines of code and an understanding
of the underlying concepts of locking can go a
long way towards ensuring robust Web applications
with maximum availability and performance.
What Are Shared Scope
Variables?
Session scope variables, application scope variables,
and server scope variables are shared scope variables.
They are so named because they are stored in a
part of memory that is shared by all of the threads
used by ColdFusion Server to run requests. The
physical pieces of memory that are used to store
these variables can be accessed by any of the
threads within the server. Variables are "accessed"
when reading their values or writing values to
them.
Why Does Shared Scope
Variable Access Need to Be Locked?
Because ColdFusion Server uses multiple threads
(multithreading), it is able to simultaneously
work on requests from multiple users at the same
time. It is also able to work on multiple requests
from the same user at the same time. This can
happen with a Web site that utilizes frames, when
a user clicks the reload button on her browser
before the initial request has completed, or when
a user has multiple browser windows open.
Since these threads can access the same variables
in memory at the same time, we are presented with
the problem of the threads competing for the same
resource. This competition normally leads to memory
corruption. Corruption occurs because of the way
the data is structured in memory and the changes
that take place in these structures when the variables
are altered. If one thread modifies the shared
scope variable while another thread attempts to
read the value of the variable, for example, memory
corruption may result. Because the read and write
are happening concurrently, the read may try to
access the structure in a way that would be invalid
after the write to the variable modified the data
structure. Accessing memory in an invalid way,
such as requesting the contents of a physical
piece of memory that doesn't exist, causes memory
corruption and process crashes.
In addition, if the data structures that track
memory allocation become corrupted, the allocator
will work incorrectly and large amounts of memory
can "leak." Memory corruption also leads to crashes
and/or server instability. Some of the symptoms
of memory corruption follow:
- ColdFusion PCode errors
- cfserver process crashing/stopping/restarting
- Unexpected shared scope variable evaluation
results
- Large growth in the amount of memory used
by the cfserver process
- Operating system instability
Locking variables prevent these problems by only
allowing one thread to access the shared scope
variable at a time. All of the other threads must
wait in line to access the shared variables until
the previous thread completes its action. In effect,
access to the locked piece of code is single threaded.
How to Lock Access to
Shared Scope Variables
Locking is accomplished by encapsulating CFML
that accesses shared scope variables with CFLOCK.
The six basic scenarios in which you use CFLOCK
for variable access are: writing to server scope
variables, reading from server scope variables,
writing to application scope variables, reading
from application scope variables, writing to session
scope variables, and reading from session scope
variables.
Writing to server scope variables:
<cflock scope="SERVER" type="EXCLUSIVE" timeout="10">
<cfset server.myservervar="1">
</cflock>
Reading from server scope variables:
<cflock scope="SERVER" type="READONLY" timeout="10">
<cfoutput>#server.myservervar#</cfoutput>
</cflock>
Writing to application scope variables:
<cflock scope="APPLICATION" type="EXCLUSIVE" timeout="10">
<cfset application.myappvar="1">
</cflock>
Reading from application scope variables:
<cflock scope="APPLICATION" type="READONLY" timeout="10">
<cfoutput>#application.myappvar#</cfoutput>
</cflock>
Writing to session scope variables:
<cflock scope="SESSION" type="EXCLUSIVE" timeout="10">
<cfset session.mysessionvar="1">
</cflock>
Reading from session scope variables:
<cflock scope="SESSION" type="READONLY" timeout="10">
<cfoutput>#session.mysessionvar#</cfoutput>
</cflock>
How Does CFLOCK Work?
The CFLOCK tag can be implemented as exclusive
or as read only. As the pervious examples showed,
exclusive locking is used for writing to variables,
and read-only locking is used for reading variables.
An exclusive lock prevents more than one request
from the same application or session from entering
the code enclosed within the CFLOCK code at the
same time. This means that the code can only be
executed one request at a time. If any other request
is executing at the same time, it will stop when
it reaches a CFLOCK tag and wait for the other
request holding the exclusive lock to finish executing
the code. Once the other request releases the
lock, the queued requests will be processed on
a first-come, first-served basis.
A read-only lock does not force single threading,
however, it will prevent an exclusive lock from
being concurrently established. This means that
multiple requests can read from the same variable
at the same time, but a request won't be able
to write to a variable while another request is
reading from it.
The scope attribute of the CFLOCK tag corresponds
to the scope of the variables whose access should
be locked. The server keeps track of locks defined
with different scopes separately. This is advantageous
because a CFLOCK defined with a session scope
does not have to contend with requests for locks
from other sessions outside its own. Likewise,
a CFLOCK defined with an application scope does
not have to contend for locks with locking requests
from other applications on the same server. It
is important to match the scope of the lock to
the scope of the data so that the server knows
the context for which you want to protect access
to the data. Application variables locked with
a session scoped CFLOCK would not prevent other
sessions in the same application from performing
unlocked reads and writes. Likewise, mixed shared
scopes (i.e. application and session) should not
be combined in the same CFLOCK statement.
The required timeout attribute takes a value
in seconds to wait for ColdFusion to issue the
requested lock. To maximize throughput, set a
low value for timeout. Under normal circumstances,
it should never take more than a few seconds for
the tag to be able to obtain a lock. If your tag
is consistently timing out, eliminate excess code
from within all CFLOCK tags. Only access to variables
need to be included. One common mistake is to
include a CFQUERY call within the CFLOCK tag.
Bad:
<cflock scope="application" timeout="2" type="exclusive">
<cfquery name="application.qUser" datasource="#request.dsn#">
SELECT FirstName, LastName
FROM Users
WHERE UserID = #request.UserID#
</cfquery>
</cflock>
Good:
<cfquery name="variables.qUser" datasource="#request.dsn#">
SELECT FirstName, LastName
FROM Users
WHERE UserID = #request.UserID#
</cfquery>
<cflock scope="application" timeout="2" type="exclusive">
<cfset application.qUser=variables.qUser>
</cflock>
This avoids the problem of the lock persisting
for the duration of the query execution time.
Now, the lock only persists for the duration of
the execution of the CFSET tag. Here is another
common mistake:
Bad:
<cflock scope="application" timeout="2" type="readonly">
<cfquery name="qUser" datasource="#application.dsn#">
SELECT FirstName, LastName
FROM Users
WHERE UserID = #form.UserID#
</cfquery>
</cflock>
Good:
<cflock scope="application" timeout="2" type="readonly">
<cfset request.dsn=application.dsn>
</cflock>
<cfquery name="qUser" datasource="#request.dsn#">
SELECT FirstName, LastName
FROM Users
WHERE UserID = #form.UserID#
</cfquery>
Again, this solution again allows us to prevent
the lock from persisting for the duration of the
database query. Although application variables
are widely used to store data source names, it
probably makes more sense to avoid using an application
variable in this case. This can be done by instead
setting a request scope variable in the application.cfm
file.
Care should be taken when using CFSET to move
complex data types like structures from one scope
to another. This action does not really copy the
structure from one scope to the other; it creates
a pointer to the original object. This means that
accesses to this new structure will be accesses
to a shared scope variable if the original variable
was shared scope, regardless of the new scope.
The Duplicate( ) function can be used to copy
the structure instead of creating a pointer to
it.
Using CFSET:
<CFSET request.strDATA=application.strDATA>
In this case, request.strDATA should still be
treated as an application variable and locked
appropriately.
Using Duplicate( ):
<CFSET request.strDATA=Duplicate(application.strDATA)>
In this case, request.strDATA is not a pointer
and can be treated like a request scope variable.
To avoid deadlocks, do not nest CFLOCK tags,
especially those of mixed scope.
Single Threaded Sessions
In the ColdFusion Administrator, there is a setting
on the locking page called single threaded sessions.
Turning this switch on enforces individual browser
sessions to use only one thread at a time. One
request from the browser must completely execute
before the next one can start. This would prevent
a request spawned by clicking the refresh button
in the browser from running until the first request
from that browser finished. Enabling single threaded
sessions eliminates the need to lock reads and
writes to session variables because there are
no longer concerns about concurrent access. This
feature may cause performance degradation in frames-based
sites because it will prevent both frames from
being processed simultaneously.
Automatic Read Locking
For each of the shared scopes, there are options
on the locking page of the ColdFusion Administrator
to enable automatic read locking. This feature
effectively places read-only locks around all
read accesses to variables in the specific scope.
Additionally, it checks all writes to these variables
and ensures that locks are in the code. An error
will be thrown if it discovers that a write to
one of these variables was left unlocked.
There are pros and cons to using this feature.
Due to the need to insert read-only locks, this
feature adds an additional step to the precompilation
process. Performance degradation is realized because
of the overhead associated with checking all shared
scope variable access for locking. On the other
hand, this feature can be valuable when retrofitting
a site that was mistakenly coded without locking
variables. If performance is not most important,
a site can be retrofitted by simply locking all
writes to shared scope variables and then enabling
this feature.
Full Checking
For each of the shared scopes, there are options
on the locking page of the ColdFusion Administrator
to enable full checking. This feature will cause
the server to throw an error whenever an unlocked
read or write access to a shared scope variable
is discovered.
It is highly desirable to enable this feature
on your development server before you begin your
project. Doing so will force all developers on
the project to lock variables appropriately to
make their code work. This feature can also be
useful to quality assurance personnel while testing
an application. If there is no doubt that all
shared scope variables in an application have
been locked prior to "going live," it is recommended
that this feature not be enabled in production
because performance degradation will occur due
to the overhead associated with checking all accesses
to shared scope variables. However, unless QA
testing can confirm that all variables have been
locked, use of this feature should be considered
as a fail-safe to ensure protection against inadvertently
unlocked variable accesses.
Why Isn't There Automatic
Write Locking?
Automatic write locking or universal locking
has not been included as a feature of ColdFusion
Server for several reasons. Performance is one
reason. Automatic write locking would place a
lock around the code that writes to each variable
causing a single lock to surround each individual
CFSET statement. It is much more efficient to
use one CFLOCK to surround multiple CFSET statements.
The granularity itself is another reason. Although
the highly granular locking model would prevent
corrupted memory with individual locks for each
access, it would not provide any options for transactional
integrity. Here is a hypothetical scenario:
An e-commerce application provides the user with
a shopping cart interface. When the user adds
a widget, with quantity "1," to her cart, the
following two statements of code execute:
1A.
[auto exclusive lock]
<cfset session.cart.product[1]="widget">
[/auto lock]
1B.
[auto exclusive lock]
<cfset session.cart.quantity[1]="1">
[/auto exclusive lock]
In our hypothetical scenario, the user decides
(after clicking the add button) that she really
doesn't want one widget. She really wants two
sprockets. She clicks the stop button on her Web
browser and addss two sprockets to her cart. A
new thread executes the following two statements
of code:
2A.
[auto exclusive lock]
<cfset session.cart.product[1]="sprocket">
[/auto exclusive lock]
2B.
[auto exclusive lock]
<cfset session.cart.quantity[1]="2">
[/auto exclusive lock]
Clicking the stop button on the browser does
not kill the first thread.
For this hypothetical situation, imagine that
Thread 1 starts up and finishes Statement 1A.
In turn, the operating system's thread management
system bumps Thread 1, and Thread 2 starts. Thread
2 finishes Statements 2A and 2B before being bumped,
and Thread 1 resumes to complete Statement 1B.
Because of the automatic locking, nothing is happening
concurrently; everything is sequential. The sequence
looks like this:
| Thread |
Statement |
| 1 |
1A |
| 2 |
2A |
| 2 |
2B |
| 1 |
1B |
Because of the arbitrary order that the threads
completed the statements, we end up with the following:
session.cart.product[1] = "sprocket"
session.cart.quantity[1] = "1"
The problem here is that we set the product value
of the second request and the quantity value of
the first request.
This problem is avoided using CFLOCK by including
both CFSET statements in a single lock. This transactional
coding would not be an option with automatic write
locking.
Conclusion
As with all Web application server platforms,
a multithreaded architecture provides a robust,
high-performance system. However, this architecture
does create certain challenges for dealing with
shared memory. The CFLOCK tag, as well as automatic
read locking, provides the tools for avoiding
the pitfalls.
References
Allaire Knowledgebase : Changes to CFLOCK in
CF Server 4.5 http://www.allaire.com/Handlers/index.cfm?ID=14165&Method=Full
Developing ColdFusion Applications, Macromedia
ColdFusion 5, Chapter 12, Using the Application
Framework, "Locking Code with cflock" p. 233 http://livedocs.allaire.com/cf50docs/Developing_ColdFusion_Applications/appFramework10.jsp
CFML Reference, Macromedia ColdFusion 5, Chapter
2, ColdFusion Tags, "cflock" p. 172 http://livedocs.allaire.com/cf50docs/CFML_Reference/Tags57.jsp#1100787
|