Ask Ben: Keeping Parallel Sessions Alive In ColdFusion
Thanks for the Reply. If we are accessing the session variable via URL/FORM whether we can extend the session timeout. My doubt is how can we extend the timeout of the session variable that declared in the first application from second application (After redirecting to the second application, if the user taking more time to come back to the first application session will timeout).
This is part of a back-and-forth I have going on, so sorry if you are not getting the full picture, sorry. Basically, what is going on is that this person has two ColdFusion applications - AppA and AppB - both of which have their own Application files and separate session management. A user is jumping from one application to the other and then back and the concern here is, how can we keep both sessions running in parallel, ensuring that one doesn't time out while the user is in the other.
In ColdFusion, you can't really reach into the sessions or memory space of other ColdFusion applications. There's probably ways of doing it via session pointers and what not, but that all feels hacky to me. I have a solution, that is also a tad big hacky, but just feels safer. What we have to do is create a "heart beat" URL for one of the applications (or both if need-be). The "heart beat" URL is a URL that can be pinged regularly for the specific purpose of making sure a given session does not die (aka timeout).
One of the awesome features of ColdFusion is that we can associate a page request to a given session by passing along CFID and CFTOKEN values in the query string. Once we embrace this, all we really need to do is be able to access the "heart beat" URL of one application from another AND send along the original application's CFID and CFTOKEN values. While this might sound complicated, it's actually pretty easy.
To demonstrate this idea, we are going to create two ColdFusion applications in parallel directories - AppA and AppB. AppA has the following Application.cfc ColdFusion component:
<cfcomponent
output="false"
hint="Handle the application level events.">
<!--- Set up the application. --->
<cfset THIS.Name = "CrossPing - AppA" />
<cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 10, 0 ) />
<cfset THIS.SessionManagement = true />
<cfset THIS.SessionTimeout = CreateTimeSpan( 0, 0, 0, 10 ) />
<cfset THIS.SetClientCookies = true />
<!--- Set up the page request. --->
<cfsetting
requesttimeout="10"
showdebugoutput="false"
enablecfoutputonly="true"
/>
<cffunction
name="OnSessionStart"
access="public"
returntype="void"
output="false"
hint="Fires when the session is first created.">
<!---
When the session first starts, we are going to
create a time stamp for the session initiation.
That way, when we pop back into the app, we can
see what the time difference is.
--->
<cfset SESSION.DateInitialized = Now() />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
Not a whole lot going on here. Notice that the SESSION TIMEOUT is only 10 seconds. This is going to be important when we are playing around inside of AppB in parallel. Also notice that when the session starts, we are storing the date and time for the session initialization. This way, we will be able to tell if the current page request started a new session or merely continued an existing session.
AppA's index.cfm just outputs the intialization timestamp as well as the current timestamp so we can see what is going on:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Cross Application Ping - Application A</title>
</head>
<body>
<cfoutput>
<p>
Application A
</p>
<p>
Session Started:
#TimeFormat(
SESSION.DateInitialized,
"hh:mm:ss TT"
)#
</p>
<p>
Current Time:
#TimeFormat(
Now(),
"hh:mm:ss TT"
)#
</p>
<!---
Provide a link to the App B. We need to send
along the current session information for the
"heart beat" bing action. We have to be careful
NOT to send CFID/CFTOKEN as names otherwise that
will try to hook into the AppB session management,
which is not relevant to us. Therefore, we are
going to send them as AID and ATOKEN.
--->
<cfset strAppBURL = (
"../AppB/index.cfm?" &
"AID=#SESSION.CFID#&" &
"ATOKEN=#SESSION.CFTOKEN#"
) />
<p>
<a href="#strAppBURL#">Jump To App B</a>
</p>
</cfoutput>
</body>
</html>
The important thing to see here is that in the index file of AppA, we are providing a link to the index file of AppB. That link will take us to the new application with a new session. As part of that link, we are passing the CFID and CFTOKEN values of the current app's session (AppA). BE CAREFUL! Do not try to pass it as CFID/CFTOKEN named values, otherwise, AppB will think that you are trying to hook into an existing AppB session. In order to avoid this session conflict, we are going to pass over the CFID and CFTOKEN values a AID and ATOKEN respectively.
Then finally, there is AppA's "heart beat" URL, which we are putting at ping.cfm:
<!--- Kill extra output. --->
<cfsilent>
<!---
Get Base64 encoding of a 1x1 transparent GIF
image to return with our ping. We are breaking it
up into several lines just for code display.
--->
<cfset strImageData = (
"R0lGODlhAQABAIAAAP///////yH5BAEHAAEALAAAAAAB" &
"AAEAAAICTAEAOw=="
) />
<!---
Convert the image to binary and just stream it back
as the ping response. We are doing this because we
are expecting the ping to come from a Javascript
image object.
--->
<cfcontent
type="image/gif"
variable="#ToBinary( strImageData )#"
/>
</cfsilent>
Here, we are just returning a 1x1 transparent GIF as the response body. While I would rather do less work since this is going to be a high-traffic URL, I am comfortable with this as a demo concept. The ping.cfm must be a ColdFusion template so that we can use it to hook into AppA's application and session management.
Ok, so that takes care of AppA. Once we click on the cross-over link in AppA's index page, we will be taken to AppB. AppB is a bit more simple, consisting of just an Application.cfc and an index.cfm page. Here is AppB's Application.cfc ColdFusion component:
<cfcomponent
output="false"
hint="Handle the application level events.">
<!--- Set up the application. --->
<cfset THIS.Name = "CrossPing - AppB" />
<cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 10, 0 ) />
<cfset THIS.SessionManagement = true />
<cfset THIS.SessionTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
<cfset THIS.SetClientCookies = true />
<!--- Set up the page request. --->
<cfsetting
requesttimeout="10"
showdebugoutput="false"
enablecfoutputonly="false"
/>
<cffunction
name="OnSessionStart"
access="public"
returntype="void"
output="false"
hint="Fires when the session is first created.">
<!---
When this session starts, we need to store
the CFID and CFTOKEN that come from the other
application. We don't know that they exist, so
param them first in the URL.
--->
<cftry>
<cfparam
name="URL.AID"
type="numeric"
default="0"
/>
<cfparam
name="URL.ATOKEN"
type="numeric"
default="0"
/>
<!--- Catch any numeric param erros. --->
<cfcatch>
<cfset URL.AID = 0 />
<cfset URL.ATOKEN = 0 />
</cfcatch>
</cftry>
<!---
ASSERT: Whether or not we were passed any ID or
TOKEN values from the other application, we will
definitely have numeric valus for AID and ATOKEN
in our URL scope.
--->
<!--- Store the external ID/Token values. --->
<cfset SESSION.AID = URL.AID />
<cfset SESSION.ATOKEN = URL.ATOKEN />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
Notice here that AppB's session timeout is much longer than AppA's session timeout. This is just to drive home the point that we will be able to hang out in AppB for a while as we test the ability to keep AppA's session living in parallel. The other important point to notice is that when AppB's sessions start, we param the URL values for AppA's incoming session IDs: AID and ATOKEN. We then store these values into the current session so that we can use them in future page requests.
Now, let's take a look at AppB's index.cfm - this is where the magic happens:
<!--- Kill extra output. --->
<cfsilent>
<!--- Param URL values. --->
<cftry>
<cfparam
name="URL.count"
type="numeric"
default="0"
/>
<cfcatch>
<cfset URL.count = 0 />
</cfcatch>
</cftry>
</cfsilent>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Cross Application Ping - Application B</title>
<!---
To demonstrate that the pinging is working, we are
going to refresh this page every few seconds until
we are sure that (should pinging NOT have taken place)
the App A sessoin will timeout.
--->
<cfif (URL.count LT 4)>
<!---
We need to kill some time to see if the App A
session has timed out. Refresh this page again
in 5 seconds.
--->
<cfoutput>
<meta
http-equiv="refresh"
content="5; url=index.cfm?count=#(URL.count + 1)#">
</meta>
</cfoutput>
<cfelse>
<!---
This page has been here (or refreshed) for at
least 20 seconds - twice the session timeout
of the AppA. Now, let's jump back ot App A.
--->
<meta
http-equiv="refresh"
content="0; url=../AppA/">
</meta>
</cfif>
</head>
<body>
<p>
Application B
</p>
<!---
At the bottom of this page, make sure that we ping
the other application (App A) to keep that session
alive. In order to make sure that we can hook into
that session, we have to use the ID and TOKEN values
that App A passed to us.
Also, we only care about doing this IF we have a
valid AID and ATOKEN value set.
--->
<cfif (
SESSION.AID AND
SESSION.ATOKEN
)>
<cfoutput>
<script type="text/javascript">
// Create an image.
var imgPing = new Image();
// Set image src to App A ping url.
imgPing.src = (
"../AppA/ping.cfm?" +
"CFID=#SESSION.AID#&" +
"CFTOKEN=#SESSION.ATOKEN#"
);
</script>
</cfoutput>
</cfif>
</body>
</html>
Mos of the index.cfm file just consists of a mechanism for killing time. We are using the META tag to refresh the page every 5 seconds until we are satisfied that AppA's session should have timed out. What we really need to concentrate on is the Javascript at the bottom of the page. This is where AppB is pinging AppA's "heart beat" URL. The page is creating a Javascript image object and then setting its source to point at the "heart beat" (that is why our ping.cfm returns an image). Notice that as part of the image src, we are passing AppA's CFID and CFTOKEN values in the query string. That is the magic step! That is how AppB can "tie into" AppA's session in such a way that we can let them run in parallel.
Once AppB's index.cfm page has refreshed sufficient number of times, it them forwards itself back over to AppA's index.cfm page where we can see the updated timestamps. If AppB was able to keep AppA's session running via the "heart beat" URL, then these time stamps should be different. If it was not able to keep the session running, then this page request would create a new session and the two timestamps would be just about the same.
Moment, of truth! When we click on the cross-over link (the link from AppA to AppB), we end up getting this output:
Application A
Session Started: 02:49:06 PM
Current Time: 02:49:30 PM
It worked! As you can see, the current timestamp is 24 seconds beyond that of the session intialization. And, since AppA's session timeout is a mere 10 seconds, this could only have been done if the session was being successfully pinged from AppB.
Now, this only works if the pages in AppB get refreshed on consistent basis and specifically, in less time that AppA's session timeout. If you expect someone to be sitting on a single page in AppB for too long, then you can actually set up a Javascript Interval or Timeout that will ping the "heart beat" URL on a regular basis without a page refresh.
Technically, we don't really need to pass the CFID and CFTOKEN values into the ping.cfm URL. The reason for this is that the browser will send along AppA's cookies as part of the ping.cfm request. However, I like to pass along the CFID and CFTOKEN values explicitly to drive home the point that we are really hooking back into AppA's session management via the ping.cfm "heart beat" URL.
Want to use code from this post? Check out the license.
Reader Comments
Of course, a better question to ask is why to use session variables in the first place? They pretty much suck from both a usability and a scalability standpoint.
@Michael,
Just so we are on the same page here, are you talking about SESSION variables in general?
Yep. From a usability standpoint keeping any significant amount of state information in SESSION precludes multiple windows/tabs/workflows into your site.
Also putting a large amount of information in session means that you're in trouble the first time you're digged or slashdotted. Plus you're in trouble from day one if you need to go multi-server as you now need to do a rewrite.
Client var's, OTOH, let you go multiserver at any time w/o needing a LB that allows sticky sessions.
@Michael,
I would argue with you on some of the points:
Yep. From a usability standpoint keeping any significant amount of state information in SESSION precludes multiple windows / tabs / workflows into your site.
I would say that poor application architecture breaks with multiple windows/tabs. This has nothing to do with SESSION scoped variables. I don't see how this has anything to do with SESSION.
Also putting a large amount of information in session means that you're in trouble the first time you're digged or slashdotted
Again, I don't see the connection. What does being Digged have to do with anything. That's like saying you are scewed the second anyone bookmarks your site. I think you are confusing the ideas of HAVING SESSIONS and passing CFID and CFTOKEN values in the URL. The only way I can ever see Digg being a problem is if they link to a URL that has your session information in the URL.... this is VERY different than the idea of having sessions.
Plus you're in trouble from day one if you need to go multi-server as you now need to do a rewrite.
I have never done anything multi-server so I cannot speak one way or the other on this. However, I think you can use Jsessionid or something rather than CFID / CFTOKEN to go across servers... but again, that is not something I know anything about.
And, as far as client variables, those might be easier to work cross-server, but I think they can lead to pseudo memory leaks, especially on high-spider-traffic sites (from what I have heard).
I've gone multi-server using SESSION variables and have had no problems whatsoever (with 20,000 hits a day). There is no rewrite necessary as long as you do it correctly the first time. I don't try to keep a great deal of data in the SESSION variable either (I just feel it's not best practice), but if I had to, it never seemed to impact site usability.
Are there caching issues that you'd have to worry about? Wouldn't you need to send the no cache, etc. headers to ensure the page wasn't pulled from the cache?
Would you also recommend the 1px gif method outlined above to keep a session alive on a page with a form that may take a while to fill out?
For instance, right now if a user is creating a discussion post and needs more time, I let them know via setTimeout about 5 minutes before the end of the session. That should give them enough time to finish their sentence and go to a preview page for more editing. Could one instead use an Ajax request to the 1px image to keep the session alive automatically if they hit a button that says need more time?
Hope that makes sense,
-Lyle
@Lyle,
I used a 1x1 image, but this is more overhead than necessary. You could simply have returned a blank page - you don't really need to return anything at all. I really just used the 1x1 GIF cause my "ping" was performed via a Javascript Image().
However, even with an Image() object, you don't have to return an object - the image just won't be valid. We don't really care about this, so the GIF return was probably overkill.
If you are on a long page, you could certainly do this on a regular basis:
setInterval(
// Every 5 minuites
(1000 * 60 * 5),
// Call this function.
function(){
var objImg = new image();
objImg.src = "YOUR_PING_URL";
}
This will ping the YOUR_PING_URL every 5 minutes.
I use the Image object over the AJAX methodology because we don't really care about any response value (at least not for any sort of validation). If you don't care about the return value, AJAX is just overkill. It has the overhead of creating the XMLHttpObject, which raises cross browser issues). The Image, IMO, is just easy and safe.
);
I am looking to do something similar but I need ad ADMIN user to have the ability to login in a a regular user but the admin's session remains.
Any ideas?