mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-14 19:45:02 +00:00
Tests cleanup and stuff
This commit is contained in:
parent
0328333d81
commit
02aad40aa0
@ -51,5 +51,16 @@ dependencies {
|
|||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter-engine'
|
||||||
|
testImplementation 'org.mockito:mockito-core'
|
||||||
|
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package stirling.software.common.aop;
|
package stirling.software.common.aop;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
@ -42,7 +40,7 @@ public class AutoJobAspect {
|
|||||||
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
Object arg = args[i];
|
Object arg = args[i];
|
||||||
|
|
||||||
if (arg instanceof PDFFile pdfFile) {
|
if (arg instanceof PDFFile pdfFile) {
|
||||||
// Case 1: fileId is provided but no fileInput
|
// Case 1: fileId is provided but no fileInput
|
||||||
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
||||||
@ -54,24 +52,25 @@ public class AutoJobAspect {
|
|||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||||
else if (isAsyncRequest && pdfFile.getFileInput() != null) {
|
else if (isAsyncRequest && pdfFile.getFileInput() != null) {
|
||||||
try {
|
try {
|
||||||
log.debug("Making persistent copy of uploaded file for async processing");
|
log.debug("Making persistent copy of uploaded file for async processing");
|
||||||
MultipartFile originalFile = pdfFile.getFileInput();
|
MultipartFile originalFile = pdfFile.getFileInput();
|
||||||
String fileId = fileStorage.storeFile(originalFile);
|
String fileId = fileStorage.storeFile(originalFile);
|
||||||
|
|
||||||
// Store the fileId for later reference
|
// Store the fileId for later reference
|
||||||
pdfFile.setFileId(fileId);
|
pdfFile.setFileId(fileId);
|
||||||
|
|
||||||
// Replace the original MultipartFile with our persistent copy
|
// Replace the original MultipartFile with our persistent copy
|
||||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||||
pdfFile.setFileInput(persistentFile);
|
pdfFile.setFileInput(persistentFile);
|
||||||
|
|
||||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Failed to create persistent copy of uploaded file", e);
|
throw new RuntimeException(
|
||||||
|
"Failed to create persistent copy of uploaded file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.service.FileStorage;
|
||||||
|
import stirling.software.common.service.TaskManager;
|
||||||
|
|
||||||
|
/** REST controller for job-related endpoints */
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class JobController {
|
||||||
|
|
||||||
|
private final TaskManager taskManager;
|
||||||
|
private final FileStorage fileStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the status of a job
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return The job result
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/job/{jobId}")
|
||||||
|
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
if (result == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the result of a job
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return The job result
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/job/{jobId}/result")
|
||||||
|
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
if (result == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isComplete()) {
|
||||||
|
return ResponseEntity.badRequest().body("Job is not complete yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getError() != null) {
|
||||||
|
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getFileId() != null) {
|
||||||
|
try {
|
||||||
|
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Type", result.getContentType())
|
||||||
|
.header(
|
||||||
|
"Content-Disposition",
|
||||||
|
"form-data; name=\"attachment\"; filename=\""
|
||||||
|
+ result.getOriginalFileName()
|
||||||
|
+ "\"")
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Error retrieving file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result.getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about jobs in the system
|
||||||
|
*
|
||||||
|
* @return Job statistics
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/job/stats")
|
||||||
|
public ResponseEntity<JobStats> getJobStats() {
|
||||||
|
JobStats stats = taskManager.getJobStats();
|
||||||
|
return ResponseEntity.ok(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger cleanup of old jobs
|
||||||
|
*
|
||||||
|
* @return A response indicating how many jobs were cleaned up
|
||||||
|
*/
|
||||||
|
@PostMapping("/api/v1/general/job/cleanup")
|
||||||
|
public ResponseEntity<?> cleanupOldJobs() {
|
||||||
|
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
taskManager.cleanupOldJobs();
|
||||||
|
int afterCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
int removedCount = beforeCount - afterCount;
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"message", "Cleanup complete",
|
||||||
|
"removedJobs", removedCount,
|
||||||
|
"remainingJobs", afterCount));
|
||||||
|
}
|
||||||
|
}
|
@ -7,64 +7,43 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/** Represents the result of a job execution. Used by the TaskManager to store job results. */
|
||||||
* Represents the result of a job execution.
|
|
||||||
* Used by the TaskManager to store job results.
|
|
||||||
*/
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class JobResult {
|
public class JobResult {
|
||||||
|
|
||||||
/**
|
/** The job ID */
|
||||||
* The job ID
|
|
||||||
*/
|
|
||||||
private String jobId;
|
private String jobId;
|
||||||
|
|
||||||
/**
|
/** Flag indicating if the job is complete */
|
||||||
* Flag indicating if the job is complete
|
|
||||||
*/
|
|
||||||
private boolean complete;
|
private boolean complete;
|
||||||
|
|
||||||
/**
|
/** Error message if the job failed */
|
||||||
* Error message if the job failed
|
|
||||||
*/
|
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
/**
|
/** The file ID of the result file, if applicable */
|
||||||
* The file ID of the result file, if applicable
|
|
||||||
*/
|
|
||||||
private String fileId;
|
private String fileId;
|
||||||
|
|
||||||
/**
|
/** Original file name, if applicable */
|
||||||
* Original file name, if applicable
|
|
||||||
*/
|
|
||||||
private String originalFileName;
|
private String originalFileName;
|
||||||
|
|
||||||
/**
|
/** MIME type of the result, if applicable */
|
||||||
* MIME type of the result, if applicable
|
|
||||||
*/
|
|
||||||
private String contentType;
|
private String contentType;
|
||||||
|
|
||||||
/**
|
/** Time when the job was created */
|
||||||
* Time when the job was created
|
|
||||||
*/
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
/**
|
/** Time when the job was completed */
|
||||||
* Time when the job was completed
|
|
||||||
*/
|
|
||||||
private LocalDateTime completedAt;
|
private LocalDateTime completedAt;
|
||||||
|
|
||||||
/**
|
/** The actual result object, if not a file */
|
||||||
* The actual result object, if not a file
|
|
||||||
*/
|
|
||||||
private Object result;
|
private Object result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new JobResult with the given job ID
|
* Create a new JobResult with the given job ID
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return A new JobResult
|
* @return A new JobResult
|
||||||
*/
|
*/
|
||||||
@ -75,10 +54,10 @@ public class JobResult {
|
|||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this job as complete with a file result
|
* Mark this job as complete with a file result
|
||||||
*
|
*
|
||||||
* @param fileId The file ID of the result
|
* @param fileId The file ID of the result
|
||||||
* @param originalFileName The original file name
|
* @param originalFileName The original file name
|
||||||
* @param contentType The content type of the file
|
* @param contentType The content type of the file
|
||||||
@ -90,10 +69,10 @@ public class JobResult {
|
|||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
this.completedAt = LocalDateTime.now();
|
this.completedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this job as complete with a general result
|
* Mark this job as complete with a general result
|
||||||
*
|
*
|
||||||
* @param result The result object
|
* @param result The result object
|
||||||
*/
|
*/
|
||||||
public void completeWithResult(Object result) {
|
public void completeWithResult(Object result) {
|
||||||
@ -101,10 +80,10 @@ public class JobResult {
|
|||||||
this.result = result;
|
this.result = result;
|
||||||
this.completedAt = LocalDateTime.now();
|
this.completedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this job as failed with an error message
|
* Mark this job as failed with an error message
|
||||||
*
|
*
|
||||||
* @param error The error message
|
* @param error The error message
|
||||||
*/
|
*/
|
||||||
public void failWithError(String error) {
|
public void failWithError(String error) {
|
||||||
@ -112,4 +91,4 @@ public class JobResult {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
this.completedAt = LocalDateTime.now();
|
this.completedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package stirling.software.common.model.job;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/** Represents statistics about jobs in the system */
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class JobStats {
|
||||||
|
|
||||||
|
/** Total number of jobs (active and completed) */
|
||||||
|
private int totalJobs;
|
||||||
|
|
||||||
|
/** Number of active (incomplete) jobs */
|
||||||
|
private int activeJobs;
|
||||||
|
|
||||||
|
/** Number of completed jobs */
|
||||||
|
private int completedJobs;
|
||||||
|
|
||||||
|
/** Number of failed jobs */
|
||||||
|
private int failedJobs;
|
||||||
|
|
||||||
|
/** Number of successful jobs */
|
||||||
|
private int successfulJobs;
|
||||||
|
|
||||||
|
/** Number of jobs with file results */
|
||||||
|
private int fileResultJobs;
|
||||||
|
|
||||||
|
/** The oldest active job's creation timestamp */
|
||||||
|
private LocalDateTime oldestActiveJobTime;
|
||||||
|
|
||||||
|
/** The newest active job's creation timestamp */
|
||||||
|
private LocalDateTime newestActiveJobTime;
|
||||||
|
|
||||||
|
/** The average processing time for completed jobs in milliseconds */
|
||||||
|
private long averageProcessingTimeMs;
|
||||||
|
}
|
@ -13,22 +13,22 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for storing and retrieving files with unique file IDs.
|
* Service for storing and retrieving files with unique file IDs. Used by the AutoJobPostMapping
|
||||||
* Used by the AutoJobPostMapping system to handle file references.
|
* system to handle file references.
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class FileStorage {
|
public class FileStorage {
|
||||||
|
|
||||||
@Value("${stirling.tempDir:/tmp/stirling-files}")
|
@Value("${stirling.tempDir:/tmp/stirling-files}")
|
||||||
private String tempDirPath;
|
private String tempDirPath;
|
||||||
|
|
||||||
private final FileOrUploadService fileOrUploadService;
|
private final FileOrUploadService fileOrUploadService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a file and return its unique ID
|
* Store a file and return its unique ID
|
||||||
*
|
*
|
||||||
* @param file The file to store
|
* @param file The file to store
|
||||||
* @return The unique ID assigned to the file
|
* @return The unique ID assigned to the file
|
||||||
* @throws IOException If there is an error storing the file
|
* @throws IOException If there is an error storing the file
|
||||||
@ -36,20 +36,20 @@ public class FileStorage {
|
|||||||
public String storeFile(MultipartFile file) throws IOException {
|
public String storeFile(MultipartFile file) throws IOException {
|
||||||
String fileId = generateFileId();
|
String fileId = generateFileId();
|
||||||
Path filePath = getFilePath(fileId);
|
Path filePath = getFilePath(fileId);
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
Files.createDirectories(filePath.getParent());
|
Files.createDirectories(filePath.getParent());
|
||||||
|
|
||||||
// Transfer the file to the storage location
|
// Transfer the file to the storage location
|
||||||
file.transferTo(filePath.toFile());
|
file.transferTo(filePath.toFile());
|
||||||
|
|
||||||
log.debug("Stored file with ID: {}", fileId);
|
log.debug("Stored file with ID: {}", fileId);
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a byte array as a file and return its unique ID
|
* Store a byte array as a file and return its unique ID
|
||||||
*
|
*
|
||||||
* @param bytes The byte array to store
|
* @param bytes The byte array to store
|
||||||
* @param originalName The original name of the file (for extension)
|
* @param originalName The original name of the file (for extension)
|
||||||
* @return The unique ID assigned to the file
|
* @return The unique ID assigned to the file
|
||||||
@ -58,55 +58,55 @@ public class FileStorage {
|
|||||||
public String storeBytes(byte[] bytes, String originalName) throws IOException {
|
public String storeBytes(byte[] bytes, String originalName) throws IOException {
|
||||||
String fileId = generateFileId();
|
String fileId = generateFileId();
|
||||||
Path filePath = getFilePath(fileId);
|
Path filePath = getFilePath(fileId);
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
Files.createDirectories(filePath.getParent());
|
Files.createDirectories(filePath.getParent());
|
||||||
|
|
||||||
// Write the bytes to the file
|
// Write the bytes to the file
|
||||||
Files.write(filePath, bytes);
|
Files.write(filePath, bytes);
|
||||||
|
|
||||||
log.debug("Stored byte array with ID: {}", fileId);
|
log.debug("Stored byte array with ID: {}", fileId);
|
||||||
return fileId;
|
return fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a file by its ID as a MultipartFile
|
* Retrieve a file by its ID as a MultipartFile
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file to retrieve
|
* @param fileId The ID of the file to retrieve
|
||||||
* @return The file as a MultipartFile
|
* @return The file as a MultipartFile
|
||||||
* @throws IOException If the file doesn't exist or can't be read
|
* @throws IOException If the file doesn't exist or can't be read
|
||||||
*/
|
*/
|
||||||
public MultipartFile retrieveFile(String fileId) throws IOException {
|
public MultipartFile retrieveFile(String fileId) throws IOException {
|
||||||
Path filePath = getFilePath(fileId);
|
Path filePath = getFilePath(fileId);
|
||||||
|
|
||||||
if (!Files.exists(filePath)) {
|
if (!Files.exists(filePath)) {
|
||||||
throw new IOException("File not found with ID: " + fileId);
|
throw new IOException("File not found with ID: " + fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] fileData = Files.readAllBytes(filePath);
|
byte[] fileData = Files.readAllBytes(filePath);
|
||||||
return fileOrUploadService.toMockMultipartFile(fileId, fileData);
|
return fileOrUploadService.toMockMultipartFile(fileId, fileData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a file by its ID as a byte array
|
* Retrieve a file by its ID as a byte array
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file to retrieve
|
* @param fileId The ID of the file to retrieve
|
||||||
* @return The file as a byte array
|
* @return The file as a byte array
|
||||||
* @throws IOException If the file doesn't exist or can't be read
|
* @throws IOException If the file doesn't exist or can't be read
|
||||||
*/
|
*/
|
||||||
public byte[] retrieveBytes(String fileId) throws IOException {
|
public byte[] retrieveBytes(String fileId) throws IOException {
|
||||||
Path filePath = getFilePath(fileId);
|
Path filePath = getFilePath(fileId);
|
||||||
|
|
||||||
if (!Files.exists(filePath)) {
|
if (!Files.exists(filePath)) {
|
||||||
throw new IOException("File not found with ID: " + fileId);
|
throw new IOException("File not found with ID: " + fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Files.readAllBytes(filePath);
|
return Files.readAllBytes(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a file by its ID
|
* Delete a file by its ID
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file to delete
|
* @param fileId The ID of the file to delete
|
||||||
* @return true if the file was deleted, false otherwise
|
* @return true if the file was deleted, false otherwise
|
||||||
*/
|
*/
|
||||||
@ -119,10 +119,10 @@ public class FileStorage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file exists by its ID
|
* Check if a file exists by its ID
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file to check
|
* @param fileId The ID of the file to check
|
||||||
* @return true if the file exists, false otherwise
|
* @return true if the file exists, false otherwise
|
||||||
*/
|
*/
|
||||||
@ -130,23 +130,23 @@ public class FileStorage {
|
|||||||
Path filePath = getFilePath(fileId);
|
Path filePath = getFilePath(fileId);
|
||||||
return Files.exists(filePath);
|
return Files.exists(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path for a file ID
|
* Get the path for a file ID
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file
|
* @param fileId The ID of the file
|
||||||
* @return The path to the file
|
* @return The path to the file
|
||||||
*/
|
*/
|
||||||
private Path getFilePath(String fileId) {
|
private Path getFilePath(String fileId) {
|
||||||
return Path.of(tempDirPath).resolve(fileId);
|
return Path.of(tempDirPath).resolve(fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a unique file ID
|
* Generate a unique file ID
|
||||||
*
|
*
|
||||||
* @return A unique file ID
|
* @return A unique file ID
|
||||||
*/
|
*/
|
||||||
private String generateFileId() {
|
private String generateFileId() {
|
||||||
return UUID.randomUUID().toString();
|
return UUID.randomUUID().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,37 +4,54 @@ import java.io.IOException;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.controller.WebSocketProgressController;
|
import stirling.software.common.controller.WebSocketProgressController;
|
||||||
import stirling.software.common.model.job.JobProgress;
|
import stirling.software.common.model.job.JobProgress;
|
||||||
import stirling.software.common.model.job.JobResponse;
|
import stirling.software.common.model.job.JobResponse;
|
||||||
|
|
||||||
/**
|
/** Service for executing jobs asynchronously or synchronously */
|
||||||
* Service for executing jobs asynchronously or synchronously
|
|
||||||
*/
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class JobExecutorService {
|
public class JobExecutorService {
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
private final TaskManager taskManager;
|
||||||
private final WebSocketProgressController webSocketSender;
|
private final WebSocketProgressController webSocketSender;
|
||||||
private final FileStorage fileStorage;
|
private final FileStorage fileStorage;
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
private final long effectiveTimeoutMs;
|
||||||
|
|
||||||
|
public JobExecutorService(
|
||||||
|
TaskManager taskManager,
|
||||||
|
WebSocketProgressController webSocketSender,
|
||||||
|
FileStorage fileStorage,
|
||||||
|
@Value("${spring.mvc.async.request-timeout:1200000}") long asyncRequestTimeoutMs,
|
||||||
|
@Value("${server.servlet.session.timeout:30m}") String sessionTimeout) {
|
||||||
|
this.taskManager = taskManager;
|
||||||
|
this.webSocketSender = webSocketSender;
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
|
|
||||||
|
// Parse session timeout and calculate effective timeout once during initialization
|
||||||
|
long sessionTimeoutMs = parseSessionTimeout(sessionTimeout);
|
||||||
|
this.effectiveTimeoutMs = Math.min(asyncRequestTimeoutMs, sessionTimeoutMs);
|
||||||
|
log.info(
|
||||||
|
"Job executor configured with effective timeout of {} ms", this.effectiveTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a job either asynchronously or synchronously
|
* Run a job either asynchronously or synchronously
|
||||||
*
|
*
|
||||||
* @param async Whether to run the job asynchronously
|
* @param async Whether to run the job asynchronously
|
||||||
* @param work The work to be done
|
* @param work The work to be done
|
||||||
* @return The response
|
* @return The response
|
||||||
@ -50,10 +67,22 @@ public class JobExecutorService {
|
|||||||
executor.execute(
|
executor.execute(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
Object result = work.get();
|
log.debug(
|
||||||
|
"Running async job {} with timeout {} ms",
|
||||||
|
jobId,
|
||||||
|
effectiveTimeoutMs);
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
Object result =
|
||||||
|
executeWithTimeout(() -> work.get(), effectiveTimeoutMs);
|
||||||
processJobResult(jobId, result);
|
processJobResult(jobId, result);
|
||||||
webSocketSender.sendProgress(
|
webSocketSender.sendProgress(
|
||||||
jobId, new JobProgress(jobId, "Done", 100, "Complete"));
|
jobId, new JobProgress(jobId, "Done", 100, "Complete"));
|
||||||
|
} catch (TimeoutException te) {
|
||||||
|
log.error("Job {} timed out after {} ms", jobId, effectiveTimeoutMs);
|
||||||
|
taskManager.setError(jobId, "Job timed out");
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId, new JobProgress(jobId, "Error", 100, "Job timed out"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
||||||
taskManager.setError(jobId, e.getMessage());
|
taskManager.setError(jobId, e.getMessage());
|
||||||
@ -65,25 +94,32 @@ public class JobExecutorService {
|
|||||||
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
Object result = work.get();
|
log.debug("Running sync job with timeout {} ms", effectiveTimeoutMs);
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
Object result = executeWithTimeout(() -> work.get(), effectiveTimeoutMs);
|
||||||
|
|
||||||
// If the result is already a ResponseEntity, return it directly
|
// If the result is already a ResponseEntity, return it directly
|
||||||
if (result instanceof ResponseEntity) {
|
if (result instanceof ResponseEntity) {
|
||||||
return (ResponseEntity<?>) result;
|
return (ResponseEntity<?>) result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process different result types
|
// Process different result types
|
||||||
return handleResultForSyncJob(result);
|
return handleResultForSyncJob(result);
|
||||||
|
} catch (TimeoutException te) {
|
||||||
|
log.error("Synchronous job timed out after {} ms", effectiveTimeoutMs);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Job timed out after " + effectiveTimeoutMs + " ms");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||||
return ResponseEntity.internalServerError().body("Job failed: " + e.getMessage());
|
return ResponseEntity.internalServerError().body("Job failed: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the result of an asynchronous job
|
* Process the result of an asynchronous job
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @param result The result
|
* @param result The result
|
||||||
*/
|
*/
|
||||||
@ -97,25 +133,27 @@ public class JobExecutorService {
|
|||||||
} else if (result instanceof ResponseEntity) {
|
} else if (result instanceof ResponseEntity) {
|
||||||
ResponseEntity<?> response = (ResponseEntity<?>) result;
|
ResponseEntity<?> response = (ResponseEntity<?>) result;
|
||||||
Object body = response.getBody();
|
Object body = response.getBody();
|
||||||
|
|
||||||
if (body instanceof byte[]) {
|
if (body instanceof byte[]) {
|
||||||
// Extract filename from content-disposition header if available
|
// Extract filename from content-disposition header if available
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
if (response.getHeaders().getContentDisposition() != null) {
|
if (response.getHeaders().getContentDisposition() != null) {
|
||||||
String disposition = response.getHeaders().getContentDisposition().toString();
|
String disposition =
|
||||||
|
response.getHeaders().getContentDisposition().toString();
|
||||||
if (disposition.contains("filename=")) {
|
if (disposition.contains("filename=")) {
|
||||||
filename = disposition.substring(
|
filename =
|
||||||
disposition.indexOf("filename=") + 9,
|
disposition.substring(
|
||||||
disposition.lastIndexOf("\""));
|
disposition.indexOf("filename=") + 9,
|
||||||
|
disposition.lastIndexOf("\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.getHeaders().getContentType() != null) {
|
if (response.getHeaders().getContentType() != null) {
|
||||||
contentType = response.getHeaders().getContentType().toString();
|
contentType = response.getHeaders().getContentType().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String fileId = fileStorage.storeBytes((byte[]) body, filename);
|
String fileId = fileStorage.storeBytes((byte[]) body, filename);
|
||||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||||
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
|
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
|
||||||
@ -124,26 +162,30 @@ public class JobExecutorService {
|
|||||||
if (body != null && body.toString().contains("fileId")) {
|
if (body != null && body.toString().contains("fileId")) {
|
||||||
try {
|
try {
|
||||||
// Try to extract fileId using reflection
|
// Try to extract fileId using reflection
|
||||||
java.lang.reflect.Method getFileId = body.getClass().getMethod("getFileId");
|
java.lang.reflect.Method getFileId =
|
||||||
|
body.getClass().getMethod("getFileId");
|
||||||
String fileId = (String) getFileId.invoke(body);
|
String fileId = (String) getFileId.invoke(body);
|
||||||
|
|
||||||
if (fileId != null && !fileId.isEmpty()) {
|
if (fileId != null && !fileId.isEmpty()) {
|
||||||
// Try to get filename and content type
|
// Try to get filename and content type
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getOriginalFileName = body.getClass().getMethod("getOriginalFilename");
|
java.lang.reflect.Method getOriginalFileName =
|
||||||
|
body.getClass().getMethod("getOriginalFilename");
|
||||||
String origName = (String) getOriginalFileName.invoke(body);
|
String origName = (String) getOriginalFileName.invoke(body);
|
||||||
if (origName != null && !origName.isEmpty()) {
|
if (origName != null && !origName.isEmpty()) {
|
||||||
filename = origName;
|
filename = origName;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Could not get original filename: {}", e.getMessage());
|
log.debug(
|
||||||
|
"Could not get original filename: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getContentType = body.getClass().getMethod("getContentType");
|
java.lang.reflect.Method getContentType =
|
||||||
|
body.getClass().getMethod("getContentType");
|
||||||
String ct = (String) getContentType.invoke(body);
|
String ct = (String) getContentType.invoke(body);
|
||||||
if (ct != null && !ct.isEmpty()) {
|
if (ct != null && !ct.isEmpty()) {
|
||||||
contentType = ct;
|
contentType = ct;
|
||||||
@ -151,18 +193,20 @@ public class JobExecutorService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Could not get content type: {}", e.getMessage());
|
log.debug("Could not get content type: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||||
log.debug("Extracted fileId from response body: {}", fileId);
|
log.debug("Extracted fileId from response body: {}", fileId);
|
||||||
|
|
||||||
taskManager.setComplete(jobId);
|
taskManager.setComplete(jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Failed to extract fileId from response body: {}", e.getMessage());
|
log.debug(
|
||||||
|
"Failed to extract fileId from response body: {}",
|
||||||
|
e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store generic result
|
// Store generic result
|
||||||
taskManager.setResult(jobId, body);
|
taskManager.setResult(jobId, body);
|
||||||
}
|
}
|
||||||
@ -170,26 +214,25 @@ public class JobExecutorService {
|
|||||||
MultipartFile file = (MultipartFile) result;
|
MultipartFile file = (MultipartFile) result;
|
||||||
String fileId = fileStorage.storeFile(file);
|
String fileId = fileStorage.storeFile(file);
|
||||||
taskManager.setFileResult(
|
taskManager.setFileResult(
|
||||||
jobId,
|
jobId, fileId, file.getOriginalFilename(), file.getContentType());
|
||||||
fileId,
|
|
||||||
file.getOriginalFilename(),
|
|
||||||
file.getContentType());
|
|
||||||
log.debug("Stored MultipartFile result with fileId: {}", fileId);
|
log.debug("Stored MultipartFile result with fileId: {}", fileId);
|
||||||
} else {
|
} else {
|
||||||
// Check if result has a fileId field
|
// Check if result has a fileId field
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
try {
|
try {
|
||||||
// Try to extract fileId using reflection
|
// Try to extract fileId using reflection
|
||||||
java.lang.reflect.Method getFileId = result.getClass().getMethod("getFileId");
|
java.lang.reflect.Method getFileId =
|
||||||
|
result.getClass().getMethod("getFileId");
|
||||||
String fileId = (String) getFileId.invoke(result);
|
String fileId = (String) getFileId.invoke(result);
|
||||||
|
|
||||||
if (fileId != null && !fileId.isEmpty()) {
|
if (fileId != null && !fileId.isEmpty()) {
|
||||||
// Try to get filename and content type
|
// Try to get filename and content type
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getOriginalFileName = result.getClass().getMethod("getOriginalFilename");
|
java.lang.reflect.Method getOriginalFileName =
|
||||||
|
result.getClass().getMethod("getOriginalFilename");
|
||||||
String origName = (String) getOriginalFileName.invoke(result);
|
String origName = (String) getOriginalFileName.invoke(result);
|
||||||
if (origName != null && !origName.isEmpty()) {
|
if (origName != null && !origName.isEmpty()) {
|
||||||
filename = origName;
|
filename = origName;
|
||||||
@ -197,9 +240,10 @@ public class JobExecutorService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Could not get original filename: {}", e.getMessage());
|
log.debug("Could not get original filename: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getContentType = result.getClass().getMethod("getContentType");
|
java.lang.reflect.Method getContentType =
|
||||||
|
result.getClass().getMethod("getContentType");
|
||||||
String ct = (String) getContentType.invoke(result);
|
String ct = (String) getContentType.invoke(result);
|
||||||
if (ct != null && !ct.isEmpty()) {
|
if (ct != null && !ct.isEmpty()) {
|
||||||
contentType = ct;
|
contentType = ct;
|
||||||
@ -207,32 +251,33 @@ public class JobExecutorService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Could not get content type: {}", e.getMessage());
|
log.debug("Could not get content type: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||||
log.debug("Extracted fileId from result object: {}", fileId);
|
log.debug("Extracted fileId from result object: {}", fileId);
|
||||||
|
|
||||||
taskManager.setComplete(jobId);
|
taskManager.setComplete(jobId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Failed to extract fileId from result object: {}", e.getMessage());
|
log.debug(
|
||||||
|
"Failed to extract fileId from result object: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default case: store the result as is
|
// Default case: store the result as is
|
||||||
taskManager.setResult(jobId, result);
|
taskManager.setResult(jobId, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
taskManager.setComplete(jobId);
|
taskManager.setComplete(jobId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error processing job result: {}", e.getMessage(), e);
|
log.error("Error processing job result: {}", e.getMessage(), e);
|
||||||
taskManager.setError(jobId, "Error processing result: " + e.getMessage());
|
taskManager.setError(jobId, "Error processing result: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle different result types for synchronous jobs
|
* Handle different result types for synchronous jobs
|
||||||
*
|
*
|
||||||
* @param result The result object
|
* @param result The result object
|
||||||
* @return The appropriate ResponseEntity
|
* @return The appropriate ResponseEntity
|
||||||
* @throws IOException If there is an error processing the result
|
* @throws IOException If there is an error processing the result
|
||||||
@ -242,7 +287,8 @@ public class JobExecutorService {
|
|||||||
// Return byte array as PDF
|
// Return byte array as PDF
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(
|
||||||
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
||||||
.body(result);
|
.body(result);
|
||||||
} else if (result instanceof MultipartFile) {
|
} else if (result instanceof MultipartFile) {
|
||||||
@ -250,13 +296,74 @@ public class JobExecutorService {
|
|||||||
MultipartFile file = (MultipartFile) result;
|
MultipartFile file = (MultipartFile) result;
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(
|
||||||
"form-data; name=\"attachment\"; filename=\"" +
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
file.getOriginalFilename() + "\"")
|
"form-data; name=\"attachment\"; filename=\""
|
||||||
|
+ file.getOriginalFilename()
|
||||||
|
+ "\"")
|
||||||
.body(file.getBytes());
|
.body(file.getBytes());
|
||||||
} else {
|
} else {
|
||||||
// Default case: return as JSON
|
// Default case: return as JSON
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse session timeout string (e.g., "30m", "1h") to milliseconds
|
||||||
|
*
|
||||||
|
* @param timeout The timeout string
|
||||||
|
* @return The timeout in milliseconds
|
||||||
|
*/
|
||||||
|
private long parseSessionTimeout(String timeout) {
|
||||||
|
if (timeout == null || timeout.isEmpty()) {
|
||||||
|
return 30 * 60 * 1000; // Default: 30 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String value = timeout.replaceAll("[^\\d.]", "");
|
||||||
|
String unit = timeout.replaceAll("[\\d.]", "");
|
||||||
|
|
||||||
|
double numericValue = Double.parseDouble(value);
|
||||||
|
|
||||||
|
return switch (unit.toLowerCase()) {
|
||||||
|
case "s" -> (long) (numericValue * 1000);
|
||||||
|
case "m" -> (long) (numericValue * 60 * 1000);
|
||||||
|
case "h" -> (long) (numericValue * 60 * 60 * 1000);
|
||||||
|
case "d" -> (long) (numericValue * 24 * 60 * 60 * 1000);
|
||||||
|
default -> (long) (numericValue * 60 * 1000); // Default to minutes
|
||||||
|
};
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not parse session timeout '{}', using default", timeout);
|
||||||
|
return 30 * 60 * 1000; // Default: 30 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a supplier with a timeout
|
||||||
|
*
|
||||||
|
* @param supplier The supplier to execute
|
||||||
|
* @param timeoutMs The timeout in milliseconds
|
||||||
|
* @return The result from the supplier
|
||||||
|
* @throws TimeoutException If the execution times out
|
||||||
|
* @throws Exception If the supplier throws an exception
|
||||||
|
*/
|
||||||
|
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs)
|
||||||
|
throws TimeoutException, Exception {
|
||||||
|
java.util.concurrent.CompletableFuture<T> future =
|
||||||
|
java.util.concurrent.CompletableFuture.supplyAsync(supplier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new TimeoutException("Execution timed out after " + timeoutMs + " ms");
|
||||||
|
} catch (java.util.concurrent.ExecutionException e) {
|
||||||
|
throw (Exception) e.getCause();
|
||||||
|
} catch (java.util.concurrent.CancellationException e) {
|
||||||
|
throw new Exception("Execution was cancelled", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new Exception("Execution was interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,65 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
|
||||||
/**
|
/** Manages async tasks and their results */
|
||||||
* Manages async tasks and their results
|
|
||||||
*/
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TaskManager {
|
public class TaskManager {
|
||||||
private final Map<String, JobResult> jobResults = new ConcurrentHashMap<>();
|
private final Map<String, JobResult> jobResults = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Value("${stirling.jobResultExpiryMinutes:30}")
|
@Value("${stirling.jobResultExpiryMinutes:30}")
|
||||||
private int jobResultExpiryMinutes = 30;
|
private int jobResultExpiryMinutes = 30;
|
||||||
|
|
||||||
private final FileStorage fileStorage;
|
private final FileStorage fileStorage;
|
||||||
|
private final ScheduledExecutorService cleanupExecutor =
|
||||||
|
Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
/** Initialize the task manager and start the cleanup scheduler */
|
||||||
|
public TaskManager(FileStorage fileStorage) {
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
|
|
||||||
|
// Schedule periodic cleanup of old job results
|
||||||
|
cleanupExecutor.scheduleAtFixedRate(
|
||||||
|
this::cleanupOldJobs,
|
||||||
|
10, // Initial delay
|
||||||
|
10, // Interval
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Task manager initialized with job result expiry of {} minutes",
|
||||||
|
jobResultExpiryMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new task with the given job ID
|
* Create a new task with the given job ID
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
*/
|
*/
|
||||||
public void createTask(String jobId) {
|
public void createTask(String jobId) {
|
||||||
jobResults.put(jobId, JobResult.createNew(jobId));
|
jobResults.put(jobId, JobResult.createNew(jobId));
|
||||||
log.debug("Created task with job ID: {}", jobId);
|
log.debug("Created task with job ID: {}", jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the result of a task as a general object
|
* Set the result of a task as a general object
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @param result The result object
|
* @param result The result object
|
||||||
*/
|
*/
|
||||||
@ -50,24 +68,25 @@ public class TaskManager {
|
|||||||
jobResult.completeWithResult(result);
|
jobResult.completeWithResult(result);
|
||||||
log.debug("Set result for job ID: {}", jobId);
|
log.debug("Set result for job ID: {}", jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the result of a task as a file
|
* Set the result of a task as a file
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @param fileId The file ID
|
* @param fileId The file ID
|
||||||
* @param originalFileName The original file name
|
* @param originalFileName The original file name
|
||||||
* @param contentType The content type of the file
|
* @param contentType The content type of the file
|
||||||
*/
|
*/
|
||||||
public void setFileResult(String jobId, String fileId, String originalFileName, String contentType) {
|
public void setFileResult(
|
||||||
|
String jobId, String fileId, String originalFileName, String contentType) {
|
||||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||||
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
||||||
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set an error for a task
|
* Set an error for a task
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @param error The error message
|
* @param error The error message
|
||||||
*/
|
*/
|
||||||
@ -76,24 +95,26 @@ public class TaskManager {
|
|||||||
jobResult.failWithError(error);
|
jobResult.failWithError(error);
|
||||||
log.debug("Set error for job ID: {}: {}", jobId, error);
|
log.debug("Set error for job ID: {}: {}", jobId, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a task as complete
|
* Mark a task as complete
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
*/
|
*/
|
||||||
public void setComplete(String jobId) {
|
public void setComplete(String jobId) {
|
||||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||||
if (jobResult.getResult() == null && jobResult.getFileId() == null && jobResult.getError() == null) {
|
if (jobResult.getResult() == null
|
||||||
|
&& jobResult.getFileId() == null
|
||||||
|
&& jobResult.getError() == null) {
|
||||||
// If no result or error has been set, mark it as complete with an empty result
|
// If no result or error has been set, mark it as complete with an empty result
|
||||||
jobResult.completeWithResult("Task completed successfully");
|
jobResult.completeWithResult("Task completed successfully");
|
||||||
}
|
}
|
||||||
log.debug("Marked job ID: {} as complete", jobId);
|
log.debug("Marked job ID: {} as complete", jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a task is complete
|
* Check if a task is complete
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return true if the task is complete, false otherwise
|
* @return true if the task is complete, false otherwise
|
||||||
*/
|
*/
|
||||||
@ -101,86 +122,153 @@ public class TaskManager {
|
|||||||
JobResult result = jobResults.get(jobId);
|
JobResult result = jobResults.get(jobId);
|
||||||
return result != null && result.isComplete();
|
return result != null && result.isComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the result of a task
|
* Get the result of a task
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return The result object, or null if the task doesn't exist or is not complete
|
* @return The result object, or null if the task doesn't exist or is not complete
|
||||||
*/
|
*/
|
||||||
public JobResult getJobResult(String jobId) {
|
public JobResult getJobResult(String jobId) {
|
||||||
return jobResults.get(jobId);
|
return jobResults.get(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about all jobs in the system
|
||||||
|
*
|
||||||
|
* @return Job statistics
|
||||||
|
*/
|
||||||
|
public JobStats getJobStats() {
|
||||||
|
int totalJobs = jobResults.size();
|
||||||
|
int activeJobs = 0;
|
||||||
|
int completedJobs = 0;
|
||||||
|
int failedJobs = 0;
|
||||||
|
int successfulJobs = 0;
|
||||||
|
int fileResultJobs = 0;
|
||||||
|
|
||||||
|
LocalDateTime oldestActiveJobTime = null;
|
||||||
|
LocalDateTime newestActiveJobTime = null;
|
||||||
|
long totalProcessingTimeMs = 0;
|
||||||
|
|
||||||
|
for (JobResult result : jobResults.values()) {
|
||||||
|
if (result.isComplete()) {
|
||||||
|
completedJobs++;
|
||||||
|
|
||||||
|
// Calculate processing time for completed jobs
|
||||||
|
if (result.getCreatedAt() != null && result.getCompletedAt() != null) {
|
||||||
|
long processingTimeMs =
|
||||||
|
java.time.Duration.between(
|
||||||
|
result.getCreatedAt(), result.getCompletedAt())
|
||||||
|
.toMillis();
|
||||||
|
totalProcessingTimeMs += processingTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getError() != null) {
|
||||||
|
failedJobs++;
|
||||||
|
} else {
|
||||||
|
successfulJobs++;
|
||||||
|
if (result.getFileId() != null) {
|
||||||
|
fileResultJobs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeJobs++;
|
||||||
|
|
||||||
|
// Track oldest and newest active jobs
|
||||||
|
if (result.getCreatedAt() != null) {
|
||||||
|
if (oldestActiveJobTime == null
|
||||||
|
|| result.getCreatedAt().isBefore(oldestActiveJobTime)) {
|
||||||
|
oldestActiveJobTime = result.getCreatedAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newestActiveJobTime == null
|
||||||
|
|| result.getCreatedAt().isAfter(newestActiveJobTime)) {
|
||||||
|
newestActiveJobTime = result.getCreatedAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate average processing time
|
||||||
|
long averageProcessingTimeMs =
|
||||||
|
completedJobs > 0 ? totalProcessingTimeMs / completedJobs : 0;
|
||||||
|
|
||||||
|
return JobStats.builder()
|
||||||
|
.totalJobs(totalJobs)
|
||||||
|
.activeJobs(activeJobs)
|
||||||
|
.completedJobs(completedJobs)
|
||||||
|
.failedJobs(failedJobs)
|
||||||
|
.successfulJobs(successfulJobs)
|
||||||
|
.fileResultJobs(fileResultJobs)
|
||||||
|
.oldestActiveJobTime(oldestActiveJobTime)
|
||||||
|
.newestActiveJobTime(newestActiveJobTime)
|
||||||
|
.averageProcessingTimeMs(averageProcessingTimeMs)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a job result
|
* Get or create a job result
|
||||||
*
|
*
|
||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return The job result
|
* @return The job result
|
||||||
*/
|
*/
|
||||||
private JobResult getOrCreateJobResult(String jobId) {
|
private JobResult getOrCreateJobResult(String jobId) {
|
||||||
return jobResults.computeIfAbsent(jobId, JobResult::createNew);
|
return jobResults.computeIfAbsent(jobId, JobResult::createNew);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Clean up old completed job results */
|
||||||
* REST controller for job-related endpoints
|
public void cleanupOldJobs() {
|
||||||
*/
|
LocalDateTime expiryThreshold =
|
||||||
@RestController
|
LocalDateTime.now().minus(jobResultExpiryMinutes, ChronoUnit.MINUTES);
|
||||||
public class JobController {
|
int removedCount = 0;
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Get the status of a job
|
for (Map.Entry<String, JobResult> entry : jobResults.entrySet()) {
|
||||||
*
|
JobResult result = entry.getValue();
|
||||||
* @param jobId The job ID
|
|
||||||
* @return The job result
|
// Remove completed jobs that are older than the expiry threshold
|
||||||
*/
|
if (result.isComplete()
|
||||||
@GetMapping("/api/v1/general/job/{jobId}")
|
&& result.getCompletedAt() != null
|
||||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||||
JobResult result = jobResults.get(jobId);
|
|
||||||
if (result == null) {
|
// If the job has a file result, delete the file
|
||||||
return ResponseEntity.notFound().build();
|
if (result.getFileId() != null) {
|
||||||
}
|
try {
|
||||||
return ResponseEntity.ok(result);
|
fileStorage.deleteFile(result.getFileId());
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
/**
|
"Failed to delete file for job {}: {}",
|
||||||
* Get the result of a job
|
entry.getKey(),
|
||||||
*
|
e.getMessage());
|
||||||
* @param jobId The job ID
|
}
|
||||||
* @return The job result
|
}
|
||||||
*/
|
|
||||||
@GetMapping("/api/v1/general/job/{jobId}/result")
|
// Remove the job result
|
||||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
jobResults.remove(entry.getKey());
|
||||||
JobResult result = jobResults.get(jobId);
|
removedCount++;
|
||||||
if (result == null) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.isComplete()) {
|
|
||||||
return ResponseEntity.badRequest().body("Job is not complete yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.getError() != null) {
|
|
||||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.getFileId() != null) {
|
|
||||||
try {
|
|
||||||
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header("Content-Type", result.getContentType())
|
|
||||||
.header("Content-Disposition",
|
|
||||||
"form-data; name=\"attachment\"; filename=\"" +
|
|
||||||
result.getOriginalFileName() + "\"")
|
|
||||||
.body(fileContent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
|
||||||
return ResponseEntity.internalServerError()
|
|
||||||
.body("Error retrieving file: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(result.getResult());
|
if (removedCount > 0) {
|
||||||
|
log.info("Cleaned up {} expired job results", removedCount);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during job cleanup: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shutdown the cleanup executor */
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
try {
|
||||||
|
log.info("Shutting down job result cleanup executor");
|
||||||
|
cleanupExecutor.shutdown();
|
||||||
|
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
cleanupExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
cleanupExecutor.shutdownNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,227 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.service.FileStorage;
|
||||||
|
import stirling.software.common.service.TaskManager;
|
||||||
|
|
||||||
|
class JobControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskManager taskManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private JobController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStatus_ExistingJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(mockResult, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStatus_NonExistentJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "non-existent-job";
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedSuccessfulWithObject() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.setComplete(true);
|
||||||
|
String resultObject = "Test result";
|
||||||
|
mockResult.completeWithResult(resultObject);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(resultObject, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedSuccessfulWithFile() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
byte[] fileContent = "Test file content".getBytes();
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(contentType, response.getHeaders().getFirst("Content-Type"));
|
||||||
|
assertTrue(response.getHeaders().getFirst("Content-Disposition").contains(originalFileName));
|
||||||
|
assertEquals(fileContent, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedWithError() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.failWithError(errorMessage);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_IncompleteJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.setComplete(false);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains("not complete"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_NonExistentJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "non-existent-job";
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_ErrorRetrievingFile() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains("Error retrieving file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStats() {
|
||||||
|
// Arrange
|
||||||
|
JobStats mockStats = JobStats.builder()
|
||||||
|
.totalJobs(10)
|
||||||
|
.activeJobs(3)
|
||||||
|
.completedJobs(7)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(taskManager.getJobStats()).thenReturn(mockStats);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<JobStats> response = controller.getJobStats();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(mockStats, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupOldJobs() {
|
||||||
|
// Arrange
|
||||||
|
when(taskManager.getJobStats())
|
||||||
|
.thenReturn(JobStats.builder().totalJobs(10).build())
|
||||||
|
.thenReturn(JobStats.builder().totalJobs(7).build());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.cleanupOldJobs();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
||||||
|
assertEquals("Cleanup complete", responseBody.get("message"));
|
||||||
|
assertEquals(3, responseBody.get("removedJobs"));
|
||||||
|
assertEquals(7, responseBody.get("remainingJobs"));
|
||||||
|
|
||||||
|
verify(taskManager).cleanupOldJobs();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
|
||||||
|
class WebSocketProgressControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private WebSocketProgressController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendProgress_WithMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
controller.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(messagingTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendProgress_WithNullMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
WebSocketProgressController controllerWithNullTemplate = new WebSocketProgressController();
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
|
||||||
|
// Act - should not throw exception even with null template
|
||||||
|
controllerWithNullTemplate.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// No assertion needed - test passes if no exception is thrown
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
WebSocketProgressController newController = new WebSocketProgressController();
|
||||||
|
SimpMessagingTemplate newTemplate = mock(SimpMessagingTemplate.class);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
newController.setMessagingTemplate(newTemplate);
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
newController.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(newTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,190 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.AdditionalAnswers.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
class FileStorageTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileOrUploadService fileOrUploadService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
private MultipartFile mockFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
ReflectionTestUtils.setField(fileStorage, "tempDirPath", tempDir.toString());
|
||||||
|
|
||||||
|
// Create a mock MultipartFile
|
||||||
|
mockFile = mock(MultipartFile.class);
|
||||||
|
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
||||||
|
when(mockFile.getContentType()).thenReturn("application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStoreFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
when(mockFile.getBytes()).thenReturn(fileContent);
|
||||||
|
|
||||||
|
// Set up mock to handle transferTo by writing the file
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
java.io.File file = invocation.getArgument(0);
|
||||||
|
Files.write(file.toPath(), fileContent);
|
||||||
|
return null;
|
||||||
|
}).when(mockFile).transferTo(any(java.io.File.class));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String fileId = fileStorage.storeFile(mockFile);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(fileId);
|
||||||
|
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||||
|
verify(mockFile).transferTo(any(java.io.File.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStoreBytes() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String originalName = "test.pdf";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String fileId = fileStorage.storeBytes(fileContent, originalName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(fileId);
|
||||||
|
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||||
|
assertArrayEquals(fileContent, Files.readAllBytes(tempDir.resolve(fileId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
MultipartFile expectedFile = mock(MultipartFile.class);
|
||||||
|
when(fileOrUploadService.toMockMultipartFile(eq(fileId), eq(fileContent)))
|
||||||
|
.thenReturn(expectedFile);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
MultipartFile result = fileStorage.retrieveFile(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertSame(expectedFile, result);
|
||||||
|
verify(fileOrUploadService).toMockMultipartFile(eq(fileId), eq(fileContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveBytes() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
byte[] result = fileStorage.retrieveBytes(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertArrayEquals(fileContent, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveFile_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertThrows(IOException.class, () -> fileStorage.retrieveFile(nonExistentFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveBytes_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertThrows(IOException.class, () -> fileStorage.retrieveBytes(nonExistentFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.deleteFile(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result);
|
||||||
|
assertFalse(Files.exists(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteFile_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.deleteFile(nonExistentFileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFileExists() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.fileExists(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFileExists_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.fileExists(nonExistentFileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
|
||||||
|
class TaskManagerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private TaskManager taskManager;
|
||||||
|
|
||||||
|
private AutoCloseable closeable;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
closeable = MockitoAnnotations.openMocks(this);
|
||||||
|
ReflectionTestUtils.setField(taskManager, "jobResultExpiryMinutes", 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
closeable.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTask() {
|
||||||
|
// Act
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(jobId, result.getJobId());
|
||||||
|
assertFalse(result.isComplete());
|
||||||
|
assertNotNull(result.getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
Object resultObject = "Test result";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setResult(jobId, resultObject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(resultObject, result.getResult());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetFileResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(fileId, result.getFileId());
|
||||||
|
assertEquals(originalFileName, result.getOriginalFileName());
|
||||||
|
assertEquals(contentType, result.getContentType());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetError() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setError(jobId, errorMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(errorMessage, result.getError());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetComplete_WithExistingResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
Object resultObject = "Test result";
|
||||||
|
taskManager.setResult(jobId, resultObject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(resultObject, result.getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetComplete_WithoutExistingResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals("Task completed successfully", result.getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsComplete() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Assert - not complete initially
|
||||||
|
assertFalse(taskManager.isComplete(jobId));
|
||||||
|
|
||||||
|
// Act - mark as complete
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert - now complete
|
||||||
|
assertTrue(taskManager.isComplete(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStats() {
|
||||||
|
// Arrange
|
||||||
|
// 1. Create active job
|
||||||
|
String activeJobId = "active-job";
|
||||||
|
taskManager.createTask(activeJobId);
|
||||||
|
|
||||||
|
// 2. Create completed successful job with file
|
||||||
|
String successFileJobId = "success-file-job";
|
||||||
|
taskManager.createTask(successFileJobId);
|
||||||
|
taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf");
|
||||||
|
|
||||||
|
// 3. Create completed successful job without file
|
||||||
|
String successJobId = "success-job";
|
||||||
|
taskManager.createTask(successJobId);
|
||||||
|
taskManager.setResult(successJobId, "Result");
|
||||||
|
|
||||||
|
// 4. Create failed job
|
||||||
|
String failedJobId = "failed-job";
|
||||||
|
taskManager.createTask(failedJobId);
|
||||||
|
taskManager.setError(failedJobId, "Error message");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
JobStats stats = taskManager.getJobStats();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(4, stats.getTotalJobs());
|
||||||
|
assertEquals(1, stats.getActiveJobs());
|
||||||
|
assertEquals(3, stats.getCompletedJobs());
|
||||||
|
assertEquals(1, stats.getFailedJobs());
|
||||||
|
assertEquals(2, stats.getSuccessfulJobs());
|
||||||
|
assertEquals(1, stats.getFileResultJobs());
|
||||||
|
assertNotNull(stats.getNewestActiveJobTime());
|
||||||
|
assertNotNull(stats.getOldestActiveJobTime());
|
||||||
|
assertTrue(stats.getAverageProcessingTimeMs() >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupOldJobs() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
// 1. Create a recent completed job
|
||||||
|
String recentJobId = "recent-job";
|
||||||
|
taskManager.createTask(recentJobId);
|
||||||
|
taskManager.setResult(recentJobId, "Result");
|
||||||
|
|
||||||
|
// 2. Create an old completed job with file result
|
||||||
|
String oldJobId = "old-job";
|
||||||
|
taskManager.createTask(oldJobId);
|
||||||
|
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||||
|
|
||||||
|
// Manually set the completion time to be older than the expiry
|
||||||
|
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
||||||
|
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
||||||
|
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
||||||
|
|
||||||
|
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||||
|
|
||||||
|
// Obtain access to the private jobResults map
|
||||||
|
Map<String, JobResult> jobResultsMap = (Map<String, JobResult>) ReflectionTestUtils.getField(taskManager, "jobResults");
|
||||||
|
|
||||||
|
// 3. Create an active job
|
||||||
|
String activeJobId = "active-job";
|
||||||
|
taskManager.createTask(activeJobId);
|
||||||
|
|
||||||
|
// Verify all jobs are in the map
|
||||||
|
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(oldJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.cleanupOldJobs();
|
||||||
|
|
||||||
|
// Assert - the old job should be removed
|
||||||
|
assertFalse(jobResultsMap.containsKey(oldJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||||
|
verify(fileStorage).deleteFile("file-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShutdown() throws Exception {
|
||||||
|
// This mainly tests that the shutdown method doesn't throw exceptions
|
||||||
|
taskManager.shutdown();
|
||||||
|
|
||||||
|
// Verify the executor service is shutdown
|
||||||
|
// This is difficult to test directly, but we can verify it doesn't throw exceptions
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user