Timee Product Team Blog

タイミー開発者ブログ

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日間の数値は当該期間全体の数値ではなく、デイリーの移動平均の数値です

続きを読む

タイミーでOSTを開催しました

こんにちは! Agile Practice Teamでプロセス改善やアジャイルコーチとしてチームの支援を担当しています、吉野です。 2024年4月にタイミーに入社後、初めてオフラインにて社内のOST(オープンスペーステクノロジー)イベントを体験してきましたので、レポートします。

今回お話しする内容

  • どんなイベントを行ったのか?
  • タイミーでのOSTはどんな雰囲気で開催されたのか?

を、お話ししていきます!

どんなイベントを行ったのか?

今回開催されたイベントの概要

という形式でのOSTが開催されました。

参加者について

タイミーのプロダクト開発組織に関わっている方の中から参加希望者を募っての開催でした。先陣を切って動かれていたrazさん、 りっきーさんの一声で、20人近くの人が一気に集まりました!(しかも3日ぐらいで!すごい!!)

結構OSTとか慣れているのかな?と思いながら当日参加したところ、なんと今までOSTを経験したことのある参加者は1/3ほどでした。

OSTの様子

私はまだ社内のオフサイトイベントに参加していなかったので、応募時点ではみんなのモチベーションがどれぐらい高いのかわからずの参加でした。

未体験のイベントにオフサイトで参加することには、一定のハードルがあると思っていましたが、一気に参加者が集まり「みんなのイベント参加へのオープンさ」を感じることができました! イベント参加や勉強へ前向きな環境は、その場にいてめちゃくちゃテンションが上がります!

タイミーでのOSTはどんな雰囲気で開催されたのか?

どんなOSTだったのか?

枠としては、15分区切りの4枠にて開催されました。

お題がめっちゃ出てきた!

OSTの原則として、

💡 OSTの原則の一部

  • いつ始まろうと、始まったときが適切なときである
  • いつ終わろうと、終わったときが終わりのときである

があるので、1枠の長さはそこまで気にしなくても良さそうと思いつつ、15分という時間は自分が経験してきた中で一番短い時間だったので、どうなるのかな?忙しくならないかな?と少し心配でした。

ですが、そのような心配は杞憂に終わり、15分の中でみんなポイントを絞って議論したり、時間が足りなかったら自分たちでテーブルや場を用意して議論を継続したりしていました。

全体の時間としても1.5h(マーケットプレイスを含めず)という短時間での開催でした。 そんな中、各々が自主的に話したいことを話して時間を最大限に活用したOSTらしいOSTだったと思います。 (運営されていたお二人も、ひたすら運営に集中、というわけではなく話し合いにも参加して自身でも楽しむスタイルで立ち回られていました!)

どんな雰囲気だったのか?

タイミーでは、プロダクト開発に関わる多くの方がフルリモートでお仕事をしています。 そのため、今回オフサイトで集まった直後は「ワイワイ!ガヤガヤ!」というわけではなく、少し緊張している空気を感じました。

しかし、オフサイトイベントへ自主的に参加されていることもあり、いざOSTが始まると自己紹介や自身が向き合っているお仕事の紹介など、積極的にコミニュケーションを取りにいっていました。

最終的には、お互いの悩みに共感する声や、笑い声が多く飛び交い、一体感を感じられるイベントになっていたと思います。 「初めまして」とか、「社内イベントへの参加が初めてなんです」という声も多かった中、2hもしないうちに熱量の高い場になっており、人見知りな私も最後はすごく楽しませて頂きました!笑

今後のイベントにも期待をしていきたいプロダクト開発組織

私は社外のオフサイトで開催されるコミュニティイベントへの参加が好きで、よく参加しています。

社内でも、何かイベントが開催できないかな?もしくは開かれたら参加できないかな?と考えていたところ、今回のOSTイベントへ参加しました。 初めての参加でしたが、熱い議論をしたり、同じ組織の人との繋がりができたりと、とても有意義な時間を過ごすことができました。

普段からリモートでお仕事しているからこそ、オフラインで集まれた時はコミュニケーションを重視する時間に対しての熱量は高くなるのかなーと思いました!

今後も、社内のオフサイトイベントが立ち上がれば積極的に参加していきたいですし、自分でも何か開催してみたいと思います!


あわせて、主催であるりっきーさん、razさんのご感想です!

OSTにかける思い

スクラムマスターをやっているりっきーです。

弊社はフルリモートの環境のため、会議の集中力を阻害する要因(Slackの通知だったり、会議とは関係ないことを勧めたり)が多いと感じています。そこで、どのような設計であれば参加者が集中できるかを模索した結果、OSTに辿り着きました。

OSTが優れている点は「自分自身で興味あるテーマを公募する / 自分自身で興味あるテーマにサインアップする」に集約されています。誰かに呼ばれて会議に参加するのではなく、”自分自身”でアクションを起こさなければ会議がうまくいかないので、参加者としても集中して会議に参加できる環境になると考えています。

今回は最もやりやすい環境であることと、全社のイベントで出社する機会があったのでオフラインで開催しましたが、オンライン環境ではより効果が発揮できると思うので、今後は回数を増やしていければと思います。

初参加でもあり、初運営のOST、うまくいってよかった

どうもスクラムマスターやったりエンジニアやったりしてるrazです。今はエンジニアをしております。思いつきでやってみたいと言ったところ、2人や参加者の協力のもとOSTを開催できました。僕の記憶してる範囲では社内で実施するのは初めてだったと思います。

OST開催の動機

今回、全社のイベントがありオフサイトで集まる機会がありました。その会までの時間の使い方として提案させてもらいました。普段フルリモートだったり、違うチームだったりであまり会話する機会のない人も参加してくださったのはとても嬉しかったです。

なぜOSTなのか?の理由は3つあります。

  1. 実は自分が未経験で興味があった
  2. 毎週開催してるアジャイル相談会の様子から、普段話せてないことが色々ありそうだった
  3. 雑に大人数集めて開催してもなんとかなりそうだから

1と3はあまり説明することもないので省略しますが、2のアジャイル相談会について補足します。

アジャイル相談会とは、毎週水曜の夕方ごろにりっきーさんが主催してくれてるその名の通りアジャイルとかスクラムに関して気軽に相談していい会です。ただ、実際にはアジャイルとかスクラムの枠にとらわれず、組織論やマネジメントといった様々な内容で会話しています。その中でも、組織全体に関わることは、話す機会が少ないんだろうと感じていました。そこで、熱量のある人たちが集まって会話できるOSTをやれたらいいのではと思って開催しました。

開催してみた感想

一応運営ではあるので、全体の様子をみながらではありますが、極力会話に参加してテーマを盛り上げていきました。経験者が1/3程度いましたが、半数以上は未経験で何もわからない状態での参加でした。しかし、みんなの自主性の高さがあってか、初回セッションから盛り上がっていて安心しました。自分もその様子に安心できたので、2セッション目以降はより会話に集中できたと思います。途中会話に夢中になりすぎてタイムキープを忘れていたほどです笑。

みなさん初めての開催にもかかわらず適応能力が高いので、事前に出したテーマじゃないことに転換していて、会話を楽しんでいると感じました。個人的にはもう少し「どうしよ?」感に包まれるんかな?と心配してたのですが、杞憂でした笑

次回開催なるか?

今回、準備期間も会自体も短い中での開催でしたが、思ったよりも盛り上がってましたし、個人的には成功したんじゃないかと思ってます。いい成功体験になったと思うので、次回開催に向けて思っていること3つあげて、僕の感想を終わりにしようと思います。

  • フルリモートへの適応
  • 多種多様な役職や職種の人の参加
  • 組織を変革していくアクションを生み出せる会へ

フルリモートへの適応: タイミーのプロダクト開発組織は、フルリモートなのでリモートでも開催してみてもいいかなとは思いました。ただ、実際に運営してみてオフサイト環境だからこそ成り立っているものがありそうとも感じました。四半期に1度くらいのペースで、オンラインとオフラインを交互で開催できても面白そうです。

多種多様な役職や職種の人の参加: 急な呼びかけなのもあって参加できたメンバーは限られていました。組織に存在している様々な役職や職種の人が充足できてない状態でしたので、次回はより多種多様な人に参加してもらえるような会を開催できたらなと思います。

組織を変革していくアクションを生み出せる会へ: 会話メインで会は終わりました。しかし、もう少し時間を長くとれれば、会話も深まりますし、具体的なアクションを生み出したりして、組織やチームに変化をもたらす会にもしていけると思うので、そういう会を目指せればと思います!


以上レポートでした!

ハピネスドアもハッピー感想めっちゃ多かったです!

効果検証の事前設計と結果の管理について

こんにちは、タイミーのデータアナリティクス部でデータアナリストをしている夏目です。普段は主にタイミーのプロダクトに関する分析業務に従事しています。

本日はタイミーにおいて、効果検証設計を施策前に正しく行える仕組みづくりと効果検証設計・結果を一元的に管理できるデータベースについてご紹介します。

解決したかった課題

タイミーでは、プロダクト、マーケティング、営業組織などで様々な施策が行われています。しかしながら、それらの施策の結果を判断する効果検証には課題も多く存在しています。今回は以下の2つの課題にフォーカスしてブログを書きます。

  1. 効果検証設計が事前になされていない施策があった
  2. 効果検証設計や検証結果がバラバラに保管され、会社として知見が溜まっていなかった

まず1つ目の「効果検証設計が事前になされていない施策があった」に関してです。タイミーではアナリストの数も限られており、事前に全ての施策に目を通すことは難しいです。施策によっては事前に効果検証設計がされておらず、必要なログが取れていなかったり、検証に必要なサンプルサイズが担保されていなかったりと、正確な効果検証ができないケースが存在しました。

次に2つ目の「効果検証設計や検証結果がバラバラに保管され、全体として知見が溜まっていなかった」に関してです。タイミーでは様々なチームが施策を行っています。基本的に効果検証の結果はチームごとに管理されており、別のチームの人がその結果を探すことが難しいケースもありました。

取ったアプローチ

以上の2つの課題を解決するために、行ったことは主に以下の3つです。それぞれをこの項では説明していきます。

  1. 各チームが効果検証の設計と結果を記入できるNotion上のデータベースを作成
  2. 効果検証設計と結果を記入するテンプレートを作成
  3. 他アナリストや他チームへの説明の実施
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テスト実践ガイド 真のデータドリブンへ至る信用できる実験とは