継続でget_multiとかまとめたい。

memcachedのgetsとかJSONRPCのbulkリクエストとかを出来る限り透過的にまとめたい。
Apps::MemcachedはCache::Memcached::Fastのラッパーをイメージしてくだしあ。

Scope::Session::start {
    my $promised = Apps::Memcached::Promise->new;

    for my $c (1..5){
        $promised->reserve(sub{
            my $rand = 5;
            for( 1..$rand) {
                my $result = Apps::Memcached->new->get("hello:$c:$_");
                ::ok "$c-$_ value";
            }
        });
    }
    $promised->join;
};

こんなかんじで、getしか書いてないんだけど、その時点で
ステートを停止して、一通り検査し終えたところでgets!して、
復元して最後まで回すみたいなやつやりたい。

試しに書いてみた:

package Apps::Memcached::Promise;
use strict;
use warnings;
use Apps::Memcached;

use Coro;
use Data::Util;

sub new {
    return bless {},shift;
}

sub reserve {
    my ($self,$logic) = @_;
    $self->{_reserved} ||= [];
    push @{ $self->{_reserved} },$logic;
}



sub join {
    my ($self) = @_;
    no warnings qw/redefine/;
    my @keys;
    local *Apps::Memcached::get = Data::Util::modify_subroutine(
        Apps::Memcached->can('get'),
        around => [
            sub {
                my ( $origin, $self, $key ) = @_;
                push @keys, $key;
                cede;
                return $origin->($self,$key);
            }
        ]
    );
    my @reserved = @{ $self->{_reserved} };
    my $wait = scalar @reserved;
    my @threads = map {
        my $logic = $_;
        async {
            cede;
            $logic->();
            $wait--;
        };
    } @{ $self->{_reserved} };
    while( $wait){
        cede;
        if( @keys){
            Apps::Memcached->new->get_multi(@keys);
            warn "get_multi keys:". join ',',@keys;
        }
        @keys = ();
    }
    return ;

}
1;
get_multi keys:hello:1:1,hello:2:1,hello:3:1,hello:4:1,hello:5:1 
ok 1
ok 2
ok 3
ok 4
ok 5
get_multi keys:hello:1:2,hello:2:2,hello:3:2,hello:4:2,hello:5:2 
ok 6
ok 7
ok 8
ok 9
ok 10
get_multi keys:hello:1:3,hello:2:3,hello:3:3,hello:4:3,hello:5:3 
ok 11
ok 12
ok 13
ok 14
ok 15
get_multi keys:hello:1:4,hello:2:4,hello:3:4,hello:4:4,hello:5:4 
ok 16
ok 17
ok 18
ok 19
ok 20
get_multi keys:hello:1:5,hello:2:5,hello:3:5,hello:4:5,hello:5:5 
ok 21
ok 22
ok 23
ok 24
ok 25
1..25

Sessionの生成・破棄を任意のタイミングで制御可能にするScope::Session

@kazeburo先生から解説記事の希望とあれば、まじめにブログ書かないと@kzysにDISられようとも気にも留めない僕ですら、記事を書きます。

前段のところを丁寧にまとめると、こりゃ厄介だなと思っていたところ@kazeburo先生の記事が出てきたので、引用しながらScope::Sessionなる昔書いたCPANモジュールの話をします。

mod_perl のアプリケーションでは、Apacheモジュールの提供するpnotesを使うとリクエスト毎のデータを簡単に持つことができます。pnotesに入れたデータはリクエストの処理が終了したところで自動的にクリーンアップされます。これを利用したのがリクエストごとにインスタンスを作成破棄できる、Apache::Singleton(::Request)です。


通常Singletonというと「常にインスタンスが一意」という性質をもったクラスなんですが、cgi時代ならいざ知らず、Webアプリケーションのプロセスの寿命は長いので、適当なところ(リクエスト単位)で一度リソースを手放してくれるとうれしいことが多いです。


とくに外部リソースに問い合わせたりするような処理を最適化していこうとおもったら、多少のコヒーランシーを無視しても、キャッシュできたほうがうれしかったりします。

また、pnotesはデータベースの接続の管理にもしばしば使われます。1リクエストを裁いている間だけデータベースとの接続を維持し、リクエストが完了したところで接続を閉じるような処理に利用されています。このようにすることでmod_perlのプロセス数分(数百)の接続がMySQLに常に張られることもなく、また1度のリクエストの中で何度も接続を行うコストを押さえる事ができます。


このようにDBConnectionというリソースに関しても同様で、リクエスト単位の寿命というやつが適切だったりします。


ところで、
DBコネクション管理やSingleton/Flyweightといったものが必要なモジュールとはアプリケーションにとって、どういった部分でしょうか?


MVCで言えば、間違いなくM。Model系モジュールになります。


こういったものが、ApacheというWebServerやPlack/PSGICatalystやその他WAFに依存した実装になっていると、WebServerプロダクトの変更、WAFの刷新といった楽しげなことに禍根を残すこと請け合いです。


また、まともなWebアプリケーションにはWebサーバへのリクエストを捌く機能の他にも
次のようなサブシステムが存在しているはずです。

  • JobQueue Worker用framework
  • 内部RPCサーバ用framework
  • cronのbatch用framework
  • オペレーション用script


こういったサブシステム全般で、このぐらいの粒度で寿命を持ってほしいという理想があると思います。
たとえば、JobQueueなら一つのジョブを処理し終えるまでとか、cronであれば、イテレーション対象のデータを一つ処理し終えるまで。といった風に。

# ダミーコード
# リクエストを取得して
while( my $req = $itr->() ){
# リクエストを処理
    handle_req( $req ); 
# handle_reqの間だけ、リソースの保持をしていてほしい。
}

そうなったときにScope::Sessionを使います。

use Scope::Session;
while( my $req = $itr->() ){
    Scope::Session::start {
        handle_req( $req );
    };
}

こんな感じに。これで、handle_req内でScope::Sessionのkey-valueを利用すると、それが維持されて抜けると破棄されます。

@kazeburoさんのDBコネクションのサンプルをそのまま置き換えると

sub connect {
    my $class = shift;
    my @dsn = @_;
    my $dbh;

    my $dsn_key = Data::MessagePack->pack(\@dsn);
-  my $connector = scope_container($dsn_key);
+  my $connector = Scope::Session->notes($dsn_key);
    eval {
        $dbh = $connector->_dbh;
    };
    return $dbh if $dbh;

    my $connector = DBIx::Connector->new(@dsn);
    $dbh = $connector->dbh;
-    scope_container($dsn_key, $connector);
+    Scope::Session->notes($dsn_key => $connector);
    return wantarray ? ( $dbh, $connector ) : $dbh;
}

動かしてないけど。

あと、Plack製品で利用したい場合はMiddlewareもcpanにあります。

Scope::Sessionには同梱で、Scope::Session::Singleton / Scope::Session::Flyweightがついてきます。
これは、Scope::Sessionをつかった場合によく使うであろうデザインパターンのクラスを簡単につくるためのユーティリティです。
Singletonはひとつのインスタンスしか作れないクラスをつくり、
Flyweightは任意の生成済みインスタンスを再利用可能なクラスをつくるのを手伝います。


Scope::SessionとScope::Containerの違いは、
Scope::Sessionはlocalでダイナミックスコープを利用していて、
Scope::ContainerはRAIIでDESTORYで削除。


どっちも普通に使うにはたいした違いはないと思いますが、実装を比べてみると楽しいです。

http://ja.wikipedia.org/wiki/動的スコープ
http://ja.wikipedia.org/wiki/RAII

ちなみにRAIIはref count方式や自動変数の破棄がある言語処理系を前提としているところが
あるので、CPythonとJythonだと同じモジュールが動かなかったりするよ:
http://d.hatena.ne.jp/hirokidaichi/20101017


enjoy!

メモ:シグナル

シグナルを処理するだけの簡単なサンプルをc/node.js/pythonで。

#include <unistd.h>
#include <string.h>

static void
func(){
    write(2,"SIGINT\n",7);
}

int 
main(void){
    struct sigaction act;
    //struct sigaction act2;
    memset(&act,0,sizeof act );
    act.sa_handler = func;
    if( sigaction(SIGINT ,&act,NULL) < 0){
        perror("syserror");
        return 1;
    }
    sleep(60);
    return 0;
}
import signal
import time

def func(signam,stack):
    print signam
    exit(1)


signal.signal(signal.SIGINT,func)


time.sleep(60)
process.on('SIGINT',function(){
    console.log("SIGINT");
    process.exit(1);
});

setTimeout(function(){
    process.exit(0);
},60*1000);

JythonとCPython

PythonにはJVMで動作するJythonとネイティブのCpythonが存在していて、
それぞれ異なるGCのメカニズムを採用している。

CPythonは、基本的に参照カウント方式で循環参照が発生した場合に限り
マーク&スイープらしい。

Jythonはというと、JVMに乗っかっているのだから世代別ガーベジコレクション(コピーGCとマークアンドスイープ)。

ということで、次のようなコードを動作確認。

class SomethingSpecial(object):
    def __init__(self):
        print("hello")
    def __del__(self):
        print("goodbye")

def func():
    print("start func")
    x = SomethingSpecial()

func()
print("endfunc")

CPythonだと、スコープからはずれるとデストラクタが呼ばれているのがわかる。

# python memory.py
start func
hello
goodbye
endfunc

一方、Jythonだとデストラクタは呼ばれていない。

# jython memory.py      
start func
hello
endfunc

CPythonでRAII(Resource Acquisition Is Initialization)ができるからといって、安易にそれを前提としたモジュールを作ってしまうと移植性がなくなってしまったりするうえ、気づきにくいバグを生みそうで怖い。

メモ:スマートポインタ

#include <iostream>
#include <memory>
#include <tr1/memory>
#include <string>
#include <tr1/tuple>
using namespace std;
using namespace std::tr1;

class Item {
private:
    string value_;
public:
    Item( const char* v="???") : value_(v) {
        cout << "create Item(" << value_ << ")" << endl;
    }
    ~Item() {
        cout << "delete Item(" << value_ << ")" << endl;
    }
    string value() const { return value_; }
};

typedef auto_ptr<Item>      auto_item;
typedef shared_ptr<Item>    shared_item;
typedef weak_ptr<Item>      weak_item;


void func( auto_item item) {
    cout << "item points to " << (item.get() ? item->value() : "(null)") << endl;
}
auto_item 
ret_func( auto_item item ){
    cout << "item points to " << (item.get() ? item->value() : "(null)") << endl;
    return item;
}

void test_auto_ptr(){
    cout << "# test auto_ptr" << endl;
    auto_item p1(new Item("One"));
    auto_item p2(new Item("Two"));
    // p1の所有権をfuncの引数に渡す
    func( p1 );
    cout << "end func" << endl;
    // 渡されて参照がなくなったので、削除。
    p1 = p2;
    // p2の所有権をp1に委譲。
    cout << "p1 points to " << (p1.get() ? p1->value() : "(null)") << endl;
    cout << "p2 points to " << (p2.get() ? p2->value() : "(null)") << endl;
    // p2の中身がなくなる
    // p1の所有権をret_funcの引数に渡して、再度p1に戻す、
    p1 = ret_func( p1 );
    cout << "end ret_func" << endl;
}
//ここでp1削除


void func2( shared_item item) {
    cout << "item points to " << (item.get() ? item->value() : "(null)") << endl;
}
shared_item 
ret_func2( shared_item item ){
    cout << "item points to " << (item.get() ? item->value() : "(null)") << endl;
    return item;
}

void test_shared_ptr(){
    cout << "# test shared_ptr" << endl;
    shared_item p1( new Item("one") );
    shared_item p2( new Item("two") );
    {
        shared_item p3 = p1;
        cout << "p3 points to " << (p3.get() ? p3->value() : "(null)") << endl;
    }
    p1 = p2;
    func2(p2);
    cout << "end func2" << endl;
    p2 = ret_func2(p2);
    cout << "end ret_func2" << endl;

}

int main(void){

    test_auto_ptr();
    test_shared_ptr();

    return 1;
}

メモ:node.jsの拡張

js

#!/usr/local/bin/node

require.paths.push("./build/default/");

var obj = require("hello");

console.log(obj);

cpp

#include <v8.h>

using namespace v8;
extern "C" {
    void init ( Handle<Object> target ){
        HandleScope scope;
        target->Set( String::New("hello") , String::New("world") );
    }
}

wscript

srcdir = '.'
blddir = 'build'
VERSION = '0.0.1'

def set_options(opt):
  opt.tool_options('compiler_cxx')

def configure(conf):
  conf.check_tool('compiler_cxx')
  conf.check_tool('node_addon')

def build(bld):
  obj = bld.new_task_gen('cxx', 'shlib', 'node_addon')
  obj.target = 'hello'
  obj.source = 'hello.cc'

CSSTransitionに薄皮

今のところwebkit限定。prefixを-mozに振り分ければfirefoxでも動くかもね。

(function(){
    var preventDefault = function(e){
        e.preventDefault();
    };
    this.__defineSetter__('enableTouchSlide',function(enableTouchSlide){
        this.__enableTouchSlide = enableTouchSlide ? true : false;
        if( this.__enableTouchSlide ){
            this.removeEventListener('touchmove',preventDefault);
        }else{
            this.addEventListener('touchmove',preventDefault);
        }
    });
    this.__defineGetter__('enableTouchSlide',function(){
        return (this.__enableTouchSlide !== false );
    });
}).apply(HTMLElement.prototype);

(function(){
    var objectKeys = (Object.keys) ? Object.keys :function(obj){
        var ret = [];
        for( var p in obj )
            if( obj.hasOwnProperty( p ) )
                ret.push(p);
        return ret;
    };
    var camelize = function(str){
        return str.replace(/-+(.)?/g,function(match,chr){
            return chr ? chr.toUpperCase():'';
        })
    };
    var getPrefixedObject = function(prefix,obj){
        var ret = {};
        objectKeys(obj).forEach(function(p){
            ret[prefix+p] = obj[p];
        });
        return ret;
    };
    var prefixed = function(prop){
        if( /^transition/.test(prop) )
            return 'webkit-' + prop;
        if( /^animation/.test(prop) )
            return 'webkit-' + prop;
        if( /^transform/.test(prop) )
            return 'webkit-' + prop;
        return prop;
    };
    this.setStyle = function(styleObject){
        var _self = this;
        objectKeys(styleObject).forEach(function(p){
            _self.style[camelize(prefixed(p))] = styleObject[p].toString();
        });
        return this;
    };
    this.getStyle = function(name){
        return this.style[camelize(prefixed(name))];
    };
}).apply(HTMLElement.prototype);

var $ = function(a){return document.getElementById(a)};



CSSRule.Transform = (function(){
    var _Transform = function(){
        this.expression = '';
    };
    (function(){
        this.scale = function(x,y){
            this.expression += 'scale('+[x||1,y||1].join(',')+') ';
            return this;
        };
        this.rotate = function(deg){
            this.rotate += 'rotate(' + deg + 'deg) ';
            return this;
        };
        this.translate = function(x,y){
            this.expression += 'translate(' + 
                [x ||'0',y||'0'].map(function(ex){return ex + 'px';}).join(',') +
            ') ';
            return this;
        };
        this.skew   = function(x,y){
            this.expression += 'skew(' + 
                [x ||'0',y||'0'].map(function(ex){return ex + 'deg';}).join(',') +
            ') ';
            return this;
        };
        this.toString = function(){
            return this.expression;
        };
    }).apply(_Transform.prototype);

    return {
        scale : function(){
            var instance = new _Transform;
            _Transform.prototype.scale.apply( instance ,arguments );
            return instance;
        },
        rotate : function(){
            var instance = new _Transform;
            _Transform.prototype.rotate.apply( instance ,arguments );
            return instance;
        },
        translate : function(){
            var instance = new _Transform;
            _Transform.prototype.translate.apply( instance ,arguments );
            return instance;
        },
        skew : function(){
            var instance = new _Transform;
            _Transform.prototype.skew.apply( instance ,arguments );
            return instance;
        }
    };
})();
Element.Transition = (function(){
    var parseTime = function(str){
        return parseFloat(str||"0") * (( /ms$/.test(str) ) ? 1: 1000);
    };

    var _TransitionElement = function(element,transitions){
        var options   = transitions || {};
        this.element  = element;
        this._toStyle = {};
        this._afterStyle = {};
        this.element.setStyle({
            'transition-duration' : ( options.duration || 600 ) + 'ms',
            'transition-property' : ( options.property || 'all'),
            'transition-timing-function' : 
                ( typeof options['timing-function'] === 'string' ) ? options['timing-function'] :
                ( typeof options['timing-function'] === 'undefined') ? 'ease' :
                'cubic-bezier(' + options['timing-function'].join(',') + ')'
        });
        var _self = this;
        setTimeout(function(){ 
            _self.element.setStyle(_self._toStyle || {});
        },10);
        var durationTime = parseTime(_self.element.getStyle('transition-duration')) + 
            parseTime(_self.element.getStyle('transition-delay'));

        setTimeout(function(){
            _self
                .element
                .setStyle( { 'transition-duration' : '0s' ,'transition-property' : 'none'})
                .setStyle( _self._afterStyle || {} );
            ( _self._callback || function(){}).apply(_self);
        }, durationTime );
    };
    (function(){
        this.from = function(styleObject){
            this.element
                .setStyle(styleObject||{})
            return this;
        };
        this.to = function(styleObject){
            this._toStyle = styleObject;
            return this;
        };
        this.after = function(styleObject){
            this._afterStyle = styleObject;
            return this;
        };
        this.callback = function(callback){
            this._callback = callback;
        };
    }).apply(_TransitionElement.prototype);
    return function(element,extraOptions){ return new _TransitionElement(element,extraOptions);}

})();

HTMLElement.prototype.transition = function(options){
    return Element.Transition( this,options );
};