How do avoid race conditions in a Ruby on Rails application? - ruby-on-rails

I'm developing a Rails application in which each subdomain has a separate database. And I'm doing something like this.
#app/controller/application_controller.rb
class ApplicationController < ActionController::Base
before_filter :select_database
private
def select_database
MyModel.use_database(request.subdomains.first)
end
end
#app/model/my_model.rb
class MyModel < ActiveRecord::Base
def self.use_database path
establish_connection :adapter => 'sqlite3', :database =>
File.join(RAILS_ROOT, "db", "sqlite3", path)
end
end
Now, in production mode, requests come and execute in this order.
Request "A" comes for subdomain a.example.net and MyModel establishes a connection with database "a".
Now another request "B" comes for subdomain b.example.net and MyModel establishes a connection with database "b".
Now if request "A" tries to do a MyModel.find_*() which database would it access? "a" or "b"?
I believe that this is what we call "thread safety" or "race condition", and if this is so, then how can we avoid it while implementing an application using one database per subdomain?
In the above question, my assumption is that executing two requests simultaneously is a normal behaviour for production servers. Or is there some better approach. I don't have much experience of production servers, so please advice.

It seems like you are using Replications... However I do suggest that you watch the following casts from newrelic to get a better idea on DB scaling:
Scaling Your Database - Part 1
Scaling Your Database - Part 2

If there are two models and two databases,
why not subclass your MyModel for each database?
class MyModelDomainA < MyModel
DBA_PATH = "first/db/path"
def self.use_database
establish_connection :adapter => 'sqlite3', :database => File.join(RAILS_ROOT, "db", "sqlite3", DBA_PATH)
end
end
# then in controller:
def select_database
# or whatever string-manipulation you need to do...
# even have a lookup hash to get the correct model?
model_klass = "MyModel#{request.subdomains.first.capitalize}"
model_klass.constantize.use_database
end
etc.
Obviously this only works if you have a small, fixed number of domain/database pairs.
But if so - it means that anybody coming into DomainA will always access the database for Domain A = no race conditions.
Generally, I find that unless you're hacking kernel code, the solution to a race condition is not thread-safety... but to re-think the problem.

Related

Use specific RAILS_ENV for a single unit test

I have a specific test I'd like to write to check for regressions. I've inherited a Rails environment that had no unit tests and there have been very few guards against what is put into the database via external (outside of Rails) means.
The unit test would iterate over a specific model in the database and render a view, ensuring that every instance could still be rendered correctly in that view. I'd like to run this against a development database instance which is usually a clone of the production database (mysql).
The data is too large to put into fixtures... I'd like to keep the rest of the tests using the 'test' sqlite database.
So - is there a way to override for a single test which database Rails is connected to?
I'm using Rails 3.2.22.
Edit:
Based on Babar's answer, I'm using:
class MyTest < ActionController::TestCase
def setup
#controller = MyController.new
end
test "should successfully render all" do
begin
connection_config = ActiveRecord::Base.connection_config()
ActiveRecord::Base.establish_connection('development')
MyModel.find_each do |i|
print "#{i.id} - #{i.name}\n"
get(:show, {'name' => i.name})
end
ensure
ActiveRecord::Base.establish_connection(connection_config)
# or ActiveRecord::Base.establish_connection(ENV['RAILS_ENV'])
end
end
end
But I get an exception at teardown of trying to rollback. I assume this is because the test was originally wrapped around a transaction, and since I swapped databases, the connection is now closed and we aren't really in a transaction anymore.
What's the most straight forward workaround?
Edit:
Adding this looks to be the most straightforward:
def setup
#controller = DashboardController.new
#use_transactional_fixtures = false
end
There is a class method called establish_connection which can do what you want. Just connect to a different db while running that specific test using ActiveRecord::Base.establish_connection.
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "myuser",
:password => "mypass",
:database => "somedatabase"
)
You can use the database alias from your database.yml file. It would be something like this:
ActiveRecord::Base.establish_connection('development')
You may code it right into your specific test file.
Check the doc: http://apidock.com/rails/v3.2.1/ActiveRecord/Base/establish_connection/class

Multi-domain Rails app with multiple MongoDB's: Choose DB based on the domain?

I have a multi-domain Rails 4 app where the request.domain of the http request determines which functionality I expose a given visitor to. A visitor can sign up through Devise. During the user creation a user.domain field will be filled in with the domain he signed up for, and he will then only have access to this particular domain and its functionality.
Question:
Each domain in my app should be served by its own MongoDB database. If user.domain == 'domain1.com' the user object, as well as all objects that belongs_to it, should be stored in the database that serves domain1.com. How do I set this up?
Complication 1:
I do not only interact with my database through Mongoid, but also through mongo Shell methods like db.collection.insert(). I need to have the correct database connection in both cases. Example:
class ApplicationController < ActionController::Base
before_filter :connect_to_db
def connect_to_db
domain = request.domain
# Establish connection
end
end
Complication 2:
A lot of the database interaction happens outside the MVC context. I need to have the correct database connection in e.g. a Sidekiq context. Example:
class MyJob
include Sidekiq::Worker
def perform(domain)
connect_to_db(domain)
User.all.each do |user|
...
end
end
def connect_to_db(domain)
# Establish connection
end
end
Potentially relevant SO answers:
This answer and this answer suggests that you can apply a set_database or store_in session method on a model level, but this would not be sufficient for me, because models are shared between my domains. Various stragegies have also been discussed in this Google group.

Cross Database Connection with Active Record

I have two websites that each connect to their own unique databases. I need to validate in website 'A' that an email address exists in the website 'B' database. I'm doing the validation as follows:
Called from website 'A's AccountController < ApplicationController class:
config = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
ActiveRecord::Base.establish_connection(config["database B"])
if ActiveRecord::Base.connection.select_values("SELECT "database B".X
FROM 'database B".X WHERE 'database B'.X.email = #member_email")
This call works when I test it in my development and QA environments but fails in my production environment. What appears to happen in production is that the value of the ActiveRecord and also the select get's co-mingled with currently logged-in user's active records, but only in production.
Okay so I've modified my files to the following, based on the feedback. Still not working... Could someone please review the files below and see what step(s) I'm missing? Thanks!
Thanks! I think that is what I did, but I created the 'model', and, being a newbie, I'm not sure if that would normally be generated by Rails...
Still failing, would you mind taking a look at the following and see if you see what I'm doing wrong?
First, this is the 'legacy' database model for the second database that I want to connect to in the existing application (Note that doing the 'Fileload' was the only way I could get this to not error out.):
class MMSDB < ActiveRecord::Base
self.abstract_class = true #important!
config = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
establish_connection(config["deals_qa"])
end
Second, this is the model that calls the 'MMSDB' Model (see above)
class Redirect < MMSDB
def initialize
end
Checking to see if the email address exists in the legacy database, and, if it does, the #increment the redirect count on the # database table 'members'
Do I somehow need to tell the application what table I want to pull from since the table # in the legacy database (members) would be different then in the current application #database (users)
def email_exists?(email)
if find_by_email(email)
user = find_by_email(email)
user.redirect_count += 1
user.save
end
end
end
Then this is the code snippet inside the account controller file.
else
if user == Redirect::User.email_exists?(#email)
#Redirect_Flag = true
else
flash.now[:error] = 'Invalid email or password; please try again.'
end
end
Subclassing ActiveRecord::Base will allow you to make multiple connections to different databases.
module DatabaseB
class Base < ActiveRecord::Base
#abstract_class = true
establish_connection(config["database B"])
end
end
class YourAwesomeModel < DatabaseB::Base
set_table_name "X"
# Use your regular active record magic
end
You will still be able to use your other models with the connection established using ActiveRecord::Base to your primary database.

Rails + Devise + CouchDB with one database per subdomain

I'm building an application that will require CouchDB's mobile syncing feature.
So for each 'account' on the service I want to create a separate CouchDB database instance so that only this account's data is synced.
I'm using CouchRest Model and Devise, which handles subdomain authentication via a separate users database.
However what is the correct way to connect to the appropriate database at runtime for each model?
A before_filter that sets up a named connection, then loops through each model and does something like this: ?
[Post, Tag, Comment].each do |model|
model_server = CouchRest::Server.new(couch_config[:connection])
model_server.default_database = "my_project-#{Rails.env}-#{model.to_s.downcase}"
model.database = model_server.default_database
end
(Pseudocode)
Assuming that the web server (Heroku) runs each request in a separate thread, this should mean that on each request, the database connection is changed dynamically.
Seems like there should be an easier way!
As a solution to question you can override the database method:
class OneDbPerAccountDocument < CouchRest::ExtendedDocument
def self.database
Account.current_database
end
...
end
And then just subclass your models (Post, Tag, Comment) from this class.
class Account < OneDbPerAccountDocument
def self.current=(name)
#current_database = #couch_server.database("my-project_#{name}")
end
def self.current_database
#current_database
end
end
With this trick all you need to do in controller is just call something like
Account.current = request.subdomain
But, beware that this approach will become a little messy when you'll have several thousands of accounts (databases).

What is the best way to do per-user database connections in Rails

What is the best way to do per-user database connections in Rails?
I realize this is a poor Rails design practice, but we're gradually replacing an existing web application that uses one database per user. A complete redesign/rewrite is not feasible.
Put something like this in your application controller. I'm using the subdomain plus "_clientdb" to pick the name of the database. I have all the databases using the same username and password, so I can grab that from the db config file.
Hope this helps!
class ApplicationController < ActionController::Base
before_filter :hijack_db
def hijack_db
db_name = request.subdomains.first + "_clientdb"
# lets manually connect to the proper db
ActiveRecord::Base.establish_connection(
:adapter => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['adapter'],
:host => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['host'],
:username => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['username'],
:password => ActiveRecord::Base.configurations[ENV["RAILS_ENV"]]['password'],
:database => db_name
)
end
end
Take a look at ActiveRecord::Base.establish_connection. That's how you connect to a different database server. I can't be of much more help since I don't know how you recognize the user or map it to it's database, but I suppose a master database will have that info (and the connection info should be on the database.yml file).
Best of luck.

Resources