diff --git a/BMA.EHR.CheckInConsumer/CHANGELOG-checkin-speedup.md b/BMA.EHR.CheckInConsumer/CHANGELOG-checkin-speedup.md new file mode 100644 index 00000000..7b20dd1b --- /dev/null +++ b/BMA.EHR.CheckInConsumer/CHANGELOG-checkin-speedup.md @@ -0,0 +1,83 @@ +# สรุปการปรับปรุงระบบลงเวลา (CheckInConsumer) + +วันที่แก้ไข: 23 มิถุนายน 2026 + +--- + +## ปัญหาเดิม + +ตอนที่พนักงานลงเวลาพร้อมกันจำนวนมาก (ประมาณ 2,000 รายการ) ระบบประมวลผลทีละรายการ ทำให้ต้องรอคิวนานถึง **22 นาที** กว่าจะประมวลผลเสร็จทั้งหมด + +เปรียบเทียบเหมือน **โต๊ะบัญชี 1 คน รับคิวทีละคน** ทั้งที่มีคนรอ 2,000 คน → คิวยาวมาก + +--- + +## วิธีที่แก้ (เข้าใจง่าย ๆ) + +### 1. เพิ่มคนช่วยประมวลผลพร้อมกัน (Concurrency) +- **ก่อน:** ประมวลผลทีละรายการ (เหมือนมีโต๊ะบัญชี 1 โต๊ะ) +- **หลัง:** ประมวลผลพร้อมกันได้สูงสุด **5 รายการ** (เหมือนเปิดโต๊ะบัญชี 5 โต๊ะ) + +> ผลที่ได้: เวลารอคิวลดลงจาก **22 นาที → ประมาณ 4–5 นาที** + +### 2. จัดคิวล่วงหน้าให้ RabbitMQ (Prefetch) +- **ก่อน:** ระบบดึงข้อมูลมาทีละชิ้น ทำให้เสียเวลารอส่งต่อ +- **หลัง:** ระบบดึงข้อมูลมาเป็นชุด ๆ ละ 20 ชิ้นไว้เตรียมพร้อม → ลดเวลารอระหว่างรายการ + +### 3. ลดเวลารอเมื่อ API มีปัญหา (Timeout) +- **ก่อน:** ถ้า API ค้าง ระบบจะรอนานถึง **5 นาที** ต่อรายการ +- **หลัง:** ลดเหลือ **1 นาที** → รายการที่มีปัญหาจะถูกปฏิเสธเร็วขึ้น ไม่ทำให้คิวค้าง + +### 4. ปรับปรุงการเชื่อมต่อ HTTP +- เปลี่ยนระบบเชื่อมต่อให้รองรับการส่งคำขอหลายรายการพร้อมกันโดยไม่สะดุด + +--- + +## ตัวเลขเปรียบเทียบ + +| รายการ | ก่อนแก้ | หลังแก้ | +|---|---|---| +| จำนวนรายการที่ประมวลผลพร้อมกัน | 1 | 5 | +| เวลารอคิวสูงสุด (2,000 รายการ) | ~22 นาที | ~4–5 นาที | +| เวลารอเมื่อ API มีปัญหา | 5 นาที | 1 นาที | + +--- + +## ไฟล์ที่แก้ไข + +1. **`Program.cs`** — โค้ดหลักของตัวประมวลผลคิว +2. **`appsettings.json`** — ไฟล์ตั้งค่าระบบ + +--- + +## วิธีปรับความเร็วเพิ่มเติม (ไม่ต้องเขียนโค้ดใหม่) + +ถ้าหลังทดสอบแล้วเห็นว่าระบบรับได้ และอยากให้เร็วขึ้นอีก ให้แก้ไขไฟล์ `appsettings.json` แล้ว restart โปรแกรมได้เลย: + +```json +{ + "MaxConcurrency": 10, ← เพิ่มจาก 5 เป็น 10 (ประมวลผลพร้อมกัน 10 รายการ) + "PrefetchCount": 50, ← ควรตั้งเป็น ประมาณ MaxConcurrency × 2 ขึ้นไป + "HttpTimeoutSeconds": 60 ← เวลารอ API วินาที +} +``` + +**ค่าที่ใช้และผลที่คาดการณ์:** +- `MaxConcurrency = 5` → ใช้เวลา ~4–5 นาที (ค่าเริ่มต้นปลอดภัย) +- `MaxConcurrency = 10` → ใช้เวลา ~2–3 นาที +- `MaxConcurrency = 20` → ใช้เวลา ~1–2 นาที (ต้องตรวจสอบว่าระบบหลังบ้านรับไหวก่อน) + +--- + +## ข้อควรระวัง / คำแนะนำ + +1. **ควรทดสอบในระบบทดสอบก่อน** โดยดูว่า + - ไม่มี error ในระบบหลัก (API) + - ฐานข้อมูลไม่ช้าผิดปกติ + - ไม่พบปัญหาลงเวลาซ้ำซ้อน + +2. ถ้าพบปัญหา เช่น + - มี error ใน API → **ลด** `MaxConcurrency` เหลือ 2 หรือ 3 + - ลงเวลาซ้ำ → แจ้งทีมเทคนิคเพื่อแก้ฝั่ง API เพิ่มเติม + +3. **ค่า `MaxConcurrency = 5` เป็นค่าปลอดภัย** เพราะระบบ API ด้านหลังยังมีข้อจำกัดอยู่บางส่วน หากต้องการเพิ่มให้สูงกว่านี้ (เช่น 20–50) ควรปรึกษาทีมเทคนิคเพื่อปรับปรุงฝั่ง API ก่อน diff --git a/BMA.EHR.CheckInConsumer/Program.cs b/BMA.EHR.CheckInConsumer/Program.cs index 95dac001..a0ff686c 100644 --- a/BMA.EHR.CheckInConsumer/Program.cs +++ b/BMA.EHR.CheckInConsumer/Program.cs @@ -18,6 +18,13 @@ var user = configuration["Rabbit:User"] ?? ""; var pass = configuration["Rabbit:Password"] ?? ""; var queue = configuration["Rabbit:Queue"] ?? "basic-queue"; +// Concurrency & prefetch (configurable via appsettings.json) +var maxConcurrency = int.TryParse(configuration["MaxConcurrency"], out var c) && c > 0 ? c : 5; +var prefetchCount = ushort.TryParse(configuration["PrefetchCount"], out var p) && p > 0 ? p : (ushort)20; +var httpTimeoutSec = int.TryParse(configuration["HttpTimeoutSeconds"], out var t) && t > 0 ? t : 60; + +WriteToConsole($"Config -> MaxConcurrency: {maxConcurrency}, PrefetchCount: {prefetchCount}, HttpTimeout: {httpTimeoutSec}s"); + // create connection var factory = new ConnectionFactory() { @@ -32,39 +39,61 @@ using var channel = connection.CreateModel(); channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false, arguments: null); -// Create a SINGLE static HttpClient instance to prevent socket exhaustion -using var httpClient = new HttpClient(); -httpClient.Timeout = TimeSpan.FromSeconds(300); // 5 นาที +// Prefetch: RabbitMQ จะส่ง message หลายตัวมาที่ consumer พร้อมกัน (ลด network round-trip) +channel.BasicQos(prefetchSize: 0, prefetchCount: prefetchCount, global: false); + +// HttpClient แบบ SocketsHttpHandler พร้อม connection pooling รองรับ concurrent requests +var socketsHandler = new SocketsHttpHandler +{ + MaxConnectionsPerServer = maxConcurrency * 2, + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30) +}; +using var httpClient = new HttpClient(socketsHandler); +httpClient.Timeout = TimeSpan.FromSeconds(httpTimeoutSec); + +// SemaphoreSlim คุมจำนวน message ที่ประมวลผลพร้อมกัน (เนื่องจาก API มีข้อจำกัดเรื่อง concurrency) +using var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency); var consumer = new AsyncEventingBasicConsumer(channel); -consumer.Received += async (model, ea) => +consumer.Received += (model, ea) => { - try + // รอ semaphore ก่อนเริ่มประมวลผล + semaphore.WaitAsync().ContinueWith(async _ => { - var body = ea.Body.ToArray(); - var message = Encoding.UTF8.GetString(body); - - WriteToConsole($"Received message: {message}"); - - var success = await CallRestApi(message, httpClient, configuration); - - if (success) + try { - channel.BasicAck(ea.DeliveryTag, multiple: false); - WriteToConsole("Message processed successfully"); + var body = ea.Body.ToArray(); + var message = Encoding.UTF8.GetString(body); + + WriteToConsole($"Received message: {message}"); + + var success = await CallRestApi(message, httpClient, configuration); + + if (success) + { + channel.BasicAck(ea.DeliveryTag, multiple: false); + WriteToConsole("Message processed successfully"); + } + else + { + channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); + WriteToConsole("Message processing failed - message rejected"); + } } - else + catch (Exception ex) { + WriteToConsole($"Error processing message: {ex.Message}"); channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); - WriteToConsole("Message processing failed - message rejected"); } - } - catch (Exception ex) - { - WriteToConsole($"Error processing message: {ex.Message}"); - channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); - } + finally + { + semaphore.Release(); + } + }, TaskScheduler.Default).ConfigureAwait(false); + + return Task.CompletedTask; }; channel.BasicConsume(queue: queue, autoAck: false, consumer: consumer); diff --git a/BMA.EHR.CheckInConsumer/appsettings.json b/BMA.EHR.CheckInConsumer/appsettings.json index 76f86c86..1fbba85b 100644 --- a/BMA.EHR.CheckInConsumer/appsettings.json +++ b/BMA.EHR.CheckInConsumer/appsettings.json @@ -5,5 +5,8 @@ "Password": "12345678", "Queue": "hrms-checkin-queue-dev" }, - "API": "https://localhost:7283/api/v1" + "API": "https://localhost:7283/api/v1", + "MaxConcurrency": 5, + "PrefetchCount": 20, + "HttpTimeoutSeconds": 60 }