From d28888c80adeb08fd69637038e0213fe333645a7 Mon Sep 17 00:00:00 2001 From: Taeik Lim Date: Sun, 2 Feb 2025 23:35:42 +0900 Subject: [PATCH] Polish java module test codes Signed-off-by: Taeik Lim --- ...mReaderProcessorWriterIntegrationTest.java | 30 +- ...ItemStreamReaderWriterIntegrationTest.java | 21 +- .../ItemStreamFluxReaderProcessorIT.java | 266 +++++++++++ ...eamFluxReaderProcessorIntegrationTest.java | 381 --------------- ...ItemStreamFluxReaderProcessorWriterIT.java | 298 ++++++++++++ ...xReaderProcessorWriterIntegrationTest.java | 424 ----------------- .../adapter/ItemStreamFluxReaderWriterIT.java | 286 ++++++++++++ ...StreamFluxReaderWriterIntegrationTest.java | 400 ---------------- .../ItemStreamIterableReaderProcessorIT.java | 272 +++++++++++ ...terableReaderProcessorIntegrationTest.java | 394 ---------------- ...StreamIterableReaderProcessorWriterIT.java | 304 ++++++++++++ ...eReaderProcessorWriterIntegrationTest.java | 437 ------------------ .../ItemStreamIterableReaderWriterIT.java | 292 ++++++++++++ ...amIterableReaderWriterIntegrationTest.java | 413 ----------------- .../ItemStreamIteratorReaderProcessorIT.java | 270 +++++++++++ ...teratorReaderProcessorIntegrationTest.java | 394 ---------------- ...StreamIteratorReaderProcessorWriterIT.java | 304 ++++++++++++ ...rReaderProcessorWriterIntegrationTest.java | 437 ------------------ .../ItemStreamIteratorReaderWriterIT.java | 292 ++++++++++++ ...amIteratorReaderWriterIntegrationTest.java | 413 ----------------- .../ItemStreamSimpleReaderProcessorIT.java | 250 ++++++++++ ...mSimpleReaderProcessorIntegrationTest.java | 365 --------------- ...emStreamSimpleReaderProcessorWriterIT.java | 285 ++++++++++++ ...eReaderProcessorWriterIntegrationTest.java | 408 ---------------- .../ItemStreamSimpleReaderWriterIT.java | 272 +++++++++++ ...reamSimpleReaderWriterIntegrationTest.java | 384 --------------- .../plus/item/adapter/AdapterFactoryTest.java | 4 +- .../adapter/ItemProcessorAdapterTest.java | 5 +- .../adapter/ItemStreamReaderAdapterTest.java | 6 +- .../adapter/ItemStreamWriterAdapterTest.java | 5 +- .../StepScopeItemStreamReaderTest.java | 3 +- .../plus/job/ClearRunIdIncrementerTest.java | 28 +- .../plus/step/adapter/AdapterFactoryTest.java | 46 +- .../adapter/ItemProcessorAdapterTest.java | 16 +- .../ItemStreamFluxReaderAdapterTest.java | 29 +- .../ItemStreamIterableReaderAdapterTest.java | 29 +- .../ItemStreamIteratorReaderAdapterTest.java | 31 +- .../ItemStreamSimpleReaderAdapterTest.java | 42 +- .../adapter/ItemStreamWriterAdapterTest.java | 57 +-- .../StepScopeItemStreamReaderTest.java | 42 +- 40 files changed, 3520 insertions(+), 5115 deletions(-) create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIntegrationTest.java create mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIT.java delete mode 100644 spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIntegrationTest.java diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderProcessorWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderProcessorWriterIntegrationTest.java index 9db9bbff..48dd63b2 100644 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderProcessorWriterIntegrationTest.java +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderProcessorWriterIntegrationTest.java @@ -18,9 +18,6 @@ package com.navercorp.spring.batch.plus.item.adapter; -import static com.navercorp.spring.batch.plus.item.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.item.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.item.adapter.AdapterFactory.itemStreamWriter; import static org.assertj.core.api.Assertions.assertThat; import java.util.UUID; @@ -56,7 +53,8 @@ import reactor.core.publisher.Flux; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class ItemStreamReaderProcessorWriterIntegrationTest { private static final int TEST_REPEAT_COUNT = 5; @@ -112,9 +110,9 @@ void testReaderProcessorWriter() throws Exception { .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .processor(AdapterFactory.itemProcessor(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -157,9 +155,9 @@ void testReaderProcessorWriterWithSameTaskletShouldKeepContext() throws Exceptio .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .processor(AdapterFactory.itemProcessor(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -198,9 +196,9 @@ void testStepScopeReaderProcessorWriter() throws Exception { .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .processor(AdapterFactory.itemProcessor(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -243,9 +241,9 @@ void testStepScopeReaderProcessorWriterWithSameTaskletShouldNotKeepCountContext( .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .processor(AdapterFactory.itemProcessor(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderWriterIntegrationTest.java index 8787e963..33411f26 100644 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderWriterIntegrationTest.java +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderWriterIntegrationTest.java @@ -18,8 +18,6 @@ package com.navercorp.spring.batch.plus.item.adapter; -import static com.navercorp.spring.batch.plus.item.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.item.adapter.AdapterFactory.itemStreamWriter; import static org.assertj.core.api.Assertions.assertThat; import java.util.UUID; @@ -55,7 +53,8 @@ import reactor.core.publisher.Flux; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class ItemStreamReaderWriterIntegrationTest { private static final int TEST_REPEAT_COUNT = 5; @@ -107,8 +106,8 @@ void testReaderWriter() throws Exception { .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -151,8 +150,8 @@ void testReaderWriterWithSameTaskletShouldKeepCountContext() throws Exception { .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -190,8 +189,8 @@ void testStepScopeReaderWriter() throws Exception { .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); @@ -234,8 +233,8 @@ void testStepScopeReaderWriterWithSameTaskletShouldNotKeepCountContext() throws .start( new StepBuilder("testStep", jobRepository) .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) + .reader(AdapterFactory.itemStreamReader(testTasklet)) + .writer(AdapterFactory.itemStreamWriter(testTasklet)) .build() ) .build(); diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIT.java new file mode 100644 index 00000000..cdd24800 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIT.java @@ -0,0 +1,266 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +import reactor.core.publisher.Flux; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamFluxReaderProcessorIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderProcessorShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderProcessorShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamFluxReaderProcessor { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Flux readFlux(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return Flux.generate(sink -> { + if (count < itemCount) { + sink.next(count); + ++count; + } else { + sink.complete(); + } + }); + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIntegrationTest.java deleted file mode 100644 index 34db8e94..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorIntegrationTest.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamWriter; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -import reactor.core.publisher.Flux; - -@SuppressWarnings("unchecked") -class ItemStreamFluxReaderProcessorIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamFluxReaderProcessorIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - - logger.debug("itemCount: {}, chunkCount: {}", itemCount, chunkCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamFluxReaderProcessor testTasklet() { - return new ItemStreamFluxReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamFluxReaderProcessor stepScopeTestTasklet() { - return new ItemStreamFluxReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamWriter emptyItemStreamWriter() { - return chunk -> { /* noop */ }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIT.java new file mode 100644 index 00000000..8d1a7685 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIT.java @@ -0,0 +1,298 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +import reactor.core.publisher.Flux; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamFluxReaderProcessorWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderProcessorWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderProcessorWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamFluxReaderProcessorWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Flux readFlux(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return Flux.generate(sink -> { + if (count < itemCount) { + sink.next(count); + ++count; + } else { + sink.complete(); + } + }); + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIntegrationTest.java deleted file mode 100644 index 48780657..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderProcessorWriterIntegrationTest.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -import reactor.core.publisher.Flux; - -@SuppressWarnings("unchecked") -class ItemStreamFluxReaderProcessorWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamFluxReaderProcessorWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriterWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamFluxReaderProcessorWriter testTasklet() { - return new ItemStreamFluxReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamFluxReaderProcessorWriter stepScopeTestTasklet() { - return new ItemStreamFluxReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIT.java new file mode 100644 index 00000000..ecd48047 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIT.java @@ -0,0 +1,286 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +import reactor.core.publisher.Flux; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamFluxReaderWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void fluxReaderWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamFluxReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamFluxReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamFluxReaderWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Flux readFlux(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return Flux.generate(sink -> { + if (count < itemCount) { + sink.next(count); + ++count; + } else { + sink.complete(); + } + }); + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIntegrationTest.java deleted file mode 100644 index 2b6c2c3b..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderWriterIntegrationTest.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -import reactor.core.publisher.Flux; - -@SuppressWarnings("unchecked") -class ItemStreamFluxReaderWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger(ItemStreamFluxReaderWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriterWithSameTaskletShouldKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamFluxReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamFluxReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamFluxReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamFluxReaderWriter testTasklet() { - return new ItemStreamFluxReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamFluxReaderWriter stepScopeTestTasklet() { - return new ItemStreamFluxReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Flux readFlux(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return Flux.generate(sink -> { - if (count < itemCount) { - sink.next(count); - ++count; - } else { - sink.complete(); - } - }); - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIT.java new file mode 100644 index 00000000..23e07601 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIT.java @@ -0,0 +1,272 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIterableReaderProcessorIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderProcessorShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderProcessorShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIterableReaderProcessor { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterable readIterable(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return () -> new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIntegrationTest.java deleted file mode 100644 index 09e1412f..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorIntegrationTest.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamWriter; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIterableReaderProcessorIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamIterableReaderProcessorIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - - logger.debug("itemCount: {}, chunkCount: {}", itemCount, chunkCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIterableReaderProcessor testTasklet() { - return new ItemStreamIterableReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamIterableReaderProcessor stepScopeTestTasklet() { - return new ItemStreamIterableReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamWriter emptyItemStreamWriter() { - return chunk -> { /* noop */ }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIT.java new file mode 100644 index 00000000..91d83484 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIT.java @@ -0,0 +1,304 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIterableReaderProcessorWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderProcessorWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderProcessorWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIterableReaderProcessorWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterable readIterable(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return () -> new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIntegrationTest.java deleted file mode 100644 index 0107fe16..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderProcessorWriterIntegrationTest.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIterableReaderProcessorWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamIterableReaderProcessorWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriterWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIterableReaderProcessorWriter testTasklet() { - return new ItemStreamIterableReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamIterableReaderProcessorWriter stepScopeTestTasklet() { - return new ItemStreamIterableReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIT.java new file mode 100644 index 00000000..fd5f062d --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIT.java @@ -0,0 +1,292 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIterableReaderWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iterableReaderWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIterableReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamIterableReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIterableReaderWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterable readIterable(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return () -> new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIntegrationTest.java deleted file mode 100644 index b43afb98..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderWriterIntegrationTest.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIterableReaderWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger(ItemStreamIterableReaderWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriterWithSameTaskletShouldKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIterableReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIterableReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIterableReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIterableReaderWriter testTasklet() { - return new ItemStreamIterableReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamIterableReaderWriter stepScopeTestTasklet() { - return new ItemStreamIterableReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterable readIterable(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return () -> new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIT.java new file mode 100644 index 00000000..8193f12e --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIT.java @@ -0,0 +1,270 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIteratorReaderProcessorIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderProcessorShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderProcessorShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIteratorReaderProcessor { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterator readIterator(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIntegrationTest.java deleted file mode 100644 index e5ff4afa..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorIntegrationTest.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamWriter; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIteratorReaderProcessorIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamIteratorReaderProcessorIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - - logger.debug("itemCount: {}, chunkCount: {}", itemCount, chunkCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIteratorReaderProcessor testTasklet() { - return new ItemStreamIteratorReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamIteratorReaderProcessor stepScopeTestTasklet() { - return new ItemStreamIteratorReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamWriter emptyItemStreamWriter() { - return chunk -> { /* noop */ }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIT.java new file mode 100644 index 00000000..34329980 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIT.java @@ -0,0 +1,304 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIteratorReaderProcessorWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderProcessorWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderProcessorWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIteratorReaderProcessorWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterator readIterator(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIntegrationTest.java deleted file mode 100644 index 603acfa9..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderProcessorWriterIntegrationTest.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIteratorReaderProcessorWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamIteratorReaderProcessorWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriterWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIteratorReaderProcessorWriter testTasklet() { - return new ItemStreamIteratorReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamIteratorReaderProcessorWriter stepScopeTestTasklet() { - return new ItemStreamIteratorReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIT.java new file mode 100644 index 00000000..8058a921 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIT.java @@ -0,0 +1,292 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamIteratorReaderWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void iteratorReaderWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamIteratorReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamIteratorReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // read context should be invoked + assertThat(invokeCountContext.readContextCallCount).isEqualTo(repeatCount); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamIteratorReaderWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @NonNull + @Override + public Iterator readIterator(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.readContextCallCount++; + return new Iterator<>() { + @Override + public boolean hasNext() { + return count < itemCount; + } + + @Override + public Integer next() { + if (count < itemCount) { + return count++; + } else { + return null; + } + } + }; + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int readContextCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIntegrationTest.java deleted file mode 100644 index 16994d61..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderWriterIntegrationTest.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Iterator; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamIteratorReaderWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger(ItemStreamIteratorReaderWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int readContextCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - readContextCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriterWithSameTaskletShouldKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamIteratorReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(readContextCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamIteratorReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamIteratorReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamIteratorReaderWriter testTasklet() { - return new ItemStreamIteratorReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamIteratorReaderWriter stepScopeTestTasklet() { - return new ItemStreamIteratorReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @NonNull - @Override - public Iterator readIterator(@NonNull ExecutionContext executionContext) { - ++readContextCallCount; - return new Iterator<>() { - @Override - public boolean hasNext() { - return count < itemCount; - } - - @Override - public Integer next() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - }; - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIT.java new file mode 100644 index 00000000..16ee812b --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIT.java @@ -0,0 +1,250 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamSimpleReaderProcessorIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderProcessorShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderProcessorShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderProcessor testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderProcessor.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer($ -> { + }) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet(InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet(InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamSimpleReaderProcessor { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @Override + public Integer read() { + if (this.count < this.itemCount) { + return this.count++; + } else { + return null; + } + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIntegrationTest.java deleted file mode 100644 index 2f58dcc3..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorIntegrationTest.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamWriter; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamSimpleReaderProcessorIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamSimpleReaderProcessorIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - - logger.debug("itemCount: {}, chunkCount: {}", itemCount, chunkCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessor testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessor() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessor testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderProcessor.class); - ItemStreamWriter emptyItemStreamWriter = context.getBean( - "emptyItemStreamWriter", - ItemStreamWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(emptyItemStreamWriter) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamSimpleReaderProcessor testTasklet() { - return new ItemStreamSimpleReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamSimpleReaderProcessor stepScopeTestTasklet() { - return new ItemStreamSimpleReaderProcessor<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - }; - } - - @Bean - @StepScope - ItemStreamWriter emptyItemStreamWriter() { - return chunk -> { /* noop */ }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIT.java new file mode 100644 index 00000000..59816027 --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIT.java @@ -0,0 +1,285 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamSimpleReaderProcessorWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderProcessorWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + assertThat(invokeCountContext.processCallCount).isEqualTo(repeatCount * itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderProcessorWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderProcessorWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .processor(itemProcessor(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // process, write should be invoked only once per iteration + assertThat(invokeCountContext.processCallCount).isEqualTo(itemCount); + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet( + InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet(InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamSimpleReaderProcessorWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @Override + public Integer read() { + if (this.count < this.itemCount) { + return this.count++; + } else { + return null; + } + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public Integer process(@NonNull Integer item) { + this.invokeCountContext.processCallCount++; + return item; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int processCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIntegrationTest.java deleted file mode 100644 index 20ac7e91..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderProcessorWriterIntegrationTest.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemProcessor; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamSimpleReaderProcessorWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger( - ItemStreamSimpleReaderProcessorWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int processCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - processCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderProcessorWriterWithSameTaskletShouldKeepContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(processCallCount).isEqualTo(itemCount); - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderProcessorWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderProcessorWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderProcessorWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .processor(itemProcessor(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(processCallCount).isEqualTo(beforeRepeatCount * itemCount + itemCount); - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamSimpleReaderProcessorWriter testTasklet() { - return new ItemStreamSimpleReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamSimpleReaderProcessorWriter stepScopeTestTasklet() { - return new ItemStreamSimpleReaderProcessorWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public Integer process(@NonNull Integer item) { - ++processCallCount; - return item; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIT.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIT.java new file mode 100644 index 00000000..8ef4948a --- /dev/null +++ b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIT.java @@ -0,0 +1,272 @@ +/* + * Spring Batch Plus + * + * Copyright 2022-present NAVER Corp. + * + * 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 com.navercorp.spring.batch.plus.step.adapter; + +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; +import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.RepeatedTest; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.lang.NonNull; +import org.springframework.transaction.TransactionManager; + +@SuppressWarnings({"unchecked", "unused"}) +class ItemStreamSimpleReaderWriterIT { + + private static final int TEST_REPEAT_COUNT = 5; + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderWriterShouldNotKeepCountWhenStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(StepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(repeatCount * writeCountPerIteration); + } + + @RepeatedTest(TEST_REPEAT_COUNT) + void simpleReaderWriterShouldKeepCountWhenNotStepScoped() throws Exception { + int itemCount = ThreadLocalRandom.current().nextInt(10, 100); + int chunkCount = ThreadLocalRandom.current().nextInt(1, 10); + InvokeCountContext invokeCountContext = new InvokeCountContext(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("itemCount", Integer.class, () -> itemCount); + context.registerBean("invokeCountContext", InvokeCountContext.class, () -> invokeCountContext); + context.register(NotStepScopedConfiguration.class); + context.refresh(); + ItemStreamSimpleReaderWriter testTasklet = context.getBean("testTasklet", + ItemStreamSimpleReaderWriter.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + Job job = new JobBuilder("testJob", jobRepository) + .start( + new StepBuilder("testStep", jobRepository) + .chunk(chunkCount, new ResourcelessTransactionManager()) + .reader(itemStreamReader(testTasklet)) + .writer(itemStreamWriter(testTasklet)) + .build() + ) + .build(); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + + int repeatCount = ThreadLocalRandom.current().nextInt(1, 5); + List jobExecutions = new ArrayList<>(); + for (int i = 0; i < repeatCount; ++i) { + JobParameters jobParameters = new JobParametersBuilder() + .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + jobExecutions.add(jobExecution); + } + + assertThat(jobExecutions).allSatisfy(it -> assertThat(it.getStatus()).isEqualTo(BatchStatus.COMPLETED)); + // stream callback should be invoked + assertThat(invokeCountContext.onOpenReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateReadCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseReadCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onOpenWriteCallCount).isEqualTo(repeatCount); + assertThat(invokeCountContext.onUpdateWriteCallCount).isGreaterThanOrEqualTo(repeatCount); + assertThat(invokeCountContext.onCloseWriteCallCount).isEqualTo(repeatCount); + // write should be invoked only once per iteration + int writeCountPerIteration = (int)Math.ceil((double)itemCount / (double)chunkCount); + assertThat(invokeCountContext.writeCallCount).isEqualTo(writeCountPerIteration); + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class StepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @StepScope + @Bean + TestTasklet testTasklet(InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + @EnableBatchProcessing( + dataSourceRef = "metadataDataSource", + transactionManagerRef = "metadataTransactionManager" + ) + private static class NotStepScopedConfiguration { + + @Bean + TransactionManager metadataTransactionManager() { + return new DataSourceTransactionManager(metadataDataSource()); + } + + @Bean + DataSource metadataDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("/org/springframework/batch/core/schema-h2.sql") + .generateUniqueName(true) + .build(); + } + + @Bean + TestTasklet testTasklet(InvokeCountContext invokeCountContext, int itemCount) { + return new TestTasklet(invokeCountContext, itemCount); + } + } + + private static class TestTasklet implements ItemStreamSimpleReaderWriter { + + private int count = 0; + private final InvokeCountContext invokeCountContext; + private final int itemCount; + + public TestTasklet(InvokeCountContext invokeCountContext, int itemCount) { + this.invokeCountContext = invokeCountContext; + this.itemCount = itemCount; + } + + @Override + public void onOpenRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenReadCallCount++; + } + + @Override + public Integer read() { + if (this.count < this.itemCount) { + return this.count++; + } else { + return null; + } + } + + @Override + public void onUpdateRead(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateReadCallCount++; + } + + @Override + public void onCloseRead() { + this.invokeCountContext.onCloseReadCallCount++; + } + + @Override + public void onOpenWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onOpenWriteCallCount++; + } + + @Override + public void write(@NonNull Chunk chunk) { + this.invokeCountContext.writeCallCount++; + } + + @Override + public void onUpdateWrite(@NonNull ExecutionContext executionContext) { + this.invokeCountContext.onUpdateWriteCallCount++; + } + + @Override + public void onCloseWrite() { + this.invokeCountContext.onCloseWriteCallCount++; + } + } + + private static class InvokeCountContext { + int onOpenReadCallCount = 0; + int onUpdateReadCallCount = 0; + int onCloseReadCallCount = 0; + int onOpenWriteCallCount = 0; + int writeCallCount = 0; + int onUpdateWriteCallCount = 0; + int onCloseWriteCallCount = 0; + } +} diff --git a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIntegrationTest.java b/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIntegrationTest.java deleted file mode 100644 index f9d9812b..00000000 --- a/spring-batch-plus/src/integrationTest/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderWriterIntegrationTest.java +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Spring Batch Plus - * - * Copyright 2022-present NAVER Corp. - * - * 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 com.navercorp.spring.batch.plus.step.adapter; - -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamReader; -import static com.navercorp.spring.batch.plus.step.adapter.AdapterFactory.itemStreamWriter; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.lang.NonNull; -import org.springframework.transaction.TransactionManager; - -@SuppressWarnings("unchecked") -class ItemStreamSimpleReaderWriterIntegrationTest { - - private static final int TEST_REPEAT_COUNT = 5; - - private static final Logger logger = LoggerFactory.getLogger(ItemStreamSimpleReaderWriterIntegrationTest.class); - - private static int onOpenReadCallCount = 0; - private static int onUpdateReadCallCount = 0; - private static int onCloseReadCallCount = 0; - - private static int onOpenWriteCallCount = 0; - private static int writeCallCount = 0; - private static int onUpdateWriteCallCount = 0; - private static int onCloseWriteCallCount = 0; - - private static int itemCount = 0; - private static int chunkCount = 0; - private static int expectedWriteCount = 0; - - @BeforeEach - void beforeEach() { - onOpenReadCallCount = 0; - onUpdateReadCallCount = 0; - onCloseReadCallCount = 0; - - onOpenWriteCallCount = 0; - writeCallCount = 0; - onUpdateWriteCallCount = 0; - onCloseWriteCallCount = 0; - - itemCount = ThreadLocalRandom.current().nextInt(10, 100); - chunkCount = ThreadLocalRandom.current().nextInt(1, 10); - expectedWriteCount = (int)Math.ceil((double)itemCount / (double)chunkCount); - - logger.debug("itemCount: {}, chunkCount: {}, writeCount: {}", itemCount, chunkCount, expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testReaderWriterWithSameTaskletShouldKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderWriter testTasklet = context.getBean( - "testTasklet", - ItemStreamSimpleReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // it's not changed since it keeps 'count' in a bean - assertThat(writeCallCount).isEqualTo(expectedWriteCount); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriter() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - assertThat(onOpenReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateReadCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseReadCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onOpenWriteCallCount).isEqualTo(beforeRepeatCount + 1); - assertThat(onUpdateWriteCallCount).isGreaterThanOrEqualTo(beforeRepeatCount + 1); - assertThat(onCloseWriteCallCount).isEqualTo(beforeRepeatCount + 1); - } - - @RepeatedTest(TEST_REPEAT_COUNT) - void testStepScopeReaderWriterWithSameTaskletShouldNotKeepCountContext() throws Exception { - // given - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); - JobRepository jobRepository = context.getBean(JobRepository.class); - ItemStreamSimpleReaderWriter testTasklet = context.getBean( - "stepScopeTestTasklet", - ItemStreamSimpleReaderWriter.class); - Job job = new JobBuilder("testJob", jobRepository) - .start( - new StepBuilder("testStep", jobRepository) - .chunk(chunkCount, new ResourcelessTransactionManager()) - .reader(itemStreamReader(testTasklet)) - .writer(itemStreamWriter(testTasklet)) - .build() - ) - .build(); - JobLauncher jobLauncher = context.getBean(JobLauncher.class); - int beforeRepeatCount = ThreadLocalRandom.current().nextInt(0, 3); - for (int i = 0; i < beforeRepeatCount; ++i) { - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - jobLauncher.run(job, jobParameters); - } - logger.debug("beforeRepeatCount: {}", beforeRepeatCount); - - // when - JobParameters jobParameters = new JobParametersBuilder() - .addString(UUID.randomUUID().toString(), UUID.randomUUID().toString()) - .toJobParameters(); - JobExecution jobExecution = jobLauncher.run(job, jobParameters); - - // then - assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); - // 'count' field is isolated per job instances since it is step scoped. so count is 0 for all job instances - assertThat(writeCallCount).isEqualTo(beforeRepeatCount * expectedWriteCount + expectedWriteCount); - } - - @SuppressWarnings("unused") - @EnableBatchProcessing( - dataSourceRef = "metadataDataSource", - transactionManagerRef = "metadataTransactionManager" - ) - static class TestConfiguration { - - @Bean - TransactionManager metadataTransactionManager() { - return new DataSourceTransactionManager(metadataDataSource()); - } - - @Bean - DataSource metadataDataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("/org/springframework/batch/core/schema-h2.sql") - .generateUniqueName(true) - .build(); - } - - @Bean - ItemStreamSimpleReaderWriter testTasklet() { - return new ItemStreamSimpleReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - - @Bean - @StepScope - ItemStreamSimpleReaderWriter stepScopeTestTasklet() { - return new ItemStreamSimpleReaderWriter<>() { - - private int count = 0; - - @Override - public void onOpenRead(@NonNull ExecutionContext executionContext) { - ++onOpenReadCallCount; - } - - @Override - public Integer read() { - if (count < itemCount) { - return count++; - } else { - return null; - } - } - - @Override - public void onUpdateRead(@NonNull ExecutionContext executionContext) { - ++onUpdateReadCallCount; - } - - @Override - public void onCloseRead() { - ++onCloseReadCallCount; - } - - @Override - public void onOpenWrite(@NonNull ExecutionContext executionContext) { - ++onOpenWriteCallCount; - } - - @Override - public void write(@NonNull Chunk chunk) { - ++writeCallCount; - } - - @Override - public void onUpdateWrite(@NonNull ExecutionContext executionContext) { - ++onUpdateWriteCallCount; - } - - @Override - public void onCloseWrite() { - ++onCloseWriteCallCount; - } - }; - } - } -} diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/AdapterFactoryTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/AdapterFactoryTest.java index d44bb52d..85be5db0 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/AdapterFactoryTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/AdapterFactoryTest.java @@ -28,6 +28,8 @@ import reactor.core.publisher.Flux; +// note: it's deprecated. Do not change it. +@SuppressWarnings("deprecation") class AdapterFactoryTest { @Test @@ -61,7 +63,7 @@ void testItemWriter() { assertThat(actual).isInstanceOf(ItemStreamWriterAdapter.class); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test void testPassingNull() { assertThatThrownBy(() -> AdapterFactory.itemStreamReader((ItemStreamReaderDelegate)null)); diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemProcessorAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemProcessorAdapterTest.java index 9a99030a..2dc64e0d 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemProcessorAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemProcessorAdapterTest.java @@ -29,7 +29,8 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.item.ItemProcessor; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class ItemProcessorAdapterTest { @Test @@ -47,7 +48,7 @@ void testProcess() throws Exception { assertThat(actual).isEqualTo(expected); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test void testPassingNull() { // when, then diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderAdapterTest.java index 9e30b649..3fd76852 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamReaderAdapterTest.java @@ -35,10 +35,10 @@ import reactor.core.publisher.Flux; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class ItemStreamReaderAdapterTest { - @Test void testOpen() { // given @@ -110,7 +110,7 @@ void testClose() { verify(delegate, times(1)).onCloseRead(); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test void testPassingNull() { // when, then diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamWriterAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamWriterAdapterTest.java index ecd32b58..90392b9c 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamWriterAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/ItemStreamWriterAdapterTest.java @@ -29,7 +29,8 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamWriter; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class ItemStreamWriterAdapterTest { @Test @@ -84,7 +85,7 @@ void testClose() { verify(delegate, times(1)).onCloseWrite(); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test void testPassingNull() { // when, then diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/StepScopeItemStreamReaderTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/StepScopeItemStreamReaderTest.java index c724af63..cb6cd789 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/StepScopeItemStreamReaderTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/item/adapter/StepScopeItemStreamReaderTest.java @@ -35,7 +35,8 @@ import org.springframework.batch.test.MetaDataInstanceFactory; import org.springframework.batch.test.StepScopeTestUtils; -@SuppressWarnings("unchecked") +// note: it's deprecated. Do not change it. +@SuppressWarnings({"unchecked", "deprecation"}) class StepScopeItemStreamReaderTest { @Test diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/job/ClearRunIdIncrementerTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/job/ClearRunIdIncrementerTest.java index 25eb1e01..07cc0a0d 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/job/ClearRunIdIncrementerTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/job/ClearRunIdIncrementerTest.java @@ -18,7 +18,6 @@ package com.navercorp.spring.batch.plus.job; -import static com.navercorp.spring.batch.plus.job.ClearRunIdIncrementer.DEFAULT_RUN_ID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -33,54 +32,45 @@ class ClearRunIdIncrementerTest { @Test - void testGetNextReturnsOneWhenNoPreviousOne() { - // given + void getNextShouldReturnOneWhenNoPreviousValue() { JobParametersIncrementer clearRunIdIncrementer = ClearRunIdIncrementer.create(); - // when JobParameters jobParameters = clearRunIdIncrementer.getNext(new JobParameters()); - // then - assertThat(jobParameters.getLong(DEFAULT_RUN_ID)).isEqualTo(1L); + assertThat(jobParameters.getLong("run.id")).isEqualTo(1L); } @Test - void testGetNextReturnsNextValue() { - // given + void getNextShouldReturnPlusOneWhenPreviousExists() { JobParametersIncrementer clearRunIdIncrementer = ClearRunIdIncrementer.create(); - // when long previousId = ThreadLocalRandom.current().nextLong(); JobParameters parameters = new JobParametersBuilder() - .addLong(DEFAULT_RUN_ID, previousId) + .addLong("run.id", previousId) .toJobParameters(); JobParameters jobParameters = clearRunIdIncrementer.getNext(parameters); - // then - assertThat(jobParameters.getLong(DEFAULT_RUN_ID)).isEqualTo(previousId + 1); + assertThat(jobParameters.getLong("run.id")).isEqualTo(previousId + 1); } @Test - void testCustomRunId() { - // given + void getNextShouldReturnPlusOneWhenPreviousExistsUsingCustomRunId() { String runId = UUID.randomUUID().toString(); JobParametersIncrementer clearRunIdIncrementer = ClearRunIdIncrementer.create(runId); - // when long previousId = ThreadLocalRandom.current().nextLong(); JobParameters parameters = new JobParametersBuilder() .addLong(runId, previousId) .toJobParameters(); JobParameters jobParameters = clearRunIdIncrementer.getNext(parameters); - // then assertThat(jobParameters.getLong(runId)).isEqualTo(previousId + 1); - assertThat(jobParameters.getLong(DEFAULT_RUN_ID)).isNull(); + assertThat(jobParameters.getLong("run.id")).isNull(); } + @SuppressWarnings("DataFlowIssue") @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ClearRunIdIncrementer.create(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/AdapterFactoryTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/AdapterFactoryTest.java index 93fd4b55..2cf19dde 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/AdapterFactoryTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/AdapterFactoryTest.java @@ -34,75 +34,57 @@ class AdapterFactoryTest { @Test - void testItemReaderWithFlux() { - // when + void itemStreamReaderShouldReturnStepScopedOneWhenPassingItemReaderWithFluxDelegate() { ItemStreamFluxReaderDelegate delegate = executionContext -> Flux.empty(); - ItemStreamReader actual = AdapterFactory.itemStreamReader( - delegate); + ItemStreamReader actual = AdapterFactory.itemStreamReader(delegate); - // then assertThat(actual).isInstanceOf(StepScopeItemStreamReader.class); } @Test - void testItemReaderWithIterable() { - // when + void itemStreamReaderShouldReturnStepScopedOneWhenPassingItemReaderWithIterableDelegate() { ItemStreamIterableReaderDelegate delegate = executionContext -> List.of(); - ItemStreamReader actual = AdapterFactory.itemStreamReader( - delegate); + ItemStreamReader actual = AdapterFactory.itemStreamReader(delegate); - // then assertThat(actual).isInstanceOf(StepScopeItemStreamReader.class); } @Test - void testItemReaderWithIterator() { - // when + void itemStreamReaderShouldReturnStepScopedOneWhenPassingItemReaderWithIteratorDelegate() { ItemStreamIteratorReaderDelegate delegate = executionContext -> Collections.emptyIterator(); - ItemStreamReader actual = AdapterFactory.itemStreamReader( - delegate); + ItemStreamReader actual = AdapterFactory.itemStreamReader(delegate); - // then assertThat(actual).isInstanceOf(StepScopeItemStreamReader.class); } @Test - void testItemReaderWithSimple() { - // when + void itemStreamReaderShouldReturnStepScopedOneWhenPassingItemReaderWithSimpleDelegate() { ItemStreamSimpleReaderDelegate delegate = () -> null; - ItemStreamReader actual = AdapterFactory.itemStreamReader( - delegate); + ItemStreamReader actual = AdapterFactory.itemStreamReader(delegate); - // then assertThat(actual).isInstanceOf(StepScopeItemStreamReader.class); } @Test - void testItemProcessor() { - // when + void itemProcessorShouldReturnAdapterWhenPassingProcessorDelegate() { ItemProcessorDelegate delegate = item -> null; - ItemProcessor actual = AdapterFactory.itemProcessor( - delegate); + ItemProcessor actual = AdapterFactory.itemProcessor(delegate); - // then assertThat(actual).isInstanceOf(ItemProcessorAdapter.class); } @Test - void testItemWriter() { - // when + void itemStreamWriterShouldReturnAdapterWhenPassingWriterDelegate() { ItemStreamWriterDelegate delegate = items -> { }; - ItemStreamWriter actual = AdapterFactory.itemStreamWriter( - delegate); + ItemStreamWriter actual = AdapterFactory.itemStreamWriter(delegate); - // then assertThat(actual).isInstanceOf(ItemStreamWriterAdapter.class); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test - void testPassingNull() { + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> AdapterFactory.itemStreamReader((ItemStreamFluxReaderDelegate)null)); assertThatThrownBy(() -> AdapterFactory.itemStreamReader((ItemStreamIterableReaderDelegate)null)); assertThatThrownBy(() -> AdapterFactory.itemStreamReader((ItemStreamIteratorReaderDelegate)null)); diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemProcessorAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemProcessorAdapterTest.java index 62c74d8d..2719cf57 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemProcessorAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemProcessorAdapterTest.java @@ -33,26 +33,20 @@ class ItemProcessorAdapterTest { @Test - void testProcess() throws Exception { - // given + void processShouldReturnValueFromDelegate() throws Exception { Integer expected = ThreadLocalRandom.current().nextInt(); - ItemProcessorDelegate delegate = mock( - ItemProcessorDelegate.class); + ItemProcessorDelegate delegate = mock(ItemProcessorDelegate.class); when(delegate.process(any())).thenReturn(expected); - ItemProcessor itemProcessorAdaptor = ItemProcessorAdapter.of( - delegate); + ItemProcessor itemProcessorAdaptor = ItemProcessorAdapter.of(delegate); - // when Integer actual = itemProcessorAdaptor.process(ThreadLocalRandom.current().nextInt()); - // then assertThat(actual).isEqualTo(expected); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemProcessorAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderAdapterTest.java index 0118a7e1..03f85863 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamFluxReaderAdapterTest.java @@ -39,79 +39,64 @@ class ItemStreamFluxReaderAdapterTest { @Test - void testOpen() { - // given + void openShouldInvokeProperDelegateMethods() { ItemStreamFluxReaderDelegate delegate = mock(ItemStreamFluxReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamFluxReaderAdapter.of(delegate); - // when itemStreamReader.open(new ExecutionContext()); - // then verify(delegate, times(1)).onOpenRead(any()); verify(delegate, times(1)).readFlux(any()); } @Test - void testRead() throws Exception { - // given + void readShouldReturnValuesFromDelegate() throws Exception { List expected = List.of(1, 2, 3); ItemStreamFluxReaderDelegate delegate = mock(ItemStreamFluxReaderDelegate.class); when(delegate.readFlux(any())).thenAnswer($ -> Flux.fromIterable(expected)); ItemStreamReader itemStreamReader = ItemStreamFluxReaderAdapter.of(delegate); - - // when itemStreamReader.open(new ExecutionContext()); + List items = new ArrayList<>(); Integer item; while ((item = itemStreamReader.read()) != null) { items.add(item); } - // then assertThat(items).isEqualTo(expected); } @Test - void testReadWithOpenShouldThrowsException() { - // given + void readShouldThrowExceptionWhenNoOpenInvoked() { ItemStreamFluxReaderDelegate delegate = mock(ItemStreamFluxReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamFluxReaderAdapter.of(delegate); - // when, then assertThatThrownBy(itemStreamReader::read).isInstanceOf(IllegalStateException.class); } @Test - void testUpdate() { - // given + void updateShouldInvokeProperDelegateMethod() { ItemStreamFluxReaderDelegate delegate = mock(ItemStreamFluxReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamFluxReaderAdapter.of(delegate); - // when itemStreamReader.update(new ExecutionContext()); - // then verify(delegate, times(1)).onUpdateRead(any()); } @Test - void testClose() { - // given + void closeShouldInvokeProperDelegateMethod() { ItemStreamFluxReaderDelegate delegate = mock(ItemStreamFluxReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamFluxReaderAdapter.of(delegate); - // when itemStreamReader.close(); - // then verify(delegate, times(1)).onCloseRead(); } @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemStreamFluxReaderAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderAdapterTest.java index f2adc896..f807a892 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIterableReaderAdapterTest.java @@ -37,79 +37,64 @@ class ItemStreamIterableReaderAdapterTest { @Test - void testOpen() { - // given + void openShouldInvokeProperDelegateMethods() { ItemStreamIterableReaderDelegate delegate = mock(ItemStreamIterableReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIterableReaderAdapter.of(delegate); - // when itemStreamReader.open(new ExecutionContext()); - // then verify(delegate, times(1)).onOpenRead(any()); verify(delegate, times(1)).readIterable(any()); } @Test - void testRead() throws Exception { - // given + void readShouldReturnValuesFromDelegate() throws Exception { List expected = List.of(1, 2, 3); ItemStreamIterableReaderDelegate delegate = mock(ItemStreamIterableReaderDelegate.class); when(delegate.readIterable(any())).thenAnswer($ -> expected); ItemStreamReader itemStreamReader = ItemStreamIterableReaderAdapter.of(delegate); - - // when itemStreamReader.open(new ExecutionContext()); + List items = new ArrayList<>(); Integer item; while ((item = itemStreamReader.read()) != null) { items.add(item); } - // then assertThat(items).isEqualTo(expected); } @Test - void testReadWithOpenShouldThrowsException() { - // given + void readShouldThrowExceptionWhenNoOpenInvoked() { ItemStreamIterableReaderDelegate delegate = mock(ItemStreamIterableReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIterableReaderAdapter.of(delegate); - // when, then assertThatThrownBy(itemStreamReader::read).isInstanceOf(IllegalStateException.class); } @Test - void testUpdate() { - // given + void updateShouldInvokeProperDelegateMethod() { ItemStreamIterableReaderDelegate delegate = mock(ItemStreamIterableReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIterableReaderAdapter.of(delegate); - // when itemStreamReader.update(new ExecutionContext()); - // then verify(delegate, times(1)).onUpdateRead(any()); } @Test - void testClose() { - // given + void closeShouldInvokeProperDelegateMethod() { ItemStreamIterableReaderDelegate delegate = mock(ItemStreamIterableReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIterableReaderAdapter.of(delegate); - // when itemStreamReader.close(); - // then verify(delegate, times(1)).onCloseRead(); } @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemStreamIterableReaderAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderAdapterTest.java index df18237d..286df35f 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamIteratorReaderAdapterTest.java @@ -37,79 +37,64 @@ class ItemStreamIteratorReaderAdapterTest { @Test - void testOpen() { - // given + void openShouldInvokeProperDelegateMethods() { ItemStreamIteratorReaderDelegate delegate = mock(ItemStreamIteratorReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIteratorReaderAdapter.of(delegate); - // when itemStreamReader.open(new ExecutionContext()); - // then verify(delegate, times(1)).onOpenRead(any()); verify(delegate, times(1)).readIterator(any()); } @Test - void testRead() throws Exception { - // given + void readShouldReturnValuesFromDelegate() throws Exception { List expected = List.of(1, 2, 3); ItemStreamIteratorReaderDelegate delegate = mock(ItemStreamIteratorReaderDelegate.class); when(delegate.readIterator(any())).thenAnswer($ -> expected.iterator()); ItemStreamReader itemStreamReader = ItemStreamIteratorReaderAdapter.of(delegate); - - // when itemStreamReader.open(new ExecutionContext()); + List items = new ArrayList<>(); Integer item; while ((item = itemStreamReader.read()) != null) { items.add(item); } - // then assertThat(items).isEqualTo(expected); } @Test - void testReadWithOpenShouldThrowsException() { - // given + void readShouldThrowExceptionWhenNoOpenInvoked() { ItemStreamIteratorReaderDelegate delegate = mock(ItemStreamIteratorReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIteratorReaderAdapter.of(delegate); - // when, then assertThatThrownBy(itemStreamReader::read).isInstanceOf(IllegalStateException.class); } @Test - void testUpdate() { - // given + void updateShouldInvokeProperDelegateMethod() { ItemStreamIteratorReaderDelegate delegate = mock(ItemStreamIteratorReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIteratorReaderAdapter.of(delegate); - // when itemStreamReader.update(new ExecutionContext()); - // then verify(delegate, times(1)).onUpdateRead(any()); } @Test - void testClose() { - // given + void closeShouldInvokeProperDelegateMethod() { ItemStreamIteratorReaderDelegate delegate = mock(ItemStreamIteratorReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamIteratorReaderAdapter.of(delegate); - // when itemStreamReader.close(); - // then verify(delegate, times(1)).onCloseRead(); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemStreamIteratorReaderAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderAdapterTest.java index 42a7e844..57cb45a8 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamSimpleReaderAdapterTest.java @@ -26,8 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.Test; import org.springframework.batch.item.ExecutionContext; @@ -37,67 +36,50 @@ class ItemStreamSimpleReaderAdapterTest { @Test - void testOpen() { - // given + void openShouldInvokeProperDelegateMethod() { ItemStreamSimpleReaderDelegate delegate = mock(ItemStreamSimpleReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamSimpleReaderAdapter.of(delegate); - // when itemStreamReader.open(new ExecutionContext()); - // then verify(delegate, times(1)).onOpenRead(any()); } @Test - void testRead() throws Exception { - // given + void readShouldReturnValuesFromDelegate() throws Exception { + Integer expected = ThreadLocalRandom.current().nextInt(); ItemStreamSimpleReaderDelegate delegate = mock(ItemStreamSimpleReaderDelegate.class); - when(delegate.read()).thenReturn(1, 2, 3, null); + when(delegate.read()).thenReturn(expected); ItemStreamReader itemStreamReader = ItemStreamSimpleReaderAdapter.of(delegate); - // when - itemStreamReader.open(new ExecutionContext()); - List items = new ArrayList<>(); - Integer item; - while ((item = itemStreamReader.read()) != null) { - items.add(item); - } - - // then - assertThat(items).isEqualTo(List.of(1, 2, 3)); + Integer actual = itemStreamReader.read(); + + assertThat(actual).isEqualTo(expected); } @Test - void testUpdate() { - // given + void updateShouldInvokeProperDelegateMethod() { ItemStreamSimpleReaderDelegate delegate = mock(ItemStreamSimpleReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamSimpleReaderAdapter.of(delegate); - // when itemStreamReader.update(new ExecutionContext()); - // then verify(delegate, times(1)).onUpdateRead(any()); } @Test - void testClose() { - // given + void closeShouldInvokeProperDelegateMethod() { ItemStreamSimpleReaderDelegate delegate = mock(ItemStreamSimpleReaderDelegate.class); ItemStreamReader itemStreamReader = ItemStreamSimpleReaderAdapter.of(delegate); - // when itemStreamReader.close(); - // then verify(delegate, times(1)).onCloseRead(); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemStreamSimpleReaderAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamWriterAdapterTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamWriterAdapterTest.java index 6def1763..32824379 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamWriterAdapterTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/ItemStreamWriterAdapterTest.java @@ -33,69 +33,48 @@ class ItemStreamWriterAdapterTest { @Test - void testOpen() { - // given - ItemStreamWriterDelegate delegate = mock( - ItemStreamWriterDelegate.class); - ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of( - delegate); - - // when + void openShouldInvokeProperDelegateMethod() { + ItemStreamWriterDelegate delegate = mock(ItemStreamWriterDelegate.class); + ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of(delegate); + itemStreamWriterAdaptor.open(new ExecutionContext()); - // then verify(delegate, times(1)).onOpenWrite(any()); } @Test - void testWrite() throws Exception { - // given - ItemStreamWriterDelegate delegate = mock( - ItemStreamWriterDelegate.class); - ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of( - delegate); - - // when + void writeShouldInvokeProperDelegateMethod() throws Exception { + ItemStreamWriterDelegate delegate = mock(ItemStreamWriterDelegate.class); + ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of(delegate); + itemStreamWriterAdaptor.write(Chunk.of()); - // then verify(delegate, times(1)).write(any()); } @Test - void testUpdate() { - // given - ItemStreamWriterDelegate delegate = mock( - ItemStreamWriterDelegate.class); - ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of( - delegate); - - // when + void updateShouldInvokeProperDelegateMethod() { + ItemStreamWriterDelegate delegate = mock(ItemStreamWriterDelegate.class); + ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of(delegate); + itemStreamWriterAdaptor.update(new ExecutionContext()); - // then verify(delegate, times(1)).onUpdateWrite(any()); } @Test - void testClose() { - // given - ItemStreamWriterDelegate delegate = mock( - ItemStreamWriterDelegate.class); - ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of( - delegate); - - // when + void closeShouldInvokeProperDelegateMethod() { + ItemStreamWriterDelegate delegate = mock(ItemStreamWriterDelegate.class); + ItemStreamWriter itemStreamWriterAdaptor = ItemStreamWriterAdapter.of(delegate); + itemStreamWriterAdaptor.close(); - // then verify(delegate, times(1)).onCloseWrite(); } - @SuppressWarnings({"ResultOfMethodCallIgnored", "ConstantConditions"}) + @SuppressWarnings({"ConstantConditions"}) @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> ItemStreamWriterAdapter.of(null)); } } diff --git a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/StepScopeItemStreamReaderTest.java b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/StepScopeItemStreamReaderTest.java index 633b1389..3bc2fd46 100644 --- a/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/StepScopeItemStreamReaderTest.java +++ b/spring-batch-plus/src/test/java/com/navercorp/spring/batch/plus/step/adapter/StepScopeItemStreamReaderTest.java @@ -39,84 +39,65 @@ class StepScopeItemStreamReaderTest { @Test - void testOpen() throws Exception { - // given + void openShouldInvokeProperDelegateMethod() throws Exception { ItemStreamReader delegate = mock(ItemStreamReader.class); - ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of( - () -> delegate); + ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of(() -> delegate); - // when StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(); StepScopeTestUtils.doInStepScope(stepExecution, () -> { itemStreamReader.open(new ExecutionContext()); return null; }); - // then verify(delegate, times(1)).open(any()); } @Test - void testRead() throws Exception { - // given + void readShouldReturnValueFromDelegate() throws Exception { Integer expected = ThreadLocalRandom.current().nextInt(); ItemStreamReader delegate = mock(ItemStreamReader.class); when(delegate.read()).thenReturn(expected); - ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of( - () -> delegate); + ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of(() -> delegate); - // when StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(); Integer actual = StepScopeTestUtils.doInStepScope(stepExecution, itemStreamReader::read); - // then assertThat(actual).isEqualTo(expected); } @Test - void testUpdate() throws Exception { - // given + void updateShouldInvokeProperDelegateMethod() throws Exception { ItemStreamReader delegate = mock(ItemStreamReader.class); - ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of( - () -> delegate); + ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of(() -> delegate); - // when StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(); StepScopeTestUtils.doInStepScope(stepExecution, () -> { itemStreamReader.update(new ExecutionContext()); return null; }); - // then verify(delegate, times(1)).update(any()); } @Test - void testClose() throws Exception { - // given + void closeShouldInvokeProperDelegateMethod() throws Exception { ItemStreamReader delegate = mock(ItemStreamReader.class); - ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of( - () -> delegate); + ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of(() -> delegate); - // when StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(); StepScopeTestUtils.doInStepScope(stepExecution, () -> { itemStreamReader.close(); return null; }); - // then verify(delegate, times(1)).close(); } @Test - void testInvokeShouldThrowsExceptionWhenNoStepScope() { - // given + void invokeShouldThrowExceptionWhenNoStepScope() { ItemStreamReader delegate = mock(ItemStreamReader.class); - ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of( - () -> delegate); + ItemStreamReader itemStreamReader = StepScopeItemStreamReader.of(() -> delegate); - // when assertThatThrownBy( () -> itemStreamReader.open(new ExecutionContext()) ).hasMessageContaining("No step context is set. Make sure if it's invoked in a stepScope."); @@ -132,8 +113,7 @@ void testInvokeShouldThrowsExceptionWhenNoStepScope() { } @Test - void testPassingNull() { - // when, then + void createShouldThrowExceptionWhenPassingNull() { assertThatThrownBy(() -> StepScopeItemStreamReader.of(null)); } }