Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Unified Diff 形式の差分から Go AST を構築して feature flag ...

Unified Diff 形式の差分から Go AST を構築して feature flag を自動計装する

Go Conference 2024 の登壇資料

https://gocon.jp/2024/sessions/11/

Shota Iwami

June 07, 2024
Tweet

More Decks by Shota Iwami

Other Decks in Technology

Transcript

  1. Uni fi ed Di ff 形式の差分から Go AST を構築して feature

    fl ag を 自 動計装する 株式会社サイバーエージェント 岩 見 彰太 GitHub:@BIwashi X: @B_Sardine Go Conference 2024
  2. 自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW

    アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ
  3. 自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW

    アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ
  4. 1 .トランクベース開発と feature fl ag 2 .feature fl ag と

    QA 品質 3 .feature fl ag を 自 動計装したい 4 .uber-go/gopatch を活 用 する 5 .feature fl ag を 自 動計装する 6 .まとめ
  5. トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2

    日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ
  6. トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2

    日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ
  7. feature fl ag とは • コードを変更することなくシステムの振る舞いを変更可能にする仕組み • 利点と使い所や課題 •トランクベース開発においては修正を fl

    ag 付きで実装しておくことで、その機能の反映可否 をコードを変更することなく 行 えるようにできる •「 fl ag 付きで実装する = fl ag を含む if 分岐で実装を囲んでおくことによっていつでもその実装を有効化無効化できるよ うに実装をしておく」 •役割を終えたら feature fl ag を削除する、といいうのを徹底してやらないとコードが複雑化 してしまう •feature fl ag の実装は属 人 化しがちなので、早めに実装した 人 が削除しないと簡単に負債化してしまう The Go gopher was designed by Renée French. GO Feature Flag
  8. HOGE_FLAG := true if HOGE_FLAG == true { // flag

    ON ͷ࣌ʹ࣮ߦ͍ͨ͠ॲཧ // ex.) ൓ө͍ͨ͠मਖ਼ // ex.) AB ςετͷ A Λ༗ޮԽ͢Δ // ... } else { // flag OFF or flag Λઃఆ͍ͯ͠ͳ͍࣌ʹ // ࣮ߦ͍ͨ͠ॲཧ // ex.) मਖ਼લͷطଘͷ࣮૷ // ex.) AB ςετͷ B Λ༗ޮԽ͢Δ // ... } 実装イメージ • HOGE_FLAG とういう fl ag が ON だっ た場合と OFF だった場合の処理を if 文 で書く • AB テストや特定の実装を有効化する場 合などに使える The Go gopher was designed by Renée French. GO Feature Flag
  9. feature fl ag と QA 品質 • QAによる品質担保を 高 めるためには、できるだけ細かい単位で

    feature fl ag を 入 れて QA をこまめに実 行 する必要がある 実装の複雑化、実装コストとQA品質はトレードオフ • 全てのテストが CI によって 自 動化できればいいが、現実はそう 甘 くない QAフローがある程度必要になってくる
  10. feature fl ag 多 feature fl ag と QA 品質

    これをなんとかしたい
  11. 理想 • 数が多くなると差し込む処理を書くのがめんどくさい 自 動で計装してくれる仕組みが欲しい • 実は feature fl ag

    の計装の仕 方 にブレがある時がある linter か 自 動計装で揃えたい • feature fl ag を 入 れる単位はどれくらい? ひとまず1PR単位 複数PRに渡って同じ fl ag を使 用 する場合は、 入 れる際に同名の fl ag を指定する • 1 PR における差分に対して feature fl ag を計装すればいい? git di f を使 用 してfeature fl ag を差し込みたい // ൱ఆܥͰೖΕΔύλʔϯ if !FLAG { ... } else { ... } // ෳ਺৚݅Λࠞͥͯ͠·͏ύλʔϯ if FLAG && hoge == foo && bar != 10 { ... } else { ... }
  12. // diff file now source ------------- ------------- A A +

    B B - C D D 理想 // instrumented feature flag ------------- A if true { B } else { C } D
  13. // diff file now source ------------- ------------- A A +

    B B - C D D 理想 // instrumented feature flag ------------- A if true { B } else { C } D + を if true 側配置し、- を if false 側に配置する
  14. diff --git a/difftest/main.go b/difftest/main.go index 2678f63..1c0c6e8 100644 --- a/difftest/main.go +++

    b/difftest/main.go @@ -3,7 +3,9 @@ package main import "fmt" func main() { - fmt.Println("Hello, World!") + fmt.Println("Hello, ੈք!") + + fmt.Println("New Message") fmt.Printf("testFn: %s\n", testFn("Hi!")) } 理想 • git di ff で出 力 される Uni fi ed Di ff 形式の差分を使っていい感じに feature fl ag を 入 れられないだろう か… uber-go/gopatchという リファクタリングツールを発 見 使えそうな気配
  15. uber-go/gopatch • 大 規模なリファクタリングとリスタリングする ためのツール • di ff のような形式のパッチファイルにリファク タリングしたい内容を記載すると、対象ファイ

    ルから形式を探し出して修正してくれる VCFSHPHPQBUDI3FGBDUPSJOHBOEDPEFUSBOTGPSNBUJPOUPPMGPS(P @@ @@ -import "errors" -errors.New(fmt.Sprintf(...)) +fmt.Errorf(...) .-------. .-------. /_| |. /_| |. | ||. +---------+ | ||. | .go |||>-->| gopatch |>-->| .go ||| | ||| +---------+ | ||| '--------'|| ^ '--------'|| '--------'| | '--------'| '--------' | '--------' .-------. | /_| | | | +----' | .patch | | | '--------'
  16. VCFSHPHPQBUDI3FGBDUPSJOHBOEDPEFUSBOTGPSNBUJPOUPPMGPS(P .-------. .-------. /_| |. /_| |. | ||. +---------+

    | ||. | .go |||>-->| gopatch |>-->| .go ||| | ||| +---------+ | ||| '--------'|| ^ '--------'|| '--------'| | '--------'| '--------' | '--------' .-------. | /_| | | | +----' | .patch | | | '--------' uber-go/gopatch • 大 規模なリファクタリングとリスタリングする ためのツール • di ff のような形式のパッチファイルにリファク タリングしたい内容を記載すると、対象ファイ ルから形式を探し出して修正してくれる @@ @@ -import "errors" -errors.New(fmt.Sprintf(...)) +fmt.Errorf(...) git di ff の形式に似てる これどうやって利 用 してる…?
  17. patch の分割 • patch を分割して2つのバージョンを作成する • 各バージョンを個別に解析でき、両 方 が有効 な構

    文 であることが保証される • 同じ解析ロジックを利 用 できる // patch -x, err := foo(...) +x, err := bar(...) if err != nil { ... - return err + return nil, err } Before(-) ------------------ x, err := foo(...) if err != nil { ... return err } After(+) ------------------ x, err := bar(...) if err != nil { ... return nil, err }
  18. Patch Go (pgo) の構築と拡張 • Before、After は有効な Go の構 文

    であるとは限らない • go/parserを使 用 するために、pgo を go/scanner を使 用 して go/parser で 使 用 可能な形に変換、拡張する x, err := foo(...) if err != nil { ... return err } package _ func _() { x, err := foo(dts) if err != nil { dts return err } }
  19. Matcher/Replacer の Compile • Matcher Go のコードに 一 致するかどうかを検知 -

    側のデータ • Replacer 一 致したコードの代わりにASTを配置 + 側のデータ // patch -err := f() -if err != nil { +if err := f(); err != nil { ... } // Matcher err := f() if err != nil { ... } // Replacer if err := f(); err != nil { ... }
  20. Data share • Matcher と Replacer は Data オブジェクトを使 用

    して情報を共有する context.Context に近いイメージ
  21. diff file A + B - C D git di

    ff による出 力 の意味を再確認 • + 今回新しく追加されたもの 現在の実装 if true に 入 れたい • - 元々実装されていたもの 今回の実装で削除された if false に 入 れたい
  22. git di f • git di ff には 大 量の

    Option がある • できるだけ細切れのものではなく、 一 つの有効なファイル状態の 方 が gopatch の patch fi le として扱いやすい 将来的には細切れでも補完できるようにしたい 1 fi le の差分を丸々出 力 できるような Option を設定する
  23. git di f • -U<n> / --uni fi ed=<n> 変更された

    行 の前後に出 力 する、変更されていない 行 数を変更する デフォルトは3 0にすると変更された 行 しか出 力 されない とりあえず 大 きい値を設定しておけば差分ファイル全体が表 示 される 今回は repository 内のファイルの最 大行 数を設定した • -W / --function-context 変更があった 行 に関連する context 情報を表 示 する 変更があった 行 の所属している function 名が表 示 される、という想定だったが結構意図していないものが出 力 されることがあ るので今回はあまり使えなかった • --no-pre fi x ファイル名のような情報を表 示 しない 本来はここでのファイルと patch fi le を紐づけて gopach で検索する対象を絞った 方 がいいが、 一 旦ミニマムでは repository 全体に対して検索しにいくようにな形にした
  24. git di ff -> patch fi le に整形 • @@

    とかの正規表現マッチングで header を整形する • - と + が連続する場合は順番を 入 れ替える git di ff の形式で「 行 を書き換える」という処理をした場合、- が先で + が後に出 力 される 後の処理(if 文 の挿 入 )のしやすさのために 入 れ替える 構 文 や処理には影響を与えないので 入 れ替えても問題はない(はず) // before @ test1 @ @@ func main() { testFn("test1") - testFn1("test") + fmt.Printf("%s", "test1") + testFn10("test") testFn2("test") } // after @ test1 @ @@ func main() { testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") }
  25. +/- version を 生 成する • patch fi le を上から順番に

    見 ていって、- version / + version の構造体を作成 • 振り分けていく際に、+/- がついていた 行 はその位置を記録しておく • - だった場合は、最後に - ではなかった位置を記録しておく - が連続していた場合でも、最後に - ではなかった位置を記録 後にここに if else の node を差し込む // patchVersion is one of the versions of // a patch specified in a unified diff. type patchVersion struct { // Contents of the file. Contents []byte // Positional information for each line in Contents. // // Each LinePos contains matches an offset // in Contents to a token.Pos in // the original patch file. Lines []section.LinePos } // LinePos contains positional information about a line in a buffer. type LinePos struct { // Offset of the first character of this line. Offset int // Original position from which this line was extracted. Pos token.Pos // * New Added // Type of the line: '+' for true, ‘-' for false, ' ' // for unchanged Type LineType // * New Added // position of the plus side when minus elements are embedded MinusToPlusPos token.Pos }
  26. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 
  27. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() {  1 func main() {
  28. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1")  1 func main() { 2 testFn("test1")
  29. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1")  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1")
  30. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1")  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test")
  31. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test")  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 4 + version の位置を記録
  32. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test")  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 4 + version の位置を記録
  33. +/- version を 生 成する • - だった場合は、最後に - ではなかった

    位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 }  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 + version の位置を記録
  34. + version を Matcher に登録 • 本来は Matcher には -

    version を Matcher に 入 れる • 今回は現在の実装に対して 一 致を探したいので + version を設定する
  35. + version を Matcher に登録 • 本来は Matcher には -

    version を Matcher に 入 れる • 今回は現在の実装に対して 一 致を探したいので + version を設定する + version を設定
  36. +/- version を組み合わせて AST を 入 れ替える • + の場合は

    if true に 入 れる 記録しておいた+位置の node を if true に 入 れる 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 }  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } OFX WFSTJPO
  37. +/- version を組み合わせて AST を 入 れ替える • + の場合は

    if true に 入 れる 記録しておいた+位置の node を if true に 入 れる 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 }  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn(“test1") if true { 3 + fmt.Printf("%s", "test1") 4 + testFn10(“test") } 5 testFn2("test") 6 } OFX WFSTJPO
  38. +/- version を組み合わせて AST を 入 れ替える • + の場合は

    if true に 入 れる 記録しておいた+位置の node をIfStmt にreplace • - の場合は if false に 入 れる 4 の node を IfStmt Else に replace 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 }  1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn(“test1") if true { 3 + fmt.Printf("%s", "test1") 4 + testFn10(“test") } else { - testFn1("test") } 5 testFn2("test") 6 } OFX WFSTJPO
  39. new + version を Matcher に登録 • AST を書き変えた new

    + version を replacer に設定する new + version を設定
  40. 計装できた "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test")

    testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f
  41. 計装できた func main() { if true { testFn("test") } testFn("test1")

    if true { fmt.Printf("%s", "test1") testFn10("test") } else { testFn1("test") } testFn2("test") if true { } else { testFn3("test") } testFn4("test") } "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test") testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f
  42. 計装できた func main() { if true { testFn("test") } testFn("test1")

    if true { fmt.Printf("%s", "test1") testFn10("test") } else { testFn1("test") } testFn2("test") if true { } else { testFn3("test") } testFn4("test") } "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test") testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f 否定系を使わないようにしている
  43. まだできていない ・ 悩みどころ • 関数の引数 自 体を変更するパターン これは 自 動計装関係なしに

    feature fl ag を 入 れるの難しい • 関数 自 体の作成、削除するパターン 削除されていた場合は、- で呼ばれている場所があるかもしれないので復活させる + の場合はそのままにする • 構造体のフィールドを変更する フィールドを削除したり変更したりするパターン 元々の構造体を変更していない場合、丸々構造体を丸ごと if 文 に 入 れて残しておく ASTは構築できているので 対象 Node の構造体の型によって愚直に対応していくい
  44. まとめ • gopatch のツールの内部実装を紹介した • di ff fi le を元に

    AST を構築して feature fl ag 用 に AST を差し替えることが できた • まだ実戦導 入 するには不 足 している部分が多いが、ここからさらに 色 々なパ ターンに対応していきたい The Go gopher was designed by Renée French. GO Feature Flag