TECH PLAY

タイミー

タイミー の技術ブログ

274

こんにちは、タイミーのデータアナリティクス部でデータアナリストをしている夏目です。普段は主にタイミーのプロダクトに関する分析業務に従事しています。 本日はタイミーにおいて、効果検証設計を施策前に正しく行える仕組みづくりと効果検証設計・結果を一元的に管理できるデータベースについてご紹介します。 解決したかった課題 タイミーでは、プロダクト、マーケティング、営業組織などで様々な施策が行われています。しかしながら、それらの施策の結果を判断する効果検証には課題も多く存在しています。今回は以下の2つの課題にフォーカスしてブログを書きます。 効果検証設計が事前になされていない施策があった 効果検証設計や検証結果がバラバラに保管され、会社として知見が溜まっていなかった まず1つ目の「効果検証設計が事前になされていない施策があった」に関してです。タイミーではアナリストの数も限られており、事前に全ての施策に目を通すことは難しいです。施策によっては事前に効果検証設計がされておらず、必要なログが取れていなかったり、検証に必要なサンプルサイズが担保されていなかったりと、正確な効果検証ができないケースが存在しました。 次に2つ目の「効果検証設計や検証結果がバラバラに保管され、全体として知見が溜まっていなかった」に関してです。タイミーでは様々なチームが施策を行っています。基本的に効果検証の結果はチームごとに管理されており、別のチームの人がその結果を探すことが難しいケースもありました。 取ったアプローチ 以上の2つの課題を解決するために、行ったことは主に以下の3つです。それぞれをこの項では説明していきます。 各チームが効果検証の設計と結果を記入できるNotion上のデータベースを作成 効果検証設計と結果を記入するテンプレートを作成 他アナリストや他チームへの説明の実施 1. 各チームが効果検証設計・結果を記入できるNotion上のデータベースを作成 1つ目のデータベースの作成に関しては、正確にはあるチームがすでに使用しているデータベースを少し改変し別チームにも展開しました。 イメージとしては、ダミーですが以下の画像で、行の一つ一つが効果検証設計と結果をまとめるドキュメントとなっています。チーム横断で1つのデータベースにまとめることにより、別チームの検証結果や検証方法を簡単に参照できるようになっています。 ダミーデータベース 2. 効果検証設計・結果を記入するテンプレートを作成 次は2つ目のテンプレートに関してです。データベースから効果検証ドキュメントを作る際に利用するテンプレートを作成しました。 テンプレートには、大きく効果検証設計と検証結果を書くパートの2つを用意しています。以下の画像は効果検証設計パートのテンプレートの一部です。 比較の手法には、A/Bテスト、DID、目標値との比較といった手法が入ることを想定しています。最後のScenarioは、設定したMetricsの動きによって施策担当チームの次のアクションがどう変わるのかを記入します。 このScenarioを事前に書くことによって、どのようなMetricを見るべきかが明らかになり、またそれらのMetricを計測するための手段が逆算されるはずです。   テンプレートの一部 3. 他アナリストや他チームへの説明の実施 最後に「他アナリストや他チームへの説明の実施」に関してです。作ったデータベースやテンプレートを展開するため、他のアナリストや、マーケティング担当の部署などに資料を作って説明を行いました。概ね好評で受け入れられるまでのハードルは少なかったです。 結果 データベースを作って3ヶ月ほど経ちました。現状約10個ほどの施策チームがこのデータベース・テンプレートを利用して効果検証の設計を行っています。またアナリストからも、効果検証の設計をPdMとやりやすくなったといった声をもらっています。 残課題 残課題は2つほど明確なものがあると思っています。 1つ目は、効果検証設計のテンプレートの不十分さです。現状は受け入れやすさを重視し、意図的に効果検証のテンプレートをシンプルにしています。しかしながら、A/Bテストなどでは他にも設定をしないといけない項目はまだまだあるはずです。 2つ目は、検証結果を横断したメタ分析ができる体制になっていないことです。検証結果をチーム横断でまとめているので、過去どういった施策が当たりやすかったのかといったメタ的な分析もやりやすくなるはずです。しかしながら現状こういった分析に耐えうる設計はデータベースに表現されていません。 最後に 今回は、タイミーにおける効果検証設計に関して記載しました。弊社では分析自体だけではなく、今回のような分析をより活用するための仕組みづくりも沢山行っております。 We’re Hiring! タイミーでは、一緒に働くメンバーを募集しています。 https://hrmos.co/pages/timee/jobs カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。 Reference A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは
アバター
タイミーでバックエンドのテックリードをしている新谷( @euglena1215 )です。 この記事は先日公開した「前編:YARD から rbs-inline に移行しました」の後編となっています。前編では rbs-inline の紹介、移行の目的などをまとめています。前編を読んでいない方はぜひ読んでみてください。 tech.timee.co.jp 後編では実際の移行の流れや詰まったポイント、今後の展望について紹介します。 移行の流れ 1. 型をやっていくことを表明する 2. rbs-inline のセットアップを行う 3. YARD から rbs-inline への移行を進める 4. 後片付け sord gem の削除 rbs subtract をやめる 今後の展望 型検査を通す リアルタイムな実装へのフィードバック まとめ 移行の流れ 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 rbs prototype で untyped ながらも全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 rbs subtract で 1. で生成された型に対して重複した型定義を除外 After rbs-inline で全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 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.define 、 Struct.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 rbs-inline で全体の型を生成 rbs_rails:all で Rails によって生成されたメソッドの型を生成 sord で YARD から型を生成 rbs subtract で 1. で生成された型に対して重複した型定義を除外 After rbs-inline で全体の型を生成 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
アバター
タイミーでバックエンドのテックリードをしている新谷( @euglena1215 )です。 タイミーのバックエンドはモノリスの Rails を中心に構成されています。そのモノリスな Rails に書かれていた YARD を rbs-inline に一通り移行した事例を紹介します。 前編では、rbs-inline の紹介と rbs-inline への移行理由について触れ、後編では実際の移行の流れや詰まったポイント、今後の展望について触れる予定です。 rbs-inline とは RBS 活用推進の背景 移行理由 1. YARD(sord) よりも rbs-inline の方が表現力が高い 2. YARD は書いていたが yardoc は使っていなかった 3. 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 : 事実として我々のフィードバックによってバグ修正や新たな構文サポートが行われました。詳しくは後編で紹介します
アバター
読んで欲しいと思っている人 POやステークホルダーと品質について共通言語や目標が欲しい開発者 開発者と品質について共通言語や目標が欲しいPO スクラムで品質について困っている人 読むとわかること 完成の定義(Definition of Done)とはどんなものか スクラムと非機能的な品質の関係性 タイミーのWorkingRelationsSquadでどんな完成の定義を作り、活用していきたいと思っているか 完成の定義(Definition of Done)とは インクリメントが 常に 守るべき状態のことです。スクラムガイド 1 では以下のように説明されています。 完成の定義とは、プロダクトの品質基準を満たすインクリメントの状態を⽰した正式な記述である。 プロダクトバックログアイテムが完成の定義を満たしたときにインクリメントが誕⽣する。 つまり完成の定義を満たしていない成果物は、いかに優れた機能であってもインクリメントとなることはありません。スクラムの原理原則として、完成の定義で合意した事柄は後回しにされることはありません。 また、完成の定義の対象はインクリメント全体です。プロダクトバックログアイテムによって生まれる差分ではありません。プロダクトは常に全ての完成の定義を満たすことが要求されます。 なぜ完成の定義を作ったのか スクラムの原理原則に則るのであれば、そもそも必要だから作ります。もっと言えば、完成の定義の作成はありとあらゆるプロダクトバックログアイテムの完了よりも優先されます。完成の定義がないとプロダクトバックログアイテムが完成した状態がわからないためです。 しかし今回はそのような教科書的な話ではなく、もっと現実で発生した問題に基づいて作成理由をお伝えできればと思います。大きく3つの問題を解決したかったために作成しました。 プロダクトが抱えている課題を明らかにしたい スクラムチーム内の開発者が非機能的な品質を改善をするために発生する説明責任を緩和したい 技術改善Weekをやめたい 1つずつ解説したいと思います。 プロダクトが抱えている課題を明らかにしたい さて、この説明をするためにはタイミーの組織構造を説明する必要があります。タイミーの組織は一部Spotifyモデルを参考にした概念を採用しています。 タイミーの組織構造と名称 普段共に活動するチームのことをSquadと呼び、同じ技術領域を担当しているエンジニアの集合をChapterと呼びます。 タイミーでは基本的に開発を行なっているチームが運用も行います。一方でそうは言ってもSquadが生んだ問題をChapterが改善を試みているという場合があります。また、特定技術において生まれた新技術などはChapterの単位で取り組まれることがほとんどです。本質的にはプロダクトが開発速度向上やメンテナンス性を保つためにやらなければならないことであるのに、その管理をChapterに負わせてしまっているという状況になっています。 結果として、プロダクトの課題であるはずなのにスクラムチームの関心から外れてしまっています。これをなんとかするために今回作成した完成の定義ではこれまでChapterの関心事とされていたことも明確に記述しています。下記はその一例です。 Android Chapterの関心ごとだったもの これまではChapterというところで隠されていた課題がスクラムチームに対して透明になっています。これによって今のプロダクトはどんな課題を抱えているのかが明らかになりました。 スクラムチーム内の開発者が非機能的な品質を改善をするために発生する説明責任を緩和したい 先ほどの話につながっているのですが、Chapterは課題は扱っているもののそれを解決する人は基本的にSquadに所属しています。結果としてChapterで対応するとなった課題はSquadに持ち帰られ、POに交渉を行い着手するという状況になっています。 交渉するというのは基本的にコストです。また、POがプロダクトバックログに対して説明責任を果たせば果たすほどその並び順は開発者にとって納得のいくものとなっていきます。結果としてChapterに持ってきた課題は対応が後回しにされることがしばしばありました。 スクラムの基本的な考え方としては品質が基準に達していないインクリメントはリリースできないはずなのに、品質に対する対応が劣後していく特殊な事態となっています。 ここに対して完成の定義としてチームで合意してしまうことで果たすべき説明責任は少なくなっていき、品質の改善に対応しやすくなるのではないかと考えています。 技術改善Weekをやめたい WorkingRelationsSquadでは技術改善Weekという取り組みを実施しています。6週間に1度プロダクトバックログアイテムではなく、各々好きな技術的な課題に取り組むという時間です。 開発者が交渉を行うことなく非機能的な品質を維持するために始めた取り組みです。 改めて考えるとこれはスクラムチームの課題を覆い隠すことに一役買っています。なぜならスクラムチームが品質に一部問題があるようなリリースをしたとしても後日対応できてしまい、スクラムチームに対応しなければならないこととしてFBが蓄積していかないためです。 常に品質の高い開発をしていれば技術改善Weekは必要ないはずなのに、なぜか品質が技術改善Weekに依存する形になり、スクラムチームは問題を解決するきっかけを失っています。ただし、現時点では技術改善Weekは日々開発を続ける中で対応できない課題に対応するために存在しています。やめるためには日常的により品質の高いインクリメントを作り続ける必要があります。技術改善Weekは直ちに止める話になるわけではありませんが、通常のスプリントで品質の高いプロダクトが開発できるようになれば不要になると考えています。 技術改善Weekは本質的には リリーススプリント と大差ありません。この存在によって意味のあるインクリメントをスプリントレビューで提供し続けることができない瞬間があるというのも大きな問題だと考えています。 成果物 上記を考えながらWorkingRelationsSquadでは下記のような完成の定義を作ってみました(画像は一部抜粋)。 作成した完成の定義(一部抜粋) 現状Doneとなっているものも、Undoneとなっているものもあります。Undoneの中でも開発対象の差分だけであれば守れるものもあれば、そうでないものもあるというのが現状です。全てをDoneにできるように頑張りたいと思っています。 余談 チームメンバーの感想 完成の定義を作った時に下記のような感想がありました。 理想はありつつ、やっていないことが可視化されて厳しい気持ちにもなった これをみた時はかなり嬉しかったです。 スクラムはできていること、できていないことを全て透明にしていくフレームワークです。スクラムの取り組みによって、できていないことが明らかになったのであればそれはスクラムの考え方ではとてもポジティブで大いなる第一歩だと感じています。 こう言った感想を素直に伝えてくれるチームメンバーには感謝の気持ちでいっぱいです。 完成の定義とプラットフォームエンジニアリング 完成の定義はプラットフォームエンジニアリングをやっていく上でかなり面白いツールだと思っています。なぜならUndoneが多ければ多いほど、チームは認知負荷が高いと考えることができるためです。本来やらねばならないことが頑張らないとできないという状態ということもできます。また、複数のチームで運用して共通してUndoneなのであれば、そこにプラットフォームチームが介入する余地があるでしょう。うまくゴールデンパスを作れれば課題内在性負荷や課題外在性負荷が下がり、1つDoneが増えるのかもしれません。同じ期間のスプリントでやれることが増えたとしたら、それはプラットフォームエンジニアリングの大きな成果になると思っています。 また、場合によってはプラットフォームチームから何かを強制するためにも活用できると考えています。これはプラットフォームチームがセキュリティやコストなどの側面から何かを依頼する際に良いインターフェースになると考えています。 タイミーではそれぞれのストリームアラインドチームの認知負荷が高いという話があって2年くらい経っていますが、いろんなツールや考え方を駆使して下げていければ良いと考えています。 終わりに 僕自身としては非機能的な品質と機能開発のバランスを取る活動を続けています。 振り返ってみると、2021年の発表でも似たようなこと話をしていました。ソフトウェアエンジニアとしては常に向き合っていかなければならない課題なのかなと強く思っています。 speakerdeck.com 辿り着くまでかなり時間がかかってしまいましたが、完成の定義はそういう意味ではかなり強力なツールになると思っています。今後うまく活用できればと思っています。 追記 完成の定義の画像を定義の内容に絞った画像に更新しました。 スクラムガイド 2020年版, https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf ↩
アバター
こんにちは、タイミーでデータサイエンティストとして働いている小栗です。 今回は、機械学習バッチ予測およびA/BテストをVertex AI PipelinesとCloud Run jobsを使ってシンプルに実現した話をご紹介します。 経緯 タイミーのサービスのユーザーは2種類に大別されます。お仕事内容を掲載して働く人を募集する「事業者」と、お仕事に申し込んで働く「働き手」です。 今回、事業者を対象に機械学習を用いた予測を行い、予測結果を元にWebアプリケーション上で特定の処理を行う機能を開発することになりました。 要件としては以下を実現する必要がありました。 定期的なバッチ処理でのMLモデルの学習・予測 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 : 募集中のエンジニア系のポジションは こちら です!
アバター
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点があることに気づきました。 知ってもらう 頼ってもらう 評価してもらう 何を提供しているか知ってもらって使ってもらわなければ価値提供できているとは言えない。頼ってもらう存在にならなければ、そもそも価値提供につなげることができない。また、提供したものが評価されるものでなければ、価値提供できているとは言えない。ということになります。 これらのことは何か特別な定義でもなく、プロダクトを提供する立場の人であれば当たり前のことであると感じられると思います。この当たり前のことを当たり前のこととして”やっていく”ことがプラットフォームエンジニアリングを担うものとして意識すべきことだと強く意識した良い機会となりました。 (@橋本 和宏)
アバター
こんにちは、タイミーのデータエンジニアリング部 データサイエンス(以下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 flake8 Pluginの書き方公式ドキュメント: https://flake8.pycqa.org/en/latest/plugin-development/index.html Pluginの入力として利用できる変数一覧: https://flake8.pycqa.org/en/latest/plugin-development/plugin-parameters.html flake8 Version 2.5.4を用いた参考記事(本記事でType Aと呼んでいるのと同じタイプのもの) https://blog.amedama.jp/entry/2016/04/12/063359#google_vignette さらに別な本記事でType Aと呼んでいるタイプのプラグインの作成記事: https://qiita.com/misohagi/items/756954d7f4315cea0230
アバター
タイミー 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日間の日程があっという間に過ぎる素晴らしいイベントでした。 主催のファインディ様やスポンサー、登壇者の皆さまに感謝するとともに、来年の開催も心より楽しみにしています。
アバター
こんにちは、タイミーのデータエンジニアリング部データサイエンス(以下DS)グループ所属の菊地です。 今回は、 H3 を使用したBigQueryでの空間クラスタリングについて検証した内容を紹介したいと思います! BigQueryでの空間クラスタリングとは BigQueryにはクラスタリングという機能があり、うまく活用すると、クエリのパフォーマンスを向上させ、クエリ費用を削減できます。 クラスタリングは空間データにも適用でき、BigQuery がデフォルトで使用するS2インデックス システムを使用して、空間クラスタリングを行うことができます。 また、H3やGeohashなどの他の空間インデックスに対しても空間クラスタリングを行うことができ、今回はタイミーでも良く使用している H3 を使用して、空間クラスタリングを行う方法を検証してみました。 BigQueryでのクラスタリング及び空間クラスタリングについては、下記の記事が参考になるかと思います。 cloud.google.com cloud.google.com H3を使用した BigQueryでの空間クラスタリングの検証 上記の参考記事でも挙げましたが、基本的にこちら記事の内容に沿いつつ、一部具体の実装が記載されていない箇所を補完しながら検証を行いました。 cloud.google.com 1. 検証用のテーブル作成 検証用のテーブルとして、経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブルを作成します。 DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13 ; -- 連番を格納しておくためだけのテーブル -- CTEだと後続のテーブル作成が遅かったので実テーブルにしてます CREATE OR REPLACE TABLE `tmp.tmprows` as SELECT x FROM UNNEST(GENERATE_ARRAY( 1 , 10000 )) AS x; -- 経度と緯度のランダムポイントを、H3セルID(解像度13)に変換したテーブル DROP TABLE IF EXISTS `tmp.h3_points`; CREATE OR REPLACE TABLE `tmp.h3_points` CLUSTER BY h3_index AS WITH points AS ( SELECT `carto-os`.carto.H3_FROMLONGLAT(RAND() * 360 - 180 , RAND() * 180 - 90 , H3_INDEX_RESOLUTION) AS h3_index -- 後の検証のために追加 , RAND() AS amount FROM `tmp.tmprows` AS _a CROSS JOIN `tmp.tmprows` AS _b ) select h3_index , amount FROM points テーブルのストレージ情報と内容は以下のようになります。 2. クラスタリングによる絞り込みが効かないクエリ例 次に、 参考記事 で紹介されているように、親セルID(今回は解像度7)をWHERE句で指定してクエリを実行してみましたが、このクエリはテーブルをフルスキャンしてしまいます。 DECLARE PARENT_CELL_ID STRING DEFAULT ' 870000000ffffff ' ; -- H3解像度7のセルID SELECT ROUND ( SUM (amount), 6 ) AS sum_amount FROM `tmp.h3_points` WHERE `carto-os`.carto.H3_TOPARENT(h3_index, 7 ) = PARENT_CELL_ID ジョブ情報と結果 H3インデックスでクラスタリングを行っているにもかかわらず、テーブルをフルスキャンしてしまう理由としては、 H3_ToParentにはビット演算が関係し、複雑すぎて BigQuery のクエリアナライザが、クエリの結果がクラスタ境界にどのように関連しているかを把握できないために発生します。 と 参考記事 では言及されています。 3. クラスタリングによる絞り込みが効くクエリ例 次に、クラスタリングによる絞り込みが適用されるクエリを検証してみます。 「2. クラスタリングによる絞り込みが効かないクエリ例」との違いとしては、低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得し、WHERE句で指定していることです。 DECLARE H3_PARENT_ID STRING DEFAULT ' 870000000ffffff ' ; -- H3解像度7のセルID DECLARE H3_INDEX_RESOLUTION INT64 DEFAULT 13 ; DECLARE RANGE_START STRING; DECLARE RANGE_END STRING; -- 低解像度の親セルに含まれる、高解像度セルの開始IDと終了IDを取得しセットする SET (RANGE_START, RANGE_END) = ( SELECT AS STRUCT `carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION)[ 0 ], ARRAY_REVERSE(`carto-os`.carto.H3_TOCHILDREN(H3_PARENT_ID, H3_INDEX_RESOLUTION))[ 0 ] ); SELECT ROUND ( SUM (amount), 6 ) AS sum_amount FROM `tmp.h3_points` WHERE h3_index BETWEEN RANGE_START AND RANGE_END ジョブ情報と結果は以下のようになっており、スキャン量が削減され、クエリのパフォーマンスも向上しています。クエリ結果も「2. クラスタリングによる絞り込みが効かないクエリ例」の結果と合致しています。 ジョブ情報と結果 まとめ H3を使用した BigQueryでの空間クラスタリングについて検証してきました。 タイミーでは位置情報を活用した分析を行うシーンが多く、うまく活用することで機械学習時の特徴量生成や、BIツールからのクエリ最適化に繋げることができる可能性があるので、今後のデータ分析に活かしていきたいと思います。 We’re Hiring! タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度 カジュアル面談 でお話ししましょう!
アバター
2024年6月22日(土)に Kotlin Fest 2024 が開催されました。Kotlin Festは「Kotlinを愛でる」というビジョンを掲げた技術カンファレンスです。タイミーのAndroidエンジニアはエンジニアの成長を支援する制度の一つである Kaigi Pass を利用して参加しました。 本投稿では、Kotlin Fest 2024に参加したメンバー(中川、haru、みかみ、 しゃむ 、 むらた 、 tick-tack )が気になったセッションや感想のレポートします! メンバーによるレポート 中川編 効果的なComposable関数のAPI設計 私が気になったセッションは、haru067さんによる「効果的なComposable関数のAPI設計」です。このセッションでは、Composable関数を書くときに引数をどのように定義すべきかという、現場で直面する具体的な疑問に対して、様々なケーススタディを通じて考察が行われました。 セッションでは以下のプラクティスに触れられました: State hoisting Slot API DSLでのslot APIの活用 デフォルト引数 Property drilling 特に印象的だったのは、これらのプラクティスが常に最適な解決策とは限らないという点が強調されていたことです。むやみに使うのではなく、適切な場面で使うことが重要であるという、現場での経験に裏打ちされた具体的なアドバイスが参考になりました。 haru編 Kotlinで愉しむクリエイティブコーディング まず最初にご紹介するのは、 畠山 創太 さんによる Kotlinで愉しむクリエイティブコーディング です。 私はクラブイベントにたまに行くので、VJさんという存在を元々知っていたのですが、そんなVJさんの中でもジェネ系と呼ばれる画面をリアルタイムに生成するライブコーディング的なアプローチのVJさんとプライベートで繋がりがあり、それに利用されているフレームワークなどを知っていました。 そんな中、このセッションではKotlinでリアルタイムにグラフィックスを処理できて、ジェネ系VJにも使えそうな OPENRNDR が紹介されていました。 OPENRNDRはProcessingやTouch Designerなどのジェネ系VJで使われるフレームワークとよく似たフレームワークで、KotlinベースのDSLでグラフィックス処理を記述することができます。 このセッションでは、OPENRNDRで書かれたいくつかのデモ(Boidsなど)が紹介され、OPENRNDRでできることの自由度や簡単に記述できることを紹介していました。 OpenGLベースのグラフィックスバックエンドをもち、RealSense, Kinect, TensorFlow, DMXなど多種多様な連携先が存在しており、これらを使えばセッションで紹介されていた以上のこともできそうだなと感じました。 Okioに愛を込めて 次にご紹介するのは、RyuNen344さんによる Okioに愛を込めて です。 OkioはBlock社が開発しているKotlin向けのI/O ライブラリで、OkHttpやMoshiのベースにも使われているライブラリです。 まず、Kotlinの標準ライブラリが充実しているのに、なぜOkioを採用するのかという話から始まりました。 いくつかの理由を紹介されていましたが、地味に落とし穴だなと思ったのは、Kotlinが元々JVMをターゲットとした言語としてスタートしているが故にJava標準ライブラリを呼び出しているところが多々あったり、それをKMPから使えなかったりするというところでした。 そんな中、OkioはJava標準ライブラリなどへの依存がなく、それでいて使い勝手の良いI/Oライブラリになっているということで、これから直接的・間接的問わず利用する頻度は増えていきそうでした。 これからKotlin向けのライブラリを作る上では、JVM以外のターゲットで使われることも前提として考えないといけないと思いました。 そして、綺麗なダジャレでセッションは終了。お見事でした。 みかみ編 例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する 「例外設計について考えて Kotlin(Spring Boot&Arrow)で実践する」というセッションを紹介します。例外設計の重要性とプロダクト開発に与える影響について深く掘り下げ、KotlinとArrowライブラリを活用した柔軟な例外設計の実践方法が詳しく説明されていた発表でした。 特に印象的だったのは「例外設計とモデリング」についてです。このセッションでは、例外を「技術的例外とビジネス例外」および「予期する例外と予期しない例外」の組み合わせで大きく4つに分類できるという説明がありました。そしてそれぞれの例外に対して、ドメイン駆動設計(DDD)の考え方を基に、具体的にどのようにコードに反映させるかが紹介されました。例外をドメインに結びつけて考えることにより、プロダクト開発に良い影響を与える例外設計を行うことができると感じました。 例外自体は普段の実装でも意識しますが、その複雑さのため設計に関しては深く意識できていないことが多いと感じています。本セッション内容を通してプロダクト開発をより良くしていくための例外設計の考えた方と実践に挑戦していきたいと感じました。 しゃむ編 しゃむ( @arus4869 )です。FF16を最近ようやくクリアできたので、FFVIIリバースやり始めました。最高ですね。 KotlinのLinterまなびなおし2024 私が気になったセッションは「 KotlinのLinterまなびなおし2024 」です。このセッションでは、各種Lintツールの紹介だけでなく、Lintツールを効果的に活用するための実践的なアドバイスも多数紹介されました。 中でも特に気になったのはkonsistです。konsistは、標準セットルールがなく、各プロジェクトの特性に合わせたルール設定が可能である点が魅力的でした。また、テスト環境やユニットテストでの動作が主な特徴で、アノテーションを活用することで特定の用途に応じたルール設定ができる点も興味深かったです。 またセッションの中で、Lintルールを段階的に導入することでチームの負担を軽減しつつ、徐々にコード品質を向上させるアプローチも印象的でした。 このセッションを通じて、KotlinのLintの効果的な使い方について多くの知見を得ることができ、学び直しの良い機会になりました。ありがとうございました。 むらた編 むらた( @orerus )です。最近夫婦でカイロソフトさんのアプリにハマっています。 withContextってスレッド切り替え以外にも使えるって知ってた? さて、早速ですが私が気付きを得たセッションとしてT45Kさんによる「withContextってスレッド切り替え以外にも使えるって知ってた?」 を紹介させていただきます。 スライド も公開されています。 Kotlin coroutinesを使っていると頻繁に登場する withContext ですが、セッションタイトルでズバリ指摘されている通り、私もスレッドの切り替え用関数であるかのように意識してしまっていたことに気づきました。 使い方が間違っているわけではありませんが、セッションで紹介されている通り、withContextの挙動は正確にはスレッド切り替えではなく「CoroutineContextを切り替える」(厳密には既存のCoroutineContextと引数で渡されたCoroutineContextをマージする)ことにあります。そのうえで、渡されたブロックをcoroutineContextで指定されているcoroutineDispatcherにて実行するという形になります。(詳細については是非T45Kさんのスライド資料を参照ください) そのため、 withContext(Dispatchers.IO) のように切り替え先のスレッド (厳密には CoroutineDispatcher ) を指定するだけでなく、 withContext(Job() + Dispatchers.Default + CoroutineName("BackgroundCoroutine")) のように、複数のCoroutineContextを合成する形で引数を指定することができるんですね。( CoroutineContextの要素についてはこちらを参照ください ) なお、 withContext 以外のコルーチンビルダー( launch や async など)についても、引数で指定されたCoroutineContextと既存のCoroutineContextをマージして用いる挙動は同じです。 今回のセッションを通じて、Kotlin coroutinesへの理解がさらに深まりました。とても良いセッションをありがとうございました! tick-tack編 まだ JUnit を使ってるの? kotest を使って快適にテストを書こう Kotest についての HowTo を熱く語っておられるセッションで Kotest への愛を感じました。最近よく名前を聞くライブラリな気がします。 タイミーでも hamcrest を採用していますが Java 向けのテストライブラリは Kotlin の予約語が使われていてエスケープしないととても見づらいことがあります。やっぱり Kotlin first に書けるのは非常に気持ちがいいですね。Kotest は Runner が JUnit で安定した環境で動かせるのもグッド。 個人的にセッション内で刺さったポイントとしては Eventually と Property Based Testing です。 Eventually 内部で非同期処理を実行するメソッドのテストを書くときに実行しても assertion のタイミングが変更前で失敗するといったケースはよくあります。そういう時に eventually を使うと一定時間評価しつづけ期待する結果に変わったら成功と見なしてループを抜けてくれます。めちゃめちゃかしこい。逆に一定時間変更がないことを評価する continually もあるそうです。 Property Based Testing 都度実行する度に自前で用意しなくても、ランダムに自動生成された property を利用して複数回テストするといったことができます。境界値テストを用意する場合に役立ちそうです。 さっそく assertion だけですが触ってみました。 記述方法だけでも inifix で name shouldBe "tick-taku" みたいに書けて最高にワクワクします。楽しくテストが書けそうですね。 触ってみていいなと思ったのが、例えばインスタンスが別だけど中の property が同じな事だけ確認したい場合はこんな感じに書けました。1つずつ取り出して equals とかしなくてもスッキリしていいですね。 data class User( val id: Long , val name: String , val age: Int ) checkAll( iterations = 3 , Arb.long(), Arb.string( 1 .. 10 , Codepoint.katakana()), Arb.int( 1 .. 100 ) ) { id, name, age -> val user = User(id = id, name = name, age = age) repository.save(user) repository.getUser() shouldBeEqualToComparingFields user } 一応あまり有用な例ではないですが上で紹介した property testing の checkAll や property のランダム生成もせっかくなので書いてみました。 個人的には Google の Truth が好きでしたが推し変しそうです。Android プロジェクトに導入するのもよさそうでした。 まとめ Kotlin Fest 2024はKotlinという言語の可能性を改めて再認識するとともに熱意と活気に満ちたイベントでした。また、普段リモートワークで働くタイミーのエンジニアにとってもチームメンバーと対面で交流する貴重な機会でした。今回得られた知見を活かして今後のプロダクト開発にもさらに力を入れていきたいと思います、次回のKotlin Festも楽しみにしています!
アバター
こんにちは、タイミーでデータアナリストをしている yuzuka です。 主にプロダクトの分析に携わっています。 ビジネス職からデータアナリストに転向して約1年経った私が、1年前の自分に教えてあげたい、BigQueryや LookerStudioに関する落とし穴を、いくつか挙げてみようと思います。 はじめに 弊社では、分析環境として BigQueryを採用しています。LookerStudioを使って、 BigQueryのデータを参照してダッシュボードを作ることもよくあります。 BigQueryの SQLを使った分析を進めていく中で、想定と異なるデータが出てきてしまい、原因を特定するのに苦労し、無駄な時間を費やしてしまった経験が何度もあります(実際には、そんな過程もきっと無駄ではないと信じたい)。 こちらのブログを読んでいただいたみなさまには、同じ苦労を味わっていただきたくないので、私が今までにハマってきた落とし穴をいくつか紹介します。 1. BigQueryで使える一部の記法は、LookerStudioでサポートされておらず、接続エラーになる BigQueryでは正常に動いていたクエリが、LookerStudioを使った途端に謎のエラーになることがあります。 これは、一部の記法が LookerStudioでサポートされていないことに起因しているようです。 私が遭遇した範囲では、以下の2つの記法でエラーになることが確認できています。 DECLARE , CREATE DECLARE , CREATE を使うと、事前に変数や関数の内容を宣言できます。 DECLARE , CREATE を含むクエリを書くと、BigQueryでは正常に動きますが、LookerStudioではエラーになります。 これを回避するには、大人しく LookerStudioのパラメータ機能 を使うなどするのが良さそうです。 QUALIFY句 QUALIFY句は WHERE句と異なり、Window関数の結果で絞り込めるという特徴があります。 基本的に、QUALIFY句を使ったクエリは、BigQueryでは正常に動きますが、LookerStudioではエラーになります。 これは QUALIFY句と WHERE句を併用することで回避できるようです(なにゆえ・・・) (参考記事: BigQuery "QUALIFY" Function is not supported by data studio? ) なので QUALIFY句を使うときは、なるべく習慣的に WHERE句をつけるようにしています。 SELECT column1 ,ROW_NUMBER()OVER(PARTITION BY xx ORDER BY yy) AS rank FROM table WHERE true -- エラー回避のためだけに追加 QUALIFY rank = 1 2. LAST_VALUEは使い方を間違えると、最後の値を返さないことがある LAST_VALUEを使っても、なぜか最後の値が返ってこないことがあります。 これは、LAST_VALUEの処理範囲がデフォルトで「RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(最初から現在までの行)」になっているためです( 公式ドキュメント )。 つまり、以下のようなクエリを書いた場合、 SELECT LAST_VALUE(aa)OVER(PARTITION BY bb ORDER BY ymd) AS rank FROM table ① まずはymdが古い順に並び替える ② 最初から現在の行までで、ymdが最新の場所を探す → 現在の行になる ③ 現在の行のaaが返ってきてしまう ということになっているようです。 これを回避するには、処理範囲を「ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(最初から最後の行まで)」と指定するか、以下のようにFIRST_VALUEとDESCを使う形にするのが良さそうです。 SELECT FIRST_VALUE(aa)OVER(PARTITION BY bb ORDER BY ymd DESC ) AS rank FROM table 3. 日付の表示フォーマットでYYYY を使うと、正しい西暦が返ってこないことがある LookerStudioなどの日付の表示フォーマットで、西暦の表示形式に「YYYY」を指定すると、正しい西暦が返ってこないことがあります。 これは、YYYYが単純な西暦ではなく、「その暦週の基準年」を返しているからでした。 簡単に言うと、「新年度の1月1日と同じ週に属する日については、新年度に属することにする」という考え方になっているそうです。 単純な西暦を出したい場合は、大文字の「YYYY」ではなく小文字の「yyyy」を使わなければならないようです。暦週の基準年を出したいケースはそうないと思うので、とりあえず「西暦は小文字」と覚えてしまうのが良さそうです。 もはや SQLの話ではないですが、当時こちらの答えに辿り着くまでにちょっぴり苦労しており、どうしても紹介したかったので最後にご紹介しました。 おわりに ここまで、私が経験してきた BigQuery・LookerStudio のニッチな落とし穴についてまとめてみました。 今回の記事が、少しでもみなさまの業務のお役に立てれば幸いです。 (「それはニッチな落とし穴でもなんでもないよ」「他にもこんなのがあるよ」など、ご意見ご感想ありましたら、当ブログや X などでコメントいただけますと幸いです) 分析の正確性を担保するためには、このような落とし穴を知っておくことも大事ですが、実際には、これらを理解したところで、毎回1つもミスをせず、一発で正しいクエリを書きあげることは難しいのではないかと思います。 常に自分の書いたクエリを疑いつつ、実際のデータを見て検証したり、別の指標と比較して違和感がないか確かめたり、必要に応じて他の人にクエリのレビューをお願いしたり、といった工夫の方が、個人的には大事なのかなと思っています。 We’re Hiring! タイミーでは、一緒に働くメンバーを募集しています。 https://hrmos.co/pages/timee/jobs カジュアル面談も実施していますので、少しでも興味を持っていただけましたら気軽にお申し込みください! 個人的にもアナリストやデータ関連職の方と繋がりたいと思っているので、よければ X のフォローもよろしくお願いします。
アバター
株式会社タイミーのkatsumiです! dbtのバージョン1.8以上を利用することで、unit testsが利用可能になります。今までもSingular テスト(単一テスト)やGeneric テスト(汎用テスト)は可能でしたが、テストデータを利用した単体テストも行うことができます。 導入準備 dbt-coreの場合 dbt v1.8 以上を利用してください。 dbt-cloudの場合 2024/06/12時点では dbt「Keep on latest version」を選択することで利用できます。 弊社ではunit-test用の環境のみlatest versionを利用しています。 Unit Testの基本 # run data and unit tests dbt test # run only data tests dbt test -- select test_type : data # run only unit tests dbt test -- select test_type : unit # run tests for one_specific_model dbt test -- select "one_specific_model" # run data tests limited to one_specific_model dbt test -- select "one_specific_model,test_type:data" # run unit tests limited to one_specific_model dbt test -- select "one_specific_model,test_type:unit" unit-testに関係する新しいコマンドが追加されました。このコマンドは、以前のデータテストで使用していたselect機能と同様に、特定のテストケースを選択して実行することができます。 ymlによるテストレコードの書き方 - name: test_name description : "テストの説明" model : my_model given : - input: ref ( 'users' ) rows : - { id : 1 , user_email : example@example . com } expect : rows : - { id : 1 , domain : example . com } name: test_name これはテストの名前です。この名前はテストケースを識別するために使用します。 description: “テストの説明” これはテストケースの説明です。この説明には、テストが何を意図しているのか、テストの目的や背景について記載します。 model: my_model これはテスト対象となるモデルの名前です。ここでは「my_model」がテスト対象のモデルとして指定されています。 given データの内容です。ここでは「id: 1」で「user_email」「example@example.com」のユーザーを指定しています。このデータがテストの入力として使用されます。 expect これは期待される結果を指定します。テストが成功するためには、モデルが「id: 1」のユーザーに対して「domain」が「example.com」として返される必要があります。期待される結果と実際の結果が一致するかどうかを検証します。 ファイルによるテストレコードの書き方 unit_tests : - name: test_my_model model : my_model given : - input: ref ( 'users' ) format : csv fixture : users プロジェクトのtests/fixturesディレクトリにあるCSVファイル名を指定することで利用できます。test-pathsオプションを使用することで、ディレクトリ構成を柔軟に指定することもできます。 未定義のカラムの挙動 未入力のカラムに関しては、safe_cast(null as INT64)のように型が定義されたnullのデータで補完されます。リレーションが必要なものや、ロジックに影響を与えるカラムの記入が必要になります。 実施における知見 大規模なクエリは”ephemeral”で細かいテスト行う。 with句が複数ありテストケースが複雑で見通しが悪くなるケースがあります。弊社ではSQLのテスト単位のロジックを”ephemeral”で分けて個別のmodelにてテストを書く実装を試しています。 通常のモデルと同じ書き方でテストを実施することが可能です。 WITH 処理1_cte AS ( SELECT * FROM {{ ref( ' 処理1のephemeral ' ) }} ) , 処理2_cte AS ( SELECT * FROM {{ ref( ' 処理2のephemeral ' ) }} ) , 処理3_cte AS ( SELECT * FROM {{ ref( ' 処理3のephemeral ' ) }} ) 時系列系の時間の停止をマクロで行う。 テストしたいケースにはcurrent_datetimeなど現在の時刻を利用するものがあります。その場合、テストを書く際に時間を固定する必要があります。 dbtのユニットテストでは、YAMLファイル上でdbtのマクロを置き換える機能があります。この機能を利用して、時間を固定する実装を行っています。 - name: test_case model: my_model overrides: macros: current_datetime_jst: "date('2024-01-01')" {{ config ( materialized = 'ephemeral' ) }} SELECT --  ここにロジックを書く FROM {{ ref ( 'users' ) }} AS users WHERE DATETIME_TRUNC ( created_at , MONTH ) = DATE_TRUNC ({ { current_datetime_jst () } } , MONTH ) Testに関するSQLの確認ができる。 実際の仕組みとしてはテスト用のSQLが生成され、フィクスチャ(テストデータ)も含めたSQLが実行されます。debugコマンドやコンパイルされたSQLを確認することで、テストの挙動をチェックできます。 テストケースの問題が起きた時にSQLにて要因分析を行いました。 まとめ 重要指標の計算や複雑な時系列処理、プロダクトのロジックを再現する箇所では、テストケースを用意していこうと考えています。またテストケースを先に定義したのちにクエリを書くことも簡単にできるようになったように感じます。信頼性の高いモデルにするために、重要な機能になっていきそうです。 以上、unit-testsを試した時に得られた知見のまとめでした。この情報が役立てば幸いです! We’re Hired タイミーでは、一緒に働くメンバーを募集しています!! product-recruit.timee.co.jp 参考資料 Unit tests | dbt Developer Hub https://docs.getdbt.com/docs/build/unit-tests Unit Testing https://github.com/dbt-labs/dbt-core/discussions/8275
アバター
はじめに dbt snapshotとは(ざっくり) 今回の例 全体の流れ snapshot内部処理の詳細 delete処理:宛先テーブルに存在するレコードがソーステーブルでdeleteされていた場合 update処理:宛先テーブルと比較してソーステーブルのレコードがupdateされていた場合 insert処理:宛先テーブルに無いレコードがソーステーブル側に新規で作成されていた場合 check戦略の場合 check戦略の詳細 まとめ We’re Hired はじめに こんにちは☀️okodooooonです 最近、社内のdbt snapshotモデルでパフォーマンスの問題が発生し、その解決に苦労しました。dbt snapshotの内部処理が公式ドキュメントなどで提示されておらず、詳細なクエリを理解していなかったためです。 そこで、今回、dbt snapshotの内部クエリについて解説してみることにしました。ただし、今回の解説内容は、ドキュメントで説明されている通りの挙動がどのようにSQLで表現されているのか確認したもので、新しい発見やTipsみたいなものは特にないです! 内部処理をしっかり理解することで、dbtによって抽象化された処理をより効果的に活用できることもあるかな〜と思っておりますので、どなたかの参考になれば幸いです! (今回解説するクエリは、dbt-bigqueryで生成されるクエリです) dbt snapshotとは(ざっくり) SCD Type2 Dimensionという思想に従って、過去時点の状態の遷移を蓄積できるような仕組みです。 ソースシステム側ではステータス変更が行われると、そのナチュラルキーのレコードが上書き処理されますが、その上書き処理前後のレコードをそれぞれ有効期限付きで保存します 公式Doc: https://docs.getdbt.com/docs/build/snapshots 今回の例 以下のようなモデルを仮定して、snapshotのクエリを見ていきたいと思います。 モデルファイル上の定義はこんな感じです。 {% snapshot snapshotted_sample_table %} {{ config( target_schema= ' sample_dataset ' , strategy= ' timestamp ' , unique_key= ' id ' , updated_at= ' updated_at ' , invalidate_hard_deletes= True , ) }} select * from {{ source( ' sample_dataset ' , ' sample_data ' ) }} {% endsnapshot %} ソーステーブル側で一意であるカラムをunique_key, レコード更新日時を記録するカラムをupdated_atに指定しています。 左のテーブルがsnapshot化されることで、右のように有効期限(dbt_valid_from, dbt_valid_to)とsnapshot後のレコードに対するユニークキー(dbt_scd_id)が付与されます 全体の流れ dbt snapshotはBigQueryにおいて2つのクエリを実行しています。 ソーステーブルと宛先テーブルからデータを抽出して、snapshot先にmergeするためのtmpテーブルを、update,delete,insertそれぞれの処理ごとに分割して作成する処理 tmpテーブルでラベリングされた処理ごとにMERGEクエリを実行する処理 それぞれ実行されるクエリの詳細は以下のようになります。 tmpテーブル作成クエリ全文  (クリックで展開) ```sql create or replace table `sample_project`.`sample_dataset`.`sample_table__dbt_tmp` OPTIONS( description="""""", expiration_timestamp=TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 12 hour) ) as ( with snapshot_query as ( SELECT * FROM `sample_project`.`sample_dataset`.`sample_table` ), snapshotted_data as ( select *, id as dbt_unique_key from `sample_project`.`sample_dataset`.`snapshotted_sample_table` where dbt_valid_to is null ), insertions_source_data as ( select *, id as dbt_unique_key, updated_at as dbt_updated_at, updated_at as dbt_valid_from, nullif(updated_at, updated_at) as dbt_valid_to, to_hex(md5(concat(coalesce(cast(id as string), ''), '|',coalesce(cast(updated_at as string), '')))) as dbt_scd_id from snapshot_query ), updates_source_data as ( select *, id as dbt_unique_key, updated_at as dbt_updated_at, updated_at as dbt_valid_from, updated_at as dbt_valid_to from snapshot_query ), deletes_source_data as ( select *, id as dbt_unique_key from snapshot_query ), insertions as ( select 'insert' as dbt_change_type, source_data.* from insertions_source_data as source_data left outer join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where snapshotted_data.dbt_unique_key is null or ( snapshotted_data.dbt_unique_key is not null and ( (snapshotted_data.dbt_valid_from < source_data.updated_at) ) ) ), updates as ( select 'update' as dbt_change_type, source_data.*, snapshotted_data.dbt_scd_id from updates_source_data as source_data join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where ( (snapshotted_data.dbt_valid_from < source_data.updated_at) ) ), deletes as ( select 'delete' as dbt_change_type, source_data.*, current_timestamp() as dbt_valid_from, current_timestamp() as dbt_updated_at, current_timestamp() as dbt_valid_to, snapshotted_data.dbt_scd_id from snapshotted_data left join deletes_source_data as source_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where source_data.dbt_unique_key is null ) select * from insertions union all select * from updates union all select * from deletes ); ``` merge実行クエリ全文  (クリックで展開) ```sql merge into `sample-project`.`sample_dataset`.`sample_table` as DBT_INTERNAL_DEST using `sample-project`.`sample_dataset`.`sample_table__dbt_tmp` as DBT_INTERNAL_SOURCE on DBT_INTERNAL_SOURCE.dbt_scd_id = DBT_INTERNAL_DEST.dbt_scd_id when matched and DBT_INTERNAL_DEST.dbt_valid_to is null and DBT_INTERNAL_SOURCE.dbt_change_type in ('update', 'delete') then update set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to when not matched and DBT_INTERNAL_SOURCE.dbt_change_type = 'insert' then insert (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`) values (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`) ``` 上記クエリ内の各CTEで行われる処理をざっくりまとめると以下のような処理のフローになります。 処理の詳細を詳しく見ていきたいのですが、クエリ自体がちょっと長いので、insert, update, deleteそれぞれの処理に分割して詳細を見ていこうと思います! snapshot内部処理の詳細 delete処理:宛先テーブルに存在するレコードがソーステーブルでdeleteされていた場合 tmpテーブル生成クエリのうち、ソース側でdeleteされたレコードをmerge用レコードに変換する処理の抜粋 (クリックで展開) -- 宛先履歴テーブルから履歴が確定していないレコードを抽出 snapshotted_data as ( select *, -- unique_keyに指定したカラムをdbt_unique_keyとする id as dbt_unique_key from {{ 宛先テーブル }} where dbt_valid_to is null ), deletes_source_data as ( select *, -- unique_keyに指定したカラムをdbt_unique_keyとする id as dbt_unique_key from {{ ソーステーブル }} ) deletes as ( select ' delete ' as dbt_change_type, source_data.*,                  current_timestamp ()  as dbt_valid_from,                  current_timestamp ()  as dbt_updated_at,                  current_timestamp ()  as dbt_valid_to, snapshotted_data.dbt_scd_id from snapshotted_data left join deletes_source_data as source_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where source_data.dbt_unique_key is null ) tmpテーブル生成の処理の内訳は以下のようになります。 【処理の概要】 - 履歴が確定していない(valid_toに値が入っていない)レコード群を宛先テーブルから抽出 - 履歴が確定していないレコードのうち、ソーステーブルに存在しない(削除された)レコードに絞り込み - dbt_valid_from, dbt_valid_toをクエリの実行時刻に設定 - dbt_change_typeを’delete’に設定 ソーステーブル側で削除されたmerge用レコードをmergeするクエリ (クリックで展開) merge into {{宛先テーブル}} using {{マージ用tmpテーブル}} on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id when matched and {{宛先テーブル}}.dbt_valid_to is null and {{マージ用tmpテーブル}}.dbt_change_type in ( ' delete ' ) then update set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to 【処理の概要】 - dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合 - 宛先テーブルの履歴が未確定で、tmpテーブルのdbt_change_typeが’delete’の場合 - 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(クエリ実行時刻)に上書き 以下図に表したような処理の流れによって、ソーステーブル側で削除されたレコードdbt_valid_toにsnapshot時の時刻が入るようになります。 update処理:宛先テーブルと比較してソーステーブルのレコードがupdateされていた場合 tmpテーブル生成クエリのうち、ソース側でupdateされたレコードをmerge用レコードに変換する処理の抜粋 (クリックで展開) -- 宛先履歴テーブルから履歴が確定していないレコードを抽出 snapshotted_data as ( select *, -- unique_keyに指定したカラムをdbt_unique_keyとする id as dbt_unique_key from {{ 宛先テーブル }} where dbt_valid_to is null ), updates_source_data as ( select *, -- unique_keyに指定したカラムをdbt_unique_keyとする id as dbt_unique_key, updated_at as dbt_updated_at, updated_at as dbt_valid_from, updated_at as dbt_valid_to from {{ ソーステーブル }} ), updates as ( select ' update ' as dbt_change_type, source_data.*, snapshotted_data.dbt_scd_id from updates_source_data as source_data join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where ( (snapshotted_data.dbt_valid_from < source_data.updated_at) ) ) 【処理の概要】 - 履歴が確定していないレコード群を宛先テーブルから抽出 - ソーステーブルから抽出したレコードのdbt_valid_from, dbt_valid_toを現在時刻に設定 - 履歴が確定していないレコードのうち、宛先のdbt_valid_fromより後にupdated_atがソーステーブルに存在するレコードに絞る - dbt_change_typeを’update’に設定 ソーステーブル側でupdateされたmerge用レコードをmergeするクエリ (クリックで展開) merge into {{宛先テーブル}} using {{マージ用tmpテーブル}} on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id when matched and {{宛先テーブル}}.dbt_valid_to is null and {{マージ用tmpテーブル}}.dbt_change_type in ( ' update ' ) then update set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to 【処理の概要】 - dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合 - 宛先テーブルの履歴が未確定で、tmpテーブルのdbt_change_typeが’update’の場合 - 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(現在時刻)に上書き 以下図に表したような処理の流れによって、宛先テーブルの履歴が未確定のデータのうち、ソースで更新が走ったレコードのdbt_valid_toにスナップショット時の日時が入ります。 insert処理:宛先テーブルに無いレコードがソーステーブル側に新規で作成されていた場合 tmpテーブル生成クエリのうち、insert対象のレコードをmerge用レコードに変換する処理の抜粋 (クリックで展開) -- 宛先履歴テーブルから履歴が確定していないレコードを抽出 snapshotted_data as ( select *, -- unique_keyに指定したカラムをdbt_unique_keyとする id as dbt_unique_key from {{ 宛先テーブル }} where dbt_valid_to is null ), insertions_source_data as ( select *, id as dbt_unique_key, updated_at as dbt_updated_at, updated_at as dbt_valid_from, nullif (updated_at, updated_at) as dbt_valid_to, to_hex(md5( concat ( coalesce ( cast (id as string), '' ), ' | ' , coalesce ( cast (updated_at as string), '' )))) as dbt_scd_id from {{ ソーステーブル }} ), insertions as ( select ' insert ' as dbt_change_type, source_data.* from insertions_source_data as source_data left outer join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key where snapshotted_data.dbt_unique_key is null or ( snapshotted_data.dbt_unique_key is not null and ( (snapshotted_data.dbt_valid_from < source_data.updated_at) ) ) ), 【処理の概要】 - ソーステーブルのunique_keyにしていたカラムとupdated_atに指定していたカラムを組み合わせてsurrogate_keyを生成 - ソーステーブルに対して履歴未確定の宛先テーブルをLEFT JOINして以下の条件に絞る - 宛先テーブルに指定したunique_keyが存在しないが、ソーステーブルには存在するレコード - 宛先テーブルに指定したunique_keyのレコードが存在して、ソーステーブル側のupdated_atが宛先テーブルのvalid_fromよりも後のレコード - dbt_change_typeを’insert’に設定 ソーステーブル側でinsertされたmerge用レコードをmergeするクエリ (クリックで展開) merge into {{宛先テーブル}} using {{マージ用tmpテーブル}} on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id when not matched and {{マージ用tmpテーブル}}.dbt_change_type = ' insert ' then insert (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`) values (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`) 【処理の概要】 - dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合 - dbt_scd_idがマッチしなくて、dbt_change_type=’update’の場合にinsert処理を実行 - 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(現在時刻)に上書き 以下図に表したような処理の流れによって、指定したユニークキーが宛先に存在しないか、履歴が未確定のレコードのうちソース側で前回実行からupdateが走ったものがinsertされます。 check戦略の場合 上で紹介したのは snapshot_strategy=timestamp の場合のスナップショットの挙動であり、ソーステーブル側で updated_at に指定したカラムが更新された場合に、すべてのプロパティの情報を履歴的に保持するものです。 dbtにはもう一つのスナップショット戦略として、 check 戦略があります。 {% snapshot snapshotted_sample_table %} {{ config( target_schema= ' sample_dataset ' , strategy= ' check ' , unique_key= ' id ' , invalidate_hard_deletes= True , check_cols=[ ' foo ' , ' bar ' , ' created_at ' , ' updated_at ' , ], ) }} select * from {{ source( ' sample_dataset ' , ' sample_data ' ) }} {% endsnapshot %} このモデルでは全カラムを選択していますが、特定のカラムの変更のみを履歴的にトラッキングする仕組みです。 strategy=check においても、 strategy=timestamp の時と同様に、snapshot処理はtmpテーブルを作成するクエリとmerge処理を実行するクエリに分割されます。 strategy=checkの場合のtmpテーブル作成クエリ (クリックで展開) strategy=checkの場合のmerge実行クエリ (クリックで展開) merge実行クエリはstrategy=timestampの時と変わらず、tmpテーブルの生成方法が異なっているので、詳しく見ていこうと思います check戦略の詳細 insert 用データや update 用データを出力するCTEでは、以下のようなWHERE条件が使用されます。 (( snapshotted_data.`foo` != source_data.`foo` or ( ((snapshotted_data.`foo` is null ) and not (source_data.`foo` is null )) or (( not snapshotted_data.`foo` is null ) and (source_data.`foo` is null )) ) or snapshotted_data.`bar` != source_data.`bar` or ( ((snapshotted_data.`bar` is null ) and not (source_data.`bar` is null )) or (( not snapshotted_data.`bar` is null ) and (source_data.`bar` is null )) ) or snapshotted_data.`created_at` != source_data.`created_at` or ( ((snapshotted_data.`created_at` is null ) and not (source_data.`created_at` is null )) or (( not snapshotted_data.`created_at` is null ) and (source_data.`created_at` is null )) ) or snapshotted_data.`updated_at` != source_data.`updated_at` or ( ((snapshotted_data.`updated_at` is null ) and not (source_data.`updated_at` is null )) or (( not snapshotted_data.`updated_at` is null ) and (source_data.`updated_at` is null )) ) )) この条件により、insert と update の対象となるレコードの抽出条件は次のようになります。 insert用データの抽出条件 ( 宛先にユニークキーが存在しない ) OR ( (宛先にユニークキーが存在する) AND (ユニークキー以外のcheck_colsに指定したカラムが、宛先とソースで何かしら変化が発生している) ) update用データの抽出条件 (宛先にユニークキーが存在する) AND (ユニークキー以外のcheck_colsに指定したカラムが、宛先とソースで何かしら変化が発生している) checkで指定されたカラムの変更をどのように追跡しているかを確認できました。 まとめ 今回はdbt snapshotの内部処理をdelete, update, insertの処理に分解して説明してみました。 公式ドキュメントで説明されている通りの処理が生成されるSQLによって行われていることが確認できました。 dbt snapshotを使用している際に、期待した挙動が得られない場合や何かしらエラーが発生したときに、この情報が役立てば幸いです! We’re Hired タイミーでは、一緒に働くメンバーを募集しています!! product-recruit.timee.co.jp
アバター
タイミーの yorimitsu です。 世界中で開催されているDevOpsDaysカンファレンスは、ソフトウェア開発、ITインフラ運用を中心としたカンファレンスで、2024/4/16、17の2日間にわたって開催されました。 www.devopsdaystokyo.org 今回の参加はタイミーのプロダクトおよびエンジニア向けに用意している、技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度を利用しています。この制度は世界中で開催されるカンファレンスを対象にしています。 productpr.timee.co.jp 価値貢献を意識したチームの作り方 タイミーでも顧客に価値を届けることを大切に日々の開発運用を行っていますが、それを担うチームをより良くする取り組みのノウハウが、「 Value-Driven DevOps Team〜価値貢献を大切にするチームがたどり着いたDevOpsベストプラクティス〜 」のセッションで紹介されていました。 仮説検証を早く回すための開発環境の工夫や、チームのカルチャーの作り方はとても参考になる話でした。特にチームにメンバーが増えた際に「チームの状態を理解して各メンバーのやりたいことを共有して、チームの型に落とし込んでいくか」という部分は参考にしたい考えが多くありました。 開発チームに限らず、チームを組成すると何を目的としているのか、何の価値を提供するのかなど チームに属しているメンバーの認識をある程度合わせる必要があり、その際にインセプションデッキを利用したり、ワーキングアグリーメントを作成するのは、改めて有効だなと感じました。 How先行ではなくWhyを意識しなくてはいけない 「 君もテスト自動化の同志を増やすパターンで大勝利! 」のセッションでは、SETチームを立ち上げる際に経験した問題について発表されていました。SETチームなので当然ながらテスト自動化を 推進すべく、初手は自動E2Eテストに力を入れて取り組まれていたそうですが、その取組みはSETがやりたいことであり、開発チームがやりたいことではなかったので、テスト自動化の推進が止まってしまったとのこと。 そして推進方法を見直す中で、自動E2Eテストを推進するというHowが選考して、何のために実施するのか、ニーズが有るのかという部分が欠けていたことに気が付き、改めて開発チームの困り事を把握してから、取り組んだらテスト自動化の取り組みが進んだとのことでした。 タイミーでも各スクラムチームの困り事を把握して、品質管理に関する支援に取り組まなくてはいけないと学んだセッションでした。 おわりに 今回はオンラインでの参加になりましたが、来年は現地で参加して積極的な情報交換を行ってみたいと思いました。
アバター
こんにちは!タイミーのデータアナリストの @akki です。 タイミーのデータアナリティクス部では、様々な形式の勉強会が盛んに行われています。 アナリスト自身のスキルアップはもちろん、チーム全体での知識の共有や実務への応用を目指し、常に3〜4つの勉強会が開催され、有志が参加する形式をとっています。 今回は『 効果検証入門〜正しい比較のための因果推論/計量経済学の基礎 』(安井 翔太)を題材にした輪読と実践の勉強会についてお話しします。 この本は 以前も勉強会で取り上げ ましたが、今回は新しいメンバーとともに再度実施しました。 輪読パート 輪読パートでは、本書を10に分けて担当を設定しました。毎週、各自が自分の担当分を要約し、Notionに記載して発表します。その後、参加者からの質疑応答や、本書の内容に基づいたディスカッションを行いました。 今回の勉強会での分担イメージ 感想: すでに知っていた手法についても、改めて理論的な背景や実践する上での注意事項を学ぶことができ、理解が深まりました。 同じチームで働いているメンバーとの勉強会なので、議論の中で「実務で使うためには」という観点の話が出やすいのもよかったです。 実践パート 輪読を終えた後、本書で触れられた効果検証の手法をタイミーのデータに実際に適用してみたので、その要点をお伝えします。 やったこと: タイミーの営業領域の施策で、施策を実施した効果について検証しました。 分析手法: DiD(Difference in Differences)を選択しました。 勉強会の題材ということもあり、事前に検証設計に入ることができなかったため、RCT等は実施できませんでした。 ただ介入群・非介入群ともに介入前後のデータが取得できたため、DiDを採用しました。 DiDによる効果検証のイメージ 結果: 施策による改善効果が具体的に把握できました。 またいろんな分析軸とクロスしてみることで追加の知見も得られ、今後の意思決定につながりました。 今回の分析の課題: 特にDiDについては、「何をもって平行トレンドとするか」が悩ましいポイントだなと改めて感じました。 スタートアップのような変化が激しい状況では、常に平行トレンドを満たしている対照群が設定しにくく、期間の切り方次第で平行トレンドを満たす対照群が変わることもあります。 過去の目的変数の変化だけを見ても、本来満たすべき「時間を通じた目的変数の変化が同一である」という仮定が十分に満たせていないケースも多いです。 定量面だけでなく、実験群や対照群の特性や、営業活動のオペレーションといった定性情報も把握した上で、対照群を決定することが重要であると感じました。 対照群の選定イメージ。目的変数だけ見ると、時期次第で平行トレンドの認識が変わりうる。 まとめ 今回の勉強会を通じて、手法を正しく理解することでより精緻な検証ができ、それが意思決定につながることを改めて実感できました。 これからも座学と実践のサイクルを回すことで、より会社に貢献できるデータ分析をしていきたいと思います! We’re Hiring 私たちは、ともに働くメンバーを募集しています!! カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください!
アバター
タイミーのyajiri、yorimitsu、seigiです。 アジャイルとテストのコミュニティの祭典に関する国内最大級のカンファレンス「Scrum Fest Niigata(スクフェス新潟)2024」が2024/05/10、11の2日間にわたって開催されました。 www.scrumfestniigata.org タイミーからもQAコーチ、マネージャー、スクラムマスターの3名が参加。世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度を利用しました。 productpr.timee.co.jp 本レポートでは、印象に残ったセッションの内容を中心に、2日間の会の様子をお伝えします。 タイミーのQAコンセプトに役立つノウハウを得られた話 Yorimitsuです。私が参考になったセッションについてお話させてください。 タイミーではQAの仕組みを現在構築している最中で、コンセプトとしてQAの活動を2つに分類してプロダクト組織に導入しようと考えています。コンセプトの1つ目は、QA Enablingとしており 各スクラムチームが行っている品質管理活動の支援、例えばテスト観点の作成支援や、テスト設計の手法支援、自動テストの支援などを考えています。そして2つ目のコンセプトはQA Platformとして、品質分析、自動テストのインフラ整備、スクラムチームを横断するリグレッションテストの運営(自動テスト)などを計画しています。 このコンセプトを実現するにあたり、今回の発表セッションにあった「 スクラムチームが一体になるために行ったQAプロセス変革の道のり 」は大変参考になる内容でした。 各スクラムチームへのQAの入り方や、その後に起こる課題、そして課題の整理方法など、実例が 多く紹介されていました。 そして改めて、QAマニフェストの必要性にも注目しました。QAが何を担うのか、QAを担当する人及び、QAに何かを期待する人にとって、同じ方向性を向いて会話するために非常に有効な取り組みだと受け止め、タイミーでの策定を検討してみよう思いました。 ベイビーステップで不確実性を乗り越えるQAエンジニアの挑戦に感銘を受けた話 駆け出しQAコーチのYajiriです。 私はこの春にCSM研修を受講し、晴れてスクラムマスターに認定されたこともあり、初めてスクラムフェスに参加しました。 現在の役割がQAコーチということもあり、スクラムチームにおける品質保証に関するプログラムを中心に視聴しました。どれも有意義なものでしたが、特に印象に残ったセッションは「 受け入れテスト駆動開発によって不確実性を段階的に解消するアプローチ 」です。 今回紹介された事例は、ビジネスや経済のインテリジェンスを軸にサービスを展開するWebシステムの開発チームで、XP(エクストリーム・プログラミング)開発プロセスの中でATDD(受け入れテスト駆動開発)をどのように実践しているかが紹介されました。 Web開発で厳格にXPを取り入れるには様々な困難がある中、「ベイビーステップ(よちよち歩き)」を徹底することで、不確実性をコントロール可能な粒度に落とし込み、結果的に不確実性を低減させながら大きな成果を成し遂げるというものでした。 このベイビーステップは、Gaugeで実装される受け入れテストのコードでも徹底されており、一つのステップに対して多くの期待結果を盛り込むことを「ベイビーステップ違反」として統制するカルチャーが根付いているとのことでした。 これらの取り組みは、開発手法が変わっても不確実性をコントロールする手法として非常に参考になり、私たちの自動テストでもこの考え方を取り入れていきたいと感じました。 スクラムフェスはやっぱり楽しい!そしてオフラインで参加したい!! スクラムマスターの正義です! アジャイル/スクラムに関するコミュニティ活動が好きで、いくつかのカンファレンスに参加したり、自身でもスクラムフェス神奈川を運営したりしています。 今回、スクラムフェス新潟へオンラインで参加しました。 (家庭の都合でどうしても現地参加できず…泣) スクフェス新潟では現地参加した時の体験が素晴らしいらしく、是非とも次回は現地で参加したいと思います!ネットワーキングパーティでは、新潟ならではのご飯やお酒を堪能できるんだとか・・・! 今回、私はスクラムフェス新潟というアジャイルコミュニティイベントで「素敵だなー!」と思った点についてレポートさせていただきます! スクフェス新潟の趣旨について 主催者のじゅんぺーさんによる、イベントの趣旨が説明されていました まだまだ、アジャイルのコミュニティとテストのコミュニティはそれぞれが別のジャンルとして開催されていますが、スクラムフェス新潟ではその2つを合わせて開催する流れを作ることが目指されていました! テストとアジャイルに対して熱い想いがあるからこそのビジョンだと思います! 行動規範を徹底したイベント イベントを最高の形で終えるためには、参加者の行動がとても大切になります。 特に、ギャザリングを大切にするアジャイル/スクラムのコミュニティイベントでは、特に意識したい事項となります。 どのようなことを自分たちは大切にしているのか、具体的な楽しみ方、困った時にどうすれば良いのかについて、しっかり時間をとって説明されていました。 (運営の方によるハラスメントの寸劇までありました!) 今後、イベントを開催して業界を盛り上げていく人たち、コミュニティ、企業は是非とも参考にしたい点だと思います。 コミュニティイベントの楽しみ方を最初に紹介! Keynoteの前に、菩薩さんによる最初のセッションがありました! セッション名は「いかにしてオンラインで知り合いを増やすか」 現地に参加されている方だけではなく、オンラインに参加されている方もギャザリングで楽しめるように、どういうことを考えておくと良いかを経験を交えて紹介されていました。 視聴しつつチャットを楽しむコツとして、いくつかピックアップすると… わからないことをはわからない、と正直に言おう! テクニックとしては…「つまり〇〇….ってことコト!?」と言えば、なんとかなる チャットの流れが早くてついていけなくても、気にせず喋ろう! 勇気を出して発言しても、誰も反応しない…でもそんなこと気にしなくて良い!とにかく接触回数を増やしていこう! とのことでした! 今まで幾つかのスクラムフェスに参加してきた中で、個人的に気になっていたことでしたがあらためて気にしなくていいんだ!という安心感をえられました。 また、相手の発言に対して、リアクションをつけたり、反応をしてみるだけで一緒にワイワイできていいよね!という話は下記スライドでクスッときましたw 今までは「さしすせそ」を使ってきましたが、今後は「はひふへほ」も活用しようと思います! 最後に スクラムフェス新潟は、セッションで得られる情報だけに価値を置くわけではなく、オンライン/オフラインでのギャザリングや、パーティで素敵な料理を食べられるなどセッション以外でのコンテンツでも参加者が得られる体験がとても素敵なイベントだと感じました! 次回は…絶対に新潟の現地会場に行きたいです…! 一番印象的だった言葉は…「新潟のお酒は水」でした! 以上ですmm おわりに 次回以降はなんらかの形でコミュニティを盛り上げることに貢献できたらと思います。
アバター
はじめに はじめまして、タイミーでモバイルアプリエンジニアをやっている tick-taku です。 5/15 - 5/17 の三日間にわたって沖縄で RubyKaigi 2024 が開催されました。全国から Rubyist が集結するイベントで弊社からもたくさんのメンバーが参加しており、自分も初めて参加してきました。 今回はそんな RubyKaigi に参加して感じたことや気になったポイントを紹介します。 rubykaigi.org タイミーメンバーの参加レポートはこちら。 みなさん各セッションを解像度深く解説されていてとても勉強になりました。 tech.timee.co.jp tech.timee.co.jp tech.timee.co.jp なぜ参加しようと思ったか 冒頭で自己紹介した通り、僕はモバイルアプリエンジニアで普段は Android や iOS アプリ開発がメインです。 そんな自分がなぜ RubyKaigi に参加しようと思ったか。それはこれから Ruby (rails) の開発ができるようになりたいと考えているからです。 そこで、まずは言語やコミュニティの雰囲気を掴むため Ruby の中で大規模なカンファレンスに参加してみようと思ったのがきっかけでした。 タイミーでは開発組織においてチームトポロジーをベースとしたストリームアラインドチームを運営しています。 その中で僕が所属しているチームはクライアント様に向けた機能開発を目的としており、クライアント様が使う管理画面の改善などが多いです。そのため、稀にワーカー様向けであるモバイルアプリの開発タスクが希薄になることがあります。 逆にバックエンドタスクがまだまだ手が足りていないので、モバイルにクローズせずにケイパビリティを発揮していきたいと考えました。 また僕は専門領域特化型ではないと昔から感じているため、全体を満遍なくできるようになることでゴール達成のためにどこか人が足りていない部分を補う動き方をしていきたいと考えています。 チームの方向性ともマッチしているためバックエンドで採用している Ruby を勉強していこうと思いました。 後は沖縄で開催と言うのもかなり魅力でした。沖縄ですよ、沖縄。こんなに聞くだけで胸躍るキーワードなかなかありません。キラキラドキドキですね。 RubyKaigi は毎回日本各地を転々と開催しており、毎年同じチームの Rubyist がワクワクしていたのを羨ましく感じていました。 参加してみて 「 楽しかった 」 これに尽きると思います。 コミュニティの交流であったり「こういう事を考えて言語をよりよくアップデートしている」といった事が聞けたり、普段の関わりから遠い話がたくさん自分事として聞けたことがとても楽しかったです。 知識的な話 RubyKaigi に関しては参加理由にも少し言及していますが Ruby に慣れる事を目的として参加しました。 と言うのも、Ruby を使って開発している人たちによる「どういう課題をどう解決したか」と言った話が聞けると Ruby をより身近に感じられ、モチベーションに繋がるかなと思ったからです。 モバイル系のカンファレンスで言うと DroidKaigi や iOSDC みたいなものを想定していました。 ですが、実際に RubyKaigi に参加してみるともっと低レイヤーの、Ruby の中はこう動いているだったりコンパイラの話などばかりでした。正直何言ってるか分からないことだらけでしたが、セッションの端々から Namespace や RBS など気になる単語が聞こえてきて知的探求心が刺激されました。 アスキーアートもあまり馴染みがなく新鮮で面白かったです。ゲームを動かしてみたりとスピーカーの Ruby が愛が伝わってきました。 以下に気になったワードを列挙します。 Namespace 今回のセッションの中で一番興味を持ったテーマがこの Namespace について です。 Java で言う package (や Kotlin の alias import )を Ruby でやりたいのかなと思いました。 自分は今まで Ruby (と言わずスクリプト言語全般) の変数がどう参照されているかが分かり辛く、またクラスや変数のコンフリクトが起きやすいのではと思っていました。Google が Ruby のライブラリなんか作ったらそれはもう大変なことに... モジュラモノリスなアプリケーションにおいてチームの規模が増えるにつれ、こういった話の課題感は飛躍的に上がっていきそうな気がするので重要度は大きいのではないでしょうか。 Refinements Namespace のセッション内で Refinements というワードが聞こえたので、Refinements について調べました。 こちらはメソッドに対してある特定のスコープ内の挙動を書き換えるものだとわかりました。Namespace が package に対して Refinements はどちらかと言うと extensions なのかな?少し違うかも... Refinements が Namespace に成り代わる(統合される)わけではないと言及されていて 、上記の比較が正しければ確かにそもそもの目的・用途が別物ですね。実際に触って理解していきたいと思います。 RBS 自分は Java からスタートしたので馴染みがありますが、動的型付けの Ruby でもタイプセーフのメリットを傍受したい!的な話でしょうか。コンパイルでエラーを吐き出されたりエディタ上で確認できた方がいいのは開発スピードや品質にも関わってくるのでそれはそうだと思います。 Java の記述が冗長になりがちなデメリットを Kotlin が型推論でカバーしていることを考えると自然な流れに見えます。 ただし定義が .rbs (別ファイル) に定義されることが、必要に応じて定義できるフレキシブルさを持っている反面運用時のネガティブコストにならないかは心配になりました。 TypeProf そして型推論をやろうとしているのが TypeProf でしょうか。( Good first issues of TypeProf ) .rbs ファイルを自動生成するから管理を気にしなくてよくなるのかもしれない? こちらも触って確かめてみようと思います。 Parser 今回の RubyKaigi で最も聞いた単語だと思います。普段プログラムの Parser を意識することはあまりありませんでしたが、実際にどういうアルゴリズムで動いているとかこの言語だとこうだけど Ruby やこのツールはこうなんですよみたいなのが聞けて面白かったです。 調べているとかなり歴史や思想があって興味深いのですが詳細を書くととても長くなってしまいそうなので割愛します。 The grand strategy of Ruby Parser を発表されていた kaneko-san の こちらの記事 がとても勉強になりました。 コミュニティの話 社内外問わずたくさんの人にはじめましてが出来たことも良い刺激でした。 モバイル界隈に生息しているため社外の Rubyist はもちろんのこと、タイミーではフルリモートを採用しており、自分はまだ入社して半年も経っていないためチームでも現地で初めて顔を合わせる人がたくさんいました。 そういった人たちとパーティやランチで普段何しているかだったり業務では聞けない話をたくさんできて楽しかったです。 RubyKaigi で驚いたと共にいいなと思ったことが、 各スポンサーや有志がアフターイベントを企画しそのイベントをオフィシャルが公表していること です。 参加する人が口を揃えて RubyKaigi はお祭りだと言っている意味がわかりました。Official Party はもちろんですが、最終日にも懇親会があることも驚きましたし、各社が企画する DrinkUp やカラオケ大会、果てにはクラブを貸しきる DJ イベントもあり、なんでもありだな...と。 タイミーも初日から二日続けて DrinkUp を開催するという狂気っぷりを発揮しています。 timeedev.connpass.com せっかく初参加なのでと時間に都合がつく限り参加してみました。 とは言え、初参加だし専門領域も違うので単身乗り込んで行って大丈夫か...?ちゃんとコミュニケーションできるか...?知らん人に囲まれて歌えるか...?と不安ばかりでした。 ですが実際に飛び込んでみるとそんな不安は杞憂に終わりました。話す人みなさんが歓迎ムードで相手へのリスペクトを感じ、Ruby コミュニティのウェルカムマインドはなんて素晴らしいんだと感動しました。 こちらは2日目の rubykaraoke で午前3時まで完走した猛者たちの様子。面構えが違う… 一人不安に思いながら参加しましたが、楽しみ過ぎて完全に声が出なくなりました😇 #rubykaraoke 完走組🤣おつかれさまでした! #rubykaigi #rubykaigi2024 #rubyfriends pic.twitter.com/9tx4AJyJTW — ヤノ | ROUTE06 (@ynndino88) 2024年5月16日 このお祭りみたいな雰囲気と、それをオフィシャルが大々的に謳っていることは社外の人と交流するハードルが一気に下がってとてもよい取り組みだと思います。旅先であることも盛り上がりの燃料となっている気がしますね。 だからこそ Ruby コミュニティはここまで規模が大きくなっているんだと実感できました。 ここまでの規模でこれだけ盛り上がりの大きいカンファレンスは自分が知る限り国内ではあまり見かけないので非常に良い機会提供の場になっていると思いました。(Android のカンファレンスでもこういうのないかなぁ) そりゃ毎年みんな行きたがるし帰ってきてからもわいわいしてるわけだ... さいごに まずは関わってくださったみなさまに感謝を。 Ruby を開発してくださっているコミッターの人、RubyKaigi を運営してくださったスタッフの人、雑に話に行って歓迎してくださった人、チームのメンバー、専門領域が違うにもかかわらず参加させてくれた上司・会社などなど、本当にありがとうございます。 タイミーでは KaigiPass と呼ばれる制度があって、レポートを書いたり登壇するなど何かしらのアウトプットでコミュニティに貢献することを前提に、国内外問わずカンファレンス参加の費用を負担してくれる制度があります。 冒頭で紹介した通り僕はモバイルアプリエンジニアで Ruby とはほど遠く、社歴もまだ半年も経っていないのに、それでも参加を認めてくれています。タイミーはなんて素晴らしい組織なんだ。 今後のための教訓として RubyKaigi や Ruby についてある程度事前に調べていくべきだったと反省しています 。 上述した通り RubyKaigi の趣旨もそうですが、知らない単語を調べながらセッションを聞いていると途中でついていけなくなったりしました。まぁついていけてもわかってなかったですが... とは言え Ruby に関しては何から手をつけていいかわからなかったので、ワードからこういうことがあるんだなと調べるためのとっかかりが得られたのはとても大きな一歩だと感じています。 またネックストラップの色によって写真の掲載に承諾するかを意思表示できるようになっています。黄/赤 は 🙆‍♀️、白/青 は 🙅‍♂️ です。 ところがアイキャッチの写真をよく見てください。1人だけ青いですね。 そう、僕です。完全に理解していませんでした。自分のイメージカラーだから青にしよ♪くらいの気持ちでいました。 撮り終わった後に教えていただいて慌てて付け替えたんですが、ちゃんと会のレギュレーションをチェックしておけばと後悔しています...お手数おかけしました... 何事も事前準備が大事ですね。 以上、モバイルアプリエンジニアが RubyKaigi に初参加してみた参加レポートでした。 振り返ってみると圧倒的によかったこと・得られたものが多く、今回参加してみて本当に満足しています。 来年は愛媛県松山市ということでぜひ次回も参加したいですね! せっかくだから自転車持っていって帰りはしまなみ海道渡ってから帰ろうかな...
アバター
こんにちは、タイミーの @masarakki です。 先日、5月15日から3日間開催された「RubyKaigi2024」に参加しました。 本記事で取り上げるのは、そのRubyKaigi2024の最後のセッションであるmatzのキーノートで、「これが入ったらRuby 4.0」とまで言われた @tagomoris 氏のNamespace機能。 セッション終了後、目の前に本人が座っていたので「責任重大だねwww」と煽りに行こうとしたところ、感極まって帽子を目深に被りなおしている瞬間だったのでそっとしておきました。 というわけで、 セッションの内容 は他にいくらでも記事があると思うので、実際に手を動かしてみようと思います。 参考: https://gist.github.com/tagomoris/4392f1091f658294bd4d473d8ff631cb 作業ブランチが Namespace on read にあるのでビルドしてみましょう。 $ git clone https://github.com/tagomoris/ruby $ cd ruby $ git checkout namespace-on-read $ ./autogen.sh $ mkdir build $ cd build $ ../configure --prefix=$HOME/ns-ruby $ make $ make install $ ~/ns-ruby/bin/ruby -v ruby 3.4.0dev (2024-03-28T13:58:33Z namespace-on-read f0649a2577) [x86_64-linux] どうやらうまくビルドできたようです (rubyのビルド人生で初めてやった)。 かんたんな検証コードを動かしてみましょう。 # foo.rb ------ require ' ./bar ' class Foo def self . var= (val) @@var = val end def self . var @@var end end # ------------- # bar.rb ------ class Bar end # ------------- # nstest.rb --- def dump (obj) puts "#{ obj } : #{ obj.object_id }" end require ' ./foo ' ns = Namespace .new ns.require ' ./foo ' dump Foo dump Bar dump ns:: Foo dump ns:: Bar Foo .var = ' abc ' ns:: Foo .var = ' xyz ' puts "#{ Foo .var } , #{ ns:: Foo .var }" # ------------- 実行してみましょう。 ~/ns-ruby/bin/ruby nstest.rb Foo: 100 Bar: 120 #<Namespace:0x00007ff908cf1cd8>::Foo: 140 #<Namespace:0x00007ff908cf1cd8>::Bar: 160 abc, xyz Foo と ns::Foo が全く独立していることがわかります。 普段遣いのrubyでは動かないことを確認しましょう。 $ ruby -v ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux] $ ruby nstest.rb nstest.rb:8:in `<main>': uninitialized constant Namespace (NameError) ns = Namespace.new ^^^^^^^^^ 確かセッションでは例として Oj.default_options が 行儀の悪い gem によって書き換えられてしまう事例が挙げられていたので試してみましょう。 # foo.rb ------ require ' oj ' Oj .default_options = { symbol_keys : true } class Foo def self . oj Oj .load( ' {"key":"symbol_or_string"} ' end end # ------------- # nstest.rb --- require ' ./foo ' ns = Namespace .new ns.require ' ./foo ' Oj .default_options = { symbol_keys : false } p Foo .oj p ns:: Foo .oj 実行してみましょう。 $ ~/ns-ruby/bin/gem i oj $ ~/ns-ruby/bin/ruby nstest.rb $HOME/ns-ruby/lib/ruby/3.4.0+0/date.rb:51: [BUG] Segmentation fault at 0x0000000000000168 ruby 3.4.0dev (2024-03-28T13:58:33Z namespace-on-read f0649a2577) [x86_64-linux] -- Control frame information ----------------------------------------------- c:0013 p:0039 s:0052 e:000051 CLASS $HOME/ns-ruby/lib/ruby/3.4.0+0/date.rb:51 _人人人人人人人_ > 突然のSEGV <  ̄Y^Y^Y^Y^Y^Y^Y^ ̄ ちなみに date.rb:51 は def coerce(other) です。 なぜ・・・ そういえばセッションで @tagomoris も祈りながら実行していたな・・・というのを思い出し、何度か実行してみたところ、 確率10% くらいで成功しました。 $ ~/ns-ruby/bin/ruby nstest.rb {"key"=>"symbol_or_string"} {:key=>"symbol_or_string"} 他のライブラリでも試してみましょう。 json 標準ライブラリで oj と同じようにC拡張を持つライブラリです。 特に何も問題なく読み込めました。 csv '<top (required)>': uninitialized constant Array (NameError) ネームスペースの中でArrayが見つからないみたいです。 もっと単純なコードで試してみましょう。 # foo.rb ----- puts "#{ Array } : #{ Array .object_id }" module Mod def foo p :foo end end Array .include( Mod ) # ------------ # nstest.rb -- puts "#{ Array } : #{ Array .object_id }" ns = Namespace .new ns.require ' ./foo ' [].foo # ------------ $ ~/ns-ruby/bin/ruby nstest.rb Array: 80 Array: 80 :foo なにかおかしいですね。 Array は見つかるものの、予想外にトップレベルの Array まで汚されてしまっています。 さらに foo.rb に require 'csv' を追加すると $ ~/ns-ruby/bin/ruby nstest.rb $HOME/ns-ruby/lib/ruby/gems/3.4.0+0/specifications/csv-3.2.8.gemspec:4: [BUG] vm_cref_dup: unreachable _人人人人人人人_ > 突然のSEGV <  ̄Y^Y^Y^Y^Y^Y^Y^ ̄ トップレベルで require ‘csv’ した後に ns.require './foo' すると uninitialized constant Array (NameError) のエラーに戻ります json と csv $ ~/ns-ruby/bin/ruby nstest.rb $HOME/ns-ruby/lib/ruby/3.4.0+0/forwardable.rb:230: [BUG] Segmentation fault at 0x00000000000001da -- Control frame information ----------------------------------------------- c:0017 p:---- s:0088 e:000087 CFUNC :proc c:0016 p:0004 s:0084 E:001920 TOP $HOME/ns-ruby/lib/ruby/3.4.0+0/forwardable.rb:230 [FINISH] _人人人人人人人_ > 突然のSEGV <  ̄Y^Y^Y^Y^Y^Y^Y^ ̄ 問題のない json と問題のある csv を両方requireするとなぜか別の問題が発生しました。 面白いですね。 (自分のrubyのビルド方法が間違ってる疑惑・・・?) こういう開発途中の機能を触ってみるのは初めてなので、Rubyがこんなに簡単にぶっ壊れるんだ・・・って新鮮な気持ちです。 いろいろなパターンでSEGVが出たので次回はコードを読んでみたいと思います。 最後に コロナ禍で人生が一変し、結婚 + 出産 * 2 が続き、2019年以来 実に5年ぶりのRubyKaigi参加になりました。 RubyKaigi、やっぱりドチャクソ楽しいですね・・・特にrubykaraoke・・・ッ!! 1歳と0歳を連れて参加できたのも、ひとえに託児所やイベントに子供を受け入れてくださった各社様のおかげです。この場を借りてお礼申し上げます。 なお、RubyKaigiには「Kaigi Pass」という制度を利用し参加しました。 Kaigi Passとは、世界中で開催されている全ての技術カンファレンスに無制限で参加できるタイミーの制度です。 制度について気になる方はぜひ以下の記事もご覧ください。 productpr.timee.co.jp
アバター
5月15日から17日の3日間、RubyKaigi 2024が沖縄県那覇市で開催されました。 rubykaigi.org タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「 Kaigi Pass 」という制度があります。今年もこの制度を活用してタイミーから総勢12名のエンジニアが参加しました。 前回 に引き続き参加レポートの2回目として、各エンジニアが印象に残ったセッションの感想をお届けします。 YJIT Makes Rails 1.7x Faster rubykaigi.org ShopifyでYJITの開発をしているk0kubunさんによるRuby 3.2から3.3(と3.4)にかけてのYJITの進化についての発表でした。YJITはRubyの実行速度を向上させるJITコンパイラで、Ruby 3.3ではいくつもの改善が施されています。 その中でも特にパフォーマンス改善にインパクトがあったものとして強調していたのが、メソッド呼び出しのフォールバック、例外ハンドラのコンパイル、スタックに置いていた値のレジスタ割り当て、メソッドのインライン化の4つでした。 こうした地道な改善の積み重ねにより、Ruby 3.3のYJITはRubyプログラムのパフォーマンスを10-20%も向上させることができるようになったそうです。今後のRubyの高速化に期待が持てるセッションでした。 3月末に Rubyを3.3.0にバージョンアップした 際にYJITのことを少し調べていたので自分ごととして聞けたセッションでした(タイミーではYJITは Ruby3.2のときに有効化 しています)。 リリースノート のYJITの"Major performance improvements over Ruby 3.2"というセクションにも目を通してはいたのですが、そのときは「なんかいろいろ最適化して速くなったんだろうな」くらいの気持ちで読み流していました。 このセッションでリリースノートの1行1行の背後で具体的にどういう改善を行ったのかがわかりやすく解説されたおかげで、自分の中でYJITに対する理解が少し深まった気がします。 同じくYJITに関する@maximecbさんの「Breaking the Ruby Performance Barrier」も聞いていて、YJITを最大限活用するようなコードの書き方やgemの選定といった観点がパフォーマンス文脈では重要になるかも?などと思ったりしました。 が、本来はあまりそういうことを意識しなくてもふつうにRubyのコードを書いてYJITが有効だったら速い、が理想な気もしますし、YJIT開発チームの精力的な活動を見ていると自分の想像を超えるようなブレイクスルーが起きる気もしていて今後が楽しみです。 ( 須貝 ) speakerdeck.com RuboCop: LSP and Prism rubykaigi.org セッションの大きなテーマは以下の二つでした。 Rubocop and LSP Rubocop and Prism 「Rubocop and LSP」ではLSPがどう実装されているかやVSCodeやvimなどのEditorでどう利用されているか事例などの話がありました。加えて、LSPを利用しないと毎回CLIなどでコマンドを叩かないといけないが、LSPを利用するとその流れが省略されるのでフィードバックが早いメリットがあることや、VSCodeでは既にYJITを利用している話もありました。 メリットなどの話もありましたが、LSPの対応で新しく発生したイシューもありました。CLI上では作業を完了してからコマンドを実行するので問題ないが、Editorだとどのタイミングで作業が終わったのか判断が難しいというところです。現在Parser Gemではエラー耐性設計(Error Tolerance)をサポートしてないのでこの問題が解決できないが、それをサポートしているのがPrismで、「Rubocop and Prism」で関連の話がありました。 「Rubocop and Prism」ではどのタイミングで作業が終わったのか判断が難しい問題を解決するための説明やどういうメリットがあるかなどの話がありました。Prismではエラー耐性設計(Error Tolerance)をサポートしているため、作業が終わるタイミングの判断の問題が解決できるかつ、Parsingスピードが速くなるらしいです。 最後には現在はParser Gemのインターフェースを利用してPrismを利用しているが、将来的にはそれを無くして直接Prismを利用したい話、Prismを利用すると得られるメリットの話でセッションが終わりました。 rubocopがなんとなくパーサーで動いているんだろくらいは考えていたがどういう原理や技術で動いているか分かってなかったので、話を聞くだけでも非常に勉強になりました。現在vimを利用していて、素早くrubocopの結果がEditor上で表示されるのは非常に助かっていますが、それがLSPのおかげだということがわかりました。まだPrismではエラー耐性設計を対応してないので、すぐprismを利用するのは難しいかと思いますが、対応出来次第もっと速くなったrubocopを利用するのが楽しみです! ( @JinsuKim26 ) speakerdeck.com From LALR to IELR: A Lrama's Next Step rubykaigi.org 概要 RubyのParserであるLramaは次にIELR化に取り組むよという話。 課題 現行のLramaのParserはLALR Parserであるが、Lexerの状態管理に関して課題がある。 事例として p 1.. || 2 が正しくパースできない問題は、 シンボルを "||" と解釈するか "|" + "|" と解釈するかの問題に起因しており、これが文脈に寄って異なるというLexerとParserの複合問題である。 これを現状のLALR Parserで対応しようとすると当該Parseロジックに個別カスタム処理を書く必要があるが、構文を管理するparse.yは既に現時点で16kLを超えており個別カスタム対応には限界がある。 解決策 この問題はLexerとParserを直列に動作させていることに起因しており、LexerとParserが相互に連携しながら解析することで解決可能である。 これを実現するアルゴリズムがPSLRアルゴリズムである。 実装 PSLRアルゴリズムは Canonical LR parser で実装可能であるが、 Canonical LR parser は動作が重たいという問題がある。 LALR Parser は Canonical LR parser を軽量化したものであるが、軽量化の代償としてPSLRアルゴリズムを実現するのに必要な表現能力を失っている。 そこで両者のいいとこ取りをしたIELR Parser というものがある。 着想としては高速な LALR Parser を基本としつつ、表現能力を超えた場合に Canonical LR parser を動かすと言ったシンプルなものもの。 感想 Parserが順当に進化しているなという所感。 言語工学の基本としては字句解析と構文解析は直列に動かすものですが、実際の課題としてそれでは解けない問題が存在するというのは興味深かったです。 LR Parserの改善の文脈でありLR Parserの知識があればベターですが、実際には字句解析処理の文脈依存性の問題です。非常に豊富な文法をもつRubyらしい課題であり、他言語ではなかなかココまで言語工学の研究に踏み込むことは無いのではないか思われます。 問題の複雑さに対して、実際の解法はシンプルに既存の技法を組み合わせるものであり、これは言語工学における先行研究の成果物の豊富さを物語っていると感じました。 (@Kazuki Tsunemi - tsunemin) speakerdeck.com Cross-platform mruby on Sega Dreamcast and Nintendo Wii rubykaigi.org 概要 mrubyをドリームキャストとWiiで動かしてやったぜという内容。 なおスライド自体もWiiを動かして表示していた。 ドリームキャストは2020年 、 Wiiは2023年 にmrubyで正式サポートされている。 なぜこの2つを選んだかと言うとドリキャスとWiiをスペック対決させるため。両者はハードウェアアーキテクチャからSDKに至るまでが全く異なるのだが、ともにFreeSDKがあるので採用された。 実機デバッグは非常に辛いものがあるので、エミュレータが非常に助かるなどの苦労話があった。実機で動作デモも行われた。 感想 登壇者のSEGA愛がすごい。 SEGAロゴデザインのRUBYシャツ(多分自家製)を着て現れた時点ですべてを察しました。Wiiは如何にドリームキャストがすごいのかと伝えるために引き合いに出された感じもあります。 mrubyのv3.3.0 のリリースノートを見るとNintendo Wii、Dreamcastがきっちり記載されています。(AndroidやMS-DOSと同じ並びにあることで異様さが際立つ) ゲーム機は汎用PCとは違って独特のハードウェア構成をしており、特にVRAM周りの深い見識がないと動作させるのは難しいためハードウェアの話がメインになりました。 ドリームキャストはビジネスとしてはヒットこそしませんでしたが、技術としては凄いものがあったらしくハードウェアオタクの心を未だに掴み続けているようです。尺の都合でハードウェアの深い話にまでは踏み込まず、使われている技術の違いの話がメインでした。 エミュレータ開発を含めてゲーム機ハック界隈は独特の文化が醸成されていますが、ローレベルプログラミングの深淵を除きたい人はいい窓口かもしれません。 Rubyでやる理由は謎に思いつつ、Rubyでなければマージされないような気もします。 それだけRubyが「懐の深い言語」ということでしょうか。 備考 Ruby Conf in Taiwanで同様の発表が行われていた模様 (@Kazuki Tsunemi - tsunemin) www.youtube.com Finding Memory Leaks in the Ruby Ecosystem rubykaigi.org メモリデバッグやメモリリークの検出に使われる Valgrind をRubyプログラムに適用して、メモリリークを検出するアイデアを実現した発表でした。 ValgrindをそのままRubyプログラムで用いるとRuby自体がシャットダウン時に全てのリソースを解放しないため、Valgrindのレポートで数千のエラーが誤検知され調査が困難な状態でした。そこで、 ValgrindのXML出力オプションを有効にし全てのエラーを標準出力し、その結果を1つずつ解析してRuby自体のメモリリークではなく、Rubyプログラムのメモリリークを検知できないかというアイデアから生まれたのが [ruby_memcheck](https://github.com/Shopify/ruby_memcheck) です。 ruby_memcheckでは、Valgridの出力したエラー1つずつのリークされた 各メモリ割当てのスタックトレースを走査し、Ruby起因のエラーかネイティブGem起因なのかを確認 しています。実際にこのツールによってnokogiriやprotobufといったGemでメモリリークを検知し、修正されたそうです。アイデア実現のアプローチも面白かったのですが、個人的にはその後が興味深かったです。 彼らはそこで終わらず、そもそも Rubyインタプリタが終了する際に確保されたメモリ全てを解放してくれればメモリリークの検知は今より容易になるということで問題提起 を行い、Ruby3.3から RUBY_FREE_AT_EXIT という機能が導入されました。このオプションを有効にすると、Rubyインタプリタ終了時に確保されたメモリを解放するため、より効率的にメモリリークの検出ができると言うものです。これによって最新の ruby_memcheck の メモリリーク検出のコード もスッキリしているのも良かったです。 Day3のMatzのKeynoteで触れられていた様々な側面のパフォーマンスの話のように、YJITとRubyアプリへの直接的なパフォーマンス改善だけでなく、今回のようなメモリリークの検出を手助けする開発者体験向上文脈のRubyの発展の一例を知れて面白かったです。 今回の発表者の1人である Peter Zhuさん が、最近RubyのGCのチューニングに使える示唆を与えてRailsアプリを高速化をサポートする Autotuner というGemに関する記事を公開していたので早速使ってみたいなと思っています。 ( ぽこひで ) https://blog.peterzhu.ca/assets/rubykaigi_2024_slides.pdf blog.peterzhu.ca 最後に パート1 に引き続き、興味深いセッションが並びました。 あらためて、スピーカーの皆様に感謝をお伝えさせていただきます。貴重なお話をありがとうございました! RubyKaigi 2024への参加で得たさまざまな知見を「タイミー」の開発にも活かしていきたいと思います。 Written by:須貝, @JinsuKim26 , @Kazuki Tsunemi - tsunemin, ぽこひで
アバター
5月15日から17日の3日間、RubyKaigi 2024が沖縄県那覇市で開催されました。 rubykaigi.org タイミーには世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「 Kaigi Pass 」という制度があります。今年もこの制度を活用してタイミーから総勢12名のエンジニアが参加しました。 今回から2回に分けて、各エンジニアが印象に残ったセッションの感想を参加レポートとしてお届けします。 Good first issues of TypeProf rubykaigi.org このセッションでは、動的型付け言語が苦手とするエディタ上でのエラー表示、コードジャンプ、コード補完などの機能を公式で提供しようとしている TypeProf の紹介と、TypeProf に貢献するための方法や tips の紹介がメインでした。 手元で TypeProf を動かして遊んでみる方法、バグを見つけたら修正しなくても known-issues として Pull Request を作るだけでも歓迎していること、TypeProf の実装における設計のコンセプトの紹介など TypeProf への貢献のハードルが下がるような発表でした。 自分としては、まず patch を歓迎していると分かったのが1つの収穫でした。TypeProf は mame さんが数年前から取り組んでいることは認知していましたが、まだ試験的な状態で他の開発者からの patch はあまり歓迎されないんじゃないかと感じていました。(そもそも patch を送るという発想すらなかった) 貢献へのハードルの下がった自分はいくつかの patch を投げてみました。迅速にレビューしてもらってありがたかったです。 github.com github.com github.com patch を投げてみた感想として、難しいのは間違いないものの新しい構文をサポートすることはクイズを解くような楽しさや、少しずつ機能が増え成長していく育成ゲーム的な要素を感じました。楽しい。ウキウキしながら Ruby "enbugging" quiz を解いた人は多分楽しめるんじゃないでしょうか。 また、個人的な TypeProf の見どころの1つはシナリオテストを動かすための構文をサポートしている ScenarioCompiler クラスです。正直処理を追いかけるのは大変ですが、Ruby の表現力の高さを再認識させてくれます。 github.com 個人的には TypeProf をはじめとする Ruby の型システム周りの技術要素には Ruby での開発体験を大きく高める可能性を秘めていると感じています。これからの動向を追いつつ、自分でもできそうな貢献を見つけていきたいと思います。 ( @euglena1215 ) speakerdeck.com Unlocking Potential of Property Based Testing with Ractor rubykaigi.org 概要 Ractorを使う機会というのがあまりないというのが、Rubyを利用する側の正直なところだと思うが「Propaty based testingがRactorの良きuse caseになるのでは?」と思い、その仮説をもとに取り組んだ。 普段MinitestやRSpecで書くテストは、あるプログラムの実行結果が指定した期待値になっているかどうかというテストを行っているだろう。これをExample based testingという。このテストでは、プログラマの思いつく範囲のテストしか行えないという弱点がある。 Propaty based testingとは、入力値に取りうる値をランダムに多くのパターンを機械的に与えながら実行し、与えられたすべての入力値に対してプログラムの実行がパスするかどうかをテストするものである。こちらはプログラマが想定しなかったような大量で多様なテストケースが実行されるのが強みである。ただし、大量のテストケースを実行するため、テストの実行時間は当然長くなってしまう。これをRactorを使うことで軽減できるのではないか?というのが今回の仮説である。Propaty based testingは大量にテストケースが実行されるものの、それぞれに関連性は全くなく、順序など気にせず並列で動かしても何の問題もないことからRactorのuse caseにぴったりだと言える。 Propaty based testingを簡単に行えるようにするために、 pbt gemを作成した。これはMinitestやRSpecといったテストフレームワークの代替となるようなものではなく、それらと組み合わせて使用されることを想定したライブラリである。また、pbtはテストが失敗した場合、その時点からシュリンキングという手法を使ってテストの失敗を再現する最小の入力値を求めに行く。 実際にPropaty based testingをRactorを使って実行したところ、CPU-boundな処理を実行するテストではRactorを使うことでシーケンシャルなものより5倍ほど速いという結果になった。しかしそれ以外のケースでは圧倒的にシーケンシャルなものの方が速いという結果であった。 Ractorを使うことで、Propaty based testingの実行時間が短くなるのか?という仮説に対する結果は「部分的にそう」と言える。また、今回の実験でRactorに対応したエコシステムがまだまだ不足しているなどの課題感も見えた。 感想 Propaty based testing自体には別件で raap gemを知った際に興味を持っていたため、デモ含めとても勉強になりました。確かに実行時間がネックになるだろうなとは思っていたものの、 Ractorではほとんど解決しないという結果に驚きました。勉強不足であるためにCPU-boundな処理というものが具体的にすぐにイメージできないのが悔しいところではあるのですが、少なくとも利用できるケースがあることがわかっただけでも学びになりました。 pbt gemについて言及させてもらうと、これは所謂テストにロジックを書くものになりそうだなというのがパッと見の感想です。しかし試したわけではないので、実際に使ってみるとまた違った感想になるかもしれません。仮に本当にテストにロジックを書くものになってしまう場合、好みは分かれてしまいそうだなと感じました。 周辺ライブラリがRactor互換ではないことは、Ractorを使いたい人たちからするととても大きな問題だと思います。自分もRactorに興味のある一人ではありますが、未だRactorを使うために真剣に動き出せてはいないので、いい刺激をもらいました。周辺ライブラリがRactor互換になることはとても良いことだと思う一方で、そのための実装は複雑になってしまうかもしれません。それでもRactor対応を歓迎してくれるライブラリは、その旨をどこかに明記しておくと、有志からのcontributeを得やすいのではないかと感じています。 (@rhiroe) speakerdeck.com It's about time to pack Ruby and Ruby scripts in one binary rubykaigi.org 概要 Rubyで作ったゲームを配布したいが、配布先の環境でRubyのコードを動かすためには、Ruby本体やGemのインストールが必要であったり、それらのバージョンを揃えたりする必要があるため面倒である。これを解決し、配布先で事前準備なしで気軽にプログラムを実行できるようにするため、RubyをOne binaryに変換して配布したいと思うようになった。ここでのOne binaryとは単一ファイルのみで実行可能なプログラムを指している。 プログラムの実行環境をパッケージングして配布するという意味ではDockerやWasmも似たような手段として考えられるが、ちょっとしたプログラムを実行したいだけでDockerを使うのはヘビーだし、WasmだとRubyの機能が一部制限されてしまう。また、RubyのコードをOne binary化するライブラリはすでに存在するものの、以下のような課題を抱えている。 対応しているRubyバージョンが古い Ruby本体にパッチを当てているためメンテが大変 Windows限定 一時ファイルに書き出す処理があり遅い そのため、これらを解消した Kompo gemを開発した。Kompo gemはモンキーパッチのみ、一時ファイルへの書き込みなし、gemのインストールのサポートをしているとのこと。 感想 Rubyで書かれたプログラムをOne binary化し配布するという発想は、Rubyが好きだからこそ生まれるものだと感じており、とてもRubyKaigiらしい話でした。なので、ここではあえてRuby以外の言語を使用するという話はしないことにします。 Rubyで書かれたプログラムをOne binary化したい動機をゲーム以外で考えてみると、CLIツールが真っ先に思い浮かびます。またGUIツールもRubyで作成可能なので、これを配布したい場合もOne binary化したい動機につながりそうです。自分の身の回りにはプログラムのことなんて全くわからないという人の割合の方が多く、そういった人たちに作業の効率化のツールとして配布したい場合に、One binaryであるという点はとても魅力的に感じました。 セッション中に、手元のRubyファイルに変更が加わると実行結果が変わるという、面白いデモを見せてもらいました。これは、Kernel#require等にモンキーパッチを当てることで実現しているそうです。One binary化するツールとしてKompo gemの紹介がされていましたが、モンキーパッチを当てていたりOne binary化するにあたってのキモの実装は Kompo-vfs の方にありそうです。 Kernel#require等にモンキーパッチを当てたという話はこの辺りのことだと思われます。 github.com RubyKaigi中の別の発表でKernel#requireはよく上書きされている話を聞いており、「ここでもKernel#requireが上書きされる事例が…!」と思いながら聞いていたのを覚えています。Kernel#requireの上書きって、具体的にどういう用途で使われるんだろう?というのが気になっていたので、勉強のための良い教材となってくれそうです。 普段の業務で「RubyをOne binary化したい!」と感じたことは残念ながらないのですが、趣味で作ったプログラムを「プログラムを1ミリも知らない友人に使って欲しい」と思ったことはあります。そういったものを作る場合にRubyが選択肢の1つとして挙がることはRubyistとして大変喜ばしいことです。Kompo gem、同じRubyが好きな者として、機会があればぜひ利用させていただこうと思います。 (@rhiroe) Let's use LLMs from Ruby 〜 Refine RBS types using LLM 〜 rubykaigi.org Leaner Technologies に所属する黒曜さん(kokuyouwind) の発表です。 Rubyの型定義であるRBSを大規模言語モデル(Large Language Models: LLM)を用いて推測し、アウトプットの質やスピードをLLMのプラットフォームやモデル間で比較検証した内容でした。 RubyにおけるLLMを用いた型推測については、昨年のRubyKaigi 2023において MatzのKeynote でも触れられており、個人的に関心があるテーマでした。今回は現実的な活用性がどれほどあるか気になって視聴しました。 結論から述べると、まだ業務レベルでLLMの推測一本では期待した型定義が生成可能ではありませんでした。LLMのプラットフォームやモデル間でも成績に大幅な差がある状態となっております(詳しくは発表スライドをご覧ください) 一方でOpenAIのGPT-4 Omniモデルは比較した他モデルに比べても圧倒的な精度とスピードを誇っていました。 GPT-4 Omniは偶然にもRubyKaigi 2024のDay0(開催前日)に発表されていたのですが、流石に日程的に反映は難しいだろうと思っていたところ、黒曜さんが急ピッチで実行結果を取ったらしくDay1の発表に間に合っていて心の中で称賛の拍手を送りました笑。 発表中にはRubyコードから型推測を行うgemとして本人が作成された rbs_goose やRubyでLLMを活用するためのgemである Langchain.rb などが紹介されました。いずれも私自身の知見が乏しかったので実例を通してライブラリを知ることが出来たのは有益でした。RubyのLangchainはPythonやJavaScript版のそれと比較してGitHub上のスター数などが見劣りしてはいますが、今後Rubyへの型システムの浸透が進むと関連して活用も増えていくのではないかと考えています。 (江田) slides.com 最後に RubyKaigiでは毎年多くの学びがありますが、今年もたくさんの知見を得ることができました。今から来年のRubyKaigiが楽しみです。 さて、RubyKaigi参加メンバーによる残りのセッションまとめも発信予定。次回パート2の更新もぜひお楽しみに! Written by: @euglena1215 , @rhiroe, 江田
アバター