Operational Notifications Exception Design / ONED for DevOps

&gt; to English Pages 
https://kyowg.blogspot.com/2018/05/operational-notifications-exception.html

## 1. 要約
このポストでは、開発速度、品質、機会損失などの改善サイクルの向上を目的とした DevOps のサービス運営視点に基づく Web アプリケーションのドメインロジック例外処理設計の一例として、Operational Notifications Exception Design / ONED をご紹介します。
（私的研究論文を抜粋しました。）

## 2. 注記
### 2-1. 記事の有効範囲
* この記事は、ONED の概念の解説が目的のため、検査例外、非検査例外、その他の例外メカニズムなどまでは言及しません。
* この記事では、Web アプリケーションに限定し、汎用ライブラリ、汎用ツール、ローカルアプリケーションは対象外とします。
* 開発言語によって構文が異なるため、適宜読み替えてください。
* 例外の言葉の定義は対象外です。

### 2-2. ONED 固有の表現
* ONED
Operational Notifications Exception Design を “ONED” と定義します。

* イベント例外クラス
SyntaxException や ArgumentException などのイベントベースの例外クラスを “イベント例外クラス” と定義します。

* 例外トリガー
throw や raise 構文などを “例外トリガー” と定義します。

* 例外キャプチャ
catch や resucue や except 構文などを “例外キャプチャ” と定義します。

* 例外インスタンス
例外クラスのインスタンスを “例外インスタンス” と定義します。

## 3. ONED の概念
### 3-1. 例外クラスの構造
ONED が提唱する例外クラスの構造は以下の通りとなります。
下記の要点と有用性を追って解説していきます。
（構文は開発言語によって適宜読み替えてください。）

Example 1: 例外クラス

```
InternalCriticalException("messages: HTTP_STATUS_CODE")
```


### 3-2. 責任分界点
Example 2: 例外クラス名

```
InternalCriticalException
```

Example 2 の例外クラス名の接頭辞 “Internal” は責任分界点を意味します。
ONED で推奨する責任分界点の例は以下の通りとなります。

```conf:
# Internal
内部システム： 例外が発生したホストの責任
例えば、この事例の場合は Web サーバーなど...

# External
外部システム： 外部システムの責任
例えば、Google APIs のような外部システムなど...

# Medial
中間部システム： 組織内リソースの責任
例えば、Web サーバーからみた DB サーバーなど...

# Terminal
クライアントシステム： クライアントの責任
例えば、Web クライアントや、API クライアント、ユーザーデバイスなど...
```

このように、責任分界点を引いて、例外発生の責任を明確にしています。
例外発生の責任の明確化の有用性は、システム障害の初動対応におけるリカバリー速度の向上が見込める事です。
もし、アプリケーション開発経験が豊富なトップエンジニアさんは、この解説だけでこれらの有用性がご理解いただけるかもしれません。
要するに、ONED の概念は、サービス運営を意識したコーディングを重視しているということになります。
次のセクションで、ONED における重要度の考え方について解説します。

### 3-3. 例外レベル
Example 2: 例外クラス名

```
ExternalCriticalException
```

上記のミドル Fix の Critical は、例外レベルを表します。
通常、ロギングなどで使われるエラーレベルは、重要度を表すことが多いと思いますが ONED の例外レベルでも重要度によって段階分けされます。

しかし、たまに以下のような質問を受けることがあります。
「どの重要度の例外を配置したら良いかわからない。」
「どの事象が重要でどの事象が重要じゃないかわからない。」
「これらの概念を使いこなすにはある程度の経験が必要だ。」

決してそういうことではありません。
それをいまから解説します。
例えば、「ファイルが存在しない」という例外処理コードを実装している最中だとします。
もし、サービスリリース後にこの例外が発生したら、開発責任者は、全てのケースにおいて緊急メール通知を受けるべきでしょうか？
もし、自動回復処理によって後続処理の継続が可能な例外ならば、緊急メール通知ではなく、ロギングに留める方が生産的という考え方もあるかもしれません。
全てをメール通知する事によって、もしかしたら緊急体制の形骸化を招くかもしれません。
ONED における例外レベルの定義は、運営中に実行されなければならない自動処理の内容に過ぎないという事です。
言い換えれば、運営中にどのような処理を例外ハンドラに実行させておきたいかという事になります。

* ログを吐きたいか？
* ユーザーにエラー画面は出力すべきか？
* 処理は継続するべきか？
* 緊急メールで通知して欲しいか？
* 緊急電話で叩き起こして欲しいか？

| Level    | Log | Alert | Display | Processing |
|----------|---|---|-------|----------|
| Info     | o |   |       | 継続     |
| Notice   | o |   | 開発時 | 継続     |
| Warning  | o |   | 運営時 | 継続     |
| Partial  | o | o | 一部   | 継続     |
| Error    | o | o | 運営時 | 中断     |
| Critical | o | o | 運営時 | 中断     |
| Fatal    | o | o | 運営時 | 全経路処理中断 |

これらのことを、例外処理の実装ポイントごとに考えたらいいだけです。
もし、過度な通知だったら、サービスリリース後にキャリブレーションすればいいでしょう。
したがって、ONED の例外レベルは、例外発生時に何の処理をさせておきたいかの段階分けに過ぎず、その段階を言葉で命名したに過ぎません。
極端にいえば、1, 2, 3, 4, 5, 6, 7 でも、A, B, C, D, E, F, G で良いわけですし、Warning 自体に何か特別な意味を含ませているわけではありません。
例外レベルの概念にワードを選択した理由の一つは、軽微な事象は 1 なのか？ 7 なのか？重大な事象は A なのか？ G なのか？という迷いを無くすためです。
したがって、ワードが持つ意味を起点にして、例外を実装すべきではないという事です。
例えば、「この例外が発生したらこの内容で自動処理しておきたい。この処理内容のレベルの名前は何だっけ？」という流れであるべきという事です。

```
例外の処理内容 => 重要度 => 例外レベル 
```

例外レベルの概念にワードを選択したもう一つの理由は、緊急度と混同させないようにするためです。
したがって、あえて重要度という言葉を使用しています。
緊急度の賛否については、次のセクションで解説します。

また、例外レベルの処理内容の定義は、「運営時に実用的なもの」であるべきでしょう。
したがって上記の処理内容の定義は、全汎用的である必要はなく、サービスやプロダクトの必要に応じて適宜変更できるようにすべきでもあります。
（例えば、A サービスでは、処理内容を追加するなど...）
ONED における例外処理コーディングは、「運営の観点に重心を置くべき」という考え方です。


### 3-4. 緊急度の賛否
```
例外レベル ≠ 緊急度
```

ただし、ONED の例外レベルで注意しなければならないのは、重要度であるべきで、緊急度であるべきではないという点です。
なぜなら、Web アプリケーションの例外発生時における緊急度は、運営時の総合判断に委ねられるべきと考えるからである。
運営時の総合判断とは、サービスの運営状況、リソース状況、ユーザーへの影響範囲、データの整合性などを、運営時の状況から総体的に判断したものを指しています。
同じ例外の事象（例えばファイルが存在しない）でも、運営状況よっては緊急対応しないという判断もありうるからです。
したがって、開発段階で定義した緊急度は、運営リカバリー時においては無意味になりかねないという事からも、緊急性こそ運営時で判断されるべきと考えるからです。
例えば、PSR-3 のように Web 言語が推奨するログレベルも emergency レベルが使われ、また緊急性と関係のある解説も並んでいます。
このような緊急性を開発段階だけで定義してしまうことこそ、開発主体志向や、縦割り組織のプロジェクトを育てる要因になってしまうかもしれません。

### 3-5. イベント例外クラス
ONED では、イベント例外クラスの使用はあまり推奨されていません。

```
FileNotFoundException
```
なぜなら、3-3. のセクションでも解説しましたように、イベント駆動でトリガーされる例外クラス名は、その通知内容に責任分解点や例外レベルが明示されないため、ONED で重視している、運営時の初動リカバリーのパフォーマンスが十分に発揮できない可能性があると考えるからです。
例えば FileNotFoundException や ArgumentException などの事象表現については、開発視点によるものであり、それらの事象表現は引数などの例外メッセージに十分含める事ができるでしょう。
もし、イベント例外クラスを使用する場合には、その例外メッセージに責任分解点や例外レベルを含めるなどの実装が推奨されます。

### 3-5. HTTP ステータスコード
Web アプリケーションの例外処理の場合、HTTP クライアントに対して適切に HTTP ステータスコードをレスポンスできるように、例外トリガーは HTTP ステータスコードを伝達すべきでしょう。

Ruby Example:

```ruby:
raise Exception.new('messages: code')
```

PHP Example:

```php:
throw nex Exception('message', 500)
```

なぜなら、最上位の例外キャプチャは、HTTP クライアントに対してどのステータスコードを返すべきかを判断できない場合があるからです。
これは、HTTP ステータスコードをセンシティブに取り扱う事が求められる Web API サーバーにとっては重要な事かもしれません。

### 3-6. 中断処理
```
Exception ≠ Interruption
```

ONED では、例外 = 中断という概念はありません。
Web アプリケーションであっても、例外の継続処理は非常に重要な要素と考えるからです。
そこで、継続処理の考え方と実装例を次項で示します。

### 3-7. 継続処理の実装例
ONED の継続処理とは、例外発生時に必要な回復処理と通知処理などを実施し、通常の後続処理に戻すことを指し、その実現方法は問いません。
以下にその実装例を示します。

#### Retry Mechanism
Ruby や Python パッケージのような、リトライ機構を使った例外の継続処理後に例外ブロックを再試行する方法。
ただし、同じ階層の例外ブロックにしか使えため注意が必要です。

#### Goto Mechanism
C 系や、PHP、Go、Python パッケージのような goto 機構を使った例外の継続処理後に例外トリガー付近で宣言したラベルまで処理を戻す方法。
ただし、同じスコープ内でないと goto 機構は効かないため注意が必要です。

#### 例外ブロックを駆使する
回復/継続処理に例外ブロックを駆使する方法。
（Java の場合は検査例外）

#### 回復・継続処理を例外クラス内にラップする
例外クラス内で回復/継続処理をラップする方法。
（例外は最上位で捕捉）

```php:
if (! is_file("/dir/file.txt")) {
	// Instance
	$FileNotFound = new FileNotFoundException("Could not find the file.", 500);
	// Recovery processing
	if (! $FileNotFound->makeFile("/dir/file.txt")) {
		// Interrupt processing
		throw $FileNotFound;
	}
	// Continue processing
	$FileNotFound->log("Notice");
    $FileNotFound->mail("Notice");
}
// Standard processing
```

#### 継続・中断処理を例外クラス内にラップ
例外クラス内で継続/中断処理をラップする方法。
（throw は最上位で捕捉）

```php:
if (! is_file("/dir/file.txt")) {
	// Try recovery processing
    if (! touch("/dir/file.txt")) {
		// Interrupt processing
		new InternalCriticalException("Could not create the file.", 500);
	}
    // Continue processing
	new InternalNoticeException("Could not find the file.", 500);
}
// Standard processing

class InternalNoticeException extends X
{
    const DISPALY;
	const LOG;
}

class X extends Exception
{
    logger
	mailer
	thrower
	display
}
```

### 3-8. 最終捕捉
ONED による Web アプリケーション開発においては、最上位の例外キャプチャを使って、想定内の例外と想定外の例外を全て制御すべきだと考えます。

### 3-9. throw キーワード
```
throw ≠ instance
```

例外の throw キーワードについて、たまに誤解されているエンジニアさんをお見かけする事があります。
例えば、

> throwable なクラスを継承したサブクラスは、インスタンスと同時に throw しなければならない。

というものです。
もし仮にそのような制約があるとするならば、まずはインスタンス済みオブジェクトを再 throw してる矛盾について気がつかなければならないでしょう。
C 系, Python, Java, Ruby, Perl, PHP, Javascript など、あらゆる言語においてこのような制約はどこにも存在しません。
throw は、単にオブジェクトを throw している構文に過ぎず、そのタイミングや実際に throw するかどうかは任意となっています。
下記のような定型な構文は、単にインスタンスと同時にオブジェクトを throw してるに過ぎません。

C++ Example:

```c++:
throw new FooException;
```

## 4. まとめ
この投稿では、開発速度、品質、機会損失などの改善サイクルの向上を目的とした DevOps のサービス運営視点に基づく Web アプリケーションのドメインロジック例外処理設計の一例として、Operational Notifications Exception Design / ONED をご紹介しました。
このように、ONED は運営視点にも基づいた例外処理設計の概念となります。
実際に、ONED を導入したいくつかのプロジェクトでは、開発速度、品質、機会損失などの改善サイクルが向上しました。