TL;DR

它们都是线程缓存工具,并且能在请求结束时自动清理

CurrentAttributes通过定义子类的方式,帮助归类线程变量,而RequestStore则通常是直接存取

RequestStore的清理是通过增加middleware,而CurrentAttributes是通过ActiveSupport::Executor

RequestStore有一个扩展request_store-sidekiq,可以让你的sidekiq程序也能自动清理线程缓存,而CurrentAttributes没有配套工具,需要自己实现

RequestStore

request_store的使用方法非常直接,例如:RequestStore.store[:foo] ||= 123

module RequestStore
  def self.store
    Thread.current[:request_store] ||= {}
  end

  def self.clear!
    Thread.current[:request_store] = {}
  end
end


线程缓存的清空方法,是通过增加一个middleware来实现的:

module RequestStore
  class Railtie < ::Rails::Railtie
    initializer "request_store.insert_middleware" do |app|
      if ActionDispatch.const_defined? :RequestId
        app.config.middleware.insert_after ActionDispatch::RequestId, RequestStore::Middleware
      else
        app.config.middleware.insert_after Rack::MethodOverride, RequestStore::Middleware
      end

      if ActiveSupport.const_defined?(:Reloader) && ActiveSupport::Reloader.respond_to?(:to_complete)
        ActiveSupport::Reloader.to_complete do
          RequestStore.clear!
        end
      elsif ActionDispatch.const_defined?(:Reloader) && ActionDispatch::Reloader.respond_to?(:to_cleanup)
        ActionDispatch::Reloader.to_cleanup do
          RequestStore.clear!
        end
      end
    end
  end
end


顺提,request_store-sidekiq就是这样利用sidekiq chain而已,也是非常简单

module RequestStore
  module Sidekiq
    class ServerMiddleware
      def call(worker, job, queue)
        yield
      ensure
        ::RequestStore.clear!
      end
    end
  end
end


CurrentAttributes

以下是CurrentAttributes的解析

class ActiveSupport::CurrentAttributes
  include ActiveSupport::Callbacks
  define_callbacks :reset

  class << self
    # 在线程变量中找回CurrentAttributes子类的实例(如果没有就会设置一个)
    def instance
      current_instances[current_instances_key] ||= new
    end

    # 方法generated_attribute_methods会创建一个匿名module,并让CurrentAttributes子类include之
    # 然后在该module上定义一些名字为names的一些实例方法和类方法
    # 类方法其实调用线程变量中的子类的实例的实例方法,去set或get在@attributes中的对应name的值
    #
    # 例如以下,每个请求都会由一个线程去运行,于是调用Current.user会在线程中创建一个Current实例
    # 然后在设置该实例@attributes[:user] = User.find_by...
    #
    # class Current < ActiveSupport::CurrentAttributes
    #   attribute :user
    #
    #   resets { Time.zone = nil }
    #
    #   def user=(user)
    #     super
    #     self.account = user.account
    #     Time.zone    = user.time_zone
    #   end
    # end
    #
    # class SampleController < ApplicationController
    #   before_action :set_context
    #
    #   def set_context
    #     Current.user = User.find_by(id: cookies.encrypted[:user_id])
    #   end
    # end
    #
    def attribute(*names)
      generated_attribute_methods.module_eval do
        names.each do |name|
          define_method(name) do
            attributes[name.to_sym]
          end

          define_method("#{name}=") do |attribute|
            attributes[name.to_sym] = attribute
          end
        end
      end

      names.each do |name|
        define_singleton_method(name) do
          instance.public_send(name)
        end

        define_singleton_method("#{name}=") do |attribute|
          instance.public_send("#{name}=", attribute)
        end
      end
    end

    # 还可以定义一些回调,在请求结束时调用
    def resets(&block)
      set_callback :reset, :after, &block
    end

    private
      # 这里的include,是让CurrentAttributes子类include匿名module
      def generated_attribute_methods
        @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
      end

      # 如果有定义多个CurrentAttributes子类,例如CurrentUser < CurrentAttributes,UpStream < CurrentAttributes
      # 则Thread.current[:current_attributes_instances] = {'CurrentUser' => CurrentUser.new, 'UpStream' => UpStream.new}
      # 当然,在不同线程中分配的是不同的数据块
      def current_instances
        Thread.current[:current_attributes_instances] ||= {}
      end

      # key用的是子类的类名
      def current_instances_key
        @current_instances_key ||= name.to_sym
      end
  end

  attr_accessor :attributes

  def initialize
    @attributes = {}
  end
end


那么它是如何做到请求结束时清空线程缓存的呢?通过ActiveSupport::Executor。

每个rails应用启动时都会内含一个ActiveSupport::Executor实例,它会被包装在ActionDispatch::Executor中,

而ActionDispatch::Executor本身是一个middleware,会在请求结束时调用ActiveSupport::Executor#complete!,

从而触发ActiveSupport::CurrentAttributes.reset_all

# lib/active_support/railtie.rb
module ActiveSupport
  class Railtie < Rails::Railtie

    initializer "active_support.reset_all_current_attributes_instances" do |app|
      app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
      app.executor.to_run              { ActiveSupport::CurrentAttributes.reset_all }
      app.executor.to_complete         { ActiveSupport::CurrentAttributes.reset_all }
    end
  end
end


# lib/action_dispatch/middleware/executor.rb
module ActionDispatch
  class Executor
    def initialize(app, executor)
      @app, @executor = app, executor
    end

    def call(env)
      state = @executor.run!
      begin
        response = @app.call(env)
        returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
      ensure
        state.complete! unless returned
      end
    end
  end
end


# lib/rails/application/default_middleware_stack.rb
module Rails
  class Application
    class DefaultMiddlewareStack
      def build_stack
        ActionDispatch::MiddlewareStack.new do |middleware|
          # ...
          middleware.use ::ActionDispatch::Executor, app.executor


清空的方法reset_all,如下

class ActiveSupport::CurrentAttributes
  include ActiveSupport::Callbacks
  define_callbacks :reset

  class << self
    # 可以设置before回调
    def before_reset(&block)
      set_callback :reset, :before, &block
    end

    # 可以设置after回调
    def resets(&block)
      set_callback :reset, :after, &block
    end
    alias_method :after_reset, :resets

    # 在类上调用reset,实质上是调用本线程变量中的实例的reset
    delegate :set, :reset, to: :instance

    # 对本线程中所有创建过的CurrentAttributes子类实例,调用reset方法
    def reset_all
      current_instances.each_value(&:reset)
    end

    # 当重新加载类时,除了通过reset_all把attributes清空
    # 还要把CurrentAttributes子类实例清掉
    def clear_all
      reset_all
      current_instances.clear
    end
  end

  def reset
    run_callbacks :reset do
      self.attributes = {}
    end
  end
end