"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/