[QA Brain MVP] T3: Atomic Transactional Idempotency Engine for Brain Mutations
สรุปงาน (Summary)
พัฒนา Transactional Idempotency Engine สำหรับคำสั่งสร้างและแก้ไขใน QA Brain (happyqa_brain_record_gotcha, POST /api/v1/workspaces/:wsId/brain/create และคำสั่งที่เกี่ยวข้อง) เพื่อรองรับการทำงานของ AI Agent หลายตัวพร้อมกัน ป้องกันการบันทึกข้อมูลซ้ำซ้อนจาก Network Retries และรับประกันการ Replay ข้อมูลที่ปลอดภัยต่อสิทธิ์การเข้าถึง โดยกำหนดอัลกอริทึมจัดการ Race Condition อย่างเป็นรูปธรรมเพื่อป้องกัน Unique Constraint Error หลุดไปหาผู้เรียก
อ้างอิงข้อกำหนดสถาปัตยกรรม: Canonical RFC v4.1 (Section 2.7, 3.2, 5.4)
ลำดับการทำงาน (Dependency): ขึ้นต่อกับ T1 (Data Model) และ T2 (Sanitizer)
ขอบเขตและงานที่ต้องทำ (Scope of Work)
1. โครงสร้างและการจัดการ Idempotency Record
- ใช้ตาราง
QaBrainIdempotencyKeyซึ่งมี Unique Key:[workspaceId, principalType, principalId, operation, key] - กำหนดอายุ Key (TTL) เป็น 24 ชั่วโมง นับจากเวลาที่สร้าง (
expiresAt = now() + 24 hours)
2. ล็อคอัลกอริทึมจัดการ Concurrent Idempotency (Atomic Resolution Algorithm)
เพื่อแก้ปัญหา Race Condition ระหว่างคำขอที่ยิงเข้ามาพร้อมกัน (เช่น 10 คำขอพร้อมกันด้วย Key เดียวกัน) ห้ามใช้วิธี "ค้นหาก่อน แล้วสร้างทีหลัง" (Check-then-act) แต่ให้ใช้กระบวนการระดับฐานข้อมูลที่ป้องกัน Race Condition 100%:
┌───────────────────────────┐
│ คำขอใหม่พร้อม IdemKey │
└─────────────┬─────────────┘
│
┌──────────────────────────▼──────────────────────────┐
│ 1. Transactional Lock │
│ - ใช้ pg_advisory_xact_lock ด้วย Hash ของ Key │
│ หรือ INSERT ... ON CONFLICT DO NOTHING │
│ ตามด้วย SELECT ... FOR UPDATE │
└──────────────────────────┬──────────────────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
[พบแถว Idempotency เดิม] [ไม่พบแถว / ผู้ชนะแรก]
│ │
┌──────────────┴──────────────┐ │
│ 2. ตรวจสอบสถานะแถวเดิม │ │
│ - แถวกำลังประมวลผล (In-flight)│ │
│ (Lock จะบล็อกรอจนกว่า │ │
│ Tx แรก commit) │ │
│ - เมื่อ Tx แรก commit แล้ว: │ │
│ Reload ข้อมูลแถวล่าสุด │ │
└──────────────┬──────────────┘ │
│ │
┌──────────────┴──────────────┐ │
│ 3. ตรวจความถูกต้องของคำขอ │ │
│ - Policy Ver ตรงกันไหม │ │
│ - Fingerprint ตรงกันไหม │ │
│ (ไม่ตรง -> 409 CONFLICT) │ │
│ - Re-check สิทธิ์ผู้เรียก │ │
│ (ไม่มีสิทธิ์ -> 403 FORBIDDEN)│ │
└──────────────┬──────────────┘ │
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ 4. ส่งผล Replay แคชเดิม │ │ 5. ประมวลผลสร้าง Gotcha │
│ (replayed: true) │ │ - บันทึกแถว Gotcha │
│ │ │ - เขียน responseSnap │
└─────────────────────────┘ │ - Commit Transaction │
│ (replayed: false) │
└─────────────────────────┘
รายละเอียดขั้นตอนในโค้ด:
-
Advisory Transactional Lock:
- คำนวณ 64-bit lock key จาก
brain_idem:${workspaceId}:${principalType}:${principalId}:${operation}:${key} - รัน
SELECT pg_advisory_xact_lock(:lockKey)ภายใน Prisma Interactive Transaction (tx.$executeRaw) เพื่อให้ทุกคำขอที่ใช้ Key เดียวกันเข้าคิวเรียงหนึ่งอย่างสมบูรณ์
- คำนวณ 64-bit lock key จาก
-
ค้นหาแถวที่มีอยู่แล้ว:
- รัน
tx.qaBrainIdempotencyKey.findUnique(...)
- รัน
-
กรณีพบแถวเดิม (Replay Resolution):
- ตรวจสอบ
sanitizerPolicyVer: ถ้านโยบายเปลี่ยน ตอบ 409 - ตรวจสอบ
requestFingerprint: เทียบกับ SHA-256 ของ Canonical Sanitized Payload ปัจจุบัน หากไม่ตรงกัน $\rightarrow$ ตอบกลับ HTTP 409IDEMPOTENCY_CONFLICT -
Replay Authorization Re-check: ตรวจสอบสิทธิ์ปัจจุบันของผู้เรียกกับ Project ID ของ Gotcha นั้น หากสิทธิ์ถูกเพิกถอนไปแล้ว ตอบกลับ HTTP 403
FORBIDDENทันที (ห้ามส่งข้อมูลจากแคชกลับ) - หากผ่านทุกเงื่อนไข $\rightarrow$ ส่งคืน
responseSnapshotเดิมพร้อมระบุ"replayed": true
- ตรวจสอบ
-
กรณีไม่พบแถว (First Winner):
- สร้าง Gotcha ในฐานข้อมูล (สถานะ
DRAFT) - บันทึก Audit Log ผ่าน
appendAudit - บันทึกแถว
QaBrainIdempotencyKeyพร้อมแนบresponseSnapshotและexpiresAt = now() + 24 hours - Commit Transaction $\rightarrow$ ส่งผลลัพธ์กลับด้วย
"replayed": false
- สร้าง Gotcha ในฐานข้อมูล (สถานะ
3. ระบบจัดการและล้าง Key ที่หมดอายุ (Key Expiration Pruning)
- สร้างฟังก์ชัน Pruning หรือ scheduled task สำหรับลบแถวที่
expiresAt < now()ออกจากตารางเพื่อไม่ให้ฐานข้อมูลบวม
เกณฑ์การตรวจรับ (Acceptance Criteria)
-
Concurrent Execution Test: ยิงคำขอ
record_gotchaพร้อมกัน 10 คำขอด้วย Idempotency Key เดียวกันแบบขนานกัน (Parallel Requests) ต้องสร้าง Gotcha ในฐานข้อมูลเพียง 1 แถวเท่านั้น และอีก 9 คำขอต้องได้รับผล Replay เดิม ("replayed": true) โดยไม่มีข้อผิดพลาด Unique Constraint Violation หลุดออกมาเด็ดขาด -
Conflict Detection Test: ส่งคำขอเดิมซ้ำด้วย Key เดิมแต่เปลี่ยนเนื้อหา Payload ระบบต้องตอบกลับ
IDEMPOTENCY_CONFLICT(HTTP 409) -
Authorization Revocation Immunity Test: เมื่อผู้ใช้บันทึก Gotcha สำเร็จ แล้วถูกเพิกถอนสิทธิ์ออกจากโปรเจกต์ การส่งคำขอเดิมซ้ำด้วย Idempotency Key เดิมต้องได้ผลลัพธ์
FORBIDDENไม่ส่งข้อมูลเดิมกลับไป