読者です 読者をやめる 読者になる 読者になる

dalliはfork safeか?

ふと、dalliがunicornなどのmulti processで動作するアプリケーションで動かしたときにコネクションを張りなおす処理をしないといけないか気になったので、調べてみた。


gist8284358

これを実行してみると、

true
1
I, [2014-01-07T00:27:41.396287 #9290]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.396622 #9291]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.396905 #9292]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.397327 #9293]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.397726 #9294]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.398108 #9295]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.398495 #9296]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.399108 #9297]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.399440 #9298]  INFO -- : localhost:11211 failed (count: 0)
I, [2014-01-07T00:27:41.399961 #9299]  INFO -- : localhost:11211 failed (count: 0)
[9290, 0, 1]   
[9292, 0, 1]   
[9291, 0, 1]   
[9290, 1, 1]   
[9293, 0, 1] 


こんな感じになり、forkした直後のログで失敗した、というエラーメッセージが出るが、その後、再接続し、通信できているようだ。

dalliのソースコード(acb1ff3afd4d)をpidでgrepすると、

# lib/dalli/server.rb
    def verify_state
      failure!(RuntimeError.new('Already writing to socket')) if @inprogress
      failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid
    end


という処理が出てきて、newしたときと、pidが変わっているかをチェックしていることがわかる。

このverify_stateはクライアントがリクエストしようとしたときには必ず通過するようになっているので、これでpidの変更を検出しているようだ。

failureの中では、

    def failure!(exception)
      message = "#{hostname}:#{port} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
      Dalli.logger.info { message }

      @fail_count += 1
      if @fail_count >= options[:socket_max_failures]
        down!
      else
        close
        sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
        raise Dalli::NetworkError, "Socket operation failed, retrying..."
      end
    end

のようになっていて、失敗回数を保持するカウンタを1あげつつ、例外を吐いている。

ここで発生させた例外は、

# lib/dalli/client.rb
    # Chokepoint method for instrumentation
    def perform(*all_args, &blk)
      return blk.call if blk
      op, key, *args = *all_args

      key = key.to_s
      key = validate_key(key)
      begin
        server = ring.server_for_key(key)
        ret = server.request(op, key, *args)
        ret
      rescue NetworkError => e
        Dalli.logger.debug { e.inspect }
        Dalli.logger.debug { "retrying request with new server" }
        retry
      end
    end

serverを呼びだしている元の、performメソッドで、rescueされて、retryするようだ。