PythonでThriftを通してHibari

HibariというErlangによる分散KVSがあるようです
http://hibari.github.com/hibari-doc/hibari-app-developer-guide.ja.html
流行りのEventualConsistencyではなく、高い一貫性を守りつつスケールするという代物です。

インストールする

http://hibari.github.com/hibari-doc/hibari-app-developer-guide.ja.html
に従っていけばできます。Repoというソフトウェアに依存しています。
比較的新しいErlang環境を要求するところも注意が必要です。
逆に、repoさえあれば

$ repo init -u git://github.com/hibari/manifests.git -m hibari-default.xml
$ repo sync
$ make
$ make package

でほぼ完了します。

Repoの入手場所のサーバが落ちていて途方に暮れていたら[twitter:@tatsuya6502]さんが緊急回避的にgistを教えてくださいました。

Repoから参照しているandroid.git.kernel.orgが使えなくて途方に暮れていたらまたしても[twitter:@tatsuya6502]さんに助けて頂けました。至りつくせりです。本当にありがとうございます。

無事立ち上がりました

Thriftを使う

インストールしたHibariの内部に
hibari/lib/gdss_ubf_proto-0.1.7/priv/thrift/hibari.thrift
というファイルがあるので好きな場所に持ってきます。持ってきた場所で

$ thrift --gen py hibari.thrift

と打つとgen-pyディレクトリが作成され、中にimportできる物が一式揃います。便利ですね。

$ ls
hibari.thrift gen-py/
$ ls gen-py
__init__.py hibari/

早速使ってみます。

import thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

from sys import path
path.append('gen-py')
from hibari import Hibari # Clientを使う
from hibari import ttypes # 命令に使うオブジェクト群

# ソケットを作成
socket = TSocket.TSocket('localhost',7600)
socket.setTimeout(None)
transport = TTransport.TBufferedTransport(socket)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
client = Hibari.Client(protocol)
socket.open()

# 使ってみる
request = ttypes.Add(b"tab1", b"fookey", b"Hello Hibari!")
response = client.Add(request) #=> None or Exception
print response
$ python hibari_thrift.py
HibariResponse(value=None, key=None, timestamp=135)
$ python hibari_thrift.py
Traceback (most recent call last):
  File "app_main.py", line 53, in run_toplevel
  File "samp.py", line 21, in 
    response = client.Add(request)
  File "gen-py/hibari/Hibari.py", line 200, in Add
    return self.recv_Add()
  File "gen-py/hibari/Hibari.py", line 223, in recv_Add
    raise result.ouch
HibariException: HibariException(timestamp=None, what=101, why='Key Exist')

Add命令は「キーが存在していない場合に限り成功する」ので二度目に失敗するのは仕様通りの挙動です。(何故boolでなく例外で返してるか分かりませんが)
確かに動いてるのですが、自分好みにラップしたのが以下です。
thrift依存っぽいところをクラス内に押し込んで、python-memcacheっぽいインタフェースを提供しつつ、boolで返せそうな所はboolを使うようにして、valueには好きなデータを使いたいのでpickleによりシリアライズするよう。

import thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
import serializer

from sys import path
path.append('gen-py')
from hibari import Hibari
from hibari import ttypes

class HibariClient(object):
  def __init__(self, host, port):
    self.socket = TSocket.TSocket(host, port)
    self.socket.setTimeout(None)
    self.transport = TTransport.TBufferedTransport(self.socket)
    self.protocol = TBinaryProtocol.TBinaryProtocol(self.transport)
    self.client = Hibari.Client(self.protocol)
    self.socket.open()
    self.timestamp_cache = {}
  def __del__(self):
    self.socket.close()

  def add(self, table, key, value):
    value = serializer.serialize(value)
    try:
      self.client.Add(ttypes.Add(table, key, value))
      return True
    except Exception, e:
      return False
  def get(self, table, key):
    try:
      value = self.client.Get(ttypes.Get(table,key)).value
    except Exception, e:
      return None
    return serializer.deserialize(value)

  def gets(self, table, key):
    try:
      result = self.client.Get(ttypes.Get(table,key))
    except Exception, e:
      return None
    self.timestamp_cache[(table,key)] = result.timestamp
    return serializer.deserialize(result.value)

  def set(self, table, key, value):
    value = serializer.serialize(value)
    self.client.Set(ttypes.Set(table, key, value))
  def replace(self, table, key, value):
    value = serializer.serialize(value)
    try:
      self.client.Replace(ttypes.Replace(table, key, value))
    except Exception, e:
      return False
    return True
  def delete(self, table, key):
    try:
      self.client.Delete(ttypes.Delete(table,key,False))
    except Exception, e:
      return False
    return True
  def cas(self, table, key, value):
	"""
	it does'nt work!!! because of thrift-hibari doesn't implement
	"""
    if (table,key) not in self.timestamp_cache:
      raise Exception("you must 'gets' key before cas")
    try:
      self.client.Replace(ttypes.Replace(table, key, value)) # TODO
    except:
      pass

pickleも自分好みにラップしてます。Pythonがまだちょっと身体に馴染まないのです。
https://gist.github.com/1239225#file_serializer.py

ラップしたものの使い方はこんな感じです。

client = HibariClient('localhost', 7600)

# 保存
client.set('tab1','key3',3)

# 存在する場合に限り置換
result = client.replace('tab1','key3',4)
print 'replace:',result

# 獲得
result = client.get('tab1','key3')
print 'get:',result

# 削除(二度目は失敗するのでFalseが帰る
result = client.delete('tab1','key3')
print 'delete:',result
result = client.delete('tab1','key3')
print 'delete:',result

# 削除済みデータをGet。もちろん失敗する
result = client.get('tab1','key3')
print 'get:',result

実行するとこんな感じ

$ python HibariClientTest.py
replace: True
get: 4
delete: True
delete: False
get: None

上手くいってる感じですが、今の実装だとサーバ障害などによる例外を握りつぶしてしまうのでもう少し調整が必要そうですが今日はここまでで。