Timee Product Team Blog

タイミー開発者ブログ

後編:YARD から rbs-inline に移行しました

タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

この記事は先日公開した「前編:YARD から rbs-inline に移行しました」の後編となっています。前編では rbs-inline の紹介、移行の目的などをまとめています。前編を読んでいない方はぜひ読んでみてください。

tech.timee.co.jp

後編では実際の移行の流れや詰まったポイント、今後の展望について紹介します。

移行の流れ

YARD が日常的に書かれている状況から YARD がほとんど rbs-inline になり、YARD 関連のツールが削除されるまでの流れを紹介します。

1. 型をやっていくことを表明する

まずはバックエンド開発者に対してやっていく気持ちを表明しました。

YARD から rbs-inline への移行は自分1人で進めるよりは誰かに手伝ってもらったほうが自分ごとに感じられる方が増えると思い、表明と同時に手伝ってもらえる方を募集しました。

ここで、 @Juju_62q @dak2 の2名に立候補いただきました。ありがたい。

こんな感じで表明しました

2. rbs-inline のセットアップを行う

移行を段階的に進めるためにも、まずは rbs-inlne コメントを書いたらきちんと反映されるような状況を作ります。

タイミーでの型生成は pocke さんが作った便利 Rake Task rbs:setup をアレンジして使っています。そのため、 rbs:setup を実行することで rbs-inline による型定義も生成されるように変更しました。

また、タイミーでは sord gem という YARD コメントから RBS を生成するツールも使っています。rbs-inline はアノテーションがないメソッドにも RBS を生成するため、ただ生成しただけでは RBS が重複してしまいエラーになってしまいます。且つ、rbs-inline コメントが少なく YARD コメントが多い現段階においては YARD コメントによる RBS は捨てずに互換性を維持する必要がありました。

これらは sord gem の --exclude-untyped オプションを設定しつつ、 rbs subtract コマンドによって YARD → rbs-inline の優先順位で重複を削除することで解決しました。

--exclude-untyped オプションは名前の通り、YARD コメントがなく untyped にせざるを得ない RBS を生成結果から除外できます。ですが、 --exclude-untyped オプションもユースケースに完全に一致するものではなく、YARD コメントがない定数や initialize メソッドが untyped として生成されるので rbs-inline での記述が反映されない形になっていました。

定数が untyped になる問題は最終的に sord gem を削除するまでは解決しませんでしたが、YARD コメントがない initialize メソッドも生成されてしまう問題は以下のように生成されたファイルの中身を書き換えることで生成結果から強引に除外する対応を行いました。

  task sord: :environment do
    sord_path = 'sig/sord/generated.rbs'
    sh 'sord', '--exclude-untyped', '--rbs', sord_path
    Rails.root.join(sord_path).then do |f|
      content = f.read

      # sord は --exclude-untyped をつけていても initialize メソッドの型を出力するが、
      # rbs-inline を優先したいので削除する。
      content.gsub!(/def initialize:.*?(\n\s+.*?)*-> void/, '')

      f.write(content)
    end
  end

また、rbs-inline をセットアップした時点の rbs-inline 0.4.0 では ActiveAdmin 用のいくつかの実装にて rbs-inline コマンドがクラッシュする事象を観測していたので、rbs-inline の変換対象から除外していました。この辺りを soutaro さんにフィードバックしたところ、0.5.0 に入ったこの変更で修正してもらえました 🎉

社内でバグ報告できて便利

結果として、タイミーの RBS 生成ステップは以下のように変化しました。

Before

  1. rbs prototype で untyped ながらも全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

After

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

rbs-inline が全体のRBSファイルを生成をしてくれるので、 rbs prototype によって型定義を生成するステップを削除しました。そのため、実質的にrbs prototype の上位互換として扱っています。rbs-inline に興味があってrbs prototype コマンドを使っているプロジェクトがあれば、とりあえず rbs prototype コマンドを rbs-inline コマンドに置き換えてみても良いのではないでしょうか。

 

また、このタイミングで開発ルールにも変更を加えています。

「新規で型アノテーションするときは YARD よりも rbs-inline を使うことを推奨する」ルールを追加しました。この時点ではコードベース上に rbs-inline によるアノテーションはほとんどなく、サンプルコードが少ないので義務ではなく推奨という形に留めています。(学んでみてほしくはありつつも、書き方分からないので rbs-inline ではなく YARD を書くことは許容する形)

3. YARD から rbs-inline への移行を進める

2.でrbs-inline 書き始められる状況を作れたので、ようやく YARD から rbs-inline への移行を進めていきます。

1.で手伝ってくれると立候補してもらった2名と一緒に方針を以下のように決めました。

「機械的に変換できる部分は機械的に変換していくが、自動変換を頑張りすぎない。手動で手直しした方が早い部分は手動で書き換える。コスパの良い方法を適宜選んで置き換えを進めていく」

また、変換スクリプトはメソッドに対するコメントを完全に変換可能なもののみ変換するという方針を取りました。

例えば以下のような YARD コメントが書かれたメソッドがあったとして

# @param x [String]
# @return [String]
def foo(x)
  x
end

YARD コメントの@return タグのみ変換できるスクリプトがあったとすると以下のように変換されます。

# @param x [String]
# @rbs return: String
def foo(x)
  x
end

この状態だと、YARD コメントから生成された RBS は (String) -> untyped になり、rbs-inline コメントから生成された RBS は (untyped) -> String になります。

今の rbs-inline コメントによる RBS の生成結果と YARD コメントによる RBS の生成結果だと YARD コメントが優先されるため、結果として (String) -> untyped が最終的な型になります。

元々 YARD コメントだけで記述していた際は (String) -> String と正しい型が定義できていたのに、YARD と rbs-inline が混在することで型情報が落ちてしまうことは意図したものではないため、完全に変換できるもののみ変換対象としました。

前述した方針より、YARD タグの全てをサポートしているわけではないためライブラリとしての公開は控えたいと思いますが、興味ある方向けにソースコードは公開します。興味があればご覧ください。

yard-rbs-inline-sample/tasks/yard_to_rbs_inline.rake at main · euglena1215/yard-rbs-inline-sample · GitHub

また、実行例として YARD アノテーションが多く記述されている yard gem に対して変換スクリプトを実行した結果も載せておきます。

github.com

コードを書かずにエディタの一括置換機能を使って移行したものもいくつか存在します。

  • yard-sig の記法 @!sig@rbs に置換
  • @example に対応する rbs-inline はないので NOTE: に置換
  • @see に対応する rbs-inline はないので refs に置換
  • @raise に対応する rbs-inline はないので Raises に置換
  • @deprecated に対応する rbs-inline はないので Deprecated に置換

数が少なく手動で手直しした例も挙げておきます。

  • YARD のタプル(e.g. Array(String, Integer))を使っている箇所を rbs-inline に修正
  • YARD の @option タグを rbs-inline で Hash のリテラルに修正
# @param [Hash] h
# @option h arg1 [String]
# @option h arg2 [Integer]
# @return [Integer]
def foo(h) = h.size

# @rbs h: { arg1: String, arg2: Integer }
# @rbs return: Integer
def foo(h) = h.size

これらの作業でコードベースからほとんどの YARD が rbs-inline のコメントに移行が完了しました 🎉

移行期間としてはサブタスクとして取り組んで1ヶ月半ほどでした。このタスクに集中すれば1~2週間で終わったのではないかと思います。

4. 後片付け

YARD コメントがほとんどなくなったので YARD 関連のツールを削除を始めとする後片付けを進めていきました。基本的にはスムーズに進んだのですが、進めていく中でハードルとなった点をいくつか紹介します。

sord gem の削除

YARD コメントから RBS を生成する sord gem を削除しようとしたところ、Data, Struct の型定義が見つからなくてエラーが発生しました。

sord が Data.defineStruct.new に対応する型定義を生成していたのに対し、rbs-inline は生成しておらず、sord 削除のタイミングでその問題が健在化しました。

対応としては、以下のように @rbs! を使って直接 RBS をコードベース上に手書きしました。

# @rbs!
#   class Foo
#     attr_reader bar: String
#     attr_reader baz: Integer
#   end

# @rbs skip
Foo = Data.define(:bar, :baz)

さすがにこれはちょっと辛いですという話を soutaro さんにしたところ、rbs-inline 0.6.0 で Data, Struct がサポートされました 🎉

github.com

Data, Struct が面倒なことをsoutaroさんに共有している図

Data, Struct がサポートされたことによって @rbs! を使う必要がなくなり、以下のようにシュッと書けるようになりました。めっちゃ便利…!

Foo = Data.define(
  :bar, #: String
  :baz #: Integer
)

rbs subtract をやめる

sord gem が削除されたことでアプリケーションコードから RBS を自動生成する方法が rbs-rails と rbs-inline のみになりました。rbs-rails は Rails 側が自動的に生成するメソッドに対して RBS 生成を行うのが目的で、rbs-inline は開発者が実装したメソッドに対して RBS 生成するのが目的です。

それぞれ RBS の生成対象が異なることから重複を吸収する必要はないだろうと考え、 rbs subtract をやめました。

rbs subtract をやめてみたところ、以下のコードでエラーが出るようになりました。

class User < ApplicationRecord
  has_one :profile
  
  after_create :create_profile
  
  private
  
  # create_profile メソッドの型定義が重複しているエラーが発生
  def create_profile
    ...
  end
end

ActiveRecord はアソシエーションで関連付けたモデルに対して create_xxx メソッドを動的に定義します。動的に定義されたメソッドとアプリケーションコードで定義したメソッドによる型定義が重複したことによるエラーでした。今回のケースでは意図的にメソッドの上書きをしていたわけではなかったため、本来は別名をつけることが望ましいパターンでした。

rbs subtract をやめたことで、意図せずメソッドを上書きしていた場合はエラーによって別名で定義できるようになりますし、意図的にメソッドを上書きしていた場合は @rbs override を記載することでその意図をコード上に残せるようになります。

必要なくなったから rbs subtract をやめようくらいの気持ちで消していましたが、これは思いがけない発見でした。

さらに細かい話になりますが、上記の重複エラーの参照先が rbs-inline ではなく rbs-rails 側になっていることに気付きました。なんでだろうと思って調べてみると、rbs-inline は sig/rbs_inline/ に格納していて rbs-rails は sig/rbs_rails/ に格納していたのですが、RBS はアルファベット順でファイルを読み込んでいくため sig/rbs_inline/ → sig/rbs_rails/ という順番に RBS を読み込んでいたことに起因するものでした。

なので、rbs-inline は sig/rbs_inline ディレクトリではなく、sig/z_rbs_inline ディレクトリに格納するように変更し、必ず rbs-rails の後に読み込まれるようにしました。

これらの取り組みによって、最終的に RBS 生成のステップが以下のようにシンプルになりました。

Before

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

After

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成

今後の展望

やりたいと思っているものの、やりきれていないいくつかの点について共有します。

型検査を通す

今回の取り組みで rbs-inline を書いていく土壌は整いましたし、実際に書かれるようになってきましたが、型検査(steep check)を通すところまでは進められていません。これから始まる長い旅のスタート地点に立ったかなという気持ちです。

また、Rails プロジェクトに対して全てのディレクトリに対して型検査を通すようにすべきなのか、それとも特定のディレクトリだけで実施するのが妥当なのかの整理はできていません。これから検証含め進めていく必要があります。

リアルタイムな実装へのフィードバック

前編「RBS 活用推進の背景」で説明したように、実装のフィードバックサイクルを早めるためには rbs-inline のコメントを更新したらリアルタイムに RBS に反映され、その RBS を元にした型検査がエディタ上ですぐに走るのが理想だと考えています。

上記の型検査を通すだけでは理想の状況は実現できません。コーディング環境のセットアップを含む包括的な開発環境の提供を推進していく必要があります。

まとめ

RBS 導入の背景から YARD から rbs-inline への移行理由、移行方法、これからの展望について紹介しました。rbs-inline は experimental ではあるものの本番運用している会社がある事実があなたの会社 rbs-inline 導入への後押しになれば幸いです。

この辺りについてもっと話したい方はカジュアル面談でお話ししましょう!

product-recruit.timee.co.jp

前編:YARD から rbs-inline に移行しました

タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

タイミーのバックエンドはモノリスの Rails を中心に構成されています。そのモノリスな Rails に書かれていた YARD を rbs-inline に一通り移行した事例を紹介します。
前編では、rbs-inline の紹介と rbs-inline への移行理由について触れ、後編では実際の移行の流れや詰まったポイント、今後の展望について触れる予定です。

rbs-inline とは

まずは rbs-inline について簡単に紹介します。

rbs-inline とは Ruby コードにコメントの形式で RBS を記述することで、対応する RBS ファイルを自動生成してくれるツールです。

github.com

README にあるサンプルコードを引用するだけになってしまいますが、以下の Ruby コードに対して rbs-inline コマンドを実行すると

# rbs_inline: enabled

class Person
  attr_reader :name #: String

  attr_reader :addresses #: Array[String]

  # @rbs name: String
  # @rbs addresses: Array[String]
  # @rbs return: void
  def initialize(name:, addresses:)
    @name = name
    @addresses = addresses
  end

  def to_s #: String
    "Person(name = #{name}, addresses = #{addresses.join(", ")})"
  end

  # @rbs &block: (String) -> void
  def each_address(&block) #:: void
    addresses.each(&block)
  end
end

以下の RBS ファイルが生成されるようになっています。

class Person
  attr_reader name: String

  attr_reader addresses: Array[String]

  def initialize: (name: String, addresses: Array[String]) -> void

  def to_s: () -> String

  def each_address: () { (String) -> void } -> void
end

サポートしている構文はこちらにまとまっています。
Syntax guide · soutaro/rbs-inline Wiki · GitHub

rbs-inline が作られた動機に関しては RubyKaigi 2024 での発表スライドを見てもらうのが一番かなと思います。

speakerdeck.com

RBS 活用推進の背景

まずは rbs-inline の前段階である RBS 活用推進の背景について紹介します。

メルカリがメルカリハロをリリースし、リクルートもスポットワーク業界への参入を表明するなどタイミーを取り巻く環境は激化の一途をたどっています。まさに戦国時代です。競合サービスと切磋琢磨し勝ち抜いていくために我々は1段階ギアを上げた開発をしていく必要があります。

そして、開発速度を高める方法はいくつかありますが、その中でも実装のフィードバックサイクルの高速化は良いアイデアの1つだと考えています。

タイミーを含む一般的な Rails アプリケーションの開発では、実装の検証はテストコードを用いた自動テストもしくは開発・検証環境での手動テストがほとんどなのではないかと思います。手動テストに一定の時間がかかるのは当然として、自動テストもそこそこのサイズの Rails アプリケーションでは数秒かかることは少なくありません。

それが仮にエディタ上でリアルタイムに静的な型検査という形でフィードバックが返ってくるとなるとどうでしょうか。もちろん手動・自動テストほど詳細なロジックミスは検知できませんが、多くのミスはちょっとした NoMethodError など型検査で気付けるものが大半です。

これまで数秒かけて気付いていたことにリアルタイムで気付けるようになれば、開発速度の改善には間違いなく寄与するはずです。

また、前提としてタイミーは元々 YARD コメントを書く文化があり、YARD コメントから RBS を生成する sord gem を使って RBS を補完用途で導入していました。詳しくは以下の資料をご覧ください。

tech.timee.co.jp

移行理由

RBS を活用し、型検査をしていくぞ!というのは前述の通りです。
一方、sord gem を使うことで YARD から RBS の生成はできていました。それでも rbs-inline に移行した理由は以下の通りです。

1. YARD(sord) よりも rbs-inline の方が表現力が高い

YARD(sord) では interface や type などの表現ができません。rbs-inline では@rbs! を使えば記述できます。

# RBS
class X
    type name = String | Symbol
    def foo: () -> name
end

# Ruby with YARD
class X
    # (String | Symbol) は表現できるが type を使った alias は表現できない
    # @return [String, Symbol]
    def foo = ['foo', :foo].sample
end

# Ruby with rbs-inline
class X
    # @rbs!
    #   type name = String | Symbol

    # @rbs () -> name
    def foo = ['foo', :foo].sample
end

2. YARD は書いていたが yardoc は使っていなかった

これは社内の事情ですが、YARD をコードリーディングを手助けするドキュメンテーションツールとしてしか使っておらず yardoc を用いてドキュメントページの生成はしていませんでした。(正確には一時期生成していましたが、誰も見ていなかったので生成をストップしました。)

同様の YARD を使っているプロジェクトであっても、yardoc を活用しているプロジェクトは yardoc 相当の挙動を rbs-inline で実現するツールを自作するか、yardoc によるドキュメント生成を諦めるかの判断を迫られることになります。

3. rbs-inline が今後言語標準の機能になっていく

rbs-inline gem は今後廃止されて rbs gem に統合される予定です。
rbs gem は Ruby 標準の機能なので rbs-inline も Ruby 標準の機能になるはずです。なので、今のうちに乗り換えておいて損はないだろう、という魂胆です。

また、開発者が社内にいる soutaro さんというのも大きなポイントでした。

productpr.timee.co.jp

rbs-inline はまだ experimental なので仕様が不安定という側面はあるものの、逆に考えるとフィードバックをすれば受け入れてもらう可能性が高いということでもあります。標準機能になるのなら、社内にいる soutaro さんに今のうちにフィードバックしておくことで我々のユースケースで困りにくい形で仕様が確定するといいなと思っています。*1

 


 

前編では rbs-inline の紹介、移行の目的などを紹介しました。このまま肝心の「実際どうだったのか」もお伝えしたいところですが、長くなったので一旦ここで一区切り。後編では実際の移行の流れや詰まったポイント、今後の展望をまとめます。お楽しみに!

今回は RBS 活用によって開発速度を向上させる作戦を取りましたが、開発速度向上には色んな方法があると思っています。各社がどんなアプローチで取り組まれているのかはとても興味があります。カジュアル面談でお待ちしています!

product-recruit.timee.co.jp

追記:後編を公開しました。

tech.timee.co.jp

*1:事実として我々のフィードバックによってバグ修正や新たな構文サポートが行われました。詳しくは後編で紹介します

スクラムで品質を上げ続けるために完成の定義(Definition of Done)を作りました

読んで欲しいと思っている人

  • POやステークホルダーと品質について共通言語や目標が欲しい開発者
  • 開発者と品質について共通言語や目標が欲しいPO
  • スクラムで品質について困っている人

読むとわかること

  • 完成の定義(Definition of Done)とはどんなものか
  • スクラムと非機能的な品質の関係性
  • タイミーのWorkingRelationsSquadでどんな完成の定義を作り、活用していきたいと思っているか
続きを読む

Vertex AI PipelinesとCloud Run jobsを使って機械学習バッチ予測とA/Bテストをシンプルに実現した話

こんにちは、タイミーでデータサイエンティストとして働いている小栗です。

今回は、機械学習バッチ予測およびA/BテストをVertex AI PipelinesとCloud Run jobsを使ってシンプルに実現した話をご紹介します。

経緯

タイミーのサービスのユーザーは2種類に大別されます。お仕事内容を掲載して働く人を募集する「事業者」と、お仕事に申し込んで働く「働き手」です。

今回、事業者を対象に機械学習を用いた予測を行い、予測結果を元にWebアプリケーション上で特定の処理を行う機能を開発することになりました。

要件としては以下を実現する必要がありました。

  1. 定期的なバッチ処理でのMLモデルの学習・予測
  2. MLモデルのA/Bテスト

最終的に、Vertex AI PipelinesとCloud Run jobsを活用したシンプルな構成でバッチ予測とA/Bテストを実現することにしました。

本記事では主に構成とA/B割り当ての仕組みをご紹介します。

構成

まず、全体構成とその構成要素についてご紹介します。

構成図

Webアプリケーション側の構成・実装についてもご紹介したいところですが、今回は機械学習に関係する部分に絞ってお話しします。

前提として、データサイエンス(以下DS)グループはGoogle CloudをベースとしたML基盤を構築しています。MLパイプライン等はCloud Composerに載せて統一的に管理しており、今回も例に漏れずワークフロー管理ツールとして採用しています。

MLパイプラインはVertex AI Pipelinesで実装しています。MLモデルのA/Bテストを実現するため、MLモデルごとにパイプラインを構築し、並行で稼働させています。同時に、それぞれのMLモデルの予測値と付随情報をBigQueryの予測結果テーブルに蓄積する責務もMLパイプラインに持たせています。

もちろんそれだけでは予測結果がテーブルに蓄積されるだけでA/Bテストは実現できないので、各事業者に対する予測結果のA/B割り当てをCloud Run jobsの責務とし、MLパイプライン実行の後段タスクとして実行しています。同時に、割り当て結果をBigQueryテーブルに出力する処理も実施します。

当初はA/B割り当てを含めたすべての責務をVertex AI Pipelinesに集約する案も議論の中で出たのですが、将来的に類似の取り組みにて実装や思想を使い回せそう等の理由から、取り回しのしやすいCloud Run jobsを採用しました。

Cloud Run jobsの利用はDSグループ内でも初めてではありましたが、グループ内のMLOpsエンジニアに相談・依頼してCloud Run jobs用CI/CDの導入などML基盤のアップデートを並行して進めていただくことで、スムーズに開発を進められました。

今回、モジュール間のデータやり取りのIFとしてBigQueryを採用していますが、読み込み・書き込みの操作に関しては、以前ご紹介した社内ライブラリを活用することでサクッと実装できました。

また、各処理には実行日時などの情報が必要なため、Cloud Composerのオペレータからパラメータを渡してキックする形にしています。

例えば、Cloud Run jobsは2023年のアップデートからジョブ構成のオーバーライドが可能になっており、それに併せてCloud Composer側でもCloudRunExecuteJobOperatorを介したオーバーライドが可能になったため、そちらを利用して必要なパラメータを実行時に渡しています。

さて、A/B割り当ての結果が出力されたのち、Webアプリケーション側はデータ連携用テーブルを参照して、事業者に対してバッチ処理を行います。残念ながら機能や施策の具体についてはご紹介できないのですが、機械学習の予測結果を元に事業者ごとに特定の処理を行う仕組みになっています。

A/B割り当ての仕組み

次に、Cloud Run jobsの中身で実施しているA/B割り当てについて、より具体的にご紹介します。

どう設定を管理するか

A/B割り当てに必要なパラメータはyamlファイルで指定する形にしています。例えば、実験期間や各MLモデルへの割り当て割合などです。

- experiment_name: str # 実験名。割り当てに用いるキーも兼ねる e.g. 'experiment_1'
  start_date: str # A/Bテストの開始日 e.g. '2024-08-01'
  end_date: str # A/Bテストの終了日 e.g. '2024-08-31'
  groups:
    - model_name: str # MLモデルの名前 e.g. 'model_1'
      weight: float # このMLモデルに割り当てる割合 e.g. 0.5
    - model_name: ...
- experiment_name: ...
  ...

この方法を用いる問題点として、”PyYAML”というライブラリを使えばyamlを読み込むこと自体は可能なのですが、開発者が想定していない形式でyamlが記述されるとエラーや予期せぬ挙動に繋がります。

当初の開発者以外がyamlファイルを更新することを見越して、ファイルの中身をバリデーションすることが望ましいと考えました。そこで、型・データのバリデーションが可能なライブラリである”Pydantic”を活用することにしました。

上記の形式のyamlファイルを安全にパースするために、以下のようなPydanticモデルクラスを定義しています。

# コードの一部を抜粋・簡略化して記載しています

import datetime

from pydantic import BaseModel, Field, field_validator

class ABTestGroup(BaseModel):
    model_name: str
    weight: float = Field(..., ge=0.0, le=1.0)

class ABTestExperiment(BaseModel):
    experiment_name: str
    start_date: datetime.date
    end_date: datetime.date
    groups: list[ABTestGroup]

    @field_validator('groups')
    @classmethod
    def validate_total_weight(cls, v: list[ABTestGroup]) -> list[ABTestGroup]:
        """
        各groupのweightの合計が1.0であることを確認する。
        """
        total_weight = sum(group.weight for group in v)
        if not math.isclose(total_weight, 1.0, rel_tol=1e-9):
            raise ValueError('Total weight of groups must be 1.0')
        return v

class ABTestConfig(BaseModel):
    experiments: list[ABTestExperiment]

例えば、weight(各グループに割り当てる事業者の割合)に対しては、型アノテーションとFieldを使って型と数値の範囲のバリデーションを実施しています。

加えて、各グループの割合の合計が1.0を超えることを避けるため、フィールドごとのカスタムバリデーションを定義可能なfield_validatorを使用し、独自のロジックでバリデーションを実装しました。

このようなPydanticを使ったバリデーション処理をML基盤のCIを通して呼び出すことにより、不適切なyamlファイルを事前に検知できるようにしています。

どう割り当てるか

A/B割り当てに関しては、事業者を適切にバケット(=グループ)に割り当てるために、事業者のIDとキーを使用しています。

# コードの一部を抜粋・簡略化して記載しています

import hashlib

def _compute_allocation_bucket(company_id: int, key: str, bucket_size: int) -> int:
    """
    company_idとkeyに基づき、company_idに対してバケットを割り当てる。
    keyはexperiment_nameなどを想定。
    """
    hash_key = key + str(company_id)
    hash_str = hashlib.sha256(hash_key.encode("utf-8")).hexdigest()
    return int(hash_str, 16) % bucket_size + 1

まず、実験名をkeyとして事業者のIDと結合し、ハッシュ化の元となる文字列を生成します。次に文字列をハッシュ化し、16進数の文字列を取得します。ハッシュ値を整数に戻した後、バケットサイズで割った余りをバケット番号とします。

その後、各MLモデルに対してweightに基づいてバケット範囲を割り当てることでMLモデルと事業者を紐付けます(コードは省略します)。

ややこしい点はありつつも上記のロジックにより、同じ事業者とキーの組み合わせに対して一貫して同じバケットを割り当てることができます。

実行のタイミングによって割り当てが変化する等の問題が生じず、A/Bテストの管理が容易になります。また、ハッシュ関数を使うことで入力値をほぼ均等に分散させることができ、実質的にランダムにグループ分けすることができます。*1

おわりに

機械学習バッチ予測とA/Bテストをシンプルに実現した話をご紹介しました。

今回、データサイエンティストとバックエンドエンジニアの共同開発については黎明期といった状況での開発でしたが(そこがまた楽しいのですが)、主にPdM含めた3人で協力しながら手探りで設計・実装を進めていきました。

その他にもMLOpsエンジニア、データエンジニアなど多くのポジションの方々に協力いただいており、部門を跨いでスムーズに協業できる組織体制が整ってきたことを開発を通して感じました。

今回ご紹介した構成にはやや課題が残っていたりするのですが、部門を横断しつつ解決を図っていけるのではと考えています。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!*2

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

*1:ハッシュ関数を活用したA/B割り当てに関してはGunosyさんのブログ記事が分かりやすいです。

*2:募集中のエンジニア系のポジションはこちらです!

Platform Engineering Kaigi 2024 に参加しました

OGP

2024/07/09 に Platform Engineering Kaigi 2024(PEK2024) が docomo R&D OPENLAB ODAIBA で開催されました。

www.cnia.io

タイミーは Platinum スポンサーとして協賛させていただき、プラットフォームエンジニアリンググループ グループマネージャーの恩田が「タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介」を発表しました。

タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があります。この制度を活用してタイミーから4名のエンジニアがオフライン参加しました。

productpr.timee.co.jp

各エンジニアが印象に残ったセッションの感想を参加レポートとしてお届けします。

What is Platform as a Product and Why Should You Care

What is Platform as a Product and Why Should You Care

speakerdeck.com

チームトポロジーの共著者であるマニュエルさんによる Platform as a Product という考え方がなぜ重要なのか、Platform as a Product を実現するためには特に何を念頭においてプラットフォームを構築すべきなのかを紹介するセッションでした。

価値あるプラットフォームとはストリームアラインドチームの認知負荷を下げるものであり、価値あるプロダクトとは顧客の何らかの仕事を簡単・楽にするものという話がありました。

私は元々 Platform as a Product という単語を知らない状態でこのセッションを聞いていたのですが、顧客をストリームアラインドチームと置き換えるとプラットフォームとプロダクトが同一視でき、価値あるプロダクトを生み出すためのアプローチをプラットフォームに応用というのはなるほど一理あると感じました。

プラットフォームをプロダクトと同一視すると、色々と伸び代が見えてきます。

  • プラットフォームは作って終わりではなく Go To Market まで考える
  • プラットフォームは足し算で機能を足していくのではなく引き算も考える必要がある

このあたりに関してはエンジニアリングというよりもプロダクトマネジメントの領域です。この発表を聞いてプラットフォームチームにもプロダクトマネージャーを配置する重要性を強く感じました。

もう一つ印象的だったのが、ストリームアラインドチームからプラットフォームチームを信頼してもらうことが重要という点です。チームトポロジーを踏まえた組織設計を考える上で、ストリームアラインドチーム・プラットフォームチームは認知していたのですが、あくまで構造として認知をしていました。ですが、チームを構成するのは人です。ストリームアラインドチームがプラットフォームチームを信頼していなければ、プラットフォームチームが作ったプラットフォームは信頼されないし、信頼されないと諸々デバフがかかった状態で物事を進める必要が出てくるため、プラットフォームチームが価値あるプラットフォームを生み出していたとしても浸透に時間がかかるようになります。この点は認識できていなかったのでハッとしたポイントでした。

(@euglena1215)

Platform Engineering at Mercari

Platform Engineering at Mercari

speakerdeck.com

Mercai deeeetさんの講演です。個人的にはSRE(Site Reliability Engineering)の導入やGoogle CloudでのGKE運用の方法など様々な情報発信をされていて、ありがたく参考にさせていただいています。このセッションでは、deeeetさんが入社して7年、立ち上げ当初から関わっている MercariにおけるPlatformEngineeringの歴史を振り返ってお話をされるという内容でした。

MercariのPlatform Engineeringのミッションは「メルカリグループの開発者がメルカリのお客様に対して新しい価値やより良い体験を早く安全に届けることができるようにサポートするインフラ、ツール、ワークフローを提供すること」とありました。今まで別のmeetupなどでもお話をされていますし、さらっと冒頭にあった言葉ですが、改めて目にするとシンプルで分かりやすく過不足なく定義されているなという印象を受けました。

さて、Platform Engineeringの具体的な実施事項(どういう組織・どういうツールセットか)などは是非セッション動画を見ていただく方が良いのでここでは記述を割愛いたしますが、”コラボレーションから始める”という言葉がすべてを物語っていました。

Platform Engineeringの歴史と、どのように作り上げていったかについての説明がなされました。それはモノリス・レガシーインフラからマイクロサービス・Kubernetes環境へのマイグレーションを開発者とコラボレーションしながら設計したりツールセットを整えたりしてきた、とのことでした。

マイグレーション当時はPlatform Engineeringチームメンバーが開発者の席に散っていって隣に座って一緒に会話をしながら設計やツールセットを一緒に作ったりしていたそうです。これは狙ってやったわけではなく、目的を達成するためにやっていたことが結果的に後から振り返って良い戦略だったなと思ったとのこと。泥臭いけれど、近くで会話するというのは本当に良いことです。現在はリモートワークが主流のところも(弊社も含め)多く、物理的に離れた場所にいる組織ではどのようにデザインするか工夫が必要そうだなと思いました。

以上までは立ち上げフェーズでの話でしたが、2020年ごろから現在まではPlatform Engineering組織のアップデートをしているとのこと。一つはメンバーが増えてきて(10名→15名程度→さらに増やす)、かつ見るべき領域が多くなり認知負荷が高まってきたため分割を考えたとのこと。この中でも印象的なのはProductチームとPlatformチーム間のInterfaceとなるPlatform DX(Developer Exprerience)チームを配置したとのこと。今まで1つの塊だったPlatformチームが細分化されるにあたって、Productチームが「これはこのチーム、あれはあのチーム…」と細かな事情を抑えてコミュニケーションをする認知負荷を軽減する目的のようでした。

まだいくつか気になった点はありますが、長くなりそうですのでこのあたりで留めておこうと思います。もし興味を持たれましたら是非資料やアーカイブをご覧ください!(@橋本和宏)

タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介

タイミーを支えるプラットフォームエンジニアリング・成果指標設計から考える組織作り事例の紹介

speakerdeck.com

弊社 恩田からの講演となります。直近1年での元々あったチーム課題をどのように解決してきたのか、また取り組んだ内容の反省点などが示されています。

ここでは詳細をあえて書かず、是非アーカイブ動画もしくは下記のスライドをご覧いただき、皆様の参考になると幸いと考えています。是非ご覧くださいませ! (@橋本和宏)

プラットフォームエンジニアリングの功罪

プラットフォームエンジニアリングの功罪

speakerdeck.com

DMMさんにおけるプラットフォームエンジニアリングについて、題名にもあるとおり”功罪”という側面で実例を交えてお話をされていました。

  • マルチクラウド”k8s”の功罪

    話を聞いている途中で私自身が”マルチクラウド”の功罪だと聞いてしまっていた節があったので、”k8s”と強調した題名にしました。あくまでkubernetes環境が異なるプラットフォーム上に存在して両方の足並みを揃えて運用するところにツラみがあったということになります。

    さまざまなビジネス要件があり、それらのシステムが個別に作られるにあたってAWSとGCP、異なる技術スタックなどがあり大変だったところが出発点だったとのこと。これらをまずはkubernetesと周辺エコシステム(ArgoCDなど)に揃えることは良かった(功)とのこと。

    対して、EKS(AWS)とGKE(GCP)という同じマネージドkubernetesであるものの、細かな違いに翻弄されたり、エコシステムの選定が両環境で動くことに引きづられてしまったりと運用の大変である(罪)であるとのことでした。マルチクラウド”k8s”は手を出すときには覚悟がいるものだなということを実例を持って示していただけて大変参考になりました。

  • セルフサービスの功罪

    多くの(15のサービス)の開発者からの依頼を4人のプラットフォームエンジニアがレビューすることは捌けなくはなかったが、規模の拡大に伴いボトルネックになりうることを懸念してセルフサービス化を推進したとのことです。この点は他の会社等の事例でも語られている通り正しい選択で良かった(功)であったとのこと。

    対して、レビューの人的コストを削減することは達成できたものの、その後表出した課題はレビューそのものを少なくしたことによるもの(罪)であった点はとても学びがありました。podの割当リソース最適化ができていないことや(request/limitが同じ値でかなり余裕をもった値で開発者が設定してしまっていると想像)、セキュリティリスク(ACLが適切でないingressが作成できてしまった)などは確かに通常はインフラが分かる人のレビューを持って担保しているものです。

    これらの課題に対しては仕組みによる担保(Policy as Codeなどによる機械的なチェック)をすることとして、セルフサービス化の恩恵獲得に倒しているという決断も共感できるものでした。

  • プラットフォームチームの功罪

    共通化されたプラットフォームシステムを作って移行することで、組織としてもプラットフォームチームというものが組成され、インフラ(プラットフォーム)エンジニアがボトルネックになることなく開発者が開発に集中できるようになったのは良かった(功)とのことです。

    罪の部分に対してどのようなものだろう?と聞いている途中で興味深かったのですが、プラットフォーム化やセルフサービス化を進めても、開発者側で対応可能な問い合わせが結構な割合で来てしまい、対応コストが依然としてかかる = プラットフォームチームがボトルネックになってしまっている部分がまだまだあるとのことでした。

やはり「実際にやってみたら色々と大変だった」という話はとても価値があります。使っている技術スタックが異なることがあっても行き着く先にあるのはヒトや組織にあり、ある程度通底するものがあるのだなと改めて痛感しました。

(@橋本 和宏 )

いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜

いつPlatform Engineeringを始めるべきか?〜レバテックのケーススタディ〜

speakerdeck.com

レバテックさんの基盤システムグループという Platform Engineering の流れ以前に作られたチームが機能不全になっていることに気付き、棚卸しを行なって基盤システムグループを大幅に縮小するまでの流れを紹介しながら、今あなたの会社は Platform Engineering を始めるべきなのか?に対して示唆を与えるセッションでした。

基盤グループメンバーの声、ストリームアラインドチームの声から現状を深掘っていくスタイルはその頃の状況がとてもイメージがしやすく分かりやすかったです。個人的には聞いたセッションの中ではマニュエルさんの keynote の次に良かったセッションです。

タイミーにはプラットフォームチームが既に存在しているため、いつ Platform Engineering を始めるべきか?に悩むことはないですが、ユーザーの声を元に仮説を立てて仮説を検証し次の洞察につなげていく進め方・考え方はとても参考になりました。

発表内容からは少し逸れるのですが、基盤システムグループを大幅に縮小するという決断をグループリーダーが行えたことは素晴らしいと感じました。リーダーの影響力はチームメンバー数に比例すると思っていて、チームの縮小とはリーダーの影響力の縮小を意味すると思っています。ここに対してしっかりとアプローチを行なっていたのは素晴らしいなと思いました。

(@euglena1215)

Platform Engineering Kaigi 2024 トラックB

マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング

マルチクラスタの認知負荷に立ち向かう!Ubieのプラットフォームエンジニアリング | Platform Engineering Kaigi 2024

speakerdeck.com

Ubieのセッションでは、アプリケーションエンジニアやSREが直面する設定作業や問い合わせ対応の負担を軽減するための取り組みが紹介されました。

Ubieの講演会では、アプリケーションエンジニアとSREが直面する課題と、それに対するソリューションについての興味深い話がありました。特に、設定作業や問い合わせ対応の負担が大きい現状に対して、マルチクラスター移行を検討する必要性が述べられました。しかし、クラスター間の通信やデプロイメントの課題が浮上するため、簡単ではないとのことでした。

その解決策として、Ubieは「ubieform」と「ubieHub」を開発しました。

  • ubieform これは、GKEクラスターやBackStageなどを自動で作成し、k8sの設定からアクションの設定、GCPの設定までを出力してくれるツールです。このツールを導入することで、認知負荷を減らし、エンジニアの作業を大幅に効率化できるとされています。しかし、ある程度の完成度になるまで展開しない方針を取っており、質の確保に注力している点が印象的でした。
  • ubieHub 情報を取得するためのハブとなるもので、基本的にはBackStageをベースにしているようです。新しいツールやポータルの導入は、小規模に始めて早期にフィードバックを得ることが重要とされています。また、リンク切れなどの問題が発生しやすいため、定期的に情報を更新し、最新の状態を保つ必要があります。

新しいツールの導入時には、「ドッグフーディング」つまり、自社内で試用することが必須とされており、初期のバグ対応やチーム内でのコミュニケーションが重要であることが強調されました。

全体として、Ubieはツールの開発と導入において非常に戦略的かつ実践的なアプローチを取っており、その姿勢が印象深かったです。ツールの完成度と認知負荷のバランスを取りながら、効率的な業務環境の構築を目指す姿勢に感銘を受けました。

(@hiroshi tokudomi )

最後に

Platform Engineering という言葉は概念を表すものであり、細かな実装は各社各様さまざまなものがあります。大事なのは言葉の定義そのものではなく、何に対して価値提供するか・できているかを考え続けることだと考えます。

PEK2024は様々な環境・会社における具体的な課題やその解決のための実装を知ることで、自社における課題解決へとつながるきっかけが多くありました。

様々なセッションの聴講を通じて共通のキーワードとして以下の3点があることに気づきました。

  • 知ってもらう
  • 頼ってもらう
  • 評価してもらう

何を提供しているか知ってもらって使ってもらわなければ価値提供できているとは言えない。頼ってもらう存在にならなければ、そもそも価値提供につなげることができない。また、提供したものが評価されるものでなければ、価値提供できているとは言えない。ということになります。

これらのことは何か特別な定義でもなく、プロダクトを提供する立場の人であれば当たり前のことであると感じられると思います。この当たり前のことを当たり前のこととして”やっていく”ことがプラットフォームエンジニアリングを担うものとして意識すべきことだと強く意識した良い機会となりました。

(@橋本 和宏)

flake8 pluginを書いてみた

こんにちは、タイミーのデータエンジニアリング部 データサイエンス(以下DS)グループ所属のYukitomoです。

今回はPythonのLinterとしてメジャーなflake8のプラグインの作り方を紹介したいと思います。

コードの記述形式やフォーマットを一定に保つため、black/isort/flake8などのformat/lintツールを使うことはpythonに限らずよく行われていますが、より細部のクラス名や変数名を細かく規制したい(例:このモジュールのクラスはこういう名前付けルールを設定したい等)、けれどコードレビューでそんな細かい部分を目視で指摘するのは効率的でない、といったケースはありませんか?そんな時、flake8のプラグインを用意して自動検出できるようにしておくと便利です。

ネット上には公式サイトを含めいくつかプラグイン作成の記事があるのですが、我々の想定ケースと微妙に異なる部分がありそのままでは利用できなかったため、

  • 最新のflake8(2024/7現在, 7.1.0)を用い
  • 比較的新しいパッケージマネージャーであるpoetry(1.8.3を想定)を利用して
  • 2種類のプラグインのそれぞれの作り方

を改めてここにまとめます。

準備するもの

  • Python: Versionは特に問いませんが、3.11.9で動作確認しています。
  • Poetry: 1.8 以上 (後述しますが1.8より導入されたpackage-mode = falseを指定しているため)。この記述を変えることで1.8以前のバージョンでも動くとは思いますが、この記事では1.8を前提としています。

上記が利用可能な環境をvenvやコンテナを利用して作成しておいてください。flake8本体はpyproject.tomlの依存モジュールとして導入されるため事前に準備する必要はありません(3.8以降で動作するはずですが、本記事では7.1を利用します)。

全体の構成

サンプルで利用するファイル群は以下の通りです。構文木を利用するタイプと1行ずつ読み込んでいくタイプと2種類あるため、それぞれをtype_a、type_bとしてサンプルを用意し、それら2つのサンプルを束ねる上位のプロジェクトを一つ用意しています。本来なら各プラグイン毎にユニットテスト等も実装すべきですが、本記事ではプラグインの書き方自体の紹介が目的のため割愛しています。なお、type A, type Bの呼称はflake8プラグインにおいて一般的な呼び名ではなく、本記事の中で2つのタイプを識別するために利用しているだけなので注意してください。

# poetry.lock 等本記事の本質と関係のないものは省略しています
(.venv) % tree .  # この位置を$REPOSITORY_ROOTとします。
.
├── pyproject.toml
└── plugins
    ├── type_a
    │   ├── pyproject.toml
    │   └── type_a.py
    └── type_b
        ├── pyproject.toml
        └── type_b.py

${REPOSITORY_ROOT}/pyproject.tomlは以下の通り。

# cat ${REPOSITORY_ROOT}/pyproject.toml
[tool.poetry]
name = "flake8 plugin samples"
version = "0.0.1"
description = "A sample project to demonstrate flake8 plugins"
authors = ["timee-datascientists"]
package-mode = false # この記述を外せばきっとpoetry 1.8より前でも動くはず

[tool.poetry.dependencies]
python = ">=3.11.9"

[[tool.poetry.source]]
name = "PyPI"
priority = "primary"

[tool.poetry.group.dev.dependencies]
# flake8を利用するので一緒によく利用されるblack/isortも導入
flake8 = "~7.1.0"
isort = "~5.13.2"
black = "~24.4.0"

# プラグインはローカルからeditable modeで登録
type_a = { path="./plugins/type_a", develop = true}
type_b = { path="./plugins/type_b", develop = true}

[build-system]
requires = ["poetry>=1.8"]
build-backend = "poetry.masonry.api"

Type A: AST Treeを利用する場合

Python codeの1ファイルをparseして抽象構文木(AST)として渡すタイプのプラグインです。ネットでflake8のプラグインを検索した時、こちらのタイプの実装例が出てくることが多く、また、構文木の処理が実装できるなら、こちらの方が使いやすいです。

構文木で渡されたpython ファイルを巡回し、その過程で違反を発見するとエラーを報告しますが、本記事のサンプルでは構文木の巡回結果は無視し、巡回後必ずエラーを報告しています。詳細はast.NodeVisitorを参照いただきたいのですが、各ノードを巡回する際に呼ばれるvisit()だけでなく、visit_FunctionDef() などファイル内で関数定義された場合、など個別の関数が用意されているので、これらを適切に上書きすることで、目的の処理を実現していくことになります。

なお、プラグインのコンストラクタには抽象構文木(ast)の他、lines, total_lines等公式ドキュメントのここに記述されているものを追加することができます。

以下にサンプルの実装(type_a/type_a.py)とプロジェクトの定義ファイル(type_a/pyproject.toml)を示します。

# type_a/type_a.py
import ast
from typing import Generator, List, Tuple

# プラグインの本体
class TypeAPluginSample:
    def __init__(
        self, tree: ast.AST  #, lines, total_lines: int = 0
    ) -> None:
        self.tree = tree

    def run(self) -> Generator[Tuple[int, int, str, None], None, None]:
        visitor = MyVisitor()
        visitor.visit(self.tree)
        # サンプルでは常にエラーを報告するが本来ならvisitorに結果を溜め込んで
        # 結果に応じてエラーをレポート
        if True:
            yield 0, 0, "DSG001 sample error message", None

# プラグインから利用する構文木の巡回機
class MyVisitor(ast.NodeVisitor):
    # visit() やvisit_FunctionDef()を目的に応じて上書き
    pass

# 他のサンプルでは必須っぽく書いてあるが、pyproject.tomlのentry-points
# の指定と被ってるなぁと思ってコメントアウトしても動いたので今はいらない気がする。
# def get_parser():
#    return TypeAPluginSample
(.venv) % cat plugins/type_a/pyproject.toml
# 親プロジェクトから直接ロードするため [project]の記述もしていますが
# プラグイン単体で独立したプロジェクトとするなら不要。
[project]
name = "type_a"
version = "0.1.0"
description = "Sample type a plugin"
authors = [{name = "timee-datascientists", email = "your.email@example.com"}]

[tool.poetry]
name = "type_a"
version = "0.1.0"
description = "Sample type-a plugin"
authors = ["timee-datascientists"]

[build-system]
requires = ["setuptools", "wheel", "poetry>=1.8.3"]
build-backend = "setuptools.build_meta"

[tool.poetry.dependencies]
python = ">=3.11.9"
flake8 = ">=7.1.0"

# ここでプラグインのクラス名を登録
[project.entry-points."flake8.extension"]
DSG = "type_a:TypeAPluginSample"

Type B: 1行ずつ処理する場合

対象となるpython ファイルを1行ずつ処理していくタイプのプラグインです。公式ドキュメントにある通り、歴史的な経緯で2種類あるようですが、こちらの1行ずつ処理するタイプを使ったサンプルを見かけたことがありません。特に非推奨とされているわけでもないですし、実装したいルール自体がシンプルであればこちらの方法で実装するのもありだと私は思います。physical_lineもしくはlogical_lineを第一引数に設定し、physical_lineの場合はファイルに書かれている1行ずつ、logical_lineの場合はpython の論理行の単位で指定した関数が呼ばれます。physical_line, logical_lineの両方を同時に指定することはできず、他の変数を追加する場合もphysical_line/logical_lineは第一引数とする必要があります。

以下にサンプルの実装(type_b/type_b.py)とプロジェクトの定義ファイル(type_b/pyproject.toml)を示します。

# type_b/type_b.py
from typing import Optional

# プラグイン本体
def plugin_physical_lines(
    physical_line: Optional[str] = None,
    line_number: Optional[int] = None,
    filename: Optional[str] = None,
):
    if line_number == 2:
        yield line_number, "DSG002 sample error message"
(.venv) % cat plugins/type_b/pyproject.toml

# type_aのものとほぼ同じ。project.nameおよびtool.poetry.nameをtype_bに書き換えた後、
# 差分は以下。プラグイン本体の関数を指定してやれば良い。

:
[project.entry-points."flake8.extension"]
DSG = "type_b:plugin_physical_lines"

実行結果

以下のようなサンプルファイルを用意し、flake8を実行した結果を示します。

# sample.py
def main():
    print(
        'Hello, World!'
    )

if __name__ == '__main__':
    main()

実行結果

% flake8 sample.py
sample.py:0:1: DSG001 sample error message
sample.py:2:3: DSG002 sample error message

注意点

Type A, Type B両方とも公式ドキュメントに書いてある変数は全てコンストラクタに追加できるのですが、それぞれのタイプにおいて意味のあるものは限られるため、必要なもののみを追加すれば良いです。

まとめ

flake8 のプラグインの定義方法を2通りご紹介しました。

タイミーのデータサイエンスグループでは通常のformat/lintだけでカバーできない(けれど少しの工夫により機械作業で抽出できる)運用ルールを本記事のようなflake8プラグインを用いてCIで事前に検出することで、コードレビューはできるだけ本質的な部分に集中できるよう取り組んでいます。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

References

開発生産性カンファレンス2024に参加しました

タイミー QA Enabling Teamのyajiriです。

去る6月28日〜29日の2日間、ファインディ様主催の「開発生産性カンファレンス2024」に参加してきました。

(タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度があり、今回もこれを利用して新潟からはるばる参加してきました。) productpr.timee.co.jp

タイミーでは弊社VPoE(VP of ええやん Engineering)の赤澤の登壇でもご紹介した通り、チームトポロジーを組織に適用し、プロダクト組織の強化と改善にチャレンジしています。 speakerdeck.com

この登壇でも紹介されておりますが、私自身もイネイブリングチームの一員として、プロダクト組織全体のQA(品質保証)ケイパビリティの向上や、障害予防プロセスの改善に取り組んでいます。

開発生産性の観点から考える自動テスト

まずQAの視点で最も印象に残ったのは、皆さんもご存知のt_wadaさんによる「開発生産性の観点から考える自動テスト(2024/06版)」です。 speakerdeck.com

なぜ自動テストを書くのか?

この問いに対してt_wadaさんは「コストを削減するためではなく、素早く躊躇なく変化し続ける力を得るため」そして「信頼性の高い実行結果に短い時間で到達する状態を保つことで、開発者に根拠ある自信を与え、ソフトウェアの成長を持続可能にすること」と表現されていました。

(ここまで一言一句に無駄のない文章は久々に見た気がします)

タイミーでもアジャイル開発の中で高速なテストとフィードバックのサイクルを意識し、自動テストを含むテストアーキテクチャの強化に取り組んでいます。しかし、活動がスケールすると共にテストの信頼不能性(Flakiness)や実行時間の肥大化、費用対効果などの問題が発生します。

これらの問題に対する合理的な対応策を検討する上で、各々のテストの責務(タイプ)や粒度(レベル)を分類し、費用対効果と合目的性の高いものから重点的に対応していく必要があります。

そのためのツールとして「アジャイルテストの四象限」や「テストピラミッド」「テスティングトロフィー」などを活用し、テストレベルを整理し、テストのポートフォリオを最適化するアプローチを取っていましたが、具体的なアーキテクチャに落とし込んだ際に「これってどのテストレベルなんだっけ?」といった想定と実態の乖離がしばしば発生していました。

サイズで分類しテストダブルでテスト容易性を向上する

それを解決する手段として、テストレベルではなくテスト「サイズ」で整理する方法が提唱されました。

テストサイズの概念は古くは「テストから見えてくる グーグルのソフトウェア開発」、最近では「Googleのソフトウェアエンジニアリング」で紹介されていました。今回紹介されたのは、テストピラミッドにおいても具体的なテストタイプではなく「サイズ」で分類し、テストダブル(実際のコンポーネントの代わりに使用される模擬オブジェクト)を積極的に利用することでテスタビリティを向上させ、テストサイズを下げ、速度と決定性の高いテストが多く実装される状態を作るというアプローチです。

このアプローチは、タイミーのDevOpsカルチャーにも親和性が高く、ぜひ自動テスト戦略に取り入れたいと感じました。

おわりに

他にも魅力的で参考になる登壇が盛りだくさんで、丸々2日間の日程があっという間に過ぎる素晴らしいイベントでした。

主催のファインディ様やスポンサー、登壇者の皆さまに感謝するとともに、来年の開催も心より楽しみにしています。