1616package org .springframework .batch .core .repository .support ;
1717
1818import java .time .LocalDateTime ;
19+ import java .util .Collections ;
1920import java .util .Map ;
2021
2122import com .mongodb .client .MongoCollection ;
2930import org .springframework .test .context .junit .jupiter .SpringJUnitConfig ;
3031import org .testcontainers .junit .jupiter .Testcontainers ;
3132
33+ import org .springframework .batch .core .BatchStatus ;
3234import org .springframework .batch .core .ExitStatus ;
3335import org .springframework .batch .core .job .Job ;
3436import org .springframework .batch .core .job .JobExecution ;
37+ import org .springframework .batch .core .job .JobInstance ;
3538import org .springframework .batch .core .job .parameters .JobParameters ;
3639import org .springframework .batch .core .job .parameters .JobParametersBuilder ;
3740import org .springframework .batch .core .launch .JobOperator ;
41+ import org .springframework .batch .core .repository .JobRepository ;
42+ import org .springframework .batch .core .repository .dao .StepExecutionDao ;
43+ import org .springframework .batch .core .step .StepExecution ;
3844import org .springframework .beans .factory .annotation .Autowired ;
3945import org .springframework .data .mongodb .core .MongoTemplate ;
46+ import org .springframework .data .mongodb .core .query .Criteria ;
47+ import org .springframework .data .mongodb .core .query .Query ;
48+ import org .springframework .data .mongodb .core .query .Update ;
4049
4150/**
4251 * @author Mahmoud Ben Hassine
52+ * @author Jinwoo Bae
4353 * @author Yanming Zhou
4454 */
4555@ DirtiesContext
@@ -53,18 +63,25 @@ public class MongoDBJobRepositoryIntegrationTests {
5363 @ SuppressWarnings ("removal" )
5464 @ BeforeEach
5565 public void setUp () {
56- // collections
66+ // Clear existing collections to ensure clean state
67+ mongoTemplate .getCollection ("BATCH_JOB_INSTANCE" ).drop ();
68+ mongoTemplate .getCollection ("BATCH_JOB_EXECUTION" ).drop ();
69+ mongoTemplate .getCollection ("BATCH_STEP_EXECUTION" ).drop ();
70+ mongoTemplate .getCollection ("BATCH_SEQUENCES" ).drop ();
71+
72+ // sequences
5773 mongoTemplate .createCollection ("BATCH_JOB_INSTANCE" );
5874 mongoTemplate .createCollection ("BATCH_JOB_EXECUTION" );
5975 mongoTemplate .createCollection ("BATCH_STEP_EXECUTION" );
60- // sequences
6176 mongoTemplate .createCollection ("BATCH_SEQUENCES" );
77+
6278 mongoTemplate .getCollection ("BATCH_SEQUENCES" )
6379 .insertOne (new Document (Map .of ("_id" , "BATCH_JOB_INSTANCE_SEQ" , "count" , 0L )));
6480 mongoTemplate .getCollection ("BATCH_SEQUENCES" )
6581 .insertOne (new Document (Map .of ("_id" , "BATCH_JOB_EXECUTION_SEQ" , "count" , 0L )));
6682 mongoTemplate .getCollection ("BATCH_SEQUENCES" )
6783 .insertOne (new Document (Map .of ("_id" , "BATCH_STEP_EXECUTION_SEQ" , "count" , 0L )));
84+
6885 // indices
6986 mongoTemplate .indexOps ("BATCH_JOB_INSTANCE" )
7087 .ensureIndex (new Index ().on ("jobName" , Sort .Direction .ASC ).named ("job_name_idx" ));
@@ -112,6 +129,58 @@ void testJobExecution(@Autowired JobOperator jobOperator, @Autowired Job job) th
112129 dump (stepExecutionsCollection , "step execution = " );
113130 }
114131
132+ /**
133+ * Test for GitHub issue #4943: getLastStepExecution should work when JobExecution's
134+ * embedded stepExecutions array is empty.
135+ *
136+ * <p>
137+ * This can happen after abrupt shutdown when the embedded stepExecutions array is not
138+ * synchronized, but BATCH_STEP_EXECUTION collection still contains the data.
139+ *
140+ */
141+ @ Test
142+ void testGetLastStepExecutionWithEmptyEmbeddedArray (@ Autowired JobOperator jobOperator , @ Autowired Job job ,
143+ @ Autowired StepExecutionDao stepExecutionDao ) throws Exception {
144+ // Step 1: Run job normally
145+ JobParameters jobParameters = new JobParametersBuilder ().addString ("name" , "emptyArrayTest" )
146+ .addLocalDateTime ("runtime" , LocalDateTime .now ())
147+ .toJobParameters ();
148+
149+ JobExecution jobExecution = jobOperator .start (job , jobParameters );
150+ JobInstance jobInstance = jobExecution .getJobInstance ();
151+
152+ // Verify job completed successfully
153+ Assertions .assertEquals (BatchStatus .COMPLETED , jobExecution .getStatus ());
154+
155+ // Step 2: Simulate the core issue - clear embedded stepExecutions array
156+ // while keeping BATCH_STEP_EXECUTION collection intact
157+ Query jobQuery = new Query (Criteria .where ("jobExecutionId" ).is (jobExecution .getId ()));
158+ Update jobUpdate = new Update ().set ("stepExecutions" , Collections .emptyList ());
159+ mongoTemplate .updateFirst (jobQuery , jobUpdate , "BATCH_JOB_EXECUTION" );
160+
161+ // Step 3: Verify embedded array is empty but collection still has data
162+ MongoCollection <Document > jobExecutionsCollection = mongoTemplate .getCollection ("BATCH_JOB_EXECUTION" );
163+ MongoCollection <Document > stepExecutionsCollection = mongoTemplate .getCollection ("BATCH_STEP_EXECUTION" );
164+
165+ Document jobDoc = jobExecutionsCollection .find (new Document ("jobExecutionId" , jobExecution .getId ())).first ();
166+ Assertions .assertTrue (jobDoc .getList ("stepExecutions" , Document .class ).isEmpty (),
167+ "Embedded stepExecutions array should be empty" );
168+ Assertions .assertEquals (2 , stepExecutionsCollection .countDocuments (),
169+ "BATCH_STEP_EXECUTION collection should still contain data" );
170+
171+ // Step 4: Test the fix - getLastStepExecution should work despite empty embedded
172+ // array
173+ StepExecution lastStepExecution = stepExecutionDao .getLastStepExecution (jobInstance , "step1" );
174+ Assertions .assertNotNull (lastStepExecution ,
175+ "getLastStepExecution should find step execution even with empty embedded array" );
176+ Assertions .assertEquals ("step1" , lastStepExecution .getStepName ());
177+ Assertions .assertEquals (BatchStatus .COMPLETED , lastStepExecution .getStatus ());
178+
179+ // Step 5: Test countStepExecutions also works
180+ long stepCount = stepExecutionDao .countStepExecutions (jobInstance , "step1" );
181+ Assertions .assertEquals (1L , stepCount , "countStepExecutions should work despite empty embedded array" );
182+ }
183+
115184 private static void dump (MongoCollection <Document > collection , String prefix ) {
116185 for (Document document : collection .find ()) {
117186 System .out .println (prefix + document .toJson ());
0 commit comments