github.com/hy3/cuto@v0.9.8-0.20160830082821-aa6652f877b7/master/jobnet/job.go (about)

     1  // Copyright 2015 unirita Inc.
     2  // Created 2015/04/10 honda
     3  
     4  package jobnet
     5  
     6  import (
     7  	"fmt"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/unirita/cuto/console"
    13  	"github.com/unirita/cuto/db"
    14  	"github.com/unirita/cuto/db/tx"
    15  	"github.com/unirita/cuto/log"
    16  	"github.com/unirita/cuto/master/config"
    17  	"github.com/unirita/cuto/master/remote"
    18  	"github.com/unirita/cuto/message"
    19  	"github.com/unirita/cuto/util"
    20  )
    21  
    22  type sendFunc func(string, int, string, chan<- string) (string, error)
    23  
    24  // ジョブを表す構造体
    25  type Job struct {
    26  	id            string   // ジョブID
    27  	Name          string   // ジョブ名
    28  	Node          string   // ノード
    29  	Port          int      // ポート番号
    30  	FilePath      string   // ジョブファイル
    31  	Param         string   // ジョブ引き渡しパラメータ
    32  	Env           string   // ジョブ実行に渡す環境変数
    33  	Workspace     string   // ジョブ実行時の作業フォルダ
    34  	WrnRC         int      // 警告終了と判断する戻り値の下限値
    35  	WrnPtn        string   // 警告終了と判断するジョブの出力メッセージ
    36  	ErrRC         int      // 異常終了と判断する戻り値の下限値
    37  	ErrPtn        string   // 異常終了と判断するジョブの出力メッセージ
    38  	Timeout       int      // ジョブ実行時間のタイムアウト
    39  	SecondaryNode string   // セカンダリサーバントのノード
    40  	SecondaryPort int      // セカンダリサーバントのポート番号
    41  	Next          Element  // 次ノード
    42  	Instance      *Network // ネットワーク情報構造体のポインタ
    43  	sendRequest   sendFunc // リクエスト送信メソッド
    44  	IsRerunJob    bool     // リランジョブであるかどうか
    45  }
    46  
    47  // Job構造体のコンストラクタ関数。
    48  //
    49  // param ; id  ジョブネットワークID。
    50  //
    51  // param : name  ジョブネットワーク名。
    52  //
    53  // param : nwk  ジョブネットワーク構造体。
    54  //
    55  // return : ジョブ情報構造体。
    56  //
    57  // return : エラー情報。
    58  func NewJob(id string, name string, nwk *Network) (*Job, error) {
    59  	if util.JobnameHasInvalidRune(name) {
    60  		return nil, fmt.Errorf("Job name[%s] includes forbidden character.", name)
    61  	}
    62  	job := new(Job)
    63  	job.id = id
    64  	job.Name = name
    65  	job.Instance = nwk
    66  	job.sendRequest = remote.SendRequest
    67  	return job, nil
    68  }
    69  
    70  // IDを取得する
    71  //
    72  // return : ジョブID.
    73  func (j *Job) ID() string {
    74  	return j.id
    75  }
    76  
    77  // ノードタイプを取得する
    78  //
    79  // return : ノードタイプ
    80  func (j *Job) Type() elementType {
    81  	return ELM_JOB
    82  }
    83  
    84  // 後続エレメントの追加を行う。
    85  //
    86  // param : 追加する要素情報。
    87  func (j *Job) AddNext(e Element) error {
    88  	if j.Next != nil {
    89  		return fmt.Errorf("ServiceTask cannot connect with over 1 element.")
    90  	}
    91  	j.Next = e
    92  
    93  	return nil
    94  }
    95  
    96  // 後続エレメントの有無を調べる。
    97  //
    98  // return : 要素が存在する場合はtrueを返す。
    99  func (j *Job) HasNext() bool {
   100  	return j.Next != nil
   101  }
   102  
   103  // 拡張ジョブ情報のデフォルト値をセットする
   104  func (j *Job) SetDefaultEx() {
   105  	if j.Node == "" {
   106  		j.Node = config.Job.DefaultNode
   107  	}
   108  	if j.Port == 0 {
   109  		j.Port = config.Job.DefaultPort
   110  	}
   111  	if j.FilePath == "" {
   112  		j.FilePath = j.Name
   113  	}
   114  	if j.Timeout < 0 {
   115  		j.Timeout = config.Job.DefaultTimeoutMin * 60
   116  	}
   117  }
   118  
   119  // ジョブ実行リクエストをservantへ送信する。
   120  //
   121  // return : 次の実行ノード
   122  //
   123  // return : エラー情報。
   124  func (j *Job) Execute() (Element, error) {
   125  	if j.IsRerunJob {
   126  		jobres, _ := j.Instance.Result.GetJobResults(j.id)
   127  		if jobres.Status == db.NORMAL || jobres.Status == db.WARN {
   128  			j.resumeJobValue()
   129  			return j.Next, nil
   130  		} else {
   131  			j.Node = jobres.Node
   132  			j.Port = jobres.Port
   133  			result, err := j.requestLatestJobResult()
   134  			if err != nil {
   135  				return nil, j.abnormalEnd(err)
   136  			}
   137  
   138  			switch result.Stat {
   139  			case db.RUNNING:
   140  				return nil, fmt.Errorf("Job ID [%s] still running.", j.id)
   141  			case db.NORMAL:
   142  				fallthrough
   143  			case db.WARN:
   144  				j.updateNormalEndResult(result)
   145  				return j.Next, nil
   146  			default:
   147  				j.changeStatusRunning()
   148  			}
   149  		}
   150  	}
   151  	res, err := j.executeRequest()
   152  	if err != nil {
   153  		return nil, j.abnormalEnd(err)
   154  	}
   155  	defer j.end(res)
   156  
   157  	if isAbnormalEnd(res) {
   158  		joblogFile := res.JoblogFile
   159  		if len(joblogFile) == 0 {
   160  			j.createJoblogFileName(res)
   161  		}
   162  		console.Display("CTM026I", joblogFile, j.Node)
   163  		return nil, fmt.Errorf("")
   164  	}
   165  
   166  	return j.Next, nil
   167  }
   168  
   169  func (j *Job) executeRequest() (*message.Response, error) {
   170  	if !j.IsRerunJob {
   171  		j.start()
   172  	}
   173  	console.Display("CTM023I", j.Name, j.Node, j.Instance.ID, j.id)
   174  
   175  	resMsg, err := j.requestAndWaitResult()
   176  	if j.isNecessaryToRetry(err) && j.SecondaryNode != "" {
   177  		j.useSecondaryNode()
   178  		console.Display("CTM028W", j.Name, j.SecondaryNode)
   179  		resMsg, err = j.requestAndWaitResult()
   180  	}
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	res := new(message.Response)
   186  	err = res.ParseJSON(resMsg)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	return res, nil
   192  }
   193  
   194  func (j *Job) requestAndWaitResult() (string, error) {
   195  	req := j.createRequest()
   196  	err := req.ExpandMasterVars()
   197  	if err != nil {
   198  		return "", err
   199  	}
   200  
   201  	reqMsg, err := req.GenerateJSON()
   202  	if err != nil {
   203  		return "", err
   204  	}
   205  
   206  	timerEndCh := make(chan struct{}, 1)
   207  	go j.startTimer(timerEndCh)
   208  	defer close(timerEndCh)
   209  
   210  	stCh := make(chan string, 1)
   211  	defer close(stCh)
   212  	go j.waitAndSetResultStartDate(stCh)
   213  
   214  	return j.sendRequestWithRetry(reqMsg, stCh)
   215  }
   216  
   217  func (j *Job) requestLatestJobResult() (*message.JobResult, error) {
   218  	chk := new(message.JobCheck)
   219  	chk.NID = j.Instance.ID
   220  	chk.JID = j.ID()
   221  
   222  	chkMsg, err := chk.GenerateJSON()
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	resultMsg, err := j.sendResultCheckRequestWithRetry(chkMsg)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	result := new(message.JobResult)
   233  	err = result.ParseJSON(resultMsg)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return result, nil
   239  }
   240  
   241  // ジョブ実行リクエストを送信する。
   242  // 送信失敗時には必要な回数だけリトライを行う。
   243  func (j *Job) sendRequestWithRetry(reqMsg string, stCh chan<- string) (string, error) {
   244  	limit := config.Job.AttemptLimit
   245  	var resMsg string
   246  	var err error
   247  	for i := 0; i < limit; i++ {
   248  		if i != 0 {
   249  			console.Display("CTM027W", j.Name, i, limit-1)
   250  		}
   251  
   252  		host, _, _ := explodeNodeString(j.Node)
   253  		resMsg, err = j.sendRequest(host, j.Port, reqMsg, stCh)
   254  		if !j.isNecessaryToRetry(err) {
   255  			break
   256  		}
   257  	}
   258  
   259  	return resMsg, err
   260  }
   261  
   262  func (j *Job) sendResultCheckRequestWithRetry(chkMsg string) (string, error) {
   263  	stCh := make(chan string, 1)
   264  	defer close(stCh)
   265  
   266  	limit := config.Job.AttemptLimit
   267  	var resultMsg string
   268  	var err error
   269  	for i := 0; i < limit; i++ {
   270  		if i != 0 {
   271  			console.Display("CTM027W", j.Name, i, limit-1)
   272  		}
   273  
   274  		host, _, _ := explodeNodeString(j.Node)
   275  		resultMsg, err = j.sendRequest(host, j.Port, chkMsg, stCh)
   276  		if err == nil {
   277  			break
   278  		}
   279  	}
   280  
   281  	return resultMsg, err
   282  }
   283  
   284  // リトライの必要があるかを判定する。
   285  // 判定条件:リクエスト送信でエラーが発生しており、スタート時刻がセットされていないこと
   286  func (j *Job) isNecessaryToRetry(err error) bool {
   287  	if err != nil {
   288  		jobres, exist := j.Instance.Result.GetJobResults(j.id)
   289  		if !exist {
   290  			return true
   291  		} else if jobres.StartDate == "" {
   292  			return true
   293  		}
   294  	}
   295  
   296  	return false
   297  }
   298  
   299  // responseメッセージrのステータスを参照し、ジョブが異常終了している場合はtrueを返す。
   300  // それ以外はfalseを返す。
   301  func isAbnormalEnd(r *message.Response) bool {
   302  	if r.Stat == db.ABNORMAL {
   303  		return true
   304  	}
   305  	return false
   306  }
   307  
   308  // ジョブの開始処理を行う。
   309  func (j *Job) start() {
   310  	jobres := db.NewJobResult(int(j.Instance.ID))
   311  	jobres.JobId = j.ID()
   312  	jobres.JobName = j.Name
   313  	jobres.Node = j.Node
   314  	jobres.Port = j.Port
   315  	jobres.Status = db.RUNNING
   316  
   317  	j.Instance.Result.AddJobResults(j.id, jobres)
   318  	tx.InsertJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   319  }
   320  
   321  // 実行ノードをセカンダリノードに変更する。
   322  func (j *Job) useSecondaryNode() {
   323  	j.Node = j.SecondaryNode
   324  	j.Port = j.SecondaryPort
   325  
   326  	jobres, exist := j.Instance.Result.GetJobResults(j.id)
   327  	if !exist {
   328  		log.Error(fmt.Errorf("Job result[id = %s] is unregisted.", j.id))
   329  		return
   330  	}
   331  
   332  	jobres.Node = j.SecondaryNode
   333  	jobres.Port = j.SecondaryPort
   334  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   335  
   336  	j.SecondaryNode = ""
   337  	j.SecondaryPort = 0
   338  }
   339  
   340  // ジョブ実行結果にジョブの開始時刻をセットする。
   341  func (j *Job) waitAndSetResultStartDate(stCh <-chan string) {
   342  	st := <-stCh
   343  	if len(st) == 0 {
   344  		// (主にチャネルがクローズされることにより)空文字列が送られてきた場合は何もしない。
   345  		return
   346  	}
   347  	log.Debug(fmt.Sprintf("JOB[%s] StartDate[%s]", j.Name, st))
   348  
   349  	jobres, exist := j.Instance.Result.GetJobResults(j.id)
   350  	if !exist {
   351  		log.Error(fmt.Errorf("Job result[id = %s] is unregisted.", j.id))
   352  		return
   353  	}
   354  	jobres.StartDate = st
   355  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   356  }
   357  
   358  // ジョブの終了メッセージから、ジョブ状態の更新を行う。
   359  func (j *Job) end(res *message.Response) {
   360  	var jobres *db.JobResult
   361  	var exist bool
   362  
   363  	if jobres, exist = j.Instance.Result.GetJobResults(j.id); !exist {
   364  		log.Error(fmt.Errorf("Job result[id = %s] is unregisted.", j.id))
   365  		return
   366  	}
   367  	jobres.StartDate = res.St
   368  	jobres.EndDate = res.Et
   369  	jobres.Status = res.Stat
   370  	jobres.Rc = res.RC
   371  	jobres.Detail = res.Detail
   372  	jobres.Variable = res.Var
   373  
   374  	message.AddJobValue(j.Name, res)
   375  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   376  
   377  	var st string
   378  	switch jobres.Status {
   379  	case db.NORMAL:
   380  		st = db.ST_NORMAL
   381  	case db.WARN:
   382  		st = db.ST_WARN
   383  	default:
   384  		st = db.ST_ABNORMAL
   385  	}
   386  	if jobres.Status != db.ABNORMAL {
   387  		console.Display("CTM024I", j.Name, j.Node, j.Instance.ID, j.id, st)
   388  	} else {
   389  		console.Display("CTM025W", j.Name, j.Node, j.Instance.ID, j.id, st, jobres.Detail)
   390  	}
   391  }
   392  
   393  // サーバントへ送受信失敗した場合の異常終了処理
   394  func (j *Job) abnormalEnd(err error) error {
   395  	jobres, exist := j.Instance.Result.GetJobResults(j.id)
   396  	if !exist {
   397  		return fmt.Errorf("Job result[id = %s] is unregisted.", j.id)
   398  	}
   399  	jobres.Status = db.ABNORMAL
   400  	jobres.Detail = err.Error()
   401  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   402  
   403  	console.Display("CTM025W", j.Name, j.Node, j.Instance.ID, j.id, jobres.Status, jobres.Detail)
   404  	return err
   405  }
   406  
   407  func (j *Job) resumeJobValue() {
   408  	jobres, _ := j.Instance.Result.GetJobResults(j.id)
   409  
   410  	res := new(message.Response)
   411  	res.JID = j.id
   412  	res.RC = jobres.Rc
   413  	res.St = jobres.StartDate
   414  	res.Et = jobres.EndDate
   415  	res.Var = jobres.Variable
   416  	message.AddJobValue(j.Name, res)
   417  }
   418  
   419  func (j *Job) updateNormalEndResult(result *message.JobResult) {
   420  	jobres, exists := j.Instance.Result.GetJobResults(j.id)
   421  	if !exists {
   422  		log.Error(fmt.Errorf("Job result[id = %s] is unregisted.", j.id))
   423  		return
   424  	}
   425  
   426  	jobres.Status = result.Stat
   427  	jobres.Rc = result.RC
   428  	jobres.StartDate = result.St
   429  	jobres.EndDate = result.Et
   430  	jobres.Detail = ""
   431  	jobres.Variable = result.Var
   432  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   433  
   434  	j.resumeJobValue()
   435  }
   436  
   437  func (j *Job) changeStatusRunning() {
   438  	jobres, exists := j.Instance.Result.GetJobResults(j.id)
   439  	if !exists {
   440  		log.Error(fmt.Errorf("Job result[id = %s] is unregisted.", j.id))
   441  		return
   442  	}
   443  
   444  	jobres.Status = db.RUNNING
   445  	tx.UpdateJob(j.Instance.Result.GetConnection(), jobres, &j.Instance.localMutex)
   446  }
   447  
   448  func (j *Job) startTimer(endCh chan struct{}) {
   449  	span := config.Job.TimeTrackingSpanMin
   450  	if span == 0 {
   451  		// 出力間隔の設定が0の場合は出力しない。
   452  		return
   453  	}
   454  
   455  	rapTime := 0
   456  	for {
   457  		select {
   458  		case <-time.After(time.Duration(span) * time.Minute):
   459  			rapTime += span
   460  			console.Display("CTM022I", j.Name, rapTime)
   461  		case <-endCh:
   462  			return
   463  		}
   464  	}
   465  }
   466  
   467  func (j *Job) createJoblogFileName(r *message.Response) string {
   468  	// ジョブ名(拡張子なし)の取得
   469  	job := j.FilePath
   470  	if strings.LastIndex(job, "\\") != -1 {
   471  		tokens := strings.Split(job, "\\")
   472  		job = tokens[len(tokens)-1]
   473  	} else if strings.LastIndex(job, "/") != -1 {
   474  		tokens := strings.Split(job, "/")
   475  		job = tokens[len(tokens)-1]
   476  	}
   477  	if extpos := strings.LastIndex(job, "."); extpos != -1 {
   478  		job = job[:extpos]
   479  	}
   480  
   481  	timestamp := r.St
   482  	timestamp = strings.Replace(timestamp, "-", "", -1)
   483  	timestamp = strings.Replace(timestamp, " ", "", -1)
   484  	timestamp = strings.Replace(timestamp, ":", "", -1)
   485  
   486  	return fmt.Sprintf("%v.%v.%v.%v.log", j.Instance.ID, job, j.ID(), timestamp)
   487  }
   488  
   489  func (j *Job) createRequest() *message.Request {
   490  	req := new(message.Request)
   491  	req.NID = j.Instance.ID
   492  	req.JID = j.ID()
   493  	req.Path = j.FilePath
   494  	req.Param = j.Param
   495  	req.Env = j.Env
   496  	req.Workspace = j.Workspace
   497  	req.WarnRC = j.WrnRC
   498  	req.WarnStr = j.WrnPtn
   499  	req.ErrRC = j.ErrRC
   500  	req.ErrStr = j.ErrPtn
   501  	req.Timeout = j.Timeout
   502  
   503  	_, cntHost, cntName := explodeNodeString(j.Node)
   504  	if cntName != "" {
   505  		req.Path = message.DockerTag
   506  
   507  		req.Param = fmt.Sprintf("exec %s %s %s", cntName, j.FilePath, j.Param)
   508  		if cntHost != "" {
   509  			req.Param = fmt.Sprintf("-H=%s %s", cntHost, req.Param)
   510  		}
   511  	}
   512  
   513  	return req
   514  }
   515  
   516  func explodeNodeString(node string) (string, string, string) {
   517  	hostAndContainer := strings.SplitN(node, ">", 2)
   518  	if len(hostAndContainer) == 1 {
   519  		return node, "", ""
   520  	}
   521  
   522  	host := hostAndContainer[0]
   523  	container := hostAndContainer[1]
   524  	containerURL, err := url.Parse(container)
   525  	if err != nil || !containerURL.IsAbs() {
   526  		return host, "", container
   527  	}
   528  
   529  	containerHost := fmt.Sprintf("%s://%s", containerURL.Scheme, containerURL.Host)
   530  	containerName := strings.TrimLeft(containerURL.Path, "/")
   531  	return host, containerHost, containerName
   532  }