admin のすべての投稿

SeleniumのStaleElementReferenceExceptionに対応する

概要

Selenium (javaAPI) を用いてスクレイピングを実施しするような仕組みを作っていたところタイトルにもあるエラーに遭遇した
(そうでなくてもSeleniumはなれるまでなかなかピーキーな動きをするときもあるが)

厄介なことに毎回必ず同じタイミングで発生するわけでもなく、その発生はかなり不定期である。
実施しているスクレピング処理においては、条件を引き起こすページすら一致せずにどのページでもまんべんなく発生する可能性が存在するようである。

今回はその解決方法について調査して実装までを検討する。

調査

まずはスタックトレースを見てみる、関連部分だけを抽出すると概ね下記のようなメッセージが出ている。

Caused by: org.openqa.selenium.StaleElementReferenceException: Element not found in the cache - perhaps the page has changed since it was looked up
Command duration or timeout: 7.64 seconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: 'unknown', revision: 'unknown', time: 'unknown'
System info: host: 'something host', ip: 'xxx.xxx.xxx.xxx', os.name: 'Linux', os.arch: 'amd64', os.version: 'something os', java.version: '1.8.0_91'
Driver info: org.openqa.selenium.firefox.FirefoxDriver
Capabilities [{applicationCacheEnabled=true, rotatable=false, pageLoadStrategy=unstable, handlesAlerts=true, databaseEnabled=true, version=42.0, platform=LINUX, nativeEvents=false, acceptSslCerts=true, webStorageEnabled=true, locationContextEnabled=true, browserName=firefox, takesScreenshot=true, javascriptEnabled=true, pageLoadingStrategy=unstable, cssSelectorsEnabled=true}]
Session ID: xxxxxxxxxx

ご丁寧にドキュメントのURLがエラーメッセージに含まれているので、公式ページへアクセスしてみる。

読んでみるとこの例外が発生するパターンは大きく分けて2つ存在するようだ。


参照しているエレメントが削除された場合

大概はこちらのケースに該当するようである。
例えばエレメントの参照を取得した時点から、別ページに移動、その後さらに保持していた参照を引き続き利用しようとした場合や、はたまた何らかのjsライブラリによって保持していたエレメントの参照が別の参照に置き換えられてしまった場合などがある。

この場合例えばid属性などは同じものを保持しているのにもかかわらず、内部的には別のDOMインスタンスとして管理されているため、例えば同じ属性を用いてセレクタを作成し、参照を引っ張ってきたとしても以前参照していたものはなくなってしまったためこのような例外を引き起こす。

エレメントがDOMにアタッチされていない場合

例えばtabを表現するテクニックにDivを予め用意しておいて、実際に表示されるDomは一つだけで他のDomは単純に値を保持するためだけに使用されているような場合、場合によっては他のDivはDOMから参照が保持されていない場合があるようである。
こんなケースは非常にまれでしょう。


その他jsによってエレメントタイプが変更された場合などもこの例外を引き起こす可能性があるようです。
が、どれにしても今回のケースに当てはまっていないように思う。

そのためweb上を検索しほかのサイトを調査していたところ、ページが読み込まれる際に間髪入れずにDOMを検索する際にうまくいかないことがあるようである。
またリトライすることでうまくDOMを検索できるケースもあるようであった。

これらを踏まえて対応を考える。

対応

はっきりとした原因はわからなかったが、おそらく今回のケースはページを移動した直後にDOMを検索することが原因だと予想する。

そのためページを移動した後にwait処理を導入する。またそれと同時にリトライ機構を導入する。

実装

下記の用に実装を行った。概念的なものしか示していないがなんとなくイメージは掴めると思います。
もともとの処理としてはループで色々なページを探索していたので、forの概形だけ残してあります。

変更前

    for( someCondition ) {
        driver.get(url);

        // スクレイピング処理
        someScrape();
    }

変更後

    for ( someCondition ) {
        driver.get(url);

        // StaleElementReferenceException を回避するため、ここで明示的にDOMの読み込みを待つ
        _sleepSec(5);

        for(int count=1; count<=RETRY_COUNT; count++) {
            try {

                // スクレイピング処理
                someScrape();

                break;
            } catch (StaleElementReferenceException e) {
                // StaleElementReferenceException が発生した場合は規定回数内であればリトライを行う
                if (count >= RETRY_COUNT) {
                    throw e;
                }
                _sleepSec(5);
            }
        }
    }

対応したところ、今のところ問題なさそうである。

参考ページ

http://www.software-testing-tutorials-automation.com/2015/02/how-to-handle-stale-element-reference.html
http://blog.afnf.net/blog/69


MVCというデザインパターンはもはや十分ではない

概要

今さら私が提示するような話でもないほど同じような話はネット上に腐るほど存在しているだろう。
にも関わらず不思議とシステム開発の現場レベルで見てみると、昔と変わらない手法をずっと続けていたり、また優秀なアーキテクトに恵まれていない現場などはひどく絡みあったコードと格闘していることは多い。
改めて近代的なデザインパターン・アーキテクチャとは何かということについて自分なりに詰めてみようと思う。

初めてに言ってしまうと、ずばりこの記事の着地は ドメイン駆動設計(DDD)への招待である。
同じような疑問を感じている人の何かヒントになればと思う。

人々はオブジェクト指向を手にした

昨今のソフトウェア製作の設計パターンは変化しているのだろうか、ということをまず考えてみよう。
「コンピュータの登場から今まで」のような大きなくくりで見た時には、ソフトウェアの設計パターンは大きく変化していると断言していいだろう。(といってもそんな昔に私は生まれていないが)

時代ごとに変化する大きな要因の一つに、偉大な先人たちが開発してきたコンピュータ言語がある。
昔はアセンブラ・C言語などの、手続き型言語が大部分を占めていた時代があるが、この頃開発においてドメインに着目するというよりかは手続きそれ自体に着目している。
これは言語のパラダイム的にやむを得ない部分が大きいが、いずれにしてもプログラミングされたその内容はそれぞれが意味をあまり持たない。個別には意味がわからない処理の連続が、結果として業務ロジックとして存在している。

その後時代は流れてついに人類はオブジェクト指向言語を手にする。このオブジェクト指向というのはプログラミングパラダイムの革命の一つと言っていいだろう。
オブジェクト指向によってドメインモデルと実装がかなり親和性を持って結合できるようになったのである。
そしてコンピュータ資源も豊富になってきた今、高級言語を使っていればあまりメモリなどの資源を期にすることもない。
実装都合でねじ曲げてきた手続き型への束縛からも開放されつつある。

これが爆発的となり、様々なツールなどが世にでる。sbtなどのマルチプロジェクトを取り替えるような機構ができたりして、その実装性能よりも、実装とドメインモデルをどれだけ近づけて、人間の脳に理解しやすいようにプログラミングを行う時代になっている。

MVCの誕生

Web業界ではそれまで多くのシステムは cgi と呼ばれる仕組みを利用してシステムを実装していた。
これは先程の話で言うとオブジェクト指向の誕生前で、割りかし手続き型の側面が強かった。

これがオブジェクト指向の出現によってMVCというデザインパターンの実装が用意になり、それをサポートする多くのフレームワークが誕生した。
多くの(初級)開発者たちはとりあえずMVCすればそれなりに構造化された実装を手にすることができた。

MVCの功として、まず多くの初級開発者たちにとって開発の目印になってくれたことである。
それまでとくらべてエンジニアたちの一種のヒントのようなトピックを投下して議論の対象となった。

また実際そのデザインパターンはシステムがあまり大きくないうちは非常に効率的に開発できる(ように見える)ためそれほど規模の大きくないシステムにとっては問題がそれほど露呈しない場合も多い。

MVCは銀の弾丸ではない

実際MVCのすべてが否定されるべきものではない、一種のアンチパターンとして利口なUIパターンなどというものがあるがそういった多くの場合はとらないでおくべき選択肢を序盤に排除してくれるような働きをしている。

ただ単純にMVCという一言で対応するにはシステム開発はそれほどシンプルではなかったという話である。

個人的に思うところは MVC の表す Model の責務が大きすぎるのである。
そのため人によって解釈が異なったり、またそもそもそこの設計を怠ったりということで大きなシステムに対応できなくなっているパターンをよく目にするように思う。

具体的によく目にするのが Model 内部にDBレイヤへのアクセス処理などが記述されていて、どんどんモデルが膨らんでいくなどのようなことである。

本来のクラスの責務をしっかり検討する必要があり、MVC を拡張するようなよりドメインとマッチした設計思想が必要なのである。

DDDへの招待

そこで DDD である。昔からあったのかもしれないが、最近特に多く目にするようになってきた。

DDD とはドメイン駆動設計の略称であるが、これはドメイン(業務ロジック)に特に着目して、それをいかに齟齬なく実装に落としこむかということに着目した設計手法である。

実際かなり具体的な設計思想を(先人たちの経験の蓄積として)もっており、これらを盛り込むことでシステムが大きくなってきた際にも拡張性がある実装を担保できる。

たとえば具体的には先程の話で一緒くたに Model と称していたものを Entity や ValueObject など(他にもいろいろあるが)いうようなより詳細なドメインモデルに落としこむ。
これによりMVCで取り扱っていたものを、より適切な責務へと分割することができる、、、などなど

その内容については長くなるのでまたの機会に割愛するが、DDDのバイブルである下記の書籍に目を通すと良いと思う。一度目を通しておいて損はないです。


osxでsshにて公開鍵認証を行っていてkeychainにパスワードを毎回聞かれる際の対応

なんてことはない小ネタですが、備忘録として

対応としては秘密鍵に対応する公開鍵が存在していないことが原因のようです。

keychainの仕様と言ってしまえばそれまでですが、公開鍵を ~/.ssh ディレクトリ内部に保存することで対応できました。

~/.ssh/configを確認したりssh-addを明示的に実行したりといろいろ試しましたがやはり公開鍵がなければ駄目なようです。

公開鍵がないという場合も対向サーバから scp などで持ってきましょう。置くだけで対応できました。


【AD】フリーランスを応援します

何度か本ブログでも取り上げていますが、自分の腕で生き抜いていきたいと考えているエンジニアほど、独立すべきだと思っています。

とはいえ不安、何をどうしたら良いかわからない。など悩みは尽きないと思います。

そんなあなたに、とりあえず相談してみることからオススメします。
今の仕事をすぐ変える必要はなく、リスク無しでフリーランスのライフスタイルを明確にすることができます。

私のオススメは断然下記の「株式会社レバレジーズ」です。
取り扱っている案件数が豊富で、フリーランス界隈では実績も豊富でサポート体制も充実しております。



独立を考えているエンジニアの方は、まずは登録してお話を伺ってみてはいかがでしょうか?


scalikejdbcでmysqlとの通信時のCommunicationsExceptionに対処する

概要

scalaアプリケーションを作成している際に下記のような例外が発生しているケースがありました。

[CommunicationsException: Communications link failure

The last packet successfully received from the server was 15,048 milliseconds ago.  The last packet sent successfully to the server was 10 milliseconds ago.]

今回はこのエラーの原因とその対処についてまとめたいと思います。
ちなみにタイトルにはscalikejdbcと記述してありますが、原因分析についてはmysqlを使用していれば例外なくほぼ同じことが原因であることが多いです。
またアプリケーションについてもscalaといわず、javaを用いていれば同じような対応で解決できます。
(言わずともscalaってjavaのラッパーみたいなものなので)

また私の環境ではscalikejdbcというライブラリを使用したコネクションプールを使っており、検証にそれを用いることとしますし、特にその点については詳しく述べます。

原因

初めに原因だけ端的に記述しておきましょう。
原因としてはアプリケーションが確立したmysqlとの接続、いわゆる「コネクション」が途切れているにもかかわらずアプリケーションではその使えなくなったコネクションを利用してmysqlとの通信をしようとしているからである。

ネットなどで検索するとよくあるのが、一日立ってからアプリケーションが動き出そうとしたタイミングで一回だけ発生する。などの記述が少なくない。

これはアプリケーション(クライアント)が認識しているコネクションの有効性と、mysql(サーバ)が認識しているコネクションの有効性が食い違っているからである。

クライアントでは通常一度作成したコネクションに関してそのまま使い回す。
サーバでは一度作成されたコネクションをいつまでも有効にしていると、新しいクライアントとのコネションを作成する際の邪魔になってしまう。
そのため一定時間アイドル状態(通信が発生していない)であったコネクションをクローズする仕組みが存在する。

そのためサーバ側では閉じたはずのコネクションに対して、クライアントから通信が行われ、結果通信ができずエラーとなるのである。

コネクションプール

さて更にここで掘り下げてコネクションプールを用いている場合にはどうなっているのかということについてより詳しく記述する。
私の環境ではscalikejdbcというライブラリのコネクションプール機能を利用しているが、まあ他のアプリケーションでもコネクションプールを利用している場合はそんなに変わらないでしょう。

コネクションプールとはコネクションを確立する際のコストを減らすため、予め確立しておいたコネクションをすぐに使えるように準備する(プールする、という)仕組みである。

コネクションプールを用いた場合にも先の原因で説明したように全く同じことが言える。
プールされたコネクションは特に読み取り専用のコネクションであれば、一回使用した後もコネクションはクローズされずに何度も使いまわされる。
そして通信がしばらく行われなかった場合、プールされていたコネクションはサーバ側のタイムアウト設定により、使えなくなってしまうのである。

コネクションプールの設定にもよるが、プールされていたコネクションはサーバ側でコネクションが破棄されていたとしてもずっとプールされ続けることがある(というか特に設定しないかぎりは通常だとそうなる)
そのため同様にしばらく(サーバのタイムアウト設定以上)に通信が行われなかった後、再び実行しようとすると同じエラーを引き起こす。

対応

対応としてはいくつかあるが、適切なものとしては通信を行う前にコネクションの検証を行うsqlを発行することで回避できる。
scalikejdbcのような内部でDBCPを利用しているようなコネクションプールであれば、公式にサポートされている機能がある。

設定パラメータに存在するvalidationQueryという項目に適当なsqlを設定する。
例えばselect 1 as oneなど。
そうすることでコネクションを利用する前にその検証クエリを毎回発行するようになり、コネクションが切れていた場合は自動的に再接続を行なってくれるようになる。
コネクションプールを利用していない場合については、自身で実装を工夫する必要がある。

検証

ここまでの話を実装レベルで検証してみよう。

mysqlのタイムアウト値

mysqlの実装におけるコネクションのタイムアウト値はwait_timeoutという項目により設定される。
その項目を確認してみよう

show variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

デフォルトでは28800秒である。これは8時間に値する。
すなわちよくあるケースではアプリケーションが最後にデータベースと通信をおこない、その8時間以上経過してから再度データベースと通信を行うような処理を実行しようとしたところでエラーが発生する、というケースである。

検証する上で流石に8時間も待てないので、この時間を変更する

/etc/my.cnfを編集してタイムアウト時間を更新する
今回は下記のように設定することで10秒に指定した

wait_timeout=10

アプリケーションでエラーが発生することの確認

さて、ここでscalikejdbcを用いたplay scalaアプリケーションにて同エラーが発生することを確認する。
テスト用に作成したアプリケーションを用いることとする。
同じ状況が再現できれば、別にアプリケーションはなんでもよい。

application.conf

コネクションプールを利用するためdefaultという接続子を用いて下記のような設定を行う。

db {
  default.driver=com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/test"
  default.username=user
  default.password=password

  default.poolInitialSize = 10
  default.poolMaxSize = 10
}

アプリケーション

検証のため適当なコントローラに下記のような記述をする。
純粋にループしながらselectを行う。ループ中では15秒のwaitを入れる。

def index = Action {
  scalikejdbc.config.DBs.setupAll()

  var name: Option[String] = None

  (0 to 10).foreach(_ => {
    println("try read")
    name = DB readOnly { implicit session =>
      sql"select name from t order by id desc limit 1".map(rs => rs.string("name")).single().apply()
    }
    println(name)
    Thread.sleep(15000)
  })

  Ok("ok")
}

このアプリケーションを実行すると先のエラーが発生した。
予測はあっているようである。

さらに対応としてvalidationQueryを設定してみる。
scalikejdbcにおけるvalidationQueryの設定は下記のようになる。

db {
  default.driver=com.mysql.jdbc.Driver
  default.url="jdbc:mysql://localhost/test"
  default.username=user
  default.password=password

  default.poolValidationQuery="select 1 as one"
  default.poolInitialSize = 10
  default.poolMaxSize = 10
}

この設定を用いてアプリケーションを実行してみると、今度は無事にエラーが発生しないで実行できる。

またこの際のquery.logを確認してみる

...
2016-05-15T23:36:57.488754Z   71 Query	select 1 as one
2016-05-15T23:36:57.489167Z   71 Query	set session transaction read only
2016-05-15T23:36:57.489512Z   71 Query	SET autocommit=1
2016-05-15T23:36:57.490026Z   71 Query	select name from t order by id desc limit 1
...

目的のsqlの直前にvalidationQueryが存在していることが確認できる。
これによって無事に対応することができた。


mac osx におけるファイルディスクリプタの上限

概要

mac osxでmysql5.7.11を用いてアプリケーション開発を行ったところ、特定の動作をした時点でアプリケーションが動作しなくなる現象に直面した。

mysqlのエラーログを確認してみると下記のようなエラーが出ていることが確認できた。

[Warning] File Descriptor 2032 exceedeed FD_SETSIZE=1024

開発にともなって行った「特定の動作」とはmysqlにおいて、あるテーブルにpartitioningを適用したことである。それも結構な数の。
なるほどmysqlエラーログと合わせてみてみるとその状況は察することができる。
partitioningを切ったことで、物理的に異なるファイルとしてその実態データファイル(InnoDBでいうところのibdファイル)が分離された。
mysql上でそれら実体ファイルをオープンしているfile descriptorがosの上限を上回ったのであろう。

この点について原因の調査と、対応方法をまとめる。

検証

さて、ここでアプリケーション上で起こった問題を再現して、再度その問題が憶測と合っているか検証してみよう。

ここで、sqlを作成する簡単なphpプログラムを用意する。
余談だがやっぱ私の年代となるとスクリプトももっぱらphpだな。

テーブルを作成するsqlを生成するphpコード

下記のコードを実行することで、3000個のパーティショニングを日付で切ったテーブルを作成する
ちなみに、paritioningの最後のところでカンマが一つ多くなるので注意されたし。
そのまま実行する場合は構文エラーになるので手動で削除してください。

<?php
date_default_timezone_set('Asia/Tokyo');

$pivot = strtotime('2010-01-01 00:00:00');
create_partition($pivot);

// create partition ddl
function create_partition($date){
	echo "CREATE TABLE t (id BIGINT auto_increment, name VARCHAR(50), purchased DATE, primary key(id, purchased)) ENGINE=InnoDB DEFAULT charset=utf8 PARTITION BY RANGE COLUMNS(purchased) (";
	for($i=0; $i<3000; $i++) {
		echo "PARTITION p{$i} VALUES LESS THAN ('" . date('Y-m-d', $date) . "')," . PHP_EOL;
		$date = strtotime(date('Y-m-d H:i:s', $date) . " +1 days");
	}
	echo ");";
}

データを挿入するsqlを生成するphpコード

続いて各パーティションにデータを挿入してみる。
下記のコードを実行する。

<?php
date_default_timezone_set('Asia/Tokyo');

$pivot = strtotime('2010-01-01 00:00:00');
create_data($pivot);

// insert data
function create_data($date){
	for($i=0; $i<2999; $i++) {
		echo "INSERT INTO t(name, purchased) VALUES('name{$i}', '" . date('Y-m-d H:i:s', $date) . "');" . PHP_EOL;
		$date = strtotime(date('Y-m-d H:i:s', $date) . " +1 days");
	}
}

そしてその状態で再度mysqlへ接続し、use文を用いてデータベースを選択すると

[Warning] File Descriptor 2032 exceedeed FD_SETSIZE=1024

再現した。

調査

原因は概ね特定できたので、この件について対応方法がないかweb上で検索する。

するとピタリ。こちらのmysqlのバグレポートに全く同じ事象が報告されている。

詳しくは上記のリンクに目を通していただけるとよい。
要約すると osx 固有のバグであり、予想していたように FD_SETSIZE が os の規定値を上回っているためである。
また、するとこの os が持っているファイルディスクリプタの上限値を変更できないのか?
ということを想像するが、残念ながらこれは mac osx で動作している現在の mysql5.7.11 では不可能である。

というのもこちらの github コードを参照してもらうとわかるように osx では FD_SETSIZE 決め打ちで上限を規定している。
これを回避するためには自分でソースコードを改修し、リビルドするしかない。

対応

では、この問題に対応できないのか?というと、そういうわけでもない。
別のアプローチで対処する事が可能である。

問題は mysql がテーブルを参照する際にオープンしている file descriptor の数が多すぎることである。
mysql アプリケーションのパラメータを変更することである程度コントロールすることができる。
具体的には一番大きな影響を与えている、テーブルのキャッシュを数を減らすことである

テーブルエントリのキャッシュ数を変更するには下記の項目を my.cnf に付け加える
数値は上限(osxでは1024)に達しないように調整する。

table_open_cache=400

すると一度オープンしたテーブルの file descriptor を保持しないようになるため、問題を解決できる。
なお、この対応方法はパフォーマンスに大きく影響をあたえる場合もあるので十分注意するべきである。
通常の linux ベースのサーバなどでは file descriptor の上限を上げるなどの対応を合わせて検討するべきである。


nginxでbasic認証をhtaccessライクに設定できるモジュールを作成しました

今回nginxにてbasic認証を取り扱うファイルを公開ディレクトリに設置することで解釈してくれるモジュールを作成しました。
githubにて公開してあります。

このモジュールの振る舞いは端的にいうとapacheでいう.htaccessファイルに等しいです。
(機能的には認証のみに限定しておりますが)

インストール方法や設定ファイルのシンタックスなどについてはgithub上に公開してありますので参考にしてみてください。
設定ファイルなどはnginxの公式モジュールである、ngx_http_auth_basicと同様のものになるので、こちらも合わせて参照すると良いと思います。

nginxってbasic認証などを設定するときにはconfigurationファイルに設置してnginxを再起動するしかないんですね。
それだと結構手間になることも多いし、もしかしたら企業などにしてみたらnginxなどのミドルウェアの再起動を行える人間っていうのが限られてくることも少なく無いと思います。
ファイル設置でbasic認証を認識してくれるのであれば、多くの側面から取り回し軽くてよいかなと思って作成しました。

なおそもそも公式の見解としては、主に速度面からこのような設定ファイルをリクエスト毎にチェックするということを推奨していません。
当然そこら辺のボトルネックについては増加するということで、検証環境や開発環境などで利用すると良いかと思います。


nginx拡張モジュールをアップデートしました

先日作成しましたnginxの拡張モジュールを更新しました。
(前回の更新はこちらになります)

前回の拡張ではunixのshared memory segment上にデータを展開するようにしました。
これによって各プロセス間でユーザからのアクセス情報を共有できるといったものでした。

今回の対応では更にネットワーク上で構成された複数のnginxサーバでもデータを共有することを目的として、ネットワーク上のストレージ(今回の対応では実装としてはmemcachedを対象)に対応するようにしました。

一見ストレージとして memcached に格納できる様になっただけですが、内部的なコードはまた別の側面が改良されました。
当初は特定のストレージ、具体的にはプロセスのheap領域や前回対応したosの共有メモリセグメントにデータを置くという想定でしたが、ngxinのconfigurationファイルでストレージを指定できる方式にし、コードをほぼすべて書きなおしてあります。

これによって今現在指定できるストレージであるshmemとmemcachedに関してはソースコードが完全に分離できる形でインターフェースを切り分けています。
nginxが起動した時点でこのモジュールはconfigurationファイルを読み取り、指定されたストレージに基づいた初期化を行います。

実装としてはこのインタフェースは下記のような関数ポインタを定義した構造体として定義しており、このモジュール内部でのライフサイクルを定義しています。
例えばinit関数ポインタには各種ストレージを初期化する関数ポインタが代入されることを期待しており、update_entryでは各種ストレージ内部での更新処理を行う関数ポインタが代入されることを期待しております。

handler側ではこの構造体を元にライフサイクルを初期化〜破棄に関するまで定めており、適宜それぞれの箇所で関数を呼び出します。
ストレージ側の実装ではこれらのインタフェースを元に各種イベントにおける処理を実装することになります。

/**
 * function pointers. these behave like interface.
 */
typedef struct {
	int (*init)(ngx_cycle_t *cycle, ngx_http_access_filter_conf_t *afcf);
	void* (*get_entry)(char *key, ngx_http_access_filter_conf_t *afcf);
	storage_entry_t* (*get_data)(void *entry_p);
	void (*free_entry)(void *entry_p);
	int (*add_count)(char *key, void *data, ngx_http_access_filter_conf_t *afcf);
	int (*set_banned)(char *key, void *data, ngx_http_access_filter_conf_t *afcf);
	int (*update_entry)(char *key, void *entry_p, ngx_http_access_filter_conf_t *afcf);
	int (*create_entry)(char *key, ngx_http_access_filter_conf_t *afcf);
	int (*fin)(ngx_cycle_t *cycle, ngx_http_access_filter_conf_t *afcf);
} storage_accessor;

つまりこのnginxモジュールの中で更にストレージをモジュールとして扱い実装しており、これによりストレージは任意のものに差し替えられる拡張性を手に入れることになります。
これはすなわち、今後他のストレージを使用したいというような場合において、追加で実装したい時にはこのインタフェースで期待する関数を実装することだけで可能になります。


nginx拡張モジュールを作りました

nginxの拡張モジュールを作成してgithubにて公開しました。

DOSのような攻撃的なアクセスを制御するようなモジュールで、指定時間に指定回数のアクセスを検知すると、任意の時間403を返却するようになります。

正確に言うと以前から公開していたものなのですが、改良を加えております。以前のものはprocess固有の領域にユーザのアクセス情報を格納していたのですが、これだとworker processが増えた際にそれぞれのプロセスで処理したアクセス履歴を共有できません。
むしろ通常はworker processは複数に設定することが多いと思いますので、これはすごく大きな問題です。

なので今回ユーザからのアクセス情報をosのshared memory segmentに格納するように改良を加えました。
データ構造の変更やデータ更新時の排他制御などを付け加えた形になります。

直にメモリを操作するとか、排他制御とか、あとは開発する上でデバッガを用いたりしているので、開発過程でosの知識が身についたりして非常に嬉しい限りです。


phpの配列はどのようにして初期化され実行されるのか

概要

phpなどのLLは、記述するだけでコンパイルなしに実行されますがその中身はどうなっているのでしょうか。
今回は配列を例にとって、実際にphp処理系をおってみます。
主に字句解析、構文解析の実装について順を追って解説していきたいと思います。

zend処理系

まずはphpの処理系について全体像を軽く解説します。
ドキュメントも結構まとまっておりこの辺りなど非常に参考になります。(4年前ですがこの勉強会行きたかったな・・・)

さて、話を戻しますがphpのコアな部分はzend処理系と呼ばれる実装によって成り立っています。
zend frameworkというフレームワークが存在しますが、そちらのフレームワークとしてのzendではなくてphpの初期の発展に寄与したグループのzendです。

zend処理系ではphpのコードから実際に処理を実行するまでに下記のフェーズを実行します。

  1. 字句解析
  2. コードを字句(トークン)に分解します。トークンとは文が構成される最小単位のことです

  3. 構文解析
  4. 分解されたトークンを実際にどういった命令であるかということを解釈します

  5. OPcodeに翻訳
  6. 解釈した公文をOPcodeという中間コードに翻訳します

  7. 実行
  8. 実際にOPcodeを実行します

通常のアプリケーション開発なんかで言うとOPcodeなどは聞いたことがある方も多いのではないでしょうか。
有名どころではOPcacheなどがありますね。

OPcacheはその名の通りOPcodeをキャッシングします。
phpのコードに対して字句解析・構文解析を実行することで中間コードを生成するのですが、同じコードを毎回解析するのはかなり無駄なコストですね。
この中間コードをキャッシングすることで実行性能を上昇させることを目的とした機能です。
余談ですがこちらはphp5.5以降では標準搭載されるようになっていますね。

実際にはその前のフェーズとして字句解析・構文解析が行われます。
これらはごく普通にphpアプリケーションを開発しているだけだとあまり関わることがありません。今回はこちらについて掘り下げていきたいと思います。

字句解析

まず字句解析について。先に簡単に説明したようにコードを字句(トークン)に分解する作業です。

イメージしやすいように下記のサンプルコードを実行してみます。

<?php
$tokens = token_get_all('<?php
$hoge = array();');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

echo PHP_EOL;

$tokens = token_get_all('<?php
$fuga = [];');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

token_get_allはphpコードを引数として受け取り、そのコードをトークンレベルまで分解します。
またtoken_nameは分解されたトークンにzend処理系で管理している enum yytokentype を取得します。詳しくは zend_language_parser.c などを参照しましょう。

さて、サンプルコードの実行結果は下記のようになります。

T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
T_ARRAY
(
)
;


T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
[
]
;

T_OPEN_TAGはphpの開始タグを表すenum定数になります。またT_WHITESPACEはホワイトスペースを表すenum定数になります。
このようにphpのセンテンスをzend処理系で用意されたトークンに細かく分解していきます。

さらにここでarrayはT_ARRAYにトークン解析されていることがわかります。またそのあとのカッコについてはそのままになっていることがわかりますね。
これはそれ自体で意味を持っているメタ文字になるからです。

各種zend処理系での予約後に関してはトークン解析によってenumに置き換えられ、それ以外はメタ文字としてそのまま認識されることになります。

構文解析

続いて解析できたトークンを意味のある文として解釈します。zend処理系ではこの構文解析の実装とbisonと呼ばれるgnuツールを利用しています。bisonはBNFを基本とした亜種で文法を表現します。

bisonについてはこちらでかなり詳しく説明しておられるので参考にしてみると良いでしょう。

bisonでは終端トークン(最小単位のトークン)を組み合わせて文として認識できる構成を定義していきます。
zend処理系における実装はzend_language_parser.yに記述されておりますので確認してみてください。

例えば下記のようなコードが合った時

<?php
$a = 'str';

先に述べたようにこれらはまずトークン解析され、続いてbisonによって記述された文に一致するパターンを検索します。
もし文が適合すれば処理しますし、仮に文が解釈できないようであればパースエラーとして実行されないで終了します。
bison(およびBNF記法)の強力なところはそれぞれの文章を再帰的に表現することや、文の定義に他の文を含むことができるので非常に柔軟に文を定義できるところにあります。

arrayの文を確認する

構文解析まで理解できたところで実際にphpではarrayというセンテンスをどのように解釈して処理系が動作しているのかというところを追いかけてみます。

例えば下記のようなphpコードを対象として取り上げてみてみましょう。

$a = array();

字句解析の項目で解説したようにarrayという文字列はT_ARRAYとしてトークナイズされます。bisonの定義ファイルからこれの定義を探してみます。

%token T_ARRAY           "array (T_ARRAY)"

すると上記のような項目が見つかりました。これはトークンT_ARRAYを定義していることを表します。
続いてT_ARRAYを文として定義している箇所(=構文解析を行っていると思われる箇所)を探してみましょう。

combined_scalar:
		T_ARRAY '(' array_pair_list ')' { $$ = $3; }
	|	'[' array_pair_list ']' { $$ = $2; }
;

見つかりましたcombined_scalarという文が定義されており、トークンとしてT_ARRAYが使用されていることから、これが配列の初期化としての文であることが理解できます。

bisonではパイプで区切ることで複数の文を定義することができます。
よって二つ目の文についてはPHP5.4以降で新たに定義されたブラケット(カギカッコ)による配列の定義が確認できますね。
カギカッコの中は構文により呼び出されるc言語による実装です。$$は戻り値を示しており、$xはx番目にマッチするトークンを表しています。

さてここで先に述べたようにbison(BNF)では文を再帰的に定義できますし、文の定義に他の文の定義を含むことができます。
ここではarray_pair_listという文が定義されていることが確認できるので更に文の定義を追いかけます。

array_pair_list:
		/* empty */ { zend_do_init_array(&$$, NULL, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list possible_comma	{ $$ = $1; }
;

non_empty_array_pair_list:
		non_empty_array_pair_list ',' expr T_DOUBLE_ARROW expr	{ zend_do_add_array_element(&$$, &$5, &$3, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr			{ zend_do_add_array_element(&$$, &$3, NULL, 0 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW expr	{ zend_do_init_array(&$$, &$3, &$1, 0 TSRMLS_CC); }
	|	expr 				{ zend_do_init_array(&$$, &$1, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr T_DOUBLE_ARROW '&' w_variable { zend_do_add_array_element(&$$, &$6, &$3, 1 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' '&' w_variable { zend_do_add_array_element(&$$, &$4, NULL, 1 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW '&' w_variable	{ zend_do_init_array(&$$, &$4, &$1, 1 TSRMLS_CC); }
	|	'&' w_variable 			{ zend_do_init_array(&$$, &$2, NULL, 1 TSRMLS_CC); }
;

possible_comma:
		/* empty */
	|	','
;

array_pair_listの文を確認するとempty(0個のトークンで構成される)またはnot_empty_array_pair_list possible_commaで構成されることがわかります。

possible_commaの文定義を確認するとコンマもしくはなにもなし、と定義されることがわかります。PHPの配列では末尾にコンマをつけてもつけなくても認識してくれますが、それはこの文定義によるものだということが理解できますね。

さらにnot_empty_array_pair_listを確認してみます。定義がそれぞれありますがphpの多様な配列定義を文として定義しています。
例えばphpのセンテンスとして下記のようなものがありますが、それぞれがこの構文として定義されています。

<?php
$array1 = array('key' => value);
$array2 = array(1, 2);
$array3 = array();

最終的にzend_do_init_arrayというcの関数が呼び出されていることがわかります。
今回は構文解析からarrayの処理系の解釈を追うことが目的ですので、zend_do_init_arrayについては次の機会に取り上げたいと思います。

まとめ

いかがでしたでしょうか。
意外とおってみるとそんなに難しくないことがわかります。これもbisonなど先人が作り出してきた便利なツールによるものですね。