メールの通知の新戦略

今まで重要なメールはYMobileのメールアドレスに転送する、という戦略をとっていたのだけれど、 今まで知らなかったのだが、Virtualで(Virtualのみか?)拡張アドレスを含むアドレスのメールを受け取り、外部ホスト(@を含むメールアドレス)に対して転送すると拡張アドレスを含んだまま転送してしまうため、転送先でエラーになる。

そこで、後々の脱YMobileも視野に、Discordで通知するようにした。 もっとも、Discrodはあまり通知で起きてくれないのだけど。

以前、[Discordへの通知方法は記事にしたし]、既にメールから起動するwebhookでの通知というのは結構使っている。 Extract Mailtextは新開発のメールフィルタのコンポーネントで、メールを可読形式にするのはちょっと難しいのだが、ここで作っていた簡単なフィルタが有効に働いてくれた。 テキストフィルタで処理できるように作ったものだけに応用が効いた。

基本的にはこれでいいのだが、注意点が

  • Virtualからはコマンドが起動できない
  • Aliases(local)から起動されたコマンドには$PATHが入っていない
  • Aliasesから起動されるコマンドはnobodyで実行される

Virtualでコマンドが実行できない問題は、Aliasesのほうに専用の(外部には絶対わからないような)名前を作って、Virtualでローカルユーザー(Aliases上のエントリ)にまわして、さらにコマンドに回すのが良い。 拡張アドレスをつけてローカルユーザーに回すようにすれば、Aliasesで直接記述された拡張アドレスを含む場合の専用引数と、含まない場合、もしくは対応していない場合のデフォルトの引数を使うことができて便利。

「ZshからRubyにしたら速くなる」 その理由とテクニック

現在取り組んでいるプロジェクトで、パフォーマンスチューニングの一環として当初Zshで書かれていたスクリプトをRubyで書き直すことによって、60倍程度の高速化を実現した。 もちろん、単純に書き換えただけではなく、可能な限りfork/execをしないようにしたり、コストがかかる処理を最小にするなどの工夫を伴って手に入れた結果だが、「ZshでしていたことをRubyに書き換えた」だけでも相当な効果があった。

このパフォーマンスチューニングは単にプログラムを書くだけの人には生まれにくい発想である。 Unix、そしてLinuxのシステムや、プログラミング言語処理系に関する知識がないと考えられない要素が多いのだ。

そこで、この話を解説する。

「ZshよりRubyが速い」そのわけ

根本的な話として、Zshはそもそも遅い処理系だ。 「Zshが遅い」という話はZshのメーリングリストでもちらほら話をされる。 別にBashと比べて遅いということではないのだが(Bashもまた非常に遅い処理系だからだ)、状況によっては速度が問題になる程度に遅い。

Rubyも相当に遅い処理系であると言われていたし、実際かなり遅かったのは事実だ。 それでもZshに比べれば随分早かったのだが。

だが、それ以降、Rubyは高速化に取り組み続けている。対して、Zshはあまり高速化には取り組んでいない。だから、差が開いている。

しかし、理由がそれだけというわけではない。

Zshは純粋なインタープリタである。対して、Rubyはスクリプト言語ではあるがバイトコードインタプリタ型である。 この違いは、syntax errorが起きるタイミングが、Rubyがスクリプトを実行しようとしたタイミングであるのに対し、Zshはその行に到達したときであることからもわかる。

インタープリタ型であれコンパイラ型であれ、ソースコードを機械語に変換しなければならない、という点は変わらない。 その違いは方法とタイミングである。

インタープリタ型言語の場合、「1行ずつ(1コマンドずつ)変換する」のである。 その変換方法はもちろん処理系によって異なるのだが、Zshの場合、complex commandでも複数の文をまとめて変換することはしないし、ループによって繰り返される場合でも一度変換したものを使いまわしたりはしない。

対してRubyは、最初にコード全体を構文木に変換する。 RUby 1.8までは構文木インタープリタによってこれを実行していたが、Ruby 1.9以降はこれをさらにバイトコードに変換し、バイトコードインタープリタ(VM)によって実行するようになった。 バイトコードはRuby専用の機械語のようなもので、VMによって非常に小さなコストで実行できる。 Ruby 2.6からはJITコンパイラも追加され、部分的にCコードを生成し、これをネイティブコンパイラ(例えばgcc)によってバイナリコードに変換する(こともできる)。

これで1行だけのようなコードだとあまり差は出ないし、Zshでは1行だけどRubyでは何十行という可能性もあるので、このようなケースではRuby有利というわけではなくなる。 だが、ループで何度も同じコードを実行するような場合には非常に大きな差になってくる。 今回の場合、テスト段階で500回のループであったことから、大きな差になったということである。 だからループ回数が増えると倍率的にも速度差はさらに開く。

fork/execとコンパイルにかかる時間

Unix関連に少し知識がある人であれば、「forkはコストが重く遅い」というのを聞いたことがあると思う。

だが、この認識にはちょっと注意が必要だ。 というのも、C言語の速度から見た時に「forkする時間があればどれだけ実行できるか」という点を考えるとsystemで外部コマンドを呼び出すとそこだけ局所的に時間がかかる、という状況が発生する。

だが、実際にはfork(2)しても1000分の数秒にすぎない。 どちらかといえばそれよりもexec(2)のほうが重いのだが、それでもせいぜい100分の1秒程度だ。 だから、C言語で書いている場合ですらそれなりに長くなる場合はむしろ実行コストを省略できてコマンドを呼び出すほうが速かったりする。

昔のUnixではfork(2)はもっともっと遅かった。 現在のLinuxにおいてfork(2)が速くなったのはコピーオンライト形式であることの恩恵が大きい。 古典的なUnixではfork(2)は呼び出した時点でプロセスのメモリをコピーしていた。直後にexec(2)する場合はコピーしたメモリの内容は全く使わないのでかなりの無駄だ。

ところが、現在のLinuxにおいてはfork(2)によってメモリはコピーされない。共有されるのである。 そしてforkされたプロセスが共有されているメモリに対して書き込みを行った時に別に領域を確保してそれを変更する仕組みだ。

結果的にfork自体は一瞬に近くなっている。

そして、もうひとつ重要なのが「コンパイル時間」だ。 Rubyは起動時に対象スクリプトの変換を行う。 だが、この変換コストは速くなるに従って増加している。以前は構文木に変換するだけだったのが、1.9からはさらにバイトコードに変換する時間が必要になったし、2.6でJITを使うとさらにCコードを生成してそれをコンパイルする時間まで必要になっている。 つまり、Rubyはだんだん「実行は速くなっているが、実行に着手するまでは時間がかかるようになっている」のである。

これは、例えばechoであれば

% time /bin/echo > /dev/null
/bin/echo > /dev/null  0.00s user 0.00s system 79% cpu 0.001 total

ということになるのだが、Rubyだと空っぽに近くても

% time ruby -e 'nil'         
ruby -e 'nil'  0.04s user 0.02s system 59% cpu 0.089 total

結構時間がかかる。 つまり、一瞬で実行が終わるRubyスクリプトを何度も何度も繰り返して呼び出すと、トータルではかなり時間がかかるわけだ。 もともとのスクリプトは本体はRubyで、呼び出しがZshだったので、20並列で各500回、Rubyによるコンパイルがかかっていた。だから、かなりの時間がかかっていたのだ。

だが、「Linuxのforkはメモリが共有され、ほとんど一瞬で終わる」という点を利用すると改善の余地がある。 それは、実行可能なRubyスクリプトをライブラリ化する、という方法だ。

ZshからRubyを呼び出す場合、どうしてもRubyを呼び出すたびにRubyによるコンパイルをかけざるをえない。 当初は10000回コンパイルされていたのだが、500回のループをZshではなくRubyで行うようにすれば20回で済むようになる。だが、それでも20回のコンパイルが必要だ。

しかし、呼び出すスクリプト自体をRubyに変えてしまえば、実行しようとするスクリプトをライブラリとしてロードするという方法がとれるようになる。 ライブラリとしてロードすると、そのコンパイルは呼び出し元スクリプトをロードしたときに行われる。 もちろん、呼び出しの目的は呼び出すだけであり、直接そのライブラリの機能を使うわけではない。だが、この状態からforkすると、「コンパイル済みコードがメモリ上にあるRubyプロセス」が出来上がる。

この時点でスクリプトを実行する方法は「メソッドを呼び出す」(あるいは、その機能を果たすオブジェクトを作ってメソッドを呼び出す)だけである。 繰り返し呼び出すループを書くのも、単にRubyのループを書いて、そこで繰り返しメソッドを呼び出すなりオブジェクトを作るなりすれば良い。 呼び出し元スクリプト側では並列分だけforkしたあと、Process.waitallでもしていればいいわけだ。

これはZshに対して、「Rubyスクリプトのコンパイルが1度だけでいい」「execする必要がない」というメリットをもたらしている。 どちらも結構コストの高い処理であるから、繰り返し実行する場合は非常に大きなコストになり速度を低下させる。「処理自体は軽いのだが果てしなくループする」タイプのスクリプトに対してこの方法は本当に効く。 なぜならば、そのようなスクリプトに対してはコストの高い呼び出しをしているとコストのほとんどは呼び出しで占められ、実行コストは小さいためにスクリプト自体を高速化しようとがんばったところでほとんど無意味だし、逆に呼び出しコストを軽くすると劇的に速くなるからだ。

ループ回数の多い処理でのログ出力

概要

現状、1処理が0.0012秒程度で終わるシステムがある。1回の処理は非常に短いが、これが呼ばれる回数は1処理あたり数百万回にも及ぶ。 速度がそれなりに求められる案件だ。

しかし、その中で何度かログファイルに書き出したいタイミングというのがある。それぞれのログファイルに書き出すタイミングは呼び出しの中では1度だけだ。 だが、「処理時間が短くて」「回数が多い」のに共有のログファイルに書くというのはなかなか鬼門だ。 ファイルに書いている時間に待ち合わせがあると、速度に対してものすごくロスが発生する。

まずは普通に

普通に共有されているログファイルをロックする方針でやってみる。

ログファイルにアクセスできるのは1プロセスだけなので、同時にログに書こうとすると待たされることになる。 同時に書くとファイルが壊れてしまうので、すごく妥当な方法なのだが…

% time ruby test-logging1.rb
ruby test-logging1.rb  20.24s user 676.50s system 1102% cpu 1:03.18 total

1分3秒ということでかなり長い。

ソケットで投げてみる

UNIXドメインソケットにして投げつけてみる。 サーバー側が受け取った後の処理はクライアント側の処理時間には影響しないので、サーバーは投げ捨てる。

クライアントは直接ファイルに書くのではなくUNIXドメインソケットを使う。

待ち合わせは全く発生しないわけではない(ソケットを受付てからスレッドに移るまでの間はブロックされる)が、速くなるはずだと思うのだが。

% time ruby test-logging2.rb
ruby test-logging2.rb  25.51s user 33.05s system 62% cpu 1:33.02 total

1分33秒。却って遅くなってしまった。 やはりソケットに接続するコストが高いようだ。

ただ、並列数としては今回の場合最大24で、繰り返しの間というのは別にソケットをつなぎ直す必要はない設計になっている。 そのため、ループの外側に接続を置くことができる。

1行入れ替えただけだが、効果てきめん。

% time ruby test-logging2.rb
ruby test-logging2.rb  2.91s user 2.63s system 1488% cpu 0.373 total

今度は0.3秒で終わった。 Linuxの場合ソケットへの書き込みはカーネルがバッファするため、この程度の量であればメモリへの書き込みだけで終了する。そのため、かなり高速である。

ちなみに、ファイルの場合共有されているファイルをみんなが開いていたらぐちゃぐちゃになってしまうのでこの方法は使えない。 また、親子関係にある別プロセスについてはファイルディスクリプタ自体は共有されているため、fd番号さえわかっていれば共有できるのだが、 その場合は同じ経路から出ていくことになり、ファイルを共有した場合と同じ問題に直面するため、バグになってしまう。

PIDを使う

私の新しいアイディアは、「ログファイルにPIDを含める」という方法だ。

プロセス自体がマルチスレッドになっていなければ、その瞬間には同一PIDのプロセスはそのプロセスしかいないため、そもそもロックする必要がない。 ファイルのロックはあくまで「同時に」書かれるから必要になるのであって、どれだけ速くても順次であれば何も問題はないのだ。

ファイル名にPIDを含めているため、同時に走っている別のプロセスが同じファイルにアクセスすることはない。 もちろん、これは並列化の方法にプロセスを選択している場合だけだが。

% time ruby test-logging3.rb
ruby test-logging3.rb  12.26s user 22.37s system 1778% cpu 1.948 total

なかなか速い。だが、ソケットとはだいぶ差があるようだ。 しかし考えてみればこれもプロセスをオープンしてからであればなにもループのたびに開き直す必要はない。ソケットと同じ話だ。 そこで、ソケットと同じく順序を入れ替えてみる。

ループのたびにファイルを開くのではなく、予めファイルを開いてループで書いていくようにした。

% time ruby test-logging3.rb
ruby test-logging3.rb  2.52s user 0.13s system 1356% cpu 0.196 total

ファイルとソケットのオープンコストの差か、ソケットよりも速くなった。 ちなみに、ソケットを一回だけ開く方法よりも速い。

「forkでの並列処理に追記のログファイルはファイル名にPIDで」、このテクニック、私はどこかで見た記憶はないのだが、かなり有用だと思う。お勧めだ。

素晴らしきRinda

Rindaって

Java生まれのTuppleSpaceのRuby実装である。

Javaの実装はLindaというので、RubyだからRindaということらしい。

Rindaは基本的に分散Ruby(dRuby)のデモンストレーションのようなライブラリのようだが、今(2.5.2)に至るまでずっと標準添付され続けている。

dRubyすらあまり使ったことのある人がいない中、dRubyはPStoreやYAMLStoreなどと同様、 「知っている人は少ないが使ったことのある人はほとんどいない」標準ライブラリになっている。

TuppleSpaceって

TuppleSpaceは共有して置いておけるプールである。

「ホワイトボードシステム」と呼ばれることが多いのでホワイトボードになぞらえるが、このホワイトボードにマグネットシートを貼ったり取ったりできる。 ボードから取ってくるマグネットシートは種類による選別が可能だ。

一般的なソケットと違い、違う種類のやり取りをひとつの経路に簡単に混ぜることができる。

TCPサーバーとして起動できるので、ネットワーク分散も可能だ。

Rindaの概要

Rindaに対して置くことができるのは配列またはハッシュなのだが、「使い勝手はハッシュだが普通は配列」というのが私が感じている空気である。

例えば次のようなオブジェクトを配置する。

このメソッドはTuppleSpaceに接続し、この値を入力する間はブロックするが、受け取り手がいようがいまいが、すぐに終わる。 とにかく一旦TuppleSpaceに置かれるのだ。

そして、別のプロセスが次のようにして受け取る。

nilはワイルドカードになるので、valの値がなんだったかに関係なく受け取られる。 ただし、さらに他のプロセスが

のようにしていたら、この値は受け取らない。[0]に明に指定された値と異なるからだ。

全体リソースの中での曖昧さ

サーバーでは無限にコネクションが維持できるわけではないし、無限にリソースがあるわけでもない。 そのため、処理できるものからどんどん処理していきたい、と考えるのだが、Rindaはこれによって「要請」と「結果」をプールしておくことができる。

例えば、次の例では検索クエリを受け取って、検索を行った結果をTuppleSpaceに置く。 クエリがないときはブロックする。

検索を要請する側がidを発行しておくことで、一意に応答することができる。 この処理を行うプロセスの数によって「並列で検索できるワーカーの数」を制御することができる。 余っているワーカーがあったとしてもそれはRinda::TuppleSpaceProxy#takeによってブロックされるだけであり、「同時最大並列数」だけプロセスを起動しておけば良い。

さらにこれを取っていくワーカーは別のホストでも構わないのだ。

TuppleSpaceによって簡単に分散処理、並列処理が可能なのだが、 これは「入れた順番には出てこない」。処理が大量にたまってしまった場合でも順番を守って処理してほしい場合には適さない。

だが、「手が空いているならば仕事を持ってきて処理する」という形式はなんとも人間的で、かつコンピュータ的にも結構優れたモデルだと思うのだ。 Rindaが有効に機能するように設計することにより、並列処理に伴う難しさをかなり軽減することができる。 そもそも並列処理は厳密さを求めるにはあまり向いていないので、このようなふわっとしたレイヤーをはさむと結構いい感じに動作する。

in action

今回は「ワーカーが処理するデータはそのワーカー単独が処理するディレクトリに分割し、ワーカーは処理が終了したらRinda経由でディレクトリパスを取得する」という方法をとった。

以前はHTMLチャットサーバーの実装で、要求に対して即座にリターンさせるためにRindaを使っていたこともある。

あまり知られておらず、使われる機会もないが、使ってみると結構面白いのではなかろうか。

Zshでの連続的呼び出しに環境変数を使ってみる (Zshのガベージコレクション)

Zshは極めて強力なシェルだが、大規模プログラミングにおいて使われることは少ない。 だが、Orbital designにおいてはシェルスクリプトは強力な武器だ。できれば積極的に活用していきたい。 そうなると、Zshにおいて明らかでない、あるいは公知ではないことと付き合っていかなければならない。

単純に言えば今回は

のような処理を行うスクリプトを、invoke_workerがファイルを読み込むのではなく、事前に読み込んでおくことでワーカーの処理時間を短縮したいということだ。

のようにしたいということだ。

だが、問題は「これがメモリを食いつぶさないか」ということである。

意図と効果

普通のスクリプトの場合、この方法はほとんど効果をもたらさない。もしかしたらマイナスになるかもしれない。

そもそもこれらの情報が「単純にファイルとして読み込むだけ」の設計になっているところも工夫のなせる技なのだが、必ずしもそのようにできるわけではない。 特にクラスタ仕様になっているとファイルアクセスが遅いかもしれないし、その違いを吸収するためにさらに別の方法をとるかもしれないのだ。

ワーカーは起動数も多く、当然ながら処理としては重い。 逐次実行されるワーカー部分であまりその処理を担わせたくない。 特に設定のほうは起動される全てのワーカーにとって共通である。 これを環境変数にするメリットは大きい。

Linuxのfork(2)はコピーオンライトだ。 configは文字列であり、起動されるワーカーではその文字列をパースすることになるから、この$config環境変数が変更されることはない。 つまり、この環境変数は実体はひとつであり、常に同一の実体が参照される。コピーされることもないので速度が上がる。ディスクIOとメモリへのコピーが2回目以降省略できるのだ。

ただし、環境変数で渡す文字列は「システムコールがCライブラリである」ためにC文字列(の配列)であり、\000が含まれていると文字列がそこで終わってしまう。 私は勘違いと慎重さを欠いたことから赤っ恥をやらかしてしまった。

データのほうは、ほぼ単純に、ワーカー実行中に時間がかかる(かもしれない)データの取得を裏でやっておきたい、ということである。 これも環境変数にしてしまえばワーカーはリード時間をとられない。所要時間の総量は変わらないが、裏でやることで逐次実行部分に含めることを避けるということである。

問題と検証

だが、これは

というのが100万回実行されるということだ。 しかも、これはループのたびに異なる値になる。

もしZshにガベージコレクタがなければ100万件のデータを抱えてしまうことになり、これはメモリ消費量的にまずい。

というわけで、「Zshは変数を上書きしたらメモリを開放してくれる(ガベージコレクションしてくれる)のか?」という疑問を解き明かす必要ができた。

この調査は割と簡単で

とし、

のようにしてメモリ消費量を監視すれば良い。

ちなみに、100Mを指定しているが、/dev/urandomは一度に読めるのは32MiBまでである。

80340
215640
80332
80332
80348
43344
80340
80340
80332
80332
215636
80340
80332

だいたい2倍程度と見ていいと思うのだが、ときどき3.5倍程度まで増加する。 ただ、だいたい2倍ということはかなり迅速にメモリを回収してくれている印象だ。

しかし、実際のワーカーはこんなに長時間実行はしないので、実際のワーカーの実行時間に近い感じにしてみよう。

108932
80340
195300
80340
215628
80340
211472
111616
80332
178644
43344
215644

回収しきれないタイミングが増えてメモリ使用量にばらつきは生じているが、回収ペースを上回ってメモリ消費量が果てしなく増えていくようなことはないようだ。

結論

  • Zshにはガベージコレクタがある
  • Zshでパラメータを上書きすると、ローカル変数か環境変数かによらず迅速にメモリは解放される
  • IOコストが高いなどで逐次的に実行するコストを削減したい場合、コントローラで環境変数にする作戦はなかなか有効

追加の注意点

フルスケールするように設計されているワーカーの場合、CPUコア数を使い切るだけワーカーを起動していると、結果的に裏で色々するためのコンテキストスイッチコストが高くついて全体では遅くなる可能性もある。 というか、ワーカーの実行がそれほど時間のかからないものであるならばwaitの時間は短くなり、waitの時間が短くなるほどスケール阻害要素としては大きくなる。

ウェブサイト全文検索システムの開発

開発経緯

NamazuやGroongaも試したのだが、いまひとつ望むものにはならなかったので、シンプルで美しい全文検索システムを書くことにした。

これは、第一にはGoogle検索に依存しているMimir Yokhamaの検索機能を自前で持つこと、 第二にはChienomiを含むWordPressのシステムの置換えである。

検索システムの開発自体はそれほど難しくないと思うのだが、どのように動作するのが望ましいかということを考えると非常に難しい。 Googleの検索システムは非常に高度なので、それに匹敵するものを作るのは難しいのだ。

だが、ここはPureBuilder Simplyにふさわしいシンプルなものを目指すことにする。

設計その1

とりあえず、grepを使えば話が早いのだが、HTMLだと余計な要素を含んだ検索になってしまう。 HTMLからタグを除去するのは難しくないが、どういうポリシーで除去するのか、いつどうやって除去するのか、などが難しい。

PureBuilder Simplyの構成から言えば、生成時に、Pandocで生成するのが望ましい。

$ pandoc index.md -t plain index.txt

だが、生成時に生成を全く無視してインデックスを生成するのはどうだろうか? そもそもPre pluginsはソースファイルを、Post Pluginsは生成したHTMLファイルを加工するものであるため、本来の目的から逸脱してしまう。 例えばPre Pluginsを使って

とかもできる。

ただ、今のところ検索対象になるようなソースファイルを加工するようなPre plguinsを使っていないため、別にこのようにする必要性はない。

設計その2

あとから処理するためのもの。 .indexes.rbm に基づいて処理を行う方式。

検索

いずれにせよここまでやってしまえば検索は簡単。 grepで検索できる状態なので、シンプルに検索可能。

AND検索の要領としては

ものすごく検索対象が多い場合は、検索対象そのものを絞り込んでいくほうがいいだろう。 だが、プロセス起動回数が増えることを考えると、そのような場合は自前実装のほうが良い可能性が高い。

OR検索はもっと簡単で

スペースの取り扱い方とか、case問題とか考え始めると難しい。 ただ、世の中そんな複雑な検索をしている検索エンジンはあまりないし、多分ローカルにそんなもの作ったところで報われないのでこれくらいでいいような気もする。

ANDまたはORではなく自由にANDとORを結合できるようにした場合は、expr自体に評価できるメソッドを追加すると良い。例えば

といった感じである。

あとがき

検索機能の実装自体は難しくないのだが、ChienomiをPureBuilder Simply化するという話になると結構難しい。

既にかなりの記事があり、検索からの流入も多いため、どうしても全記事に対してマップせざるをえない。 これもなかなか面倒だ。

だが、もっと問題Chienomiの記事は書き方が一定でない、ということだ。

Chienomiの記述形式はPOD, RDoc, ACCS2, PureDoc, PureDoc2, PureDoc2::Markdown, Pandoc Markdownがある。 PureBuilder SimplyはPandocでの処理を前提としているため、なんとかしないといけない。

過去記事については諦める方針ならばHTMLとして抜き出すという方法もあるのだが(Mimir Yokohamaでウェブサイトのサルベージでよく使う方法だ)、 できれば避けたいというのもある。

また、タグとカテゴリのつけ方が一定ではないため、これを処理しなければならない。

さらに厄介なのがメディアファイルだ。 WordPressはメディアファイルの使い方が独特だし、そのためにメディアファイルについてはWordPress上で追加する方法をとっていた。 さらにサイト移行時にメディアファイルを紛失したこともあって、結構大規模な作業になると思う。

そのことを考えると一筋縄ではいかない。

それはともかくとして、サイトの検索で非常に複雑な演算子を使いたがる人はまずいない、 どころかサイト内検索なんてほぼ使われていないに等しいので、基本的な検索機能で十分だと思うのだが、それであればこの通り実装はとても簡単だ(例によって設計で稼いだ感があるが)。

というわけでちょっとした実装例、そしてシェルスクリプトサンプルとして役立てば幸いである。

Mimir Yokohamaで続く改修、力を注ぐ

2018年8月5日、Mimir Yokohamaのソースリポジトリにはじめてのタグ、5.0が打たれた。

なぜはじめてのタグなのに5.0だったのか。

これは、Aki SI&Eのプロトタイプを1.0, Aki SIEのウェブサイトを2.0, Mimri YokohamaのWordPressを3.0, 現在のウェブサイトになったときを4.0とカウントしたものだ。

つまり、はじまって以来の、同一システムのままのバージョンアップとなった。

しかもこのタイミングである。 先日、大幅な編成変更と、いいね機能、コメント機能の追加を行った。 これで新しい船出だというタグではない。かといって同バージョン最終仕様としてのタグでもない。

これから次々とアップデートが予定されている。 その中での一区切りだった。

13年積み上げてきたものとは

PureBuiler Simplyの原型となっているのは2005年のACCS1である。

もうブログが流行り始めていた頃だったが、既に「事前生成戦略」に関するイメージはあった。 ブログを避けた理由は、一連の流れを持つ記事群を拾いにくくなること、そして時間とともに消えてしまうことだった。

「普遍的な内容を、きちんと分類して読みやすいように提供したい」と考えたわけだ。

様々な機能、様々なアイディアがあった。 実装されたものもあるし、実装されなかったものもある。 言語もPerl, PHP, Zsh, Rubyと変わってきた。 このような変遷をたどっているのはEQAIとこれだけで、まさに私のライフワークであり、また成長の軌跡でもあった。

この中でこだわってきたものもある。

例えば、デザイン性を保ったまま軽量・高速なウェブサイトを構築すること、可能な限り高いアクセシビリティを提供すること(環境や回線の違い、身体的ハンディキャップなどで差別しないこと)などもそうだ。 意味あるコンテンツを、読みやすい形で提供する、というのもある。

ACCSが追い求めてきた機能は次のようなものだった。

  • カテゴリで分類されて探しやすいインデックス
  • 検索機能
  • 意思表示機能 (コメント、と考えていることが多かった)
  • 連続した記事のページめくり
  • 要約の先読み
  • インライン用語集
  • prev, next, glossary, index, description情報を持たせる

PureBuilder Simplyは当初、Mimir YokohamaのWordPressページで提供されている全機能を提供する、ということを目標としていた。 これについては既に達成されている。そのために、従来のPureBuilderにはなかったタグ機能なども追加された。

「いいね機能」「コメント機能」はWordPressに完全な意味で標準であるものではないのだが、事実上付属するようなもの(特にコメント機能は)なので、これを追加したことで、事実上「WordPressを置き換える」というミッションは完遂した。

だが、同時に既にWordPressにはない機能の搭載もはじまっていた。 用語集機能はWordPressではなく、PureBuilderの伝統に由来する。そう、ACCSが目指していたものを達成する、という次のミッションに向かいはじめたのだ。

そこでつけられたのが5.0タグだった。

用語集機能

用語集機能という発想のスタート地点は、私が使っていたWindows 98SEマシンに搭載されていたインライン翻訳ソフトだった。

カーソルを載せるだけでその単語を翻訳してくれる、というソフトウェアは今持ってそれ以上のユーザービリティを提供するものはない。

「カーソルを載せたらわからない言葉を教えてくれる」というのは最高に便利だと思ったのだ。 現在はニコニコやはてなが似たような機能を提供しているが、あれはページが変遷してしまうため私からすれば理想的ではない。 どらちかといえばWikiに搭載されているインライン展開のほうがずっと理想的だ。

この機能は

  • 表示後にJavaScriptでtreatする
  • 生成時にHTMLを置き換える形で組み込む
  • ソースドキュメントを改変してから生成する

という3つのパターンを行ったり来たりしている。

PureBuilder SimplyではPost Plugins/Pre Pluginsの構造からこの3つとも選択肢として取ることができる。 Mimir YokohamaではPost PluginによるHTML置換え方式を取っている。

このあたりは試行錯誤の成果といえるだろう。 用語集ページも含めてYAMLの辞書ファイルから生成しており、文書に対して特別な処理は必要ない。

次の記事、前の記事

ACCSで最も苦戦したのがこの機能だ。

この解決については何度か言及しているが、今は前後関係をメタ情報として書く、という仕様になっている。

解決方法としては後退しているように見えるが、前後関係の自動解決はファイル名なり、もしくはなんらかのヒントなりを厳密に管理する必要があり、結構バグりやすいということを経験したのだ。

複雑な置換えやユーザーの管理によってバグを発生しうるようなものであるならばメタ情報をユーザー自身が書くべきだ、という割り切りは、これまでの歴史の中で手にしたバランス感覚である。

実のところ今の構成では前後を自動化することは難しくない。 だが、ユーザーがそれを守ってくれることを期待すべきではないだろう。

Post pluginsはページ生成が終わってから実行されるが、これは「他の記事の情報を取得できるようにする必要がある」からであり、 環境変数$pbsimply_indexesという形でデータベースのパスが渡っていることから

のようにして取得できるし、 連番に限るのであればもっと簡単に

とできる。 ちなみに、ディレクトリを認識させる方法は最新のコミットで環境変数$pbsimply_subdirに含まれるようになった。

このように「できるけれど、あえて手書き」だ。 これは、問題を簡単にするためと、この処理をPandocテンプレートを通じて行いたいためだ。 Pre Pluginsを使えばできるが、かなり複雑なことをすることになるため避けている。

意思表示機能

過去には「コメントを直接にHTMLファイル化し、objectで読ませる」ということをしていたこともある。

表示するかどうかを別として、コメント機能はそのときとあまり変わっていない。 表示させるために必要な部分を削ってシンプルになったくらいだ。

このような機能は本質的な部分は極めて簡単に書けることはこれまでの経験によって証明されている。 どの程度正当性を検証する必要があるかという点がwebアプリケーションの分量になる。

要約の先読み

まずは要約を入れる

いよいよ今回のハイライトだ。

descriptionへの対応自体は最初のリリース時点で

という記述があり、対応はちゃんとしていた。 だが、「書くのが面倒」「書いてもあまり意味がない」ということで放置していた。

「descriptionの先読み」は今まで実装計画には入っていたが、実装されたことはなかった。 そもそもdescriptionってSEOのために入れられているくらいで、「descriptionを読ませる」という発想はあまりない。

Firefoxだとこんなふうにブックマークのプロパティを表示するか、ブラウジングライブラリー上で詳細表示にするとdescriptionが表示されたりするのだが、これを見たことがある人という人は地球上に5桁いないのではないだろうか。

Firefoxのブックマークの詳細

だがdescriptionは入れたいと思っていたし、それを活用したいと思っていた。

そもそもの発端はトップページのレイアウト更新で、最新の更新記事と要約を(ニュースとは別に)表示したい、ということだった。 「どうせ記事の要約書くんだったらdescriptionに入れようよ」ということだ。

実はAtomフィードもスタンバイしている。

要約を見える形に

だが、これだけではおもしろくない。どうせ要約を表示するのならばぜひともユーザーに見える形にしたい。

私の文章は基本的に長いので(体系的でない短い文章に価値を感じていない)、読むのがしんどい人もいるだろう。 読むかどうか決めるために要約は重要だ。

要約を見たいタイミングとはいつだろう? やはり記事を読み始める前だろう。ならば本文前に

とかやってやればいいし、そのほうが効果的なのかもしれないが、既に「文書情報」という項目があることを考えるとちょっといただけない。

そこで文書情報に追加した上で「記事タイトルをクリックすると文書情報にジャンプする」という仕様にした。

これはヘルプページにも書いてあるけれども、誰も気づかなそうだ…

もうひとつ、利用者は多くなさそうだが、カテゴリインデックスがある。 ACCSとしてはこれが中心であり、ぜひとも使って欲しい機能だ。 世の中、情報を整頓するということに怠けすぎて、検索が全てになってしまっているので、使われていないような気もするけれど…

しかし私の意図としてはこのようなインデックスを活用してほしいというのがあるし、やはりタイトルだけではわかりにくい。 かといって変遷するとだるいので、変遷せずにインデックス上で要約を確認できると便利だ。

これはタイトルで関心をそそられた後の二次的な情報であり、通常は一覧性が高いほうがいい。 というわけで、ツールチップにしてみた。

PureBuilder Simply ACCS上でDescriptionを扱う

単純には記事タイトルにtitleで入れてあるため、ロールオーバーツールチップとして表示される。 だが、スマホだとこれが効かないので、補助的に“📖?”と表示して、これをタップすればツールチップが表示されるようにしてみた。 全く標準的でないインターフェイスなので、あまり気づいてもらえないような気もするけれど…

PBS ACCS用ツールチップ実装

何度見てもJavaScriptの複数代入が慣れない。 慣れればみやすそうだけども。

世の中的には割と珍しいDOM操作をしているが、これはelementに対してイベントリスナを設定するためで、innerHTMLだと二度手間になる。 基本的にやっていることは「記事部分 > リスト全体 > リンク」と絞り込んでいって要素を作成して追加する、という手順だ。 末っ子要素を追加するとき(今の要素の親要素の最後の子要素として追加する)はelement.parentNode.appendChildという手順は覚えておいてもいいかもしれない。

glosarryと共通のコードがライブラリとして読まれるようになっている。ライブラリはdeferだがasyncではない。

900pxを堺にしているのは、「サイドカラムがあるのであれば表示領域は少なくとも右側にサイドカラム幅はあるが、シングルカラムになるとそうではない」からだ。(このページのシングルカラム境界は800pxである)

機能チェックしているが、document.addEventListenerできないブラウザでJavaScriptに対応しているものはあまり残っていないだろうし、あっとしても単純にイベントリスナー設定時にエラーになるので放置してもいいかもしれない。 ただし、間違って複数回ライブラリが読まれたときのためにArt.tooltipはしておかないとイベントリスナが複数設定されてしまう。

PureBuilder Simplyはうまくいっている

PureBuilder Simplyがここまでうまくいっている理由としては、やはりPandocの強力さがなによりだろう。

PureBuilder SimplyはPandocが持っている機能をちょっと拡張する…という考え方をしている。 今までドキュメントジェネレーター自体を制作していた(PureDoc)ことと比べると問題はかなり簡単になっている。

Pandocの動作は必ずしも簡潔ではなく、自分で実装するのであればドキュメントジェネレーターにここまでの機能をもたせることはないだろう。 だが、Pandocがある以上はPandocを使いたい。

もしPandocがなければdocutilsを拡張することを考えただろうが、その場合はPureBuilder Simplyは今のように良いツールにはなっていなかっただろう。

PureBuilder Simplyが今ほど素晴らしいツールになったのは、Pandocがあったからこそだ。

Mimir Yokohamaに対して行われている様々な拡張は今の所PureBuilder Simplyに対して適用されていない。 これは、PureBuilder Simplyが生成するものに対する機能ではなく、Mimir Yokohama固有の、そしてテンプレートとCSSによるものだからだ。

だが、いくらかでも一般化してPureBuilder Simplyに還元していければと思っている。 PureBuilder Simplyのエコシステムの充実は普及には不可欠だろうから。

Mimir Yokohamaに「いいね機能」「コメント機能」を追加

概要

Weekly 10000PVを達成して機能強化に力の入っているMimir Yokohamaのウェブサイト。

連続の機能強化でついに「いいね機能」と「コメント機能」が追加された。

実は先日の「お問い合わせフォームの実装」は単にその機能を実装する最小限ではなく、簡単なアプリケーションを実装できるプラットフォームになっており、 アプリケーションを追加する条件が整っていたのだ。 また、そのためのテストもしてあった。

そのため、実は今回コード追加はわずかで、両方合わせても23行ほどにとどまる。 ごく簡単だが、確証が持てないためにテストと本番環境のための修正を行ったりして結構な時間がかかった。

これに関してはみるべきところはあまりない。 受け取ったパラメータをファイルに書き込めば良いだけだからだ。

ちなみに、連打しやすいアプリケーションを入れるために連打の対策もサーバーにしてあった。 実は先のパフォーマンスチューニングはこの対策によってパフォーマンスが低下してしまったため、これをカバーするついでに行ったものだった。

いいね機能の設計

いいね機能はごくシンプルだ。

Pandocテンプレートを使ってページタイトルを埋め込むことができるので、これだけ使うのであれば単にテンプレートの中にフォームを書けばいいだけ、ということになる。

実際はリファラ(Rack::Request#referer)及びユーザーエージェント(Rack::Request#user_agent), IPアドレス(Rack::Request#ip)も特定に使用している。

ポイントは「一度送信したら表示を変更、送信を無効化する」ということだ。

inputタグのdisabled属性を使うことで送信ボタンを無効化している。

そう、この機能はHTML上でインラインで書かれているのだ。 このような書き方はW3C的には推奨されないのだが、Googleは推奨している。 実際、これだけのためにJavaScriptファイルをロードさせることをしたくなかったので、インラインにした。

ポイントは

  • Legacy DOMにおいてフォーム部品は連想配列のようにアクセスできるようになっている
  • submitボタンのラベルはvalueである
  • disabledによってフォーム部品を無効化できる

である。

これらの処理は送信の「前に」行われる。 これは正しいことではないが、問題はない。 なぜならば

  • 送信できなかったからといってユーザーが修正するなどの手を入れる余地はない
  • 特に返信を必要とするものでもないので、送信失敗はクリティカルな問題でもない
  • サーバーエラーなどは表示されるようになっている

からだ。

また、画面変遷せずに送るだけ…というと、Ajaxで非同期に送るしかないように考えるかもしれないが、 実際は現代のブラウザは基本的に2XXステータスで空コンテンツを返すとページ変遷しないようになっている。

このような用途のために204(No Content)が用意されているため、204を返す仕様だ。 できるだけブラウザの標準機能に頼るようにしている。

このボタン、めっちゃgooglebotが押してくる…

コメント機能の設計

公開されるものではなく、sanitizeしなくても問題が発生しない設計になっている(単に文字列として扱う以上の取り扱いがなされる条件で使用しない)ため、非常に楽だ。

難しく考えるよりも、シンプルで挙動をちゃんと把握できていて、余計なことをしない方法をとるのが最も楽に、確実に、バグなく設計できる。

ただ、この機能に関しては「コメントフォームの表示」などが必要になり、JavaScriptが必要になった。 また、コメントするというあまりとらない行為のための部品であり、常にロードすることは非常に好ましくない。

そこでとった方法は「JavaScriptの遅延ロード」であり、「クリック時にelementを追加してJavaScriptを読み込む」という方法だ。

これでscript要素を追加している。

では呼び出されたあとどうしているのか…というと、こんな感じだ。

そう、コメントフォームは標準でHTMLに含まれていない。

パフォーマンス的にみても親切機能のためにもう重くなりつつあるドキュメントをこれ以上重くしたくないので、 「フォーム部分はJavaScriptがロードされたときにinnerHTMLで書く、という方法をとっている。

「クリックされたときにはじめて必要とされるのでドキュメントノードを追加してロードする」という手法は稀に使われるが、さすがにそれでHTMLドキュメントそのものを生成するというのはまず見ない手法になっている。

連打されたときのことを考えて、clickイベントを発生する値に特殊なプロパティを埋め込み、何度も実行されないようにしている。 なお、これは本当に連打したときだけ機能するもので、JavaScriptシングルスレッドなので一回このスクリプトに入っていまえばこのスクリプト中に割り込まれることはない。 だが、連打されるとイベントがキューに入ってしまうので何度も実行されてしまうことから、そうしたことがないようにしている。 シングルスレッドなので、このスクリプトが実行されているのにプロパティが設定されないうちにまた実行されるということはない。

元になるフォーム自体は存在していて、submitボタンを配置すればフォームの送信は可能だ。 そのため、submitイベントに対するイベントリスナーを設定するものはHTML上に静的に存在している。

その上で「フォームはやっぱり閉じられたほうがいいな」ということでボタンクリックに対するイベントの変更、及びボタンラベルの変更を行っている。

送信を行った場合はもうコメントフォームは使わないので、コメントフォームは非表示にして(削除はしていない)ラベルを変更している。

ラベル変更だが、input部品とは違ってbutton要素は子要素テキストノードがラベルになっているので、dataプロパティの書き換えによって変更している。

もともとHTML上でLevel0 DOMイベントを使っているので、スクリプト上でもLevel0 DOMによってイベントを変更している。 いつもaddEventListenerを使っていたので、珍しい。 イベントの削除はイベントにnullを代入するだけだ。

なお、HTMLで全て含めてしまえばJavaScriptは排除できるのだが、これ以上余計な要素を組み込むことはできれば避けたいためこのような仕様になっている。既にDOMコンパイルは割と重い。

また、デザインポリシーからいって、多くの場合余計なコメントフォームを常に表示させておくというのは美しくないとも思っている。 JavaScriptを使わずCSSでオンオフするようにもできるが、そうすると今度はボタンのテキストを変更するのが難しいし、連打や再送信を防ぐのも難しい。

このことから、なにがなんでもJavaScriptを使わないのが正義、ということでなければ、 このような付加コンテンツはユーザービリティの点からも素直に使うべきだと思う。

こちらもいいね機能同様、送信する前に状態を変更してしまい、成功すれば204を返す仕様。

Legacy DOM と DOM Level0 Eventに関して

Legacy DOMやDOM Level0 Eventについて意見をもとめられることがたまにあるのだが、私としてはあまり勧められないと考えている。

非常に簡単なので学習にはいいが、Legacy DOMはHTMLの構造に依存する。例えば

として、

とかやってしまうと、ドキュメントの構造が変わるたびに修正だ。

だが、「フォームにのみnameで使い」「フォームはW3C DOMで特定する」のであれば悪くない。

DOM Level0 Eventはイベントリスナーが1つしか登録できないためモジュール設計になっている場合や、なんらかのライブラリを使っている場合は使ってはいけない。 イベントを「追加」する場合はLevel2で、イベントを「設定」する場合はLevel0という使い分けも考えられる。 HTMLに直接書く場合はLevel0しか書けないので、その部品の基本的な動作と定義されているならLevel0でもいい。

ただし、その場合でもできればLevel2 Eventを「後から追加する」のが適切なので、DOM Level0 Eventの使いどころは今回のようなものが唯一だと思う。

メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)

あらまし

Mimir Yokohamaでついにお問い合わせ方法として「メッセージフォーム」が追加された。

なにがついになのか、なにをドヤっているのかと思うかもしれない。 まぁ、ドヤってはいないのだが。

実は私はかなり長い間ウェブアプリケーションをほとんど作っていない。 そして、今まで私が作ったウェブアプリケーションは、専用サーバーを持つサーブレットタイプか、もしくはCGIだった。

馬鹿にされがちなCGIだが、利便性は高く、頻繁にアクセスする性質を持たないアプリケーションには適している。

そして、そもそもウェブアプリケーションを作っていなかったのは、私が「事前生成戦略」の研究と実験に注力していたからで、 どちらかといえばウェブアプリケーションからは離れる方向にあった。 そして、ウェブアプリケーションを必要とするとしても大部分は静的ページとして提供できる方式を目指していたため、CGIで十分事足りたのである。

ちなみに、これまでウェブサーバーは

  • Apache
  • lighttpd
  • delegate
  • Nginx

という経過をたどっている。 Apacheは言うに及ばずlighttpdとdelegateはApacheよりもCGIが簡単だったので、「ほぼCGI」だった。

だが、時代は変わった。NginxはCGIをそもそもサポートしない。 私も新しい時代に対応する必要がある。

ちなみに、この作業は次の仕事のための実戦テストという意味合いもあった。

方針を考える

最も話が速いのはFastCGI Wrapである。

NginxはFastCGIをサポートしている。 FastCGIはプログラムをデーモンのように起動しっぱなしにする。

だが、通しで実行するプログラムとデーモンではそもそもの前提が違う。 そのためCGIプログラムをFastCGIとして動かすのはそれなりにハードルが高い。

そこでFastCGI Wrapの登場である。 FastCGIとして利用されるプログラムをFastCGI Wrapにする方式だ。 このラッパープログラムは要求に合わせて都度CGIプログラムをCGIインターフェイス経由で起動する。 結果的にFastCGIの意図は無視して従来型CGIを動作させるようにするというものだ。

この方法は結構出てくるのだが、基本的には既存のCGIプログラムを動作させる話である。

個人的な感覚としては、無駄なプロキシを噛ませるような方法を使ってまでCGIに固執したくない…というか、実はfcgi-wrapってそれなりにめんどくさい。

だったらFastCGI直というのもありかなぁ、と考えるわけだ。

ところが、やっぱりFastCGIはデーモン状のプログラムを想定しているわけで、やはり前提が違う。 要求として割と複雑なのか、デーモン化に関してはspawn-fcgiに担ってもらって、さらにRackを使う、というのがどうやら主流らしい。

だいぶ話が複雑になってきた。

サーバーはNginxである。NginxはFastCGIインターフェイスを経由してFastCGIプログラムにパラメータを渡し、応答を受け取る。

FastCGIプログラムはデーモンである。 Rubyでは次のようにしてFastCGIプログラムを書くことができる。

あるいは、CGIライブラリ互換インターフェイスを使うことで、#each_cgiの中身はまるっきりCGIと同じにすることもできる。

spawn-fcgiはこのデーモン部分を担う。 つまりeachしてる部分を担ってくれるわけだ。

プロセスとしてCGIインターフェイスで起動するわけではないので、fcgiwrapほどの互換性はない。 感覚はCGIに近いが、インターフェイスは意識する必要がある。

Rackはミドルウェアと呼ばれている。これはまずFastCGI抜きで話そう。

Rackはインターフェイスを担っている。 今までプログラムはCGIなり、あるいはFCGIなり、さらには各種フレームワークやサーブレットの様式(例えばSinatraとか)で書いていた。

Rackはこれらの違いを吸収するモジュール設計のものだ。 Rackに準拠したプログラムを書いておけば、たとえ愛用のフレームワークがディスコンになっても、サーバーが変わっても安心、というわけだ。

だが、Rack自身はサーバーではないからサーバーがいるのだが、Rack組み込みのサーバーというのはもう完全にRuby世界の住人だ。 だってRackはRubyのWebアプリケーションインターフェイスだから。

Passengerというソフトウェアがあって、これはwebサーバーのモジュールとしてRackに対応する。 Apacheでは比較的簡単だけれど、Nginxだと結構きつい。

そこでRackに対応したサーバーを立ててサーバーとサーバーでやりとりさせる、という方式がすごく現代的。 直接にRack経由でプログラムとやりとりするのはRackに対応したサーバーだけれど、Rackに対応したサーバーにwebサーバーとしての機能を持たせると大変なので、「本物のwebサーバーに矢面に立ってもらって、RackサーバーはあくまでRack対応に特化」というわけである。

Rackに特化したサーバーとしては(別にRackだけではないんだけど)、Webrick, Mongrel, Puma, Thin, Unicornあたりがある。

しかしRackでやりとりする方法があればいいので、FastCGI + Rackという方法もある。 それはRack側でFastCGI経由で受け取って、応答するためのハンドラが用意されている。

つまり、Unicornのようなサーバーを立てる代わりの手段としてFastCGIが使える。 FastCGIもデーモンを必要とするので別にFastCGIにすることで間に挟まってるものを減らす効果はない。 ただ話が楽になるだけである。

Unicornはむちゃくちゃ速いので、UnicornでUnixドメインソケットを使えば形式とししてはspawn-fcgiでUnixドメインソケットを使っているのと一緒だし、やっていることははるかに高度になる。 これが超モダンなやり方である。

が、あえてのFastCGI。 理由は管理する要素数を減らすためである。必要がないのにいかついものを使うことはしない。 これはサーバー運用のコツでもある。

なお、Rackに関してはかなり情報が少ない。 なんらかのフレームワーク…というか、ほぼRailsのバックエンドとしてのRackの話だけで、Rack単独の話ってない。 そして、FastCGIを使う話もない。これもだいたいなんらかのアプリケーションが「使ってる」あるいは「使わせる」話になる。

なんというか、みんなそんなに自分でプログラム作るってことをしてないのか… 世の中エンジニアたくさんいるのに、WordPressとRailsだけで満足なのか…

そんなわけで情報が猛烈に足りていない中、FastCGIとRackについて勉強することになったわけだ。

なお、Nginxでアプリケーションとやりとりする方法に関してはDiscourceで散々やったので経験済みだ。

なぜRackなのか

もちろんこのことからもわかるようにRackはなくても構わない。 spawn-cgiも使用せず単独のFastCGIアプリケーションを開発するのは容易である。

私が気にしたのはRubyのfcgiライブラリは2013年から更新が止まっているとい点だ。 また、Arch LinuxではfcgiライブラリはAURにもなく

# gem install --no-user-install fcgi

とするよりない。

ベーシックな機構であるFastCGIそのものが廃止になるようなことは考えにくいが、NginxのCGIの扱いのように消極的なサポートへと変遷する可能性はある。 その場合にアプリケーションの書き直しが発生してしまう。

Rackは現在主流であり、新規採用例も多い。 Rackが廃止になると影響を受ける範囲も非常に広いので今後10年は安泰だと思われる。

そこでFastCGI+Rackという構成にしたわけだ。 この場合でもRackはFastCGIをネイティブサポートしているわけではく、fcgiライブラリを使ったハンドラを同梱しているだけなのでfcgiライブラリは必要となる。実はこれを回避したかったのだが、結局はできなかった形だ。

とはいえ、この状態であればFastCGIを捨ててUnicornに移行するのも難しくはない。

とりあえずやってみる

Nginx

location / {
    root /var/www/testapp;
    fastcgi_pass /var/run/fcgi-testapp.sock
    fastcgi_index testapp.rb;
    include fastcgi_params;
}

Rack Application

Requestのほうはインターフェイスに絡むけれど、 Responseは単純に#finishでRackに沿った配列を返すための便利クラス。なくてもいい。

spawn-fcgi

# spawn-fcgi -U http -s /var/run/fcgi-testapp.sock /var/www/testapp/testapp.rb -n

試してるうちは-nつきにしてフォアグラウンドで実行するのが楽

実用的にする

起動スクリプト

forkingなので停止・再起動の制御のためPIDファイルを作る。

Systemd Unit

[Unit]
Description = FastCGI Rack Test Application
After = nginx.service

[Service]
Type = forking
PIDFile = /var/run/fcgi-testapp.pid
ExecStart = /usr/local/sbin/fcgi-testapp.bash
ExecStop = kill $MAINPID

[Install]
WantedBy = multi-user.target

forkingなので$MAINPIDがそのままでは使えないため、PIDFileで指定しておく。 Nginxのあとに起動しておいたほうがいいような気がしたけど、なくても構わない。 アクセスが激しい場合は逆にNginxの前に起動したほうがいいだろう

spawn-fcgi自体にはアプリをリロード、再起動するような機能はない。

おまけ

S-NailがSubjectも本文も、UTF-8をちゃんとエンコードしてくれるのですごくびっくりした。

「mailxとは違うのだよ!!!」ってことか。 さすがSMTPやPOPやIMAPにも対応しているだけのことはある。

ここの部分(MIMEエンコーディング)も自分でやるつもりだったので、かなり省力化された形。

今回の構築は他にも色々やったのだけれど、共有して意味のある部分はこれくらいのものだろう。

Mimir Yokohamaに用語集機能

Mimir Yokohamaに用語集機能が追加された。

用語集機能はACCS(PureBuilderに取り込まれる以前の)やAki PHP Content Collection時代に実装されていた。

シンプルにACCSではJavaScript+Ruby/CGIで、本文エレメント(テキストノード)を探索し、文字列を送信してRubyで置き変えたものを返してもらい、innerHTMLを置き換える方式だった。

APCCでは単純に力技で置換えていた。

PureBuilderではこの機能を実現していなかった。 正確には一時期実装していたこともあるが、十分な質でなかったのだ。

そしてついに復活した用語集機能。

Pre Pluginsで処理することも考えたが、Post PluginsでRubyで処理している。

「辞書をワード長が長い順に並べ替えて|で結合し、正規表現としてコンパイルして置き換える」という手法はかつてのコードで考えたものである。

用語集のページも単純な手法でMarkdownを生成している。

で、今回もライブラリなしのPureJSで処理している。 今回は44行ほどだが、18行は共有化できるものなので、もう少し短くすることも可能だ。 DHTMLのお手本のような初歩的なものである。

Event.stopPropagation()Event.stopPropagationと書いて悩むはめになったけど。