hglock エクステンションによる Mercurial でのファイルロック
先日のエントリで:
(『DVCS はファイルのロックは取れないのですか?』という質問の) 答えは勿論 No なのですが〜
と書いたところ、MARUYAMA 氏から以下の指摘が:
へぇ、LockExtension(hglock) なんてものがあったのかぁ。TimesstampMod エクステンションも見落としていたし、非同梱エクステンションは、あんまりチェックしてないので、認識漏れ等あれば、随時指摘してもらえると嬉しいです > 各位
で、それじゃぁ、実際にどういった原理で「ロック」を実現しているのか?というところは、当然非常に気になるところなので、早速ソースをチェックして、色々実験してみることに。
hglock 利用の手順/標準フロー
● 共有リポジトリでの設定
共有リポジトリ上では、以下の事前準備が必要となる。
- hglock エクステンションを有効化
- "hg init-lock" を実行: 「ファイルロック」情報格納用の管理ファイルを生成 (詳細は後述)
「ファイルロック」情報の管理を行うためには、共有リポジトリ上で hglock エクステンションが常時有効になっていなければならない点に注意。
● 「ファイルロック」操作
「ファイルロック」に関する明示的な操作には以下のものがある。
- "hg lock FILE": 対象の「ファイルロック」を獲得
- "hg unlock FILE": 対象の「ファイルロック」を解放
- "hg locks": 現在の「ファイルロック」一覧を表示
「ファイルロック」情報は、共有リポジトリ側で一元管理されるので、上記コマンドの実行では:
- "default-lock" ないし "default" 接続先が paths セクションで設定されていて、且つ
- 対象リポジトリがアクセス可能な状態にある(e.g.: ネットワーク接続等の保証)
という条件を満たす必要がある。
余談になるが、"default-lock"(「ファイルロック」情報管理) と "default" (「履歴情報」の取り込み/反映)が分離できることで、「ファイルロック」管理を一元化しつつ、履歴取り込み/反映は負荷分散、みたいな構成も可能である点は嬉しいところ。
他のユーザによって「ファイルロック」獲得済みのファイルに対しては、ローカルリポジトリにおける変更の commit 禁止や:
$ hg showconfig ui
ui.username = bar@bar.com
$ hg locks
foo@foo.com default 1h55m a
$ hg status
M a
$ hg commit
a: the file is locked by foo@foo.com, modifications ignored
変更なし
$
当該ファイルへの変更を含むリビジョンの、共有リポジトリへの push が抑止される。
$ hg push 中断: rejecting push because of unlocked files: a (pull to get the latest versions of the files, lock, merge your changes, push again) $
「ファイルロック」は、先述した "hg unlock FILE" でも解放できるが、当該ファイルに関する変更が共有リポジトリに対して push された契機でも解放される。
なお、「ファイルロック」は名前付きブランチ単位に管理されているため、あるファイルの「ファイルロック」が獲得済みであっても、別な名前付きブランチ上であれば、当該ファイルの改変は可能である ("hg locks" コマンドの仕様も「display locked files on the current branch」)。
これは、「開発用メインライン」と「リリースライン」等で、作業が完全に分離されるようなケースを想定しているのだろう。
また、hglock 有効時は --force 付きの push (= 複数ヘッドの反映) ができないのだが、これも、各名前付きブランチ毎に改変実施対象リビジョンを1つに限定(= 複数ヘッドの禁止)するため、と考えれば納得が行く。
ちなみに、「ファイルロック」の獲得ユーザは、ui.username 設定情報を元にした「ユーザ」に紐付けられるので、同一ユーザ設定であれば、別の作業領域での改変は可能になってしまうので注意 (Subversion の「ファイルロック」主体は「作業領域」に紐付けられる模様)。
● ".hglocks" ファイルによる read-only update
作業領域のルートに ".hglocks" ファイルがある場合、".hglocks" 中に書かれたパターンに合致するファイルは、「ファイルロック」を獲得するまでは、作業領域の update の際には常に read-only で取り出される: Subversion で言うところの svn:needs-lock 属性の設定に相当。
".hglocks" ファイルの書式は、".hgignore" と同じとのこと。
例えば、"hg lock FILE" での「ファイルロック」獲得のみで運用した場合、ユーザ A による「ファイルロック」獲得と、ユーザ B による「ファイルロック」獲得無しでの改変コミット (= 手順遵守違反) の順序が前後してしまうと、ユーザ B による commit 実行が防止できない。
ここで ".hglocks" を導入すると:
- 「ファイルロック」未獲得ファイルは read-only で作業領域に取り出し
- 改変実施時点で「書き込み不可」が検出
- 「ファイルロック」獲得が必要なことを思い出す
- 「ファイルロック」獲得を実施
といったフローを保証することができる。
なお、hglock のドキュメント上は「ファイルに対する強制ロック (mandatory lock)」というような言い方をしているけれど、ファイルに直接書き込み属性を追加することは容易であるので、あくまで「hglock のレイヤ上での強制ロック」(= 実質は「助言ロック (advisory lock)」)と考えておいた方が良い。
ちなみに、Windows のアクセス制御機構と組み合わせた場合に、当該ファイルが期待通りに read-only になるのかは未確認なので、実際に利用する人は要事前確認ということで(笑)。
hglock による「ファイルロック」の実現方式
hglock による「ファイルロック」の実現は、phase や bookmark の実現にも使用されている、pushkey 機構を使用している。
Mercurial の pushkey 機構は、簡単に言うと「namespace + key に対して value の設定/取得を行う」仕組みで、値の情報は履歴とは独立して管理されている。
hglock では、pushkey により設定された情報 (= 「ファイルロック」情報) は、共有リポジトリの .hg/locks ファイルに記録される: "hg init-lock" によって作成されるのはこのファイル。
「ファイルロック」が獲得された状況でのファイルの内容は以下の様な感じ:
(('default', 'a'), ('foo@enabled1', 1338110963))
格納されている情報は、順に「対象名前付きブランチ」/「対象ファイル」/「獲得主体」/「獲得日時」。
作業用のリポジトリでは、hglock によって埋め込まれた確認用コードが、共有リポジトリに対して pushkey の問い合わせを行い、当該ファイルの改変可否を確認する、という流れ。
hglock の問題点
hglock での書き込み禁止の実現は、「ファイルロック」情報こそ共有リポジトリ側で一元管理されているものの、基本的にはクライアント側での改変操作禁止がメインとなっている。
そのため現状では、「hglock エクステンションを有効」にするという運用ルールを守らないユーザによって、「ファイルロック」を獲得せずに実施された変更が共有リポジトリに反映される事は防止できない (履歴が反映された契機で、当該ファイルに対する「ファイルロック」は解放される)。
この辺は、pretxnchangegroup 用の防衛的なフック実装を hglock に追加して、共有リポジトリ側でそれを有効にすれば、少なくとも運用ルールを守っているメンバーには迷惑が掛からない、という落とし所に持っていけるような気がする。っていうか、誰か追加実装を書いて、開発元に pull request を送ってみない?
ちなみに、現行の獲得ユーザとは別のユーザによる操作(e.g.: 強制解放や、先述した不正改変の反映)を契機に「ファイルロック」の獲得/解放が実施された場合、メールによる通知が行われる実装になっているのだけれど、メール通知 ⇒ 「ファイルロック」情報の記録/破棄の順で実行されているのに、メール通知の失敗 (e.g.: ネットワーク不通) 時に、例外が投げっぱなしになっているため、「ファイルロック」管理情報の記録が更新されない(笑)っぽい感じなので、バグ報告を送っておいた。