I have this method inside a model with this code inside. It calls a gem and returns either the object I want or a 404 resource not found. if I do a method on a 404 then I need to rescue it as shown below. If I just use rescue the linter fails. If I do this brakeman fails.
find_object
return_object = Rails.cache.fetch(cache_key + '/variableInsideObject') do
GemClient.find(id).variableInsideObject
rescue HttpServices::ResourceNotFoundError
raise ApplicationController::ExternalServiceError,
"variable inside object not found for id: #{id}"
end
end
How can I rescue this error without failing the linter and brakeman.
Imo this is a more Ruby-esque implementation of this code:
def find_object
return_object = begin
Rails.cache.fetch(cache_key + '/variableInsideObject') do
GemClient.find(id).variableInsideObject
end
rescue HttpServices::ResourceNotFoundError => e
Rails.logger.error(e)
raise ApplicationController::ExternalServiceError,
"variable inside object not found for id: #{id}"
end
end
Of course, it's hard to say without knowing what the linter or brakeman are complaining about exactly.... but this should be better. You don't of course need to use begin end blocks, but sometimes linters/community finds it is neater...
Related
I have a class with following transaction:
# frozen_string_literal: true
class InactivateEmployee
include ServiceResult
def call(id)
begin
ActiveRecord::Base.transaction do
employee = Employee.find(id)
employee.update(is_active: false)
if employee.tasks.any?
employee.tasks.delete_all
end
response(code: 204, value: employee)
rescue ActiveRecord::ActiveRecordError
raise ActiveRecord::Rollback
end
rescue ActiveRecord::Rollback => e
response(code: 422, errors: e)
end
end
end
where ServiceResult is:
# frozen_string_literal: true
# ServiceResult should be included in each Service Class to have a unified returned object from each service
ServiceResultResponse = Struct.new(:success?, :response_code, :errors, :value, keyword_init: true)
module ServiceResult
def response(code:, errors: nil, value: nil )
ServiceResultResponse.new(
success?: code.to_s[0] == '2',
response_code: code,
errors: errors,
value: value
)
end
end
Question 1:
Is this code ok? what could be improved?
Question 2
How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error? i tried sth like that - but it does not work....
before do
allow(ActiveRecord::Associations::CollectionAssociation).to receive(:delete_all).and_return(ActiveRecord::ActiveRecordError.new)
end
Question 1: Is this code ok? what could be improved?
First and foremost, call should not be determining response codes. That wields together making the call with a specific context. That's someone else's responsibility. For example, 422 seems inappropriate, the only possible errors here are not finding the Employee (404) or an internal error (500). In general if you're rescuing ActiveRecordError you could probably be rescuing something more specific.
Does this need to be an entire service object? It's not using a service. It's only acting on Employee. If it's a method of Employee it can be used on any existing Employee object.
class Employee
def deactivate!
# There's no need for the find to be inside the transaction.
transaction do
# Use update! so it will throw an exception if it fails.
update!(is_active: false)
# Don't check first, it's an extra query and a race condition.
tasks.delete_all
end
end
end
Something else is responsible for catching errors and determining response codes. Probably the controller. Generic errors like a database failure should be handled higher up, probably by a default template.
begin
employee = Employee.find(id)
employee.deactivate!
rescue ActiveRecord::RecordNotFound
render status: :not_found
end
render status: :no_content
In ServiceResult you're checking success with code.to_s[0] == '2', use math or a Range instead. The caller should not be doing that at all, but it does because you have a module returning a Struct which can't do anything for itself.
ServiceResult should a class with a success? method. It's more flexible, more obvious what's happening, and doesn't pollute the caller's namespace.
class ServiceResult
# This makes it act like a Model.
include ActiveModel::Model
# These will be accepted by `new`
# You had "errors" but it takes a single error.
attr_accessor :code, :error, :value
def success?
(200...300).include?(code)
end
end
result = ServiceResult.new(code: 204, error: e)
puts "Huzzah!" if result.success?
I question if it's needed at all. It seems to be usurping the functionality of render. Is it an artifact of InactivateEmployee trying to do too much and having to pass its interpretation of what happened around?
Question 2 How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error?
Now that you're not doing too much in a single method, it's much simpler.
describe '#deactivate!' do
context 'with an active employee' do
# I'm assuming you're using FactoryBot.
let(:employee) { create(:employee, is_active: true) }
context 'when there is an error deleting tasks' do
before do
allow(employee.tasks).to receive(:delete_all)
# Exceptions are raised, not returned.
.and_raise(ActiveRecord::ActiveRecordError)
end
# I'm assuming there's an Employee#active?
it 'remains active' do
# same as `expect(employee.active?).to be true` with better diagnostics.
expect(employee).to be_active
end
end
end
end
I ran into a problem, when PG fails out of sync (well known problem)(example).
PG fails out of sync, sequence of id stops incrementing and raises ActiveRecord::RecordNotUnique error.
But all solutions proposed here (all I found) propose some manual solutions - either do some operations in console, either run custom rake task.
However, I find this unsatisfying for production: each times it happens, users get 500, while someone administrating server should operatively save the day. (And according to test data for some reason it possible will occur frequently in my case).
So I would like rather to patch ActiveRecord Base class to catch this specific error and rescue it.
I use this logic sometimes in controller:
class ApplicationController < ActionController::Base
rescue_from ActionController::ParameterMissing, ActiveRecord::RecordNotFound do |e|
# some logic here
end
end
However, here I don't need retry. Also, I would like to not to go deep in monkey patching, for example, without overriding Base create method.
So I was thinking of something like this:
module ActiveRecord
class Base
rescue ActiveRecord::RecordNotUnique => e
if e.message.include? '_pkey'
table =e.message.match(//) #regex to define table
ActiveRecord::Base.connection.reset_pk_sequence!(table)
retry
else
raise
end
end
end
But it most likely doesn't work, as I'm not sure if Rails/Ruby will understand what exactly it asked to retry.
Is there any solution?
P.S. Not related solution for overall problem of sequence which will work without manual command line commands and having unserved users are also appreciated.
To answer the question you're asking, no. rescue can only be used from within a begin..end block or method body.
begin
bad_method
rescue SomeException
retry
end
def some_method
bad_method
rescue SomeException
retry
end
rescue_from is just a framework helper method created because of how indirect the execution is in a controller.
To answer the question you're really asking, sure. You can override create_or_update with a rescue/retry.
module NonUniquePkeyRecovery
def create_or_update(*)
super
rescue ActiveRecord::RecordNotUnique => e
raise unless e.message.include? '_pkey'
self.class.connection.reset_pk_sequence!(self.class.table_name)
retry
end
end
ActiveSupport.on_load(:active_record) do
include NonUniquePkeyRecovery
end
User model has defined indexes to be searched using ThinkingSphinx. However when I stop my searchd deamon, I would like my method to fail gracefully and not throw an error. Normally I do this by using a rescue block for catching exceptions. But in this case, it still throws the error and the puts statement is never executed.
def search_users(key)
begin
search_results = User.search(key,options)
rescue Exception
puts "Hello World!!!"
search_results = []
end
return search_results
end
Following is the error i get:
Riddle::ConnectionError (Connection to 127.0.0.1 on 3201 failed. Connection refused - connect(2)):
Is there any way out?
Solved it.
Add the :populate => true option to your search calls.
Normally, Thinking Sphinx lazily loads search results (allowing for
sphinx scopes and such) - but if you want the rescue to take effect,
then you'll need to force the results to load immediately - hence the
:populate option.
Refer the link posted above for further reading.
Given ruby return semantics, you can compress your code:
def search_users(key)
begin
User.search(key,options)
rescue
puts "Hello World!!!"
[]
end
end
It is evil to rescue Exception. Just use rescue, which rescues StandardError, which captures most of the stuff you want it to. Otherwise you also capture SyntaxError, LoadError, SystemExit and other stuff you don't intend. In this case, rescue Riddle::ConnectionError is appropriate, but not necessary.
I am using Ruby on Rails 3.2.2 and I would like to properly rescue the following process flow by raising a "custom" error message:
def rescue_method
# sample_string.class
# => String
# sample_string.inspect
# => "ARubyConstantThatDoesNotExist"
begin
build_constant(sample_string)
rescue
raise("My custom error message: #{build_constant(sample_string)} doesn't exist.")
end
end
def build_constant(sample_string)
"AModule::#{sample_string}".constantize
end
Note: I feel "forced" to use the constantize method also in the raised "custom" message in order to DRY code...
When the rescue_method is executed it seems that the raise("My custom error message") code is never executed and I get the following error:
uninitialized constant AModule::ARubyConstantThatDoesNotExist
How to properly display the raised "custom" message (since a further error exception is raised in the subsequent raised "custom" message)? What do you advice about?
The problem is that your build_constant method is doing two things:
Building the class name.
Turning the name into a class using constantize.
One of those things wants to use the other when an exception is raised. A simple solution is to pull those separate tasks apart:
def build_class_name(sample_string)
"AModule::#{sample_string}"
end
def rescue_method
name = build_class_name(...)
name.constantize
rescue NameError
raise("My custom error message: #{name} doesn't exist.")
end
You should also be more specific about the exception you're looking for so I added that for free.
If you don't want to rely on catching any exception, you could use safe_constantize (https://apidock.com/rails/ActiveSupport/Inflector/safe_constantize).
Same purpose as constantize but will return nil instead, whenever a Module does not exist.
'UnknownModule::Foo::Bar'.safe_constantize # => nil
begin
"AModule::#{sample_string}".constantize
rescue SyntaxError, NameError => err
raise("My custom error message")
end
How do I pass an error from my Module back to the rake task that called it?
My rake task looks like this:
require 'mymodule.rb'
task :queue => :environment do
OPERATOR = Mymodule::Operator.new
begin
OPERATOR.initiate_call (1234567189)
rescue StandardError => bang
puts "Shit happened: #{ bang} "
end
end
And here is my module..
module Mymodule
class Operator
def initiate_call (number)
begin
# make the call
rescue StandardError => bang
flash[:error] = "Error #{bang}"
return
end
end
end
end
I also call this module from a controller so it would be nice to have an error handling solution that is more or less agnostic.
Running Rails 3. Any unrelated comments (i.e. suggestions) on my code structure are more than welcomed :)
Your Operator#initiate_call method traps StandardError exceptions so your rake task will never see them. I'd drop the rescue from initiate_call and let the caller deal with all the exception handling. Then, you'd have flash[:error] = "Error #{bang}" in your controller's exception handler and the rake task would remain as-is.
The basic approach is to push the error handling up the call stack all the way to someone that can do something about it; initiate_call can't really do anything useful with the exception so it shouldn't try to handle it.