บทนำ
ในบทความนี้ เราจะ Implement Waiting Room Page สำหรับเว็บไซต์ของคุณ
ทำไมต้องมี Waiting Room?
Visitor จำนวนมากโดยทั่วไปเป็นสิ่งดี แต่ไม่เสมอไป Traffic Surge ระทดทะลุอาจ Overwhelm แอปพลิเคชันทำให้ Service Down Cloudflare Waiting Room เป็นโซลูชันที่ดีแต่มีเฉพาะ Business/Enterprise Accounts 别担心 เราจะสร้าง Waiting Room สำหรับเว็บไซต์ทุกประเภทด้วย Cloudflare Workers และ Upstash Redis
วิธีทำงาน (How?)
มี 2 Parameters ควบคุม Waiting Room:
- Max Session Duration: Visitor อยู่ Idle บนเว็บได้นานแค่ไหน?
- Max Website Capacity: เว็บไซต์รองรับ Visitor พร้อมกันได้กี่คน?
เมื่อ Visitor เข้ามา เราจะสร้าง Unique Key (Session Key) เก็บใน Redis พร้อม Expiration Time = Max Session Duration ก่อนปล่อยเข้าเว็บ ตรวจสอบ dbsize ของ Redis หาก Size > Max Website Capacity จะ Forward ไป Waiting Room Waiting Room เป็น Static HTML ที่ Refresh ทุก 30 วินาที ทุก 30 วินาที Visitor อาจได้เข้าถึงเว็บหากมีที่ว่าง
Unique Key ที่สร้างจะเขียนเป็น Cookie ใน Request ของ Visitor เดียวกันจะมี Key เดียวกัน จำเป็นสำหรับตรวจสอบว่า Visitor มี Session อยู่ในเว็บแล้วหรือไม่เมื่อ Capacity เต็ม Visitor จะได้เข้าเว็บแม้ Capacity เต็ม ถ้า Key ยังมีอยู่ใน Redis Keyspace หาก Idle เกิน Max Session Duration Key จะถูกลบจาก Redis และถ้า Capacity เต็มตอน Request จะ Forward ไป Waiting Room
เราจะ Implement Logic นี้ที่ Cloudflare Workers ใช้ Upstash เป็น Redis Store มาอธิบายพื้นฐานการตัดสินใจเทคโนโลยี
ทำไม Cloudflare Workers?
Waiting Room จะ Intercept ทุก Request ไปเว็บ Performance Overhead ต้องน้อยที่สุด Cloudflare Workers ใช้ Edge Infrastructure ให้ Latency ต่ำสุดทั่วโลก ไม่เหมือน AWS Lambda ไม่มี Cold Start Problem เป็น Serverless Technology ดังนั้น Scalability ไม่ใช่ปัญหา
ทำไม Upstash Redis?
ต้องเช็ค Current Size ของเว็บก่อนปล่อย Visitor ใหม่ Cloudflare Workers เป็น Stateless ต้องเก็บข้อมูลภายนอก Redis เหมาะที่สุดด้วย Low Latency แต่ Redis Offerings ต้องการ TCP Connections ซึ่ง Cloudflare Workers ไม่รองรับ Upstash เป็น Redis Offering 唯一ที่มี Built-in REST API นอกจากนี้ Global Replication ให้ Low Latency ทั่วโลก
การ Implement แบบ Step-by-Step
Implement โปรเจกต์ทีละขั้นตอน หากต้องการ Clone และ Setup Waiting Room โดยตรง ทำตาม Steps ใน README ของ Source Code
1. Project Setup
สร้างโปรเจกต์ด้วย Wrangler:
wrangler generate waiting-room
ติดตั้ง Dependencies:
npm install cookie @upstash/redis
2. อัปเดต wrangler.toml
Update Type:
type = "webpack"
ตั้ง Cloudflare Account ID (ดูวิธีหา ที่นี่):
account_id = "REPLACE_HERE"
เพิ่ม Variables:
[vars]
UPSTASH_REDIS_REST_TOKEN = "REPLACE_HERE"
UPSTASH_REDIS_REST_URL = "REPLACE_HERE"
TOTAL_ACTIVE_USERS = 10
SESSION_DURATION_SECONDS = 30
สร้าง Global Database จาก Upstash Console Copy/Paste REST Token และ URL จาก Console Database ควรว่างเปล่า initially และใช้โดยแค่ App นี้ ตั้ง TOTAL_ACTIVE_USERS และ SESSION_DURATION_SECONDS ตามความต้องการ
3. index.js (Cloudflare Workers Implementation)
Copy/Paste โค้ดด้านล่าง:
import { parse } from "cookie";
import { Redis } from "@upstash/redis/cloudflare";
const redis = Redis.fromEnv();
addEventListener("fetch", (event) => {
event.respondWith(
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});
const COOKIE_NAME_ID = "__waiting_room_id";
const COOKIE_NAME_TIME = "__waiting_room_last_update_time";
const init = {
headers: {
Authorization: "Bearer " + UPSTASH_REDIS_REST_TOKEN,
},
};
async function handleRequest(request) {
const { pathname } = new URL(request.url);
if (!pathname.startsWith("/favicon")) {
const cookie = parse(request.headers.get("Cookie") || "");
let userId;
if (cookie[COOKIE_NAME_ID] != null) {
userId = cookie[COOKIE_NAME_ID];
} else {
userId = makeid(8);
}
const size = await redis.dbsize();
console.log("current capacity:" + size);
// There is enough capacity
if (size < TOTAL_ACTIVE_USERS) {
return getDefaultResponse(request, cookie, userId);
} else {
// Site capacity is full
const user = await redis.get(userId);
if (user === "1") {
// User already has active session
return getDefaultResponse(request, cookie, userId);
} else {
// Capacity full -> forward to waiting room
return getWaitingRoomResponse(userId);
}
}
} else {
return fetch(request);
}
}
async function getDefaultResponse(request, cookie, userId) {
// Uncomment below to test with static html content
// const newResponse = new Response(default_html)
// newResponse.headers.append('content-type', 'text/html;charset=UTF-8')
const response = await fetch(request);
const newResponse = new Response(response.body, response);
const now = Date.now();
let lastUpdate = cookie[COOKIE_NAME_TIME];
if (!lastUpdate) lastUpdate = 0;
const diff = now - lastUpdate;
const updateInterval = (SESSION_DURATION_SECONDS * 1000) / 2;
if (diff > updateInterval) {
await redis.setex(userId, SESSION_DURATION_SECONDS, 1);
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_TIME}=${now}; path=/`
);
}
newResponse.headers.append(
"Set-Cookie",
`${COOKIE_NAME_ID}=${userId}; path=/`
);
return newResponse;
}
async function getWaitingRoomResponse(userId) {
const newResponse = new Response(waiting_room_html);
newResponse.headers.set("content-type", "text/html;charset=UTF-8");
return newResponse;
}
function makeid(length) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const waiting_room_html = `
Waiting Room