1717
1818import java .io .IOException ;
1919import java .nio .file .Files ;
20+ import java .util .Collections ;
2021
2122import com .mongodb .client .MongoCollection ;
2223import org .bson .Document ;
2526import org .junit .jupiter .api .Test ;
2627import org .testcontainers .junit .jupiter .Testcontainers ;
2728
29+ import org .springframework .batch .core .BatchStatus ;
2830import org .springframework .batch .core .ExitStatus ;
2931import org .springframework .batch .core .job .Job ;
3032import org .springframework .batch .core .job .JobExecution ;
4244import org .springframework .core .io .Resource ;
4345import org .springframework .data .mongodb .MongoTransactionManager ;
4446import org .springframework .data .mongodb .core .MongoTemplate ;
47+ import org .springframework .data .mongodb .core .query .Criteria ;
48+ import org .springframework .data .mongodb .core .query .Query ;
49+ import org .springframework .data .mongodb .core .query .Update ;
4550import org .springframework .test .context .junit .jupiter .SpringJUnitConfig ;
51+ import org .springframework .util .Assert ;
4652
4753/**
4854 * @author Mahmoud Ben Hassine
@@ -56,6 +62,11 @@ public class MongoDBJobRestartIntegrationTests {
5662
5763 @ BeforeEach
5864 public void setUp () throws IOException {
65+ // TODO put drop statements in schema-drop-mongodb.jsonl
66+ mongoTemplate .dropCollection ("BATCH_JOB_INSTANCE" );
67+ mongoTemplate .dropCollection ("BATCH_JOB_EXECUTION" );
68+ mongoTemplate .dropCollection ("BATCH_STEP_EXECUTION" );
69+ mongoTemplate .dropCollection ("BATCH_SEQUENCES" );
5970 Resource resource = new FileSystemResource (
6071 "src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl" );
6172 Files .lines (resource .getFilePath ()).forEach (line -> mongoTemplate .executeCommand (line ));
@@ -109,4 +120,54 @@ void testJobExecutionRestart(@Autowired JobOperator jobOperator, @Autowired JobR
109120 Assertions .assertEquals (3 , lastStepExecution .getId ());
110121 }
111122
123+ /*
124+ * Test for https://github.com/spring-projects/spring-batch/issues/4943: after abrupt
125+ * shutdown, the embedded job.execution.stepExecutions array is not synchronized, but
126+ * BATCH_STEP_EXECUTION collection still contains the data.
127+ */
128+ @ Test
129+ void testRestartAfterRecoverFromAbruptShutdown (@ Autowired JobOperator jobOperator ,
130+ @ Autowired JobRepository jobRepository , @ Autowired Job job ) throws Exception {
131+ // Step 1: Run job normally
132+ JobParameters jobParameters = new JobParametersBuilder ().addString ("name" , "foo" ).toJobParameters ();
133+
134+ JobExecution jobExecution = jobOperator .start (job , jobParameters );
135+
136+ // Verify job completed successfully
137+ Assertions .assertEquals (BatchStatus .COMPLETED , jobExecution .getStatus ());
138+
139+ // Step 2: Simulate the core issue:
140+ // - set job execution status to STARTED
141+ // - clear embedded stepExecutions array while keeping BATCH_STEP_EXECUTION
142+ // collection intact
143+
144+ jobExecution .setStatus (BatchStatus .STARTED );
145+ jobRepository .update (jobExecution );
146+
147+ Query jobQuery = new Query (Criteria .where ("jobExecutionId" ).is (jobExecution .getId ()));
148+ Update jobUpdateStepExecutions = new Update ().set ("stepExecutions" , Collections .emptyList ());
149+ mongoTemplate .updateFirst (jobQuery , jobUpdateStepExecutions , "BATCH_JOB_EXECUTION" );
150+
151+ // Step 3: Verify that job's status = STARTED and embedded array is empty but
152+ // collection still has data
153+ MongoCollection <Document > jobExecutionsCollection = mongoTemplate .getCollection ("BATCH_JOB_EXECUTION" );
154+ MongoCollection <Document > stepExecutionsCollection = mongoTemplate .getCollection ("BATCH_STEP_EXECUTION" );
155+
156+ Document document = jobExecutionsCollection .find (new Document ("jobExecutionId" , jobExecution .getId ())).first ();
157+ Assertions .assertTrue (document .getString ("status" ).equals ("STARTED" ), "job must be in STARTED status" );
158+ Assertions .assertTrue (document .getList ("stepExecutions" , Document .class ).isEmpty (),
159+ "Embedded stepExecutions array should be empty" );
160+ Assertions .assertEquals (2 , stepExecutionsCollection .countDocuments (),
161+ "BATCH_STEP_EXECUTION collection should still contain data" );
162+
163+ // Step 4: recover the job execution
164+ JobExecution recoveredJobExecution = jobOperator .recover (jobExecution );
165+ Assert .notNull (recoveredJobExecution .getExecutionContext ().get ("recovered" ),
166+ "Job execution should be marked as recovered" );
167+
168+ // Step 5: restart the job
169+ JobExecution restartedJobExecution = jobOperator .restart (recoveredJobExecution );
170+ Assertions .assertEquals (BatchStatus .COMPLETED , restartedJobExecution .getStatus ());
171+ }
172+
112173}
0 commit comments