彻底理解了Rails的session

こんにちは、食べログ DevOps チーム / データサイエンスチームの爲岡です。
食べログ Advent Calendar 2018 に書くのは2度めです。
お時間ございましたら、1つめの記事も読んでいただけると嬉しいです。

使用 Docker 容器来构建 Ansible 控制器/目标机

首先

请看 https://togetter.com/li/1268851。
以下是文章的摘录。

【工程师术语解释】

「完全理解」
指完成了使用产品所需的教程,能够完整地掌握。

「一窍不通」
表示已经深入了解产品固有问题,面对直接的挑战。

「能勉强做到」
指可以独自从零开始制作相同的产品,或者指开发者本人。

私は今は DevOps チームとデータサイエンスチームでエンジニアをしてますが、新卒で入社した当初は決済システムの Rails エンジニアでした。
正直に言って、未だに Rails について詳しいとは言えません。

有一天,我在编写应用程序的控制器时,突然想到了一个问题:”Rails 的会话是通过什么机制工作的呢?”

为了完全理解 Rails 的 session,只需要完成 Rails 教程中的 Session 部分就可以了,所以我试着做了一下(Ruby on Rails 的 Session 教程在这里)。

然而,只是说“我完成了教程!”还有点寂寞,所以……

怎么样将 session 的值加载到 session[:hoge] 中?

我深入研究了Rails的代码,所以决定将这些研究写成一篇文章。

また、Rails では Session store には Cookie store を利用することを推奨していますが、一方でサービス開発において Memcached などの KVS を session 管理に利用することも多いと思います。
ということで、改めまして、今回は、

如何从Memcached加载session[:hoge]中的session值?

这是关于……的话题。

session[]的实体存在于哪里? (Sesssion [] de yú ?)

应用程序:’hoge’

たとえば、Rails がインストールされた状態でrails new hogeすると Rails アプリケーションができあがって、hogeディレクトリ内でrails g controller sessionsすると ApplicationController を継承した SessionsController が生成されますが、はじめからsession[:hoge]といった書き方で session を利用することができます。

class SessionsController < ApplicationController
  def create
    if session[:user_id]
      flash[:notice] = 'ログイン済みです'
    else
      session[:user_id] = params[:user_id]
    end
  end

  ...
end

这个家伙的本质是什么?

会话不是实例变量,也没有作为本地变量定义,所以我们怀疑 ApplicationController 拥有此方法,并且我们将查看 ApplicationController 的实现。

然而,在这里并没有关于session的描述。

由于 ApplicationController 继承自 ActionController::Base,现在我们来看这个实现。从现在开始,进入了 Rails 自身的世界。从我的应用程序 hoge 中出发,阅读 Rails 自身的代码。

参考:GitHub 上的 rails/rails 项目

宝石:’Rails’

当我们查看ActionController::Base时,我们只能找到有关session接口的注释,而实际上并没有session方法。

そろそろ飽きてきましたが、ActionController::Base の継承元の ActionController::Metal というクラスにsessionメソッドがありました。

module ActionController
  ...

  class Metal < AbstractController::Base
    ...

    delegate :session, to: "@_request"

    ...
  end

  ...
end

なるほど、ActionController::Metal はsessionメソッドをインタフェースとして持っていますが、その中身はインスタンス変数@_requestにdelegateされていますね。

那么,这个@_request是什么呢?

因为我不明白,所以我将稍微调整之前创建的应用程序hoge的SessionsController来确认一下。

class SessionsController < ApplicationController
  def create
    raise session.inspect

    # if session[:user_id]
    #   flash[:notice] = 'ログイン済みです'
    # else
    #   session[:user_id] = params[:user_id]
    # end
  end

  ...
end

当以某种方式发送请求或提出要求后,你会发现@_request的本质是ActionDispatch::Request::Session类的对象。

那么让我们去看一下 ActionDispatch::Request::Session。

...

module ActionDispatch
  class Request
    ...

    class Session
      ...

      def [](key)
          @delegate[key]
      end
      ...

      def []=(k, v);        @delegate[k] = v; end
      ...
    end
  end
end

ようやくありました!
[]メソッドと[]=メソッドです。

つまりまとめると、

アプリケーションの controller にsession[:hoge]と書くと、リクエストが来たときに、その controller の継承元クラスである ActionController::Metal がdelegateしている ActionDispatch::Request::Session クラスのオブジェクトであるsessionのインスタンスメソッドである[]メソッドが呼び出される、と。

こうしてコードを追ってみるとなかなか複雑ですね。

如何从Memcached加载数据?

到目前为止,我们已经了解了提供接口session []的方式,但是如果将Memcached用作会话存储,那么在[]方法中应该存在从Memcached加载会话值的过程。让我们找出它。

继续使用 gem: ‘rails’

[]メソッドはload_for_read!メソッドを呼び出した後にインスタンス変数@delegateから key に対する value を read していますが、load_for_read!メソッドでは下記のload!メソッドを参照しています。

def load!
  id, session = @by.load_session @req
  options[:id] = id
  @delegate.replace(stringify_keys(session))
  @loaded = true
end

このように、load!メソッドの中で、インスタンス変数@byのload_sessionに@reqを引数に渡して呼び出していますが、
@reqは何かというと、ActionDispatch::Request クラスのオブジェクトであり、
@byは、今回の場合は Memcached をミドルウェアとして利用しているので、 ActionDispatch::Middleware::Session::MemCacheStore クラスのオブジェクトになります。

那么,让我们来看一下ActionDispatch::Middleware::Session::MemCacheStore。
由于整个代码量很短,我将全部贴出来。

require "action_dispatch/middleware/session/abstract_store"
begin
  require "rack/session/dalli"
rescue LoadError => e
  $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
  raise e
end

module ActionDispatch
  module Session
    # A session store that uses MemCache to implement storage.
    #
    # ==== Options
    # * <tt>expire_after</tt>  - The length of time a session will be stored before automatically expiring.
    class MemCacheStore < Rack::Session::Dalli
      include Compatibility
      include StaleSessionCheck
      include SessionObject

      def initialize(app, options = {})
        options[:expire_after] ||= options[:expires]
        super
      end
    end
  end
end

嗯,似乎这里没有load_session方法。

当查看 ActionDispatch::Middleware::Session::AbstractStore 被 require 的时候,可以发现 load_session 方法本身是存在的,但是它没有实际实现。

def load_session(env)
  stale_session_check! { super }
end

在load_session函数内部,进行了stale_session_check!操作,并且在其中使用yield语句。

def stale_session_check!
  yield
rescue ArgumentError => argument_error
  if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
    begin
      # Note that the regexp does not allow $1 to end with a ':'.
      $1.constantize
    rescue LoadError, NameError
      raise ActionDispatch::Session::SessionRestoreError
    end
    retry
  else
    raise
  end
end

在这里,stale_session_check!的块参数是super,所以应该在继承的类中有load_session的实体。当查看继承的Rack::Session::Abstract::Persisted时,可以找到其中的内容。

因此,我們現在轉往Rack的世界,離開Rails的領域。

参考:GitHub上的rack/rack

宝石:’架’

以下是提取Rack::Session::Abstract::Persisted代码的摘录。

...

class Persisted
  ...

  def load_session(req)
    sid = current_session_id(req)
    sid, session = find_session(req, sid)
    [sid, session || {}]
  end

  ...
end

在load_session中调用了find_session,但是find_session并未在Rack::Session::Abstract::Persisted中实现。
find_session位于Rails的ActionDispatch::Middleware::Session::MemCacheStore的父类Rack::Session::Dalli中。

所以,最后我们来谈谈Dalli的世界。
Dalli是一个备受赞誉的Rails Memcached客户端库。

参考: GitHub上的petergoldstein/dalli

宝石:“达利”

在 Rack::Session::Dalli 类中有一个 find_session 方法,但在其中调用了 get_session 方法。

def find_session(req, sid)
  get_session req.env, sid
end

进一步观察,get_session函数内部对with_block方法的返回值传递了一个块,而在该块中执行了dc.get操作。

def get_session(env, sid)
  with_block(env, [nil, {}]) do |dc|
    unless sid and !sid.empty? and session = dc.get(sid)
      old_sid, sid, session = sid, generate_sid_with(dc), {}
      unless dc.add(sid, session, @default_ttl)
        sid = old_sid
        redo # generate a new sid and try again
      end
    end
    [sid, session]
  end
end

with_blockは Rack::Session::Dalli 内でメソッドとして定義されていて、インスタンス変数@poolに対してwithメソッドを呼び出しています。

def with_block(env, default=nil, &block)
  @mutex.lock if @mutex and env['rack.multithread']
  @pool.with(&block)
rescue ::Dalli::DalliError, Errno::ECONNREFUSED
  raise if $!.message =~ /undefined class/
  if $VERBOSE
    warn "#{self} is unable to find memcached server."
    warn $!.inspect
  end
  default
ensure
  @mutex.unlock if @mutex and @mutex.locked?
end

在这里,实例变量@pool在Rack::Session::Dalli的初始化方法中被定义,如果缓存不存在,则会创建一个 ::Dalli::Client 对象。换句话说,@pool是 ::Dalli::Client 类的一个实例。

在Dalli::Client中定义了with方法,其内容只有yield self。

def with
  yield self
end

换句话说,在 with_block 内部,dc.get 调用了 ::Dalli::Client 类的对象的 get 方法。在这里,从 Memcached 中获取的值会作为 ActionDispatch::Request::Session 类对象的 @_request 实例的键和值。

尽管变得很长,但现在我们可以通过代码了解如何从Memcached加载session[:hoge]到session变量中。

我推荐您尝试阅读一次Ruby库的代码,如Rails,因为它不仅能帮助您了解其工作原理,还能提供有用的设计和实现参考。

下次是由@ham0215创作的”创建一个能将大和精神注入Ruby的宝石”。敬请期待!

广告
将在 10 秒后关闭
bannerAds