How to check whether Quartz cron job is running? - java

How to check if scheduled Quartz cron job is running or not? Is there any API to do the checking?

scheduler.getCurrentlyExecutingJobs() should work in most case. But remember not to use it in Job class, for it use ExecutingJobsManager(a JobListener) to put the running job to a HashMap, which run before the job class, so use this method to check job is running will definitely return true. One simple approach is to check that fire times are different:
public static boolean isJobRunning(JobExecutionContext ctx, String jobName, String groupName)
throws SchedulerException {
List<JobExecutionContext> currentJobs = ctx.getScheduler().getCurrentlyExecutingJobs();
for (JobExecutionContext jobCtx : currentJobs) {
String thisJobName = jobCtx.getJobDetail().getKey().getName();
String thisGroupName = jobCtx.getJobDetail().getKey().getGroup();
if (jobName.equalsIgnoreCase(thisJobName) && groupName.equalsIgnoreCase(thisGroupName)
&& !jobCtx.getFireTime().equals(ctx.getFireTime())) {
return true;
}
}
return false;
}
Also notice that this method is not cluster aware. That is, it will only return Jobs currently executing in this Scheduler instance, not across the entire cluster. If you run Quartz in a cluster, it will not work properly.

If you notice in the QUARTZ_TRIGGERS table, there is a TRIGGER_STATE column. This tells you the state of the trigger (TriggerState) for a particular job. In all likelihood your app doesn't have a direct interface to this table but the quartz scheduler does and you can check the state this way:
private Boolean isJobPaused(String jobName) throws SchedulerException {
JobKey jobKey = new JobKey(jobName);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobDetail.getKey());
for (Trigger trigger : triggers) {
TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
if (TriggerState.PAUSED.equals(triggerState)) {
return true;
}
}
return false;
}

Have you looked at this answer? Try with:
scheduler.getCurrentlyExecutingJobs()

Related

Keep row locked when exception occurs inside transaction - Spring Boot

We are having trouble to keep a transaction running when a exception occurs and also keep rows locked when this happens.
Firs, a bit of code to clarify.
With this query we are performing a for update skip locked (JobDao):
#NotNull
default Optional<EventEntity> getJobForProcessing(#NotNull final Instant now) {
final List<EventEntity> events = getUnprocessedEvent(now, PageRequest.of(0, 1));
if (events.isEmpty()) {
return Optional.empty();
}
return Optional.ofNullable(events.get(0));
}
#NotNull
#Query("""
select ee from EventEntity ee
where ee.processed = false
order by ee.createdAt""")
#Lock(LockModeType.PESSIMISTIC_WRITE)
#QueryHints({#QueryHint(name = "javax.persistence.lock.timeout", value = LockOptions.SKIP_LOCKED + "")})
Optional<EventEntity> getJobForProcessing(#NotNull final Instant now, #NotNull final Pageable pageable);
The following code runs on another thread using #Async (JobTransaction called from scheduler):
#Transactional
public void runInTransaction() {
final Instant now = Now.getInstantUtc();
final Optional<Job> jobForProcessing = dao.getJobForProcessing(now);
if (jobForProcessing.isPresent()) {
final Job job = jobForProcessing.get();
jobProcessor.processJob(job);
} else {
Log.d(TAG, "No more jobs, stopping thread %s.".formatted(Thread.currentThread().getName()));
}
}
The processJob(job) method — simplified — is the following (JobProcessor):
private void processJob(#NotNull final Job job) {
try {
worker.execute(job);
jobService.markJobAsProcessed(job);
} catch (final Throwable throwable) {
Log.e(TAG, "Error to process job: " + job.getId(), throwable);
jobService.rescheduleJobExecution(job, throwable);
throw new RuntimeException(throwable);
}
}
On the above code, the important part is the catch block that calls jobService.rescheduleJobExecution(...).
This is the rescheduleJobExecution (JobService):
public void rescheduleJobExecution(#NotNull final Job job,
#NotNull final Throwable throwable) {
final var nextRetryAt = backoffCalculator.calculateNextRetryAt(job, now);
final var stackTraceAsString = Throwables.getStackTraceAsString(throwable);
final var executionAttempts = job.getExecutionAttempts() + 1;
final var newJob = job.cloneEntity()
.withExecutionAttempts(executionAttempts)
.withErrorLogLastRetry(stackTraceAsString)
.withNextRetryAt(nextRetryAt)
.build();
jobDao.save(newJob);
}
The problem I'm facing is that, when one error occurs and we try to reschedule the job, the following is throw: Transaction silently rolled back because it has been marked as rollback-only.
I understand why this happens, but have not found any way to fix this. What I already tried:
Change the runInTransaction to #Transactional(noRollbackFor = RuntimeException.class);
Add #Transactional(propagation = Propagation.REQUIRES_NEW) on the rescheduleJobExecution method. This solves the problem but brings a new one: since the outer transaction has already failed, the row is available to other threads to select and in a multithread environment this could lead to race conditions (ie: the job is reselected to execution before the reschedule is done).
So, I'm looking for a solution that enables me to deal with job rescheduling and also that keep jobs safe on a multithread environment. The getUnprocessedEvent method is used by many threads and we also like to keep using the for update skip locked that postgres provides.

How to solve if `RotatingServerAdvice` is not fetching file for multiple times?

I am using IntegrationFlow as Sftp Inbound DSL Configuration where I am using CustomTriggerAdvice to handle manual trigger. Please see below code snippet for reference.
I am also using RotatingServerAdvice for handling multiple path in same host.
But when I start the Sftp Inbound it fetch file for the first time from every path but it does not work for second time and onward. Sftp Inbound Starts but does not fetch file from paths. I couldn't figure out the problem. Is there anything that I am missing?
SftpConfiguration
public IntegrationFlow fileFlow() {
SftpInboundChannelAdapterSpec spec = Sftp
.inboundAdapter(dSF())
.preserveTimestamp(true)
.remoteDirectory(".")
.autoCreateLocalDirectory(true)
.deleteRemoteFiles(false)
.localDirectory(new File(getDestinationLocation()));
return IntegrationFlows
.from(spec, e -> e.id(BEAN_ID)
.autoStartup(false)
.poller(Pollers
.fixedDelay(5000)
.advice(
customRotatingServerAdvice(dSF()),
customTriggerAdvice()
)
)
)
.channel(sftpReceiverChannel())
.handle(sftpInboundMessageHandler())
.get();
}
private MessageChannel sftpReceiverChannel() {
return MessageChannels.direct().get();
}
... ... ...
#Bean
public RotatingServerAdvice customRotatingServerAdvice(
DelegatingSessionFactory<LsEntry> dSF
) {
List<String> pathList = getSourcePathList();
for (String path : pathList) {
keyDirectories.add(new RotationPolicy.KeyDirectory(KEY, path));
}
return new RotatingServerAdvice(
dSF,
keyDirectories
);
}
#Bean
public CustomTriggerAdvice customTriggerAdvice() {
return new CustomTriggerAdvice(customControlChannel(),BEAN_ID);
}
#Bean
public IntegrationFlow customControlBus() {
return IntegrationFlows.from(customControlChannel())
.controlBus()
.get();
}
#Bean
public MessageChannel customControlChannel() {
return MessageChannels.direct().get();
}
CustomTriggerAdvice
public class CustomTriggerAdvice extends AbstractMessageSourceAdvice {
private final MessageChannel controlChannel;
private final String BEAN_ID;
public CustomTriggerAdvice(MessageChannel controlChannel, String beanID) {
this.controlChannel = controlChannel;
this.BEAN_ID = beanID;
}
#Override
public boolean beforeReceive(MessageSource<?> source) {
return true;
}
#Override
public Message<?> afterReceive(Message<?> result, MessageSource<?> source) {
if (result == null) {
controlChannel.send(new GenericMessage<>("#" + BEAN_ID + ".stop()"));
}
return result;
}
}
Starting Sftp Inbound using MessageChannel
#Qualifier("customControlChannel") MessageChannel controlChannel;
public void startSftpInbound(){
controlChannel.send(new GenericMessage<>("#" + beanID + ".start()"));
}
I need the system to start on demand and fetch file completing one cycle. If it is not stopped after that, it will continue polling and won't stop and my system will fall into an infinite loop. Is there any way to get that when the RotatingServerAdvice is completing polling from all servers at least one? Does it throw any event or something like that ?
You probably misunderstood the logic with the afterReceive(#Nullable Message<?> result, MessageSource<?> source) contract. You can't stop a channel adapter for your requirements when one of the servers has returned nothing to poll. This way you don't give a chance for another server to poll on the next polling cycle.
I think your idea is to iterate over all the servers only once and then stop. Probably independently of the result from any of them. It looks like the best way to stop for you it is to use a RotatingServerAdvice with the fair = true to move to the next server every time. The stop might be performed from the custom afterReceive() independently of the result when you see that the RotationPolicy.getCurrent() is equal to a last one in the list. So, this way you iterate over all of them and stop moving the first one for the next poling cycle.

How to make StatefulJobs run consecutively (not concurrently)?

I have a scheduler:
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory("quartz.properties");
sched = schedFact.getScheduler();
sched.start();
JobDetail jobDetail;
CronTrigger trigger;
for (ReportDetails report : reports) {
jobDetail = new JobDetail(report.getName() + _REPORT, GRP, ReportJob.class);
jobDetail.getJobDataMap().put(ReportJob.DATA_REPORT, report);
sched.addJob(jobDetail, true);
if (report.getCronExp() != null && report.getCronExp().length() > 0) {
trigger = new CronTrigger(report.getName() + _TRIGGER, GRP);
trigger.setCronExpression(report.getCronExp());
trigger.setJobGroup(GRP);
trigger.setJobName(report.getName() + _REPORT);
sched.scheduleJob(trigger);
}
}
And there is my job instance:
public class ReportJob implements StatefulJob {
private static final Logger logger = Logger.getLogger(ReportJob.class);
public void execute(JobExecutionContext context) throws JobExecutionException {
ReportDetails report = (ReportDetails) context.getJobDetail().getJobDataMap().get(DATA_REPORT);
report.getLogger().info("job for report started");
...
report.getLogger().info("Job for report ended");
}
}
The thing is that though ReportJob implements StatefulJob indeed jobs run concurrently.
QuartzScheduler_Worker-1 | job for report started
QuartzScheduler_Worker-2 | job for report started
QuartzScheduler_Worker-2 | job for report ended
QuartzScheduler_Worker-1 | job for report ended
I want them to run consecutively, one by one. How to resolve this issue?
I understood my error: StatefulJob prevents multiple instances of a job WITH THE SAME KEY from running at the same time. While I'd created the jobs with different keys.

Quartz triggering same instance twice

We are using DisallowConcurrentExecutionAttribute annotation inside the Java class to prevent concurrent execution of multiple instances, however, looks like Quartz has triggered twice the same instance concurrently. Please address this issue and provide us more information and fix this issue if it is a bug.
#Override
#Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public void execute(final JobExecutionContext jobExecutionContext) throws JobExecutionException {
logger.log(Log.DEBUG, "++++ Quartz JOB BatchJobDetector started");
try {
this.setJobExecutionContext(jobExecutionContext);
boolean triggerNextJob = true;
while (triggerNextJob) {
TriggeredBatchProcessDTO triggeredBatchProcessDTO = getNextJob(jobExecutionContext, 0);
if (triggeredBatchProcessDTO != null) {
triggerJobImmediatly(triggeredBatchProcessDTO.getId(), jobExecutionContext);
triggeredBatchProcessDTO.setState(StatusType.RUNNING);
triggeredBatchProcessDTO.setProcessDtTm(triggeredBatchProcessDTO.getProcessDtTm());//CRGRO022
updateTriggeredBatchProcessDTO(triggeredBatchProcessDTO);
} else {
triggerNextJob = false;
}
}
} catch (final UnexpectedRuntimeException e) {
logger.log(Log.ERROR, "Error during execution of TriggeredBatchProcessDetectorJob: " + e.getMessage(), e);
throw e;
} catch (final Throwable t) {
throw new UnexpectedRuntimeException(CoreExceptionId.RUN_0001_UNEXPECTED_EXCEPTION,
new Object[] { "TriggeredBatchProcessDetectorJob error" }, t);
}
logger.log(Log.DEBUG, "++++ Quartz JOB BatchDetector finished");
}
You need to set up quartz correctly through properties to run it into the cluster mode and I'm not sure but imho you should also use #PersistJobDataAfterExecution annotation. I was using clustered quartz without any problems also with depracated job implementations
StatefulJob. You need to show us your config - here is sample - and give quartz lib versions

Spring batch restrict single instance of job only

I have one spring batch job which can be kicked of by rest URL. I want to make sure only one job instance is allowed to run. and if another instance already running then don't start another. even if the parameters are different.
I searched and found nothing out of box solution. thinking of extending SimpleJobLauncher. to check if any instance of the job running or not.
You could try to intercept the job execution, implementing the JobExecutionListener interface:
public class MyJobExecutionListener extends JobExecutionListener {
//active JobExecution, used as a lock.
private JobExecution _active;
public void beforeJob(JobExecution jobExecution) {
//create a lock
synchronized(jobExecution) {
if(_active!=null && _active.isRunning()) {
jobExecution.stop();
} else {
_active=jobExecution;
}
}
}
public void afterJob(JobExecution jobExecution) {
//release the lock
synchronized(jobExecution) {
if(jobExecution==_active) {
_active=null;
}
}
}
}
And then, inject to the Job definition:
<job id="myJobConfig">
<listeners>
<listener ref="myListener"/>
</listeners>
</job>
I solved this by creating an JobExecutionListner and with the help of JobExplorer I checked if any other instance is running if running then stop current job.I created listener so that it can be plugged in to any job that requires this kind of scenario.
Set<JobExecution> jobExecutions = ((SimpleJobExplorer) jobExplorer.getObject()).findRunningJobExecutions(jobExecution.getJobInstance().getJobName());
if(jobExecutions.size()>1){
Long currentTime = (new Date()).getTime();
for(JobExecution execution : jobExecutions ){
if(execution.getJobInstance().getId().compareTo(jobExecution.getJobInstance().getId())!=0 && (currentTime - execution.getStartTime().getTime()) <lockOverideTime){
jobExecution.stop();
throw new IllegalStateException("Another instance of the job running job name : " +jobExecution.getJobInstance().getJobName() );
}
}
}
Or, in response to REST URL, check using JobExplorer if your job is running using job's specifics business rules
I think a simple method like the following might do the trick:
#Autowire
private JobExplorer jobExplorer;
private boolean isJobRunning(Job job) {
Set<JobExecution> jobExecutions = jobExplorer.findRunningJobExecutions(job.getName());
return !jobExecutions.isEmpty();
}
Then, prior to executing your job make the check:
private void executeJob(Job job, #Nonnull JobParameters params) {
if (isJobRunning(job)) {
return;
}
try {
jobLauncher.run(job, params);
} catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) {
log.error("could not run job " + jobIdentifier, e);
}
}

Categories

Resources