update フィルター詳しく

バージョン0.10.0からドキュメントの部分更新ができるようになったようです。また、リビジョンを指定しなくても更新できるようになり、リラックス度が飛躍的に向上しています。

http://d.hatena.ne.jp/z-ohnami/20091110/1257863136

ということで、私も試しています。ひとまず、「これ使うとGET→INCREMENT→UPDATE みたいなことできるの?」という点は突っ込まれると思っています。

update フィルターの並列実行

// test/updates/read_and_write.js 
function(doc, req){
   doc.count = doc.count || 0;
   doc.count +=1;
   return [doc, "Now: " + doc.count + "\n"];
}

ひとまず、ab で -n 100 -c 1 とします。100リクエストで、同時実行数1。PUTリクエストするには -u ファイル名 だそうです。今回はリクエストボディはどうでもいいので適当にhogeという空のファイルを指定しています。そして、あらかじめ、test_read_and_write という_idのドキュメントをFutonで作っておいて、abします。

$ URL=http://127.0.0.1:5984/mydb/_design/test/_update/read_and_write/test_read_and_write
$ ab -n 100 -c 1 -u hoge "$URL"
(snip)
Benchmarking 127.0.0.1 (be patient).....done
(snip)

で、curl で確認します。

$ curl -X GET http://localhost:5984/mydb/test_read_and_write
{"_id":"test_read_and_write","_rev":"326-435041d4fc6cc97e4e895b5cfdb76b87","count":100}

いいですね。100リクエストをシリアルに飛ばしたんですから、100になるはずです。

では次に、10並列で飛ばします。countの値が200になればいいのですが。。。

$ URL=http://127.0.0.1:5984/mydb/_design/test/_update/read_and_write/test_read_and_write
$ ab -n 100 -c 10 -u hoge "$URL"
(snip)
Benchmarking 127.0.0.1 (be patient).....done
(snip)
Failed requests:        88
   (Connect: 0, Receive: 0, Length: 88, Exceptions: 0)
(snip)

一応終わりはしました。が、、88個エラーになっちゃいました。実際に確認すると、

$ curl -X GET http://localhost:5984/mydb/test_read_and_write
{"_id":"test_read_and_write","_rev":"338-5e5198cf73a875736388643bc5cc6519","count":112}

ということで12個の成功の分だけしかカウントされていません。?_conflicts=true をしても、特に衝突は見つかりませんでした。純粋にHTTPレベルでエラーになってコミットしないんですね。

じゃ、どんなエラーになるの?

困ったことにJavaScriptは、sleep相当の組み込み関数がないので、とりあえずビジーウェイトさせて重い処理にしてしまいます。あまり重すぎると OS Process timeout になるので、注意。適当だけれど以下をフィルタに登録。

// test/updates/read_and_write_heavy.js 
function(doc, req){
   doc.count = doc.count || 0;
   doc.count +=1;
   for(var i=0; i<50000000; i++){
     // do nothing;
   }
   return [doc, "Now: " + doc.count];
}

これで、うちのiMac(3.06GHz 2core / 4GB RAM)だと、1リクエストに3,4秒かかるようになりました。で、適当に2つのターミナルで、

$ curl -v -X PUT http://127.0.0.1:5984/mydb/_design/test/_update/read_and_write_heavy/test_read_and_write

を実行します。すると片方は、

$ curl -v -X PUT $URL
* About to connect() to 127.0.0.1 port 5984 (#0)
*   Trying 127.0.0.1... connected
* Connected to 127.0.0.1 (127.0.0.1) port 5984 (#0)
> PUT /mydb/_design/test/_update/read_and_write_heavy/test_read_and_write HTTP/1.1
> User-Agent: curl/7.19.7 (i386-apple-darwin10.2.0) libcurl/7.19.7 zlib/1.2.3
> Host: 127.0.0.1:5984
> Accept: */*
> 
< HTTP/1.1 409 Conflict
< Server: CouchDB/0.10.0 (Erlang OTP/R13B)
< Date: Sat, 12 Dec 2009 11:25:39 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 58
< Cache-Control: must-revalidate
< 
{"error":"conflict","reason":"Document update conflict."}
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

こんな感じで返してくるでしょう。ということで、_rev指定で不一致だったときと同じように、409 Conflict になって、いつものJSONのエラーメッセージが帰ってきますよ。

で、GET→INCREMENT→UPDATE みたいなことできるの?

yesといえばyesでしょうか。でも、SELECT FOR UPDATE のようにドキュメントにロックをかけるようなやりかたはない、という点は注意です。失敗することを前提にロジックを組む必要があります。クライアントでリトライを何回か繰り返して、それでもだめなら、今混んでるからあとでやってね、とユーザーに通知するとか。

あるいは、要するにOptimistic Lockだと思って、

// app/updates/lock.js
function(doc, req){
   if( doc.locked ){
      // エラーを返す(省略)
   }else{
      doc.locked = true;
      return [doc, toJSON({ok : "true"})];
   }
}

とか、そんな感じで、アプリケーションレベルでロック制御をするのでもいいかもしれません。まぁこれは、ドキュメントがどういう用途で、トランザクションをどう設計するか、でしょうね。