Quartz + Spring Boot: Concurrent execution of multiple jobs - spring-boot

I have simple Spring Boot (2.5) application to get multiple data from multiple sources at once. I have created 3 job classes, each of them have several tasks to do. First one have two tasks (defined in separate methods) to do, second have 3 and thirds have 2 - there's 7 in all count.
Each job have this construction:
#Component
public class FirstJob {
[...]
#Scheduled(cron = "0 * * * * *")
public void getLastTaskStatistics() {
[...]
}
#Scheduled(cron = "0 * * * * *")
public void getMetersStatusCount() {
[...]
}
}
#Component
public class SecondJob {
[...]
#Scheduled(cron = "0 * * * * *")
public void getIndexStats() {
[...]
}
#Scheduled(cron = "0 * * * * *")
public void getCollectionStats() {
[...]
}
#Scheduled(cron = "0 * * * * *")
public void getActiveMetersStats() {
[...]
}
}
Third one is the same. As you can see, all methods have fixed and the same firing time. Everything is working almost properly, but I want to execute them in parallel, in available execution thread pool: actually observation is different, all tasks are executed in single execution task (always named scheduling-1), and also in sequential. A part of log:
2021-05-04 14:39:00.020 INFO 9004 --- [ scheduling-1] FirstJob : getMetersStatusCount: start
2021-05-04 14:39:00.166 INFO 9004 --- [ scheduling-1] FirstJob : getMetersStatusCount: end
2021-05-04 14:39:00.166 INFO 9004 --- [ scheduling-1] FirstJob : getLastTaskStatistics: start
2021-05-04 14:39:00.235 INFO 9004 --- [ scheduling-1] FirstJob : getLastTaskStatistics: end
2021-05-04 14:39:00.235 INFO 9004 --- [ scheduling-1] SecondJob : getActiveMetersStats: start
2021-05-04 14:39:05.786 INFO 9004 --- [ scheduling-1] SecondJob : getActiveMetersStats: end
2021-05-04 14:39:05.786 INFO 9004 --- [ scheduling-1] SecondJob : getCollectionStats: start
2021-05-04 14:39:05.833 INFO 9004 --- [ scheduling-1] SecondJob : getCollectionStats: end
2021-05-04 14:39:05.833 INFO 9004 --- [ scheduling-1] SecondJob : getIndexStats: start
2021-05-04 14:39:05.902 INFO 9004 --- [ scheduling-1] SecondJob : getIndexStats: end
2021-05-04 14:39:05.902 INFO 9004 --- [ scheduling-1] ThirdJob : getExchangesDetails: start
2021-05-04 14:39:06.187 INFO 9004 --- [ scheduling-1] ThirdJob : getExchangesDetails: end
2021-05-04 14:39:06.187 INFO 9004 --- [ scheduling-1] ThirdJob : getQueueDetails: start
2021-05-04 14:39:06.303 INFO 9004 --- [ scheduling-1] ThirdJob : getQueueDetails: end
Please help me: how to avoid single instance of Quartz executor and run in parallel all tasks in all jobs? Quartz auto configuration in application.properties is based on default values, except:
spring.quartz.properties.org.quartz.scheduler.batchTriggerAcquisitionMaxCount=5
(change made for experiment purposes, value differ from default '1', but nothing interesting happened).

You can configure the pool size. Default is 1.
Sample:
spring.task.scheduling.pool.size=10

Why don't you try the async approach i.e. from a single #scheduled method call other method in async manners.
you can follow the approach over here How to asynchronously call a method in Java
eg:
CompletableFuture.runAsync(() -> {
// method calls
});
or
CompletableFuture.allOf(
CompletableFuture.runAsync(() -> ...),
CompletableFuture.runAsync(() -> ...)
);

Thanks to Ezequiel, it's enough to set:
spring.task.scheduling.pool.size=10
in application.properties, now all tasks starts at the same time.

Related

KafkaProducer InterruptedException during gracefull shutdown on spring boot application

For a project we are sending some events to kafka. We use spring-kafka 2.6.2.
Due to usage of spring-vault we have to restart/kill the application before the end of credentials lease (application is automatically restarted by kubernetes).
Our problem is that when using applicationContext.close() to proceed with our gracefull shutdown, KafkaProducer gets an InterruptedException Interrupted while joining ioThread inside it's close() method.
It means that in our case some pending events are not sent to kafka before shutdown as it's forced to close due to an error during destroy.
Here under a stacktrace
2020-12-18 13:57:29.007 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
2020-12-18 13:57:29.009 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2020-12-18 13:57:29.013 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Destroying Spring FrameworkServlet 'dispatcherServlet'
2020-12-18 13:57:29.014 INFO [titan-producer,,,] 1 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
2020-12-18 13:57:29.020 WARN [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [kafka-producer-network-thread | titan-producer-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
java.base#11.0.9.1/sun.nio.ch.EPoll.wait(Native Method)
java.base#11.0.9.1/sun.nio.ch.EPollSelectorImpl.doSelect(Unknown Source)
java.base#11.0.9.1/sun.nio.ch.SelectorImpl.lockAndDoSelect(Unknown Source)
java.base#11.0.9.1/sun.nio.ch.SelectorImpl.select(Unknown Source)
org.apache.kafka.common.network.Selector.select(Selector.java:873)
org.apache.kafka.common.network.Selector.poll(Selector.java:469)
org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:544)
org.apache.kafka.clients.producer.internals.Sender.runOnce(Sender.java:325)
org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:240)
java.base#11.0.9.1/java.lang.Thread.run(Unknown Source)
2020-12-18 13:57:29.021 WARN [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [micrometer-kafka-metrics] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
java.base#11.0.9.1/jdk.internal.misc.Unsafe.park(Native Method)
java.base#11.0.9.1/java.util.concurrent.locks.LockSupport.parkNanos(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.ThreadPoolExecutor.getTask(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.base#11.0.9.1/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
java.base#11.0.9.1/java.lang.Thread.run(Unknown Source)
2020-12-18 13:57:29.046 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2020-12-18 13:57:29.048 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.s.s.c.ThreadPoolTaskScheduler : Shutting down ExecutorService 'taskScheduler'
2020-12-18 13:57:29.051 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=titan-producer-1] Closing the Kafka producer with timeoutMillis = 30000 ms.
2020-12-18 13:57:29.055 ERROR [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=titan-producer-1] Interrupted while joining ioThreadjava.lang.InterruptedException: null
at java.base/java.lang.Object.wait(Native Method)
at java.base/java.lang.Thread.join(Unknown Source)
at org.apache.kafka.clients.producer.KafkaProducer.close(KafkaProducer.java:1205)
at org.apache.kafka.clients.producer.KafkaProducer.close(KafkaProducer.java:1182)
at org.springframework.kafka.core.DefaultKafkaProducerFactory$CloseSafeProducer.closeDelegate(DefaultKafkaProducerFactory.java:901)
at org.springframework.kafka.core.DefaultKafkaProducerFactory.destroy(DefaultKafkaProducerFactory.java:428)
at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:258)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:587)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:559)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:1092)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:520)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingletons(DefaultListableBeanFactory.java:1085)
at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1061)
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1030)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.doClose(ServletWebServerApplicationContext.java:170)
at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:979)
at org.springframework.cloud.sleuth.instrument.async.TraceRunnable.run(TraceRunnable.java:68)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)2020-12-18 13:57:29.055 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=titan-producer-1] Proceeding to force close the producer since pending requests could not be completed within timeout 30000 ms.
2020-12-18 13:57:29.056 WARN [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'kafkaProducerFactory': org.apache.kafka.common.errors.InterruptException: java.lang.InterruptedException
2020-12-18 13:57:29.064 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService
2020-12-18 13:57:29.065 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] c.l.t.p.zookeeper.ZookeeperManagerImpl : Closing zookeeperConnection
2020-12-18 13:57:29.197 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] org.apache.zookeeper.ZooKeeper : Session: 0x30022348ba6000b closed
2020-12-18 13:57:29.197 INFO [titan-producer,,,] 1 --- [d-1-EventThread] org.apache.zookeeper.ClientCnxn : EventThread shut down for session: 0x30022348ba6000b
2020-12-18 13:57:29.206 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] com.zaxxer.hikari.HikariDataSource : loadtest_fallback_titan_pendingEvents - Shutdown initiated...
2020-12-18 13:57:29.221 INFO [titan-producer,222efdd2a07966ce,222efdd2a07966ce,true] 1 --- [ scheduling-1] com.zaxxer.hikari.HikariDataSource : loadtest_fallback_titan_pendingEvents - Shutdown completed.
Here is my configuration class
#Flogger
#EnableKafka
#Configuration
#RequiredArgsConstructor
#ConditionalOnProperty(
name = "titan.producer.kafka.enabled",
havingValue = "true",
matchIfMissing = true)
public class KafkaConfiguration {
#Bean
DefaultKafkaProducerFactoryCustomizer kafkaProducerFactoryCustomizer(ObjectMapper mapper) {
return producerFactory -> producerFactory.setValueSerializer(new JsonSerializer<>(mapper));
}
#Bean
public NewTopic createTopic(TitanProperties titanProperties, KafkaProperties kafkaProperties) {
TitanProperties.Kafka kafka = titanProperties.getKafka();
String defaultTopic = kafkaProperties.getTemplate().getDefaultTopic();
int numPartitions = kafka.getNumPartitions();
short replicationFactor = kafka.getReplicationFactor();
log.atInfo()
.log("Creating Kafka Topic %s with %s partitions and %s replicationFactor", defaultTopic, numPartitions, replicationFactor);
return TopicBuilder.name(defaultTopic)
.partitions(numPartitions)
.replicas(replicationFactor)
.config(MESSAGE_TIMESTAMP_TYPE_CONFIG, LOG_APPEND_TIME.name)
.build();
}
}
and my application.yaml
spring:
application:
name: titan-producer
kafka:
client-id: ${spring.application.name}
producer:
key-serializer: org.apache.kafka.common.serialization.UUIDSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
properties:
max.block.ms: 2000
request.timeout.ms: 2000
delivery.timeout.ms: 2000 #must be greater or equal to request.timeout.ms + linger.ms
template:
default-topic: titan-dev
Our vault configuration which executes the applicationContext.close() using a scheduledTask. We do it kind randomly as we have multiple replicas of the app running in parallel and avoid all the replicas to be killed at the same time.
#Flogger
#Configuration
#ConditionalOnBean(SecretLeaseContainer.class)
#ConditionalOnProperty(
name = "titan.producer.scheduling.enabled",
havingValue = "true",
matchIfMissing = true)
public class VaultConfiguration {
#Bean
public Lifecycle scheduledAppRestart(Clock clock, TitanProperties properties, TaskScheduler scheduler, ConfigurableApplicationContext applicationContext) {
Instant now = clock.instant();
Duration maxTTL = properties.getVaultConfig().getCredsMaxLease();
Instant start = now.plusSeconds(maxTTL.dividedBy(2).toSeconds());
Instant end = now.plusSeconds(maxTTL.minus(properties.getVaultConfig().getCredsMaxLeaseExpirationThreshold()).toSeconds());
Instant randomInstant = randBetween(start, end);
return new ScheduledLifecycle(scheduler, applicationContext::close, "application restart before lease expiration", randomInstant);
}
private Instant randBetween(Instant startInclusive, Instant endExclusive) {
long startSeconds = startInclusive.getEpochSecond();
long endSeconds = endExclusive.getEpochSecond();
long random = RandomUtils.nextLong(startSeconds, endSeconds);
return Instant.ofEpochSecond(random);
}
}
The ScheduledLifecycle class we use to run the scheduledtasks
import lombok.extern.flogger.Flogger;
import org.springframework.context.SmartLifecycle;
import org.springframework.scheduling.TaskScheduler;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ScheduledFuture;
#Flogger
public class ScheduledLifecycle implements SmartLifecycle {
private ScheduledFuture<?> future = null;
private Duration delay = null;
private final TaskScheduler scheduler;
private final Runnable command;
private final String commandDesc;
private final Instant startTime;
public ScheduledLifecycle(TaskScheduler scheduler, Runnable command, String commandDesc, Instant startTime) {
this.scheduler = scheduler;
this.command = command;
this.commandDesc = commandDesc;
this.startTime = startTime;
}
public ScheduledLifecycle(TaskScheduler scheduler, Runnable command, String commandDesc, Instant startTime, Duration delay) {
this(scheduler, command, commandDesc, startTime);
this.delay = delay;
}
#Override
public void start() {
if (delay != null) {
log.atInfo().log("Scheduling %s: starting at %s, running every %s", commandDesc, startTime, delay);
future = scheduler.scheduleWithFixedDelay(command, startTime, delay);
} else {
log.atInfo().log("Scheduling %s: execution at %s", commandDesc, startTime);
future = scheduler.schedule(command, startTime);
}
}
#Override
public void stop() {
if (future != null) {
log.atInfo().log("Stop %s", commandDesc);
future.cancel(true);
}
}
#Override
public boolean isRunning() {
boolean running = future != null && (!future.isDone() && !future.isCancelled());
log.atFine().log("is %s running? %s", running);
return running;
}
}
Is there a bug with spring-kafka? Any idea?
Thanks
future.cancel(true);
This is interrupting the producer thread and is likely the root cause of the problem.
You should use future.cancel(false); to allow the task to terminate in an orderly fashion, without interruption.
/**
* Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, has already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when {#code cancel} is called,
* this task should never run. If the task has already started,
* then the {#code mayInterruptIfRunning} parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.
*
* <p>After this method returns, subsequent calls to {#link #isDone} will
* always return {#code true}. Subsequent calls to {#link #isCancelled}
* will always return {#code true} if this method returned {#code true}.
*
* #param mayInterruptIfRunning {#code true} if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete
* #return {#code false} if the task could not be cancelled,
* typically because it has already completed normally;
* {#code true} otherwise
*/
boolean cancel(boolean mayInterruptIfRunning);
EDIT
In addition, the ThreadPoolTaskScheduler.waitForTasksToCompleteOnShutdown is false by default.
/**
* Set whether to wait for scheduled tasks to complete on shutdown,
* not interrupting running tasks and executing all tasks in the queue.
* <p>Default is "false", shutting down immediately through interrupting
* ongoing tasks and clearing the queue. Switch this flag to "true" if you
* prefer fully completed tasks at the expense of a longer shutdown phase.
* <p>Note that Spring's container shutdown continues while ongoing tasks
* are being completed. If you want this executor to block and wait for the
* termination of tasks before the rest of the container continues to shut
* down - e.g. in order to keep up other resources that your tasks may need -,
* set the {#link #setAwaitTerminationSeconds "awaitTerminationSeconds"}
* property instead of or in addition to this property.
* #see java.util.concurrent.ExecutorService#shutdown()
* #see java.util.concurrent.ExecutorService#shutdownNow()
*/
public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) {
this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
}
You might also have to set awaitTerminationSeconds.

Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart

Spring Batch decider is going into forloop. I have below requirements.
If Step1 execute, check decider() if "NO" then end Job, if "Yes" then execute Step2, if Step2 is COMPLETED then execute decider() f "NO" then end Job, if "Yes" then execute Step3.
Any guidance how can we configure in the batch?
2020-12-08 11:41:11.473 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step1]
step1
2020-12-08 11:41:11.493 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step1] executed in 20ms
2020-12-08 11:41:11.508 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.513 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 5ms
2020-12-08 11:41:11.568 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.571 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.577 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 6ms
2020-12-08 11:41:11.585 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.589 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.594 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 5ms
2020-12-08 11:41:11.601 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.604 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.608 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 3ms
2020-12-08 11:41:11.616 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.618 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.623 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 5ms
2020-12-08 11:41:11.630 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.634 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
2020-12-08 11:41:11.638 INFO 16800 --- [ main] o.s.batch.core.step.AbstractStep : Step: [step2] executed in 4ms
2020-12-08 11:41:11.646 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Duplicate step [step2] detected in execution of job=[job]. If either step fails, both will be executed again on restart.
2020-12-08 11:41:11.648 INFO 16800 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step2]
step2
Java code
#Configuration
public class Config {
#Autowired
private JobBuilderFactory jobs;
#Autowired
private StepBuilderFactory steps;
#Bean
public Step step1() {
return steps.get("step1")
.tasklet((contribution, chunkContext) -> {
System.out.println("step1");
return RepeatStatus.FINISHED;
})
.build();
}
#Bean
public JobExecutionDecider decider() {
return (jobExecution, stepExecution) -> new FlowExecutionStatus("SUCCESS"); // or NO
}
#Bean
public Step step2() {
return steps.get("step2")
.tasklet((contribution, chunkContext) -> {
System.out.println("step2");
return RepeatStatus.FINISHED;
})
.build();
}
#Bean
public Step step3() {
return steps.get("step3")
.tasklet((contribution, chunkContext) -> {
System.out.println("step3");
return RepeatStatus.FINISHED;
})
.build();
}
#Bean
public Step step5() {
return steps.get("step5")
.tasklet((contribution, chunkContext) -> {
System.out.println("Step 5");
return RepeatStatus.FINISHED;
})
.build();
}
#Bean
public Step step4() {
return steps.get("step4")
.tasklet((contribution, chunkContext) -> {
System.out.println("Step 4");
return RepeatStatus.FINISHED;
})
.build();
}
#Bean
public Job job() {
return jobs.get("job")
.incrementer(new RunIdIncrementer())
.start(step1())
.next(decider())
.from(decider()).on("SUCCESS").to(step2())
.from(decider()).on("NO").end()
.from(step2()).on("COMPLETED").to(decider())
.from(decider()).on("SUCCESS").to(step3())
.from(decider()).on("NO").end()
.end()
.build();
}
}
Note - Due to security restriction, I'm unable to load Flow diagram from office workstation :(
You have a bug in your flow definition. The SUCCESS outcome from the decider goes to two different steps:
.from(decider()).on("SUCCESS").to(step2())
...
.from(decider()).on("SUCCESS").to(step3())
Moreover, the transition definitions from step2 are incomplete. You've only defined the transition from step2 on COMPLETED:
.from(step2()).on("COMPLETED").to(decider())
you should define the transition from step2 for other cases as well.

Simple unit test for Apache Camel SNMP route

I'm having some trouble getting a working Camel Spring-Boot unit test written, that tests a simple SNMP route. Here is what I have so far:
SnmpRoute.kt
open class SnmpRoute(private val snmpProperties: SnmpProperties, private val repository: IPduEventRepository) : RouteBuilder() {
#Throws(Exception::class)
override fun configure() {
logger.debug("Initialising with properties [{}]", snmpProperties)
from("snmp:0.0.0.0:1161?protocol=udp&type=TRAP")
.process { exchange ->
// do stuff
}
.bean(repository, "save")
}
}
SnmpRouteTest.kt
#CamelSpringBootTest
#SpringBootApplication
#EnableAutoConfiguration
open class SnmpRouteTest : CamelTestSupport() {
object SnmpConstants {
const val SNMP_TRAP = "<snmp><entry><oid>...datadatadata...</oid><value>123456</value></entry></snmp>"
const val MOCK_SNMP_ENDPOINT = "mock:snmp"
}
#Mock
lateinit var snmpProperties: SnmpProperties
#Mock
lateinit var repository: IPduEventRepository
#InjectMocks
lateinit var snmpRoute: SnmpRoute
#EndpointInject(SnmpConstants.MOCK_SNMP_ENDPOINT)
lateinit var mock: MockEndpoint
#Before
fun setup() {
initMocks(this)
}
#Throws(Exception::class)
override fun createRouteBuilder(): RouteBuilder {
return snmpRoute
}
#Test
#Throws(Exception::class)
fun `Test SNMP endpoint`() {
mock.expectedBodiesReceived(SnmpConstants.SNMP_TRAP)
template.sendBody(SnmpConstants.MOCK_SNMP_ENDPOINT,
SnmpConstants.SNMP_TRAP)
mock.assertIsSatisfied()
verify(repository).save(PduEvent(1234, PDU.TRAP))
}
}
However, when I run this test, it fails as the repository mock never has any interactions:
Wanted but not invoked:
repository.save(
PduEvent(requestId=1234, type=-89)
);
-> at org.meanwhile.in.hell.camel.snmp.route.SnmpRouteTest.Test SNMP endpoint(SnmpRouteTest.kt:61)
Actually, there were zero interactions with this mock.
Can someone help me understand why this isn't interacting correctly? When run manually, this works and saves as expected.
Now I see what is going on here!
Your RouteBuilder under test has a from("snmp"). If you wish to deliver a mock message there for testing, you need to swap the snmp: component with something like a direct: or seda: component, during test execution.
Your current test is delivering a message to a Mock endpoint and verifying if it was received there. It does not interact with the real route builder. That's why your mock endpoint assertions do passed but Mockito.verify() failed.
TL;DR
Presuming that you are using Apache Camel 3.x, here is how to do it. I'm not fluent in Kotlin so, I'll show how to do that in Java.
AdviceWithRouteBuilder.adviceWith(context, "route-id", routeBuilder -> {
routeBuilder.replaceFromWith("direct:snmp-from"); //Replaces the from part of the route `route-id` with a direct component
});
You need to modify your route builder code to assign an ID to the route (say, route-id)
Replace the SNMP component at the start of the route with a direct component
Deliver test messages to the direct: component instead of SNMP
TL;DR ends.
Full blown sample code below.
PojoRepo.java
#Component
public class PojoRepo {
public void save(String body){
System.out.println(body);
}
}
SNMPDummyRoute.java
#Component
public class SNMPDummyRoute extends RouteBuilder {
PojoRepo pojoRepo;
public SNMPDummyRoute(PojoRepo pojoRepo) {
this.pojoRepo = pojoRepo;
}
#Override
public void configure() throws Exception {
from("snmp:0.0.0.0:1161?protocol=udp&type=TRAP")
.id("snmp-route")
.process(exchange -> {
exchange.getMessage().setBody(String.format("Saw message [%s]", exchange.getIn().getBody()));
})
.to("log:snmp-log")
.bean(pojoRepo, "save");
}
}
SNMPDummyRoteTest.java
Note: This class uses CamelSpringBootRunner instead of extending CamelTestSupport, but the core idea is same.
#RunWith(CamelSpringBootRunner.class)
#SpringBootTest
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
#DisableJmx(false)
#MockEndpoints("log:*")
public class SNMPDummyRouteTest {
#MockBean
PojoRepo repo;
#EndpointInject("mock:log:snmp-log")
MockEndpoint mockEndpoint;
#Produce
ProducerTemplate testTemplate;
#Autowired
CamelContext camelContext;
#Test
public void testRoute() throws Exception {
AdviceWithRouteBuilder.adviceWith(camelContext,"snmp-route",routeBuilder -> {
routeBuilder.replaceFromWith("direct:snmp-from");
});
testTemplate.sendBody("direct:snmp-from","One");
testTemplate.sendBody("direct:snmp-from","Two");
mockEndpoint.expectedMinimumMessageCount(2);
mockEndpoint.setAssertPeriod(2_000L);
mockEndpoint.assertIsSatisfied();
Mockito.verify(repo, Mockito.atLeast(2)).save(anyString());
}
}
Logs from test run below. Take a closer look at the XML piece where the SNMP endpoint gets swapped in with a direct component.
2019-11-12 20:52:57.126 INFO 32560 --- [ main] o.a.c.component.snmp.SnmpTrapConsumer : Starting trap consumer on udp:0.0.0.0/1161
2019-11-12 20:52:58.363 INFO 32560 --- [ main] o.a.c.component.snmp.SnmpTrapConsumer : Started trap consumer on udp:0.0.0.0/1161 using udp protocol
2019-11-12 20:52:58.364 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Route: snmp-route started and consuming from: snmp://udp:0.0.0.0/1161
2019-11-12 20:52:58.368 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Total 1 routes, of which 1 are started
2019-11-12 20:52:58.370 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Apache Camel 3.0.0-M4 (CamelContext: MyCamel) started in 2.645 seconds
2019-11-12 20:52:59.670 INFO 32560 --- [ main] o.a.c.i.engine.DefaultShutdownStrategy : Starting to graceful shutdown 1 routes (timeout 10 seconds)
2019-11-12 20:52:59.680 INFO 32560 --- [ - ShutdownTask] o.a.c.component.snmp.SnmpTrapConsumer : Stopped trap consumer on udp:0.0.0.0/1161
2019-11-12 20:52:59.683 INFO 32560 --- [ - ShutdownTask] o.a.c.i.engine.DefaultShutdownStrategy : Route: snmp-route shutdown complete, was consuming from: snmp://udp:0.0.0.0/1161
2019-11-12 20:52:59.684 INFO 32560 --- [ main] o.a.c.i.engine.DefaultShutdownStrategy : Graceful shutdown of 1 routes completed in 0 seconds
2019-11-12 20:52:59.687 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Route: snmp-route is stopped, was consuming from: snmp://udp:0.0.0.0/1161
2019-11-12 20:52:59.689 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Route: snmp-route is shutdown and removed, was consuming from: snmp://udp:0.0.0.0/1161
2019-11-12 20:52:59.691 INFO 32560 --- [ main] o.apache.camel.builder.AdviceWithTasks : AdviceWith replace input from [snmp:0.0.0.0:1161?protocol=udp&type=TRAP] --> [direct:snmp-from]
2019-11-12 20:52:59.692 INFO 32560 --- [ main] org.apache.camel.reifier.RouteReifier : AdviceWith route after: Route(snmp-route)[From[direct:snmp-from] -> [process[Processor#0x589dfa6f], To[log:snmp-log], Bean[org.foo.bar.POJORepo$MockitoMock$868728200]]]
2019-11-12 20:52:59.700 INFO 32560 --- [ main] org.apache.camel.reifier.RouteReifier : Adviced route before/after as XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="snmp-route">
<from uri="snmp:0.0.0.0:1161?protocol=udp&type=TRAP"/>
<process id="process1"/>
<to id="to1" uri="log:snmp-log"/>
<bean id="bean1" method="save"/>
</route>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="snmp-route">
<from uri="direct:snmp-from"/>
<process id="process1"/>
<to id="to1" uri="log:snmp-log"/>
<bean id="bean1" method="save"/>
</route>
2019-11-12 20:52:59.734 INFO 32560 --- [ main] .i.e.InterceptSendToMockEndpointStrategy : Adviced endpoint [log://snmp-log] with mock endpoint [mock:log:snmp-log]
2019-11-12 20:52:59.755 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Route: snmp-route started and consuming from: direct://snmp-from
2019-11-12 20:52:59.834 INFO 32560 --- [ main] snmp-log : Exchange[ExchangePattern: InOnly, BodyType: String, Body: Saw message [One]]
2019-11-12 20:52:59.899 INFO 32560 --- [ main] snmp-log : Exchange[ExchangePattern: InOnly, BodyType: String, Body: Saw message [Two]]
2019-11-12 20:52:59.900 INFO 32560 --- [ main] o.a.camel.component.mock.MockEndpoint : Asserting: mock://log:snmp-log is satisfied
2019-11-12 20:53:01.903 INFO 32560 --- [ main] o.a.camel.component.mock.MockEndpoint : Re-asserting: mock://log:snmp-log is satisfied after 2000 millis
2019-11-12 20:53:01.992 INFO 32560 --- [ main] o.a.c.s.boot.SpringBootCamelContext : Apache Camel 3.0.0-M4 (CamelContext: MyCamel) is shutting down
2019-11-12 20:53:01.993 INFO 32560 --- [ main] o.a.c.i.engine.DefaultShutdownStrategy : Starting to graceful shutdown 1 routes (timeout 10 seconds)
2019-11-12 20:53:01.996 INFO 32560 --- [ - ShutdownTask] o.a.c.i.engine.DefaultShutdownStrategy : Route: snmp-route shutdown complete, was consuming from: direct://snmp-from
2019-11-12 20:53:01.996 INFO 32560 --- [ main] o.a.c.i.engine.DefaultShutdownStrategy : Graceful shutdown of 1 routes completed in 0 seconds

java.lang.IllegalStateException: Encountered invalid #Scheduled method: Could not resolve placeholder #PropertySource("classpath:dev.yml")

I'm trying to load the following cron-execution-expression:
---
####################### Cron-Job Every 2mins every day #######################
cron:
exe:
expression: 0 0/2 * * * ?
The catch is, the above cron-expression is in a Spring-Cloud-Config (Let's say Springboot Project A, running on port: 8001) github repository. named: microservice-dev.yml
Project B (port: 8002) loads all the configurations provided by Project A at startup & I'm happy with that. But how do I locate this expression?
${cron.exe.expression}
#Component
//Couldn't get it to work with Spring-Cloud-Config
#PropertySource("classpath:microservice-dev.yml")
public class MergeCachedRecordsToDBImpl {
private static final Logger LOGGER = LoggerFactory.getLogger(MergeCachedRecordsToDBImpl.class);
//Couldn't get it to work with Spring-Cloud Config
#Scheduled(cron = "${cron.exe.expression}")
public void purgeExpired() {
LOGGER.info("Cron-Job Notification....");
LOGGER.info("Cron-Job executed at: {}", new Timestamp(new Date().getTime()));
}
}
At some point, I got it to work, but I'm not sure how? I'm trying to retrace my steps.
Now I'm getting this exception:
Caused by: java.lang.IllegalStateException: Encountered invalid #Scheduled method 'purgeExpired': Could not resolve placeholder 'cron.exe.expression' in value "${cron.exe.expression}"
Currently the spring boot #PropertySource doesn't support the yaml as the property file.
Here is the similar question - Spring #ConfigurationProperties not mapping list of objects
Ok, got it to work again with the .yml file. Happy Days.
In the following .yml file:
src/main/resources/bootstrap.yml
has the following contents:
---
spring:
application:
name: leaderboard
profiles:
active: dev
server:
port: 8004
---
####################### Cron-Job Every 2mins every day #######################
cron:
exe:
expression: 0 0/2 * * * ?
in the code, I annotated my class and method with following annotations:
#PropertySource("classpath:bootstrap.yml")
public class CronClass{
private static final Logger LOGGER = LoggerFactory.getLogger(CronClass.class);
#Scheduled(cron = "${cron.exe.expression}")
private void cornJob(){
LOGGER.info("Cron-Job Notification....");
LOGGER.info("Cron-Job executed at: {}", new Timestamp(new Date().getTime()));
}
}
Results:
2018-09-29 11:19:00.007 INFO [leaderboard,66037f3d8052cf6b,66037f3d8052cf6b,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job Notification....
2018-09-29 11:19:00.007 INFO [leaderboard,66037f3d8052cf6b,66037f3d8052cf6b,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job executed at: 2018-09-29 11:19:00.007
2018-09-29 11:20:00.000 INFO [leaderboard,25cab214549b76a6,25cab214549b76a6,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job Notification....
2018-09-29 11:20:00.000 INFO [leaderboard,25cab214549b76a6,25cab214549b76a6,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job executed at: 2018-09-29 11:20:00.0
2018-09-29 11:21:00.000 INFO [leaderboard,c2f241d8a806fd26,c2f241d8a806fd26,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job Notification....
2018-09-29 11:21:00.000 INFO [leaderboard,c2f241d8a806fd26,c2f241d8a806fd26,false] 7937 --- [ scheduling-1] i.s.l.task.CronClass : Cron-Job executed at: 2018-09-29 11:21:00.0

How to prevent Spring Batch Job to from being restarted on step fail?

I have simple single-step job:
#Bean(name = "restProcessorJob")
public Job job(#Qualifier("step") Step step) throws Exception {
return jobBuilderFactory.get("restProcessorJob")
.start(step)
.build();
}
And if there was exception during step execution, Batch framework will try to execute it again immediately.
2016-12-12 18:49:45.558 INFO 10872 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=restProcessorJob]] launched with the following parameters: [{id=1481568585432}]
2016-12-12 18:49:45.572 INFO 10872 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step]
2016-12-12 18:49:45.597 DEBUG 10872 --- [ main] o.s.web.client.RestTemplate : Created GET request for "http://myhost:myport.."
2016-12-12 18:49:45.646 DEBUG 10872 --- [ main] o.s.web.client.RestTemplate : Setting request Accept header to [application/json, application/*+json]
2016-12-12 18:49:46.670 ERROR 10872 --- [ main] o.s.batch.core.step.AbstractStep : Encountered an error executing step step in job restProcessorJob
org.springframework.web.client.ResourceAccessException: I/O error on GET request Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect
...
2016-12-12 18:49:46.689 INFO 10872 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=restProcessorJob]] completed with the following parameters: [{id=1481568585432}] and the following status: [FAILED]
2016-12-12 18:49:46.690 INFO 10872 --- [ main] o.s.b.a.b.JobLauncherCommandLineRunner : Running default command line with: []
2016-12-12 18:49:46.727 INFO 10872 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=restProcessorJob]] launched with the following parameters: [{id=1481568585432}]
2016-12-12 18:49:46.736 INFO 10872 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [step]
...
Is there any way to configure such behavior and do not run the job twice on fail?
EDIT:
It seems that I've found the source of problem. Spring boot application tries to launch every CommandLineRunner it founds in the context. In my case it is the org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner. I have been trying to exclude this class:
#EnableAutoConfiguration(excludeName="org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner")
But without success...
Use .preventRestart with an incrementer
#Bean(name = "restProcessorJob")
public Job job(#Qualifier("step") Step step) throws Exception {
return jobBuilderFactory.get("restProcessorJob")
.start(step)
.incrementer(new RunIdIncrementer())
.preventRestart()
.build();
}
You can configure it in your config file, you can put the skip limit to 0

Resources