Isn’t defining a job class for each async execution unit too much boilerplate?
Let’s try to implement seamless async execution in Ruby, using ActiveJob and proxy objects!
https://kzone.winosx.com/posts/asynchronous-method-calls-using-rails-s-activejob
Asynchronous method calls using Rails’s ActiveJob
ActiveJob is great – but every time you need to perform some kind of delayed execution, you need to create a new job class for it. Something like: class DestroyAccountJob < ActiveJob def perform(account) account.destroy! end end # In the code, this will be used like: DestroyAccountJob.perform_later(account) And ideally, the job #perform method should fit on a single line: no complex code in the job itself, to make it easier to test. But many other programming languages allow performing any method call in the background, without needing to declare a specific class. Could we cut the boilerplate also in Ruby? Turns out we can. Introducing AsyncExecutor This is a small toy snippet, to call any method in a background job. No need to define custom job classes! The simplest usage looks like this: include AsyncExecutor # Call `self.destroy!` in a background job perform_later(:destroy!) It will create and enqueue a AsyncExecutorJob, that will perform the given method. And we also get more advanced usages, using a Proxy object: balance = AccountBalance.create!(account: account) start_date = 3.months.ago end_date = Time.now # 1. With a method name balance.perform_later(:compute_balance, start_date:, end_date:) # 2. With a proxy object balance.perform_later.compute_balance(start_date:, end_date:) # 3. With a block balance.perform_later do compute_balance(start_date:, end_date:) end The code Only two files: # app/lib/active_job/async_executor.rb # Perform any action later (by enqueuing an ad-hoc job), without needing to define a new ActiveJob subclass. module ActiveJob::AsyncExecutor # Internal class. Converts each method called on `target` into an asynchronous job. class Proxy # @param target [StandardObject] def initialize(target) @target = target end def method_missing(method, *args, &block) AsyncExecutorJob.perform_later(@target, method, *args) end end # Call the given method later, in an ad-hoc job. # # @overload perform_later(method) # Invoke the given method on `self` later. # @param method [String, Symbol, nil] the name of the method to call # @return [Proxy] # @example # perform_later(:compute_balance, start_date:, end_date:) # # @overload perform_later() # Return a proxy to `self`, that will execute methods called on it later. # @return [Proxy] # @example # balance.perform_later.compute_balance(start_date:, end_date:) # # @overload perform_later{} # Evaluate the block in the receiver's context, and execute methods called on it later. # @yield [] a block evaluated in the receiver's context (optional) # @return [Proxy] # @example # balance.perform_later { compute_balance(start_date:, end_date:) } def perform_later(method = nil, *args, &block) proxy = Proxy.new(self) if block_given? proxy.instance_eval(&block) elsif method proxy.public_send(method, *args) else proxy end end end # app/jobs/async_executor_job.rb # See ActiveJob::AsyncExecutor class AsyncExecutorJob < ApplicationJob def perform(target, method, *args) target.public_send(method, *args) end end How does it work? The AsyncExecutor::Proxy object convert method calls into a job invocation. The receiver and its arguments are serialized using ActiveJob usual mechanisms – which means for instance that ActiveRecord objects get serialized into a neat and compact globalID. Should I use this in production? A nice property of class-based jobs is that is makes queuing and debugging easier: you can enqueue some jobs in specific queues, and get a neat dashboard with a description of all your jobs. Using AsyncExecutor#perform_later means that all invocations will be mixed in the same queue, and harder to debug. But for small systems, I’m curious of how it can reduce the boilerplate. It is small, monkey-patch-free, and works with any ActiveJob backend. Fell free to experiment with it. Prior art Sidekiq’s delay: a similar idea, but removed in 2022, to avoid monkey-patching and serialization of large objects. AsyncExecutor doesn’t monkey-patch, and uses ActiveJob’s lightweight serialization. perform-later: a nice gem, independent from ActiveJob (which means it can invoke on any Ruby object). However you have to handle serialization yourself, and it is tied to Sidekiq.
Qiita - 人気の記事
