はじめに
過去記事は「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ソース