# How JobQueue and JobExecutorService Handle Async PDF Processing in Stirling-PDF

> Discover how Stirling-PDF's JobQueue and JobExecutorService manage async PDF processing. Learn about resource-aware job management and background thread pools for efficient PDF operations.

- Repository: [Stirling Tools/Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF)
- Tags: internals
- Published: 2026-03-01

---

**Stirling-PDF processes heavy PDF operations on a background thread pool using a dynamic, resource-aware JobQueue and a facade JobExecutorService that decides whether to queue, execute immediately, or reject jobs based on real-time system load.**

Stirling-Tools/Stirling-PDF relies on a sophisticated asynchronous processing pipeline to prevent server overload during intensive document operations. The `JobQueue` and `JobExecutorService` classes work together to manage **async PDF processing** with backpressure, dynamic capacity tuning, and virtual-thread execution. This architecture ensures that HTTP responses remain snappy while heavy conversion, compression, or manipulation tasks run safely in the background.

## The Decision Point: JobExecutorService.runJobGeneric

Every async request enters the system through `JobExecutorService.runJobGeneric` in [`app/common/src/main/java/stirling/software/common/service/JobExecutorService.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java). The method first mints a **scoped job ID** and calculates the effective timeout:

```java
String baseJobId = UUID.randomUUID().toString();
String scopedJobKey = getScopedJobKey(baseJobId);
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;

```

Before spawning work, the executor queries the `ResourceMonitor` to determine whether the current load warrants queuing:

```java
boolean shouldQueue =
        queueable
        && async
        && resourceMonitor.shouldQueueJob(resourceWeight);

```

If `shouldQueue` is `true`, the job is handed to `JobQueue.queueJob`; otherwise it executes immediately on the **virtual-thread executor** provided by `ExecutorFactory.newVirtualThreadExecutor()`.

## Dynamic Resource-Aware Queuing

When the system is under pressure, `JobQueue` (located in [`app/common/src/main/java/stirling/software/common/service/JobQueue.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/app/common/src/main/java/stirling/software/common/service/JobQueue.java)) absorbs surplus work in a `BlockingQueue<QueuedJob>` whose capacity fluctuates based on free memory and CPU.

### Creating the CompletableFuture

`queueJob` creates a `CompletableFuture` for the caller, wraps the supplied work in a `QueuedJob` object, and stores the job in two concurrent structures:

- **`jobMap`** – `ConcurrentHashMap<String, QueuedJob>` for O(1) status lookups by job ID
- **`jobQueue`** – the dynamic `BlockingQueue` that holds jobs awaiting execution

```java
QueuedJob job = new QueuedJob(jobId, resourceWeight, work,
        timeoutMs, Instant.now(), future, false);
jobMap.put(jobId, job);
jobQueue.offer(job, 5, TimeUnit.SECONDS);

```

If the queue is full, the future is completed exceptionally and the client receives a rejection signal.

### Scheduler Architecture

`JobQueue` implements Spring’s `SmartLifecycle`. On context start, `start()` invokes `initializeSchedulers()`, which launches two repeating tasks:

- **`processQueue`** – runs every `queueCheckIntervalMs` (default 1 s) to drain jobs when resources permit
- **`updateQueueCapacity`** – runs every 30 s to recalculate maximum queue size via `ResourceMonitor`

## Job Execution Flow

### Selecting Jobs Under Load

Inside `processQueue`, the scheduler samples the current resource status:

```java
ResourceMonitor.ResourceStatus status = resourceMonitor.getCurrentStatus().get();
boolean canExecuteJobs = (status != ResourceMonitor.ResourceStatus.CRITICAL);

```

Depending on the status, a small batch (1–3) of jobs is polled from `jobQueue`, removed from `jobMap`, and collected into `jobsToExecute`. Jobs that exceed `maxWaitTimeMs` are still executed but flagged in the `TaskManager` for observability.

### Virtual Thread Execution

Each `QueuedJob` is submitted to the virtual-thread executor (`jobExecutor`) defined in `ExecutorFactory`:

```java
jobExecutor.execute(() -> {
    Object result = executeWithTimeout(job.work, job.timeoutMs);
    // Complete the future with ResponseEntity or error
});

```

`executeWithTimeout` runs the supplier on the same executor and enforces the per-job timeout, cancelling the `CompletableFuture` if the deadline passes.

## Result Handling and Task Management

When the future completes, `JobExecutorService.processJobResult` stores any `byte[]`, `ResponseEntity<byte[]>`, `MultipartFile`, or generic object into the **`TaskManager`**. This decouples the heavy PDF work from the HTTP layer—controllers often return a `JobResponse` containing the job ID immediately, while the actual bytes are fetched later via polling endpoints.

## Direct Async Execution (No Queue)

For lightweight operations or when the server is healthy, `JobExecutorService` bypasses the queue entirely:

```java
executor.execute(() -> {
    Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
    processJobResult(capturedJobId, result);
});

```

This path uses the same virtual-thread pool but skips the `BlockingQueue` overhead, minimizing latency for thumbnail generation or metadata extraction tasks that carry low `resourceWeight` values.

## Cancellation and Status Monitoring

Clients can interrogate or abort jobs through `JobQueue` APIs:

- **`cancelJob`** – removes the entry from `jobMap`, flags the `QueuedJob` as cancelled, aborts its future, and removes it from the queue
- **`getJobPosition`** – walks the queue to report a job’s place in line for UI progress bars
- **`getQueueStats`** – returns live metrics including queued count, capacity, rejected count, and current `ResourceStatus`

## Summary

- **JobExecutorService** acts as the façade, deciding between immediate execution and queuing by consulting `ResourceMonitor.shouldQueueJob()`.
- **JobQueue** provides a dynamic `BlockingQueue` tuned every 30 seconds based on system resources, preventing out-of-memory errors during traffic spikes.
- **Virtual threads** (via `ExecutorFactory`) handle all actual work, enabling high concurrency without traditional thread-pool exhaustion.
- **CompletableFuture** bridges asynchronous execution with HTTP responses, allowing controllers to return job IDs instantly while processing continues in the background.
- **TaskManager** persists results and errors, enabling clients to poll for completion status or download generated PDFs after the fact.

## Frequently Asked Questions

### How does Stirling-PDF decide whether to queue a job or run it immediately?

The `JobExecutorService` evaluates three factors: whether the caller requested async mode, whether the job is marked as `queueable`, and whether `ResourceMonitor.shouldQueueJob(resourceWeight)` reports high system load. If all conditions are true, the job enters `JobQueue`; otherwise it runs immediately on the virtual-thread executor.

### What happens when the JobQueue reaches capacity?

If `jobQueue.offer()` returns `false` (queue full), the `CompletableFuture` is completed exceptionally and the client receives a rejection response. This backpressure mechanism protects the JVM from `OutOfMemoryError` during bursty traffic.

### How are job timeouts enforced during async PDF processing?

Both queued and directly-executed jobs pass through `executeWithTimeout`, which submits the work to the virtual-thread executor and starts a separate watchdogtimer. If the supplier does not complete within the configured `timeoutMs`, the future is cancelled and the error is recorded in `TaskManager`.

### Can clients cancel a job after it has been queued?

Yes. Calling `JobQueue.cancelJob(jobId)` atomically removes the job from `jobMap`, marks it cancelled so the scheduler skips it, and calls `future.cancel(true)`. If the job is already running, the cancellation flag propagates to the virtual thread, although interruption depends on the PDF library’s responsiveness to thread interrupts.