Earlier, I had posted this question – and thought it was resolved:
Rails background worker always fails first time, works second
However, after continuing with tests and development, the error is back again, but in a slightly different way.
I'm using Sidekiq (with Rails 3.2.8, Ruby 1.9.3) to run background processes, after_save. Below is the code for my model, worker, and controller.
Model:
class Post < ActiveRecord::Base
attr_accessible :description,
:name,
:key
after_save :process
def process
ProcessWorker.perform_async(id, key) if key.present?
true
end
def secure_url
key.match(/(.*\/)+(.*$)/)[1]
end
def nonsecure_url
key.gsub('https', 'http')
end
end
Worker:
class ProcessWorker
include Sidekiq::Worker
def perform(id, key)
post = Post.find(id)
puts post.nonsecure_url
end
end
(Updated) Controller:
def create
#user = current_user
#post = #user.posts.create(params[:post])
render nothing: true
end
Whenever jobs are first dispatched, no matter the method, they fail initially:
undefined method `gsub' for nil:NilClass
Then, they always succeed on the first retry.
I've come across the following github issue, that appears to be resolved – relating to this same issue:
https://github.com/mperham/sidekiq/issues/331
Here, people are saying that if they create initializers to initialize the ActiveRecord methods on the model, that it resolves their issue.
To accomplish this, I've tried creating an initializer in lib/initializers called sidekiq.rb, with the following, simply to initialize the methods on the Post model:
Post.first
Now, the first job created completes successfully the first time. This is good. However, a second job created fails the first time – and completes upon retry... putting me right back to where I started.
This is really blowing my mind – has anyone had the same issue? Any help is appreciated.
Change your model callback from after_save to after_commit for the create action. Sometimes, sidekiq can initialize your worker before the model actually finishes saving to the database.
after_commit :process, :on => :create
Related
I'm trying to prevent deletion of models from the db and pretty much follow this guide (see 9.2.5.3 Exercise Your Paranoia with before_destroy) from a Rails 4 book.
I have a simple model:
class User < ActiveRecord::Base
before_destroy do
update_attribute(:deleted_at, Time.current)
false
end
and in the controller:
def destroy
#user = User.find(params[:id])
# #user.update!(deleted_at: Time.zone.now) # if I do it here it works
#user.destroy # if I also comment this line...
render :show
end
The callback gets called and the attribute gets set, but then the database transaction always gets rolled back. It I leave out the returning of false the model gets deleted because the execution of delete is not halted.
As you can see in the comments I can get it to work but what I really want to do is use a Service Object and put the logic out of the controller.
if your callback returns false the transaction will always be rollbacked.
For what you want you should not call to the destroy method on your arel object.
Instead, make your own method like soft_destroy or something like that and update your attribute.
And to prevent others from calling the destroy method on your arel object, just add a callback raising and exception for instance.
Your model is just an object. If you really want to change the concept of destroy, change it:
def destroy
condition ? alt_action : super
end
I have a TreatmentEvent model. Here are the relevant parts:
class TreatmentEvent < ActiveRecord::Base
attr_accessible :taken #boolean
attr_accessible :reported_taken_at #DateTime
end
When I set the taken column, I want to set reported_taken_at if taken is true. So I tried an after_save callback like so:
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
I think update_attribute calls save, so that's causing the stack level too deep error. But using the after_commit callback is causing this to happen, too.
Is there a better way to conditionally update one column when another changes? This answer seems to imply you should be able to call update_attributes in an after_save.
Edit
This also happens when using update_attributes:
def set_reported_taken_at
self.update_attributes(reported_taken_at: Time.now) if self.taken?
end
As a note, stack level too deep generally means an infinite loop
--
In your case, the issue will almost certainly be caused by:
after_commit :set_reported_token_at
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
--
The problem is after_commit is going to try and save the reported_taken_at even if you've just saved a record. So you're going to go over the record again and again and again and again...
Often known as a recursive loop - it's used a lot in native development, but for request (HTTP) based apps, it's bad as it leads to a never-ending processing of your request
Fix
Your fix should be like this:
#model
before_save :set_reported_token_at
def set_reported_taken_at
self.reported_taken_at = Time.now if taken? #-> assuming you have a "taken" method
end
Can't you use a before_save? You can see if the other field value has changed and if so update this field. That way you just have one DB call.
so some context, I got some advice here:
Scheduling events in Ruby on Rails
aand have been tying to implement it today. I cant seem to make it work though. this is my scheduler job that is used to move my questions around between a delayed queue and a ready to send out queue (i've since decided to use email instead of SMS)
require 'Assignment'
require 'QuestionMailer'
module SchedulerJob
#delayed_queue = :delayed_queue
#ready_queue
def self.perform()
#delayed_queue.each do |a|
if(Time.now >= a.question.schedule)
#ready_queue << a
#delayed_queue.delete(a)
end
end
push_questions
end
def self.gather()
assignments = Assignment.find :all
assignments.each do |a|
#delayed_queue << a unless #delayed_queue.include? a
end
end
private
def self.push_questions
#ready_queue.each do |a|
QuestionMailer.question(a)
end
end
end
I use a callback on_create to call the gather method every time an assignment is created, and then the perform action actually does the sending of emails when resque runs.
I'm getting a strange error from the callback though.
undefined method `include?' for :delayed_queue:Symbol
here is the code from the assignment model
class Assignment < ActiveRecord::Base
belongs_to :user
belongs_to :question
attr_accessible :title, :body, :user_id, :question_id , :response , :correct
after_create :queue_assignments
def grade
self.correct = (response == self.question.solution) unless response == nil
end
def queue_assignments
SchedulerJob.gather
end
Any ideas what's going on? I think this is a problem with my understanding of how these queue's work with resque-scheduler. I assumed that if the queues were list-like objects then I could operate on them , but it appears that it a symbol instead of something with methode like include? I assume the << notation for adding something to it is also invalid.
Also please advise if this isn't the way to go about handling this kind of job scheduling
It appears you may have not restarted your Rails app after adding the new method gather to the SchedulerJob module. Try restarting your app to resolve this.
You may also be able to add the directory containing your Resque worker to Rails' watchable_dirs array so that changes you make to Resque worker modules in development don't require restarting your app. See this blog post for details:
http://wondible.com/2012/01/13/rails-3-2-autoloading-in-theory/
In our Rails app. We save a model (Video). We have a callback on that object:
after_create :send_to_background_job, :if => :persisted?
The method looks like:
def send_to_background_job
Resque.enqueue(AddVideo, self.id)
end
When the worker is called. It does the following:
class AddVideo
#queue = :high
def self.perform(video_id)
video = Video.find(video_id)
video.original_file_name
....
Resque-web reports an error:
AddVideo
Arguments
51061
Exception
NoMethodError
Error
undefined method `original_filename' for nil:NilClass
Which is a bit odd, because, if I go to Rails console to look for this video. It does exist. Furthermore, calling Resque.enqueue(AddVideo, 51061) a SECOND time, runs without any errors.
It is as if it takes more time to save the record in the database, than it takes to create the worker/job. But even this statement doesn't add up, since the object calls the Resque job only after the object is saved. In Rails, this is done via a callback method in the model (after_create).
Don't know if this plays a role in the issue. In an initializer file, I have:
Resque.before_fork do
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
Resque.after_fork do
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
This probably happens because the actual transaction in which the object is saved is not yet commited and the background job already started working on it.
You should switch the after_create to
after_commit :send_to_background_job, on: :create
Ugh. Not my day today.
I have an observer in our Rails 3.0x app
class EventObserver < ActionController::Caching::Sweeper
observe :live_event, :event
def after_update(event)
Rails.logger.debug "EventObserver::after_update #{event.class.name}.#{event.id}"
channels = Channel.being_used
inc=channels.reject {|c| !c.current_source.events.include?(event) }
inc.each do |c|
expire_action("channel_tickers/#{c.id}.js")
expire_action("channel_events/#{c.id}.js")
Rails.logger.debug "expired stuff for channel #{c.id}"
end
end
end
I do an update of one of the watched classes and the after_update is getting called, but it is not expiring anything. I see no calls to expire_action in the log.
I do see:
expired stuff for channel 63
So it is hitting that code and not bombing out.
I want to use an observer because these models are not updated from a controller but internally.
After some trolling around I found the answer. I added this line to the beginning of my 'after_update' method:
#controller ||= ActionController::Base.new # <= this line is the key! without this nothing works
You will get no exceptions, but without this it doesn't work. I guess somewhere down the line it is looking to see if #controller is present and if so do things.