Skip to main content

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}