mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Compare commits
No commits in common. "083b68e2a0e9a44843ce41d7099e940ab97f0c60" and "2d1841367625ce61be894606e46f25352fc81ae7" have entirely different histories.
083b68e2a0
...
2d18413676
@ -1,15 +1,11 @@
|
|||||||
package stirling.software.common.aop;
|
package stirling.software.common.aop;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.*;
|
import org.aspectj.lang.annotation.*;
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@ -28,11 +24,8 @@ import stirling.software.common.service.JobExecutorService;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Order(0) // Highest precedence - executes before audit aspects
|
|
||||||
public class AutoJobAspect {
|
public class AutoJobAspect {
|
||||||
|
|
||||||
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
|
|
||||||
|
|
||||||
private final JobExecutorService jobExecutorService;
|
private final JobExecutorService jobExecutorService;
|
||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
private final FileOrUploadService fileOrUploadService;
|
private final FileOrUploadService fileOrUploadService;
|
||||||
@ -41,7 +34,6 @@ public class AutoJobAspect {
|
|||||||
@Around("@annotation(autoJobPostMapping)")
|
@Around("@annotation(autoJobPostMapping)")
|
||||||
public Object wrapWithJobExecution(
|
public Object wrapWithJobExecution(
|
||||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
||||||
// This aspect will run before any audit aspects due to @Order(0)
|
|
||||||
// Extract parameters from the request and annotation
|
// Extract parameters from the request and annotation
|
||||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||||
long timeout = autoJobPostMapping.timeout();
|
long timeout = autoJobPostMapping.timeout();
|
||||||
@ -55,23 +47,50 @@ public class AutoJobAspect {
|
|||||||
retryCount,
|
retryCount,
|
||||||
trackProgress);
|
trackProgress);
|
||||||
|
|
||||||
// Copy and process arguments
|
// Inspect and possibly mutate arguments
|
||||||
// In a test environment, we might need to update the original objects for verification
|
Object[] args = joinPoint.getArgs();
|
||||||
boolean isTestEnvironment = false;
|
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
Object arg = args[i];
|
||||||
|
|
||||||
|
if (arg instanceof PDFFile pdfFile) {
|
||||||
|
// Case 1: fileId is provided but no fileInput
|
||||||
|
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
||||||
try {
|
try {
|
||||||
isTestEnvironment = Class.forName("org.junit.jupiter.api.Test") != null;
|
log.debug("Using fileId {} to get file content", pdfFile.getFileId());
|
||||||
} catch (ClassNotFoundException e) {
|
MultipartFile file = fileStorage.retrieveFile(pdfFile.getFileId());
|
||||||
// Not in a test environment
|
pdfFile.setFileInput(file);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||||
|
else if (async && pdfFile.getFileInput() != null) {
|
||||||
|
try {
|
||||||
|
log.debug("Making persistent copy of uploaded file for async processing");
|
||||||
|
MultipartFile originalFile = pdfFile.getFileInput();
|
||||||
|
String fileId = fileStorage.storeFile(originalFile);
|
||||||
|
|
||||||
|
// Store the fileId for later reference
|
||||||
|
pdfFile.setFileId(fileId);
|
||||||
|
|
||||||
|
// Replace the original MultipartFile with our persistent copy
|
||||||
|
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||||
|
pdfFile.setFileInput(persistentFile);
|
||||||
|
|
||||||
|
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to create persistent copy of uploaded file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object[] args =
|
// Extract queueable and resourceWeight parameters
|
||||||
isTestEnvironment
|
|
||||||
? processArgsInPlace(joinPoint.getArgs(), async)
|
|
||||||
: copyAndProcessArgs(joinPoint.getArgs(), async);
|
|
||||||
|
|
||||||
// Extract queueable and resourceWeight parameters and validate
|
|
||||||
boolean queueable = autoJobPostMapping.queueable();
|
boolean queueable = autoJobPostMapping.queueable();
|
||||||
int resourceWeight = Math.max(1, Math.min(100, autoJobPostMapping.resourceWeight()));
|
int resourceWeight = autoJobPostMapping.resourceWeight();
|
||||||
|
|
||||||
// Integrate with the JobExecutorService
|
// Integrate with the JobExecutorService
|
||||||
if (retryCount <= 1) {
|
if (retryCount <= 1) {
|
||||||
@ -119,17 +138,14 @@ public class AutoJobAspect {
|
|||||||
boolean queueable,
|
boolean queueable,
|
||||||
int resourceWeight) {
|
int resourceWeight) {
|
||||||
|
|
||||||
|
AtomicInteger attempts = new AtomicInteger(0);
|
||||||
// Keep jobId reference for progress tracking in TaskManager
|
// Keep jobId reference for progress tracking in TaskManager
|
||||||
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
||||||
|
|
||||||
return jobExecutorService.runJobGeneric(
|
return jobExecutorService.runJobGeneric(
|
||||||
async,
|
async,
|
||||||
() -> {
|
() -> {
|
||||||
// Use iterative approach instead of recursion to avoid stack overflow
|
int currentAttempt = attempts.incrementAndGet();
|
||||||
Throwable lastException = null;
|
|
||||||
|
|
||||||
// Attempt counter starts at 1 for first try
|
|
||||||
for (int currentAttempt = 1; currentAttempt <= maxRetries; currentAttempt++) {
|
|
||||||
try {
|
try {
|
||||||
if (trackProgress && async) {
|
if (trackProgress && async) {
|
||||||
// Get jobId for progress tracking in TaskManager
|
// Get jobId for progress tracking in TaskManager
|
||||||
@ -149,15 +165,12 @@ public class AutoJobAspect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to execute the operation
|
|
||||||
return joinPoint.proceed(args);
|
return joinPoint.proceed(args);
|
||||||
|
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
lastException = ex;
|
|
||||||
log.error(
|
log.error(
|
||||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||||
currentAttempt,
|
currentAttempt,
|
||||||
maxRetries,
|
Math.max(1, maxRetries),
|
||||||
ex.getMessage(),
|
ex.getMessage(),
|
||||||
ex);
|
ex);
|
||||||
|
|
||||||
@ -178,184 +191,35 @@ public class AutoJobAspect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use non-blocking delay for all retry attempts to avoid blocking
|
|
||||||
// threads
|
|
||||||
// For sync jobs this avoids starving the tomcat thread pool under
|
|
||||||
// load
|
|
||||||
long delayMs = RETRY_BASE_DELAY.toMillis() * currentAttempt;
|
|
||||||
|
|
||||||
// Execute the retry after a delay through the JobExecutorService
|
|
||||||
// rather than blocking the current thread with sleep
|
|
||||||
CompletableFuture<Object> delayedRetry = new CompletableFuture<>();
|
|
||||||
|
|
||||||
// Use a delayed executor for non-blocking delay
|
|
||||||
CompletableFuture.delayedExecutor(delayMs, TimeUnit.MILLISECONDS)
|
|
||||||
.execute(
|
|
||||||
() -> {
|
|
||||||
// Continue the retry loop in the next iteration
|
|
||||||
// We can't return from here directly since
|
|
||||||
// we're in a Runnable
|
|
||||||
delayedRetry.complete(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for the delay to complete before continuing
|
|
||||||
try {
|
try {
|
||||||
delayedRetry.join();
|
// Simple exponential backoff
|
||||||
} catch (Exception e) {
|
Thread.sleep(100 * currentAttempt);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No more retries, we'll throw the exception after the loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, all retries failed
|
// Recursive call to retry
|
||||||
if (lastException != null) {
|
return executeWithRetries(
|
||||||
throw new RuntimeException(
|
joinPoint,
|
||||||
"Job failed after "
|
args,
|
||||||
+ maxRetries
|
async,
|
||||||
+ " attempts: "
|
timeout,
|
||||||
+ lastException.getMessage(),
|
maxRetries,
|
||||||
lastException);
|
trackProgress,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should never happen if lastException is properly tracked
|
// No more retries, throw the exception
|
||||||
throw new RuntimeException("Job failed but no exception was recorded");
|
throw new RuntimeException("Job failed: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
timeout,
|
timeout,
|
||||||
queueable,
|
queueable,
|
||||||
resourceWeight);
|
resourceWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Get the job ID from the context for progress tracking in TaskManager
|
||||||
* Creates deep copies of arguments when needed to avoid mutating the original objects
|
|
||||||
* Particularly important for PDFFile objects that might be reused by Spring
|
|
||||||
*
|
|
||||||
* @param originalArgs The original arguments
|
|
||||||
* @param async Whether this is an async operation
|
|
||||||
* @return A new array with safely processed arguments
|
|
||||||
*/
|
|
||||||
private Object[] copyAndProcessArgs(Object[] originalArgs, boolean async) {
|
|
||||||
if (originalArgs == null || originalArgs.length == 0) {
|
|
||||||
return originalArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object[] processedArgs = new Object[originalArgs.length];
|
|
||||||
|
|
||||||
// Copy all arguments
|
|
||||||
for (int i = 0; i < originalArgs.length; i++) {
|
|
||||||
Object arg = originalArgs[i];
|
|
||||||
|
|
||||||
if (arg instanceof PDFFile pdfFile) {
|
|
||||||
// Create a copy of PDFFile to avoid mutating the original
|
|
||||||
PDFFile pdfFileCopy = new PDFFile();
|
|
||||||
|
|
||||||
// Use Spring's BeanUtils to copy all properties, avoiding missed fields if PDFFile
|
|
||||||
// grows
|
|
||||||
BeanUtils.copyProperties(pdfFile, pdfFileCopy);
|
|
||||||
|
|
||||||
// Case 1: fileId is provided but no fileInput
|
|
||||||
if (pdfFileCopy.getFileInput() == null && pdfFileCopy.getFileId() != null) {
|
|
||||||
try {
|
|
||||||
log.debug("Using fileId {} to get file content", pdfFileCopy.getFileId());
|
|
||||||
MultipartFile file = fileStorage.retrieveFile(pdfFileCopy.getFileId());
|
|
||||||
pdfFileCopy.setFileInput(file);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to resolve file by ID: " + pdfFileCopy.getFileId(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
|
||||||
else if (async && pdfFileCopy.getFileInput() != null) {
|
|
||||||
try {
|
|
||||||
log.debug("Making persistent copy of uploaded file for async processing");
|
|
||||||
MultipartFile originalFile = pdfFileCopy.getFileInput();
|
|
||||||
String fileId = fileStorage.storeFile(originalFile);
|
|
||||||
|
|
||||||
// Store the fileId for later reference
|
|
||||||
pdfFileCopy.setFileId(fileId);
|
|
||||||
|
|
||||||
// Replace the original MultipartFile with our persistent copy
|
|
||||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
|
||||||
pdfFileCopy.setFileInput(persistentFile);
|
|
||||||
|
|
||||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to create persistent copy of uploaded file", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processedArgs[i] = pdfFileCopy;
|
|
||||||
} else {
|
|
||||||
// For non-PDFFile objects, just pass the original reference
|
|
||||||
// If other classes need copy-on-write, add them here
|
|
||||||
processedArgs[i] = arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes arguments in-place for testing purposes This is similar to our original
|
|
||||||
* implementation before introducing copy-on-write It's only used in test environments to
|
|
||||||
* maintain test compatibility
|
|
||||||
*
|
|
||||||
* @param originalArgs The original arguments
|
|
||||||
* @param async Whether this is an async operation
|
|
||||||
* @return The original array with processed arguments
|
|
||||||
*/
|
|
||||||
private Object[] processArgsInPlace(Object[] originalArgs, boolean async) {
|
|
||||||
if (originalArgs == null || originalArgs.length == 0) {
|
|
||||||
return originalArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all arguments in-place
|
|
||||||
for (int i = 0; i < originalArgs.length; i++) {
|
|
||||||
Object arg = originalArgs[i];
|
|
||||||
|
|
||||||
if (arg instanceof PDFFile pdfFile) {
|
|
||||||
// Case 1: fileId is provided but no fileInput
|
|
||||||
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
|
||||||
try {
|
|
||||||
log.debug("Using fileId {} to get file content", pdfFile.getFileId());
|
|
||||||
MultipartFile file = fileStorage.retrieveFile(pdfFile.getFileId());
|
|
||||||
pdfFile.setFileInput(file);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
|
||||||
else if (async && pdfFile.getFileInput() != null) {
|
|
||||||
try {
|
|
||||||
log.debug("Making persistent copy of uploaded file for async processing");
|
|
||||||
MultipartFile originalFile = pdfFile.getFileInput();
|
|
||||||
String fileId = fileStorage.storeFile(originalFile);
|
|
||||||
|
|
||||||
// Store the fileId for later reference
|
|
||||||
pdfFile.setFileId(fileId);
|
|
||||||
|
|
||||||
// Replace the original MultipartFile with our persistent copy
|
|
||||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
|
||||||
pdfFile.setFileInput(persistentFile);
|
|
||||||
|
|
||||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to create persistent copy of uploaded file", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getJobIdFromContext() {
|
private String getJobIdFromContext() {
|
||||||
try {
|
try {
|
||||||
return (String) request.getAttribute("jobId");
|
return (String) request.getAttribute("jobId");
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package stirling.software.common.model.job;
|
package stirling.software.common.model.job;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@ -44,12 +41,6 @@ public class JobResult {
|
|||||||
/** The actual result object, if not a file */
|
/** The actual result object, if not a file */
|
||||||
private Object result;
|
private Object result;
|
||||||
|
|
||||||
/**
|
|
||||||
* Notes attached to this job for tracking purposes. Uses CopyOnWriteArrayList for thread safety
|
|
||||||
* when notes are added concurrently.
|
|
||||||
*/
|
|
||||||
private final List<String> notes = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new JobResult with the given job ID
|
* Create a new JobResult with the given job ID
|
||||||
*
|
*
|
||||||
@ -100,22 +91,4 @@ public class JobResult {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
this.completedAt = LocalDateTime.now();
|
this.completedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a note to this job
|
|
||||||
*
|
|
||||||
* @param note The note to add
|
|
||||||
*/
|
|
||||||
public void addNote(String note) {
|
|
||||||
this.notes.add(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all notes attached to this job
|
|
||||||
*
|
|
||||||
* @return An unmodifiable view of the notes list
|
|
||||||
*/
|
|
||||||
public List<String> getNotes() {
|
|
||||||
return Collections.unmodifiableList(notes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -102,22 +102,6 @@ public class JobExecutorService {
|
|||||||
// Store the job ID in the request for potential use by other components
|
// Store the job ID in the request for potential use by other components
|
||||||
if (request != null) {
|
if (request != null) {
|
||||||
request.setAttribute("jobId", jobId);
|
request.setAttribute("jobId", jobId);
|
||||||
|
|
||||||
// Also track this job ID in the user's session for authorization purposes
|
|
||||||
// This ensures users can only cancel their own jobs
|
|
||||||
if (request.getSession() != null) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
java.util.Set<String> userJobIds =
|
|
||||||
(java.util.Set<String>) request.getSession().getAttribute("userJobIds");
|
|
||||||
|
|
||||||
if (userJobIds == null) {
|
|
||||||
userJobIds = new java.util.concurrent.ConcurrentSkipListSet<>();
|
|
||||||
request.getSession().setAttribute("userJobIds", userJobIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
userJobIds.add(jobId);
|
|
||||||
log.debug("Added job ID {} to user session", jobId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which timeout to use
|
// Determine which timeout to use
|
||||||
@ -225,13 +209,10 @@ public class JobExecutorService {
|
|||||||
private void processJobResult(String jobId, Object result) {
|
private void processJobResult(String jobId, Object result) {
|
||||||
try {
|
try {
|
||||||
if (result instanceof byte[]) {
|
if (result instanceof byte[]) {
|
||||||
// Store byte array directly to disk to avoid double memory consumption
|
// Store byte array as a file
|
||||||
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
||||||
taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf");
|
taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf");
|
||||||
log.debug("Stored byte[] result with fileId: {}", fileId);
|
log.debug("Stored byte[] result with fileId: {}", fileId);
|
||||||
|
|
||||||
// Let the byte array get collected naturally in the next GC cycle
|
|
||||||
// We don't need to force System.gc() which can be harmful
|
|
||||||
} 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();
|
||||||
@ -256,12 +237,9 @@ public class JobExecutorService {
|
|||||||
contentType = response.getHeaders().getContentType().toString();
|
contentType = response.getHeaders().getContentType().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store byte array directly to disk
|
|
||||||
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);
|
||||||
|
|
||||||
// Let the GC handle the memory naturally
|
|
||||||
} else {
|
} else {
|
||||||
// Check if the response body contains a fileId
|
// Check if the response body contains a fileId
|
||||||
if (body != null && body.toString().contains("fileId")) {
|
if (body != null && body.toString().contains("fileId")) {
|
||||||
@ -454,10 +432,8 @@ public class JobExecutorService {
|
|||||||
*/
|
*/
|
||||||
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs)
|
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs)
|
||||||
throws TimeoutException, Exception {
|
throws TimeoutException, Exception {
|
||||||
// Use the same executor as other async jobs for consistency
|
|
||||||
// This ensures all operations run on the same thread pool
|
|
||||||
java.util.concurrent.CompletableFuture<T> future =
|
java.util.concurrent.CompletableFuture<T> future =
|
||||||
java.util.concurrent.CompletableFuture.supplyAsync(supplier, executor);
|
java.util.concurrent.CompletableFuture.supplyAsync(supplier);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
@ -6,17 +6,18 @@ import java.util.concurrent.*;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.SmartLifecycle;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.util.ExecutorFactory;
|
import stirling.software.common.util.ExecutorFactory;
|
||||||
import stirling.software.common.util.SpringContextHolder;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a queue of jobs with dynamic sizing based on system resources. Used when system resources
|
* Manages a queue of jobs with dynamic sizing based on system resources. Used when system resources
|
||||||
@ -24,9 +25,7 @@ import stirling.software.common.util.SpringContextHolder;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class JobQueue implements SmartLifecycle {
|
public class JobQueue {
|
||||||
|
|
||||||
private volatile boolean running = false;
|
|
||||||
|
|
||||||
private final ResourceMonitor resourceMonitor;
|
private final ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
@ -42,11 +41,10 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
@Value("${stirling.job.queue.max-wait-time-ms:600000}")
|
@Value("${stirling.job.queue.max-wait-time-ms:600000}")
|
||||||
private long maxWaitTimeMs = 600000; // 10 minutes
|
private long maxWaitTimeMs = 600000; // 10 minutes
|
||||||
|
|
||||||
private volatile BlockingQueue<QueuedJob> jobQueue;
|
private BlockingQueue<QueuedJob> jobQueue;
|
||||||
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||||
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
|
|
||||||
|
|
||||||
private boolean shuttingDown = false;
|
private boolean shuttingDown = false;
|
||||||
|
|
||||||
@ -78,8 +76,8 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
this.jobQueue = new LinkedBlockingQueue<>(capacity);
|
this.jobQueue = new LinkedBlockingQueue<>(capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove @PostConstruct to let SmartLifecycle control startup
|
@PostConstruct
|
||||||
private void initializeSchedulers() {
|
public void initialize() {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Starting job queue with base capacity {}, min capacity {}",
|
"Starting job queue with base capacity {}, min capacity {}",
|
||||||
baseQueueCapacity,
|
baseQueueCapacity,
|
||||||
@ -97,8 +95,8 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove @PreDestroy to let SmartLifecycle control shutdown
|
@PreDestroy
|
||||||
private void shutdownSchedulers() {
|
public void shutdown() {
|
||||||
log.info("Shutting down job queue");
|
log.info("Shutting down job queue");
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
|
|
||||||
@ -111,22 +109,8 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Shutdown schedulers and wait for termination
|
|
||||||
try {
|
|
||||||
scheduler.shutdown();
|
|
||||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
scheduler.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
jobExecutor.shutdown();
|
|
||||||
if (!jobExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
||||||
jobExecutor.shutdownNow();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
scheduler.shutdownNow();
|
scheduler.shutdownNow();
|
||||||
jobExecutor.shutdownNow();
|
jobExecutor.shutdownNow();
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"Job queue shutdown complete. Stats: total={}, rejected={}",
|
"Job queue shutdown complete. Stats: total={}, rejected={}",
|
||||||
@ -134,40 +118,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
rejectedJobs);
|
rejectedJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmartLifecycle methods
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
log.info("Starting JobQueue lifecycle");
|
|
||||||
if (!running) {
|
|
||||||
initializeSchedulers();
|
|
||||||
running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
log.info("Stopping JobQueue lifecycle");
|
|
||||||
shutdownSchedulers();
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRunning() {
|
|
||||||
return running;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPhase() {
|
|
||||||
// Start earlier than most components, but shutdown later
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAutoStartup() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues a job for execution when resources permit.
|
* Queues a job for execution when resources permit.
|
||||||
*
|
*
|
||||||
@ -192,9 +142,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
totalQueuedJobs++;
|
totalQueuedJobs++;
|
||||||
|
|
||||||
// Synchronize access to the queue
|
|
||||||
synchronized (queueLock) {
|
|
||||||
currentQueueSize = jobQueue.size();
|
currentQueueSize = jobQueue.size();
|
||||||
|
|
||||||
// Try to add to the queue
|
// Try to add to the queue
|
||||||
@ -223,7 +170,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current capacity of the job queue.
|
* Gets the current capacity of the job queue.
|
||||||
@ -231,16 +177,13 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
* @return The current capacity
|
* @return The current capacity
|
||||||
*/
|
*/
|
||||||
public int getQueueCapacity() {
|
public int getQueueCapacity() {
|
||||||
synchronized (queueLock) {
|
return ((LinkedBlockingQueue<QueuedJob>) jobQueue).remainingCapacity() + jobQueue.size();
|
||||||
return ((LinkedBlockingQueue<QueuedJob>) jobQueue).remainingCapacity()
|
|
||||||
+ jobQueue.size();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Updates the capacity of the job queue based on available system resources. */
|
/** Updates the capacity of the job queue based on available system resources. */
|
||||||
private void updateQueueCapacity() {
|
private void updateQueueCapacity() {
|
||||||
try {
|
try {
|
||||||
// Calculate new capacity once and cache the result
|
// Calculate new capacity
|
||||||
int newCapacity =
|
int newCapacity =
|
||||||
resourceMonitor.calculateDynamicQueueCapacity(
|
resourceMonitor.calculateDynamicQueueCapacity(
|
||||||
baseQueueCapacity, minQueueCapacity);
|
baseQueueCapacity, minQueueCapacity);
|
||||||
@ -250,10 +193,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
log.debug(
|
log.debug(
|
||||||
"Updating job queue capacity from {} to {}", currentCapacity, newCapacity);
|
"Updating job queue capacity from {} to {}", currentCapacity, newCapacity);
|
||||||
|
|
||||||
synchronized (queueLock) {
|
|
||||||
// Double-check that capacity still needs to be updated
|
|
||||||
// Use the cached currentCapacity to avoid calling getQueueCapacity() again
|
|
||||||
if (newCapacity != currentCapacity) {
|
|
||||||
// Create new queue with updated capacity
|
// Create new queue with updated capacity
|
||||||
BlockingQueue<QueuedJob> newQueue = new LinkedBlockingQueue<>(newCapacity);
|
BlockingQueue<QueuedJob> newQueue = new LinkedBlockingQueue<>(newCapacity);
|
||||||
|
|
||||||
@ -263,8 +202,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
|
|
||||||
currentQueueSize = jobQueue.size();
|
currentQueueSize = jobQueue.size();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating queue capacity: {}", e.getMessage(), e);
|
log.error("Error updating queue capacity: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -272,11 +209,6 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
|
|
||||||
/** Processes jobs in the queue, executing them when resources permit. */
|
/** Processes jobs in the queue, executing them when resources permit. */
|
||||||
private void processQueue() {
|
private void processQueue() {
|
||||||
// Jobs to execute after releasing the lock
|
|
||||||
java.util.List<QueuedJob> jobsToExecute = new java.util.ArrayList<>();
|
|
||||||
|
|
||||||
// First synchronized block: poll jobs from the queue and prepare them for execution
|
|
||||||
synchronized (queueLock) {
|
|
||||||
if (shuttingDown || jobQueue.isEmpty()) {
|
if (shuttingDown || jobQueue.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -315,47 +247,20 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
"Job {} exceeded maximum wait time ({} ms), executing anyway",
|
"Job {} exceeded maximum wait time ({} ms), executing anyway",
|
||||||
job.jobId,
|
job.jobId,
|
||||||
waitTimeMs);
|
waitTimeMs);
|
||||||
|
|
||||||
// Add a specific status to the job context that can be tracked
|
|
||||||
// This will be visible in the job status API
|
|
||||||
try {
|
|
||||||
TaskManager taskManager =
|
|
||||||
SpringContextHolder.getBean(TaskManager.class);
|
|
||||||
if (taskManager != null) {
|
|
||||||
taskManager.addNote(
|
|
||||||
job.jobId,
|
|
||||||
"QUEUED_TIMEOUT: Job waited in queue for "
|
|
||||||
+ (waitTimeMs / 1000)
|
|
||||||
+ " seconds, exceeding the maximum wait time of "
|
|
||||||
+ (maxWaitTimeMs / 1000)
|
|
||||||
+ " seconds.");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(
|
|
||||||
"Failed to add timeout note to job {}: {}",
|
|
||||||
job.jobId,
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from our map
|
// Remove from our map
|
||||||
jobMap.remove(job.jobId);
|
jobMap.remove(job.jobId);
|
||||||
currentQueueSize = jobQueue.size();
|
currentQueueSize = jobQueue.size();
|
||||||
|
|
||||||
// Add to the list of jobs to execute outside the synchronized block
|
// Execute the job
|
||||||
jobsToExecute.add(job);
|
executeJob(job);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error processing job queue: {}", e.getMessage(), e);
|
log.error("Error processing job queue: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now execute the jobs outside the synchronized block to avoid holding the lock
|
|
||||||
for (QueuedJob job : jobsToExecute) {
|
|
||||||
executeJob(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a job from the queue.
|
* Executes a job from the queue.
|
||||||
*
|
*
|
||||||
|
@ -172,8 +172,8 @@ public class ResourceMonitor {
|
|||||||
if (oldStatus != newStatus) {
|
if (oldStatus != newStatus) {
|
||||||
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||||
log.info(
|
log.info(
|
||||||
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
"Current metrics - CPU: {:.1f}%, Memory: {:.1f}%, Free Memory: {} MB",
|
||||||
String.format("%.1f", cpuUsage * 100), String.format("%.1f", memoryUsage * 100), freeMemory / (1024 * 1024));
|
cpuUsage * 100, memoryUsage * 100, freeMemory / (1024 * 1024));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
||||||
|
@ -133,25 +133,6 @@ public class TaskManager {
|
|||||||
return jobResults.get(jobId);
|
return jobResults.get(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a note to a task. Notes are informational messages that can be attached to a job for
|
|
||||||
* tracking purposes.
|
|
||||||
*
|
|
||||||
* @param jobId The job ID
|
|
||||||
* @param note The note to add
|
|
||||||
* @return true if the note was added successfully, false if the job doesn't exist
|
|
||||||
*/
|
|
||||||
public boolean addNote(String jobId, String note) {
|
|
||||||
JobResult jobResult = jobResults.get(jobId);
|
|
||||||
if (jobResult != null) {
|
|
||||||
jobResult.addNote(note);
|
|
||||||
log.debug("Added note to job ID: {}: {}", jobId, note);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
log.warn("Attempted to add note to non-existent job ID: {}", jobId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get statistics about all jobs in the system
|
* Get statistics about all jobs in the system
|
||||||
*
|
*
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import org.springframework.beans.BeansException;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.context.ApplicationContextAware;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class to access Spring managed beans from non-Spring managed classes. This is especially
|
|
||||||
* useful for classes that are instantiated by frameworks or created dynamically.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class SpringContextHolder implements ApplicationContextAware {
|
|
||||||
|
|
||||||
private static ApplicationContext applicationContext;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
|
||||||
SpringContextHolder.applicationContext = applicationContext;
|
|
||||||
log.debug("Spring context holder initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a Spring bean by class type
|
|
||||||
*
|
|
||||||
* @param <T> The bean type
|
|
||||||
* @param beanClass The bean class
|
|
||||||
* @return The bean instance, or null if not found
|
|
||||||
*/
|
|
||||||
public static <T> T getBean(Class<T> beanClass) {
|
|
||||||
if (applicationContext == null) {
|
|
||||||
log.warn(
|
|
||||||
"Application context not initialized when attempting to get bean of type {}",
|
|
||||||
beanClass.getName());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return applicationContext.getBean(beanClass);
|
|
||||||
} catch (BeansException e) {
|
|
||||||
log.error("Error getting bean of type {}: {}", beanClass.getName(), e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a Spring bean by name
|
|
||||||
*
|
|
||||||
* @param <T> The bean type
|
|
||||||
* @param beanName The bean name
|
|
||||||
* @return The bean instance, or null if not found
|
|
||||||
*/
|
|
||||||
public static <T> T getBean(String beanName) {
|
|
||||||
if (applicationContext == null) {
|
|
||||||
log.warn(
|
|
||||||
"Application context not initialized when attempting to get bean '{}'",
|
|
||||||
beanName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
T bean = (T) applicationContext.getBean(beanName);
|
|
||||||
return bean;
|
|
||||||
} catch (BeansException e) {
|
|
||||||
log.error("Error getting bean '{}': {}", beanName, e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the application context is initialized
|
|
||||||
*
|
|
||||||
* @return true if initialized, false otherwise
|
|
||||||
*/
|
|
||||||
public static boolean isInitialized() {
|
|
||||||
return applicationContext != null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,208 +0,0 @@
|
|||||||
package stirling.software.common.annotations;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.Captor;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
import stirling.software.common.aop.AutoJobAspect;
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
|
||||||
import stirling.software.common.service.FileOrUploadService;
|
|
||||||
import stirling.software.common.service.FileStorage;
|
|
||||||
import stirling.software.common.service.JobExecutorService;
|
|
||||||
import stirling.software.common.service.JobQueue;
|
|
||||||
import stirling.software.common.service.ResourceMonitor;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class AutoJobPostMappingIntegrationTest {
|
|
||||||
|
|
||||||
private AutoJobAspect autoJobAspect;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private JobExecutorService jobExecutorService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private HttpServletRequest request;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileOrUploadService fileOrUploadService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
autoJobAspect = new AutoJobAspect(
|
|
||||||
jobExecutorService,
|
|
||||||
request,
|
|
||||||
fileOrUploadService,
|
|
||||||
fileStorage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ProceedingJoinPoint joinPoint;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private AutoJobPostMapping autoJobPostMapping;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<Supplier<Object>> workCaptor;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<Boolean> asyncCaptor;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<Long> timeoutCaptor;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<Boolean> queueableCaptor;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<Integer> resourceWeightCaptor;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldExecuteWithCustomParameters() throws Throwable {
|
|
||||||
// Given
|
|
||||||
PDFFile pdfFile = new PDFFile();
|
|
||||||
pdfFile.setFileId("test-file-id");
|
|
||||||
Object[] args = new Object[] { pdfFile };
|
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
|
||||||
when(autoJobPostMapping.timeout()).thenReturn(60000L);
|
|
||||||
when(autoJobPostMapping.retryCount()).thenReturn(3);
|
|
||||||
when(autoJobPostMapping.trackProgress()).thenReturn(true);
|
|
||||||
when(autoJobPostMapping.queueable()).thenReturn(true);
|
|
||||||
when(autoJobPostMapping.resourceWeight()).thenReturn(75);
|
|
||||||
|
|
||||||
MultipartFile mockFile = mock(MultipartFile.class);
|
|
||||||
when(fileStorage.retrieveFile("test-file-id")).thenReturn(mockFile);
|
|
||||||
|
|
||||||
|
|
||||||
when(jobExecutorService.runJobGeneric(
|
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
|
||||||
.thenReturn(ResponseEntity.ok("success"));
|
|
||||||
|
|
||||||
// When
|
|
||||||
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(ResponseEntity.ok("success"), result);
|
|
||||||
|
|
||||||
verify(jobExecutorService).runJobGeneric(
|
|
||||||
asyncCaptor.capture(),
|
|
||||||
workCaptor.capture(),
|
|
||||||
timeoutCaptor.capture(),
|
|
||||||
queueableCaptor.capture(),
|
|
||||||
resourceWeightCaptor.capture());
|
|
||||||
|
|
||||||
assertTrue(asyncCaptor.getValue(), "Async should be true");
|
|
||||||
assertEquals(60000L, timeoutCaptor.getValue(), "Timeout should be 60000ms");
|
|
||||||
assertTrue(queueableCaptor.getValue(), "Queueable should be true");
|
|
||||||
assertEquals(75, resourceWeightCaptor.getValue(), "Resource weight should be 75");
|
|
||||||
|
|
||||||
// Test that file was resolved
|
|
||||||
assertNotNull(pdfFile.getFileInput(), "File input should be set");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRetryOnError() throws Throwable {
|
|
||||||
// Given
|
|
||||||
when(joinPoint.getArgs()).thenReturn(new Object[0]);
|
|
||||||
when(request.getParameter("async")).thenReturn("false");
|
|
||||||
when(autoJobPostMapping.timeout()).thenReturn(-1L);
|
|
||||||
when(autoJobPostMapping.retryCount()).thenReturn(2);
|
|
||||||
when(autoJobPostMapping.trackProgress()).thenReturn(false);
|
|
||||||
when(autoJobPostMapping.queueable()).thenReturn(false);
|
|
||||||
when(autoJobPostMapping.resourceWeight()).thenReturn(50);
|
|
||||||
|
|
||||||
// First call throws exception, second succeeds
|
|
||||||
when(joinPoint.proceed(any()))
|
|
||||||
.thenThrow(new RuntimeException("First attempt failed"))
|
|
||||||
.thenReturn(ResponseEntity.ok("retry succeeded"));
|
|
||||||
|
|
||||||
// Mock jobExecutorService to execute the work immediately
|
|
||||||
when(jobExecutorService.runJobGeneric(
|
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
|
||||||
.thenAnswer(invocation -> {
|
|
||||||
Supplier<Object> work = invocation.getArgument(1);
|
|
||||||
return work.get();
|
|
||||||
});
|
|
||||||
|
|
||||||
// When
|
|
||||||
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(ResponseEntity.ok("retry succeeded"), result);
|
|
||||||
|
|
||||||
// Verify that proceed was called twice (initial attempt + 1 retry)
|
|
||||||
verify(joinPoint, times(2)).proceed(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandlePDFFileWithAsyncRequests() throws Throwable {
|
|
||||||
// Given
|
|
||||||
PDFFile pdfFile = new PDFFile();
|
|
||||||
pdfFile.setFileInput(mock(MultipartFile.class));
|
|
||||||
Object[] args = new Object[] { pdfFile };
|
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
|
||||||
when(autoJobPostMapping.retryCount()).thenReturn(1);
|
|
||||||
|
|
||||||
when(fileStorage.storeFile(any(MultipartFile.class))).thenReturn("stored-file-id");
|
|
||||||
when(fileStorage.retrieveFile("stored-file-id")).thenReturn(mock(MultipartFile.class));
|
|
||||||
|
|
||||||
// Mock job executor to return a successful response
|
|
||||||
when(jobExecutorService.runJobGeneric(
|
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
|
||||||
.thenReturn(ResponseEntity.ok("success"));
|
|
||||||
|
|
||||||
// When
|
|
||||||
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
|
||||||
"FileId should be set to the stored file id");
|
|
||||||
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
|
||||||
|
|
||||||
// Verify storage operations
|
|
||||||
verify(fileStorage).storeFile(any(MultipartFile.class));
|
|
||||||
verify(fileStorage).retrieveFile("stored-file-id");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,202 +0,0 @@
|
|||||||
package stirling.software.common.service;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.doAnswer;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.Captor;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobProgress;
|
|
||||||
import stirling.software.common.model.job.JobResponse;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class JobExecutorServiceTest {
|
|
||||||
|
|
||||||
private JobExecutorService jobExecutorService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private TaskManager taskManager;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private HttpServletRequest request;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@Captor
|
|
||||||
private ArgumentCaptor<String> jobIdCaptor;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// Initialize the service manually with all its dependencies
|
|
||||||
jobExecutorService = new JobExecutorService(
|
|
||||||
taskManager,
|
|
||||||
fileStorage,
|
|
||||||
request,
|
|
||||||
resourceMonitor,
|
|
||||||
jobQueue,
|
|
||||||
30000L, // asyncRequestTimeoutMs
|
|
||||||
"30m" // sessionTimeout
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRunSyncJobSuccessfully() throws Exception {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
|
|
||||||
// When
|
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
assertEquals("test-result", response.getBody());
|
|
||||||
|
|
||||||
// Verify request attribute was set with jobId
|
|
||||||
verify(request).setAttribute(eq("jobId"), anyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRunAsyncJobSuccessfully() throws Exception {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
|
|
||||||
// When
|
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(true, work);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
assertTrue(response.getBody() instanceof JobResponse);
|
|
||||||
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
|
|
||||||
assertTrue(jobResponse.isAsync());
|
|
||||||
assertNotNull(jobResponse.getJobId());
|
|
||||||
|
|
||||||
// Verify task manager was called
|
|
||||||
verify(taskManager).createTask(jobIdCaptor.capture());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleSyncJobError() {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> {
|
|
||||||
throw new RuntimeException("Test error");
|
|
||||||
};
|
|
||||||
|
|
||||||
// When
|
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, String> errorMap = (Map<String, String>) response.getBody();
|
|
||||||
assertEquals("Job failed: Test error", errorMap.get("error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldQueueJobWhenResourcesLimited() {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
|
||||||
|
|
||||||
// Configure resourceMonitor to indicate job should be queued
|
|
||||||
when(resourceMonitor.shouldQueueJob(80)).thenReturn(true);
|
|
||||||
|
|
||||||
// Configure jobQueue to return our future
|
|
||||||
when(jobQueue.queueJob(anyString(), eq(80), any(), anyLong())).thenReturn(future);
|
|
||||||
|
|
||||||
// When
|
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(
|
|
||||||
true, work, 5000, true, 80);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
assertTrue(response.getBody() instanceof JobResponse);
|
|
||||||
|
|
||||||
// Verify job was queued
|
|
||||||
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
|
||||||
verify(taskManager).createTask(anyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldUseCustomTimeoutWhenProvided() throws Exception {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
long customTimeout = 60000L;
|
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
|
||||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
|
||||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
|
||||||
executeMethod.setAccessible(true);
|
|
||||||
|
|
||||||
// Create a spy on the JobExecutorService to verify method calls
|
|
||||||
JobExecutorService spy = Mockito.spy(jobExecutorService);
|
|
||||||
|
|
||||||
// When
|
|
||||||
spy.runJobGeneric(false, work, customTimeout);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
verify(spy).runJobGeneric(eq(false), any(Supplier.class), eq(customTimeout));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldHandleTimeout() throws Exception {
|
|
||||||
// Given
|
|
||||||
Supplier<Object> work = () -> {
|
|
||||||
try {
|
|
||||||
Thread.sleep(100); // Simulate long-running job
|
|
||||||
return "test-result";
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
|
||||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
|
||||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
|
||||||
executeMethod.setAccessible(true);
|
|
||||||
|
|
||||||
// When/Then
|
|
||||||
try {
|
|
||||||
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
|
||||||
} catch (Exception e) {
|
|
||||||
assertTrue(e.getCause() instanceof TimeoutException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,102 +0,0 @@
|
|||||||
package stirling.software.common.service;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
|
||||||
import static org.mockito.Mockito.lenient;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobProgress;
|
|
||||||
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class JobQueueTest {
|
|
||||||
|
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
|
|
||||||
private final AtomicReference<ResourceStatus> statusRef = new AtomicReference<>(ResourceStatus.OK);
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// Mark stubbing as lenient to avoid UnnecessaryStubbingException
|
|
||||||
lenient().when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt())).thenReturn(10);
|
|
||||||
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
|
||||||
|
|
||||||
// Initialize JobQueue with mocked ResourceMonitor
|
|
||||||
jobQueue = new JobQueue(resourceMonitor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldQueueJob() {
|
|
||||||
String jobId = "test-job-1";
|
|
||||||
int resourceWeight = 50;
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
long timeoutMs = 1000;
|
|
||||||
|
|
||||||
jobQueue.queueJob(jobId, resourceWeight, work, timeoutMs);
|
|
||||||
|
|
||||||
|
|
||||||
assertTrue(jobQueue.isJobQueued(jobId));
|
|
||||||
assertEquals(1, jobQueue.getTotalQueuedJobs());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCancelJob() {
|
|
||||||
String jobId = "test-job-2";
|
|
||||||
Supplier<Object> work = () -> "test-result";
|
|
||||||
|
|
||||||
jobQueue.queueJob(jobId, 50, work, 1000);
|
|
||||||
boolean cancelled = jobQueue.cancelJob(jobId);
|
|
||||||
|
|
||||||
assertTrue(cancelled);
|
|
||||||
assertFalse(jobQueue.isJobQueued(jobId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldGetQueueStats() {
|
|
||||||
when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
|
||||||
|
|
||||||
jobQueue.queueJob("job1", 50, () -> "ok", 1000);
|
|
||||||
jobQueue.queueJob("job2", 50, () -> "ok", 1000);
|
|
||||||
jobQueue.cancelJob("job2");
|
|
||||||
|
|
||||||
Map<String, Object> stats = jobQueue.getQueueStats();
|
|
||||||
|
|
||||||
assertEquals(2, stats.get("totalQueuedJobs"));
|
|
||||||
assertTrue(stats.containsKey("queuedJobs"));
|
|
||||||
assertTrue(stats.containsKey("resourceStatus"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCalculateQueueCapacity() {
|
|
||||||
when(resourceMonitor.calculateDynamicQueueCapacity(5, 2)).thenReturn(8);
|
|
||||||
int capacity = resourceMonitor.calculateDynamicQueueCapacity(5, 2);
|
|
||||||
assertEquals(8, capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCheckIfJobIsQueued() {
|
|
||||||
String jobId = "job-123";
|
|
||||||
Supplier<Object> work = () -> "hello";
|
|
||||||
|
|
||||||
jobQueue.queueJob(jobId, 40, work, 500);
|
|
||||||
|
|
||||||
assertTrue(jobQueue.isJobQueued(jobId));
|
|
||||||
assertFalse(jobQueue.isJobQueued("nonexistent"));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
package stirling.software.common.service;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.lang.management.MemoryMXBean;
|
|
||||||
import java.lang.management.MemoryUsage;
|
|
||||||
import java.lang.management.OperatingSystemMXBean;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.Spy;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
|
|
||||||
import stirling.software.common.service.ResourceMonitor.ResourceMetrics;
|
|
||||||
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class ResourceMonitorTest {
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private OperatingSystemMXBean osMXBean;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private MemoryMXBean memoryMXBean;
|
|
||||||
|
|
||||||
@Spy
|
|
||||||
private AtomicReference<ResourceStatus> currentStatus = new AtomicReference<>(ResourceStatus.OK);
|
|
||||||
|
|
||||||
@Spy
|
|
||||||
private AtomicReference<ResourceMetrics> latestMetrics = new AtomicReference<>(new ResourceMetrics());
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// Set thresholds for testing
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "memoryCriticalThreshold", 0.9);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "memoryHighThreshold", 0.75);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "cpuCriticalThreshold", 0.9);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "cpuHighThreshold", 0.75);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "osMXBean", osMXBean);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "memoryMXBean", memoryMXBean);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "currentStatus", currentStatus);
|
|
||||||
ReflectionTestUtils.setField(resourceMonitor, "latestMetrics", latestMetrics);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCalculateDynamicQueueCapacity() {
|
|
||||||
// Given
|
|
||||||
int baseCapacity = 10;
|
|
||||||
int minCapacity = 2;
|
|
||||||
|
|
||||||
// Mock current status as OK
|
|
||||||
currentStatus.set(ResourceStatus.OK);
|
|
||||||
|
|
||||||
// When
|
|
||||||
int capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(baseCapacity, capacity, "With OK status, capacity should equal base capacity");
|
|
||||||
|
|
||||||
// Given
|
|
||||||
currentStatus.set(ResourceStatus.WARNING);
|
|
||||||
|
|
||||||
// When
|
|
||||||
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(6, capacity, "With WARNING status, capacity should be reduced to 60%");
|
|
||||||
|
|
||||||
// Given
|
|
||||||
currentStatus.set(ResourceStatus.CRITICAL);
|
|
||||||
|
|
||||||
// When
|
|
||||||
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(3, capacity, "With CRITICAL status, capacity should be reduced to 30%");
|
|
||||||
|
|
||||||
// Test minimum capacity enforcement
|
|
||||||
assertEquals(minCapacity, resourceMonitor.calculateDynamicQueueCapacity(1, minCapacity),
|
|
||||||
"Should never go below minimum capacity");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({
|
|
||||||
"10, OK, false", // Light job, OK status
|
|
||||||
"10, WARNING, false", // Light job, WARNING status
|
|
||||||
"10, CRITICAL, true", // Light job, CRITICAL status
|
|
||||||
"30, OK, false", // Medium job, OK status
|
|
||||||
"30, WARNING, true", // Medium job, WARNING status
|
|
||||||
"30, CRITICAL, true", // Medium job, CRITICAL status
|
|
||||||
"80, OK, true", // Heavy job, OK status
|
|
||||||
"80, WARNING, true", // Heavy job, WARNING status
|
|
||||||
"80, CRITICAL, true" // Heavy job, CRITICAL status
|
|
||||||
})
|
|
||||||
void shouldQueueJobBasedOnWeightAndStatus(int weight, ResourceStatus status, boolean shouldQueue) {
|
|
||||||
// Given
|
|
||||||
currentStatus.set(status);
|
|
||||||
|
|
||||||
// When
|
|
||||||
boolean result = resourceMonitor.shouldQueueJob(weight);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertEquals(shouldQueue, result,
|
|
||||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
|
||||||
weight, status, shouldQueue));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void resourceMetricsShouldDetectStaleState() {
|
|
||||||
// Given
|
|
||||||
Instant now = Instant.now();
|
|
||||||
Instant pastInstant = now.minusMillis(6000);
|
|
||||||
|
|
||||||
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
|
||||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
|
||||||
|
|
||||||
// When/Then
|
|
||||||
assertTrue(staleMetrics.isStale(5000), "Metrics from 6 seconds ago should be stale with 5s threshold");
|
|
||||||
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,287 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testAddNote() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = UUID.randomUUID().toString();
|
|
||||||
taskManager.createTask(jobId);
|
|
||||||
String note = "Test note";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
boolean result = taskManager.addNote(jobId, note);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertTrue(result);
|
|
||||||
JobResult jobResult = taskManager.getJobResult(jobId);
|
|
||||||
assertNotNull(jobResult);
|
|
||||||
assertNotNull(jobResult.getNotes());
|
|
||||||
assertEquals(1, jobResult.getNotes().size());
|
|
||||||
assertEquals(note, jobResult.getNotes().get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testAddNote_NonExistentJob() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "non-existent-job";
|
|
||||||
String note = "Test note";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
boolean result = taskManager.addNote(jobId, note);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertFalse(result);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
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.springframework.context.ApplicationContext;
|
|
||||||
|
|
||||||
class SpringContextHolderTest {
|
|
||||||
|
|
||||||
private ApplicationContext mockApplicationContext;
|
|
||||||
private SpringContextHolder contextHolder;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
mockApplicationContext = mock(ApplicationContext.class);
|
|
||||||
contextHolder = new SpringContextHolder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSetApplicationContext() {
|
|
||||||
// Act
|
|
||||||
contextHolder.setApplicationContext(mockApplicationContext);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertTrue(SpringContextHolder.isInitialized());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetBean_ByType() {
|
|
||||||
// Arrange
|
|
||||||
contextHolder.setApplicationContext(mockApplicationContext);
|
|
||||||
TestBean expectedBean = new TestBean();
|
|
||||||
when(mockApplicationContext.getBean(TestBean.class)).thenReturn(expectedBean);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertSame(expectedBean, result);
|
|
||||||
verify(mockApplicationContext).getBean(TestBean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetBean_ApplicationContextNotSet() {
|
|
||||||
// Don't set application context
|
|
||||||
|
|
||||||
// Act
|
|
||||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetBean_BeanNotFound() {
|
|
||||||
// Arrange
|
|
||||||
contextHolder.setApplicationContext(mockApplicationContext);
|
|
||||||
when(mockApplicationContext.getBean(TestBean.class)).thenThrow(new org.springframework.beans.BeansException("Bean not found") {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertNull(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple test class
|
|
||||||
private static class TestBean {
|
|
||||||
}
|
|
||||||
}
|
|
@ -26,8 +26,6 @@ import stirling.software.proprietary.service.AuditService;
|
|||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@org.springframework.core.annotation.Order(
|
|
||||||
10) // Lower precedence (higher number) - executes after AutoJobAspect
|
|
||||||
public class AuditAspect {
|
public class AuditAspect {
|
||||||
|
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
|
@ -36,8 +36,6 @@ import stirling.software.proprietary.service.AuditService;
|
|||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@org.springframework.core.annotation.Order(
|
|
||||||
10) // Lower precedence (higher number) - executes after AutoJobAspect
|
|
||||||
public class ControllerAuditAspect {
|
public class ControllerAuditAspect {
|
||||||
|
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
@ -79,12 +77,6 @@ public class ControllerAuditAspect {
|
|||||||
return auditController(joinPoint, "PATCH");
|
return auditController(joinPoint, "PATCH");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Intercept all methods with AutoJobPostMapping annotation */
|
|
||||||
@Around("@annotation(stirling.software.common.annotations.AutoJobPostMapping)")
|
|
||||||
public Object auditAutoJobMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
return auditController(joinPoint, "POST");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod)
|
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod)
|
||||||
throws Throwable {
|
throws Throwable {
|
||||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
package stirling.software.proprietary.controller;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
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.JobStats;
|
|
||||||
import stirling.software.common.service.JobQueue;
|
|
||||||
import stirling.software.common.service.TaskManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin controller for job management. These endpoints require admin privileges and provide insight
|
|
||||||
* into system jobs and queues.
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
public class AdminJobController {
|
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
|
||||||
private final JobQueue jobQueue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get statistics about jobs in the system (admin only)
|
|
||||||
*
|
|
||||||
* @return Job statistics
|
|
||||||
*/
|
|
||||||
@GetMapping("/api/v1/admin/job/stats")
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
public ResponseEntity<JobStats> getJobStats() {
|
|
||||||
JobStats stats = taskManager.getJobStats();
|
|
||||||
log.info(
|
|
||||||
"Admin requested job stats: {} active, {} completed jobs",
|
|
||||||
stats.getActiveJobs(),
|
|
||||||
stats.getCompletedJobs());
|
|
||||||
return ResponseEntity.ok(stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get statistics about the job queue (admin only)
|
|
||||||
*
|
|
||||||
* @return Queue statistics
|
|
||||||
*/
|
|
||||||
@GetMapping("/api/v1/admin/job/queue/stats")
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
public ResponseEntity<?> getQueueStats() {
|
|
||||||
Map<String, Object> queueStats = jobQueue.getQueueStats();
|
|
||||||
log.info("Admin requested queue stats: {} queued jobs", queueStats.get("queuedJobs"));
|
|
||||||
return ResponseEntity.ok(queueStats);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually trigger cleanup of old jobs (admin only)
|
|
||||||
*
|
|
||||||
* @return A response indicating how many jobs were cleaned up
|
|
||||||
*/
|
|
||||||
@PostMapping("/api/v1/admin/job/cleanup")
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
public ResponseEntity<?> cleanupOldJobs() {
|
|
||||||
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
|
||||||
taskManager.cleanupOldJobs();
|
|
||||||
int afterCount = taskManager.getJobStats().getTotalJobs();
|
|
||||||
int removedCount = beforeCount - afterCount;
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Admin triggered job cleanup: removed {} jobs, {} remaining",
|
|
||||||
removedCount,
|
|
||||||
afterCount);
|
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
Map.of(
|
|
||||||
"message", "Cleanup complete",
|
|
||||||
"removedJobs", removedCount,
|
|
||||||
"remainingJobs", afterCount));
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,19 +3,17 @@ package stirling.software.common.controller;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
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;
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
import stirling.software.common.service.JobQueue;
|
|
||||||
import stirling.software.common.service.TaskManager;
|
import stirling.software.common.service.TaskManager;
|
||||||
|
|
||||||
/** REST controller for job-related endpoints */
|
/** REST controller for job-related endpoints */
|
||||||
@ -26,8 +24,6 @@ public class JobController {
|
|||||||
|
|
||||||
private final TaskManager taskManager;
|
private final TaskManager taskManager;
|
||||||
private final FileStorage fileStorage;
|
private final FileStorage fileStorage;
|
||||||
private final JobQueue jobQueue;
|
|
||||||
private final HttpServletRequest request;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of a job
|
* Get the status of a job
|
||||||
@ -41,19 +37,6 @@ public class JobController {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the job is in the queue and add queue information
|
|
||||||
if (!result.isComplete() && jobQueue.isJobQueued(jobId)) {
|
|
||||||
int position = jobQueue.getJobPosition(jobId);
|
|
||||||
Map<String, Object> resultWithQueueInfo =
|
|
||||||
Map.of(
|
|
||||||
"jobResult",
|
|
||||||
result,
|
|
||||||
"queueInfo",
|
|
||||||
Map.of("inQueue", true, "position", position));
|
|
||||||
return ResponseEntity.ok(resultWithQueueInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,75 +82,33 @@ public class JobController {
|
|||||||
return ResponseEntity.ok(result.getResult());
|
return ResponseEntity.ok(result.getResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin-only endpoints have been moved to AdminJobController in the proprietary package
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel a job by its ID
|
* Manually trigger cleanup of old jobs
|
||||||
*
|
*
|
||||||
* <p>This method should only allow cancellation of jobs that were created by the current user.
|
* @return A response indicating how many jobs were cleaned up
|
||||||
* The jobId should be part of the user's session or otherwise linked to their identity.
|
|
||||||
*
|
|
||||||
* @param jobId The job ID
|
|
||||||
* @return Response indicating whether the job was cancelled
|
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/api/v1/general/job/{jobId}")
|
@PostMapping("/api/v1/general/job/cleanup")
|
||||||
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> cleanupOldJobs() {
|
||||||
log.debug("Request to cancel job: {}", jobId);
|
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
taskManager.cleanupOldJobs();
|
||||||
|
int afterCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
int removedCount = beforeCount - afterCount;
|
||||||
|
|
||||||
// Verify that this job belongs to the current user
|
|
||||||
// We can use the current request's session to validate ownership
|
|
||||||
Object sessionJobIds = request.getSession().getAttribute("userJobIds");
|
|
||||||
if (sessionJobIds == null
|
|
||||||
|| !(sessionJobIds instanceof java.util.Set)
|
|
||||||
|| !((java.util.Set<?>) sessionJobIds).contains(jobId)) {
|
|
||||||
// Either no jobs in session or jobId doesn't match user's jobs
|
|
||||||
log.warn("Unauthorized attempt to cancel job: {}", jobId);
|
|
||||||
return ResponseEntity.status(403)
|
|
||||||
.body(Map.of("message", "You are not authorized to cancel this job"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check if the job is in the queue
|
|
||||||
boolean cancelled = false;
|
|
||||||
int queuePosition = -1;
|
|
||||||
|
|
||||||
if (jobQueue.isJobQueued(jobId)) {
|
|
||||||
queuePosition = jobQueue.getJobPosition(jobId);
|
|
||||||
cancelled = jobQueue.cancelJob(jobId);
|
|
||||||
log.info("Cancelled queued job: {} (was at position {})", jobId, queuePosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not in queue or couldn't cancel, try to cancel in TaskManager
|
|
||||||
if (!cancelled) {
|
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
|
||||||
if (result != null && !result.isComplete()) {
|
|
||||||
// Mark as error with cancellation message
|
|
||||||
taskManager.setError(jobId, "Job was cancelled by user");
|
|
||||||
cancelled = true;
|
|
||||||
log.info("Marked job as cancelled in TaskManager: {}", jobId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled) {
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
Map.of(
|
Map.of(
|
||||||
"message",
|
"message", "Cleanup complete",
|
||||||
"Job cancelled successfully",
|
"removedJobs", removedCount,
|
||||||
"wasQueued",
|
"remainingJobs", afterCount));
|
||||||
queuePosition >= 0,
|
|
||||||
"queuePosition",
|
|
||||||
queuePosition >= 0 ? queuePosition : "n/a"));
|
|
||||||
} else {
|
|
||||||
// Job not found or already complete
|
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
|
||||||
if (result == null) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
} else if (result.isComplete()) {
|
|
||||||
return ResponseEntity.badRequest()
|
|
||||||
.body(Map.of("message", "Cannot cancel job that is already complete"));
|
|
||||||
} else {
|
|
||||||
return ResponseEntity.internalServerError()
|
|
||||||
.body(Map.of("message", "Failed to cancel job for unknown reason"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,406 +0,0 @@
|
|||||||
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 org.springframework.mock.web.MockHttpServletRequest;
|
|
||||||
import org.springframework.mock.web.MockHttpSession;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
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.JobQueue;
|
|
||||||
import stirling.software.common.service.TaskManager;
|
|
||||||
|
|
||||||
class JobControllerTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private TaskManager taskManager;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private HttpServletRequest request;
|
|
||||||
|
|
||||||
private MockHttpSession session;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private JobController controller;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
MockitoAnnotations.openMocks(this);
|
|
||||||
|
|
||||||
// Setup mock session for tests
|
|
||||||
session = new MockHttpSession();
|
|
||||||
when(request.getSession()).thenReturn(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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_ExistingJobInQueue() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "test-job-id";
|
|
||||||
JobResult mockResult = new JobResult();
|
|
||||||
mockResult.setJobId(jobId);
|
|
||||||
mockResult.setComplete(false);
|
|
||||||
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
|
||||||
when(jobQueue.isJobQueued(jobId)).thenReturn(true);
|
|
||||||
when(jobQueue.getJobPosition(jobId)).thenReturn(3);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
|
||||||
assertEquals(mockResult, responseBody.get("jobResult"));
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> queueInfo = (Map<String, Object>) responseBody.get("queueInfo");
|
|
||||||
assertTrue((Boolean) queueInfo.get("inQueue"));
|
|
||||||
assertEquals(3, queueInfo.get("position"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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<?> 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(); }
|
|
||||||
*
|
|
||||||
* @Test void testGetQueueStats() { // Arrange Map<String, Object>
|
|
||||||
* mockQueueStats = Map.of( "queuedJobs", 5, "queueCapacity", 10,
|
|
||||||
* "resourceStatus", "OK" );
|
|
||||||
*
|
|
||||||
* when(jobQueue.getQueueStats()).thenReturn(mockQueueStats);
|
|
||||||
*
|
|
||||||
* // Act ResponseEntity<?> response = controller.getQueueStats();
|
|
||||||
*
|
|
||||||
* // Assert assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
* assertEquals(mockQueueStats, response.getBody());
|
|
||||||
* verify(jobQueue).getQueueStats(); }
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void testCancelJob_InQueue() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "job-in-queue";
|
|
||||||
|
|
||||||
// Setup user session with job authorization
|
|
||||||
java.util.Set<String> userJobIds = new java.util.HashSet<>();
|
|
||||||
userJobIds.add(jobId);
|
|
||||||
session.setAttribute("userJobIds", userJobIds);
|
|
||||||
|
|
||||||
when(jobQueue.isJobQueued(jobId)).thenReturn(true);
|
|
||||||
when(jobQueue.getJobPosition(jobId)).thenReturn(2);
|
|
||||||
when(jobQueue.cancelJob(jobId)).thenReturn(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.cancelJob(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
|
||||||
assertEquals("Job cancelled successfully", responseBody.get("message"));
|
|
||||||
assertTrue((Boolean) responseBody.get("wasQueued"));
|
|
||||||
assertEquals(2, responseBody.get("queuePosition"));
|
|
||||||
|
|
||||||
verify(jobQueue).cancelJob(jobId);
|
|
||||||
verify(taskManager, never()).setError(anyString(), anyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCancelJob_Running() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "job-running";
|
|
||||||
JobResult jobResult = new JobResult();
|
|
||||||
jobResult.setJobId(jobId);
|
|
||||||
jobResult.setComplete(false);
|
|
||||||
|
|
||||||
// Setup user session with job authorization
|
|
||||||
java.util.Set<String> userJobIds = new java.util.HashSet<>();
|
|
||||||
userJobIds.add(jobId);
|
|
||||||
session.setAttribute("userJobIds", userJobIds);
|
|
||||||
|
|
||||||
when(jobQueue.isJobQueued(jobId)).thenReturn(false);
|
|
||||||
when(taskManager.getJobResult(jobId)).thenReturn(jobResult);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.cancelJob(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
|
||||||
assertEquals("Job cancelled successfully", responseBody.get("message"));
|
|
||||||
assertFalse((Boolean) responseBody.get("wasQueued"));
|
|
||||||
assertEquals("n/a", responseBody.get("queuePosition"));
|
|
||||||
|
|
||||||
verify(jobQueue, never()).cancelJob(jobId);
|
|
||||||
verify(taskManager).setError(jobId, "Job was cancelled by user");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCancelJob_NotFound() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "non-existent-job";
|
|
||||||
|
|
||||||
// Setup user session with job authorization
|
|
||||||
java.util.Set<String> userJobIds = new java.util.HashSet<>();
|
|
||||||
userJobIds.add(jobId);
|
|
||||||
session.setAttribute("userJobIds", userJobIds);
|
|
||||||
|
|
||||||
when(jobQueue.isJobQueued(jobId)).thenReturn(false);
|
|
||||||
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.cancelJob(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCancelJob_AlreadyComplete() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "completed-job";
|
|
||||||
JobResult jobResult = new JobResult();
|
|
||||||
jobResult.setJobId(jobId);
|
|
||||||
jobResult.setComplete(true);
|
|
||||||
|
|
||||||
// Setup user session with job authorization
|
|
||||||
java.util.Set<String> userJobIds = new java.util.HashSet<>();
|
|
||||||
userJobIds.add(jobId);
|
|
||||||
session.setAttribute("userJobIds", userJobIds);
|
|
||||||
|
|
||||||
when(jobQueue.isJobQueued(jobId)).thenReturn(false);
|
|
||||||
when(taskManager.getJobResult(jobId)).thenReturn(jobResult);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.cancelJob(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
|
||||||
assertEquals("Cannot cancel job that is already complete", responseBody.get("message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCancelJob_Unauthorized() {
|
|
||||||
// Arrange
|
|
||||||
String jobId = "unauthorized-job";
|
|
||||||
|
|
||||||
// Setup user session with other job IDs but not this one
|
|
||||||
java.util.Set<String> userJobIds = new java.util.HashSet<>();
|
|
||||||
userJobIds.add("other-job-1");
|
|
||||||
userJobIds.add("other-job-2");
|
|
||||||
session.setAttribute("userJobIds", userJobIds);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ResponseEntity<?> response = controller.cancelJob(jobId);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
|
||||||
assertEquals("You are not authorized to cancel this job", responseBody.get("message"));
|
|
||||||
|
|
||||||
// Verify no cancellation attempts were made
|
|
||||||
verify(jobQueue, never()).isJobQueued(anyString());
|
|
||||||
verify(jobQueue, never()).cancelJob(anyString());
|
|
||||||
verify(taskManager, never()).getJobResult(anyString());
|
|
||||||
verify(taskManager, never()).setError(anyString(), anyString());
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user