How to invoke Rails cache sweeper from a rake task? - ruby-on-rails

I set up a Rails cache sweeper like below:
class ArticleSweeper < ActionController::Caching::Sweeper
observe Article
def sweep(article)
expire_page articles_path
expire_page author_path(article.author)
FileUtils.rm_rf "#{page_cache_directory}/articles/page"
end
alias_method :after_update, :sweep
alias_method :after_create, :sweep
end
I connected this cache sweeper with the article controller.
The cache sweeping works as a result of controller action of create and update.
However, the main way I update my database records are through a rake task, which uses active record methods of save, update_attributes, etc on the database records.
I thought about adding a model observer class and let this class invoke the sweeper method.
There're alot of comments online about how the cache sweeper should be triggered by model actions instead of controller actions.
But for my application, the rake task may add 100 records when it executes. So I don't want the cache sweeper to execute 100 times. As it won't be efficient.
I think it's better to execute it once at the end of the rake task.
So I tried adding this in my rake task:
ArticleSweeper.instance.sweep(Article.last)
I put a "debugger" statement in there and see that the sweep() method WAS being executed.
However, the cache files are NOT being removed, even though the same code works when it's triggered by controller action.
So my question is: is it possible to trigger the cache sweeper method from a rake task?
And if so, what can I do to make it work?
Thanks

I was having the same problem and stumbled across this: http://www.madebymade.co.uk/blog/2013/08/cache-sweepers-invalidating-caches-from-outside-the-controller/

Related

How to call the function from the controller to the rake file : Ruby on Rails

I'm trying to add the function that is defined in the controller.rb file to the rake file. The Name of this controller file is "home_controller.rb". Here I put a part of the code.
class HomeController < ApplicationController
def rclass_creation
#output = "rclass is created"
a = Rclass.all
a.destroy_all...
I made my_namespace.rake file under tasks in the lib folder. Here I put the code a bit.
namespace :my_namespace do
desc "TODO"
task my_task2: :environment do
a = Userpool.all
a.each do |user|
puts user.name
end
puts "wow it is working"
I was able to call the database. I think this is because the rake file has an access to the database ( Userpool ). In this rake file, I want to call the function "rclass_creation" that was stated in the "home_controller.rb" because I want to avoid the situation that I have to do hardcopy.
The sudo code might look like this ( I hope (: )
namespace :my_namespace do
desc "TODO"
task my_task2: :environment do
status = rclass_creation <= from home_controller.rb
a = Userpool.all
a.each do |user|
puts user.name
end
puts "wow it is working"
The function I want to call is "rclass_creation" from the home_controller.
How can I call the function from the controller to rake file ? I'm looking forward to seeing opinions from the experts!!
You don't call controller methods from anywhere but the controller.
If you want to use a controller action from a rake file you should invoke it with a HTTP request. Just like any client would do.
You can initialize a controller and call methods on it but that's really hacky. Don't.
If you need to share code between your rake task and controller than it should not be in a controller at all.
So where do you put the code?
Does it act on a model? Is it independent from application state? Does it belong in a model? Put in a your model.
Is a mixin? Or just a plain method with takes some input and shoots something out? Put in into a helper or a plain old module.
Is it some kind of service that takes input and does something with models? Put it in a service object.

Method calls in one rake file task being handled by methods of same name in another rake file

Using Rails 2.3.*
Say I have a method called some_method() in two rake files - A.rake and B.rake.
I'm finding that if I call some_method() in B.rake, the method in A.rake is what actually gets called.
So what's the best approach to defining helpers methods inside rake files that will be "local" to the rake task defined in that file?
Thanks
You can define your helper within a task to make it available to that task and all subsequent ones:
desc 'has access to local helper'
task :accessible do
def helper
return "the helper"
end
puts "I have access to #{helper}"
end
desc 'has access too'
task 'after-accessible' => ['accessible'] do
puts "this ran after 'accessible' but still has access to '#{helper}"
end
desc 'does not have access to the helper'
task :outside do
puts helper # fails if runs before :accessible
end
Perhaps the best thing to do though is to refactor your Rakefiles and the helper code so that the two Rakefiles do not load each other.

Rails: invoking a mailer from a rake task with parameter/env variable

Using this railscast http://railscasts.com/episodes/127-rake-in-background?autoplay=true as an example/inspiration (i.e. I'm not trying to implement the rails cast code, just use it as an inspiration), I tried to move a mailer, that was triggered by an after_create callback in the user.rb model, into a rake task, so it would run in the background. The mailer was working before I moved it into a rake task, but it's not working anymore.
Instead of calling the mailer from the User.rb model, which it was how it was set up originally (see the commented out code in user.rb), I instead call the rake task, which then invokes the UserMailer.welcome_email method.
In the original code, "self" was submitted (from User.rb) as a parameter to the method welcome_email(user) in user_mailer.rb. In my attempt to turn it into a rake task, I assigned "self" to USER_INSTANCE, which is supposed to be picked up in the mailer.rake as ENV["USER_INSTANCE"]. This was also suggested by the railscast.
Somewhere along the way it's not working. Any ideas?
User.rb
after_create :send_welcome_email
def send_welcome_email
system "rake :send_mailing USER_INSTANCE=self &" #note the & forks the task
#UserMailer.welcome_email(self).deliver <-- how it was originally.
end
mailer.rake
desc "Send mailing"
task :send_mailing => :environment do
UserMailer.welcome_email(ENV["USER_INSTANCE"]).deliver #moved from user.rb to here but now with environment variable instead of parameter
end
unchanged User_mailer.rb
class UserMailer < ActionMailer::Base
default :from => "blahblah#gmail.com"
def welcome_email(user)
mail(:to => user.email, :subject => "Invitation Request Received")
end
end
currently you are doing this
system "rake :send_mailing USER_INSTANCE=self &"
which is the same as going to the command line and typing
rake :send_mailing USER_INSTANCE=self &
self is just a literal string, I think what you are trying to do is this
system "rake :send_mailing USER_INSTANCE=#{self} &"
but that will end up being the equivalent of running this on the command line
rake :send_mailing USER_INSTANCE=<User::xxxxx>
rake won't serialize this into your User ActiveRecord object;
when you shell out with system there is no relation to the calling code
an alternative - your rake task could take an integer - user_id and then access the record via User.find
but it gets more complicated as after_create is going to be running in a transaction so once your rake task runs it may or may not have finished that transaction
I would advise against trying to re-invent a way to do background processing in rails, there are already good tried and true solutions available
see http://railscasts.com/?tag_id=32 for some options

ActiveRecord::Dirty and Rake

I have a Model (let's call it A) in a Rails project that checks an attribute (let's call it a) with the ActiveRecord::Dirty a_changed? function on before_save. I want to be able to save an instance of A in a Rake task, but simply including :environment isn't cutting it--I'm getting a "no method a_changed? defined on A" message in the Rake task. How do I get ActiveRecord to remember about ActiveRecord::Dirty within a Rake task?
Rails version is 2.3.11
namespace :some_namespace do
namespace :some_subnamespace do
desc "This is a Rake Task"
task :some_taskname, [:some_arg] => [:environment] do |t,arg|
foo = A.find(11111)
foo.save #<=== fails with "no method a_changed? defined on A"
end
end
end
Since that's a pretty dense bunch of info, here's the breakdown:
I have a model A with an attribute a.
Model A has a before_save trigger defined that calls a_changed?, which is a method added by ActiveRecord::Dirty in the Rails environment. There are no problems calling this from a controller.
In my Rake task, however, the a_changed? call in the before_save trigger causes a NoMethodError exception to be raised, presumably because the [:environment] requirement is not sufficient to include ActiveRecord::Dirty. My question is how to make this not happen (my workaround is to rescue NoMethodError from inside the before_save, which is an obvious hack).
Looks like your question has already been answered on a previous question asked on StackOverflow.
In order to determine what methods your object has you can do this:
...
desc "This is a Rake Task"
task :some_taskname, [:some_arg] => :environment do |t, args|
foo = A.find(11111)
p foo.methods
...
This will print out a list of the available methods. If the array includes :some_attr_changed? (where some_attr is an attribute), then you can be certain that ActiveRecord::Dirty is indeed working fine in the rake task. If those methods don't show up in the array, then your assumptions are correct.

In Rails, a Sweeper isn't getting called in a Model-only setup

I'm working on a Rails app, where I'm using page caching to store static html output. The caching works fine. I'm having trouble expiring the caches, though.
I believe my problem is, in part, because I'm not expiring the cache from my controller. All of the actions necessary for this are being handled within the model. This seems like it should be doable, but all of the references to Model-based cache expiration that I'm finding seem to be out of date, or are otherwise not working.
In my environment.rb file, I'm calling
config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
And I have, in the /sweepers folder, a LinkSweeper file:
class LinkSweeper < ActionController::Caching::Sweeper
observe Link
def after_update(link)
clear_links_cache(link)
end
def clear_links_cache(link)
# expire_page :controller => 'links', :action => 'show', :md5 => link.md5
expire_page '/l/'+ link.md5 + '.html'
end
end
So ... why isn't it deleting the cached page when I update the model? (Process: using script/console, I'm selecting items from the database and saving them, but their corresponding pages aren't deleting from the cache), and I'm also calling the specific method in the Link model that would normally invoke the sweeper. Neither works.
If it matters, the cached file is an md5 hash off a key value in the Links table. The cached page is getting stored as something like /l/45ed4aade64d427...99919cba2bd90f.html.
Essentially, it seems as though the Sweeper isn't actually observing the Link. I also read (here) that it might be possible to simply add the sweeper to config.active_record.observers in environment.rb, but that didn't seem to do it (and I wasn't sure if the load_path of app/sweepers in environment.rb obviated that).
So I've tried a number of different approaches, to see what works, and what doesn't.
Again, to summarize the situation: My goal is to expire cached pages when an object updates, but to expire them without relying on a Controller action. Conventional sweepers use a line in the controller to notify the sweeper that it needs to function. In this case, I can't use a line in the controller, as the update is happening within the model. Normal sweeper tutorials aren't working, as they presume that your main interaction with the database object is through the controller.
If, in reading this, you see a way to tighten up my code, please comment and let me know.
First, let's look at the things that DO work, in case you're stuck on this, too, and need help.
Of all the things I tried, the only thing that really seemed to work was to declare an after_update command in the Observer for the model. In that command, I used the explicit command for the expire_page action, and included a path that had been declared in routes.rb.
So. This works:
In config/routes.rb:
map.link 'l/:md5.:format', :controller => 'links', :action => 'show'
In app/models/link_observer.rb:
def after_update(link)
ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
end
Note that that "md5" is specific to my app. You might want to use :id or some other unique identifier.
I also found that declaring that ActionController::Base... line from the method in the model that's doing the updating worked. That is, within Link.rb, in the method that's actually updating the database, if I just stuck that whole line in, it worked. But since I might want to expire that page cache on other methods in the future, I'd rather have it extracted into the Observer.
Now, let's look at some things that DID NOT work, in case you're Googling around for this.
Calling "expire_page(...)" within the after_update(link) method within link_observer.rb did not work, as it returned an "undefined method `expire_page'" error
Creating a Sweeper file that observed the Model did not work. I couldn't find any error codes, but it just seemed to not even be aware that it had a job to do. This was after explicitly calling "config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )" within environment.rb. Just in case I fat-fingered something in that code, here it is:
class LinkSweeper < ActionController::Caching::Sweeper
observe Link
def after_update(link)
clear_links_cache(link)
end
def clear_links_cache(link)
# DID NOT WORK expire_page :controller => 'links', :action => 'show', :md5 => link.md5
# DID NOT WORK expire_page '/l/'+ link.md5 + '.html'
# DID NOT WORK ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
end
end
That above example had the link_sweeper.rb file in a directory, /app/sweepers. I also tried putting link_sweeper.rb within the app/models directory, and tried calling it with the config.active_record.observers command in environment.rb:
config.active_record.observers = :link_observer, :link_sweeper
But that didn't work, either.
So, yeah. It's quite possible that one of these methods would work, and that I messed up something in the code. But I think I did everything by the book.
Ultimately, to summarize: Rather than using a Sweeper to expire page caching, you want to set up an after_ callback in the model's Observer. You'll want to use the explicit path to the Base.expire_page method:
def after_update(<model>) # where <model> is the name of the model you're observing
ActionController::Base.expire_page(app.<model>_path(:id => <model>.id)) # where <model> is the name of the model you're observing
end
Hopefully this will help someone else down the road. Again, if you see anywhere in my not-working code where I should have done something differently, please let me know. If you see something in my working code that can be tighter, please let me know that, too.
Just a note: you can use cache_sweeper in ApplicationController.
class ApplicationController < ActionController::Base
cache_sweeper :my_sweeper
end
class MySweeper < ActionController::Caching::Sweeper
observe MyModel
def after_update(my_model)
expire_page(...)
end
end
I was experiencing the same problem when trying to do fragment caching (rails 3). Couldn't get the sweeper to observe, so I settled for the solution to make it an AR Observer as described above and calling ApplicationController.new.expire_fragment(...).
I did get this working. The only slight difference in my setup is that the sweeper is part of a Rails engine; which accounts for slight differences (loading the sweeper file with a require in the engine's init instead of adding it to the load path in environment.rb, etc).
So, the sweeper is loaded in the init.rb of the engine like this:
require File.join(File.dirname(__FILE__), 'app', 'sweepers', cached_category_count_sweeper')
I called it a sweeper because it "sweeps" the cache, but I guess its just an observer on the model:
class CachedCategoryCountSweeper < ActiveRecord::Observer
observe CategoryFeature
def before_save(cf)
expire_cache(cf.category_id_was) if cf.category_id_changed?
end
def after_save(cf)
expire_cache(cf.category_id)
end
def after_destroy(cf)
expire_cache(cf.category_id)
end
def expire_cache(c)
ApplicationController.expire_page("/categories/#{c}/counts.xml") if !c.nil?
end
end
Frankly, I don't like having to hard-code the path, but I tried adding:
include ActionController:UrlWriter
and then using the path method, but it only worked for me in development. It didn't work in production, because my production server uses a relative url root (instead of virtual hosts) and the internal method "page_cache_path" would consistently get the file path wrong so it couldn't expire.
Since this is an observer, I added to the environment.rb:
config.active_record.observers = :cached_category_count_sweeper
Finally the controller that uses the cache (doesn't expire it, that is done through the model observer):
class CachedCategoryCountsController < ApplicationController
caches_page :index
# GET /cached_category_counts.xml
def index
...
end
end
Anyhow, hope this helps.
Andres Montano
I've been able to get it to work, by way of adding
ActionController::Base.expire_page(app.link_path(:md5 => #link.md5))
to the method in the Model itself that's updating the database. This feels somewhat hacky, though, and I'd love to know if anyone can explain why it's not working with the normal sweeper setup, and if there's a more elegant way to handle this.
That snippet of code (apart from customizations I put in for my own app) came from this post on ruby-forum.com.
I wrote a bit about this topic here: Rails Cache Sweeper Confusion. Would love to hear your opinions.
Based on #moiristo and #ZoogieZork 's answers, I am guessing this would work (untested).
class LinkSweeper < ActiveRecord::Observer
include ActionController::Caching::Pages
# or if you want to expire fragments
#include ActionController::Caching::Fragments
observe Link
def after_update(link)
expire_page( ... )
#expire_fragment( ... )
end
end

Resources