TT Lab
はじめる
学ぶ 学習パス コース

フィールド番号を変えたら、古いクライアントが黙って間違った値を読んだ

古いクライアントの前でスキーマを変える

TT Labで続きを見る

目標

v1スキーマをコードに埋め込んだ「旧クライアント」はそのままにして、スキーマをv2に変更します。 新しいフィールドを新しい番号で追加し、削除したフィールドをreservedで封印し、わざと番号を 入れ替えたバイト列を旧クライアントがエラーなしでどう誤って読むかを再現します。その後、 デフォルト値とpresenceの落とし穴を実測し、optionalとreservedを備えた最終 スキーマと互換性の表を残します。

なぜ重要なのか

クライアントとサーバーは、決して同じ瞬間にはデプロイされません。ロールバックもあり、ログに 残った古いバイトもあります。そのため、スキーマ変更の安全性は「コンパイルが通るか」では なく、「古いバイナリが新しいバイトを、新しいバイナリが古いバイトを正しく読めるか」で 判断しなければなりません。ワイヤーには名前がなく番号だけがあるので、番号を変えることは フィールドを削除して新しく作ることと同じです。ところがパーサーは何のエラーも出さず、 変更された番号の値を古い名前で読みます。このラボは、その事故を再現して記憶に 刻むことが目的です。

ステップ

  1. python3 /opt/app/grpc/old_client.py /opt/app/grpc/order_v1.binを実行し、出力された4行を/root/grpc/evolve/01-old.txtにそのまま保存してください。
  2. /root/grpc/evolve/v2.protoを書いてください。syntax = "proto3";で始め、message Orderにv1のint32 id = 1; int32 qty = 2; string note = 3;をそのまま残し、int64 unit_priceとstring currencyを新しい番号で追加します。
  3. v2.protoからnoteを削除し、reserved 3;とreserved "note";を残してください。残りはそのままです。
  4. /root/grpc/evolve/make_v2.pyで/opt/app/grpc/pbmini.pyを使い、id=9001, qty=2, unit_price=15000, currency="KRW"をv2.protoの番号どおりにエンコードして/root/grpc/evolve/order_v2.binに保存し、旧クライアントでそのファイルを読んだ出力を/root/grpc/evolve/04-old-reads-v2.txtに保存してください。
  5. /opt/app/grpc/renumbered.binは、/opt/app/grpc/renumbered.proto(番号を振り直したスキーマ)で作った同じ注文です。旧クライアントで読んだ出力を/root/grpc/evolve/05-renumbered.txtに保存し、その下にtruth: id=<진짜 id> qty=<진짜 qty>を1行(プレースホルダーは本当のidと本当のqtyです)、why: <왜 오류 없이 틀린 값이 나왔는지>を1行(プレースホルダーは、なぜエラーなしで誤った値が出たのかの説明です)書いてください。
  6. pbminiで、id=9002, currency="KRW"かつqty=0の注文を2通り作ってください。/root/grpc/evolve/06-zero.binは暗黙的なpresence(0なら送らない)、/root/grpc/evolve/06-zero-explicit.binは明示的なpresence(0も送る)です。/root/grpc/evolve/06-presence.txtに、implicit=<16진수> explicit=<16진수> implicit_qty=<옛 클라이언트가 읽은 qty> explicit_qty=<옛 클라이언트가 읽은 qty>の4行を書いてください(プレースホルダーは16進数と、旧クライアントが読んだqtyです)。
  7. 最終の/root/grpc/evolve/Order.protoを書いてください。optional int32 qty = 2;、reserved 3;とreserved "note";、unit_priceとcurrency、そしてmessage Customer { string name = 1; int32 tier = 2; }とOrderのCustomer customer = <새 번호>;が入ります(プレースホルダーは新しい番号です)。
  8. /root/grpc/evolve/compat.mdにマークダウンの表を書いてください。最初の列はadd-new-number remove-and-reserve reuse-number renumber int32-to-string rename-onlyの6つのキー、2列目はsafeまたはunsafe、3列目は理由を1行で書きます。

参考

旧クライアントでv1のバイト列を読む

python3 /opt/app/grpc/old_client.py /opt/app/grpc/order_v1.binの出力の4行を、/root/grpc/evolve/01-old.txtにそのまま保存してください。

mkdir -p /root/grpc/evolveの後に、出力を>で保存すれば済みます。旧クライアントはフィールド1をid、2をqty、3をnoteだと信じて読み、それ以外の番号はunknownに書き出します。手で書き写さずに、出力を保存してください。採点ツールが同じコマンドをもう一度実行して比較します。

新しいフィールドは新しい番号で

/root/grpc/evolve/v2.protoにv1の3つのフィールドをそのまま残し、int64 unit_priceとstring currencyを新しい番号で追加してください。

既存のフィールドの番号・名前・型は、1文字も変えません。新しいフィールドには、これまで使われたことのない番号を割り当てます。4と5が自然ですが、6と7でも構いません。19,000–19,999は実装が予約している区間なので使えず、1–15はタグが1バイトなので、よく使うフィールドに向いています。

採点ツールはsyntax = "proto3";とmessage Order { ... }を探し、フィールド1・2がv1と同じか、unit_priceがint64でcurrencyがstringか、その番号が1・2・3ではないかを確認します。

削除した番号を封印する

v2.protoからnoteを削除し、reserved 3;とreserved "note";を残してください。

削除すること自体は、ワイヤー上で安全です。危険なのは後で誰かが3をもう一度使うことで、reservedは、protocがそれをコンパイルエラーで止めるための仕組みです。番号と名前は1つの文に混ぜて書けないので、2行に分けて書きます。

reserved 3;は、messageブロックの中のどこにあっても構いません。

v2のバイト列を旧クライアントで読む

/root/grpc/evolve/make_v2.pyでpbminiを使い、id=9001, qty=2, unit_price=15000, currency="KRW"をv2.protoの番号どおりにエンコードして/root/grpc/evolve/order_v2.binに保存し、旧クライアントで読んだ出力を/root/grpc/evolve/04-old-reads-v2.txtに保存してください。

pb.encode_message([(1, "int32", 9001), (2, "int32", 2), (4, "int64", 15000), (5, "string", "KRW")])のように、v2.protoで選んだ番号をそのまま使います。note(3)は削除したフィールドなので入れません。

旧クライアントは4・5を知りませんが、ワイヤータイプから長さがわかるので読み飛ばし、id・qtyは正しく読みます。unknown=の行に、その2つの番号が表示されるはずです。

番号を入れ替えたバイト列を旧クライアントで読む

旧クライアントで/opt/app/grpc/renumbered.binを読んだ出力を/root/grpc/evolve/05-renumbered.txtに保存し、truth: id=<진짜> qty=<진짜>とwhy: <이유>の2行を書き足してください(プレースホルダーは本当の値と理由です)。

まず/opt/app/grpc/renumbered.protoを読んでください。誰かが番号を振り直しています。同じ注文(order_v1.binと同じ値)をそのスキーマでシリアライズしたものがrenumbered.binです。

旧クライアントは、番号1を相変わらずidだと信じています。ワイヤータイプが同じ(どちらもVARINT)なので、エラーも警告もなく、値が入れ替わって出力されます。truthの行は、renumbered.protoのとおりに読んだ本当の値です。

qty=0は送ったのか、送っていないのか

id=9002, currency="KRW", qty=0の注文を、/root/grpc/evolve/06-zero.bin(暗黙的なpresence、0は送らない)と/root/grpc/evolve/06-zero-explicit.bin(明示的なpresence、0も送る)として作り、/root/grpc/evolve/06-presence.txtにimplicit= explicit= implicit_qty= explicit_qty=の4行を書いてください。

proto3の修飾子のないスカラーは、デフォルト値(0、"")をシリアライズしません。そのため、暗黙的な側はフィールド2のレコードをまったく入れず、明示的(optional)な側は(2, "int32", 0)を入れます。10 00の2バイトが余分に載ります。

2つのファイルを旧クライアントでそれぞれ読ませて、qtyを書いてください。どちらも0になります。旧クライアントは「0を受け取った」と「何も受け取れなかった」を区別できません。これがoptionalが必要な理由です。

optionalとreservedを備えた最終スキーマ

/root/grpc/evolve/Order.protoを書いてください。optional int32 qty = 2;、reserved 3;とreserved "note";、int64 unit_priceとstring currency、message Customer { string name = 1; int32 tier = 2; }とOrderのCustomer customer = <새 번호>;を含めます(プレースホルダーは新しい番号です)。

optional修飾子を付けると、qtyにexplicit presenceが生まれ、0を送ったことと送っていないことを区別できるようになります。バイナリのワイヤーでは、修飾子を付けること自体はバイトを変えません(0が送られるようになるだけです)。

Customerは別のmessageにして、Orderから型として使います。customerの番号は、これまで使われたことがなく、reservedでもないものにしてください。

互換性の表を書く

/root/grpc/evolve/compat.mdにマークダウンの表を書いてください。最初の列はadd-new-number remove-and-reserve reuse-number renumber int32-to-string rename-only、2列目はsafe/unsafe、3列目は理由です。

このラボで自分の目で見たことを整理するステップです。ステップ4(新しい番号で追加)、ステップ3(削除してreserved)、ステップ5(番号の入れ替え)が、それぞれ1行の根拠になります。型の変更は、ワイヤータイプが変わる場合(int32 VARINT → string LEN)、古いパーサーが値を別の意味で読みます。名前だけを変えるのは、バイナリのワイヤーに名前がないので、バイトは同じです。

判定の語はsafeかunsafeのどちらか1つだけを使ってください。採点ツールは、行の中で最初に出てくる判定の語を見ます。