DEV Community

Carlos Ghan
Carlos Ghan

Posted on

Decorating Rake tasks for fun and profit

Alt Text

Why?

I just thought it would be fun to try this experiment: decorate (wrap) dynamically any Rake task execution with some arbitrary code.
Also, it would be nice if I could do it even for tasks defined outside my project, for example in gems, without having to modify the source-code.

Examples:

  • Custom logging around task's execution (e.g. time spent, maybe using funny colors)
  • Run a Rails DB task in read-only mode (setting ActiveRecord connection to use a replica DB)
  • Notify in Slack the results of some tasks
  • ...etc., you name it

I'm not going to use Rake::Task.enhance which, as you may already know, allows you to execute another Rake task as a dependency before and/or after the "enhanced" task, because you lose some context, and in order to do things like referencing a variable defined at the "before"-task from the "after"-task, you may need to resort to some trickery...

How?

Recently, I've learned about Rule Tasks. As per the documentation, "rules" let you

synthesize a task by looking at a list of rules supplied in the Rakefile.

This feature can be (ab)used to accomplish my goal... The rule-block receives (as arguments) an instance of Rake::FileTask (which responds to #name) and the arguments passed to the task; we could do something along the lines of:

rule /^decorated:.*/ do |t, args| task_name = t.name.delete_prefix('decorated:') # Code to be executed BEFORE the task... Rake::Task[task_name]).invoke(*args) # Code to be executed AFTER the task... end ~~~{% endraw %} In the previous snipppet, invoking Rake with a task name prefixed by a "marker" string, will be processed by this rule. (I chose {% raw %}`decorated:`{% endraw %} but actually it could be anything, I just felt that using {% raw %}`<label>:`{% endraw %} looked more Rake-ish 😉) Despite it seems "rules" where designed to be used with file-name matching patterns in mind (like you do with GNU Make), it does the trick anyway; yeah, hacky, I know. This is an example that logs task's execution time:{% raw %} ~~~ruby rule /^timed:.+/ do |t, args| task_name = t.name.delete_prefix('timed:') start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) Rake::Task[task_name].invoke(*args) end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) total_time = end_time - start_time Rails.logger.info("#{task_name} executed in #{total_time} seconds") end ~~~ Then, you can simply do, for example: ~~~shell bundle exec rake timed:db:seed ~~~ And, yes, the meticulous reader may have noticed that it doesn't return the "total" time as when using the shell's `time` command (as in `time bundle exec rake ...`), since it doesn't take into account any startup related overhead, but the example was just for illustration purposes. Now, this code has a problem though... if the invoked task fails (which usually means it executes {% raw %}`exit`{% endraw %} or {% raw %}`abort`{% endraw %}, and thus a {% raw %}`SystemException`{% endraw %} is raised), the next line of code after {% raw %}`Rake::Task[task_name].invoke`{% endraw %} won't be executed. Well... not the cleanest way in the world, but we can leverage [`at_exit`](https://ruby-doc.org/core-2.7.2/Kernel.html#method-i-at_exit) to register a block so that it's always executed at program's exit no matter what: ~~~ruby rule /^timed:.+/ do |t, args| task_name = t.name.delete_prefix('timed:') start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) at_exit do # Here goes the "after-task" code total_time = end_time - start_time Rails.logger.info("#{task_name} executed in #{total_time} seconds") end Rake::Task[task_name].invoke(*args) end ~~~ Neat 🙂 Note that you can also compose these "decorator rules": ~~~shell bundle exec rails safe:timed:db:drop ~~~ where `safe` could be the following rule: ~~~ruby rule /^safe:/ do |t, args| task_name = t.name.delete_prefix('safe:') print "Do you really want to perform this action (yes/no)? " confirmed = gets.chomp == 'yes' if confirmed Rake::Task[task_name].invoke(*args) else puts "Phew... almost did some crazy thing" end end ~~~ And that's all. ## Closing words In the "real world", the use of this technique may be arguable and even frown-upon, because there's too much magic going on (read it "brittle non-explicit stuff"), but hey... it may help you debugging an issue (like it did for me) or with some ad-hoc code that you won't commit into that pristine application code-repository 😬 ---- *Cover image: ["Max and Ruby Party"]( https://www.flickr.com/photos/13698839@N00/3221858084) by Kid's Birthday Parties is licensed with CC BY-ND 2.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-nd/2.0/* 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)