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

八発白中

はてなブログに引越しました。

Common Lisp用のメッセージキュー「Psychiq」を作りました

2年前にLesqueという、Common Lisp向けにRubyのResqueクローンを作りました。

メッセージキューやジョブキューという種類のプロダクトで、非同期処理を別のプロセスに移譲してバックグラウンドで行うものです。Redisをバックエンドとしています。

ある程度の規模のWebアプリケーションなんかを運用しているところなら必要になってくるだろうものですが、Common Lispでそういうプロダクトを作っているところがあるのかどうかも怪しいです。弊社サムライトはどうなんだと言われると苦しい。

ブログでLesqueの紹介記事は書いたんですが、その後使うことも無いしQuicklispにも申請せずメンテナンスもしていませんでした。特にせっつくようなコメントももらってないので需要もないのかもしれません。

blog.8arrow.org

物好きな僕のフォロワーがRedditにも投稿しています。

www.reddit.com

RをLに置き換えるなんて日本人のユーモアうける、みたいなことが書いてあります。

Sidekiqの台頭

それから2年が経って、状況も変わりました。以前はResque一択のような空気だったメッセージキューも、今はSidekiqという競合があります。

またメッセージキューが必要そうだなぁ、という機運が高まってきたので、この機会にもう一度Sidekiqのアーキテクチャをベースとして懲りずに再実装してみるかと思いました。

そして作ったのが「Psychiq」です。

Resque vs Sidekiq

ResqueもSidekiqもRedisを使ってジョブを処理するのは同じですし、SidekiqはResque互換であることも強調しています。

SidekiqのWebサイトを見ると他の競合製品より最大20倍速いなどと謳われています。高速で並列に大量のジョブを処理することを売りにしているようです。

スレッドベースのアーキテクチャ

違いはアーキテクチャにあります。

Resqueはワーカーをforkで子プロセスで立ち上げ、それぞれのワーカーがジョブをRedisからデキューして処理します。

一方のSidekiqはスレッドで処理します。スレッドのほうが軽量なので、多くのワーカーを起動できます。デフォルトでは25個のスレッドを立ち上げます。

また、スレッドのほうがメモリ空間を共有できるため、ワーカーをまたいだ処理が高速でシンプルに書けます。具体的には処理したジョブの数は失敗したジョブの数などの統計情報を記録する処理などがそれに当たります。

Redisに完全依存

SidekiqはバックエンドがRedis一択で、Resqueのように他のものを使うことができません。

これは欠点のように見えますが、実際はコードベースも小さくシンプルでメンタナブルに保つのに一躍買っているようです。バックエンドが増えるとそれぞれのバックエンドで挙動が変わらないことのテストとか書かないといけないし、大変だしね。

Psychiq

PsychiqはSidekiqのほぼ完全なCommon Lisp移植版です。Redisをバックエンドにしたメッセージキューで、Resque/Sidekiq互換になっています。

Worker (ジョブを処理するコード) を書く

まずは非同期処理をする部分を書きます。これはメインアプリケーションと、Psychiqプロセスで共有するコードなので独立したASDFシステムにしておくのが望ましいです。

引数を受け取って何か処理を行います。返り値は特に使われません。

(defclass my-worker (psy:worker) ())
(defmethod psy:perform ((worker my-worker) &rest args)
  ;; Do something
  )

メインアプリケーションでenqueueする

メインアプリケーションでジョブをenqueueします。先ほどのワーカークラスを指定し、引数リストを渡します。

;; ジョブをenqueueする
(psy:enqueue 'my-worker '("arg1" "arg2"))

;; 300秒後にジョブをenqueueする
(psy:enqueue-in-sec 300 'my-worker '("arg1" "arg2"))

;; 複数のジョブを同時に追加する
;; ループで回すよりも効率が良い
(psy:enqueue-bulk 'my-worker '("arg1" "arg2") '("another" "one") ...)

このデータはRedisに記録され、Psychiqにより処理されるのを待ちます。

Psychiqプロセスを立ち上げる

非同期でジョブを処理するPsychiqプロセスを立ち上げると処理が始まります。このプロセスは通常は立ち上げっぱなしにします。

Roswellスクリプトが提供されているのでコマンドラインから起動できます。そういえばLesqueを作ったときにはRoswellスクリプトなんてなかったんですよね。

現在はQuicklispに登録されていないのでまずはGitHubからインストールが必要です。*1

$ cd ~/common-lisp
$ git clone https://github.com/fukamachi/psychiq
$ ros -l psychiq/psychiq.asd install psychiq
# ~/.roswell/bin/psychiq がインストールされているはずです
$ psychiq --host localhost --port 6379 --system myapp-workers

これでメインアプリケーションでenqueueが行われるたびに、Redisを介してジョブがこのPsychiqプロセスに渡り、非同期で処理が行われます。

失敗したジョブはどうなるか

ジョブの処理中にerror conditionが発生するとジョブは失敗したものとして扱われます。

失敗したジョブはfailedキューに移動し、間を置いてから再度enqueueされて自動リトライされます。一定回数の失敗を繰り返すとdeadキューに移動します。

逆に言うなら、ジョブを処理中に失敗として扱いたいときはerrorを発生させれば良いわけです。

Web UI

PsychiqにはResqueやSidekiqのようなWeb UIはありません。しかし、Resque/Sidekiq互換なので、SidekiqのWeb UIを流用することができます。

以下のようなconfig.ruを用意し、

require 'sidekiq'

Sidekiq.configure_client do |config|
  config.redis = { :size => 1, url: 'redis://localhost:6379', namespace: 'psychiq' }
end

require 'sidekiq/web'
run Sidekiq::Web

Rackサーバを起動します。

$ rackup config.ru

http://localhost:9292/busyを開くと以下のような画面が見えます。

f:id:nitro_idiot:20160118174428p:plain

左上のロゴが「Sidekiq」なので不思議な感じがしますが、Psychiq用のRedisキューを読み込んで情報を表示しています。

ここから実行中のプロセスやジョブを一覧したり、失敗したジョブの手動リトライなどができます。

おわりに

以上、Psychiqを紹介しました。いつも通りGitHubで公開しています。ライセンスはLLGPLです。

そういえば、今月末はLisp Meetupがありますね。

lisp.connpass.com

今回のテーマはCommon Lispです。特にPsychiqの話をする予定はありません。

takagiさんの「Common Lisp Scriptの話」が気になります。フロントエンドもCommon Lispで書ける未来も近いんでしょうか。

どうぞ奮ってご参加ください。

*1:現在Quicklisp Alpha distには含まれています。1月のリリースには含まれる予定です。

割と本気で家庭用Slack Botを作ってみた

僕は妻と二人暮らしをしています。かつてはLINEを使って普段のやり取りをしていたのですが、一年ほど前からSlackを使い始めました。

Slackの良いところはハッカビリティが高いところです。Google Calendarなど他のサービスと連携することができるし、IFTTTを使って多少凝ったこともできます。

IFTTT時代

IFTTTを使えば天気予報をSlackチャンネルに流せます。

f:id:nitro_idiot:20160113142319p:plain

英語というのは不本意ですが、一応今日の天気はわかります。英語が読めなくてもアイコンを見れば、雨が降りそうな気がするってくらいはわかります。

しかし、しばらく運用しているうちに疑念が。

f:id:nitro_idiot:20160113144739p:plain

天気予報が当たらない。

IFTTTが連携している天気予報は「The Weather Channel」の情報なのですが、これが日本の気象庁の予想と違っていて全然当たらない。

ちなみにiOS8から標準の天気アプリが提供している予報もこの情報のようです。

当たらない天気予報をいつまで見ているのもバカらしい。そこで移行を考えます。

myThings時代

myThingsはYahoo Japanが提供するIFTTTのようなサービスです。IFTTTと違って日本製なので日本語で使えますし、日本ローカルなサービスの対応も手厚いです。

f:id:nitro_idiot:20160113145714p:plain

myThingsはYahoo!天気に対応しているので、ルールを追加するだけでSlackに導入できます。

f:id:nitro_idiot:20160113145100p:plain

Slackの連携に自分のアカウントを使ってしまったため僕が毎日投稿しているように見えるのがかっこ悪いですが、まともな天気予報を知ることができるようになりました。

Hubot時代

しかし人間は欲深い。myThingsを使い始めて程なく不満が出てきます。

「一目で天気がわかるようにアイコンが欲しい」
「降水確率20%って傘必要?不要?」
「URLの展開が幅とりすぎ」

こうなると自分でボットを運用したほうが良さそうです。

カッとなった僕は、おもむろに趣味用に転がしてるさくらVPSにHubotをデプロイし、最強の家庭用Botを作る旅に出ました。

毎朝、天気予報を投稿する

f:id:nitro_idiot:20160113145529p:plain

まずは天気予報の投稿の改善から。Yahoo!天気はAPIが無いので実装が少しばかり複雑です。

APIはありませんが、公式のTwitterアカウント(@Yahoo_weather)が毎日「#東京の天気」というハッシュタグ付きでひと言ツイートをしています。

これが読むのにちょうどいい長さ。TwitterのSearch APIを叩いて「#東京の天気 from:Yahoo_weather」の最新ツイートの本文をSlackに流しました。

これだけでは味気が無いので、Yahoo!天気の東京の天気のページスクリーンショットを撮り、ImageMagickで今日の天気の部分だけ切り出して、切り出した画像をGyazoに投稿してSlackに流しました。

スクリーンショットの撮影にはpython-webkit2pngを使っています。サーバにQtのインストールが必要です。

試行錯誤の結果、今のところ不満はありません。

電車が遅延しているときに報告する

f:id:nitro_idiot:20160113151912p:plain

いざ出かけようというときに利用する予定の電車が止まっていて迂回しないといけない、ということが稀によくあります。

そこで、よく利用する路線の遅延情報をSlackに流すことにしました。

JRの運行情報東京メトロの運行情報スクレイピングして、よく使う路線の遅延情報を得ます。

今のところ遅延しているときには自動で教えてくれるようになっています。遅延が解消されたときには改めて教えてくれます。「fukabot 遅延」と話しかけると遅延しているかどうかに関わらず登録しているすべての路線の情報を教えてくれます。

常に流れていてもうるさいだけなので、平日の通勤時間(8:30〜9:59と18:30〜19:59)と、土日のみ動くようになっています。

宅配便が届く時間を投稿する

f:id:nitro_idiot:20160113172140p:plain

宅配便が届くときには家に誰かがいないといけません。けれど「何日の何時に荷物が届く」というのをいちいち口頭で共有するのは面倒です。そこでSlack Botに自動で報告させるようにすれば忘れることもありません。

クロネコヤマトや佐川急便は、Webでユーザー登録をすると配達予定をメールでお知らせしてくれます。GmailAPIを叩けば配達予定お知らせメールが届いているかはわかるので、それをSlackに投稿するだけです。

Slackの投稿のタイトル部分が配達業者のWebサイトへのリンクになっていて、変更したい場合はすぐにWebサイトから変更できます。

現在、AmazonやSEIYUネットスーパーにも対応しています。どれも事前にメールを送ってくれるので、本文のパース部分以外は同じです。

スーパーのお買い得情報をお知らせする

f:id:nitro_idiot:20160113181048p:plain

最近はネットスーパーを利用し始めましたが、魚や肉は店頭で見て買いたいので最寄りのスーパーも利用します。

我が家では毎夕にシュフモが提供するお買い得情報がSlackに流れます。

チラシのような表示をそのまま活かしたかったのでテキストではなく、スクリーンショットを撮ってImageMagickで切り抜き、Gyazoにアップロードして利用しています。

夫婦の記念日をお知らせする

f:id:nitro_idiot:20160113172956p:plain

普通の機能だけではつまらない。せっかく夫婦で使っているので、夫婦っぽく記念日の情報とか流してみることにしました。

はてなカウンティングでカウントアップを作成し、そのスクリーンショットを撮って切り抜き、Gyazoにアップロードします。

記念日とキリの良い日数のときにお知らせしてくれます。「fukabot 記念日」と言うとすべての記念日情報を教えてくれます。

f:id:nitro_idiot:20160113173258p:plain

適当に作ってみたのですが、今のところ妻の反応が一番良かった機能です。

書籍の情報を教える

f:id:nitro_idiot:20160113165121p:plain

夫婦の間では読んでる本や読みたい本の話をすることが多いです。

「この本買おうと思うけど」みたいな話をするときに、Amazonだといくらか、Kindleで買えるか、図書館で借りられるかを知りたい。そこで、Slackで書籍のタイトルを言うと書籍の情報を表示してくれるようにしました。

まずは楽天ブックスAPIで検索をしてISBNを取ります。これを使ってAmazonにアクセスして書籍や電子書籍の価格、サムネイルなどを取得します。図書館の貸し出し状況の確認にはカーリルAPIを使っています。

「book:本のタイトル」と言うことでも反応するので会話の中に埋め込みやすいです。

f:id:nitro_idiot:20160113171815p:plain

夫婦の読書状況を共有する

f:id:nitro_idiot:20160113174934p:plain

引き続いて本の機能です。夫婦共に日常的に本を読みます。今相手が何の本を読んでいるか、面白いか面白くないか、などの話をよくします。

夫婦二人とも読書メーターを使っているので、読み始めた本や読み終わった本が追加されたときにSlackに流すようにしました。

読書メーターにはAPIが無いのでスクレイピングしています。

まとめ

他にスケジュールの共有やゴミ出しの日のリマインダ、タスク管理などもSlackに連携しています。

紹介した機能は既存のHubotスクリプトを使わずに全てCommon Lispで実装しています。

既存のものを使わないので細かい調整ができるし、試行錯誤しながら夫婦に合わせてカスタマイズができます。

今のところ著しく生活が豊かになった、という実感はないですが、今後も生活の中でできるところは自動化なり何なりしていきたいです。プログラマですからね。

HubotスクリプトをCommon Lispで書く

f:id:nitro_idiot:20151225001557p:plain

いい加減ChatOpsにも手を付けたいなぁ、と思って、試しに家庭内SlackにHubotを導入してみました。

HubotはGitHub社製のチャットボットフレームワークです。CoffeeScriptで書かれていてNode.jsで動きます。挙動を追加するにはCoffeeScriptスクリプトを書きます。

これを利用して、チャットというインターフェイスを使って様々な日常タスクを処理させることができます。最近の流行りでは、hubot deployなどと唱えるとチャットからサーバのデプロイをしたりできるようです。

今年もChatOps Advent Calendarでチャットボットを使ったテクニックが投稿されているようです。

さて、導入したのはいいのですが、CoffeeScriptを書くのがどうにもダルいJavaScriptも受け付けるらしいけど、やっぱりさくっと書ける言語のほうがいい。つまりCommon Lispがいい。

かと言ってCommon Lispで一からボットフレームワーク作る労力は割けない。どうにかHubotを使ってできないかやってみることにしました。

ヘビーにRoswellに依存しているのでRoswellを知らない方はあらかじめこちらを参照してください。

サブプロセスでRoswellスクリプトを呼ぶ

まず考えたのが、Hubotスクリプトでサブプロセスを立ち上げてRoswellスクリプトを実行し、その結果をチャットに流す方法です。これならば主な実装をCommon Lispで書くことができます。

ディレクトリ構造
├ scripts/
│   Hubotスクリプト (.coffee, .js) 置き場
├ roswell/
│   Roswellスクリプト (.ros) 置き場
├ bin/
│   実行ファイル。hubotなど
├ Lakefile
├ README.md
├ external-scripts.json
├ hubot-scripts.json
└ package.json

以下のようなHubotスクリプトをscripts/whoami.coffeeに置いて、

module.exports = (robot) ->
  robot.respond /who are you\?/i, (msg) ->
    @exec = require('child_process').exec
    command = "sh #{ __dirname }/../roswell/whoami.ros"
    @exec command, (error, stdout, stderr) ->
      msg.send error if error?
      msg.send stdout if stdout?
      msg.send stderr if stderr?

そこから呼び出されるスクリプトをroswell/whoami.rosに書きます。

#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#

(defun main (&rest argv)
  (declare (ignorable argv))
  (format t "~&I'm fukabot!~%"))

こうすれば “who are you?” と声をかけると roswell/whoami.ros がサブプロセスで実行され、結果の “I’m fukabot!” が返ってきます。

f:id:nitro_idiot:20151224234827p:plain

前準備はサーバにRoswellをインストールすることだけです。手軽ですね。

全部Common Lispで書く

しかし、人間は欲が深い。

上記の方法では、新しくスクリプトを追加するときにCoffeeScriptとRoswellスクリプトの二つを追加しないといけません。これが何とも煩わしい。

特に、どの文言にマッチするかはCoffeeScriptに記述するのに、実装は別のファイルっていうのがあまりイケてない。全部Roswellスクリプトに書きたい。

そこで、少し変わったRoswellスクリプトを書くことで、そこからHubotスクリプトを生成する方式に変更しました。

拙作の hubotify.ros という変換用Roswellスクリプトを使います。これを実行権限をつけてbin/以下にでもダウンロードしておきます。

上述のRoswellスクリプト (roswell/whoami.ros) を以下のように追記します。

#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#

(defun main (&rest argv)
  (declare (ignorable argv))
  (format t "~&I'm fukabot!~%"))

(ql:quickload :parenscript :silent t)
(import '(ps:ps ps:@ ps:regex))

(defun js-main ()
  (ps
    ((@ robot respond) (regex "/who are you\\?/i")
     (lambda (msg)
       (run-main
        (lambda (error stdout stderr)
          (when error
            ((@ msg send) error))
          (when stdout
            ((@ msg send) stdout))
          (when stderr
            ((@ msg send) stderr))))))))

関数js-mainがあるのが特徴です。この関数はJavaScriptコードを文字列で返せば何でも良いです。JavaScriptコードを生成するのにはParenScriptを使っています。

関数run-mainは第一引数にcallback、以降の可変長引数としてRoswellスクリプトへの引数を受け取ります。

最後にこのRoswellスクリプトに対して hubotify.ros を実行します。

$ bin/hubotify.ros roswell/whoami.ros
Wrote '/Users/nitro_idiot/Programs/etc/fukabot/scripts/whoami.js'

これで scripts/whoami.js が生成されるので、そのままデプロイすれば大丈夫です。

ちなみに生成された whoami.js の中身はこのようになっています。

function runMain(callback) {
    var argv = [];
    for (var i1 = 0; i1 < arguments.length - 1; i1 += 1) {
        argv[i1] = arguments[i1 + 1];
    };
    this.execFile = require('child_process').execFile;
    __PS_MV_REG = {};
    return this.execFile(__dirname + '/../' + 'roswell/whoami.ros', argv, new(Object), function (error, stdout, stderr) {
        callback(error, stdout, stderr);
        __PS_MV_REG = {};
        return null;
    });
};
module.exports = function(robot) {
robot.respond(/who are you\?/i, function (msg) {
    return runMain(function (error, stdout, stderr) {
        if (error) {
            msg.send(error);
        };
        if (stdout) {
            msg.send(stdout);
        };
        return stderr ? msg.send(stderr) : null;
    });
});
};

新しくスクリプトを追加したいときはRoswellスクリプトを追加し、hubotifyすればHubotで使える形になります。

変換のときはLakeのタスクでlake hubotifyのように全てのRoswellスクリプトに対して実行できるようにすれば楽です。

;; Lakefile
(task "hubotify" ()
  (dolist (file (uiop:directory-files #P"roswell/"))
    (sh `("bin/hubotify" ,(namestring file)))))

まとめ

Roswell三昧でした。意外とやってやれないことはないものですね。

ParenScript部分が少し癖がありますが、そこは使っていくうちに共通化してもう少しこなれてくるんじゃないかと思います。

語らるるべき日本のCommon Lisper達

Lispのエッセイのようなものを継続的にブログに載せていこうとしたのはいいのだが、立て続けに2つ載せたきりなかなか続かない。これはその3つ目のものである。

今回は「人」に焦点をあてて、Common Lispコミュニティで現状活発に活動している人を紹介する。挙げてみると、どうしてもそれなりに親交のある人に偏っている。またいずれ時をおいて第二弾でも書くかもしれない。

佐野匡俊

佐野さんは僕がLispを始めた頃からの知り合いである。最初に会って話をしたのは2010年に米国のリノというカジノ街で開催されたInternational Lisp Conferenceで、その後も国内外のカンファレンスでよく顔を合わせた。

今回紹介する中では年長で、100kgを超える縦にも横にも大きな体つきをしている。「待ち合わせは佐野さん前」と言われるくらい、人混みの中であろうと遠くから見てすぐ分かる。

だが、兄貴分という柄ではない。自信のなさそうなぼんやりした話し方。積極的入社Common Lispの良さを広めていくわけでもない。自分はCommon Lispのことが好きだけれど周りはどう思うか知らない、という態度で佇んでいる。どうも、みんなで公園で遊んでいたはずが、いつの間にか自分だけがそのまま取り残されてしまった子供のような様子である。

かと言って自分の主義主張が無いわけではなさそうだ。

2010年の10月、当時の僕の雇い主が主催の社外勉強会で「Lisp脳」というテーマでやることになった。佐野さんも何か話してもらえませんかね、と講演を打診すると、即答はしづらいと言いながらも引き受けてくれた。

佐野さんが登壇し、Common Lispの魅力を語る下りでこんな話をしていたのを覚えている。

Common Lispには (他の言語と比して) まだまだ足りない部分が多いです。新雪を踏みたい人には、まだいくらでもあります。ぜひ一緒にやりましょう。

彼はよくこのような文学的な表現をする。

当の本人はそんな話をしたことも覚えていまい。が、これを聞いた僕が、その後ウェブ周辺の新雪を踏み荒らしていったというのはご存知の通りである。

自分のブログは持っていないのでウェブで自分を語るようなことは無いが、話せば広い視野と気の長い展望を持っている面白い人物である。

実践Common Lispの訳者であることでこれまでも多少知られていたが、最近はRoswellの作者というほうが有名になってきている。

Rudolph Miller

Rudolph MillerはCommon Lisp界の若手ホープである。コミュニティに参加する前から独学でCommon Lispを学び、僕が会ったときには一人前のCommon Lisperだった。

趣味は何かと聞けば「Common Lisp」と答える。事実、平日も週末も関係なく年がら年中コードを書いている。出かけるときはいつもMacBookHHKBを持ち歩く。彼のことだから、おそらく僕が死んで葬式に現れたとしても参列席でMacBook開いてプログラムを書いているだろう。

一時彼とはサムライトという会社で同僚となったが、彼の生産性には目を見張る。

僕が入社するまでRailsで動いていたアプリケーションをたった一人、しかもほんの一ヶ月でCommon Lispにすっかり書き直してしまった。その後続けてNode.jsの広告配信サーバもCommon Lispで書き直し、上々のパフォーマンスを出している。

コードを書くだけでなく読む量もどうやら凄まじい。「この週末に深町さんのSxQLのコードを読んでみたのだけれど、面白い設計ですね」とか、「Qlotでハマったので読んでみたんですけど、想像よりも大変なことをしていますね」などと感想をくれたことも一度や二度ではない。その度に、彼の技術は魔法のようなものではなくこういう影の活動の積み重ねで裏打ちされているのだな、と感心したものである。

また、彼は僕に高速なHTTPサーバを書かせるきっかけを作った人物でもある。

サムライトに入社する前の話だ。ある日、僕がTwitterでNode.jsの話をしていた。「Node.jsか。速い速いって言うけど遅いじゃねぇか。Common Lispに移植したら、大して努力もしないで35倍も速くなった」

するとRudolph Millerが「HTTPサーバはどうです」と言ってきた。「さあ、計測したことはないけど」Common LispのイベントドリブンなHTTPサーバといえば当時はWookieしかなかった。これとNode.jsのhttpモジュールのベンチマークを取ってみると、なんとCommon Lispが2倍も遅かった。

この屈辱的とも言える事実を見て、僕はWookieや、そのHTTPパーサ「http-parse」にパッチを何度か送ることとなった。その度にいくらかパフォーマンスの改善は見られたが、Node.jsほどではない。そのうちに、こんなんじゃやってられん、ということで自前で書き直すことにした。それが「fast-http」や「Woo」である。

それから数ヶ月Wooの改善を続け、Node.jsと同程度には速い、というくらいになった頃。これでいいかねぇと言うと、Rudolphが「高速な言語のCommon Lispがそれでいいんですか」とニヤニヤ言う。言うねぇ、生意気なやつだ、それじゃあ行けるところまでやってやるかーー。

Wooのバックエンドをlibeventからlibevに置き換え、もはや完全にWookieとは別物になった。今やWooはNode.jsの2倍速い。彼は書くコードもそうだが、言うこともなかなか鋭く煽り方も一流である。

そんな彼も、先日サムライトを退職してCommon Lispを使わない企業に転職した。彼はそうとは言わないが、今後はCommon Lispを書く機会も然程もなかろう。残念なことである。

κeen

粗忽長屋」という落語がある。

浅草の観音に参りにきた八五郎は道端に人だかりが出来ているのを見つける。どうやら昨晩から行き倒れの死骸の身元がわからないので知っている人物を探しているらしい。八五郎は死人の顔を見るなり、これは隣に住んでる熊五郎だ、と言う。今朝会ったのだからそんなわけがないのだが、あいつ自分が死んだことに気づいてねーんだ、と言って長屋に戻って熊五郎を説得する。最初は否定していた熊五郎もいろいろ言われるうちに自分は死んだんだと納得してしまうという滑稽噺である。

粗忽長屋立川談志- YouTube

咄家の立川談志は、この落語を「主観長屋」として少し脚色を加えて演じた。

「“たまには(ひげ)()たれ” つってるじゃねえか」
「髭なんぞはどうでもいいんだい」
「人間てえのは鏡見んだろ。鏡ィ見りゃ、自分の顔ってのが判るじゃねえか。俺なんぞは、(まい)(にち)顔洗って、歯ァ磨いて、髭なんぞ剃るから、自分で自分が判る。だから、街で歩いてて、あ、俺だなってすぐ判る。お前は自分のことが判んない。俺はお前が判って、俺が判って、両方判ってる。俺が“お前だ” つってンだから、間違いねえだろう」
 こいつは“粗忽”ではない。あまりに主観が強いと、人間の生死までも判らなくなってしまうという、物凄いテーマを持った落語なのだ。

談志 最後の根多帳 (談志最後の三部作 第二弾) より

κeenさんにも、この八五郎のようなところがある。

非常に思い込みが強く、一度そうだと思ったら他の全員が違うと言ってもなかなか自分の意見を曲げない。不具合報告を彼からもらっても、よくよく聞けばκeenさんの勘違いだったりする。なのに語気は強いのだから後から思い返すと笑ってしまう。どうも彼は「主観」が強いらしい。

κeenさんがコードを書くときの勢いには目を見張るものがある。たとえば、彼のプロダクトの一つである「CIM」は、次のLisp Meetupが一週間後に迫っているという頃に書き始めて、一時はどうなるものかと思っていたが当日には非常に多機能なものを仕上げてきた。

しかし、その後目立った機能追加は無く、不具合もなかなか直らない。結局CIMは後発のRoswellに取って代わられてしまった。彼はどうも飽きっぽい性格のようで、作るときは並ならぬパフォーマンスを発揮するが、ある程度形になると手をかけるのをやめてしまう。結果中途半端なものがGitHubリポジトリに並ぶ。勿体無いものである。

彼はShibuya.lispの現行運営の一人でもある。積極的に月次のLisp Meetupの開催を助けるだけでなく、発表者が集まらないときは率先してLTをする。たとえ彼が仕事や私生活で忙しそうにしているときでも何かしら興味深い話をまとめてくる。彼が粗忽さや飽きっぽいという欠点を抱えながらもコミュニティで愛される立場にいるのも、彼のこのサービス精神の旺盛さから来るのかもしれない。

g000001

g000001さんを挙げなければ日本のLispコミュニティを言い表せないように思う。

とはいえ親交があるわけではない。昔にTwitterやブログで僕が生意気なことを言って怒らせたという経緯もある。ただ、僕に限らず彼との親交を持ち続けている人は周りにいない。古くからこのコミュニティにいる人は「あぁ千葉さんか」などと知人のように呼ぶが、じゃあ知っているのかと思いきや、聞いてみても大して知っているわけでもない。おにぎりのアイコンなので「おにぎりの人」などと呼ばれている。

昔、橘右近という寄席文字の職人がいた。日本テレビの人気テレビ番組「笑点」の舞台の後ろには、彼の書いた寄席文字が額で飾られている。その彼には寄席文字だけでなく、収集家としての趣味もあったらしい。

右近さんは、寄席の古き文献を集める趣味があって、新聞、雑誌の切り抜きはもとより、「ビラ字」や「鳴り物」で稼いだ金を、惜しげもなく使い、「昔のビラ」や「楽屋帳」「本」「写真」を集めてくる。その数は大変なもので、自分達の歴史をほとんど大切にしない寄席や、咄家の中で、貴重な存在になっている。

談志人生全集〈第1巻〉生意気ざかり より

g000001さんはLispコミュニティの橘右近のような存在である。方言に関係なくLispの古い文献・プログラム・MLのメールなどを集めている考古学者だ。Lispはそれこそ歴史がある言語なのでその情報量は膨大であるはずで、その一部は彼のブログで垣間見ることができる。

現代のコミュニティにおいても、Reddit/r/lisp_jaで日本語のLisp情報を活発に収集・共有を行っている。有名なブログだけでなく初心者がちょっと試してみたというような記事まで拾ってくるのだから、彼のアンテナの広さには感嘆させられる。

その他にも逆引きCommon Lispを運用していたり、Stack OverFlowの日本語版Common Lispの質問に積極的に回答したりなど、初心者向けのサポートも熱心である。

Shibuya.lispの初期運営を降りてからは人の集まる場所には滅多に顔を出さないが、ブログでは活発に情報発信を行っている。

ISUCON5オンライン予選にclfreaksとして参加しました

9/27(日)に開催されたISUCON5のオンライン予選に参加しました。

僕はアプリケーション側の改善、他の二人はインフラ寄りの対応をするように事前に役割分担をしていました。

“ISUCON”とは

ISUCONは「Iikanjini Speed Up Contest」の略で、LINE株式会社 (昔はLivedoor) が主催する、アプリケーションやインフラのパフォーマンスチューニングを行ってそのスコアを競うイベントです。2〜3人のチームを作って参加します。

優勝賞金100万円!今年もやります ISUCON5 開催と日程のお知らせ #isucon : ISUCON公式Blog

この週末の2日間にオンライン予選が行われました。

チームビルディング

ISUCONというイベント自体は知っていたのですが、どうも自分には縁遠いものだと思っていました。まさか参加することになろうとは。

というのも、ISUCONではチューニング対象の環境 (アプリケーション)が出題者側から与えられます。その実装がPerlRubyPython、Goなどで、始めに好きな実装を選びます。

出題者から提供される参照実装に「Common Lisp」はありません。

僕は普段からCommon Lispアプリケーションしか書いていないし、インフラのチューニングも業務でやるようなこともないので、出場したところでなぁと思っていたわけです。

そんな時、κeenさんに「ISUCON出ませんか」と誘われました。

でもCommon Lispは無いんでしょう、と言うと、「ISUCONはベンチマークスコアが良ければアプリケーションにいくらでも手を入れていいという特徴があるので、いっそCommon Lispで再実装してしまいましょう」、と豪胆なことを言います。

「そんな斜め上の戦略でうまくいくのか」という無粋な話もしたのですが、インフラに合わせてアプリケーションはダイナミックに変更する必要があり、いずれにしても大きく変更するなら同じだろうというのです。実際、前回の予選ではC++でアプリケーションを書き直すことで本戦出場したチームもいたそうです。

そう言われると確かに面白そうだ、ということで、誘われるままにκeenさん、Rudolph Millerさんと3人で出ることになりました。

チーム名は「clfreaks」です。そのままですね。

前日まで

とりあえず、高速なCommon LispのWebアプリケーションを目指すならWooを使うことになりそうなのは目に見えています。

f:id:nitro_idiot:20150928044620p:plain

かねてからTODOリストにあったWooのUNIXドメインソケット対応を急遽実装することに。どう考えてもISUCONの前日にすることではない。実装自体は大体2時間くらいで終わった。

あとはRedisを使う場合に備えてcl-redisに使い方確認とか。

過去問を見るとセッションを使うものが出ることが多そうだったのでLackのセッションミドルウェアのパフォーマンスも改善しました。不要の場合はSet-Cookieヘッダを送らないとか。本番では結局使わなかったけど。

それから夜に時間があったので、Common Lispでの軽量なWebアプリケーションの雛形を作りました。ningleかCaveman2でもよかったんだけど、性能上の不安要素はできるだけ取り除きたいという理由で、Clackを生で使ってMy Wayでルーティングすることにしました。

qlot exec clackup --server :woo --listen /tmp/woo.sock app.lisp でアプリケーションが起動することを確認して当日に備えました。

当日

"オンライン"予選なんだけど、LINEさんの好意で渋谷のLINEカフェも利用可能だったので全員で9:30に渋谷のヒカリエに集合しました。

10:00にGCEのイメージが配布され、他の二人はインフラ構成、僕はアプリケーションコードを読んでいました。

かねてからの予定では、最初の2時間ほどは作戦会議だったのですが、11:00頃から、とっととCommon Lispで実装始めちゃったほうがいいかもね、という話になって、Ruby実装を参考にCommon Lispへの移植を始めました。その間に他の二人はRuby実装をデプロイして、MySQLの改善などをしていました。

使ったもの

今回のアプリケーションはERBのテンプレートファイルが多く、静的ファイルにもしづらかったのでテンプレートエンジンとしてCL-EMBを使いました。MySQLへのアクセスはCL-DBIです。

参考実装はセッションミドルウェアを使っていましたが、単にuser_idをクライアント側に保持させたいだけっぽいのでSet-Cookieヘッダで対応しました。

あとは静的ファイルはすべてNginxで配信するのでStaticミドルウェアも無効に。Lackミドルウェアで使ったのは結局Backtraceだけでした。

無限に時間がかかる

そこから割とガツガツ実装したつもりなんですが、時間はいくらでも過ぎていく。

当初はN+1クエリの改善 (is_friend?get_user) とかも書き換え中にやろうとしていたんだけど、考えている時間ももったいないので、とりあえず動かせるところまで持っていくことを優先。ローカルのDBにデータが無いのでローカルで動かすことはできない。フルスクラッチする場合はもっと早めにmysqldumpしておくべきという謎の知見を得る。

テンプレートなんてちょろっとCL-EMB流に書き換えるだけだろ、と思ったら大間違いで、テンプレート内で普通にget_user呼んだりしていて泡吹きそうだった。db.xqueryとかテンプレート内で呼ぶんじゃない…。

デプロイ

f:id:nitro_idiot:20150928051017p:plain

κeenさんが前日にCapfileを用意してくれて、Capistranoでデプロイできるようにしてくれた。

f:id:nitro_idiot:20150928051151p:plain

シェル芸人の暴挙は阻止しました。

チャットのログを見ると、だいたい16:30くらいに最初のバージョンをデプロイしたようです。残り1時間半。

バグを踏む

ただ、デプロイしてつもりがNginxが502 Bad Gatewayを返してきます。WooのUNIXドメインソケット対応のバグっぽい。

そこからWooのデバッグを始めて、その場で修正。どう考えてもISUCON中にすることではない。

あとは、RoswellでWooを起動するとC-cで終了できないとか結構不便だった。原因は未確認。

clackupコマンドに--debug nilを指定すると、:debugに文字列で"nil"が渡っていて真になるとかいう渋いバグも踏んだ。あとで直す。

デプロイが遅い

ISUCONみたいな時間制限のある競技だと、デプロイが遅いと試行回数が減ってしまいます。

Common Lispのデプロイが遅かった原因は、Capistranoだとホットデプロイのために毎回別ディレクトリにgit cloneしてqlot installを走らせるんだけど、その度にQuicklispとライブラリのインストールが走り、さらにコードのコンパイルが行われる。御存知の通りSBCLコンパイルは遅いです。競技中に「あいあんくらっど〜〜〜〜〜」って叫んでいたのはうちのチームだけだと思う。

対応としてリポジトリquicklisp/ディレクトリを含めてしまう、ということもしたけどあまり改善しませんでした。今後の課題です。

歓声

デプロイは終わったんだけど、結局アプリケーション側のバグでログインができない状態がしばらく続きました。

17:45になってようやくログインが成功し、ホーム画面が表示され、思わず「お〜〜!!!」と歓声。もはやCommon Lispで書き換えたものが時間内に動いたってだけでうれしい。

ちなみに上位チームが20000とかスコアを叩き出している中、弊チームのベンチマークスコアは5でFAILでした。

振り返り

手際の問題とかいろいろあったと思うけど、今回の出題ではアプリケーションコードのボリュームが結構あって、Common Lispで再実装するという選択をした時点で予選通過の見込みは無かったように思います。かと言って、このチームでCommon Lisp捨てて中途半端な成績を残しても悔いは残るってんで、まあ今回は仕方なかったね、っていう話をしました。

結果、Common Lispの性能上の優位性を示すことはできませんでした。

たとえば、今回は静的ファイルへのアクセスがほとんど来なかったので、思い切ってNginxを切ってWooをフロントにしてみるようなチャレンジができなかった。LackやCL-DBIのパフォーマンスがどの程度出るか、とかも踏み込めなかったのは心残りです。

いくらか見つかったCommon Lispでの問題は今後の課題にします。

ISUCON 6?

なんにせよ今回の参加は僕個人にとっては良い経験となりました。LINEの運営者陣、Treasure Dataの出題陣、あと誘ってくれたチームメンバーに感謝します。

来年もISUCONがあるとしたら、出場するよりもむしろCommon Lispのアプリケーション実装を作る出題側の手伝いをしたいと思いました。競技中にアプリケーションを書き換える時間を無くせばCommon Lispでももっと踏み込んだ性能比較ができますしね。

参考

チームメンバーのκeenさんもエントリを書いています。