spring-projects / spring-batch

Spring Batch is a framework for writing batch applications using Java and Spring
http://projects.spring.io/spring-batch/
Apache License 2.0
2.7k stars 2.33k forks source link

Missing note about not scoping Step beans with Job scope #3900

Open kzander91 opened 3 years ago

kzander91 commented 3 years ago

Bug description I have a Job that contains a Step that is annotated with @JobScope (to be able to inject job parameters). Using JobOperator.stop() from another thread to stop that job fails with the following exception:

org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'scopedTarget.step': Scope 'job' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No context holder available for job scope
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:383) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:195) ~[spring-aop-5.3.6.jar:5.3.6]
    at com.sun.proxy.$Proxy48.getName(Unknown Source) ~[na:na]
    at org.springframework.batch.core.job.SimpleJob.getStep(SimpleJob.java:109) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at org.springframework.batch.core.launch.support.SimpleJobOperator.stop(SimpleJobOperator.java:402) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at org.springframework.batch.core.launch.support.SimpleJobOperator$$FastClassBySpringCGLIB$$44ee6049.invoke(<generated>) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.batch.core.launch.support.SimpleJobOperator$$EnhancerBySpringCGLIB$$2759e0b7.stop(<generated>) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at com.example.demo.DemoApplication.lambda$runner$0(DemoApplication.java:54) ~[classes/:na]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:819) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:803) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:346) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1340) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1329) ~[spring-boot-2.4.5.jar:2.4.5]
    at com.example.demo.DemoApplication.main(DemoApplication.java:91) ~[classes/:na]
Caused by: java.lang.IllegalStateException: No context holder available for job scope
    at org.springframework.batch.core.scope.JobScope.getContext(JobScope.java:159) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at org.springframework.batch.core.scope.JobScope.get(JobScope.java:92) ~[spring-batch-core-4.3.2.jar:4.3.2]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:371) ~[spring-beans-5.3.6.jar:5.3.6]
    ... 25 common frames omitted

Environment Spring Batch: 4.3.2

Steps to reproduce

  1. Configure job with job-scoped steps.
  2. Run job asynchronously (to get hold of the pending JobExecution).
  3. Use JobOperator.stop() to stop that job.

Expected behavior Stopping the job works, regardless of the scope of its steps.

Minimal Complete Reproducible example Sample project: demo.zip

The project contains a job-scoped step. A CommandLineRunner creates a JobLauncher that launches jobs asynchronously. Then, the main thread waits two seconds before it calls JobOperator.stop().

  1. Unzip.
  2. Run ./mvnw spring-boot:run
  3. Two seconds after the job was started, JobOperator.stop() is called and fails with the exception above.
fmbenhassine commented 1 year ago

Thank you for opening this issue and for providing a minimal example.

The problem here is the scoped step. A step should not be scoped (with step scope or job scope). what should be scoped instead is the component of the step (tasklet, reader, writer, etc). In your case, it is the item processor that should be scoped to use late-binding, not the step itself. Here is an example of how your sample should be changed:

    @Bean
    Job job() {
        return jobs.get("job") //
                .start(step()) //
                .build();
    }

    @Bean
    Step step() {
        return steps.get("step") //
                .<String, String>chunk(1) //
                .reader(() -> String.valueOf(Math.random())) //
                .processor(itemProcessor(null)) //
                .writer(new ListItemWriter<>()) //
                .build();
    }

    @Bean
    @StepScope
    public ItemProcessor<String, String> itemProcessor(@Value("#{jobParameters['param']}") String param ) {
        return item -> {
            log.info("Processing item: '{}' Param: '{}'", item, param);
            TimeUnit.SECONDS.sleep(1L);
            return item;
        };
    }

With those changes, the sample works as expected.

There is a mention about not scoping Step beans in the documentation here, but it only mentions step-scope while it should include job scope as well.

I will change this into a documentation issue and update the docs accordingly.

marbon87 commented 7 months ago

Hi @fmbenhassine ,

if steps should have scope step or job, how can i dynamically set the chunkSize from a job parameter? In https://stackoverflow.com/a/67365622/4073333 your advice was to use @JobScope for the step, but that is not righy anymore, is it?

edit:

Is this a good solution?

    @Bean
    @StepScope
    public SimpleCompletionPolicy chunkCompletionPolicy(@Value("#{jobParameters['chunkSize']}") int chunkSize) {
        return new SimpleCompletionPolicy(chunkSize);
    }

    @Bean
    public Step step(PlatformTransactionManager transactionManager,
                     SimpleCompletionPolicy simpleCompletionPolicy) {
        return steps
                .get("exampleStep")
                .chunk(simpleCompletionPolicy, transactionManager)
                .reader(reader1())
                .processor(processor())
                .writer(dbResultWriter())
                .build();
    }