Perform Later
perform-later provides asynchronous worker/job support with a convention that encourages idiomatic class design.
It is a simple and lightweight abstraction that helps decouple asynchronous bus logic from object behavior logic.
It encourages more maintainable async/job/worker code that is easier to change and easier to test thoroughly and quickly.
In a nutshell, perform-later allows you to
replace this
# app/workers/do_work_worker.rb
class DoWorkWorker
include Sidekiq::Worker
def perform(async_data)
init_resource_from_async_bus(async_data)
# do work with resource
end
private
def init_resource_from_async_bus(some_param)
@resource = Resource.find_by_some_param(some_param)
end
endcalled by
DoWorkWorker.perform_later(resource.id)with this
# lib/my-app/some_class.rb
class SomeClass
include PerformLater
perform_later :do_work
def initialize(resource)
@resource = resource
end
def do_work
# do work with resource
end
module Async
def serialize(resource)
resource.id
end
def deserialize(id)
resource = Resource.find_by_id(id)
SomeClass.new(resource)
end
end
endcalled by
SomeClass.do_work_later(resource)Requirements
-
ruby2.2 or later - currently
Sidekiqis the only client supported
jruby 9 or later (experimental)
Installation
# Gemfile
gem "perform-later", "~> 1.1.1.a"or
$ gem install perform-later --preUsage Details
PerformLater adds support for making an existing method call asynchronously (e.g. through the out-of-process, asynchronous bus) with a call to perform_later.
class SomeClass
include PerformLater
perform_later :do_work
def do_work
SomeService.do_work
end
endSomeClass.do_work_laterThe class can declare that some deserialization should happen to put the object in the correct state when executing with data from the asynchronous bus.
class SomeClass
include PerformLater
attr_reader :resource1, :resource2
def initialize(resource1, resource2)
@resource1 = resource1
@resource2 = resource2
end
perform_later :do_work
def do_work
SomeService.do_work(resource1, resource2)
end
module Async
def deserialize(id1, id2)
resource1 = SomeResource.find(id1)
resource2 = SomeOtherResource.find(id2)
SomeClass.new(resource1, resource2)
end
end
endSomeClass.do_work_later(resource1.id, resource2.id)or, use the _async alias if you prefer
SomeClass.do_work_async(resource1.id, resource2.id)or, use the Async class method
SomeClass::Async.do_work(resource1.id, resource2.id)The class can further decouple from asynchronous implmentation by explicitly defining the serialization behavior within the Async module, instead having it implicitly defined separately by each caller.
class SomeClass
include PerformLater
attr_reader :resource1, :resource2
def initialize(resource1, resource2)
@resource1 = resource1
@resource2 = resource2
end
perform_later :do_work
def do_work
SomeService.do_work(resource1, resource2)
end
module Async
def serialize(resource1, resource2)
[resource1.id, resource2.id]
end
def deserialize(id1, id2)
resource1 = SomeResource.find(id1)
resource2 = SomeOtherResource.find(id2)
SomeClass.new(resource1, resource2)
end
end
endSomeClass.do_work_later(resource1, resource2)Logging
Including PerformLater adds a logger class and instance method to your class. It also logs the job id of the enqueued job at a debug level.
SomeClass.do_work_later
# => {"job_id": "a7be5c33", "class": "SomeClass", "method":"do_work", "msg": "queued for later execution"}
This includes any tags defined with TaggedLogger.
The PerformLater::logger is used, which defaults to Sidekiq.logger. It is recommended to set the Sidekiq logger to the application logger for the synchronous process, and vice-versa for the asynchronous process.
Rails/Sidekiq Example:
# production.rb
Rails.application.configure do
# only use one logger, determine based on process running
if $PROGRAM_NAME =~ /sidekiq/
config.logger = Sidekiq.logger
else
config.logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
config.log_tags = [ :uuid ]
Sidekiq.logger = config.logger
end
Rails.logger = config.loggerThis makes it easy to tie a job back to a request, among other things.
Advanced Usage
Aliasing
You may customize the name of the asynchronous entry point with the as option. One use case might be enabling asynchronous execution of methods that don't raise when they are unsuccessful. Most asynronous jobs are considered successful unless they raise an exception.
The :as option accepts a string, symbol, or array of strings/symbols each of which will act as an alias for the asynchronous entry point.
perform_later :do_work!, as: :do_work_later
def do_work
do_work!
rescue RecordInvalidError
false
end
def do_work!
#some work that might raise RecordInvalidError
endSomeClass.do_work_later
Design
Calling perform_later :do_work defines
- an
Asyncmodule on the caller (UserClass)- default implemenation for
Async.serializereturningnil - default implemenation for
Async.deserializereturningUserClass.new()
- default implemenation for
- a
Proxyclass on theAsyncmodule - a
do_workclass method on theAsyncclass- delegates to
Proxy.perform_later, which calls the asynchronous client
- delegates to
- a
do_work_laterclass method on the caller (UserClass)- delegates to
Async.do_work
- delegates to
The asynchronous client's entry point is this UserClass::Async::Proxy class, which proxies the method call to the deserialized object.
When enqueuing, the proxy class calls UserClass::Async.serialize to determine what parameters to place in the async bus, along with the method being proxied.
Upon dequeuing, this proxy object calls UserClass::Async.deserialize to determine what object will receive the proxied message.
Contributing
If you would like to see another client supported, please open an issue.
