← Level 5 LEVEL 5.3

Asynchronous Queue & Decoupled Workers (Kafka/RabbitMQ & S3 Storage)

Exam Progress: 0 / 15
Mastery: 0%
⚡

Warm-up Quick Fire: Retention Check (from 5.2 Dynamic Template Injection)

Anti-Forgetting Protocol

สถานการณ์จำลอง (Snowball Scenario): ในบทที่ 5.2 เราป้องกัน Metaspace Leak สำเร็จด้วย In-Memory AST Mutation ร่วมกับ Schema Fingerprint Caching แต่หากผู้บริหารระดับสูงกดร้องขอรายงานบัญชีแยกประเภทแบบ Ad-hoc ย้อนหลัง 10 ปี (มีข้อมูลกว่า 2,000,000 แถว ใช้เวลาประมวลผลประมาณ 6-8 นาที) แล้วระบบ REST API ของคุณยังประมวลผลคำขอนี้แบบ Synchronous Blocking HTTP Request ตรงๆ ใน Web Thread (เช่น Spring Boot Tomcat Worker Thread) จะก่อให้เกิดความล้มเหลวร้ายแรงต่อสถาปัตยกรรม Microservices อย่างไร?

5.3 Contextual Anchoring: The Synchronous Thread Starvation Crisis

Why Long-Running Document Generation Destroys Synchronous Web Runtimes & How Event-Driven Queues Save Clusters

💥 Tomcat Thread Starvation & Cascading Crash

Web Container มีขีดจำกัด Worker Threads (เช่น max-threads=200) เมื่อมีคำขอรายงานขนาดใหญ่เข้ามาพร้อมกัน 50 คำขอ แต่ละคำขอกินเวลา 5 นาที เธรดทั้งหมดจะถูกจองค้างไว้ ทำให้ API ทั่วไป (เช่น Login, Check Balance) ไม่สามารถรับการเชื่อมต่อใหม่ได้ ส่งผลให้ Health Check ล้มเหลวและ Kubernetes ทยอย Kill Pod ทิ้งทีละโหนด

⚠️ Duplicate Execution on Client Retry & Rebalance

เมื่อเกิด Network Timeout ไคลเอนต์จะ Retry คำขอซ้ำ หรือเมื่อ Worker ใน Consumer Group ค้างนานจนเกิน max.poll.interval.ms ใน Kafka คลัสเตอร์จะทริกเกอร์ Partition Rebalance และส่งงานเดียวกันไปให้ Worker ตัวอื่นทำซ้ำ สิ้นเปลืองทั้ง CPU, RAM, และ Disk ซ้อนทับกันหลายเท่าตัว

🛡️ The Enterprise Solution: Decoupled Workers & S3 Spilling

สถาปัตยกรรมระดับ Mission-Critical ตัดขาด Web Layer ออกจาก Report Engine 100%: Web API รับคำขอแล้วส่ง Job Ticket ลงในคิว (Kafka/RabbitMQ) พร้อมคืนรหัส 202 Accepted ทันที จากนั้น Isolated Worker Pods จะดึงงานไป Fill ร่วมกับ JRSwapFileVirtualizer และอัปโหลดไฟล์ตรงเข้า AWS S3 / MinIO พร้อมแจ้งเตือนผ่าน WebSocket หรือ Webhook

🏛️ Dynamic Core Pillars (4 เสาหลักสถาปัตยกรรมการประมวลผลรายงานแบบกระจายศูนย์)

Pillar 1

Event-Driven Asynchronous Decoupling

การแยกหน้าที่ระหว่าง Web Ingestion Pods (เน้นรับคำขอรวดเร็ว) และ Dedicated Report Worker Pods (เน้นใช้ CPU/Heap สูง) ผ่าน Distributed Message Broker (Kafka/RabbitMQ) ทำให้โหลดการสร้างเอกสารไม่รบกวน Core Business APIs

Analogy: Restaurant Order Ticket & Kitchen Pipeline
Pillar 2

Idempotency & Distributed Lease Locks

การใช้ Distributed Lock (Redis Redisson / PostgreSQL Advisory Lock) ควบคู่กับ Idempotency-Key ประจำงาน เพื่อรับประกันว่ารายงานเดียวกันจะไม่ถูกเรนเดอร์ซ้ำสองครั้ง แม้จะเกิด Message Retry หรือ Consumer Rebalance

Analogy: Distributed Distributed Mutex
Pillar 3

Ephemeral Storage Offload (S3/MinIO)

การสตรีมไฟล์ผลลัพธ์ (PDF/XLSX) จาก Exporter ไปยัง Cloud Object Storage ทันทีโดยไม่ต้องบันทึกค้างไว้บน Persistent Volume ของ Container และส่งมอบให้ผู้ใช้ผ่าน Pre-signed URL ที่มีอายุจำกัด

Analogy: Secure Temporary Vault Box
Pillar 4

Reactive State & Progress Telemetry

การรายงานความคืบหน้าของสถานะการ Fill (เช่น QUEUED → PROCESSING (Page 45,000) → COMPLETED) สู่ Frontend ผ่าน Server-Sent Events (SSE) หรือ Webhook Callbacks แบบ Real-time

Analogy: Package Tracking Status Dashboard
STEP 1

Engine Internals: Distributed Job State Machine & Worker Lifecycle

เมื่อระบบรายงานระดับองค์กรถูกแปลงเป็น Asynchronous Pipeline การทำงานภายในของระบบจะดำเนินตามวัฏจักร 5 ขั้นตอนหลัก:

  1. Job Dispatch & Fast Ack: Web Ingestion Pod รับคำขอจากผู้ใช้ บันทึก Metadata เบื้องต้นลงในฐานข้อมูลด้วยสถานะ PENDING จากนั้น Publish ข้อความ JSON (บรรจุ Job ID, Template Name, และ Parameters) ลงใน Kafka Topic แล้วตอบกลับ HTTP 202 Accepted ทันทีในเวลา < 20ms
  2. Distributed Lease Acquisition: Worker Pod ที่ว่างดึงข้อความจากคิว และทำการแย่งสิทธิ์ผ่าน Distributed Lock (Redis Redisson tryLock(jobId, leaseTime=15m)) เพื่อรับประกันความปลอดภัยจากการทำงานซ้ำ (Idempotency Guard)
  3. Off-Heap Isolated Execution: Worker ทำการโหลด Compiled JasperReport จาก Local Cache หรือ S3, ทำการ Initialize JRSwapFileVirtualizer เพื่อควบคุม Heap และดึงข้อมูลทีละ Batch จาก Database หรือ Data Lake เข้าสู่ JasperFillManager.fillReport()
  4. Multipart Streaming to S3: เมื่อ Fill สำเร็จ Worker จะสั่ง JRPdfExporter หรือ JRXlsxExporter ให้สตรีมไบต์เอกสารผ่าน S3 Multipart Upload Pipe สู่ Bucket โดยตรงโดยไม่ผ่าน RAM
  5. Lifecycle Disposal & Callback: คืนบล็อกดิสก์ด้วย virtualizer.cleanup(), ปล่อย Redis Lock, อัปเดตสถานะเป็น COMPLETED พร้อมบันทึก S3 Object Key และส่ง Event แจ้งเตือนไคลเอนต์ผ่าน SSE/WebSocket
Production Asynchronous Hazard Underlying Root Cause Runtime Symptom / Exception Architectural Defense
Kafka Consumer Rebalance Loop การ Fill รายงานใช้เวลานานเกินกว่าค่า max.poll.interval.ms (เช่น เกิน 5 นาที) ทำให้ Kafka คิดว่า Worker ตาย CommitFailedException: Consumer was revoked due to heartbeat failure ปรับเพิ่ม max.poll.interval.ms ให้เหมาะสม และแยกเธรด Poll ข้อความออกจากเธรด Fill ผ่าน ThreadPoolExecutor
Poison Pill Message Loop คำขอบางรายการมี Parameters ไม่ถูกต้องทำให้เกิด JRValidationException วนซ้ำในคิว Deadlock Storm: Worker พยายามรันงานเดิมซ้ำๆ จน CPU พุ่ง 100% ทั้งคลัสเตอร์ ตั้งค่า Dead Letter Queue (DLQ) กำหนด Retry สูงสุด 3 ครั้ง และส่งข้อความที่ล้มเหลวไปเก็บใน DLQ ทันที
Duplicate Report Spilling (At-Least-Once) Message Broker ส่งข้อความซ้ำหลังจาก Consumer ล้มเหลวกลางคัน Duplicate Billing / Duplicate S3 Objects: ผู้ใช้ได้รับเอกสารสองฉบับซ้อนกัน ใช้ Idempotency Key (SHA-256) เช็กสถานะใน Redis/Database ก่อนเริ่มดำเนินการ
Worker Node Out-of-Disk Leak เมื่อเกิด OutOfMemory หรือ Unhandled Exception แล้วไม่มีการเรียก virtualizer.cleanup() ในบล็อก finally IOException: No space left on device (/tmp/jasper_swap บวมข้ามสัปดาห์) ครอบโค้ดด้วย try-finally ทุกครั้ง และใช้ Kubernetes EmptyDir Volume ที่กำหนด Size Limit พร้อม CronJob คอยตรวจสอบ
STEP 2

Strict 7.0.x Architecture Implementation

Java 21 / Spring Boot 3.x / Kafka Worker / S3 Stream

โค้ดสถาปัตยกรรมด้านล่างแสดงการทำงานของ AsynchronousReportWorkerService ซึ่งทำหน้าที่ Consume งานจาก Kafka, ควบคุม Idempotency ด้วย Redisson Distributed Lock, จัดการ Off-Heap Paging ด้วย JRSwapFileVirtualizer, และสตรีมไฟล์ตรงเข้าสู่ AWS S3/MinIO Bucket:

AsynchronousReportWorkerService.java (Spring Boot 3.x / Java 21)
package com.enterprise.report.worker;

import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.fill.JRSwapFileVirtualizer;
import net.sf.jasperreports.engine.util.JRSwapFile;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.*;
import java.nio.file.Files;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class AsynchronousReportWorkerService {

    private final RedissonClient redissonClient;
    private final S3Client s3Client;
    private final JRSwapFile sharedSwapFile;

    public record ReportTaskMessage(String jobId, String reportName, Map params, String s3Bucket, String s3Key) {}

    public AsynchronousReportWorkerService(RedissonClient redissonClient, S3Client s3Client) {
        this.redissonClient = redissonClient;
        this.s3Client = s3Client;
        // Shared Paged Swap File บน NVMe storage บล็อกละ 4KB เติบโตครั้งละ 1024 บล็อก
        this.sharedSwapFile = new JRSwapFile("/tmp/jasper_swap", 4096, 1024);
    }

    @KafkaListener(topics = "report.generation.tasks", groupId = "report-worker-cluster", concurrency = "4")
    public void processReportGenerationTask(ReportTaskMessage task) throws Exception {
        // 1. Idempotency Lock: ป้องกันการประมวลผลซ้ำเมื่อเกิด Rebalance หรือ Retry
        RLock lock = redissonClient.getLock("lock:report:job:" + task.jobId());
        boolean acquired = lock.tryLock(5, 600, TimeUnit.SECONDS);
        if (!acquired) {
            // มี Worker ตัวอื่นกำลังประมวลผลงานนี้อยู่แล้ว ข้ามทันที
            return;
        }

        // 2. ควบคุม Heap ด้วย Virtualizer: ค้างใน RAM ไม่เกิน 100 หน้า ส่วนเกินลงดิสก์
        JRSwapFileVirtualizer virtualizer = new JRSwapFileVirtualizer(100, sharedSwapFile, false);
        File tempExportFile = File.createTempFile("async_report_" + task.jobId(), ".pdf");

        try {
            task.params().put(JRParameter.REPORT_VIRTUALIZER, virtualizer);

            // 3. โหลด Pre-compiled Template จาก Classpath
            InputStream templateStream = getClass().getResourceAsStream("/reports/" + task.reportName() + ".jasper");
            JasperReport jasperReport = (JasperReport) net.sf.jasperreports.engine.util.JRLoader.loadObject(templateStream);

            // 4. Fill phase: ปลอดภัยจาก OOM แม้รายงานมีขนาดนับล้านแถว
            JRDataSource dataSource = fetchDataSourceForTask(task);
            JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, task.params(), dataSource);

            // 5. Export phase ไปป์ลง Temporary File ก่อนส่งขึ้น Cloud
            try (OutputStream fileOut = new BufferedOutputStream(new FileOutputStream(tempExportFile))) {
                JRPdfExporter exporter = new JRPdfExporter();
                exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
                exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(fileOut));
                exporter.exportReport();
            }

            // 6. อัปโหลดผลลัพธ์ตรงเข้าสู่ S3 / MinIO
            s3Client.putObject(
                PutObjectRequest.builder().bucket(task.s3Bucket()).key(task.s3Key()).contentType("application/pdf").build(),
                RequestBody.fromFile(tempExportFile)
            );

            // 7. แจ้งเตือนผลลัพธ์ความสำเร็จไปยัง Notification Dispatcher
            notifyJobCompletion(task.jobId(), task.s3Key());

        } finally {
            // สำคัญที่สุด: คืนบล็อกดิสก์ใน Swap File และลบไฟล์ชั่วคราวเสมอ
            virtualizer.cleanup();
            Files.deleteIfExists(tempExportFile.toPath());
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private JRDataSource fetchDataSourceForTask(ReportTaskMessage task) {
        // เชื่อมต่อไปยัง Data Lake / Read-Replica DB
        return new JREmptyDataSource(500000); 
    }

    private void notifyJobCompletion(String jobId, String s3Key) {
        // ส่งข้อความแจ้งเตือนผ่าน SSE หรือ WebSocket
    }
}

Enterprise Omnibus Settlement Ledger (100% Unified Element Model 7.0.x)

async_settlement_ledger.jrxml (Unified Element Model 7.0.x Standard)
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
              name="AsyncOmnibusSettlementLedger"
              pageWidth="595" pageHeight="842"
              columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
              uuid="e8f1c2d3-b4a5-6789-0123-abcdef456789">

    <parameter name="TASK_ID" class="java.lang.String"/>
    <parameter name="EXECUTION_CLUSTER" class="java.lang.String"/>

    <field name="transactionRef" class="java.lang.String"/>
    <field name="clearingMember" class="java.lang.String"/>
    <field name="settlementAmount" class="java.math.BigDecimal"/>
    <field name="executionStatus" class="java.lang.String"/>

    <title height="55">
        <element kind="staticText" x="0" y="5" width="555" height="25">
            <textElement textAlignment="Center" verticalAlignment="Middle">
                <font fontName="Sarabun" size="16" isBold="true"/>
            </textElement>
            <text><![CDATA[รายงานสรุปการชำระราคาแบบกระจายศูนย์ (Omnibus Settlement Run)]]></text>
        </element>
        <element kind="textField" x="0" y="30" width="555" height="20">
            <textElement textAlignment="Center">
                <font fontName="Sarabun" size="10"/>
            </textElement>
            <expression><![CDATA["Job Ticket: " + $P{TASK_ID} + " | Worker Pod Cluster: " + $P{EXECUTION_CLUSTER}]]></expression>
        </element>
    </title>

    <columnHeader height="20">
        <element kind="staticText" x="0" y="0" width="130" height="20">
            <textElement><font fontName="Sarabun" isBold="true"/></textElement>
            <text><![CDATA[TRANSACTION REF]]></text>
        </element>
        <element kind="staticText" x="130" y="0" width="190" height="20">
            <textElement><font fontName="Sarabun" isBold="true"/></textElement>
            <text><![CDATA[CLEARING MEMBER]]></text>
        </element>
        <element kind="staticText" x="320" y="0" width="135" height="20">
            <textElement textAlignment="Right"><font fontName="Sarabun" isBold="true"/></textElement>
            <text><![CDATA[SETTLED VALUE (USD)]]></text>
        </element>
        <element kind="staticText" x="455" y="0" width="100" height="20">
            <textElement textAlignment="Center"><font fontName="Sarabun" isBold="true"/></textElement>
            <text><![CDATA[STATUS]]></text>
        </element>
    </columnHeader>

    <detail>
        <band height="20">
            <element kind="textField" x="0" y="0" width="130" height="20">
                <expression><![CDATA[$F{transactionRef}]]></expression>
            </element>
            <element kind="textField" x="130" y="0" width="190" height="20">
                <expression><![CDATA[$F{clearingMember}]]></expression>
            </element>
            <element kind="textField" x="320" y="0" width="135" height="20" pattern="#,##0.00">
                <textElement textAlignment="Right"/>
                <expression><![CDATA[$F{settlementAmount}]]></expression>
            </element>
            <element kind="textField" x="455" y="0" width="100" height="20">
                <textElement textAlignment="Center"/>
                <expression><![CDATA[$F{executionStatus}]]></expression>
            </element>
        </band>
    </detail>

    <pageFooter height="20">
        <element kind="textField" x="0" y="0" width="555" height="20">
            <textElement textAlignment="Center"><font size="8"/></textElement>
            <expression><![CDATA["หน้า " + $V{PAGE_NUMBER} + " (จัดเก็บลง AWS S3 สำเร็จ)"]]></expression>
        </element>
    </pageFooter>
</jasperReport>
STEP 3

Interactive Architecture Workspace (Split-View Studio)

Lab 5.3.1: Asynchronous Kafka Worker & Task Isolation

Target: Distributed Decoupling

โจทย์สถาปัตยกรรม: พัฒนา Asynchronous Consumer Service ที่ประมวลผลรายงานขนาดใหญ่โดยไม่บล็อก Web API และไม่ทำให้เกิด Duplicate Execution:

  • ต้องใช้ Distributed Lock (Redisson tryLock) เพื่อรับประกัน Idempotent execution 100%
  • ต้องฉีด JRSwapFileVirtualizer เข้าใน REPORT_VIRTUALIZER ก่อนเรียก fillReport()
  • ต้องมีกลไก virtualizer.cleanup() และการปลดล็อกในบล็อก finally เสมอ
  • โครงสร้างของ Template ต้องเป็น Unified Element Model (<element kind="...">) 100%
Expected High-Throughput Asynchronous Commercial Artifact
OMNIBUS CLEARING RUN (JOB TICKET: ASYNC-2026-X991) OFFLOADED TO S3 BUCKET
TRANSACTION REF CLEARING MEMBER SETTLED VALUE (USD) STATUS
TX-OMNI-9901 Barclays Capital Services $24,500,000.00 SETTLED
TX-OMNI-9902 BNP Paribas Global Securities $18,200,400.00 SETTLED
Engine: Kafka Consumer Worker Cluster (K8s Pod 04) Artifact URL: https://s3.ap-southeast-1.amazonaws.com/...
⚠️ Crash Simulator (Asynchronous Infrastructure Hazards)

ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production ในระบบสร้างรายงานแบบกระจายศูนย์:

Asynchronous Architecture Linter
Awaiting Validation
Click "Verify Architecture" to scan Worker safety and S3 compliance.
ARTIFACT PROMPTS

Production Image Specification Prompts (Visual Acceptance Standard)

Asynchronous Pipeline Architecture Standard
1. Architectural Flowchart (Vector)

"2D Vector architectural flowchart, modern dark slate background #0b0f19. JasperReports 7.0.3+ Asynchronous Decoupled Worker Pipeline: Client browser submitting report generation request to Spring Boot REST Gateway, Gateway publishing Job Ticket to Kafka cluster, returning HTTP 202 Accepted. Decoupled Kubernetes Report Worker Pod acquiring Redisson distributed lock, executing JRVerticalFiller with JRSwapFileVirtualizer, streaming output PDF directly to AWS S3/MinIO bucket via multipart upload, dispatching Server-Sent Events (SSE) notification to frontend, neon cyan (#06b6d4) and emerald (#10b981) glowing data streams, crisp typography, 8k resolution."

2. Lab 5.3.1 Output (Omnibus Clearing Ledger)

"Realistic high-resolution commercial PDF A4 portrait financial statement representing an omnibus clearing report generated asynchronously. Pristine white paper background, crisp corporate header 'CENTRAL CLEARING HOUSE - ASYNC OMNIBUS BATCH RUN', execution metadata 'Job Ticket: ASYNC-2026-X991 • Cluster Pod: Worker-04', clean tabular grid with columns TRANSACTION REF, CLEARING MEMBER, SETTLED VALUE (USD), and STATUS. Exact right-aligned currency figures like '$24,500,000.00', elegant typography, subtle green verified badges, perfectly straight layout, zero blueprint outlines, zero technical bounding boxes, enterprise banking production standard."

3. Lab 5.3.2 Output (S3 Cloud Telemetry Document)

"Commercial financial statement detail view focusing on the asynchronous execution telemetry. Bottom footer displaying 'Cloud S3 Offloaded Artifact • SHA-256 Storage Hash: 8fa4c90b • Processed Pages: 50,000 • Execution Time: 42.1s • Presigned URL Active'. Crisp monochrome DataMatrix barcode for transaction authenticity verification at bottom right, razor-sharp typography, flawless contrast, authentic bond paper texture, photorealistic document lighting."

EXAM ARENA

15 Architectural Mastery Quizzes

เจาะลึก Kafka/RabbitMQ Worker Pools, Idempotency Locks, S3 Multipart Offloading, และ Zero-Leak Lifecycle