이슈 상황
- 흔하게 볼 수 있는 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).");
}
}