LoginSignup
0

posted at

【logic,util】ファイルロックに関する見直し

はじめに

過去記事は「auカブコム証券のkabuステーションREST APIに関する記事一覧」。

当初、Jenkinsからプロセス起動されるので、REST APIへログインした際に発行されるトークンをファイルで管理するため、ファイルロックを使って、排他制御をしていた。
毎回プロセス起動すれば、メモリ解放も、ネットワーク解放も、ファイルロックの解放も、何もしなくても、自動で後片付けしてくれます。

最近、毎回プロセス起動をするのも、JavaVMの初期化のオーバーヘッド、短時間の実行にJITが効くのかどうか、ということから、軽いServlet APIレベル(Spring未使用)でHTTP Get/Postで処理実行をトリガーするWebアプリに移植したところ、特にファイルロック周りに手抜きがあったので、一つづつJUnitテストを作りながら改善していく。

全体のクラス構成

main()から近い部分から、最も深い部分まで、深い方からクラスを並べると以下の順となる(古いソースは省く)。

  • 1 util.FileLockUtil
  • 2 logic.FileLockLogic
  • 3-1 util.LockedAuthorizedTokenUtil
  • 3-2 logic.ChartDataLogic.ChartInfo
  • 3-3 v44.MainChartData_r16

1. util.FileLockUtil

staticメソッドのみで、呼び出し元がFileLockUtil.LockInfoインスタンスを生成して、引数で渡す。

JUnitテストを作ったところ、unlock()の後にLockInfoをクリアしていない。事前にnullチェックしているため、release()やclose()したらnullで初期化する。

			if (lockInfo.fl != null) {
				lockInfo.fl.release();
				lockInfo.fl = null; // 追加
			}

			if (lockInfo.fc != null) {
				lockInfo.fc.close();
				lockInfo.fc = null; // 追加
			}

同じファイルを2回lock()すると、bShared=true(共有ロック)でもOverlappingFileLockExceptionが発生するが、これは同じJavaVMの場合はこの例外が発生し、違うプロセスのJavaVM(や他の言語のツール)でlock()すると成功する。

		FileLockUtil.LockInfo lock1 = new FileLockUtil.LockInfo();
		FileLockUtil.LockInfo lock2 = new FileLockUtil.LockInfo();
		boolean bShared = true;
		String filepath = FILEPATH;
		try {
			boolean a1 = FileLockUtil.lock(lock1, bShared, filepath);
			assertTrue(a1);
			assertNotNull(lock1.fc);
			assertNotNull(lock1.fl);
			try {
				FileLockUtil.lock(lock2, bShared, filepath);
				fail("must throw OverlappingFileLockException.");
			} catch (OverlappingFileLockException e) {

lock()された情報がLockInfoインスタンスに保持している状態で、さらにlock()して上書きしてしまうと、もうunlock()できなくなるため、IllegalStateExceptionを投げる。

		if (lockInfo.isLocked()) {
			throw new IllegalStateException();
		}

2 logic.FileLockLogic

FileLockUtilはv4,v7,v9のLockedAuthorizedTokenで直接使っていたが、他のファイルでもファイルロックするため、コンストラクタにファイルパスを渡して、インスタンスごとにファイルロックできるようにした。

FileLockUtilと同等のJUnitテストを作成したら、FileLockUtil側の修正でカバーされたので、javadocコメントにRuntimeExceptionの情報を追記した。

3-1 util.LockedAuthorizedTokenUtil

最も問題のあるユーティリティクラス。
各ツールのmain()で以下のようなtry-finallyでlock-unlockをしている(ように見える)。

		String X_API_KEY = LockedAuthorizedTokenUtil.lockToken();
		try {
			new MainTrailOrder_r6(X_API_KEY).execute();
		} finally {
			LockedAuthorizedTokenUtil.unlockToken();
		}

しかし、lockToken()がファイルロックするだけでなく、REST APIのログインも実行しており、ファイルロックは成功し、REST APIのログインに失敗した際に、例外を投げるが、ファイルロックされたまま例外を投げてくる。

したがって、lockToken()の成功、失敗のどちらでもunlockToken()を呼ばなければならない。
この場合、lockToken()の例外が、OverlappingFileLockExceptionやIllegalStateExceptionの場合でも、LockInfoインスタンスの中身はnullのままなので、unlockToken()を呼んでも問題ない。

		try {
    		String X_API_KEY = LockedAuthorizedTokenUtil.lockToken();
			new MainTrailOrder_r6(X_API_KEY).execute();
		} finally {
			LockedAuthorizedTokenUtil.unlockToken();
		}

呼び出し側と、呼び出され側のどちらが問題かと考えれば、ファイルロックとREST APIのどちらも成功か、どちらかが失敗したら呼び出す前の状態に戻る、のが自然なので、LockedAuthorizedTokenUtilを修正する。

	public static String lockToken() throws ApiException {
		fileLockLogic.lockFile();
		try {
			return singleton.initToken();
		} catch (ApiException e) {
			fileLockLogic.unlockFile();
			throw e;
		}
	}

なお、元々コンソールアプリのmain()で例外を投げるので、そのままプロセスが落ちて綺麗にリソースが解放されるが、WebアプリのServletに書き換えたら、ファイルロックされたままなので、Webアプリまたはアプリサーバーを再起動するまで実行できなくなる。

テストコードは、以下のように2回呼び出すが、REST APIがログインエラーとなる環境の場合にのみ成功する。
修正前では、2回目のlockToken()でIllegalStateExceptionとなる。
JUnitテストでビルド時にテストするにはREST APIの結果を動的に変更する仕組みが必要のため、普段は無効にしておく。

		try {
			LockedAuthorizedTokenUtil.lockToken();
			fail("must throw ApiException.");
		} catch (ApiException e) {
			e.printStackTrace();
		}
		try {
			LockedAuthorizedTokenUtil.lockToken();
			fail("must throw ApiException.");
		} catch (ApiException e) {
			e.printStackTrace();
		}

3-2 logic.ChartDataLogic.ChartInfo 3-3 v44.MainChartData_r16

LockedAuthorizedTokenUtilではメソッドを抜けても、ファイルロックされた状態が長時間続くが、今回のクラスはファイルロックのみを行っているため、あまり問題は起きない。

			fileLockLogic.lockFile();
			try {
				List<String> lines = FileUtil.readAllLines(filepath);
                
			} finally {
				fileLockLogic.unlockFile();
			}

try-with-resources対応

最近は、ファイルI/Oなどは、try-with-resourcesを使うことが一般的なので、AutoCloseableインターフェイスを実装することで、unlock()を明示的に呼ぶ必要が無くなる。
ただし、コンストラクタからclose()までの間にロックされるので、自由にlock-unlockしたい場面では使えない。
せっかくなので、finalize()も実装しておいた。

util.LockedAuthorizedTokenUtilをラップしたlogic.LockedAuthorizedTokenLogicを作ってみると、ほとんど中身がない。

public class LockedAuthorizedTokenLogic implements AutoCloseable {

	private String X_API_KEY;

	public LockedAuthorizedTokenLogic() throws ApiException {
		this.X_API_KEY = LockedAuthorizedTokenUtil.lockToken();
	}

	public String getApiKey() {
		return X_API_KEY;
	}

	public void close() {
		X_API_KEY = null;
		LockedAuthorizedTokenUtil.unlockToken();
	}

	@Override
	protected void finalize() throws Throwable {
		LockedAuthorizedTokenUtil.unlockToken();
	}

}

JUnitテストを作ったら、アプリ全体(プロセス)をロックかけて1つしか実行させないときに便利かもしれない。

	public void lockTokenTest1() throws ApiException {
		try (LockedAuthorizedTokenLogic lock = new LockedAuthorizedTokenLogic()) {
			assertNotNull(lock);
			assertNotNull(lock.getApiKey());
		}
		try (LockedAuthorizedTokenLogic lock2 = new LockedAuthorizedTokenLogic()) {
			assertNotNull(lock2);
			assertNotNull(lock2.getApiKey());
		}
	}

githubソース

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0