spring_batch_rs/core/job.rs
1use std::{
2 cell::RefCell,
3 collections::HashMap,
4 time::{Duration, Instant},
5};
6
7use log::info;
8use uuid::Uuid;
9
10use crate::{core::step::StepExecution, BatchError};
11
12use super::step::Step;
13
14/// Type alias for job execution results.
15///
16/// A `JobResult` is a `Result` that contains either:
17/// - A successful `JobExecution` with execution details
18/// - A `BatchError` indicating what went wrong
19type JobResult<T> = Result<T, BatchError>;
20
21/// Represents a job that can be executed.
22///
23/// This trait defines the contract for batch job execution. A job is a container
24/// for a sequence of steps that are executed in order. The job is responsible for
25/// orchestrating the steps and reporting the overall result.
26///
27/// # Design Pattern
28///
29/// The `Job` trait follows the Command Pattern, representing an operation that can be
30/// executed and track its own execution details.
31///
32/// # Implementation Note
33///
34/// Implementations of this trait should:
35/// - Execute the steps in the correct order
36/// - Handle any errors that occur during execution
37/// - Return execution details upon completion
38///
39/// # Example Usage
40///
41/// ```rust,no_run,compile_fail
42/// use spring_batch_rs::core::job::{Job, JobBuilder};
43/// use spring_batch_rs::core::step::StepBuilder;
44///
45/// // Create a step
46/// let step = StepBuilder::new()
47/// .name("process-data".to_string())
48/// .reader(&some_reader)
49/// .writer(&some_writer)
50/// .build();
51///
52/// // Create and run a job
53/// let job = JobBuilder::new()
54/// .name("data-processing-job".to_string())
55/// .start(&step)
56/// .build();
57///
58/// let result = job.run();
59/// ```
60pub trait Job {
61 /// Runs the job and returns the result of the job execution.
62 ///
63 /// # Returns
64 /// - `Ok(JobExecution)` when the job executes successfully
65 /// - `Err(BatchError)` when the job execution fails
66 fn run(&self) -> JobResult<JobExecution>;
67}
68
69/// Represents the execution of a job.
70///
71/// A `JobExecution` contains timing information about a job run:
72/// - When it started
73/// - When it ended
74/// - How long it took to execute
75///
76/// This information is useful for monitoring and reporting on job performance.
77#[derive(Debug)]
78pub struct JobExecution {
79 /// The time when the job started executing
80 pub start: Instant,
81 /// The time when the job finished executing
82 pub end: Instant,
83 /// The total duration of the job execution
84 pub duration: Duration,
85}
86
87/// Represents an instance of a job.
88///
89/// A `JobInstance` defines a specific configuration of a job that can be executed.
90/// It contains:
91/// - A unique identifier
92/// - A name for the job
93/// - A sequence of steps to be executed
94///
95/// # Lifecycle
96///
97/// A job instance is created through the `JobBuilder` and executed by calling
98/// the `run` method. The steps are executed in the order they were added.
99pub struct JobInstance<'a> {
100 /// Unique identifier for this job instance
101 id: Uuid,
102 /// Human-readable name for the job
103 name: String,
104 /// Collection of steps that make up this job, in execution order
105 steps: Vec<&'a dyn Step>,
106 /// Step executions using interior mutability pattern
107 executions: RefCell<HashMap<String, StepExecution>>,
108}
109
110impl Job for JobInstance<'_> {
111 /// Runs the job by executing its steps in sequence.
112 ///
113 /// This method:
114 /// 1. Records the start time
115 /// 2. Logs the start of the job
116 /// 3. Executes each step in sequence
117 /// 4. If any step fails, returns an error
118 /// 5. Logs the end of the job
119 /// 6. Records the end time and calculates duration
120 /// 7. Returns the job execution details
121 ///
122 /// # Returns
123 /// - `Ok(JobExecution)` containing execution details if all steps succeed
124 /// - `Err(BatchError)` if any step fails
125 fn run(&self) -> JobResult<JobExecution> {
126 // Record the start time
127 let start = Instant::now();
128
129 // Log the job start
130 info!("Start of job: {}, id: {}", self.name, self.id);
131
132 // Execute all steps in sequence
133 let steps = &self.steps;
134
135 for step in steps {
136 let mut step_execution = StepExecution::new(step.get_name());
137
138 let result = step.execute(&mut step_execution);
139
140 // Store the execution
141 self.executions
142 .borrow_mut()
143 .insert(step.get_name().to_string(), step_execution.clone());
144
145 // If a step fails, abort the job and return an error
146 if result.is_err() {
147 return Err(BatchError::Step(step_execution.name));
148 }
149 }
150
151 // Log the job completion
152 info!("End of job: {}, id: {}", self.name, self.id);
153
154 // Create and return the job execution details
155 let job_execution = JobExecution {
156 start,
157 end: Instant::now(),
158 duration: start.elapsed(),
159 };
160
161 Ok(job_execution)
162 }
163}
164
165impl JobInstance<'_> {
166 /// Gets a step execution by name.
167 ///
168 /// This method allows retrieving the execution details of a specific step
169 /// that has been executed as part of this job.
170 ///
171 /// # Parameters
172 /// - `name`: The name of the step to retrieve execution details for
173 ///
174 /// # Returns
175 /// - `Some(StepExecution)` if a step with the given name was executed
176 /// - `None` if no step with the given name was found
177 ///
178 /// # Example
179 /// ```rust,no_run,compile_fail
180 /// let job = JobBuilder::new()
181 /// .name("test-job".to_string())
182 /// .start(&step)
183 /// .build();
184 ///
185 /// let _result = job.run();
186 ///
187 /// // Get execution details for a specific step
188 /// if let Some(execution) = job.get_step_execution("step-name") {
189 /// println!("Step duration: {:?}", execution.duration);
190 /// }
191 /// ```
192 pub fn get_step_execution(&self, name: &str) -> Option<StepExecution> {
193 self.executions.borrow().get(name).cloned()
194 }
195}
196
197/// Builder for creating a job instance.
198///
199/// The `JobBuilder` implements the Builder Pattern to provide a fluent API for
200/// constructing `JobInstance` objects. It allows setting the job name and adding
201/// steps to the job in a chainable manner.
202///
203/// # Design Pattern
204///
205/// This implements the Builder Pattern to separate the construction of complex `JobInstance`
206/// objects from their representation.
207///
208/// # Example
209///
210/// ```rust,no_run,compile_fail
211/// use spring_batch_rs::core::job::JobBuilder;
212///
213/// let job = JobBuilder::new()
214/// .name("import-customers".to_string())
215/// .start(&read_step)
216/// .next(&process_step)
217/// .next(&write_step)
218/// .build();
219/// ```
220#[derive(Default)]
221pub struct JobBuilder<'a> {
222 /// Optional name for the job (generated randomly if not specified)
223 name: Option<String>,
224 /// Collection of steps to be executed, in order
225 steps: Vec<&'a dyn Step>,
226}
227
228impl<'a> JobBuilder<'a> {
229 /// Creates a new `JobBuilder` instance.
230 ///
231 /// Initializes an empty job builder with no name and no steps.
232 ///
233 /// # Returns
234 /// A new `JobBuilder` instance
235 pub fn new() -> Self {
236 Self {
237 name: None,
238 steps: Vec::new(),
239 }
240 }
241
242 /// Sets the name of the job.
243 ///
244 /// # Parameters
245 /// - `name`: The name to assign to the job
246 ///
247 /// # Returns
248 /// The builder instance for method chaining
249 pub fn name(mut self, name: String) -> JobBuilder<'a> {
250 self.name = Some(name);
251 self
252 }
253
254 /// Sets the first step of the job.
255 ///
256 /// This method is semantically identical to `next()` but provides better readability
257 /// when constructing the initial step of a job.
258 ///
259 /// # Parameters
260 /// - `step`: The first step to be executed in the job
261 ///
262 /// # Returns
263 /// The builder instance for method chaining
264 pub fn start(mut self, step: &'a dyn Step) -> JobBuilder<'a> {
265 self.steps.push(step);
266 self
267 }
268
269 /// Adds a step to the job.
270 ///
271 /// Steps are executed in the order they are added.
272 ///
273 /// # Parameters
274 /// - `step`: The step to add to the job
275 ///
276 /// # Returns
277 /// The builder instance for method chaining
278 pub fn next(mut self, step: &'a dyn Step) -> JobBuilder<'a> {
279 self.steps.push(step);
280 self
281 }
282
283 /// Builds and returns a `JobInstance` based on the configured parameters.
284 ///
285 /// If no name has been provided, a random name is generated.
286 ///
287 /// # Returns
288 /// A fully configured `JobInstance` ready for execution
289 pub fn build(self) -> JobInstance<'a> {
290 JobInstance {
291 id: Uuid::new_v4(),
292 name: self.name.unwrap_or(Uuid::new_v4().to_string()),
293 steps: self.steps,
294 executions: RefCell::new(HashMap::new()),
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::{Job, JobBuilder};
302 use crate::core::{
303 item::{ItemReader, ItemReaderResult, ItemWriter, ItemWriterResult, PassThroughProcessor},
304 step::{StepBuilder, StepStatus},
305 };
306 use crate::BatchError;
307 use mockall::mock;
308
309 mock! {
310 TestItemReader {}
311 impl ItemReader<i32> for TestItemReader {
312 fn read(&self) -> ItemReaderResult<i32>;
313 }
314 }
315
316 mock! {
317 TestItemWriter {}
318 impl ItemWriter<i32> for TestItemWriter {
319 fn open(&self) -> ItemWriterResult;
320 fn write(&self, items: &[i32]) -> ItemWriterResult;
321 fn flush(&self) -> ItemWriterResult;
322 fn close(&self) -> ItemWriterResult;
323 }
324 }
325
326 #[test]
327 fn job_should_run_steps_in_sequence() {
328 let mut reader = MockTestItemReader::default();
329 let mut call_count = 0;
330 reader.expect_read().returning(move || {
331 call_count += 1;
332 if call_count <= 2 {
333 Ok(Some(call_count))
334 } else {
335 Ok(None)
336 }
337 });
338
339 let mut writer = MockTestItemWriter::default();
340 writer.expect_open().times(1).returning(|| Ok(()));
341 writer.expect_write().returning(|_| Ok(()));
342 writer.expect_flush().returning(|| Ok(()));
343 writer.expect_close().times(1).returning(|| Ok(()));
344
345 let processor = PassThroughProcessor::<i32>::new();
346 let step = StepBuilder::new("test")
347 .chunk(2)
348 .reader(&reader)
349 .processor(&processor)
350 .writer(&writer)
351 .build();
352
353 let job = JobBuilder::new()
354 .name("test-job".to_string())
355 .start(&step)
356 .build();
357
358 let result = job.run();
359 assert!(result.is_ok());
360 }
361
362 #[test]
363 fn job_should_fail_when_step_fails() {
364 let mut reader = MockTestItemReader::default();
365 reader
366 .expect_read()
367 .returning(|| Err(BatchError::ItemReader("read error".to_string())));
368
369 let mut writer = MockTestItemWriter::default();
370 writer.expect_open().times(1).returning(|| Ok(()));
371 writer.expect_close().times(1).returning(|| Ok(()));
372
373 let processor = PassThroughProcessor::<i32>::new();
374 let step = StepBuilder::new("failing-step")
375 .chunk(2)
376 .reader(&reader)
377 .processor(&processor)
378 .writer(&writer)
379 .build();
380
381 let job = JobBuilder::new()
382 .name("test-job".to_string())
383 .start(&step)
384 .build();
385
386 let result = job.run();
387 assert!(matches!(result.unwrap_err(), BatchError::Step(name) if name == "failing-step"));
388 }
389
390 #[test]
391 fn job_should_store_step_executions() {
392 let mut reader = MockTestItemReader::default();
393 reader.expect_read().returning(|| Ok(None));
394
395 let mut writer = MockTestItemWriter::default();
396 writer.expect_open().times(1).returning(|| Ok(()));
397 writer.expect_close().times(1).returning(|| Ok(()));
398
399 let processor = PassThroughProcessor::<i32>::new();
400 let step = StepBuilder::new("named-step")
401 .chunk(2)
402 .reader(&reader)
403 .processor(&processor)
404 .writer(&writer)
405 .build();
406
407 let job = JobBuilder::new()
408 .name("test-job".to_string())
409 .start(&step)
410 .build();
411
412 let _ = job.run();
413 let execution = job.get_step_execution("named-step");
414 assert!(execution.is_some());
415 assert_eq!(execution.unwrap().status, StepStatus::Success);
416 }
417
418 #[test]
419 fn job_should_run_multiple_steps_added_with_next() {
420 // Covers JobBuilder::next() at lines 278-280
421 let mut reader1 = MockTestItemReader::default();
422 reader1.expect_read().returning(|| Ok(None));
423
424 let mut writer1 = MockTestItemWriter::default();
425 writer1.expect_open().times(1).returning(|| Ok(()));
426 writer1.expect_close().times(1).returning(|| Ok(()));
427
428 let mut reader2 = MockTestItemReader::default();
429 reader2.expect_read().returning(|| Ok(None));
430
431 let mut writer2 = MockTestItemWriter::default();
432 writer2.expect_open().times(1).returning(|| Ok(()));
433 writer2.expect_close().times(1).returning(|| Ok(()));
434
435 let processor = PassThroughProcessor::<i32>::new();
436
437 let step1 = StepBuilder::new("step1")
438 .chunk(2)
439 .reader(&reader1)
440 .processor(&processor)
441 .writer(&writer1)
442 .build();
443
444 let step2 = StepBuilder::new("step2")
445 .chunk(2)
446 .reader(&reader2)
447 .processor(&processor)
448 .writer(&writer2)
449 .build();
450
451 let job = JobBuilder::new().start(&step1).next(&step2).build();
452
453 let result = job.run();
454 assert!(result.is_ok(), "job with two steps should succeed");
455 assert!(job.get_step_execution("step1").is_some());
456 assert!(job.get_step_execution("step2").is_some());
457 }
458}