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

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

契約検査器を作る

TT Labで続きを見る

目標

2つの.protoを比較して、ワイヤーの互換性違反を1行ずつ出力するprotocheck.pyを 作ります。ルールを1つずつ追加しながら、フィクスチャ8組と採点ツールが隠している組を通し、 最後にはディレクトリ単位で実行するcheck-all.shを作って、CIに入れられる 形に仕上げます。

なぜ重要なのか

前のラボで、番号を1つ変えると、旧クライアントがエラーなしで誤った値を読むことを 確認しました。その変更はコードレビューでは目立ちません。diffには数字が2つ 変わっただけで、コンパイルもテストも通ります。人が毎回見つけられる種類の ミスではありません。そのため、契約のチェックはレビュアーではなくスクリプトが行う必要があります。 チェッカーのルールはすべて、公式ドキュメントの「安全な変更・安全でない変更」の一覧から 導かれており、あなたが作るのは、その一覧を機械が読める形に書き写したものです。

ステップ

  1. /root/grpc/compat/protocheck.pyを作成し、python3 protocheck.py --dump <file.proto>が、メッセージごとのフィールドを<메시지> <번호> <타입> <이름>の形式で、予約を<메시지> reserved <번호>と<메시지> reserved "<이름>"の形式で、1行ずつ出力するようにしてください(プレースホルダーはメッセージ名、番号、型、名前です)。コメント(//、/* */)・空白・オプション([deprecated=true])は無視し、9 to 11は3つの番号に展開し、ネストしたメッセージはOuter.Innerと呼びます。
  2. python3 protocheck.py <old.proto> <new.proto>に最初のルールを入れてください。古い番号が新しいファイルになく、reservedでもなければREMOVED_NOT_RESERVED <메시지>.<이름>=<번호>です(プレースホルダーはメッセージ名、名前、番号です)。違反があればexit 1、なければ最後の行にOKを出力してexit 0です。
  3. 同じ名前が別の番号に移っていれば、RENUMBERED <메시지>.<이름> <옛번호>-><새번호>です(プレースホルダーはメッセージ名、名前、古い番号、新しい番号です)。
  4. 同じ番号・同じ名前で型が違えばTYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>(プレースホルダーはメッセージ名、名前、番号、古い型、新しい型です)、同じ番号で名前も型も違えばNUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>です(プレースホルダーはメッセージ名、番号、古い名前と型、新しい名前と型です)。
  5. 古いファイルがreservedにした番号(範囲を含む)を、新しいファイルがフィールドとして使えばREUSED_RESERVED <메시지>#<번호> <이름>です(プレースホルダーはメッセージ名、番号、名前です)。
  6. 同じ番号・同じ型で名前だけが違えば、WARN RENAMED <메시지>#<번호> <옛이름>-><새이름>を出力しますが、終了コードには影響を与えないでください(警告だけならOK・exit 0)(プレースホルダーはメッセージ名、番号、古い名前、新しい名前です)。
  7. /opt/app/grpc/compat/の8組をすべて実行して、/root/grpc/compat/07-report.txtに<case> OKまたは<case> FAILを1行ずつ書いてください。
  8. /root/grpc/compat/check-all.sh <디렉터리>を書いてください(プレースホルダーはディレクトリです)。その下でold.protoとnew.protoがある子ディレクトリごとにチェッカーを実行し、<case>: OK / <case>: FAILを出力して、FAILが1つでもあればexit 1とします。

参考

.protoを読むパーサーを作る

/root/grpc/compat/protocheck.pyを作成し、--dump <file.proto>がフィールドを<메시지> <번호> <타입> <이름>の形式で、予約を<메시지> reserved <번호> / <메시지> reserved "<이름>"の形式で、1行ずつ出力するようにしてください(プレースホルダーはメッセージ名、番号、型、名前です)。

まずコメントを消し(re.subを2回)、message 이름 {(プレースホルダーはメッセージ名です)を探して中括弧を数えながら対応を取り、その内側を読みます。内側にさらにmessageがあれば再帰しますが、名前はOuter.Innerと付けます。フィールドは타입 이름 = 번호;(プレースホルダーは型、名前、番号です)の正規表現1つで捕まえられ、先頭のoptional/repeatedは捨てます。

reserved 9 to 11, 15;はカンマで分けた後、toがあればrangeで展開します。reserved "foo";は名前の予約です。

採点ツールは、フィクスチャ以外にも、コメント・変わった空白・範囲の予約・ネスト・オプションが混ざったファイルを渡して試します。

予約なしで削除された番号を捕まえる

python3 protocheck.py <old> <new>に、REMOVED_NOT_RESERVED <메시지>.<이름>=<번호>のルールを入れてください(プレースホルダーはメッセージ名、名前、番号です)。違反があればexit 1、なければ最後の行OK・exit 0です。

メッセージごとに、古いファイルの番号を1つずつ見て、新しいファイルにあるか、なければ新しいファイルのreservedにあるかを確認します。どちらでもなければ違反です。remove_no_reservedとnested(Customer.tier)が不合格になり、safe_add・safe_reservedはOKになるはずです。

終了コード: sys.exit(1)は違反が1つでもあるときだけで、なければprint("OK")の後に0です。

番号が移ったものを捕まえる

RENUMBERED <메시지>.<이름> <옛번호>-><새번호>のルールを追加してください(プレースホルダーはメッセージ名、名前、古い番号、新しい番号です)。

新しいファイルで、名前 → 番号の辞書を先に作れば、1行で終わります。古い名前がその辞書にあり、番号が違えば違反です。この場合、その番号に対する他のチェック(REMOVEDなど)はスキップします。1つの原因に1行が望ましいからです。

renumberフィクスチャでは、idとqtyの2行が出力されるはずです。

型の変更と番号の再利用を捕まえる

TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>とNUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>のルールを追加してください(プレースホルダーはメッセージ名、名前、番号、古い型、新しい型、古い名前、新しい名前です)。

同じ番号が両方にあって型が違う場合、名前が同じならTYPE_CHANGED、名前も違えばNUMBER_REUSEDです。番号が新しいファイルに残っているので、REMOVEDではありません。

ネストしたメッセージ(Order.Item)の中の型の変更も捕まえられなければなりません。パーサーがOuter.Innerと名前を付けていれば、メッセージごとの比較がそのまま通用します。

予約された番号を取り出して使ったものを捕まえる

REUSED_RESERVED <메시지>#<번호> <이름>のルールを追加してください(プレースホルダーはメッセージ名、番号、名前です)。古いファイルのreserved範囲(10 to 12)も対象に含みます。

新しいファイルのフィールド番号が、古いファイルのreserved集合にあれば違反です。ステップ1で範囲を展開しておけば、in1つで終わります。reuse_reservedフィクスチャと、採点ツールが作るreserved 10 to 12+memo = 11の組が不合格になるはずで、同じファイルで13を使うのはOKです。

名前の変更は警告だけ

同じ番号・同じ型で名前だけが違えば、WARN RENAMED <메시지>#<번호> <옛이름>-><새이름>を出力しますが、終了コードには影響を与えないでください(プレースホルダーはメッセージ名、番号、古い名前、新しい名前です)。警告だけならOK・exit 0です。

警告とエラーを別のリストに集め、終了コードはエラーのリストだけを見ます。警告はエラーより先に出力し、エラーがなければ最後にOKを出力します。rename_onlyはWARN1行+OK、採点ツールが作る「名前の変更+型の変更」の組は、WARNとTYPE_CHANGEDが一緒に出力されてexit 1になります。

フィクスチャ8組をすべて実行する

/opt/app/grpc/compat/の8組をすべて実行して、/root/grpc/compat/07-report.txtに<case> OKまたは<case> FAILを1行ずつ書いてください。

for d in /opt/app/grpc/compat/*/; do ...; doneで実行し、終了コードでOK/FAILを決めます。手で書かないでください。採点ツールは、あなたのスクリプトをもう一度実行してレポートと突き合わせ、本当の答えとも突き合わせます。3つがすべて一致して合格です。

ディレクトリ単位のチェックスクリプト

/root/grpc/compat/check-all.sh <디렉터리>を書いてください(プレースホルダーはディレクトリです)。old.protoとnew.protoがある子ディレクトリごとにチェッカーを実行し、<case>: OK / <case>: FAILを出力して、FAILが1つでもあればexit 1としてください。

引数として受け取ったディレクトリを巡回しますが、2つのファイルが両方ある子ディレクトリだけを対象にします(ファイルや空のディレクトリはスキップします)。失敗の数を数えて、最後にexitします。採点ツールは、フィクスチャのディレクトリ以外にも、名前の違う一時ディレクトリを2つ作って実行します。パスを埋め込むと不合格になります。

protocheck.pyの場所は、このスクリプトがあるディレクトリを基準にすれば($(dirname "$0"))、どこから呼び出しても動きます。