空にしたはずの返事が、空ではなかった
5バイトと20バイト。安全デバイスの通報が4日間届かなくなっていた原因は、最後にはこの小さな数字に行き着いた。 昨日書いた通知の順番と同じ7月25日の話で、こちらのほうが先に見つかった。 というより、この障害を追いかけていたから、その日に安全デバイスの機能を頭から洗い直すことになった。
端末は位置や電池の残量をまとめて中継のサーバーへ送り、中継がそれをこちらのサーバーへ束で配達する。 配達を受け取ったら、こちらは「受け取った」と返事をする。この返事の決まりが、HTTPの200で、ボディが完全に空。 それ以外は受け取っていないとみなされる。 うちはそこに、処理した件数をJSONで書いて返していた。親切のつもりだったが、中継の側から見れば未達だ。 未達なら同じ束をもう一度送る。何度送っても返事はJSONなので、同じ束が永遠に先頭に居座り、 その後ろに並んだメッセージは一件も前へ進めなくなる。SOSも、その列の後ろにいた。
画面に出ていた症状はふたつ。端末の「最終受信」が4日前で止まっていること、そして止まった端末が通信途絶として警告されていること。 通信が切れたのではなく、こちらが受け取りを拒み続けていた。 警告の仕組みは正しく動いていて、ただ原因が自分たちだったというのが、いちばん嫌な形だった。
空にしたはずが、空ではなかった
直しは簡単なはずだった。返事をJSONから空の200に変えて、件数はログに出す。これで終わりだと思って本番に出した。 ところが中継の再送は止まらない。本番の生の応答を1バイト単位で見て、ようやく分かった。 アプリは空で返しているのに、手前のウェブサーバーが長さの分からない応答を分割転送の形に包み直していて、 空でも終端の印として5バイトが付く。さらに圧縮をかけるので、空でも圧縮の頭と尻尾だけで20バイトのスタブになる。 中継が求めているのは「完全に空」なので、5バイトでも20バイトでも未達だ。
答えは、応答に長さ0だと明示すること。長さが分かっていればウェブサーバーは分割も圧縮もせず、本当に何もないボディを返す。 最初の修正から8分後に、この1行を足した修正を重ねて出した。 テストには、200であることに加えて、長さ0のヘッダーがあることと本文が空文字であることを書いた。 誰かが気を利かせて件数を返すように戻しても、そこで落ちる。
同じ調査で、もうひとつ似た穴も塞いだ。束の中の1件が壊れていると例外で500を返していて、 それも束ごと再送になるので、同じ束の後ろにいる正常なSOSが処理されないまま保持期限で消えうる。 いまは1件ごとに例外を隔離して、壊れた1件だけを記録して先へ進む。 このデリヘル管理ツールの安全デバイスは、こちらの返事ひとつで全部が止まる。 返事の作法を仕様書どおりに守るだけの話なのに、ウェブサーバーが気を利かせる分まで読んでいなかった。
明日は、同じ端末で見つけたもうひとつの不達の原因、周りに無線LANが多すぎると通信が切れ続ける話を書くつもりだ。