"What users are online?" function

Ok, so you guys know the drill. I have users they log onto the site. I need a way to determine which users are currently logged onto the site. What is the "Rails" way to do this. I am willing to use whatever method is considered best practice.

I have found this article:

http://matt-beedle.com/2006/12/13/rails-how-to-find-out-who-is-online/

This seems pretty straight forward. Is this essentially the accepted standard for how to do this?

The gist of it is reading the information out of the sessions column. I already use sessions and the active_record store so basically it just means:

def who_is_online   @whos_online = Array.new()   onlines = CGI::Session::ActiveRecordStore::Session.find( :all, :conditions => [ 'updated_at > ?', Time.now() - 10.minutes ] )   onlines.each do |online|     id = Marshal.load( Base64.decode64( online.data ) )     @whos_online << User.find(id)   end   return @whos_online end

My question is how often is the "updated_at" field updated? Only when I log in? Do I need to update the field myself before every controller action? This method requires me to get a list of id's and then go through and do a query for the information of every single user.

Basically, I am just curious if this is the smart way and be done with it or if I am missing something.

updated_at is updated every time you save your model.

because of the stateless nature of HTTP you cant know who is online. the last request was made.

the simplest way to track if a user is logged in is to add a reoccurring ajax call to your layouts. that way as long as their on your site your app will be notified every x seconds. when this request comes in go into the session and if there is user info there get the user object and save it. now you can do something like

User.find(:conditions => ['updated_at > ?', 5.min.ago])

Note that this is very resource intensive and should be avoided if at all possible.

It might even be worth while to create an active_users table that stores user_id and an updated_at to track who did what how long ago which may reduce the overhead though not by much. consider this a optimization to try if you need it and not before

-K

That technique loops over the entire sessions table and unmarshalls each record. That's expensive.

These are alternative approaches. They assume it is enough for your purposes to define "being logged in" as having done a request within some time window:

1. The users table has a timestamp column seen_at that is updated each time a user makes an authenticated request. There you count the number of users whose seen_at belongs to the time window in a single query. This approach works with any session storage.

2. If you have a sessions table add to it a user_id column and maintain it with a callback in the Session model. There you count the distinct non NULL user_ids whose session updated_at belongs to the time window. That's again a single query.

-- fxn

Thanks for your input so far. If anyone has more suggestions keep them coming but I am interested in your option 1 Xavier.

Essentially, add a "seen_at" field to user and then update it every time they do something that requires being logged in. Then I could just query:

User.find(:conditions => ['seen_at > ?', 5.min.ago])

and to update seen_at, I could simply add a before_filter in my application controller that checks if there is a current user and if there is simply update their 'seen_at' attribute.

Does that cover the basics of the idea? I think I like it. Would any others agree this is a good approach or am I missing a major downside to this method?

Xavier Noria wrote:

Thanks for your input so far. If anyone has more suggestions keep them coming but I am interested in your option 1 Xavier.

Essentially, add a "seen_at" field to user and then update it every time they do something that requires being logged in. Then I could just query:

User.find(:conditions => ['seen_at > ?', 5.min.ago])

Sure you are aware of this, but just for the archives let me comment that would instantiate all the records as AR objects, that's very expensive in general.

If you were just interested in the number of people online it is much better to ask directly for the count:

    User.count(:conditions => ['seen_at > ?', 5.minutes.ago])

With this solution you can also define a simple predicate

    class User < AR::Base       def online?         seen_at && seen_at > 5.minutes.ago       end       ...     end

Etc. It works quite well.

and to update seen_at, I could simply add a before_filter in my application controller that checks if there is a current user and if there is simply update their 'seen_at' attribute.

Does that cover the basics of the idea?

That's right.

-- fxn

Thank you Xavier. I love this place. I can post a question and have it answered well within a couple of hours.

I understand about the count being cheaper but in my particular case I actually need to display a list of all users that are your friends and who are also online like a buddy list so I think the expense of creating the AR objects may be necessary.

Thanks again though.

Good, then the fine point is to scope the query directly to the user's friends. For example this way:

     class User < AR::Base        has_many :friends, ...

       def online?          seen_at && seen_at > 5.minutes.ago        end

       def friends_online          friends.select(&:online?)        end        ...      end

-- fxn

PS: That implementation is clean but fetches the entire collection of friends. That could make sense depending on the application, otherwise just fetch the online ones scoping with friends.find(...).

Xavier Noria wrote:

       def online?          seen_at && seen_at > 5.minutes.ago        end

seen_at < 5.minutes.ago :wink:

Also, 5 minutes is a bit harsh of a definition of being online. You might want to implement an away status too especially if youre going for the buddy list feel.

def online?   seen_at && seen_at > 20.minutes.ago end

def active?   seen_at && seen_at > 20.minutes.ago end

def away   # something in between end

And don't forget that when a user logs out (or their session gets cleared because they spent too much time away from the app) you might want to nullify seen_at as it would be redundant or at least kind of pointless to store both seen_at and updated_at.

err:

def online?   seen_at && seen_at < 20.minutes.ago end

def active?   online? && seen_at < 5.minutes.ago end

def online? seen_at && seen_at < 20.minutes.ago end

def active? online? && seen_at < 5.minutes.ago end

Oh, I thought because of the code that the first "sentence" was kind of a joke, but looks like it was not :-).

If a user just did a request you certainly want to consider him to be online, right?

     $ script/console      Loading development environment (Rails 2.0.2)      >> seen_at = Time.now      => Fri Jan 18 18:25:30 +0100 2008      >> seen_at > 5.minutes.ago      => true

5.minutes.ago marks a point past in time. A user is online if it has been seen *recently*, so seen_at has to be greater than what you consider to be recent enough. Note that ">" is defined such that today > yesterday.

-- fxn

I recently wrote a follow up to Matt Beedle's blog on my own blog: http://www.williambharding.com/blog/?p=99

A couple differences between his approach and mine:

* I added fields to the session model itself to avoid the Marshalling/ Unmarshalling to find a specific attribute within the session * He doesn't discuss exactly how (or if) he goes about updating which users have been seen when. I discuss how I did it with a before filter that does one SQL Update statement, which is about as efficient as possible * My query for users online is scoped such that it only returns the fields I need (using a :select), and it limits the number of users returned

Generally speaking, I needed a solution that performs as well as possible, since it will be called extremely often (both to update when a user was seen and to find which users have been seen recently). I think that the solution I came up with ought to fit the bill pretty well, though I'd welcome any comments if others have tweaks to my approach.

Bill

I follow a similar approach myself though I generally leave the session model alone.

May I suggest a possible improvement. It can reduce the SQL Update frequency if you only update say every 5 minutes. This can be done in the before_filter or in the SQL statement, whichever you prefer.

Cheers,

-- Long http://FATdrive.tv/wall/trakb/10-Long http://FATdrive.tv/