@kyanny's blog

To be a cross-functional developer

Leave from paperboy&co., join to Quipper

May 27 is my last day at paperboy&co.

I had a great time with gentle people in this 3 years and 3 months. Thank you for all my colleagues!

paperboy&co. is a good company for the following reasons:

  • Culture: There are so many nice people here. They gather and play naturally. They love their colleagues, company, and the internet. The culture of paperboy&co. is the most wonderful one I ever seen.
  • Progress: They have the will to improve their environment. They made a very good engineer evaluation system. They also are trying to introduce some good method such as Scrum.
  • Steadiness: They keep growing. The sales, profit, and stock price are rising in the past few years. They also a member of GMO internet group, one of the largest internet business corporate group in Japan.
  • and they paid a reasonable salary :)

However, a 200+ employee organization is too big for me, so I decided to leave from here and go to the other place to suit me more.

From May 28, I will join to Quipper, the mobile e-learning company based on London.

Quipper was founded by Masayuki Watanabe, a co-founder of DeNA, and the engineering team is led by Masatomo Nakano, a famous developer/blogger. They opened Japanese office at the beginning of this year, so it is the best time to join to the small, flexible team.

There are some reasons why I chose Quipper, but the most important one is passion.

During the Skype meeting at my first (and last) interview, CEO said to me:

Really, our platform and service may change the someone's life more better. It's a wonderful thing, isn't it?

I was deeply moved by his passion, therefore it was not possible to ignore it.

I have a dream in my life and work. I want to be a person who work on a world scale, while living in Japan. Since Quipper is a global company (the headquarters is located London), it's a great chance to take the first step into the world.

I miss seeing nice people, but it's my choice. I wish to learn more, so I decided to be the worst guy in the band again.

http://instagram.com/p/ZtM-yWOHPj/ from right: @demiflare168, @hogemoge and me - Photograph by Hideaki Hamada

rbenv のメカニズム

rbenv 環境下で実行された Ruby プログラムの中から他の Ruby プログラムを起動するときに、 rbenv 環境をリセットしたい―要するに別のバージョンの Ruby で外部プログラムを実行したい―という事情があったので rbenv のメカニズムについて調べた。

rbenv 環境下で ruby コマンドを実行するとき、実際にコンパイルされた ruby バイナリが直接実行されているわけではない。 rbenv 環境をお膳立てした上で ruby バイナリを exec するラッパーのシェルスクリプトが実行される。こういうものを binstub と呼ぶ。

binstub である ruby という名前のシェルスクリプトの中身をみてみると、最終的に rbenv exec というサブコマンドを呼び出している。 rbenv のサブコマンドはリポジトリでいうと libexec ディレクトリ以下にある。 rbenv execrbenv-exec スクリプトを実行する。

rbenv-exec は実行時に rbenv-version-name を呼び出して、どの Ruby バージョンを利用するかを決定する。 rbenv-version-name は環境変数 RBENV_VERSION が定義されていればその値を使い、未定義であれば rbenv-version-file を呼び出して .ruby-version または .rbenv-version というファイルを探し、見つかればその値を使う。

rbenv-version-file はあるディレクトリを起点にして、ルートディレクトリ / に向かってパスをさかのぼりながらバージョン指定ファイルを探す。この起点となるディレクトリの初期値は環境変数 RBENV_DIR で、この環境変数は libexec のなかにある rbenv (いわゆる rbenv の本体である) の実行時に、 rbenv コマンドを実行したときのカレントディレクトリが設定される。

このような内部コマンドの助けによって、 rbenv 環境下にインストールされている Ruby のバージョンが決定され、最終的に ruby バイナリへのフルパスが組み立てられてコマンドが実行される。以上の仕組みにより、 rbenv は複数バージョンの Ruby を切り替える振る舞いを実現している。


さて、冒頭の疑問に戻って、 rbenv 環境下で別バージョンの Ruby を使い外部プログラムを実行したい場合はどうすれば良いだろうか。

一番簡単な方法は、環境変数 RBENV_VERSION を再定義してから外部プログラムを呼び出すことだ。外部プログラム実行時に指定したい Ruby のバージョンがわかっている場合は、この方法で決め打ちできる。

バージョンが事前にわからない場合もある。あるディレクトリに移動してプログラムを実行するとき .ruby-version というファイルが置かれていたらそこで指定されたバージョンの Ruby を使いたい。この場合、呼び出し側プログラムが自分で .ruby-version ファイルを読み込んで RBENV_VERSION を再定義することもできるが、 rbenv 本体のバージョン判定の仕組みに任せるほうが手間が少ない。

ここでようやく、呼び出し側スクリプトの rbenv 環境をリセットしたいという話に繋がる。 rbenv は RBENV_VERSION にもとづいて Ruby のバージョンを決定するのだからこの環境変数を空にしてしまえば良いのだが、実は落とし穴がある。 RBENV_DIR が定義済みだと .ruby-version ファイルを探す起点のディレクトリが呼び出し側プログラムの実行時のままであるため、別ディレクトリに chdir してから rbenv exec を実行しても、移動後のディレクトリ内にある .ruby-version ファイルを読んでくれないのだ。よって、 RBENV_DIR 環境変数も空にする必要がある。

以上のことから、 rbenv 環境をリセットするコードは以下のようになる。

command = "ruby -v"

begin
  original_env = ENV.to_hash
  ENV.update('RBENV_VERSION' => nil)
  ENV.update('RBENV_DIR' => nil)
  
  system("rbenv exec #{command}")
ensure
  ENV.replace(original_env)
end

こういう処理はブロックつきメソッドにすると呼び出し側で利用しやすい。

def with_env(env, &block)
  original_env = ENV.to_hash
  ENV.update(env)

  yield if block_given?
ensure
  ENV.replace(original_env.to_hash)
end

def with_clean_rbenv(&block)
  with_env({'RBENV_VERSION' => nil, 'RBENV_DIR' => nil}) do
    yield if block_given?
  end
end

with_clean_rbenv do
  system("rbenv exec ruby -v")
end

余談だが、 Bundler との組み合わせについて。

Bundler も bundle コマンド実行時に環境変数をセットするが、 Bundler.with_clean_env というブロックつきメソッドが用意されており、ブロックの中は Bundler の環境変数がリセットされた状態になる。 Bundler.with_clean_env は内部で Bundler.with_original_env を呼び出しており、これは上記の with_env メソッドとほとんど同じ実装だ (with_env はこれを真似て書いたものなので似ていて当然だ)

ここで使われている定数 ORIGINAL_ENV が曲者で、これは bundle コマンド実行時の環境変数を保持するので、 rbenv exec bundle exec というコマンドを呼び出した場合、 ORIGINAL_ENV に rbenv の環境変数も含まれている。なので、 Bundler.with_clean_env の呼び出しと自前で用意した rbenv 環境のリセットのコードの呼び出し順序に気を使う必要がある。

# 正しい呼び出し方。
# Bundler の環境変数をリセットした後で rbenv の環境変数をリセットする。
Bundler.with_clean_env do
  with_clean_rbenv do
    system("rbenv exec ruby -v")
  end
end

# 間違った呼び出し方。
# `with_clean_rbenv` でリセットした rbenv の環境変数が、
# `Bundler.with_clean_env` の実行時に巻き戻る。
with_clean_rbenv do
  Bundler.with_clean_env do
    system("rbenv exec ruby -v")
  end
end

環境変数は便利だが、目に見えづらいので思わぬところではまることがある。

Redis の maxmemory-policy について

Redis をキャッシュストレージとして利用する場合、 maxmemory によって利用可能なメモリの最大値を指定できる。 maxmemory の値を超えるデータの追加が発生した場合の振る舞いを maxmemory-policy によって指定できる。デフォルトの maxmemory-policy は volatile-lru で、 LRU アルゴリズムに従って古いキーの値が優先的に破棄される。

maxmemory-policy は数種類から選べるが、そのうち

  • noeviction を選んだ場合、古いキーの値は破棄されず、新規追加はエラーとなる
  • allkeys-lru または allkeys-random を選んだ場合、 expire の有無に関わらず、全てのキーの中から破棄対象が選ばれる
  • その他を選んだ場合、 expire がセットされているキーのみが破棄対象となる

という違いがある。実装は redis.c の freeMemoryIfNeeded 関数で、テストコードは tests/unit/maxmemory.tcl にある (余談だが Redis のテストコードは Tcl で書かれていて、テスト時には実際に redis-server を起動しコマンドを発行するスタイルのようだ)

キャッシュストレージとして利用する場合、有効期限つきで古い順に消えていってくれると使い勝手が良い。有効期限をつけるには expire を設定すればよい。 そこで気になるのが「Redis は何をもってキーが古いとみなすのか?」という点だ。

maxmemory-policy のうち volatile-lruvolatile-ttl が、この「古い順に破棄する」という戦略だ。

  • volatile-lru は冒頭で触れたとおり LRU アルゴリズムを利用するので、最終アクセス時刻が古い順に破棄される
  • volatile-ttl は expire のタイムスタンプをみて、タイムスタンプがより過去のものから順に破棄される

どちらが良い悪いというものでもないが、個人的には expire で有効期限を指定するのならばそのタイムスタンプが古い順に消えていく volatile-ttl の振る舞いのほうが自然に感じられる。

さて、 volatile-ttl を選んだ場合でも、常にタイムスタンプの最も古いキーが削除されるわけではない。 ランダムに数個のキーを選びその中で最も古いものを削除対象として選ぶアルゴリズムになっているからだ。おそらく expire つきのすべてのキーのタイムスタンプを調べていると時間がかかるためだろう。 このサンプリングの回数を maxmemory-samples で指定できる (デフォルト値は 3) この数を大きくすれば、古いキーを選ぶ厳密さは増すが、パフォーマンスは落ちるだろう。

前置きが長くなったが、この振る舞いを確認したかったので簡単なプログラムを書いて実験を行った。 maxmemory-policy と maxmemory-samples の設定を変更しながら、意図的に maxmemory に収まらなくなるようにデータを追加していき、残っているキーを調べることで Redis が削除対象に選んだキーの傾向を調べた。なお、 LRU アルゴリズムによる古さの判定と expire のタイムスタンプによる古さの判定がかぶってしまうのを防ぐために、 LRU で古いものほど expire が長くなるように調整している。

https://gist.github.com/kyanny/5553300

結果は以下のようになった。

https://gist.github.com/kyanny/5553302

この実験で以下のことがわかった。

  • volatile-lru を選んだ場合、キーの先頭が a: に近いものほど古いと判定されるので、 z: ではじまるキーが多く残された
  • maxmemory-samples を大きな値にして実質全てのキーについて比較を行った場合、多少精度があがったが大きな違いは無かった
  • volatile-ttl を選んだ場合、キーの先頭が z: に近いものほど古いと判定されるので、 a: ではじまるキーが多く残されるはずだが、精度はまちまちだった
  • maxmemory-samples を大きな値にして実質すべてのキーについて比較を行った場合、著しい精度の向上が見られた

キーの数と maxmemory-samples の値の組み合わせによってパフォーマンスにどの程度影響が出るのかは調べていない。 要件によって許容できる速度低下はまちまちだろうし、 expire がより新しいキーが削除されてしまうことがどの程度まで許容できるのかも場合によりけりだろうから、 一概にどちらが良いとは言えないが、要求に最適な機能を提供できるように設定値ひとつでも上手に使い分けていきたいものだ。

crx_unpack gem released

I released crx_unpack gem.

This gem unpacks Google Chrome extension package (crx) and extracts contents of extension from packed zip archive.

require 'crx_unpack'

data = open('extension.crx', 'rb').read
crx = CrxUnpack.unpack(data)
crx.zip #=> zip data of extension contents

# unzip extension contents into `./extension' directory
CrxUnpack.unpack_contents_from_file('extension.crx', './extension')

CRX Package Format is simple, but if you are fed up with writing code to handle the binary data, it helps you.

Feel free to report any issue and patches welcome.

hatenablog.svg

Update: オフィシャル SVG ロゴデータが公開されました http://staff.hatenablog.com/entry/2013/04/10/105917 https://github.com/hatena/Hatena-Blog-Logo


はてなブログのロゴを SVG で描いてみた。

オリジナルのロゴ画像(png)を横に並べてブラウザで拡大しつつ目と手でちまちま調整したので、ピクセル単位で測ったらずれてると思うけど、 SVG 処女作としてはまずまずの出来かなと。2次ベジェ曲線の制御点もカンと目視によるトライアルアンドエラーでがんばりました。

オレが本物のベクターデータってやつを見せてやるぜ!というはてな社のデザイナーの方がいらっしゃいましたら https://github.com/kyanny/hatenablog.svg まで上書き Pull Request を是非お送りください。