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:
Reading from server
scope variables:
#server.myservervar#
Writing to
application scope variables:
Reading from
application scope variables:
#application.myappvar#
Writing to session
scope variables:
Reading from session
scope variables:
#session.mysessionvar#
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:
SELECT FirstName,
LastName
FROM Users
WHERE UserID = #request.UserID#
Good:
SELECT FirstName,
LastName
FROM Users
WHERE UserID = #request.UserID#
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:
SELECT FirstName,
LastName
FROM Users
WHERE UserID = #form.UserID#
Good:
SELECT FirstName,
LastName
FROM Users
WHERE UserID = #form.UserID#
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:
In this
case, request.strDATA should still be treated as an application variable
and locked appropriately.
Using Duplicate(
):
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]
[/auto lock]
1B.
[auto
exclusive lock]
[/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]
[/auto
exclusive lock]
2B.
[auto
exclusive lock]
[/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.
|