Timee Product Team Blog

タイミー開発者ブログ

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テスト実践ガイド 真のデータドリブンへ至る信用できる実験とは

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

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

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

tech.timee.co.jp

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

移行の流れ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      f.write(content)
    end
  end

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

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

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

Before

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

After

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

github.com

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

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

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

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

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

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

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

4. 後片付け

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

sord gem の削除

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

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

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

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

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

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

github.com

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

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

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

rbs subtract をやめる

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

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

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

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

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

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

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

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

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

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

Before

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

After

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

今後の展望

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

型検査を通す

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

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

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

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

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

まとめ

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

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

product-recruit.timee.co.jp