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 with- wait. These objects are- Promises made by- concurrent-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 by- deferand 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.