フィールド名が変わり、合計だけが静かに狂った
目標
パートナーの注文APIの1.3と1.4を並べて、何がこちらを壊すのかをフィールド単位で見分けたあと、こちらが読むものだけを書いたコンシューマー契約と、その契約を毎回確認する契約テストを作ります。旧バージョンと新バージョンを同時に受け入れる読み取り層まで付けます。
なぜ重要なのか
接続が切れる事故は、アラートが鳴ります。フィールドの名前が変わる事故は、鳴りません。rec.get("region")は例外の代わりにNoneを返し、増えた列挙値はこちらの分岐のどこにも入らず、整数が十進文字列になると合計が黙って変わります。
そのため、統合の安全装置は「ドキュメントをよく読むこと」ではなく、機械が毎回照合する契約です。契約には、パートナーの全体のスキーマではなく、こちらが実際に読むフィールドだけを書きます。全部書くと、こちらが使わないフィールドの変更にも赤信号が点き、ノイズが増えると、人はテストを切ります。
バージョンアップは、一度で終わることもありません。旧バージョンと新バージョンが数か月一緒に動く間、読む側が両方を受け入れる必要があるので、名前が変わった箇所は別名表で、型が変わった箇所は正規化関数で、1か所にまとめます。
採点ツールは、作成した文章を信じません。作成したパートナーサーバーを、採点ツールが選んだポートで直接起動して応答を受け取り、作成した判定器と照合器を、採点ツールが作った入力で再度実行して、答えを合わせます。
ステップ
- /root/contract/partner.pyを作成してポート8011で起動し、2つのバージョンの応答を、/root/contract/v13.json、/root/contract/v14.jsonの順に保存してください。
- 2つの応答のフィールドの差を、/root/contract/diff.jsonに、added・removed・type_changed・enum_addedの4つの欄で書いてください。
- /root/contract/breaking.pyを作成して、変更1件を受け取って、こちらを壊すかどうかを判定するようにしてください。
- こちらが読むフィールドだけを書いたコンシューマー契約を、/root/contract/order.contract.jsonに書いてください。
- /root/contract/validate.pyを作成して、契約とレコードを照合し、違反をmissing・type・enumに分けて書くようにしてください。
- /root/contract/read_order.pyを作成して、1.3と1.4の応答を、どちらも同じ内部の形に移してください。
- /root/contract/contract_test.shを作成して、パートナーの現在の応答を契約と照合し、ずれていたら0以外のコードで終了するようにしてください。
- /root/contract/contract_report.mdに、4つの節で報告してください。
参考
- パートナーサーバーの実行契約:
python3 /root/contract/partner.py --port <포트> [--drift](プレースホルダーはポートです)。/healthは{"ok": true, "versions": ["1.3", "1.4"]}を、/v1.3/ordersと/v1.4/ordersは{"version": ..., "orders": [...]}を出力します。注文は24件です。 - 2つのバージョンの違いは次のとおりです。1.3は
order_id、amount(整数)、currency、status、region、updated_atを、1.4はorder_id、amount(十進文字列)、currency、status、market、channel、updated_atを出力します。1.4のstatusにはon_holdが追加されています。 --driftは、パートナーが通知なしにまた動いたバージョンです。ステップ7の契約テストが、これを0以外のコードで弾く必要があります。- 判定器の実行契約:
python3 breaking.py --change <파일>(プレースホルダーはファイルです)は、{"breaking": true|false, "reason": "..."}を出力します。変更のJSONは{"where": "response"|"request", "kind": "...", "field": "..."}で、kindはadd_field・remove_field・rename_field・type_change・add_enum_value・field_becomes_optional・add_optional_field・add_required_field・relax_requiredの9種類です。同じkind名が、応答側とリクエスト側の両方に出てくることがあり、そのとき、答えはそれぞれ異なります。表にないkindが来たら、安全な側ではなく、壊れると答えます。 - 照合器の実行契約:
python3 validate.py --contract <파일> --records <파일>(プレースホルダーはファイルです)は、{"records": n, "ok": n, "violations": [{"index": i, "field": f, "kind": k}]}を出力します。kindはmissing・type・enumです。型名は、string・integer・decimal_stringの3つです。okは、違反が1つもないレコードの数です。 - 読み取り層の実行契約:
python3 read_order.py --in <응답 파일>(プレースホルダーは応答ファイルです)は、正規化したレコードのリストを出力します。各レコードは、order_id、amount_krw(整数)、currency、status、marketの5つの欄です。 - 契約テストの実行契約:
bash contract_test.sh <BASE_URL>は、違反がなければ0、あれば1で終了します。 - このラボの判定ルール: 応答では、フィールドが追加されることだけが安全で、残りは壊すと見なします。リクエスト側は、必須フィールドが追加されることだけが壊します。これはコンシューマーの視点のルールであり、RFCが定めたものではありません。
- よくあるミス: 契約にパートナーのすべてのフィールドを書くこと(こちらが使わない変更にも赤信号が点きます)、金額をfloatで正規化すること(丸めが生じます)、パートナーサーバーをフォアグラウンドで起動してターミナルが塞がること。
- サーバーはバックグラウンドで起動し、
curl -sf http://127.0.0.1:8011/healthが通るまで待ってから、次に進みます。採点ツールは、起動しておいたプロセスを見ず、スクリプトを直接起動し直します。
2つのバージョンを一緒に提供するパートナーを起動する
/root/contract/partner.pyを作成してポート8011で起動し、/v1.3/ordersと/v1.4/ordersの応答を、順に、/root/contract/v13.json、/root/contract/v14.jsonに保存してください。注文は2つのバージョンとも24件です。
flaskで3つのパスを作ります。/healthは準備ができたことを知らせる場所で、2つのバージョンの一覧のパスは、同じ注文24件を、それぞれのフィールド名と型で出力します。--portと--driftをargparseで受け取ってください。フォアグラウンドで起動するとターミナルが塞がるので、バックグラウンドで起動して、/healthが200になるまで待ちます。
2つのバージョンの差を機械が読める形で取り出す
/root/contract/diff.jsonに、added、removed、type_changed、enum_addedの4つの欄を書いてください。addedとremovedはフィールド名のソートされたリスト、type_changedは{"field": ..., "from": ..., "to": ...}のリスト(型名はinteger・string)、enum_addedは{"field": ..., "values": [...]}のリストです。列挙と見なすフィールドは、新バージョンで異なる値が6個以下のフィールドに限定します。
2つの応答ファイルの最初のレコードだけを見れば、フィールド名の集合が得られますが、列挙値は全件を調べて初めて見えます。型名は、integerとstringの2つで十分です。statusのように値が数種類しかないフィールドは、両側の値の集合を引いてみてください。
何がこちらを壊すのかを見分ける
/root/contract/breaking.pyを作成して、--change <파일>(プレースホルダーはファイルです)で受け取った変更1件を判定し、{"breaking": true|false, "reason": "..."}を出力するようにしてください。応答側とリクエスト側のルールが違います。
応答はこちらが読む側なので、追加されることだけが安全です。リクエストはこちらが送る側なので、方向が逆です。任意フィールドが増える、または必須が任意に緩和されることは、こちらにとって何でもありません。(where, kind)の組をキーにした表1つで十分で、知らない組は、安全な側ではなく、壊れると答えてください。
こちらが読むものだけを書いた契約
/root/contract/order.contract.jsonにコンシューマー契約を書いてください。versionは1.4、unknown_fieldsはignore、fieldsには、こちらが実際に読む5つのフィールド(order_id・amount・currency・status・market)だけを、それぞれtypeとrequiredで書きます。currency・status・marketには、enumも付けます。
パートナーが出力するフィールドをすべて書きたくなりますが、こちらが読まないフィールド(channel・updated_at)を書くと、相手がそのフィールドを変えるたびに、こちらのテストが赤信号を出します。型名はstring・integer・decimal_stringの3つで、1.4の金額は十進文字列です。statusの列挙値は、1.4の応答を全件調べて得てください。
契約と実際の応答を照合する
/root/contract/validate.pyを作成して、--contractと--recordsを受け取り、{"records": n, "ok": n, "violations": [...]}を出力するようにしてください。違反はmissing、type、enumの3つに分け、各違反にindexとfieldを付けます。
レコード1件に違反が2つあることもあるので、violationsはレコードごとに複数行になりえます。一方、okは違反が1つもないレコードの数なので、違反の行数を引く方式では出ません。必須でないフィールドがないことは違反ではなく、型がすでに間違っている値に、列挙をさらに照合しないでください。
旧バージョンと新バージョンを一緒に受け入れる
/root/contract/read_order.pyを作成して、--in <응답 파일>(プレースホルダーは応答ファイルです)で1.3と1.4の応答をどちらも読み、同じ内部の形に移してください。各レコードは、order_id、amount_krw(整数)、currency、status、marketの5つの欄で、知らないフィールドは捨てます。
名前が変わった箇所は別名表1つで、型が変わった箇所は正規化関数1つでまとめます。あちこちにifをばらまくと、旧バージョンを切るときに、どこを消せばよいかわからなくなります。金額はfloatを経由しないでください。"12300.00"は、ドットを基準に切って整数に移せば、丸めが生じません。
通知なしにまた動いたパートナーを検出する
/root/contract/contract_test.shを作成して、bash contract_test.sh <BASE_URL>で、パートナーの現在の/v1.4/ordersを契約と照合するようにしてください。違反がなければ0、あれば1で終了する必要があり、違反があるときは、何件かが画面に残る必要があります。
前に作ったvalidate.pyとorder.contract.jsonを、そのまま使います。新しく作るのは、応答を取得する部分と終了コードだけです。--driftで起動したパートナーに対して実行して1が出るか、通常のパートナーに対して実行して0が出るかを、両方確認してください。
バージョンアップ点検の報告書
/root/contract/contract_report.mdに、## 무엇이 바뀌었나、## 무엇이 우리를 깨뜨리나、## 우리가 지킬 계약、## 다음부터 어떻게 잡나(韓国語の見出しで、順に何が変わったか、何がこちらを壊すか、こちらが守る契約、次からどう検出するか、を意味します)の4つの節で書いてください。前のステップで得たフィールド名と違反件数が、本文に入っている必要があります。
読む人は、こちらのチーム長ではなく、パートナー企業の担当者かもしれません。「壊れた」ではなく、「どのフィールドがどう変わって、こちら側の何がずれたか」で書いてください。4つの節のタイトルはそのままにして、数字は、自分が得た値で埋めます。