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
withblock thing.you can for example in the controller:
and then in the model:
I agree that
request_store_railswould be an alternative solution for my problems.Maybe I'm missing something but the
withblock is the "clean up mechanism".totally, my bad!