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 java.io.IOException;
018    import java.io.StringReader;
019    import java.util.Date;
020    import java.util.List;
021    
022    import org.apache.hadoop.conf.Configuration;
023    import org.apache.hadoop.fs.Path;
024    import org.apache.oozie.CoordinatorActionBean;
025    import org.apache.oozie.ErrorCode;
026    import org.apache.oozie.client.CoordinatorAction;
027    import org.apache.oozie.client.OozieClient;
028    import org.apache.oozie.command.CommandException;
029    import org.apache.oozie.coord.CoordELEvaluator;
030    import org.apache.oozie.coord.CoordELFunctions;
031    import org.apache.oozie.service.HadoopAccessorException;
032    import org.apache.oozie.service.HadoopAccessorService;
033    import org.apache.oozie.service.Services;
034    import org.apache.oozie.store.CoordinatorStore;
035    import org.apache.oozie.store.StoreException;
036    import org.apache.oozie.util.DateUtils;
037    import org.apache.oozie.util.ELEvaluator;
038    import org.apache.oozie.util.Instrumentation;
039    import org.apache.oozie.util.ParamChecker;
040    import org.apache.oozie.util.XConfiguration;
041    import org.apache.oozie.util.XLog;
042    import org.apache.oozie.util.XmlUtils;
043    import org.jdom.Element;
044    
045    public class CoordActionInputCheckCommand extends CoordinatorCommand<Void> {
046    
047        private String actionId;
048        private final XLog log = XLog.getLog(getClass());
049        private int COMMAND_REQUEUE_INTERVAL = 60000; // 1 minute
050        private CoordinatorActionBean coordAction = null;
051    
052        public CoordActionInputCheckCommand(String actionId) {
053            super("coord_action_input", "coord_action_input", 1, XLog.STD);
054            this.actionId = actionId;
055        }
056    
057        @Override
058        protected Void call(CoordinatorStore store) throws StoreException, CommandException {
059            log.debug("After store.get() for action ID " + actionId + " : " + coordAction.getStatus());
060            // this action should only get processed if current time >
061            // materialization time
062            // otherwise, requeue this action after 30 seconds
063            Date nominalTime = coordAction.getNominalTime();
064            Date currentTime = new Date();
065            if (nominalTime.compareTo(currentTime) > 0) {
066                log.info("[" + actionId
067                        + "]::ActionInputCheck:: nominal Time is newer than current time, so requeue and wait. Current="
068                        + currentTime + ", nominal=" + nominalTime);
069                queueCallable(new CoordActionInputCheckCommand(coordAction.getId()), Math.max(
070                        (nominalTime.getTime() - currentTime.getTime()), COMMAND_REQUEUE_INTERVAL));
071                // update lastModifiedTime
072                store.updateCoordinatorAction(coordAction);
073                return null;
074            }
075            if (coordAction.getStatus() == CoordinatorActionBean.Status.WAITING) {
076                log.info("[" + actionId + "]::ActionInputCheck:: Action is in WAITING state.");
077                StringBuilder actionXml = new StringBuilder(coordAction.getActionXml());// job.getXml();
078                Instrumentation.Cron cron = new Instrumentation.Cron();
079                try {
080                    Configuration actionConf = new XConfiguration(new StringReader(coordAction.getRunConf()));
081                    cron.start();
082                    StringBuilder existList = new StringBuilder();
083                    StringBuilder nonExistList = new StringBuilder();
084                    StringBuilder nonResolvedList = new StringBuilder();
085                    CoordCommandUtils.getResolvedList(coordAction.getMissingDependencies(), nonExistList, nonResolvedList);
086    
087                    String[] uriList = nonExistList.toString().split(CoordELFunctions.INSTANCE_SEPARATOR);
088                    if (uriList.length > 0) {
089                        log.info("[" + actionId + "]::ActionInputCheck:: Missing deps:" + uriList[0] + ",  NonResolvedList:"
090                                + nonResolvedList.toString());
091                    } else {
092                        log.info("[" + actionId + "]::ActionInputCheck:: No missing deps,  NonResolvedList:"
093                                + nonResolvedList.toString());
094                    }
095                    boolean status = checkInput(actionXml, existList, nonExistList, actionConf);
096                    coordAction.setLastModifiedTime(currentTime);
097                    coordAction.setActionXml(actionXml.toString());
098                    if (nonResolvedList.length() > 0 && status == false) {
099                        nonExistList.append(CoordCommandUtils.RESOLVED_UNRESOLVED_SEPARATOR).append(nonResolvedList);
100                    }
101                    coordAction.setMissingDependencies(nonExistList.toString());
102                    if (status == true) {
103                        coordAction.setStatus(CoordinatorAction.Status.READY);
104                        // pass jobID to the ReadyCommand
105                        queueCallable(new CoordActionReadyCommand(coordAction.getJobId()), 100);
106                    }
107                    else {
108                        long waitingTime = (currentTime.getTime() - Math.max(coordAction.getNominalTime().getTime(),
109                                coordAction.getCreatedTime().getTime())) / (60 * 1000);
110                        int timeOut = coordAction.getTimeOut();
111                        if ((timeOut >= 0) && (waitingTime > timeOut)) {
112                            queueCallable(new CoordActionTimeOut(coordAction), 100);
113                            coordAction.setStatus(CoordinatorAction.Status.TIMEDOUT);
114                        }
115                        else {
116                            queueCallable(new CoordActionInputCheckCommand(coordAction.getId()), COMMAND_REQUEUE_INTERVAL);
117                        }
118                    }
119                    store.updateCoordActionMin(coordAction);
120                }
121                catch (Exception e) {
122                    log.warn(actionId + ": Exception occurs: " + e + " STORE is active " + store.isActive(), e);
123                    throw new CommandException(ErrorCode.E1005, e.getMessage(), e);
124                }
125                cron.stop();
126            }
127            else {
128                log.info("[" + actionId + "]::ActionInputCheck:: Ignoring action. Should be in WAITING state, but state="
129                        + coordAction.getStatus());
130            }
131            return null;
132        }
133    
134        protected boolean checkInput(StringBuilder actionXml, StringBuilder existList, StringBuilder nonExistList,
135                Configuration conf) throws Exception {
136            Element eAction = XmlUtils.parseXml(actionXml.toString());
137            boolean allExist = checkResolvedUris(eAction, existList, nonExistList, conf);
138            if (allExist) {
139                log.debug("[" + actionId + "]::ActionInputCheck:: Checking Latest/future");
140                allExist = checkUnresolvedInstances(eAction, conf);
141            }
142            if (allExist == true) {
143                materializeDataProperties(eAction, conf);
144                actionXml.replace(0, actionXml.length(), XmlUtils.prettyPrint(eAction).toString());
145            }
146            return allExist;
147        }
148    
149        /**
150         * Materialize data properties defined in <action> tag. it includes dataIn(<DS>) and dataOut(<DS>) it creates a list
151         * of files that will be needed.
152         *
153         * @param eAction
154         * @param conf
155         * @throws Exception
156         * @update modify 'Action' element with appropriate list of files.
157         */
158        private void materializeDataProperties(Element eAction, Configuration conf) throws Exception {
159            ELEvaluator eval = CoordELEvaluator.createDataEvaluator(eAction, conf, actionId);
160            Element configElem = eAction.getChild("action", eAction.getNamespace()).getChild("workflow",
161                    eAction.getNamespace()).getChild("configuration", eAction.getNamespace());
162            if (configElem != null) {
163                for (Element propElem : (List<Element>) configElem.getChildren("property", configElem.getNamespace())) {
164                    resolveTagContents("value", propElem, eval);
165                }
166            }
167        }
168    
169        private void resolveTagContents(String tagName, Element elem, ELEvaluator eval) throws Exception {
170            if (elem == null) {
171                return;
172            }
173            Element tagElem = elem.getChild(tagName, elem.getNamespace());
174            if (tagElem != null) {
175                String updated = CoordELFunctions.evalAndWrap(eval, tagElem.getText());
176                tagElem.removeContent();
177                tagElem.addContent(updated);
178            }
179            else {
180                log.warn(" Value NOT FOUND " + tagName);
181            }
182        }
183    
184        private boolean checkUnresolvedInstances(Element eAction, Configuration actionConf)
185                throws Exception {
186            String strAction = XmlUtils.prettyPrint(eAction).toString();
187            Date nominalTime = DateUtils.parseDateUTC(eAction.getAttributeValue("action-nominal-time"));
188            String actualTimeStr = eAction.getAttributeValue("action-actual-time");
189            Date actualTime = null;
190            if (actualTimeStr == null) {
191                log.debug("Unable to get action-actual-time from action xml, this job is submitted " +
192                "from previous version. Assign current date to actual time, action = " + actionId);
193                actualTime = new Date();
194            } else {
195                actualTime = DateUtils.parseDateUTC(actualTimeStr);
196            }
197    
198            StringBuffer resultedXml = new StringBuffer();
199    
200            boolean ret;
201            Element inputList = eAction.getChild("input-events", eAction.getNamespace());
202            if (inputList != null) {
203                ret = materializeUnresolvedEvent(inputList.getChildren("data-in", eAction.getNamespace()), nominalTime,
204                        actualTime, actionConf);
205                if (ret == false) {
206                    resultedXml.append(strAction);
207                    return false;
208                }
209            }
210    
211            // Using latest() or future() in output-event is not intuitive.
212            // We need to make
213            // sure, this assumption is correct.
214            Element outputList = eAction.getChild("output-events", eAction.getNamespace());
215            if (outputList != null) {
216                for (Element dEvent : (List<Element>) outputList.getChildren("data-out", eAction.getNamespace())) {
217                    if (dEvent.getChild("unresolved-instances", dEvent.getNamespace()) != null) {
218                        throw new CommandException(ErrorCode.E1006, "coord:latest()/future()",
219                                " not permitted in output-event ");
220                    }
221                }
222                /*
223                 * ret = materializeUnresolvedEvent( (List<Element>)
224                 * outputList.getChildren("data-out", eAction.getNamespace()),
225                 * actualTime, nominalTime, actionConf); if (ret == false) {
226                 * resultedXml.append(strAction); return false; }
227                 */
228            }
229            return true;
230        }
231    
232        private boolean materializeUnresolvedEvent(List<Element> eDataEvents, Date nominalTime, Date actualTime,
233                Configuration conf) throws Exception {
234            for (Element dEvent : eDataEvents) {
235                if (dEvent.getChild("unresolved-instances", dEvent.getNamespace()) == null) {
236                    continue;
237                }
238                ELEvaluator eval = CoordELEvaluator.createLazyEvaluator(actualTime, nominalTime, dEvent, conf);
239                String uresolvedInstance = dEvent.getChild("unresolved-instances", dEvent.getNamespace()).getTextTrim();
240                String unresolvedList[] = uresolvedInstance.split(CoordELFunctions.INSTANCE_SEPARATOR);
241                StringBuffer resolvedTmp = new StringBuffer();
242                for (int i = 0; i < unresolvedList.length; i++) {
243                    String ret = CoordELFunctions.evalAndWrap(eval, unresolvedList[i]);
244                    Boolean isResolved = (Boolean) eval.getVariable("is_resolved");
245                    if (isResolved == false) {
246                        log.info("[" + actionId + "]::Cannot resolve: " + ret);
247                        return false;
248                    }
249                    if (resolvedTmp.length() > 0) {
250                        resolvedTmp.append(CoordELFunctions.INSTANCE_SEPARATOR);
251                    }
252                    resolvedTmp.append((String) eval.getVariable("resolved_path"));
253                }
254                if (resolvedTmp.length() > 0) {
255                    if (dEvent.getChild("uris", dEvent.getNamespace()) != null) {
256                        resolvedTmp.append(CoordELFunctions.INSTANCE_SEPARATOR).append(
257                                dEvent.getChild("uris", dEvent.getNamespace()).getTextTrim());
258                        dEvent.removeChild("uris", dEvent.getNamespace());
259                    }
260                    Element uriInstance = new Element("uris", dEvent.getNamespace());
261                    uriInstance.addContent(resolvedTmp.toString());
262                    dEvent.getContent().add(1, uriInstance);
263                }
264                dEvent.removeChild("unresolved-instances", dEvent.getNamespace());
265            }
266    
267            return true;
268        }
269    
270        private boolean checkResolvedUris(Element eAction, StringBuilder existList, StringBuilder nonExistList,
271                Configuration conf) throws IOException {
272    
273            log.info("[" + actionId + "]::ActionInputCheck:: In checkResolvedUris...");
274            Element inputList = eAction.getChild("input-events", eAction.getNamespace());
275            if (inputList != null) {
276                // List<Element> eDataEvents = inputList.getChildren("data-in",
277                // eAction.getNamespace());
278                // for (Element event : eDataEvents) {
279                // Element uris = event.getChild("uris", event.getNamespace());
280                if (nonExistList.length() > 0) {
281                    checkListOfPaths(existList, nonExistList, conf);
282                }
283                // }
284                return nonExistList.length() == 0;
285            }
286            return true;
287        }
288    
289        private boolean checkListOfPaths(StringBuilder existList, StringBuilder nonExistList, Configuration conf)
290                throws IOException {
291    
292            String[] uriList = nonExistList.toString().split(CoordELFunctions.INSTANCE_SEPARATOR);
293            if (uriList[0] != null) {
294                log.info("[" + actionId + "]::ActionInputCheck:: In checkListOfPaths: " + uriList[0] + " is Missing.");
295            }
296    
297            nonExistList.delete(0, nonExistList.length());
298            boolean allExists = true;
299            String existSeparator = "", nonExistSeparator = "";
300            for (int i = 0; i < uriList.length; i++) {
301                if (allExists) {
302                    allExists = pathExists(uriList[i], conf);
303                    log.info("[" + actionId + "]::ActionInputCheck:: File:" + uriList[i] + ", Exists? :" + allExists);
304                }
305                if (allExists) {
306                    existList.append(existSeparator).append(uriList[i]);
307                    existSeparator = CoordELFunctions.INSTANCE_SEPARATOR;
308                }
309                else {
310                    nonExistList.append(nonExistSeparator).append(uriList[i]);
311                    nonExistSeparator = CoordELFunctions.INSTANCE_SEPARATOR;
312                }
313            }
314            return allExists;
315        }
316    
317        private boolean pathExists(String sPath, Configuration actionConf) throws IOException {
318            log.debug("checking for the file " + sPath);
319            Path path = new Path(sPath);
320            String user = ParamChecker.notEmpty(actionConf.get(OozieClient.USER_NAME), OozieClient.USER_NAME);
321            String group = ParamChecker.notEmpty(actionConf.get(OozieClient.GROUP_NAME), OozieClient.GROUP_NAME);
322            try {
323                return Services.get().get(HadoopAccessorService.class).createFileSystem(user, group, path.toUri(),
324                        actionConf).exists(path);
325            }
326            catch (HadoopAccessorException e) {
327                throw new IOException(e);
328            }
329        }
330    
331        /**
332         * The function create a list of URIs separated by "," using the instances time stamp and URI-template
333         *
334         * @param event : <data-in> event
335         * @param instances : List of time stamp seprated by ","
336         * @param unresolvedInstances : list of instance with latest/future function
337         * @return : list of URIs separated by ",".
338         * @throws Exception
339         */
340        private String createURIs(Element event, String instances, StringBuilder unresolvedInstances) throws Exception {
341            if (instances == null || instances.length() == 0) {
342                return "";
343            }
344            String[] instanceList = instances.split(CoordELFunctions.INSTANCE_SEPARATOR);
345            StringBuilder uris = new StringBuilder();
346    
347            for (int i = 0; i < instanceList.length; i++) {
348                int funcType = CoordCommandUtils.getFuncType(instanceList[i]);
349                if (funcType == CoordCommandUtils.LATEST || funcType == CoordCommandUtils.FUTURE) {
350                    if (unresolvedInstances.length() > 0) {
351                        unresolvedInstances.append(CoordELFunctions.INSTANCE_SEPARATOR);
352                    }
353                    unresolvedInstances.append(instanceList[i]);
354                    continue;
355                }
356                ELEvaluator eval = CoordELEvaluator.createURIELEvaluator(instanceList[i]);
357                // uris.append(eval.evaluate(event.getChild("dataset",
358                // event.getNamespace()).getChild("uri-template",
359                // event.getNamespace()).getTextTrim(), String.class));
360                if (uris.length() > 0) {
361                    uris.append(CoordELFunctions.INSTANCE_SEPARATOR);
362                }
363                uris.append(CoordELFunctions.evalAndWrap(eval, event.getChild("dataset", event.getNamespace()).getChild(
364                        "uri-template", event.getNamespace()).getTextTrim()));
365            }
366            return uris.toString();
367        }
368    
369        @Override
370        protected Void execute(CoordinatorStore store) throws StoreException, CommandException {
371            log.info("STARTED CoordActionInputCheckCommand for actionid=" + actionId);
372            try {
373                coordAction = store.getEntityManager().find(CoordinatorActionBean.class, actionId);
374                setLogInfo(coordAction);
375                if (lock(coordAction.getJobId())) {
376                    call(store);
377                }
378                else {
379                    queueCallable(new CoordActionInputCheckCommand(actionId), LOCK_FAILURE_REQUEUE_INTERVAL);
380                    log.warn("CoordActionInputCheckCommand lock was not acquired - failed jobId=" + coordAction.getJobId()
381                            + ", actionId=" + actionId + ". Requeing the same.");
382                }
383            }
384            catch (InterruptedException e) {
385                queueCallable(new CoordActionInputCheckCommand(actionId), LOCK_FAILURE_REQUEUE_INTERVAL);
386                log.warn("CoordActionInputCheckCommand lock acquiring failed with exception " + e.getMessage()
387                        + " for jobId=" + coordAction.getJobId() + ", actionId=" + actionId + " Requeing the same.");
388            }
389            finally {
390                log.info("ENDED CoordActionInputCheckCommand for actionid=" + actionId);
391            }
392            return null;
393        }
394    
395        /* (non-Javadoc)
396         * @see org.apache.oozie.command.Command#getKey()
397         */
398        @Override
399        public String getKey(){
400            return getName() + "_" + actionId;
401        }
402    
403    }