Skip to content

Commit 846648b

Browse files
committed
Add initial support for MongoDB as job repository
Resolves #877
1 parent 6cc718a commit 846648b

23 files changed

+1842
-1
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
<jakarta.validation-api.version>3.1.0</jakarta.validation-api.version>
8686
<jakarta.persistence-api.version>3.1.0</jakarta.persistence-api.version>
8787
<neo4j-ogm-core.version>4.0.11</neo4j-ogm-core.version>
88-
<mongodb-driver-sync.version>5.2.0</mongodb-driver-sync.version>
88+
<mongodb-driver-sync.version>5.1.4</mongodb-driver-sync.version>
8989
<junit-jupiter.version>5.11.1</junit-jupiter.version>
9090

9191
<!-- provided dependencies -->

spring-batch-core/pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@
9696
<version>${aspectj.version}</version>
9797
<optional>true</optional>
9898
</dependency>
99+
<dependency>
100+
<groupId>org.springframework.data</groupId>
101+
<artifactId>spring-data-mongodb</artifactId>
102+
<version>${spring-data-mongodb.version}</version>
103+
<optional>true</optional>
104+
<exclusions>
105+
<exclusion>
106+
<groupId>org.slf4j</groupId>
107+
<artifactId>slf4j-api</artifactId>
108+
</exclusion>
109+
<exclusion>
110+
<groupId>org.mongodb</groupId>
111+
<artifactId>mongodb-driver-sync</artifactId>
112+
</exclusion>
113+
</exclusions>
114+
</dependency>
115+
<dependency>
116+
<groupId>org.mongodb</groupId>
117+
<artifactId>mongodb-driver-sync</artifactId>
118+
<version>${mongodb-driver-sync.version}</version>
119+
<optional>true</optional>
120+
</dependency>
99121

100122
<!-- test dependencies -->
101123
<dependency>
@@ -128,6 +150,12 @@
128150
<version>${testcontainers.version}</version>
129151
<scope>test</scope>
130152
</dependency>
153+
<dependency>
154+
<groupId>org.testcontainers</groupId>
155+
<artifactId>mongodb</artifactId>
156+
<version>${testcontainers.version}</version>
157+
<scope>test</scope>
158+
</dependency>
131159
<dependency>
132160
<groupId>org.mariadb.jdbc</groupId>
133161
<artifactId>mariadb-java-client</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.explore.support;
17+
18+
import org.springframework.batch.core.repository.dao.ExecutionContextDao;
19+
import org.springframework.batch.core.repository.dao.JobExecutionDao;
20+
import org.springframework.batch.core.repository.dao.JobInstanceDao;
21+
import org.springframework.batch.core.repository.dao.StepExecutionDao;
22+
import org.springframework.batch.core.repository.dao.MongoExecutionContextDao;
23+
import org.springframework.batch.core.repository.dao.MongoJobExecutionDao;
24+
import org.springframework.batch.core.repository.dao.MongoJobInstanceDao;
25+
import org.springframework.batch.core.repository.dao.MongoStepExecutionDao;
26+
import org.springframework.beans.factory.InitializingBean;
27+
import org.springframework.data.mongodb.core.MongoOperations;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* @author Mahmoud Ben Hassine
32+
* @since 5.2.0
33+
*/
34+
public class MongoJobExplorerFactoryBean extends AbstractJobExplorerFactoryBean implements InitializingBean {
35+
36+
private MongoOperations mongoOperations;
37+
38+
public void setMongoOperations(MongoOperations mongoOperations) {
39+
this.mongoOperations = mongoOperations;
40+
}
41+
42+
@Override
43+
protected JobInstanceDao createJobInstanceDao() {
44+
return new MongoJobInstanceDao(this.mongoOperations);
45+
}
46+
47+
@Override
48+
protected JobExecutionDao createJobExecutionDao() {
49+
return new MongoJobExecutionDao(this.mongoOperations);
50+
}
51+
52+
@Override
53+
protected StepExecutionDao createStepExecutionDao() {
54+
return new MongoStepExecutionDao(this.mongoOperations);
55+
}
56+
57+
@Override
58+
protected ExecutionContextDao createExecutionContextDao() {
59+
return new MongoExecutionContextDao(this.mongoOperations);
60+
}
61+
62+
@Override
63+
public void afterPropertiesSet() throws Exception {
64+
super.afterPropertiesSet();
65+
Assert.notNull(this.mongoOperations, "MongoOperations must not be null.");
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.repository.dao;
17+
18+
import java.util.Collection;
19+
import java.util.Map;
20+
21+
import org.springframework.batch.core.JobExecution;
22+
import org.springframework.batch.core.StepExecution;
23+
import org.springframework.batch.item.ExecutionContext;
24+
import org.springframework.data.mongodb.core.MongoOperations;
25+
import org.springframework.data.mongodb.core.query.Query;
26+
import org.springframework.data.mongodb.core.query.Update;
27+
28+
import static org.springframework.data.mongodb.core.query.Criteria.where;
29+
import static org.springframework.data.mongodb.core.query.Query.query;
30+
31+
/**
32+
* @author Mahmoud Ben Hassine
33+
* @since 5.2.0
34+
*/
35+
public class MongoExecutionContextDao implements ExecutionContextDao {
36+
37+
private static final String STEP_EXECUTIONS_COLLECTION_NAME = "BATCH_STEP_EXECUTION";
38+
39+
private static final String JOB_EXECUTIONS_COLLECTION_NAME = "BATCH_JOB_EXECUTION";
40+
41+
private final MongoOperations mongoOperations;
42+
43+
public MongoExecutionContextDao(MongoOperations mongoOperations) {
44+
this.mongoOperations = mongoOperations;
45+
}
46+
47+
@Override
48+
public ExecutionContext getExecutionContext(JobExecution jobExecution) {
49+
org.springframework.batch.core.repository.persistence.JobExecution execution = this.mongoOperations.findById(
50+
jobExecution.getId(), org.springframework.batch.core.repository.persistence.JobExecution.class,
51+
JOB_EXECUTIONS_COLLECTION_NAME);
52+
if (execution == null) {
53+
return new ExecutionContext();
54+
}
55+
return new ExecutionContext(execution.getExecutionContext().map());
56+
}
57+
58+
@Override
59+
public ExecutionContext getExecutionContext(StepExecution stepExecution) {
60+
org.springframework.batch.core.repository.persistence.StepExecution execution = this.mongoOperations.findById(
61+
stepExecution.getId(), org.springframework.batch.core.repository.persistence.StepExecution.class,
62+
STEP_EXECUTIONS_COLLECTION_NAME);
63+
if (execution == null) {
64+
return new ExecutionContext();
65+
}
66+
return new ExecutionContext(execution.getExecutionContext().map());
67+
}
68+
69+
@Override
70+
public void saveExecutionContext(JobExecution jobExecution) {
71+
ExecutionContext executionContext = jobExecution.getExecutionContext();
72+
Query query = query(where("_id").is(jobExecution.getId()));
73+
74+
Update update = Update.update("executionContext",
75+
new org.springframework.batch.core.repository.persistence.ExecutionContext(executionContext.toMap(),
76+
executionContext.isDirty()));
77+
this.mongoOperations.updateFirst(query, update,
78+
org.springframework.batch.core.repository.persistence.JobExecution.class,
79+
JOB_EXECUTIONS_COLLECTION_NAME);
80+
}
81+
82+
@Override
83+
public void saveExecutionContext(StepExecution stepExecution) {
84+
ExecutionContext executionContext = stepExecution.getExecutionContext();
85+
Query query = query(where("_id").is(stepExecution.getId()));
86+
87+
Update update = Update.update("executionContext",
88+
new org.springframework.batch.core.repository.persistence.ExecutionContext(executionContext.toMap(),
89+
executionContext.isDirty()));
90+
this.mongoOperations.updateFirst(query, update,
91+
org.springframework.batch.core.repository.persistence.StepExecution.class,
92+
STEP_EXECUTIONS_COLLECTION_NAME);
93+
94+
}
95+
96+
@Override
97+
public void saveExecutionContexts(Collection<StepExecution> stepExecutions) {
98+
for (StepExecution stepExecution : stepExecutions) {
99+
saveExecutionContext(stepExecution);
100+
}
101+
}
102+
103+
@Override
104+
public void updateExecutionContext(JobExecution jobExecution) {
105+
saveExecutionContext(jobExecution);
106+
}
107+
108+
@Override
109+
public void updateExecutionContext(StepExecution stepExecution) {
110+
saveExecutionContext(stepExecution);
111+
}
112+
113+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.repository.dao;
17+
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Set;
21+
22+
import org.springframework.batch.core.JobExecution;
23+
import org.springframework.batch.core.JobInstance;
24+
import org.springframework.batch.core.repository.persistence.converter.JobExecutionConverter;
25+
import org.springframework.batch.core.repository.persistence.converter.JobInstanceConverter;
26+
import org.springframework.data.domain.Sort;
27+
import org.springframework.data.mongodb.core.MongoOperations;
28+
import org.springframework.data.mongodb.core.query.Query;
29+
import org.springframework.data.mongodb.core.query.Update;
30+
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
31+
32+
import static org.springframework.data.mongodb.core.query.Criteria.where;
33+
import static org.springframework.data.mongodb.core.query.Query.query;
34+
35+
/**
36+
* @author Mahmoud Ben Hassine
37+
* @since 5.2.0
38+
*/
39+
public class MongoJobExecutionDao implements JobExecutionDao {
40+
41+
private static final String JOB_EXECUTIONS_COLLECTION_NAME = "BATCH_JOB_EXECUTION";
42+
43+
private static final String JOB_EXECUTIONS_SEQUENCE_NAME = "BATCH_JOB_EXECUTION_SEQ";
44+
45+
private static final String JOB_INSTANCES_COLLECTION_NAME = "BATCH_JOB_INSTANCE";
46+
47+
private final MongoOperations mongoOperations;
48+
49+
private final JobExecutionConverter jobExecutionConverter = new JobExecutionConverter();
50+
51+
private final JobInstanceConverter jobInstanceConverter = new JobInstanceConverter();
52+
53+
private DataFieldMaxValueIncrementer jobExecutionIncrementer;
54+
55+
public MongoJobExecutionDao(MongoOperations mongoOperations) {
56+
this.mongoOperations = mongoOperations;
57+
this.jobExecutionIncrementer = new MongoSequenceIncrementer(mongoOperations, JOB_EXECUTIONS_SEQUENCE_NAME);
58+
}
59+
60+
public void setJobExecutionIncrementer(DataFieldMaxValueIncrementer jobExecutionIncrementer) {
61+
this.jobExecutionIncrementer = jobExecutionIncrementer;
62+
}
63+
64+
@Override
65+
public void saveJobExecution(JobExecution jobExecution) {
66+
org.springframework.batch.core.repository.persistence.JobExecution jobExecutionToSave = this.jobExecutionConverter
67+
.fromJobExecution(jobExecution);
68+
long jobExecutionId = this.jobExecutionIncrementer.nextLongValue();
69+
jobExecutionToSave.setJobExecutionId(jobExecutionId);
70+
this.mongoOperations.insert(jobExecutionToSave, JOB_EXECUTIONS_COLLECTION_NAME);
71+
jobExecution.setId(jobExecutionId);
72+
}
73+
74+
@Override
75+
public void updateJobExecution(JobExecution jobExecution) {
76+
Query query = query(where("jobExecutionId").is(jobExecution.getId()));
77+
org.springframework.batch.core.repository.persistence.JobExecution jobExecutionToUpdate = this.jobExecutionConverter
78+
.fromJobExecution(jobExecution);
79+
this.mongoOperations.findAndReplace(query, jobExecutionToUpdate, JOB_EXECUTIONS_COLLECTION_NAME);
80+
}
81+
82+
@Override
83+
public List<JobExecution> findJobExecutions(JobInstance jobInstance) {
84+
Query query = query(where("jobInstanceId").is(jobInstance.getId()));
85+
List<org.springframework.batch.core.repository.persistence.JobExecution> jobExecutions = this.mongoOperations
86+
.find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
87+
JOB_EXECUTIONS_COLLECTION_NAME);
88+
return jobExecutions.stream()
89+
.map(jobExecution -> this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance))
90+
.toList();
91+
}
92+
93+
@Override
94+
public JobExecution getLastJobExecution(JobInstance jobInstance) {
95+
Query query = query(where("jobInstanceId").is(jobInstance.getId()));
96+
Sort.Order sortOrder = Sort.Order.desc("jobExecutionId");
97+
org.springframework.batch.core.repository.persistence.JobExecution jobExecution = this.mongoOperations.findOne(
98+
query.with(Sort.by(sortOrder)),
99+
org.springframework.batch.core.repository.persistence.JobExecution.class,
100+
JOB_EXECUTIONS_COLLECTION_NAME);
101+
return jobExecution != null ? this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance) : null;
102+
}
103+
104+
@Override
105+
public Set<JobExecution> findRunningJobExecutions(String jobName) {
106+
Query query = query(where("jobName").is(jobName));
107+
List<JobInstance> jobInstances = this.mongoOperations
108+
.find(query, org.springframework.batch.core.repository.persistence.JobInstance.class,
109+
JOB_INSTANCES_COLLECTION_NAME)
110+
.stream()
111+
.map(this.jobInstanceConverter::toJobInstance)
112+
.toList();
113+
Set<JobExecution> runningJobExecutions = new HashSet<>();
114+
for (JobInstance jobInstance : jobInstances) {
115+
query = query(
116+
where("jobInstanceId").is(jobInstance.getId()).and("status").in("STARTING", "STARTED", "STOPPING"));
117+
this.mongoOperations
118+
.find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
119+
JOB_EXECUTIONS_COLLECTION_NAME)
120+
.stream()
121+
.map(jobExecution -> this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance))
122+
.forEach(runningJobExecutions::add);
123+
}
124+
return runningJobExecutions;
125+
}
126+
127+
@Override
128+
public JobExecution getJobExecution(Long executionId) {
129+
org.springframework.batch.core.repository.persistence.JobExecution jobExecution = this.mongoOperations.findById(
130+
executionId, org.springframework.batch.core.repository.persistence.JobExecution.class,
131+
JOB_EXECUTIONS_COLLECTION_NAME);
132+
if (jobExecution == null) {
133+
return null;
134+
}
135+
org.springframework.batch.core.repository.persistence.JobInstance jobInstance = this.mongoOperations.findById(
136+
jobExecution.getJobInstanceId(),
137+
org.springframework.batch.core.repository.persistence.JobInstance.class, JOB_INSTANCES_COLLECTION_NAME);
138+
return this.jobExecutionConverter.toJobExecution(jobExecution,
139+
this.jobInstanceConverter.toJobInstance(jobInstance));
140+
}
141+
142+
@Override
143+
public void synchronizeStatus(JobExecution jobExecution) {
144+
Query query = query(where("jobExecutionId").is(jobExecution.getId()));
145+
Update update = Update.update("status", jobExecution.getStatus());
146+
// TODO the contract mentions to update the version as well. Double check if this
147+
// is needed as the version is not used in the tests following the call sites of
148+
// synchronizeStatus
149+
this.mongoOperations.updateFirst(query, update,
150+
org.springframework.batch.core.repository.persistence.JobExecution.class,
151+
JOB_EXECUTIONS_COLLECTION_NAME);
152+
}
153+
154+
}

0 commit comments

Comments
 (0)