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());
}
}
}