Subversionのファイルシステム周りを少し調べてみた

昨日、Subversionのアップデートをしていて、リポジトリのファイルシステムのバージョン周りで
どのように互換性が保たれているのか、気になったので調べてみた。

ソースコードは以下のURLのものを使用した。

http://subversion.tigris.org/downloads/subversion-1.5.3.tar.bz2

まず、ファイルシステムにどのようにアクセスしているのか、という事を見てみる。
ファイルシステムへのアクセスは、FSFSとBDBがサポートされているため、抽象化されている。
抽象化する際には、関数ポインタの構造体を用いて抽象化を行っているようである。
(SubversionがC++を使っていれば、クラスを使って抽象化していただろう)

fs-loader.h 60行付近

/*** Top-level library vtable type ***/

typedef struct fslibraryvtablet
{
/* This field should always remain first in the vtable.
Apart from that, it can be changed however you like, since exact
version equality is required between loader and module. This policy
was weaker during 1.1.x, but only in ways which do not conflict with
this statement, now that the minor version has increased. */
const svn
versiont (getversion)(void);

/* The openfs/create/openfsforrecovery/upgradefs functions are
serialized so that they may use the common
pool parameter to
allocate fs-global objects such as the bdb env cache. /
svnerrort *(
create)(svnfst fs, const char *path, aprpoolt *pool,
aprpoolt *commonpool);
svn
error_t *(
openfs)(svnfst *fs, const char *path, aprpoolt *pool,
apr
poolt *commonpool);
/* openforrecovery() is like open(), but used to fill in an fs pointer
that will be passed to recover(). We assume that the open() method
might not be immediately appropriate for recovery. /
svnerrort *(
openfsforrecovery)(svnfst *fs, const char *path,
apr
poolt *pool,
apr
poolt *commonpool);
svnerrort (upgradefs)(svnfst *fs, const char *path, aprpoolt *pool,
apr
poolt *commonpool);
svnerrort (deletefs)(const char *path, aprpoolt *pool);
svn
errort (hotcopy)(const char *srcpath, const char destpath,
svn
booleant clean, aprpool_t *pool);
const char *(
getdescription)(void);
svn
errort (recover)(svnfst *fs,
svn
cancelfunct cancelfunc, void *cancelbaton,
aprpoolt *pool);

/* Provider-specific functions should go here, even if they could go
in an object vtable, so that they are all kept together. /
svnerrort *(
bdblogfiles)(aprarrayheadert **logfiles,
const char *path, svnbooleant onlyunused,
apr
pool_t *pool);

/* This is to let the base provider implement the deprecated
svnfsparseid, which we've decided doesn't belong in the FS
API. If we change our minds and decide to add a real
svn
fsparseid variant which takes an FS object, it should go
into the FS vtable. /
svnfsid_t *(
parseid)(const char *data, aprsizet len,
apr
poolt *pool);
} fs
libraryvtablet;

ファイルシステムごとの関数ポインタの構造体を取得するためには、
getlibraryvtable関数(fs-loader.cの200行目)を使って関数ポインタの構造体を取得する。
取得する際のキーには、svnfstype関数(fs-loader.cの217行目)からのファイルシステムタイプの情報を
使って、関数ポインタの構造体を取得している。
svnfstype関数では、リポジトリのdb/fs-typeファイル(内部的にはFSTYPEFILENAME定数でファイル名が
指定されている)の内容をそのまま返している。

次に、実際にリポジトリを開いて、詳細なバージョン情報を取得するところまでを見てみる。
先の説明でfslibraryvtablet構造体のopenfs関数からファイルシステムがオープンされる。
さらに、そのopen_fs関数は関数ポインタを保持しているだけなので、実体を探す必要がある。
ここでは、FSFSの中身を見ることとして、FSFSの関数ポインタのテーブルを以下に示す。

libsvnfsfsfs.c 314行目付近

static fslibraryvtablet libraryvtable = {
fsversion,
fs
create,
fsopen,
fs
openforrecovery,
fsupgrade,
fs
deletefs,
fs
hotcopy,
fsgetdescription,
svnfsfs_recover,
fs
logfiles
};

このテーブルからFSFSでファイルシステムをオープンするにはfsopen関数を用いていることがわかる。
fs
open関数からは、svnfsfsopen関数が呼び出されている。
ここで、svnfsfs
open関数の中身を見てみることにする。

libsvnfsfsfs_fs.c 1000行目付近

svnerrort *
svnfsfs_open(svnfst *fs, const char *path, aprpoolt *pool)
{
fs
fsdatat *ffd = fs->fsapdata;
apr
filet *uuidfile;
int format, maxfilesperdir;
char buf[APR
UUIDFORMATTEDLENGTH + 2];
aprsizet limit;

fs->path = apr_pstrdup(fs->pool, path);

/* Read the FS format number. */
SVNERR(readformat(&format, &maxfilesperdir,
path
format(fs, pool), pool));

/* Now we've got a format number no matter what. */
ffd->format = format;
ffd->maxfilesperdir = maxfilesperdir;
SVNERR(checkformat(format));

/* Read in and cache the repository uuid. */
SVNERR(svniofileopen(&uuidfile, pathuuid(fs, pool),
APRREAD | APRBUFFERED, APROSDEFAULT, pool));

limit = sizeof(buf);
SVNERR(svnioreadlengthline(uuidfile, buf, &limit, pool));
ffd->uuid = apr_pstrdup(fs->pool, buf);

SVNERR(svniofileclose(uuid_file, pool));

SVNERR(getyoungest(&(ffd->youngestrevcache), path, pool));

return SVNNOERROR;
}

この中で、readformat関数を呼び出している。
この関数がFSFSの詳細なフォーマットを取得するために用いられている関数である。
read
format関数の中では、いくつかの関数を呼び出して、PATH_FORMAT定数で与えられる
ファイル(dbformat)の中身を読み込んで、戻り値を返している。

これで、リポジトリを開いて、詳細なバージョン情報を取得するまでがわかった。
それでは、次に、最初に出ていたfslibraryvtablet構造体のopenfs関数が
どうやって呼び出されているのかを見てみる。

open_fs関数を呼び出している関数を探していくと次のようなコールグラフとなる。

openfs()
← svn
fsopen(libsvnfsfs-loader.c 385行目付近)
← getrepos(libsvnrepos
epos.c 1306行目付近)
← svnreposopen(libsvn_repos
epos.c 1368行目付近)

最後のsvnreposopen関数のソースコードは次のようになっている。

svnerrort *
svnreposopen(svnrepost *reposp,
const char *path,
apr
pool_t *pool)
{
/
Fetch a repository object initialized with a shared read/write
lock on the database. */

SVNERR(getrepos(repos_p, path, FALSE, FALSE, TRUE, pool));

return SVNNOERROR;
}

この中のsvnrepost変数について調べてみると、以下のような記述がある。

  • The Repository object, created by svnreposopen() and svnreposcreate(). / struct svnrepost { / A Subversion filesystem object. */ svnfst *fs;

/* The path to the repository's top-level directory. */
char *path;

/* The path to the repository's conf directory. */
char *conf_path;

/* The path to the repository's hooks directory. */
char *hook_path;

/* The path to the repository's locks directory. */
char *lock_path;

/* The path to the Berkeley DB filesystem environment. */
char *db_path;

/* The format number of this repository. */
int format;

/* The FS backend in use within this repository. */
const char *fs_type;

/* If non-null, a list of all the capabilities the client (on the
current connection) has self-reported. Each element is a
'const char ', one of SVNRACAPABILITY_.

aprarrayheadert *clientcapabilities;

/* Maps SVNREPOSCAPABILITYfoo keys to "yes" or "no" values.
If a capability is not yet discovered, it is absent from the table.
Most likely the keys and values are constants anyway (and
sufficiently well-informed internal code may just compare against
those constants' addresses, therefore). */
apr
hasht *repositorycapabilities;
};

最初の方に、repositoryオブジェクトを作成するには、svnreposopen関数か
svnreposcreate関数を呼び出す必要があるとなっている。
従って、リポジトリを開くには、svnreposopen関数を呼び出せばいいことがわかる。

以上のことをまとめると、svnreposopen関数を呼び出すと、関数の中でバージョン情報を取得したり
して、リポジトリを開くことがわかった。また、BDBとFSFSとの切り替えは、
関数テーブルを用意して、formatファイルの情報を元に使用する関数テーブルを切り替えている。

FSFSのバージョンごとの仕様の違いについては、libsvnfsfsstructureファイルに書いてある。
コード上では、以下のようにしてバージョンごとに振り分けて互換性を保つようになっている。

libsvnfsfsfs_fs.c 4803行目付近

この中のSVNFSFS_MINNOGLOBALIDSFORMAT定数は、libsvnfs_fsfs.hの中で定義されており、
バージョンの閾値を定数で保持している。

ここまでで、FSFSの互換性が保たれていることがわかる。

ここに来て、バージョンがどのように決定されるか気になってくる。次にそれを見てみる。
FSFSのバージョンが決定されるのは、もちろんファイルシステムが作成されたときであるので、
その部分を探してみるとsvnfsfs__create関数が見つかる。

libsvnfsfsfs_fs.c 5370行目付近

svnerrort *
svnfsfscreate(svnfst *fs,
const char *path,
aprpoolt *pool)
{
int format = SVNFSFS
FORMATNUMBER;
fs
fsdatat *ffd = fs->fsap_data;

fs->path = aprpstrdup(pool, path);
/* See if we had an explicitly requested pre-1.4- or pre-1.5-compatible. */
if (fs->config)
{
if (apr
hashget(fs->config, SVNFSCONFIGPRE14COMPATIBLE,
APR
HASHKEYSTRING))
format = 1;
else if (aprhashget(fs->config, SVNFSCONFIGPRE15COMPATIBLE,
APRHASHKEY_STRING))
format = 2;
}
ffd->format = format;

定数の定義は以下の通りとなっている。

define SVNFSCONFIGPRE14COMPATIBLE "pre-1.4-compatible"

define SVNFSCONFIGPRE15COMPATIBLE "pre-1.5-compatible"

つまり、「pre-1.4-compatible」と、リポジトリを作成するときに引数を
つけると、指定されたバージョンで作成されることが何となくわかる。
実際、「pre-1.4-compatible」と検索してみると、Format1の形式で作成されることが書いてある。

aprhashget関数は、APR = Apache Portable Runtimeの意味であり、そのうちのハッシュ機能が
この関数である。

この関数の使い方については、以下のページが大変参考になる。

ハッシュテーブルを使ってみる - kurukuru-papaの日記
http://d.hatena.ne.jp/kurukuru-papa/20080114/1200289172

fs->configがハッシュテーブルであり、これがどこから来るのかをたどってみる。
svnfsfs__create関数のコールグラフをたどると次のようになる。

svnfsfs_create
← fs
create(libsvnfsfsfs.c 180行目付近)
fslibraryvtablet構造体の関数テーブルをたどる
← svn
fscreate(libsvnfsfs-loader.c 352行目付近)

ここまでたどってきて、fs->configに値を代入している場所がわかる。
svnfscreate関数の中でsvnfsnew関数(libsvn_fsfs-loader.c 329行目付近)を呼び出しており、
その中で以下のように代入している。

svnfst *
svnfsnew(aprhasht fsconfig, aprpoolt *pool)
{
svn
fst *fs = aprpalloc(pool, sizeof(
fs));
fs->pool = pool;
fs->path = NULL;
fs->warning = defaultwarningfunc;
fs->warningbaton = NULL;
fs->config = fs
config;
fs->accessctx = NULL;
fs->vtable = NULL;
fs->fsap
data = NULL;
return fs;
}

svnfsnew関数では、単純に値を代入しているだけで、fs->configの実体を探すにはさらにたどる必要がある。
さらに、svnfscreate関数をたどると、svnreposcreate関数(libsvn_repos
epos.c)にたどり着く。
この関数の内容を以下に示す。

svnerrort *
svnreposcreate(svnrepost **reposp,
const char *path,
const char *unused
1,
const char *unused2,
apr
hasht *config,
apr
hasht *fsconfig,
aprpoolt *pool)
{
svnrepost *repos;
svnerrort *err;
const char *root_path;

/* Allocate a repository object, filling in the format we will create. */
repos = createsvnrepost(path, pool);
repos->format = SVN
REPOS_FORMATNUMBER;

/* Discover the type of the filesystem we are about to create. */
if (fsconfig)
{
repos->fs
type = aprhashget(fsconfig, SVNFSCONFIGFSTYPE,
APR
HASHKEYSTRING);
if (aprhashget(fsconfig, SVNFSCONFIGPRE14COMPATIBLE,
APR
HASHKEYSTRING))
repos->format = SVNREPOSFORMATNUMBER_LEGACY;
}

if (! repos->fstype)
repos->fs
type = DEFAULTFSTYPE;

/* Don't create a repository inside another repository. */
rootpath = svnreposfindrootpath(path, pool);
if (root
path != NULL)
return svnerrorcreatef(SVNERRREPOSBADARGS, NULL, ("'%s' is a "
"subdirectory of an existing repository rooted "
"at '%s'"), path, root
path);

/* Create the various files and subdirectories for the repository. */
SVNERRW(createreposstructure(repos, path, fs_config, pool),
_("Repository creation failed"));

/* Lock if needed. */
SVNERR(lockrepos(repos, FALSE, FALSE, pool));

/* Create an environment for the filesystem. /
if ((err = svnfscreate(&repos->fs, repos->dbpath, fsconfig, pool)))
{
/
If there was an error making the filesytem, e.g. unknown/supported
* filesystem type. Clean up after ourselves. Yes this is safe because
* createreposstructure will fail if the path existed before we started
* so we can't accidentally remove a directory that previously existed. */
svnerrorclear(svnioremove_dir2(path, FALSE, NULL, NULL, pool));
return err;
}

/* This repository is ready. Stamp it with a format number. */
SVNERR(svniowriteversionfile
(svn
pathjoin(path, SVNREPOS__FORMAT, pool),
repos->format, pool));

*reposp = repos;
return SVN
NO_ERROR;
}

よくよく見てみると、svnfsfs_create関数と同じような形をしていることに気づく。
この段階になってようやく気づいたのだが、もしかしたら、Subversionのリポジトリは、
リポジトリとファイルシステムで分離されているのかもしれない。これについては、
おそらく、設計を見てみればわかるはずだが、今回は特に気にしないことにした。
あと、SVN
FSCONFIGPRE15_COMPATIBLEに関する記述がないけど、これって大丈夫なのかな・・・?

svnreposcreate関数からさらにたどって、subcommand_create関数(svnadminmain.c 520行目付近)から
呼び出されており、さらにこの関数は、以下の変数の関数ポインタに設定されていることがわかる。

(svnadminmain.c 309行目付近)

/* Array of available subcommands.
* The entire list must be terminated with an entry of nulls.
*/
static const svnoptsubcommanddesct cmdtable[] =
{
{"crashtest", subcommand
crashtest, {0}, N_
("usage: svnadmin crashtest REPOS_PATH

"
"Open the repository at REPOS_PATH, then abort, thus simulating
"
"a process that crashes while holding an open repository handle.
"),
{0} },

{"create", subcommandcreate, {0}, N
("usage: svnadmin create REPOS_PATH

"
"Create a new, empty repository at REPOSPATH.
"),
{svnadmin
bdbtxnnosync, svnadminbdblogkeep,
svnadmin
configdir, svnadminfs_type, svnadminpre14compatible,
svnadmin
pre15compatible } },

おそらく、main関数を見てみるとコマンド引数とこの変数の値を使って、
いろいろと設定しているはずである。
これ以上追うと、また複雑な部分に入ってしまうので、ここら辺でconfigの値を追うのはやめておく。

最後に、コミットをしたときにリポジトリとファイルシステムが開かれるまでを見てみる。
svnコマンドのソースコードを見ると次のような記述が見つかる。

svnmain.c 380行目付近

{ "commit", svncl_commit, {"ci"},

おそらく、svncl_commit関数がコミットのコマンドラインでの処理を行うだろう、と仮定して
これを探ってみる。
ここからソースコードをたどってコールグラフを書くと次のようなコールの手順が見つかる。

svnclcommit(svncommit-cmd.c 40行目)
→ svn
clientcommit4(libsvnclientcommit.c 1348行目付近)
→ getraeditor(libsvnclientcommit.c 597行目付近)
→ svn
client_openrasessioninternal(libsvnclient
a.c 281行目付近)
→ svn
raopen3(libsvnra
a_loader.c 381行目付近)

RAとはRepository Accessの意味である。
svnraopen3関数では、RAの方法をどれにするか決定する。
RAはライブラリとして格納されており、以下のように定義されている。

libsvnra
a
loader.c 76行目付近

static const struct ralibdefn {
/* the name of this RA library (e.g. "neon" or "local") */
const char *ra_name;

const char * const schemes;
/
the initialization function if linked in; otherwise, NULL */
svnrainitfunct initfunc;
svn
rainitfunct compatinitfunc;
} ralibraries[] = {
{
"neon",
dav
schemes,

ifdef SVNLIBSVNCLIENTLINKSRA_NEON

endif

},

{
"svn",
svn_schemes,

ifdef SVNLIBSVNCLIENTLINKSRA_SVN

endif

},

{
"local",
local_schemes,

ifdef SVNLIBSVNCLIENTLINKSRA_LOCAL

endif

},

{
"serf",
dav_schemes,

ifdef SVNLIBSVNCLIENTLINKSRA_SERF

endif

},

/* ADD NEW RA IMPLEMENTATIONS HERE (as they're written) */

/* sentinel */
{ NULL }
};

このライブラリを用いて、各スキームの値とユーザが指定したURLの形式を比較して、
どのRAを使用するか決定する。RAのライブラリには初期化関数のポインタも入っているので
それを利用して初期化処理を行う。
ここでは、ローカルアクセスのRAを使用してアクセスすると仮定して話を進めていく。

RAの方法としては、localの他にDavを使った方法などがあり、これからは抽象化されて
アクセスすることができる。このために、ファイルシステムの抽象化のときと同じように
関数テーブルが用意されていて、それを用いてアクセスするようになっている。
関数テーブルの定義は以下のようになっている。

libsvnra
a
loader.h 34行目付近

/* The RA layer vtable. /
typedef struct svnravtablet {
/
This field should always remain first in the vtable. /
const svnversiont *(
get_version)(void);

/* Return a short description of the RA implementation, as a localized
* string. /
const char *(
get_description)(void);

/* Return a list of actual URI schemes supported by this implementation.
* The returned array is NULL-terminated. /
const char * const *(
getschemes)(aprpool_t *pool);

/* Implementations of the public API functions. */

/* All fields in SESSION, except priv, have been initialized by the
time this is called. SESSION->priv may be set by this function. /
svnerrort *(
opensession)(svnrasessiont session,
const char *reposURL,
const svn
racallbacks2t *callbacks,
void *callbackbaton,
apr
hasht *config,
apr
pool_t *pool);
/
URL is guaranteed to have what getreposroot() returns as a prefix. /
svnerrort *(
reparent)(svnrasession_t *session,
const char *url,

例えば、localであれば以下の関数テーブルが使用される。

libsvnra
a
plugin.c 1389行目付近

static const svnravtablet ralocalvtable =
{
ralocalversion,
svnralocalgetdescription,
svn
ra_local
getschemes,
svn
ralocalopen,
svn
ralocalreparent,
svn
ralocalgetsessionurl,
svn
ralocalgetlatestrevnum,
svn
ralocalgetdatedrevision,
svn
ralocalchangerevprop,
svn
ralocalrevproplist,
svnralocalrevprop,
svn
ra_local
getcommiteditor,
svnralocalgetfile,
svn
ra_local
getdir,
svn
ralocalgetmergeinfo,
svnralocaldoupdate,
svn
ra_local
doswitch,
svn
ralocaldostatus,
svnralocaldodiff,
svn
ra_local
getlog,
svn
ralocaldocheckpath,
svn
ralocalstat,
svn
ralocalgetuuid,
svnralocalgetreposroot,
svnralocal
getlocations,
svn
ralocalgetlocationsegments,
svn
ralocalgetfilerevs,
svn
ralocallock,
svn
ralocalunlock,
svn
ralocalgetlock,
svnralocalgetlocks,
svn
ra_local
replay,
svnralocalhascapability,
svn
ra_local
replay_range
};

さて、ここまででRAが抽象化されていることがわかり、リポジトリをオープンする部分の
手前まで来ている。
再度、svnraopen3関数を読み進めていくと、関数テーブルのうちのopensession関数を呼び出している。
open
session関数は関数テーブルで抽象化されて、ローカルアクセスの場合は、
svnralocalopen関数(libsvnralocal
aplugin.c 411行目付近)となる。
svn
ra_local
open関数では、svnralocalsplitURL関数(libsvnralocalspliturl.c 26行目付近)を呼び出している。
svnralocal
splitURL関数の中を見ると、ようやくお目当ての関数にたどり着く。
下の方にsvn
repos_open関数があり、この関数でリポジトリを開いていることがわかる。

svnralocalsplitURL関数でリポジトリを開くというのも、なんとなく納得がいかない感じもするが
svn
ra_local
split_URL関数の呼び出し元の処理を見ていると、どうもこの関数でオープンの処理を
しているようである。

長々とSubversionの一部について書いてみたけど、これで5時間近くかかった・・・。
ただ、内部構造もなんとなくわかったし、実際に運用されているもののコードを見るというのも勉強になって良い。
バージョン管理ソフトなら、実際にどのようにバージョンを保持しているのか、とか、そういうことを
調べてみるのも良かったのかもしれないが、今回は、ファイルシステムの互換性が保たれるかどうか
ということが知りたかったので、そこら辺を重点的に調べてみた。
Blogなどで誰かが書いてあるものを読んで理解するのも良いが、ソースコードが公開されているのであれば、
そのような情報を探すだけではなく、直接ソースコードを見た方が早い場合もあるだろう。

間違っている部分などありましたら、指摘していただければ幸いです。
また、日本語が書き殴ったような文章になっているのは、すいません。
時間があるときにでも、もう少しまともにまとめたいと思っています。

Posted at : 2008-10-15 15:54:07 / Category : none

Comments

まだコメントはありません / No comment.

Send comment


Name


Mail-address (empty is OK. If you want to notify update, please fill mail-address.)


Bot check code (240426 と入力してください / Please input 240426.)