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