Count current users on the page - ruby-on-rails

I'm trying to count current viewers on the particular page. I need this count to be stored in the DB. The main trouble is to clean up after user leaves the page.
Users are anonymous. Every active user sends AJAX-request every 5 seconds.
What's the best algorithm to do that? Any suggestions?
UPD: I'm trying to reduce amount of queries to the DB, so, I think, I don't really need to store that count in the DB while I can access it other way from the code.

Don't even think about storing this in database, your app will be incredibly slowed down.
So use Cache for this kind of operation.
To count the number of people, I'd say:
assign a random ID to each anonymous user and store it in his session
send the ID within your ajax call
store an Array of Hashes in cache with [{ :user_id, :latest_ping }, {} ] (create a cache var for each page)
delete the elements of the array which appear to be too old
you've your solution: number of users = nb of elements in the array

If you store the users in the database somehow, you could store a last_seen_at field in the users table, and update that with Time.now for every AJAX request that user sends.
To display how many users you currently have, you can just perform a query such as:
#user_count = User.where("last_seen_at < ?", 5.seconds.ago).count
If you want to clean up old users, I suggest that you run some kind of cron job, or use the whenever gem, or something like that, to periodically delete all users that haven't been seen for some time.

I would suggest you create a model that contains a unique key (cookie-id or something) that you save or update with every AJAX heartbeat request.
You then have a session controller that could look like this:
def create
ActiveUser.where(:cookie => params[:id]) || ActiveUser.new
ActiveUser.cookie = prams[:id]
ActiveUser.timestamp = Time.now
ActiveUser.save
end
Your number of active users is then simply a SELECT COUNT(*) FROM ActiveUsers WHERE timestamp > NOW() - 5 or something like that.

Martin Frost is on the right track. There's the #touch method to update last_seen_at: user.touch(:last_seen_at)
But it would be even more efficient to just update the user without having to fetch the model from the database:
> User.update_all({:last_seen_at => Time.now}, {:id => params[:user_id})
SQL (3.1ms) UPDATE "users" SET "last_seen_at" = '2011-11-17 12:37:46.863660' WHERE "users"."id" = 27
=> 1

Related

.order("RANDOM()") with will_paginate gem

I am wondering if there is any way to still use the .order("RANDOM()") with will_paginate so that when a page loads and it orders the pages, all the post will stay the same on each page until the home page is reloaded.
so to have all the posts on localhost:3000/posts?page=1 stay the same until localhost:3000(root_path) is visited again.
Problem is it will paginate posts but it current re orders them for each page selected so you will often see posts on page 1 also on page 2.
One way to do this is to set the random seed which your database is ordering by, such that it returns the same sequence of random numbers each time. You can store this seed in your users' session, and reset it only when you want to. However, there's a complication -- even though setting the random seed produces the same ordering of random numbers each time, there's no guarantee your database will execute it on your rows in the same order each time, unless you force it to do so like so:
SELECT items.*
FROM (SELECT setseed(0.2)) t
, (SELECT name, rank() OVER (ORDER BY name DESC)
FROM foos ORDER BY name DESC) items
JOIN generate_series(1, (SELECT COUNT(*) FROM foos))
ON items.rank = generate_series
ORDER BY RANDOM()
LIMIT 10;
As you can tell, that's quite complicated, and it forces your database to materialize your entire table into memory. It'd work for smaller data sets, but if you've got a big data set, it's out of the question!
Instead, I'd suggest you go with a solution more like tadman suggested above: generate a page of results, store the ids into session, and when you need to generate the next page, simply ignore anything you've already shown the user. The code would look like:
class ThingsController < ApplicationController
def index
#page = params[:page].to_i
session[:pages] ||= {}
if ids = session[:pages][#page]
# Grab the items we already showed, and ensure they show up in the same order.
#things = Things.where(id: ids).sort_by { |thing| ids.index(thing.id) }
else
# Generate a new page of things, filtering anything we've already shown.
#things = Things.where(["id NOT IN (?)", shown_thing_ids])
.order("RANDOM()")
.limit(30) # your page size
# Save the IDs into our session so the above case will work.
session[:pages][#page] = #things.map(&:id)
end
end
private
def shown_thing_ids
session[:pages].values.flatten
end
end
This method uses the session to store which IDs were shown on each page, so you can guarantee the same set of items and ordering will be shown if the user goes back. For a new page, it will exclude any items already displayed. You can reset the cache whenever you want with:
session.delete(:pages)
Hope that helps! You could also use Redis or Memcache to store your page data, but the session is a good choice if you want the ordering to be random per-user.

Count from associate table

I want to have some stats from my app, User can publish post that I call Idee, and I want to know how many user have publish at least one idee.
I'm assuming that should be something like:
#total_user_active = User.where(idee.size >= 1).count
But this doesn't work.
It's user who is in table idee so maybe I should count how much unique user_id are in this column but how?
You can do the following:
#total_user_active = Idee.distinct.count(:user)

Save/update multiple rows in rails

I'm currently working on saving a user social media posts in my app. The basic idea is to check if the post exists if it does update the data or if not create a new row. Right now I'm looping through all of the post that I receive from the social platform so potentially I'm looping through 3,000 and adding them to the database.
Is there a way that I could rewrite this to save all the items at once, which hopefully would speed up the save method?
post_data.each do |post_data_details|
post_instance = Post::Tumblr.
where(platform_id: platform_id).
where("data ->> 'id' = ?", post_data_details["id"].to_s).
first_or_initialize
exisiting_data = post_instance.data
new_data = exisiting_data.merge! post_data_details.to_hash
post_instance.data = new_data
post_instance.refreshed_at = date
post_instance.save!
end
It is good practice to run such long-running jobs via sidekiq or other background jobs solution.
You can also use single ActiveRecord transaction.
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
But keep in mind, that if one of records will be invalid - whole trasaction will be rollbacked.

Querying the cache in rails still runs a call against the DB

I'm probably missing something very simple here, but can't understand what.
I'm trying to cache a simple active record query but every time I touch the cache, it runs the query against the DB again.
Controller Code:
products = Rails.cache.read("search_results")
if products
render :text => products[0].id
else
products = Product.where('name LIKE ?", 'product_name?')
Rails.cache.write("search_results", products)
end
I can see that in my second call I get to the if block and not the else, but any time I'm trying to touch products (like rendering it) I also see an active record call to the DB.
What am I missing?
The line
products = Product.where('name LIKE ?", 'product_name?')
returns an ActiveRecord::Relation, but does not hit the database unless a kicker method is called on it.
While I would still recommend using fetch as mentioned in my comment above, try changing the line to:
products = Product.where('name LIKE ?", 'product_name?').all
which will force the database hit, and save the actual results of the query into the cache, instead of the relation.

Check if the update value differs from database stored one

I'm developing an event calendar, where each hour is a slot that allows users to sign one appointment.
Since there is a feature that allows users to update the slot (and also change date/time) I'd like to perform some check before to update the value, making sure that the slot is free.
So I ended up with a method like this:
if Event.find(:all, :conditions => ["start_at = ? AND event_calendar_id = ?", self.start_at, self.id])
errors.add :base, "You cannot save this appointment in that slot."
return false
end
By the way it creates issues when updating other fields without changing the datetime field, because it finds itself and raises the exception making impossible to update it.
Is there a way I can access database data such as the current id so I can filter out itself from the values, or check if the submitted datetime field is equal to the database one (so i can skip this check).
What's the best way to do this?
Thanks.
P.S. I'm using rails 3.2.3
To exclude self from the results just add a condition to your where excluding its id. Also save some memory and processor time by calling exists?, which just returns true or false instead of fetching an entire row and building a new object:
Event.where( :start_at => start_at,
:event_calendar_id => event_calendar.id ).
where( "id <> ?", id ).
exists?

Resources