How do I cache external api response per user in rails? - ruby-on-rails

I have a rails 4.1 application wherein I have a bookings section. In this section I have multiple sub sections such as Hotels, Vacation rentals etc.
In each of these sections I am fetching data from relevant apis which user can sort and filter.
I would like to cache response per user so that once I have got the data, filtering & sorting is quick with cached data and time is not wasted on making another trip to the other site.
However one issue is users can do this even without logging in.
I have setup memcached store which caches the data fine for the first user but, when second user comes data for first user gets overwritten.
How can I cache data per user(logggd/unlogged) ? (I am willing to change the cache store provided I don't have to spend anything extra for it to work)

Rails actually supports per user "caches" out of the box. Its called the session. By default Rails uses cookies as storage but you can easily swap the storage to use Memcached or Redis to bypass the ~4kB size limit imposed on cookies by the browser.
To use Rails with memcached as the storage backend use the Dalli gem:
# Gemfile
gem 'dalli'
# config/environments/production.rb
config.cache_store = :dalli_store
This will let you store pretty much anything by:
session[:foo] = bar
And the session values can be fetched as long as the user retains his cookie containing the session id.
An alternate approach if you want to keep the performance benefits of using CookieStore for sessions is to use the session id as part of the key used to cache the request in Memcached which would give each user an individual cache.
You can get the session id by calling session.id in the controller.
require 'dalli'
options = { :namespace => "app_v1", :compress => true }
dc = Dalli::Client.new('localhost:11211', options)
cache_key = 'foo-' + session.id
#data = dc.fetch(cache_key) do
do_some_api_call
end
You can do this with view fragments and the regular rails low level cache as well. Just be note that models are not session aware.
See:
http://www.justinweiss.com/articles/how-rails-sessions-work/
http://guides.rubyonrails.org/caching_with_rails.html

Related

How to determine how many more requests my app can make before it hits the Twitter API's rate limit?

My application habitually makes a request to the Twitter API for a user's timeline (the user's tweets).
As an aside, I'm using the twitter gem and configured a Twitter client in order to enable my app to make the request:
client = Twitter::REST::Client.new do |config|
config.consumer_key = ENV["TWITTER_CONSUMER_KEY"]
config.consumer_secret = ENV["TWITTER_CONSUMER_SECRET"]
config.access_token = ENV["TWITTER_ACCESS_TOKEN"]
config.access_token_secret = ENV["TWITTER_ACCESS_TOKEN_SECRET"]
end
The endpoint that I'm making a request to -- https://api.twitter.com/1.1/statuses/user_timeline.json -- has a rate limit of 900 requests every 15 minutes.
If I understand it correctly, there are 2 common ways to determine if my app has hit its rate limit -- but neither is what I need:
First Approach:
This is a function I wrote that attempts to make the request for a user's tweets, and if the app has it the rate limit, it'll raise an exception.
def get_tweets_from_TwitterAPI(twitter_handle)
tweets = []
begin
tweets = CLIENT.user_timeline(twitter_handle)
rescue Twitter::Error::TooManyRequests => error
raise error
end
return tweets
end
The problem with that approach is I'd like to find out how many more requests my app can safely make before I even make the request to the Twitter API. I fear that this approach would have my app pinging the Twitter API long after it hit the rate limit, and that would open my app up to punitive actions by Twitter (like my app being blacklisted for instance.)
Second Approach
The second approach is to make a request to this Twitter endpoint --https://api.twitter.com/1.1/application/rate_limit_status.json -- that sends data back about where the application's status for a given rate limit.
But again, this endpoint also has its own rate limit (180 requests every 15 minutes) -- which isn't very high. My app would blow past that limit. In an ideal world, I would like to determine my app's current rate-limit status before I make a request to the API at all. And so:
def start_tweets_fetch
number_of_requests_in_last_15 minutes = WHERE DO I GET THIS NUMBER FROM??
if number_of_requests_in_last_15 minutes <= 900
get_tweets_from_TwitterAPI(twitter_handle)
end
end
I'm imagining I would have to increment some number that I've persisted to my database to keep track of requests to the API. Or is there an easier way?
I can't speak for the gem you are using, but a way to track your request limits without having to additionally call the rate_limit_status endpoint is to examine the X-Rate-Limit-Remaining headers on each API call. I don't know whether that data is available on the Ruby gem you're using, though.
Edit
This is in response to Andy Piper's answer which I think is the simplest way to keep track of the remaining calls.
Assuming you're using this Twitter gem, it looks like each response from the gem will populate a Twitter::RateLimit object with the information from the rate limiting headers like Andy has suggested.
You should be able to access that information like this:
tweets = CLIENT.user_timeline(twitter_handle)
remaining_calls = tweets.rate_limit.remaining
From there you can save that value to check it the next time you want to make a request. How you save it and check is up to you but the rest of my answer may still be useful for that.
Note: I haven't tried this method before but it's one of the first things I would try in your situation if I didn't to permanently store request logs.
One way might to be to use Rails' built in Cache API. This will allow you to store any value you wish in a cache store which should be faster and lighter than a database.
number_of_requests_in_last_15 = Rails.cache.fetch("twitter_requests", expires_in: 15.minutes) { 0 }
if number_of_requests_in_last_15 minutes <= 900
get_tweets_from_TwitterAPI(twitter_handle)
Rails.cache.increment("twitter_requests")
end
Let's break this down
Rails.cache.fetch("twitter_requests", expires_in: 15.minutes) { 0 }:
The fetch method on Rails.cache will attempt to pull the value for the key twitter_requests.
If the key doesn't exist, it will evaluate the block and set the return value as the key's new value and return that. In this case, if the key twitter_requests doesn't exist, the new key value will be 0.
The expires_in: 15.minutes option passed to the fetch method says to automatically clear this key (twitter_requests) every 15 minutes.
Rails.cache.increment("twitter_requests"):
Increments the value in the twitter_requests key by 1.
Notes
By default, Rails will use an in memory datastore. This should work without issue but any values stored in the cache will be reset every time you restart the rails server.
The backend of the cache is configurable and can be changed to other popular systems (i.e. memcache, redis) but those will also need to be running and accessible by Rails.
You may want to increment the cache before calling the API to reduce the chance of the cache expiring between when you checked it and when you increment it. Incrementing a key that doesn't exist will return nil.

How OpenStruct stored in session

I have some controller. In this controller I get OpenStruct object and want to save it to app session. Next code works fine:
session[:info] = OpenStruct.new(first_field: 1, second_field: 'two')
p session[:info] right after this line prints
#<OpenStruct first_field=1, second_field="two">
But after this I do redirect to another controller, and when I write p session[:info] in this controller I get
{"table"=>{"first_field"=>1, "second_field"=>"two"}}
So, why do I get this, and how can I load correct OpenStruct instance?
A session usually consists of a hash of values and a session id,
usually a 32-character string, to identify the hash. Every cookie sent
to the client's browser includes the session id. And the other way
round: the browser will send it to the server on every request from
the client.
You should either serialize your objects before storing them in the session.
session[:info] = OpenStruct.new(first_field: 1, second_field: 'two').to_yaml
and retrieve it using
YAML.load(session[:info])
from the rails documentation
Do not store large objects in a session. Instead you should store them
in the database and save their id in the session. This will eliminate
synchronization headaches and it won't fill up your session storage
space (depending on what session storage you chose, see below). This
will also be a good idea, if you modify the structure of an object and
old versions of it are still in some user's cookies. With server-side
session storages you can clear out the sessions, but with client-side
storages, this is hard to mitigate.
or change your session store from cookie_store to cache_store
In your environment change
config.session_store :cookie_store
to
config.session_store :cache_store

Too many sessions in a rails application

I am building an E-commerce website on ruby on rails from scratch.(This is my first project on ruby on rails)
My product belongs to a subcategory which in-turn belongs to a category.
My filters partial include multiple check-boxes for category,subcategory,additional_category(Like hand made clothes,factory built etc.),lifestyle(relaxed,corporate etc) and cloth_material_type(this has around 30 options)
I am sending 5 arrays for each of these cases to the backend to search through the associations.
Now when a non logged in user reloads the page the filters set by user resets to default.
To avoid this I have four options in mind.
Option 1. Store the filter values set by the user in the cookies which is fast.But it might slow down the user's browser.
Option2 . Store the values in a session using ActiveRecord::SessionStore gem which will increase the size of session for me to 65K but would slow down the application.
Option 3 .Using jquery modify/create document.url options so that every filter option gets appended to the document.url and on reload I get the parameters set by the user for filtering.But this looks very cumbersome to implement.
Option 4. Using gems like rails temporary database etc.
I have opted with option 2 and using session store for the purpose but I think that it will become cumbersome to maintain this in the future.
Just need some suggestions like what do other rails ecommerce websites do to solve this problem or is there any better way to solve this.
Redis
What I'd do is add a layer of abstraction; specifically I think you'd benefit from using Redis, or similar temporary db (as you alluded to in your question).
Redis is a key:value database, which basically stores JSON values for you to use within your app. If you tie it to a model, you'll be able to store temporary values without hindering your app's performance.
I think you could setup Redis to store a guest id, and an array of your values from that:
[guest_user_id] => [
1 => "x"
2 => "y"
3 => ["z", "a", "b"]
]
You'd be able to generate the guest_user_id when you initialize the Redis system, and store it in the user's session. This way, you're only storing minimal data inside your user's browser, and can populate the various controller actions with Redis data:
#config/routes.rb
resources :categories do
resources :subcategories
end
#app/models/user.rb
Class User < ActiveRecord::Base
def self.new_data
# create guest_id and send to Redis
end
end
This will allow you to populate a session with your guest_id if the user is not registered:
#app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
#user = user_signed_in? ? current_user : User.new_data
#You'll then be able to populate their Redis values with the data from the product selection etc
end
end
I could go into more specifics, but as you're only looking for suggestions, this is what I have to recommend at the moment

Migrating sessions from cookie_store to Redis in Rails 3

I need to switch the session store in my Rails 3 app from cookie_store to redis-session-store. There are many reasons for this (security, SSO with other Rails and non-Rails apps). Are there any best practices on how to do it without loosing all current sessions?
What i could imagine is a two steps approach:
Collect all user sessions for N days and store them in the DB or in Redis (update if already stored).
Use stored user sessions to create entries in Redis.
Alternatively, on the fly migration would also be possible. Means read cookies, use secret key to decrypt the session data and store it as a new session in Redis.
I realize this ticket is pretty old, but this may help others. We ended up changing our session store to Redis, but then still looking for the legacy cookie (for a week or two) before no longer respecting them.
There are probably some security concerns to consider before using this strategy - you want to make sure those risks are worth it compared to the cost of having to sign your entire user-base out all at once. With Rails, the cookies are encrypted and can't be tampered with.
Here's what we used:
class SessionsController < Devise::SessionsController
LEGACY_COOKIE_NAME = "_old_session_name".freeze
def new
return if detect_valid_cookie
super
end
private
def detect_valid_legacy_cookie
legacy_cookie = request.cookie_jar.encrypted[LEGACY_COOKIE_NAME].presence || {}
valid_user_id = legacy_cookie['warden.user.user.key'].try(:first).try(:first)
return unless valid_user_id
user = User.find_by(:id => valid_user_id)
return unless user
if sign_in user
request.cookie_jar.delete(LEGACY_COOKIE_NAME)
redirect_to root_path # or whever you want
true
else
false
end
end
end
Stolen from here:
http://www.happybearsoftware.com/almost-protect-yourself-from-cookie-session-store.html (the last two sections)
Basically, use this:
Rails.application.config.action_dispatch.cookies_serializer = :hybrid
Quote follows:
This will cause Rails to accept sessions serialized with Marshal and exchange them for sessions serialized with JSON.
After you're confident that all your users sessions have been converted to JSON, you can roll out another release that flips the config value to :json.
Note: If you're storing complex Ruby objects in the session and need them to be serialized with Marshal, you won't be able to use the JSON serializer.

Rails 3 session & cookie how to persist session id cookie

I am developing a simple shopping cart based on session_id i.e. for determination of a user cart items used session_id.
But when user closes a browser and then opens it again a session id is been changed. And he loses all his items in cart.
I suspect such feature I can provide with saving session id in cookie.
Am I right?
Any way my question is How to provide a functionality that allows users get their cart items even after closing a browser?
I recommend reading the Rails Guide: Action Controller Overview (http://guides.rubyonrails.org/action_controller_overview.html).
Sections 4 and 5 cover Sessions and Cookies and will give you a deeper understanding
on how to use them and will make it easier to tackle future challenges.
I would also check out the Ruby on Rails ActionDispatch::Cookies < Object Documentation
(http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
An example of the controller code you are looking for is listed on this resource. Here is an example:
# Sets a cookie that expires in 1 hour.
cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
cookies[:your_cookie] = {
:value => "your_cookie_value",
:expires => 100.years.from_now
}
for more details, check it out.
Rails stores the session in a cookie by default. You can influence if
the cookie lives beyond a restart of the browser (see below).
Cookies ARE NOT shared between browsers. Ever.
If you want to use Opera to look at the shopping cart you just created in Firefox you need to authenticate to the shop in some way, for example with username and password.
See Rails 3 additional session configuration options (key, expires_after, secure) for a description of all the configuration options for your session.
you'll be wanting to look at :expire_after, for example
:expire_after => 24.hours
(Found here: Setting session timeout in Rails 3)

Resources