sbtのTaskに優先順位の制御が欲しい気がしたので雑に作ってみた話

タイトルが微妙な言い回しになっているのは、色々と自信がないからですが、とりあえず説明を書いていきます。

まずここでいう 優先順位 は、それほど高機能ではある必要はなく、単に

「aとbのどちらを先に実行してもいい時に、出来るだけaから実行してほしい」

くらいの意味です。上記だと説明不足で

「sbtはtask同士の依存関係を定義可能だから、aを実行して、その後にbを実行するようにbuild.sbt書けばいいですよね?」

となってしまうかもしれませんが、あくまでaとbそのものに直接の依存関係は定義したくない、という微妙な状況です。

言い換えると、bについて

「実行可能なら、aの実行の完了を待たずして、実行開始はして欲しい」

です。

そんな状況ある???と疑問に思うかもしれませんが、例えば

  • sbtのsub projectが大量にある
  • 目的として、全てのtestを実行した際に、終わるまでの時間を最短にしたい
    • この場合は、CPUやメモリなどの効率を良くしたい、に実質同義になる
    • concurrentRestrictionsの制約の範囲で出来るだけ各種リソースを有効活用してほしい、ともある意味言い換え可能?
  • しかし、外部のDBを使ったり、いろいろな制約で、それぞれのsub project内部では並列実行したくない
  • また、sub projectが本当に大量にある、CPUやメモリの制約、などの理由で、全てのsub projectのテストを全部並列実行は実質できない
    • 100個sub projectがあったときにforkした100個のJVM一気に立ち上げるのは現実的ではない
  • 単にtestを効率よくしたい、速くしたい観点だと、その他無限に方法はありますが、あくまでtestというのは例であって、sbtのTaskの実行モデルを純粋に考えた時に(開始の)優先順位のようなものが欲しい、という話に抽象化して考えたい

という感じでしょうか?これはこれで、以下の testGrouping

xuwei-k.hatenablog.com

を使えばいい場合も多々あると思います。

testで例を出したから、確かに

「特定の長いtaskは分割すればいい」

という戦略はあり得て、確かにそれが有効な場合も多々あると思いますが、その戦略が取れないか、原理上取れるがその戦略を取ることによるデメリットが大きい場合もあり得ると思います。

さて、ここまで書いて、伝わっている人にはしっかり伝わってる気がするし、伝わってない人には全然伝わっていないかもしれませんが (それはそう) 実際に作ってみた答え含めたサンプルのbuild.sbtを貼り付けます

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

val example = project.in(file("."))

(1 to 10).map { x =>
  TaskKey[Int]("a" + x) := {
    println("start " + x)
    x match {
      case 1 | 2 =>
        latch.value.countDown()
        Thread.sleep(3000)
      case _ =>
        Thread.sleep(1000)
    }
    println("end   " + x)
    x
  }
}

Global / concurrentRestrictions := Seq(Tags.limitAll(4))

val latch = taskKey[CountDownLatch]("")

latch := new CountDownLatch(2)

val lowPriorityTasks = taskKey[Int]("")

lowPriorityTasks := Def.taskDyn {
  assert(latch.value.await(10, TimeUnit.SECONDS), "優先度高いtaskのstartを待ったけどtimeout")
  Def.task {
    TaskKey[Int]("a3").value
    TaskKey[Int]("a4").value
    TaskKey[Int]("a5").value
    TaskKey[Int]("a6").value
    TaskKey[Int]("a7").value
    TaskKey[Int]("a8").value
    TaskKey[Int]("a9").value
    TaskKey[Int]("a10").value
  }
}.value

TaskKey[Unit]("run_all_but_a1_and_a2_is_high_priority") := {
  TaskKey[Int]("a1").value
  TaskKey[Int]("a2").value
  lowPriorityTasks.value
}

TaskKey[Unit]("run_all") := {
  TaskKey[Int]("a1").value
  TaskKey[Int]("a2").value
  TaskKey[Int]("a3").value
  TaskKey[Int]("a4").value
  TaskKey[Int]("a5").value
  TaskKey[Int]("a6").value
  TaskKey[Int]("a7").value
  TaskKey[Int]("a8").value
  TaskKey[Int]("a9").value
  TaskKey[Int]("a10").value
}
  • 10個Taskがある
  • a1a2 という、特定の一部のtaskだけ時間がかかるため、出来るだけ先に開始して欲しい
  • しかし concurrentRestrictions の範囲において、それら a1a2 の終了を待たずして他のtaskは開始してほしい
  • a1a2 を特別扱いせずにsbtに完全に任せて全て運任せで実行するのが run_all
    • 少なくともこの例において、実行するたびに、どのtaskが最初に実行開始されるのか?は異なります
    • sbtはおそらくそこを何も保証してない。完全にガチャ???
  • a1a2 だけ、最初に必ず実行されるように CountDownLatch 使って無理やり頑張ったのが run_all_but_a1_and_a2_is_high_priority

といった状態です。

そもそもの前提として concurrentRestrictions の存在や、sbtがデフォルトでは可能な範囲でtaskを並列実行しようとする、的な説明をしていませんが、 それはそれで丁寧に説明をはじめると長くなってしまうので、今回は割愛します。 今回のものは、それら中級者向け?をすでに理解している、上級者向け?な話です。

さて、これまた急にマイナー?な機能を使っていくのですが、自分のblogやtweetでは何度か登場したことがあるはずですが、

github.com

sbtにはtaskがどう実行されたのか?をファイルに吐き出して分析するための機能があります。

つまり、上記のbuild.sbtを使って

sbt -Dsbt.traces=true run_all 

sbt -Dsbt.traces=true run_all_but_a1_and_a2_is_high_priority 

target/traces/build.trace に吐き出されたファイルをChromeなどで開いてみましょう。 結果は、例えば以下のようになります

片方は、a1とa2が最初に実行開始されてるので、全体が効率いいのが一目瞭然で、もう片方は、a1の実行開始が遅れてしまったので、最後にはa1だけが居残りで実行されていて、もったいないですね。このもったいない状況を防ぎたい、というお気持ちがこのblog記事です。

余談ですが、単にa1とa2を先に実行して完了まで全部待ってから他を実行開始するには、色々書き方はありますが、例えば以下のような書き方だとそうなりますが、 これは場合によっては、余計に効率が悪いので、自分が求めているのは、こういうことではない、というのは図を見れば理解してもらえるでしょうか?

TaskKey[Unit]("run_all_a1_a2_first") := Def.taskDyn{
  TaskKey[Int]("a1").value
  TaskKey[Int]("a2").value
  Def.task {
    TaskKey[Int]("a3").value
    TaskKey[Int]("a4").value
    TaskKey[Int]("a5").value
    TaskKey[Int]("a6").value
    TaskKey[Int]("a7").value
    TaskKey[Int]("a8").value
    TaskKey[Int]("a9").value
    TaskKey[Int]("a10").value
  }
}.value

さてここまで書きましたが

  • そもそも本当にsbt本体、もしくは外部のpluginで、こういったことをしてくれる機能は存在しないのだっけ?
    • かるくsbt本体を調べた限りは見つかりませんでした
    • key rankはあるけど、あれはshelのtab補完での優先順位・・・?だけ?(よくわかってない)
  • もし存在しないとしても、そもそも優先順位という考え方以外で、似たような目的を達成可能は方法は、他にもあるか?が自信ない
  • 仮に優先順位を実装するしかないとしても、こんな CountDownLatch 使った実装でいいのか?もっと綺麗な実装方法?

などが、色々と自信がないので、そのあたりの反応を待ってます。