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).
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!
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.
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.
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. Additionally,
if there are any inline tasks to be run, the response will include a
"Connection: close
header so that the browser doesn't sit waiting on its
connection while the web thread executes the deferred code.
[Warning about inline
: running
slow inline tasks runs the risk of saturating the server's available threads
listening for connections, effectively shifting the slowness of one request onto
later ones!]
The gem offers a few configuration options:
MaybeLater.config do |config|
# Will be called if a block passed to MaybeLater.run raises an error
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
# If set to true, will invoke the after_reply tasks even if the server doesn't
# provide a rack.after_reply array.
# One reason to do this is if you are using Rails controller tests
# (with no webserver) rather than system tests.
# config.invoke_even_if_server_is_unsupported = Rails.env.test?
config.invoke_even_if_server_is_unsupported = false
end
If the blocks you pass to MaybeLater.run
aren't running, possible
explanations include:
- 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
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.
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.