タイトルが微妙な言い回しになっているのは、色々と自信がないからですが、とりあえず説明を書いていきます。
まずここでいう 優先順位
は、それほど高機能ではある必要はなく、単に
「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
を使えばいい場合も多々あると思います。
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がある
a1
とa2
という、特定の一部のtaskだけ時間がかかるため、出来るだけ先に開始して欲しい- しかし
concurrentRestrictions
の範囲において、それらa1
やa2
の終了を待たずして他のtaskは開始してほしい a1
やa2
を特別扱いせずにsbtに完全に任せて全て運任せで実行するのがrun_all
- 少なくともこの例において、実行するたびに、どのtaskが最初に実行開始されるのか?は異なります
- sbtはおそらくそこを何も保証してない。完全にガチャ???
a1
とa2
だけ、最初に必ず実行されるようにCountDownLatch
使って無理やり頑張ったのがrun_all_but_a1_and_a2_is_high_priority
といった状態です。
そもそもの前提として concurrentRestrictions
の存在や、sbtがデフォルトでは可能な範囲でtaskを並列実行しようとする、的な説明をしていませんが、
それはそれで丁寧に説明をはじめると長くなってしまうので、今回は割愛します。
今回のものは、それら中級者向け?をすでに理解している、上級者向け?な話です。
さて、これまた急にマイナー?な機能を使っていくのですが、自分のblogやtweetでは何度か登場したことがあるはずですが、
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
使った実装でいいのか?もっと綺麗な実装方法?
などが、色々と自信がないので、そのあたりの反応を待ってます。