JenkinsのジョブをキューイングするGoアプリ作った

概要

トリガとなるジョブと実行の遅いジョブとを連結させた場合に全体の実行が遅いジョブに依存してしまう問題を解決する為に、実行の遅いジョブの前にジョブキュー用のGoアプリを挟むことで問題を解決しました。

問題点1 ジョブキューにどんどんジョブが溜まる

例えば、以下のようなJob関係があるとします。

+----------+----------+
| Machine1 | Machine2 |
+----------+----------+
|          |          |
| +------+ |          |
| | JOB1 | |          |
| +--+---+ |          |
|    |     |          |
|    +----------+     |
|    |     |    |     |
| +--+---+ | +--+---+ |
| | JOB2 | | | JOB3 | |
| +------+ | +------+ |
|          |          |
+----------+----------+

Job1が終了すると、同じマシンでJob2が動作して、平行してJob3が別マシンで動作する。という感じです。あと、別マシンはJobが1つしか動作しない設定になっており、Job3はJob2に比べて十分に遅いものとします。そうした場合に、このジョブ群は1回目はうまく動作しますが、2回目、3回目とJob1が動作するたびにJob3のジョブキューにどんどんたまっていきます。

軽く考え付く解決案としては、「Job3のジョブを軽くする」というのと「Job3が実行中ならジョブキューにジョブを積まない」というのがあると思います。

1つ目の「Job3のジョブを軽くする」に関しては、たとえばJob3の中で実行しているジョブを細分化して別マシンに割り振ることで可能だという認識ですが、今回私が直面した問題ではマシンを数十用意しなければならず、コストの面で却下となりました。

もう一つの「Jobが実行中ならジョブキューにジョブを積まない」に関しては、groovy-label-assignment-plugin等を使用して、Groovyスクリプト等でJob1の最後にチェックしたり、Job3の頭でやったりすれば出来ると思います。ただ、すいません、このケースはあまり調査してなくて単純に次に話す問題点が解決しなかったので諦めました。

github.com

問題点2 ジョブがスキップされる場合がある

「Jobが実行中ならジョブキューにジョブを積まない」という解決策をとった場合、ジョブが実行されているがゆえに本来実行されるべきだったジョブがスキップされてしまうという問題がありました。

つまり、

  1. Job1が実行されJob3にジョブ(ビルド1)がキューに積まれる
  2. Job3がジョブ(ビルド1)を実行する
  3. Job1が再度実行されジョブ(ビルド2)をキューに積もうとするが、ジョブが実行中なのでキューに積まれない
  4. Job3がジョブ(ビルド1)を完了する

という流れになります。当たり前ですね。これを解決するためにはジョブをキューに積むようにしないといけないですが、それだと問題点1に逆戻りというジレンマを抱えます。

最終的に解決したかったこと。

最終的に解決したかったことは、「ジョブキューが空の場合はジョブキューにジョブを積み、ジョブキューにジョブがある場合はジョブキューをクリアして、最新のジョブをジョブキューに追加する」ということでした。

疑似的にコードを書くとするならこんな感じでしょうか。

if !queue.empty? {
  queue.clear()
}
queue.add(job)

実装してみた

github.com

実装してみました。ただ、ちょっとオーバーすぎる実装にしてしまったかなと思っています (^_^;)

今回はWeb Server経由でパラメータの受け渡しをするようにしていますが、単純に実装しようとおもったらCLIとして実装すればよかったかなと、この記事を書いている途中に気付きました。

まぁ、作ってしまったものは仕方がないということで以下簡単な説明です。

ジョブの関係は以下のようになります。

+----------+-----------+
| Machine1 | Machine2  |
+----------+-----------+
|          |           |
| +------+ |           |
| | JOB1 | |           |
| +--+---+ |           |
|    |     |           |
|    +-----------+     |
|    |     |     |     |
| +--+---+ | +---+---+ |
| | JOB2 | | | CALL1 | |
| +------+ | +---+---+ |
|          |     :     |
|          | +=======+ |
|          | | queue | |
|          | +===+===+ |
|          |     :     |
|          | +---+---+ |
|          | | JOB3  | |
|          | +--+----+ |
|          |           |
+----------+-----------+

今回新たに追加されたジョブは CALL1 です、これは jenkins-job-queue をコールするだけのジョブです。httpのPOSTメソッド経由で実行するのでhttpのレスポンスが返ってくればそこでジョブは完了します。 CALL1queue(jenkins-job-queue) の間はジョブの関係がありません。queue はjenkins上のジョブとしては存在していません。なので、ジョブを実行する前にexeを実行しておく必要があります(ここら辺も今後は改善していきたいと思っています)

queueJOB3 との間も httpのリクエストで繋がっていて、jenkinsの JSON APIを使ってジョブを実行します。実装するジョブにパラメータが必要な場合は CALL1 からパラメータを渡すこともできます。

queue の仕事は、先ほども言った通り、最新のジョブをキューにため続けることです。 CALL1 から要求が来るとキューに積まれます。JOB3 のジョブ実行状況を確認し、ジョブが実行されていなければキューのジョブ実行し、キューからジョブを削除します。もしジョブが実行されている場合は実行が完了するまで待ちます。キューにジョブが積まれている状態で CALL1 から新たに要求があればキューのジョブをクリアし、新しい要求をキューに追加します。

jenkins-job-queue の実行方法

インストールは以下でできると思います。Windowsでしか確認していないです。すいません (^_^;)

> go get -u github.com/bamchoh/jenkins-job-queue

$GOPATH\bin 以下に jenkins-job-queue.exe があります。以下のパラメータを指定して実行することができます。

オプション デフォルト 意味
-db 空文字 キュー用のデータベースファイル
-addr :20000 サーバーのアドレス
-bucket MyBucket データベース内の識別子(基本的に気にする必要はないです)

実行例を以下に示します。この例では ポート20000でデータベース test.db を実行ファイルパスと同じ場所に作成して起動します。

> jenkins-job-queue.exe -db test.db

相対パス指定すると実行ファイルパスと同じ場所にデータベースファイルを作成します。あまりこの実装はよくないと思っていて、ここも今後変更の課題となっています。 (^_^;)

CALL1 からの要求方法

HTTPのPOSTメソッドを投げることで要求を送信できます。curl だと以下のようになります。cmd.exe で記述してるので多少みにくくなっています。

> curl http://localhost:20000/job -XPOST -d "{\"title\": \"JOB3\", \"buildURL\": \"http://localhost:8080/job/JOB3/buildWithParameters?token=<token>\", \"user\": \"<username>:<password>\", \"observeURL\": \"http://localhost:8080/job/JOB3/api/json?tree=lastBuild[number],lastCompletedBuild[number],inQueue\", \"parameter\": {\"Output\":\"this is value2\"}}"

URLは <hostname>:<port>/job になります。body のデータはJSON形式で指定する必要があります

{
  "title": "JOB3",
  "buildURL": "http://localhost:8080/job/JOB3/buildWithParameters?token=<build_token>",
  "user": "<username>:<api_token>",
  "observeURL": "http://localhost:8080/job/JOB3/api/json?tree=lastBuild[number],lastCompletedBuild[number],inQueue",
  "parameter": {
    "Output": "this is value2"
  }
}

APIトークンに関しては、以下の記事を参考にしてください。

blog.kyanny.me

JSONに指定するパラメータの意味は以下の通りです。

キー 指定する値
title 要求を識別するためのID
buildURL 実行するジョブ(上記例ではJOB3をパラメータ付きで実行するジョブ)
user トークAPI で使用するuserとAPIトーク
observeURL buildURLが完了したかどうかをチェックするためのURL
parameter buildURLで渡すパラメータ、json objectで渡す

まとめ

恐らく一番苦労するところは JSONを作成するところだと思います。このあたり、もうちょっと改善したいですね。observeURL なんかは buildURLから自動生成できそうですし。なんしか、改善点はあるもののある程度形にできたので発表記事を書かせていただきました。m(_ _)m