package com.thinkbiganalytics.metadata.sla; /*- * #%L * thinkbig-operational-metadata-integration-service * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.thinkbiganalytics.cluster.ClusterMessage; import com.thinkbiganalytics.cluster.ClusterService; import com.thinkbiganalytics.cluster.ClusterServiceMessageReceiver; import com.thinkbiganalytics.metadata.api.MetadataAccess; import com.thinkbiganalytics.metadata.api.PostMetadataConfigAction; import com.thinkbiganalytics.metadata.modeshape.sla.JcrServiceLevelAgreement; import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement; import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementChecker; import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider; import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementScheduler; import com.thinkbiganalytics.scheduler.JobIdentifier; import com.thinkbiganalytics.scheduler.JobScheduler; import com.thinkbiganalytics.scheduler.JobSchedulerEvent; import com.thinkbiganalytics.scheduler.JobSchedulerException; import com.thinkbiganalytics.scheduler.QuartzScheduler; import com.thinkbiganalytics.scheduler.TriggerIdentifier; import com.thinkbiganalytics.scheduler.model.DefaultJobIdentifier; import com.thinkbiganalytics.scheduler.model.DefaultTriggerIdentifier; import org.apache.commons.lang3.StringUtils; import org.quartz.SchedulerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; /** * Provides the default implementation for service level agreement scheduling. */ public class DefaultServiceLevelAgreementScheduler implements ServiceLevelAgreementScheduler, PostMetadataConfigAction, ClusterServiceMessageReceiver { private static final Logger log = LoggerFactory.getLogger(DefaultServiceLevelAgreementScheduler.class); public static String QTZ_JOB_SCHEDULED_MESSAGE_TYPE = "QTZ_JOB_SCHEDULED"; public static String QTZ_JOB_UNSCHEDULED_MESSAGE_TYPE = "QTZ_JOB_UNSCHEDULED"; @Inject ServiceLevelAgreementProvider slaProvider; private String DEFAULT_CRON = "0 0/5 * 1/1 * ? *";// every 5 min @Value("${sla.cron.default:0 0/5 * 1/1 * ? *}") private String defaultCron; @Inject private JobScheduler jobScheduler; @Inject private ServiceLevelAgreementChecker slaChecker; @Inject private MetadataAccess metadataAccess; @Inject private ClusterService clusterService; private Map<ServiceLevelAgreement.ID, String> scheduledJobNames = new ConcurrentHashMap<>(); /** * Called on startup as part of the PostMetadataConfigAction. * */ @Override public void run() { metadataAccess.read(() -> { List<? extends ServiceLevelAgreement> agreements = slaProvider.getAgreements(); if (agreements != null) { for (ServiceLevelAgreement agreement : agreements) { JobIdentifier jobIdentifier = slaJobName(agreement); QuartzScheduler scheduler = (QuartzScheduler)jobScheduler; if(!scheduler.jobExists(jobIdentifier)) { scheduleServiceLevelAgreement(agreement); } } } return null; }, MetadataAccess.SERVICE); } private String getUniqueName(String name) { String uniqueName = name; final String checkName = name; String matchingName = Iterables.tryFind(scheduledJobNames.values(), new Predicate<String>() { @Override public boolean apply(String s) { return s.equalsIgnoreCase(checkName); } }).orNull(); if (matchingName != null) { //get numeric string after '-'; if (StringUtils.contains(matchingName, "-")) { String number = StringUtils.substringAfterLast(matchingName, "-"); if (StringUtils.isNotBlank(number)) { number = StringUtils.trim(number); if (StringUtils.isNumeric(number)) { Integer num = Integer.parseInt(number); num++; uniqueName += "-" + num; } else { uniqueName += "-1"; } } } else { uniqueName += "-1"; } } return uniqueName; } /** * removes a SLA from the scheduler, so that it is no longer executed. * * @param sla The Service Level Agreement object * @return true if we were able to remove the SLA from the scheduler */ public boolean unscheduleServiceLevelAgreement(ServiceLevelAgreement sla) { boolean unscheduled = false; if (sla != null) { unscheduled = unscheduleServiceLevelAgreement(sla.getId()); } return unscheduled; } /** * removes the SLA, identified by slaId, from the scheduler, so that it is no longer executed. * * @param slaId The ServiceLevelAgreement id * @return true if we were able to remove the SLA from the scheduler */ public boolean unscheduleServiceLevelAgreement(ServiceLevelAgreement.ID slaId) { boolean unscheduled = false; JobIdentifier scheduledJobId = null; try { if (scheduledJobNames.containsKey(slaId)) { scheduledJobId = jobIdentifierForName(scheduledJobNames.get(slaId)); log.debug("Unscheduling sla job " + scheduledJobId.getName()); jobScheduler.deleteJob(scheduledJobId); scheduledJobNames.remove(slaId); unscheduled = true; if(clusterService.isClustered()) { clusterService.sendMessageToOthers(QTZ_JOB_UNSCHEDULED_MESSAGE_TYPE,new ScheduledServiceLevelAgreementClusterMessage(slaId,scheduledJobId)); } } } catch (JobSchedulerException e) { log.error("Unable to delete the SLA Job " + scheduledJobId); } return unscheduled; } private JobIdentifier slaJobName(ServiceLevelAgreement sla) { String name = sla.getName(); if (scheduledJobNames.containsKey(sla.getId())) { name = scheduledJobNames.get(sla.getId()); } else { //ensure the name is unique in the saved ist name = getUniqueName(name); } return jobIdentifierForName(name); } private JobIdentifier jobIdentifierForName(String name) { JobIdentifier jobIdentifier = new DefaultJobIdentifier(name, "SLA"); return jobIdentifier; } private TriggerIdentifier triggerIdentifier(JobIdentifier jobIdentifier) { TriggerIdentifier triggerIdentifier = new DefaultTriggerIdentifier(jobIdentifier.getName(), jobIdentifier.getGroup()); return triggerIdentifier; } private TriggerIdentifier triggerIdentifier(String name) { TriggerIdentifier triggerIdentifier = new DefaultTriggerIdentifier(name, "SLA"); return triggerIdentifier; } private ServiceLevelAgreement.ID slaIdForJobIdentifier(JobIdentifier jobIdentifier) { String jobIdentifierName = jobIdentifier.getName(); return scheduledJobNames.entrySet().stream().filter(entry -> entry.getValue().equalsIgnoreCase(jobIdentifierName)).map(entry -> entry.getKey()).findFirst().orElse(null); } /** * Used to disable the schedule of the SLA, so that it no longer executes until subsequently re-enabled * * @param sla The SLA to disable */ public void disableServiceLevelAgreement(ServiceLevelAgreement sla) { ServiceLevelAgreement.ID slaId = sla.getId(); if (scheduledJobNames.containsKey(slaId)) { JobIdentifier scheduledJobId = jobIdentifierForName(scheduledJobNames.get(slaId)); try { jobScheduler.pauseTriggersOnJob(scheduledJobId); } catch (JobSchedulerException e) { log.error("Unable to pause the schedule for the disabled SLA {} ", sla.getName()); } } } /** * Used to enable the schedule of the SLA, so that once again executes after a being disabled * * @param sla The SLA to enable */ public void enableServiceLevelAgreement(ServiceLevelAgreement sla) { ServiceLevelAgreement.ID slaId = sla.getId(); if (scheduledJobNames.containsKey(slaId)) { JobIdentifier scheduledJobId = jobIdentifierForName(scheduledJobNames.get(slaId)); try { jobScheduler.resumeTriggersOnJob(scheduledJobId); } catch (JobSchedulerException e) { log.error("Unable to resume the schedule for the enabled SLA {} ", sla.getName()); } } } /** * Schedule the SlaQuartzJobBean for the given sla * @param jobIdentifier the job identifier for this schedule * @param slaId the SLA id */ private void scheduleSlaJob(JobIdentifier jobIdentifier, ServiceLevelAgreement.ID slaId){ QuartzScheduler scheduler = (QuartzScheduler)jobScheduler; TriggerIdentifier triggerIdentifier =triggerIdentifier(jobIdentifier); Map<String,Object> map = new HashMap<String,Object>(); map.put(SlaQuartzJobBean.SLA_ID_PARAM,slaId); try { scheduler.scheduleJob(jobIdentifier, triggerIdentifier, SlaQuartzJobBean.class, (StringUtils.isBlank(defaultCron) ? DEFAULT_CRON : defaultCron), map); }catch(SchedulerException e) { e.printStackTrace(); } } /** * Schedules an SLA to be run * * @param sla The SLA to schedule */ public void scheduleServiceLevelAgreement(ServiceLevelAgreement sla) { if (scheduledJobNames.containsKey(sla.getId())) { unscheduleServiceLevelAgreement(sla); } JobIdentifier jobIdentifier = slaJobName(sla); ServiceLevelAgreement.ID slaId = sla.getId(); //schedule the job scheduleSlaJob(jobIdentifier,slaId); log.debug("Schedule sla job " + jobIdentifier.getName()); scheduledJobNames.put(sla.getId(), jobIdentifier.getName()); //notify the other schedulers in the cluster of the scheduled job name if(clusterService.isClustered()) { clusterService.sendMessageToOthers(QTZ_JOB_SCHEDULED_MESSAGE_TYPE, new ScheduledServiceLevelAgreementClusterMessage(slaId, jobIdentifier)); } if (!sla.isEnabled()) { disableServiceLevelAgreement(sla); } } /** * Called be the framework when the job is scheduled this is where we manage the life cycle of the SLAs * * @param event the job event. */ public void onJobSchedulerEvent(JobSchedulerEvent event) { try { switch (event.getEvent()) { case PAUSE_JOB: pauseServiceLevelAgreement(event); break; case RESUME_JOB: resumeServiceLevelAgreement(event); break; case PAUSE_ALL_JOBS: pauseAllServiceLevelAgreements(); break; case RESUME_ALL_JOBS: resumeAllServiceLevelAgreements(); break; default: break; } } catch (Exception e) { log.error("Error processing JobScheduler Event {}", event, e); } } private void resumeServiceLevelAgreement(JobSchedulerEvent event) { ServiceLevelAgreement.ID slaId = slaIdForJobIdentifier(event.getJobIdentifier()); if (slaId != null) { metadataAccess.commit(() -> enable(slaId), MetadataAccess.SERVICE); } } private void pauseServiceLevelAgreement(JobSchedulerEvent event) { ServiceLevelAgreement.ID slaId = slaIdForJobIdentifier(event.getJobIdentifier()); if (slaId != null) { metadataAccess.commit(() -> disable(slaId), MetadataAccess.SERVICE); } } private void pauseAllServiceLevelAgreements() { metadataAccess.commit(() -> { scheduledJobNames.keySet().stream().forEach(slaId -> disable(slaId)); }, MetadataAccess.SERVICE); } private void resumeAllServiceLevelAgreements() { metadataAccess.commit(() -> { scheduledJobNames.keySet().stream().forEach(slaId -> enable(slaId)); }, MetadataAccess.SERVICE); } private void enable(ServiceLevelAgreement.ID slaId) { findAgreement(slaId).ifPresent(sla -> ((JcrServiceLevelAgreement) sla).setEnabled(true)); } private void disable(ServiceLevelAgreement.ID slaId) { findAgreement(slaId).ifPresent(sla -> ((JcrServiceLevelAgreement) sla).setEnabled(false)); } /** * Must be called inside a metadatAccess wrapper */ private Optional<ServiceLevelAgreement> findAgreement(ServiceLevelAgreement.ID slaId) { ServiceLevelAgreement sla = slaProvider.getAgreement(slaId); return Optional.ofNullable(sla); } /** * Keep the job name cache in sync across clusters * @param from cluser address sending the message * @param message the message */ @Override public void onMessageReceived(String from, ClusterMessage message) { if(QTZ_JOB_SCHEDULED_MESSAGE_TYPE.equalsIgnoreCase(message.getType())){ ScheduledServiceLevelAgreementClusterMessage msg = (ScheduledServiceLevelAgreementClusterMessage) message.getMessage(); scheduledJobNames.put(msg.getSlaId(),msg.getJobIdentifier().getName()); } else if(QTZ_JOB_UNSCHEDULED_MESSAGE_TYPE.equalsIgnoreCase(message.getType())) { ScheduledServiceLevelAgreementClusterMessage msg = (ScheduledServiceLevelAgreementClusterMessage) message.getMessage(); scheduledJobNames.remove(msg.getSlaId()); } } }