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 }