Just when we thought we had conquered it, the VAR scope came back to haunt us.

The Problem

We installed TeaLeaf to improve the usability of our website. While mining the error reports, we found an error that was happening nearly a dozen times a week and had never been reported to our help desk.

We have a Reports application written with Mach-II where Insurance Agents can select from a list and generate HTML or XLS reports on the fly.

The Diagnosis

1. What is the error message?

ColdFusion Error Message Message Cannot insert item with key claimsReported.
Detail This key already exists.

2. In which file did this happen?

The error was occuring inside a Mach-II Listener.

So what do we know about the Mach-II Listener?

  1. It's a ColdFusion Component (CFC) file.
  2. It's setup in the mach-ii.xml file.
  3. It's a singleton.
  4. When it's created, it's cached in the application scope.

3. Which function is throwing the error?

ReportListener.cfc
view plain print about
1<cffunction name="getReports" access="public" returntype="void" output="true">
2    <cfargument name="event" type="MachII.framework.Event" required="true" />
3
4    <cfset var listItem = "" />
5    <cfset var counter = 0 />
6
7    <cfset variables.reports = structNew() />
8
9    <cfloop list="#arguments.event.getArg('reportNames')#" index="listItem">
10
11        <cfset counter = counter + 1 />
12
13        <cfset structInsert( variables.reports, listItem, getReport( arguments.event ) ) />
14    </cfloop>
15
16    <cfset arguments.event.setArg( "displayReports" , variables.reports)/>
17
18</cffunction>

Can you see the error? Does it just jump out at you?

4. Why is reports in the variables scope? Shouldn't it be var scoped?

If the error didn't jump out at you, then read up on the VAR scope.

If I hadn't been writing that series, this would not have been the next step in my thought process. The rest of my thought process would have been:
  • What does this function do?
  • Where does the data come from?
  • Where does the data go?
  • yadda
  • yadda
  • lunch
  • yadda
Thankfully, I was able to skip those steps and realized where the error lie.

What do we know about CFCs placed in the application scope?

  1. You should var scope variables specific to a function.
  2. Anything in its internal variables scope is available to every function in it.
  3. Anything in its internal variables scope is available to and can be manipulated by every thread that calls it.

The Tip-off

Now that we know the solution, what's more interesting is how we were even able to find the error. When we overhauled the site, one of the Coding Standards I asked everyone to follow was to use Struct Functions as much as possible instead of shortcutting through the code.

The error occured on this line:

view plain print about
1<cfset structInsert( variables.reports, listItem, getReport( arguments.event ) ) />

The message was "Cannot insert item with key claimsReported.". claimsReported is the name of one of the reports we list.

The message detail was "This key already exists", which means that the code was trying to add a key to the struct variables.reports that was already in the struct.

But the list we send in always has unique values, so there's no way that this error should occur. Right?

Wrong.

What had been occuring is that two or more report requests would happen concurrently and the value of variables.reports would be manipulated by all of the threads at once.

Thread A asks for "claimsReported,newPolicies,reportE,reportG".
Thread B asks for "cancelledPolicies,claimsReported,reportB,reportC,reportD"
Thread C asks for "newPolicies,reportA,reportD,reportH"

The value of variables.reports would be changed constantly as each thread entered the function and began to loop over its list of reports.

By the time Thread A completed its loop of 4 list items, it's possible that Thread B had started looping over its list, so Thread A could get back a struct with more or less than 4 keys in it.

Innacurate reports returned for Thread A
struct
claimsReported {report data}
newPolicies {report data}
reportB {report data}
reportE {report data}
reportG {report data}

Recreating the error

Let's create a simple function that loops over a list and turns each list element into a key of a struct.

struct_test.cfm
view plain print about
1<cffunction name="structFromList" access="public" output="false" returntype="struct" hint="I create struct keys from list elements.">
2    <cfargument name="myList" required="true" type="string" />
3
4    <cfset var x = 0 />
5    <cfset var counter = 1 />
6    <cfset var reports = structNew() />
7
8    <cfloop list="#arguments.myList#" index="x">
9
10        <cfset structInsert( reports, x, counter ) />
11        <cfset counter = counter + 1 />
12
13    </cfloop>
14
15    <cfreturn reports />
16
17</cffunction>
18
19<cfset theList = "a,b,c,d,e,f,g" />
20
21<cfdump var="#structFromList(theList)#" label="theList">
Output
theList - struct
a 1
b 2
c 3
d 4
e 5
f 6
g 7

Now let's try and cause the same error. We need to try and insert a struct key that already exists.

struct_test.cfm - Error
view plain print about
1<cfloop list="#arguments.myList#" index="x">
2
3    <cfset structInsert( reports, x, counter ) />
4    <cfset structInsert( reports, x, counter + 2 ) />
5    <cfset counter = counter + 1 />
6
7</cfloop>
Output Cannot insert item with key a. This key already exists.

The error occurred in C:\Inetpub\wwwroot\_testing\structs\struct_test.cfm: line 28

view plain print about
126 :
227 :         <cfset structInsert(reports, x, counter) />
328 :         <cfset structInsert(reports, x, counter + 2) />
429 :         <cfset counter = counter + 1 />
530 :

StructInsert requires the struct you're using, which key to create and what its value will be. The optional attribute is allowoverwrite, which is defaulted to false. This is why the error was thrown.

So what would have happened if we hadn't used StructInsert? What if we had used an alternate Struct syntax?

view plain print about
1structName["structKey"]

struct_test.cfm with alternate syntax
view plain print about
1<cfloop list="#arguments.myList#" index="x">
2
3    <cfset reports[x] = counter />
4    <cfset reports[x] = counter + 2 />
5
6    <cfset counter = counter + 1 />
7
8</cfloop>
Output
theList - struct
a 3
b 4
c 5
d 6
e 7
f 8
g 9

See that? No error.

ColdFusion just overwrites the value of reports["a"] (a.k.a. reports.a), no questions asked.

The Solution

So thankfully, this was an easy fix.

Corrected Function
view plain print about
1<cffunction name="getReports" access="public" returntype="void" output="true">
2    <cfargument name="event" type="MachII.framework.Event" required="true" />
3
4    <cfset var listItem = "" />
5    <cfset var counter = 0 />
6    <cfset var reports = structNew() />
7
8    <cfloop list="#arguments.event.getArg('reportNames')#" index="listItem">
9
10        <cfset counter = counter + 1 />
11
12        <cfset structInsert( reports, listItem, getReport( arguments.event ) ) />
13    </cfloop>
14
15    <cfset arguments.event.setArg( "displayReports" , reports)/>
16
17</cffunction>

Now each thread gets its own instance of reports to manipulate. The method is now thread safe.

TeaLeaf & Coding Standards Return on Investment (ROI)

FWIW, we had run Mike Schierberl's varScoper tool over our entire code base before we launched the new site. While it finds unscoped variables that should be in the var scope (unscoped variables are placed in the variables scope by default), it doesn't find explicitly variables scoped variables that should be in the var scope.

We asked the help desk if there had been any errors reported for the Agent Reports section that didn't seem to make any sense. They had been getting calls from Agents saying that from time to time, the reports they selected didn't match the reports they would receive. If they went back and selected the list again, they would get what they requested. It happened so infequently that they never filed a ticket for it.

While the impact of this syntax error was pretty small, in some of our other applications, it could have been critical.

Had we not been using a tool like TeaLeaf, we couldn't have been so proactive in finding unreported errors.

Had we not been using StructInsert, and Struct Functions in general, I doubt we would have ever figured out why this was happening to our Agents.

Sometimes being verbose with your code can be worth much more than the few seconds you would save otherwise. I know some of you newer developers are thinking, "No. That's not true. That's impossible!"

Search your feelings, you know it to be true!