I have a really simple Rails 3 application where users can reserve one of a finite number of homogeneous items for a particular day. I'm trying to avoid a race condition where two people reserve the last item available on a particular day. The model (simplified) is as follows:
class Reservation < ActiveRecord::Base
belongs_to :user
attr_accessible :date
MAX_THINGS_AVAILABLE = 20
validate :check_things_available
def check_things_available
unless things_available? errors[:base] << "No things available"
end
def things_available?
Reservation.find_all_by_date(date).count < MAX_THINGS_AVAILABLE
end
end
The reservation is being created in the controller via current_user.reservations.build(params[:reservation])
It feels like there is a better way to do this, but I can't quite put my finger on what it is. Any help on how to prevent the race condition would be greatly appreciated.
Not sure this answers your question, but it might point you towards a solution:
http://webcache.googleusercontent.com/search?q=cache:http://barelyenough.org/blog/2007/11/activerecord-race-conditions/
(the original site seems to be down so that's a link to the google cache)
The conclusion on that page is that optimistic locking and row level locking are not solutions for race conditions on create, just on update.
The author suggests reimplementing find_or_create with a db constraint.
Another suggestion is that switching the transaction isolation level to 'serializable' ought to work but there's no information on how to do that in Rails.
Just use any locking mechanism like redis locker
RedisLocker.new("thing_to_sell_#{#thing.id}").run do
current_user.create_reservation(#thing) or raise "Item already sold to another user"
end
Related
I'm currently working on a product where for some reaasons we decided to destroy the email column from a specific table and then delegate that column to an associated table using active record delegate method
https://apidock.com/rails/Module/delegate
My question here will be about what is the approach to follow in order to make sure that all the where clauses that uses the email colum are also delegated as well. Because it's basically need a lot of time to check all the places where we used table.where(dropped_column: value)
Example
class Station < ActiveRecord::Base
has_one :contact
delegate :email, to: :contact
end
class Contact < ActiveRecord::Base
belongs_to :station
end
I know that it is possible to switch all the queries from Station.where(email: value) to Station.joins(:contact).where('contacts.email': value) but this approach will take very long and also where clause can be written in many different ways so searching throught the code source and updating all of them is not efficient enough to cover all the cases.
If anyone faced a similar situation and managed to solved in way that saves us time and bugs I will be very glad to hear what are the approaches you followed.
Thanks.
Rails version: '5.2.3'
what is the approach to follow in order to make sure that all the where clauses that uses the email column are also delegated as well
You cannot "delegate" SQL commands. You need to update them all.
I know that it is possible to switch all the queries from Station.where(email: value) to Station.joins(:contact).where('contacts.email': value) but this approach will take very long
Yep, that's what you'll need to do, sorry!
If anyone face a similar situation and managed to solved in way that saves us time and bugs I will be very glad to hear what are the approaches you followd.
Before dropping the column, you can first do this:
class Station < ActiveRecord::Base
self.ignored_columns = ['email']
has_one :contact
delegate :email, to: :contact
# ...
end
This way, you can stop the application from being able to access the column, without actually deleting it from the database (like a 'soft delete'), which makes it easy to revert.
Do this on a branch, and make all the specs green. (If you have good test coverage, you're done! But if your coverage is poor, there may be errors after deploying...)
If it goes wrong after deploying, you could revert the whole PR, or just comment out the ignored_columns again, temporarily, while you push fixes.
Then finally, once your application is running smoothly without errors, drop the column.
Lets put a bit of context on this question. Given an Ecommerce application in Ruby on Rails. Let's deal with 2 models for example. User and CreditCard.
My User is in the system after a registration no issue there.
CreditCard is a model with the credit card information (yes I know about PCI compliance but that's not the point here)
In the Credit Card model, I include a callback after_validation that will do a validation of the credit card against your bank.
Let me put some simple code here.
models/user.rb
class User < ActiveRecord::Base
enum :status, [:active, :banned]
has_one :credit_card
end
models/credit_card.rb
class CreditCard < ActiveRecord::Base
belongs_to :user
after_validation :validate_at_bank
def validate_at_bank
result = Bank.validate(info) #using active_merchant by exemple
unless result.success
errors.add {credit_card: "Bank doesn't validate"}
user.banned!
end
end
end
controllers/credit_cards_controller.rb
class CreditCardsController < ApplicationController
def create
#credit_card = CreditCard.new(credit_card_params) # from Strong Parameters
if #credit_card.save
render #success
else
render #failure
end
end
end
What causing me issue
It look like Rails opens a transaction in ActiveRecord when I'm doing a new. At this point nothing is send to the database.
When the bank reject the credit card, I want to ban the user. I do this by calling banned! Now I realised this update is going to the same transaction. I can see the update, but once the save doesn't go though, everything is rollback from both models. The credit card is not saved (that is good), the user is not saved (this is not good since I want to ban him)
I try to add a Transaction wrapper, but this only add a database checkpoint. I could create a delayed job for the ban, but this seems to me to be overkill. I could use a after_rollback callback, but I'm not sure this is the right way. I'm a bit surprise, I never caught this scenario before, leading me to believe that my patern is not correct or the point where I make this call is incorrect.
After much review and more digging I came up with 3 ways to handle this situation. Depending on the needs you have, one of them should be good for you.
Separate thread and new database connection
Calling the validate function before the save explictly
Send the task to be perform to a Delayed Job
Separate thread
The following answer shows how to do this operation.
https://stackoverflow.com/a/20743433/552443
This will work, but again not that nice and simple.
Call valid? before the save
This is a very quick solution. The issue is that a new developer could erase the valid? line thinking that the .save will do the job properly.
Calling Delayed Job
This could be any ActionJob provider. You can send the task to banned the user in a separate thread. Depending on your setup this is quite clean but not everybody needs DelayedJob.
If you see anything please add it to a new solution of the comments.
I have a Ruby on Rails app where there are Sales Opportunities. Each opportunity is graded by the User according to where they believe it belongs in the sales pipeline. This is set up as an enum currently on the Sales Opportunity model, and the user just selects the appropriate stage from a dropdown list. The stages are "Prospecting", "Qualifying", "Demonstrating", "Negotiating", "Closed Won", "Closed Lost" and "Dormant".
I am now looking to build more sophisticated features for a premium part of the product - specifically I want to be able to add questions to the Sales Opportunity that the user answers - e.g. "Does the prospect have a budget?", if yes: "How much is their budget?" etc.
Depending upon the answers to these questions I want to display a result to the user telling them where in the sales pipeline this opportunity should belong, rather than where it currently is.
I was thinking of adding a qualifying_questions class relationship to Sales_Opportunity.rb:
class SalesOpportunity < ActiveRecord::Base
belongs_to :user
enum pipeline_status: [ :prospect, :qualifying, :demonstrating, :negotiating, :closed_won, :closed_lost, :dormant ]
has_many :qualifying_questions
end
and then creating QualifyingQuestions class:
class QualifyingQuestions < ActiveRecord::Base
belongs_to :sales_opportunity
end
But this is where I'm getting stuck in my mind. Each question is likely to have different characteristics - e.g. in my example above the first questions's answer is a boolean (do they have budget?) whereas the next (assuming the answer was yes) is either a fixnum ($20,000 for example) or a selector saying "unknown", or if they don't have a budget (answer no to the first question) I want to ask "is there a process to allocate budget?" etc.
I could create a class for each individual question I ask, and set the data types accordingly - but this seems to be massively bloating the code, and it means my questions need to be hard-coded into the app. Ideally in future I'd like to allow my Users to define their own questions for each stage in the sales process, so I should try and design it to be flexible for the future.
I'm sure there's a straightforward way for me to achieve this, but I'm not sufficiently experienced to know it yet. Can anyone help point me in the right direction please?
To preserve data integrity, I need to prevent some models from being modified after certain events. For example, a product shouldn't be allowed to be written off after it has been sold.
I've always implemented this in the controller, like so (pseudo-ish code):
def ProductsController < ApplicationController
before_filter require_product_not_sold, :only => [ :write_off ]
private
def require_product_not_sold
if #product.sold?
redirect_to #product, :error => "You can't write off a product that has been sold"
end
end
end
It just struck me that I could also do this in the model. Something like this:
def Product < ActiveRecord::Base
before_update :require_product_not_sold
private
def require_product_not_sold
if self.written_off_changed?
# Add an error, fail validation etc. Prevent the model from saving
end
end
end
Also consider that there may be several different events that require that a product has not been sold to take place.
I like the controller approach - you can set meaningful flash messages rather than adding validation errors. But it feels like this code should be in the model (eg if I wanted to use the model outside of my Rails app).
Am I doing it wrong?
What are the advantages of handling this in my model?
What are the disadvantages of handling this in my model?
If I handle it in the model, should I really be using validates rather than a callback? What's the cleanest way to handle it?
Thanks for your ideas :)
It seems like you already have this one covered, based on your question. Ideally a model should know how to guard its state, as data objects are typically designed with portability in mind (even when they'll never be used that way).
But in this case you want to prevent an action before the user even has access to the model. Using a model validation in this case means you're too late and the user has already gone farther than he should by having access to and attempting to write off a product which should never have been accessible based on its sold status.
So I guess the ideal answer is "both." The model "should" know how to protect itself, as a backup and in case it's ever used externally.
However in the real world we have different priorities and constraints, so I'd suggest implementing the change you listed if feasible, or saving it for the next project if not.
As far as using a model callback versus a validation, I think that's a trickier question but I'll go with a validation because you'd likely want to present a message to the user and validation is built for exactly that use (I'd consider this more of a friendly and expected user error than a hostile or security-related one which you might handle differently).
Is that along the lines of what you've been considering?
I'm running into problems implementing statuses for a model. This is probably due to wrong design.
There is a model which has a status. There can be multiple instances of the model and only a few predefined statuses (like: created, renewed, retrieved etc.). For each individual status there is some calculation logic for the model. E.g. model.cost() is differently calculated for each status.
I'd like to have ActiveRecord automatically set the correct model_status_id when saving a model. I think in the ideal situation I could do something like this:
model.status = StatusModel.retrieved
and
case status
when renewed
# ...
when retrieved
# ..
end
Thinking i need to save the status in the model row in the database this is what i've got now:
ModelStatus < ActiveRecord::Base
has_many :models
Model < ActiveRecord::Base
belongs_to :model_status
However this is giving me a lot of issues in the code. Anyone having some good ideas or patterns for this?
What you describe seems like a perfect case for a state machine.
There are many Ruby state machine implementations. You can see a fairly representative list at ruby-toolbox
When defining a state machine you can define several States and transitions. Each transitions taking your model from one state to another executing some code along the way. The DSL for it is usually quite nice.
Your example would look like
model.retrieve!
This will change the mode status from whatever it was to retrieved or throw an exception if current status doesn't transition to retrieved.
Why not keep the status part of the actual model? If they are predefined, that's not too much work:
class Model < ActiveRecord::Base
STAT_CREATED = 1
STAT_RENEWED = 2
STAT_RETRIEVED = 4
validates_inclusion_of :status,
:in => [1, 2, 4]
def created?
status & STAT_CREATED
end
def renewed?
status & STAT_RENEWED
end
def retrieved?
status & STAT_RETRIEVED
end
end
This way, you could either test the model instance directly (e.g. if #model.created?) or write your case statements like that:
case #model.status
when Model::STAT_CREATED
...
when Model::STAT_RENEWED
...
Also try taking a look at the acts_as_state_machine plugin. I recently used it on a project and it worked well.