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.CoordinatorJobBean;
021    import org.apache.oozie.ErrorCode;
022    import org.apache.oozie.XException;
023    import org.apache.oozie.command.CommandException;
024    import org.apache.oozie.store.CoordinatorStore;
025    import org.apache.oozie.store.StoreException;
026    import org.apache.oozie.util.ParamChecker;
027    import org.apache.oozie.util.XLog;
028    import java.util.Date;
029    import java.util.HashMap;
030    import java.util.Map;
031    
032    public class CoordChangeCommand extends CoordinatorCommand<Void> {
033        private String jobId;
034        private Date newEndTime = null;
035        private Integer newConcurrency = null;
036        private Date newPauseTime = null;
037        private boolean resetPauseTime = false;
038        private final XLog log = XLog.getLog(getClass());
039    
040        public CoordChangeCommand(String id, String changeValue) throws CommandException {
041            super("coord_change", "coord_change", 0, XLog.STD);
042            this.jobId = ParamChecker.notEmpty(id, "id");
043            ParamChecker.notEmpty(changeValue, "value");
044    
045            parseChangeValue(changeValue);
046        }
047    
048        /**
049         * @param changeValue change value.
050         * @throws CommandException thrown if changeValue cannot be parsed properly.
051         */
052        private void parseChangeValue(String changeValue) throws CommandException {
053            Map<String, String> map = new HashMap<String, String>();
054            String[] tokens = changeValue.split(";");
055            int size = tokens.length;
056    
057            if (size < 0 || size > 3) {
058                throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
059            }
060    
061            for (String token : tokens) {
062                String[] pair = token.split("=");
063                String key = pair[0];
064    
065                if (!key.equals(OozieClient.CHANGE_VALUE_ENDTIME) && !key.equals(OozieClient.CHANGE_VALUE_CONCURRENCY)
066                        && !key.equals(OozieClient.CHANGE_VALUE_PAUSETIME)) {
067                    throw new CommandException(ErrorCode.E1015, changeValue, "must change endtime|concurrency|pausetime");
068                }
069    
070                if (!key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && pair.length != 2) {
071                    throw new CommandException(ErrorCode.E1015, changeValue, "elements on " + key + " must be name=value pair");
072                }
073    
074                if (key.equals(OozieClient.CHANGE_VALUE_PAUSETIME) && pair.length != 2 && pair.length != 1) {
075                    throw new CommandException(ErrorCode.E1015, changeValue, "elements on " + key + " must be name=value pair or name=(empty string to reset pause time to null)");
076                }
077    
078                if (map.containsKey(key)) {
079                    throw new CommandException(ErrorCode.E1015, changeValue, "can not specify repeated change values on "
080                            + key);
081                }
082    
083                if (pair.length == 2) {
084                    map.put(key, pair[1]);
085                }
086                else {
087                    map.put(key, "");
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
154         *        time.
155         * @throws CommandException thrown if new pause time is not valid.
156         */
157        private void checkPauseTime(CoordinatorJobBean coordJob, Date newPauseTime, Date newEndTime)
158                throws CommandException {
159            // New pauseTime cannot be before coordinator job's start time.
160            Date startTime = coordJob.getStartTime();
161            if (newPauseTime.before(startTime)) {
162                throw new CommandException(ErrorCode.E1015, newPauseTime, "cannot be before coordinator job's start time ["
163                        + startTime + "]");
164            }
165    
166            // New pauseTime cannot be before coordinator job's last action time.
167            Date lastActionTime = coordJob.getLastActionTime();
168            if (lastActionTime != null) {
169                Date d = new Date(lastActionTime.getTime() - coordJob.getFrequency() * 60 * 1000);
170                if (!newPauseTime.after(d)) {
171                    throw new CommandException(ErrorCode.E1015, newPauseTime,
172                            "must be after coordinator job's last action time [" + d + "]");
173                }
174            }
175    
176            // New pauseTime must be before coordinator job's end time.
177            Date endTime = (newEndTime != null) ? newEndTime : coordJob.getEndTime();
178            if (!newPauseTime.before(endTime)) {
179                throw new CommandException(ErrorCode.E1015, newPauseTime, "must be before coordinator job's end time ["
180                        + endTime + "]");
181            }
182        }
183    
184        /**
185         * @param coordJob coordinator job id.
186         * @param newEndTime new end time.
187         * @param newConcurrency new concurrency.
188         * @param newPauseTime new pause time.
189         * @throws CommandException thrown if new values are not valid.
190         */
191        private void check(CoordinatorJobBean coordJob, Date newEndTime, Integer newConcurrency, Date newPauseTime)
192                throws CommandException {
193            if (coordJob.getStatus() == CoordinatorJob.Status.KILLED) {
194                throw new CommandException(ErrorCode.E1016);
195            }
196    
197            if (newEndTime != null) {
198                checkEndTime(coordJob, newEndTime);
199            }
200    
201            if (newPauseTime != null) {
202                checkPauseTime(coordJob, newPauseTime, newEndTime);
203            }
204        }
205    
206        @Override
207        protected Void call(CoordinatorStore store) throws StoreException, CommandException {
208            try {
209                CoordinatorJobBean coordJob = store.getCoordinatorJob(jobId, false);
210                setLogInfo(coordJob);
211    
212                check(coordJob, newEndTime, newConcurrency, newPauseTime);
213    
214                if (newEndTime != null) {
215                    coordJob.setEndTime(newEndTime);
216                    if (coordJob.getStatus() == CoordinatorJob.Status.SUCCEEDED) {
217                        coordJob.setStatus(CoordinatorJob.Status.RUNNING);
218                    }
219                }
220    
221                if (newConcurrency != null) {
222                    coordJob.setConcurrency(newConcurrency);
223                }
224    
225                if (newPauseTime != null || resetPauseTime == true) {
226                    coordJob.setPauseTime(newPauseTime);
227                }
228    
229                incrJobCounter(1);
230                store.updateCoordinatorJob(coordJob);
231    
232                return null;
233            }
234            catch (XException ex) {
235                throw new CommandException(ex);
236            }
237        }
238    
239        @Override
240        protected Void execute(CoordinatorStore store) throws StoreException, CommandException {
241            log.info("STARTED CoordChangeCommand for jobId=" + jobId);
242            try {
243                if (lock(jobId)) {
244                    call(store);
245                }
246                else {
247                    throw new CommandException(ErrorCode.E0606, "job " + jobId
248                            + " has been locked and cannot change value, please retry later");
249                }
250            }
251            catch (InterruptedException e) {
252                throw new CommandException(ErrorCode.E0606, "acquiring lock for job " + jobId + " failed "
253                        + " with exception " + e.getMessage());
254            }
255            return null;
256        }
257    }