I've recently found myself passing a lot of parameters down from controllers to service objects and then to jobs, etc.
This was one of the problems that were solved by the context pattern in React so I tried to do the same in the Rails app that I've been working on.
I had seen something a bit similar to in the I18n.with_locale
function.
So I wrote this:
# frozen_string_literal: true
require "concurrent-ruby"
class RequestValueContext
class << self
# For the multi threaded environment
@@request_value = Concurrent::ThreadLocalVar.new
def with(request_value)
if get.present?
raise ContextAlreadyDefinedError,
"Context already defined!"
end
begin
@@request_value.value = request_value
yield
ensure
@@request_value.value = nil
end
end
def get
@@request_value.value
end
end
ContextAlreadyDefinedError = Class.new(StandardError)
end
And in the ApplicationController
I've added this:
class ApplicationController < ActionController::Base
around_action :with_context
def with_context
RequestValueContext.with("foo") do
yield
end
end
end
Then I can access the value using RequestValueContext.get
from any method that is called "within the controller stack".
A nice feature of this pattern is that the current context can be captured when the using ActiveJob::Callbacks.before_enqueue
and then provided by ActiveJob::Callbacks.around_perform
like so:
# frozen_string_literal: true
module WithContexts
extend ActiveSupport::Concern
REQUEST_VALUE_KEY = "request_value"
included do
attr_reader :request_value, :deserialize_called
before_enqueue :capture_context
around_perform :provide_context
end
def serialize
super.merge(REQUEST_VALUE_KEY => request_value)
end
def deserialize(job_data)
# "detects" when a job is called by *perform_now*
@deserialize_called = true
super
@doorkeeper_application = request_value
end
def capture_context
@doorkeeper_application = RequestValueContext.get
end
def provide_context
if job_called_by_perform_now?
# if the job is called by *perform_now* it will be executed inline
# with the current context
yield
else
RequestValueContext.with_application(request_value) do
yield
end
end
end
def job_called_by_perform_now?
!deserialize_called
end
end
I believe something similar could be done for Proc/Block/Lambda.
I started writing Ruby less than a year ago and I found it to be quite a tricky language so if you have any feedback please let me know.
Top comments (6)
I really liked your approach, very clean. Why do you need the thread safe var?
It was a recommendation from one of my colleagues I believe it's for it to work with Puma and Sidekiq.
Both allow multiple requests/jobs to be executed at the same time in different threads.
I get it, since this is a class variable, the class is defined at process level (puma worker), each thread (puma thread within the worker) can access the class defined in the process and its variables, so you need it to be thread safe so each thread can define its own value.
Here is the thing. Puma uses thread pools. So a thread might be reutilized from one request to another later. In that case, the class would still have the "threadsafe" var with the old value, and a ContextAlreadyDefinedError would be risen
I suggest you to use a "clean up mechanism" or use a gem like github.com/ElMassimo/request_store...
quoting the gem:
...values can stick around even after the request is over, since some servers have a pool of Threads that they reuse, which can cause bugs.
using that gem you don't even need the
with
block thing.you can for example in the controller:
and then in the model:
I agree that
request_store_rails
would be an alternative solution for my problems.Maybe I'm missing something but the
with
block is the "clean up mechanism".totally, my bad!