dotconfig という設定ファイルフォーマットを作った - Perl 5 Advent Calendar 2015 Day 2

全国1億3千万の設定ファイルフリークの皆さんこんにちは。 手動管理するような設定ファイルフォーマットで JSON ライクなのを欲しいなと以前から思っていたので書いてみました。

特徴

コメント、ケツカンマ、ヒアドキュメント、クオート不要なキー、2/8/16進数リテラルが書ける JSON をイメージしてもらえれば問題ないはず(詳細は spec 参照)

{
    email: "foo@example.com",
    password: "s3cret",
}

実装

現状 Perl 5 実装しか無いですが、他言語のイシューを dotconfig/specに登録していただければ organization に招待します。 普通に parse してるだけなのでほぼ写経でも移植できるくらいにはなっていると思います。 こういった仕様系では珍しくテストケースも用意したので気軽に開発できますね。

余談

実装も仕様も詰めが甘々なので(主に例外処理など)何か妙な挙動や矛盾があれば patches welcome なのでお気軽にどうぞ。

明日は bayashi さんのワンライナーです!


カジュアルに HTTP サーバを建てるコマンド PAD の紹介

Perl Advent Calendar 24 日目の 2 記事目です(厳密)

カジュアルに HTTP サーバを建てるコマンド PAD の紹介をします。 Plack::App::Directory の略です。

数年前 YAPC の LT-THON で話しただけで文字起こししていなかったネタですが、一部界隈では細々と使われているようです。

使い方

使い方は至って簡単で、インストールして起動するだけです。

$ cpanm PAD
$ pad
HTTP::Server::PSGI: Accepting connections at http://0:5000/

これでカレントティレクトリ以下がポート 5000 でサーブされるようになります。ポートを変えたい場合は次のように。

$ pad Static --port 8080

プラグイン

プラグインを実装すれば特定のファイルに処理をかませたレスポンスを返せます。

デフォルトで Markdown Plugin が入っているのでこれを真似て実装すれば、

  • 画像リサイジングロジックの結果をブラウザで確認
  • shipit 直前の POD のレンダリング結果確認(これは実装しておけばよかった)

などなど夢がひろがりんぐですね。

インターフェイス

CLI のインターフェイスの悪さがあって、変えたいと思いつつも解決策が思いつかないのでいい感じなインターフェイス作れそうならぜひパッチください。

  • プラグインをひとつしか読み込めない
  • プラグインにオプション渡しにくい

あたりを解決できたら嬉しい感じ。

まとめ

plackup -MPlack::App::Directory -e 'Plack::App::Directory->new->to_app' よりタイピング量 95% 減で便利。

ちなみに PAD と何の関係もないですが、 データベースのコネクション管理は DBIx::Handler がロバストな実装になっているのでおすすめです。

明日は大取り @songmu さんによる神エントリーです。


Docker コンテナで MySQL を使ったテストの高速化

全国1億2000万の Docker ファンの皆さんこんにちは。

MySQL の起動がとてつもなく遅いのは有名な話。 ところが Docker コンテナの起動はなかなか早いので、 MySQL を使っているようなテストを高速化するケースで有用性が認められるのではないかと思って PoC を書いてみた。

(宣伝)こういった話も含めて YAPC でトークしたいので SNS 等で upvote お願いします: ( ✌'ω')✌ 楽しいモデル層開発 - YAPC::Asia Tokyo 2014 (宣伝おわり)

MySQL を使ったテスト

MySQL を使ったテストをする場合、だいたい次の 2 パターンになる。

  • MySQL をテストのたびに起動してクリーンな状態で使う
  • ローカルにデーモンとして起動した MySQL に接続して DROP TABLETRUNCATE でクリーンな状態にして使う

だけど、「起動が遅い」「setup/teardown に時間がかかる」「並列にテストできない」といった問題を抱え、最終的にはスローテスト問題に帰結する。

Test::Docker::MySQL

都度 MySQL を使うようなテストは MySQL の起動が高速化さえすれば既知の問題は解消でき、更にそのままテスト時間の短縮に繋がる。

そこで Test::Docker::MySQL を PoC として書いてみた。結果としては、

  • 起動時間が 30 sec -> 4 sec に短縮
  • MySQL を複数起動できるので並列にテストを実行できる

と、トータルのテスト時間は相当短縮できる結果になって今後の方針に影響を与える結果になった。

使い方

ドキュメントに書いてあるとおりに docker をセットアップすれば次のように使える。 Docker Hub 便利。

use Test::Docker::MySQL;
my $dm_guard = Test::Docker::MySQL->new;
my $port = $dm_guard->get_port;

my $dsn = "dbi:mysql:database=mysql;host=127.0.0.1;port=$port";
my $dbh = DBI->connect($dsn , 'root', '', { RaiseError => 1 });

ポイント

  • Test::Docker::MySQL#get_portdocker run ... が走る
    • コンテナの起動は早い
  • Test::Docker::MySQL のインスタンスがスコープを抜けると docker kill ... を自動的に実行してくれる
    • スコープをベースとして MySQL インスタンスを管理できる

ベンチマーク

だいたい次のようになって、インスタンス起動数が増えるごとにどんどん差が広がる。

  • Test::Docker::MySQL

    $ cat tdm.pl
    use Test::Docker::MySQL;
    
    
    my $dm_guard = Test::Docker::MySQL->new;
    my $port_1 = $dm_guard->get_port;
    my $port_2 = $dm_guard->get_port;
    
    
    $ time perl tdm.pl
    real    0m4.020s
    user    0m0.134s
    sys     0m0.059s
    
  • Test::mysqld

    $ cat tm.pl
    use Test::mysqld;
    
    
    my $mysqld_1 = Test::mysqld->new(
        my_cnf => { 'skip-networking' => '' }
    );
    my $mysqld_2 = Test::mysqld->new(
        my_cnf => { 'skip-networking' => '' }
    );
    
    
    $ time perl tm.pl
    real    0m30.291s
    user    0m1.002s
    sys     0m1.200s
    

既知の問題

docker run ... コマンドが戻った直後には実は MySQL に接続できなくて、 1.2 秒後くらいに接続できるようになる。 なので、ライブラリの内部では 0.2 秒ごとに接続をリトライしている。

原因がわかっていなくて、 port forwarding か何かがネックになっているのかとか邪推しているけど、理由知っている人がいたら教えて欲しい。


Rails の使いどころ

Rails 使ってもいいんじゃないかと思えるケース、個人的にはだいたいこんな感じ。

どれかひとつでも当てはまれば使うメリットありそう。

  • サービスの規模が小さくて DB 一台で済む
  • ユーザベースが増えてもたかが知れているケース
  • Rails, AR を魔改造できる
  • dev がやんちゃしても ops が何とかしてくれる
  • ひとりで作ってる

Docker 現状認識確認会をした

まえおき

  • 2つくらいデプロイしたい新規のアプリがあったので運用方法の候補に Docker も検討した
  • 検討段階で既にいくつか課題点が見えた
  • Docker に興味ありそうな人たち (@deeeet @repeatedly @kenjiskywalker @punytan) で寿司した

参考画像

tl;dr

  • プロダクションで Docker 使うのは今のところ人柱
  • 盛り上がってる感はあるけど実際使ってる人いない
    • Docker は周辺のミドルウェアが揃わないとなかなか使いにくい

話題ごとに箇条書きメモ

運用

  • アプリのログを完全に回収する方法どうする?
    • ホストのディレクトリをマウントしてそこに書き込む?
    • ログ回収しきってからコンテナ止める安全な方法は?
  • コンテナ内のアプリも監視したい
  • 高負荷時の挙動が未知なのでトラブった時怖い
  • 環境
    • カーネルバージョン新しいの使えない環境だと導入すら難易度高くてつらい
    • ディストリの壁 (CentOSェ…)

デプロイ

  • DB の接続設定とかどうする?
    • ビルド時にコンテナに突っ込む?
      • Dockerfile 内で ONBUILD 使えるかも
        • http://deeeet.com/writing/2014/03/21/docker-onbuild/
    • etcd や consul のような分散 KVS に DB の設定入れるとかしたい
  • AWS Elastic Beanstalk for Docker
    • Beanstalk では 1 container しか起動できなくてコンテナの意味あまりない
      • https://news.ycombinator.com/item?id=7636785
    • Amazon からすれば VM 内で大量にコンテナたてられたら売上増えない
      • 理屈はわからなくもない
  • 既存のデプロイフローが綺麗に確立できていると移行するメリットが見出しにくい
  • デプロイした新しいバージョンのアプリに切り替えるのどうするか
    • 古いコンテナからなんとかして同時に新しいコンテナへ向き先を切り替える?
      • 新しい VM たてて LB で向き先変える (Blue-Green Deployment) 方が楽そう

開発環境として

  • 開発環境の構築に使いたい件
    • Vagrant 1.6 で Docker provider がサポートされた
      • http://deeeet.com/writing/2014/05/08/vagrant-docker-provider/
    • Vagrant で VM 起動するよりは圧倒的に早い
  • テスト回すには良いがそれ以上に使い倒したい

IaaS? PaaS?

  • Docker の扱うレイヤーは結構下の方
    • 連携してよしなにしてくれるミドルウェアと組み合わせる必要ある
    • オンプレで IaaS/PaaS ライクなものを構築するには向いてるかも
  • Mesos みたいなクラスタマネージャと組み合わせると良いかも?
    • その後、神のお告げがあった

その他話題

  • Docker Inc.
    • dotCloud 華麗にピボット成功
    • Docker 単体で収益化するのは難しそう
      • どこかが買うかも?
  • 「◯◯来たか!」と思って調べてもみんなそれ言って盛り上がってるだけ
    • インターネットこわい
  • goroutine と Erlang の軽量プロセスの内部実装の話
  • レガシー環境は辛い
  • ステッカーの質感
  • CDN の話
  • S3 使うよりさくらでコンテンツ配信したほうが安上がり案について

雑感

  • Docker はみんなが期待してるような夢の何かではない気がした
  • オンプレか EC2 インスタンスを大量に確保していてそのリソースを最適化したい場合には使えるかもしれない
    • DockerはLXC同士を隔離するのが得意ではない
      • http://deeeet.com/writing/2014/05/01/what-is-docker/
  • Docker 一周したらカジュアルに使えるようになってると嬉しい

Vue.js を使ってみる

Vue.js

vuejs.org

MVVM パターンの ViewModel にフォーカスしたライブラリ。結構便利。

ViewModel とは

Wikipedia に書いてある通りで、

  • Model と View の間のデータバインディングしてくれる君。
  • Model の更新に合わせて View にも反映してくれる便利なレイヤー。

Viewを描画するための状態の保持と、Viewから受け取った入力を適切な形に変換してModelに伝達する役目を持つ。すなわちViewとModelの間の情報の伝達と、Viewのための状態保持のみを役割とする要素である。Viewとの通信はデータバインディング機構のような仕組みを通じて行うため、ViewModelの変更は開発者から見て自動的にViewに反映される。

http://ja.wikipedia.org/wiki/Model_View_ViewModel#ViewModel

サンプル

試しに使ってみたものがこちら (ColorPicker) HEX/RGB の値を変えると、それに対応した HEX/RGB へと値が追従する感じ。

Synopsis

<div id="color-value-container">
  <input v-model="hex" v-on="keyup: hexChanged">
  <div id="color-space">{{hex}}</div>
</div>

<script>
  var demo = new Vue({
    el: '#color-value-container',
    data: {
        hex: '#000000'
    }.
    methods: {
        hexChanged: function() {
            this.$el.querySelector('#color-space').style.backgroundColor = this.$data.hex;
        }
    }
  });
</script>

このように書いておくとだいたい次のようなことができる。

  • <input> に入力された値が div#color-space{{hex}} にバインドされる
    • これでモデルが更新されるたびに自動的に div のテキストも更新される
  • <input>keyup イベント発火時に methods 内に定義した hexChanged メソッドが呼ばれる
    • それに応じてほげほげできる

data に関するヒント

  • v-model="hex" は Vue に渡されたオブジェクトの data.hex に相当する
  • methods 内では this.$data.hex でアクセスできる
  • this.$data.hex の値を変更すると、変更された値が View に自動的に適用される

このあたりをおさえておけば基本的に迷うことなく、あとはドキュメントを見て何とかできるので今後何かに使ってみたい。


Internals::SvREADONLY() はオススメできない

Immutable な hashref, arrayref たまに欲しくなりますが Internals::SvREADONLY() はクセが強すぎてあまりオススメできないです。

たとえ次のコードの挙動を言い当てられたとしても、赤の他人(≒ 半年後の自分)が見た時に分かるかどうかは怪しいかと。

use strict;
use warnings;
use Data::Lock 'dlock';

my $foo = { bar => "baz" };
dlock($foo);

eval { my $b = $foo->{b} };

if ($@) {
    warn "oops: $@";
} else {
    warn "yay";
}

謎めいたランタイムエラー起こすよりも定数は全部サブルーチンとして記述してあった方がコンパイルに失敗してくれて嬉しい感じがします。

ちなみにConst::Commonというのを書いた | おそらくはそれさえも平凡な日々 を見て思ったんですが、似たような事やってくれるのは過去にかいてました( 定数を定義するのを楽にする Constant::Exporter を書いた - stfuawsc) 他にも Exporter::Constants とかもありますね。


Google Calendar API で日本の祝日を取得する

日本の祝日を機械的に取得したいケース、たまにありますね。

下準備

  • Developers Console で Project を作成する
  • Project > APIs & auth > APIs で Calendar API を ON にする
  • Project > APIs & auth > Credentials で Public API Access の API key を生成する

サンプル

use 5.18.0;
use URI;
use URI::Escape;
use LWP::UserAgent;
use JSON::XS;
use Encode;

my $api_key     = 'YOUR_API_KEY';
my $calendar_id = uri_escape 'ja.japanese#holiday@group.v.calendar.google.com';

my $uri = URI->new("https://www.googleapis.com/calendar/v3/calendars/$calendar_id/events");
$uri->query_form(
    orderBy      => 'startTime',
    singleEvents => 'true',
    key          => $api_key,
);

my $res = decode_json +LWP::UserAgent->new->get($uri)->decoded_content;

for my $item (@{$res->{items}}) {
    say encode_utf8 "$item->{start}{date}\t$item->{summary}";
}

__END__
2013-01-01      元日
2013-01-02      銀行休業日
2013-01-03      銀行休業日
2013-01-14      成人の日
2013-02-11      建国記念の日
2013-03-20      春分の日
2013-04-29      昭和の日
2013-05-03      憲法記念日
2013-05-04      みどりの日
2013-05-05      こどもの日
2013-05-06      こどもの日 振替休日
2013-07-15      海の日
2013-09-16      敬老の日
2013-09-23      秋分の日
2013-10-14      体育の日
2013-11-03      文化の日
2013-11-04      文化の日 振替休日
2013-11-23      勤労感謝の日
2013-12-23      天皇誕生日
2013-12-25      クリスマス
2013-12-31      大晦日
2014-01-01      元日
2014-01-02      銀行休業日
2014-01-03      銀行休業日
2014-01-13      成人の日
2014-02-11      建国記念の日
2014-03-21      春分の日
2014-04-29      昭和の日
2014-05-03      憲法記念日
2014-05-04      みどりの日
2014-05-05      こどもの日
2014-05-06      みどりの日 振替休日
2014-07-21      海の日
2014-09-15      敬老の日
2014-09-23      秋分の日
2014-10-13      体育の日
2014-11-03      文化の日
2014-11-23      勤労感謝の日
2014-11-24      勤労感謝の日 振替休日
2014-12-23      天皇誕生日
2014-12-25      クリスマス
2014-12-31      大晦日
2015-01-01      元日
2015-01-02      銀行休業日
2015-01-03      銀行休業日
2015-01-12      成人の日
2015-02-11      建国記念の日
2015-03-21      春分の日
2015-04-29      昭和の日
2015-05-03      憲法記念日
2015-05-04      みどりの日
2015-05-05      こどもの日
2015-05-06      憲法記念日 振替休日
2015-07-20      海の日
2015-09-21      敬老の日
2015-09-22      国民の休日
2015-09-23      秋分の日
2015-10-12      体育の日
2015-11-03      文化の日
2015-11-23      勤労感謝の日
2015-12-23      天皇誕生日
2015-12-25      クリスマス

補足

  • 日本の祝日カレンダーの calendarIdja.japanese#holiday@group.v.calendar.google.com
  • Events: list のリファレンスページから試しにAPIを叩ける
  • なぜかクリスマスが日本の祝日に入ってる。危ない

vim で特定のオフセットに移動するには :go [count]

'"' expected, at character offset 35 (before "}\n") at .... などと言われて壊れたJSONをなおす必要があるときに便利。

この場合は :go 35 とすれば修正すべき箇所に移動できる。はやく JSON5 普及しないものか…。


順序付きリストを効率的にデータベースに保存する

TODOリスト・本棚・プレイリスト

ユーザが好きな順番に並び替えられるTODOリスト・本棚・プレイリストのような、 特定の順番で表示するリストといったものの並び順をデータベースに保存したいケースがある。

これを愚直に実装してしまうと、リストが長くなればなるほど UPDATE すべき行が増えてしまうため、 サーバサイドで実装する場合にはパフォーマンス等も鑑みてひと工夫入れたい。

更新行を減らし、かつ使い勝手が悪くならないような実装方法をメモしておく。

概要

ここでは例としてTODOリストの並び順を保存してみる。 1ユーザにつき1つの順序付きTODOリストを管理できるものとして、テーブルを次のようにする。

キモの部分は SELECT 時に rank のみではなく、 updated_at でもソートすること。 rank の値が同一でも更新日時の新しいものが勝つようにすれば対象要素以外の行の更新は不要になる。 ちなみに、ソートはSQL内でやらなくても良い。

Schema

CREATE TABLE `todos` (
    `todo_id` int(10) unsigned NOT NULL,
    `content` varchar(32) NOT NULL,
    PRIMARY KEY (`todo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `todo_orders` (
    `user_id` int(10) unsigned NOT NULL,
    `todo_id` int(10) unsigned NOT NULL,
    `rank` int(10) unsigned NOT NULL,
    `created_at` int(10) unsigned NOT NULL,
    `updated_at` int(10) unsigned NOT NULL,
    PRIMARY KEY (`user_id`,`todo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

要素の追加と順序の入れ替え

ある要素を n 番目にする場合、 rank = n となるように UPSERT する。

INSERT INTO
    todo_orders (user_id, todo_id, rank, created_at, updated_at)
VALUES
    (1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())
ON DUPLICATE KEY UPDATE
    rank       = VALUES(rank),
    updated_at = VALUES(updated_at);

順序付きでリストを取得する

rank を ASC で、 updated_at を DESC でソートすることによって、 rank の値が同一だった場合でも更新タイミングの新しいものが優先されるので期待した順序で取得できる。

SELECT
    t.todo_id,
    t.content,
    tos.rank,
    tos.updated_at
FROM
    todo_orders tos
INNER JOIN
    todos t
ON
    t.todo_id = tos.todo_id
WHERE
    user_id = 1
ORDER BY
    rank ASC,
    updated_at DESC;

検証

初期のデータセットを元に順序を更新していく。

> SELECT * FROM todos;
+---------+-----------------------------+
| todo_id | content                     |
+---------+-----------------------------+
|       1 | Buy anniversary present     |
|       2 | Send invoices               |
|       3 | Finish homework             |
|       4 | Finalize hotel reservations |
+---------+-----------------------------+
4 rows in set (0.00 sec)

> SELECT * FROM todo_orders;
+---------+---------+------+------------+------------+
| user_id | todo_id | rank | created_at | updated_at |
+---------+---------+------+------------+------------+
|       1 |       1 |    1 | 1393300000 | 1393300000 |
|       1 |       2 |    2 | 1393300000 | 1393300000 |
|       1 |       3 |    3 | 1393300000 | 1393300000 |
|       1 |       4 |    4 | 1393300000 | 1393300000 |
+---------+---------+------+------------+------------+

> SELECT t.todo_id, t.content, tos.rank, tos.updated_at FROM todo_orders tos INNER JOIN todos t ON t.todo_id = tos.todo_id WHERE user_id = 1 ORDER BY rank ASC, updated_at DESC;
+---------+-----------------------------+------+------------+
| todo_id | content                     | rank | updated_at |
+---------+-----------------------------+------+------------+
|       1 | Buy anniversary present     |    1 | 1393300000 |
|       2 | Send invoices               |    2 | 1393300000 |
|       3 | Finish homework             |    3 | 1393300000 |
|       4 | Finalize hotel reservations |    4 | 1393300000 |
+---------+-----------------------------+------+------------+
4 rows in set (0.00 sec)

Finish homework を2番めにした後、Finalize hotel reservations を2番めにすると、

  • Buy anniversary present
  • Finalize hotel reservations
  • Finish homework
  • Send invoices

のような順番になる。

> INSERT INTO todo_orders (user_id, todo_id, rank, created_at, updated_at) VALUES (1, 3, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE rank = VALUES(rank), updated_at = VALUES(updated_at);
Query OK, 2 rows affected (0.55 sec)

> SELECT t.todo_id, t.content, tos.rank, tos.updated_at FROM todo_orders tos INNER JOIN todos t ON t.todo_id = tos.todo_id WHERE user_id = 1 ORDER BY rank ASC, updated_at DESC;
+---------+-----------------------------+------+------------+
| todo_id | content                     | rank | updated_at |
+---------+-----------------------------+------+------------+
|       1 | Buy anniversary present     |    1 | 1393300000 |
|       3 | Finish homework             |    2 | 1393300111 |
|       2 | Send invoices               |    2 | 1393300000 |
|       4 | Finalize hotel reservations |    4 | 1393300000 |
+---------+-----------------------------+------+------------+
4 rows in set (0.02 sec)

> INSERT INTO todo_orders (user_id, todo_id, rank, created_at, updated_at) VALUES (1, 4, 2, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE rank = VALUES(rank), updated_at = VALUES(updated_at);
Query OK, 2 rows affected (0.01 sec)

> SELECT t.todo_id, t.content, tos.rank, tos.updated_at FROM todo_orders tos INNER JOIN todos t ON t.todo_id = tos.todo_id WHERE user_id = 1 ORDER BY rank ASC, updated_at DESC;
+---------+-----------------------------+------+------------+
| todo_id | content                     | rank | updated_at |
+---------+-----------------------------+------+------------+
|       1 | Buy anniversary present     |    1 | 1393300000 |
|       4 | Finalize hotel reservations |    2 | 1393300222 |
|       3 | Finish homework             |    2 | 1393300111 |
|       2 | Send invoices               |    2 | 1393300000 |
+---------+-----------------------------+------+------------+
4 rows in set (0.00 sec)

1 »