プロンプトとコンテキスト
Googleカレンダーのような製品向けのイベント重複処理を設計してください。ユーザーは参加者、開始・終了時刻、タイムゾーン、定期的な予定のルール、リマインダー、公開設定、会議リンクを持つイベントを作成、更新、削除します。作成または更新時には重複を検出し、拒否、警告して続行、またはオーバーライドの各ポリシーをサポートする必要があります。空き/予約済み(free/busy)情報の読み取りは書き込みを大幅に上回り、一般的なチェックは数十ミリ秒以内で行われる必要があり、プライベートカレンダーは予約済み状態のみを公開すると想定してください。
面接官が評価するポイント
優れた回答では、データベース名を挙げる前に重複のセマンティクスを定義します。評価されるポイントは、境界での誤検出を防ぐ半開区間ルール、重複インデックスから分離された正規イベントモデル、明示的なRRULE・例外・具体化の制限、ローカル時刻ルールから絶対時刻(インスタント)への変換、派生インデックス用のOutboxまたはCDCパス、および同じリソースを予約しようとする2つの書き込み処理に対する並行性対策です。「重複がないかデータベースにクエリする」だけでは、これらの障害モードに対処できません。
前提条件の確認
- 隣接する区間は重複とみなしますか? みなさない場合は、半開区間
[start, end)を使用します。 - 定期的なシリーズはどこまで展開する必要がありますか? 終了のないシリーズには、ローリングウィンドウまたはクエリ時の展開が必要です。
- いずれかの参加者が予約済みであればイベントをブロックしますか、それとも必須参加者や主催者のみですか? これによりクエリのファンアウトとポリシーが決まります。
- ダブルブッキング(重複予約)は拒否、警告、許可のどれですか? 会議室と個人のカレンダーでは異なるポリシーを使用する場合があります。
- 他のユーザーには完全な詳細、予約済み区間、または何も表示しないのいずれにすべきですか? これによりACLとレスポンスの形式が決まります。
30秒での回答
「正規のイベントおよび参加者レコードと、カレンダーごとにパーティション分割された空き状況(free-busy)インデックスを保持します。重複が存在するのは existing.start < new.end かつ new.start < existing.end の場合のみであり、予定なし(free)のイベントは時間を占有しません。定期的なルールはイベントのタイムゾーンで保存し、制限された期間を具体化して例外を記録します。バージョンチェックまたはリソースロックで書き込みを保護し、コミット後にOutboxが派生インデックスを更新します。レスポンスにはACLで許可された予約状況のみを公開し、インデックスのドリフト、再構築、通知遅延をモニタリングします。」
ステップごとの解決策
1. 時間と重複の述語を定義する
各オカレンスを [start_utc, end_utc) として表現しつつ、表示および展開のために元のタイムゾーンとローカルルールを保持します。2つのオカレンスは、a.start < b.end かつ b.start < a.end である場合にのみ重複します。したがって、10:00–11:00 と 11:00–12:00 は隣接しており重複しません。長さゼロまたは逆転した区間は拒否します。終日イベントは、同じ述語を適用する前にカレンダーのタイムゾーンでの境界に変換します。
2. 正規モデルと空き状況インデックスを分離する
正規レイヤーには、編集および監査のためにイベント、参加者、RRULE、例外、ACL、バージョンを保存します。空き状況インデックスには、calendar_id、オカレンスの境界、占有状態、イベントID、公開ラベルのみを保存し、カレンダーと開始時刻でソートします。重複クエリは、サイズの大きいイベントJSONではなく、要求されたウィンドウをスキャンします。月単位またはテナント単位でパーティションを分割し、トラフィックの多いカレンダーには専用のシャードやキャッシュが必要になる場合があります。
3. 定期的な予定と例外の処理
iCalendar形式のRRULEを保存し、要求された期間のオカレンスを展開します。ローリング具体化により次の90日分を生成し、境界が近づくにつれて拡張します。それより先のクエリはオンデマンドで展開して結果をキャッシュします。this occurrence、this and future、およびシリーズ全体の編集では、過去のオカレンスを書き換えるのではなく、例外レコードまたは新しいシリーズバージョンを作成します。各オカレンスは、重複チェックと通知に独立して参加します。
4. 書き込みパスと並行性制御
時刻、ACL、参加者ポリシーを検証し、同じトランザクション内で現在のインデックスバージョンをチェックします。個人カレンダーではオプティミスティック(楽観的)バージョンを使用してクライアントにリトライを要求できます。排他性が厳格な会議室や予約枠には、リソースごとのロックやデータベースの排他制約(exclusion constraint)を使用できます。イベントとOutboxレコードを一緒にコミットし、コンシューマーがインデックスを更新して通知を送信します。インデックスが遅延している場合は、正規ストアから読み取るか、回答を不確実としてマークします。決して重複がないと暗黙的に判定してはなりません。
5. プライバシー、クエリ、およびスケール
レスポンスはACLによって階層化します。権限を持つ閲覧者にはタイトルと参加者を表示し、その他の閲覧者には予約済み区間のみを表示し、非公開イベントは詳細不明のブロックとして表示します。ポリシーに従って、空き(free)、仮確定(tentative)、外出中(out-of-office)の状態を占有または警告にマッピングします。読み取り主体のトラフィックのために一般的な時間枠をキャッシュし、キーにカレンダーのバージョンまたは無効化ウォーターマークを含めます。組織をまたぐ空き状況の呼び出しにはタイムアウトと部分結果セマンティクスを設け、1つの外部カレンダーによってすべての参加者の処理が停滞しないようにします。
6. 整合性、修復、障害パス
正規イベントが信頼できる唯一の情報源(Source of Truth)であり、インデックスは再構築可能です。OutboxまたはCDCにより、コミットの成功によって最終的にインデックスの更新が生成されることが保証されます。古いメッセージが新しいメッセージを上書きしないよう、コンシューマーはバージョンを冪等に処理します。定期的に正規オカレンスとインデックスのハッシュ値または件数を比較し、ドリフトが発生した場合はカレンダーを再構築して影響を受ける範囲を記録します。既にコミットされたイベントをロールバックすることなく、失敗した通知をリトライします。
高品質な回答例
まず、[start, end) で重複を定義します。厳密な積集合(重なり)のみが重複であり、隣接するイベントは重複せず、予定なしのイベントは空き状況を消費しません。正規イベントモデルには、元のタイムゾーン、RRULE、例外、参加者、およびACLを保存します。カレンダーごとにパーティション分割された個別のオカレンスインデックスには、高速な時間枠スキャンのために境界、占有状況、イベントIDのみを保存します。RRULEはイベントのタイムゾーンのまま保持し、次の90日分を具体化して、それ以降の範囲はオンデマンドで展開し、単一オカレンス、将来のオカレンス、シリーズ全体の編集を明示的な例外やバージョンとして表現します。
書き込みパスでは権限とポリシーを検証し、並行編集に対してオプティミスティックバージョンまたはリソースロックを使用します。イベントとOutboxレコードは一緒にコミットされ、バージョンを認識するコンシューマーがインデックスを冪等に更新して通知を配信します。インデックスは派生データであり、正規データから再構築可能で、重複レスポンスはACLによってフィルタリングされます。個人カレンダーでのダブルブッキングにはブロックではなく警告を行いますが、会議室には排他制約またはロックを使用します。インデックスのドリフト、古いバージョン、クエリレイテンシ、外部の空き状況取得タイムアウトをモニタリングします。
よくある間違い
- 間違い: 重複判定に
start <= other.endを使用する → 隣接するイベントが誤って重複と判定される → 半開区間ルールを明記し、無効な長さゼロのイベントを拒否する。 - 間違い: リクエストごとに無期限の定期シリーズを展開する → レイテンシと処理量に上限がなくなる → ローリング具体化期間と、それを超える範囲のオンデマンド展開を使用する。
- 間違い: 完全なイベントJSONを重複インデックスとして使用する → 読み取り時に大きなフィールドをスキャンし、プライベートな詳細が漏洩する → 時間と占有状況のプロジェクションを維持し、ACLでマスキングする。
- 間違い: インデックスの更新が失敗したときにイベントをロールバックする → 信頼できる情報源が検索や通知と結合してしまう → Outboxをコミットし、非同期でリトライし、再構築手段を提供する。
- 間違い: すべてのカレンダーに対してグローバルロックを取得する → トラフィックの多いカレンダーがサービス全体を遅延させる → 厳格に排他的なリソースのみをロックし、通常のカレンダーにはバージョンチェックを使用する。
フォローアップと回答
すべての参加者が空いている時間を見つけるにはどうすればよいですか?
対象の時間枠について各必須参加者の予約済み区間をクエリし、共通のタイムライン上で区間をマージして、その和集合の補集合を取得します。ファンアウトは参加者数に応じて増加するため、クエリを並行実行し、時間枠に上限を設け、プライベートな詳細を公開するのではなく、権限がないカレンダーやタイムアウトしたカレンダーに対しては部分的な結果や不確実な結果を返します。
DST(夏時間)の変更後、特定の日付でのみ定期的な会議が重複します。どのようにデバッグしますか?
RRULEのタイムゾーン識別子、元のローカル時刻、展開ライブラリのバージョン、例外レコードを調査します。展開前にローカル時刻をUTCに変換してタイムゾーン情報を破棄してはなりません。DSTの境界をまたぐテストフィクスチャを再生し、各オカレンスのローカル表示時刻、UTC境界、重複述語を比較して、展開のバグとインデックス具体化のバグを切り分けます。
会議室を絶対にダブルブッキングさせてはならない場合、何が変わりますか?
2つのリクエストが同時に会議室を空いていると認識する可能性があります。リソースの時間範囲をデータベースの排他制約、シリアライズ可能トランザクション、または短いリソースロックで保護し、失敗時には重複するオカレンスを返します。ロックはキャッシュではなくコミットのウィンドウのみをカバーします。ホットなリソースをシャーディングし、リトライに上限を設けることで、1つのロックキューが他のカレンダーのリソースを枯渇させないようにします。