App Engine Memcache のincrementの挙動

App EngineのMemCacheにはアトミックなインクリメントが実装されている.符号付き64ビット長で管理されていて,Long.MAX_VALUEを越えると,負の値に巻き戻る,と言うことになっている.

Incrementing by positive amounts will reach signed 64-bit max (2^63 - 1) and then wrap-around to signed 64-bit min (-2^63), continuing increments from that point.

のだが,開発環境と実環境で動作が違う.というか,実環境がバグっている.

ms.put("key3", Long.MAX_VALUE);
pw.println(ms.get("key3"));           //  9223372036854775807
pw.println(ms.increment("key3", 1));  // -9223372036854775808
pw.println(ms.get("key3"));           // -9223372036854775808

開発環境ではコメントの値が出力される.これは仕様通り.

ところが,実環境では,こんな例外がでる.

java.lang.NumberFormatException: For input string: "9223372036854775808"
	at java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at com.google.appengine.api.memcache.MemcacheSerialization.deserialize(MemcacheSerialization.java:140)
	at com.google.appengine.api.memcache.MemcacheServiceImpl.get(MemcacheServiceImpl.java:251)

よく見ると "9223372036854775808" (Long.MAX_VALUE + 1)をparseしようとしている.推測するに,MemCacheサービスの側では数値を文字列で管理しているので,64ビットもくそもなく,いくらでもインクリメントできてしまっているのだけど,値をlongに戻そうとしたところで,失敗しているらしい.

まあ,こんな大きい数のインクリメントをする機会はないだろうから実害はないのだろうけど.

Stringの場合

Stringだとまた挙動が違ったり.この例ではlongをStringに変換してから値として設定している.

ms.put("key3", "" + Long.MAX_VALUE);
pw.println(ms.get("key3"));         
pw.println(ms.increment("key3", 1));
pw.println(ms.get("key3"));         

この場合,開発環境では,

9223372036854775807
-9223372036854775808
-9223372036854775808

実環境では

9223372036854775807
-9223372036854775808
9223372036854775808

となる.javadocには

Note: The actual representation of all numbers in Memcache is a string. This means if you initially stored a number as a string (e.g., "10") and then increment it, everything will work properly, including wrapping beyond signed 64-bit int max. However, if you get the key past the point of wrapping, you will receive an unsigned integer value, not a signed integer value.

と書かれているので,この場合は,実環境の挙動が正しい.

もう一つの仕様との齟齬

そういえばもう一点仕様と違ってる点があった.

However, due to the way numbers are stored, decrementing -3 by -5 will result in -8; so the zero-floor rule only applies to decrementing numbers that were positive.

と書かれていて,負の値に対してもインクリメント,デクリメントできるかのように読めるが,実際にはインクリメント,デクリメントにかかわらず,InvalidValueExceptionが返される.これは開発環境,実環境にかかわらず,同じ.

所感

些細な点だし,Low-level APIなので,バグと言うよりは,javadocの更新が追いついていないということなんだろう.開発環境と実環境の挙動が違うのは困るが..