A few days ago I posted about how you could use ColdFusion to track the number of sessions in an application. One of themes of that post was how much easier Application.cfc makes this process. If you haven't read the first post, please do so now. I'll wait.
So first lets look at the Application.cfc file:
<cfcomponent output="false">
<cfset this.name = "sessioncounter2">
<cfset this.sessionManagement = true>
<cffunction name="onApplicationStart" returnType="boolean" output="false">
<cfset application.sessions = structNew()>
<cfreturn true>
</cffunction>
<cffunction name="onSessionStart" returnType="void" output="false">
<cfset application.sessions[session.urltoken] = 1>
</cffunction>
<cffunction name="onSessionEnd" returnType="void" output="false">
<cfargument name="sessionScope" type="struct" required="true">
<cfargument name="appScope" type="struct" required="false">
<cfset structDelete(arguments.appScope.sessions, arguments.sessionScope.urltoken)>
</cffunction>
</cfcomponent>
The first method I have is onApplicationStart. Note that, outside of the cffunction and return tags, it is one line. For the heck of it, here is the code from the Application.cfm version:
<cfset needInit = false>
<cflock scope="application" type="readOnly" timeout="30">
<cfif not structKeyExists(application,"sessions")>
<cfset needInit = true>
</cfif>
</cflock>
<!--- Yes, I do need to make it. --->
<cfif needInit>
<cflock scope="application" type="exclusive" timeout="30">
<cfif not structKeyExists(application,"sessions")>
<cfset application.sessions = structNew()>
</cfif>
</cflock>
</cfif>
Which do you prefer? So just to be clear - the onApplicationStart method creates the structure we will use to count the sessions. We could use a list, but a structure makes it easy to insert/delete items.
Moving on - the next method, onSessionStart, handles writing our session key to the Application structure. Again - notice that it is one line. This isn't much different from the Application.cfm file - but - there is one crucial difference. Because ours is in onSessionStart, it gets executed one time only.
The last method, onSessionEnd, handles removing the session key from the application structure. If you remember from the last post I had to handle that myself in the sessionCount UDF. Now I don't need to worry about it. I simply remove myself from the Application structure. Do remember though that onSessionEnd can only access the Application scope via the passed in argument.
Ok - so lastly, let's look at the sessionCount UDF:
<cffunction name="sessionCount" returnType="numeric" output="false">
<cfreturn structCount(application.sessions)>
</cffunction>
Nice, eh? You may ask - wny bother? I could certainly just output the result of the structCount myself. But the UDF is nice. What if I change from using an Application structure to a list? Or what if I store active sessions in the database? The UDF gives me a nice layer of abstraction.
Archived Comments
It appears to be one session off Ray. I hit it with one comptuer and get 0 then i hit it with another computer and get 1. Should be 2.
Impossible. :) Seriously though - not sure how that would happen. onSessionStart fires before the page is displayed. Did you perhaps have a session in use already and therefore onSessionStart didn't fire? I'd consider restarting CF, closing/reopening browsers to be sure.
Yep, i re-started the CF service and it is counting correctly now.
I would like to use something similar to track the user names that are currently logged in.
When a user logs in i set their name as a session variable. Any tips on doing this?
Thanks Ray!
Use another app structure. When a user logs on, store their name there. In onSessionEnd, remove it.
Make sense? Shall I blog an example of that?
I think i got it... i just started a list of users in onApplicationStart called application.users.
Then in my login page i appended the user name to the application.users list.
Then on onSessionEnd i use listFind to find the users name and listDeleteAt to remove the user from the list.
Might be better if i used a structure rather then list.
Thanks Ray!
I think that would be a good example since I'm sure people constantly forget that you need to pass the session structure to onSessionEnd.
>I would like to use something similar to track the user >names that are currently logged in.
>
>When a user logs in i set their name as a session >variable. Any tips on doing this?
Instead of using another one you could technically just rework the original and put a structure in a structure for each user with the token being the key, then modify the name field on login. It shouldn't break the structcount and keeps it all in one place.
DK, I was going to recommend that - but the issue is that you then have to parse the results to get the name list. Ie, "Give me all Values where Value is not 1". Not terribly hard of course, but a bit of a hack. And shoot, maybe you have a username named 1! It could happen. :)
well if I was doing it I would reference it this way:
application.userList = structnew()
application.userList.userid = structnew()
application.userList.userid.sessiontoken = blah
application.userlist.userid.username = blah
That way each is guaranteed to be unique and the top level still gives an immediate user count. Then you could go one deeper and find the token or username or ip whatever else you wanted to store in nested struct and reference in cfloop by collection I suppose.
Another way, if usernames were unique, would be to make the username the top key. You could still do a count without any excess code, then also do 1 structkeylist and get all users online.
Now that i am playing with this idea it has stemmed another question. I am a cfc newbie so forgive me if this is a dumb question.
My application.cfc looks to see if session.username is defined and if it is not it sends the user to a login page. I have a logout action page that basically kills the users session.username variable
StructDelete(session, "UserName");
On my logout page is there a way i can fire the onSessionEnd function in the application.cfc or end the session so the onSessionEnd function fires?
brain fart. userid only works if you are only tracking logged in users obviously. forgot to type that. sorry, dreaming of the end of work coming up shortly :D
Probably should make sessionCount a method of the application cfc, since it's dependent on the internals of the cfc, and because other methods inside the cfc are already directly manipulating the structure. Shouldn't expose internals to outside functions/UDFs.
As far as the goes, I'd make the entire thing into a sessionCounter cfc, called from the appropriate routines in the application (e.g. a HAS A, not an IS A relationship).
That opens up the ability to do, say, a client counter component with the same interface, and and allows you tosubstitute it in if you ever change how your system works, or port it to a new server.
CHad, you can easily call onSessionEnd() from your onRequestStart. But just because you call it - it doesn't mean your session REALLY ends.
You need to think of two things here.
First there is the concept of the Session as ColdFusion understands it. This is: A user from one browser who hits me and never stops hitting me for 20 minutes. (Or whatever number.)
Then there is the DATA in the session. That data is set by you. (Although CF sets a few small things, like urltoken.)
So you can add, edit, delete that crap whenever. So on your logout you remove the username value.
That doesn't END the session. It just changes the data in the session.
So I wouldn't call onSessionEnd. Just do what you want with the data. Let CF worry about calling onSessionEnd.
@Michael: I'm not sure I agree with that. The UDF isn't dependent on the internals of the CFC per se. It is dependent on Application variables, which exist throughout the entire site. I'm probably being picky - and App.cfc is a special case really, but I don't agree with this.
Plus - if you did - how would a template call it?
Thanks Ray, that makes perfect sense.
One last question and i will leave you alone.
Am i safe putting code like this in onSessionEnd?
<cfif listFind(application.users, session.username) NEQ 0>
<cfset userListLocation = listFind(application.users, session.username)>
<cfset application.users = listdeleteAt(application.users, userListLocation)>
</cfif>
Well, since the idea of a component (object) is to encapsulate data and methods, at the very least I'd put the "counting" structure inside the cfc and not throw it out into the application scope. (e.g. this.sessions).
Second, as indicated I'd add a function to application.cfc named sessionCount that returns the count, black-boxing the entire process. So calling application.sessionCount() from the template does the trick, and w/o needing a separate function defined elsewhere.
Finally, and again as mentioned, I'd make a sessionCounter.cfc, instantiate it in init(), and pass through the calls so that a call to application.sessionCount() in turn calls this.sessionCounter.sessionCount().
That allow you at some point to make, say, a clientCounter.cfc with the same interface, and swap out all of the counting-specific code instantly just by creating a different kind of "counter" object. This keeps your application cfc clean, a must if you start adding extra functionality to it like session counting, security and logins, page tracking, and so on.
As far as that goes, creating a "sessionCounter" cfc let's you use the code inside of an application.CFM file as well. Two for the price of one.
If you want to post it I'll do the code tonight as an example...
@Chad: No, that isn't safe, because two sessions could end at the same time. You need to wrap it with a CFLOCK, and a named based lock. You would then need to use the SAME named lock in onSessionStart.
@Michael: You can't add a function to app.cfc and call it as application.functionname. I mean you CAN add your own methods, but they don't exist as Application variables you can call later.
Now that being side - taking the _whole_ process and making an API out of it _could_ be a good idea indeed.
Actually you can add your own functions to the application scope, and they can be methods of the application.cfc. You just have to explicitly expose them in the applicationStart method.
<cfset application.sessionCount=this.sessionCount>
Unfortunately, you can't reference 'this' in the method when you do so, so the struct would have to be an application scope variable as well. (A hack, and pretty much a brain-dead implementation of application.cfc if you ask me.) Better to do as I said, and implement the thing as a separate cfc. So on applicationStart().
application.sessionCounter=createObject('component','sessionCounter').init()
Then call application.sessionCounter.sessionStart() in onSessionStart and call application.sessionCounter.count() in the template where needed.
@Michael - Ah yea - the hack. I thought you meant _w/o_ a hack. (Sorry, I consider that a hack. ;) I just don't think I like that. But - thats just my preference. :)
Wasn't my preference either, which was why I was suggesting the standalone "counter". ;)
I kept having the same problem Chad had in the first comment and decided that restarting CF each time was not a good solution. Here is the code I placed in onRequestStart to fix it:
<cfif NOT structKeyExists(application.sessions,session.urltoken)>
<cfset application.sessions[session.urltoken] = 1>
</cfif>
I think Chad's issue was that he had started a session already before adding that new code. That's why the restart was necessary. The code should work fine under normal circumstances.
Both myself and my client were getting 0 when going to their live website when no other sessions existed. The code I shared above should fix that though.
I'm still going to disagree with you though. :) Question - if you use my code, immediately change this.name to a new value (creating a new application), does it work for you?
Ray,
The problem was actually a bug in the MVC framework I was using. It is all sorted now.
Thanks!
-whew- I knew I was right. ;) OK, I didn't really, but it feels good to say that. :)
G*d I love this blog. I couldn't figure out what I was doing wrong.... I hadn't passed the app scope in as an argument to onSessionEnd. Now all is well.
You guys rock.
Glad this site is still useful to folks. :)