package me.hao0.antares.server.schedule;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import me.hao0.antares.common.dto.JobDetail;
import me.hao0.antares.common.dto.JobFireTime;
import me.hao0.antares.common.exception.JobStateTransferInvalidException;
import me.hao0.antares.common.log.Logs;
import me.hao0.antares.common.model.enums.JobState;
import me.hao0.antares.common.model.enums.JobTriggerType;
import me.hao0.antares.common.support.Lifecycle;
import me.hao0.antares.common.support.Component;
import me.hao0.antares.common.util.CollectionUtil;
import me.hao0.antares.common.util.Crons;
import me.hao0.antares.server.cluster.server.ServerHost;
import me.hao0.antares.server.schedule.executor.JobExecutor;
import me.hao0.antares.store.util.Dates;
import me.hao0.antares.server.support.Springs;
import me.hao0.antares.store.service.JobService;
import me.hao0.antares.store.support.JobSupport;
import me.hao0.antares.common.util.Response;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
/**
* The job pool
* Author: haolin
* Email: haolin.h0@gmail.com
*/
@org.springframework.stereotype.Component
public class JobPool extends Component implements Lifecycle, InitializingBean, DisposableBean {
@Autowired
private JobService jobService;
@Autowired
private ServerHost serverHost;
@Autowired
private JobSupport jobSupport;
@Autowired
private Springs springs;
@Value("${antares.scheduleThreadCount:32}")
private Integer scheduleThreadCount;
/**
* The cached job details
*/
private final Map<String, Map<String, JobDetail>> cacheJobs = Maps.newConcurrentMap();
/**
* The zk jobs
*/
private final Map<String, Map<String, ZkJob>> zkJobs = Maps.newConcurrentMap();
/**
* The scheduler factory
*/
private SchedulerFactory schedulers;
/**
* Get app names
* @return the app names
*/
public Map<String, Map<String, JobDetail>> getJobs(){
return ImmutableMap.copyOf(cacheJobs);
}
/**
* Get the job detail
* @param appName the app name
* @param jobClass the job class
* @return the job detail
*/
public JobDetail getJobDetail(String appName, String jobClass){
return cacheJobs.get(appName).get(jobClass);
}
/**
* Get the job details of the app
* @param appName the app name
* @return the job details of the app
*/
public List<JobDetail> getJobDetails(String appName){
return ImmutableList.copyOf(cacheJobs.get(appName).values());
}
/**
* Get the apps
* @return the apps
*/
public List<String> getApps(){
return ImmutableList.copyOf(cacheJobs.keySet());
}
@Override
public void afterPropertiesSet() throws Exception {
start();
}
@Override
public void doStart() {
// init the scheduler
try {
Properties properties = new Properties();
properties.setProperty("org.quartz.threadPool.threadCount", scheduleThreadCount + "");
properties.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
schedulers = new StdSchedulerFactory(properties);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
// load my jobs
Response<List<JobDetail>> jobDetailsResp = jobService.findValidJobsByServer(serverHost.get());
if (!jobDetailsResp.isSuccess()){
Logs.error("failed to load server({})'s jobs, cause: {}", serverHost.get(), jobDetailsResp.getErr());
}
try {
// initialize all zk jobs
scheduleJobs(jobDetailsResp.getData());
// start the scheduler
schedulers.getScheduler().start();
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
private void scheduleJobs(List<JobDetail> jobDetails) {
if (CollectionUtil.isNullOrEmpty(jobDetails)){
return;
}
// init all quartz jobs
for (JobDetail jobDetail : jobDetails){
scheduleJob(jobDetail);
}
}
private Boolean scheduleJob(JobDetail jobDetail) {
try {
if (!Crons.isValidExpression(jobDetail.getJob().getCron())){
Logs.warn("The job({})'s cron expression is invalid when schedule", jobDetail);
return Boolean.FALSE;
}
if(createQuartzJob(jobDetail)){
createCacheJob(jobDetail);
createZkJob(jobDetail);
return Boolean.TRUE;
}
return Boolean.FALSE;
} catch (SchedulerException e) {
Logs.error("failed to schedule job({}), cause: {}",
jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
private void createCacheJob(JobDetail jobDetail) {
String appName = jobDetail.getApp().getAppName();
Map<String, JobDetail> appJobs = cacheJobs.get(appName);
if (CollectionUtil.isNullOrEmpty(appJobs)){
appJobs = Maps.newConcurrentMap();
cacheJobs.put(appName, appJobs);
}
String jobClazz = jobDetail.getJob().getClazz();
appJobs.remove(jobClazz);
appJobs.put(jobClazz, jobDetail);
}
private Boolean createQuartzJob(JobDetail jobDetail) throws SchedulerException {
JobKey jobKey = buildJobKey(jobDetail);
// one scheduler for one app
Scheduler scheduler = schedulers.getScheduler();
// inject the executor
JobDataMap jobData = buildJobData(jobDetail, JobTriggerType.DEFAULT);
org.quartz.JobDetail quartzJob = newJob(JobAgent.class)
.withIdentity(jobKey)
.usingJobData(jobData)
.build();
CronScheduleBuilder scheduleBuilder = cronSchedule(jobDetail.getJob().getCron());
if (jobDetail.getConfig().getMisfire()){
// ignore all misfired
scheduleBuilder.withMisfireHandlingInstructionDoNothing();
} else {
scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();
}
Trigger trigger = newTrigger()
.withIdentity(buildTriggerKey(jobDetail))
.withSchedule(scheduleBuilder)
.build();
// start scheduling the job
scheduler.scheduleJob(quartzJob, trigger);
return Boolean.TRUE;
}
private JobDataMap buildJobData(JobDetail jobDetail, JobTriggerType triggerType) {
JobDataMap jobData = new JobDataMap();
jobData.put("executor", springs.getBean(JobExecutor.class));
jobData.put("jobDetail", jobDetail);
jobData.put("triggerType", triggerType);
return jobData;
}
private void createZkJob(JobDetail jobDetail) {
try {
Scheduler scheduler = schedulers.getScheduler();
Trigger trigger = scheduler.getTrigger(buildTriggerKey(jobDetail));
ZkJob zkJob = new ZkJob(jobSupport, jobDetail, serverHost.get(), buildJobFireTime(trigger));
zkJob.start();
} catch (Exception e){
Logs.error("failed to create zk job({}), cause: {}.",
jobDetail, Throwables.getStackTraceAsString(e));
}
}
private JobFireTime buildJobFireTime(Trigger trigger) {
if (trigger == null) {
return null;
}
JobFireTime fireTime = new JobFireTime();
if (trigger.getPreviousFireTime() != null){
fireTime.setPrev(Dates.format(trigger.getPreviousFireTime()));
}
if (trigger.getNextFireTime() != null){
fireTime.setNext(Dates.format(trigger.getNextFireTime()));
}
return fireTime;
}
public Boolean scheduleJob(Long jobId) {
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find job detail when schedule job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
scheduleJob(findResp.getData());
return Boolean.TRUE;
}
/**
* Trigger the job immediately
* @param jobId the job id
* @param triggerType the trigger type
* @see JobTriggerType
*/
public Boolean triggerJob(Long jobId, JobTriggerType triggerType){
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find job detail when trigger job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
return triggerJob(findResp.getData(), triggerType);
}
public Boolean triggerJob(JobDetail jobDetail, JobTriggerType triggerType) {
try {
JobKey jobKey = buildJobKey(jobDetail);
Scheduler scheduler = schedulers.getScheduler();
if(!scheduler.checkExists(jobKey)){
Logs.warn("The job({}) doesn't exists when trigger job.", jobDetail);
return Boolean.TRUE;
}
scheduler.triggerJob(jobKey, buildJobData(jobDetail, triggerType));
return Boolean.TRUE;
} catch (SchedulerException e) {
Logs.error("failed to trigger job({}), cause: {}.", jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
/**
* Pause the job
* @param jobId the job id
*/
public Boolean pauseJob(Long jobId){
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find job detail when pause job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
return pauseJob(findResp.getData());
}
public Boolean pauseJob(JobDetail jobDetail){
try {
jobSupport.updateJobStateDirectly(
jobDetail.getApp().getAppName(),
jobDetail.getJob().getClazz(), JobState.PAUSED);
JobKey jobKey = buildJobKey(jobDetail);
Scheduler scheduler = schedulers.getScheduler();
if(!scheduler.checkExists(jobKey)){
Logs.warn("The job({}) doesn't exists when pause job.", jobDetail);
return Boolean.TRUE;
}
scheduler.pauseJob(jobKey);
return Boolean.TRUE;
} catch (JobStateTransferInvalidException e){
Logs.warn("failed to transfer job({}) state when pause job: {}", jobDetail, e.toString());
return Boolean.FALSE;
} catch (Exception e) {
Logs.error("failed to pause job({}), cause: {}.", jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
/**
* Resume the job schedule
* @param jobId the job id
*/
public Boolean resumeJob(Long jobId){
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find job detail when resume job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
return resumeJob(findResp.getData());
}
public Boolean resumeJob(JobDetail jobDetail){
try {
String appName = jobDetail.getApp().getAppName();
String jobClass = jobDetail.getJob().getClazz();
jobSupport.updateJobStateDirectly(appName, jobClass, JobState.WAITING);
JobKey jobKey = buildJobKey(appName, jobClass);
Scheduler scheduler = schedulers.getScheduler();
if(!scheduler.checkExists(jobKey)){
Logs.warn("The job({}) doesn't exists when resume job.", jobDetail);
return Boolean.TRUE;
}
scheduler.resumeJob(jobKey);
// update job fire time info
JobFireTime jobFireTime = buildJobFireTime(scheduler.getTrigger(buildTriggerKey(appName, jobClass)));
if (jobFireTime != null){
jobSupport.updateJobFireTime(appName, jobClass, jobFireTime);
}
return Boolean.TRUE;
} catch (JobStateTransferInvalidException e){
Logs.warn("failed to transfer the job({}) state when resume job: {}", jobDetail, e.toString());
return Boolean.FALSE;
} catch (Exception e) {
Logs.error("failed to resume the job({}), cause: {}.", jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
/**
* Remove the job from the pool, and will delete the job from zk
* @param jobId the job id
*/
public Boolean removeJob(Long jobId){
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find the job detail when remove job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
if (removeJob(findResp.getData())){
// unbind the job from the server
Response<Boolean> unbindResp = jobService.unbindJobServer(serverHost.get(), jobId);
if (!unbindResp.isSuccess() || !unbindResp.getData()){
return Boolean.FALSE;
}
return Boolean.TRUE;
}
return Boolean.FALSE;
}
/**
* Remove the job from the pool, and will delete the job from zk
* @param jobDetail the job detail
*/
public Boolean removeJob(JobDetail jobDetail){
try {
JobKey jobKey = buildJobKey(jobDetail);
Scheduler scheduler = schedulers.getScheduler();
if(!scheduler.checkExists(jobKey)){
Logs.warn("The job({}) doesn't exists when remove job.", jobDetail);
return Boolean.TRUE;
}
// delete schedule
scheduler.deleteJob(jobKey);
// delete the job from zk
jobSupport.removeJob(jobDetail);
return Boolean.TRUE;
} catch (Exception e) {
Logs.error("failed to remove the job({}), cause: {}.", jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
/**
* Reload the job to be scheduled
* @param jobId the job id
* @return return true if reload successfully, or not
*/
public Boolean reloadJob(Long jobId) {
Response<JobDetail> findResp = jobService.findJobDetailById(jobId);
if (!findResp.isSuccess()){
Logs.error("failed to find job detail when remove job(id={}), cause: {}", jobId, findResp.getErr());
return Boolean.TRUE;
}
return reloadJob(findResp.getData());
}
/**
* Reload the job to be scheduled
* @param jobDetail the job detail
* @return return true if reload successfully, or not
*/
private Boolean reloadJob(JobDetail jobDetail) {
try {
JobKey jobKey = buildJobKey(jobDetail);
Scheduler scheduler = schedulers.getScheduler();
if(!scheduler.checkExists(jobKey)){
Logs.warn("The job({}) doesn't exists when remove job.", jobDetail);
return Boolean.TRUE;
}
// reload the job
scheduler.deleteJob(jobKey);
if(createQuartzJob(jobDetail)){
createCacheJob(jobDetail);
}
return Boolean.TRUE;
} catch (Exception e) {
Logs.error("failed to reload the job({}), cause: {}.", jobDetail, Throwables.getStackTraceAsString(e));
return Boolean.FALSE;
}
}
private JobKey buildJobKey(JobDetail jobDetail) {
return buildJobKey(jobDetail.getApp().getAppName(), jobDetail.getJob().getClazz());
}
private JobKey buildJobKey(String appName, String jobClass){
return JobKey.jobKey(jobClass, appName);
}
private TriggerKey buildTriggerKey(JobDetail jobDetail) {
return buildTriggerKey(jobDetail.getApp().getAppName(), jobDetail.getJob().getClazz());
}
private TriggerKey buildTriggerKey(String appName, String jobClass){
return TriggerKey.triggerKey(jobClass, appName);
}
@Override
public void doShutdown() {
// shutdown all jobs
if (!zkJobs.isEmpty()){
for (Map<String, ZkJob> zkJobMap : zkJobs.values()){
for (ZkJob zkJob : zkJobMap.values()){
zkJob.shutdown();
}
}
zkJobs.clear();
}
}
@Override
public void destroy() throws Exception {
shutdown();
}
}