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.text.ParseException;
022import java.util.Calendar;
023import java.util.Date;
024import java.util.Iterator;
025import java.util.List;
026
027import org.apache.oozie.CoordinatorActionBean;
028import org.apache.oozie.CoordinatorJobBean;
029import org.apache.oozie.ErrorCode;
030import org.apache.oozie.client.CoordinatorAction;
031import org.apache.oozie.client.CoordinatorJob;
032import org.apache.oozie.client.Job;
033import org.apache.oozie.command.CommandException;
034import org.apache.oozie.command.PreconditionException;
035import org.apache.oozie.executor.jpa.CoordActionQueryExecutor;
036import org.apache.oozie.executor.jpa.CoordActionQueryExecutor.CoordActionQuery;
037import org.apache.oozie.executor.jpa.CoordJobGetReadyActionsJPAExecutor;
038import org.apache.oozie.executor.jpa.CoordJobGetRunningActionsCountJPAExecutor;
039import org.apache.oozie.executor.jpa.CoordJobQueryExecutor;
040import org.apache.oozie.executor.jpa.CoordJobQueryExecutor.CoordJobQuery;
041import org.apache.oozie.executor.jpa.JPAExecutorException;
042import org.apache.oozie.service.ConfigurationService;
043import org.apache.oozie.service.JPAService;
044import org.apache.oozie.service.Services;
045import org.apache.oozie.util.DateUtils;
046import org.apache.oozie.util.LogUtils;
047import org.apache.oozie.util.XLog;
048import org.jdom.JDOMException;
049
050public class CoordActionReadyXCommand extends CoordinatorXCommand<Void> {
051    private final String jobId;
052    private final XLog log = getLog();
053    private CoordinatorJobBean coordJob = null;
054    private JPAService jpaService = null;
055
056    public CoordActionReadyXCommand(String id) {
057        super("coord_action_ready", "coord_action_ready", 1);
058        this.jobId = id;
059    }
060
061    @Override
062    protected void setLogInfo() {
063        LogUtils.setLogInfo(jobId);
064    }
065
066    @Override
067    /**
068     * Check for READY actions and change state to SUBMITTED by a command to submit the job to WF engine.
069     * This method checks all the actions associated with a jobId to figure out which actions
070     * to start (based on concurrency and execution order [FIFO, LIFO, LAST_ONLY, NONE])
071     *
072     */
073    protected Void execute() throws CommandException {
074        // number of actions to start (-1 means start ALL)
075        int numActionsToStart = -1;
076
077        // get execution setting for this job (FIFO, LIFO, LAST_ONLY)
078        CoordinatorJob.Execution jobExecution = coordJob.getExecutionOrder();
079        // get concurrency setting for this job
080        int jobConcurrency = coordJob.getConcurrency();
081        // if less than 0, then UNLIMITED concurrency
082        if (jobConcurrency >= 0) {
083            // count number of actions that are already RUNNING or SUBMITTED
084            // subtract from CONCURRENCY to calculate number of actions to start
085            // in WF engine
086
087            int numRunningJobs;
088            try {
089                numRunningJobs = jpaService.execute(new CoordJobGetRunningActionsCountJPAExecutor(jobId));
090            }
091            catch (JPAExecutorException je) {
092                throw new CommandException(je);
093            }
094
095            numActionsToStart = jobConcurrency - numRunningJobs;
096            if (numActionsToStart < 0) {
097                numActionsToStart = 0;
098            }
099            log.debug("concurrency=" + jobConcurrency + ", execution=" + jobExecution + ", numRunningJobs="
100                    + numRunningJobs + ", numLeftover=" + numActionsToStart);
101            // no actions to start
102            if (numActionsToStart == 0) {
103                log.info("Not starting any additional actions because max concurrency [{0}]" +
104                        " for coordinator [{1}] has been reached.", jobConcurrency, jobId);
105            }
106        }
107        // get list of actions that are READY and fit in the concurrency and execution
108
109        List<CoordinatorActionBean> actions;
110        try {
111            actions = jpaService.execute(new CoordJobGetReadyActionsJPAExecutor(jobId, jobExecution.name()));
112        }
113        catch (JPAExecutorException je) {
114            throw new CommandException(je);
115        }
116        log.debug("Number of READY actions = " + actions.size());
117        Date now = new Date();
118        // If we're using LAST_ONLY or NONE, we should check if any of these need to be SKIPPED instead of SUBMITTED
119        if (jobExecution.equals(CoordinatorJobBean.Execution.LAST_ONLY)) {
120            for (Iterator<CoordinatorActionBean> it = actions.iterator(); it.hasNext(); ) {
121                CoordinatorActionBean action = it.next();
122                try {
123                    Date nextNominalTime = CoordCommandUtils.computeNextNominalTime(coordJob, action);
124                    if (nextNominalTime != null) {
125                        // If the current time is after the next action's nominal time, then we've passed the window where this
126                        // action should be started; so set it to SKIPPED
127                        if (now.after(nextNominalTime)) {
128                            LOG.info("LAST_ONLY execution: Preparing to skip action [{0}] because the current time [{1}] is later "
129                                    + "than the nominal time [{2}] of the next action]", action.getId(),
130                                    DateUtils.formatDateOozieTZ(now), DateUtils.formatDateOozieTZ(nextNominalTime));
131                            queue(new CoordActionSkipXCommand(action, coordJob.getUser(), coordJob.getAppName()));
132                            it.remove();
133                        } else {
134                            LOG.debug("LAST_ONLY execution: Not skipping action [{0}] because the current time [{1}] is earlier "
135                                    + "than the nominal time [{2}] of the next action]", action.getId(),
136                                    DateUtils.formatDateOozieTZ(now), DateUtils.formatDateOozieTZ(nextNominalTime));
137                        }
138                    }
139                } catch (ParseException e) {
140                    LOG.error("Should not happen", e);
141                } catch (JDOMException e) {
142                    LOG.error("Should not happen", e);
143                }
144            }
145        }
146        else if (jobExecution.equals(CoordinatorJobBean.Execution.NONE)) {
147            for (Iterator<CoordinatorActionBean> it = actions.iterator(); it.hasNext(); ) {
148                CoordinatorActionBean action = it.next();
149                // If the current time is after the nominal time of this action plus some tolerance,
150                // then we've passed the window where this action should be started; so set it to SKIPPED
151                Calendar cal = Calendar.getInstance(DateUtils.getTimeZone(coordJob.getTimeZone()));
152                cal.setTime(action.getNominalTime());
153                int tolerance = ConfigurationService.getInt(CoordActionInputCheckXCommand.COORD_EXECUTION_NONE_TOLERANCE);
154                cal.add(Calendar.MINUTE, tolerance);
155                if (now.after(cal.getTime())) {
156                    LOG.info("NONE execution: Preparing to skip action [{0}] because the current time [{1}] is more than [{2}]"
157                                    + " minutes later than the nominal time [{3}] of the current action]", action.getId(),
158                            DateUtils.formatDateOozieTZ(now), tolerance, DateUtils.formatDateOozieTZ(action.getNominalTime()));
159                    queue(new CoordActionSkipXCommand(action, coordJob.getUser(), coordJob.getAppName()));
160                    it.remove();
161                } else {
162                    LOG.debug("NONE execution: Not skipping action [{0}] because the current time [{1}] is earlier than [{2}]"
163                                    + " minutes later than the nominal time [{3}] of the current action]", action.getId(),
164                            DateUtils.formatDateOozieTZ(now), tolerance, DateUtils.formatDateOozieTZ(action.getNominalTime()));
165                }
166            }
167        }
168
169        int counter = 0;
170        for (CoordinatorActionBean action : actions) {
171            // continue if numActionsToStart is negative (no limit on number of
172            // actions), or if the counter is less than numActionsToStart
173            if ((numActionsToStart < 0) || (counter < numActionsToStart)) {
174                log.debug("Set status to SUBMITTED for id: " + action.getId());
175                // change state of action to SUBMITTED
176                action.setStatus(CoordinatorAction.Status.SUBMITTED);
177                try {
178                    CoordActionQueryExecutor.getInstance().executeUpdate(
179                            CoordActionQuery.UPDATE_COORD_ACTION_STATUS_PENDING_TIME, action);
180                }
181                catch (JPAExecutorException je) {
182                    throw new CommandException(je);
183                }
184                // start action
185                new CoordActionStartXCommand(action.getId(), coordJob.getUser(), coordJob.getAppName(),
186                        action.getJobId()).call();
187            }
188            else {
189                break;
190            }
191            counter++;
192
193        }
194        return null;
195    }
196
197    @Override
198    public String getEntityKey() {
199        return jobId;
200    }
201
202    @Override
203    public String getKey() {
204        return getName() + "_" + jobId;
205    }
206
207    @Override
208    protected boolean isLockRequired() {
209        return true;
210    }
211
212    @Override
213    protected void loadState() throws CommandException {
214        jpaService = Services.get().get(JPAService.class);
215        if (jpaService == null) {
216            throw new CommandException(ErrorCode.E0610);
217        }
218        try {
219            coordJob = CoordJobQueryExecutor.getInstance().get(CoordJobQuery.GET_COORD_JOB_ACTION_READY, jobId);
220        }
221        catch (JPAExecutorException e) {
222            throw new CommandException(e);
223        }
224        LogUtils.setLogInfo(coordJob);
225    }
226
227    @Override
228    protected void verifyPrecondition() throws CommandException, PreconditionException {
229        if (coordJob.getStatus() != Job.Status.RUNNING && coordJob.getStatus() != Job.Status.RUNNINGWITHERROR
230                && coordJob.getStatus() != Job.Status.SUCCEEDED && coordJob.getStatus() != Job.Status.PAUSED
231                && coordJob.getStatus() != Job.Status.PAUSEDWITHERROR) {
232            throw new PreconditionException(ErrorCode.E1100, "[" + jobId
233                    + "]::CoordActionReady:: Ignoring job. Coordinator job is not in RUNNING state, but state="
234                    + coordJob.getStatus());
235        }
236    }
237}