読者です 読者をやめる 読者になる 読者になる

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とかでもパッと見てもよく分からない処理をやっているので、少し詳しく追っていた方がよいかも。

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))

hidefを試す

一流のペチパーとやらにもなれば、「PHPのdefineはコストが高いからねー」とドヤ顔で語りつつ hidef なるものを使うらしい。

しかし、"PHP hidef"とかでググっても、インストールの仕方とかごく基本的なことはともかく、hidefを使ったアプリケーション設計とか、ラッパーライブラリとか(hidef単体で使うのはキツい)、運用のベスト・プラクティス的な記事は全然出てこない感じがするのだが、本当に一流のペチパーとやらは呼吸するかのごとくhidefを使いこなしているのだろうか?

インストール

hidefパッケージを提供しているディストリビューションはあまりないように見えるので、一番無難な方法、peclでインストールする。

 $ sudo pecl install hidef

RHEL系の場合、

 $ sudo yum install php-pear php-devel pcre-devel

等のパッケージが必要かもしれない。pecl install後、/etc/php.d/hidef.ini に、

extension=hidef.so
hidef.ini_path=/apps/myapp/current/hidef/
hidef.data_path=/apps/myapp/current/hidef/

のような定義を追加する。

/apps/myapp/current/hidef/ はCapistranoでアプリケーションをデプロイするならばこんな感じになるんじゃないかな、と考えたパス。単にhidefを試してみたいだけならば、/tmp/hidef とかでも良い。

iniファイルからdefineを定義する

hidef.ini_path で定義したディレクトリに、

 str HELLOWORLD = "Hello World";

のようなファイルを拡張子.ini (例えば sample.ini など)で作る。その上で、

 $ php -r "var_dump(HELLOWORLD);"

のようにphpを実行すると、

 string(11) "Hello World"

のように出力される。

HELLOWORLDという定数がphpスクリプトの中ではどこにも定義されていないのに動作しているのは、hidef拡張が hidef.ini_path で定義したディレクトリ内の.iniファイルに基づいて define に相当する処理を行なってくれるかららしい。

dataファイルから定数を定義する

<?php
$data = array(
  "spam" => 1,
  "egg" => 2,
  "ham" => 3,
);

$outputDir = ini_get("hidef.data_path");
file_put_contents("${outputDir}/test.data", serialize($data));

のようなスクリプトを使い、hidef.data_path で定義したディレクトリにPHP serializeしたデータを test.data という拡張子で保存する。

その上で、

$  php -r "var_dump(hidef_fetch('test'));"

のように hidef_fetch 関数を使うと、呼び出し側のPHPスクリプトでは、やはりどこにもtestというデータを定義していないのにもかかわらず、

object(FrozenArray)#1 (3) {
  ["spam"]=>
  int(1)
  ["egg"]=>
  int(2)
  ["ham"]=>
  int(3)
}

のような データを取得できる。

どんな用途に使うべきか

.ini や .data で定義したデータは不可変で、Apache上でPHPを実行している場合にはApacheを再起動しないと .ini や .data に加えた変更は反映されない(らしい)。

毎回Apacheを再起動していては開発時はつらいので、開発中は通常の define や array を使って設定を動的に読み込むようにしておき、アプリケーションをデプロイするタイミングで、hidef が期待する .ini や .data のフォーマットにデータを変換するような仕組みが必要であろう。

どんな用途に hidef を使うべきかに関しては、hidef の README を読むと、次のように、「多言語化の言語データテーブルみたいなものに使うのが理想的」と言っている。

Something like a localization table would be ideal to store in this cache. Since no updates are possible in hidef without restarting apache, data with a short life-span obviously doesn't belong in this.

ここに書いてある通り、生存期間が短いデータは hidef には向かないし、あるいは、ごく少ない設定データやごく少数の定数では有意な効果はでないかもしれない。

iWebDriverでテストする

昨日のエントリのChromeDriverを試した後、SafariDriverというなかなか有望そうだがイマイチちゃんと動いている感がないソリューションまで試した結果、iPhone向けWebアプリケーションのテストにはiPhoneそのものか、iPhoneシミュレーターを使うのが一番良いという結論に達した。

インストール

iOS端末でSelenium(iWebDriver)を使うには、公式ドキュメントにある通り、レポジトリからコードをチェックアウトして、"./go iphone"でビルドする。要Xcode

$ svn co http://selenium.googlecode.com/svn/trunk selenium
$ cd selenium
$ ./go iphone

あるいは、selenium/iphone/iWebDriver.xcodeproj というXcodeプロジェクトを開いて普通にビルドしてもよい。

Python Bindingで使う

ビルドしたiWebDriverアプリを起動すると、"Started at http://127.0.0.1:3001/wd/hub/" という表示が出るので、このURLにSeleniumRemoteWebDriverで接続する。

Pythonならば次のような感じ。

# -*- coding: utf-8 -*-
import unittest2 as unittest
from selenium import webdriver

class KnightTest(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Remote(
            "http://127.0.0.1:3001/wd/hub", webdriver.DesiredCapabilities.IPHONE)

    def test_gree(self):
        driver = self.driver
        driver.get("http://tknight.gree.jp/?&render=1")
        self.assertIn(u"聖戦ケルベロス", driver.title)

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

node.jsで使う

WebDriverJsを使うと、node.js/JavaScriptでWebDriverを操作できる。

seleniumのチェックアウトディレクトリで、

./go webdriverjs

を実行し、ビルド結果として生成される build/javascript/webdriver/webdriver.js をnode.jsのスクリプトでrequireしてやればよい。

var webdriver = require('./build/javascript/webdriver/webdriver');
var assert = require('assert');

var driver = new webdriver.Builder()
    .usingServer('http://10.6.252.254:3001/wd/hub/')
    .build()
;

driver.get('http://tknight.gree.jp/').then(function() {
    driver.getTitle().then(function(title) {
        assert.equal('聖戦ケルベロス', title);
    });
});
driver.quit();

Webアプリケーションのテストが「リクエストを送信して結果を待って何かする」という非同期プログラミングのパターンが多いことから、JavaScriptSelenium WebDriverでテストケースを作成するには最適な言語であるような気がしているが、テストケースは書けるだろうけど、大規模なテストスーツを構成する方法、ベストプラクティスは全然分からない。

SeleniumのChromeDriverでUser-Agentを変更する

WebKit、しかもiOSのWebKitでしか動かないコードを通して世界を良くしていますかっ!?(挨拶)

前回似たようなエントリを書いた時には分からなかったのだが、SeleniumのChromeDriverでUser-Agentを変更するのは比較的容易だった。これでFirefoxではピクリとも動かないサイトでもテストできる!!Opera、IE、なにそれ?

ChromeDriverでUser-Agentを変更するには、SeleniumのChromeDriverのWikiページにあるように、DesiredCapabilitiesオブジェクトに--user-agentオプションを渡してChromeを起動するようにしてやればよい。

Java, C#, Rubyといった各言語ともインターフェイスはほぼ同じなのではないかと想像するが、Pythonの場合はChrome WebDriverのdesired_capabilities引数はdeprecated扱いのようなので、Optionsオブジェクトを使うのが正解のようだ。

次のような感じ。

import sys
from selenium.webdriver.chrome import webdriver, options

executable_path = sys.argv[1]

options = options.Options()
options.add_argument('--user-agent="Mozilla/5.0 (Linux; U; Android 2.3.3; ja-jp; SC-02C Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"')

driver = webdriver.WebDriver(executable_path)
driver.get("http://tknight.gree.jp/?&render=1")

他に便利そうなオプションとしては、

options.add_argument('--proxy-server=http://127.0.0.1:8888')

のように、HTTPプロキシを指定する--proxy-serverオプションなど。


困っているのは、Ubuntu 12.04の特に何もExtensionを入れていないChrome 18.0.1025では上記のコードが期待通り動くのを確認したのだが、OSX LionのChrome 19.0.1084.46では、テスト対象サイトにはリクエストは飛ぶものの描画の段階で?クラッシュしてしまうということ。

これがOSX Lionだからなのか、Chromeの問題なのか、Extension等の個人環境の問題なのか、ChromeDriverの問題なのかが現時点でさっぱり分からないので、もう少し調査しないといけない・・・

Facebookユーザー名からFacebookユーザーIDを調べる

Facebookと連携するアプリを開発していると「このユーザーのFacebookのユーザーIDを調べたい」ってことがよくある。

https://www.facebook.com/profile.php?id=123456789123445

のようにURL未設定のユーザーならば簡単なのだが、

https://www.facebook.com/{{ USERNAME }}

みたいなURLを自分で設定しているユーザーの場合、どうするのが一番簡単かずっと分からなかった。


今日思いついた、一番お手軽と思える方法。例えば、Mark ZuckerbergのFacebook上のプロフィールのURLは、

なのだが、このURLのwwwの部分をgraphに変え、

でブラウザなり、curlなりでアクセスすると、以下のようなレスポンスがJSONで返ってくる。

{
  id: "4",
  name: "Mark Zuckerberg",
  first_name: "Mark",
  last_name: "Zuckerberg",
  link: "https://www.facebook.com/zuck",
  username: "zuck",
  gender: "male",
  locale: "en_US"
}

idの部分がFacebookのユーザーID。

serializeしたオブジェクトの継承元が変わったらunserializeできるのか?

疑問点。PHPにおいてオブジェクトをserializeし、その後そのオブジェクトの継承関係が変わったら、正しくunserializeできるのか?

検証コード。親クラスなしのDogというクラスのインスタンスをserializeしたファイルに書きだした後、Dog < SocialDog < BaseDog という継承関係に変更に変更したコードで読みだす。

<?php
// dog.php

class Dog {
	protected $name;

	public function setName($name) {
		$this->name = $name;
	}

	public function getName() {
		return $this->name;
	}
}

function main() {
	$dog = new Dog();
	$dog->setName('Zynga');

	file_put_contents('zynga.dat', serialize($dog));
}

main();

実行すると以下のようにzynga.datが書き出された。

$ php dog.php
$ cat zynga.dat
O:3:"Dog":1:{s:7:"*name";s:5:"Zynga";}

以下のコードで継承関係を変えて、シリアライズしたデータからDogクラスをunserializeする。

<?php
// dog2.php

class BaseDog {
	public function isSocial() {
		return false;
	}
}

class SocialDog {
	public function isSocial() {
		return true;
	}
}

class Dog extends SocialDog {
	protected $name;

	public function setName($name) {
		$this->name = $name;
	}

	public function getName() {
		return $this->name;
	}
}

function main() {
	$data = file_get_contents('zynga.dat');
	$dog = unserialize($data);

	if ($dog->isSocial()) {
		printf("%s is a social dog!\n", $dog->getName());
	} else {
		printf("%s isn't a social dog...\n", $dog->getName());
	}
}

main();

結果。

% php dog2.php 
Zynga is a social dog!

結論としては、「できる」らしい。