Scala
PlayFramework
sbt
Mockito
Akka-Stream

sbt, scala, play, その他もろもろのライブラリを一気に最新までupdateした話

本記事はFringe81 アドベントカレンダー2017の2日目です。

書く内容は表題の通りなのですが、やってみていろいろとしんどい思いもし、もしかしたら似たようなことでハマっている人の役に立つかもしれない、ということで、備忘も兼ねて淡々と記していきたいと思います。

どれくらい一気にupdateしたのか?

sbtに記述されているアーティファクト数で言うと、test用のも含めると40〜50個くらいのライブラリについて、2年間ほとんどupdateしてなかったのを、ほぼすべて一気に最新まで上げました。

どうしてこんなになるまで(ry

  • ライブラリをupdateするための調査・検証、修正、テストに割けるリソースがなかった
  • 今現在、とりあえず動いている/使えている/困っていないがゆえの「優先度=低」扱い
  • 最新バージョンまで上げた結果、未知のバグを踏むかもしれないという恐怖
  • 苦労してupdateしてでも使いたい機能がライブラリに増えたわけでもない

といったところでしょうか。
夏休みの宿題は8/31に着手していたタイプです。

個人的なハマりポイントを挙げていく

ライブラリごとの最新バージョンを調べる

  • 最初は50件とかあるのを泣きながら手作業で調べていた。。。
  • 社内の先達にsbt-updatesという神プラグインの存在を教えてもらう
    • sbt dependencyUpdatessbt dependencyUpdatesReportだけでプロジェクトが参照しているアーティファクトの現在のバージョンと最新のバージョンを教えてくれるヤツ
    • → 一気に解決へ(※ただしバイナリ互換の問題は残る…)
    • ※先にsbtのVerUpだけは済ませておいた方が良いかもしれない

ライブラリごとのscala2.12.xへの対応状況を調べる

  • このあたりを参考に
  • 現在は大抵のOSSライブラリについて、sbt-updatesが教えてくれる最新で問題ない模様
  • 初めてチェックしたときはPlayとか作業中になっていた

sbt周り(0.13.x -> 1.0.x)

  • そもそもうまく動かない。。。
    • HOMEディレクトリの.sbt.ivyディレクトリを退避
    • ※元の状態に戻せるようにバックアップを取っておくことを推奨
  • compileタスクが途中で固まる。。。
    • マルチモジュール構成の中で、複数のモジュールで参照するconfだけを集めたモジュールがいたのが原因
    • ちゃんとモジュールの依存関係に則してconfを再編し、参照されるモジュールのreference.confに持たせることで解決
    • こんなことでハマるのは我々だけな気も。。。
  • IntelliJでうまく開けない
    • sbtをインストールしなおす

Play周り(2.4.x -> 2.6.x)

  • play2-authが使えなくなった
    • play2-authはplay2.5以降には未対応。。。
    • Silhouetteに置き換えた
    • 思想が異なるのでそこを理解するのに時間は掛かったが、本家のドキュメントが充実してるので基本その通りにやれば問題ない
  • Controllerdeprecated
    • 詳細はこちら
    • BaseControllerAbstractControllerInjectedControllerのいずれかを利用
    • ControllerComponentsをDIする必要がある
    • Action.asyncを使う場合はActorSystemをDIしてExecutionContextを取得する
    • AbstractControllerの場合は@Injectを指定してDIコンテナでconstructor injection(普通はこっちを使うっぽい)
    • InjectedControllerの場合はguice等を使って自分でinjection
// Controller周りの雰囲気
@Singleton
class HogeController @Inject() (system: ActorSystem, components: ControllerComponents) extends AbstractController {
  implicit val executionContext: ExecutionContext = system.dispatcher
  // implicit val executionContext: ExecutionContext = system.dispatchers.lookup('com.exsmple.hoge')

  def index = Action.async { implicit request =>
    // 何らかの処理
  }
}
  • MessageApiが変更された
    • play.api.i18n.I18nSupportをextendするように修正
    • 従来はimplicitでglobalのlangが設定されていた(指定がない場合はjvm起動時のlangが自動的に設定されていた)が、explicitに変更された
    • ひとまずrequestから取得したlangを messageApiに渡すように修正
  • GlobalSettingsがなくなった
  • play.Playdeprecated
  • wsオブジェクトがなくなった
    • 2.4まではval wsClient: WSClient = ws.clientでOKだったが、WSClientのtraitしかないのでインスタンスを用意しなければならない
    • 詳細はこちら
  • play.crypto.secretdeprecated
    • play.http.secret.keyを使う
    • 詳細はこちら
  • 起動コマンドtestProddeprecated

AkkaStream周り(experimental -> 2.5.4)

  • FlowGraphSourceFlowSink等の部品の作成時にimplicitでExecutionContextを渡すのは必須ではなくなった
    • ただし、この状態だと当然ながら各Flowは並列に実行されない
    • 並列化するFlowについて、Flow.mapAsyncなどで並列数を指定しつつimpicitでExecutionContextを渡す必要がある
    • FlowGraphの組み立てにMaterializerは必須で、implicitで渡す必要がある
// 旧
// ExcutionContextが要求され、良しなに並列で実行される
Flow[A].map[B](f: A  B): Repr[B]

// 新その1
// ExcutionContextは要求されない
// ※並列にはならない(このFlowについては直列で実行される)
Flow[A].map[B](f: A  B): Repr[B]

// 新その2
// 内部でFutureを生成するためimpicitでExcutionContextを渡さないとコンパイルが通らない
// parallelismで指定した並列度が適用される
Flow[A].mapAsync[B](parallelism: Int)(f: A  Future[B]): Repr[B]
  • Sourceの作成の仕方が変わった
    • Source[A]の作成をSource[A](() => Iterator[A])のようにしていたがSource.fromIterator[A](() => Iterator[A])
// 例) scala.io.Sourceから作成する場合

// 旧
Source[String](() => input.getLines())

// 新
Source.fromIterator(() => input.getLines())

Mockito(1.10.x -> 2.12.x)

  • org.mockito.Matchersdeprecated
    • org.mockito.ArgumentMatchersを使う
  • org.scalatest.mock.MockitoSugerdeprecated
    • org.scalatest.mockito.MockitoSugerを使う
  • doReturn(Object obj)のoverloadが解決できなくてコンパイルエラー
    • thenReturnに変更できるなら変更する
    • 第二引数にboxingした値を渡す(primitive型の場合。例:Int.box(1))
    • 第二引数にAnyRefを渡す(primitive型以外の場合)
    • 第二引数に空のcollectionを渡す(collectionの場合。例:Nil、Set.empty)
  • extends ArgumentMatcherでエラー
    • 型パラメータを指定する(例: IntMatcher extends ArgumentMatcher[Int])
  • 修正対象の件数が多いこともあり、こいつも地味に相当キツかった
  • ※そもそもMockitoのバージョンを最新にする必要がないならその方が良いかもしれない。。。

過去の自分たちに言ってあげたい

  • 「ライブラリのupdate、いつやろうかな。。。?」と迷っているなら、今開発中の、そのバージョンでやろう
  • 必ずしも最新まで上げる必要はない。一番新しい安定版はどれか、ライブラリ同士の依存関係上、バイナリ互換性的に一番良さそうな組み合わせはどれか、調査した上で冷静に判断しよう
  • 影響範囲が大きいライブラリ(ScalaとかPlayとか)のupdateをすると、テスト範囲が大きくなりすぎてスケジュール的に無理な場合は、ライブラリupdateのためだけに1つのバージョンを使う前提で準備を進めよう
  • 通常のバージョンアップ開発とは別にライブラリupdate専用チームを作り、並行して作業を進めることができれば、ある程度までは機能追加も止めずに済む。。。かもしれない

最後に

いかがでしたでしょうか。
Tipsとしてのボリュームはいまいちかもしれませんが、、、誰かのお役に立てば幸いです。

個人的な感想としては、今回のupdateは結構な大仕事になりそうだな、、、と予想はしていたものの、想像していた以上にいろんなところで躓き、時間がかかりました(まだ完了してないけど)。
利用しているライブラリのバージョンアップは、依存度が高ければ高いほど、ライブラリの変更に伴う修正の量も増え、結果必要なテストの量も増え、ハードルが高くなってしまうもの。ついつい機能の追加や改善を優先して、後回しになりがちです。
とはいえ、システムの寿命を長く保とうとするのであれば、絶対に必要不可欠な作業でもあります。影響が大きい変更が頻繁に発生しているようなライブラリは、その有用性とかかる手間暇とを天秤にかけて、利用の可否自体を慎重に判断する必要があるな、、、と改めて痛感しました。まさしくご利用は計画的にですね。

現場からは以上です。
最後まで読んでいただき、ありがとうございました。