*[hatefu:labs.yaneu.com/20090725/] Transactional NTFS(TxF)の活用

ASP.NETでサイトを作っている。JavaScriptのファイルを動的に生成したいのだが、毎回動的に生成するのはオーバーヘッドが無視できないのでその生成は1日に1回にしたい。この生成して.jsファイルを書き換えているときにアクセスされるとどうなるだろうか?

今回はこの問題について考える。

* ファイルのリネームによる方法

まず思い浮かぶのはファイルのリネームによる方法だろう。

** hoge.tmpに書き出す
** hoge.jsをhoge.bakにリネーム
** hoge.tmpをhoge.jsにコピー

これだと、hoge.jsをhoge.bakにリネームした直後は、hoge.jsは存在しないことになるのでその瞬間にこのファイルを要求されるとfile not foundになる。実際には、ごく短い時間にこの一連の処理が行なわれるので、現実的な運用ではほとんどあり得ないことだが、それでも気持ち悪いことこの上ない。


* ファイルのコピーによる方法

次にファイルの上書きコピーだとどうだろうか?すなわち次の手順である。

** hoge.tmpに書き出す
** hoge.tmpをhoge.jsに上書きコピー

IISはjsファイルは読み取り専用で開くので、IISがjsファイルにアクセス中であってもhoge.jsファイルを書き換えること自体は可能である。しかし、ファイルをコピーした瞬間、古いファイルをIISがクライアントブラウザに転送中だった場合、その転送が中断され、結果として不完全なファイルがクライアントブラウザに渡される。これはまずい。

ここで望む挙動は、古いhoge.jsの内容か、新しいhoge.jsの内容を完全な形でクライアントブラウザに渡してくれることであって、こんな中途半端な状態の不完全なファイルをクライアントブラウザに渡されるとJavaScriptの実行エラーになってしまう。


* では普通はどうするのか?

私はASP.NETは初心者同然だが、ASP.NETの世界は結構、スタンダードな方法について語るのが難しい。
例えば、前回、タスクスケジューラで定期実行を行なえばどうかという話をしたら、

** 普通タスクスケジューラを使うだろ、常識的に考えて。
** 特定のaspxにIEでアクセスするWSHスクリプトをタスクスケジューラに登録する
** タスクスケジューラなんか使うと移植性に問題があるだろ。クロスプラットフォームで動くようにしなきゃ
** cronでいいじゃん
** DB関係の処理はチームのDBのエキスパートがストアドで書いて、他の開発メンバーはストアド以外にアクセスできないようにカプセル化すべき。だから、定期実行する必要があるならSQL Serverのjob実行で行なうべき。
** だいたいタスクスケジューラかJP1とか持ち出される

などと予想外の答えがたくさん返ってきた。おかげで私は、みんなが普通だと思っていることは全然普通ではないということが良く分かった。また私が普通だと考えていることが、全然普通でもないということも同時に思い知らされた。

今回は、Transactional NTFS(以下、TxFと略す)について解決しようと思っているのだが、これもVista以降(Windows Server 2008以降)でしか使えないので、

** Windows Server 2003で動かないじゃん
** それだとクロスプラットフォームで使えないよ
** 普通に要求ごとにDBから取り出そうよ

などと言われても仕方がないが、まあ、「Vista/Windows Server 2008でNTFSならTxFで解決する方法があるよ」という程度に理解して欲しい。

* TxFをmanaged codeから使う

** TxFとは何か?

ファイルシステムに対して、トランザクションを実行できる。commitするまでは任意のタイミングでrollbackできるのが最大の利点だが、ここでは、ファイル置換のatomic性を保証するのに用いる。

まず、てっとり早くC#から使える形でwrapする。
>>
using System;
using System.Runtime.InteropServices;

namespace TxF
{
	internal class TransactedAPI
	{

		[DllImport("Ktmw32.dll")]
		public static extern IntPtr CreateTransaction(IntPtr securityAttributes, IntPtr guid, int options, int isolationLevel, int isolationFlags, int milliSeconds, string description);

		[DllImport("Ktmw32.dll")]
		public static extern bool CommitTransaction(IntPtr transaction);

		[DllImport("Kernel32.dll")]
		public static extern bool CloseHandle(IntPtr handle);

		[return: MarshalAs(UnmanagedType.Bool)]
		[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
		public static extern bool MoveFileTransacted(
			[In] string lpExistingFileName,[In] string lpNewFileName,
			[In] IntPtr lpProgressRoutine, [In] IntPtr lpData,
			[In] ulong /*MoveFileFlags*/ dwFlags,
			[In] IntPtr /*KtmTransactionHandle*/ hTransaction);
		// see. ttp://msdn.microsoft.com/en-us/library/aa365241(VS.85).aspx
	}

	public class Transaction : IDisposable
	{
		public Transaction()
		{
			Handle = TransactedAPI.CreateTransaction(IntPtr.Zero, IntPtr.Zero, 0, 0, 0, 0, null);
		}

		public bool MoveFile(string srcPath,string dstPath)
		{
			return TransactedAPI.MoveFileTransacted(srcPath, dstPath, IntPtr.Zero, IntPtr.Zero,
			              0x01 /* MOVEFILE_REPLACE_EXISTING */ , Handle);
		}

		public bool Commit()
		{
			return TransactedAPI.CommitTransaction(Handle);
		}

		public bool Close()
		{
			bool result = true;
			if (Handle!=IntPtr.Zero)
			{
				result = TransactedAPI.CloseHandle(Handle);
				Handle = IntPtr.Zero;
			}
			return result;
		}

		public void Dispose()
		{
			Close();
		}

		public IntPtr Handle { get; set; }
	}
}
<<

# 参考資料
WINDOWS VISTA - INTRODUCING TXR IN C# (PART 1)
http://bartdesmet.net/blogs/bart/archive/2006/12/14/Windows-Vista-_2D00_-Introducing-TxR-in-C_2300_-_2800_Part-1_2900_.aspx ファイル システム トランザクションを使用してアプリケーションを強化する
http://msdn.microsoft.com/ja-jp/magazine/cc163388.aspx#S2 ** ファイルの置き換えにTxFを用いる さて、準備が出来たので、ファイルの置き換えにTxFを用いてみよう。 >> using (var tx = new TxF.Transaction()) { result = tx.MoveFile("hoge.tmp", "hoge.js"); tx.Commit(); } << たったこれだけである。 * TxFを用いるとどうなるのか? 巨大なテキストファイルを作成して、次のような実験を行なった。 >> for (int i = 0; i < 100; i++) using (var tx = new TxF.Transaction()) { result = tx.MoveFile("test"+i+".tmp", "test.txt"); tx.Commit(); } Thread.Sleep(100); } << Thread.Sleepで0.1秒ごとに待っている。巨大なテキストファイルは0.1秒ではダウンロードが完了しないので不完全な状態でクライアントブラウザに表示されるかに見えるが、結果はそうではない。いかなるタイミングでアクセスしても完全な形でブラウザには表示された。 これはすなわち、tx.MoveFileでファイルが上書きされるが古いほうのファイルに依然としてIISがアクセスを続けているということだろう。(大きなファイルなのでIISがcacheしているとは考えにくいが、調べていないのでよくわからない。) また、上のプログラムをSystem.IO.File.Copyを用いたのではこういう挙動にはならず、クライアントブラウザには不完全なテキストファイルが表示された。System.IO.File.Copyもtx.MoveFileのような仕様になっていたほうが使いやすいと思う。 TxFを用いなくともWindows APIのCreateFileを使えば、上のtx.MoveFileのような挙動にすることが出来るのかも知れない。これについては試していない。 * まとめ 今回は、TxFをmanaged codeから利用する方法を紹介した。 また、TxFを用いると、上書きコピーをatomicに行なえることを確かめた。 しかし、私自身、TxFやIISの動作について詳しく理解していないのでこの結果は誤っているかも知れない。 はてぶなどでご指摘いただけると幸いである。 * 今回のソースの利用について 今回出てきた私の書いたソースは自由にコピペして使ってもらって構わない。 * 更新履歴 2009.07.25 公開