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

俺の報告

RoomClipを運営するエンジニアの日報(多分)です。

CodeigniterのPDOドライバで安全にDB再接続 - 日報 #128

有名なフレームワークを使っていて、
なんか変だなぁと思う時は大体自分が変な時なんですが、
極稀に「やっぱり変だなぁ」ということもあります。
今日はそんな箇所があったので少しご報告。

バージョン2.2.1がリリースされ、さらに3.0もRCとなってにわかに活気づいているCodeigniterですが、 バージョン2.1系を使用し続けている方も沢山いらっしゃると思います。
本日はsystem配下のファイルに、「泣く泣く」追加コードをいれてしまった事件について共有をいたします。

諸々の事情にて、MySQLへのコネクションタイムアウトを小さめの値にしていると、
バッチ処理などにおいて困った自体になることがあります。
サービスフロントのコードなら1秒かかるような処理なんて見当たらない「はず」ですので、
どうwait_timeoutを小さくしていても滅多なことがない限り1処理中でコネクションロストすることなんてありません。
ですが、そうでないようなシーンというのも勿論多々あるわけで。
コネクション確立をしてから計算ノード側で数十秒たってから、改めてクエリリクエストを投げるなんてことも、
まぁないわけではないんですね。
その際、コネクションが正しく存在しているのかを安全に確認しなければなりません。
CodeigniterにはDB_driverみたいなクラスにreconnectメソッドが用意されており、
あたかも任意のタイミングで再接続できるようになっております。
ですが、PDO_driverの場合、どうやらそうでもないようでした。
少なくとも僕の環境(バージョン 2.1.4)では、reconnectしても特に何も起こりませんでした。

ということで、よくよくSysytemディレクトリ配下のPDO_driverクラスを読んでみると、
確かにreconnect関数は「別段何もしてない」ように読めます。
これは困った。
というか、、、そもそも、コネクションの確認作業自体は、クエリ実行の直前で、暗黙的に呼ばれているべきです。 そうでなければ安全にクエリ実行ができません。
さらに言えば、確実にコネクションが確立されていると考えられる時は、無意味に接続確認をしたくはありません。
無駄な負荷になる可能性が高いからです。
そこのところを暗黙的に上手くやってくれるのがDB_driverだと思っていたのですが、、、
んー…

ということで、本当に仕方なく、嫌々、DB_driver.phpを改編することにしました。
やることは2つです。

  1. 実際のクエリ実行の直前で、きちんとDBコネクションが確立されているのかを確認する
  2. コネクション確立は無闇矢鱈に行わない

ということで、このような仕様にします。

  1. コネクションが確立されているかチェックするpingメソッドを作成する
  2. コネクションが無ければ、明示的にクローズし、再接続を試みる
  3. 以上の処理を一定の時間間隔で行う

3番が厄介そうですが、
まぁ「最後に接続が確認されてから、次のクエリが走る直前までに3秒以上かかっていたら、接続確認を改めて行う」
という程度にしておけば、まぁ機能は果たすでしょう。

ということで、まずpingメソッドの実装です。

<?php
/**
* Ping to the sql connection handler.
* @return bool
*/
function ping()
{
  if ( ! $this->conn_id)
  {
    return FALSE;
  }
  try
  {
    $result_id = @$this->_execute('SELECT 1');
    $driver = $this->load_rdriver();
    $RES = new $driver();
    $RES->conn_id = $this->conn_id;
    $RES->result_id = $result_id;
    $result = $RES->result();
    if ( ! $result)
      return FALSE;
    else
      return TRUE;
    }
    catch (Exception $e)
    {
        return FALSE;
    }
}

コネクションハンドラは$this->conn_idに保存されているので、
それに対してSELECT 1を実効するという方式です。
あとは、クエリ実行の直前でこのpingを放つコードを挿入します。

<?php
     // メンバ変数に書きを追加
     var $last_act_time  = 0; // 最後にアクション(クエリ実行または接続)
     var $active_timeout = 3; // タイムアウト秒数

...

          // コネクションを確立した直後にlast_act_timeを代入する
          // Connect to the database and set the connection ID
          $this->conn_id = ($this->pconnect == FALSE) ? $this->db_connect() : $this->db_pconnect();
          // 追加 start --------------------------
          $this->last_act_time = time();
          // 追加 end --------------------------

...

     // 既に存在しているはずの simple_queryメソッドに対して、コード追加
     function simple_query($sql)
     {
          // 追加 start ---------------------------
          $execute_time = time();
          if ( ! $this->last_act_time)
          {
               $this->last_act_time = $execute_time;
          }
          if ( ! $this->active_timeout)
          {
               $this->active_timeout = 3;
          }
          if (($execute_time - $this->last_act_time) > $this->active_timeout)
          {
               if ( ! $this->ping())
               {
                    $this->close();
                    $this->initialize();
               }
          }
          $this->last_act_time = $execute_time;
          // 追加 end ---------------------------

...

こうしておけば、接続してから、またはクエリを実行してから3秒後に再びクエリを実行しようとした時に、
安全に接続確認、再接続が実行されます。
多分。
systemフォルダに手を加えるのは全力でやらないほうがいいことなので、
特に必要ない限りやらないほうがいいと思いまうす。
ですが、まぁ世界の何処かに困っている人がいれば、、、ということで共有いたします。
とにかくreconnectメソッドに頼るのはちょっと避けたほうがいいかもね、という話でした。