org.lastaflute.job.cron4j.Cron4jJob Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lasta-job Show documentation
Show all versions of lasta-job Show documentation
Job Scheduling for LastaFlute
/*
* Copyright 2015-2017 the original author or authors.
*
* 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.
*/
package org.lastaflute.job.cron4j;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.dbflute.optional.OptionalThing;
import org.dbflute.optional.OptionalThingIfPresentAfter;
import org.dbflute.util.DfTypeUtil;
import org.lastaflute.job.LaJob;
import org.lastaflute.job.LaJobHistory;
import org.lastaflute.job.LaScheduledJob;
import org.lastaflute.job.exception.JobAlreadyDisappearedException;
import org.lastaflute.job.exception.JobAlreadyUnscheduleException;
import org.lastaflute.job.exception.JobTriggeredNotFoundException;
import org.lastaflute.job.key.LaJobKey;
import org.lastaflute.job.key.LaJobNote;
import org.lastaflute.job.key.LaJobUnique;
import org.lastaflute.job.log.JobChangeLog;
import org.lastaflute.job.log.JobNoticeLogLevel;
import org.lastaflute.job.subsidiary.CronOption;
import org.lastaflute.job.subsidiary.CronParamsSupplier;
import org.lastaflute.job.subsidiary.JobConcurrentExec;
import org.lastaflute.job.subsidiary.LaunchNowOpCall;
import org.lastaflute.job.subsidiary.LaunchNowOption;
import org.lastaflute.job.subsidiary.LaunchedProcess;
import org.lastaflute.job.subsidiary.NeighborConcurrentGroup;
import org.lastaflute.job.subsidiary.SnapshotExecState;
import org.lastaflute.job.subsidiary.VaryingCronOpCall;
import org.lastaflute.job.subsidiary.VaryingCronOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import it.sauronsoftware.cron4j.TaskExecutor;
/**
* @author jflute
* @since 0.2.0 (2016/01/11 Monday)
*/
public class Cron4jJob implements LaScheduledJob {
// ===================================================================================
// Definition
// ==========
private static final Logger logger = LoggerFactory.getLogger(Cron4jJob.class);
// ===================================================================================
// Attribute
// =========
protected final LaJobKey jobKey;
protected final OptionalThing jobNote;
protected final OptionalThing jobUnique;
protected volatile OptionalThing cron4jId; // mutable for non-cron
protected final Cron4jTask cron4jTask; // 1:1
protected final Cron4jNow cron4jNow; // n:1
protected volatile boolean unscheduled;
protected volatile boolean disappeared;
protected Set triggeredJobKeyList; // null allowed if no next trigger, used in synchronized
protected final Object triggeredJobKeyLock = new Object(); // for minimum lock scope to avoid deadlock
// these are same life-cycle (list to keep order for machine-gun synchronization)
protected Map neighborConcurrentGroupMap; // null allowed if no neighbor
protected List neighborConcurrentGroupList; // null allowed if no neighbor
// ===================================================================================
// Constructor
// ===========
public Cron4jJob(LaJobKey jobKey, OptionalThing jobNote, OptionalThing jobUnique,
OptionalThing cron4jId, Cron4jTask cron4jTask, Cron4jNow cron4jNow) {
this.jobKey = jobKey;
this.jobNote = jobNote;
this.jobUnique = jobUnique;
this.cron4jId = cron4jId;
this.cron4jTask = cron4jTask;
this.cron4jNow = cron4jNow;
}
// ===================================================================================
// Executing Now
// =============
@Override
public OptionalThingIfPresentAfter ifExecutingNow(Consumer oneArgLambda) {
return mapExecutingNow(execState -> {
oneArgLambda.accept(execState);
return (OptionalThingIfPresentAfter) (processor -> {});
}).orElseGet(() -> {
return processor -> processor.process();
});
}
@Override
public boolean isExecutingNow() {
return cron4jTask.isRunningNow();
}
@Override
public OptionalThing mapExecutingNow(Function oneArgLambda) {
final OptionalThing beginTime = cron4jTask.syncRunningCall(runningState -> {
return runningState.getBeginTime().get(); // locked so can get() safely
});
return beginTime.flatMap(time -> {
return OptionalThing.ofNullable(oneArgLambda.apply(new SnapshotExecState(time)), () -> {
throw new IllegalStateException("Not found the result from your scope: job=" + toIdentityDisp() + "(" + time + ")");
});
});
}
// ===================================================================================
// Launch Now
// ==========
@Override
public synchronized LaunchedProcess launchNow() {
return doLaunchNow(op -> {});
}
@Override
public synchronized LaunchedProcess launchNow(LaunchNowOpCall opLambda) {
return doLaunchNow(opLambda);
}
protected LaunchedProcess doLaunchNow(LaunchNowOpCall opLambda) {
verifyCanScheduleState();
final LaunchNowOption option = createLaunchNowOption(opLambda);
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Launching now: {}, {}", option, this);
}
// if executed by cron here, duplicate execution occurs but task level synchronization exists
final TaskExecutor taskExecutor = cron4jNow.getCron4jScheduler().launchNow(cron4jTask, option);
return createLaunchedProcess(taskExecutor);
}
protected LaunchNowOption createLaunchNowOption(LaunchNowOpCall opLambda) {
final LaunchNowOption op = new LaunchNowOption();
opLambda.callback(op);
return op;
}
protected LaunchedProcess createLaunchedProcess(TaskExecutor taskExecutor) {
return new LaunchedProcess(this, () -> joinJobThread(taskExecutor), () -> findJobHistory(taskExecutor));
}
protected void joinJobThread(TaskExecutor taskExecutor) {
try {
taskExecutor.join();
} catch (InterruptedException e) {
String msg = "The current thread has been interrupted while join: taskExecutor=" + taskExecutor + ", job=" + this;
throw new IllegalStateException(msg, e);
}
}
protected OptionalThing findJobHistory(TaskExecutor taskExecutor) {
return Cron4jJobHistory.find(taskExecutor);
}
// ===================================================================================
// Stop Now
// ========
@Override
public synchronized void stopNow() { // can be called if unscheduled, disappeared, so don't use mutable variable
verifyCanStopState();
final List executorList = findNativeExecutorList();
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Stopping {} execution(s) now: {}", executorList.size(), toString());
}
if (!executorList.isEmpty()) {
executorList.forEach(executor -> executor.stop());
}
}
protected List findNativeExecutorList() {
return cron4jNow.getCron4jScheduler().findExecutorList(cron4jTask);
}
// ===================================================================================
// Reschedule
// ==========
@Override
public synchronized void reschedule(String cronExp, VaryingCronOpCall opLambda) {
verifyCanRescheduleState();
assertArgumentNotNull("cronExp", cronExp);
assertArgumentNotNull("opLambda", opLambda);
if (isNonCromExp(cronExp)) {
throw new IllegalArgumentException("The cronExp for reschedule() should not be non-cron: " + toString());
}
if (unscheduled) {
unscheduled = false; // can revive from unscheduled
}
final String existingCronExp = cron4jTask.getVaryingCron().getCronExp();
cron4jTask.switchCron(cronExp, createCronOption(opLambda));
final Cron4jScheduler cron4jScheduler = cron4jNow.getCron4jScheduler();
cron4jId.ifPresent(id -> {
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Rescheduling {} as cron from '{}' to '{}'", jobKey, existingCronExp, cronExp);
}
if (isNativeScheduledId(cron4jScheduler, id)) {
cron4jScheduler.reschedule(id, cronExp);
} else { // after descheduled
cron4jId = scheduleNative(cronExp, cron4jScheduler);
}
}).orElse(() -> {
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Rescheduling {} as cron from non-cron to '{}'", jobKey, cronExp);
}
cron4jId = scheduleNative(cronExp, cron4jScheduler);
});
}
protected boolean isNonCromExp(String cronExp) {
return Cron4jCron.isNonCronExp(cronExp);
}
protected VaryingCronOption createCronOption(VaryingCronOpCall opLambda) {
final VaryingCronOption option = new CronOption();
opLambda.callback(option);
return option;
}
protected boolean isNativeScheduledId(Cron4jScheduler cron4jScheduler, Cron4jId id) {
return cron4jScheduler.getNativeScheduler().getTask(id.value()) != null;
}
protected OptionalThing scheduleNative(String cronExp, Cron4jScheduler cron4jScheduler) {
final String generatedId = cron4jScheduler.schedule(cronExp, cron4jTask);
return OptionalThing.of(Cron4jId.of(generatedId));
}
// ===================================================================================
// Unschedule
// ==========
@Override
public synchronized void unschedule() {
verifyCanUnscheduleState();
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Unscheduling {}", toString());
}
unscheduled = true;
cron4jId.ifPresent(id -> {
cron4jNow.getCron4jScheduler().deschedule(id);
});
}
@Override
public boolean isUnscheduled() {
return unscheduled;
}
// ===================================================================================
// Disappear
// =========
@Override
public synchronized void disappear() {
verifyCanDisappearState();
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Disappearing {}", toString());
}
disappeared = true; // should be before clearing
cron4jId.ifPresent(id -> {
cron4jNow.getCron4jScheduler().deschedule(id);
});
cron4jNow.clearDisappearedJob(); // immediately clear, executing process is kept
}
@Override
public boolean isDisappeared() {
return disappeared;
}
// ===================================================================================
// Non-Cron
// ========
@Override
public synchronized void becomeNonCron() {
verifyCanScheduleState();
if (JobChangeLog.isEnabled()) {
JobChangeLog.log("#job ...Becoming non-cron: {}", toString());
}
cron4jId.ifPresent(id -> {
cron4jTask.becomeNonCrom();
cron4jNow.getCron4jScheduler().deschedule(id);
cron4jId = OptionalThing.empty();
});
}
@Override
public boolean isNonCron() {
return !cron4jId.isPresent();
}
// ===================================================================================
// Next Trigger
// ============
@Override
public void registerNext(LaJobKey triggeredJobKey) { // uses triggered lock instead of synchronize
verifyCanScheduleState();
assertArgumentNotNull("triggeredJobKey", triggeredJobKey);
// lazy check for initialization logic
//if (!cron4jNow.findJobByKey(triggeredJobKey).isPresent()) {
// throw new IllegalArgumentException("Not found the job by the job key: " + triggeredJobKey);
//}
if (triggeredJobKey.equals(jobKey)) { // myself
throw new IllegalArgumentException("Cannot register myself job as next trigger: " + toIdentityDisp());
}
synchronized (triggeredJobKeyLock) {
if (triggeredJobKeyList == null) {
triggeredJobKeyList = new CopyOnWriteArraySet(); // just in case
}
triggeredJobKeyList.add(triggeredJobKey);
}
}
public void triggerNext() { // called in execution (at framework), so cannot synchronize with this
// needs to be able to execute even if unscheduled
// because job process that is already executed can be success
// (and this method is for framework so no worry about user call)
//verifyCanScheduleState();
synchronized (triggeredJobKeyLock) {
if (triggeredJobKeyList == null) {
return;
}
final List triggeredJobList = triggeredJobKeyList.stream().map(triggeredJobKey -> {
return findTriggeredJob(triggeredJobKey);
}).collect(Collectors.toList());
showPreparingNextTrigger(triggeredJobList);
for (Cron4jJob triggeredJob : triggeredJobList) { // expception if contains unscheduled
triggeredJob.launchNow();
}
}
}
protected Cron4jJob findTriggeredJob(LaJobKey triggeredJobKey) {
return cron4jNow.findJobByKey(triggeredJobKey).orElseTranslatingThrow(cause -> {
String msg = "Not found the next job: " + triggeredJobKey + " triggered by " + toString();
throw new JobTriggeredNotFoundException(msg, cause);
});
}
protected void showPreparingNextTrigger(List triggeredJobList) {
final List expList = triggeredJobList.stream().map(triggeredJob -> {
return triggeredJob.toIdentityDisp();
}).collect(Collectors.toList());
final String exp = expList.size() == 1 ? expList.get(0) : expList.toString();
logger.info("#job ...Preparing next job {} triggered by {}", exp, toIdentityDisp());
}
protected String buildTriggerNextJobExp(Cron4jJob triggeredJob) {
final String keyExp = triggeredJob.getJobUnique().map(unique -> unique.value()).orElseGet(() -> {
return triggeredJob.getJobKey().value();
});
return keyExp + "(" + triggeredJob.getJobType().getSimpleName() + ")";
}
// ===================================================================================
// Neighbor Concurrent
// ===================
public synchronized void registerNeighborConcurrent(String groupName, NeighborConcurrentGroup neighborConcurrentGroup) {
verifyCanScheduleState();
if (neighborConcurrentGroupMap == null) {
neighborConcurrentGroupMap = new ConcurrentHashMap(); // just in case
neighborConcurrentGroupList = new CopyOnWriteArrayList(); // just in case
}
neighborConcurrentGroupMap.put(groupName, neighborConcurrentGroup);
neighborConcurrentGroupList.add(neighborConcurrentGroup);
}
// ===================================================================================
// Display
// =======
public String toIdentityDisp() {
final Class extends LaJob> jobType = cron4jTask.getJobType();
return jobType.getSimpleName() + ":{" + jobUnique.map(uq -> uq + "(" + jobKey + ")").orElseGet(() -> jobKey.value()) + "}";
}
// ===================================================================================
// Assist Logic
// ============
protected synchronized void verifyCanScheduleState() {
if (disappeared) {
throw new JobAlreadyDisappearedException("Already disappeared the job: " + toString());
}
if (unscheduled) {
throw new JobAlreadyUnscheduleException("Already unscheduled the job: " + toString());
}
}
protected synchronized void verifyCanRescheduleState() {
if (disappeared) {
throw new JobAlreadyDisappearedException("Already disappeared the job: " + toString());
}
}
protected synchronized void verifyCanUnscheduleState() {
if (disappeared) {
throw new JobAlreadyDisappearedException("Already disappeared the job: " + toString());
}
if (unscheduled) {
throw new JobAlreadyUnscheduleException("Already unscheduled the job: " + toString());
}
}
protected synchronized void verifyCanDisappearState() {
if (disappeared) {
throw new JobAlreadyDisappearedException("Already disappeared the job: " + toString());
}
}
protected synchronized void verifyCanStopState() {
// everyday you can stop
}
// ===================================================================================
// Small Helper
// ============
protected void assertArgumentNotNull(String variableName, Object value) {
if (variableName == null) {
throw new IllegalArgumentException("The variableName should not be null.");
}
if (value == null) {
throw new IllegalArgumentException("The argument '" + variableName + "' should not be null.");
}
}
// ===================================================================================
// Basic Override
// ==============
@Override
public String toString() {
final String titlePrefix = jobNote.map(title -> title + ", ").orElse("");
final String keyExp = jobUnique.map(uq -> uq + "(" + jobKey + ")").orElseGet(() -> jobKey.value());
final String idExp = cron4jId.map(id -> id.value()).orElse("non-cron");
final String hash = Integer.toHexString(hashCode());
return DfTypeUtil.toClassTitle(this) + ":{" + titlePrefix + keyExp + ", " + idExp + ", " + cron4jTask + "}@" + hash;
}
// ===================================================================================
// Accessor
// ========
@Override
public LaJobKey getJobKey() {
return jobKey;
}
@Override
public OptionalThing getJobNote() {
return jobNote;
}
@Override
public OptionalThing getJobUnique() {
return jobUnique;
}
@Override
public synchronized OptionalThing getCronExp() { // synchronized for varying
final String cronExp = !isNonCron() ? cron4jTask.getVaryingCron().getCronExp() : null;
return OptionalThing.ofNullable(cronExp, () -> {
throw new IllegalStateException("Not found cron expression because of non-cron job: " + toString());
});
}
@Override
public Class extends LaJob> getJobType() {
return cron4jTask.getJobType();
}
@Override
public OptionalThing getParamsSupplier() {
return cron4jTask.getVaryingCron().getCronOption().getParamsSupplier();
}
@Override
public JobNoticeLogLevel getNoticeLogLevel() {
return cron4jTask.getVaryingCron().getCronOption().getNoticeLogLevel();
}
@Override
public JobConcurrentExec getConcurrentExec() {
return cron4jTask.getConcurrentExec();
}
public OptionalThing getCron4jId() {
return cron4jId;
}
public Cron4jTask getCron4jTask() { // for framework
return cron4jTask;
}
public Cron4jNow getCron4jNow() { // for e.g. test
return cron4jNow;
}
@Override
public Set getTriggeredJobKeySet() {
synchronized (triggeredJobKeyLock) {
return triggeredJobKeyList != null ? Collections.unmodifiableSet(triggeredJobKeyList) : Collections.emptySet();
}
}
@Override
public synchronized List getNeighborConcurrentGroupList() {
if (neighborConcurrentGroupList == null) {
return Collections.emptyList();
}
// unmodifiable and snapshot list for concurrent process,
// wrap wrap just in case, for machine-gun synchronization (that needs ordered groups)
return Collections.unmodifiableList(new CopyOnWriteArrayList(neighborConcurrentGroupList));
}
@Override
public synchronized Map getNeighborConcurrentGroupMap() {
return neighborConcurrentGroupMap != null ? Collections.unmodifiableMap(neighborConcurrentGroupMap) : Collections.emptyMap();
}
}