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.
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
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
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.
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?
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:
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.
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.
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(...).
def online?
seen_at && seen_at > 5.minutes.ago
end
seen_at < 5.minutes.ago
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.
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.
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.
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.