takafumi blog

日々の勉強メモ

Scala3で弱適合性(= Week Conformance)の削除で問題が発生するコードについて

環境   Scala 2.13.8  Scala 3.1.1

scala3で削除される機能の一つに「弱適合性(= Week Conformance)」というのがある。 docs.scala-lang.org

しかし説明を読み、コードを書いてみても実際に何が問題になるのかが最初よく分からなかった。 ここではscala2からscala3への移行、もしくはそれを前提としてscala2のコードを書くとき、で問題が発生しそうなパターンを書き残しておく。

そもそも弱適合性とは?

弱適合性についてをそもそも理解したい場合はscala2のREFLECTION シンボル、構文木、型#弱適合性関係 を参照。
削除されるものなので細かくは説明は書かない。

問題と解決方法

結論

先に結論を書くと、アプリケーションのレベルであれば、変数や関数に返り型を記述してあれば問題は発生しない

実際のコード

scala2とscala3で同じように書いて、scala3で問題が出るのは以下のようなコードになる。

// scala2
val d = if (true) 1f else 1d
val d2: Double = d

 

// scala3
val d = if (true) 1f else 1d
val d2: Double = d // Error
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:4:21
[error] 4 |    val d2: Double = d // Error
[error]   |                     ^
[error]   |                     Found:    (d : AnyVal)
[error]   |                     Required: Double

上記のような場合は Floatコンパイル時に自動変換されず AnyVal として扱われるためエラーとなる。

しかしscala3のドキュメントに

Therefore, Scala 3 drops the general notion of weak conformance, and instead keeps one rule: Int literals are adapted to other numeric types if necessary.

とある通り Int の場合は Double へ自動変換されるためcompile可能である。

// scala3
val d = if (true) 1 else 1d // from 1f to 1:Int
val d2: Double = d // OK 

また、次のように関数に返り型が明示されている場合は自動変換され、コンパイルを通過する。
これは Float から Double のように精度が下がらない型変換は暗黙に定義されるので許容される https://dotty.epfl.ch/api/scala/Float$.html#float2double-72d

// scala3
val d: Double = if (true) 1f else 1d // <- Add return type `Double`
val d2: Double = d // OK 

つまり複数の数値型が一つの式の中に存在した時、明示的に型指定をしている場合は、暗黙で変換される。
(ただし高精度から低い精度への変換は定義されていないためエラーとなる)
しかし明示的に型を指定しない場合は、 Int 以外は AnyVal として扱われてしまい、その後の特定の数値型として呼び出すと AnyVal から数値型への変換は暗黙変換が定義されていないためエラーになる。

結論

つまるところ、返り型を常に書いておけば基本的にこの機能削除による影響は受けないと思われる。
またそれ以外の場合も AnyVal からの変換が不可能であるというエラーがcompile時に発生するため、修正せずに妙な動きをするという可能性は低い。

・・・のではないかと考えられる。

余談

scala3ドキュメントに書いてる List の例も、返り型をきちんと書いておけば問題なくcompileできる。

NG

// scala3

def main(args: Array[String]): Unit =
  val ls: List[Double] = foo()

def foo() =  // NG: No return type
  val n: Int = 3
  val c: Char = 'X'
  val d: Double = math.sqrt(3.0)
  List(n, c, d)
[error] -- [E007] Type Mismatch Error: /path/to/Main.scala:3:30
[error] 3 |    val ls: List[Double] = foo()
[error]   |                           ^^^^^
[error]   |                           Found:    List[AnyVal]
[error]   |                           Required: List[Double]

OK

// scala3

def main(args: Array[String]): Unit =
  val ls: List[Double] = foo()

def foo(): List[Double] = // OK
  val n: Int = 3
  val c: Char = 'X'
  val d: Double = math.sqrt(3.0)
  List(n, c, d)

ポストモーテムとは

基本的にSRE本を自分で実施する際に理解しやすい形にまとめたもの。

前提

この文章はインシデント(障害や緊急対応など)発生時の対応関するポストモーテムに関して記述する。

プロジェクト終了時などその他のタイミングで行うポストモーテムとは異なる。

用語

  • ポストモーテム
    • この文章ないではインシデント時に作成するドキュメントをポストモーテムと呼ぶ
    • ドキュメント作成とそれにまつわるレビューやなど一連の流れ、仕組みを指すときはポストモーテムのプロセスと呼称する
  • インシデント
    • 障害・緊急対応などサービスに何かが起きた時の呼称
  • トイル(SRE本 p51)
    • プロダクションサービスを動作させることに関係する作業で以下のような傾向を持つもの
      • 手作業で繰り返し行われる
      • 自動化することが可能である
      • 戦術的で長期的な価値を持たない
      • 作業量がサービスの成長に比例する

まとめ

目的・文化・準備

プロセス

ポストモーテムのプロセスは以下のようになる

  1. インシデントが発生する
  2. インシデントの対応が完了する
  3. ポストモーテムを書く条件に当てはまるかを判定
  4. 必要な情報を収集する
  5. 原因の分析と対策を検討する
  6. ポストモーテムを書く
  7. レビューを行う
  8. 共有する

参考: https://postmortems.pagerduty.com/what_is/


ポストモーテムの目的

過去のインシデントから学習し、再発の可能性や影響を削減するために行われる。

具体的には以下のような事を目的となる。

  • 根本原因、教訓が十分(具体的・広範囲に)に理解されること
  • 効果的な予防策が確実に導入される事

参考: - https://postmortems.pagerduty.com/what_is/ - SRE本 p175

ポストモーテムの準備

ツールの選定

  • ポストモーテムを書くためには以下の機能を備えているツールが望ましい
    • リアルタイムコラボレーション
    • オープンなコメント/アノテーションシステム
    • メール、チャットによる自動通知
  • 管理システム

参考: SRE本 p177

ポストモーテムの作成

まずポストモーテムを書く前にポストモーテムを作成する必要があるかを判断する

書く場合は

  1. インシデント事態の情報、対応に関する情報収集
  2. インパクトの分析
  3. 根本原因の分析
  4. 対策についてアクションプランを検討
  5. 調査・対策が十分かステークホルダーに確認する

を行い上記を十分に吟味し(もしくは行いながら)、ポストモーテムの作成する。

ポストモーテムは事前に用意したテンプレートに従い記述し、どのようにな書き方がよいポストモーテムかを理解しておく必要がある

いつポストモーテムを作成するか

  • 事前に決めた条件に則り、条件を満たす場合に作成する
    • ユーザー影響あるダウンタイムの発生
    • エンジニアが介入が必要だったとき
    • 解決までの時間
    • データの損失が発生した場合
    • 支払い、請求などお金に関わる問題発生時
    • モニタリングシステム自体の障害
    • など
  • 上記の決まった条件以外で問題となったイベントのステークホルダーは条件に以外で求める事ができる

参考: SRE本 p176~p178

ポストモーテムのテンプレート

例としてポストモーテムには以下のような項目を記述する。

- 担当者
    - ポストモーテム担当者
    - 対応担当者
- 概要
- 原因分析
    - 根本原因(e.g. ○○のバグのため)
    - 発生原因(e.g. △△の操作を行ったため)
- 対応内容
- 対応自体の分析
- 教訓
- 今後のアクションリスト
    - 事後対応内容
    - 現在のステータス
    - 担当者
    - 優先度

よいポストモーテムの書き方

TODO

参考: サイトリライアビリティワークブック―SREの実践方法/10章 ポストモーテムの文化:失敗からの学び

レビュー

レビューが行われていないポストモーテムは適切さを証明できないため、無いも同然と考える必要がある。

実際の流れとしては - チームに下書きを内部共有してレビューを行う - この時、上位の役職者は下書きが完全かを評価する

となる。場合によっては広範囲に意見を求める事もある。

レビューの観点

レビューでは事前にある程度観点を定めておく。

以下は一例

  • 不要な非難が含まれていないか
    • 個人に対する指摘
  • インシデントの主要なデータが収集されているか
  • インパクトの分析が十分か
  • 根本原因の分析が十分か
  • アクションプランは適切か
    • 担当者が決まっているか
    • 優先度が与えられているか
  • 結果はステークホルダーと共有されているか

など

レビューを必ず行う仕組み

レビューが行われなければポストモーテムは無意味といっていい。

必ず行うために、何時、どのように行うかをあらかじめ考えておくとよい。

  • どんなタイミングでレビューをするか
    • 下書きの段階でチーム内や直属の上位者がレビューするのが望ましい
  • レビューされていないポストモーテムが存在しない仕組みづくり
    • レビューMTGを行いポストモーテムを完成させる
      • レビューMTGの議論内容はとりまとめ、ポストモーテムに記載する

参考: SRE本 p178

共有

ポストモーテムを広く共有し、知識や教訓を広い範囲で役立てる事を目標とする。

より広範囲なチーム、内部メーリングリスト、チャットチャンネルなどへ共有する。

ポストモーテム文化の導入

参考: SRE本 p179

非難なく行う

ポストモーテムの記述・レビューなど全てのプロセスを通して、非難なく行うことが重要である - 避難的な雰囲気は問題の隠蔽へつながる - ポストモーテムを頻繁に作成している事を非難しない - ポストモーテムはサービス改善のための提案である - ポストモーテムの作成を称賛する文化を作る事が望ましい

参考: SRE本 P176~P177

継続的な育成と強化

継続にはポストモーテム文化の育成と強化が必要となる。 - 上位管理職の積極的な参加 - 積極的な情報の発信 - よいポストモーテムの選出・定期的な発信 - 公開されたポストモーテム議論の場 - チャットチャンネル、メールグループなど - ポストモーテムの読書会 - 過去の興味深いポストモーテムの振り返り - 新旧を問わず - ディザスターロールプレイング - ポストモーテムに書かれている役割をエンジニアたちが演じ、過去のポストモーテムの再現する

ポストモーテムプロセスのフィードバック

ポストモーテムのプロセス自体に無駄がないかを見直し、改善を行う。 - ポストモーテムは業務の支援となっているか? - トイルが発生しすぎていないか? - ツールは適切か?新しい開発が必要か?

参考: SRE本 p180

将来的な価値を考える

参考: SRE本 P181

準備コストと価値に対する理解

ポストモーテム文化を組織に導入する上での最大の課題は、準備コストよりも価値があるのか?と考える人が多いかもしれないこと。

これを解決するには以下のような対策が考えられる - ワークフローに落とし込む - よいポストモーテムを書くことは評価されるように - ピアボーナスを与える - 上位のリーダーたちの承認と参加を奨励する

理解に関しては - ポストモーテムプロセスのフィードバックを行う - 継続的な育成と強化 - 将来的な価値を考える

などと合わせて考えることが理解を得る助けとなる

参考: SRE本 P179

ポストモーテムの例

SRE本 p507

参考

fluent-plugin-s3 設定と IAMロール アクションの関係と注意

環境   fluent-plugin-s3 1.6.0

関係性のまとめ

アクション 関係ある設定 説明
s3:ListBucket check_bucket true trueであればfluent起動時にbucket存在をチェック。存在しなければ起動をリトライする
falseだと起動はするが書き込み時に失敗する
s3:GetObject check_object true trueであれば同じobjectがあると上書きせずエラーになる
falseだと同名objectは上書きされる
s3:PutObject 書き込み全般

つまり check_bucket check_objectfalse ならアクションは最小限の PutObject のみでよい

check_object false の注意

check_object false の時も s3_object_key_format が同名にしない事で不用意な上書きを防ぐ事ができる。

s3_object_key_format を設定していなければ、デフォルト値自体が上書きされにくいフォーマットに自動で変更される。

(s3_object_key_format "%{path}/%{date_slice}_%{hms_slice}.%{file_extension}" になる)

さらに uuid_flush hex_random index hostname などを使えばより重複を防止し安全に扱える。

  • uuid_flush
    • bufferがフラッシュされる際、常にUUIDに置き換えられる
    • 書き込み失敗のリトライ時も新しいUUIDに置き換えられる
  • hostname
    • ホストネームに置き換えられるので、 %s%{hms_slice} などと組み合わせると重複を防止できる
  • hex_random :
    • bufferに使われるhashで置き換えられる
    • 例えば同名エラーでリトライしたときも同じ値が使われる
    • %{index} を組み合わせればリトライ時は %{index} が更新される

fluent-plugin-s3 v0.12 の注意

正確には fluent-plugin-s3 < 1.0.0rc7 のとき。

この時 check_object false だと s3_object_key_format が固定されるので注意

github.com

check_bucket trueAPIリクエストにかかる料金の注意

check_bucket は起動時のチェックなので、 true でもほとんどAPIリクエストはしない。

しかし check_objecttrue だと書き込みの度にAPI GetObject を発行する。

そのため、特に書き込み頻度が高い場合は、APIリクエストが常に GetObject -> PutObject と二重に発生し料金が嵩むので注意。

Scalaでもtry-catchを使う方がよい事もある

環境   Scala 2.13.6

Scalaだと例外を常にTryで処理しがちなので、反省メモ。

もちろん単純に例外出す可能性ある個所をTryで包むのは問題ない。
困るのはTry[Either[L, R]] みたいなネストを避けたい時。

refinedとかでよくある。
例えば以下は単純にStringEither[String, PosInt]に変換している。

import cats.implicits._
import eu.timepit.refined.api.RefType
import eu.timepit.refined.types.numeric.PosInt
import scala.util.Try

def s2pi(s: String): Either[String, PosInt] =
  Try(s.toInt).toEither
    .leftMap(_.getMessage)
    .flatMap(RefType.applyRef[PosInt](_))

val etPiR = s2pi("1") // Right(1)
val etPiL = s2pi("a") // Left(For input string: "a")

読みずらい!
まあ正直言うと、このくらいならまだマシで、実際コード書いているともっと複雑になる事も多々ある。

こういうTryとEitherのネストを避けようとするなら、以下のように書くのが断然読みやすい。

import eu.timepit.refined.api.RefType
import eu.timepit.refined.types.numeric.PosInt
import scala.util.Try

def s2piVer2(s: String): Either[String, PosInt] =
  try RefType.applyRef[PosInt](s.toInt)
  catch { case t: Throwable => Left(t.getMessage) }

val etPiV2R = s2piVer2("1") // Right(1)
val etPiV2L = s2piVer2("a") // Left(For input string: "a")

ドメイン駆動開発(DDD)とCleanArchitectureのエンティティはどう違うか?

双方と共にビジネスモデル定義のためのオブジェクトという点では共通だが、実際には意味は大幅に異なる。

DDDでは

以下のように定義される

「エリック・エヴァンスのドメイン駆動開発 第5章」から

主として同一性によって定義されるオブジェクトはエンティティと呼ばれる。

としている。

つまりDDDではビジネスモデルの中でも、常に同一である事を保証できるオブジェクトととして定義されるものがエンティティである。

CleanArchitectureでは

クリーンアーキテクチャ(The Clean Architecture翻訳)」から

エンティティーは、大規模プロジェクトレベルのビジネスルールをカプセル化する。エンティティは、メソッドを持ったオブジェクトかもしれない、あるいは、データ構造と関数の集合かもしれない。

とある。
これはDDDで言うところの、エンティティ、値オブジェクト、ドメインサービス、リポジトリ(の仕様)、ファクトリ, 集約などのドメインレイヤー全体を指している。

まとめ

回答としては「DDDにおけるエンティティ」と「CleanArchitectureにおけるエンティティ」は全く異なる定義なので、使うアーキテクチャに合わせてコードを書こう。

もっと言えば - 書いているコードのアーキテクチャが、何を採用しているか? - 各用語はどういう定義なのか?

をきちんと理解して書こうという事になる。

プロジェクトによってはCleanArchtectureだけどentityをdomainというpackage名にしたりで、一見DDD風に見える事もあったり、そもそも全く異なる意味で使っている場合もある。
プロジェクトに参加したときはビジネス的な言葉、DDDでいうユビキタス言語だけではなく、こういったコード上の意味合いも慎重に確認する事が大切。