この記事は、株式会社ACCESS Advent Calendar 2023 の19日目の記事です。
私個人としては今年はこれ含めてまだ2個しか記事投稿していないことに気づきましたが、去年一昨年が年1個だったのと比べると2倍に増えたとポジティブに捉えることにします。
はじめに
多くのツールは、設定やパラメーターでその挙動を制御できるとともに、指定しなかった場合のデフォルト動作が存在します。それを理解せずに使うと、思いもしなかった挙動に直面する危険性があります。
私個人やうちのチームが実際に体験した例を(脚色も交えながら)いくつか挙げることで、デフォルト動作を理解して使うことの大切さをこの年末に改めて再認識する機会にしょうと思います。
1) RubyのNFKモジュールで半角カナ変換時に文字化け
やろうとしたこと: 全角カタカナに変換
Rubyで半角カタカナを全角カタカナに変換したいです。
日本語のコード変換というとNKF、という知識からドキュメントを見ると、 -X X0201片仮名(いわゆる半角片仮名)をX0208の片仮名(いわゆる全角片仮名)に変換する
というオプションがあるのを見つけたので、これを使います。
require 'nkf'
NKF.nkf('-X', 'マイクロソフト、設計図共有サイトを8200億円で買収')
これだと no output encoding given (ArgumentError)
というエラーになってしまいました。出力コードの明示が必要そうなので再度ドキュメントにあたると -w UTF-8 を出力する(BOMなし)
というオプションを見つけました。
NKF.nkf('-X -w', 'マイクロソフト、設計図共有サイトを8200億円で買収')
# => "マイクロソフト、設計図共有サイトを8200億円で買収"
これでOK!
と思いきや、しばらくして以下のような問題に気づきました。
発生した問題: 一部の文字列が文字化け
NKF.nkf('-X -w', 'Hermès')
# => "Herm竪s"
NKF.nkf('-X -w', 'Hermès エルメス')
# => "Hermès エルメス"
なんか Herm竪s
って文字化けしてます。
ただ、同じような文字列でも、期待通りに動くこともあるようです。
原因: 自動判別がデフォルト動作
デフォルト動作として入力文字列のエンコーディングが自動判別されており、問題のケースだとEUC-JPと見なされていることがguessしてみるとわかります。
NKF.guess('Hermès')
# => #<Encoding:EUC-JP>
NKF.guess('Hermès エルメス')
# => #<Encoding:UTF-8>
対処: 入力コードを明示する
対策としては、出力コードを指定したのと同様に、入力コードも指定すればOKです。私の手元ではUTF-8なので -W 入力に UTF-8 を仮定する
のオプションを使います。
NKF.nkf('-X -w -W', 'Hermès')
# => "Hermès"
前述のドキュメントではデフォルト動作まで明示的に書かれてはいないので、コーディング時に問題に気づく難易度は少し高かったかもしれません。
2) JavaプログラムからのHTTPリクエストで文字化け
やろうとしたこと: ファイル名をJSONでPOST
古のAndroid OSに入っていた Apatche HTTP Client
(Android 6.0ではOSから削除され、org.apache.http.legacy ライブラリという名前に変わったもの) を使って、ファイル名をHTTP POSTする古いAndroidアプリ実装がありました。
JSONObject json = new JSONObject();
json.put("file_name", fileName);
json.put("content_type", contentType);
json.put("size", size);
StringEntity entity = new StringEntity(jsonSent.toString());
// このentityをPOSTする
発生した問題: このリクエストを受けたRails側でランタイムエラー
このリクエストをRails appで受け、リクエスト内容を再構成して更にバックエンドのデータベースサーバーにJSONで投げる処理において、 JSON::GeneratorError
source sequence is illegal/malformed utf-8
というエラーが発生することがありました。スタックトレースから、ファイル名を表す文字列が含まれ、ファイル名によって再現性が変わることが推測されました。
まずはサーバーのRails実装を、次にクライアントのAndroid実装を調査しました。
原因: StringEntityインスタンス化の際に文字コードを指定していない
使っているコンストラクターは StringEntity(String string)
ですが、それとは別に StringEntity(final String s, String charset)
というのがあります。
バージョンは違いますが https://javadoc.io/doc/org.apache.httpcomponents/httpcore/4.4.4/org/apache/http/entity/StringEntity.html でみるとcharsetのデフォルトは DEF_CONTENT_CHARSET
という定数で定義されていて、実動作を見ると値は ISO-8859-1
のようでした。
つまり、本来はUTF-8でエンコードしたい(API仕様)のに、ISO-8859-1でエンコードされていて、その結果文字化けした文字列をサーバーアプリが処理する際にランタイムエラーに繋がっているようでした。
対処: 入力コードを明示する
1個前のNKFの事例と同じです。
この問題は、長らくASCIIのみを使ったファイル名を自前でつけてサーバーに送信していた実装に加えて、アプリ外で作られたファイルも選べるようにした結果、既存のロジックで任意のファイル名も処理するようになったことで問題動作が顕在化したということになります。影響確認が漏れていた形ですね。
3) Antikythera Frameworkで重めの処理がエラー
やろうとしたこと: Webリクエストを受けて重めの処理をする
Antikythera Frameworkは当社が開発したWebアプリケーションフレームワークで、OSSとしても公開しています。
https://github.com/access-company/antikythera
よくある処理として、Webリクエストを受け付けたらそのパラメーターに従いバックエンドのデータベースからクエリし、その結果を返す実装をしました。
発生した問題: リクエスト処理中のタイムアウト
ところが、たまにこのエンドポイントだけがエラーになることがありました。
原因: アクションの処理は10秒でタイムアウトするのがデフォルト動作
原因は単純で、タイムアウトでした。 ドキュメント「Implementing Controller」 には Controller action must return a response within 10 seconds.
とあります。
10秒というデフォルトタイムアウト値が開発者が思っていたより短めであったため、比較的重めのDBクエリ処理をするこのエンドポイントではそれに抵触する可能性が高いことを認知できていませんでした。
対処
https://hexdocs.pm/antikythera/limitations.html にも記載がありますが、Routerの設定にてAPIごとに個別に、少し長めのタイムアウト設定することができます。
もしWeb APIで処理することができないほど時間がかかるのであれば AsyncJob の利用も検討すべきでしょう。
4) CircleCIのキャッシュが効かない
やろうとしたこと: PRごとにCI
Railsアプリの修正をプルリクエスト作成を契機にCircleCIでテストする仕組みを構築しています。
発生した問題: 処理時間が長い
ところがこの実行に比較的時間がかかり、CIがpassしてからレビューするという流れにおいて、若干の待ちが発生していることがわかりました。
CircleCIのドキュメントには キャッシュは、CircleCI でのジョブを高速化する最も効果的な方法の 1 つです
とある通りキャッシュの活用に問題がある可能性をまずは疑います。しかし、我々はRubyのOrbsを使っているため、その説明に Easily cache and install your Ruby Gems automatically
とあるようにOrbsがよしなにキャッシュ制御してくれていると考えていました。
ところが実際にログを確認するところ、ブランチが異なるPRではBundle Installのキャッシュが効いていないことがわかりました。
原因: ブランチが異なるとキャッシュも分かれるのがデフォルト動作
前述のRuby Orbsのドキュメントをみると、 include-branch-in-cache-key
If true, this cache bucket will only apply to jobs within the same branch.
というコンフィギュレーションがデフォルト true
であることがわかります。つまり、デフォルトではキャッシュキーにブランチ名を含める = ブランチが変わるとキャッシュは利用しない動作になるわけです。
対処: 色々
CI高速化の文脈で言うと、キャッシュはその一要因でしかないので(時間の都合で省略)
なおこの件は、別に速度が問題になってから対処するので遅くはなく、CircleCI導入当時としてその動作を認知できていなかったこと自体は何ら問題ない性質の話かなとは思います。
最後に
記事にしてみて思いましたが、ベストプラクティスに従ったり、最近だと生成AIを使っていれば、このような間違いはしづらい時代になってきているかもしれません。
ただ、とはいえ(あるいは、そのように意味も分からずコピペで動いちゃうからこそ)、設計や実装の段階はもちろんのこと、障害解析のケースでも「このモジュールのデフォルト動作はなんだろうか?」と考え調べることは大いに価値があるのだろうと改めて感じました。
ACCESS Advent Calendar 2023、明日は初日に引き続き @naohikowatanabe のChatGPTネタです!