Skip to content
··阅读时间2分钟

从 3 分钟到 500ms:那个完全说不通的注册 Bug

我追查了一个注册后延迟 3 分钟的问题,最后发现是“薛定谔用户”——由于写入与读取之间的数据库复制延迟,用户在同一时刻既存在又不存在。

你有没有遇到过那种用户反馈“听起来完全不合理”的 bug?“我注册后 app 要 3 分钟才加载出来。”3 分钟?这不是加载时间,这是喝杯咖啡的时间。这篇就是我如何追出 DIALØGUE 历史上最诡异 bug 之一的全过程。

谜案开始

起初一切都很平常。一个新用户用 Google SSO 注册,兴冲冲想试 DIALØGUE。然后……没反应。也不完全是没反应——他看到了我们设计得很漂亮的 loading skeleton。整整 3 分钟。

诡异点是:它只发生在_新用户_身上。老用户登录都秒进。而且不稳定——有时 1 分钟,有时 3 分钟,偶尔又立即正常。

我的第一反应是:“肯定是冷启动。”(旁白:并不是冷启动。

调查过程

第 1 轮:怪前端

```typescript
// First suspect: The profile loading hook
useEffect(() => {
  if (user) {
    fetchUserProfile(); // This was taking forever
  }
}, [user]);
```

我到处加计时器。API 调用确实要 3 分钟。但为什么?后端要么返回数据,要么报错,不应该就这么……卡着。

第 2 轮:怪后端

我钻进了 Supabase Edge Functions:

```typescript
// Edge Function for getting user profile
const { data: profile } = await supabase
  .from('users')
  .select('*')
  .eq('id', userId)
  .single();

if (!profile) {
  // New user - create profile
  await createUserProfile(userId);
}
```

看起来没毛病,应该很快才对。那就继续加日志。

第 3 轮:剧情升级

日志加满(真的是_到处_都加)后,我看到了一段离谱时间线:

[00:00] User signs in with Google
[00:01] Auth trigger fires - creates user record
[00:01] Frontend requests profile
[00:01] Edge Function queries for user... NO RESULT
[00:02] Edge Function tries to create user... 
[00:02] Database constraint error: User already exists
[00:03] Function retries...
[03:00] Function finally times out

等等,什么?用户不存在,但又已经存在?薛定谔用户?T.T

真相揭示

盯数据库日志盯到眼睛发酸后,我终于看到了根因。数据库里有两个进程在竞争:

  1. Supabase Auth Trigger:注册时创建用户记录
  2. Edge Function:查不到就尝试创建用户
  3. 数据库复制延迟:trigger 的 INSERT 还没同步到读副本

实际过程是:

-- Auth trigger (on primary database)
INSERT INTO users (id, email) VALUES ($1, $2);

-- Edge Function (reading from replica) 
SELECT * FROM users WHERE id = $1; -- Returns nothing!

-- Edge Function (trying to help)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLICT!

函数会用指数退避重试,每次都撞上同一个竞态,直到:

  • 复制终于追上(1-3 分钟)
  • 或函数超时(3 分钟)

更新:真正元凶与最终稳健解法

初版文章发出后,我们继续调试。虽然后端改动确实有改善,但谜团核心仍未完全解开。真正突破来自我们把焦点从后端转到客户端 hydration 与认证流程。

真正元凶:客户端竞态条件

问题不是慢 trigger,也不是冷 Edge Function。真正问题是一个典型客户端竞态:

  1. OAuth 重定向: 新用户用 Google 登录后跳回我们的应用。
  2. 异步会话建立: Supabase 客户端库(supabase-js)开始异步处理 URL token,建立 session。
  3. 过早渲染: React 应用立即渲染,并在第 2 步完成前就去读取 session。
  4. 失败点: 应用拿到 null session,于是判定用户未登录,渲染空白或错误态。几秒后 session 才可用,但已经晚了——UI 早就做了错误决策。

我们之前的临时方案(例如客户端重试)只是对抗这个根本竞态的症状处理。

解决方案:认证状态单一事实来源

正确且最终的方案,是重构前端认证状态管理,让它真正事件驱动且稳定。

1. 逻辑集中到 SupabaseProvider
我们把 SupabaseProvider 重构成认证状态唯一权威来源,移除了其他 hooks 里的重复监听与检查。

2. 正确使用 onAuthStateChange
修复核心是 依赖 Supabase 的 onAuthStateChange 监听。

// Simplified logic in SupabaseProvider.tsx

export function SupabaseProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true); // Start in a loading state

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null);
        // We only consider authentication "finished" once this listener fires.
        setLoading(false);
      }
    );

    return () => subscription.unsubscribe();
  }, []);

  // ...
}

这个模式保证:在 Supabase 明确给出“session 有效”或“session 为空”之前,整个应用始终处于 loading 状态。竞态不再存在。

Happy Ending

DIALØGUE 现在新用户 onboarding 用时不到 1 秒。注册后再也没有“喝杯咖啡再回来”的等待,也不会让用户怀疑是不是自己搞坏了什么。

这个修复已在线上跑了 3 周。超时问题 0 次,竞态问题 0 次。注册流程终于回到了它本该有的顺滑速度。

花一周调这个值不值?当我看到新用户注册后几分钟内就能顺利生成第一期播客时——绝对值。 :D

你有抓过那种“它同时存在又不存在”的 bug 吗?我感觉每个开发者都至少有一个薛定谔 bug 故事。很想听听你的版本。

致敬,

Chandler

想试试现在飞快的注册流程?来 DIALØGUE 创建你的 AI 播客吧。我保证它不再要 3 分钟了 :)

继续阅读

我的旅程
联系
语言
偏好设置