เมื่อวานผมสร้าง WordPress Plugin สำหรับ rewrite เนื้อหาหนังกว่า 5,900 เรื่องด้วย Gemini AI
วันนี้มันค้าง
ไม่ใช่ค้างแบบธรรมดานะ แต่ค้างแบบ “Completed: 6” แล้วหยุดนิ่งอยู่งั้น
ผมเลือกหนังไป 10 เรื่อง กด Generate แล้วรอ… รอ… รอ…
ค้างที่ 6 เรื่องตลอดเลย
ปัญหาคืออะไร?
Plugin ผมทำงานแบบนี้:
- User เลือกหนังที่ต้องการ 10 เรื่อง
- กด Generate
- Plugin ดึงรายการเข้า Queue
- Background Process ค่อยๆ ทำทีละเรื่อง
- เสร็จแล้ว User มา Preview และ Save
ฟังดูดีใช่ไหม?
แต่มันมี time limit
PHP บน Cloudways มี max_execution_time = 60 วินาที
ผมตั้งให้ตัวเอง break ที่ 55 วินาที เพื่อความปลอดภัย
$max_exec_time = 55;
foreach ($items as $item) {
if ((time() - $start_time) > $max_exec_time) {
break; // หยุด! หมดเวลาแล้ว
}
$this->process_item($item);
}
ปัญหาคือ:
ก่อน break ผมได้ “claim” รายการไว้แล้ว 10 รายการ
แต่ทำได้จริงแค่ 5-6 เรื่อง
อีก 4-5 เรื่องที่เหลือ… ค้างอยู่ใน status “processing” ตลอดกาล
Bug #1: รายการหนังติดค้างใน “Processing”
สถานการณ์
- เลือกหนัง 10 เรื่อง กด Generate
- Queue claim รายการทั้ง 10 → status = ‘processing’
- ประมวลผลได้ 5 เรื่อง → status = ‘preview’
- หมดเวลา 55 วินาที →
break - อีก 5 เรื่องที่เหลือ? → ยังคง status = ‘processing’
Batch ถัดไปมาทำงาน มันหาแต่ status = 'pending' เท่านั้น
แต่ 5 เรื่องที่เหลือเป็น 'processing' ไม่ใช่ 'pending'
มันถูกข้ามไป ตลอดไป
ความพยายามแรก (v1.0.13): Reset ตาม Time
ผมคิดว่า “งั้นก็ reset รายการที่ค้างนานเกิน 2 นาทีสิ!”
// Reset รายการที่ค้างอยู่ใน 'processing' นานเกิน 2 นาที
private function reset_stale_processing_items($batch_id, $threshold_sec = 120) {
global $wpdb;
$cutoff_time = gmdate('Y-m-d H:i:s', time() - $threshold_sec);
$wpdb->query($wpdb->prepare(
"UPDATE {$this->table_name}
SET status = 'pending'
WHERE batch_id = %s
AND status = 'processing'
AND updated_at < %s",
$batch_id,
$cutoff_time
));
}
ดูดีใช่ไหม?
Deploy v1.0.13 แล้วทดสอบ
ยังค้างอยู่ที่ 6 เรื่องเหมือนเดิม…
ทำไม v1.0.13 ไม่ work?
นั่งคิดอยู่นาน แล้วก็เข้าใจ
Threshold 2 นาที ไม่มีทางถูก trigger
เพราะอะไร?
- Batch แรกรัน → ค้าง 5 เรื่อง
- รอ 5 วินาที → Batch ถัดไปรัน
- Batch ถัดไปเช็ค: “มีรายการที่ค้างเกิน 2 นาทีไหม?”
- คำตอบ: ไม่มี (มันค้างแค่ประมาณ 1 นาที)
- Batch ถัดไปข้ามรายการนั้นไป
- รอ 5 วินาที → Batch ถัดไปรัน
- เช็คอีก: ค้างเกิน 2 นาทีไหม? ยังไม่เกิน
- …
- รายการค้างตลอดไป เพราะไม่มี batch ไหนรออยู่ถึง 2 นาที
บทเรียน: Time-based threshold ไม่ work ถ้า batch interval สั้นกว่า threshold
Bug #2: Race Condition ซ้อนทับ
ระหว่าง debug ผมเจออีกปัญหานึง
Debug log แสดงว่า หนังเรื่องเดียวถูก process ซ้ำ 2-3 รอบ
[19:49:18] Processing item 45 (post 21408)
[19:49:18] Processing item 45 (post 21408) // อีกแล้ว!
ปัญหา:
เมื่อ progress ดูเหมือน “ค้าง” frontend จะ trigger “manual processing”
แต่ background cron ก็ยังรันอยู่
ทั้งสองตัวแย่งรายการกัน
วิธีแก้: เพิ่ม lock mechanism
$lock_key = 'gmr_processing_' . $batch_id;
$lock_acquired = get_transient($lock_key);
if ($lock_acquired) {
$this->debug_log('มี process อื่นกำลังทำอยู่ ข้ามไปก่อน');
return;
}
// ล็อคไว้
set_transient($lock_key, time(), 120);
บทเรียน: Background jobs + Frontend triggers = ต้องมี lock เสมอ
วิธีแก้ที่ Work จริงๆ (v1.0.14): Reset ทันที
หลังจากลองผิดลองถูก ผมเข้าใจแล้วว่า:
ปัญหาคือ timing
ผมรู้ว่ารายการไหนถูก claim แต่ยังไม่ได้ process
ทำไมต้องรอ 2 นาทีถึงจะ reset ล่ะ?
Reset ทันทีหลัง loop break สิ!
$processed_ids = array(); // เก็บรายการที่ process จริง
$all_claimed_ids = wp_list_pluck($items, 'id'); // ทั้งหมดที่ claim ไว้
foreach ($items as $item) {
if ((time() - $start_time) > $max_exec_time) {
$this->debug_log('หมดเวลา ออกจาก loop');
break;
}
$this->process_item($item);
$processed_ids[] = $item->id; // เพิ่มเข้า list ที่ทำแล้ว
}
// หลัง loop break → reset ทันที!
$this->reset_unprocessed_items($all_claimed_ids, $processed_ids);
Function reset_unprocessed_items():
private function reset_unprocessed_items($all_claimed_ids, $processed_ids) {
global $wpdb;
// หารายการที่ claim ไว้แต่ยังไม่ได้ process
$unprocessed_ids = array_diff($all_claimed_ids, $processed_ids);
if (empty($unprocessed_ids)) {
return 0;
}
$this->debug_log('รายการที่ยังไม่ได้ทำ: ' . implode(', ', $unprocessed_ids));
// Reset กลับเป็น 'pending' ทันที
$placeholders = implode(', ', array_fill(0, count($unprocessed_ids), '%d'));
$wpdb->query($wpdb->prepare(
"UPDATE {$this->table_name}
SET status = 'pending'
WHERE id IN ({$placeholders})
AND status = 'processing'",
...$unprocessed_ids
));
return count($unprocessed_ids);
}
ผลลัพธ์ใน Debug Log
[19:49:57] Max exec time reached, breaking
[19:49:57] Unprocessed item IDs: 48, 49, 50, 51, 52
[19:49:57] Reset 5 unprocessed items back to pending ← ทำงานแล้ว!
[19:49:57] Remaining items: 5, scheduling next batch
[19:50:01] Claimed 5 items with token: 376a1995...
[19:50:01] Found 5 items to process
...
[19:51:13] All items processed for this batch
หนังทั้ง 10 เรื่องผ่านหมด ไม่ค้างแม้แต่เรื่องเดียว
ความแตกต่างระหว่าง v1.0.13 กับ v1.0.14
| เรื่อง | v1.0.13 | v1.0.14 |
|---|---|---|
| วิธีตรวจจับ | Time-based (ค้างเกิน 2 นาที) | เทียบ array (claimed vs processed) |
| Timing | รอ batch ถัดไปมาเช็ค | Reset ทันทีหลัง loop break |
| ความน่าเชื่อถือ | ❌ ไม่ work ถ้า interval < threshold | ✅ Work เสมอ |
| แก้ที่ไหน | แก้ที่ปลายเหตุ | แก้ที่ต้นเหตุ |
สิ่งที่เรียนรู้
1. Debug Log คือเพื่อนแท้
ถ้าไม่มี debug log ผมจะไม่รู้เลยว่า:
- รายการไหนถูก claim ไปแล้ว
- รายการไหนถูก process จริง
- Loop break ตรงไหน
- Batch ถัดไปเห็นรายการอะไรบ้าง
$this->debug_log("กำลัง process รายการ {$item->id} (post {$item->post_id})");
$this->debug_log("รายการ {$item->id} เสร็จแล้ว รวม: {$processed} รายการ");
$this->debug_log("หมดเวลา ออกจาก loop");
บทเรียน: เขียน log ให้เยอะตอน debug แล้วค่อยลบทีหลัง
2. Time-based Solutions มีจุดอ่อน
“รอ X วินาทีแล้ว reset” ฟังดูง่าย
แต่ถ้า system ไม่ได้รอนานขนาดนั้น มันไม่มีทาง trigger
บทเรียน: ถ้าเป็นไปได้ ใช้ event-based แทน time-based
3. Track State ให้ชัดเจน
v1.0.13 ไม่ได้ track ว่ารายการไหน process แล้ว รายการไหนยัง
v1.0.14 track อย่างชัดเจน:
$all_claimed_ids= ทุกรายการที่ claim ไว้$processed_ids= เฉพาะที่ process จริงarray_diff()= หาส่วนต่าง
บทเรียน: State ที่ชัดเจน = Debug ง่าย = แก้ง่าย
4. ทดสอบด้วย Load จริง
Bug นี้ไม่เคยเกิดตอนทดสอบด้วยหนังแค่ 2-3 เรื่อง
เพราะมัน process ทันภายใน 55 วินาที
แต่พอทดสอบด้วยหนัง 10 เรื่อง (ที่แต่ละเรื่องใช้เวลานาน) ถึงเจอปัญหา
บทเรียน: ทดสอบด้วย load ที่ใกล้เคียงกับของจริง
ผลลัพธ์สุดท้าย
- v1.0.14 deploy แล้วใช้งานได้
- ทดสอบหนัง 10 เรื่อง → ผ่านหมดไม่มีค้าง
- พร้อม process หนังกว่า 5,900 เรื่องจริง
- Token ที่ใช้: ประมาณ 21,000 tokens (~$0.06 USD)
สำหรับคนที่กำลังสร้าง Background Processing
- อย่าเชื่อ time-based thresholds มากเกินไป – คิดให้ดีว่า system มี timing เป็นยังไง
- Track state ให้ชัดเจน – รู้ว่าอะไร claim ไปแล้ว อะไร process จริงแล้ว
- Reset ทันทีที่รู้ว่ามีปัญหา – อย่ารอให้ batch ถัดไปมาแก้ให้
- เขียน log ให้เยอะตอน debug – แล้วค่อยลดลงทีหลัง
- ทดสอบด้วย scenario จริง – อย่าทดสอบแค่ happy path
Bug นี้สอนผมว่า…
วิธีแก้ที่ดูสมเหตุสมผล อาจไม่ work ในโลกจริง
และบางที solution ที่ดีที่สุดก็คือ วิธีที่ง่ายที่สุด
ไม่ต้อง fancy time-based recovery อะไรเลย
แค่ track ว่าทำอะไรไปแล้ว แล้ว reset ที่เหลือทันที
ถ้าคุณเจอ bug ที่ดูเหมือน “ค้าง” โดยไม่รู้สาเหตุ
ลองถามตัวเองว่า: “มันติดค้างอยู่ที่ state ไหน? และทำไมมันไม่หลุดออกมา?”
คำตอบมักจะซ่อนอยู่ใน timing

