KeyedMutex::Memcached ってモジュールをリリースして何も言ってなかった件
id:kazuhooku さんがかつて作った KeyedMutex を memcached でやってしまおうと言うのがこのモジュールの目的です。
KeyedMutex の場合は、keyedmutexd と言うそれ専用の daemon を立ち上げなければいけませんが、このモジュールの場合は既存のシステムに memcached があれば普通に使えます。
trial を 0 にすると timeout で lock が expire するか、誰かが lock を開放するまで延々と lock の獲得が出来るまでビジーループします。一方で trial を有限の正整数とすると、その試行回数で lock が獲得出来ない場合は即座に諦めます。
正規の使い方としては lock を獲得出来るのが同時に1クライアントのみで他はそれが開放されるまで待たせると言うのが正解ですが、ちょっと変わった使い方としては、例えば特定のユーザーがオンラインかどうかみたいな処理を、次のように
my $cache = Cache::Memcached::Fast->new( ... ); my $km = KeyedMutex::Memcached->new( cache => $cache, trial => 1, timeout => 5 * 60 ); my $key = "online:" . $user_id; if ( $km->lock( $key, 0 ) ) { update_online( $user_id ); }
なんてやっておくと、5分間は update_online() と言う処理がこのユーザーに対しては走らなくなります。
なんて感じでした。これも CPAN からご利用頂ける感じですよ。
余談ですが、Semaphore も考えてその際は incr/decr で管理しようと思ってたんですが、何らかの値が入ってないと incr/decr は出来ないので一度は諦めたのですが、kazeburo さんの Cache::Isolator の実装を見てたら、なるほど Cache::Memcached::Fast の add メソッドで指定個数の key をインクリメントしてく感じ*1で作って順番*2にロックの獲得を試みるようなインターフェースにすれば Semaphore も出来るなぁなんて思いました。
See Also
Test::Mock::Guard Released
さっき nekokak さんと xaicron さんにそそのかされて Test::Mock::Guard ってモジュールを書いてみました。
そもそも Perl には Test::MockObject と言う汎用の Mock モジュールがあるんですけど、あれこれ余計な機能がたくさんついてたり Mock 化すると多分元に戻せないと言うのがあってもっとシンプルな奴がほしいなと思って作ってみた次第です。SYSNOPSIS のコピペですけど、
use Test::More; use Test::Mock::Guard qw(mock_guard); package Some::Class; sub new { bless {} => shift } sub foo { "foo" } sub bar { 1; } package main; { ### このスコープでは Mock 化されてる my $guard = mock_guard( 'Some::Class', +{ foo => sub { "bar" }, bar => 10 } ); my $obj = Some::Class->new; is( $obj->foo, "bar" ); is( $obj->bar, 10 ); } ### ブロックを抜けると元に戻る my $obj = Some::Class->new; is( $obj->foo, "foo" ); is( $obj->bar, 1 ); done_testing;
って感じに書く事が出来ます。下位モジュールがよしなにテストされているとして、それを呼び出すモジュールのテストなんかを書く場合、意図的に下位モジュールの挙動をコントロールしながら書きたいケースがあると思います。
例えば、
package AKB48::Fun; sub buy_dvd { ### 何か色々やる } package Hidek; sub purge_stress { AKD48::Fun->buy_dvd; }
的な感じになっていて、AKB48::Fun モジュールは十分テストされているとすれば、
subtest "buy dvd is success" => sub { my $mock_guard = mock_guard( 'AKB48::Fun', +{ buy_dvd => sub { return +{ title => "【特典生写真付き】ここにいたこと(初回限定盤)", price => 3500 }, } } ); ### テスト書く }; subtest "buy dvd is failed" => sub { my $mock_guard = mock_guard( 'AKB48::Fun', +{ buy_dvd => sub { croak "現金が足りません!カードで支払って下さい"; }, } ); ### テスト書く };
みたいな感じの事が出来ますよと。
使いどころを適切に選べば Mock を使ったテストはとても有効だと思うのでぜひお試し下さいませ。かしこ。
IV, NV などを B モジュールで調べる
makamaka さんが YAPC Asia 2010 にて発表した XSからPPへ - YAPC::Asia Tokyo 2010 に既に書いてあるんだけど、備忘録を兼ねて。
JSON にする際に違いが出てくる訳ですがまずは論より証拠。
$ perl -MJSON -e 'warn encode_json(+{ foo => 1, bar => "1", baz => 0 + "1" })' {"bar":"1","baz":1,"foo":1} at -e line 1.
これらは IV なのか PV なのかばっちり判定してくれる訳ですね。API なんかのユースケースだとこの辺りを厳密にやらねばならないので、この辺りの制御はちゃんとやらないといかんのです。
その前に Devel::Peek で中を見てみると、
$ perl -MDevel::Peek -e 'my $i = "1"; Dump($i);' SV = PV(0x10041040) at 0x10043be0 REFCNT = 1 FLAGS = (PADMY,POK,pPOK) PV = 0x10042020 "1"\0 CUR = 1 LEN = 4 $ perl -MDevel::Peek -e 'my $i = 1; Dump($i);' SV = IV(0x10043be0) at 0x10043be0 REFCNT = 1 FLAGS = (PADMY,IOK,pIOK) IV = 1 $ perl -MDevel::Peek -e 'my $i = "1"; $i = 0 + $i; Dump($i);' SV = PVIV(0x10062010) at 0x10043be0 REFCNT = 1 FLAGS = (PADMY,IOK,pIOK) IV = 1 PV = 0x10042020 "1"\0 CUR = 1 LEN = 4
となり、一度 PV として初期化された変数も IV に変換すると PVIV になると。*1
B モジュールで B::SVp_IOK とか使えるってどこにも書いてない気がするんだけど気のせいだろうか。とりあえず sv.h で定義されている定数のうちその辺りは使えるみたいです。
B モジュールの svref_2object() によって B::SV オブジェクトにしつつ、そのオブジェクトメソッドである FLAGS を用いればどういう flag が立ってるか分かります。
即ちこんな感じでチェック出来ますよと。
#!/usr/bin/perl use strict; use warnings; use B; use Test::More; sub iv_ok { my $sv = shift; return ( B::svref_2object( \$sv )->FLAGS & B::SVp_IOK ) ? 1 : 0; } sub nv_ok { my $sv = shift; return ( B::svref_2object( \$sv )->FLAGS & B::SVp_NOK ) ? 1 : 0; } subtest 'IV' => sub { my $sv1 = 1; is( iv_ok($sv1), 1, 'IV' ); is( nv_ok($sv1), 0, 'not NV' ); my $sv2 = "1"; is( iv_ok($sv2), 0, 'not IV' ); is( nv_ok($sv2), 0, 'not NV' ); $sv2 = 0 + $sv2; is( iv_ok($sv2), 1, 'IV' ); is( nv_ok($sv2), 0, 'not NV' ); done_testing; }; subtest 'NV' => sub { my $sv1 = 1.0; is( iv_ok($sv1), 0, 'not IV' ); is( nv_ok($sv1), 1, 'NV' ); my $sv2 = "1.0"; is( iv_ok($sv2), 0, 'not IV' ); is( nv_ok($sv2), 0, 'not NV' ); $sv2 = 0 + $sv2; is( iv_ok($sv2), 0, 'not IV' ); is( nv_ok($sv2), 1, 'NV' ); done_testing; }; done_testing;
*1:多分この辺りは lestrrat さんの本の8章辺りにきっと書いてあるはず
JSON-RPC, RESTful API とクエリパラメータ
OpenSocial の JSON-RPC, RESTful API の設計についてのよもやま話です。
JSON-RPC とクエリパラメータ
OpenSocial Core API Server Specification 1.1 に URL Addression と言うセクションがあります。
これは JSON-RPC を http GET で呼び出す際に params の部分など構造化されたデータをどうやって渡すのって際の仕様になります。
JSON Object | URL Parameter | ||
{ "field" : "value" } | field=value | ||
{ "field" : [1,2,3,4,5]} | field=1,2,3,4,5 | ||
{ "field" : "12" } | field='12' | ||
{ "field" : [identifier,anotheridentifier]} | field=identifier,anotheridentifier | ||
{ "field" : ["value","another value"]} | field=value,"another value" | ||
{ "field" : ['value','another value']} | field=value,'another value' | ||
{ "field" : { "nested" : "value" }} | field.nested=value | ||
{ "field" : [{ "nested1" : "value1" }, { "nested2" : "value2" }]} | field(0).nested1=value1&field(1).nested2=value2 |
まぁシングルクオートとダブルクオートを意図的に使い分けたいユースケースがさっぱり意味が分からない上に、identifier とか意味分かんないんでこの辺り謎過ぎるのですが、そういう細かい突っ込みはとりあえず置いておきます。
ところで JSON-RPC 2.0 の extension として定義されている JSON-RPC over HTTP によれば、GET の時の挙動は3.5 GET に書いてあり、params の部分はエンコードしろと書いてあります。どういう風にエンコードするかと言えば Base64 して URL Encode しろと言う感じです。
Pre-Encoded Params: http://<end point>?method=sum¶ms={"a":3,"b":4}&id=2 http://<end point>?method=sum¶ms=[3,4]&id=1 Encoded Request: http://<end point>?method=sum¶ms=eyJhIjozLCJiIjo0fQ%3D%3D&id=2 http://<end point>?method=sum¶ms=WzMsNF0%3D&id=1
って感じ。これはこれで単純明快ですね。
ちなみに先に JSON-RPC に対して結論を言っておくと、誰が楽しくて GET で叩くのか僕には意味が分かりません!*1大人しく POST 使いましょう。
RESTful API とクエリパラメータ
それでもやっぱり RESTful API が好き!と言う方も多いでしょう。僕もそうです。何度痛い目に合っても何故か好き。
まぁそんなことはどうでも良いのですが、OpenSocial RESTful API は中々面白い機能が幾つかあって、
- Partial Update
- Filter & Sort
辺りが挙げられます。Partial Update は冷静に考えると利便性の観点から出来て然るべきなんですが、ここでは触れない事にします。
Filter & Sort の辺りは Open Search 辺りからやってきた概念でしょう。
Filter や Sort なんですけど、例えばこんな風に書きます。
http://example.com/people/@me/@friends?filterBy=displayName&filterOp=startsWith&filterValue=zigo&sortBy=id&sortOrder=descending&count=50&startIndex=1
まぁ特に説明しなくても理解出来るとは思いますけど。
現時点の RESTful API の仕様では filter をもっと複雑に記述するなんて事は出来ません。と言うのも AND なのか OR なのかを指定する仕組みが無いからだと思うのですが、まぁ OR は要らないすよね。
そこで冒頭の JSON-RPC の URL Addressing から Syntax を借りてくると、こんな風に書けるかもしれません。
http://example.com/people/@me/@friends?filterBy(0)=displayName&filterOp(0)=startsWith&filterValue(0)=zigo,kaz,hid&filterBy(1)=gender&filterOp(1)=equals&filterValue(1)=male
この部分をデータ構造にすると、
{ "filterBy": ["displayName", "gender"], "filterOp": ["startsWith", "equals"], "filterValue": [ ["zigo", "kaz", "hid"], "male" ] }
みたいな感じとなり、SQL で表現すれば、
WHERE ( displayName LIKE 'zigo%' OR displayName LIKE 'kaz%' OR displayName LIKE 'hid%' ) AND gender = 'male';
のようになると。
実は某プラットフォームの API v2 ではこの記法が使えるらしいのですが、とりあえず undocument & no supports です。
雑感
とりあえず JSON-RPC に関して仮に GET をサポートするのであれば本家の方の仕様( Base64 )の方がすっきりしていて良いかなと思います。あと、一般論としてクエリパラメータとして何か構造化されたデータを埋め込む場合も、これと同様の手法の方が便利だろうなと思います。
RESTful API と filter の議論ですけど、こういうのもありかなとは思います。
*1:監視用途とかなら意味があるかもしれない
カラム数の多いテーブルの SELECT 用にカラムを明示的に列挙する、または GROUP_CONCAT() が便利と言う話
カラム数が結構多いテーブルに対して SELECT * とは書きたく無くて明示的にカラム名を指定したいって時に徒手空拳で頑張って書いても良いんですけど、information_schema 使えば楽出来るよねーって話。
CREATE DATABASE akb48; USE akb48; CREATE TABLE members ( id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(32) NOT NULL, nickname varchar(32) NOT NULL, birthday datetime NOT NULL, created_on datetime NOT NULL, updated_on datetime NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
まぁこんなテーブルがあるとして、このテーブルのカラムを SELECT 句の為に列挙したい場合は、
SELECT GROUP_CONCAT(COLUMN_NAME SEPARATOR ', ') AS columns FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'akb48' AND TABLE_NAME = 'members' ORDER BY ORDINAL_POSITION ASC
とかやると、
*************************** 1. row *************************** columns: id, name, nickname, birthday, created_on, updated_on 1 row in set (0.00 sec)
こんな具合にカラムが列挙出来ると。
結論として information_schema と言うより GROUP_CONCAT() が便利だねって話です。IN() の中身を抽出する際にも便利です!
id:nippondanji さんのブコメでありがたいご指摘があるんですが、group_concat_max_len を超えた数のレコードを GROUP_CONCAT() するとちょん切れちゃうので注意です。
生 DBI ユーザーのための DBI Cookbook (6)
さて、今日は selectcol_arrayref です。昨日、会社のグルメな同僚に教えて貰いました。
ちょうど 生 DBI ユーザーのための DBI Cookbook (1) - Yet Another Hackadelic にて selectall_arrayref + Slice, selectall_hashref などの使い方を書きましたが、こちらもかなり便利。
CREATE TABLE `application` ( `id` int(10) unsigned NOT NULL, `title` varchar(32) CHARACTER SET sjis NOT NULL, `created_on` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', `updated_on` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO application(id, title, created_on, updated_on) VALUES(1, 'AKB48', NOW(), NOW()), (2, 'tomochin dokidoki panic', NOW(), NOW()), (3, 'Tactics Ogre', NOW(), NOW()), (4, 'hidek kanreki', NOW(), NOW());
みたいな感じで適当にレコードを入れておきます。
selectcol_arrayref で id 一覧を取得
まずはサンプルを。
#!/usr/bin/perl use strict; use warnings; use DBI; use Data::Dump qw(dump); my $dbh = DBI->connect('dbi:mysql:dbname=test', 'root', '', +{ RaiseError => 1 }); my $app_ids = $dbh->selectcol_arrayref( 'SELECT id FROM application' ); warn dump($app_ids);
これは次のようになります。
[1, 2, 3, 4]
指定したカラムの配列リファレンスとして取得出来るのが selectcol_arrayref なんですな。便利です。
Columns attribute でさらに自在にデータ整形
Columns と言う attribute を指定すると、指定したインデックス(1から始まります)のカラムを平坦にした配列リファレンスを取得する事が出来ます。
#!/usr/bin/perl use strict; use warnings; use DBI; use Data::Dump qw(dump); my $dbh = DBI->connect('dbi:mysql:dbname=test', 'root', '', +{ RaiseError => 1 }); my %app_title_map = @{$dbh->selectcol_arrayref( 'SELECT id, title FROM application', +{ Columns => [ 1, 2 ] } )}; warn dump(\%app_title_map);
これは、
{ 1 => "AKB48", 2 => "tomochin dokidoki panic", 3 => "Tactics Ogre", 4 => "hidek kanreki", }
のようになります。つまり、selectcol_arrayref($stmt, +{ Columns => [1, 2] }); は、
[ "1レコード目のid", "1レコード目のtitle", "2レコード目のid", "2レコード目のtitle", "3レコード目のid", "3レコード目のtitle", "4レコード目のid", "4レコード目のtitle", "5レコード目のid", "5レコード目のtitle" ]
みたいに取得出来る訳ですね。
ここで出したサンプルはドキュメントに思いっきり書いてありますw ドキュメント嫁って話ですな。