Drawing a Blank
by Mike Fulcher

Spinner: a Ruby gem for tasks with unknown duration
Tuesday, July 09, 2013

Inspired by gems like progressbar, spinner.rb fills a smaller niche by removing the need to increment a progress bar. Instead, spinner.rb prints to the console a tasks counter and a little animated spinner and waits for the tasks to complete. Unlike progressbar, it doesn’t report the percentage of completion or try to estimate how much time is left, which means you can feed in tasks with an unknown duration.

Originally, this was to address a simple but long-running task I’d set up in a Rails app I was working on. During early prototyping, I found myself often recontructing the development database from scratch using migrations rather than the schema. This led to a lot of this:

rake db:drop db:create db:migrate db:test:load db:seed

To make this easier to type, I set up a simple rake task so that I could run rake db:rebuild:

# in lib/tasks/db/rebuild.rake

namespace :db do
  desc "Rebuild the database (using migrations rather than the schema)"
  task :rebuild do |t, args|
    Rake::Task['db:drop'].invoke
    Rake::Task['db:create'].invoke
    Rake::Task['db:migrate'].invoke
    Rake::Task['db:test:load'].invoke
    Rake::Task['db:seed'].invoke
  end
end

Most of those tasks run very quickly, so a progress indicator certainly wasn’t necessary. Then my seed file started becoming quite large. Before long, rake db:seed was taking a minute or longer, so I decided some sort of progress indicator would be helpful.

At first I investigated using progressbar, but I didn’t want to flood my seed file with progressbar.increment. I wasn’t even concerned about exactly what the progress was at a given point in time – I just wanted to see that something was happening rather than seeing an apparently lifeless console.

So, I put together spinner.rb. Usage is simple. Install from Rubygems (gem install spinner.rb or gem 'spinner.rb') then pass in some tasks to be executed:

require 'spinner.rb'

# Create a new spinner instance
spinner = Spinner.new

# Add a task block
spinner.task("Number 1") do
  sleep(5) # simulate taking a while to do something
end

# Add another task
Spinner.task("Number 2") do
  sleep(2)
end

# Run the tasks
spinner.spin!

This will print a counter and title as it executes each task in sequence, followed by a short completion report. I also wanted to be able to run rake tasks from spinner.rb, too, so instead of passing a block you can just provide the name of the rake task:

spinner = Spinner.new
spinner.task("Dropping", 'db:drop')
spinner.task("Creating", 'db:create')
spinner.task("Migrating", 'db:migrate')
spinner.task("Loading", 'db:test:load')
spinner.task("Seeding", 'db:seed')
spinner.spin!

spinner.rb is also flexible enough to support passing in the tasks when the spinner is initialized. Individual tasks are just arrays with two items: the name of the task (defaults to 'Executing') and the task (either a string representing a rake task, or a block that responds to call).

tasks = []
tasks << [ "Drop database", "db:drop" ]
tasks << [ "Sleep 2 seconds", lambda { sleep(2) } ]
spinner = Spinner.new(*tasks)
spinner.spin!

This enabled me to change my rebuild rake task to this:

namespace :db do
  desc "Rebuild the database (using migrations rather than the schema)"
  task :rebuild do |t, args|
    spinner = Spinner.new
    spinner.task("Dropping", 'db:drop')
    spinner.task("Creating", 'db:create')
    spinner.task("Migrating", 'db:migrate')
    spinner.task("Loading", 'db:test:load')
    spinner.task("Seeding", 'db:seed')
    spinner.spin!
  end
end

Now while I wait for my crazy seed file to get loaded, I can enjoy a little animated spinner. Much better!