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 invokeMaybeLater.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.