001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.command.coord;
020
021import java.io.IOException;
022import java.io.StringReader;
023import java.net.URI;
024import java.util.Date;
025import java.util.List;
026
027import org.apache.commons.lang.StringUtils;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.oozie.CoordinatorActionBean;
030import org.apache.oozie.CoordinatorJobBean;
031import org.apache.oozie.ErrorCode;
032import org.apache.oozie.client.CoordinatorAction;
033import org.apache.oozie.client.Job;
034import org.apache.oozie.client.OozieClient;
035import org.apache.oozie.command.CommandException;
036import org.apache.oozie.command.PreconditionException;
037import org.apache.oozie.coord.ElException;
038import org.apache.oozie.coord.input.dependency.CoordInputDependency;
039import org.apache.oozie.dependency.ActionDependency;
040import org.apache.oozie.dependency.DependencyChecker;
041import org.apache.oozie.dependency.URIHandler;
042import org.apache.oozie.executor.jpa.CoordActionGetForInputCheckJPAExecutor;
043import org.apache.oozie.executor.jpa.CoordActionQueryExecutor;
044import org.apache.oozie.executor.jpa.CoordActionQueryExecutor.CoordActionQuery;
045import org.apache.oozie.executor.jpa.CoordJobGetJPAExecutor;
046import org.apache.oozie.executor.jpa.JPAExecutorException;
047import org.apache.oozie.service.CallableQueueService;
048import org.apache.oozie.service.ConfigurationService;
049import org.apache.oozie.service.EventHandlerService;
050import org.apache.oozie.service.JPAService;
051import org.apache.oozie.service.PartitionDependencyManagerService;
052import org.apache.oozie.service.RecoveryService;
053import org.apache.oozie.service.Service;
054import org.apache.oozie.service.Services;
055import org.apache.oozie.service.URIHandlerService;
056import org.apache.oozie.util.LogUtils;
057import org.apache.oozie.util.StatusUtils;
058import org.apache.oozie.util.XConfiguration;
059import org.apache.oozie.util.XLog;
060import org.apache.oozie.util.DateUtils;
061
062public class CoordPushDependencyCheckXCommand extends CoordinatorXCommand<Void> {
063    protected String actionId;
064    protected JPAService jpaService = null;
065    protected CoordinatorActionBean coordAction = null;
066    protected CoordinatorJobBean coordJob = null;
067
068    /**
069     * Property name of command re-queue interval for coordinator push check in
070     * milliseconds.
071     */
072    public static final String CONF_COORD_PUSH_CHECK_REQUEUE_INTERVAL = Service.CONF_PREFIX
073            + "coord.push.check.requeue.interval";
074    private boolean registerForNotification;
075    private boolean removeAvailDependencies;
076
077    public CoordPushDependencyCheckXCommand(String actionId) {
078        this(actionId, false, true);
079    }
080
081    public CoordPushDependencyCheckXCommand(String actionId, boolean registerForNotification) {
082        this(actionId, registerForNotification, !registerForNotification);
083    }
084
085    public CoordPushDependencyCheckXCommand(String actionId, boolean registerForNotification,
086            boolean removeAvailDependencies) {
087        super("coord_push_dep_check", "coord_push_dep_check", 0);
088        this.actionId = actionId;
089        this.registerForNotification = registerForNotification;
090        this.removeAvailDependencies = removeAvailDependencies;
091    }
092
093    protected CoordPushDependencyCheckXCommand(String actionName, String actionId) {
094        super(actionName, actionName, 0);
095        this.actionId = actionId;
096    }
097
098    @Override
099    protected void setLogInfo() {
100        LogUtils.setLogInfo(actionId);
101    }
102
103    @Override
104    protected Void execute() throws CommandException {
105        // this action should only get processed if current time > nominal time;
106        // otherwise, requeue this action for delay execution;
107        Date nominalTime = coordAction.getNominalTime();
108        Date currentTime = new Date();
109        if (nominalTime.compareTo(currentTime) > 0) {
110            queue(new CoordPushDependencyCheckXCommand(coordAction.getId(), true), nominalTime.getTime() - currentTime.getTime());
111            updateCoordAction(coordAction, false);
112            LOG.info("[" + actionId
113                    + "]::CoordPushDependency:: nominal Time is newer than current time, so requeue and wait. Current="
114                    + DateUtils.formatDateOozieTZ(currentTime) + ", nominal=" + DateUtils.formatDateOozieTZ(nominalTime));
115            return null;
116        }
117
118        CoordInputDependency coordPushInputDependency = coordAction.getPushInputDependencies();
119        CoordInputDependency coordPullInputDependency = coordAction.getPullInputDependencies();
120        if (coordPushInputDependency.getMissingDependenciesAsList().size() == 0) {
121            LOG.info("Nothing to check. Empty push missing dependency");
122        }
123        else {
124            List<String> missingDependenciesArray = coordPushInputDependency.getMissingDependenciesAsList();
125            LOG.info("First Push missing dependency is [{0}] ", missingDependenciesArray.get(0));
126            LOG.trace("Push missing dependencies are [{0}] ", missingDependenciesArray);
127            if (registerForNotification) {
128                LOG.debug("Register for notifications is true");
129            }
130
131            try {
132                Configuration actionConf = null;
133                try {
134                    actionConf = new XConfiguration(new StringReader(coordAction.getRunConf()));
135                }
136                catch (IOException e) {
137                    throw new CommandException(ErrorCode.E1307, e.getMessage(), e);
138                }
139
140
141                boolean isChangeInDependency = true;
142                boolean timeout = false;
143                ActionDependency actionDependency = coordPushInputDependency.checkPushMissingDependencies(coordAction,
144                        registerForNotification);
145                // Check all dependencies during materialization to avoid registering in the cache.
146                // But check only first missing one afterwards similar to
147                // CoordActionInputCheckXCommand for efficiency. listPartitions is costly.
148                if (actionDependency.getMissingDependencies().size() == missingDependenciesArray.size()) {
149                    isChangeInDependency = false;
150                }
151                else {
152                    String stillMissingDeps = DependencyChecker.dependenciesAsString(actionDependency.getMissingDependencies());
153                    coordPushInputDependency.setMissingDependencies(stillMissingDeps);
154                }
155
156                if (coordPushInputDependency.isDependencyMet()) {
157                    // All push-based dependencies are available
158                    onAllPushDependenciesAvailable(coordPullInputDependency.isDependencyMet());
159                }
160                else {
161                    // Checking for timeout
162                    timeout = isTimeout();
163                    if (timeout) {
164                        queue(new CoordActionTimeOutXCommand(coordAction, coordJob.getUser(), coordJob.getAppName()));
165                    }
166                    else {
167                        queue(new CoordPushDependencyCheckXCommand(coordAction.getId()),
168                                getCoordPushCheckRequeueInterval());
169                    }
170                }
171
172                updateCoordAction(coordAction, isChangeInDependency || coordPushInputDependency.isDependencyMet());
173                if (registerForNotification) {
174                    registerForNotification(coordPushInputDependency.getMissingDependenciesAsList(), actionConf);
175                }
176                if (removeAvailDependencies) {
177                    unregisterAvailableDependencies(actionDependency.getAvailableDependencies());
178                }
179                if (timeout) {
180                    unregisterMissingDependencies(coordPushInputDependency.getMissingDependenciesAsList(), actionId);
181                }
182            }
183            catch (Exception e) {
184                final CallableQueueService callableQueueService = Services.get().get(CallableQueueService.class);
185                if (isTimeout()) {
186                    LOG.debug("Queueing timeout command");
187                    // XCommand.queue() will not work when there is a Exception
188                    callableQueueService.queue(new CoordActionTimeOutXCommand(coordAction, coordJob.getUser(),
189                            coordJob.getAppName()));
190                    unregisterMissingDependencies(missingDependenciesArray, actionId);
191                }
192                else if (coordPullInputDependency.getMissingDependenciesAsList().size() > 0) {
193                    // Queue again on exception as RecoveryService will not queue this again with
194                    // the action being updated regularly by CoordActionInputCheckXCommand
195                    callableQueueService.queue(new CoordPushDependencyCheckXCommand(coordAction.getId(),
196                            registerForNotification, removeAvailDependencies),
197                            Services.get().getConf().getInt(RecoveryService.CONF_COORD_OLDER_THAN, 600) * 1000);
198                }
199                throw new CommandException(ErrorCode.E1021, e.getMessage(), e);
200            }
201        }
202        return null;
203    }
204
205    /**
206     * Return the re-queue interval for coord push dependency check
207     * @return requeueInterval returns the requeue interval for coord push dependency check
208     */
209    public long getCoordPushCheckRequeueInterval() {
210        long requeueInterval = ConfigurationService.getLong(CONF_COORD_PUSH_CHECK_REQUEUE_INTERVAL);
211        return requeueInterval;
212    }
213
214    /**
215     * Returns true if timeout period has been reached
216     *
217     * @return true if it is time for timeout else false
218     */
219    protected boolean isTimeout() {
220        long waitingTime = (new Date().getTime() - Math.max(coordAction.getNominalTime().getTime(), coordAction
221                .getCreatedTime().getTime()))
222                / (60 * 1000);
223        int timeOut = coordAction.getTimeOut();
224        return (timeOut >= 0) && (waitingTime > timeOut);
225    }
226
227    protected void onAllPushDependenciesAvailable(boolean isPullDependencyMeet) throws CommandException {
228        Services.get().get(PartitionDependencyManagerService.class)
229                .removeCoordActionWithDependenciesAvailable(coordAction.getId());
230        if (isPullDependencyMeet) {
231            Date nominalTime = coordAction.getNominalTime();
232            Date currentTime = new Date();
233            // The action should become READY only if current time > nominal time;
234            // CoordActionInputCheckXCommand will take care of moving it to READY when it is nominal time.
235            if (nominalTime.compareTo(currentTime) > 0) {
236                LOG.info("[" + actionId + "]::ActionInputCheck:: nominal Time is newer than current time. Current="
237                        + DateUtils.formatDateOozieTZ(currentTime) + ", nominal="
238                        + DateUtils.formatDateOozieTZ(nominalTime));
239            }
240            else {
241                String actionXml = resolveCoordConfiguration();
242                coordAction.setActionXml(actionXml);
243                coordAction.setStatus(CoordinatorAction.Status.READY);
244                // pass jobID to the CoordActionReadyXCommand
245                queue(new CoordActionReadyXCommand(coordAction.getJobId()), 100);
246            }
247        }
248        else if (isTimeout()) {
249            // If it is timeout and all push dependencies are available but still some unresolved
250            // missing dependencies queue CoordActionInputCheckXCommand now. Else it will have to
251            // wait till RecoveryService kicks in
252            queue(new CoordActionInputCheckXCommand(coordAction.getId(), coordAction.getJobId()));
253        }
254        coordAction.getPushInputDependencies().setDependencyMet(true);
255
256    }
257
258    private String resolveCoordConfiguration() throws CommandException {
259        try {
260            Configuration actionConf = new XConfiguration(new StringReader(coordAction.getRunConf()));
261            StringBuilder actionXml = new StringBuilder(coordAction.getActionXml());
262            String newActionXml = CoordActionInputCheckXCommand.resolveCoordConfiguration(actionXml, actionConf,
263                    actionId, coordAction.getPullInputDependencies(), coordAction
264                            .getPushInputDependencies());
265            actionXml.replace(0, actionXml.length(), newActionXml);
266            return actionXml.toString();
267        }
268        catch (ElException e) {
269            coordAction.setStatus(CoordinatorAction.Status.FAILED);
270            updateCoordAction(coordAction, true);
271            throw new CommandException(ErrorCode.E1021, e.getMessage(), e);
272        }
273        catch (Exception e) {
274            throw new CommandException(ErrorCode.E1021, e.getMessage(), e);
275        }
276    }
277
278    protected void updateCoordAction(CoordinatorActionBean coordAction, boolean isChangeInDependency)
279            throws CommandException {
280        coordAction.setLastModifiedTime(new Date());
281        if (jpaService != null) {
282            try {
283                if (isChangeInDependency) {
284                    coordAction.setPushMissingDependencies(coordAction.getPushInputDependencies().serialize());
285                    CoordActionQueryExecutor.getInstance().executeUpdate(
286                            CoordActionQuery.UPDATE_COORD_ACTION_FOR_PUSH_INPUTCHECK, coordAction);
287                    if (EventHandlerService.isEnabled() && coordAction.getStatus() != CoordinatorAction.Status.READY) {
288                        // since event is not to be generated unless action
289                        // RUNNING via StartX
290                        generateEvent(coordAction, coordJob.getUser(), coordJob.getAppName(), null);
291                    }
292                }
293                else {
294                    CoordActionQueryExecutor.getInstance().executeUpdate(
295                            CoordActionQuery.UPDATE_COORD_ACTION_FOR_MODIFIED_DATE, coordAction);
296                }
297            }
298            catch (JPAExecutorException jex) {
299                throw new CommandException(ErrorCode.E1021, jex.getMessage(), jex);
300            }
301            catch (IOException ioe) {
302                throw new CommandException(ErrorCode.E1021, ioe.getMessage(), ioe);
303            }
304        }
305    }
306
307    private void registerForNotification(List<String> missingDeps, Configuration actionConf) {
308        URIHandlerService uriService = Services.get().get(URIHandlerService.class);
309        String user = actionConf.get(OozieClient.USER_NAME, OozieClient.USER_NAME);
310        for (String missingDep : missingDeps) {
311            try {
312                URI missingURI = new URI(missingDep);
313                URIHandler handler = uriService.getURIHandler(missingURI);
314                handler.registerForNotification(missingURI, actionConf, user, actionId);
315                    LOG.debug("Registered uri [{0}] for notifications", missingURI);
316            }
317            catch (Exception e) {
318                LOG.warn("Exception while registering uri [{0}] for notifications", missingDep, e);
319            }
320        }
321    }
322
323    private void unregisterAvailableDependencies(List<String> availableDeps) {
324        URIHandlerService uriService = Services.get().get(URIHandlerService.class);
325        for (String availableDep : availableDeps) {
326            try {
327                URI availableURI = new URI(availableDep);
328                URIHandler handler = uriService.getURIHandler(availableURI);
329                if (handler.unregisterFromNotification(availableURI, actionId)) {
330                    LOG.debug("Successfully unregistered uri [{0}] from notifications", availableURI);
331                }
332                else {
333                    LOG.warn("Unable to unregister uri [{0}] from notifications", availableURI);
334                }
335            }
336            catch (Exception e) {
337                LOG.warn("Exception while unregistering uri [{0}] from notifications", availableDep, e);
338            }
339        }
340    }
341
342    public static void unregisterMissingDependencies(List<String> missingDeps, String actionId) {
343        final XLog LOG = XLog.getLog(CoordPushDependencyCheckXCommand.class);
344        URIHandlerService uriService = Services.get().get(URIHandlerService.class);
345        for (String missingDep : missingDeps) {
346            try {
347                URI missingURI = new URI(missingDep);
348                URIHandler handler = uriService.getURIHandler(missingURI);
349                if (handler.unregisterFromNotification(missingURI, actionId)) {
350                    LOG.debug("Successfully unregistered uri [{0}] from notifications", missingURI);
351                }
352                else {
353                    LOG.warn("Unable to unregister uri [{0}] from notifications", missingURI);
354                }
355            }
356            catch (Exception e) {
357                LOG.warn("Exception while unregistering uri [{0}] from notifications", missingDep, e);
358            }
359        }
360    }
361
362    @Override
363    public String getEntityKey() {
364        return actionId.substring(0, actionId.indexOf("@"));
365    }
366
367    @Override
368    public String getKey(){
369        return getName() + "_" + actionId;
370    }
371
372    @Override
373    protected boolean isLockRequired() {
374        return true;
375    }
376
377    @Override
378    protected void loadState() throws CommandException {
379        jpaService = Services.get().get(JPAService.class);
380        try {
381            coordAction = jpaService.execute(new CoordActionGetForInputCheckJPAExecutor(actionId));
382            if (coordAction != null) {
383                coordJob = jpaService.execute(new CoordJobGetJPAExecutor(coordAction.getJobId()));
384                LogUtils.setLogInfo(coordAction);
385            }
386            else {
387                throw new CommandException(ErrorCode.E0605, actionId);
388            }
389        }
390        catch (JPAExecutorException je) {
391            throw new CommandException(je);
392        }
393    }
394
395    @Override
396    protected void verifyPrecondition() throws CommandException, PreconditionException {
397        if (coordAction.getStatus() != CoordinatorActionBean.Status.WAITING) {
398            throw new PreconditionException(ErrorCode.E1100, "[" + actionId
399                    + "]::CoordPushDependencyCheck:: Ignoring action. Should be in WAITING state, but state="
400                    + coordAction.getStatus());
401        }
402
403        // if eligible to do action input check when running with backward
404        // support is true
405        if (StatusUtils.getStatusForCoordActionInputCheck(coordJob)) {
406            return;
407        }
408
409        if (coordJob.getStatus() != Job.Status.RUNNING && coordJob.getStatus() != Job.Status.RUNNINGWITHERROR
410                && coordJob.getStatus() != Job.Status.PAUSED && coordJob.getStatus() != Job.Status.PAUSEDWITHERROR) {
411            throw new PreconditionException(ErrorCode.E1100, "[" + actionId
412                    + "]::CoordPushDependencyCheck:: Ignoring action."
413                    + " Coordinator job is not in RUNNING/RUNNINGWITHERROR/PAUSED/PAUSEDWITHERROR state, but state="
414                    + coordJob.getStatus());
415        }
416    }
417
418}