001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.command.coord; 020 021import java.util.ArrayList; 022import java.util.Date; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Set; 028 029import org.apache.commons.lang.StringUtils; 030import org.apache.oozie.CoordinatorActionBean; 031import org.apache.oozie.CoordinatorJobBean; 032import org.apache.oozie.ErrorCode; 033import org.apache.oozie.XException; 034import org.apache.oozie.client.CoordinatorAction; 035import org.apache.oozie.client.CoordinatorJob; 036import org.apache.oozie.client.Job; 037import org.apache.oozie.client.OozieClient; 038import org.apache.oozie.client.rest.JsonBean; 039import org.apache.oozie.command.CommandException; 040import org.apache.oozie.command.PreconditionException; 041import org.apache.oozie.command.bundle.BundleStatusUpdateXCommand; 042import org.apache.oozie.executor.jpa.BatchQueryExecutor; 043import org.apache.oozie.executor.jpa.BatchQueryExecutor.UpdateEntry; 044import org.apache.oozie.executor.jpa.CoordActionQueryExecutor; 045import org.apache.oozie.executor.jpa.CoordJobGetJPAExecutor; 046import org.apache.oozie.executor.jpa.CoordJobQueryExecutor.CoordJobQuery; 047import org.apache.oozie.executor.jpa.JPAExecutorException; 048import org.apache.oozie.executor.jpa.SLARegistrationQueryExecutor; 049import org.apache.oozie.executor.jpa.SLARegistrationQueryExecutor.SLARegQuery; 050import org.apache.oozie.executor.jpa.SLASummaryQueryExecutor; 051import org.apache.oozie.executor.jpa.SLASummaryQueryExecutor.SLASummaryQuery; 052import org.apache.oozie.service.JPAService; 053import org.apache.oozie.service.Services; 054import org.apache.oozie.sla.SLARegistrationBean; 055import org.apache.oozie.sla.SLASummaryBean; 056import org.apache.oozie.sla.service.SLAService; 057import org.apache.oozie.util.DateUtils; 058import org.apache.oozie.util.JobUtils; 059import org.apache.oozie.util.LogUtils; 060import org.apache.oozie.util.ParamChecker; 061import org.apache.oozie.util.StatusUtils; 062 063public class CoordChangeXCommand extends CoordinatorXCommand<Void> { 064 private final String jobId; 065 private Date newEndTime = null; 066 private Integer oldConcurrency = null; 067 private Integer newConcurrency = null; 068 private Date newPauseTime = null; 069 private Date oldPauseTime = null; 070 private boolean resetPauseTime = false; 071 private CoordinatorJob.Status jobStatus = null; 072 private CoordinatorJobBean coordJob; 073 private JPAService jpaService = null; 074 private Job.Status prevStatus; 075 private List<UpdateEntry> updateList = new ArrayList<UpdateEntry>(); 076 private List<JsonBean> deleteList = new ArrayList<JsonBean>(); 077 078 private static final Set<String> ALLOWED_CHANGE_OPTIONS = new HashSet<String>(); 079 static { 080 ALLOWED_CHANGE_OPTIONS.add("endtime"); 081 ALLOWED_CHANGE_OPTIONS.add("concurrency"); 082 ALLOWED_CHANGE_OPTIONS.add("pausetime"); 083 ALLOWED_CHANGE_OPTIONS.add(OozieClient.CHANGE_VALUE_STATUS); 084 085 } 086 087 /** 088 * This command is used to update the Coordinator job with the new values Update the coordinator job bean and update 089 * that to database. 090 * 091 * @param id Coordinator job id. 092 * @param changeValue This the changed value in the form key=value. 093 * @throws CommandException thrown if changeValue cannot be parsed properly. 094 */ 095 public CoordChangeXCommand(String id, String changeValue) throws CommandException { 096 super("coord_change", "coord_change", 0); 097 this.jobId = ParamChecker.notEmpty(id, "id"); 098 ParamChecker.notEmpty(changeValue, "value"); 099 100 validateChangeValue(changeValue); 101 } 102 103 @Override 104 protected void setLogInfo() { 105 LogUtils.setLogInfo(jobId); 106 } 107 108 /** 109 * @param changeValue change value. 110 * @throws CommandException thrown if changeValue cannot be parsed properly. 111 */ 112 private void validateChangeValue(String changeValue) throws CommandException { 113 Map<String, String> map = JobUtils.parseChangeValue(changeValue); 114 115 if (map.size() > ALLOWED_CHANGE_OPTIONS.size()) { 116 throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime|status"); 117 } 118 119 java.util.Iterator<Entry<String, String>> iter = map.entrySet().iterator(); 120 while (iter.hasNext()) { 121 Entry<String, String> entry = iter.next(); 122 String key = entry.getKey(); 123 String value = entry.getValue(); 124 125 if (!ALLOWED_CHANGE_OPTIONS.contains(key)) { 126 throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime|status"); 127 } 128 129 if (!key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && value.equalsIgnoreCase("")) { 130 throw new CommandException(ErrorCode.E1015, changeValue, "value on " + key + " can not be empty"); 131 } 132 } 133 134 if (map.containsKey(OozieClient.CHANGE_VALUE_ENDTIME)) { 135 String value = map.get(OozieClient.CHANGE_VALUE_ENDTIME); 136 try { 137 newEndTime = DateUtils.parseDateOozieTZ(value); 138 } 139 catch (Exception ex) { 140 throw new CommandException(ErrorCode.E1015, value, "must be a valid date"); 141 } 142 } 143 144 if (map.containsKey(OozieClient.CHANGE_VALUE_CONCURRENCY)) { 145 String value = map.get(OozieClient.CHANGE_VALUE_CONCURRENCY); 146 try { 147 newConcurrency = Integer.parseInt(value); 148 } 149 catch (NumberFormatException ex) { 150 throw new CommandException(ErrorCode.E1015, value, "must be a valid integer"); 151 } 152 } 153 154 if (map.containsKey(OozieClient.CHANGE_VALUE_PAUSETIME)) { 155 String value = map.get(OozieClient.CHANGE_VALUE_PAUSETIME); 156 if (value.equals("")) { // this is to reset pause time to null; 157 resetPauseTime = true; 158 } 159 else { 160 try { 161 newPauseTime = DateUtils.parseDateOozieTZ(value); 162 } 163 catch (Exception ex) { 164 throw new CommandException(ErrorCode.E1015, value, "must be a valid date"); 165 } 166 } 167 } 168 169 if (map.containsKey(OozieClient.CHANGE_VALUE_STATUS)) { 170 String value = map.get(OozieClient.CHANGE_VALUE_STATUS); 171 if (!StringUtils.isEmpty(value)) { 172 jobStatus = CoordinatorJob.Status.valueOf(value); 173 } 174 } 175 } 176 177 /** 178 * Check if new end time is valid. 179 * 180 * @param coordJob coordinator job id. 181 * @param newEndTime new end time. 182 * @throws CommandException thrown if new end time is not valid. 183 */ 184 private void checkEndTime(CoordinatorJobBean coordJob, Date newEndTime) throws CommandException { 185 //It's ok to set end date before start date. 186 } 187 188 /** 189 * Check if new pause time is valid. 190 * 191 * @param coordJob coordinator job id. 192 * @param newPauseTime new pause time. 193 * @param newEndTime new end time, can be null meaning no change on end time. 194 * @throws CommandException thrown if new pause time is not valid. 195 */ 196 private void checkPauseTime(CoordinatorJobBean coordJob, Date newPauseTime) 197 throws CommandException { 198 //no check. 199 } 200 201 /** 202 * Check if status change is valid. 203 * 204 * @param coordJob the coord job 205 * @param jobStatus the job status 206 * @throws CommandException the command exception 207 */ 208 private void checkStatusChange(CoordinatorJobBean coordJob, CoordinatorJob.Status jobStatus) 209 throws CommandException { 210 if (!jobStatus.equals(CoordinatorJob.Status.RUNNING) && !jobStatus.equals(CoordinatorJob.Status.IGNORED)) { 211 throw new CommandException(ErrorCode.E1015, jobStatus, " must be RUNNING or IGNORED"); 212 } 213 214 if (jobStatus.equals(CoordinatorJob.Status.RUNNING)) { 215 if (!(coordJob.getStatus().equals(CoordinatorJob.Status.FAILED) || coordJob.getStatus().equals( 216 CoordinatorJob.Status.KILLED) || coordJob.getStatus().equals(CoordinatorJob.Status.IGNORED))) { 217 throw new CommandException(ErrorCode.E1015, jobStatus, 218 " Only FAILED, KILLED, IGNORED job can be changed to RUNNING. Current job status is " 219 + coordJob.getStatus()); 220 } 221 } 222 else { 223 if (!(coordJob.getStatus().equals(CoordinatorJob.Status.FAILED) || coordJob.getStatus().equals( 224 CoordinatorJob.Status.KILLED)) 225 || coordJob.isPending()) { 226 throw new CommandException(ErrorCode.E1015, jobStatus, 227 " Only FAILED or KILLED non-pending job can be changed to IGNORED. Current job status is " 228 + coordJob.getStatus() + " and pending status is " + coordJob.isPending()); 229 } 230 } 231 } 232 233 /** 234 * Process lookahead created actions that become invalid because of the new pause time, 235 * These actions will be deleted from DB, also the coordinator job will be updated accordingly 236 * 237 * @param coordJob coordinator job 238 * @param newPauseTime new pause time 239 * @throws JPAExecutorException, CommandException 240 */ 241 private void processLookaheadActions(CoordinatorJobBean coordJob, Date newTime) throws CommandException, 242 JPAExecutorException { 243 int lastActionNumber = coordJob.getLastActionNumber(); 244 Date lastActionTime = null; 245 Date tempDate = null; 246 247 while ((tempDate = deleteAction(lastActionNumber, newTime)) != null) { 248 lastActionNumber--; 249 lastActionTime = tempDate; 250 } 251 if (lastActionTime != null) { 252 LOG.debug("New pause/end date is : " + newTime + " and last action number is : " + lastActionNumber); 253 coordJob.setLastActionNumber(lastActionNumber); 254 coordJob.setLastActionTime(lastActionTime); 255 coordJob.setNextMaterializedTime(lastActionTime); 256 coordJob.resetDoneMaterialization(); 257 } 258 } 259 260 /** 261 * Delete coordinator action 262 * 263 * @param actionNum coordinator action number 264 */ 265 private Date deleteAction(int actionNum, Date afterDate) throws CommandException { 266 try { 267 if (actionNum <= 0) { 268 return null; 269 } 270 271 String actionId = jobId + "@" + actionNum; 272 CoordinatorActionBean bean = CoordActionQueryExecutor.getInstance().getIfExist( 273 CoordActionQueryExecutor.CoordActionQuery.GET_COORD_ACTION, actionId); 274 if (bean == null) { 275 return null; 276 } 277 if (afterDate.compareTo(bean.getNominalTime()) <= 0) { 278 if (bean.getStatus() == CoordinatorAction.Status.WAITING 279 || bean.getStatus() == CoordinatorAction.Status.READY) { 280 // delete SLA registration entry (if any) for action 281 if (SLAService.isEnabled()) { 282 Services.get().get(SLAService.class).removeRegistration(actionId); 283 } 284 SLARegistrationBean slaReg = SLARegistrationQueryExecutor.getInstance().get(SLARegQuery.GET_SLA_REG_ALL, 285 actionId); 286 if (slaReg != null) { 287 LOG.debug("Deleting registration bean corresponding to action " + slaReg.getId()); 288 deleteList.add(slaReg); 289 } 290 SLASummaryBean slaSummaryBean = SLASummaryQueryExecutor.getInstance().get( 291 SLASummaryQuery.GET_SLA_SUMMARY, actionId); 292 if (slaSummaryBean != null) { 293 LOG.debug("Deleting summary bean corresponding to action " + slaSummaryBean.getId()); 294 deleteList.add(slaSummaryBean); 295 } 296 deleteList.add(bean); 297 } 298 else { 299 throw new CommandException(ErrorCode.E1022, bean.getId()); 300 } 301 return bean.getNominalTime(); 302 } 303 else { 304 return null; 305 } 306 307 } 308 catch (JPAExecutorException e) { 309 throw new CommandException(e); 310 } 311 } 312 313 /** 314 * Check if new end time, new concurrency, new pause time are valid. 315 * 316 * @param coordJob coordinator job id. 317 * @param newEndTime new end time. 318 * @param newConcurrency new concurrency. 319 * @param newPauseTime new pause time. 320 * @throws CommandException thrown if new values are not valid. 321 */ 322 private void check(CoordinatorJobBean coordJob, Date newEndTime, Integer newConcurrency, Date newPauseTime, 323 CoordinatorJob.Status jobStatus) throws CommandException { 324 325 if (coordJob.getStatus() == CoordinatorJob.Status.KILLED 326 || coordJob.getStatus() == CoordinatorJob.Status.IGNORED) { 327 if (jobStatus == null || (newEndTime != null || newConcurrency != null || newPauseTime != null)) { 328 throw new CommandException(ErrorCode.E1016); 329 } 330 } 331 332 if (newEndTime != null) { 333 checkEndTime(coordJob, newEndTime); 334 } 335 336 if (newPauseTime != null) { 337 checkPauseTime(coordJob, newPauseTime); 338 } 339 if (jobStatus != null) { 340 checkStatusChange(coordJob, jobStatus); 341 } 342 } 343 344 /* (non-Javadoc) 345 * @see org.apache.oozie.command.XCommand#execute() 346 */ 347 @Override 348 protected Void execute() throws CommandException { 349 LOG.info("STARTED CoordChangeXCommand for jobId=" + jobId); 350 351 try { 352 oldConcurrency = this.coordJob.getConcurrency(); 353 if (newEndTime != null) { 354 // during coord materialization, nextMaterializedTime is set to 355 // startTime + n(actions materialized) * frequency and this can be AFTER endTime, 356 // while doneMaterialization is true. Hence the following checks 357 // for newEndTime being in the middle of endTime and nextMatdTime. 358 // Since job is already done materialization so no need to change 359 boolean dontChange = coordJob.getEndTime().before(newEndTime) 360 && coordJob.getNextMaterializedTime() != null 361 && coordJob.getNextMaterializedTime().after(newEndTime); 362 if (!dontChange) { 363 coordJob.setEndTime(newEndTime); 364 // OOZIE-1703, we should SUCCEEDED the coord, if it's in PREP and new endtime is before start time 365 if (coordJob.getStartTime().compareTo(newEndTime) >= 0) { 366 if (coordJob.getStatus() != CoordinatorJob.Status.PREP) { 367 processLookaheadActions(coordJob, newEndTime); 368 } 369 if (coordJob.getStatus() == CoordinatorJob.Status.PREP 370 || coordJob.getStatus() == CoordinatorJob.Status.RUNNING) { 371 LOG.info("Changing coord status to SUCCEEDED, because it's in " + coordJob.getStatus() 372 + " and new end time is before start time. Startime is " + coordJob.getStartTime() 373 + " and new end time is " + newEndTime); 374 375 coordJob.setStatus(CoordinatorJob.Status.SUCCEEDED); 376 coordJob.resetPending(); 377 } 378 coordJob.setDoneMaterialization(); 379 } 380 else { 381 // move it to running iff new end time is after starttime. 382 if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED) { 383 coordJob.setStatus(CoordinatorJob.Status.RUNNING); 384 } 385 if (coordJob.getStatus() == CoordinatorJob.Status.DONEWITHERROR 386 || coordJob.getStatus() == CoordinatorJob.Status.FAILED) { 387 // Check for backward compatibility for Oozie versions (3.2 and before) 388 // when RUNNINGWITHERROR, SUSPENDEDWITHERROR and 389 // PAUSEDWITHERROR is not supported 390 coordJob.setStatus(StatusUtils 391 .getStatusIfBackwardSupportTrue(CoordinatorJob.Status.RUNNINGWITHERROR)); 392 } 393 coordJob.setPending(); 394 coordJob.resetDoneMaterialization(); 395 processLookaheadActions(coordJob, newEndTime); 396 } 397 } 398 399 else { 400 LOG.info("Didn't change endtime. Endtime is in between coord end time and next materialization time." 401 + "Coord endTime = " + DateUtils.formatDateOozieTZ(newEndTime) 402 + " next materialization time =" 403 + DateUtils.formatDateOozieTZ(coordJob.getNextMaterializedTime())); 404 } 405 } 406 407 if (newConcurrency != null) { 408 this.coordJob.setConcurrency(newConcurrency); 409 } 410 411 if (newPauseTime != null || resetPauseTime == true) { 412 this.coordJob.setPauseTime(newPauseTime); 413 if (oldPauseTime != null && newPauseTime != null) { 414 if (oldPauseTime.before(newPauseTime)) { 415 if (this.coordJob.getStatus() == Job.Status.PAUSED) { 416 this.coordJob.setStatus(Job.Status.RUNNING); 417 } 418 else if (this.coordJob.getStatus() == Job.Status.PAUSEDWITHERROR) { 419 this.coordJob.setStatus(Job.Status.RUNNINGWITHERROR); 420 } 421 } 422 } 423 else if (oldPauseTime != null && newPauseTime == null) { 424 if (this.coordJob.getStatus() == Job.Status.PAUSED) { 425 this.coordJob.setStatus(Job.Status.RUNNING); 426 } 427 else if (this.coordJob.getStatus() == Job.Status.PAUSEDWITHERROR) { 428 this.coordJob.setStatus(Job.Status.RUNNINGWITHERROR); 429 } 430 } 431 if (!resetPauseTime) { 432 processLookaheadActions(coordJob, newPauseTime); 433 } 434 } 435 if (jobStatus != null) { 436 coordJob.setStatus(jobStatus); 437 LOG.info("Coord status is changed to " + jobStatus + " from " + prevStatus); 438 if (jobStatus.equals(CoordinatorJob.Status.RUNNING)) { 439 coordJob.setPending(); 440 if (coordJob.getNextMaterializedTime() == null 441 || coordJob.getEndTime().after(coordJob.getNextMaterializedTime())) { 442 coordJob.resetDoneMaterialization(); 443 } 444 } else if (jobStatus.equals(CoordinatorJob.Status.IGNORED)) { 445 coordJob.resetPending(); 446 coordJob.setDoneMaterialization(); 447 } 448 } 449 450 if (coordJob.getNextMaterializedTime() != null && coordJob.getEndTime() 451 .compareTo(coordJob.getNextMaterializedTime()) <= 0) { 452 LOG.info("[" + coordJob.getId() + "]: all actions have been materialized, job status = " + coordJob.getStatus() 453 + ", set pending to true"); 454 // set doneMaterialization to true when materialization is done 455 coordJob.setDoneMaterialization(); 456 } 457 458 coordJob.setLastModifiedTime(new Date()); 459 updateList.add(new UpdateEntry<CoordJobQuery>(CoordJobQuery.UPDATE_COORD_JOB_CHANGE, coordJob)); 460 BatchQueryExecutor.getInstance().executeBatchInsertUpdateDelete(null, updateList, deleteList); 461 462 if (newConcurrency != null && newConcurrency > oldConcurrency) { 463 queue(new CoordActionReadyXCommand(jobId)); 464 } 465 466 return null; 467 } 468 catch (XException ex) { 469 throw new CommandException(ex); 470 } 471 finally { 472 LOG.info("ENDED CoordChangeXCommand for jobId=" + jobId); 473 // update bundle action 474 if (coordJob.getBundleId() != null) { 475 //ignore pending as it'sync command 476 BundleStatusUpdateXCommand bundleStatusUpdate = new BundleStatusUpdateXCommand(coordJob, prevStatus, true); 477 bundleStatusUpdate.call(); 478 } 479 } 480 } 481 482 /* (non-Javadoc) 483 * @see org.apache.oozie.command.XCommand#getEntityKey() 484 */ 485 @Override 486 public String getEntityKey() { 487 return this.jobId; 488 } 489 490 /* (non-Javadoc) 491 * @see org.apache.oozie.command.XCommand#loadState() 492 */ 493 @Override 494 protected void loadState() throws CommandException{ 495 jpaService = Services.get().get(JPAService.class); 496 497 if (jpaService == null) { 498 throw new CommandException(ErrorCode.E0610); 499 } 500 501 try { 502 this.coordJob = jpaService.execute(new CoordJobGetJPAExecutor(jobId)); 503 oldPauseTime = coordJob.getPauseTime(); 504 prevStatus = coordJob.getStatus(); 505 } 506 catch (JPAExecutorException e) { 507 throw new CommandException(e); 508 } 509 510 LogUtils.setLogInfo(this.coordJob); 511 } 512 513 /* (non-Javadoc) 514 * @see org.apache.oozie.command.XCommand#verifyPrecondition() 515 */ 516 @Override 517 protected void verifyPrecondition() throws CommandException,PreconditionException { 518 check(this.coordJob, newEndTime, newConcurrency, newPauseTime, jobStatus); 519 } 520 521 /* (non-Javadoc) 522 * @see org.apache.oozie.command.XCommand#isLockRequired() 523 */ 524 @Override 525 protected boolean isLockRequired() { 526 return true; 527 } 528}