Hatena::ブログ(Diary)

スコトプリゴニエフスク通信 このページをアンテナに追加 RSSフィード

2013-05-24

[][] Appiumを使ってMobile Safari上でSeleniumのテストケースを実行する

SeleniumiPhone DriverはDEPRECATEDらしいので、代替として紹介されている Appium を試してみた。

Appium is an open source test automation framework for use with native and hybrid mobile apps. It drives iOS and Android apps using the WebDriver JSON wire protocol.

http://appium.io/

最初は、AppiumはSelenium WebDriverとしてWebのテストだけに使えるものだと思っていたが、実はiOS/AndroidのネイティブUIの自動操作・自動テストにも使える(むしろそっちがメイン)ということを知って、さらに興味をそそられた。

が、ドキュメントが少なく、地雷があり過ぎる印象があるので、軽くメモを残しておく。環境はMacを想定しています。

インストール

公式のドキュメントにはdmgを使う方法と、npmで入れる方法の二通りが記載されているが、どっちもどっちという感じがする。

dmgを使うとAppium.appというGUIアプリをインストールできるが、これはnodeとnode_modulesをラッピングしたごく単純なアプリ。

$ nvm install v0.10.7
$ npm install -g appium

でnpmでインストールしたのと実質的に同じものであるはずなのだが、npmで入れると後述のMobile SafariのテストがInstrumentsのタイムアウトで動かなかったので、何か手順が不足しているのかもしれない。

dmgの方のソースはこれっぽい。

iOSアプリをテストする

Appium.appを使う場合は、App Pathにアプリのパスを指定する。

f:id:perezvon:20130524195923p:image

アプリのパスはXcodeデバッグビルドした場合には、

~/Library/Application Support/iPhone Simulator/6.0/Applications/049AE569-A182-439A-BA3A-36D02DB4BB09/TestApp.app

のようなパスになると思う。

アプリを指定したら、"Lauch"ボタンでSeleniumのRemoteWebDriverが起動する。

その上で、Appiumのサンプルコードを参考に、次のような感じでテストを書く。

import unittest
import os
from random import randint
from selenium import webdriver


class TestSequenceFunctions(unittest.TestCase):
    def setUp(self):
        # set up appium
        self.driver = webdriver.Remote(
            command_executor='http://127.0.0.1:4723/wd/hub',
            desired_capabilities={
                'browserName': 'iOS',
                'platform': 'Mac',
                'version': '6.0',
            })
        self._values = []

    def _populate(self):
        # populate text fields with two random number
        elems = self.driver.find_elements_by_tag_name('textField')
        for elem in elems:
            rndNum = randint(0, 10)
            elem.send_keys(rndNum)
            self._values.append(rndNum)

    def test_ui_computation(self):
        # populate text fields with values
        self._populate()
        # trigger computation by using the button
        buttons = self.driver.find_elements_by_tag_name("button")
        buttons[0].click()
        # is sum equal ?
        texts = self.driver.find_elements_by_tag_name("staticText")
        self.assertEqual(int(texts[0].text), self._values[0] + self._values[1])

    def tearDown(self):
        self.driver.quit()


if __name__ == '__main__':
    unittest.main()

Mobile Safariでテストを実行する

いくつか地雷がある。

npmでインストールしたappiumに接続してもタイムアウトになる
$ npm install -g appium
$ appium --force-iphone --safari -a 0.0.0.0 -p 4723
info: Appium REST http interface listener started on 0.0.0.0:4723

でAppium.appと同じことをやっていると思え、実際にSelenium RemoteWebDriverで接続しに行っても処理がスタートした雰囲気があるのだが、Instrumentsのタイムアウト?でテストが実行できない。

Appium.appで頑張る方針に切り替えたのであまり深追いしていない。

配布されているAppium.appにはバグがあるかもしれない

現時点での最新版0.5.2のAppium.appでMobile Safariを起動しようとすると、

Could not find mobile safari with version '6.0': Error: EACCES, mkdir '/tmpAppium-MobileSafari.app'

というエラーになる。これはgithubのIssue 635として報告されている。

すでに修正済みなのでdmgも修正されるだろうが、僕は手っ取り早く直したかったので、バンドル内のファイルに、

/Applications/Appium.app/Contents/Resources/node_modules/appium/app/helpers.js

このコミットと同じ修正を加えた。

appにsafariを指定しなくてはいけない

Safariを起動するには、次のどれかの方法でappにsafariを指定しなくてはいけない。

  • desired_capabilitiesに'app': 'safari'を追加する
  • Appium.appを使う場合
    • Appium.appの"Use Mobile Safari"にチェックを入れる
  • npmのappiumを使う場合
    • "--safari" オプションをつけてappiumを起動する
driver.quit() で例外が発生する

地雷を乗り越えてようやくテストを実行できるところまで来ても、tearDownに書いた driver.quit() で例外が発生して、毎回Appiumのプロセスが死んでしまう。これも深追いしていない。

# -*- coding: utf-8 -*-
import unittest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class MobileSafariTest(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Remote(
            command_executor='http://127.0.0.1:4723/wd/hub',
            desired_capabilities={
                'platform': 'Mac',
                'device': 'iPhone Simulator',
                'version': '6.1',
                'app': 'safari'
                })

    def tearDown(self):
        try:
            self.driver.quit()
        except:
            pass

    def test_title(self):
        self.driver.get('http://m.yahoo.co.jp/')

        input_element = self.driver.find_element_by_name('p')
        input_element.send_keys("Appium")
        input_element.submit()

        WebDriverWait(self.driver, 10).until(EC.title_contains("Appium"))
        self.assertRegexpMatches(self.driver.title, r'^Appium')

if __name__ == '__main__':
    unittest.main()


まとめ

  • iOSアプリ、Androidアプリの自動操作、自動テストにはAppiumは有望そう
  • ドキュメントが少なく、バグなのか、使い方が間違っているのか分からないことが多い点がキツイ

2013-02-19

[][] BrunchでChaplinアプリのプロジェクトを作る

プロジェクトの作成

brunchをインストールする。

$ npm install -g brunch

brunch-with-chaplinをスケルトンにしてプロジェクトを作成。

$ brunch new myproject --skeleton=git://github.com/paulmillr/brunch-with-chaplin.git

プロジェクトディレクトリに必要なnode_moduelsをインストールし*1、`brunch w` でbrunchを起動する。

$ cd myproject
$ npm install -d
$ brunch w

brunchのスタンドアローンサーバを使うには、`-s` オプションを使う。

$ brunch w -s

プロジェクト内のCoffeeScriptやテンプレートを変更すると、brunchが勝手にビルドしてくれる。便利。

Underscore.jsをLo-Dashに変更

これは好みもあると思うが、Underscore.jsではなくて、Lo-Dashを使いたいので、vendor以下にあるスクリプトを入れ替える。

githubから最新版のLo-Dashをダウンロード。

$ curl -o vendor/scripts/lodash.underscore-1.0.1.js https://raw.github.com/bestiejs/lodash/v1.0.1/dist/lodash.underscore.js

プロジェクト直下のconfig.coffeeにあるunderscoreをLo-Dashのものに置き換える。

% git diff config.coffee 
diff --git a/config.coffee b/config.coffee
index db9d206..d10c363 100644
--- a/config.coffee
+++ b/config.coffee
@@ -13,7 +13,7 @@ exports.config =
         before: [
           'vendor/scripts/console-polyfill.js',
           'vendor/scripts/jquery-1.9.1.js',
-          'vendor/scripts/underscore-1.4.4.js',
+          'vendor/scripts/lodash.underscore-1.0.1.js',
           'vendor/scripts/backbone-0.9.10.js'
         ]
         after: [

*1:これはbranch newでプロジェクトを作った直後はいらないかもしれないが、gitでcloneした直後には必要

2012-11-29

[][] Riak Source Code Reading #1に参加しました

昨日開催された「Riak Source Code Reading @東京 #1」に参加しました。そして、Riakのbitcaskバックエンドとeleveldbバックエンドのコードに関して発表してきました。

その時のスライドがこちら

スライドといってもたいした事は書いてなくて、発表中にポイントとなるコードに集中するために、元々のコードをコピー&ペーストして整形しただけと言っていいものです。コードリーディングの対象としたのは、ほぼ次の2ファイルのみ。

正直、Riakのメインの処理というよりは、別ライブラリを呼び出すためのグルーコードという感じで、どういうコードリーディングになるか不安だったのですが、予想以上に発表中に質問が出て、個人的には発見があったり、次に繋がる内容だったのではないかと思っております。当日全く仕事をしないで資料を作った業務時間の合間を縫って資料を仕上げたかいがあった!

以下、強調しておきたいところや反省点。

ソースコードより先にドキュメントを読もう

自分自身、ドキュメントよりソースコードを先に読んで後からちゃんとドキュメントに書いてあったと気づくことが多々あるのですが、今回もそういう経験がありました。grepであたりをつけていったり、逐一処理を追っていくよりは、ドキュメントからあたりを付けていったほうが早かったこともありますね。今回だとBitcaskのMergeとSyncとか。

コードリーディングの上では、やはりConceptsのページが重要に感じます。

Riakの開発を始めるならば、次の2つあたり。特にkerlの辺りは、今回発表するにあたり下調べをしていて初めて知ったのですが、特に重要。

Riakの実運用やチューニングを始めるならば、このあたり。

HanoiDB

大変興味深いスライドなので再掲。必見だと思います。

Pandoc

今回発表するにあたり、kuenishi氏のRiak Source Code Reading @TokyoのgithubレポジトリにPull Requestする形式で資料を用意したいなと思っていたのですが、MarkdownからHTMLのスライドにするのはどういう方法が一般的なのか知らなくて少し悩みました。

空前のHaskellマイブーム、もとい、Haskellは本当に流行っているので、空前のHaskellブームということもあり、"markdown slide Haskell"でググって、PandocでHTMLスライド化できるということを今回初めて知りました。

PandocのREADMEを参考にして、

$ pandoc -s -t slidy -i --self-contained -o output.html input.md 

というオプションで簡単にスライドに出来た!素晴らしい!

ErlangのSyntax Highlightが崩れてしまうようなことがあったので、Syntax Highlightを使わなかったのですが、そのせいでスライドでは多少コードが読みにくくなっていたかもしれません。それが反省点。

良かった点

久しぶりに会社の外で発表とかしましたが、うっかり「Riakを実際にプロダクションで使う人なんているはずないじゃないですか」とか口走らなくて、本当に良かったと思います。(たぶん言わなかったと思う。)

2012-11-20

[][] RiakのBackendのコードを読む(事前調査篇)

僕よりも詳しそうな人はいっぱいいるのに、なぜかRiak Source Code Reading @東京 #1の担当になってしまったので、Riakのbitcask, eleveldbバックエンドのコードを読んでいく。

たぶん最終的にはgistかgithubに資料をまとめるけど、ここに書いてあるのはその前段階のかなり個人的なメモ。

あたりをつける

grep 'bitcask'やgrep 'eleveldb'であたりをつけると、riak_kv_*_backend.erlがバックエンドを実装しているモジュールであることが推測できる。

riak_kv_*_backend.erlには次のようなファイルがあった。

% ls deps/riak_kv/src/*backend*.erl
deps/riak_kv/src/riak_kv_backend.erl
deps/riak_kv/src/riak_kv_eleveldb_backend.erl
deps/riak_kv/src/riak_kv_multi_backend.erl
deps/riak_kv/src/riak_kv_bitcask_backend.erl
deps/riak_kv/src/riak_kv_memory_backend.erl
deps/riak_kv/src/riak_kv_yessir_backend.erl

試しにriak_kv_eleveldb_backend.erlを見てみる。

-behavior(riak_kv_backend).

ということなので、バックエンドとなる各モジュールはriak_kv_backendというbehaviorを実装しているらしい。

上からざっと見ていくと、まずcpabilitiesという関数が目に入る。riak_kv_elevel_backend.erlだと以下のようになっている。

-define(CAPABILITIES, [async_fold, indexes]).
capabilities(_) ->
    {ok, ?CAPABILITIES}.

他のバックエンド・モジュールを見ていくと、

% grep CAPABILITIES deps/riak_kv/src/*.erl | grep define
deps/riak_kv/src/riak_kv_bitcask_backend.erl:-define(CAPABILITIES, [async_fold]).
deps/riak_kv/src/riak_kv_eleveldb_backend.erl:-define(CAPABILITIES, [async_fold, indexes]).
deps/riak_kv/src/riak_kv_memory_backend.erl:-define(CAPABILITIES, [async_fold, indexes]).
deps/riak_kv/src/riak_kv_multi_backend.erl:-define(CAPABILITIES, [async_fold]).
deps/riak_kv/src/riak_kv_yessir_backend.erl:-define(CAPABILITIES, [async_fold]).

ということなので、eleveldbとmemoryがindexesに対応していて、他はasync_foldのみということらしい。

bitcaskの方が機能が少ないってことだろうから、bitcaskから読んでいったほうがよいかなと考え、ここからriak_kv_bitcask_backend.erlに切り替えてbehaviorを追ってみる。

バックエンドAPI

あらためてexportしているAPIを見てみると、数は非常に少なく、名前から何をやっているかが明瞭なものが多い。これならソースを読んでいくのも楽そうだという感触を得る。

%% KV Backend API
-export([api_version/0,
         capabilities/1,
         capabilities/2,
         start/2,
         stop/1,
         get/3,
         put/5,
         delete/4,
         drop/1,
         fold_buckets/4,
         fold_keys/4,
         fold_objects/4,
         is_empty/1,
         status/1,
         callback/3]).

start, stopはだれがこのプロセスを起動しているのかがまだ良く分からないので、後回しにする。

Bitcask

get, put, delete

get, put, deleteはbitcask:get, bitcask:put, bitcask:deleteを呼び出す非常に薄いグルーコードになっているだけなのでほぼ自明。

riak_kv_bitcask_backend:fold_buckets
fold_buckets(FoldBucketsFun, Acc, Opts, #state{opts=BitcaskOpts,
                                               data_dir=DataFile,
                                               ref=Ref,
                                               root=DataRoot}) ->
    FoldFun = fold_buckets_fun(FoldBucketsFun),

fold_buckets_funは、Bucketに対する処理を行うFoldBucketsFunをとって、このBackendのすべてのBackendにFoldBucketsFunを適用する関数を返す関数。

async_foldがオプションに設定されているかどうかで分岐する。

    case lists:member(async_fold, Opts) of
        true ->
            %% omit
        false ->
            %% omit

処理の実体は、

bitcask:fold_keys(Ref, FoldFun, {Acc, sets:new()})

ということで、FoldFunで全キーをfoldするということだが、async_foldの場合は{async, fun()}を返すのみ。

riak_kv_bitcask_backend:fold_keys

fold_bucketsと構造は似ている。

fold_keys(FoldKeysFun, Acc, Opts, #state{opts=BitcaskOpts,
                                         data_dir=DataFile,
                                         ref=Ref,
                                         root=DataRoot}) ->
    Bucket =  proplists:get_value(bucket, Opts),
    FoldFun = fold_keys_fun(FoldKeysFun, Bucket),
    case lists:member(async_fold, Opts) of
        true ->
            %% omit
        false ->
            %% omit
    end.
riak_kv_bitcask_backend:fold_objects

これも、fold_buckets, fold_keysと構造は同じ。

fold_objects(FoldObjectsFun, Acc, Opts, #state{opts=BitcaskOpts,
                                               data_dir=DataFile,
                                               ref=Ref,
                                               root=DataRoot}) ->
    Bucket =  proplists:get_value(bucket, Opts),
    FoldFun = fold_objects_fun(FoldObjectsFun, Bucket),
    case lists:member(async_fold, Opts) of
        true ->
            %% omit
        false ->
            %% omit
    end.
riak_kv_bitcask_backend:drop
%% @doc Delete all objects from this bitcask backend
%% @TODO once bitcask has a more friendly drop function
%%  of its own, use that instead.
-spec drop(state()) -> {ok, state()} | {error, term(), state()}.

かなり親切にコメントが書かれているので、なんとなく分かった気になっちゃうけど、実際のbitcaskが作るファイル・ディレクトリの構造を把握していないので、もう少し深追いしたほうがよいかも。

riak_kv_bitcask_backend:is_empty

要調査。

%% @doc Returns true if this bitcasks backend contains any
%% non-tombstone values; otherwise returns false.
-spec is_empty(state()) -> boolean().
    %% Estimate if we are empty or not as determining for certain
    %% requires a fold over the keyspace that may block. The estimate may
    %% return false when this bitcask is actually empty, but it will never
    %% return true when the bitcask has data.
    bitcask:is_empty_estimate(Ref).
riak_kv_bitcask_backend:status

bitcask:statusのラッパーで自明。

%% @doc Get the status information for this bitcask backend
-spec status(state()) -> [{atom(), term()}].
status(#state{ref=Ref}) ->
    {KeyCount, Status} = bitcask:status(Ref),
    [{key_count, KeyCount}, {status, Status}].
riak_kv_bitcask_backend:callback
%% @doc Register an asynchronous callback
-spec callback(reference(), any(), state()) -> {ok, state()}.

ソースを読んでいくと、bitcaskバックエンドが対応しているcallbackは、

{sync, SyncInterval}

と、

merge_check

callbackとはバックエンド特有の処理を登録するものらしく、eleveldbのcallbackでは何もやっていなかった。syncとmerge_checkが何をやっているのかを追っていけば、bitcaskの特性がわかるかもしれない。

eleveldb

ここからはeleveldbバックエンドを読む。

riak_kv_eleveldb_backend:get

bitcask:getがeleveldb:getに変わったくらいで、riak_kv_bitcask_backend:getとほぼ同じ。

riak_kv_eleveldb_backend:put

これもやっていることは単純なのだが、bitcaskと違ってindexに対応しているので、そのための処理が増えている。

    %% Create the KV update...
    StorageKey = to_object_key(Bucket, PrimaryKey),
    Updates1 = [{put, StorageKey, Val}],

Bucket名とキーから実際にデータを書き込む際のキーを作成して、eleveldbに与えるリストUpdate1を作る。

    %% Convert IndexSpecs to index updates...
    F = fun({add, Field, Value}) ->
                {put, to_index_key(Bucket, PrimaryKey, Field, Value), <<>>};
           ({remove, Field, Value}) ->
                {delete, to_index_key(Bucket, PrimaryKey, Field, Value)}
        end,
    Updates2 = [F(X) || X <- IndexSpecs],

IndexSpecsの内容に応じて、putかdeleteでeleveldbに与えるリストUpdate2を作る。

    %% Perform the write...
    case eleveldb:write(Ref, Updates1 ++ Updates2, WriteOpts) of
        ok ->
            {ok, State};
        {error, Reason} ->
            {error, Reason, State}
    end.

最後にUpdate1とUpdate2の内容を実際にeleveldbに書き込む。

riak_kv_eleveldb_backend:delete

putがdeleteになるだけで、riak_kv_eleveldb_backend:putと処理の流れはほぼ同じ。

riak_kv_eleveldb_backend:fold_buckets

eleveldb:fold_keysを呼び出す前にFoldOptsをいじっている。

    FirstKey = to_first_key(undefined),
    FoldOpts1 = [{first_key, FirstKey} | FoldOpts],

のto_first_keyが、

%% @private Given a scope limiter, use sext to encode an expression
%% that represents the starting key for the scope. For example, since
%% we store objects under {o, Bucket, Key}, the first key for the
%% bucket "foo" would be `sext:encode({o, <<"foo">>, <<>>}).`
to_first_key(undefined) ->
    %% Start at the first object in LevelDB...
    to_object_key(<<>>, <<>>);

のように定義されているので、ここではLevelDBの最初のオブジェクトからfoldするということ。

次にやること

残りのriak_kv_eleveldb_backendの関数は軽く眺めただけだが、ここまで読んでみての次にやる必要が感じたこと。

  • バックエンドのプロセスを起動しているのは誰かを調べる。(riak_kv_vnode?)
  • async_foldとは何かを調べる。
  • %%get, put, deleteとかは自明として%%fold_buckets, fold_keys, fold_objectsの実際の内部での使い方を調べる。
    • riak_kv_vnodeのprivateな関数を眺めていると、putとかでもパッと見てもよく分からない処理をやっているので、少し詳しく追っていた方がよいかも。

2012-10-30

[][]couchbase-python-clientをmemcacheクライアントとして使う

couchbase-python-clientはCouchbase社が開発しているCouchbaseのクライアント・ライブラリ。「Couchbaseとは何ぞ?」という方は、CouchDBのストレージをMemcache/Membaseに置き換えたKVSだと考えて頂きたい。

PythonのMemcacheクライアントたち

PythonのMemcacheクライアントだと、Pure Pythonなライブラリであるpython-memcachedと、libmemcachedバインディングのpylibmcが標準的なライブラリだと思う。

が、両方とも標準的なニーズはほぼ100%満たせるとはいえ、分散アルゴリズムやCASの振る舞い、データのシリアライズや圧縮の仕方を少しカスタマイズしたいとなると、拡張性に不満を覚えることがあった。

個人的に欲していたのが、最低限のことだけが出来て、しかもバイナリプロトコルが使える、非常に薄いMemcachedクライアントで、bmemcachedというやつは結構よさそうと思って注目していた。

しかしながら、couchbase-python-clientがすごい勢いでリファクタされつつあるのを見て、もはやcouchbase-python-clientが標準的なMemcachedクライアントの座を獲得するのではないかという感すらある。

couchbaseをMemcacheクライアントとして使う

couchbase-python-clientをインストールするには、PyPIから

$ pip install couchbase

で最新版(現時点では0.8.0)をインストールするか、gitでgithubのmasterをインストールする。

$ pip install git+git://github.com/couchbase/couchbase-python-client.git

いろいろ説明をすっ飛ばして簡単にいうと、Couchbaseは永続化機能を備えたMemcacheを複数束ねた分散DBなので、各ノードはMemcacheプロトコルを話すKVSになっている。故に、このcouchbase-python-clientクライアントライブラリはMemcacheクライアントも実装している。

非常に乱暴な方法だが、下記のようなコードで、couchbase-python-clientをMemcacheクライアントとして使えてしまう。

from couchbase.memcachedclient import MemcachedClient as Memcache

host = '127.0.0.1'
port = 11211

client = Memcache(host=host, port=port)
opaque, cas, data = client.set("KEY", exp=600, flags=0, val="value")
print((opaque, cas, data))

opaque, cas, data = client.get("KEY")
print((opaque, cas, data))

getlを使う

全然知られていない特徴だと思うが、Membase/Couchbaseが使っているmemcache拡張プロトコルにはgetlというコマンドがある。

getlとは、ロック期間を指定してキーに対応する値を取得し、そのロック期間が過ぎるまでsetやcasによる更新を失敗させることができる、という機能。

公式ドキュメントだと、以下のあたりが詳しい。

「こんなんで排他制御が上手く行くのか?」と思われるかもしれないが、案外この機能を使うと実装できてしまう。少なくとも、Zyngaで上手く行っている程度には上手くいく。

以下はgetl拡張コマンドを試してみる例。

# -*- coding: utf-8 -*-
from couchbase.vbucketawareclient import VBucketAwareClient as Memcache

host = '127.0.0.1'
port = 11211

client = Memcache(host=host, port=port)

opaque, cas, data = client.set("KEY", exp=600, flags=0, val="OK")
print((opaque, cas, data))

opaque, cas, data = client.getl("KEY", exp=5)
print((opaque, cas, data))

# ロック期間が過ぎていないのでこのsetは失敗する
opaque, cas, data = client.set("KEY", exp=600, flags=0, val="ERROR")
print((opaque, cas, data))

プロトコルを試してみるという例で、本来ならば、VBucketAwareClientを直接使ってデータをロックしたり、取得したりするのはCouchbase的にはNGのはずだが、あのZとかいう会社は・・・。

unlockを使う

unlコマンドを使うとgetlで取得したロックを主導でアンロックできるようだが、なぜかJavaクライアントにしか実装されていないっぽい。

未実装なのはそれなりの理由があると思うが、unlの挙動を試すだけならば、以下の様なコードで実現できる。

# -*- coding: utf-8 -*-
import time
from couchbase.vbucketawareclient import VBucketAwareClient

class Memcache(VBucketAwareClient):
    def unlock(self, key, cas, vbucket=-1):
        self._set_vbucket_id(key, vbucket)
        return self._doCmd(0x95, key, '', cas=cas)

host = '127.0.0.1'
port = 11211

client = Memcache(host=host, port=port)

opaque, cas, data = client.set("KEY2", exp=600, flags=0, val="VALUE")
print((opaque, cas, data))

# 15秒ロック期間を設けて"KEY2"をget
opaque, cas, data = client.getl("KEY2", exp=15)
print((opaque, cas, data))

# 手動でunlock
opaque, cas, data = client.unlock("KEY2", cas)
print((opaque, cas, data))

# "KEY2"に再度set. アンロックしていない場合はここでエラーになる
opaque, cas, data = client.set("KEY2", exp=600, flags=0, val="VALUE2")
print((opaque, cas, data))