僕が SE になって初めて起こした本番障害のバグ

今年2023年の4月をもって、僕は SE として丸10年働いたことになる。転職や休職も経験したが、2013年の4月に新卒入社してから10年間、一応はプログラマ・SE として仕事をしてきた。

今回は、そんな僕が10年前、新卒1年目の時にやらかした、本番障害とバグの話を紹介する。


僕は2013年4月に新卒入社し、新人研修を3ヶ月ミッチリやって、参画する案件が決まるまでさらに1ヶ月間研修をして、8月から現場配属になった。最初の現場では客先社内の業務管理用のウェブアプリみたいなモノの要件定義フェーズから携わった。僕はウェブサイトを作ってきた経験があったので、HTML・CSS・簡単な JS を書いて「動く画面イメージ」をこしらえて、新卒ながら顧客にプレゼンしたりしてた。

それからも自分はその開発案件の主要メンバとして、複数の画面の設計・開発・テストをこなした。チーム全体としては順調にスケジュール遅延して炎上したりしながら、翌年2014年の3月頭に本番リリースへとこぎつけた。自分が作った画面が実際に本番稼動し、お客様が使い始めているというのは、ワクワク半分、ドキドキ半分といった感じだった。

リリースから1週間ほどして、その「ドキドキ」が現実のモノとなってしまった。

ある朝、お客様から問合せがあった。

○○画面にある『添付ファイル』欄から、対象のファイルをダウンロードできず違うファイルがダウンロードできてしまう」というモノだった。

状況をイメージするには、Redmine や Backlog などのチケット管理システム、もしくは社内 Wiki みたいなモノを思い浮かべてもらいたい。1つのチケットに、添付できるファイルの上限が9つと決まっていて、そのファイルリンクが 3×3 マスの表に表示されている、という UI だった。以下に当時の UI とバグを再現したデモ画面を作ってみたので、触ってみてほしい。

ちなみに、当時の現物は JSP (Java サーブレット) で作っていたが、今回のデモは JavaScript で再現している。バグの本質は言語の違いではないので気になさらず。

表の左上、「File 1」のマスにカーソルを当てると、選択された実際のファイル名は file-1.txt と表示される。コレは正しい動きだ。

しかし、「File 2」のマスにカーソルを当てると、選択されるファイル名は file-4.txt となり、表示されている番号と実際のファイル名の番号がズレてしまっている。

じゃあ file-2.txt はどこにあるのかというと、「File 4」のマスにカーソルを当てると表示される。

表のセルに表示されている番号と、カーソルを当てて取得できる実際のファイル名とが一致しているのは、「File 1」「File 5」「File 9」の時だけで、後はセルの番号と実際のファイル名がチグハグになってしまっている。

コレは元々「添付ファイル欄」だったので、ファイルを2つしかアップロードしていない時は、「File 2」をクリックすると file-4.txt を探しに行ってしまい 404 となり、「File 4」の欄は空欄になっていたので、file-2.txt をダウンロードする術がない、というような状態になっていた。サーバ上にはアップロードされたファイル自体は残っているのだが、お客様からしてみれば「アップロードしたファイルがダウンロードできない」という挙動になる。


さて、それではこのバグが発生する実際のコードを見てみよう。

// セルに表示するタイトルと、実際のファイル名とを宣言したオブジェクトの配列
const items = [
  { title: 'File 1', fileName: 'file-1.txt' },
  { title: 'File 2', fileName: 'file-2.txt' },
  { title: 'File 3', fileName: 'file-3.txt' },
  { title: 'File 4', fileName: 'file-4.txt' },
  { title: 'File 5', fileName: 'file-5.txt' },
  { title: 'File 6', fileName: 'file-6.txt' },
  { title: 'File 7', fileName: 'file-7.txt' },
  { title: 'File 8', fileName: 'file-8.txt' },
  { title: 'File 9', fileName: 'file-9.txt' }
];

// セルに触れた時の処理をとりあえず実装しておく
window.onSelect = (item) => {
  document.getElementById('selected-item').innerText = item;
};

// テーブル HTML を組み立てる
let tableHtml = '<table>';
for(let row = 0; row < 3; row++) {
  tableHtml += '<tr>';
  for(let col = 0; col < 3; col++) {
    tableHtml += `<td onmouseover="onSelect('${items[3 * col + row].fileName}')" onmouseleave="onSelect('')">${items[3 * row + col].title}</td>`;  // この行にバグがあります
  }
  tableHtml += '</tr>';
}
tableHtml += '</table>';
// テーブル HTML を画面に出力する
document.getElementById('container').innerHTML = tableHtml;

tableHtml という変数に、文字列結合でテーブルの HTML を組み立てているワケだが、コレが実際には JSP で書いていた、という違いだけ。バグの本質はそこではない。

tabletrtd 要素の構成に従って、二重の for ループがあり、外側のループで行を、内側のループで列 (セル) を組み立てている。添付ファイルの上限は9つなので、3列・3行となるように 3 は決め打ちでループを回している。

そしてバグが潜む問題の行は、二重の for ループ内で td 要素を組み立てているところ。選択したセルのファイル番号と、実際に取得できるファイル名の番号が異なってしまうバグの原因が分かっただろうか?


まず、セルに表示するタイトルは、配列 items の添字を指定して次のように参照している。

items[3 * row + col].title

当然ながら for ループは 0 始まりなので、1行目・2列目のセルは items[3 * 0 + 1].title となり、items[1].title、つまり File 2 という文字列が取得できる。

そして <td onmouseover=""> 部分で指定している、実際のファイル名を取得する部分は、配列 items の添字を次のように指定している。

items[3 * col + row].fileName

よーく読み比べてみてほしい。そう、3 に掛け算する値が rowcol とで違ってしまっているのだ。コチラの場合、1行目・2列目のセルでは items[3 * 1 + 0].fileName となり、items[3].fileName、つまり file-4.txt というファイル名を取得してしまうワケだ。

改めてデモ画面でカーソルを動かしてみてほしいのだが、「File 1」「File 4」「File 7」と順にカーソルを移動させると、file-1.txtfile-2.txtfile-3.txt とファイル名は順番に取得できていることが分かる。セルに対する並び順が横並びなのか縦並びなのか、というのが、添字の指定の仕方によってチグハグになってしまったのである。


最初はどうしてこういうバグが発生しているのか全く分からなかった。ファイルを4つだけなど、9つまでアップロードしていないタイミングでは余計に意味不明な挙動に感じられたので、調査から原因特定までに半日ほどかかった。

この画面の設計書上は、タイトルの表示どおり横並びに表示されるのが正。つまりファイル名を取得する部分の添字が間違っていたのだ。

items[3 * col + row].fileName;  // ← コレが誤り

items[3 * row + col].fileName;  // ← こうすべきだった

単純なコーディングミスだったが、細かすぎるあまり、実装工程での上長による静的コードレビューはすり抜けてしまっていた。また、テスト時は

というテスト項目だけにしてしまっていたので、2・3・4・6・7・8個目のファイルが正常にダウンロードできないことに気付けなかった。

アップロード処理を含めて類似の機能を持った既存画面があったので、そこからコードをツギハギして作った結果、こんなショボいミスを生み不十分なテストケースでテストをすり抜けてしまったワケだ。

アップロードしたファイルが消えてしまったワケではないので、リンク部分のコードを直せば済む程度のバグだったのが不幸中の幸い。先輩や上司達は「しゃーない、こんなミスよくあること」とフォローしてくれたし、お客様も「すぐ直せるなら直しといてー」という感じで楽観的だった。

しかし、こんな些細なミスでも PM は「障害報告書」を書いて、お客様に頭を下げていた。僕は PM に頭を下げさせるようなことをしてしまったのだ。バグの程度の問題ではない。なんならショボい程度のバグで再リリースになってしまって、自分のミスが本当に恥ずかしく、悔しく、周りに申し訳ない気持ちでいっぱいだった。


結局このバグは、前述のとおりコードを修正し、

…という形で、大きく9パターンのテストケースをサボらずキッチリこなし、再リリースすることで不具合解消できた。

「初の本番障害がこの程度で良かったじゃないか〜」とフォローしてくれた上司の発言のとおり、この後もっとひどいバグに悩まされるのだが、それは以前話した。w


このバグを通じて、僕は新人なりに良い教訓を得た。

// 例えばこんな感じ
let tableHtml = '<table>';
for(let i = 0; i < 9; i++) {
  const item = items[i];
  const isRowBegin = (i === 0 || i === 3 || i === 6);  // ココをマジックナンバーにしない方法もあるけど
  const isRowEnd   = (i === 2 || i === 5 || i === 8);  // コレでも二重ループよりはまだ分かりやすいかも
  if(isRowBegin) tableHtml += '<tr>';
  tableHtml += `<td onmouseover="onSelect('${item.fileName}')" onmouseleave="onSelect('')">${item.title}</td>`;
  if(isRowEnd) tableHtml += '</tr>';
}
tableHtml += '</table>';

確かに、データが破損・欠落するような重大なバグではなくて良かった。簡単なミスで、被害も少ないバグで「本番障害」の重たさを経験できて、運が良かったのだと思う。

だが、僕はこのミスで一気に血の気が引いたし、それ以来システム開発を「ミスなく」「確実に」行おうという意識が強くなった。


中小企業に転職してみて、この時の現場のようなキチッとした案件など皆無で、「なんとなくイケるっしょ!」みたいな大学生的なノリで皆が仕事していてウンザリした。だがそれも数年在籍すると、単純接触効果で慣れてしまったところがある。僕が思う水準の品質はこの会社では求められておらず、抜け漏れや失敗があっても別にいい。この会社はそういう場所であり、皆それで良しとされているのだ。

郷に入っては郷に従え、で良いのか。そこそこの品質でバグの不安がある成果物を納品する、そんな今の仕事のやり方に合わせていけば良いのだろうか。コレから先、どういう風に働いていこうかのう。