DEV Community

loading...

Decorating Rake tasks for fun and profit

Carlos Ghan
・3 min read

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
Enter fullscreen mode Exit fullscreen mode

In the previous snipppet, invoking Rake with a task name prefixed by a "marker" string, will be processed by this rule. (I chose decorated: but actually it could be anything, I just felt that using <label>: 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:

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
Enter fullscreen mode Exit fullscreen mode

Then, you can simply do, for example:

bundle exec rake timed:db:seed
Enter fullscreen mode Exit fullscreen mode

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 exit or abort, and thus a SystemException is raised), the next line of code after Rake::Task[task_name].invoke won't be executed.

Well... not the cleanest way in the world, but we can leverage at_exit to register a block so that it's always executed at program's exit no matter what:

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
Enter fullscreen mode Exit fullscreen mode

Neat 🙂

Note that you can also compose these "decorator rules":

bundle exec rails safe:timed:db:drop
Enter fullscreen mode Exit fullscreen mode

where safe could be the following rule:

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
Enter fullscreen mode Exit fullscreen mode

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" 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/

Discussion (0)