Oct 31

先輩のamo-kさんから、「Symfonyのトランザクション処理時の動作について調べてみて」と言われたので、Symfonyの勉強がてら調べてみました。

Synfony のログからわかること

まず Symfony のログを見てみると、begin -> begin -> category テーブルの update -> commit -> begin -> begin -> bookmark テーブルの update -> commit -> commit という処理の流れになっています。

アクションで明示的に記述したトランザクションとは別に、$category->save では1重、$bookmark->save では2重のトランザクションが走っているように見えます。

モデルクラスの中身を見てみる

$bookmark と $category はそれぞれ Symfony のスクリプトによって自動生成されたモデルクラスである BookMark と Category のインスタンスです。

Bookmark と Category はそれぞれ BaseBookmark と BaseCategory を継承しており、実際の更新処理はこれらのベースクラスに記述されています。

save関数の中でもトランザクションが使われています。

BaseBookmark と BaseCategory の save 関数内に以下のような記述があります。

// BaseBookmark::save 関数の一部(BaseBookmark.php)、BaseCategory::save (BaseCategory.php)も同様
$con->begin();
$affectedRows = $this->doSave($con);
$con->commit();
return $affectedRows;

これを見てわかるとおり、 save 関数内でも $con->begin() ~ $con->commit() によって、トランザクション処理を行っています。

doSave 関数の中では、このインスタンスが新規のレコードなのか、レコードの更新なのかを判定し、新規であれば Peer クラスの doInsert を、更新であれば doUpdate を呼び出すようになっています。

以下、該当部分のソースコード。

// BaseBookmark::doSave 関数の一部(BaseBookmark.php)
  if ($this->isNew()) {
    $pk = BookmarkPeer::doInsert($this, $con);
    $affectedRows += 1;
    $this->setId($pk);
    $this->setNew(false);
  } else {
    $affectedRows += CategoryPeer::doUpdate($this, $con);
  }

doInsert 関数と doUpdate 関数は BookmarkPeer の親クラスである BaseBookmarkPeer クラスで定義されていています。

doInsert関数の中では、以下のように $con->begin() でトランザクションを開始したあと、BasePeer クラスの doInsert 関数を呼び出しています。これが、 $bookmark->save() を実行したときの、最も深い begin ~ commit の正体です。

// BaseBookmarkPeer::doInsert 関数の一部(BaseBookmark.php)
  try {
    $con->begin();
    $pk = BasePeer::doInsert($criteria, $con);
    $con->commit();
  } catch(PropelException $e) {
    $con->rollback();
    throw $e;
  }

なお、doUpdate 関数内ではトランザクションをかけていません。アクション内で記述していた $category->save() はすでにあるレコードの更新操作なので doUpdate が実行され、該当部分のトランザクションは2重までになります。

コネクションオブジェクトに対する関数呼び出しのログ出力処理

アプリケーションをデバッグモードで実行しているときは $con は sfDebugConnection というクラスのインスタンスになっていて、各モデルクラスはこのインスタンスを通して、DB操作を行うようになっています。

このクラスはsymfonyで提供されているもので、コネクションオブジェクトに対して実行された処理のログをとっています。例えば、commit 関数なら以下のようになっています。

 // sfDebugConnection.php から抜粋
  public function commit()
  {
    $this->log("{sfCreole} committing transaction.");
    return $this->childConnection->commit();
  }

関数が呼び出されると無条件にログ出力が行われます。そのため、今回のようにコネクションオブジェクトに対して最大で3重のトランザクションをかけている場合は、ログにもそのように出力されるわけです。

その次に $this->childConnection の同名の関数を呼び出すという処理になっています。
$this->childConnection は実際に使われているDBに応じた、Connectionインタフェースを実装したドライバになります。今回はMySQLを使用しているので、MySQLConnectionというクラスになっています。

MySQLConnection は ConnectionCommon というクラスを継承しています。DBの実装に関係なくコネクションで共通の処理が ConnectionCommon に記述されています。

begin関数、commit関数はConnectionCommonで以下のように定義されています。

// sfConnectionCommon.php から抜粋
    public function begin()
    {
        if ($this->transactionOpcount === 0 || $this->supportsNestedTrans()) {
            $this->beginTrans();
        }
        $this->transactionOpcount++;
    }

    public function commit()
    {
        if ($this->transactionOpcount > 0) {
            if ($this->transactionOpcount == 1 || $this->supportsNestedTrans()) {
                $this->commitTrans();
            }
            $this->transactionOpcount--;
        }
    }

begin関数で transactionOpcount を +1 し、commit関数で -1 するようになっています。

ここで supportNestedTrans 関数はDBがトランザクションの入れ子をサポートしている場合にtrueを返します。今回使用しているMySQLはトランザクションの入れ子をサポートしないので、この関数は false を返します。

ConnectionCommon のサブクラスで beginTrans および commitTrans 関数がオーバーライドされ、実際のトランザクション開始~コミット処理を行っています。

この部分の処理で、MySQLのようにトランザクションの入れ子をサポートしていないDBを使用している場合は最も外側のトランザクションのみが有効になることがわかります。

以上の処理をシーケンス図にすると、以下のようになります。



トランザクション処理時のシーケンス(拡大画像へリンク)

と、ここまでわかったところで、この記事を発見。まさに知りたかったことが書いてあるじゃないか・・・。

まあ、もう少し詳細にコードを追っているので、本記事も参考になるのではないでしょうか。

Oct 31

Symfony(phpのフレームワーク)にて気になる部分があったので若手ホープのhonda-hに調査してもらった。
※データベースはMySQL5、InnoDBを利用。

気になった部分
以下のコードサンプルのように
明示的にトランザクション処理を行ったとする。
Symfonyのログを見ると何故か4回づつbeginやcommitが行われたように
sfCreoleのログが出力されている。
これでは、以下のコードサンプルの更新2の部分で
エラーとなった場合に更新1も含めてロールバックしてほしいのに
更新1はコミットされたように見える。
しかし、SQLログを見るとサンプルコードで期待する結果となっている。
(begin ⇒ 更新1⇒ 更新2 ⇒ commit)

コードサンプル:

    // 実際はもう少し処理をしているがコアな部分以外は割愛する
...
    $bookmark = new Bookmark();
    $bookmark->setTitle($this->getRequestParameter('title'));
    $bookmark->setUrl($this->getRequestParameter('url'));

    if ($this->getRequestParameter('category_id'))
    {
        $bookmark->setCategoryId($this->getRequestParameter('category_id'));
        $category = CategoryPeer::retrieveByPk($this->getRequestParameter('category_id'));
        $category->incrementSize();
    }

    $con = Propel::getConnection();  // DBコネクションオブジェクト取得

    try
    {
        $con->begin();  // トランザクション開始
        if ($this->getRequestParameter('category_id'))
        {
            $this->logMessage('{sfAction} save category.', 'info');
            $category->save($con);  // 更新1
        }

        $this->logMessage('{sfAction} save bookmark.', 'info');
        $bookmark->save($con);  // 更新2

        $this->logMessage('{sfAction} commit transaction.', 'info');
        $con->commit();  // コミット
    }
    catch (Exception $e)
    {
        $con->rollback();  // ロールバック
...

Symfonyログサンプル

Oct 28 11:29:51 symfony [info] {sfCreole} prepareStatement(): SELECT category.ID, category.TITLE, category.SIZE FROM category WHERE category.ID=?
Oct 28 11:29:51 symfony [info] {sfCreole} executeQuery(): [0.47 ms] SELECT category.ID, category.TITLE, category.SIZE FROM category WHERE category.ID=5
Oct 28 11:29:51 symfony [info] {sfCreole} connect(): DSN: array (   'compat_assoc_lower' => NULL,   'compat_rtrim_string' => NULL,   'database' => 'criteria_test',   'encoding' => 'cp932',   'hostspec' => 'localhost',   'password' => 'criteria_test',   'persistent' => NULL,   'phptype' => 'mysql',   'port' => NULL,   'protocol' => NULL,   'socket' => NULL,   'username' => 'criteria_test', ), FLAGS: 0
Oct 28 11:29:51 symfony [info] {sfCreole} beginning transaction.
Oct 28 11:29:51 symfony [info] {sfAction} save category.
Oct 28 11:29:51 symfony [info] {sfCreole} beginning transaction.
Oct 28 11:29:51 symfony [info] {sfCreole} prepareStatement(): UPDATE category SET SIZE = ? WHERE category.ID=?
Oct 28 11:29:51 symfony [info] {sfCreole} executeUpdate(): UPDATE category SET SIZE = 5 WHERE category.ID=5
Oct 28 11:29:51 symfony [info] {sfCreole} committing transaction.
Oct 28 11:29:51 symfony [info] {sfAction} save bookmark.
Oct 28 11:29:51 symfony [info] {sfCreole} beginning transaction.
Oct 28 11:29:51 symfony [info] {sfCreole} beginning transaction.
Oct 28 11:29:51 symfony [info] {sfCreole} prepareStatement(): INSERT INTO bookmark (CATEGORY_ID,TITLE,URL,CREATED_AT,UPDATED_AT) VALUES (?,?,?,?,?)
Oct 28 11:29:51 symfony [info] {sfCreole} executeUpdate(): INSERT INTO bookmark (CATEGORY_ID,TITLE,URL,CREATED_AT,UPDATED_AT) VALUES (5,'koreha hidoi','http://korea.hidoi/','2008-10-28 11:29:51','2008-10-28 11:29:51')
Oct 28 11:29:51 symfony [info] {sfCreole} committing transaction.
Oct 28 11:29:51 symfony [info] {sfCreole} committing transaction.
Oct 28 11:29:51 symfony [info] {sfAction} commit transaction.
Oct 28 11:29:51 symfony [info] {sfCreole} committing transaction.

SQLログサンプル:

# at 3946
#081028 11:29:51 server id 1  end_log_pos 4023 	Query	thread_id=47	exec_time=0	error_code=0
SET TIMESTAMP=1225160991/*!*/;
BEGIN
/*!*/;
# at 4023
#081028 11:29:51 server id 1  end_log_pos 4143 	Query	thread_id=47	exec_time=0	error_code=0
SET TIMESTAMP=1225160991/*!*/;
UPDATE category SET SIZE = 5 WHERE category.ID=5
/*!*/;
# at 4143
#081028 11:29:51 server id 1  end_log_pos 4171 	Intvar
SET INSERT_ID=40/*!*/;
# at 4171
#081028 11:29:51 server id 1  end_log_pos 4401 	Query	thread_id=47	exec_time=0	error_code=0
SET TIMESTAMP=1225160991/*!*/;
INSERT INTO bookmark (CATEGORY_ID,TITLE,URL,CREATED_AT,UPDATED_AT) VALUES (5,'koreha hidoi','http://koreha.hidoi/','2008-10-28 11:29:51','2008-10-28 11:29:51')
/*!*/;
# at 4401
#081028 11:29:51 server id 1  end_log_pos 4428 	Xid = 334
COMMIT/*!*/;
DELIMITER ;

ということで、honda-hの調査結果を次に書きます。
ではhonnda-h、あとはよろしく~
調査結果

Oct 31

takada-atです。こんにちは。
amo-k先輩に「自分SMTPコマンドなんて打ったことないッス」と言うと、「おまえも打てよ、な?」と、まるで後輩に煙草を薦める不良の先輩のような調子で、SMTPコマンドを打つようにすごまれました。
というわけで今日は、telnetとSMTPコマンドを使い、メーラーになった気持ちでメールを送信してみます。

SMTPといっても何だかわからないという方もおられるでしょうが、SMTPは「Simple Mail Transfer Protocol(単純なメール転送プロトコロル)」の略であり、メール送信のために定められた手続きのことです。要するに「この決まりを守っていればメールを送受信できるよー」というきまりのことです。
どんなメーラーもメールサーバーも基本的には、SMTPに従った動作を実装しています。
Wikipediaの記事にリンクをはっておきます。
-Simple Mail Transfer Protocol - Wikipedia

最新のSMTPプロトコルは、RFC 5321で標準化されています。
わたしも全部読んだことはないのですが、以下にリンクを載せておきます。RFC5321の日本語訳は見つけられませんでしたが、旧版のRFC 821には複数の日本語訳があるようです。
-RFC 5321 - Simple Mail Transfer Protocol
-RFC日本語版リスト

SMTPでメールを送信するには、メールサーバーにtelnetでアクセスし、SMTPコマンドを送っていけばよいようです。
実際にやってみましょう。
同期の honda-h に「ランチに行きませんか」というメールを出してみます。
↓honda-h

以下入力したコマンドを、C:からはじまる行に、サーバーからの返答をH:からはじまる行に書きます。

まずtelnetコマンドを使いmail.example.comの25番ポートにログインします。
(アドレスはすべて架空のものです)。

# telnet mail.example.com 25
> 220 mail.example.com ESMTP

HELOコマンドを入力し、こちらのサーバー名を伝えます。

C:HELO localhost
H:250 mail.example.com

MAILコマンドを利用し、差し出しアドレスを伝えます。

C:MAIL FROM:takada-at@example.com
H:250 ok

RCPTコマンドを利用し、送り先アドレスを伝えます。

C:RCPT TO:honda-h@example.com
H:250 ok

メールの本文はDATAコマンドを使って送ります。「.」だけで終る行がメッセージの終了を意味します。
今回はマルチバイト文字を使わず、ローマ字で送ってみることにします。

C:DATA
H:354 Please start mail input.
C:
Subject: lunch
From: takada-at@example.com

honda-h san. gohan tabe ni ikimasyou.
.

H:250 Mail queued for delivery.

最後にQUITコマンドを利用し、コネクションを切断します。メールサーバーはちゃんと挨拶ができる子のようです。

C:QUIT
H:221 Closing connection. Good bye.

もう少し実験してみましょう。
HELO の際に、nothing.example.com と答え、noone@example.com という存在しないアドレスにメールを送ります。
DATA コマンド中の From: の値も、noone@example.com にしておきます。

220 mail.example.com ESMTP
HELO nothing.example.com
250 mail.example.com
MAIL FROM: takada-at@example.com
250 ok
RCPT TO: noone@example.com
250 ok
DATA:
354 Please start mail input.
Subject: aaa
From: noone@example.com
cccc

.
250 Mail queued for delivery.
QUIT
221 Closing connection. Good bye.

こういう入力の仕方でも、メーラーデーモンからのリプライが takada-at@example.com に届きました。
デーモンからの返信先は必ず MAIL FROMで指定したアドレスとなるようです。

以上です。簡単ながら、自分で打ってみると、ブラックボックスに見えていたメール送信の仕組みが、心で理解できた気がします。やったことのない方はぜひ一度試してみるとよいのではないでしょうか。

Oct 31

amo-kです。
さて、SMTPクライント絡みでSMTPの話題です。
聞くところによるとtakada-at以外はSMTPコマンドを打ったことがあるそうです。
ということで今回はtakada-atにSMTPコマンドを使って手動でメールを送ってもらうことにします。

では、takada-at、よろしくお願いします~

Oct 18

amo-kです。phpでSMTPクライアント書きました~

書いてみたきっかけ

PHPだとmail()mb_send_mail()でメール送信できちゃうけどよくよく見てみると、SMTPコマンドのHELOコマンドやMAIL FROMコマンドに値を指定できないぽい。最後の引数に、MTAに渡すコマンドラインオプションを指定可能だが、これはMTAに依存するということだ。MAIL FROMコマンドの値を指定したくても、この最後の引数に指定するしかないということになる。

では、何処でHELOコマンドやMAIL FROMコマンドの値を設定しているかというと、php.iniのSMTPディレクティヴやsendmail_fromディレクティヴの値となる。(Windows版phpのみ)つまり、アプリケーションレベルでこれ等の値を指定したい場合に、明確に指定するIFが無いということだ。

あれやこれやと考える時間がもったいないので、この前HTTPクライアント書いたし、せっかくなのでSMTPクライアントも書いて見たw

コード:
Continue reading »

Oct 17

takada-atです。
Rubyでソケットをいじっていたら、同じものをC/C++でも書いてみたくなりました。
そこで、C++でもHTTPクライアントに挑戦してみました。C/C++はよくわからないので、変なコードになっていると思いますが、遠慮なくつっこみをいただけるとうれしいです。
(そもそもコードが長すぎる気がします。。)

Continue reading »

Oct 17

amo-kさんにつづき、私(takda-at)もHTTPクライアントを実装してみました。
まずはRubyのコードです。Rubyでは、socketというネットワークプログラミング用のライブラリが標準で用意されています。その中でもTCPSocketなどのクラスを利用するとHTTPクライアントなども非常に簡単につくれるのですが、今回は勉強のため、あえて低レイヤーなところから書いています。

socketライブラリの中でも、Socketクラスは、ソケットをシステムコールレベルで操作するための機能を提供しています。メソッド名などもシステムコールと同じ名前が採用されているようです。
Rubyリファレンスマニュアルの説明にはそこまで詳細な解説が無いので、LinuxなどのManPageも合わせて見た方が参考になります。
また今回は勉強のために、socketライブラリのソースコードも少しのぞいてみました。socketライブラリはC言語で書かれたRubyの拡張ライブラリです。最新の安定板であるRuby1.8.7では、ruby-1.8.7-p72/ext/socket/socket.c にソースコードがあります。

参考
-Socket - Rubyリファレンスマニュアル-
-Manpage of SOCKET
-Manpage of GETHOSTBYNAME

難しかったのはSocket::connectを呼び出し、接続を行なうところです。このメソッドの引数には、バイナリデータを文字列の形でわたします。C 言語のconnect関数には、引数として、sockaddr構造体というものをわたすのですが、Socket::conncetメソッドの場合、Rubyの側からCの構造体を文字列の形でわたしてやる必要があります。
Rubyで、データをバイナリ文字列に変換するにはArrayクラスのpackメソッドを利用します。
Rubyでバイナリデータを扱うプログラムを書いたのははじめてだったので、非常に勉強になりました。

参考
-Manpage of CONNECT
-Array::pack - Rubyリファレンスマニュアル
-packテンプレート文字列 - Rubyリファレンスマニュアル

Continue reading »

Oct 16

Ruby でHTTP通信をする方法はいくつかあります。
最も簡単なのは、open-uriを使う方法でしょう。

単純にあるURIに対してGETリクエストを送り、返されたHTMLを表示するだけなら、以下のように1行で済ませることもできます。

$ ruby -ropen-uri -e 'open(ARGV[0]){|f| puts f.read }' http://www.klab.jp/

Continue reading »

Oct 11

最初の投稿はSocketプログラミング!

最初のネタは、Socketプログラミングをしてみよう!というネタです。

通常、我々はライブラリやパッケージを使ってWebアプリケーションを実装します。
例えばアプリケーションから任意のHTTPリクエストするような処理を実装する際は、TCPクライアントの処理を意識せずにライブラリを呼び出すだけで実装できたりします。それでもいいっちゃいいのですが、あえて自前で実装してみようという試みです。

こうすることで、普段我々があまり意識していないTCP/IPの世界を意識するきっかけとなるのではと考えております。ということで、まずはSocket関数を用いてHTTPクライアントを書いてみることにします。

書いてみたきっかけ

PHPやRuby等で何気に使っているHTTPクライアント。

PHPだとfopen()cURLでHTTPリクエストできたり、Rubyだとopen-uriのopen()等でHTTPリクエストができちゃいます。

ある日、自社Web API “FlaMixer” と連携する部分をPHPで書く機会があった。前述のfopen()やcURLのオプション指定だけでは連携が困難だったので、モジュールの使い方であれこれと悩むくらいなら自分で書いちゃえって事で書いてみました。これがきっかけでSocket関数を使ったコーディングを経験し、他の若手メンバにも経験してもらおう思いました。

ということで、言語はなんでもいいからHTTPクライアントを書いてみよう~

まずは言いだしっぺの若手amo-kのコード:

Continue reading »

Oct 11

  この度、KLab(株)の若手エンジニアがブログを始めることになりました。

日々の業務における技術的トピックや、何気なく使っている便利なライブラリの内部で行われている処理を覗いた時のハナシ。これ等をブログというカタチで面白いものを発信できるのでは、という発想からこのブログをはじめることとなりました。

まだまだ未熟な若手エンジニアですが、技術・知識に対するアグレッシヴなアプローチや新しいものを試すチャレンジ等見どころ満載なブログです!

コメントなど大歓迎です、ぜひチェックしてください!!