Skip to main content

pg_notify และ Event Outbox

ภาพรวม

ระบบใช้ PostgreSQL เป็น event bus ชั้นแรก โดย trigger บนตารางสำคัญจะเรียก pg_notify(...) และฝั่ง worker-go มี pg-listener คอยฟังช่องสัญญาณเหล่านั้นแบบเรียลไทม์

ข้อจำกัดสำคัญคือ pg_notify ไม่มีการรับประกันการส่ง หาก listener หลุดการเชื่อมต่อในจังหวะที่ event ถูกยิงออกมา event นั้นจะหายไปทันทีโดยไม่มีร่องรอย ระบบจึงเพิ่มตาราง event_outbox ให้ทุก trigger เขียนสำเนาของ event ลงฐานข้อมูลควบคู่กันไปด้วย (รูปแบบ transactional outbox) listener ที่เพิ่งกลับมาออนไลน์จึงสามารถไล่อ่านย้อนหลัง (catch up) จากตารางนี้ได้

โครงสร้างข้อมูลหลัก

event_outbox

ตารางนี้ถูกสร้างผ่าน manual SQL เท่านั้น ไม่มีนิยามอยู่ใน Prisma

  • id BIGSERIAL, channel VARCHAR(100), payload JSONB
  • created_at, processed (ค่าเริ่มต้น false), processed_at
  • ใช้ partial index สองตัวที่แยกหน้าที่กันชัดเจน คือ (created_at) WHERE processed = FALSE สำหรับการ catch up และ (processed_at) WHERE processed = TRUE สำหรับงานลบข้อมูลเก่า

ช่องสัญญาณและ trigger ที่ยิง

ช่องสัญญาณตารางต้นทางTriggerฟังก์ชัน
attribute_changedline_usertrg_line_user_attribute_change (AFTER UPDATE)notify_attribute_change()
form_submittedform_submissionform_submission_notify (AFTER INSERT/UPDATE)notify_form_submitted()
campaign_clicktracking_logtracking_log_campaign_click_notify (AFTER INSERT)notify_campaign_click()
friend_track_event_insertedfriend_track_eventfriend_track_event_insert_notify (AFTER INSERT)notify_friend_track_event_insert()
booking_eventappointment.bookingtrg_booking_event_insert / trg_booking_event_updateappointment.notify_booking_event()

พฤติกรรมที่ควรทราบ

  • notify_attribute_change() ตรวจจับการเปลี่ยนแปลงทั้งในคอลัมน์คงที่ (display_name, firstname, lastname, email, mobile_no, language, follow) และคีย์ภายใน custom_attribute โดยส่งชื่อคีย์ในรูปแบบ custom.<ชื่อคีย์>
  • มีสวิตช์ปิด trigger ชั่วคราวในระดับ transaction ผ่าน current_setting('app.skip_triggers', true) = 'true' ใช้ระหว่างการนำเข้าข้อมูลจำนวนมาก เพื่อไม่ให้เกิด event นับหมื่นครั้ง วิธีนี้ปลอดภัยกับระบบ multi-tenant เพราะเป็นค่าระดับ session หรือ transaction ไม่ใช่ค่าระดับฐานข้อมูล
  • appointment.notify_booking_event() เขียน outbox แบบ best-effort คือดักจับ undefined_table แล้วข้ามไปเงียบ ๆ หากยังไม่มีตาราง outbox ในฐานข้อมูลนั้น

ไฟล์ที่เกี่ยวข้อง

  • manual-sql/5.sql — สร้าง event_outbox เขียนทับทุกฟังก์ชัน notify ให้บันทึกลง outbox และเพิ่ม trigger friend_track_event_insert_notify ที่หายไป (แก้ไขเมื่อ 24 ก.ค. 2026)
  • manual-sql/1.notify_attribute_change.sql, manual-sql/3.bank.sql — พัฒนาการของ trigger ฝั่ง attribute
  • manual-sql/2.notify_attribute_change.sqlnotify_form_submitted
  • manual-sql/4.sqlnotify_campaign_click
  • apps/appointment/migrations/004_add_booking_event_trigger.sql — ช่อง booking_event
  • manual-sql/0.select.sql — คำสั่งตรวจสอบ trigger ทั้งหมดจาก information_schema.triggers
  • schema-dumps/2026-07-24/triggers-summary.txt, schema-dumps/2026-07-24/notify-functions.txt — สรุปสถานะจริงบน preprod
  • schema-dumps/2026-07-24/schema.sql:1681event_outbox

จุดเชื่อมต่อกับ Service อื่น