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.sla;
020
021import java.sql.Timestamp;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Date;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.concurrent.ConcurrentHashMap;
031
032import org.apache.hadoop.conf.Configuration;
033import org.apache.oozie.AppType;
034import org.apache.oozie.CoordinatorActionBean;
035import org.apache.oozie.CoordinatorJobBean;
036import org.apache.oozie.ErrorCode;
037import org.apache.oozie.WorkflowActionBean;
038import org.apache.oozie.WorkflowJobBean;
039import org.apache.oozie.XException;
040import org.apache.oozie.client.CoordinatorAction;
041import org.apache.oozie.client.WorkflowAction;
042import org.apache.oozie.client.WorkflowJob;
043import org.apache.oozie.client.event.JobEvent;
044import org.apache.oozie.client.event.SLAEvent.EventStatus;
045import org.apache.oozie.client.event.SLAEvent.SLAStatus;
046import org.apache.oozie.client.rest.JsonBean;
047import org.apache.oozie.executor.jpa.BatchQueryExecutor;
048import org.apache.oozie.executor.jpa.CoordActionGetForSLAJPAExecutor;
049import org.apache.oozie.executor.jpa.CoordActionQueryExecutor;
050import org.apache.oozie.executor.jpa.CoordActionQueryExecutor.CoordActionQuery;
051import org.apache.oozie.executor.jpa.CoordJobQueryExecutor;
052import org.apache.oozie.executor.jpa.CoordJobQueryExecutor.CoordJobQuery;
053import org.apache.oozie.executor.jpa.JPAExecutorException;
054import org.apache.oozie.executor.jpa.SLARegistrationQueryExecutor;
055import org.apache.oozie.executor.jpa.WorkflowActionQueryExecutor;
056import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor;
057import org.apache.oozie.executor.jpa.SLARegistrationQueryExecutor.SLARegQuery;
058import org.apache.oozie.executor.jpa.SLASummaryQueryExecutor;
059import org.apache.oozie.executor.jpa.WorkflowActionGetForSLAJPAExecutor;
060import org.apache.oozie.executor.jpa.WorkflowJobGetForSLAJPAExecutor;
061import org.apache.oozie.executor.jpa.WorkflowActionQueryExecutor.WorkflowActionQuery;
062import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor.WorkflowJobQuery;
063import org.apache.oozie.executor.jpa.sla.SLASummaryGetRecordsOnRestartJPAExecutor;
064import org.apache.oozie.executor.jpa.SLASummaryQueryExecutor.SLASummaryQuery;
065import org.apache.oozie.executor.jpa.BatchQueryExecutor.UpdateEntry;
066import org.apache.oozie.lock.LockToken;
067import org.apache.oozie.service.ConfigurationService;
068import org.apache.oozie.service.EventHandlerService;
069import org.apache.oozie.service.JPAService;
070import org.apache.oozie.service.JobsConcurrencyService;
071import org.apache.oozie.service.MemoryLocksService;
072import org.apache.oozie.service.SchedulerService;
073import org.apache.oozie.service.ServiceException;
074import org.apache.oozie.service.Services;
075import org.apache.oozie.sla.service.SLAService;
076import org.apache.oozie.util.DateUtils;
077import org.apache.oozie.util.LogUtils;
078import org.apache.oozie.util.XLog;
079
080import com.google.common.annotations.VisibleForTesting;
081
082
083/**
084 * Implementation class for SLACalculator that calculates SLA related to
085 * start/end/duration of jobs using a memory-based map
086 */
087public class SLACalculatorMemory implements SLACalculator {
088
089    private static XLog LOG = XLog.getLog(SLACalculatorMemory.class);
090    // TODO optimization priority based insertion/processing/bumping up-down
091    protected Map<String, SLACalcStatus> slaMap;
092    protected Set<String> historySet;
093    private static int capacity;
094    private static JPAService jpaService;
095    protected EventHandlerService eventHandler;
096    private static int modifiedAfter;
097    private static long jobEventLatency;
098
099    @Override
100    public void init(Configuration conf) throws ServiceException {
101        capacity = ConfigurationService.getInt(conf, SLAService.CONF_CAPACITY);
102        jobEventLatency = ConfigurationService.getInt(conf, SLAService.CONF_JOB_EVENT_LATENCY);
103        slaMap = new ConcurrentHashMap<String, SLACalcStatus>();
104        historySet = Collections.synchronizedSet(new HashSet<String>());
105        jpaService = Services.get().get(JPAService.class);
106        eventHandler = Services.get().get(EventHandlerService.class);
107        // load events modified after
108        modifiedAfter = conf.getInt(SLAService.CONF_EVENTS_MODIFIED_AFTER, 7);
109        loadOnRestart();
110        Runnable purgeThread = new HistoryPurgeWorker();
111        // schedule runnable by default 1 day
112        Services.get()
113                .get(SchedulerService.class)
114                .schedule(purgeThread, 86400, Services.get().getConf().getInt(SLAService.CONF_SLA_HISTORY_PURGE_INTERVAL, 86400),
115                        SchedulerService.Unit.SEC);
116    }
117
118    public class HistoryPurgeWorker implements Runnable {
119
120        public HistoryPurgeWorker() {
121        }
122
123        @Override
124        public void run() {
125            if (Thread.currentThread().isInterrupted()) {
126                return;
127            }
128            Iterator<String> jobItr = historySet.iterator();
129            while (jobItr.hasNext()) {
130                String jobId = jobItr.next();
131
132                if (jobId.endsWith("-W")) {
133                    WorkflowJobBean wfJob = null;
134                    try {
135                        wfJob = WorkflowJobQueryExecutor.getInstance().get(WorkflowJobQuery.GET_WORKFLOW_STATUS, jobId);
136                    }
137                    catch (JPAExecutorException e) {
138                        if (e.getErrorCode().equals(ErrorCode.E0604)) {
139                            jobItr.remove();
140                        }
141                        else {
142                            LOG.info("Failed to fetch the workflow job: " + jobId, e);
143                        }
144                    }
145                    if (wfJob != null && wfJob.inTerminalState()) {
146                        try {
147                            updateSLASummary(wfJob.getId(), wfJob.getStartTime(), wfJob.getEndTime());
148                            jobItr.remove();
149                        }
150                        catch (JPAExecutorException e) {
151                            LOG.info("Failed to update SLASummaryBean when purging history set entry for " + jobId, e);
152                        }
153
154                    }
155                }
156                else if (jobId.contains("-W@")) {
157                    WorkflowActionBean wfAction = null;
158                    try {
159                        wfAction = WorkflowActionQueryExecutor.getInstance().get(
160                                WorkflowActionQuery.GET_ACTION_COMPLETED, jobId);
161                    }
162                    catch (JPAExecutorException e) {
163                        if (e.getErrorCode().equals(ErrorCode.E0605)) {
164                            jobItr.remove();
165                        }
166                        else {
167                            LOG.info("Failed to fetch the workflow action: " + jobId, e);
168                        }
169                    }
170                    if (wfAction != null && (wfAction.isComplete() || wfAction.isTerminalWithFailure())) {
171                        try {
172                            updateSLASummary(wfAction.getId(), wfAction.getStartTime(), wfAction.getEndTime());
173                            jobItr.remove();
174                        }
175                        catch (JPAExecutorException e) {
176                            LOG.info("Failed to update SLASummaryBean when purging history set entry for " + jobId, e);
177                        }
178                    }
179                }
180                else if (jobId.contains("-C@")) {
181                    CoordinatorActionBean cAction = null;
182                    try {
183                        cAction = CoordActionQueryExecutor.getInstance().get(CoordActionQuery.GET_COORD_ACTION, jobId);
184                    }
185                    catch (JPAExecutorException e) {
186                        if (e.getErrorCode().equals(ErrorCode.E0605)) {
187                            jobItr.remove();
188                        }
189                        else {
190                            LOG.info("Failed to fetch the coord action: " + jobId, e);
191                        }
192                    }
193                    if (cAction != null && cAction.isTerminalStatus()) {
194                        try {
195                            updateSLASummaryForCoordAction(cAction);
196                            jobItr.remove();
197                        }
198                        catch (JPAExecutorException e) {
199                            XLog.getLog(SLACalculatorMemory.class).info(
200                                    "Failed to update SLASummaryBean when purging history set entry for " + jobId, e);
201                        }
202
203                    }
204                }
205                else if (jobId.endsWith("-C")) {
206                    CoordinatorJobBean cJob = null;
207                    try {
208                        cJob = CoordJobQueryExecutor.getInstance().get(CoordJobQuery.GET_COORD_JOB_STATUS_PARENTID,
209                                jobId);
210                    }
211                    catch (JPAExecutorException e) {
212                        if (e.getErrorCode().equals(ErrorCode.E0604)) {
213                            jobItr.remove();
214                        }
215                        else {
216                            LOG.info("Failed to fetch the coord job: " + jobId, e);
217                        }
218                    }
219                    if (cJob != null && cJob.isTerminalStatus()) {
220                        try {
221                            updateSLASummary(cJob.getId(), cJob.getStartTime(), cJob.getEndTime());
222                            jobItr.remove();
223                        }
224                        catch (JPAExecutorException e) {
225                            LOG.info("Failed to update SLASummaryBean when purging history set entry for " + jobId, e);
226                        }
227
228                    }
229                }
230            }
231        }
232
233        private void updateSLASummary(String id, Date startTime, Date endTime) throws JPAExecutorException {
234            SLASummaryBean sla = SLASummaryQueryExecutor.getInstance().get(SLASummaryQuery.GET_SLA_SUMMARY, id);
235            if (sla != null) {
236                sla.setActualStart(startTime);
237                sla.setActualEnd(endTime);
238                if (startTime != null && endTime != null) {
239                    sla.setActualDuration(endTime.getTime() - startTime.getTime());
240                }
241                sla.setLastModifiedTime(new Date());
242                sla.setEventProcessed(8);
243                SLASummaryQueryExecutor.getInstance().executeUpdate(
244                        SLASummaryQuery.UPDATE_SLA_SUMMARY_FOR_ACTUAL_TIMES, sla);
245            }
246        }
247
248        private void updateSLASummaryForCoordAction(CoordinatorActionBean bean) throws JPAExecutorException {
249            String wrkflowId = bean.getExternalId();
250            if (wrkflowId != null) {
251                WorkflowJobBean wrkflow = WorkflowJobQueryExecutor.getInstance().get(
252                        WorkflowJobQuery.GET_WORKFLOW_START_END_TIME, wrkflowId);
253                if (wrkflow != null) {
254                    updateSLASummary(bean.getId(), wrkflow.getStartTime(), wrkflow.getEndTime());
255                }
256            }
257        }
258    }
259
260    private void loadOnRestart() {
261        boolean isJobModified = false;
262        try {
263            long slaPendingCount = 0;
264            long statusPendingCount = 0;
265            List<SLASummaryBean> summaryBeans = jpaService.execute(new SLASummaryGetRecordsOnRestartJPAExecutor(
266                    modifiedAfter));
267            for (SLASummaryBean summaryBean : summaryBeans) {
268                String jobId = summaryBean.getId();
269                LockToken lock = null;
270                switch (summaryBean.getAppType()) {
271                    case COORDINATOR_ACTION:
272                        isJobModified = processSummaryBeanForCoordAction(summaryBean, jobId);
273                        break;
274                    case WORKFLOW_ACTION:
275                        isJobModified = processSummaryBeanForWorkflowAction(summaryBean, jobId);
276                        break;
277                    case WORKFLOW_JOB:
278                        isJobModified = processSummaryBeanForWorkflowJob(summaryBean, jobId);
279                        break;
280                    default:
281                        break;
282                }
283                if (isJobModified) {
284                    try {
285                        boolean update = true;
286                        if (Services.get().get(JobsConcurrencyService.class).isHighlyAvailableMode()) {
287                            lock = Services
288                                    .get()
289                                    .get(MemoryLocksService.class)
290                                    .getWriteLock(
291                                            SLACalcStatus.SLA_ENTITYKEY_PREFIX + jobId,
292                                            Services.get().getConf()
293                                                    .getLong(SLAService.CONF_SLA_CALC_LOCK_TIMEOUT, 5 * 1000));
294                            if (lock == null) {
295                                update = false;
296                            }
297                        }
298                        if (update) {
299                            summaryBean.setLastModifiedTime(new Date());
300                            SLASummaryQueryExecutor.getInstance().executeUpdate(
301                                    SLASummaryQuery.UPDATE_SLA_SUMMARY_FOR_STATUS_ACTUAL_TIMES, summaryBean);
302                        }
303                    }
304                    catch (Exception e) {
305                        LOG.warn("Failed to load records for " + jobId, e);
306                    }
307                    finally {
308                        if (lock != null) {
309                            lock.release();
310                            lock = null;
311                        }
312                    }
313                }
314                try {
315                    if (summaryBean.getEventProcessed() == 7) {
316                        historySet.add(jobId);
317                        statusPendingCount++;
318                    }
319                    else if (summaryBean.getEventProcessed() <= 7) {
320                        SLARegistrationBean slaRegBean = SLARegistrationQueryExecutor.getInstance().get(
321                                SLARegQuery.GET_SLA_REG_ON_RESTART, jobId);
322                        SLACalcStatus slaCalcStatus = new SLACalcStatus(summaryBean, slaRegBean);
323                        slaMap.put(jobId, slaCalcStatus);
324                        slaPendingCount++;
325                    }
326                }
327                catch (Exception e) {
328                    LOG.warn("Failed to fetch/update records for " + jobId, e);
329                }
330
331            }
332            LOG.info("Loaded SLASummary pendingSLA=" + slaPendingCount + ", pendingStatusUpdate=" + statusPendingCount);
333
334        }
335        catch (Exception e) {
336            LOG.warn("Failed to retrieve SLASummary records on restart", e);
337        }
338    }
339
340    private boolean processSummaryBeanForCoordAction(SLASummaryBean summaryBean, String jobId)
341            throws JPAExecutorException {
342        boolean isJobModified = false;
343        CoordinatorActionBean coordAction = null;
344        coordAction = jpaService.execute(new CoordActionGetForSLAJPAExecutor(jobId));
345        if (!coordAction.getStatusStr().equals(summaryBean.getJobStatus())) {
346            LOG.trace("Coordinator action status is " + coordAction.getStatusStr() + " and summary bean status is "
347                    + summaryBean.getJobStatus());
348            isJobModified = true;
349            summaryBean.setJobStatus(coordAction.getStatusStr());
350            if (coordAction.isTerminalStatus()) {
351                WorkflowJobBean wfJob = jpaService.execute(new WorkflowJobGetForSLAJPAExecutor(coordAction
352                        .getExternalId()));
353                setEndForSLASummaryBean(summaryBean, wfJob.getStartTime(), coordAction.getLastModifiedTime(),
354                        coordAction.getStatusStr());
355            }
356            else if (coordAction.getStatus() != CoordinatorAction.Status.WAITING) {
357                WorkflowJobBean wfJob = jpaService.execute(new WorkflowJobGetForSLAJPAExecutor(coordAction
358                        .getExternalId()));
359                setStartForSLASummaryBean(summaryBean, summaryBean.getEventProcessed(), wfJob.getStartTime());
360            }
361        }
362        return isJobModified;
363    }
364
365    private boolean processSummaryBeanForWorkflowAction(SLASummaryBean summaryBean, String jobId)
366            throws JPAExecutorException {
367        boolean isJobModified = false;
368        WorkflowActionBean wfAction = null;
369        wfAction = jpaService.execute(new WorkflowActionGetForSLAJPAExecutor(jobId));
370        if (!wfAction.getStatusStr().equals(summaryBean.getJobStatus())) {
371            LOG.trace("Workflow action status is " + wfAction.getStatusStr() + "and summary bean status is "
372                    + summaryBean.getJobStatus());
373            isJobModified = true;
374            summaryBean.setJobStatus(wfAction.getStatusStr());
375            if (wfAction.inTerminalState()) {
376                setEndForSLASummaryBean(summaryBean, wfAction.getStartTime(), wfAction.getEndTime(), wfAction.getStatusStr());
377            }
378            else if (wfAction.getStatus() != WorkflowAction.Status.PREP) {
379                setStartForSLASummaryBean(summaryBean, summaryBean.getEventProcessed(), wfAction.getStartTime());
380            }
381        }
382        return isJobModified;
383    }
384
385    private boolean processSummaryBeanForWorkflowJob(SLASummaryBean summaryBean, String jobId)
386            throws JPAExecutorException {
387        boolean isJobModified = false;
388        WorkflowJobBean wfJob = null;
389        wfJob = jpaService.execute(new WorkflowJobGetForSLAJPAExecutor(jobId));
390        if (!wfJob.getStatusStr().equals(summaryBean.getJobStatus())) {
391            LOG.trace("Workflow job status is " + wfJob.getStatusStr() + "and summary bean status is "
392                    + summaryBean.getJobStatus());
393            isJobModified = true;
394            summaryBean.setJobStatus(wfJob.getStatusStr());
395            if (wfJob.inTerminalState()) {
396                setEndForSLASummaryBean(summaryBean, wfJob.getStartTime(), wfJob.getEndTime(), wfJob.getStatusStr());
397            }
398            else if (wfJob.getStatus() != WorkflowJob.Status.PREP) {
399                setStartForSLASummaryBean(summaryBean, summaryBean.getEventProcessed(), wfJob.getStartTime());
400            }
401        }
402        return isJobModified;
403    }
404
405    private void setEndForSLASummaryBean(SLASummaryBean summaryBean, Date startTime, Date endTime, String status) {
406        byte eventProc = summaryBean.getEventProcessed();
407        summaryBean.setEventProcessed(8);
408        summaryBean.setActualStart(startTime);
409        summaryBean.setActualEnd(endTime);
410        long actualDuration = endTime.getTime() - startTime.getTime();
411        summaryBean.setActualDuration(actualDuration);
412        if (eventProc < 4) {
413            if (status.equals(WorkflowJob.Status.SUCCEEDED.name()) || status.equals(WorkflowAction.Status.OK.name())
414                    || status.equals(CoordinatorAction.Status.SUCCEEDED.name())) {
415                if (endTime.getTime() <= summaryBean.getExpectedEnd().getTime()) {
416                    summaryBean.setSLAStatus(SLAStatus.MET);
417                }
418                else {
419                    summaryBean.setSLAStatus(SLAStatus.MISS);
420                }
421            }
422            else {
423                summaryBean.setSLAStatus(SLAStatus.MISS);
424            }
425        }
426
427    }
428
429    private void setStartForSLASummaryBean(SLASummaryBean summaryBean, byte eventProc, Date startTime) {
430        if (((eventProc & 1) == 0)) {
431            eventProc += 1;
432            summaryBean.setEventProcessed(eventProc);
433        }
434        if (summaryBean.getSLAStatus().equals(SLAStatus.NOT_STARTED)) {
435            summaryBean.setSLAStatus(SLAStatus.IN_PROCESS);
436        }
437        summaryBean.setActualStart(startTime);
438    }
439
440    @Override
441    public int size() {
442        return slaMap.size();
443    }
444
445    @Override
446    public SLACalcStatus get(String jobId) throws JPAExecutorException {
447        SLACalcStatus memObj;
448        memObj = slaMap.get(jobId);
449        if (memObj == null && historySet.contains(jobId)) {
450            memObj = new SLACalcStatus(SLASummaryQueryExecutor.getInstance().get(SLASummaryQuery.GET_SLA_SUMMARY, jobId),
451                    SLARegistrationQueryExecutor.getInstance().get(SLARegQuery.GET_SLA_REG_ON_RESTART, jobId));
452        }
453        return memObj;
454    }
455
456    @Override
457    public Iterator<String> iterator() {
458        return slaMap.keySet().iterator();
459    }
460
461    @Override
462    public boolean isEmpty() {
463        return slaMap.isEmpty();
464    }
465
466    @Override
467    public void clear() {
468        slaMap.clear();
469        historySet.clear();
470    }
471
472    /**
473     * Invoked via periodic run, update the SLA for registered jobs
474     */
475    protected void updateJobSla(String jobId) throws Exception {
476        SLACalcStatus slaCalc = slaMap.get(jobId);
477        synchronized (slaCalc) {
478            boolean change = false;
479            // get eventProcessed on DB for validation in HA
480            Object eventProcObj = ((SLASummaryQueryExecutor) SLASummaryQueryExecutor.getInstance()).getSingleValue(
481                    SLASummaryQuery.GET_SLA_SUMMARY_EVENTPROCESSED, jobId);
482            byte eventProc = ((Byte) eventProcObj).byteValue();
483            if (eventProc >= 7) {
484                if (eventProc == 7) {
485                    historySet.add(jobId);
486                }
487                slaMap.remove(jobId);
488                LOG.trace("Removed Job [{0}] from map as SLA processed", jobId);
489            }
490            else {
491                slaCalc.setEventProcessed(eventProc);
492                SLARegistrationBean reg = slaCalc.getSLARegistrationBean();
493                // calculation w.r.t current time and status
494                if ((eventProc & 1) == 0) { // first bit (start-processed) unset
495                    if (reg.getExpectedStart() != null) {
496                        if (reg.getExpectedStart().getTime() + jobEventLatency < System.currentTimeMillis()) {
497                            confirmWithDB(slaCalc);
498                            eventProc = slaCalc.getEventProcessed();
499                            if (eventProc != 8 && (eventProc & 1) == 0) {
500                                // Some DB exception
501                                slaCalc.setEventStatus(EventStatus.START_MISS);
502                                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
503                                eventProc++;
504                            }
505                            change = true;
506                        }
507                    }
508                    else {
509                        eventProc++; // disable further processing for optional start sla condition
510                        change = true;
511                    }
512                }
513                // check if second bit (duration-processed) is unset
514                if (eventProc != 8 && ((eventProc >> 1) & 1) == 0) {
515                    if (reg.getExpectedDuration() == -1) {
516                        eventProc += 2;
517                        change = true;
518                    }
519                    else if (slaCalc.getActualStart() != null) {
520                        if ((reg.getExpectedDuration() + jobEventLatency) < (System.currentTimeMillis() - slaCalc
521                                .getActualStart().getTime())) {
522                            slaCalc.setEventProcessed(eventProc);
523                            confirmWithDB(slaCalc);
524                            eventProc = slaCalc.getEventProcessed();
525                            if (eventProc != 8 && ((eventProc >> 1) & 1) == 0) {
526                                // Some DB exception
527                                slaCalc.setEventStatus(EventStatus.DURATION_MISS);
528                                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
529                                eventProc += 2;
530                            }
531                            change = true;
532                        }
533                    }
534                }
535                if (eventProc < 4) {
536                    if (reg.getExpectedEnd().getTime() + jobEventLatency < System.currentTimeMillis()) {
537                        slaCalc.setEventProcessed(eventProc);
538                        confirmWithDB(slaCalc);
539                        eventProc = slaCalc.getEventProcessed();
540                        change = true;
541                    }
542                }
543                if (change) {
544                    try {
545                        boolean locked = true;
546                        slaCalc.acquireLock();
547                        locked = slaCalc.isLocked();
548                        if (locked) {
549                            // no more processing, no transfer to history set
550                            if (slaCalc.getEventProcessed() >= 8) {
551                                eventProc = 8;
552                                // Should not be > 8. But to handle any corner cases
553                                slaCalc.setEventProcessed(8);
554                                slaMap.remove(jobId);
555                            }
556                            else {
557                                slaCalc.setEventProcessed(eventProc);
558                            }
559                            SLASummaryBean slaSummaryBean = new SLASummaryBean();
560                            slaSummaryBean.setId(slaCalc.getId());
561                            slaSummaryBean.setEventProcessed(eventProc);
562                            slaSummaryBean.setSLAStatus(slaCalc.getSLAStatus());
563                            slaSummaryBean.setEventStatus(slaCalc.getEventStatus());
564                            slaSummaryBean.setActualEnd(slaCalc.getActualEnd());
565                            slaSummaryBean.setActualStart(slaCalc.getActualStart());
566                            slaSummaryBean.setActualDuration(slaCalc.getActualDuration());
567                            slaSummaryBean.setJobStatus(slaCalc.getJobStatus());
568                            slaSummaryBean.setLastModifiedTime(new Date());
569                            SLASummaryQueryExecutor.getInstance().executeUpdate(
570                                    SLASummaryQuery.UPDATE_SLA_SUMMARY_FOR_STATUS_ACTUAL_TIMES, slaSummaryBean);
571                            if (eventProc == 7) {
572                                historySet.add(jobId);
573                                slaMap.remove(jobId);
574                                LOG.trace("Removed Job [{0}] from map after End-processed", jobId);
575                            }
576                        }
577                    }
578                    catch (InterruptedException e) {
579                        throw new XException(ErrorCode.E0606, slaCalc.getId(), slaCalc.getLockTimeOut());
580                    }
581                    finally {
582                        slaCalc.releaseLock();
583                    }
584                }
585            }
586        }
587    }
588
589    /**
590     * Periodically run by the SLAService worker threads to update SLA status by
591     * iterating through all the jobs in the map
592     */
593    @Override
594    public void updateAllSlaStatus() {
595        LOG.info("Running periodic SLA check");
596        Iterator<String> iterator = slaMap.keySet().iterator();
597        while (iterator.hasNext()) {
598            String jobId = iterator.next();
599            try {
600                LOG.trace("Processing SLA for jobid={0}", jobId);
601                updateJobSla(jobId);
602            }
603            catch (Exception e) {
604                setLogPrefix(jobId);
605                LOG.error("Exception in SLA processing for job [{0}]", jobId, e);
606                LogUtils.clearLogPrefix();
607            }
608        }
609    }
610
611    /**
612     * Register a new job into the map for SLA tracking
613     */
614    @Override
615    public boolean addRegistration(String jobId, SLARegistrationBean reg) throws JPAExecutorException {
616        try {
617            if (slaMap.size() < capacity) {
618                SLACalcStatus slaCalc = new SLACalcStatus(reg);
619                slaCalc.setSLAStatus(SLAStatus.NOT_STARTED);
620                slaCalc.setJobStatus(getJobStatus(reg.getAppType()));
621                slaMap.put(jobId, slaCalc);
622                List<JsonBean> insertList = new ArrayList<JsonBean>();
623                final SLASummaryBean summaryBean = new SLASummaryBean(slaCalc);
624                final Timestamp currentTime = DateUtils.convertDateToTimestamp(new Date());
625                reg.setCreatedTimestamp(currentTime);
626                summaryBean.setCreatedTimestamp(currentTime);
627                insertList.add(reg);
628                insertList.add(summaryBean);
629                BatchQueryExecutor.getInstance().executeBatchInsertUpdateDelete(insertList, null, null);
630                LOG.trace("SLA Registration Event - Job:" + jobId);
631                return true;
632            }
633            else {
634                setLogPrefix(reg.getId());
635                LOG.error(
636                        "SLACalculator memory capacity reached. Cannot add or update new SLA Registration entry for job [{0}]",
637                        reg.getId());
638                LogUtils.clearLogPrefix();
639            }
640        }
641        catch (JPAExecutorException jpa) {
642            throw jpa;
643        }
644        return false;
645    }
646
647    private String getJobStatus(AppType appType) {
648        String status = null;
649        switch (appType) {
650            case COORDINATOR_ACTION:
651                status = CoordinatorAction.Status.WAITING.name();
652                break;
653            case WORKFLOW_ACTION:
654                status = WorkflowAction.Status.PREP.name();
655                break;
656            case WORKFLOW_JOB:
657                status = WorkflowJob.Status.PREP.name();
658                break;
659            default:
660                break;
661        }
662        return status;
663    }
664
665    /**
666     * Update job into the map for SLA tracking
667     */
668    @Override
669    public boolean updateRegistration(String jobId, SLARegistrationBean reg) throws JPAExecutorException {
670        try {
671            if (slaMap.size() < capacity) {
672                SLACalcStatus slaCalc = new SLACalcStatus(reg);
673                slaCalc.setSLAStatus(SLAStatus.NOT_STARTED);
674                slaCalc.setJobStatus(getJobStatus(reg.getAppType()));
675                slaMap.put(jobId, slaCalc);
676                List<UpdateEntry> updateList = new ArrayList<UpdateEntry>();
677                updateList.add(new UpdateEntry<SLARegQuery>(SLARegQuery.UPDATE_SLA_REG_ALL, reg));
678                updateList.add(new UpdateEntry<SLASummaryQuery>(SLASummaryQuery.UPDATE_SLA_SUMMARY_ALL,
679                        new SLASummaryBean(slaCalc)));
680                BatchQueryExecutor.getInstance().executeBatchInsertUpdateDelete(null, updateList, null);
681                LOG.trace("SLA Registration Event - Job:" + jobId);
682                return true;
683            }
684            else {
685                setLogPrefix(reg.getId());
686                LOG.error(
687                        "SLACalculator memory capacity reached. Cannot add or update new SLA Registration entry for job [{0}]",
688                        reg.getId());
689                LogUtils.clearLogPrefix();
690            }
691        }
692        catch (JPAExecutorException jpa) {
693            throw jpa;
694        }
695        return false;
696    }
697
698    /**
699     * Remove job from being tracked in map
700     */
701    @Override
702    public void removeRegistration(String jobId) {
703        if (slaMap.remove(jobId) == null) {
704            historySet.remove(jobId);
705        }
706    }
707
708    /**
709     * Triggered after receiving Job status change event, update SLA status
710     * accordingly
711     */
712    @Override
713    public boolean addJobStatus(String jobId, String jobStatus, JobEvent.EventStatus jobEventStatus, Date startTime,
714            Date endTime) throws JPAExecutorException, ServiceException {
715        SLACalcStatus slaCalc = slaMap.get(jobId);
716        SLASummaryBean slaInfo = null;
717        boolean hasSla = false;
718        if (slaCalc == null) {
719            if (historySet.contains(jobId)) {
720                slaInfo = SLASummaryQueryExecutor.getInstance().get(SLASummaryQuery.GET_SLA_SUMMARY, jobId);
721                if (slaInfo == null) {
722                    throw new JPAExecutorException(ErrorCode.E0604, jobId);
723                }
724                slaInfo.setJobStatus(jobStatus);
725                slaInfo.setActualStart(startTime);
726                slaInfo.setActualEnd(endTime);
727                if (endTime != null) {
728                    slaInfo.setActualDuration(endTime.getTime() - startTime.getTime());
729                }
730                slaInfo.setEventProcessed(8);
731                historySet.remove(jobId);
732                slaInfo.setLastModifiedTime(new Date());
733                SLASummaryQueryExecutor.getInstance().executeUpdate(
734                        SLASummaryQuery.UPDATE_SLA_SUMMARY_FOR_STATUS_ACTUAL_TIMES, slaInfo);
735                hasSla = true;
736            }
737            else if (Services.get().get(JobsConcurrencyService.class).isHighlyAvailableMode()) {
738                // jobid might not exist in slaMap in HA Setting
739                SLARegistrationBean slaRegBean = SLARegistrationQueryExecutor.getInstance().get(
740                        SLARegQuery.GET_SLA_REG_ALL, jobId);
741                if (slaRegBean != null) { // filter out jobs picked by SLA job event listener
742                                          // but not actually configured for SLA
743                    SLASummaryBean slaSummaryBean = SLASummaryQueryExecutor.getInstance().get(
744                            SLASummaryQuery.GET_SLA_SUMMARY, jobId);
745                    if (slaSummaryBean.getEventProcessed() < 7) {
746                        slaCalc = new SLACalcStatus(slaSummaryBean, slaRegBean);
747                        slaMap.put(jobId, slaCalc);
748                    }
749                }
750            }
751        }
752        if (slaCalc != null) {
753            synchronized (slaCalc) {
754                try {
755                    // only get ZK lock when multiple servers running
756                    boolean locked = true;
757                    slaCalc.acquireLock();
758                    locked = slaCalc.isLocked();
759                    if (locked) {
760                        // get eventProcessed on DB for validation in HA
761                        Object eventProcObj = ((SLASummaryQueryExecutor) SLASummaryQueryExecutor.getInstance())
762                                .getSingleValue(SLASummaryQuery.GET_SLA_SUMMARY_EVENTPROCESSED, jobId);
763                        byte eventProc = ((Byte) eventProcObj).byteValue();
764                        slaCalc.setEventProcessed(eventProc);
765                        slaCalc.setJobStatus(jobStatus);
766                        switch (jobEventStatus) {
767                            case STARTED:
768                                slaInfo = processJobStartSLA(slaCalc, startTime);
769                                break;
770                            case SUCCESS:
771                                slaInfo = processJobEndSuccessSLA(slaCalc, startTime, endTime);
772                                break;
773                            case FAILURE:
774                                slaInfo = processJobEndFailureSLA(slaCalc, startTime, endTime);
775                                break;
776                            default:
777                                LOG.debug("Unknown Job Status for SLA purpose[{0}]", jobEventStatus);
778                                slaInfo = getSLASummaryBean(slaCalc);
779                        }
780                        if (slaCalc.getEventProcessed() == 7) {
781                            slaInfo.setEventProcessed(8);
782                            slaMap.remove(jobId);
783                        }
784                        slaInfo.setLastModifiedTime(new Date());
785                        SLASummaryQueryExecutor.getInstance().executeUpdate(
786                                SLASummaryQuery.UPDATE_SLA_SUMMARY_FOR_STATUS_ACTUAL_TIMES, slaInfo);
787                        hasSla = true;
788                    }
789                }
790                catch (InterruptedException e) {
791                    throw new ServiceException(ErrorCode.E0606, slaCalc.getEntityKey(), slaCalc.getLockTimeOut());
792                }
793                finally {
794                    slaCalc.releaseLock();
795                }
796            }
797            LOG.trace("SLA Status Event - Job:" + jobId + " Status:" + slaCalc.getSLAStatus());
798        }
799
800        return hasSla;
801    }
802
803    /**
804     * Process SLA for jobs that started running. Also update actual-start time
805     *
806     * @param slaCalc
807     * @param actualStart
808     * @return SLASummaryBean
809     */
810    private SLASummaryBean processJobStartSLA(SLACalcStatus slaCalc, Date actualStart) {
811        slaCalc.setActualStart(actualStart);
812        if (slaCalc.getSLAStatus().equals(SLAStatus.NOT_STARTED)) {
813            slaCalc.setSLAStatus(SLAStatus.IN_PROCESS);
814        }
815        SLARegistrationBean reg = slaCalc.getSLARegistrationBean();
816        Date expecStart = reg.getExpectedStart();
817        byte eventProc = slaCalc.getEventProcessed();
818        // set event proc here
819        if (((eventProc & 1) == 0)) {
820            if (expecStart != null) {
821                if (actualStart.getTime() > expecStart.getTime()) {
822                    slaCalc.setEventStatus(EventStatus.START_MISS);
823                }
824                else {
825                    slaCalc.setEventStatus(EventStatus.START_MET);
826                }
827                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
828            }
829            eventProc += 1;
830            slaCalc.setEventProcessed(eventProc);
831        }
832        return getSLASummaryBean(slaCalc);
833    }
834
835    /**
836     * Process SLA for jobs that ended successfully. Also update actual-start
837     * and end time
838     *
839     * @param slaCalc
840     * @param actualStart
841     * @param actualEnd
842     * @return SLASummaryBean
843     * @throws JPAExecutorException
844     */
845    private SLASummaryBean processJobEndSuccessSLA(SLACalcStatus slaCalc, Date actualStart, Date actualEnd) throws JPAExecutorException {
846        SLARegistrationBean reg = slaCalc.getSLARegistrationBean();
847        slaCalc.setActualStart(actualStart);
848        slaCalc.setActualEnd(actualEnd);
849        long expectedDuration = reg.getExpectedDuration();
850        long actualDuration = actualEnd.getTime() - actualStart.getTime();
851        slaCalc.setActualDuration(actualDuration);
852        //check event proc
853        byte eventProc = slaCalc.getEventProcessed();
854        if (((eventProc >> 1) & 1) == 0) {
855            processDurationSLA(expectedDuration, actualDuration, slaCalc);
856            eventProc += 2;
857            slaCalc.setEventProcessed(eventProc);
858        }
859
860        if (eventProc < 4) {
861            Date expectedEnd = reg.getExpectedEnd();
862            if (actualEnd.getTime() > expectedEnd.getTime()) {
863                slaCalc.setEventStatus(EventStatus.END_MISS);
864                slaCalc.setSLAStatus(SLAStatus.MISS);
865            }
866            else {
867                slaCalc.setEventStatus(EventStatus.END_MET);
868                slaCalc.setSLAStatus(SLAStatus.MET);
869            }
870            eventProc += 4;
871            slaCalc.setEventProcessed(eventProc);
872            eventHandler.queueEvent(new SLACalcStatus(slaCalc));
873        }
874        return getSLASummaryBean(slaCalc);
875    }
876
877    /**
878     * Process SLA for jobs that ended in failure. Also update actual-start and
879     * end time
880     *
881     * @param slaCalc
882     * @param actualStart
883     * @param actualEnd
884     * @return SLASummaryBean
885     * @throws JPAExecutorException
886     */
887    private SLASummaryBean processJobEndFailureSLA(SLACalcStatus slaCalc, Date actualStart, Date actualEnd) throws JPAExecutorException {
888        slaCalc.setActualStart(actualStart);
889        slaCalc.setActualEnd(actualEnd);
890        if (actualStart == null) { // job failed before starting
891            if (slaCalc.getEventProcessed() < 4) {
892                slaCalc.setEventStatus(EventStatus.END_MISS);
893                slaCalc.setSLAStatus(SLAStatus.MISS);
894                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
895                slaCalc.setEventProcessed(7);
896                return getSLASummaryBean(slaCalc);
897            }
898        }
899        SLARegistrationBean reg = slaCalc.getSLARegistrationBean();
900        long expectedDuration = reg.getExpectedDuration();
901        long actualDuration = actualEnd.getTime() - actualStart.getTime();
902        slaCalc.setActualDuration(actualDuration);
903
904        byte eventProc = slaCalc.getEventProcessed();
905        if (((eventProc >> 1) & 1) == 0) {
906            if (expectedDuration != -1) {
907                slaCalc.setEventStatus(EventStatus.DURATION_MISS);
908                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
909            }
910            eventProc += 2;
911            slaCalc.setEventProcessed(eventProc);
912        }
913        if (eventProc < 4) {
914            slaCalc.setEventStatus(EventStatus.END_MISS);
915            slaCalc.setSLAStatus(SLAStatus.MISS);
916            eventProc += 4;
917            slaCalc.setEventProcessed(eventProc);
918            eventHandler.queueEvent(new SLACalcStatus(slaCalc));
919        }
920        return getSLASummaryBean(slaCalc);
921    }
922
923    private SLASummaryBean getSLASummaryBean (SLACalcStatus slaCalc) {
924        SLASummaryBean slaSummaryBean = new SLASummaryBean();
925        slaSummaryBean.setActualStart(slaCalc.getActualStart());
926        slaSummaryBean.setActualEnd(slaCalc.getActualEnd());
927        slaSummaryBean.setActualDuration(slaCalc.getActualDuration());
928        slaSummaryBean.setSLAStatus(slaCalc.getSLAStatus());
929        slaSummaryBean.setEventStatus(slaCalc.getEventStatus());
930        slaSummaryBean.setEventProcessed(slaCalc.getEventProcessed());
931        slaSummaryBean.setId(slaCalc.getId());
932        slaSummaryBean.setJobStatus(slaCalc.getJobStatus());
933        return slaSummaryBean;
934    }
935
936    private void processDurationSLA(long expected, long actual, SLACalcStatus slaCalc) {
937        if (expected != -1 && actual > expected) {
938            slaCalc.setEventStatus(EventStatus.DURATION_MISS);
939            eventHandler.queueEvent(new SLACalcStatus(slaCalc));
940        }
941        else if (expected != -1 && actual <= expected) {
942            slaCalc.setEventStatus(EventStatus.DURATION_MET);
943            eventHandler.queueEvent(new SLACalcStatus(slaCalc));
944        }
945    }
946
947    /*
948     * Confirm alerts against source of truth - DB. Also required in case of High Availability
949     */
950    private void confirmWithDB(SLACalcStatus slaCalc) {
951        boolean ended = false, isEndMiss = false;
952        try {
953            switch (slaCalc.getAppType()) {
954                case WORKFLOW_JOB:
955                    WorkflowJobBean wf = jpaService.execute(new WorkflowJobGetForSLAJPAExecutor(slaCalc.getId()));
956                    if (wf.getEndTime() != null) {
957                        ended = true;
958                        if (wf.getStatus() == WorkflowJob.Status.KILLED || wf.getStatus() == WorkflowJob.Status.FAILED
959                                || wf.getEndTime().getTime() > slaCalc.getExpectedEnd().getTime()) {
960                            isEndMiss = true;
961                        }
962                    }
963                    slaCalc.setActualStart(wf.getStartTime());
964                    slaCalc.setActualEnd(wf.getEndTime());
965                    slaCalc.setJobStatus(wf.getStatusStr());
966                    break;
967                case WORKFLOW_ACTION:
968                    WorkflowActionBean wa = jpaService.execute(new WorkflowActionGetForSLAJPAExecutor(slaCalc.getId()));
969                    if (wa.getEndTime() != null) {
970                        ended = true;
971                        if (wa.isTerminalWithFailure()
972                                || wa.getEndTime().getTime() > slaCalc.getExpectedEnd().getTime()) {
973                            isEndMiss = true;
974                        }
975                    }
976                    slaCalc.setActualStart(wa.getStartTime());
977                    slaCalc.setActualEnd(wa.getEndTime());
978                    slaCalc.setJobStatus(wa.getStatusStr());
979                    break;
980                case COORDINATOR_ACTION:
981                    CoordinatorActionBean ca = jpaService.execute(new CoordActionGetForSLAJPAExecutor(slaCalc.getId()));
982                    if (ca.isTerminalWithFailure()) {
983                        isEndMiss = ended = true;
984                        slaCalc.setActualEnd(ca.getLastModifiedTime());
985                    }
986                    if (ca.getExternalId() != null) {
987                        wf = jpaService.execute(new WorkflowJobGetForSLAJPAExecutor(ca.getExternalId()));
988                        if (wf.getEndTime() != null) {
989                            ended = true;
990                            if (wf.getEndTime().getTime() > slaCalc.getExpectedEnd().getTime()) {
991                                isEndMiss = true;
992                            }
993                        }
994                        slaCalc.setActualEnd(wf.getEndTime());
995                        slaCalc.setActualStart(wf.getStartTime());
996                    }
997                    slaCalc.setJobStatus(ca.getStatusStr());
998                    break;
999                default:
1000                    LOG.debug("Unsupported App-type for SLA - " + slaCalc.getAppType());
1001            }
1002
1003            byte eventProc = slaCalc.getEventProcessed();
1004            if (ended) {
1005                if (isEndMiss) {
1006                    slaCalc.setSLAStatus(SLAStatus.MISS);
1007                }
1008                else {
1009                    slaCalc.setSLAStatus(SLAStatus.MET);
1010                }
1011                if (slaCalc.getActualStart() != null) {
1012                    if ((eventProc & 1) == 0) {
1013                        if (slaCalc.getExpectedStart().getTime() < slaCalc.getActualStart().getTime()) {
1014                            slaCalc.setEventStatus(EventStatus.START_MISS);
1015                        }
1016                        else {
1017                            slaCalc.setEventStatus(EventStatus.START_MET);
1018                        }
1019                        eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1020                    }
1021                    slaCalc.setActualDuration(slaCalc.getActualEnd().getTime() - slaCalc.getActualStart().getTime());
1022                    if (((eventProc >> 1) & 1) == 0) {
1023                        processDurationSLA(slaCalc.getExpectedDuration(), slaCalc.getActualDuration(), slaCalc);
1024                    }
1025                }
1026                if (eventProc < 4) {
1027                    if (isEndMiss) {
1028                        slaCalc.setEventStatus(EventStatus.END_MISS);
1029                    }
1030                    else {
1031                        slaCalc.setEventStatus(EventStatus.END_MET);
1032                    }
1033                    eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1034                }
1035                slaCalc.setEventProcessed(8);
1036            }
1037            else {
1038                if (slaCalc.getActualStart() != null) {
1039                    slaCalc.setSLAStatus(SLAStatus.IN_PROCESS);
1040                }
1041                if ((eventProc & 1) == 0) {
1042                    if (slaCalc.getActualStart() != null) {
1043                        if (slaCalc.getExpectedStart().getTime() < slaCalc.getActualStart().getTime()) {
1044                            slaCalc.setEventStatus(EventStatus.START_MISS);
1045                        }
1046                        else {
1047                            slaCalc.setEventStatus(EventStatus.START_MET);
1048                        }
1049                        eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1050                        eventProc++;
1051                    }
1052                    else if (slaCalc.getExpectedStart().getTime() < System.currentTimeMillis()) {
1053                        slaCalc.setEventStatus(EventStatus.START_MISS);
1054                        eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1055                        eventProc++;
1056                    }
1057                }
1058                if (((eventProc >> 1) & 1) == 0 && slaCalc.getActualStart() != null
1059                        && slaCalc.getExpectedDuration() != -1) {
1060                    if (System.currentTimeMillis() - slaCalc.getActualStart().getTime() > slaCalc.getExpectedDuration()) {
1061                        slaCalc.setEventStatus(EventStatus.DURATION_MISS);
1062                        eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1063                        eventProc += 2;
1064                    }
1065                }
1066                if (eventProc < 4 && slaCalc.getExpectedEnd().getTime() < System.currentTimeMillis()) {
1067                    slaCalc.setEventStatus(EventStatus.END_MISS);
1068                    slaCalc.setSLAStatus(SLAStatus.MISS);
1069                    eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1070                    eventProc += 4;
1071                }
1072                slaCalc.setEventProcessed(eventProc);
1073            }
1074        }
1075        catch (Exception e) {
1076            LOG.warn("Error while confirming SLA against DB for jobid= " + slaCalc.getId() + ". Exception is "
1077                    + e.getClass().getName() + ": " + e.getMessage());
1078            if (slaCalc.getEventProcessed() < 4 && slaCalc.getExpectedEnd().getTime() < System.currentTimeMillis()) {
1079                slaCalc.setEventStatus(EventStatus.END_MISS);
1080                slaCalc.setSLAStatus(SLAStatus.MISS);
1081                eventHandler.queueEvent(new SLACalcStatus(slaCalc));
1082                slaCalc.setEventProcessed(slaCalc.getEventProcessed() + 4);
1083            }
1084        }
1085    }
1086
1087    @VisibleForTesting
1088    public boolean isJobIdInSLAMap(String jobId) {
1089        return this.slaMap.containsKey(jobId);
1090    }
1091
1092    @VisibleForTesting
1093    public boolean isJobIdInHistorySet(String jobId) {
1094        return this.historySet.contains(jobId);
1095    }
1096
1097    private void setLogPrefix(String jobId) {
1098        LOG = LogUtils.setLogInfo(LOG, jobId, null, null);
1099    }
1100}