I am using Memcached as an Object Store with my Rails application where I store search results which are User objects in memcached
Now when I fetch the data out I get the Memcached Undefined Class/Module Error. I found a solution for this problem in this blog
http://www.philsergi.com/2007/06/rails-memcached-undefinded-classmodule.html
before_filter :preload_models
def preload_models
Model1
Model2
end
which recommends pre-loading the models before hand. I would like to know if there is a more elegant solution to this problem and are there any drawbacks in using the preloading technique.
Thanks in advance
I had this problem as well and I think i came up with a nice solution.
You can overwrite the fetch method and rescue the error and load the right constants.
module ActiveSupport
module Cache
class MemCacheStore
# Fetching the entry from memcached
# For some reason sometimes the classes are undefined
# First rescue: trying to constantize the class and try again.
# Second rescue, reload all the models
# Else raise the exception
def fetch(key, options = {})
retries = 2
begin
super
rescue ArgumentError, NameError => exc
if retries == 2
if exc.message.match /undefined class\/module (.+)$/
$1.constantize
end
retries -= 1
retry
elsif retries == 1
retries -= 1
preload_models
retry
else
raise exc
end
end
end
private
# There are errors sometimes like: undefined class module ClassName.
# With this method we re-load every model
def preload_models
#we need to reference the classes here so if coming from cache Marshal.load will find them
ActiveRecord::Base.connection.tables.each do |model|
begin
"#{model.classify}".constantize
rescue Exception
end
end
end
end
end
end
Ran across this today, managed to come up with a more terse solution that should work for all classes.
Rails.cache.instance_eval do
def fetch(key, options = {}, rescue_and_require=true)
super(key, options)
rescue ArgumentError => ex
if rescue_and_require && /^undefined class\/module (.+?)$/ =~ ex.message
self.class.const_missing($1)
fetch(key, options, false)
else
raise ex
end
end
end
Not sure why [MemCacheStore] is not having is [MemCacheStore.const_missing] method called and everything getting called in the normal “Rails-y” way. But, this should emulate that!
Cheers,
Chris
Related
Is it possible to specify which find_by! exception is raised in the following example (I want the second one to be raised, not the first):
def self.test
Instance.stubs(:find_by!).raises(ActiveRecord::RecordNotFound)
begin
function_one
rescue ActiveRecord::RecordNotFound
puts 'Failure'
end
begin
function_two
rescue ActiveRecord::RecordNotFound
puts 'Success'
end
end
def self.function_one
Model.find_by!(id: 1)
end
def self.function_two
Model.find_by!(id: 1)
end
*Assume id: 1 does not exist. As in the example, also assume these will be static class functions, but please mention any differences in the case that add any_instance will not be enough for instance methods.
How about this?
Instance.stubs(:find_by!).returns('result').then.raises(ActiveRecord::RecordNotFound)
How can I go about rescuing an error that was generated in an after_save callback, and then ultimately display it to the user? The code in my model looks something like this:
Class MyModel
after_save :call_other_class_responsible_for_parsing
def call_other_class_responsible_for_parsing
# this method is used by multiple models
ModelTwo.parse_css
end
end
In my controller, I currently redirect the user elsewhere if the update was successful, however, I consider the update to be successful if it passed all of the existing validations and there were no errors in the callback (from the Less::Parser).
EDIT:
I mixed up my thoughts in my original question. MyModel gets saved from it's corresponding controller, which then runs the after_save callback from the model. Inside call_other_class_responsible_for_parsing, there is a call to another model, let's say ModelTwo, which does the Less parsing. I've tried using code like this:
def self.parse_css
#my_model = MyModel.find(1)
css_to_compile = Less::Parser.new.parse(css).to_css
rescue Less::Error => error
#my_model.errors[:base] << "Error message"
false
end
end
But the false does not prevent the transaction from succeeding, therefore a redirect happens.
You could use transaction :
def create
MyModel.transaction do
#my_model.save
#my_model.my_method
end
rescue ActiveRecord::RecordInvalid => exception
# rescue active record exception here
rescue Less::Error => exception
# rescue less error here
end
This remove the need for a callback.
I hope this helps!
Callback chains are implicitly wrapped in a transaction. When a callback returns false or raises an exception then the whole transaction is rolled back and saving fails.
In your case, you're parsing some CSS, so I'm not sure whether after_save is the right place for this. I recommend you give validations a try. Consider the following:
class MyModel < ActiveRecord::Base
validate :valid_css
private
def parsed_css
#parsed_css ||= Less::Parser.new.parse(css).to_css
end
def valid_css
parsed_css
rescue Less::Error => error
errors.add(:css, "Cannot parse CSS: #{error}")
end
end
This will parse the CSS before saving the object and add an error if it's invalid. Also, the result of #to_css will be stored so that you won't need to recompute it. This is the approach that I'd recommend.
If you'd like to stick with after_save then you should raise an exception to abort the transaction. In your case, it's simply about not rescuing Less::Error:
class MyModel < ActiveRecord::Base
after_save :compile_css
private
def compile_css
css_to_compile = Less::Parser.new.parse(css).to_css
end
end
If you're having trouble deciding which approach to use then leave a question below and I'll help.
I am wondering on how do you access ActiveJob perform parameters in the resue block, such as
def perform object
end
rescue_from Exception do |e|
if e.class != ActiveRecord::RecordNotFound
**job.arguments.first**
# do something
end
end
Thank You !!
It is possible with arguments inside the rescue_from block:
rescue_from(StandardError) do |exception|
user = arguments[0]
post = arguments[1]
# ...
end
def perform(user, post)
# ...
end
This also works for callbacks as well (e.g. inside after_perform).
I had no idea on that as well, then simply decided to try and use self inside the rescue_from block, and it worked.
rescue_from(StandardError) do |ex|
puts ex.inspect
puts self.job_id
# etc.
end
As a side note -- NEVER ever rescue Exception:
Why is it a bad style to `rescue Exception => e` in Ruby?
You can access all Bindings by ex.bindings. To make sure you get the correct binding for your job you should check the receiver like this1:
method_binding = ex.bindings.find { |b| b.receiver.is_a?(self.class) }
Then you can get all local variables with .local_variable_get. Since method arguments are also local variables, you can at least explicitly fetch them:
user = method_binding.local_variable_get(:user)
post = method_binding.local_variable_get(:post)
So for you example:
def perform object
end
rescue_from Exception do |e|
if e.class != ActiveRecord::RecordNotFound
method_binding = ex.bindings.find { |b| b.receiver.is_a?(self.class) }
object = method_binding.local_variable_get(:object)
# do something
end
end
1. It is still possible that this binding is not the one of perform if you call other instance methods in your job's perform method and the error happens there. This can also be taken in account but is left out for brevity.
I have this piece of code that I am trying to test:
def error_from_exception(ex)
if ex.is_a?(ActiveRecord::RecordInvalid)
...
To get into the if block, I need to pass in the correct ex param.
How do I create an ActiveRecord::RecordInvalid?
With rspec, I'm trying to do something like this:
context 'exception is ActiveRecord::RecordInvalid' do
it 'returns the validation error' do
begin
raise ActiveRecord::RecordInvalid
rescue Exception => ex
binding.pry
###
# From my pry session:
# $ ex
# $ <ArgumentError: wrong number of arguments (0 for 1)>
# $ ex.class
# $ ArgumentError < StandardError
###
end
end
end
How do I find out what argument type the library is looking for?
RecordInvalid link
None of the above methods worked for me so I finally did the following while doing a spec:
class InvalidRecord
include ActiveModel::Model
end
raise ActiveRecord::RecordInvalid.new(InvalidRecord.new)
Hope it helps!
EDIT: This is now possible in Rails 5.1.1. The record argument is no longer required after this commit: https://github.com/rails/rails/commit/4ff626cac901b41f86646dab1939d2a95b2d26bd
If you are on a Rails version under 5.1.1, see the original answer below:
It doesn't seem like it is possible to raise an ActiveRecord::RecordInvalid by itself. If you look at the source code for ActiveRecord::RecordInvalid, it requires a record when initializing:
class RecordInvalid < ActiveRecordError
attr_reader :record # :nodoc:
def initialize(record) # :nodoc:
#record = record
...
end
end
(source: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/validations.rb)
Something you could do to work around this would be to simply create an actual record that is invalid and attempt to save it using save! (such as calling User.new.save! when User.name is required). However, keep in mind that this might potentially become a problem in the future if the model you use is changed and it becomes valid inside your test (User.name is no longer required).
I needed to do something similar to test my code's handling of ActiveRecord::RecordInvalid. I wanted to do
allow(mock_ar_object).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
but that gives ArgumentError: wrong number of arguments (0 for 1) when RSpec tries to instantiate RecordInvalid.
Instead I wrote a RecordInvalid subclass and overrode initialize like this:
class MockRecordInvalid < ActiveRecord::RecordInvalid
def initialize
end
end
allow(mock_ar_object).to receive(:save!).and_raise(MockRecordInvalid)
Then a rescue ActiveRecord::RecordInvalid will catch MockRecordInvalid and MockRecordInvalid.new.is_a?(ActiveRecord::RecordInvalid) is true.
ActiveRecord::RecordInvalid needs an object to be created.
If you want to test just the exception itself, try:
null_object = double.as_null_object
ActiveRecord::RecordInvalid.new(null_object)
Understand the double as null object here (https://www.relishapp.com/rspec/rspec-mocks/v/2-6/docs/method-stubs/as-null-object)
I am using update_attribute(attr, 'value') instead of save! and I can mock the update_attribute method as follows:
expect(mock_object).to receive(:update_attribute).with(attr, 'value').and_raise(ActiveRecord::RecordInvalid)
We can stub it to the Model like this,
ModelName.any_instance.stubs(<method_name>).raises(ActiveRecord::RecordInvalid.new(record))
Example:
Post.any_instance.stubs(:save!).raises(ActiveRecord::RecordInvalid.new(post))
I am receiving NoMethodErrors when my DeltaSynWorker runs. This happens in an application that was built as a revision of a currently working application. I am not the original programmer, and I am coming at it from a Java background (I mention this because it is possible I am missing something very obvious to others). I cannot figure out why NoMethodeError is being thrown when the code is very similar to code that is currently working fine in a different web application.
The Error:
NoMethodError: undefined method `client' for #<ApiRequestBuilder:0x0000000705a8f0>
delta_sync_worker.rb
class DeltaSyncWorker < SyncWorker
include Sidekiq::Worker
sidekiq_options queue: "delta_syncs"
def perform(subscriber_id, client_id)
sleep(10)
current_subscriber = ApiSubscriberDecorator.decorate(Subscriber.find(subscriber_id))
Time.zone = current_subscriber.time_zone
client = ApiClientDecorator.decorate(Client.find(client_id))
arb = ApiRequestBuilder.new(URI.parse(SR_HOST + '/servlet/sync/process'))
if arb.client(:subscriber => current_subscriber, :client => client)
arb.transmit
if arb.success?
current_subscriber.touch(:sync_updated_at)
decorated_client = ClientDecorator.decorate(client.model)
ConfirmationsSyncWorker.perform_in(1.hours, current_subscriber.id)
else
error_params = {:subscriber => current_subscriber.id, :response_body => arb.response.body, :request_body => arb.request.body, :sync_type => "DeltaSyncWorker"}
Airbrake.notify(:error_class => "sync_error", :error_message => "Sync Error: #{arb.response.message}", :params => error_params)
end
end
end
end
api_request_builder.rb
require 'nokogiri'
class ApiRequestBuilder < AbstractController::Base
include AbstractController::Rendering
include AbstractController::Layouts
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
self.view_paths = "app/api"
attr_accessor :request_body, :request, :response, :append_request_headers, :request_method, :url
def initialize(url, *args)
#url = url
if args
args.each do |arg|
arg.each_pair{ |k,v| instance_variable_set("##{k.to_s}", v) }
end
end
end
# this will search for an api request template in api/requests, render that template and set any instance variables
def method_missing(meth, *args, &block)
if lookup_context.template_exists?("requests/#{meth.to_s}")
if args
args.each do |arg|
arg.each_pair{|k,v| instance_variable_set("##{k.to_s}", v) }
end
end
#request_body = (render :template => "requests/#{meth.to_s}")
else
super
end
end
def transmit
#request_method ||= "Post"
#request = "Net::HTTP::#{#request_method}".constantize.new(#url.path)
#request['x-ss-user'] = #subscriber.sr_user if #subscriber && #subscriber.sr_user.present?
#request['x-ss-pwd'] = #subscriber.sr_password if #subscriber && #subscriber.sr_password.present?
unless #append_request_headers.nil?
#append_request_headers.each_pair{ |k,v| #request[k] = v }
end
#request.body = #request_body if request_body? && #request.request_body_permitted?
#http = Net::HTTP.new(#url.host, #url.port)
#http.use_ssl = true
#http.verify_mode = OpenSSL::SSL::VERIFY_NONE
#response = #http.request(#request)
end
def success?
if #response.code == 200.to_s
return true
else
return false
end
end
def request_body?
unless #request_body.nil?
return true
else
return false
end
end
end
I have been looking at other NoMethodError questions here, but I cannot find an answer I feel applies to my situation. Any help is greatly appreciated. Thanks!
method_missing will catch sent messages for which there is no method defined, and the call to super at the end will pass it up to Ruby's standard method_missing behavior, which is what you are seeing (NoMethodError). However, this only happens if the if condition is not met, which is what I suspect is happening here, and would explain why it works in some situations but not in others. The call to :client, having found no matching methods along the inheritance chain, will look for a template called "requests/client" - try adding this template and see if that fixes the issue.
I know Ive seen this before and I feel like it wasn't what it appeared to be, but ya basically method missing is just intercepting the method call and when you call arb.client, it is caught by method missing and therefore tries to render api/client.xml.arb or api whatever the file type is. -- Note that there should be a file in the initializers directory named somethig like api_template_handler.rb or arb_temmplate_handler.rb, which is what allows rails to see that template type in the view directory -- make sure that is there first. Also sidenote, _client.xml.api is a partial used by the other api request in that directory (full sync),
To debug Id start by, above the following line
if lookup_context.template_exists?("requests/#{meth.to_s}")
Add a log statement
Rails.logger.debug "Can I see View?" + lookup_context.template_exists?("requests/#{meth.to_s}")
If true, then the problem is the error isnt getting caught properly because of method missing. If false, then the sidekiq worker isnt loading rails properly, or the view path isn't being added onto the rails view paths.
If true, Im guessing it might have something to do with the client model not being loaded, or an attribute on the client model not existing, that the builder is trying to call, and the error is somehow bubbling up to the api request builder class.
Oh also, just general stuff, but make sure redis and sidekiq are running, and restart passenger if its non local environment.
Let me know how it goes.