I've been seeing intermittent errors in a couple of systems I've been working on, when using the same methodology (not the same code) leading me to believe the problem may be linked to creating and using structs in the same request. I'm wondering if it's possible there's a race condition?
The scenario is this:
We're on an e-commerce system, looking at a product, or in some cases a list of products. The code in question is designed to return the images associated with each product, in a struct that we can use for display of said images.
At the beginning of the request, the code looks for database records associated with the item in question. These records represent images for the product(s).
These records are returned in a single CFQuery call (or more accurately, a call to a function which returns the results of a CFQuery call, shaped into a struct containing various info).
The code then loops through the supplied image struct, and adds various information to a Local struct. Later in the request we use the data in the struct to display the images in our <img> tags. We also populate the <img> tag with data- attributes for use with JavaScript.
In the case that any particular image was not correctly returned by the query - usually because the physical file is missing - we use a generic placeholder image. This is done by putting the struct creation inside a try/catch block.
Importantly: this works.
What's happening however, is that very intermittently, when referring to a node in the struct we've created, we find that it does not exist and CF throws an error - this happens maybe 1% of the time and reloading the same page, everything will work perfectly.
I've had this same problem on multiple systems, on multiple servers, on different versions of ColdFusion (8 & 10 to be specific) and using completely different code to achieve similar results.
The first system I saw this issue on, actually used FileExists to check that the image file was available and thus I thought that the problem was probably caused by the bottleneck of the filesystem - I tried many ways around this and eventually eliminated it altogether in the new system - but the problem persists.
The only thing I can think of, is that when creating a struct and then using that struct later in the same request, there's a possibility that a race condition occurs; whereby I refer to a node in the struct before it's finished being created. I'm not using threading here though, so I can't really see how that's possible... I'm out of other ideas.
Some code is below to show what I'm doing, but given that the same issue arises on completely different systems, I think it's the methodology rather than the code that has a problem.
<!--- Get product images --->
<cfset Local.stProductImages = Application.cfcParts.getPartImages(
l_iItemID = Arguments.pid
) />
<!--- Loop through images --->
<cfloop list="#ListSort(structKeyList(Local.stProductImages['item_' & Arguments.pid]), 'text')#" index="i">
<cftry>
<cfset Local['ImageURL_' & i & '_Large'] = Local.stProductImages['item_' & Local.arguments.pid][i].large_watermarked.URL />
<cfcatch>
<cfset Local['ImageURL_' & i & '_Large'] = Application.com.Images.getMissingImages().large />
</cfcatch>
</cftry>
<cftry>
<cfset Local['ImageURL_' & i & '_Med'] = Local.stProductImages['item_' & Local.arguments.pid][i].med.URL />
<cfcatch>
<cfset Local['ImageURL_' & i & '_Med'] = Application.com.Images.getMissingImages().med />
</cfcatch>
</cftry>
<cftry>
<cfset Local['ImageURL_' & i & '_Small'] = Local.stProductImages['item_' & Local.arguments.pid][i].small.URL />
<cfcatch>
<cfset Local['ImageURL_' & i & '_Small'] = Application.com.Images.getMissingImages().small />
</cfcatch>
</cftry>
<img class = "altProdImg<cfif i EQ 'image_03'> endImage</cfif>"
src = "#Local['ImageURL_' & i & '_Small']#"
image = "#i#"
alt = ""
data-imgsmall = "#Local['ImageURL_' & i & '_Small']#"
data-imgmed = "#Local['ImageURL_' & i & '_Med']#"
data-imglarge = "#Local['ImageURL_' & i & '_Large']#"
data-imgnum = "#i#"
data-pid = "#Arguments.pid#"
/>
</cfloop>
The error occurs in the <img> tag, when referring to a node created in the preceding code - Something like:
Element ImageURL_image_02_Large is undefined in a Java object of type class coldfusion.runtime.LocalScope.
But only very occasionally... I'll reload and it'll work perfectly every time.
So... sorry for the epic length of question, but can anybody see how this could occur?
Answer from comments...
The behaviour you describe is symptomatic of not var scoping, so it might be as simple to fix as using index="local.i" in the cfloop tag (you only need the scoping when writing the variable).
Side note: A relatively easy way to check if you're in a function, without going through code, is by throwing an error (i.e. <cfthrow message="where am i?" />) then check the stack trace - if you see stuff like coldfusion.runtime.UDFMethod or $funcSTUFF.runFunction(filename:line) you know you're inside a function (even when the template you're in shows no sign of it).
Related
I wrote the following function:
<cffunction name="check_session_valid" returntype="boolean">
<cfif NOT StructKeyExists(session,"username") OR (len(session.username) EQ 0)>
<script>location.href = 'logout.cfm'</script>
<cfabort>
</cfif>
<cfset session.myApp_start = now()>
<cfreturn true>
</cffunction>
In my .cfm page, I can call that function using
<cfset session_valid = application.lib.check_session_valid()>
OR
#application.lib.check_session_valid()#
What's the difference? Best practice?
Since you asked about best practice, which is a matter of opinion, I think you can improve your function by having it returning either true or false depending on whether or not session.username exists and has a length greater than 0. Then you can use it like this:
<cfif application.lib.check_session_valid()>
code for this condition
<cfelse>
<cflocation href = "logout.cfm">
<!--- note that cfabort is not necessary --->
<cfif>
Regarding your specific question, I think the extra variable, session_valid, is a waste of typing. However, that is simply my opinion.
Not related to your question, I found it curious that you would direct users to a page called logout.cfm. Often users are directed to a page that allows them to log in.
To be honest, both are valid and both would be considered best practice depending on what you are trying to do.
My rule of thumb is if I will need to use the result of a function call more than once, I will set it to a variable
myResult = application.lib.check_session_valid();
If I will only need to use the variable once I would do what Dan mentioned
if( application.lib.check_session_valid() ){
// Do stuff
}
The difference between the examples you showed are
<cfset session_valid = application.lib.check_session_valid()>
This will set the variable named session_valid to whatever is returned from the call to check_session_valid().
#application.lib.check_session_valid()#
This will, in .cfm pages, simply render the value returned from the call to check_session_valid() assuming it is inside of a <cfoutput> tag. There are other places this would also render the value, such as inside a <cfsavecontent>.
Consider the following code:
<cfset local.quiz = getQuiz(param1,param2) />
<!--- returns a struct that has a key called unlock_at --->
<cfset quiz.unlock_at = (quiz.unlock_at EQ '') ? '' : DateConvert('utc2Local',createODBCDateTime(ISOToDateTime(quiz.unlock_at))) />
This is how I origionally wrote my code. When I called the page that ran this code multiple times, the value of quiz.unlock_at was changing in 6 hour increments (the amount of adjustment in the function). When I say increments I mean first page load the time was say 12:00. The next page load 6:00, the next page load 00:00 and so on. The physical time was changing each time. I changed the code to this:
<cfset local.unlock_at = (quiz.unlock_at EQ '') ? '' : DateConvert('utc2Local',createODBCDateTime(ISOToDateTime('#quiz.unlock_at#'))) />
The first thing is I'm storing the value in a local variable and no longer updating the existing struct. The second thing is I am passing in the date to the function as a string and not as the variable. I wasn't sure how it was getting altered. In either case:
How the heck was this changing between requests. I didn't think these variables lives between page requests. All of this code is inside a function that lives inside a CFC. What of any of that data is persistent. I called the page a few times and watched the dates change then opened a NEW browser and the data was altered too. How is that possible?
Is this a scoping issue, a data persistence issue with components? Threw me for a loop and I still and not sure what happened and until I do I'm afraid I may have other issues.
I figured it out. Grrr...
The function getQuiz created a struct and then cached it. The reason being is that it's an API call and I don't want to have to run an API call every time for data that doesn't change that often. So what I was doing was:
<cffunction name="getQuiz">
<cfset local.variable = {} />
<!--- Load Sturct Data --->
<!--- CachePut the variable --->
<cfreturn variable />
</cffunction>
<cfset quiz = getQuiz() />
<cfset quiz.unlock_at = 1 />
<!--- At this point I have actually edited a direct reference to the cached variable quiz even though I tried locally scoping it etc.
So, apparently returning a struct is the same as passing a struct into a function, i.e. it passes the struct by reference not by value. For some reason I thought returning a struct didn't return it as a reference, but it makes sense because that is the way CF passes values between functions.
I have an cfmail function set-up in a particular file, email_output.cfm, which requires an ID passed to it to work properly, like email_output.cfm?ID=1. I want to set up a cron job that runs through a query returning the various needed IDs to pass. In testing, I can do the following:
<cflocation url="email_output.cfm?ID=10" >
But, since cflocation stops all other execution and opens another page, I can't loop through it. How would I pass parameters from a query to a single CF page multiple times?
Thanks - Joe
A custom tag sample implementation of this...
If this is your first time using a custom tag, it's easiest to put it in the same folder as the page calling it. There are a few options for putting it in a different directory, but let's start simple.
EmailMembers.cfm
<cfquery name="GetUIDs">
select userid from users
</cfquery>
<cfoutput query="GetUIDs">
<cf_maileach uid="#userID#">
</cfoutput>
Notice how I called my tag cf_maileach?
In the same directory, place maileach.cfm, see how the names match?
maileach.cfm
<cfif StructKeyExists(attributes,"uid") and val(attributes.uid) gt 0>
<cfquery name="getinfo">
select fname,lname,email
from users
where userID = <cfqueryparam cfsqltype="cf_sql_integer" value="#attributes.uid#">
</cfquery>
<cfmail to="#getinfo.email#" subject="Hi #getinfo.fname#">...</cfmail>
</cfif>
Notes
Depending on your version of cf, and whether you're using application.cfc or not, there are several ways to place a custom tag in an outside directory. There is also <cfmodule>
This is a sample only, something this basic is redundant, I was just trying to mimic what asker outlined. In this sample, I'm calling a query that could get all the data, only to use it to query row by row.
If you're not familiar with <cfqueryparam>, look it up, use it, love it.
Edit: While a CFHTTP method can serve this purpose, it suffers a few problems
Sessions are not automatically passed (even if the requesting server and destination server are the same.).
The page is accessed like a browser request. Application/OnRequestEnd are processed (and since session info is passed as well, this can cause problems trying to access files in secured areas.
Because of the above, the page would need to be in a folder with its own Application file to negate any application files above it in the directory hierarchy.
To combat 1, 2, and 3, You'd need to code in a layer of security, similar to your application's own security, so that the file is not vulnerable should the url be found.
Each call to the file via cfhttp would need to invoke some extra security checking.
It is significantly slower. In a very simple test with a zero-content application.cfc, the custom tag method was executing in literally <= 1/100th of the time. As actual function is added to the method, the difference in results would change.
Here is some sample code to test this yourself.
Contents of folder "safe":
Application.cfc
[ blank file, to negate my testing site's actual application.cfc ]
Testrun.cfm
<cfoutput><cfset starttick = GetTickCount()>
<cfloop from="1" to="20" index="i">
<cfhttp url="http://mysamesite.com/safe/http.cfm?u=#i#" method="get" result="test">
#test.filecontent#<br>
</cfloop>
CFHTTP Execution Time: #(GetTickCount() - starttick)#<br><br>
<cfset starttick = GetTickCount()>
<cfloop from="1" to="20" index="i">
<cf_testtag u="#i#"><br>
</cfloop>
CustomTag Execution Time: #(GetTickCount() - starttick)#<br><br>
</cfoutput>
testtag.cfm
<cfoutput>The ID entered was #attributes.u#</cfoutput>
http.cfm
<cfoutput>The ID entered was #url.u#</cfoutput>
Results (in milliseconds)
Each test was 20 passes at HTTP and 20 Passes at the custom tag.
CFHTTP Tag
661ms 6ms
1624 5
616 5
460 4
522 6
816 4
You can do this by using cfhttp also
<cfquery name="GetUIDs">
select userid from users
</cfquery>
<cfloop query="GetUIDs">
<cfhttp url="http://localhost:8500/cf10/test.cfm?id=#userid#" method="get" result="test">
</cfloop>
I am using CF10. I have a Select:
<cfselect name="company" id="company" query="qcompany" display="placename" value="placeid" queryposition="below">
<option value="0">--Select--
</cfselect>
I have another cfselect that is bound to the first:
<cfselect name="People" bind="cfc:schedule.GetPeopleArray({company})" ></cfselect>
I cannot get the second cfselect to display any results. To test whether or not I am receiving data from my component (which I will display at the bottom), I bound a text box:
<cfinput name="test" bind="cfc:schedule.GetPeopleArray({company})" bindonload="false"/>
This text box is displaying the results of the call to my component every time, but the cfselect never displays anything.
What could I possibly be doing wrong?
I have tried returning arrays and queries from my component. No help. I have tried adding display and value attributes to the second cfselect. No help.
Here is my component:
<cfcomponent output="false">
<cffunction name="GetPeopleArray" access="remote" returnType="array" output="false">
<cfargument name="company" type="string" >
<!--- Define variables --->
<cfset var data="">
<cfset var result=ArrayNew(2)>
<cfset var i=0>
<cfquery name="qEmployee" datasource="texas" >
SELECT 0 as personid,'Select Person' as fullname,0 as sortorder
UNION
SELECT p.personid ,concat(firstname,' ',lastname) as fullname,3 as sortorder
FROM person p
INNER JOIN placeperson pp
ON p.personid=pp.personid
where personstatus='ACT'
and pp.placeid=#arguments.company#
order by sortorder,fullname
</cfquery>
<!--- Convert results to array --->
<cfloop index="i" from="1" to="#qEmployee.RecordCount#">
<cfset result[i][1]=qEmployee.personid[i]>
<cfset result[i][2]=qEmployee.fullname[i]>
</cfloop>
<!--- And return it --->
<cfreturn result>
</cffunction>
</cfcomponent>
Ultimately, you may want use jQuery anyway, but FWIW your existing code worked fine with CF10. (The only change was removing the JOIN for simplicity) So either you are using different code, or there is something else going on we are unaware of ..
Truthfully the Ajax functionality does have some "quirks". However, you should not have any problem with a simple case like this. Aside from adding a text field, what other debugging or troubleshooting steps did you perform? What I usually recommend is:
Test the CFC independently first. Access it directly in your browser with a variety of sample values:
http://localhost/path/to/schedule.cfc?method=GetPeopleArray&company=someValue
I did this with the original code and discovered an error occurred when the company value is not numeric, like an empty string. (I suspect that might have been the problem) You can prevent that error by substituting an invalid ID like 0 instead. Note, be sure to use cfqueryparam to prevent sql injection.
AND pp.placeid = <cfqueryparam value="#val(arguments.company)#"
cfsqltype="cf_sql_integer">
Enable the CF AJAX debugger in the CF Administrator. Then append ?cfdebug to your test script so you can view the console and check for problems/errors.
http://localhost/path/to/yourTestForm.cfm?cfdebug
Again, I did this after tweaking the query. But there were no errors. Your existing cfform code worked perfectly.
Usually those two steps are enough to pinpoint any problems. If not, make sure your Application.cfc file (if you are using one) is not interfering with the Ajax request. That is a common gotcha. Test the code in a separate directory that is outside any Application files.
EDIT: Also, you may as well set bindonload="false" for the select list too. Since you do not want to call the function when the page first loads. Only when the user selects something from the list.
Let's say i've just parsed someone else's XML document which is a response to an API request. I want to know if a value nested deep inside exists. If my API request worked, it will be in the same place every time. If my API request fails, the root of the XML is very different.
If I try <cfif structKeyExists(myStruct.level1.level2.level3, 'myTarget')> on a failed api request, I get the fatal error: Element LEVEL1.LEVEL2 is undefined in MYSTRUCT.
Of course, I could try to depend on the root level of the XML telling me of success or failure, and not looking for the result if it failed, but... barring that solution, what should i do?
Do i need to check for the existence of each level of the struct? As in:
<cfif structKeyExists(myStruct, 'level1')
and structKeyExists(myStruct.level1, 'level2')
and structKeyExists(myStruct.level1.level2, 'level3')
and structKeyExists(myStruct.level1.level2.level3, 'myTarget')>
<!--- ... --->
</cfif>
This is not a real-world problem, this is just something i've faced too many times. Please don't tell me solutions that involve changing the API or solutions like those in the third paragraph.
Thanks!
edit: i should have mentioned why i can't use isDefined() - some of the keys do not have syntactically valid names, so isDefined() throws an error, eg myStruct.level1[42].level3
XMLSearch
I would use the parsed XML document (i.e. xmlDoc) and XMLSearch:
<cfset xmlDoc = xmlParse(responseData)>
<cfset nodes = XmlSearch(xmlDoc, '/level1/level2/level3/myTarget')>
<cfif arrayLen(nodes)>
<!--- do something, you have the "nodes" array to work with too --->
</cfif>
xpath for XMLSearch() assumes the structure keys are nodes. You would need to modify accordingly if, for instance, 'myTarget' is an attribute of a node.
StructFindKey
Another way of doing this would be StructFindKey.
<cfset result = structFindKey(myStruct, "myTarget")>
<cfif arrayLen(result) AND result.path EQ "level1.level2.level3">
<!--- do something --->
</cfif>
Conclusion
Haven't tested, but I believe either will be faster than using IsDefined() or a try-catch block. Has the advantage over XMLValidate() of not needing a DTD. And, even with a DTD, the node you want may be defined as optional, so it could still validate.
You could validate the XML against a DTD to make sure the document was in the right format. XmlParse() and XmlValidate() both take a DTD as a parameter.
<cfset validateResult = XmlValidate(myXmlDocument, myDTD)>
<cfif validateResult.status>
<!--- xml is valid continue processing --->
<cfelse>
<!--- xml did not validate handle the error --->
</cfif>
Personally I wouldn't go crazy checking for every level of a 'deep' structure like this. I would presume that if the top level exists the rest of the document will be as you expect, and I'd just address the document from there.
If you wanted you could perhaps try to address the value in your struct and wrap it in a try/catch. That way you can handle any errors at any 'level' in the same way.
<cftry>
<cfset myVar = myStruct.level1.level2.level3 />
<cfcatch type="any">
<!--- Handle error --->
</cfcatch>
</cftry>
Hope that helps some.
I know I'm going to get booed off the stage here, but this is where isDefined() can save you a lot of typing:
<cfif isDefined(structKeyExists(myStruct.level1.level2.level3)>
<!--- do something --->
</cfif>
I know this is a year old, but I'm going to put in an answer here. I struggled for a good long time with this one, till I found a simple solution. If I know the structure of the XML already, a simple IsDefined works to test if the node or node attribute exists. I don't think most people know you can do this, or have tried and failed because they didn't include single quotes in the IsDefined function.
So say I grab some user xml from a web service somewhere and want to display the user's ID.
<cfhttp url="https://mycompany.com/mywebservices/getusers" username="me" password="mysecret">
<cfset userXMLDoc = XMLParse(ToString(cfhttp.FileContent).trim())>
<cfif IsDefined('userXMLDoc.Data.Record.User.XmlAttributes.id')>
<cfdump var="#userXMLDoc.Data.Record.User.XmlAttributes.id#">
<cfelse>
<cfoutput>Failed: No User ID found</cfoutput>
</cfif>