2025-11-10 00:26:55 +00:00
# PDF JSON Editor - Threading, Concurrency, and Performance Analysis
**Date:** 2025-01-09
**Version:** 1.0
**Status:** Comprehensive analysis combining automated review and manual verification
---
## Executive Summary
This analysis identifies **CRITICAL** security vulnerabilities, thread safety issues, and performance problems in the PDF JSON editor codebase. The service contains:
2025-11-10 10:16:57 +00:00
- **2 CRITICAL issues** requiring immediate attention
2025-11-10 00:26:55 +00:00
- **2 HIGH severity resource leaks** causing memory exhaustion
- **Multiple performance bottlenecks** limiting scalability
**Immediate Action Required:**
2025-11-10 10:16:57 +00:00
1. Fix pageFontResources PDFont key issue (Issue #1 )
2. Replace unbounded thread spawning (Issue #2 )
3. Add cache size limits (Issue #3 )
4. Fix Type3 cache race condition (Issue #4 )
2025-11-10 00:26:55 +00:00
---
## CRITICAL ISSUES
2025-11-10 10:16:57 +00:00
### Issue #1: pageFontResources Keyed by PDFont Instances - CRITICAL
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:5075-5081, 5158-5159`
**Severity:** CRITICAL (Broken Functionality)
**Type:** Object Identity Mismatch, Cache Miss
**Verified:** ✅ TRUE
**Description:**
```java
// Line 5075-5081: Initial metadata extraction (first PDF load)
try ( PDDocument document = pdfDocumentFactory . load ( pdfBytes , true )) {
Map < Integer , Map < PDFont , String >> pageFontResources = new HashMap <> ();
for ( PDPage page : document . getPages ()) {
Map < PDFont , String > resourceMap = collectFontsForPage (...);
// resourceMap keys are PDFont instances from this document
pageFontResources . put ( pageNumber , resourceMap );
}
// Cache it
documentCache . put ( jobId , new CachedPdfDocument (..., pageFontResources , ...));
} // PDDocument closed, PDFont instances now reference freed document
// Line 5158-5159: Lazy page extraction (reloads PDF)
try ( PDDocument document = pdfDocumentFactory . load ( cached . getPdfBytes (), true )) {
// NEW PDFont instances created!
PDPage page = document . getPage ( pageIndex );
// Try to lookup fonts
Map < PDFont , String > cachedResourceMap = cached . getPageFontResources (). get ( pageNum );
// cachedResourceMap keys are OLD PDFont instances from closed document
// Lookup using NEW PDFont instances
String fontId = cachedResourceMap . get ( newFont ); // Always NULL! ← BUG
}
```
**Why It Fails:**
1. **Object Identity:** `Map<PDFont, String>` uses PDFont object identity as key
2. **Different Instances:** Each PDF load creates new PDFont instances with different identities
3. **Lookup Fails:** `cachedResourceMap.get(newFont)` returns null because `newFont != oldFont`
4. **Defeat Caching Goal:** Every lazy page request rebuilds font metadata, defeating the cache
**Impact:**
- Lazy page loading doesn't reuse cached font metadata
- CPU wasted rebuilding font info on every page request
- Cache only stores garbage (unusable keys)
- "Consistent font UID" feature completely broken
**Evidence:**
```java
// No code actually uses the cached pageFontResources successfully
// Every extractSinglePage call rebuilds fonts from scratch
```
**Recommendation:**
```java
// Use resource names as keys instead of PDFont objects
Map < Integer , Map < String , String >> pageFontResources = new HashMap <> ();
// Key: font resource name (e.g., "F1"), Value: font UID
// Or use font UID directly
Map < Integer , Set < String >> pageFontUids = new HashMap <> ();
```
---
2025-11-10 10:16:57 +00:00
### Issue #2: Unbounded Thread Creation - CRITICAL Resource Leak
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:5550-5562`
**Severity:** CRITICAL (Resource Exhaustion)
**Type:** Thread Leak, Memory Leak
**Verified:** ✅ TRUE
**Description:**
```java
private void scheduleDocumentCleanup ( String jobId ) {
new Thread (
() -> {
try {
Thread . sleep ( TimeUnit . MINUTES . toMillis ( 30 )); // Sleep 30 minutes!
clearCachedDocument ( jobId );
log . debug ( "Auto-cleaned cached document for jobId: {}" , jobId );
} catch ( InterruptedException e ) {
Thread . currentThread (). interrupt ();
}
})
. start (); // Unmanaged thread!
}
```
**Also in:** `PdfLazyLoadingService.java:256-269` (duplicate implementation)
**Problems:**
1. **One Thread Per Upload:** Each PDF upload spawns a new thread
2. **No Thread Pool:** Unlimited thread creation (no cap)
3. **Long Sleep:** Threads sleep for 30 minutes holding resources
4. **No Cancellation:** Cannot stop cleanup threads if job completes early
5. **Non-Daemon:** Threads prevent JVM shutdown (daemon=false by default)
6. **No Monitoring:** No visibility into active cleanup threads
**Impact Under Load:**
```
100 concurrent uploads → 100 cleanup threads spawned
Each thread: ~1MB stack + overhead
Total: ~100MB+ wasted on sleeping threads
1000 uploads/day → 1000+ threads accumulate
OS thread limit (ulimit -u) exceeded → OutOfMemoryError: unable to create new native thread
```
**Production Failure Scenario:**
```
09:00 - Peak traffic: 500 PDFs uploaded
09:00 - 500 cleanup threads spawned (each sleeps 30 min)
09:15 - 400 more uploads → 400 more threads
09:30 - First batch wakes up, cleans, exits
But new threads keep getting created faster than old ones die
10:00 - Thread count > 2000
10:15 - JVM hits OS thread limit
10:16 - Server crashes: "OutOfMemoryError: unable to create new native thread"
```
**Recommendation:**
```java
@Service
public class PdfJsonConversionService {
// Fixed-size thread pool for cleanup
private final ScheduledExecutorService cleanupScheduler =
Executors . newScheduledThreadPool (
2 , // Only 2 threads needed
new ThreadFactoryBuilder ()
. setNameFormat ( "pdf-cache-cleanup-%d" )
. setDaemon ( true )
. build ()
);
private void scheduleDocumentCleanup ( String jobId ) {
cleanupScheduler . schedule (
() -> clearCachedDocument ( jobId ),
30 ,
TimeUnit . MINUTES
);
}
@PreDestroy
public void shutdown () {
cleanupScheduler . shutdown ();
try {
if ( ! cleanupScheduler . awaitTermination ( 10 , TimeUnit . SECONDS )) {
cleanupScheduler . shutdownNow ();
}
} catch ( InterruptedException e ) {
cleanupScheduler . shutdownNow ();
Thread . currentThread (). interrupt ();
}
}
}
```
---
## HIGH SEVERITY ISSUES
2025-11-10 10:16:57 +00:00
### Issue #3: Unbounded Cache Growth - HIGH Memory Leak
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:147-148, 154`
**Severity:** HIGH (Memory Exhaustion)
**Type:** Memory Leak, Missing Eviction Policy
**Verified:** ✅ TRUE
**Description:**
```java
// Line 147: Type3 normalized fonts - cleared only in convertJsonToPdf (line 454)
private final Map < String , PDFont > type3NormalizedFontCache = new ConcurrentHashMap <> ();
// Line 148: Type3 glyph coverage - NEVER CLEARED anywhere in codebase!
private final Map < String , Set < Integer >> type3GlyphCoverageCache = new ConcurrentHashMap <> ();
// Line 154: Document cache - relies on buggy cleanup threads
private final Map < String , CachedPdfDocument > documentCache = new ConcurrentHashMap <> ();
```
**Growth Patterns:**
**1. type3NormalizedFontCache:**
- Written at line 3766: `type3NormalizedFontCache.put(fontModel.getUid(), font)`
- Cleared only at line 454: `type3NormalizedFontCache.clear()` (JSON→PDF conversion)
- **NOT cleared during PDF→JSON** conversion (most common operation)
- Each PDFont holds references to native resources (C++ objects via JNI)
- Grows unbounded during PDF→JSON operations
**2. type3GlyphCoverageCache:**
- Written at line 1122: `type3GlyphCoverageCache.put(fontUid, coverageSet)`
- **NEVER CLEARED** in entire codebase (verified via grep)
- Accumulates Set<Integer> for every Type3 font ever processed
- Each Set can contain thousands of integers (Unicode codepoints)
- Pure memory leak
**3. documentCache:**
- Stores full PDF bytes in memory
- Each entry can be 1MB-100MB+ (document bytes + metadata)
- Relies on cleanup threads (which have issues from Issue #5 )
- If cleanup fails (exception, server restart), entries stay forever
- No max size check
**Impact:**
```
Long-running server processes 10,000 Type3 fonts:
- type3GlyphCoverageCache: 10,000 entries × ~1KB each = 10MB
- type3NormalizedFontCache: 1,000 cached fonts × ~100KB each = 100MB
- documentCache: 50 active jobs × 10MB each = 500MB
After 1 week: Caches grow to 1GB+
After 1 month: OutOfMemoryError, server restart required
```
**Recommendation:**
```java
// Use Caffeine cache with eviction policies
private final Cache < String , PDFont > type3NormalizedFontCache =
Caffeine . newBuilder ()
. maximumSize ( 1000 ) // Max 1000 fonts
. expireAfterAccess ( 1 , TimeUnit . HOURS ) // Expire after 1hr unused
. removalListener (( key , value , cause ) -> {
// Cleanup PDFont resources if needed
})
. build ();
private final Cache < String , Set < Integer >> type3GlyphCoverageCache =
Caffeine . newBuilder ()
. maximumSize ( 5000 )
. expireAfterWrite ( 1 , TimeUnit . HOURS )
. build ();
private final Cache < String , CachedPdfDocument > documentCache =
Caffeine . newBuilder ()
. maximumWeight ( 500_000_000 ) // 500MB max
. weigher (( String key , CachedPdfDocument doc ) ->
doc . getPdfBytes (). length )
. expireAfterWrite ( 30 , TimeUnit . MINUTES )
. removalListener (( key , value , cause ) -> {
log . info ( "Evicted document {} (cause: {})" , key , cause );
})
. build ();
```
---
2025-11-10 10:16:57 +00:00
### Issue #4: Type3 Cache Race Condition - HIGH
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:3759-3773`
**Severity:** HIGH (Duplicate Work)
**Type:** Check-Then-Act Race Condition
**Verified:** ✅ TRUE
**Description:**
```java
private void loadNormalizedType3Font (
PDDocument document ,
PdfJsonFont fontModel ,
List < FontByteSource > candidates ,
String originalFormat ) throws IOException {
if ( fontModel . getUid () == null || candidates == null || candidates . isEmpty ()) {
return ;
}
if ( type3NormalizedFontCache . containsKey ( fontModel . getUid ())) { // CHECK
return ;
}
for ( FontByteSource source : candidates ) {
PDFont font = loadFontFromSource (...); // EXPENSIVE: 10-50ms
if ( font != null ) {
type3NormalizedFontCache . put ( fontModel . getUid (), font ); // ACT
log . info ( "Cached normalized font {} for Type3 {}" , ...);
break ;
}
}
}
```
**Race Condition:**
```
Thread A: Check cache for "1:F1" → MISS (line 3759)
Thread B: Check cache for "1:F1" → MISS (line 3759) [both pass check!]
Thread A: Load font from bytes (10ms I/O + parsing)
Thread B: Load font from bytes (10ms I/O + parsing) ← DUPLICATE WORK
Thread A: Put font in cache (line 3766)
Thread B: Put font in cache (line 3766) [overwrites A's entry]
```
**Why ConcurrentHashMap Doesn't Help:**
- ConcurrentHashMap prevents **corruption** (map state stays consistent)
- ConcurrentHashMap does NOT prevent **duplicate work** (both threads compute)
- The check (`containsKey` ) and act (`put` ) are separate operations
**Impact:**
- Wasted CPU cycles loading same font twice
- Temporary memory spike (two fonts in heap simultaneously)
- Font loading is expensive: Base64 decode + PDFBox parsing + font validation
- Under high concurrency, 10+ threads could all load the same font
**Recommendation:**
```java
private void loadNormalizedType3Font (...) throws IOException {
if ( fontModel . getUid () == null || candidates == null || candidates . isEmpty ()) {
return ;
}
// Atomic compute-if-absent
type3NormalizedFontCache . computeIfAbsent ( fontModel . getUid (), uid -> {
for ( FontByteSource source : candidates ) {
try {
PDFont font = loadFontFromSource (...);
if ( font != null ) {
log . info ( "Cached normalized font {} for Type3 {}" , ...);
return font ;
}
} catch ( IOException e ) {
log . warn ( "Failed to load font from {}: {}" , source . originLabel (), e . getMessage ());
}
}
return null ;
});
}
```
---
2025-11-10 10:16:57 +00:00
### Issue #5: PDDocument Resource Lifecycle - NEEDS INVESTIGATION
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:3766, 5158`
**Severity:** UNKNOWN (Requires Investigation)
**Type:** Unclear Resource Ownership
**Verified:** ⚠️ SPECULATIVE (No concrete evidence of failure)
**Description:**
```java
// Line 3766: Cache PDFont created from a PDDocument
try ( PDDocument document = ...) {
PDFont font = loadFontFromSource ( document , fontModel , source , ...);
type3NormalizedFontCache . put ( fontModel . getUid (), font );
} // PDDocument is closed here!
// Later: cached PDFont is used with a DIFFERENT PDDocument
try ( PDDocument newDocument = ...) {
PDFont cachedFont = type3NormalizedFontCache . get ( fontUid );
// Is cachedFont safe to use after original document closed?
// Does it hold references to freed native resources?
}
```
**Theoretical Concerns:**
1. **Native Memory:** PDFBox uses JNI for some operations
2. **Resource Ties:** PDFont may hold references to the source PDDocument
3. **Freed Resources:** Using PDFont after document closes could access freed memory
4. **Unclear Contract:** PDFBox documentation doesn't explicitly address font lifecycle
**Current Status:**
- ⚠️ **NO EVIDENCE OF ACTUAL FAILURES** - System appears to work in practice
- ⚠️ **NO CRASHES OBSERVED** - No segmentation faults or memory corruption reported
- ⚠️ **NO MEMORY LEAKS DETECTED** - No profiler data showing leaks
- ⚠️ **PURELY THEORETICAL CONCERN** - Based on API design, not observed behavior
**Why This May Actually Be Safe:**
- PDFBox may create self-contained PDFont objects
- Font data may be copied rather than referenced
- PDFBox may be designed for this use case
- Current code has been running without apparent issues
**Required Investigation:**
1. **PDFBox Source Code Review:** Check if PDFont copies or references document data
2. **Load Testing:** Create PDFont, close document, use font in new document
3. **Memory Profiling:** Monitor for native memory leaks over extended runs
4. **PDFBox Documentation/Forums:** Search for guidance on font lifecycle
**Recommendation:**
- **Priority: MEDIUM** (needs investigation but not blocking)
- Add monitoring for potential issues
- Test font reuse after document closure explicitly
- If problems found, cache serialized bytes instead of PDFont objects
```java
// Option 1: Cache font bytes instead of PDFont objects
private void cacheType3FontBytes ( String fontUid , byte [] fontBytes ) {
type3FontBytesCache . put ( fontUid , fontBytes );
}
// Option 2: Verify font is safe to use
private PDFont getCachedFont ( String fontUid ) {
PDFont cached = type3NormalizedFontCache . get ( fontUid );
if ( cached != null && ! isFontValid ( cached )) {
log . warn ( "Cached font {} is invalid, removing" , fontUid );
type3NormalizedFontCache . remove ( fontUid );
return null ;
}
return cached ;
}
```
---
## MEDIUM SEVERITY ISSUES
2025-11-10 10:16:57 +00:00
### Issue #6: Full PDF Reload Per Page - MEDIUM Performance
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:5158-5159`
**Severity:** MEDIUM (Performance)
**Type:** Inefficient I/O
**Verified:** ✅ TRUE
**Description:**
```java
// extractSinglePage method
try ( PDDocument document = pdfDocumentFactory . load ( cached . getPdfBytes (), true )) {
// Full PDF loaded from bytes (10-100ms for large PDFs)
PDPage page = document . getPage ( pageIndex );
// Extract just one page...
}
```
**Problem:**
Every page request loads the entire PDF from bytes:
- 100-page PDF = Load 10MB, extract 1 page
- 10 page requests = 10× full PDF loads (100MB I/O)
- No incremental parsing or streaming
**Impact:**
```
100MB PDF, 50 pages requested sequentially:
- Total I/O: 100MB × 50 = 5GB
- Time: 50× parse time (5-10 seconds total)
- Memory: 100MB peak per request
Concurrent page requests for same PDF:
- 10 threads × 100MB = 1GB temporary memory spike
```
**Why This Exists:**
Lazy loading design trades memory (don't cache full extraction) for CPU (reload on demand). But the tradeoff is poor because:
- PDFBox parsing is expensive
- Repeated decompression of streams
- Could cache extracted page data instead
**Recommendation:**
```java
// Option 1: Cache extracted page data
private static class CachedPage {
List < PdfJsonTextElement > textElements ;
List < PdfJsonImageElement > imageElements ;
// ... other page data
}
Map < String , Map < Integer , CachedPage >> pageCache = ...;
// Option 2: Keep PDF open with RandomAccessFile
private static class CachedPdfDocument {
private final RandomAccessReadBufferedFile randomAccess ;
private final PDDocument document ; // Keep open!
}
// Option 3: Pre-split pages at upload time
// Store each page as separate lightweight JSON blob
```
---
2025-11-10 10:16:57 +00:00
### Issue #7: Large Base64 Operations - MEDIUM Performance
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:1062, 1428, 3570, 3584, 3612, 3630`
**Severity:** MEDIUM (Performance Bottleneck)
**Type:** Synchronous Blocking Operation
**Verified:** ✅ TRUE
**Description:**
```java
// Encode large font programs
String base64 = Base64 . getEncoder (). encodeToString ( fontBytes ); // 10MB → 13MB
// Decode large font programs
byte [] bytes = Base64 . getDecoder (). decode ( pdfProgram ); // 13MB → 10MB
```
**Problem:**
- Large fonts (embedded TrueType, Type3) can be 5-10MB
- Base64 encoding inflates size by ~33%
- All encoding/decoding is synchronous on request threads
- CPU-intensive operation (20-50ms for 10MB)
**Impact:**
```
100 concurrent requests processing 10MB fonts:
- Each request: 30ms CPU time for Base64
- All threads blocked on encoding simultaneously
- Thread pool saturation (if using fixed-size pool)
- Other requests starved waiting for threads
Large PDF with 50 fonts:
- 50 × 30ms = 1.5 seconds just for Base64 operations
- User perceives slowness
```
**Recommendation:**
```java
// Option 1: Size limits
private static final int MAX_FONT_SIZE = 10 * 1024 * 1024 ; // 10MB
if ( fontBytes . length > MAX_FONT_SIZE ) {
throw new IllegalArgumentException ( "Font too large: " + fontBytes . length );
}
// Option 2: Streaming Base64 (for very large files)
OutputStream base64Out = Base64 . getEncoder (). wrap ( outputStream );
inputStream . transferTo ( base64Out );
// Option 3: Async processing
CompletableFuture < String > encodeFuture = CompletableFuture . supplyAsync (
() -> Base64 . getEncoder (). encodeToString ( fontBytes ),
fontEncodingExecutor
);
```
---
2025-11-10 10:16:57 +00:00
### Issue #8: File I/O on Request Threads - MEDIUM
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonConversionService.java:276, 405, 5066`
**Severity:** MEDIUM (Performance)
**Type:** Blocking I/O
**Verified:** ❌ PARTIALLY TRUE
**Description:**
```java
// Line 276: Write upload to disk
file . transferTo ( originalFile . getFile ());
// Line 405: Read full file into memory
byte [] cachedPdfBytes = Files . readAllBytes ( workingPath );
// Line 5066: Get uploaded file bytes
byte [] pdfBytes = file . getBytes ();
```
**Clarification:**
- These are in DIFFERENT methods (not double-reads within one operation)
- Each method reads the file once
- Still synchronous blocking I/O
**Impact:**
- Large uploads (100MB) block request thread for seconds
- No async or streaming support
- Thread pool saturation under high upload volume
**Recommendation:**
```java
// Async file I/O
CompletableFuture < Path > uploadFuture = CompletableFuture . supplyAsync (
() -> {
Path tempPath = Files . createTempFile ( "pdf-upload" , ".pdf" );
file . transferTo ( tempPath . toFile ());
return tempPath ;
},
fileIoExecutor
);
// Stream large files
try ( InputStream in = file . getInputStream ();
OutputStream out = Files . newOutputStream ( targetPath )) {
in . transferTo ( out );
}
```
---
## LOW SEVERITY ISSUES
2025-11-10 10:16:57 +00:00
### Issue #9: PdfLazyLoadingService Unused - LOW
2025-11-10 00:26:55 +00:00
**Location:** `PdfLazyLoadingService.java` (entire file)
**Severity:** LOW (Code Quality)
**Type:** Dead Code
**Verified:** ✅ TRUE
**Description:**
- Complete service implementation exists
- Has its own `documentCache` and cleanup logic
- Duplicates functionality in `PdfJsonConversionService`
- Not wired to any controller
- Not imported by any other class
**Impact:**
- Code maintenance burden
- Confusing for developers
- Potential for accidental use in future
- Cache divergence if both ever get used
**Recommendation:**
```java
// Delete PdfLazyLoadingService.java entirely
// Or clearly mark as @Deprecated with explanation
```
---
2025-11-10 10:16:57 +00:00
### Issue #10: PdfJsonFontService Volatile Fields - LOW
2025-11-10 00:26:55 +00:00
**Location:** `PdfJsonFontService.java:46-47`
**Severity:** LOW (Actually Correct)
**Type:** None (Good Practice)
**Verified:** ✅ TRUE (No issue, correctly implemented)
**Description:**
```java
private volatile boolean pythonCffConverterAvailable ;
private volatile boolean fontForgeCffConverterAvailable ;
@PostConstruct
private void initialiseCffConverterAvailability () {
pythonCffConverterAvailable = isCommandAvailable ( pythonCommand );
fontForgeCffConverterAvailable = isCommandAvailable ( fontforgeCommand );
}
```
**Why This Is Correct:**
- `volatile` ensures visibility across threads
- Set once at startup
- Read many times (thread-safe)
- No synchronization needed
**Recommendation:** None - this is good practice.
---
## VERIFIED FALSE CLAIMS
### Claim: file.getBytes() Called Twice
**Status:** ❌ FALSE
**Explanation:** The claim stated that `file.getBytes()` is called twice (lines 446, 5065). Investigation shows:
- Line 446: `convertJsonToPdf` method
- Line 5065: `extractDocumentMetadata` method
- These are DIFFERENT methods for DIFFERENT operations
- Each method calls `getBytes()` only once
**Conclusion:** Not a double-read issue.
---
### Claim: Image Base64 Encoding Per Call
**Status:** ❌ FALSE
**Explanation:** The claim stated images are Base64-encoded on every call. Investigation shows:
```java
// PdfJsonImageService.java:430-450
private EncodedImage getOrEncodeImage ( PDImage pdImage ) {
COSBase key = xObject . getCOSObject ();
EncodedImage cached = imageCache . get ( key ); // Cache check!
if ( cached != null ) {
return cached ; // Cache hit
}
EncodedImage encoded = encodeImage ( pdImage );
imageCache . put ( key , encoded ); // Cache miss, encode and store
return encoded ;
}
```
**Conclusion:** Images ARE cached. Only stencil and inline images bypass cache.
---
## ARCHITECTURE ISSUES
2025-11-10 10:16:57 +00:00
### Issue #11: Singleton Service Architecture - MEDIUM
2025-11-10 00:26:55 +00:00
**Location:** All `@Service` and `@Component` classes
**Severity:** MEDIUM (Maintainability)
**Type:** Architectural Pattern
**Description:**
All services use default singleton scope:
```java
@Service // Defaults to singleton
public class PdfJsonConversionService {
// Shared instance variables across all requests
private final Map < String , PDFont > type3NormalizedFontCache = ...;
}
```
**Implications:**
✅ **Good:**
- Most dependencies are stateless and injected
- Caches use ConcurrentHashMap (thread-safe)
- No mutable instance variables beyond caches
⚠️ **Risks:**
- Singleton means shared state across all requests
- Requires careful synchronization
- Easy for future developers to introduce thread-unsafe code
- Difficult to test concurrent scenarios
**Recommendation:**
- Document thread-safety requirements prominently
- Add unit tests for concurrent access
- Consider request-scoped services for mutable state
- Code review checklist for new instance variables
---
## SUMMARY BY SEVERITY
### CRITICAL (Fix Immediately)
2025-11-10 10:16:57 +00:00
1. **pageFontResources PDFont keys** - Broken feature
2. **Unbounded thread creation** - Resource exhaustion
2025-11-10 00:26:55 +00:00
### HIGH (Fix Soon)
2025-11-10 10:16:57 +00:00
3. **Unbounded cache growth** - Memory leak
4. **Type3 cache race** - Duplicate work
5. **PDDocument lifecycle** - Needs investigation (speculative)
2025-11-10 00:26:55 +00:00
### MEDIUM (Plan and Address)
2025-11-10 10:16:57 +00:00
6. **Full PDF reload per page** - Performance
7. **Large Base64 operations** - Performance
8. **File I/O blocking** - Performance
9. **PdfLazyLoadingService unused** - Dead code
10. **Singleton architecture** - Maintainability
2025-11-10 00:26:55 +00:00
### LOW (Monitor)
2025-11-10 10:16:57 +00:00
11. **PdfJsonFontService volatile** - Correctly implemented (no action needed)
2025-11-10 00:26:55 +00:00
### VERIFIED FALSE
2025-11-10 10:16:57 +00:00
12. file.getBytes() called twice
13. Image Base64 encoding per call
2025-11-10 00:26:55 +00:00
---
## IMPLEMENTATION ROADMAP
2025-11-10 10:16:57 +00:00
### Phase 1: Critical Fixes (1-2 weeks)
2025-11-10 00:26:55 +00:00
2025-11-10 10:16:57 +00:00
**1. Fix pageFontResources PDFont keys (Issue #1)**
2025-11-10 00:26:55 +00:00
```java
// Priority: CRITICAL
// Time: 3-5 days
// Risk: Medium (requires careful testing)
// Replace PDFont keys with String resource names
2025-11-10 10:16:57 +00:00
// Update cache lookup logic
2025-11-10 00:26:55 +00:00
```
2025-11-10 10:16:57 +00:00
**2. Fix Thread Leaks (Issue #2)**
2025-11-10 00:26:55 +00:00
```java
// Priority: CRITICAL
// Time: 1 day
// Risk: Low (well-understood solution)
// Replace new Thread() with ScheduledExecutorService
// Add @PreDestroy cleanup
// Monitor thread counts
```
2025-11-10 10:16:57 +00:00
### Phase 2: Resource Management (1 week)
**3. Add Cache Eviction (Issue #3)**
2025-11-10 00:26:55 +00:00
```java
// Priority: HIGH
// Time: 3 days
// Risk: Low (library-based solution)
// Integrate Caffeine cache
// Set size limits, TTL
// Add eviction logging
// Monitor cache metrics
```
2025-11-10 10:16:57 +00:00
**4. Fix Type3 Cache Race (Issue #4)**
```java
// Priority: HIGH
// Time: 1-2 days
// Risk: Low (straightforward fix)
// Use computeIfAbsent for atomic operations
```
2025-11-10 00:26:55 +00:00
### Phase 3: Performance Optimization (2-3 weeks)
2025-11-10 10:16:57 +00:00
**5. Optimize Lazy Loading (Issue #6)**
2025-11-10 00:26:55 +00:00
```java
// Priority: MEDIUM
// Time: 1 week
// Risk: Medium (requires benchmarking)
// Cache extracted page data
// Or: Keep PDDocument open with RandomAccessFile
// Or: Pre-split pages at upload
```
2025-11-10 10:16:57 +00:00
**6. Async I/O (Issues #7, #8)**
2025-11-10 00:26:55 +00:00
```java
// Priority: MEDIUM
// Time: 3-5 days
// Risk: Medium (requires async architecture changes)
// Add dedicated I/O thread pool
// Async file operations
// Stream large files
```
### Phase 4: Code Quality (1 week)
2025-11-10 10:16:57 +00:00
**7. Remove Dead Code (Issue #9)**
2025-11-10 00:26:55 +00:00
```java
// Priority: LOW
// Time: 1 day
// Risk: None
// Delete PdfLazyLoadingService
// Clean up unused imports
```
**8. Documentation & Testing**
```java
// Priority: MEDIUM
// Time: 3-5 days
// Add thread-safety documentation
// Concurrent integration tests
// Load testing scripts
```
---
## TESTING STRATEGY
### 1. Concurrency Tests
```java
@SpringBootTest
@TestInstance ( TestInstance . Lifecycle . PER_CLASS )
class ConcurrencyTest {
@Test
void testConcurrentCacheAccess () throws Exception {
ExecutorService executor = Executors . newFixedThreadPool ( 20 );
CountDownLatch latch = new CountDownLatch ( 100 );
List < Future <?>> futures = new ArrayList <> ();
// 100 requests across 10 jobIds (10 requests per job)
for ( int i = 0 ; i < 100 ; i ++ ) {
String jobId = "job-" + ( i % 10 );
int pageNum = ( i % 5 ) + 1 ;
futures . add ( executor . submit (() -> {
try {
service . extractSinglePage ( jobId , pageNum );
} catch ( Exception e ) {
log . error ( "Concurrent access failed" , e );
throw e ;
} finally {
latch . countDown ();
}
}));
}
// Wait for completion
assertTrue ( latch . await ( 60 , TimeUnit . SECONDS ));
// Check for exceptions
for ( Future <?> future : futures ) {
future . get (); // Throws if any task failed
}
}
@Test
void testCacheNotCorrupted () throws Exception {
// Upload document
String jobId = "test-job" ;
service . extractDocumentMetadata ( testPdf , jobId );
// Concurrent page requests
ExecutorService executor = Executors . newFixedThreadPool ( 10 );
List < Future < byte []>> futures = new ArrayList <> ();
for ( int i = 0 ; i < 50 ; i ++ ) {
int page = ( i % 10 ) + 1 ;
futures . add ( executor . submit (() ->
service . extractSinglePage ( jobId , page )));
}
// All should succeed without ConcurrentModificationException
for ( Future < byte []> future : futures ) {
assertNotNull ( future . get ());
}
}
}
```
### 2. Memory Leak Tests
```java
@Test
void testCacheDoesNotGrowUnbounded () {
long initialHeap = getHeapUsage ();
// Process 10,000 small PDFs with Type3 fonts
for ( int i = 0 ; i < 10000 ; i ++ ) {
service . convertPdfToJson ( createTestPdfWithType3Fonts ());
}
// Force GC
System . gc ();
Thread . sleep ( 1000 );
long finalHeap = getHeapUsage ();
long growth = finalHeap - initialHeap ;
// Cache should not grow beyond reasonable limit
assertThat ( growth ). isLessThan ( 100_000_000 ); // 100MB max
}
@Test
void testThreadsNotLeaking () {
int initialThreads = getActiveThreadCount ();
// Upload 100 PDFs (spawns 100 cleanup threads)
for ( int i = 0 ; i < 100 ; i ++ ) {
service . extractDocumentMetadata ( testPdf , "job-" + i );
}
int peakThreads = getActiveThreadCount ();
// Should not create 100+ threads
assertThat ( peakThreads - initialThreads ). isLessThan ( 10 );
}
private long getHeapUsage () {
Runtime runtime = Runtime . getRuntime ();
return runtime . totalMemory () - runtime . freeMemory ();
}
private int getActiveThreadCount () {
return Thread . getAllStackTraces (). size ();
}
```
### 3. Security Tests
```java
@Test
void testJobIdIsolation () {
// User A uploads PDF
String jobIdA = service . extractDocumentMetadata ( userAPdf , sessionA );
// User B tries to access User A's jobId
assertThrows ( AccessDeniedException . class , () -> {
service . extractSinglePage ( jobIdA , 1 , sessionB );
});
}
@Test
void testJobIdUnpredictable () {
Set < String > jobIds = new HashSet <> ();
for ( int i = 0 ; i < 1000 ; i ++ ) {
String jobId = service . extractDocumentMetadata ( testPdf , session );
jobIds . add ( jobId );
}
// All jobIds should be unique UUIDs
assertThat ( jobIds ). hasSize ( 1000 );
// Should not be sequential
List < String > sorted = new ArrayList <> ( jobIds );
Collections . sort ( sorted );
assertThat ( sorted ). isNotEqualTo ( new ArrayList <> ( jobIds ));
}
```
### 4. Performance Tests
```java
@Test
void testLargeFilePerformance () {
// 100MB PDF
byte [] largePdf = createLargePdf ( 100 * 1024 * 1024 );
long start = System . currentTimeMillis ();
String json = service . convertPdfToJson ( largePdf );
long duration = System . currentTimeMillis () - start ;
// Should complete in reasonable time
assertThat ( duration ). isLessThan ( 30_000 ); // 30 seconds
}
@Test
void testConcurrentThroughput () throws Exception {
ExecutorService executor = Executors . newFixedThreadPool ( 50 );
CountDownLatch latch = new CountDownLatch ( 500 );
long start = System . currentTimeMillis ();
for ( int i = 0 ; i < 500 ; i ++ ) {
executor . submit (() -> {
try {
service . convertPdfToJson ( testPdf );
} finally {
latch . countDown ();
}
});
}
latch . await ();
long duration = System . currentTimeMillis () - start ;
// 500 conversions should complete in reasonable time
double throughput = 500 . 0 / ( duration / 1000 . 0 );
assertThat ( throughput ). isGreaterThan ( 10 ); // At least 10 conversions/sec
}
```
---
## MONITORING & METRICS
### Recommended Metrics (Micrometer)
```java
@Service
public class PdfJsonConversionService {
private final MeterRegistry meterRegistry ;
// Cache size gauges
@PostConstruct
void registerMetrics () {
Gauge . builder ( "pdf.cache.document.size" , documentCache , Map :: size )
. description ( "Number of cached documents" )
. register ( meterRegistry );
Gauge . builder ( "pdf.cache.type3font.size" , type3NormalizedFontCache , Map :: size )
. description ( "Number of cached Type3 fonts" )
. register ( meterRegistry );
Gauge . builder ( "pdf.cache.coverage.size" , type3GlyphCoverageCache , Map :: size )
. description ( "Number of cached glyph coverage sets" )
. register ( meterRegistry );
Gauge . builder ( "pdf.threads.cleanup" , this :: getCleanupThreadCount )
. description ( "Active cleanup threads" )
. register ( meterRegistry );
}
// Operation timers
public String convertPdfToJson ( byte [] pdfBytes ) {
Timer . Sample sample = Timer . start ( meterRegistry );
try {
String result = doConvertPdfToJson ( pdfBytes );
sample . stop ( meterRegistry . timer ( "pdf.convert.toJson" ));
return result ;
} catch ( Exception e ) {
meterRegistry . counter ( "pdf.convert.errors" , "operation" , "toJson" ). increment ();
throw e ;
}
}
// Cache hit/miss counters
private PDFont getCachedType3Font ( String uid ) {
PDFont cached = type3NormalizedFontCache . get ( uid );
if ( cached != null ) {
meterRegistry . counter ( "pdf.cache.type3font.hits" ). increment ();
} else {
meterRegistry . counter ( "pdf.cache.type3font.misses" ). increment ();
}
return cached ;
}
}
```
### Alerts
```yaml
alerts :
# Cache growth
- name : DocumentCacheTooLarge
condition : pdf_cache_document_size > 100
severity : warning
- name : Type3CacheTooLarge
condition : pdf_cache_type3font_size > 1000
severity : warning
# Thread leaks
- name : TooManyCleanupThreads
condition : pdf_threads_cleanup > 10
severity : critical
# Memory pressure
- name : HeapUsageHigh
condition : jvm_memory_used_bytes / jvm_memory_max_bytes > 0.8
severity : warning
# Performance
- name : SlowConversions
condition : pdf_convert_toJson{quantile="0.95"} > 10s
severity : warning
# Error rate
- name : HighErrorRate
condition : rate(pdf_convert_errors[5m]) > 0.1
severity : critical
```
---
## CONCLUSION
The PDF JSON editor has **CRITICAL** issues that must be fixed before production deployment:
### Must-Fix Issues (Blocks Production):
2025-11-10 10:16:57 +00:00
1. **pageFontResources broken** - Lazy page loading completely broken (PDFont instances as keys)
2. **Thread leaks** - Unbounded thread creation causes OutOfMemoryError
2025-11-10 00:26:55 +00:00
### Should-Fix Issues (Prevents Scale):
2025-11-10 10:16:57 +00:00
3. **Unbounded cache growth** - Memory leaks require server restarts
4. **Type3 cache races** - Wasted CPU doing duplicate work
5. **PDDocument lifecycle** - Needs investigation (no evidence of actual problems yet)
2025-11-10 00:26:55 +00:00
### Performance Improvements (Nice-to-Have):
2025-11-10 10:16:57 +00:00
6. Full PDF reload per page
7. Large Base64 operations
8. Synchronous file I/O
2025-11-10 00:26:55 +00:00
### Code Quality Issues:
2025-11-10 10:16:57 +00:00
9. PdfLazyLoadingService dead code
10. Documentation of thread-safety requirements
2025-11-10 00:26:55 +00:00
**Estimated Effort:**
2025-11-10 10:16:57 +00:00
- Critical fixes: 1-2 weeks (Issues #1 , #2 )
- High priority: 1 week (Issues #3 , #4 )
- Performance: 2-3 weeks (Issues #6 -8)
- **Total:** 4-6 weeks for complete remediation
2025-11-10 00:26:55 +00:00
2025-11-10 10:16:57 +00:00
**Recommendation:** Fix critical issues (#1 , #2 ) immediately, then address high priority issues before beta testing.