Java + kumofsは苦痛が伴うのでオススメできない


2010/04/22追記
この情報はkumofs-0.3.2での調査結果です。0.3.4ではkumo-gatewayに-Fオプションを付けて起動することで、memcached client for Javaでも動作するようになりました。
memcached client for Java + kumofs 0.3.4 での接続確認 - DenkiYagi

研究開発しているJava製システムにkumofsを採用しようと思い色々調査したのだが、とりあえず採用は見送る(最終決定は先送りする)ことにした。ここでは、なぜ見送ることにしたのか記しておく。
なお、調査段階でkumofs作者の古橋さん(id:viver@frsyuki)にTwitterで助けて頂いたのにも関わらず、若干ネガティブな記事になってしまっているので多少心苦しい…。

結論

kumofs自体には問題はない。
だが、JavaにはロクなMemcachedクライアントライブラリが存在しない。なので自前でSocketを書くか、既製のライブラリを改造するかのどちらかが必要になってくる。

何がいけないのか

kumofsの制約

Twitterで古橋さんから直接教えて頂いたのだが、kumofsではexpireとflagsを0にしなければならない。

kumofsとは?
(中略)

  • memcachedプロトコルをサポートしています(get, set, delete のみ。expireとflagsには必ず0を指定する必要があります)
http://github.com/etolabo/kumofs/blob/master/doc/doc.ja.md
memcached client for Java

memcached client for Javaはsetの際にvalueの型を見て、勝手に以下のようなflagsをセットしてしまう。以下に該当しない場合は-1がセットされるようだ。

	// values for cache flags 
	public static final int MARKER_BYTE             = 1;
	public static final int MARKER_BOOLEAN          = 8192;
	public static final int MARKER_INTEGER          = 4;
	public static final int MARKER_LONG             = 16384;
	public static final int MARKER_CHARACTER        = 16;
	public static final int MARKER_STRING           = 32;
	public static final int MARKER_STRINGBUFFER     = 64;
	public static final int MARKER_FLOAT            = 128;
	public static final int MARKER_SHORT            = 256;
	public static final int MARKER_DOUBLE           = 512;
	public static final int MARKER_DATE             = 1024;
	public static final int MARKER_STRINGBUILDER    = 2048;
	public static final int MARKER_BYTEARR          = 4096;
	public static final int F_COMPRESSED            = 2;
	public static final int F_SERIALIZED            = 8;

このフラグをセットしないように改造してしまえば、おそらく動作するのだが、そこまでは試していない。

spymemcached

spymemcachedを使って以下のようなコードを書くと、期待した通りに値が出力されるので、一見すると正常に動作しているようにみえる。

package memcachedtest;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.spy.memcached.MemcachedClient;

public class KumofsTest {

    public void run() {
        // connect to kumofs and start connection thread
        MemcachedClient client;
        try {
            client = new MemcachedClient(new InetSocketAddress("localhost", 11211));
        } catch (IOException ex) {
            Logger.getLogger(KumofsTest.class.getName()).log(Level.SEVERE, null, ex);
            return;
        }

        String key = "hoge";
        int expire = 0;
        Object value = "fuga";
        
        // set
        client.set(key, expire, value);
        
        // get
        String result = (String)client.get(key);
        Logger.getLogger(KumofsTest.class.getName()).log(Level.ALL, result);

        // disconnect from kumofs and stop connection thread
        client.shutdown();
    }

    public static void main(String[] args) {
        KumofsTest test = new KumofsTest();
        test.run();
    }
}

私もこのコードで「あー、大丈夫そうだわー」と思いかけた。だが、このコードではset()/get()のエラー処理が実装されていない。適切なエラー処理を書くにはspymemcachedをコードを読む必要があると考え、コードを流し読んだのだが「うわぁ、こいつはダメだ・・・」と思わせてくれる実装になっていた。

まずset()が怪しい、というかインタフェースとしてなってない。set()の実装は以下のようになっている。

	public Future<Boolean> set(String key, int exp, Object o) {
		return asyncStore(StoreType.set, key, exp, o, transcoder);
	}

get()には対になるasyncGet()が用意されているのに、set()にはasyncSet()が用意されていない(この記事書いてるときに気がついた)。「俺は同期モードだと思って書きこんでいたのに実は非同期モードで書きこんでいた」とか、どうなのよ。
まぁそれはともかく、set()が成功したか否かを判別するには以下のように記述する。

        Future<Boolean> future = client.set(key, expire, value);
        try {
            if (!future.get(5, TimeUnit.SECONDS)) {
                //TODO error handling
            }
        } catch (InterruptedException ex) {
            //TODO error handling
        } catch (ExecutionException ex) {
            //TODO error handling
        } catch (TimeoutException ex) {
            //TODO error handling
        }

また、get()の方も以下のような実装になっている。なぜRuntimeExceptionでエラーを投げるんだ…。

	public <T> T get(String key, Transcoder<T> tc) {
		try {
			return asyncGet(key, tc).get(
				operationTimeout, TimeUnit.MILLISECONDS);
		} catch (InterruptedException e) {
			throw new RuntimeException("Interrupted waiting for value", e);
		} catch (ExecutionException e) {
			throw new RuntimeException("Exception waiting for value", e);
		} catch (TimeoutException e) {
			throw new OperationTimeoutException("Timeout waiting for value", e);
		}
	}

get()の実装があまりに雑なので、素直にasyncGet()を使った方が良い。

        String result;
        Future<Object> future = client.asyncGet(key);
        try {
            result = (String) future.get(5, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            //TODO error handling
        } catch (ExecutionException ex) {
            //TODO error handling
        } catch (TimeoutException ex) {
            //TODO error handling
        }
        Logger.getLogger(KumofsTest.class.getName()).log(Level.ALL, result);

あと、コネクション周りの実装も流し読んだ感じでは怪しい臭いがする。私は深追いせず、ここで見切りを付けてしまったのだが、spymemcachedを使いたい方はコネクション周りの実装が本当に大丈夫か確認しておいた方が良い。

なお、memcached client for Javaの方で問題になったflagsについては、set()でTranscoderを指定しなければ問題はないようだ。