Socket.IO ใช้ทำอะไรใน Frappe?
ถ้าไม่มี Socket.IO, ERPNext ก็ยังใช้งานได้ - แต่ประสบการณ์จะเปลี่ยนจาก "live" เป็น "refresh แล้วลุ้น" นี่คือสิ่งที่มันทำ:
- Notifications และ alerts -
msgprintจาก server-side code จะแสดงเป็น toast messages, progress bars ทำงานได้ตอน imports/exports และ background jobs สามารถรายงานสถานะกลับไปให้ user ได้ - Live document collaboration - เราจะเห็นว่า "X กำลังดูเอกสารนี้อยู่" (
doc_viewers) และ forms จะอัปเดตแบบ real-time เมื่อ user คนอื่นบันทึก record เดียวกัน - Auto-refreshing lists - list views อัปเดตอัตโนมัติเมื่อ records ถูกสร้าง, แก้ไข หรือลบ ไม่ต้อง reload หน้าเอง
- Background job feedback -
frappe.enqueue()jobs สามารถ push progress updates ไปยัง browser ได้ และ build processes (เช่น asset compilation) สามารถรายงานผลสำเร็จหรือล้มเหลว
ถ้า Socket.IO พัง สิ่งเหล่านี้จะไม่ทำงานเลย ผู้ใช้ต้อง refresh หน้าเองเพื่อดูการเปลี่ยนแปลง, progress bars จะค้างนิ่ง และไม่มีทางรู้ว่ามี user คนอื่นกำลังแก้เอกสารเดียวกันอยู่
ภาพรวมของระบบ
ระบบ Socket.io ของ ERPNext / Frappe มี 4 เซอร์วิสหลัก ที่ทำงานร่วมกัน การเข้าใจว่าแต่ละตัวเชื่อมต่อกันอย่างไรจะช่วยให้เรา debug ปัญหา real-time ได้ง่ายขึ้น
ไฟล์ที่เกี่ยวข้อง (ตัวหลัก):
frappe/frappe/public/js/frappe/socketio_client.js- Socket.io client ฝั่ง browserfrappe/socketio.js- Socket.io server ที่รันบน Node.jsfrappe/realtime/handlers.js- event handlers สำหรับ message ที่เข้ามาfrappe/realtime/middlewares/authenticate.js- authentication middleware สำหรับ socket connections
frappe/frappe/realtime.py- API ฝั่ง Python สำหรับ publish real-time events
ตรวจสอบว่าระบบทั้งหมดทำงานได้
วิธีที่เร็วที่สุดในการตรวจสอบว่าระบบ real-time ทำงานได้ครบ loop คือ publish test message เข้าไป เปิด bench console แล้วรัน:
frappe.publish_realtime(event="msgprint", message={"message": "Hello!"}, user="Administrator")
ถ้าทุกอย่างทำงานปกติ เราจะเห็นข้อความ "Hello!" แสดงขึ้นมาที่หน้า frontend

การทดสอบง่าย ๆ นี้ต้องการหลายอย่างทำงานพร้อมกัน
สิ่งที่ดูเหมือนแค่ส่ง message ธรรมดา จริง ๆ แล้วผ่านหลายเซอร์วิส ถ้า message ไม่แสดงขึ้นมา ปัญหาอาจเกิดที่จุดไหนก็ได้ใน chain มาดูกันว่าเบื้องหลังเกิดอะไรขึ้นบ้าง
Initial Connect
ตอนที่ connect ครั้งแรก Frappe ใช้ cookies เพื่อระบุตัวตนของ user แล้ว join Socket.io rooms ที่เหมาะสมให้โดยอัตโนมัติ ซึ่งหมายความว่า browser ต้องมี session cookies ที่ถูกต้อง และ Socket.io server ต้องอ่าน cookies เหล่านั้นได้ ถ้า cookies หายหรือไม่ถูกต้อง (เช่น ถูก redirect ทำให้หลุดไป) user จะถูกมองเป็น guest
Publish จาก Python
flow การ publish ทำงานแบบนี้:
- Python publish event ไปยัง Redis ผ่าน
frappe.publish_realtime() - Socket.io subscribe อยู่ที่ Redis channel
eventsแล้วรับ message มา - Socket.io broadcast message ไปยัง room/channel ที่ถูกต้อง
- Client รับ message แล้วทำงานตาม (เช่น แสดง notification)
นี่คือ pub/sub pattern แบบคลาสสิก โดย Redis ทำหน้าที่เป็น message broker ระหว่าง Python backend กับ Node.js Socket.io server
การ Debug ระบบ Socket.io ของ ERPNext / Frappe
ถ้าลองทำ publish test ด้านบนแล้วได้ผล ก็แสดงว่าระบบ real-time ทำงานปกติ แต่ถ้าไม่ได้ผล มาดูวิธี debug อย่างเป็นระบบกัน
Setup ของเราใช้ Docker containers ถ้าใช้ environment อื่น ก็ปรับ commands ตามความเหมาะสม แต่แนวทางการ debug เหมือนกัน
มี 4 จุดหลัก ที่ต้องตรวจสอบ แนะนำให้เช็คตามลำดับ เพราะแต่ละจุดต่อเนื่องจากจุดก่อนหน้า:
- Socket.io connection - browser connect เข้า Socket.io server ได้ไหม?
- Publish event (จาก Python) - Python publish message ไป Redis ได้ไหม?
- Subscribe event (จาก Node.js / Socket.io) - Socket.io server รับ message จาก Redis ได้ไหม?
- Authentication และ room joining - user ผ่านการยืนยันตัวตนถูกต้องไหม และ join room ที่ถูกต้องหรือเปล่า?
Socket connection
เปิด developer console ของ browser แล้วรัน checks เหล่านี้
Check A: ตรวจสอบสถานะ connection
frappe.socketio.socket.connected - ต้องได้ค่า true
Check B: ตรวจสอบการสื่อสารแบบสองทางด้วย ping/pong test
frappe.socketio.socket.on('pong', () => console.log('Confirmed, socket.io connection works.'));
frappe.socketio.socket.emit('ping');เราควรจะเห็นข้อความ Confirmed, socket.io connection works. ใน console ถ้าเห็นแสดงว่า Socket.io client connect เข้า server ได้สำเร็จ และ message สามารถวิ่งไปกลับได้
Note: ที่ทำแบบนี้ได้เพราะมี ping/pong handler อยู่ใน
frappe/realtime/handlers.jsที่คอยตอบpingevents จาก client
Publish event (จาก Python)
ขั้นตอนนี้ตรวจสอบว่า Python สามารถ publish events ไป Redis ได้ ต้องใช้ 2 terminals
Terminal 1 - Subscribe เข้า Redis events channel:
เชื่อมต่อไปยัง Redis instance ผ่าน redis-cli ในกรณีที่ใช้ Docker เราเข้าไปที่ Frappe container แล้วรัน redis-cli -h fp-redis-queue (hostname -h จะต่างกันตาม environment ของแต่ละคน)
พอเห็น prompt fp-redis-queue:6479> ให้รัน SUBSCRIBE events terminal จะรอรับ message ที่เข้ามา
Terminal 2 - Publish test event:
เข้าไปที่ Frappe container เปิด bench console แล้วรัน:
frappe.publish_realtime(event="msgprint", message={"message": "Hello!"}, user="Administrator")
กลับไปดูที่ Terminal 1 ควรจะเห็น output แบบนี้:
1) "message"
2) "events"
3) "{\n \"event\": \"msgprint\",\n \"message\": {\n \"message\": \"Hello!\"\n },\n \"namespace\": \"staging.erpnext.summitindustech.com\",\n \"room\": \"user:Administrator\"\n}"ถ้าได้ผลแบบนี้ แสดงว่า Python สามารถ publish events ไป Redis ได้สำเร็จ ปัญหาอยู่ที่ขั้นตอนถัดไปใน chain
Subscribe event (จาก Node.js / Socket.io)
ขั้นตอนนี้ตรวจสอบว่า Socket.io server รับ message จาก Redis ได้ เราจะรัน Node.js script เล็ก ๆ ตรงในSocket.io container เพื่อจำลองสิ่งที่ server ทำ
Terminal 1 - รัน subscriber script:
docker exec -it frappe_socketio_container node -e "
const { get_redis_subscriber } = require('/home/frappe/frappe-bench/apps/frappe/node_utils');
(async () => {
try {
const subscriber = get_redis_subscriber();
await subscriber.connect();
console.log('Redis connected');
subscriber.subscribe('events', (message) => {
message = JSON.parse(message);
let namespace = '/' + message.namespace;
console.log('GOT:', {
message: message,
namespace: namespace,
});
});
console.log('Subscribed OK');
setTimeout(() => process.exit(), 15000);
} catch (e) {
console.error('FAILED:', e);
process.exit(1);
}
})();
"Terminal 2 - Publish test event เหมือนเดิม:
เข้าไปที่ Frappe container เปิด bench console แล้วรัน:
frappe.publish_realtime(event="msgprint", message={"message": "Hello!"}, user="Administrator")
ที่ Terminal 1 ควรจะเห็นแบบนี้:
Redis connected
Subscribed OK
GOT: {
message: {
event: 'msgprint',
message: { message: 'Hello!' },
namespace: 'staging.erpnext.summitindustech.com',
room: 'user:Administrator'
},
namespace: '/staging.erpnext.summitindustech.com'
}Note: script นี้พิสูจน์ได้แค่ว่า Redis pub/sub ทำงานจากฝั่ง Node.js เท่านั้น มันไม่สามารถส่ง message ไปยัง browser client ได้ เพราะเป็นแค่ Redis subscriber ธรรมดา ไม่ใช่ Socket.io server ที่มี client เชื่อมต่ออยู่
ถ้าได้ผลแสดงว่า Socket.io server สามารถ subscribe และรับ events จาก Redis ได้ ถ้าตรงนี้ผ่านแต่ browser ยังไม่ได้รับ message ปัญหาน่าจะอยู่ที่ authentication หรือ room joining
Middleware และการ join WebSocket room
ถ้า checks ก่อนหน้าผ่านหมด ปัญหาน่าจะอยู่ที่ authentication หรือ room assignment ผู้ใช้อาจ connect เข้ามาในฐานะ guest แทนที่จะเป็น account จริง ทำให้ join ผิด room และไม่ได้รับ message ที่ควรจะได้
Socket.IO มี debug mode ที่เปิดใช้ได้ นี่คือค่า DEBUG ที่ใช้บ่อย:
| ค่า | แสดง |
|---|---|
socket.io:socket | Room joins/leaves, emits ของแต่ละ socket |
socket.io:server | การสร้าง namespace, connections |
socket.io* | ทุกอย่าง (verbose มาก) |
เปิด debug logging โดยแก้ Docker Compose configuration:
fp-websocket:
<<: *frappe-base
+ environment:
+ - DEBUG=socket.io:socket
command:
- /bin/sh
- -c
- sleep 20 && node /home/frappe/frappe-bench/apps/frappe/socketio.jsหลังจาก restart service แล้ว ดู logs ของ Socket.io ควรจะเห็นแบบนี้:
socket.io:socket joining room user:Administrator
socket.io:socket joining room allสิ่งที่ต้องดูคือ user ไหน join room ไหน ลอง reload client แล้วดู logs ว่าแสดง user ที่ถูกต้อง join room ไม่ใช่ guest ถ้าเห็น guest แทนที่จะเป็น user ที่คาดหวัง แสดงว่า authentication ทำงานไม่ถูกต้อง
Note: อาจมี noise เยอะใน logs ถ้า server ถูกใช้งานหนัก แนะนำให้ทดสอบบน staging environment
สรุป
ในกรณีของเรา หลังจากทำตาม debugging steps เหล่านี้ พบว่า sockets join ผิด room เมื่อ Socket.io เรียก get_user_info ได้ค่ากลับมาเป็น guest แทนที่จะเป็น user จริง
สาเหตุ คือ HTTP-to-HTTPS redirect ทำให้ session cookies หลุดหายไป เมื่อ Socket.io server ส่ง internal request เพื่อยืนยันตัวตนของ user redirect ทำให้ cookie headers หายไป server เลยมองทุก connection เป็น guest
วิธีแก้ คือเพิ่ม network alias ให้ Nginx container เพื่อให้ internal requests ไปยัง domain ถูก serve ภายใน Docker network โดยตรง ไม่ต้องผ่าน external redirect ทำให้ cookies ไม่หลุด และระบุ user ได้ถูกต้อง