Timee Product Team Blog

タイミー開発者ブログ

Real-World Product Deployment of Adaptive Push Notification Scheduling on Smartphones を読んでみた

株式会社タイミーでデータサイエンティストをしている渡邉です。

タイミーでは、スマートフォンへのプッシュ通知を利用してタイミーを利用されているワーカーの方にキャンペーン情報やおすすめのお仕事を通知しています。通知した情報をワーカーの方が開封することで初めて詳細な情報を確認することができるのですが、タイミーのアプリ以外のアプリでもプッシュ通知を利用されていると、様々なアプリから日々大量の通知を受け取っているという状況が発生しているかと思います。 このような状況下では、ワーカーの方々が重要な情報を見逃したり、通知の頻度が多すぎてストレスを感じたりする可能性があります。そのため、より効果的でユーザーフレンドリーな通知システムの構築が必要だと考えています。

そんな中でこの問題について有効と思われる手段を提案されている論文 「Real-World Product Deployment of Adaptive Push Notification Scheduling on Smartphones」[1] を見つけたので、本ブログではこの論文の内容を紹介したいと思います。

論文の概要

本論文では、プッシュ通知技術のアプローチである「適応型通知スケジューリング」について詳細に報告しています。慶應大学と Yahoo! JAPAN が実施した大規模研究(382,518人のユーザー、28日間)を通じて、この新しいアプローチの仕組みとその効果が検証されました。 まず従来のプッシュ通知システムの問題点を明らかにし、それらを解決するための適応型通知スケジューリングの手法を提案しています。この手法は、ユーザーの行動パターンを学習し、最適なタイミングで通知を配信することで、通知の効果を大幅に向上させることを目指しています。 システムの設計から実装、そして大規模な実験結果までが詳細に記述されており、この新しいアプローチがユーザーエンゲージメントにどのような影響を与えるかを明らかにしています。さらに、この技術の実用化に向けた課題や今後の展望についても議論されています。

従来のプッシュ通知手法の問題点

従来のプッシュ通知システムには、以下のような問題点があると述べられています。

  1. ユーザーの状況を考慮しない一方的な通知配信

    ユーザーが何をしているかに関係なく通知を送信するため、作業の中断や集中力の低下につながる可能性がある。

  2. 頻繁な割り込みによるユーザーの生産性低下とストレス増加

    頻繁に作業を中断されることで、ユーザーにストレスや不満を与える可能性がある。

  3. 通知の無視や設定オフにつながるユーザーの疲弊

    インタラプション過負荷の状態になると、ユーザーは通知を無視したり、通知設定をオフにしたりする可能性がある。これは、プッシュ通知本来の目的である情報伝達を阻害する要因となる。

  4. クリック率や反応時間の低下によるマーケティング効果の減少

    通知が頻繁に送られてくることで、ユーザーは通知に対して鈍感になり、クリック率や反応時間の低下につながる可能性がある。

これらの問題は、ユーザーエンゲージメントの低下と、プラットフォームの価値低下につながる危険性があります。

適応型通知スケジューリングの概要

適応型通知スケジューリングの大事なポイントは、ユーザーの「ブレークポイント」を検出することです。ブレークポイントとは、ユーザーが一つのタスクを終え、次のタスクに移る瞬間のことを指します。この瞬間は、ユーザーの認知負荷が低く、新しい情報を受け取るのに適しているとされています。 本論文では、スマートフォンのセンサーデータと機械学習を組み合わせて、リアルタイムでブレークポイントを検出するシステムについて述べられています。このシステムは、ユーザーの活動状態、時間帯、デバイスの状態などの多様なコンテキスト情報を活用しています。 具体的には、474 の特徴量を用いた分類モデルを構築し、ユーザーの状態をリアルタイムで評価します。

主な特徴量には以下が含まれます。

  • 時間帯情報
  • デバイスの状態(充電状況、音量設定など)
  • ユーザーの活動状態(Google Activity Recognitionを使用)
  • アプリケーションの使用状況

モデルは線形回帰をもとにしており、日々のユーザーデータを用いて更新されます。また、平日と週末で異なる行動パターンに対応するため、コンテキストに応じた重み付けを導入しています。 そして、ブレークポイントが検出されるまで通知の配信を遅延させ、最適なタイミングで通知を表示します。

システムアーキテクチャ

システムは主にクライアント側とサーバー側のコンポーネントで構成されています。

クライアント側:

  • センサーデータの収集
  • 特徴抽出
  • リアルタイムのブレークポイント検出
  • 通知の遅延と表示

サーバー側:

  • ログデータの収集と分析
  • 日次モデルの更新
  • 新しいモデルのクライアントへの配信

このアーキテクチャにより、ユーザーの行動パターンを日々学習し、モデルを継続的に改善することが可能となります。

実験と結果

382,518人の Android 利用ユーザーを対象に実験を実施しています。実験では、ユーザーを Test 群と Control 群にランダムに分割し、Test 群には新しい適応型通知システムを適用し、Control 群には従来の通知システムを使用しています。

主な結果は以下の通りです。

  1. クリック率の平均が 23.3% 向上(最大 41.6%)

    適応型通知スケジューリングを導入した結果、ユーザーのクリック率は平均で 23.3% 向上し、最大で 41.6% の向上率が確認されました。

  2. ターゲット通知のクリック率の平均が 30.6% 向上(最大 60.7%)

    特に、ユーザーの興味関心にもとづいてパーソナライズされたコンテンツを含むターゲット通知では、平均 30.6%、最大 60.7% と、より高いクリック率の向上が見られました。

  3. 一般通知のクリック率の平均が 11.1% 向上(最大 42.9%)

    一方、すべてのユーザーに同じ内容が配信される一般通知でも、平均 11.1%、最大 42.9% のクリック率向上が確認されました。

  4. 通知配信後 120 秒以内のクリック率が 2.6〜2.86 倍に向上

    適応型通知スケジューリングでは、通知配信後 120 秒以内のクリック率が、大幅に向上しました。

    • 一般的な通知の場合:クリック率が2.60倍に向上 (実験群のクリック率 / 対照群のクリック率)
    • ターゲット通知の場合:クリック率が2.86倍に向上 (実験群のクリック率 / 対照群のクリック率)

    これらの結果はユーザーのブレークポイントに合わせて通知が配信されることで、ユーザーの関心を高め、即座に反応しやすくなったためと考えられます。特にターゲット通知(パーソナライズされた内容)の場合、より大きな効果が見られました。この結果は、適切なタイミングとパーソナライズされた内容の組み合わせが、ユーザーエンゲージメントを大幅に向上させる可能性を示唆しています。

  5. 速報ニュースが多い日でも高いパフォーマンスを維持

    速報ニュースが多い日でも、適応型通知スケジューリングは高いパフォーマンスを維持しました。これは、ユーザーのブレークポイント以外のタイミングで配信される速報ニュースが多い日でも、適応型通知は、ユーザーの適切なタイミングで配信されるため、クリック率への影響が少なかったと考えられます。

これらの結果は、適応型通知スケジューリングの有効性を強く示唆しています。

考察

実験結果から、以下の点が考察されます。

  1. ユーザーの状況を考慮した通知は、大幅にエンゲージメントを向上させる
  2. パーソナライズされた内容(ターゲット通知)と適切なタイミングの組み合わせが最も効果的
  3. 適応型システムは、緊急時の通知と日常的な通知のバランスを効果的に管理できる
  4. ユーザーの即時反応性の向上は、情報のタイムリーな伝達に有効

これらの知見は、マッチングプラットフォームにおける通知戦略の最適化に大きく寄与すると考えられます。

今後の課題と展望

今後の主な課題と展望として、以下が挙げられています。

  1. さらなるパーソナライゼーションの追求(通知内容と配信タイミングの最適化)
  2. クロスプラットフォーム対応(iOS, ウェブなど)
  3. リアルタイム処理の効率化

これらの課題に取り組むことで、より効果的でユーザーフレンドリーな通知システムの実現が期待されます。

まとめ

適応型通知スケジューリングは、ユーザーの状態を考慮したアプローチです。大規模実験の結果は、この手法が従来の問題点を大きく改善し、ユーザーエンゲージメントを向上させることを示しています。 マッチングプラットフォームにおいても、求職者と求人情報のマッチング精度の向上、ユーザー満足度の向上、そしてプラットフォーム全体の価値向上が期待できます。今後もこのような論文から得られた知見をもとに、タイミーアプリの継続的な改善を重ね、ユーザーとビジネスの双方に価値をもたらすことに貢献していきたいと考えています。

現在、タイミーでは、データサイエンスやエンジニアリングの分野で、共に成長し、革新を推し進めてくれる新たなチームメンバーを積極的に探しています!

また、気軽な雰囲気でのカジュアル面談も随時行っておりますので、ぜひお気軽にエントリーしてください。↓

product-recruit.timee.co.jp

hrmos.co

hrmos.co

参考文献

[1] : Okoshi, T., Tsubouchi, K., & Tokuda, H. (2019). Real-World Product Deployment of Adaptive Push Notification Scheduling on Smartphones. In Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining (KDD ’19) (pp. 2792-2800), DOI: 10.1145/3292500.3330732 (2019)

DroidKaigi 2024登壇レポート

ふなち(https://x.com/_hunachi)です。

DroidKaigi 2024で登壇してきました!

初めてのDroidKaigiでの登壇はとても緊張しましたが、良い経験になりました。

登壇内容

タイムテーブル

2024.droidkaigi.jp

登壇資料

speakerdeck.com

登壇動画

www.youtube.com

この内容で登壇した経緯

昨年から今年の頭にかけて、基本一人でもくもくPDFを表示するコードを書く機会がありました。

話はそれますが、iOSでPDFを表示させたいとなった時、公式のFrameworkであるPDFKit(https://developer.apple.com/documentation/pdfkit)を使うことができます。これが高性能で、リンクの表示や文字列検索など色々できてしまうのです。

しかしAndroidは… 調査した結果、公式から出ているAPI(PdfRenderer)では、Bitmap生成とページ数取得くらいしかできませんでした。 また、実装するときに気にすることも色々あり少し大変な道でした。Bitmapを大量に扱うので何も考えないで実装すると簡単にメモリリーク等させることができるのです。 しかもネット上の記事にも、(当時は特に)英語のもの含めPDFビュアーに関してメモリを考慮して実装したというようなものはありませんでした。

そんな困難に見舞われながらも実装することができたので一旦よかったのですが、一部悔しさはありました。

そんな時です。 Android 15のリリース予告記事でPDF周りのAPIがかなり強化されると発表されました🎉 記事を読んでみると、文字列検索などもできるらしく、革命だ!と思いました。

そのような中、Android 15(Vanilla Ice Cream)をエミュレーターで動かせるようになったタイミングで、休日も使い色々実装してみました。

ハッカソンで作ったようなコードと見た目なので今後も公開はしない予定ですが、PDFを要約する機能や、特定の文字列がある部分を枠で囲ってくれる機能、フォームの内容を変更できる機能を試すことができるアプリを作りました。 色々できるようになっていることを知れて、嬉しかったのを覚えています。

しかもこの時、運よくDroidKaigiのプロポーザル提出期間でした。 もともと緊張するタイプの私にとって、40分という時間のハードルはそこそこ高く、特に発表したいことがなければ提出するつもりはなかったのですが、PdfRendererの素晴らしい進化を伝えたいという気持ちに負けて、プロポーザルを提出しました。

その結果嬉しいことに、採択されました🙌

登壇準備

採択された後は、発表資料を作成するため、今までやってきた経験をもとに一から簡易的なPDF Viewerを作り、そのコードを使い発表資料を作成しました。 コードを書く方に時間を費やしすぎて、焦って資料作りすることになったので今後は気をつけようと思います。

資料を作る中で、やらかしたかも!と思う出来事もありました。 プロポーザルの編集期限が過ぎた後に、GDEの方のSNSでの発言により、androidx配下にpdf/pdf-viewerというディレクトリが爆誕していることに気がついたのです。プロポーザルに書き損ねてしまったことに後悔していたら、資料作成途中の8月頭にandroidx.pdf:pdf-vieweralphaがリリースされました。

そこで、プロポーザルに書いていないが話さないわけにはいかないなと思い、発表資料にandroidx.pdf:pdf-viewerのことも組み込むことになりました。

このandroidx.pdf:pdf-viewerの内部実装について紹介しようか迷いましたが、このライブラリと同じようなことをしたいのであればこのライブラリを使えばいいなと思い、重要な部分(プロセス分離の部分)を除いて内部実装については発表しないことにしました。

また同時通訳対象だったため、日英両方でスライドを書いたり、事前にスライドを提出したりする必要がありました。 大変な部分もありましたが、英語のチェックを社内の方や家族にしてもらえて助かりました🙇

発表数日前にはタイミーのメンバーの前で発表練習もさせてもらいました。 その際に色々と質問等をもらえたおかげで、ブラッシュアップすることができました。 忙しい中時間を作っていただき感謝です!

余談ですが、弊社には登壇や執筆をサポートする制度もあります。

productpr.timee.co.jp

今回私は入社前に登壇準備をすることになったため、あまり利用していませんが、今後はこちらの制度も活用したいです!

発表当日

発表では、めちゃくちゃ緊張していましたが、なんとか発表を無事終えることができました。 緊張で止まってしまったりすることもなく、発表時間もちょうどで終わったようでよかったです。

ただ、早く喋り過ぎたので同時通訳の方には負荷をかけただろうなと思い反省しています。次に同じような機会に恵まれた際には気をつけます。

少しニッチな内容なので、Ask the Speakerも暇になるかなと思っていましたが、 「今PDF関連のことを触っていて〜」や「ちょうど調べている内容なので助かりました!」と英語話者の方も含め質問していただいたり、声をかけていただいたりして嬉しかったです。

当日だけじゃなく後日含め、SNSやブログでもピックアップしてコメントをくださった方々もありがとうございます。 直接の反応はあまりできていませんが、見るたびにとても嬉しい気持ちになっています。

写真はDroidKaigiが公開しているアルバムから引用

引用元:https://x.com/DroidKaigi/status/1840692565538136507

発表以外の感想

実は今年もスタッフをしており、主にセッション進行のお手伝いを行なっていました。

また、スタッフ業務をしながら他の方のセッションを見ていたのですが、そこにも多くの学びがありました。 まだ全てのセッションを見ることができてないので時間を作って見てみようと思います。

アフターパーティなどでも他の登壇者の方や私の発表を聞いてくださった方、初めましての方や久しぶりにお会いした方々とお話しすることができ、とても楽しかったです。

まとめ

私の発表を聞いてくださった方々、登壇のサポートをしてくださった方々、スタッフの方々など、皆様に感謝しています。ありがとうございます!

40分の登壇を経験したことで、他の登壇している人たちをさらに尊敬できるようになりました。 とても良い経験でした。

これからも積極的にインプットとアウトプットをしていこうと思います!

余談

発表中緊張して途中で声が出なくなるといけないので、いざという時にお面をつけて落ち着こうと考えてました。お面は前日の夜に緊張具合を考慮し作成しました。 登壇直前のマイク確認でお面をつけて喋ってもマイクが通るかの確認もしました。 確認の結果、お面をしていてもマイクに音は乗ることがわかりました。 このいざという時の備えができた安心感も発表を乗り切れた理由の一つかもしれません。 お面参戦なら登壇できるかもという方は、来年以降プロポーザル提出チャレンジをしてみると良いと思います!

また、他のメンバーによるDroidKaigi 2024参加レポートも公開しています。是非こちらも読んでみて下さい↓

tech.timee.co.jp

DroidKaigi 2024 参加レポート

9/11~9/13 にかけて DroidKaigi 2024 が開催され、タイミーの Android アプリエンジニアチームが参加してきました。

はじめに

9月にジョインされた hunachi が登壇しています。Android の PDF Viewer に関する歴史や詳細な実装からライブラリの紹介まで PDF Viewer を網羅したセッションとなっているのでぜひアーカイブでご覧ください。

2024.droidkaigi.jp

タイミーではブースも出しておりノベルティやタイミンの写真を撮りに来られる方など盛況でした。足を運んでいただいた皆様ありがとうございます!

この DroidKaigi から配布するノベルティに新しく「マイクロファイバークロス」が追加されました。めちゃめちゃかわいいデザインになっているので手に入れた方はぜひ使ってみてください!

また、ネイルブースの横に出していたので「せっかくだし」と初めてネイルを体験しましたが、とてもかわいくテンションも上がって最高でした。

今回は DroidKaigi 2024 に参加した Android アプリ開発メンバー(tick-taku, murata, haru, nashihara)が気になったセッションの感想などをレポートします。

セッション紹介

nashihara

Day1: From 0 to 100 with Kotlin and Compose Multiplatform

2024.droidkaigi.jp

Kotlin Multiplatform / Compose Multiplatform について、初学者向けにどうやって書いたらいいのかや platform ごとの書き分けの方法などが解説されていました。

Day1のセッションはこちらのみでワークショップ形式での講演でした。

ワークショップの内容は、platform ごとのコードを書く場所の説明から始まり、platform ごとに使えるAPIの種類(例えば kotlin api は全ての platform で使えるが android api は android platform でしか使えない)の説明やそれぞれの Lifecycle がどうなっているかなど、主にこれから KMP/CMP を触り始める人を対象とした内容でした。

自分は今回初めて触る内容が多く新鮮で面白かったです。

特にワークショップの課題として実際に Compose で書いたUIを web / desktop / android platform ごとに実行し確認する、という内容がありましたが、いつも android でUIを作る感覚でUIを書くだけで、 web / desktop アプリを作れてしまうのは、わかっていても感動しました。

Lifecycle に関しても、ちゃんとハンドリングできるようになっていて、web であれば focus → onResume、 blur → onPause 、ios であれば viewWillAppear → onStart、 viewDidDisapper → onStop となっており、 android アプリ開発者が見慣れた形で実装できる点がすごく良いです。

また、collectAsStateWithLifecycle を使っておけばいい感じになる、という便利メソッドも教えてもらいました。

初学者にとって、KMP / CMP の良い導入になるワークショップで楽しいセッションでした。

tick-taku

デザインからアプリ実装まで一貫したデザインシステムを構築するベストプラクティス

2024.droidkaigi.jp

デザインシステムにおけるFigma との連携やプラクティス、導入することのメリットや心構えなどが解説されています。 最近タイミーではデザインシステムを導入しました。その時にリードしてくれたデザイナーの方が話していた内容が、エンジニア目線から解説されていてより理解が深まりました。

個人的には Fimga を中間言語として会話できるようになる ことにとても共感しています。 UI 実装においてデザインファイルがスナップショットになっており、実装との差が発生することでデザイナーとのコミュニケーションが都度発生し、開発スピードが遅くなったり心理的ハードルになったりしていました。デザインシステムを導入することで Figma を起点としてデザイン製作時にエンジニアとデザイナー双方のエッセンスが考慮されたコンポーネントで作られることになり、これらの課題のコスト軽減が期待できます。

また Figma の property と Composable の引数を同期させれば、 Figma 上に実装のために必要な情報が全て書かれています。それだけ見ればある種脳死で実装できるようになり、より統一感のある UI 実装がスピーディ & スムーズに行えるようになります。

Figma は素晴らしいツールですね!

デザインシステム導入時に非常に勉強になるポイントがちりばめられたセッションでした。スライドもとても分かりやすく見やすかったです。

2024年のナビゲーション・フォーカス対応:Composeでキーボード・ナビゲーションをサポートしよう

2024.droidkaigi.jp

Android のアクセシビリティにおける focus についての紹介や実装のポイントが解説されているセッションでした。

実装についてはアクセシビリティを意識したユースケースの紹介と実装や、xml を何年も書いてない人のために Compose での実装方法や tips も紹介されていました。

特に「宣言順序とレベルによって focus が流れる」という話は、なんとなくそうなんだろうなと思っていたところもあって納得できました。focus のことを考えるとやはり ConstraintLayout を多用するのはよくないのかも。 やっぱり Modifier 順序問題は難しいですね...

また動作確認方法についても触れられていました。 特に Android Studio 上で物理デバイスのミラーリングができることは個人的に初耳で知れて非常に良かったです。

いつもスプリントレビューなどでチームに画面共有する際に、ブラウザと並べて表示するため scrcpy を使っていましたが IDE だけで済みそうです。

ソフトウェアキーボード体験改善の tips はまた今度とのことなのでそちらも楽しみに待っています。

haru

Jetpack ComposeにおけるShared Element Transitionsの実例と導入方法 またその仕組み

2024.droidkaigi.jp

Shared Element Transition, 皆さんは使っていますか?

私たちタイミーのアプリ内でもいくつかの場所で使用しており、Compose化するときに泣く泣くShared Element Transitionを使わない方法で実装し直したりしていましたが、ついにComposeでも取り入れられるようになってきました。

このセッションでは、実際にGoogle PhotosのようなUIをComposeで実装していくことによって実際の実装方法、つまづきポイント、その解決方法などを順番に紹介してもらうことができました。

Transitionにもいくつか種類があり、どちらの方が見え方が綺麗なのかといったところまで説明されていて実際に取り入れる際にもとても参考になるセッションでした。

murata

Kotlin 2.0が与えるAndroid開発の進化

2024.droidkaigi.jp

タイトル通り、Kotlin 2.0によって受けられる恩恵がこれでもかというくらい紹介されていたセッションでした!

数も多かったですが、特に個人的に刺さったものをピックアップして紹介します。

Power AssertをKotlinが正式にサポート 🎉

従来のUnitTestにおけるFailed Messageは非常にシンプルにExpectedとActualの値が表示されるだけのメッセージでした。

Expected :0
Actual   :6

Power Assertを導入した場合は、以下のようにテストが失敗した理由を事細かに表示してくれます。※公式ページより引用

Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
       |     |      |  |     |               |
       |     |      |  |     |               3
       |     |      |  |     orl
       |     |      |  world!
       |     |      false
       |     5
       Hello

注意点として、assert式の書き方をPower Assertを意識した書き方に少し変える必要はありそうです。

例えば、 assert(isValidName && isValidAge) のような変数のみをassert式に入れてしまうと以下のようにFailed Messageの情報量も減ってしまう為、先ほどの例のように変数をインライン化する必要があります。

Assertion failed
assert(isValidName && isValidAge)
       |              |
       |              false
       true

ですが、導入することでテストを用いた開発やCIが落ちた時の要因調査が捗ること間違い無しの素晴らしい機能ですね!!

Jetpack Compose Strong Skip Mode enabled by default

Kotlin 2.0.20 より、ComposeのStrong Skip ModeがデフォルトでONになりました。

具体的には、Unstableな引数を使用していても、同一instanceであればRecomposeされなくなります。

Strong Skip Modeについては既に各所で話題になっていましたが、さらにこのセクションで紹介されていた以下のポイントが個人的に刺さりました。

Stability Configuration File

使用する箇所とは遠いところでStableの指定を行う行為自体に懐疑的でしたが、セッションで紹介されていた「java.time.LocalDateのようなJavaのクラスをStableだと認識させる」ようなユースケースではかなり有用だと感じました。

Lambdaの再生成の条件

Lambdaの中で一般的なViewModelのようなUnstableな変数を参照していても再生成されなくなるといった点は非常に便利だなと感じました。onClickのコールバックにてViewModelのメソッドを呼ぶようなこと、よくありますもんね。

代わりに、Lambdaの再生成を前提にしていたようなケースでは @DontMemoize を新たに指定する必要が出てくる点にも地味に注意が必要だと感じました。たまによくありそう。

Object equalsとInstance equals

従来はListは常にUnstableだと見なされていた為、タイミーではComposableの引数にリストを指定する場合はListではなくkotlinx.collections.immutable.ImmutableList を利用するルールとしていました。

ただし、セッションで紹介されている通り、パフォーマンス観点において「Object equalsはO(n)に対してinstance equalsはO(1)」となります。

よって、件数が多く複雑なlistである場合には、ImmutableListではなくListを指定することでinstance equalsとした方が速くなるケースが考えられる為、Strong Skip ModeがONになった際にはどちらを使用するのか都度検討する必要がありそうです。

これらの他にもKotlin 2.0 の良きポイントを理解できる情報がたっぷりな、とても良いセッションでした!! 発表いただき本当にありがとうございました!

まとめ

9月に新たにジョインされたメンバーも KaigiPass 制度を利用してみんなでワイワイ参加できました。カンファレンス後にチーム内でセッションのシェアやディスカッションを行うなど、非常に多くの学びやヒントを得ることができモチベーションへ繋がるカンファレンスとなりました。

ブースやアフターパーティーなどでさまざまな人と交流できたり、セッションのアーカイブのアップが早くその日に見直すこともできたり素晴らしい体験ができました。スタッフの皆様ありがとうございました!

次回の開催も楽しみにしています!

余談

今回タイミーはDroidKaigi 2024にゴールドスポンサーとして協賛し、冒頭のブース出展だけでなくDroidKaigiスカラーシップの活動もお手伝いさせていただきました。 そして、DroidKaigiスカラーシップでは企業訪問の取り組みに参加し、DroidKaigiに参加されていた学生さんたちをタイミーのオフィスにご招待しました!

タイミー社員と一緒にランチをしたり、オフィス内を一緒に周り紹介したりする中で、熱心に活動されている学生のみなさんとお話できたことは、私たちにとっても楽しく、とても良い機会でした。

タイミーはこれからも技術コミュニティへの協賛、協力を通して一緒に盛り上げていきます!それではまたどこかの勉強会・カンファレンスでお会いしましょう👋

Steep エラーリファレンスを作りました(2024/09/30 時点)

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

タイミーでは RBS の活用を推進する取り組みを少しずつ進めています。意図はこちら

メンバーと雑談していたときに「steep check でコケたときにその名前で調べても全然ヒットしないので型周りのキャッチアップが難しい」という話を聞きました。
いくつかのエラー名でググってみたところ、 Ruby::ArgumentTypeMismatchRuby::NoMethod など有名なエラーはヒットしますがほとんどのエラーはヒットせず、ヒットするのは Steep リポジトリの該当実装のみでした。
これでは確かにキャッチアップは難しいだろうと感じたので、Steep のエラーリファレンスを作ってみました。ググってヒットするのが目的なのでテックブログとして公開してインデックスされることを期待します。

 

各エラーの説明は以下のフォーマットで行います。

エラー名

説明: 簡単なエラーの説明

例:

エラーが検出される Ruby コード
steep check を実行して得られるエラーメッセージ

severity:
Steep のエラープリセットに対して、該当エラーの severity がどのように設定されているかの表

 


Ruby::ArgumentTypeMismatch

説明: メソッドの型が一致しない場合に発生します。

違反例:

'1' + 1
test.rb:1:6: [error] Cannot pass a value of type `::Integer` as an argument of type `::string`
│   ::Integer <: ::string
│     ::Integer <: (::String | ::_ToStr)
│       ::Integer <: ::String
│         ::Numeric <: ::String
│           ::Object <: ::String
│             ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ '1' + 1
        ~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::BlockBodyTypeMismatch

説明: ブロックの body の返り値の型が期待される型と一致しない場合に発生します。

違反例:

lambda {|x| x + 1 } #: ^(Integer) -> String
test.rb:1:7: [error] Cannot allow block body have type `::Integer` because declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::BlockBodyTypeMismatch
│
└ lambda {|x| x + 1 } #: ^(Integer) -> String
         ~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error warning error information nil

Ruby::BlockTypeMismatch

説明: ブロックの型が期待される型と一致しない場合に発生します。

違反例:

multi = ->(x, y) { x * y } #: ^(Integer, Integer) -> Integer
[1, 2, 3].map(&multi)
test.rb:2:14: [error] Cannot pass a value of type `^(::Integer, ::Integer) -> ::Integer` as a block-pass-argument of type `^(::Integer) -> U(1)`
│   ^(::Integer, ::Integer) -> ::Integer <: ^(::Integer) -> U(1)
│     (Params are incompatible)
│
│ Diagnostic ID: Ruby::BlockTypeMismatch
│
└ [1, 2, 3].map(&multi)
                ~~~~~~

severity:

all_error default strict lenient silent
error warning error information nil

Ruby::BreakTypeMismatch

説明: break の型が期待される型と一致しない場合に発生します。

違反例:

123.tap { break "" }
test.rb:1:10: [error] Cannot break with a value of type `::String` because type `::Integer` is assumed
│   ::String <: ::Integer
│     ::Object <: ::Integer
│       ::BasicObject <: ::Integer
│
│ Diagnostic ID: Ruby::BreakTypeMismatch
│
└ 123.tap { break "" }
            ~~~~~~~~

severity:

all_error default strict lenient silent
error hint error hint nil

Ruby::DifferentMethodParameterKind

説明: メソッドのパラメータの種類が一致しない場合に発生します。省略可能な引数の prefix に ? をつけ忘れることで発生することが多いです。

違反例:

# @type method bar: (name: String) -> void
def bar(name: "foo")
end
test.rb:2:8: [error] The method parameter has different kind from the declaration `(name: ::String) -> void`
│ Diagnostic ID: Ruby::DifferentMethodParameterKind
│
└ def bar(name: "foo")
          ~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::FallbackAny

説明: 型が不明な場合に untyped が使用されることを示します。一度 [] で値を初期化したのちに再代入するような実装で発生することが多いです。

違反例:

a = []
a << 1
test.rb:1:4: [error] Cannot detect the type of the expression
│ Diagnostic ID: Ruby::FallbackAny
│
└ a = []
      ~~

severity:

all_error default strict lenient silent
error hint warning nil nil

Ruby::FalseAssertion

説明: Steep の型アサーションが誤っている場合に発生します。

違反例:

array = [] #: Array[Integer]
hash = array #: Hash[Symbol, String]
test.rb:2:7: [error] Assertion cannot hold: no relationship between inferred type (`::Array[::Integer]`) and asserted type (`::Hash[::Symbol, ::String]`)
│ Diagnostic ID: Ruby::FalseAssertion
│
└ hash = array #: Hash[Symbol, String]
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::ImplicitBreakValueMismatch

説明: 引数無し break の値( nil )がメソッドの返り値の期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs () { (String) -> Integer } -> String
  def foo
    ''
  end
end

Foo.new.foo do |x|
  break
end
test.rb:9:2: [error] Breaking without a value may result an error because a value of type `::String` is expected
│   nil <: ::String
│
│ Diagnostic ID: Ruby::ImplicitBreakValueMismatch
│
└   break
    ~~~~~

severity:

all_error default strict lenient silent
error hint information nil nil

Ruby::IncompatibleAnnotation

説明: 型注釈が不適切または一致しない場合に発生します。

違反例:

a = [1,2,3]

if _ = 1
  # @type var a: String
  a + ""
end
test.rb:5:2: [error] Type annotation about `a` is incompatible since ::String <: ::Array[::Integer] doesn't hold
│   ::String <: ::Array[::Integer]
│     ::Object <: ::Array[::Integer]
│       ::BasicObject <: ::Array[::Integer]
│
│ Diagnostic ID: Ruby::IncompatibleAnnotation
│
└   a + ""
    ~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::IncompatibleArgumentForwarding

説明: 引数に ... を使ってメソッドの引数を forward する際に、引数の型が一致しない場合に発生します。

違反例:

class Foo
  # @rbs (*Integer) -> void
  def foo(*args)
  end

  # @rbs (*String) -> void
  def bar(...)
    foo(...)
  end
end
test.rb:8:8: [error] Cannot forward arguments to `foo`:
│   (*::Integer) <: (*::String)
│     ::String <: ::Integer
│       ::Object <: ::Integer
│
│ Diagnostic ID: Ruby::IncompatibleArgumentForwarding
│
└     foo(...)
          ~~~

severity:

all_error default strict lenient silent
error warning error information nil

Ruby::IncompatibleAssignment

説明: 代入の際の型が不適切または一致しない場合に発生します。

違反例:

# @type var x: Integer
x = "string"
test.rb:2:0: [error] Cannot assign a value of type `::String` to a variable of type `::Integer`
│   ::String <: ::Integer
│     ::Object <: ::Integer
│       ::BasicObject <: ::Integer
│
│ Diagnostic ID: Ruby::IncompatibleAssignment
│
└ x = "string"
  ~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error hint nil

Ruby::InsufficientKeywordArguments

説明: キーワード引数が不足している場合に発生します。

違反例:

class Foo
  def foo(a:, b:)
  end
end
Foo.new.foo(a: 1)
test.rb:5:8: [error] More keyword arguments are required: b
│ Diagnostic ID: Ruby::InsufficientKeywordArguments
│
└ Foo.new.foo(a: 1)
          ~~~~~~~~~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::InsufficientPositionalArguments

説明: 位置引数が不足している場合に発生します。

違反例:

class Foo
  def foo(a, b)
  end
end
Foo.new.foo(1)
test.rb:5:8: [error] More keyword arguments are required: b
│ Diagnostic ID: Ruby::InsufficientKeywordArguments
│
└ Foo.new.foo(a: 1)
          ~~~~~~~~~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::InsufficientTypeArgument

説明: 型引数に対する型注釈が不足している場合に発生します。

違反例:

class Foo
  # @rbs [T, S] (T, S) -> [T, S]
  def foo(x, y)
    [x, y]
  end
end

Foo.new.foo(1, 2) #$ Integer
test.rb:8:0: [error] Requires 2 types, but 1 given: `[T, S] (T, S) -> [T, S]`
│ Diagnostic ID: Ruby::InsufficientTypeArgument
│
└ Foo.new.foo(1, 2) #$ Integer
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::InvalidIgnoreComment

説明: steep:ignore:start コメントはあるが steep:ignore:end コメントがないなど、無効なコメントが存在する場合に発生します。

違反例:

# steep:ignore:start
test.rb:1:0: [error] Invalid ignore comment
│ Diagnostic ID: Ruby::InvalidIgnoreComment
│
└ # steep:ignore:start
  ~~~~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error warning warning warning nil

Ruby::MethodArityMismatch

説明: キーワード引数なのに順序引数としてメソッドの引数の型を記述しているなど、メソッドの引数の型が一致しない場合に発生します。

違反例:

class Foo
  # @rbs (Integer x) -> Integer
  def foo(x:)
    x
  end
end
test.rb:3:9: [error] Method parameters are incompatible with declaration `(::Integer) -> ::Integer`
│ Diagnostic ID: Ruby::MethodArityMismatch
│
└   def foo(x:)
           ~~~~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::MethodBodyTypeMismatch

説明: メソッドの返り値が期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs () -> String
  def foo
    1
  end
end
test.rb:3:6: [error] Cannot allow method body have type `::Integer` because declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::MethodBodyTypeMismatch
│
└   def foo
        ~~~

severity:

all_error default strict lenient silent
error error error warning nil

Ruby::MethodDefinitionMissing

説明: メソッドの型定義が存在するがメソッドの実装が欠落している場合に発生します。

違反例:

class Foo
  # @rbs!
  #   def bar: () -> void
end
test.rb:1:6: [error] Cannot find implementation of method `::Foo#bar`
│ Diagnostic ID: Ruby::MethodDefinitionMissing
│
└ class Foo
        ~~~

severity:

all_error default strict lenient silent
error nil hint nil nil

Ruby::MethodParameterMismatch

説明: メソッドのパラメータの型が一致しない場合に発生します。

違反例:

class Foo
  # @rbs (Integer x) -> Integer
  def foo(x:)
    x
  end
end
test.rb:3:10: [error] The method parameter is incompatible with the declaration `(::Integer) -> ::Integer`
│ Diagnostic ID: Ruby::MethodParameterMismatch
│
└   def foo(x:)
            ~~

severity:

all_error default strict lenient silent
error error error warning nil

Ruby::MethodReturnTypeAnnotationMismatch

説明: メソッドの戻り値の型注釈が期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs () -> String
  def foo
    # @type return: Integer
    123
  end
end
test.rb:3:2: [error] Annotation `@type return` specifies type `::Integer` where declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::MethodReturnTypeAnnotationMismatch
│
└   def foo
    ~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::MultipleAssignmentConversionError

説明: 複数代入の変換に失敗した場合に発生します。

違反例:

class WithToAry
  # @rbs () -> Integer
  def to_ary
    1
  end
end

a, b = WithToAry.new()
test.rb:8:8: [error] Cannot convert `::WithToAry` to Array or tuple (`#to_ary` returns `::Integer`)
│ Diagnostic ID: Ruby::MultipleAssignmentConversionError
│
└ (a, b = WithToAry.new())
          ~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::NoMethod

説明: 型定義が存在しないメソッドが呼び出された場合に発生します。

違反例:

"".non_existent_method
test.rb:1:3: [error] Type `::String` does not have method `non_existent_method`
│ Diagnostic ID: Ruby::NoMethod
│
└ "".non_existent_method
     ~~~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::ProcHintIgnored

説明: Proc に関する型注釈が無視された場合に発生します。

違反例:

# @type var proc: (^(::Integer) -> ::String) | (^(::String, ::String) -> ::Integer)
proc = -> (x) { x.to_s }
test.rb:2:7: [error] The type hint given to the block is ignored: `(^(::Integer) -> ::String | ^(::String, ::String) -> ::Integer)`
│ Diagnostic ID: Ruby::ProcHintIgnored
│
└ proc = -> (x) { x.to_s }
         ~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint information nil nil

Ruby::ProcTypeExpected

説明: Proc 型が期待される場合に発生します。

違反例:

-> (&block) do
  # @type var block: Integer
end
test.rb:1:4: [error] Proc type is expected but `::Integer` is specified
│ Diagnostic ID: Ruby::ProcTypeExpected
│
└ -> (&block) do
      ~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::RBSError

説明: 型アサーションや型適用に書かれたRBS型がエラーを生じる場合に発生します。

違反例:

a = 1 #: Int
test.rb:1:9: [error] Cannot find type `::Int`
│ Diagnostic ID: Ruby::RBSError
│
└ a = 1 #: Int
           ~~~

severity:

all_error default strict lenient silent
error information error information nil

Ruby::RequiredBlockMissing

説明: メソッド呼び出し時に必要な block が欠落している場合に発生します。

違反例:

class Foo
  # @rbs () { () -> void } -> void
  def foo
    yield
  end
end
Foo.new.foo
test.rb:7:8: [error] The method cannot be called without a block
│ Diagnostic ID: Ruby::RequiredBlockMissing
│
└ Foo.new.foo
          ~~~

severity:

all_error default strict lenient silent
error error error hint nil

Ruby::ReturnTypeMismatch

説明: return の型とメソッドの戻り値の型が一致しない場合に発生します。

違反例:

# @type method foo: () -> Integer
def foo
  return "string"
end
test.rb:3:2: [error] The method cannot return a value of type `::String` because declared as type `::Integer`
│   ::String <: ::Integer
│     ::Object <: ::Integer
│       ::BasicObject <: ::Integer
│
│ Diagnostic ID: Ruby::ReturnTypeMismatch
│
└   return "string"
    ~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error error error warning nil

Ruby::SetterBodyTypeMismatch

説明: セッターメソッドの戻り値の型が期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs (String) -> String
  def foo=(value)
    123
  end
end
test.rb:3:6: [error] Setter method `foo=` cannot have type `::Integer` because declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::SetterBodyTypeMismatch
│
└   def foo=(value)
        ~~~~

severity:

all_error default strict lenient silent
error information error nil nil

Ruby::SetterReturnTypeMismatch

説明: セッターメソッドの return の型が期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs (String) -> String
  def foo=(value)
    return 123
  end
end
test.rb:4:4: [error] The setter method `foo=` cannot return a value of type `::Integer` because declared as type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::SetterReturnTypeMismatch
│
└     return 123
      ~~~~~~~~~~

severity:

all_error default strict lenient silent
error information error nil nil

Ruby::SyntaxError

説明: Ruby の構文エラーが発生した場合に発生します。

違反例:

if x == 1
  puts "Hello"
test.rb:2:14: [error] SyntaxError: unexpected token $end
│ Diagnostic ID: Ruby::SyntaxError
│
└   puts "Hello"

severity:

all_error default strict lenient silent
error hint hint hint nil

Ruby::TypeArgumentMismatchError

説明: 型引数が期待される型と一致しない場合に発生します。

違反例:

class Foo
  # @rbs [T < Numeric] (T) -> T
  def foo(x)
    x
  end
end
Foo.new.foo("") #$ String
test.rb:7:19: [error] Cannot pass a type `::String` as a type parameter `T < ::Numeric`
│   ::String <: ::Numeric
│     ::Object <: ::Numeric
│       ::BasicObject <: ::Numeric
│
│ Diagnostic ID: Ruby::TypeArgumentMismatchError
│
└ Foo.new.foo("") #$ String
                     ~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::UnexpectedBlockGiven

説明: ブロックが予期されない場面で渡された場合に発生します。

違反例:

[1].at(1) { 123 }
test.rb:1:10: [error] The method cannot be called with a block
│ Diagnostic ID: Ruby::UnexpectedBlockGiven
│
└ [1].at(1) { 123 }
            ~~~~~~~

severity:

all_error default strict lenient silent
error warning error hint nil

Ruby::UnexpectedDynamicMethod

説明: 動的に定義されたメソッドが存在しない場合に発生します。

違反例:

class Foo
  # @dynamic foo

  def bar
  end
end
test.rb:1:6: [error] @dynamic annotation contains unknown method name `foo`
│ Diagnostic ID: Ruby::UnexpectedDynamicMethod
│
└ class Foo
        ~~~

severity:

all_error default strict lenient silent
error hint information nil nil

Ruby::UnexpectedError

説明: 予期しない一般的なエラーが発生した場合に発生します。

違反例:

class Foo
  # @rbs () -> String123
  def foo
  end
end
test.rb:1:0: [error] UnexpectedError: sig/generated/test.rbs:5:17...5:26: Could not find String123(RBS::NoTypeFoundError)
│ ...
│   (36 more backtrace)
│
│ Diagnostic ID: Ruby::UnexpectedError
│
└ class Foo
  ~~~~~~~~~

severity:

all_error default strict lenient silent
error hint information hint nil

Ruby::UnexpectedJump

説明: 予期しないジャンプが発生した場合に発生します。

違反例:

break
test.rb:1:0: [error] Cannot jump from here
│ Diagnostic ID: Ruby::UnexpectedJump
│
└ break
  ~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::UnexpectedJumpValue

説明: ジャンプの値を渡しても値が無視される場合に発生します。

違反例:

while true
  next 3
end
test.rb:2:2: [error] The value given to next will be ignored
│ Diagnostic ID: Ruby::UnexpectedJumpValue
│
└   next 3
    ~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::UnexpectedKeywordArgument

説明: 予期しないキーワード引数が渡された場合に発生します。

違反例:

class Foo
  # @rbs (x: Integer) -> void
  def foo(x:)
  end
end

Foo.new.foo(x: 1, y: 2)
test.rb:7:18: [error] Unexpected keyword argument
│ Diagnostic ID: Ruby::UnexpectedKeywordArgument
│
└ Foo.new.foo(x: 1, y: 2)
                    ~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::UnexpectedPositionalArgument

説明: 予期しない位置引数が渡された場合に発生します。

違反例:

class Foo
  # @rbs (Integer) -> void
  def foo(x)
  end
end

Foo.new.foo(1, 2)
test.rb:7:15: [error] Unexpected positional argument
│ Diagnostic ID: Ruby::UnexpectedPositionalArgument
│
└ Foo.new.foo(1, 2)
                 ~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::UnexpectedSuper

説明: super を呼び出した際に親クラスに同名のメソッドが定義されていないなど、予期しない場面で super が使用された場合に発生します。

違反例:

class Foo
  def foo
    super
  end
end
test.rb:3:4: [error] No superclass method `foo` defined
│ Diagnostic ID: Ruby::UnexpectedSuper
│
└     super
      ~~~~~

severity:

all_error default strict lenient silent
error information error nil nil

Ruby::UnexpectedTypeArgument

説明: 予期しない型引数が渡された場合に発生します。

違反例:

class Foo
  # @rbs [T] (T) -> T
  def foo(x)
    x
  end
end

Foo.new.foo(1) #$ Integer, Integer
test.rb:8:27: [error] Unexpected type arg is given to method type `[T] (T) -> T`
│ Diagnostic ID: Ruby::UnexpectedTypeArgument
│
└ Foo.new.foo(1) #$ Integer, Integer
                             ~~~~~~~

severity:

all_error default strict lenient silent
error hint error nil nil

Ruby::UnexpectedYield

説明: yield が予期しない場面で使用された場合に発生します。

違反例:

class Foo
  # @rbs () -> void
  def foo
    yield
  end
end
test.rb:4:4: [error] No block given for `yield`
│ Diagnostic ID: Ruby::UnexpectedYield
│
└     yield
      ~~~~~

severity:

all_error default strict lenient silent
error warning error information nil

Ruby::UnknownConstant

説明: 未知の定数が参照された場合に発生します。

違反例:

FOO
test.rb:1:0: [error] Cannot find the declaration of constant: `FOO`
│ Diagnostic ID: Ruby::UnknownConstant
│
└ FOO
  ~~~

severity:

all_error default strict lenient silent
error warning error hint nil

Ruby::UnknownGlobalVariable

説明: 未知のグローバル変数が参照された場合に発生します。

違反例:

$foo
test.rb:1:0: [error] Cannot find the declaration of global variable: `$foo`
│ Diagnostic ID: Ruby::UnknownGlobalVariable
│
└ $foo
  ~~~~

severity:

all_error default strict lenient silent
error warning error hint nil

Ruby::UnknownInstanceVariable

説明: 未知のインスタンス変数が参照された場合に発生します。

違反例:

class Foo
  def foo
    @foo = 'foo'
  end
end
test.rb:3:4: [error] Cannot find the declaration of instance variable: `@foo`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @foo = 'foo'
      ~~~~

severity:

all_error default strict lenient silent
error information error hint nil

Ruby::UnreachableBranch

説明: if ,unless による到達不可能な分岐が存在する場合に発生します。

違反例:

if false
  1
end
test.rb:1:0: [error] The branch is unreachable
│ Diagnostic ID: Ruby::UnreachableBranch
│
└ if false
  ~~

severity:

all_error default strict lenient silent
error hint information hint nil

Ruby::UnreachableValueBranch

説明: case when による到達不可能な分岐が存在し、分岐の型が bot でなかった場合に発生します。

違反例:

x = 1
case x
when Integer
  "one"
when String
  "two"
end
test.rb:5:0: [error] The branch may evaluate to a value of `::String` but unreachable
│ Diagnostic ID: Ruby::UnreachableValueBranch
│
└ when String
  ~~~~

severity:

all_error default strict lenient silent
error hint warning hint nil

Ruby::UnresolvedOverloading

説明: オーバーロードが行われているメソッドに対して型が解決できない場合に発生します。

違反例:

3 + "foo"
test.rb:1:0: [error] Cannot find compatible overloading of method `+` of type `::Integer`
│ Method types:
│   def +: (::Integer) -> ::Integer
│        | (::Float) -> ::Float
│        | (::Rational) -> ::Rational
│        | (::Complex) -> ::Complex
│
│ Diagnostic ID: Ruby::UnresolvedOverloading
│
└ 3 + "foo"
  ~~~~~~~~~

severity:

all_error default strict lenient silent
error error error information nil

Ruby::UnsatisfiableConstraint

説明: RBSと型注釈の辻褄が合わないなど、どうやっても型制約が満たされない場合に発生します。

違反例:

class Foo
  # @rbs [A, B] (A) { (A) -> void } -> B
  def foo(x)
  end
end

test = Foo.new

test.foo(1) do |x|
  # @type var x: String
end
test.rb:9:0: [error] Unsatisfiable constraint `::Integer <: A(1) <: ::String` is generated through (A(1)) { (A(1)) -> void } -> B(2)
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::UnsatisfiableConstraint
│
└ test.foo(1) do |x|
  ~~~~~~~~~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint error hint nil

Ruby::UnsupportedSyntax

説明: Steep としてサポートされていない構文が使用された場合に発生します。

違反例:

(_ = []).[]=(*(_ = nil))
test.rb:1:13: [error] Unsupported splat node occurrence
│ Diagnostic ID: Ruby::UnsupportedSyntax
│
└ (_ = []).[]=(*(_ = nil))
               ~~~~~~~~~~

severity:

all_error default strict lenient silent
error hint information hint nil

 


狙ったエラーを引き起こすというのは今年の RubyKaigi であった Ruby "enbugging" Quiz に近い感覚でした。難しい。

基本的には Steep リポジトリにあるテストケースを見ながら埋めていったんですが、中にはテストケースがないものもあったので soutaro さんに直接質問をしながら進めていきました。

また、副産物として Steep で使われなくなったが定義として残っているルールを発見し、削除する patch を作れたのも個人的には良かったです。

github.com

Aurora MySQLのアップグレード後ロールバック方法を検討してみた

エンジニアリング本部 プラットフォームエンジニアリング1G 橋本です。我々のグループでは業務の柱の一つとして、クラウドインフラの構築・運用を行っています。その中でAmazon Aurora MySQL(以下、AuroraもしくはAurora MySQL)のアップグレードがビジネスインパクトが大きい作業となりました。本記事はAurora MySQLアップグレード方法の検討について記述した投稿になります。

この記事のまとめ

  • 比較的大きなデータで且つ更新量の多いAuroraクラスターのアップグレードで且つダウンタイムが少ないロールバック方式を検討していました
  • ダウンタイム最小化の部分で大きな課題感があったが、Auroraの機能追加により大きく緩和できることが分かりました
  • この記事では、ダウンタイム最小化を軸にした場合のロールバックに関する課題感と解決方法を追加された当該機能に触れながら紹介します

前提情報や課題感について

Blue/Green Deploymentsによるアップグレードとは

Auroraの機能により既存クラスタをベースに無停止で新規クラスターをBlue/Green Deploymentsという機能を用いて作成することができます。なお、Green側クラスターにはEngineVersion(8.0.mysql_aurora.3.02.0, 5.7.mysql_aurora.2.11.2 etc.)や、パラメータグループを既存クラスターと異なるものを適用できるので、アップグレードや新規設定の適用をダウンタイム少なく行うことができます。

切替自体はワンクリックで可能でSwitchOverという命令を与えると、1分程度のダウンタイムは発生しますが特に難しいことはなくGreenクラスターに切り替えることができます。この際にアプリケーションサーバー等が参照しているエンドポイントも自動で切り替わります。

B/Gデプロイメントのイメージ

もしもの場合はロールバックしたい

この記事のテーマはGreenクラスターに切り替えたあとにアプリケーションサーバー等で不具合があった場合に、元のクラスター(Blueクラスターそのもの or 同等の構成のもの)にロールバックできるかどうか、その手法についてになります。

ロールバックの可能性はそれほど高くないと考えています。当然事前に動作検証はテスト環境で行い、アップグレードに臨みます。しかし、何らかの不具合が本番環境でのみ発生する可能性はゼロではない為、万が一の備えであってもロールバックの手法はコンティンジェンシープランとして持っておく必要があります。弊社でもサービスの中核にAurora MySQLを利用しているため、事業継続性の観点でも重要なものとなります。

ロールバックから戻せるか?

検討したロールバック手法

当初は次の表の3つの方式を検討しました。なおダウンタイムはできれば数分、長くても30分程度のダウンタイムを許容(◯)として考えています。

この比較は更新頻度や量が多く、ダウンタイムを極小化したいユースケースでAurora MySQLを使用している前提としています。たとえば、分析用DBであったり、小規模のDBであったりすると許容できるポイントが変わると思われます。

方式名 ダウンタイム 互換性 容易さ 特徴
AWS Database Migration Service(DMS) データ互換性の担保が難しい
GreenのバックアップデータをBlueへリストア ダウンタイムはデータ量に比例
GreenからBlueへの逆方向レプリケーション(没案) ロールバック時のダウンタイムに加えて、SwitchOver前にダウンタイムが必要

DMS方式

AWS DMSを利用してGreenクラスタの更新情報をロールバック用クラスターに同期する方式です。当初この方式での移行を検討していました。流れは以下の通りになります。

  • ★ 事前準備
    • 予めロールバック用クラスターを既存クラスターと同じ定義で作成しておく
    • DMSでGreenからロールバック用クラスターに変更データキャプチャ(CDC)により適宜データ同期が行われる構成にしておく
  • ★ SwitchOver!
  • ★ 問題発生!ロールバック開始 - ダウンタイム
    • サービス・メンテナンス状態にする(=DBの更新が行われないようにする)
    • DMS定義を削除する
  • ★ ロールバック終了 - サービス再開
    • アプリケーション・サーバーの接続先をロールバック用クラスターに変更してサービスを再開する

この方式はDMSの設定をしてしまえば複雑な操作・設定を必要とせず、簡単にロールバックができるところが特徴になります。この点が最大の魅力であり、当初の採用理由だったのですが、テストを行っているとデータ互換性の担保が難しいことに気づきました。

特に、DMSのユーザーガイドに記載されている制限やデータ型の変換が大きな問題となりました。

例えば、上記ガイドに制約として 列の AUTO_INCREMENT 属性は、ターゲットデータベース列に移行されません。 と記載されています。AUTO_INCREMENTのような属性はMySQL独自の自動増分の機能であり、ターゲットデータベースがOracleやPostgreSQLなど異なる場合にも汎用的に移行可能なように引き継がれない仕様となっているようです。

同じく、例えばJSON型がCLOB型に型変換をして同期するデフォルトマッピングがあり、ソースとターゲットデータベースでデータ型が変わってしまう点も、MySQL to MySQLでの単純なデータコピーに使いたい用途としては考慮事項が多くデータ互換性という観点では☓(バツ)を付けざるを得ないと判断しました。

リストア方式

次に検討したのはMySQLのバックアップ・リストア機能を用いてロールバックをする方式です。互換性の維持を主眼にすると以下の方式は確実な方式となります。

  • ★ 事前準備
    • 予めロールバック用クラスターを既存クラスターと同じ定義で作成しておく
  • ★ SwitchOver!
  • ★ 問題発生!ロールバック開始 - ダウンタイム
    • サービス・メンテナンス状態にする(=DBの更新が行われないようにする)
    • GreenクラスタのDBバックアップを取得する
    • ロールバック用クラスターに先のバックアップデータでリストアを行う
  • ★ ロールバック終了 - サービス再開
    • アプリケーション・サーバーの接続先をロールバック用クラスターに変更してサービスを再開する

DBバックアップ取得やリストアはmysqldumpなどのバックアップツールを用います。マネージド・サービスを普段利用しているとCLIベースでのバックアップ・リストアには一定の習熟が必要となるため、”容易さ”は△としています。

この方式の最大の問題点はバックアップ・リストアに要する時間でした。1TB弱(数百GB)オーダーのデータを対象として事前にテストしたところ、mysqldumpでは数十時間かかることが分かり全く現実的ではありませんでした。

mysqlshを用いて並列度を上げることで高速化できますが、それでも4時間程度の時間を要することが分かりました。Aurora MySQLのデータ書き込みがボトルネックとなっておりインスタンスサイズを大きくするなどして検証しましたが、これ以上の高速化は見込めず採用が難しいと判断しました。

なお、データ量が100GB以下程度と比較的小さければ数分〜30分程度のダウンタイムを許容する限りは、この方式が確実ではないかと考えています。

逆レプリ方式(没案)

ここまで来て、最後の手段ではありますがGreenクラスターからのデータ同期にMySQLのレプリケーション機能(Primary/Secondary方式)を用いることができれば良いのではないかということに思い当たります。Auroraでレプリケーション設定が可能か分からなかったのですが、Cyber Agentさまの記事 にズバリ書いていたため参考にさせていただきました。流れは以下のようになります。

  • ★ 事前準備
    • Blueクラスターを利用するため、ロールバック用クラスターは”作成しない”
  • ★ 静止断面取得作業 - ダウンタイム
    • サービス・メンテナンス状態にする(=DBの更新が行われないようにする)
    • ★ SwitchOver!
    • GreenクラスターをPrimary、BlueクラスターをSecondaryとしたレプリケーションを設定して同期させる
    • サービス・メンテナンスを解除する
  • ★ サービス再開
  • ★ 問題発生!ロールバック開始 - ダウンタイム
    • サービス・メンテナンス状態にする(=DBの更新が行われないようにする)
    • Blueクラスターを昇格させる(レプリケーション設定を解除)
  • ロールバック終了 - サービス再開
    • アプリケーション・サーバーの接続先をBlueクラスターに変更してサービスを再開する

この方式は標準的なMySQLレプリケーションを用いるため、データ互換性に対する懸念は少ないことが期待できます。しかしながら、SwitchOverを行う前にサービスダウンを発生させる必要があることが問題となりました。サービスダウンが必要な理由は所謂、静止断面を作るためになります。

# ※前提: サービス停止(更新停止)をした状態でSwichOverを行う

# Greenクラスターで現在のポジション(静止断面)を取得する
[Green] > show master status;
+----------------------------+----------+--------------+------------------+-------------------+
| File                       | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------------------+----------+--------------+------------------+-------------------+
| mysql-bin-changelog.000133 |      941 |              |                  |                   |
+----------------------------+----------+--------------+------------------+-------------------+

# BlueクラスターでGreenクラスターをPrimaryとしたレプリケーション定義を行う(CHANGE MASTER TO相当)
CALL mysql.rds_set_external_master(
  'GreenクラスターのWriterEndpoint',
  3306,
  'replicationユーザ名',
  'replicationパスワード',
  'mysql-bin-changelog.000133',
  941,
  0);

Green, Blueクラスターは、それぞれ異なるbinlogファイル・ポジションを保持しているため、BlueクラスターにとってGreenクラスターのどのポジションから読み出しにいくべきか?、レプリケーション開始時点のポジションが必要になります。

上記のコマンド例のように、show master status; コマンドによりポジション取得を行うためには書き込みを一旦停止する必要があり、これがSwitchOver前にサービス・メンテナンスを行う理由となります。

結果的には、本案は没案となりました。そもそもロールバックは万が一の備えであるため、その備えのための事前作業でサービス停止が100%発生することは許容できなかったからです。

困った。どうしよう?

ここまで長々と各方式の説明をしてきましたが、これ!という方式が見つかりません。大きめのデータを持つAuroraをダウンタイム少なくロールバックする手法を確立すべく検討をしていましたが、行き詰まってしまいました。

タイムリーな機能を教えてもらえた?

AWSの方にも相談をしながら検討をしていたのですが、24’ 8/6に公開されたこのブログポストを紹介していただきました。先ほど没案となった逆レプリ方式を改善できる一手になる!という手応えを得て検証を開始しました。

SwitchOver時に静止断面を教えてくれる

ポイントはSwitchOverでGreenクラスターに切り替わった瞬間のポジションを静止断面として教えてくれることにあります。次の図のようにSwitchOverしたときにクラスターEventとしてバイナリファイルとポジションがメッセージに出力されます。

一見すると地味な機能ですが、先に没案の課題として述べたとおり通常はサービス停止をしなければ取得できない情報が無停止で取得できるので、とても強力な機能です。

SwitchOver時のイベントメッセージ

あとは次の図のように、もともとあったBlueクラスターをSecondaryとしたレプリケーション設定を行います。なお、ブログ執筆時点では記事中の設定に誤りがあり、BlueクラスターのWriterEndpointを指定する記述になっています。正しくはここに記載したように”GreenのEndpoint”を指定する必要があるのでご注意ください。

詳細の手順は先のAWSブログポストにすべて記載されていますので、ここではこれ以上の詳細は割愛します。気になった方は是非ご参照ください。

逆レプリケーションのイメージ

逆レプリ(新案)はどうなるのか?

逆レプリ(新案)は以下のような流れになります。SwitchOverを行ったあとに逆レプリ設定をして同期を行っておく点がポイントになります。

  • ★ 事前準備
    • Blueクラスターを利用するため、ロールバック用クラスターは”作成しない”
  • ★ SwitchOver!
    • クラスターEventに出力された静止断面のポジションを確認する
    • GreenクラスターをPrimary、BlueクラスターをSecondaryとしたレプリケーションを設定して同期させる
  • ★ 問題発生!ロールバック開始 - ダウンタイム
    • サービス・メンテナンス状態にする(=DBの更新が行われないようにする)
    • Blueクラスターを昇格させる(レプリケーション設定を解除)
  • ★ ロールバック終了 - サービス再開
    • アプリケーション・サーバーの接続先をBlueクラスターに変更してサービスを再開する

いままでの方式案との比較

上記を踏まえて、他案との比較を行いました。あくまで弊社環境においてダウンタイムは30分以内という条件で選んだ場合、この逆レプリ(新案)方式はベストな選択となりました。

1点、容易さが△としているのは、Aurora MySQLの運用において普段はあまり行わないCLIベースでの操作を行うことであったり、binlogによるレプリケーション同期の機序について理解する必要がある点に起因しています。

組織的な対応強度を備えるために一定の学習コストがかかりますが、一つの技術習得としてじっくりと時間をかけて取り組んでいこうと考えています。

方式名 ダウンタイム 互換性 容易さ 特徴
AWS Database Migration Service(DMS) データ互換性の担保が難しい
GreenのバックアップデータをBlueへリストア ダウンタイムはデータ量に比例
GreenからBlueへの逆方向レプリケーション(没案) ロールバック時のダウンタイムに加えて、SwitchOver前にダウンタイムが必要
GreenからBlueへの逆方向レプリケーション(新案) 没案の事前ダウンタイムが克服された!

まとめ

この投稿では弊社でAurora MySQLアップグレードを行う際に、データ量が比較的多いデータベースで、ダウンタイムを極小化してロールバックを行う方式について検討した軌跡についてシェアをしました。

同じような課題に突き当たった方もいるのではないかなと思います。この記事が課題解決の参考になれば幸いです。

【イベントレポート】iOSDC Japan 2024

  こんにちは、iOSエンジニアの前田(@naoya_maeda) 、Androidエンジニアの伊藤(@tick_taku77)です。

2024年8月22-24日に早稲田大学理工学部 西早稲田キャンパスで開催されたiOSDC Japan 2024に、タイミーもゴールドスポンサーとして協賛させていただきました。 イベントは以下のように、3日間連続で行われました。

8月22日(木):day0(前夜祭)

8月23日(金):day1(本編1日目)

8月24日(土):day2(本編2日目)

私達もイベントに参加したので、メンバーそれぞれが気になったセッションや感想をご紹介します。

naoya編

前夜祭で、「動かして学ぶDockKit入門」というタイトルで発表しました。 www.docswell.com

去年行われたWWDC23で発表されたDockKitフレームワークでできることを、体系的に紹介するトークになります。DockKit対応デバイス自体はよくメディアに取り上げられていて認知度が高いですが、DockKitフレームワークはまだ歴史が浅いこともあり、DockKitフレームワークを使用して何ができるかということはあまり知られていません。

本トークでは、DockKitフレームワークでできることを開発者目線でデモを通して紹介しました。 技術記事ではさらに詳しく解説していますので、ご興味をお持ちいただけた方は是非ご覧いただけますと幸いです。 zenn.dev zenn.dev zenn.dev

ここからは僕が気になったセッションをご紹介します。

タイトル : GIS入門 - 地理情報をiOSで活用する

登壇者 : 堤 修一 さん

www.docswell.com

地図の仕組みを一から理解し、iOSアプリでさまざまな応用ができるようになることを目的としたトークです。iOSアプリエンジニアの方であれば、MapKitのAPIを使用してiPhoneの画面に地図を表示したことは、一度はあるのではないでしょうか。 一方で、地図が表示される仕組みについて深く考える機会はほとんどないと思います。本トークでは、地図の仕組みから始まり、実際にコードを見ながら地図の仕組みの解説を進めていきます。最初はベーシックな地図を表示する方法、最後はマップ上に人間とモンスターを配置した、某ゲームのようなデモを披露してくださいました。

タイトル : iPhone × NFC で実現するスマートキーの開発方法

登壇者 : 岡 優志 さん

www.docswell.com NFCのハードウェア特性や規格といった基礎知識的な話に始まり、NFCでスマートキーを作成する方法をデモを通じて紹介するトークです。NFC周りは僕も触っていたことがあるのですが、曖昧な理解だった部分が多いことを実感したトークでした。 このトークを聞いた後、僕もNFCデバイスを使用して何か作ってみたいなと思いました。 基礎を丁寧にわかりやすく説明してくださる、聞き手のことをしっかり考えてお話しされるokaさんらしいトークだなと感じました! iOSDC 2023でご登壇された「作って学ぶBluetoothの基本攻略 〜スマートキーアプリを作ってみよう〜」も非常に面白いので是非ご覧ください!

www.youtube.com

tick_taku編

タイトル : App Clipの魔法: iOSデザイン開発の新時代 by log5

登壇者 : log5 さん

speakerdeck.com

App Clipの概要やユースケースについて熱量高く解説されていて、iOSDC 2024のトップバッターを飾るにふさわしい未来にワクワクできる素晴らしいセッションでした!

App Clipは名前だけ知っていましたが、Androidで言うInstant Appのようなものでしょうか。 想定している活用方法を聞いてとても感動しました。

自分自身(特に普段使わないのにクーポンなどのために)アプリをインストールすることに結構抵抗があるタイプですし、昔バイトでレジを打ちながらアプリを勧めてインストールのヘルプまで行うのはカロリーも高く、忙しい時間帯ですとレジ待ちの列が長くなりかなり大変でした。 そのステップが短縮されるならユーザーにとっても店舗の方にとってもかなり負担が減ると思われるのでとても効果が高そうだと感じました。

また開発においてもモバイルアプリの共有には非常に課題を感じていて、webと違いURLを共有するだけでは完結せず準備に手間がかかります。App Clipを利用し、スプリントレビューなどにおいてその場でエンジニア以外にロールプレイをしてもらうことでより当事者意識の高いフィードバックが得られるデモンストレーションができそうな予感がします。

ゆくゆくはモバイルアプリはインストールするものではなくなる未来が待っているかもしれませんね。

とはいえ、話されていたユースケースを実現するにはまだまだ課題がありそうなので今後の動向に注目したいと思います。

タイトル : Wallet API, Verifier APIで実現するIDカード on iPhoneの世界

登壇者 : 下森 周平 さん

speakerdeck.com

ここ最近マイナンバーカードをiPhone(Apple Wallet)に搭載できるようになると話題になっていますね。自分も持ち歩くものがまた減るので非常に楽しみにしています!

www.digital.go.jp

モバイルeID (モバイル端末に搭載される身分証明書)に関する国際標準規格 (ISO 23220)があり、マイナンバーカード搭載の話もこの一環だそうです。このセッションでは モバイルeID についての理解と、証明書情報を取り扱うAPIの特徴やユースケースを紹介されていました。

我々アプリケーションエンジニアが気になっているアプリからの取得もAPI (Verify with Wallet API)経由でサポートされているとのことなので、ユーザー登録に本人確認が必要なサービスはセキュリティ的にも利便性的にもぜひ対応した方がいいとのことでした。タイミーでも登録時に本人確認を行っているので導入できたら面白そうだなと思っています。

ただし、現時点ではAppleへの利用申請が必要なことに加えて金融など特定のカテゴリーに分類されるサービスでしか利用が許可されていないそうなので注意が必要です。

これからのデジタル社会に向けてキャッチアップが必要そうな内容が解説されていて非常に勉強になりました。

ちなみにスピーカーの方は日本在住でカナダの仕事をしているそうです。僕はカナダへのあこがれがあるので職の探し方などもとても興味があります。

参考

https://www.soumu.go.jp/main_content/000779585.pdf

https://www.jssec.org/column/20231127.html

最後に

この三日間を通して技術的な知見を深めたり、久しい友人に会って話をすることができ、すごく有意義な時間を過ごすことができました。この場を用意してくださったiOSDCスタッフの方々、参加者のみなさん本当にありがとうございました!

上記で紹介したセッション以外にも非常に興味深いセッションが多くありました。 記事にある内容や、その他の内容についても、もしタイミーのエンジニアと話したいという方がいらっしゃればぜひお気軽にお話ししましょう!

product-recruit.timee.co.jp

#DroidKaigi に向けて数字で振り返るタイミーのAndroid開発

こんにちは、タイミーDevRelの河又です。

タイミーはDroidKaigi 2024にゴールドスポンサーとして協賛しています。
当日はブースも出展しておりますので是非、お立ち寄りください。

今回はDroidKaigiを前に一度、タイミーのAndroid開発を数字で振り返ろうという企画です。 Androidエンジニアの中川をインタビューアーとしてAndroid領域のリードエンジニアである村田にタイミーのAndroid開発についてインタビューする形式でお届けします!

タイミーのAndroidアプリのクラッシュフリーレートについて

2022年


2024年現在


※グラフ上の7日間、30日間の数値は当該期間全体の数値ではなく、デイリーの移動平均の数値です

続きを読む