彻底理解了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的宝石”。敬请期待!