Deferred execution
Defer adds three methods for working with deferred code execution:
deferaccepts a block and executes it (potentially) on a thread pool. It returns an object that can be awaited withwait. These objects arePromises made byconcurrent-ruby. You can use their API, but it's not fully supported and tested in conjunction with effects.waitaccepts a promise or an array of promises returned bydeferand returns their values. The method blocks the current thread until all values are available.laterpostpones block execution until the handler is finished (see examples below).
Defer
A simple example:
class CreateUser
include Dry::Effects.Resolve(:user_repo, :send_invitation)
include Dry::Effects.Defer
def call(values)
user = user_repo.create(values)
defer { send_invitation.(user) }
user
end
end
In the code above, send_invitation is run on a thread pool. It's the simplest way to run code concurrently.
Code within the defer block can use some effects but not all of them. For instance, Interrupt is not supported because you cannot return from one thread to another. This is a limitation of Ruby and threads in general.
Handling
The default handler uses concurrent-ruby to do the heavy lifting. As an option, it accepts the executor—usually a thread pool where the code will be run.
Three special values are also supported:
:ioreturns the global pool for long, blocking (IO) tasks,:fastreturns the global pool for short, fast operations, and:immediatereturns the global ImmediateExecutor object.
By default, Dry::Effects::Handler.Defer uses :io.
class HandleDefer
include Dry::Effects::Handler.Defer(executor: :immediate)
def initialize(app)
@app = app
end
def call(env)
# defer tasks in @app will be run on the same thread
with_defer { @app.(env) }
end
end
The executor can be passed directly to with_defer:
def call(env)
with_defer(executor: :fast) { @app.(env) }
end
Using null executor
For skipping deferred tasks, create a mocked executor
require 'concurrent/executor/executor_service'
NullExecutor = Object.new.extend(Concurrent::ExecutorService).tap do |null|
def null.post
end
end
and provide it in middleware
class HandleDefer
include Dry::Effects::Handler.Defer
include Dry::Effects::Handler.Env(:environment)
def initialize(app)
@app = app
end
def call(env)
with_defer(executor: executor) { @app.(env) }
end
def executor
environment.equal?(:test) ? NullExecutor : :io
end
end
Later
later doesn't return a result that can be awaited. Instead, it starts deferred blocks on handler exit. Consider this example:
class CreateUser
def call(values)
user_repo.transaction do
user = user_repo.create(values)
defer { send_invitation.(user) }
user_account = account_repo.create(user)
user
end
end
end
There is no guarantee send_invitation will be run after the transaction finishes. It may lead to race conditions or anomalies. If account_repo.create fails with an exception, the transaction will be rolled back yet the invitation will be sent!
later captures the block but doesn't run it:
later { send_invitation.(user) }
The invitaition will be sent when with_defer exits:
with_defer { @app.(env) }
It usually happens outside of any transaction so that anomalies don't occur.