Using an if statement within a transaction to delete multiple objects - ruby-on-rails

I'm a junior dev trying to write code that separates out beeps that are owned by a user and beeps that another user is authorized for.
The beeps come from a device that a user owns. An owner can authorize other users to use that device and receive their own beep alerts. This gives us two different types of beeps: owned beeps and authorized beeps. I want the authorized user to be able to delete multiple beep events at the same time and only their own, while I want the owner to be able to delete multiple beeps and those beep's corresponding events.
The application I'm working on is very large and architecturally has been set up with this functionality for the owner to delete only single beeps.
The beep events go through a method that talks to a separate application that handles the deletion of events through the device_broker method.
What I ended up doing was a transaction with an if statement and then looping through an each iterator, checking each beep that's passing through if it was owned or authorized by the user.
My problem is that my broker_mock test isn't receiving any arguments. When I use a debugger on the code, the methods all seem to be working, so I'm very puzzled. Is using an if statement with an each loop a good idea? Also wrapping it all in a transaction means that if one tiny thing breaks, it doesn't work. Is there a better alternative?
Thanks for any wisdom or light you can shed.
beeps_controller.rb
def bulk_deletion
BulkBeepRemover.run!(params[:beep_ids], current_user)
end
bulk_beep_remover.rb
class BulkBeepRemover
class Unauthorized < StandardError; end;
attr_reader :beep_ids, :user, :beeps
attr_accessor :owned_beeps, :unowned_beeps
def self.run!(beep_ids, user)
new(beep_ids, user).run!
end
def initialize(beep_ids, user)
#beep_ids = beep_ids
raise Unauthorized if user.nil?
#user = user
end
def beeps
#beeps ||= Beep.finished.find(beep_ids)
end
def device_broker
#device_broker ||= $device_broker
end
attr_writer :device_broker
def run!
#handles deletion of the owned beeps
Beep.transaction do
beeps.each do |beep|
if beep.device.is_owner?(user)
beep.destroy
remove_beep_event(beep.id)
end
# elsif beep.device.is_authorized?(user) logic goes here
end
end
end
private
def remove_beep_event(beep_id)
device_broker.publish('beep_deleted', { beep_id: beep_id })
end
dings_spec.rb
describe "POST /clients_api/beeps/bulk_deletion" do
let(:user_id) { user.id }
let!(:shared_beep_1) { create(:beep, id: 33, device: authorized_device, state: :completed) }
let!(:shared_beep_2) { create(:beep, id: 34, device: authorized_device, state: :completed) }
let!(:owned_beep_1) { create(:beep, id: 35, device: owned_device, state: :completed) }
let!(:owned_beep_2) { create(:beep, id: 36, device: owned_device, state: :completed) }
before do
post "/clients_api/beeps/:beep_id/bulk_deletion", default_params.merge(beep_ids: [shared_beep_1.id, shared_beep_2.id, owned_beep_1.id, owned_beep_2.id], user_id: user.id)
allow_any_instance_of(BulkBeepRemover).to receive(:device_broker).and_return(broker_mock)
end
context "the owner's array of beep ids" do
let(:beep_id) { owned_beep_2.id }
it "deletes the beeps" do
expect { Beep.find(beep_id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it "publishes the owner beep_deleted event" do
expect(broker_mock).to have_received(:publish).with("beep_deleted", { beep_id: beep_id })
end
end
end
end

It looks like your /clients_api/beeps/:beep_id/bulk_deletion route doesn't take a :user_id argument. As such, there's no parameter for the user_id arguments to be passed.
Also, the parameter name :beep_id is mismatched from the usage in the test, which calls it :beep_ids; this may or may not play an issue, but it's worth making sure that the names are aligned to avoid conflicts.
You asked about the if statement inside an each block, and there's no problem with that at all; this is very common in Ruby code. You've done just find with the if statement.
Regarding the transaction, you can protect the transaction block with a rescue, but whether you should depends entirely on a few consideration.
1) The purpose of the transaction block is important, and not at all obvious from the code. You'll have to determine if the transaction is strictly for efficiency in updating the database (to reduce the number of SQL calls), or if it's to maintain transactional integrity.
2) You have to find out whether using rescue in this way is considered acceptable practice in your organization; it's very much verboten in some organizations. When it's not allowed, you must take steps to ensure that the exception that occurs will be avoided before it can happen. One such way is to query whether it's safe to perform an operation prior to performing it, and avoid doing so if it's not safe.
3) You have to know whether it's acceptable to disregard deleting beeps within the transaction, for any reason, without giving proper feedback; or, on the flip side, determine what the proper feedback mechanism is. Processes that silently fail are not well-prized. For instance, if someone asks for a beep to be deleted, and the deletion returns successfully, it can be assumed that the beep has been deleted; if not, the user of the process should be informed in some way that it was not, so that they may take appropriate action, even if that action is simply relaying the message upstream.
Realistically, with questions about the use of the transaction and whether the if can (or should) be used, you may be treading into software design territory, and you may need clarification on the goals and restrictions that you have to work with. This is life of a software developer; every simple question has a number of complex considerations. Learn to recognize and deal with them appropriately, and you'll be well ahead of the pack!

Related

Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails 6)

For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. Below the destroy_all, I call another method, get_car_nums_not_deleted_from_portal, which looks like the following:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Here, I check to see if any cars were not deleted during the destroy_all transaction. If there are any, I just add a puts message. I also return the ActiveRecord::Collection whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
How can I force just one record to fail when my code executes the destroy_all, WITHOUT adding extra code to my Car model (in the form of a before_destroy or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(#location.name.upcase, from: 'Location')
select(#track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{#first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
One way to "mock" not deleting a record for a test could be to use the block version of .to receive to return a falsy value.
The argument for the block is the instance of the record that would be :destroyed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil, which would indicate a "failure" from the :destroy method.
In this example, we check for the record of the first Car record in the database and return nil if it is.
If it is not the first record, we use the :delete method, as to not cause an infinite loop in the test (the test would keep calling the mock :destroy).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
You could create a method that accepts a list of records and decide which one you want to :destroy for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult record that must always refer to a valid Car and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT). You could write a test that creates the RaceResult records for some of your Car records:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all calls each record's destroy method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).

Send email to only certain Users, Rails 5

I'm having trouble sending an email blast to only certain users who have a boolean set to true and to not send the email to those users who have it set to false.
In my app I have Fans following Artists through Artists Relationships. Inside my ArtistRelationship model I have a boolean that fans can set to true or false based on if they want email blasts from Artists or not when the Artist makes a post.
So far, I have this:
artist.rb
class Artist < ApplicationRecord
def self.fan_post_email
Artist.inlcudes(:fans).find_each do |fan|
fan.includes(:artist_relationships).where(:post_email => true).find_each do |fan|
FanMailer.post_email(fan).deliver_now
end
end
end
end
posts_controller.rb
class Artists::PostsController < ApplicationController
def create
#artist = current_artist
#post = #artist.artist_posts.build(post_params)
if #post.save
redirect_to artist_path(#artist)
#artist.fan_post_email
else
render 'new'
flash.now[:alert] = "You've failed!"
end
end
end
I'm having trouble getting the fan_post_email method to work. I'm not entirely sure how to find the Fans that have the boolean set to true in the ArtistRelationship model.
You want to send mails to fans of a particular artist. Therefore you call
#artist.fan_post_email
That is you call a method on an instance of the Artist class. Instance methods are not defined with a self.[METHOD_NAME]. Doing so defines class methods (if you where to call e.g. Artist.foo).
First part then is to remove the self. part, second is adapting the scope. The complete method should look like this:
def fan_post_email
artists_relationships
.includes(:fan)
.where(post_email: true)
.find_each do |relationship|
FanMailer.post_email(relationship.fan).deliver_now
end
end
end
Let's walk through this method.
We need to get all fans in order to send mails to them. This can be done by using the artist_relationships association. But as we only want to have those fans having checked the e-mail flag, we limit those by the where statement.
The resulting SQL condition will give us all such relationships. But we do it in batches (find_each) in order to not have to load all of the records into memory upfront.
The block provided to find_each is yielded with an artists_relationships instance. But we need the fan instances and not the artists_relationships instances to send the mail in our block and thus call post_email with the fan instance associated with the relationship. In order to avoid N+1 queries (a query for the fan record of every artists_relationships record one by one) there, we eager load the fan association on the artists_relationships.
Unrelated to the question
The usage of that method within the normal request/response cycle of a user's request will probably slow down the application quite a lot. If an artists has many fans, the application will send an e-mail to every one of them before rendering the response for the user. If it is a popular artist, I can easily imagine this taking minutes.
There is a counterpart to deliver_now which is deliver_later (documentation. Jobs, like sending an e-mail, can be queued and resolved independent from the request/response cycle. It will require setting up a worker like Sidekiq or delayed_job but the increase in performance is definitely worth it.
If the queueing mechanism is set up, it probably makes sense to move the call to fan_post_email there as well as the method itself might also take some time.
Additionally, it might make sense to send e-mail as BCC which would allow you to send one e-mail to multiple fans at the same time.

Rails flash messages: is there a good reason they work the way they do?

The Rails 'flash' message system is very useful, but I've always found the details of its implementation make its use awkward.
Specifically, the flash messages are cleared at the end of a response. This means that you have to work out whether they're going to be used in a direct page render or a redirect, then inform the flash system by using 'now' if it's not a redirect.
This seems overly complex and error-prone.
Occasionally I find myself building things that need to exhibit flash-like behaviour, and the process I use is slightly different:
class FlashlikeStore
attr_accessor :session
def initialize(session)
session[:flashlike] ||= []
self.session = session
end
def add(name, data)
self.store << { name: name, data: data }
end
def read
session.delete(:flashlike).to_json
end
def any?
store && store.any?
end
protected
def store
session[:flashlike]
end
end
With a little syntactic sugar in the application helper I can easily add my name-value pairs, and the act of reading it deletes the data. (In this case I'm actually reading this in via AJAJ, but that isn't important here.)
The upshot is that messages are only ever read once, with no need up-front to determine or guess when they're going to appear. They're read when they're needed, then they go away.
One could argue, I suppose, that a call to an object shouldn't both read data and change state, so the 'read' method could be split into a 'read' and a 'wipe' if you wanted to be purist that way. (Although you probably wouldn't be using Rails if you were.)
So the question is this: am I missing something? Is there a compelling reason for the way Rails flash messaging works, with its need for the 'now' method?

Rails balance withdraw action threading issue

To allow users create balance withdrawal request, I have a WithdrawalsController#create action. The code checks if the user has sufficient balance before proceeding to creating the withdrawal.
def create
if amount > current_user.available_balance
error! :bad_request, :metadata => {code: "002", description: "insufficient balance"}
return
end
withdrawal = current_user.withdrawals.create(amount: amount, billing_info: current_user.billing_info)
exposes withdrawal
end
This can pose a serious issue in a multi-threaded server. When two create requests arrive simultaneously, and both requests pass the the balance check before creating the withdrawal, then both withdrawal can be created even if the sum of two could be exceeding the original balance.
Having a class variable of Mutex will not be a good solution because that will lock this action for all users, where a lock on a per-user level is desired.
What is the best solution for this?
The following diagram illustrates my suspected threading issue, could it be occurring in Rails?
As far as I can tell your code is safe here, mutlithreading is not a much of a problem. Even with more app instances generate by your app server, each instance will end up testing amount > current_user.available_balance.
If you are really paranoiac about it. you could wrap the all with a transacaction:
ActiveRecord::Base.transaction do
withdrawal = current_user.withdrawals.create!(
amount: amount,
billing_info: current_user.billing_info
)
# if after saving the `available_balance` goes under 0
# the hole transaction will be rolled back and any
# change happened to the database will be undone.
raise ActiveRecord::Rollback if current_user.available_balance < 0
end

rails - implementing a simple lock to prevent user's from editing the same data concurrently

I have an app where I need to prevent users from editing data while it is being edited by a different user. I'm trying to think of the best way to do it and wanted to ask for ideas. So far, I've created a settings model that stores application wide configuration on the db in key/value pairs. So, for the lock, I have a settings instance that's called LOCKED_TABLE_UID, and it stored the user_id of the user editing the table or null (nil) if the table is free.
>> lock = Setting.find_by_key('LOCKED_TABLE_UID')
Then, I implemented 2 methods in my application controller to acquire and release the lock:
# current_user returns the user currently logged in
def acquire_lock
lock = Setting.find_by_key("LOCKED_TABLE_UID")
if lock.value
# if lock taken, see if it's the current_user or someone else
if lock.value.to_i == current_user.id.to_i
return true
else
return false
end
else
# lock is free, assign it to this user
lock.value = current_user.id
return true if lock.save
end
end
def release_lock
lock = Setting.find_by_key("LOCKED_TABLE_UID")
if lock.value
# the lock belongs to current_user, so he can release it
if lock.value.to_i == current_user.id.to_i
lock.value = nil
return true if lock.save
else
# not your lock, go away
return false
end
else
# lock is free, quit bugging
return true
end
end
What I want is to create some kind of block code that contains the locking mechanism, something like this:
def some_crud_action
requires_locking do |lock|
if lock
# do some CRUD stuff here
else
# decline CRUD and give some error
end
end
end
I'd appreciate help on this - but I'm also open to other suggestions on how to accomplish all that, or some things I may have overlooked. This lock doesn't have to be atomic, but fairly basic and most important - that it works :)
thanks.
Have you seen the ActiveRecord built-in locking feature?
Optimistic Locking
Pessimistic Locking
You're almost there. Create your require_locking? action as you see fit. Then process it with a before_filter.
before_filter :requires_locking?, :only => [:update, :destroy]
after_filter :release_lock, :only => [:update, :destroy]
def requires_locking do |lock|
unless acquire_lock
lock = Setting.find_by_key("LOCKED_TABLE_UID")
user_with_lock = User.find(lock.value)
flash[:message] = "Action denied: Table locked by: #{user_with_lock.name}"
redirect_to :back
end
end
I like the idea but see a big problem with your solution, and it is that you are getting and releasing locks of entire tables.
For a very small app that could be fine, but imagine if you have thousands of users trying to access to, say, the 'PRODUCTS' table and having to wait because someone is editing an entry totally unrelated to their own products.
Maybe you could have a finer grain approach and lock particular rows instead of tables. Then the lock would include the table name, row ID and user ID.
I think acts_as_lockable_by gem is exactly doing what you asked for in simpler terms and less code. It's easily integrable with rails or even a bare ruby project.
With this gem, you get atomic lock, unlock and renew_lock methods. Also, you get an auto expiring ttl locks, so if shit hits the fan and you could not unlock the resource it will be automatically unlocked for you!

Resources