最近お仕事で開発している SignalR 製の Web アプリケーションが本日...
を遂げるという怪奇事件が起こりました。常時接続なので落ちないように結構気を配っていたので、かなり思わぬ事態でした。何事かとイベントビューアーでログを見ていると、ODP.NET を使ったデータベースアクセスで AccessViolationException が発生して捕捉できずに死んでいる模様。ちゃんと try/catch しているのに一体どういうことだ...。
例外捕捉の挙動変更
MSDN のドキュメントによると、AccessViolationException は主にマネージド領域外のメモリに対するアクセス違反が発生した際にスローされるようです。以下はその抜粋。
割り当てられていないメモリ、またはコードがアクセス権を持たないメモリに対して、コードが読み取りまたは書き込みを試行すると、アンマネージコード (アンセーフコード) でアクセス違反が発生します。
しかしそうは言っても普通の System.Exception の派生クラスなわけなので、どうあろうと catch 句で捕捉できるはずでアプリケーションが突然の死を遂げるほどではない...と思っていました。しかしドキュメントを読み進めてみると以下のように書いてあります。
.NET Framework 4 以降では、共通言語ランタイムによってスローされる例外 AccessViolationException は構造化例外ハンドラーの catch のステートメントによって例外が共通言語ランタイムによって予約されているメモリの外部にある場合は処理されません。AccessViolationException のこのような例外を処理するには、例外がスローされる HandleProcessCorruptedStateExceptionsAttribute メソッドに属性を適用する必要があります。(中略).NET Framework の以前のバージョン用に記述されたコードに .NET Framework 4 で変更なしでコンパイルし、実行するアプリケーションの構成ファイルに <legacyCorruptedStateExceptionsPolicy> 要素を追加できます。
30 年ぐらい日本人やってる僕にもだいぶ難しい日本語ですが、要点をまとめると.NET Framework 3.5 以前は AccessViolationException を catch できていたが、.NET Framework 4 以降ではできなくしたということです。だいぶ破壊的変更ですね。
捕捉方法 1 : HandleProcessCorruptedStateExceptionsAttribute
AccessViolationException を catch したいメソッドに HandleProcessCorruptedStateExceptionsAttribute を付けれることで捕捉可能となります。以下のような感じです。
[HandleProcessCorruptedStateExceptions] void DoSomething() { try { AccessViolationExceptionが発生する何か(); } catch (AccessViolationException e) { //--- 捕捉可能 } }
局所的に対応する場合に使えそうです。
捕捉方法 2 : legacyCorruptedStateExceptionsPolicy
後方互換のため、これまで (.NET Framework 3.5 以前) 通りアプリケーション全体で捕捉できるようにしたい方もいるでしょう。その場合は構成ファイル (*.config) に <legacyCorruptedStateExceptionsPolicy> 要素を入れることで解決できます。
<configuration> <runtime> <legacyCorruptedStateExceptionsPolicy enabled="true" /> </runtime> </configuration>
こっちは対応が簡単ですね。
アクセス違反の例外を捕捉すべきか
.NET Framework 4 以降で挙動が変更されたことにはそれなりに理由があります。というのも、すでにメモリアクセス違反が発生してしまったものを復旧しつつ挙動させることは極めて困難だということです。保証できない状態になっているのを無視してアプリケーションを継続してしまう場合がほとんどなので、それぐらいならアプリケーションをクラッシュさせる方がよほどマシとの考えでしょう。中途半端になって 2 次災害を引き起こすよりも突然の死を選ぶ方が安全である、と。
アクセス違反を起こすような破壊状態の例外に関する内容は、以下が大変詳しいのでオススメです。
推定される原因
さて、今回 AccessViolationException が発生したところは ODP.NET の内部です。完全には検証できていないのですが、状況からするとだいたい以下のような感じです。
- 一意制約違反になるデータを merge 文 (Upsert) でバルク処理
やはりバルク処理はそういう諸々のエラーが絶対起こらないようにして実行しないと危険ということか...(白目