001    package org.apache.oozie.command.coord;
002    
003    import java.util.Date;
004    import java.util.HashSet;
005    import java.util.Map;
006    import java.util.Set;
007    import java.util.Map.Entry;
008    
009    import org.apache.oozie.CoordinatorActionBean;
010    import org.apache.oozie.CoordinatorJobBean;
011    import org.apache.oozie.ErrorCode;
012    import org.apache.oozie.XException;
013    import org.apache.oozie.client.CoordinatorJob;
014    import org.apache.oozie.client.Job;
015    import org.apache.oozie.client.OozieClient;
016    import org.apache.oozie.command.CommandException;
017    import org.apache.oozie.command.PreconditionException;
018    import org.apache.oozie.command.bundle.BundleStatusUpdateXCommand;
019    import org.apache.oozie.executor.jpa.CoordActionRemoveJPAExecutor;
020    import org.apache.oozie.executor.jpa.CoordJobGetActionByActionNumberJPAExecutor;
021    import org.apache.oozie.executor.jpa.CoordJobGetJPAExecutor;
022    import org.apache.oozie.executor.jpa.CoordJobUpdateJPAExecutor;
023    import org.apache.oozie.executor.jpa.JPAExecutorException;
024    import org.apache.oozie.service.JPAService;
025    import org.apache.oozie.service.Services;
026    import org.apache.oozie.util.DateUtils;
027    import org.apache.oozie.util.JobUtils;
028    import org.apache.oozie.util.LogUtils;
029    import org.apache.oozie.util.ParamChecker;
030    
031    public class CoordChangeXCommand extends CoordinatorXCommand<Void> {
032        private final String jobId;
033        private Date newEndTime = null;
034        private Integer newConcurrency = null;
035        private Date newPauseTime = null;
036        private Date oldPauseTime = null;
037        private boolean resetPauseTime = false;
038        private CoordinatorJobBean coordJob;
039        private JPAService jpaService = null;
040        private Job.Status prevStatus;
041    
042        private static final Set<String> ALLOWED_CHANGE_OPTIONS = new HashSet<String>();
043        static {
044            ALLOWED_CHANGE_OPTIONS.add("endtime");
045            ALLOWED_CHANGE_OPTIONS.add("concurrency");
046            ALLOWED_CHANGE_OPTIONS.add("pausetime");
047        }
048    
049        /**
050         * This command is used to update the Coordinator job with the new values Update the coordinator job bean and update
051         * that to database.
052         *
053         * @param id Coordinator job id.
054         * @param changeValue This the changed value in the form key=value.
055         * @throws CommandException thrown if changeValue cannot be parsed properly.
056         */
057        public CoordChangeXCommand(String id, String changeValue) throws CommandException {
058            super("coord_change", "coord_change", 0);
059            this.jobId = ParamChecker.notEmpty(id, "id");
060            ParamChecker.notEmpty(changeValue, "value");
061    
062            validateChangeValue(changeValue);
063        }
064    
065        /**
066         * @param changeValue change value.
067         * @throws CommandException thrown if changeValue cannot be parsed properly.
068         */
069        private void validateChangeValue(String changeValue) throws CommandException {
070            Map<String, String> map = JobUtils.parseChangeValue(changeValue);
071    
072            if (map.size() > ALLOWED_CHANGE_OPTIONS.size()) {
073                throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
074            }
075    
076            java.util.Iterator<Entry<String, String>> iter = map.entrySet().iterator();
077            while (iter.hasNext()) {
078                Entry<String, String> entry = iter.next();
079                String key = entry.getKey();
080                String value = entry.getValue();
081    
082                if (!ALLOWED_CHANGE_OPTIONS.contains(key)) {
083                    throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
084                }
085    
086                if (!key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && value.equalsIgnoreCase("")) {
087                    throw new CommandException(ErrorCode.E1015, changeValue, "value on " + key + " can not be empty");
088                }
089            }
090    
091            if (map.containsKey(OozieClient.CHANGE_VALUE_ENDTIME)) {
092                String value = map.get(OozieClient.CHANGE_VALUE_ENDTIME);
093                try {
094                    newEndTime = DateUtils.parseDateUTC(value);
095                }
096                catch (Exception ex) {
097                    throw new CommandException(ErrorCode.E1015, value, "must be a valid date");
098                }
099            }
100    
101            if (map.containsKey(OozieClient.CHANGE_VALUE_CONCURRENCY)) {
102                String value = map.get(OozieClient.CHANGE_VALUE_CONCURRENCY);
103                try {
104                    newConcurrency = Integer.parseInt(value);
105                }
106                catch (NumberFormatException ex) {
107                    throw new CommandException(ErrorCode.E1015, value, "must be a valid integer");
108                }
109            }
110    
111            if (map.containsKey(OozieClient.CHANGE_VALUE_PAUSETIME)) {
112                String value = map.get(OozieClient.CHANGE_VALUE_PAUSETIME);
113                if (value.equals("")) { // this is to reset pause time to null;
114                    resetPauseTime = true;
115                }
116                else {
117                    try {
118                        newPauseTime = DateUtils.parseDateUTC(value);
119                    }
120                    catch (Exception ex) {
121                        throw new CommandException(ErrorCode.E1015, value, "must be a valid date");
122                    }
123                }
124            }
125        }
126    
127        /**
128         * Check if new end time is valid.
129         *
130         * @param coordJob coordinator job id.
131         * @param newEndTime new end time.
132         * @throws CommandException thrown if new end time is not valid.
133         */
134        private void checkEndTime(CoordinatorJobBean coordJob, Date newEndTime) throws CommandException {
135            // New endTime cannot be before coordinator job's start time.
136            Date startTime = coordJob.getStartTime();
137            if (newEndTime.before(startTime)) {
138                throw new CommandException(ErrorCode.E1015, newEndTime, "cannot be before coordinator job's start time ["
139                        + startTime + "]");
140            }
141    
142            // New endTime cannot be before coordinator job's last action time.
143            Date lastActionTime = coordJob.getLastActionTime();
144            if (lastActionTime != null) {
145                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
146                if (!newEndTime.after(d)) {
147                    throw new CommandException(ErrorCode.E1015, newEndTime,
148                            "must be after coordinator job's last action time [" + d + "]");
149                }
150            }
151        }
152    
153        /**
154         * Check if new pause time is valid.
155         *
156         * @param coordJob coordinator job id.
157         * @param newPauseTime new pause time.
158         * @param newEndTime new end time, can be null meaning no change on end time.
159         * @throws CommandException thrown if new pause time is not valid.
160         */
161        private void checkPauseTime(CoordinatorJobBean coordJob, Date newPauseTime)
162                throws CommandException {
163            // New pauseTime has to be a non-past time.
164            Date d = new Date();
165            if (newPauseTime.before(d)) {
166                throw new CommandException(ErrorCode.E1015, newPauseTime, "must be a non-past time");
167            }
168        }
169    
170        /**
171         * Process lookahead created actions that become invalid because of the new pause time,
172         * These actions will be deleted from DB, also the coordinator job will be updated accordingly
173         *
174         * @param coordJob coordinator job
175         * @param newPauseTime new pause time
176         */
177        private void processLookaheadActions(CoordinatorJobBean coordJob, Date newPauseTime) throws CommandException {
178            Date lastActionTime = coordJob.getLastActionTime();
179            if (lastActionTime != null) {
180                // d is the real last action time.
181                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
182                int lastActionNumber = coordJob.getLastActionNumber();
183    
184                boolean hasChanged = false;
185                while (true) {
186                    if (!newPauseTime.after(d)) {
187                        deleteAction(coordJob.getId(), lastActionNumber);
188                        d = new Date(d.getTime() - coordJob.getFrequency() * 60 * 1000);
189                        lastActionNumber = lastActionNumber - 1;
190    
191                        hasChanged = true;
192                    }
193                    else {
194                        break;
195                    }
196                }
197    
198                if (hasChanged == true) {
199                    coordJob.setLastActionNumber(lastActionNumber);
200                    Date d1 = new Date(d.getTime() + coordJob.getFrequency() * 60 * 1000);
201                    coordJob.setLastActionTime(d1);
202                    coordJob.setNextMaterializedTime(d1);
203                    coordJob.resetDoneMaterialization();
204                }
205            }
206        }
207    
208        /**
209         * Delete last action for a coordinator job
210         *
211         * @param coordJob coordinator job
212         * @param lastActionNum last action number of the coordinator job
213         */
214        private void deleteAction(String jobId, int lastActionNum) throws CommandException {
215            try {
216                CoordinatorActionBean actionBean = jpaService.execute(new CoordJobGetActionByActionNumberJPAExecutor(jobId, lastActionNum));
217                jpaService.execute(new CoordActionRemoveJPAExecutor(actionBean.getId()));
218            }
219            catch (JPAExecutorException e) {
220                throw new CommandException(e);
221            }
222        }
223    
224        /**
225         * Check if new end time, new concurrency, new pause time are valid.
226         *
227         * @param coordJob coordinator job id.
228         * @param newEndTime new end time.
229         * @param newConcurrency new concurrency.
230         * @param newPauseTime new pause time.
231         * @throws CommandException thrown if new values are not valid.
232         */
233        private void check(CoordinatorJobBean coordJob, Date newEndTime, Integer newConcurrency, Date newPauseTime)
234                throws CommandException {
235            if (coordJob.getStatus() == CoordinatorJob.Status.KILLED) {
236                throw new CommandException(ErrorCode.E1016);
237            }
238    
239            if (newEndTime != null) {
240                checkEndTime(coordJob, newEndTime);
241            }
242    
243            if (newPauseTime != null) {
244                checkPauseTime(coordJob, newPauseTime);
245            }
246        }
247    
248        /* (non-Javadoc)
249         * @see org.apache.oozie.command.XCommand#execute()
250         */
251        @Override
252        protected Void execute() throws CommandException {
253            LOG.info("STARTED CoordChangeXCommand for jobId=" + jobId);
254    
255            try {
256                if (newEndTime != null) {
257                    coordJob.setEndTime(newEndTime);
258                    if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED
259                            || coordJob.getStatus() == CoordinatorJob.Status.RUNNING
260                            || coordJob.getStatus() == CoordinatorJob.Status.DONEWITHERROR
261                            || coordJob.getStatus() == CoordinatorJob.Status.FAILED) {
262                        coordJob.setStatus(CoordinatorJob.Status.RUNNING);
263                        coordJob.setPending();
264                        coordJob.resetDoneMaterialization();
265                    }
266                }
267    
268                if (newConcurrency != null) {
269                    this.coordJob.setConcurrency(newConcurrency);
270                }
271    
272                if (newPauseTime != null || resetPauseTime == true) {
273                    this.coordJob.setPauseTime(newPauseTime);
274                    if (oldPauseTime != null && newPauseTime != null) {
275                        if (oldPauseTime.before(newPauseTime) && this.coordJob.getStatus() == Job.Status.PAUSED) {
276                            this.coordJob.setStatus(Job.Status.RUNNING);
277                        }
278                    }
279                    else if (oldPauseTime != null && newPauseTime == null && this.coordJob.getStatus() == Job.Status.PAUSED) {
280                        this.coordJob.setStatus(Job.Status.RUNNING);
281                    }
282    
283                    if (!resetPauseTime) {
284                        processLookaheadActions(coordJob, newPauseTime);
285                    }
286                }
287    
288                jpaService.execute(new CoordJobUpdateJPAExecutor(this.coordJob));
289    
290                return null;
291            }
292            catch (XException ex) {
293                throw new CommandException(ex);
294            }
295            finally {
296                LOG.info("ENDED CoordChangeXCommand for jobId=" + jobId);
297                // update bundle action
298                if (coordJob.getBundleId() != null) {
299                    BundleStatusUpdateXCommand bundleStatusUpdate = new BundleStatusUpdateXCommand(coordJob, prevStatus);
300                    bundleStatusUpdate.call();
301                }
302            }
303        }
304    
305        /* (non-Javadoc)
306         * @see org.apache.oozie.command.XCommand#getEntityKey()
307         */
308        @Override
309        protected String getEntityKey() {
310            return this.jobId;
311        }
312    
313        /* (non-Javadoc)
314         * @see org.apache.oozie.command.XCommand#loadState()
315         */
316        @Override
317        protected void loadState() throws CommandException{
318            jpaService = Services.get().get(JPAService.class);
319    
320            if (jpaService == null) {
321                throw new CommandException(ErrorCode.E0610);
322            }
323    
324            try {
325                this.coordJob = jpaService.execute(new CoordJobGetJPAExecutor(jobId));
326                oldPauseTime = coordJob.getPauseTime();
327                prevStatus = coordJob.getStatus();
328            }
329            catch (JPAExecutorException e) {
330                throw new CommandException(e);
331            }
332    
333            LogUtils.setLogInfo(this.coordJob, logInfo);
334        }
335    
336        /* (non-Javadoc)
337         * @see org.apache.oozie.command.XCommand#verifyPrecondition()
338         */
339        @Override
340        protected void verifyPrecondition() throws CommandException,PreconditionException {
341            check(this.coordJob, newEndTime, newConcurrency, newPauseTime);
342        }
343    
344        /* (non-Javadoc)
345         * @see org.apache.oozie.command.XCommand#isLockRequired()
346         */
347        @Override
348        protected boolean isLockRequired() {
349            return true;
350        }
351    }