ジョブネットとコントローラの自作2

ジョブネットコントローラの自作第二弾。 第一弾は awk を駆使して実現したが、 awk はコードが見づらく癖もあるため、ジョブコントロールに向かいないと感じていた。 また、ジョブの完了状態チェックをステータスファイルを用いて簡単化するため、今回は基本的にシェルスクリプトだけで実現した。

例として、図のようなフローで J01 からJ09 までのジョブを今回作成するジョブコントローラで実行してみる。


仕様

実行ファイル : jobctl
引数 : ジョブ格納ディレクトリ

ジョブフロー設定ファイル jobnet.csv に従ってジョブを実行する。 jobflow.csv の第1列は実行対象ジョブ、第2列目以降は依存ジョブが記載されている。 実行対象ジョブは依存ジョブの完了後に実行される。 このような行がリストされることで、ジョブフローが形成される。

実行対象ジョブは可能な限り並行稼働して、無駄な待機時間をなくす。 ジョブの実行ログファイルがログディレクトリに出力される。 実行対象ジョブは、任意の一つのディレクトリにまとめて存在する必要がある。

戻り値は、実行したジョブの終了ステータスに依存しない。 実行したジョブが異常終了した場合、他に実行中のジョブがあれば、それが完了してからジョブコントローラを終了する。

ディレクトリ構成とファイル


このジョブコントロールシステムは、親ジョブを直接コントロールする。 親ジョブとは、コントロールしたいジョブを内包(ラッピング)したシェルスクリプト。 上図の dir1 について、特に明記していなくてもジョブは親ジョブのことを指す。

コントロールしたいジョブは、任意の一つのディレクトリ(上図の dir2)にまとめて格納されていること。 ジョブは、任意の実行形式ファイル(シェルスクリプト等)。

「任意の〜」と記載されていないディレクトリやファイルは、このジョブコントロールシステム本体であり、変更は許されない。

親ジョブ作成ツール ( makeParent )

makeParent(親ジョブ作成ツール)は、 jobnet.csv とtemplate をもとに親ジョブを作成する。 template には、以下の処理が記述されている。

  1. ジョブをラッピングして実行
  2. 1.のジョブ実行の前後で、開始/終了時刻をログ出力
  3. status に実行状態ファイルの出力

makeParent のコード例

 1#!/bin/bash 
 2  
 3# jobnet.csv の実行対象ジョブに対して、それをラッピングして実行する親ジョブ>を job ディレクトリに作成する
 4
 5err(){
 6  STATUS=$? 
 7  echo "単純なコマンドの実行でエラーが発生しました"
 8  echo "${BASH_SOURCE##*/}: $BASH_LINENO: $BASH_COMMAND: $STATUS"
 9  exit 1
10}
11trap err ERR
12    
13# 既存の親ジョブを削除
14rm -f job/P_* >& /dev/null
15
16# 親ジョブの作成
17awk -F, '!/^#|^$/{print $1}' <(gsed -E 's/( |\t)//g' jobflow.csv.tmp) |
18while read; do
19    cp -p job/template job/P_$REPLY
20    chmod 755 job/P_$REPLY
21done
22
23exit 0

template のコード例

 1#!/bin/bash
 2
 3# 自分自身のファイル名と、実行するジョブ名を取得(ex. P_ABC -> ABC)
 4p_jobname=${BASH_SOURCE##*/}
 5jobname=${p_jobname:2}
 6
 7# メッセージ出力先をログファイルに向ける
 8exec > $logdir/$p_jobname.log 2>&1
 9
10# startファイル作成
11touch $stsdir/$p_jobname.start
12echo "$jobname 開始   : $(date)"
13
14# ジョブの実行
15$jobdir/$jobname  # jobdirはjobctl実行時の引数値(exportされている)
16if [ $? -eq 0 ];then
17    echo "$jobname 正常終了 : $(date)"
18    SUF="end"
19else
20    echo "$jobname 異常終了 : $(date)"
21    SUF="err"
22fi
23
24# end または err ファイル作成
25touch $stsdir/$p_jobname.$SUF

ジョブフロー定義ファイル ( jobflow.csv )

実行順序を規定する jobflow.csv (ジョブフロー定義ファイル)をあらかじめ作成する。 下記のように、左端に実行するジョブ、その右側にカンマ区切りで依存ジョブ(実行するために完了しておくべきジョブ)を記載する。 なお、 jobctl の処理により、 # より右側はコメントと見做され、空行は無視される。

 1# シャープ(#)の右側はコメント扱いとなる
 2# 空行は無視される
 3
 4J01
 5J02
 6J03
 7J04
 8
 9# J05はJ01,J02,J03が終わってから実行
10J05,J01,J02,J03
11
12# J06はJ03,J04が終わってから実行
13J06,J03,J04
14
15J07
16
17# J08はJ05が終わってから実行
18J08,J05
19
20# J09はJ06,J07が終わってから実行
21J09,J06,J07

ジョブコントローラ ( jobctl )

処理の流れ

  1. jobflow.csv の整形
  2. ステータスファイルのクリア
  3. メインループ
    1. 全ジョブ完了チェック。 完了していたら終了する。
    2. ジョブの実行。 jobflow.csv を走査し、依存ジョブが全て正常終了していることを確認後、ジョブを実行する。

コード例を示す。 チェック処理を端折っている(コードが長くなって理解しづらくなるため)。 引数チェック(引数がディレクトリとして存在するか)、 jobflow.csv の存在チェック、 jobflow.csv 内の各ジョブファイルの存在チェックは必要だろう。

さらには、対話型にしてジョブが異常終了した場合、ジョブフローの最初から開始するか、異常終了ジョブから開始するか、異常終了ジョブをスキップするかを選択させるメニューを表示するなど、改良することも可能だ。

 1#!/bin/bash
 2pwd=$PWD
 3p_jobdir=$pwd/job
 4stsdir=$pwd/status; export stsdir
 5logdir=$pwd/log; export logdir
 6
 7# 共通の終了処理
 8finish(){
 9    cd $pwd
10    wait # 実行中のジョブがあれば待機する(このコードは不要の判断もありうる)
11    if [ -f jobflow.csv.tmp ];then rm jobflow.csv.tmp; fi
12}
13
14# 引数のディレクトリパスを絶対パスに置換
15jobdir=$(realpath $1); export jobdir
16
17# 初期処理:csvの整形(以下処理順序に注意)
18sed 's/#.*//' jobflow.csv > jobflow.csv.tmp   # コメント文字以降を除去
19gsed -i -E 's/( |\t)//g' jobflow.csv.tmp      # 空白・タブを除去
20gsed -i '/^$/d' jobflow.csv.tmp               # 空行を除去
21
22# 初期処理:親ジョブの作成
23./makeParent
24if [ $? -ne 0 ];then
25    echo "makeParent error"
26    exit 1
27fi
28
29# 初期処理:ステータスファイルのクリア
30(cd $stsdir; rm -f *.start *.end *.err 2>/dev/null)
31(cd $logdir; rm -f *.log)
32
33# 関数:全ジョブ完了チェック
34completeCheck(){
35    # completeFlg: 0(未完了ジョブあり), 1(全て完了), 99(異常終了ジョブあり)
36	# errファイルの存在チェック
37	ls $stsdir/*.err >& /dev/null
38	if [ $? -eq 0 ];then return 99;fi
39
40	# errファイルが無い場合は、endファイルの存在チェックを行う
41    completeFlg=1
42    while IFS=, read -a line;do
43        job="${line[0]}"
44        # endファイルが無い場合は未完了
45        if [ ! -f $stsdir/P_$job.end ];then
46            completeFlg=0
47            break
48        fi
49    done < $pwd/jobflow.csv.tmp
50    return $completeFlg
51}
52
53# メイン処理
54cd $p_jobdir
55while :;do
56    # 全ジョブ完了チェック。完了していたら終了する。
57    completeCheck
58    ret=$?
59    if [ $ret -eq 1 ];then
60        echo "全てのジョブが完了しました"
61        exit 0
62	elif [ $ret -eq 99 ];then
63        echo "異常終了したジョブが見つかったためジョブフローを終了します。"
64        exit 1
65    fi
66
67    # ジョブの実行。csvを走査し、依存ジョブが全て正常終了していることを確認後、ジョブを実行する。
68    while IFS=, read -a line;do
69        job="${line[0]}" # 今から実行しようとしているジョブ名を取得
70        if [ -f $stsdir/P_$job.start ];then continue; fi # startファイルがあればすでに実行中なのでスキップ
71        if [[ ${line[@]:1} =~ ^( )*$ ]];then # 依存ジョブ(2列目以降)が無ければ、即時実行
72            echo $job を実行します
73            ./P_$job &
74            continue
75        fi
76        execflg="OK"
77        for prev in "${line[@]:1}";do # 依存ジョブ(2列目以降)を繰り返し取得
78            if [ "$prev" == "" ];then continue;fi # 取得した依存ジョブが空ならスキップ
79            if [ ! -f $stsdir/P_${prev}.end ];then # 依存ジョブが未実行・実行中・エラー終了の場合は実行不可
80                execflg="NG"
81                break
82            fi
83        done
84        if [ "$execflg" == "OK" ];then # 実行可能と判断された場合は実行
85            echo $job を実行します
86            ./P_$job &
87        fi
88    done < $pwd/jobflow.csv.tmp
89    sleep 1
90done
91cd $pwd
92
93exit 0
94

実行例

適当なディレクトリに J01 から J09 までのジョブ(シェルスクリプト)を作成した。 その内容は sleep コマンドの一行だけとした。sleep に渡す秒数は以下とした。

  • J01 : sleep 10
  • J02 : sleep 2
  • J03 : sleep 3
  • J04 : sleep 4
  • J05 : sleep 5
  • J06 : sleep 6
  • J07 : sleep 7
  • J08 : sleep 8
  • J09 : sleep 9

実行結果

 1$ ./jobctl ../sh
 2J01 を実行します
 3J02 を実行します
 4J03 を実行します
 5J04 を実行します
 6J07 を実行します
 7J06 を実行します
 8J05 を実行します
 9J09 を実行します
10J08 を実行します
11全てのジョブが完了しました
12$
13$
14$ ls -1 log
15P_J01.log
16P_J02.log
17P_J03.log
18P_J04.log
19P_J05.log
20P_J06.log
21P_J07.log
22P_J08.log
23P_J09.log
24$
25$
26$ cat log/*
27J01 開始   : 2024年 3月 3日 日曜日 11時05分46秒 JST
28J01 正常終了 : 2024年 3月 3日 日曜日 11時05分56秒 JST
29J02 開始   : 2024年 3月 3日 日曜日 11時05分47秒 JST
30J02 正常終了 : 2024年 3月 3日 日曜日 11時05分49秒 JST
31J03 開始   : 2024年 3月 3日 日曜日 11時05分46秒 JST
32J03 正常終了 : 2024年 3月 3日 日曜日 11時05分50秒 JST
33J04 開始   : 2024年 3月 3日 日曜日 11時05分47秒 JST
34J04 正常終了 : 2024年 3月 3日 日曜日 11時05分51秒 JST
35J05 開始   : 2024年 3月 3日 日曜日 11時05分57秒 JST
36J05 正常終了 : 2024年 3月 3日 日曜日 11時06分02秒 JST
37J06 開始   : 2024年 3月 3日 日曜日 11時05分51秒 JST
38J06 正常終了 : 2024年 3月 3日 日曜日 11時05分57秒 JST
39J07 開始   : 2024年 3月 3日 日曜日 11時05分46秒 JST
40J07 正常終了 : 2024年 3月 3日 日曜日 11時05分53秒 JST
41J08 開始   : 2024年 3月 3日 日曜日 11時06分03秒 JST
42J08 正常終了 : 2024年 3月 3日 日曜日 11時06分11秒 JST
43J09 開始   : 2024年 3月 3日 日曜日 11時05分58秒 JST
44J09 正常終了 : 2024年 3月 3日 日曜日 11時06分07秒 JST 
45$

図示すると実行順序が分かりやすい。 少しのタイムラグがあるものの待機時間はほとんどなく、 jobflow.csv に従って実行されていることがわかる。

関連ページ