Spring dynamic async - spring

Trying to have a dynamic number of async processes running in Spring 2.0.5. I do have it working on a fixed number but need the flexibility.
the current methods are:
in class AsyncFactoryService:
public void doAsyncBetter(long rowsPerBucket) throws InterruptedException, SQLException, ExecutionException {
List<Bucket> buckets = sourceData01Service.bucketsByRowsPerBucket(rowsPerBucket);
AsyncDataLoadService[] asyncDataLoadServiceArray = new AsyncDataLoadService[buckets.size()];
for(int i = 0; i < buckets.size(); i++) {
asyncDataLoadServiceArray[i] = new AsyncDataLoadService();
asyncDataLoadServiceArray[i].loadSourceData01(buckets.get(i));
}
}
in class AsyncDataLoadService
#Autowired SourceData01Service sourceData01Service;
#Async
public Future<String> loadSourceData01(Bucket bucket) throws InterruptedException, SQLException {
LOGGER.info(String.format("(loadSourceData01) [Thread id => %d, bucket => %s]",Thread.currentThread().getId(), bucket.toString()));
List<SourceData01> sourceData01s = null;
sourceData01s = sourceData01Service.rowsByBucket(bucket);
Thread.sleep(5000);
String info = String.format("finish sourceData01s.size() => %d, Thread id => %d", sourceData01s.size(), Thread.currentThread().getId());
return new AsyncResult<>(info);
}
this fails with
Caused by: java.lang.NullPointerException
at demo.service.AsyncDataLoadService.loadSourceData01(AsyncDataLoadService.java:35) ~[classes/:?]
at demo.service.AsyncFactoryService.doAsyncBetter(AsyncFactoryService.java:43) ~[classes/:?]
at demo.RunApp.run(RunApp.java:39) ~[classes/:?]
As it seems in the service the sourceData01Service is null.
When I try with a fixed number like
public void doAsync(long rowsPerBucket) throws InterruptedException, SQLException, ExecutionException {
List<Bucket> buckets = sourceData01Service.bucketsByRowsPerBucket(rowsPerBucket);
Future<String> process0 = asyncDataLoadService.loadSourceData01(buckets.get(0));
Future<String> process1 = asyncDataLoadService.loadSourceData01(buckets.get(1));
Future<String> process2 = asyncDataLoadService.loadSourceData01(buckets.get(2));
Future<String> process3 = asyncDataLoadService.loadSourceData01(buckets.get(3));
Future<String> process4 = asyncDataLoadService.loadSourceData01(buckets.get(4));
while(!(process0.isDone()) && !(process2.isDone()) && !(process3.isDone()) && !(process4.isDone())) {
Thread.sleep(2000);
}
LOGGER.info("Process 0 => " + process0.get());
LOGGER.info("Process 1 => " + process1.get());
LOGGER.info("Process 2 => " + process2.get());
LOGGER.info("Process 3 => " + process3.get());
LOGGER.info("Process 4 => " + process4.get());
}
it works as expected but in my case the number of buckets is dependent on the number of rows in the table so I do not know how many buckets there will be. Any ideas how to do this?

The service (sourceData01Service) injection in AsyncDataLoadService didn't happen as the application is creating the instance of AsyncDataLoadService (in the factory class) instead of spring creating it.
We can modify the implementation to let spring create the required instance.
#Component
class AsyncDataLoadService {
#Autowired SourceData01Service sourceData01Service;
#Async
public Future<String> loadSourceData01(Bucket bucket) throws InterruptedException, SQLException {
LOGGER.info(String.format("(loadSourceData01) [Thread id => %d, bucket => %s]",Thread.currentThread().getId(), bucket.toString()));
List<SourceData01> sourceData01s = null;
sourceData01s = sourceData01Service.rowsByBucket(bucket);
Thread.sleep(5000);
String info = String.format("finish sourceData01s.size() => %d, Thread id => %d", sourceData01s.size(), Thread.currentThread().getId());
return new AsyncResult<>(info);
}
}
We can then change the factory class implementation to autowire AsyncDataLoadService and submit the job to be executed (in this case loadSourceData01).
Since asyncDataLoadService.loadSourceData01 is annotated with #Async, every call to that method will be executed in a separate thread. By default spring use SimpleAsyncTaskExecutor that fires up new thread for every invocation. Configure thread pool implementation if required.
#Component
class AsyncFactoryService {
#Autowired
AsyncDataLoadService asyncDataLoadService;
public void doAsyncBetter(long rowsPerBucket) throws InterruptedException, SQLException, ExecutionException {
List<Bucket> buckets = sourceData01Service.bucketsByRowsPerBucket(rowsPerBucket);
for(int i = 0; i < buckets.size(); i++) {
asyncDataLoadService.loadSourceData01(buckets.get(i));
}
}
}

Related

Springboot Kafka #Listener consumer pause/resume not working

I have a springboot Kafka Consumer & Producer. The consumer is expected to read data from topic 1 by 1, process(time consuming) it & write it to another topic and then manually commit the offset.
In order to avoid rebalancing, I have tried to call pause() and resume() on KafkaContainer but the consumer is always running & never responds to pause() call, tried it even with a while loop and faced no success(unable to pause the consumer). KafkaListenerEndpointRegistry is Autowired.
Springboot version = 2.6.9, spring-kafka version = 2.8.7
#KafkaListener(id = "c1", topics = "${app.topics.topic1}", containerFactory = "listenerContainerFactory1")
public void poll(ConsumerRecord<String, String> record, Acknowledgment ack) {
log.info("Received Message by consumer of topic1: " + value);
String result = process(record.value());
producer.sendMessage(result + " topic2");
log.info("Message sent from " + topicIn + " to " + topicOut);
ack.acknowledge();
log.info("Offset committed by consumer 1");
}
private String process(String value) {
try {
pauseConsumer();
// Perform time intensive network IO operations
resumeConsumer();
} catch (InterruptedException e) {
log.error(e.getMessage());
}
return value;
}
private void pauseConsumer() throws InterruptedException {
if (registry.getListenerContainer("c1").isRunning()) {
log.info("Attempting to pause consumer");
Objects.requireNonNull(registry.getListenerContainer("c1")).pause();
Thread.sleep(5000);
log.info("kafkalistener container state - " + registry.getListenerContainer("c1").isRunning());
}
}
private void resumeConsumer() throws InterruptedException {
if (registry.getListenerContainer("c1").isContainerPaused() || registry.getListenerContainer("c1").isPauseRequested()) {
log.info("Attempting to resume consumer");
Objects.requireNonNull(registry.getListenerContainer("c1")).resume();
Thread.sleep(5000);
log.info("kafkalistener container state - " + registry.getListenerContainer("c1").isRunning());
}
}
Am I missing something? Could someone please guide me with the right way of achieving the required behaviour?
You are running the process() method on the listener thread so pause/resume will not have any effect; the pause only takes place when the listener thread exits the listener method (and after it has processed all the records received by the previous poll).
The next version (2.9), due later this month, has a new property pauseImmediate, which causes the pause to take effect after the current record is processed.
You can try like this. This work for me
public class kafkaConsumer {
public void run(String topicName) {
try {
Consumer<String, String> consumer = new KafkaConsumer<>(config);
consumer.subscribe(Collections.singleton(topicName));
while (true) {
try {
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(80000));
for (TopicPartition partition : consumerRecords.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = consumerRecords.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
kafkaEvent = record.value();
consumer.pause(consumer.assignment());
/** Implement Your Business Logic Here **/
Once your processing done
consumer.resume(consumer.assignment());
try {
consumer.commitSync();
} catch (CommitFailedException e) {
}
}
}
} catch (Exception e) {
continue;
}
}
} catch (Exception e) {
}
}

Springboot - Rest call from Scheduled task using Rest template throwing java.lang.IllegalStateException: No thread-bound request found

I am making a Rest call from scheduled task using resttemplate and I am getting the below exception
Exception java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. occurred while running scheduled task
Code snippet
#Service
#Slf4j
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class TestSchdeuledTaskService {
#Autowired
private TestServiceRestClient testService;
#Scheduled(fixedDelayString = "${scheduler.fixedDelay:60000}")
public void scheduledtask() {
try {
Thread.currentThread().setName("testscheduledtask");
testService.testRestCall();
} catch (Exception e) {
String logError = String.format("Exception %s occurred while running scheduled task ", e);
log.error(logError);
}
}
#Slf4j
#Service
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class TestServiceRestClient{
#Autowired
private RestTemplate restTemplate;
public void testRestCall() {
log.debug("Get call testing");
int min = 1;
int max = 500;
Random r = new Random();
int id = r.nextInt((max - min) + 1) + min;
Comment comment = restTemplate.getForObject("http://jsonplaceholder.typicode.com/comments/" + id, Comment.class);
log.debug(comment.toString());
}
}
Any pointer on why this exception is happening. Thanks

How to seek a particular offset in kafka listener method?

I am trying to seek offset from a SQL database in my kafka listener method .
I have used registerSeekCallback method in my code but this method gets invoked when we run the consumer (or container is started) . Let's say my consumer is running and last committed offset is 20 in MySql database. I manually change the last committed offset in Mysql database to 11 but my consumer will keep reading from 21 unless i restart my consumer(container restarted) . I am looking out for any option if i can override or seek offset in my listener method itself. Any help would be appreciated.
public class Listen implements ConsumerSeekAware
{
#Override
public void registerSeekCallback(ConsumerSeekCallback callback)
{
// fetching offset from a database
Integer offset = offsetService.getOffset();
callback.seek("topic-name",0,offset);
}
#KafkaListener(topics = "topic-name", groupId = "group")
public void listen(ConsumerRecord record Acknowledgment acknowledgment) throws Exception
{
// processing the record
acknowledgment.acknowledge(); //manually commiting the record
// committing the offset to MySQL database
}
}
Editing with new listener method :-
#KafkaListener(topics = "topic-name", groupId = "group")
public void listen(ConsumerRecord record Acknowledgment acknowledgment,
#Header(KafkaHeaders.CONSUMER) Consumer<?, ?> consumer)) throws Exception {
// seeking old offset stored in database (which is 11 )
consumer.seek(partition,offsetService.getOffset());
log.info("record offset is {} and value is {}" , record.offset(),record.value() );
acknowledgment.acknowledge();
}
In database my last committed offset is 11 and last committed offset on kafka end is 21. When i wrote a new record in kafka topic(i.e on offset 22) , my consumer triggers and processes 22 offset first then it goes back to seek offset 11 & start processing from there.
why is it consuming offset 22 first although i am seeking offset 11 ?
With my above code , every time i write a new message to my kafka top it processes that record first then it seeks the offset present in my database . Is there any way i can avoid that ?
There are several techniques in this answer.
Bear in mind that performing a seek on the consumer will not take effect until the next poll (any records fetched on the last poll will be sent to the consumer first).
EDIT
Here's an example:
#SpringBootApplication
public class So63429201Application {
public static void main(String[] args) {
SpringApplication.run(So63429201Application.class, args).close();
}
#Bean
public ApplicationRunner runner(KafkaTemplate<String, String> template, Listener listener) {
return args -> {
IntStream.range(0, 10).forEach(i -> template.send("so63429201", i % 3, null, "foo" + i));
Thread.sleep(8000);
listener.seekToTime(System.currentTimeMillis() - 11000);
Thread.sleep(8000);
listener.seekToOffset(new TopicPartition("so63429201", 0), 11);
Thread.sleep(8000);
};
}
#Bean
public NewTopic topic() {
return TopicBuilder.name("so63429201").partitions(3).replicas(1).build();
}
}
#Component
class Listener extends AbstractConsumerSeekAware {
#KafkaListener(id = "so63429201", topics = "so63429201", concurrency = "2")
public void listen(String in) {
System.out.println(in);
}
#Override
public void onPartitionsAssigned(Map<TopicPartition, Long> assignments, ConsumerSeekCallback callback) {
System.out.println(assignments);
super.onPartitionsAssigned(assignments, callback);
callback.seekToBeginning(assignments.keySet());
}
public void seekToTime(long time) {
getSeekCallbacks().forEach((tp, callback) -> callback.seekToTimestamp(tp.topic(), tp.partition(), time));
}
public void seekToOffset(TopicPartition tp, long offset) {
getSeekCallbackFor(tp).seek(tp.topic(), tp.partition(), offset);
}
}
Starting with spring kafka version 2.5.5, we can apply an initial offset to all assigned partitions:
#KafkaListener( groupId = "group_json", containerFactory = "userKafkaListenerFactory", topicPartitions =
{#org.springframework.kafka.annotation.TopicPartition(topic = "Kafka_Topic", partitions = {"0"},
partitionOffsets = #PartitionOffset(partition = "*", initialOffset = "3")),
#org.springframework.kafka.annotation.TopicPartition(topic = "Kafka_Topic_2", partitions = {"0"},
partitionOffsets = #PartitionOffset(partition = "*", initialOffset = "4"))
})
public void consumeJson(User user, ConsumerRecord<?, ?> consumerRecord, Acknowledgment acknowledgment) throws Exception {
/*
Reading the message into a String variable.
*/
String message = consumerRecord.value().toString();
}
Source: https://docs.spring.io/spring-kafka/docs/2.5.5.RELEASE/reference/html/#reference

Spring batch patitioning of db not working properly

I have configured a job as follow, which is to read from db and write into files but by partitioning data on basis of sequence.
//Job Config
#Bean
public Job job(JobBuilderFactory jobBuilderFactory) throws Exception {
Flow masterFlow1 = (Flow) new FlowBuilder<Object>("masterFlow1").start(masterStep()).build();
return (jobBuilderFactory.get("Partition-Job")
.incrementer(new RunIdIncrementer())
.start(masterFlow1)
.build()).build();
}
#Bean
public Step masterStep() throws Exception
{
return stepBuilderFactory.get(MASTERPPREPAREDATA)
//.listener(customSEL)
.partitioner(STEPPREPAREDATA,new DBPartitioner())
.step(prepareDataForS1())
.gridSize(gridSize)
.taskExecutor(new SimpleAsyncTaskExecutor("Thread"))
.build();
}
#Bean
public Step prepareDataForS1() throws Exception
{
return stepBuilderFactory.get(STEPPREPAREDATA)
//.listener(customSEL)
.<InputData,InputData>chunk(chunkSize)
.reader(JDBCItemReader(0,0))
.writer(writer(null))
.build();
}
#Bean(destroyMethod="")
#StepScope
public JdbcCursorItemReader<InputData> JDBCItemReader(#Value("#{stepExecutionContext[startingIndex]}") int startingIndex,
#Value("#{stepExecutionContext[endingIndex]}") int endingIndex)
{
JdbcCursorItemReader<InputData> ir = new JdbcCursorItemReader<>();
ir.setDataSource(batchDataSource);
ir.setMaxItemCount(DBPartitioner.partitionSize);
ir.setSaveState(false);
ir.setRowMapper(new InputDataRowMapper());
ir.setSql("SELECT * FROM FIF_INPUT fi WHERE fi.SEQ > ? AND fi.SEQ < ?");
ir.setPreparedStatementSetter(new PreparedStatementSetter() {
#Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setInt(1, startingIndex);
ps.setInt(2, endingIndex);
}
});
return ir;
}
#Bean
#StepScope
public FlatFileItemWriter<InputData> writer(#Value("#{stepExecutionContext[index]}") String index)
{
System.out.println("writer initialized!!!!!!!!!!!!!"+index);
//Create writer instance
FlatFileItemWriter<InputData> writer = new FlatFileItemWriter<>();
//Set output file location
writer.setResource(new FileSystemResource(batchDirectory+relativeInputDirectory+index+inputFileForS1));
//All job repetitions should "append" to same output file
writer.setAppendAllowed(false);
//Name field values sequence based on object properties
writer.setLineAggregator(customLineAggregator);
return writer;
}
Partitioner provided for partitioning db is written separately in other file so as follows
//PartitionDb.java
public class DBPartitioner implements Partitioner{
public static int partitionSize;
private static Log log = LogFactory.getLog(DBPartitioner.class);
#SuppressWarnings("unchecked")
#Override
public Map<String, ExecutionContext> partition(int gridSize) {
log.debug("START: Partition"+"grid size:"+gridSize);
#SuppressWarnings("rawtypes")
Map partitionMap = new HashMap<>();
int startingIndex = -1;
int endSize = partitionSize+1;
for(int i=0; i< gridSize; i++){
ExecutionContext ctxMap = new ExecutionContext();
ctxMap.putInt("startingIndex",startingIndex);
ctxMap.putInt("endingIndex", endSize);
ctxMap.put("index", i);
startingIndex = endSize-1;
endSize += partitionSize;
partitionMap.put("Thread:-"+i, ctxMap);
}
log.debug("END: Created Partitions of size: "+ partitionMap.size());
return partitionMap;
}
}
This one is executing properly but problem is even after partitioning on the basis of sequence i am getting same rows in multiple files which is not right as i am providing different set of data for each partition. Can anyone tell me whats wrong. I am using HikariCP for Db connection pooling and spring batch 4
This one is executing properly but problem is even after partitioning on the basis of sequence i am getting same rows in multiple files which is not right as i am providing different set of data for each partition.
I'm not sure your partitioner is working properly. A quick test shows that it is not providing different sets of data as you are claiming:
DBPartitioner dbPartitioner = new DBPartitioner();
Map<String, ExecutionContext> partition = dbPartitioner.partition(5);
for (String s : partition.keySet()) {
System.out.println(s + " : " + partition.get(s));
}
This prints:
Thread:-0 : {endingIndex=1, index=0, startingIndex=-1}
Thread:-1 : {endingIndex=1, index=1, startingIndex=0}
Thread:-2 : {endingIndex=1, index=2, startingIndex=0}
Thread:-3 : {endingIndex=1, index=3, startingIndex=0}
Thread:-4 : {endingIndex=1, index=4, startingIndex=0}
As you can see, almost all partitions will have the same startingIndex and endingIndex.
I recommend you unit test your partitioner before using it in a partitioned step.

Nullpointer injecting a bean when creating a job via quartz

The context is the next:
I have a web app using Spring 2.5 and Struts 1.1
I create a job dynamically in an Action using Quartz:
JobDetailBean jobDetail = new JobDetailBean();
jobDetail.setBeanName("foo");
Map<String, String> map = new HashMap<String,String>();
map.put("idFeed","foo");
map.put("idSite","foo");
jobDetail.setJobDataAsMap(map);
jobDetail.setJobClass(FeedJob.class);
jobDetail.afterPropertiesSet();
CronTriggerBean cronTrigger = new CronTriggerBean();
cronTrigger.setBeanName("foo");
String expression = " * * * * * *";
cronTrigger.setCronExpression(expression);
cronTrigger.afterPropertiesSet();
// add to schedule
scheduler.scheduleJob((JobDetail) jobDetail, cronTrigger);
scheduler is a org.quartz.Scheduler injected in the Action.
The class FeedJob has the method executeInternal(JobExecutionContext ctx) which is the code the job has to run:
public class FeedJob extends QuartzJobBean {
private FeedBL feedBL;
public void setFeedBL(FeedBL feedBL) {this.feedBL = feedBL;}
public FeedJob() {}
public String idFeed;
public String idSite;
public String getIdFeed() {
return idFeed;
}
public void setIdFeed(String idFeed) {
this.idFeed = idFeed;
}
public String getIdSite() {
return idSite;
}
public void setIdSite(String idSite) {
this.idSite = idSite;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
try {
feedBL.sincronizacionProductFeed(idFeed, idSite);
} catch (Exception e) {
e.printStackTrace();
}
}
}
And when its going to run, I get a java.lang.NullPointerException when trying to run this line of code:
feedBL.sincronizacionProductFeed(idFeed, idSite);
The reason is when I'm creating the job in the Action I'm setting the job:
jobDetail.setJobClass(FeedJob.class);
And Spring doesn't notice about the bean he has already created, so that instance of the FeedJob class hasn't god injected the feedBL class.
Any good idea for solving this problem?
I have tried to give the job the context like this:
jobDetail.setApplicationContext(applicationContext);
But doesnt work.
You may want to check this answer. It solves the same problem you are experiencing.

Resources