maybe_later - get slow code out of your users' way

Maybe you've been in this situation: you want to call some Ruby while responding to an HTTP request, but it's a time-consuming operation, its outcome won't impact the response you send, and naively invoking it will result in a slower page load or API response for your users.

In almost all cases like this, Rubyists will reach for a job queue. And that's usually the right answer! But for relatively trivial tasks—cases where the only reason you want to defer execution is a faster page load—creating a new job class and scheduling the work onto a queuing system can feel like overkill.

If this resonates with you, the maybe_later gem might be the best way to run that code for you (eventually).

Bold Font Disclaimer

⚠️ If the name maybe_later didn't make it clear, this gem does nothing to ensure that your after-action callbacks actually run. If the code you're calling is very important, use sidekiq or something! ⚠️

Setup

Add the gem to your Gemfile:

gem "maybe_later"

If you're using Rails, the gem's middleware will be registered automatically. If you're not using Rails but are using a rack-based server that supports env["rack.after_reply"] (which includes puma and unicorn), just add use MaybeLater::Middleware to your config.ru file.

Usage

Using the gem is pretty straightforward, just pass the code you want to run to MaybeLater.run as a block:

MaybeLater.run {
  AnalyticsService.send_telemetry!
}

Each block passed to MaybeLater.run will be run after the HTTP response is sent and the response will include a "Connection: close header.

If the code you're calling needs to be executed in the same thread that's handling the HTTP request, you can pass inline: true:

MaybeLater.run(inline: true) {
  # Thread un-safe code here
}

And your code will be run right after the HTTP response is sent (note: this can potentially saturate the server's available threads and effectively block subsequent HTTP requests!)

Configuration

The gem offers a few configuration options:

MaybeLater.config do |config|
  # Will be called if a `MaybeLater.run {}` callback errors, e.g.:
  config.on_error = ->(error) {
    # e.g. Honeybadger.notify(error)
  }

  # Will run after each `MaybeLater.run {}` block, even if it errors
  config.after_each = -> {}

  # By default, tasks will run in a fixed thread pool. To run them in the
  # thread dispatching the HTTP response, set this to true
  config.inline_by_default = false

  # How many threads to allocate to the fixed thread pool (default: 5)
  config.max_threads = 5
end

Help! Why isn't my code running?

If the blocks you pass to MaybeLater.run aren't running, possible explanations:

  • Because the blocks passed to MaybeLater.run are themselves stored in a thread-local array, if you invoke MaybeLater.run from a thread that isn't handling with a Rack request, the block will never run
  • If your Rack server doesn't support rack.after_reply, the blocks will never run
  • If the block is running and raising an error, you'll only know about it if you register a MaybeLater.config.on_error handler

Acknowledgement

The idea for this gem was triggered by this tweet in reply to this question. Also, many thanks to Matthew Draper for answering a bunch of questions I had while implementing this.

Code of Conduct

This project follows Test Double's code of conduct for all community interactions, including (but not limited to) one-on-one communications, public posts/comments, code reviews, pull requests, and GitHub issues. If violations occur, Test Double will take any action they deem appropriate for the infraction, up to and including blocking a user from the organization's repositories.