3分から500msへ:意味不明だったサインアップバグの話
3分かかるサインアップの遅延を追跡した結果、原因はシュレディンガーのユーザーでした。データベースの書き込みと読み込みのレプリケーションラグにより、ユーザーが「存在している」と「存在していない」が同時に起きていたのです。
ユーザーから全く意味が分からないバグの報告を受けた時の、あの気持ちを知っていますか?「サインアップ後、アプリの読み込みに3分かかります。」3分?それはもう読み込み時間じゃなくて、コーヒーブレイクです。これはDIALØGUEの歴史の中で最も奇妙なバグの1つを追跡した物語です。
謎の始まり
始まりは何の変哲もありませんでした。新しいユーザーがGoogle SSOでサインアップし、DIALØGUEを試すことにワクワクしていました。すると...何も起きません。正確に言えば何もないわけではありません。美しくデザインされたローディングスケルトンが表示されていました。3分間も。
変だったのは、これが_新規_ユーザーにしか起きないことです。既存ユーザーは即座にログインできました。しかも再現性がなく、1分の時もあれば3分の時もあり、たまにすぐ動くこともありました。
最初に思ったのは:「コールドスタートの問題に違いない。」(ナレーター:コールドスタートの問題ではありませんでした。)
調査開始
ラウンド1:フロントエンドのせいにする
```typescript
// 最初の容疑者:プロフィール読み込みhook
useEffect(() => {
if (user) {
fetchUserProfile(); // これがずっと時間かかっていた
}
}, [user]);
```
あちこちにタイマーを追加しました。APIコールが確かに3分かかっていました。でもなぜ?バックエンドはデータを返すかエラーを出すかのどちらかのはずで、ただ...待つだけなんてありえません。
ラウンド2:バックエンドのせいにする
SupabaseのEdge Functionsの中に飛び込みました:
```typescript
// ユーザープロフィール取得用Edge Function
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) {
// 新規ユーザー - プロフィールを作成
await createUserProfile(userId);
}
```
これは問題なさそうです。すぐに終わるはずですよね?もっとログを追加する時間です。
ラウンド3:事態の複雑化
あらゆる場所(本当に_あらゆる場所_)にログを追加した結果、奇妙なことを発見しました:
[00:00] ユーザーがGoogleでサインイン
[00:01] Auth triggerが発火 - ユーザーレコードを作成
[00:01] フロントエンドがプロフィールをリクエスト
[00:01] Edge Functionがユーザーを検索... 結果なし
[00:02] Edge Functionがユーザー作成を試行...
[00:02] データベース制約エラー:ユーザーは既に存在します
[00:03] Functionがリトライ...
[03:00] Functionが最終的にタイムアウト
ちょっと待って。ユーザーが存在しないのに、既に存在している?シュレディンガーのユーザー? T.T
真相の発見
目が痛くなるまでデータベースログを見つめ続けて、ついに見つけました。データベースには競合するプロセスがありました:
- Supabase Auth Trigger:サインアップ時にユーザーレコードを作成
- Edge Function:ユーザーが見つからない場合に作成を試行
- データベースレプリケーションラグ:triggerのINSERTがまだリードレプリカにレプリケートされていなかった
実際に起きていたことはこうです:
-- Auth trigger(プライマリデータベース上)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function(レプリカから読み取り)
SELECT * FROM users WHERE id = $1; -- 何も返さない!
-- Edge Function(助けようとして)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLICT!
Functionは指数バックオフでリトライし、各試行で同じレースコンディションに当たり、以下のどちらかが起きるまで続きました:
- レプリケーションが最終的に追いつく(1-3分)
- Functionがタイムアウトする(3分)
更新:本当の原因と最終的な堅牢なソリューション
最初の投稿後、デバッグを続けましたが、バックエンドの改善は改善だったものの、謎の根本原因は依然として不明でした。ブレークスルーは、バックエンドからクライアントサイドのハイドレーションと認証フローに焦点を移した時に訪れました。
本当の原因:クライアントサイドのレースコンディション
問題はデータベースtriggerの遅さでもEdge Functionのコールドスタートでもありませんでした。真の問題は典型的なクライアントサイドのレースコンディションでした:
- OAuthリダイレクト: 新しいユーザーがGoogleでサインインし、アプリにリダイレクトされます。
- 非同期セッション: Supabaseクライアントライブラリ(
supabase-js)がURLからトークンを処理してセッションを確立し始めます。これは非同期プロセスです。 - 早すぎるレンダリング: しかし、Reactアプリはすぐにレンダリングされます。ステップ2の非同期プロセスが完了する_前に_ユーザーのセッションを要求します。
- 失敗: アプリは
nullセッションを受け取り、ユーザーがログインしていないと判断し、空白またはエラー状態をレンダリングします。数瞬後にセッションが利用可能になりますが、もう手遅れです。UIは既に判断を下してしまっています。
クライアントサイドのリトライを追加するなどの最初の回避策は、この根本的なレースコンディションと戦っている症状に過ぎませんでした。
ソリューション:認証の単一の信頼できる情報源
正しい最終的なソリューションは、フロントエンドの認証状態管理を真にイベントドリブンで堅牢なものに再設計することでした。
1. SupabaseProviderにロジックを集中化:
SupabaseProviderを認証の唯一の信頼できる情報源にリファクタリングしました。他のhookからすべてのリスナーとチェックを削除しました。
2. onAuthStateChangeを正しく使用:
修正の核心は、SupabaseのonAuthStateChangeリスナーに_専ら_依存することでした。
// SupabaseProvider.tsxの簡略化されたロジック
export function SupabaseProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // ローディング状態で開始
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
// このリスナーが発火して初めて認証が「完了」したとみなす
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
このパターンにより、Supabaseがユーザーのセッションが有効かnullかを確認するまで、アプリケーション全体がloading状態を維持します。もうレースコンディションは起きません。
ハッピーエンド
DIALØGUEは現在、1秒以内に新規ユーザーのオンボーディングが完了します。サインアップ中のコーヒーブレイクはもうありません。何か壊したのではないかと困惑するユーザーもいません。
この修正は3週間前から本番環境で動いています。タイムアウトの問題はゼロ。レースコンディションもゼロ。最初からそうあるべきだった、スムーズで高速なサインアップだけです。
この問題のデバッグに1週間費やした価値はあったのか?新規ユーザーがサインアップ後数分以内にシームレスに最初のポッドキャストを作成している姿を見ると — 間違いなくそうです。 :D
あるものが「同時に存在していて存在していない」というバグを追跡したことはありますか?すべての開発者には少なくとも1つのシュレディンガーバグの話があると思います。ぜひ皆さんの話を聞かせてください!
よろしくお願いします、Chandler
今やスピーディーになったサインアップを試してみませんか?DIALØGUEでAIポッドキャストを作成してください。もう3分かからないことをお約束します :)





