001    /**
002     * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
003     * Licensed under the Apache License, Version 2.0 (the "License");
004     * you may not use this file except in compliance with the License.
005     * You may obtain a copy of the License at
006     *
007     *   http://www.apache.org/licenses/LICENSE-2.0
008     *
009     *  Unless required by applicable law or agreed to in writing, software
010     *  distributed under the License is distributed on an "AS IS" BASIS,
011     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012     *  See the License for the specific language governing permissions and
013     *  limitations under the License. See accompanying LICENSE file.
014     */
015    package org.apache.oozie.command.coord;
016    
017    import org.apache.oozie.util.DateUtils;
018    import org.apache.oozie.client.CoordinatorJob;
019    import org.apache.oozie.client.OozieClient;
020    import org.apache.oozie.CoordinatorActionBean;
021    import org.apache.oozie.CoordinatorJobBean;
022    import org.apache.oozie.ErrorCode;
023    import org.apache.oozie.XException;
024    import org.apache.oozie.command.CommandException;
025    import org.apache.oozie.executor.jpa.CoordActionRemoveJPAExecutor;
026    import org.apache.oozie.executor.jpa.CoordJobGetActionByActionNumberJPAExecutor;
027    import org.apache.oozie.executor.jpa.JPAExecutorException;
028    import org.apache.oozie.service.JPAService;
029    import org.apache.oozie.service.Services;
030    import org.apache.oozie.store.CoordinatorStore;
031    import org.apache.oozie.store.StoreException;
032    import org.apache.oozie.util.JobUtils;
033    import org.apache.oozie.util.ParamChecker;
034    import org.apache.oozie.util.XLog;
035    
036    import java.util.Date;
037    import java.util.HashSet;
038    import java.util.Map;
039    import java.util.Set;
040    import java.util.Map.Entry;
041    
042    public class CoordChangeCommand extends CoordinatorCommand<Void> {
043        private String jobId;
044        private Date newEndTime = null;
045        private Integer newConcurrency = null;
046        private Date newPauseTime = null;
047        private boolean resetPauseTime = false;
048        private static final XLog LOG = XLog.getLog(CoordChangeCommand.class);    
049        
050        private static final Set<String> ALLOWED_CHANGE_OPTIONS = new HashSet<String>();
051        static {
052            ALLOWED_CHANGE_OPTIONS.add("endtime");
053            ALLOWED_CHANGE_OPTIONS.add("concurrency");
054            ALLOWED_CHANGE_OPTIONS.add("pausetime");
055        }
056    
057        public CoordChangeCommand(String id, String changeValue) throws CommandException {
058            super("coord_change", "coord_change", 0, XLog.STD);
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         * @param coordJob coordinator job id.
129         * @param newEndTime new end time.
130         * @throws CommandException thrown if new end time is not valid.
131         */
132        private void checkEndTime(CoordinatorJobBean coordJob, Date newEndTime) throws CommandException {
133            // New endTime cannot be before coordinator job's start time.
134            Date startTime = coordJob.getStartTime();
135            if (newEndTime.before(startTime)) {
136                throw new CommandException(ErrorCode.E1015, newEndTime, "cannot be before coordinator job's start time [" + startTime + "]");
137            }
138    
139            // New endTime cannot be before coordinator job's last action time.
140            Date lastActionTime = coordJob.getLastActionTime();
141            if (lastActionTime != null) {
142                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
143                if (!newEndTime.after(d)) {
144                    throw new CommandException(ErrorCode.E1015, newEndTime,
145                            "must be after coordinator job's last action time [" + d + "]");
146                }
147            }
148        }
149    
150        /**
151         * @param coordJob coordinator job id.
152         * @param newPauseTime new pause time.
153         * @param newEndTime new end time, can be null meaning no change on end time.
154         * @throws CommandException thrown if new pause time is not valid.
155         */
156        private void checkPauseTime(CoordinatorJobBean coordJob, Date newPauseTime)
157                throws CommandException {
158            // New pauseTime has to be a non-past time.
159            Date d = new Date();
160            if (newPauseTime.before(d)) {
161                throw new CommandException(ErrorCode.E1015, newPauseTime, "must be a non-past time");            
162            }
163        }
164        
165        /**
166         * Process lookahead created actions that become invalid because of the new pause time,
167         * These actions will be deleted from DB, also the coordinator job will be updated accordingly
168         * 
169         * @param coordJob coordinator job
170         * @param newPauseTime new pause time
171         */
172        private void processLookaheadActions(CoordinatorJobBean coordJob, Date newPauseTime) throws CommandException {
173            Date lastActionTime = coordJob.getLastActionTime();
174            if (lastActionTime != null) {
175                // d is the real last action time.
176                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
177                int lastActionNumber = coordJob.getLastActionNumber();
178                
179                boolean hasChanged = false;
180                while (true) {
181                    if (!newPauseTime.after(d)) {
182                        deleteAction(coordJob.getId(), lastActionNumber);
183                        d = new Date(d.getTime() - coordJob.getFrequency() * 60 * 1000);
184                        lastActionNumber = lastActionNumber - 1;
185                        
186                        hasChanged = true;
187                    }
188                    else {
189                        break;
190                    }
191                }
192                
193                if (hasChanged == true) {
194                    coordJob.setLastActionNumber(lastActionNumber);
195                    Date d1 = new Date(d.getTime() + coordJob.getFrequency() * 60 * 1000);
196                    coordJob.setLastActionTime(d1);
197                    coordJob.setNextMaterializedTime(d1);
198                    
199                    if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED) {
200                        coordJob.setStatus(CoordinatorJob.Status.RUNNING);
201                    }
202                }
203            }
204        }
205    
206        /**
207         * delete last action for a coordinator job
208         * @param coordJob coordinator job
209         * @param lastActionNum last action number of the coordinator job
210         */
211        private void deleteAction(String jobId, int lastActionNum) throws CommandException {
212            JPAService jpaService = Services.get().get(JPAService.class);
213            if (jpaService == null) {
214                throw new CommandException(ErrorCode.E0610);
215            }
216            
217            try {
218                CoordinatorActionBean actionBean = jpaService.execute(new CoordJobGetActionByActionNumberJPAExecutor(jobId, lastActionNum));
219                jpaService.execute(new CoordActionRemoveJPAExecutor(actionBean.getId()));
220            }
221            catch (JPAExecutorException e) {
222                throw new CommandException(e);
223            }
224        }
225    
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        @Override
249        protected Void call(CoordinatorStore store) throws StoreException, CommandException {
250            try {
251                CoordinatorJobBean coordJob = store.getCoordinatorJob(jobId, false);
252                setLogInfo(coordJob);
253    
254                check(coordJob, newEndTime, newConcurrency, newPauseTime);
255    
256                if (newEndTime != null) {
257                    coordJob.setEndTime(newEndTime);
258                    if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED) {
259                        coordJob.setStatus(CoordinatorJob.Status.RUNNING);
260                    }
261                }
262    
263                if (newConcurrency != null) {
264                    coordJob.setConcurrency(newConcurrency);
265                }
266    
267                if (newPauseTime != null || resetPauseTime == true) {
268                    coordJob.setPauseTime(newPauseTime);
269                    if (!resetPauseTime) {
270                        processLookaheadActions(coordJob, newPauseTime);
271                    }
272                }
273    
274                store.updateCoordinatorJob(coordJob);
275    
276                return null;
277            }
278            catch (XException ex) {
279                throw new CommandException(ex);
280            }
281        }
282    
283        @Override
284        protected Void execute(CoordinatorStore store) throws StoreException, CommandException {
285            LOG.info("STARTED CoordChangeCommand for jobId=" + jobId);
286            try {
287                if (lock(jobId)) {
288                    call(store);
289                }
290                else {
291                    throw new CommandException(ErrorCode.E0606, "job " + jobId
292                            + " has been locked and cannot change value, please retry later");
293                }
294            }
295            catch (InterruptedException e) {
296                throw new CommandException(ErrorCode.E0606, "acquiring lock for job " + jobId + " failed "
297                        + " with exception " + e.getMessage());
298            }
299            finally {
300                LOG.info("ENDED CoordChangeCommand for jobId=" + jobId);
301            }
302            return null;
303        }
304    }