Từ 3 phút xuống 500ms: Bug đăng ký vô nghĩa
Tôi truy tìm bug delay đăng ký 3 phút hóa ra là Schrödinger's user — tồn tại và không tồn tại cùng lúc do database replication lag giữa write và read.
Bạn biết cảm giác khi người dùng báo bug vô nghĩa không? "App mất 3 phút load sau khi đăng ký." Ba phút? Đó không phải thời gian load, đó là giờ nghỉ uống cà phê. Đây là câu chuyện tôi truy tìm một trong những bug kỳ lạ nhất lịch sử DIALØGUE.
Bí ẩn bắt đầu
Bắt đầu khá bình thường. Người dùng mới đăng ký bằng Google SSO, háo hức thử DIALØGUE. Rồi... không có gì. Chính xác hơn, họ nhận được loading skeleton thiết kế đẹp. Trong ba phút liền.
Điều kỳ lạ? Chỉ xảy ra với người dùng mới. Người dùng hiện tại đăng nhập tức thì. Và không nhất quán — đôi khi 1 phút, đôi khi 3, đôi khi hoạt động ngay.
Suy nghĩ đầu tiên: "Chắc vấn đề cold start." (Người dẫn chuyện: Không phải vấn đề cold start.)
Cuộc điều tra
Vòng 1: Đổ lỗi Frontend
```typescript
// Nghi phạm đầu: Hook load profile
useEffect(() => {
if (user) {
fetchUserProfile(); // Cái này mất mãi
}
}, [user]);
```
Thêm timer khắp nơi. API call thực sự mất 3 phút. Nhưng tại sao? Backend nên trả dữ liệu hoặc lỗi, không chỉ... đợi.
Vòng 2: Đổ lỗi Backend
Đào sâu vào Supabase Edge Functions:
```typescript
// Edge Function lấy user profile
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) {
// Người dùng mới - tạo profile
await createUserProfile(userId);
}
```
Trông ổn mà. Nên nhanh, đúng không? Thời gian thêm log.
Vòng 3: Cốt truyện dày thêm
Sau khi thêm log khắp mọi nơi, tôi phát hiện thứ kỳ lạ:
[00:00] User đăng nhập Google
[00:01] Auth trigger kích hoạt - tạo user record
[00:01] Frontend yêu cầu profile
[00:01] Edge Function truy vấn user... KHÔNG CÓ KẾT QUẢ
[00:02] Edge Function thử tạo user...
[00:02] Database constraint error: User already exists
[00:03] Function retry...
[03:00] Function cuối cùng timeout
Khoan, cái gì? User không tồn tại, nhưng cũng đã tồn tại? Schrödinger's user? T.T
Phát hiện
Sau khi nhìn database log đến đau mắt, tôi cuối cùng thấy nó. Database có các process cạnh tranh:
- Supabase Auth Trigger: Tạo user record khi đăng ký
- Edge Function: Thử tạo user nếu không tìm thấy
- Database Replication Lag: INSERT của trigger chưa replicate đến read replica
Đây là chuyện đang xảy ra:
-- Auth trigger (trên primary database)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function (đọc từ replica)
SELECT * FROM users WHERE id = $1; -- Trả về không có gì!
-- Edge Function (thử giúp)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLICT!
Function retry với exponential backoff, mỗi lần gặp cùng race condition cho đến khi:
- Replication cuối cùng bắt kịp (1-3 phút)
- Function timeout (3 phút)
Cập nhật: Thủ phạm thực và giải pháp cuối cùng
Sau bài viết ban đầu, chúng tôi tiếp tục debug, và dù thay đổi backend là cải thiện, gốc rễ bí ẩn vẫn khó nắm bắt. Bước đột phá đến khi chúng tôi chuyển focus từ backend sang luồng hydration và authentication phía client.
Thủ phạm thực: Race Condition phía client
Vấn đề không phải database trigger chậm hay Edge Function cold start. Vấn đề thực là race condition phía client kinh điển:
- OAuth Redirect: Người dùng mới đăng nhập Google và được redirect về app.
- Async Session: Supabase client library (
supabase-js) bắt đầu xử lý token từ URL để thiết lập session. Đây là quy trình bất đồng bộ. - Render sớm: Tuy nhiên, React app render ngay lập tức. Nó hỏi session trước khi quy trình async từ bước 2 hoàn thành.
- Thất bại: App nhận session
null, kết luận user chưa đăng nhập, và render trạng thái trống hoặc lỗi. Vài khoảnh khắc sau, session khả dụng, nhưng quá muộn — UI đã quyết định rồi.
Giải pháp ban đầu, như thêm retry phía client, chỉ là triệu chứng của việc chống lại race condition cơ bản này.
Giải pháp: Nguồn sự thật duy nhất cho Authentication
Giải pháp đúng và cuối cùng là tái kiến trúc quản lý trạng thái authentication frontend thực sự event-driven và robust.
1. Logic tập trung trong SupabaseProvider:
Chúng tôi refactor SupabaseProvider thành nguồn sự thật duy nhất, có thẩm quyền cho authentication. Xóa tất cả listener và check khác từ hook khác.
2. Sử dụng onAuthStateChange đúng cách:
Cốt lõi của fix là dựa hoàn toàn vào listener onAuthStateChange của Supabase.
// Logic đơn giản hóa trong SupabaseProvider.tsx
export function SupabaseProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // Bắt đầu ở trạng thái loading
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
// Chỉ coi authentication "hoàn thành" khi listener này kích hoạt.
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
Pattern này đảm bảo toàn bộ ứng dụng ở trạng thái loading cho đến khi Supabase xác nhận session người dùng hợp lệ hoặc null. Không còn race condition.
Kết thúc có hậu
DIALØGUE giờ onboard người dùng mới trong dưới một giây. Không còn giờ nghỉ cà phê khi đăng ký. Không còn người dùng bối rối tự hỏi có phá gì đó không.
Fix đã chạy production 3 tuần. Zero vấn đề timeout. Zero race conditions. Chỉ đăng ký mượt mà, nhanh chóng như nên từ đầu.
Có đáng dành một tuần debug không? Khi thấy người dùng mới liền mạch tạo podcast đầu tiên trong vài phút đăng ký — chắc chắn. :D
Bạn đã bao giờ truy tìm bug mà thứ đó đồng thời tồn tại và không tồn tại chưa? Tôi cảm thấy mọi developer đều có ít nhất một câu chuyện bug Schrödinger. Tôi rất muốn nghe của bạn!
Thân mến, Chandler
Muốn thử đăng ký giờ đã nhanh? Tạo podcast AI tại DIALØGUE. Tôi hứa không mất 3 phút nữa :)





