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.CoordinatorActionBean;
018    import org.apache.oozie.client.CoordinatorAction;
019    import org.apache.oozie.service.Services;
020    import org.apache.oozie.service.UUIDService;
021    import org.apache.oozie.util.DateUtils;
022    import org.apache.oozie.client.CoordinatorJob;
023    import org.apache.oozie.client.OozieClient;
024    import org.apache.oozie.CoordinatorJobBean;
025    import org.apache.oozie.ErrorCode;
026    import org.apache.oozie.XException;
027    import org.apache.oozie.command.CommandException;
028    import org.apache.oozie.store.CoordinatorStore;
029    import org.apache.oozie.store.StoreException;
030    import org.apache.oozie.util.ParamChecker;
031    import org.apache.oozie.util.XLog;
032    import java.util.Date;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    
037    public class CoordChangeCommand extends CoordinatorCommand<Void> {
038        private String jobId;
039        private Date newEndTime = null;
040        private Integer newConcurrency = null;
041        private Date newPauseTime = null;
042        private boolean resetPauseTime = false;
043        private final XLog log = XLog.getLog(getClass());
044    
045        public CoordChangeCommand(String id, String changeValue) throws CommandException {
046            super("coord_change", "coord_change", 0, XLog.STD);
047            this.jobId = ParamChecker.notEmpty(id, "id");
048            ParamChecker.notEmpty(changeValue, "value");
049    
050            parseChangeValue(changeValue);
051        }
052    
053        /**
054         * @param changeValue change value.
055         * @throws CommandException thrown if changeValue cannot be parsed properly.
056         */
057        private void parseChangeValue(String changeValue) throws CommandException {
058            Map<String, String> map = new HashMap<String, String>();
059            String[] tokens = changeValue.split(";");
060            int size = tokens.length;
061    
062            if (size < 0 || size > 3) {
063                throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
064            }
065    
066            for (String token : tokens) {
067                String[] pair = token.split("=");
068                String key = pair[0];
069    
070                if (!key.equals(OozieClient.CHANGE_VALUE_ENDTIME) && !key.equals(OozieClient.CHANGE_VALUE_CONCURRENCY)
071                        && !key.equals(OozieClient.CHANGE_VALUE_PAUSETIME)) {
072                    throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
073                }
074    
075                if (!key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && pair.length != 2) {
076                    throw new CommandException(ErrorCode.E1015, changeValue, "elements on " + key + " must be name=value pair");
077                }
078    
079                if (key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && pair.length != 2 && pair.length != 1) {
080                    throw new CommandException(ErrorCode.E1015, changeValue, "elements on " + key + " must be name=value pair or name=(empty string to reset pause time to null)");
081                }
082    
083                if (map.containsKey(key)) {
084                    throw new CommandException(ErrorCode.E1015, changeValue, "can not specify repeated change values on "
085                            + key);
086                }
087    
088                if (pair.length == 2) {
089                    map.put(key, pair[1]);
090                }
091                else {
092                    map.put(key, "");
093                }
094            }
095    
096            if (map.containsKey(OozieClient.CHANGE_VALUE_ENDTIME)) {
097                String value = map.get(OozieClient.CHANGE_VALUE_ENDTIME);
098                try {
099                    newEndTime = DateUtils.parseDateUTC(value);
100                }
101                catch (Exception ex) {
102                    throw new CommandException(ErrorCode.E1015, value, "must be a valid date");
103                }
104            }
105    
106            if (map.containsKey(OozieClient.CHANGE_VALUE_CONCURRENCY)) {
107                String value = map.get(OozieClient.CHANGE_VALUE_CONCURRENCY);
108                try {
109                    newConcurrency = Integer.parseInt(value);
110                }
111                catch (NumberFormatException ex) {
112                    throw new CommandException(ErrorCode.E1015, value, "must be a valid integer");
113                }
114            }
115    
116            if (map.containsKey(OozieClient.CHANGE_VALUE_PAUSETIME)) {
117                String value = map.get(OozieClient.CHANGE_VALUE_PAUSETIME);
118                if (value.equals("")) { // this is to reset pause time to null;
119                    resetPauseTime = true;
120                }
121                else {
122                    try {
123                        newPauseTime = DateUtils.parseDateUTC(value);
124                    }
125                    catch (Exception ex) {
126                        throw new CommandException(ErrorCode.E1015, value, "must be a valid date");
127                    }
128                }
129            }
130        }
131    
132        /**
133         * @param store coordinator store.
134         * @param coordJob coordinator job id.
135         * @param newEndTime new end time.
136         * @throws CommandException thrown if new end time is not valid.
137         */
138        private void checkEndTimeAnUpdateAction(CoordinatorStore store, CoordinatorJobBean coordJob, Date newEndTime) 
139            throws StoreException, CommandException {
140            // New endTime cannot be before coordinator job's start time.
141            Date startTime = coordJob.getStartTime();
142            if (newEndTime.before(startTime)) {
143                throw new CommandException(ErrorCode.E1015, newEndTime, "cannot be before coordinator job's start time [" + startTime + "]");
144            }
145    
146            // New endTime cannot be before coordinator job's last action time that is running or run.
147            Date lastActionTime = coordJob.getLastActionTime();
148            if (lastActionTime != null) {
149                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
150                if (!newEndTime.after(d)) {
151                    int lowestActionNumber = -1;
152                    for (CoordinatorActionBean action : store.getActionsOlderThan(coordJob.getId(), newEndTime, true)) {
153                        if (action.getStatus() == CoordinatorAction.Status.WAITING ||
154                            action.getStatus() == CoordinatorAction.Status.READY) {
155                            int actionNumber = action.getActionNumber();
156                            if (lowestActionNumber == -1 || actionNumber < lowestActionNumber) {
157                                lowestActionNumber = actionNumber;
158                            }
159                            log.info("Deleting not started action [{0}]", action.getId());
160                            store.deleteAction(action);
161                        }                    
162                        else {
163                            throw new CommandException(ErrorCode.E1015, newEndTime,
164                                    "must be after coordinator job's last started action time [" + d + "]");
165                        }
166                    }
167                    if (lowestActionNumber > -1) {
168                        log.info("Setting last materialized action to [{0}]", lowestActionNumber - 1);
169                        coordJob.setLastActionNumber(lowestActionNumber - 1);
170                        coordJob.setLastActionTime(null);
171                        store.updateCoordinatorJob(coordJob);
172                    }
173                }
174            }
175        }
176    
177        /**
178         * @param coordJob coordinator job id.
179         * @param newPauseTime new pause time.
180         * @param newEndTime new end time, can be null meaning no change on end
181         *        time.
182         * @throws CommandException thrown if new pause time is not valid.
183         */
184        private void checkPauseTime(CoordinatorJobBean coordJob, Date newPauseTime, Date newEndTime)
185                throws CommandException {
186            // New pauseTime cannot be before coordinator job's start time.
187            Date startTime = coordJob.getStartTime();
188            if (newPauseTime.before(startTime)) {
189                throw new CommandException(ErrorCode.E1015, newPauseTime, "cannot be before coordinator job's start time ["
190                        + startTime + "]");
191            }
192    
193            // New pauseTime cannot be before coordinator job's last action time.
194            Date lastActionTime = coordJob.getLastActionTime();
195            if (lastActionTime != null) {
196                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
197                if (!newPauseTime.after(d)) {
198                    throw new CommandException(ErrorCode.E1015, newPauseTime,
199                            "must be after coordinator job's last action time [" + d + "]");
200                }
201            }
202    
203            // New pauseTime must be before coordinator job's end time.
204            Date endTime = (newEndTime != null) ? newEndTime : coordJob.getEndTime();
205            if (!newPauseTime.before(endTime)) {
206                throw new CommandException(ErrorCode.E1015, newPauseTime, "must be before coordinator job's end time ["
207                        + endTime + "]");
208            }
209        }
210    
211        /**
212         * @param store coordinator store.
213         * @param coordJob coordinator job id.
214         * @param newEndTime new end time.
215         * @param newConcurrency new concurrency.
216         * @param newPauseTime new pause time.
217         * @throws CommandException thrown if new values are not valid.
218         */
219        private void checkAnUpdateAction(CoordinatorStore store, CoordinatorJobBean coordJob, Date newEndTime,
220                                         Integer newConcurrency, Date newPauseTime)
221                throws StoreException, CommandException {
222            if (coordJob.getStatus() == CoordinatorJob.Status.KILLED) {
223                throw new CommandException(ErrorCode.E1016);
224            }
225    
226            if (newEndTime != null) {
227                checkEndTimeAnUpdateAction(store, coordJob, newEndTime);
228            }
229    
230            if (newPauseTime != null) {
231                checkPauseTime(coordJob, newPauseTime, newEndTime);
232            }
233        }
234    
235        @Override
236        protected Void call(CoordinatorStore store) throws StoreException, CommandException {
237            try {
238                CoordinatorJobBean coordJob = store.getCoordinatorJob(jobId, false);
239                setLogInfo(coordJob);
240    
241                checkAnUpdateAction(store, coordJob, newEndTime, newConcurrency, newPauseTime);
242    
243                if (newEndTime != null) {
244                    coordJob.setEndTime(newEndTime);
245                    if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED) {
246                        coordJob.setStatus(CoordinatorJob.Status.RUNNING);
247                    }
248                }
249    
250                if (newConcurrency != null) {
251                    coordJob.setConcurrency(newConcurrency);
252                }
253    
254                if (newPauseTime != null || resetPauseTime == true) {
255                    coordJob.setPauseTime(newPauseTime);
256                }
257    
258                incrJobCounter(1);
259                store.updateCoordinatorJob(coordJob);
260    
261                return null;
262            }
263            catch (XException ex) {
264                throw new CommandException(ex);
265            }
266        }
267    
268        @Override
269        protected Void execute(CoordinatorStore store) throws StoreException, CommandException {
270            log.info("STARTED CoordChangeCommand for jobId=" + jobId);
271            try {
272                if (lock(jobId)) {
273                    call(store);
274                }
275                else {
276                    throw new CommandException(ErrorCode.E0606, "job " + jobId
277                            + " has been locked and cannot change value, please retry later");
278                }
279            }
280            catch (InterruptedException e) {
281                throw new CommandException(ErrorCode.E0606, "acquiring lock for job " + jobId + " failed "
282                        + " with exception " + e.getMessage());
283            }
284            return null;
285        }
286    }