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
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
Then, you can simply do, for example:
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 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
Neat π
Note that you can also compose these "decorator rules":
bundle exec rails safe:timed:db:drop
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
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/
Top comments (0)