Google 태그 관리자 아이콘

스쳐가는 이슈들

.springbatch(JobRestartException: JobInstance already exists and is not restartable)

silvergoni 2022. 4. 15. 09:32
반응형

이슈 상황

- 흔하게 볼 수 있는 spring-batch 에러다.

- 보통 해결책으로 incrementer를 붙이면 해결된다.

- 그런데 특수하게도! incrementer를 붙여도 해결이 안되는 순간이 있다. 그 순간이 왜 일어나는지 어떻게 해결할지 알아보자

JobRestartException: JobInstance already exists and is not restartable

 

 

이슈 배경

- job 설정을 가장 간단하게 정의해보았다.

@Slf4j
@RequiredArgsConstructor
@Configuration
public class SampleJobConfiguration {
	private final static String JOB_NAME = "sampleJob";
	private final static String STEP_NAME = "sampleStep";
	private final JobBuilderFactory jobBuilderFactory;
	private final StepBuilderFactory stepBuilderFactory;

	@Bean(JOB_NAME)
	public Job job() {
		return jobBuilderFactory.get(JOB_NAME)
			.preventRestart()
			.start(step())
			.build();
	}

	@Bean(STEP_NAME)
	public Step step() {
		return stepBuilderFactory.get(STEP_NAME)
			.tasklet((contribution, chunkContext) -> {
				log.info("test");
				return RepeatStatus.FINISHED;
			})
			.build();
	}
}

- 파라미터가 매번 같아도 실행이 되게하려면 incrementer를 세팅해서 매번 실행이 다른것처럼 인식하도록 한다.

	@Bean(JOB_NAME)
	public Job job() {
		return jobBuilderFactory.get(JOB_NAME)
			.preventRestart()
			.start(step())
			.incrementer(/* 여기에 구현된 객체 주입*/)
			.build();
	}

 

이슈 상황

- 위 처럼 incrementer를 세팅했는데도 jobinstance already exists라는 메세지가 나오면 십중팔구 분명 incrementer없이 실행을 한번은 해본 것이라고 볼 수 있다.

- incrementer기능은 기본 변수에 추가적으로 유일성을 갖는 변수를 추가해주는 것이다.

- 그런데 문제는 이 incrementer에서 나오는 유일성을 가진 변수가 기본 변수와 머지되기 전에 순수한 기본 변수만으로 jobInstance여부를 검사한다. 자세히 코드를 보자.

 

 

이슈 점검

- jobRepository.isJobInstanceExists(~) 에서 기존에 실행되었는지를 판단한다.

// JobLauncherApplicationRunner.class
private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) { 
    if (this.jobRepository != null && this.jobRepository.isJobInstanceExists(job.getName(), jobParameters)) {       //here
        return getNextJobParametersForExisting(job, jobParameters);
    }
    if (job.getJobParametersIncrementer() == null) {
        return jobParameters;
    }
    JobParameters nextParameters = new JobParametersBuilder(jobParameters, this.jobExplorer)
            .getNextJobParameters(job).toJobParameters();
    return merge(nextParameters, jobParameters);
}

- 별다른 설정이 없다면 SimpleJobRepository내부 메소드로 실행된다.

// SimpleJobRepository.class
@Override
public boolean isJobInstanceExists(String jobName, JobParameters jobParameters) {
    return jobInstanceDao.getJobInstance(jobName, jobParameters) != null;   //here
}

- 여기서 보면 2가지가 중요하다.

- 하나는 키를 만드는 jobKeyGenerator.generateKey(jobParameters) 이 부분이다. 키 생성부분은 마지막에 참고로 다루도록 하고 넘어가자.  또다른 하나는 instances = getJdbcTemplate().query(getQuery(FIND_JOBS_WITH_KEY), rowMapper, jobName, jobKey); 이다. 이 부분에서 결국 주석처럼 BATCH_JOB_INSTANCE테이블에서 jobInstacne가 있는지 확인한다. 

// JdbcJobInstanceDao.class
public JobInstance getJobInstance(final String jobName,
        final JobParameters jobParameters) {

    Assert.notNull(jobName, "Job name must not be null.");
    Assert.notNull(jobParameters, "JobParameters must not be null.");

    String jobKey = jobKeyGenerator.generateKey(jobParameters);	// key 생성

    RowMapper<JobInstance> rowMapper = new JobInstanceRowMapper();

    List<JobInstance> instances;
    if (StringUtils.hasLength(jobKey)) {
        // SELECT JOB_INSTANCE_ID, JOB_NAME from BATCH_JOB_INSTANCE where JOB_NAME = '잡이름' and JOB_KEY = '변수를 압축한 문자열'
        instances = getJdbcTemplate().query(getQuery(FIND_JOBS_WITH_KEY),
                rowMapper, jobName, jobKey);	// 여기서 쿼리로 확인
    } else {
        instances = getJdbcTemplate().query(
                getQuery(FIND_JOBS_WITH_EMPTY_KEY), rowMapper, jobName,
                jobKey);
    }

    if (instances.isEmpty()) {
        return null;
    } else {
        Assert.state(instances.size() == 1, "instance count must be 1 but was " + instances.size());
        return instances.get(0);
    }
}

 

이슈 정리

- 결국 문제는 이거다. incrementer이 세팅이 되지 않았는데 Job을 실행하면 BATCH_JOB_INSTANCE 테이블에 순수한 기본 변수 기준으로 키가 생성(JOB_KEY 부분)되고 그 키가 저장된다.

- 그리고 spring-batch에서는 JOB실행을 하기 전에 BATCH_JOB_INSTANCE 테이블의 키(JOB_KEY 부분)를 비교해서 이미 실행되었는지를 확인한다. 이때는 확인은 순수한 기본 변수로만 판단한다.

- 따라서 순수한 기본 변수로만 실행한 기록이 있으면 이미 실행되었다고 판단이 되는 것이다.

- 만약에 처음부터 incrementer를 세팅하고 실행했다면 BATCH_JOB_INSTANCE 테이블에 머지된 변수로 키가 생성되었을 것이고, 그렇기에 이미 실행된 기록이라고 나오지 않는다.

 

이슈 해결

- 그러면 이미 incrementer를 세팅하지 않고 돌린 기록이 있다면 어떻게 해결할 것인가?

- 당연히 테이블에서 기록을 지워주면 된다. 하지만 FK로 연결되어 있기때문에 아래의 순서대로 지워주면 된다. 

- BATCH_JOB_INSTANCE에서 삭제가 되야 실행 기록이 완전히 지워진다.

// JOB_EXECUTION_ID=2212077
1, delete from BATCH_JOB_EXECUTION where JOB_EXECUTION_ID=2212077
2, delete from BATCH_JOB_PARAMS where JOB_EXECUTION_ID=2212077
3, delete from BATCH_JOB_EXECUTION where JOB_EXECUTION_ID=2212077
4, delete from BATCH_JOB_INSTANCE where JOB_EXECUTION_ID=2212077

 

참고

- 위에서 언급한 파라미터를 가지고 하나의 String 키를 만드는 재밌는 코드다. 정렬도 들어가 있고 짧게 만들기 위해 쓰는 기법들을 보면 참고가 될 만하다.

// DefaultJobKeyGenerator.class
@Override
public String generateKey(JobParameters source) {

    Assert.notNull(source, "source must not be null");
    Map<String, JobParameter> props = source.getParameters();
    StringBuilder stringBuffer = new StringBuilder();
    List<String> keys = new ArrayList<>(props.keySet());
    Collections.sort(keys);
    for (String key : keys) {
        JobParameter jobParameter = props.get(key);
        if(jobParameter.isIdentifying()) {
            String value = jobParameter.getValue()==null ? "" : jobParameter.toString();
            stringBuffer.append(key).append("=").append(value).append(";");
        }
    }

    MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException e) {
        throw new IllegalStateException(
                "MD5 algorithm not available.  Fatal (should be in the JDK).");
    }

    try {
        byte[] bytes = digest.digest(stringBuffer.toString().getBytes(
                "UTF-8"));
        return String.format("%032x", new BigInteger(1, bytes));
    } catch (UnsupportedEncodingException e) {
        throw new IllegalStateException(
                "UTF-8 encoding not available.  Fatal (should be in the JDK).");
    }
}