さくらコードSakura Code
小説/第9話

Novel

第9話 レビューの向こう側

第9話 レビューの向こう側のイメージイラスト

Script

Cold Open

シーン1 秋雨の朝とPR #103

 九月十五日。朝から、雨が降っていた。秋雨前線が、関東の上空に、低く、長く伸びている、と、駅で読んだスマホのニュースに、書いてあった。

 桜子のスニーカーは、駅から会社までの数分で、もう、つま先が濡れていた。オフィスに入ると、エアコンの効いた空気と、雨の湿度が、入り口の床で混ざり合っている。

 桜子は、鞄から薄手のタオルを取り出して、靴の上を軽く拭いてから、自分の席についた。

 昨夜のうちに、夏の計測タスクから続いていた未読件数のキャッシュ導入PRを最後まで書き上げていた。CIは、夜のうちに緑になっていた。

PR #103: perf(cache): cache count_unread for 10s with Redis
- replace full-scan with SELECT COUNT(*)
- cache result for 10s in Redis (key: unread:{user_id})
- add tests for cache hit/miss
- benchmark numbers attached

 Redisの設定、テスト、ベンチマーク、すべて入っている。鈴木がPMに確認してくれた「許容遅延10秒」の根拠も、PR本文に貼ってある。

 (今日こそ、Approvedかな)

 桜子は、ブラウザを開いて、PR #103 のページを更新した。

 遥のレビューは、すでに付いていた。ステータスは、Changes requested。

 桜子は、息を、ひとつ、ゆっくり、吐いた。

 半年弱で、遥からのレビューは、もう三十本近くに、なる。Approved のほうが、たぶん、件数としては多い。けれど、Changes requested の付いた数本は、いつも、共通の手触りがあった。コードの正しさだけでは、なかった。「これ、本番で困ったときに、どう戻す?」「これ、夜、誰がオンコールでも、踏める手順?」「これ、五年後の人が読んでも、同じ判断ができる?」――どれも、いま動いているコードの、その先の時間軸を、聞いてくる種類の指摘。

 (また、その種類だ)

 桜子は、画面を開く前に、もう、そう、感じていた。


Part A

シーン2 三度の差し戻し

 最初の指摘。

@yamashita-haruka: キャッシュON/OFFの切替手段はある?
本番で「やっぱり戻したい」と思ったとき、どこを触れば
全ユーザー一括でキャッシュ参照を止められる?

 桜子は、画面のすぐ下に、自分の if cacheenabled: の箇所を見つけて、コメントで返した。

@mochizuki-sakurako: コード内に cache_enabled フラグがあります。
書き換えてデプロイすれば、止められます

 昼前、二度目の指摘が来た。

@yamashita-haruka: 「書き換えてデプロイ」は、最速でも10分かかる。
夜の障害中にそれは長い。設定ファイルか管理画面で、
コード変更なしに切れるようにしてほしい

 桜子は、しばらく画面を見て、featureflags.yaml を新規に切り、unreadcacheenabled: true のフラグを追加した。コードからは featureflags.get("unreadcacheenabled") で読む。

 修正をpushして、レビューを依頼し直す。

 午後、三度目の指摘。

@yamashita-haruka: フラグはOK。次に、キャッシュキーの無効化手順は?
あるユーザーの未読件数だけ、いま、すぐ、消したい場合、
どのキーをどう消す手順を書いたドキュメントはある?
あと、有効期間中の更新手順も。
キャッシュONのまま、未読が動いた瞬間に古い値が見える時間は、
本番で何秒、業務影響としてどう許容されているのか

 桜子は、その三度目の指摘を、上から下まで、二回、読んだ。

 画面の下のテキストボックスで、指先が止まった。


シーン3 反発の数秒

 ノートPCのキーが、すこし、冷たく感じた。

 (テストも、CIも、通ってるのに)

 PR本文には、ベンチマークの数字も、許容遅延10秒の根拠も、Redisの設定も、全部、貼ってある。それでも、レビューは、まだ通らない。Changes requested のラベルは、これで三度目だった。

 手元の引き出しから、桜子は、メモ帳をそっと取り出した。普段は学びを書きとめるページの裏のほうを開いて、ペンを構える。

 書きかけて、ペン先が、止まった。

 書こうとしていたのは、

- 山下さんは、わたしに、厳しすぎる?

 という一行だった。

 桜子は、そのペン先を、しばらく宙で止めていた。

 初めてレビューを受けた日。レビューには、五種類のコメントがある。指摘、質問、提案、共有、称賛。あの日、遥が言った言葉が、頭の中で、もう一度、再生された。

 画面の三度目の指摘を、もう一度、五分類のラベルを貼りながら、読み直してみる。

 「キャッシュキーの無効化手順は?」これは、質問。

 「あるユーザーの未読件数だけ、消したい場合、どう?」これは、より具体的な質問。

 「ONのまま、古い値が見える時間は、業務影響としてどう許容されているか」これは、共有を求める質問でもあり、提案でもある。「ここまで考えてあると、本番で困らないよ」という。

 (指摘じゃなくて、ぜんぶ、質問だ)

 桜子は、ペン先を、メモから外した。

 書きかけた一行は、書かないまま、ページをそっと閉じた。


Part B

シーン4 雨の喫煙コーナーの会話

 夕方。雨は、ようやく弱くなっていた。

 桜子が、まだPRの修正案を頭の中で組み立てているとき、遥が、ふっと立ち上がって声をかけた。

「桜子さん、ちょっと、外、出ない?」

「あ、はい」

 階段を下りて、屋上の喫煙コーナーの前のベンチへ。雨上がりで、誰もいない。アスファルトに、まだ水たまりが残っている。

 遥はタバコを吸わない。桜子も吸わない。ただの、屋根のあるベンチだった。

「桜子さんのPR、三度差し戻したよね」

「はい」

「ちょっとだけ、わたしの話、していい?」

 遥の声は、社内で聞くより、少しだけ、低かった。

「はい」

「わたしね、今の会社の前に、別の会社にいたの。決済システムを触る部署だった」

「決済」

「うん。お金が動くやつ。お客さんから預かったお金を、別の口座にうつす、みたいな処理」

 遥は、ベンチの背に、軽く寄りかかった。

「二十五歳の頃、わたし、ある夜、本番でひとつだけ、設定を変えたの。リリースの後段の、ささいな数値。テストも、ステージングも、緑だった。レビューも、通ってた」

「そうなんですね」

「夜中に、アラートが鳴った。ある条件のとき、決済の二重計上が起きてた」

 遥は、桜子の方を見ずに、雨上がりの空を見ていた。

「わたしは、慌てて、設定を戻したつもり、だった。でも、戻し方が、ちゃんと整理されてなかった。アプリ側は戻せても、データベース側に書き込まれた中間状態を戻す手順が、決まってなかった」

「……」

「結果として、戻したつもりが、片側だけ戻った。整合性が崩れて、被害は、最初の二重計上より、広がった」

 桜子は、息を、吐かないまま、聞いていた。

「お客さんからの問い合わせは、最終的に三十数件。お金は、ちゃんと、全部、戻った。会社が、丁寧に、最後まで対応した。でも、そのときに、わたしが、自分の手で、見たもの。あれが、わたしのレビューの厳しさの、出所」

 桜子は、雨上がりのアスファルトを、しばらく、見ていた。

 遥は、その夜のことを、ここで言葉にしたあとも、まだ、声色を変えなかった。淡々と、事実を、事実として、置いていく。けれど、桜子は、聞きながら、いままでの三十本のレビューに付いていた、あの共通の手触りの正体が、急に、輪郭を持ったのを、感じた。

 (だから、戻し方、を、いつも聞かれてた)

 age >= 18 の境界条件PRに付いた一行。「同種の境界、user.py のほかにも、ある?洗ってからマージ可」。テストを先に書いたリファクタリングPRに付いた一行。「これ、半年後に別の人が、外したくなったときの戻し方は?」。観察と段階分割の改善PRに付いた一行。「フェーズ間で、戻れる地点を、ちゃんと残す形に」。

 全部、いま、屋上で語られている、あの夜の続きだった。


シーン5 戻すほど、壊れた夜

 遥は、しばらく黙ってから、続けた。

「あの夜から、わたしの中で、決まってる原則が、ひとつだけあるの」

「原則」

「ロールバックがない変更は、出してはいけない」

 桜子は、その一行を、心の中で、もう一度、なぞった。

「コードが緑でも、テストが揃ってても、ベンチマークが速くても。戻し方が、その変更と一緒に来てない場合は、出さない。出すときは、戻し方を、出した本人が、自分の手で書いた状態で出す」

「……」

「だから、桜子さんのPR #103 は、コードとして、よくできてる。テストも、ベンチも、ちゃんとある。でも、わたしの中の原則を通せていない、ってだけ」

「私が、嫌で、厳しく見てるわけじゃない、ってことですか」

 遥は、初めて、桜子の顔を見て、ほんの少しだけ笑った。

「うん。むしろ、桜子さんだから、ちゃんと書ききれると思ってる。書ききらせたい」

「……書ききらせたい」

「うん。厳しさ、って、結局、相手を信用しているから、出てくるものなんだよね。信用してない相手には、わたしは、レビューを、ここまで、書かない。たぶん、書く前に、自分で、直してしまう」

 桜子は、半拍、息を、止めた。

 半年弱、Changes requested を、自分の出来の悪さの目印として、受け取り続けていた。けれど、遥のなかでは、それは、信用の目印だった。同じラベルが、二つの違う方向から、貼られていた。

「あの、山下さん」

「うん」

「いま、わたしが、いずれ、誰かの新人のPRをレビューする側に、回ったとき」

「うん」

「わたし、たぶん、山下さんと、同じ厳しさを、ここまで持てない、と思います」

「……うん」

「持てるように、なりたい、けれど、わたしの厳しさの出所は、まだ、わたしの中に、ないので」

 遥は、しばらく、沈黙した。雨上がりの空に、雲の切れ間が、ほんの少しだけ、見えていた。

「桜子さん、それ、いま、無理に、出所を、作りにいかないでね」

「はい」

「出所は、たぶん、桜子さんの仕事の中で、自然に、できていく。早く厳しくなりたい、って思うと、出所の代わりに、桜子さんの中の、『早く厳しくなりたい』っていう焦りが、人を傷つけることになる」

「……はい」

「いまは、わたしの厳しさを、桜子さんの中で、いったん、預かっておく、くらいで、十分」

 雨は、やんでいた。アスファルトの水たまりに、夕方の薄い光が、差し込んでいる。

「ごめんね。最初に、わたしの過去の話まで言わずに、三度も差し戻して」

「いえ……、その。書こうかなと思ったメモが、わたし、ありました」

「メモ」

「『山下さんは、わたしに、厳しすぎる?』って、書きかけました。でも、書く前に、初めてのレビューで教わった五分類を思い出して、止めました」

 遥は、ふっと、息を抜くように笑った。

「書きかけたなら、書いておけば良かったよ。書いて、自分で消す。それも、健全」

「……はい」

「もどろう、寒くなってきた」


Part C

シーン6 PR #103 のリライト

 席に戻った桜子は、PR #103 を、もう一度、上から下まで書き直した。

 まず、docs/runbook/unreadcache.md という、新しいRunbook を切った。

# 未読件数キャッシュ Runbook

## 全体無効化(夜間障害時)
- feature_flags.yaml の `unread_cache_enabled: true` を `false` に変更
- main にマージ → 自動デプロイ完了 (約3分)

## 緊急: コード変更を待たずに止めたい
- Redis CLI で `DEL unread:*` (全キー削除)
- 同時に flag をオフにすること

## 個別ユーザーのキー無効化
- `DEL unread:{user_id}` (例: DEL unread:1234)

## 有効期間中の更新ルール
- TTL: 10秒
- 仕様上、未読件数の表示は最大10秒、過去のスナップショットになる
- PMからの許容: 10秒以内
- 問い合わせ業務に対するインパクト: 既読化の反映が遅れて見える可能性。
  実害は問い合わせ件数の重複対応のみ (実測でゼロ件相当)

## 関連
- PR #103
- feature_flag: unread_cache_enabled

 次に、PR本文に、Runbookへのリンクと、移行中の二重読み戦略の簡易図を加えた。

## 二重読み戦略
- フェーズ1 (PR #103): キャッシュON、ただしフラグでいつでもOFFに戻せる
- フェーズ2 (一週間後): フラグONがデフォルト前提でモニタリング
- フェーズ3 (二週間後): キャッシュ層が外せないものか、外せないなら恒久化を判断

各フェーズの停止条件:
- 業務側からの「件数が古い」報告が1件以上 → フラグOFFに即時戻す
- Redisレイテンシが平均20ms超 → フラグOFFに即時戻す

 最後に、PR本文に、桜子は、自分の言葉で、こう書いた。

レビューありがとうございます。
三度の差し戻しで、なぜ厳しく見ていただいているのか、輪郭が見えました。
コードと一緒に、戻し方を書いて持ってきました。

 push を済ませて、レビュー依頼を送る。

 遥のレビューは、夕食どきに付いた。ステータスは、Approved。コメントは、短かった。

@yamashita-haruka: ありがとう。
これで、夜中に呼び出されても、誰かが手順だけで戻せる

 桜子は、Approvedの緑色のチェックを、しばらく見てから、PRのレビュー履歴を、上から下まで、もう一度、自分の目で、読み返した。

 最初のレビュー。キャッシュON/OFFの切替手段はある?

 二度目。「書き換えてデプロイ」は、最速でも10分かかる

 三度目。キャッシュキーの無効化手順は?

 半日前の桜子なら、これらを、ぜんぶ、桜子のコードへの、不足の指摘として、読んでいた。けれど、いまの桜子は、同じ三行を、いくぶん、別の角度から、読めた。

 (ぜんぶ、夜の話を、しているんだ)

 夜中の三時に、ある条件で、本番に、何か、起きる。そのときに、誰かが――遥かもしれないし、鈴木かもしれないし、来年の新人かもしれないし、桜子自身かもしれない――眠い目をこすりながら、PRを開いて、Runbookを開いて、フラグを切る。三度の差し戻しは、いままさに眠い、その人に、桜子のコードと一緒に、戻し方 を、ちゃんと届けるための、やりとりだった。

 桜子は、Approvedのチェックの横の、自分の名前を、しばらく、見ていた。


Ending

シーン7 帰路、傘の柄

 二十時前。

 雨はもう、すっかりやんでいた。ただ、夜の街路は、湿って、街灯の光が、アスファルトの上で長く伸びていた。

 桜子は、傘の柄を握ったまま、駅まで歩いていた。

 濡れた傘の柄の、布のところが、てのひらの中で、わずかに、ぬるい温度を返してくる。初めてのレビューで十四件のコメントを読んだときも、赤い × のアイコンの前で息を止めたときも、桜子のてのひらは、いつも、こんなふうに、自分の体温を、つたえてくれていた。

 屋上のベンチで聞いた、遥の声が、まだ、耳の奥で、薄く残っている。

 (戻すほど、壊れた夜)

 たぶん、遥は、それを、いまだに、たまに、思い出している。だから、桜子の cacheenabled フラグの「コード変更でデプロイすれば止められます」を、そのまま通せなかった。

 桜子は、駅前のカフェに寄って、メモ帳を開いた。

2022/09/15 新人日記

- レビューの厳しさは、未来の障害から人を守るための姿勢だった
- 「コードと一緒に、戻し方を書いて持ってくる」
- ロールバックがない変更は、出してはいけない(山下さんの原則)
- フラグ、Runbook、停止条件、二重読み戦略
- レビュアーの過去には、たいてい、夜がある
- 厳しさは、相手を信用していないから出るのではなく、信用しているから出る
- 自分の厳しさの「出所」は、急いで作りにいかない

 ペンを置いて、桜子は、しばらく、メモを眺めた。

 初めてのレビューの日に書いた「レビューは会話」の続きが、今日、ようやく、少しだけ、見えた気がした。会話の向こう側には、人がいる。その人には、過去がある。

 桜子は、メモ帳の、初めてレビューを受けた日の見開きにある、五種類 の表のページを、もう一度、開いた。指摘、質問、提案、共有、称賛。あの五分類の隣に、桜子は、新しい欄を、ひとつ、書き足した。

- 動機 (オプション):
  そのレビュアーは、どの種類の夜を、覚えていてくれたのか

 書きながら、桜子は、これは、自分以外の誰かに見せるためのメモ、ではなく、自分が、いつか、誰かをレビューする日のための、自分への送付物だと、感じた。

 書きかけて、止めた一行のことを、思い出した。

 「山下さんは、わたしに、厳しすぎる?」

 桜子は、そのページを、わざわざ開いて、もう一度、ペン先を当てた。

 そして、その一行を、書かなかった代わりに、新しい一行を書いた。

- 山下さんは、わたしに、厳しい。たぶん、ずっと、これからも
  わたしも、いつか、誰かに、厳しくなれるかな

 メモ帳を閉じる。窓の外、街灯の下を、傘を持った人たちが、まばらに歩いていた。

 明日も雨だと、天気予報は言っていた。