Timee Product Team Blog

タイミー開発者ブログ

dbt 1.8のUnit Tests 実施とその知見(時間ロックとSQLの分割について)

株式会社タイミーのkatsumiです!

dbtのバージョン1.8以上を利用することで、unit testsが利用可能になります。今までもSingular テスト(単一テスト)やGeneric テスト(汎用テスト)は可能でしたが、テストデータを利用した単体テストも行うことができます。

導入準備

dbt-coreの場合

dbt v1.8 以上を利用してください。

dbt-cloudの場合

2024/06/12時点では dbt「Keep on latest version」を選択することで利用できます。

弊社ではunit-test用の環境のみlatest versionを利用しています。

Unit Testの基本

# run data and unit tests
dbt test

# run only data tests
dbt test --select test_type:data

# run only unit tests
dbt test --select test_type:unit

# run tests for one_specific_model
dbt test --select "one_specific_model"

# run data tests limited to one_specific_model
dbt test --select "one_specific_model,test_type:data"

# run unit tests limited to one_specific_model
dbt test --select "one_specific_model,test_type:unit"

unit-testに関係する新しいコマンドが追加されました。このコマンドは、以前のデータテストで使用していたselect機能と同様に、特定のテストケースを選択して実行することができます。

ymlによるテストレコードの書き方

  - name: test_name
    description: "テストの説明"
    model: my_model
    given:
      - input: ref('users')
        rows:
          - {id: 1, user_email: example@example.com}
          
    expect:
      rows:
        - {id: 1, domain: example.com}

name: test_name

これはテストの名前です。この名前はテストケースを識別するために使用します。

description: “テストの説明”

これはテストケースの説明です。この説明には、テストが何を意図しているのか、テストの目的や背景について記載します。

model: my_model

これはテスト対象となるモデルの名前です。ここでは「my_model」がテスト対象のモデルとして指定されています。

given

データの内容です。ここでは「id: 1」で「user_email」「example@example.com」のユーザーを指定しています。このデータがテストの入力として使用されます。

expect

これは期待される結果を指定します。テストが成功するためには、モデルが「id: 1」のユーザーに対して「domain」が「example.com」として返される必要があります。期待される結果と実際の結果が一致するかどうかを検証します。

ファイルによるテストレコードの書き方

unit_tests:
  - name: test_my_model
    model: my_model
    given:
        - input: ref('users')
            format: csv
            fixture: users

プロジェクトのtests/fixturesディレクトリにあるCSVファイル名を指定することで利用できます。test-pathsオプションを使用することで、ディレクトリ構成を柔軟に指定することもできます。

未定義のカラムの挙動

未入力のカラムに関しては、safe_cast(null as INT64)のように型が定義されたnullのデータで補完されます。リレーションが必要なものや、ロジックに影響を与えるカラムの記入が必要になります。

実施における知見

大規模なクエリは”ephemeral”で細かいテスト行う。

  • with句が複数ありテストケースが複雑で見通しが悪くなるケースがあります。弊社ではSQLのテスト単位のロジックを”ephemeral”で分けて個別のmodelにてテストを書く実装を試しています。
  • 通常のモデルと同じ書き方でテストを実施することが可能です。
WITH 処理1_cte AS (
    SELECT * FROM {{ ref('処理1のephemeral') }}
)

, 処理2_cte AS (
    SELECT * FROM {{ ref('処理2のephemeral') }}
)

, 処理3_cte AS (
    SELECT * FROM {{ ref('処理3のephemeral') }}
)

時系列系の時間の停止をマクロで行う。

  • テストしたいケースにはcurrent_datetimeなど現在の時刻を利用するものがあります。その場合、テストを書く際に時間を固定する必要があります。
  • dbtのユニットテストでは、YAMLファイル上でdbtのマクロを置き換える機能があります。この機能を利用して、時間を固定する実装を行っています。
  - name: test_case
    model: my_model
    overrides:
      macros:
        current_datetime_jst: "date('2024-01-01')"
{{ config(materialized='ephemeral') }}

SELECT 
-- ここにロジックを書く
FROM {{ ref('users') }} AS users
WHERE DATETIME_TRUNC(created_at, MONTH) = DATE_TRUNC({{ current_datetime_jst() }}, MONTH)

Testに関するSQLの確認ができる。

  • 実際の仕組みとしてはテスト用のSQLが生成され、フィクスチャ(テストデータ)も含めたSQLが実行されます。debugコマンドやコンパイルされたSQLを確認することで、テストの挙動をチェックできます。
  • テストケースの問題が起きた時にSQLにて要因分析を行いました。

まとめ

重要指標の計算や複雑な時系列処理、プロダクトのロジックを再現する箇所では、テストケースを用意していこうと考えています。またテストケースを先に定義したのちにクエリを書くことも簡単にできるようになったように感じます。信頼性の高いモデルにするために、重要な機能になっていきそうです。

以上、unit-testsを試した時に得られた知見のまとめでした。この情報が役立てば幸いです!

We’re Hired

タイミーでは、一緒に働くメンバーを募集しています!!

参考資料

  • Unit tests | dbt Developer Hub

https://docs.getdbt.com/docs/build/unit-tests

  • Unit Testing

https://github.com/dbt-labs/dbt-core/discussions/8275

dbt snapshotの内部クエリを理解して正確に挙動を把握しよう!

はじめに

こんにちは☀️okodooooonです

最近、社内のdbt snapshotモデルでパフォーマンスの問題が発生し、その解決に苦労しました。dbt snapshotの内部処理が公式ドキュメントなどで提示されておらず、詳細なクエリを理解していなかったためです。

そこで、今回、dbt snapshotの内部クエリについて解説してみることにしました。ただし、今回の解説内容は、ドキュメントで説明されている通りの挙動がどのようにSQLで表現されているのか確認したもので、新しい発見やTipsみたいなものは特にないです!

内部処理をしっかり理解することで、dbtによって抽象化された処理をより効果的に活用できることもあるかな〜と思っておりますので、どなたかの参考になれば幸いです!

(今回解説するクエリは、dbt-bigqueryで生成されるクエリです)

dbt snapshotとは(ざっくり)

SCD Type2 Dimensionという思想に従って、過去時点の状態の遷移を蓄積できるような仕組みです。

ソースシステム側ではステータス変更が行われると、そのナチュラルキーのレコードが上書き処理されますが、その上書き処理前後のレコードをそれぞれ有効期限付きで保存します

公式Doc: https://docs.getdbt.com/docs/build/snapshots

今回の例

以下のようなモデルを仮定して、snapshotのクエリを見ていきたいと思います。

モデルファイル上の定義はこんな感じです。

{% snapshot snapshotted_sample_table %}

    {{
        config(
          target_schema='sample_dataset',
          strategy='timestamp',
          unique_key='id',
          updated_at='updated_at',
          invalidate_hard_deletes=True,
        )
    }}
    select * from {{ source('sample_dataset', 'sample_data') }}

{% endsnapshot %}

ソーステーブル側で一意であるカラムをunique_key, レコード更新日時を記録するカラムをupdated_atに指定しています。

左のテーブルがsnapshot化されることで、右のように有効期限(dbt_valid_from, dbt_valid_to)とsnapshot後のレコードに対するユニークキー(dbt_scd_id)が付与されます

全体の流れ

dbt snapshotはBigQueryにおいて2つのクエリを実行しています。

  • ソーステーブルと宛先テーブルからデータを抽出して、snapshot先にmergeするためのtmpテーブルを、update,delete,insertそれぞれの処理ごとに分割して作成する処理
  • tmpテーブルでラベリングされた処理ごとにMERGEクエリを実行する処理

それぞれ実行されるクエリの詳細は以下のようになります。

tmpテーブル作成クエリ全文 (クリックで展開)

```sql
    create or replace table `sample_project`.`sample_dataset`.`sample_table__dbt_tmp`

    OPTIONS(
      description="""""",    
      expiration_timestamp=TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 12 hour)
    )
    as (
      with snapshot_query as (
    SELECT
        *
    FROM
        `sample_project`.`sample_dataset`.`sample_table`
    ),

    snapshotted_data as (
        select *,
            id as dbt_unique_key
        from `sample_project`.`sample_dataset`.`snapshotted_sample_table`
        where dbt_valid_to is null
    ),

    insertions_source_data as (
        select
            *,
            id as dbt_unique_key,
            updated_at as dbt_updated_at,
            updated_at as dbt_valid_from,
            nullif(updated_at, updated_at) as dbt_valid_to,
            to_hex(md5(concat(coalesce(cast(id as string), ''), '|',coalesce(cast(updated_at as string), '')))) as dbt_scd_id
        from snapshot_query
    ),

    updates_source_data as (
        select
            *,
            id as dbt_unique_key,
            updated_at as dbt_updated_at,
            updated_at as dbt_valid_from,
            updated_at as dbt_valid_to
        from snapshot_query
    ),

    deletes_source_data as (
        select
            *,
            id as dbt_unique_key
        from snapshot_query
    ),


    insertions as (
        select
            'insert' as dbt_change_type,
            source_data.*
        from insertions_source_data as source_data
        left outer join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
        where snapshotted_data.dbt_unique_key is null
           or (
                snapshotted_data.dbt_unique_key is not null
            and (
                (snapshotted_data.dbt_valid_from < source_data.updated_at)
            )
        )
    ),

    updates as (
        select
            'update' as dbt_change_type,
            source_data.*,
            snapshotted_data.dbt_scd_id
        from updates_source_data as source_data
        join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
        where (
            (snapshotted_data.dbt_valid_from < source_data.updated_at)
        )
    ),

    deletes as (
        select
            'delete' as dbt_change_type,
            source_data.*,
    current_timestamp()
 as dbt_valid_from,       
    current_timestamp()
 as dbt_updated_at,
    current_timestamp()
 as dbt_valid_to,
            snapshotted_data.dbt_scd_id
        from snapshotted_data
        left join deletes_source_data as source_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
        where source_data.dbt_unique_key is null
    )

    select * from insertions
    union all
    select * from updates
    union all
    select * from deletes
    );

```

merge実行クエリ全文 (クリックで展開)

```sql
merge into `sample-project`.`sample_dataset`.`sample_table` as DBT_INTERNAL_DEST
using `sample-project`.`sample_dataset`.`sample_table__dbt_tmp` as DBT_INTERNAL_SOURCE
on DBT_INTERNAL_SOURCE.dbt_scd_id = DBT_INTERNAL_DEST.dbt_scd_id

when matched
  and DBT_INTERNAL_DEST.dbt_valid_to is null
  and DBT_INTERNAL_SOURCE.dbt_change_type in ('update', 'delete')
     then update
     set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to

when not matched
  and DBT_INTERNAL_SOURCE.dbt_change_type = 'insert'
     then insert (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`)
     values (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`)

```

上記クエリ内の各CTEで行われる処理をざっくりまとめると以下のような処理のフローになります。 処理の詳細を詳しく見ていきたいのですが、クエリ自体がちょっと長いので、insert, update, deleteそれぞれの処理に分割して詳細を見ていこうと思います!

snapshot内部処理の詳細

delete処理:宛先テーブルに存在するレコードがソーステーブルでdeleteされていた場合

tmpテーブル生成クエリのうち、ソース側でdeleteされたレコードをmerge用レコードに変換する処理の抜粋(クリックで展開)

-- 宛先履歴テーブルから履歴が確定していないレコードを抽出
snapshotted_data as (
    select *,
        -- unique_keyに指定したカラムをdbt_unique_keyとする
        id as dbt_unique_key
    from {{ 宛先テーブル }}
    where dbt_valid_to is null
),
deletes_source_data as (
    select
        *,
        -- unique_keyに指定したカラムをdbt_unique_keyとする
        id as dbt_unique_key
    from {{ ソーステーブル }}
)
deletes as (
    select
        'delete' as dbt_change_type,
        source_data.*,
                current_timestamp() as dbt_valid_from,       
                current_timestamp() as dbt_updated_at,
                current_timestamp() as dbt_valid_to,
        snapshotted_data.dbt_scd_id
    from snapshotted_data
    left join deletes_source_data as source_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
    where source_data.dbt_unique_key is null
)

tmpテーブル生成の処理の内訳は以下のようになります。

【処理の概要】
- 履歴が確定していない(valid_toに値が入っていない)レコード群を宛先テーブルから抽出
- 履歴が確定していないレコードのうち、ソーステーブルに存在しない(削除された)レコードに絞り込み
- dbt_valid_from, dbt_valid_toをクエリの実行時刻に設定
- dbt_change_typeを’delete’に設定

ソーステーブル側で削除されたmerge用レコードをmergeするクエリ(クリックで展開)

merge into {{宛先テーブル}}
using {{マージ用tmpテーブル}}
on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id

when matched
  and {{宛先テーブル}}.dbt_valid_to is null
  and {{マージ用tmpテーブル}}.dbt_change_type in ('delete')
     then update
     set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to

【処理の概要】
- dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合
- 宛先テーブルの履歴が未確定で、tmpテーブルのdbt_change_typeが’delete’の場合
    - 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(クエリ実行時刻)に上書き

以下図に表したような処理の流れによって、ソーステーブル側で削除されたレコードdbt_valid_toにsnapshot時の時刻が入るようになります。

update処理:宛先テーブルと比較してソーステーブルのレコードがupdateされていた場合

tmpテーブル生成クエリのうち、ソース側でupdateされたレコードをmerge用レコードに変換する処理の抜粋(クリックで展開)

-- 宛先履歴テーブルから履歴が確定していないレコードを抽出
snapshotted_data as (
    select *,
        -- unique_keyに指定したカラムをdbt_unique_keyとする
        id as dbt_unique_key
    from {{ 宛先テーブル }}
    where dbt_valid_to is null
),
updates_source_data as (
    select
        *,
        -- unique_keyに指定したカラムをdbt_unique_keyとする
        id as dbt_unique_key,
        updated_at as dbt_updated_at,
        updated_at as dbt_valid_from,
        updated_at as dbt_valid_to
    from {{ ソーステーブル }}
),
updates as (
    select
        'update' as dbt_change_type,
        source_data.*,
        snapshotted_data.dbt_scd_id
    from updates_source_data as source_data
    join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
    where (
        (snapshotted_data.dbt_valid_from < source_data.updated_at)
    )
)

【処理の概要】
- 履歴が確定していないレコード群を宛先テーブルから抽出
- ソーステーブルから抽出したレコードのdbt_valid_from, dbt_valid_toを現在時刻に設定
- 履歴が確定していないレコードのうち、宛先のdbt_valid_fromより後にupdated_atがソーステーブルに存在するレコードに絞る
- dbt_change_typeを’update’に設定

ソーステーブル側でupdateされたmerge用レコードをmergeするクエリ(クリックで展開)

merge into {{宛先テーブル}}
using {{マージ用tmpテーブル}}
on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id

when matched
  and {{宛先テーブル}}.dbt_valid_to is null
  and {{マージ用tmpテーブル}}.dbt_change_type in ('update')
     then update
     set dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to

【処理の概要】
- dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合
- 宛先テーブルの履歴が未確定で、tmpテーブルのdbt_change_typeが’update’の場合
    - 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(現在時刻)に上書き

以下図に表したような処理の流れによって、宛先テーブルの履歴が未確定のデータのうち、ソースで更新が走ったレコードのdbt_valid_toにスナップショット時の日時が入ります。

insert処理:宛先テーブルに無いレコードがソーステーブル側に新規で作成されていた場合

tmpテーブル生成クエリのうち、insert対象のレコードをmerge用レコードに変換する処理の抜粋(クリックで展開)

-- 宛先履歴テーブルから履歴が確定していないレコードを抽出
snapshotted_data as (
    select *,
        -- unique_keyに指定したカラムをdbt_unique_keyとする
        id as dbt_unique_key
    from {{ 宛先テーブル }}
    where dbt_valid_to is null
),

insertions_source_data as (
    select
        *,
        id as dbt_unique_key,
        updated_at as dbt_updated_at,
        updated_at as dbt_valid_from,
        nullif(updated_at, updated_at) as dbt_valid_to,
        to_hex(md5(concat(coalesce(cast(id as string), ''), '|',coalesce(cast(updated_at as string), '')))) as dbt_scd_id
    from {{ ソーステーブル }}
),

insertions as (
    select
        'insert' as dbt_change_type,
        source_data.*
    from insertions_source_data as source_data
    left outer join snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
    where snapshotted_data.dbt_unique_key is null
       or (
            snapshotted_data.dbt_unique_key is not null
        and (
            (snapshotted_data.dbt_valid_from < source_data.updated_at)
        )
    )
),

【処理の概要】
- ソーステーブルのunique_keyにしていたカラムとupdated_atに指定していたカラムを組み合わせてsurrogate_keyを生成
- ソーステーブルに対して履歴未確定の宛先テーブルをLEFT JOINして以下の条件に絞る
    - 宛先テーブルに指定したunique_keyが存在しないが、ソーステーブルには存在するレコード
    - 宛先テーブルに指定したunique_keyのレコードが存在して、ソーステーブル側のupdated_atが宛先テーブルのvalid_fromよりも後のレコード
- dbt_change_typeを’insert’に設定

ソーステーブル側でinsertされたmerge用レコードをmergeするクエリ(クリックで展開)

merge into {{宛先テーブル}}
using {{マージ用tmpテーブル}}
on {{宛先テーブル}}.dbt_scd_id = {{マージ用tmpテーブル}}.dbt_scd_id

when not matched
  and {{マージ用tmpテーブル}}.dbt_change_type = 'insert'
     then insert (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`)
     values (`id`, `foo`, `bar`, `created_at`, `updated_at`, `dbt_updated_at`, `dbt_valid_from`, `dbt_valid_to`, `dbt_scd_id`)

【処理の概要】
- dbt_scd_idをキーにして宛先テーブルとマージ用tmpテーブルを結合
- dbt_scd_idがマッチしなくて、dbt_change_type=’update’の場合にinsert処理を実行
- 宛先テーブルのdbt_valid_toをtmpテーブルのdbt_valid_to(現在時刻)に上書き

以下図に表したような処理の流れによって、指定したユニークキーが宛先に存在しないか、履歴が未確定のレコードのうちソース側で前回実行からupdateが走ったものがinsertされます。

check戦略の場合

上で紹介したのは snapshot_strategy=timestamp の場合のスナップショットの挙動であり、ソーステーブル側でupdated_at に指定したカラムが更新された場合に、すべてのプロパティの情報を履歴的に保持するものです。

dbtにはもう一つのスナップショット戦略として、check 戦略があります。

{% snapshot snapshotted_sample_table %}

    {{
        config(
          target_schema='sample_dataset',
          strategy='check',
          unique_key='id',
          invalidate_hard_deletes=True,
            check_cols=[
                            'foo',
                            'bar',
                'created_at',
                'updated_at',
            ],
        )
    }}
    select * from {{ source('sample_dataset', 'sample_data') }}

{% endsnapshot %}

このモデルでは全カラムを選択していますが、特定のカラムの変更のみを履歴的にトラッキングする仕組みです。

strategy=check においても、strategy=timestamp の時と同様に、snapshot処理はtmpテーブルを作成するクエリとmerge処理を実行するクエリに分割されます。

strategy=checkの場合のtmpテーブル作成クエリ(クリックで展開)
strategy=checkの場合のmerge実行クエリ(クリックで展開)

merge実行クエリはstrategy=timestampの時と変わらず、tmpテーブルの生成方法が異なっているので、詳しく見ていこうと思います

check戦略の詳細

insert 用データや update 用データを出力するCTEでは、以下のようなWHERE条件が使用されます。

((
    snapshotted_data.`foo` != source_data.`foo`
    or
    (
        ((snapshotted_data.`foo` is null) and not (source_data.`foo` is null))
        or
        ((not snapshotted_data.`foo` is null) and (source_data.`foo` is null))
    ) 
    or
    snapshotted_data.`bar` != source_data.`bar`
    or
    (
        ((snapshotted_data.`bar` is null) and not (source_data.`bar` is null))
        or
        ((not snapshotted_data.`bar` is null) and (source_data.`bar` is null))
    ) 
    or
    snapshotted_data.`created_at` != source_data.`created_at`
    or
    (
        ((snapshotted_data.`created_at` is null) and not (source_data.`created_at` is null))
        or
        ((not snapshotted_data.`created_at` is null) and (source_data.`created_at` is null))
    )
    or
    snapshotted_data.`updated_at` != source_data.`updated_at`
    or
    (
        ((snapshotted_data.`updated_at` is null) and not (source_data.`updated_at` is null))
        or
        ((not snapshotted_data.`updated_at` is null) and (source_data.`updated_at` is null))
    )
))

この条件により、insert と update の対象となるレコードの抽出条件は次のようになります。

  • insert用データの抽出条件
( 宛先にユニークキーが存在しない ) 
OR
  ( 
    (宛先にユニークキーが存在する)
    AND 
    (ユニークキー以外のcheck_colsに指定したカラムが、宛先とソースで何かしら変化が発生している)
  )
  • update用データの抽出条件
(宛先にユニークキーが存在する)
AND
(ユニークキー以外のcheck_colsに指定したカラムが、宛先とソースで何かしら変化が発生している)

checkで指定されたカラムの変更をどのように追跡しているかを確認できました。

まとめ

今回はdbt snapshotの内部処理をdelete, update, insertの処理に分解して説明してみました。

公式ドキュメントで説明されている通りの処理が生成されるSQLによって行われていることが確認できました。

dbt snapshotを使用している際に、期待した挙動が得られない場合や何かしらエラーが発生したときに、この情報が役立てば幸いです!

We’re Hired

タイミーでは、一緒に働くメンバーを募集しています!!

DevOpsDays Tokyo 2024に参加しました

タイミーの yorimitsu です。

世界中で開催されているDevOpsDaysカンファレンスは、ソフトウェア開発、ITインフラ運用を中心としたカンファレンスで、2024/4/16、17の2日間にわたって開催されました。

www.devopsdaystokyo.org

今回の参加はタイミーのプロダクトおよびエンジニア向けに用意している、技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度を利用しています。この制度は世界中で開催されるカンファレンスを対象にしています。

productpr.timee.co.jp

価値貢献を意識したチームの作り方

タイミーでも顧客に価値を届けることを大切に日々の開発運用を行っていますが、それを担うチームをより良くする取り組みのノウハウが、「Value-Driven DevOps Team〜価値貢献を大切にするチームがたどり着いたDevOpsベストプラクティス〜」のセッションで紹介されていました。

仮説検証を早く回すための開発環境の工夫や、チームのカルチャーの作り方はとても参考になる話でした。特にチームにメンバーが増えた際に「チームの状態を理解して各メンバーのやりたいことを共有して、チームの型に落とし込んでいくか」という部分は参考にしたい考えが多くありました。

開発チームに限らず、チームを組成すると何を目的としているのか、何の価値を提供するのかなど チームに属しているメンバーの認識をある程度合わせる必要があり、その際にインセプションデッキを利用したり、ワーキングアグリーメントを作成するのは、改めて有効だなと感じました。

How先行ではなくWhyを意識しなくてはいけない

君もテスト自動化の同志を増やすパターンで大勝利!」のセッションでは、SETチームを立ち上げる際に経験した問題について発表されていました。SETチームなので当然ながらテスト自動化を 推進すべく、初手は自動E2Eテストに力を入れて取り組まれていたそうですが、その取組みはSETがやりたいことであり、開発チームがやりたいことではなかったので、テスト自動化の推進が止まってしまったとのこと。

そして推進方法を見直す中で、自動E2Eテストを推進するというHowが選考して、何のために実施するのか、ニーズが有るのかという部分が欠けていたことに気が付き、改めて開発チームの困り事を把握してから、取り組んだらテスト自動化の取り組みが進んだとのことでした。

タイミーでも各スクラムチームの困り事を把握して、品質管理に関する支援に取り組まなくてはいけないと学んだセッションでした。

おわりに

今回はオンラインでの参加になりましたが、来年は現地で参加して積極的な情報交換を行ってみたいと思いました。

『効果検証入門』の輪読会&実践をしました!

こんにちは!タイミーのデータアナリストの @akki です。

タイミーのデータアナリティクス部では、様々な形式の勉強会が盛んに行われています。

アナリスト自身のスキルアップはもちろん、チーム全体での知識の共有や実務への応用を目指し、常に3〜4つの勉強会が開催され、有志が参加する形式をとっています。

今回は『効果検証入門〜正しい比較のための因果推論/計量経済学の基礎』(安井 翔太)を題材にした輪読と実践の勉強会についてお話しします。

この本は以前も勉強会で取り上げましたが、今回は新しいメンバーとともに再度実施しました。

輪読パート

輪読パートでは、本書を10に分けて担当を設定しました。毎週、各自が自分の担当分を要約し、Notionに記載して発表します。その後、参加者からの質疑応答や、本書の内容に基づいたディスカッションを行いました。

今回の勉強会での分担イメージ

感想:

  • すでに知っていた手法についても、改めて理論的な背景や実践する上での注意事項を学ぶことができ、理解が深まりました。
  • 同じチームで働いているメンバーとの勉強会なので、議論の中で「実務で使うためには」という観点の話が出やすいのもよかったです。

実践パート

輪読を終えた後、本書で触れられた効果検証の手法をタイミーのデータに実際に適用してみたので、その要点をお伝えします。

やったこと: タイミーの営業領域の施策で、施策を実施した効果について検証しました。

分析手法:

  • DiD(Difference in Differences)を選択しました。
    • 勉強会の題材ということもあり、事前に検証設計に入ることができなかったため、RCT等は実施できませんでした。
    • ただ介入群・非介入群ともに介入前後のデータが取得できたため、DiDを採用しました。

DiDによる効果検証のイメージ

結果:

  • 施策による改善効果が具体的に把握できました。
  • またいろんな分析軸とクロスしてみることで追加の知見も得られ、今後の意思決定につながりました。

今回の分析の課題:

  • 特にDiDについては、「何をもって平行トレンドとするか」が悩ましいポイントだなと改めて感じました。
    • スタートアップのような変化が激しい状況では、常に平行トレンドを満たしている対照群が設定しにくく、期間の切り方次第で平行トレンドを満たす対照群が変わることもあります。
    • 過去の目的変数の変化だけを見ても、本来満たすべき「時間を通じた目的変数の変化が同一である」という仮定が十分に満たせていないケースも多いです。
    • 定量面だけでなく、実験群や対照群の特性や、営業活動のオペレーションといった定性情報も把握した上で、対照群を決定することが重要であると感じました。

対照群の選定イメージ。目的変数だけ見ると、時期次第で平行トレンドの認識が変わりうる。

まとめ

今回の勉強会を通じて、手法を正しく理解することでより精緻な検証ができ、それが意思決定につながることを改めて実感できました。

これからも座学と実践のサイクルを回すことで、より会社に貢献できるデータ分析をしていきたいと思います!

We’re Hiring

私たちは、ともに働くメンバーを募集しています!!

カジュアル面談も行っていますので、少しでも興味がありましたら、気軽にご連絡ください!

Scrum Fest Niigata2024に参加しました

タイミーのyajiri、yorimitsu、seigiです。

アジャイルとテストのコミュニティの祭典に関する国内最大級のカンファレンス「Scrum Fest Niigata(スクフェス新潟)2024」が2024/05/10、11の2日間にわたって開催されました。 www.scrumfestniigata.org タイミーからもQAコーチ、マネージャー、スクラムマスターの3名が参加。世界中で開催されるすべての技術系カンファレンスに無制限で参加できる「Kaigi Pass」という制度を利用しました。 productpr.timee.co.jp 本レポートでは、印象に残ったセッションの内容を中心に、2日間の会の様子をお伝えします。

タイミーのQAコンセプトに役立つノウハウを得られた話

Yorimitsuです。私が参考になったセッションについてお話させてください。

タイミーではQAの仕組みを現在構築している最中で、コンセプトとしてQAの活動を2つに分類してプロダクト組織に導入しようと考えています。コンセプトの1つ目は、QA Enablingとしており 各スクラムチームが行っている品質管理活動の支援、例えばテスト観点の作成支援や、テスト設計の手法支援、自動テストの支援などを考えています。そして2つ目のコンセプトはQA Platformとして、品質分析、自動テストのインフラ整備、スクラムチームを横断するリグレッションテストの運営(自動テスト)などを計画しています。

このコンセプトを実現するにあたり、今回の発表セッションにあった「スクラムチームが一体になるために行ったQAプロセス変革の道のり」は大変参考になる内容でした。

各スクラムチームへのQAの入り方や、その後に起こる課題、そして課題の整理方法など、実例が 多く紹介されていました。

そして改めて、QAマニフェストの必要性にも注目しました。QAが何を担うのか、QAを担当する人及び、QAに何かを期待する人にとって、同じ方向性を向いて会話するために非常に有効な取り組みだと受け止め、タイミーでの策定を検討してみよう思いました。

ベイビーステップで不確実性を乗り越えるQAエンジニアの挑戦に感銘を受けた話

駆け出しQAコーチのYajiriです。

私はこの春にCSM研修を受講し、晴れてスクラムマスターに認定されたこともあり、初めてスクラムフェスに参加しました。

現在の役割がQAコーチということもあり、スクラムチームにおける品質保証に関するプログラムを中心に視聴しました。どれも有意義なものでしたが、特に印象に残ったセッションは「受け入れテスト駆動開発によって不確実性を段階的に解消するアプローチ」です。

今回紹介された事例は、ビジネスや経済のインテリジェンスを軸にサービスを展開するWebシステムの開発チームで、XP(エクストリーム・プログラミング)開発プロセスの中でATDD(受け入れテスト駆動開発)をどのように実践しているかが紹介されました。

Web開発で厳格にXPを取り入れるには様々な困難がある中、「ベイビーステップ(よちよち歩き)」を徹底することで、不確実性をコントロール可能な粒度に落とし込み、結果的に不確実性を低減させながら大きな成果を成し遂げるというものでした。

このベイビーステップは、Gaugeで実装される受け入れテストのコードでも徹底されており、一つのステップに対して多くの期待結果を盛り込むことを「ベイビーステップ違反」として統制するカルチャーが根付いているとのことでした。

これらの取り組みは、開発手法が変わっても不確実性をコントロールする手法として非常に参考になり、私たちの自動テストでもこの考え方を取り入れていきたいと感じました。

スクラムフェスはやっぱり楽しい!そしてオフラインで参加したい!!

スクラムマスターの正義です! アジャイル/スクラムに関するコミュニティ活動が好きで、いくつかのカンファレンスに参加したり、自身でもスクラムフェス神奈川を運営したりしています。

今回、スクラムフェス新潟へオンラインで参加しました。 (家庭の都合でどうしても現地参加できず…泣)

スクフェス新潟では現地参加した時の体験が素晴らしいらしく、是非とも次回は現地で参加したいと思います!ネットワーキングパーティでは、新潟ならではのご飯やお酒を堪能できるんだとか・・・!

今回、私はスクラムフェス新潟というアジャイルコミュニティイベントで「素敵だなー!」と思った点についてレポートさせていただきます!

スクフェス新潟の趣旨について

主催者のじゅんぺーさんによる、イベントの趣旨が説明されていました

まだまだ、アジャイルのコミュニティとテストのコミュニティはそれぞれが別のジャンルとして開催されていますが、スクラムフェス新潟ではその2つを合わせて開催する流れを作ることが目指されていました!

テストとアジャイルに対して熱い想いがあるからこそのビジョンだと思います!

行動規範を徹底したイベント

イベントを最高の形で終えるためには、参加者の行動がとても大切になります。 特に、ギャザリングを大切にするアジャイル/スクラムのコミュニティイベントでは、特に意識したい事項となります。 どのようなことを自分たちは大切にしているのか、具体的な楽しみ方、困った時にどうすれば良いのかについて、しっかり時間をとって説明されていました。 (運営の方によるハラスメントの寸劇までありました!)

今後、イベントを開催して業界を盛り上げていく人たち、コミュニティ、企業は是非とも参考にしたい点だと思います。

コミュニティイベントの楽しみ方を最初に紹介!

Keynoteの前に、菩薩さんによる最初のセッションがありました!

セッション名は「いかにしてオンラインで知り合いを増やすか」 現地に参加されている方だけではなく、オンラインに参加されている方もギャザリングで楽しめるように、どういうことを考えておくと良いかを経験を交えて紹介されていました。

視聴しつつチャットを楽しむコツとして、いくつかピックアップすると…

  • わからないことをはわからない、と正直に言おう!
    • テクニックとしては…「つまり〇〇….ってことコト!?」と言えば、なんとかなる
  • チャットの流れが早くてついていけなくても、気にせず喋ろう!
  • 勇気を出して発言しても、誰も反応しない…でもそんなこと気にしなくて良い!とにかく接触回数を増やしていこう!

とのことでした!

今まで幾つかのスクラムフェスに参加してきた中で、個人的に気になっていたことでしたがあらためて気にしなくていいんだ!という安心感をえられました。

また、相手の発言に対して、リアクションをつけたり、反応をしてみるだけで一緒にワイワイできていいよね!という話は下記スライドでクスッときましたw

今までは「さしすせそ」を使ってきましたが、今後は「はひふへほ」も活用しようと思います!

最後に

スクラムフェス新潟は、セッションで得られる情報だけに価値を置くわけではなく、オンライン/オフラインでのギャザリングや、パーティで素敵な料理を食べられるなどセッション以外でのコンテンツでも参加者が得られる体験がとても素敵なイベントだと感じました!

次回は…絶対に新潟の現地会場に行きたいです…!

一番印象的だった言葉は…「新潟のお酒は水」でした!

以上ですmm

おわりに

次回以降はなんらかの形でコミュニティを盛り上げることに貢献できたらと思います。

モバイルアプリエンジニアが RubyKaigi 参加してみた

はじめに

はじめまして、タイミーでモバイルアプリエンジニアをやっている tick-taku です。

5/15 - 5/17 の三日間にわたって沖縄で RubyKaigi 2024 が開催されました。全国から Rubyist が集結するイベントで弊社からもたくさんのメンバーが参加しており、自分も初めて参加してきました。

今回はそんな RubyKaigi に参加して感じたことや気になったポイントを紹介します。

rubykaigi.org

タイミーメンバーの参加レポートはこちら。

みなさん各セッションを解像度深く解説されていてとても勉強になりました。

tech.timee.co.jp

tech.timee.co.jp

tech.timee.co.jp

なぜ参加しようと思ったか

冒頭で自己紹介した通り、僕はモバイルアプリエンジニアで普段は Android や iOS アプリ開発がメインです。

そんな自分がなぜ RubyKaigi に参加しようと思ったか。それはこれから Ruby (rails) の開発ができるようになりたいと考えているからです。

そこで、まずは言語やコミュニティの雰囲気を掴むため Ruby の中で大規模なカンファレンスに参加してみようと思ったのがきっかけでした。

タイミーでは開発組織においてチームトポロジーをベースとしたストリームアラインドチームを運営しています。

その中で僕が所属しているチームはクライアント様に向けた機能開発を目的としており、クライアント様が使う管理画面の改善などが多いです。そのため、稀にワーカー様向けであるモバイルアプリの開発タスクが希薄になることがあります。

逆にバックエンドタスクがまだまだ手が足りていないので、モバイルにクローズせずにケイパビリティを発揮していきたいと考えました。

また僕は専門領域特化型ではないと昔から感じているため、全体を満遍なくできるようになることでゴール達成のためにどこか人が足りていない部分を補う動き方をしていきたいと考えています。

チームの方向性ともマッチしているためバックエンドで採用している Ruby を勉強していこうと思いました。

後は沖縄で開催と言うのもかなり魅力でした。沖縄ですよ、沖縄。こんなに聞くだけで胸躍るキーワードなかなかありません。キラキラドキドキですね。

RubyKaigi は毎回日本各地を転々と開催しており、毎年同じチームの Rubyist がワクワクしていたのを羨ましく感じていました。

参加してみて

楽しかった

これに尽きると思います。

コミュニティの交流であったり「こういう事を考えて言語をよりよくアップデートしている」といった事が聞けたり、普段の関わりから遠い話がたくさん自分事として聞けたことがとても楽しかったです。

知識的な話

RubyKaigi に関しては参加理由にも少し言及していますが Ruby に慣れる事を目的として参加しました。

と言うのも、Ruby を使って開発している人たちによる「どういう課題をどう解決したか」と言った話が聞けると Ruby をより身近に感じられ、モチベーションに繋がるかなと思ったからです。 モバイル系のカンファレンスで言うと DroidKaigiiOSDC みたいなものを想定していました。

ですが、実際に RubyKaigi に参加してみるともっと低レイヤーの、Ruby の中はこう動いているだったりコンパイラの話などばかりでした。正直何言ってるか分からないことだらけでしたが、セッションの端々から NamespaceRBS など気になる単語が聞こえてきて知的探求心が刺激されました。

アスキーアートもあまり馴染みがなく新鮮で面白かったです。ゲームを動かしてみたりとスピーカーの Ruby が愛が伝わってきました。

以下に気になったワードを列挙します。

Namespace

今回のセッションの中で一番興味を持ったテーマがこの Namespace について です。

Java で言う package (や Kotlin の alias import)を Ruby でやりたいのかなと思いました。

自分は今まで Ruby (と言わずスクリプト言語全般) の変数がどう参照されているかが分かり辛く、またクラスや変数のコンフリクトが起きやすいのではと思っていました。Google が Ruby のライブラリなんか作ったらそれはもう大変なことに...

モジュラモノリスなアプリケーションにおいてチームの規模が増えるにつれ、こういった話の課題感は飛躍的に上がっていきそうな気がするので重要度は大きいのではないでしょうか。

Refinements

Namespace のセッション内で Refinements というワードが聞こえたので、Refinements について調べました。

こちらはメソッドに対してある特定のスコープ内の挙動を書き換えるものだとわかりました。Namespace が package に対して Refinements はどちらかと言うと extensions なのかな?少し違うかも...

Refinements が Namespace に成り代わる(統合される)わけではないと言及されていて、上記の比較が正しければ確かにそもそもの目的・用途が別物ですね。実際に触って理解していきたいと思います。

RBS

自分は Java からスタートしたので馴染みがありますが、動的型付けの Ruby でもタイプセーフのメリットを傍受したい!的な話でしょうか。コンパイルでエラーを吐き出されたりエディタ上で確認できた方がいいのは開発スピードや品質にも関わってくるのでそれはそうだと思います。

Java の記述が冗長になりがちなデメリットを Kotlin が型推論でカバーしていることを考えると自然な流れに見えます。

ただし定義が .rbs (別ファイル) に定義されることが、必要に応じて定義できるフレキシブルさを持っている反面運用時のネガティブコストにならないかは心配になりました。

TypeProf

そして型推論をやろうとしているのが TypeProf でしょうか。(Good first issues of TypeProf) .rbs ファイルを自動生成するから管理を気にしなくてよくなるのかもしれない? こちらも触って確かめてみようと思います。

Parser

今回の RubyKaigi で最も聞いた単語だと思います。普段プログラムの Parser を意識することはあまりありませんでしたが、実際にどういうアルゴリズムで動いているとかこの言語だとこうだけど Ruby やこのツールはこうなんですよみたいなのが聞けて面白かったです。

調べているとかなり歴史や思想があって興味深いのですが詳細を書くととても長くなってしまいそうなので割愛します。The grand strategy of Ruby Parserを発表されていた kaneko-san のこちらの記事がとても勉強になりました。

コミュニティの話

社内外問わずたくさんの人にはじめましてが出来たことも良い刺激でした。

モバイル界隈に生息しているため社外の Rubyist はもちろんのこと、タイミーではフルリモートを採用しており、自分はまだ入社して半年も経っていないためチームでも現地で初めて顔を合わせる人がたくさんいました。 そういった人たちとパーティやランチで普段何しているかだったり業務では聞けない話をたくさんできて楽しかったです。

RubyKaigi で驚いたと共にいいなと思ったことが、各スポンサーや有志がアフターイベントを企画しそのイベントをオフィシャルが公表していることです。

参加する人が口を揃えて RubyKaigi はお祭りだと言っている意味がわかりました。Official Party はもちろんですが、最終日にも懇親会があることも驚きましたし、各社が企画する DrinkUp やカラオケ大会、果てにはクラブを貸しきる DJ イベントもあり、なんでもありだな...と。 タイミーも初日から二日続けて DrinkUp を開催するという狂気っぷりを発揮しています。

timeedev.connpass.com

せっかく初参加なのでと時間に都合がつく限り参加してみました。 とは言え、初参加だし専門領域も違うので単身乗り込んで行って大丈夫か...?ちゃんとコミュニケーションできるか...?知らん人に囲まれて歌えるか...?と不安ばかりでした。

ですが実際に飛び込んでみるとそんな不安は杞憂に終わりました。話す人みなさんが歓迎ムードで相手へのリスペクトを感じ、Ruby コミュニティのウェルカムマインドはなんて素晴らしいんだと感動しました。

こちらは2日目の rubykaraoke で午前3時まで完走した猛者たちの様子。面構えが違う…

一人不安に思いながら参加しましたが、楽しみ過ぎて完全に声が出なくなりました😇

このお祭りみたいな雰囲気と、それをオフィシャルが大々的に謳っていることは社外の人と交流するハードルが一気に下がってとてもよい取り組みだと思います。旅先であることも盛り上がりの燃料となっている気がしますね。

だからこそ Ruby コミュニティはここまで規模が大きくなっているんだと実感できました。

ここまでの規模でこれだけ盛り上がりの大きいカンファレンスは自分が知る限り国内ではあまり見かけないので非常に良い機会提供の場になっていると思いました。(Android のカンファレンスでもこういうのないかなぁ) そりゃ毎年みんな行きたがるし帰ってきてからもわいわいしてるわけだ...

さいごに

まずは関わってくださったみなさまに感謝を。 Ruby を開発してくださっているコミッターの人、RubyKaigi を運営してくださったスタッフの人、雑に話に行って歓迎してくださった人、チームのメンバー、専門領域が違うにもかかわらず参加させてくれた上司・会社などなど、本当にありがとうございます。

タイミーでは KaigiPass と呼ばれる制度があって、レポートを書いたり登壇するなど何かしらのアウトプットでコミュニティに貢献することを前提に、国内外問わずカンファレンス参加の費用を負担してくれる制度があります。 冒頭で紹介した通り僕はモバイルアプリエンジニアで Ruby とはほど遠く、社歴もまだ半年も経っていないのに、それでも参加を認めてくれています。タイミーはなんて素晴らしい組織なんだ。

今後のための教訓として RubyKaigi や Ruby についてある程度事前に調べていくべきだったと反省しています

上述した通り RubyKaigi の趣旨もそうですが、知らない単語を調べながらセッションを聞いていると途中でついていけなくなったりしました。まぁついていけてもわかってなかったですが...

とは言え Ruby に関しては何から手をつけていいかわからなかったので、ワードからこういうことがあるんだなと調べるためのとっかかりが得られたのはとても大きな一歩だと感じています。

またネックストラップの色によって写真の掲載に承諾するかを意思表示できるようになっています。黄/赤 は 🙆‍♀️、白/青 は 🙅‍♂️ です。

ところがアイキャッチの写真をよく見てください。1人だけ青いですね。

そう、僕です。完全に理解していませんでした。自分のイメージカラーだから青にしよ♪くらいの気持ちでいました。 撮り終わった後に教えていただいて慌てて付け替えたんですが、ちゃんと会のレギュレーションをチェックしておけばと後悔しています...お手数おかけしました...

何事も事前準備が大事ですね。

以上、モバイルアプリエンジニアが RubyKaigi に初参加してみた参加レポートでした。 振り返ってみると圧倒的によかったこと・得られたものが多く、今回参加してみて本当に満足しています。

来年は愛媛県松山市ということでぜひ次回も参加したいですね! せっかくだから自転車持っていって帰りはしまなみ海道渡ってから帰ろうかな...

【RubyKaigi 2024 参加レポート】Namespaceを実際に触ってみた

こんにちは、タイミーの @masarakki です。

先日、5月15日から3日間開催された「RubyKaigi2024」に参加しました。
本記事で取り上げるのは、そのRubyKaigi2024の最後のセッションであるmatzのキーノートで、「これが入ったらRuby 4.0」とまで言われた @tagomoris 氏のNamespace機能。 セッション終了後、目の前に本人が座っていたので「責任重大だねwww」と煽りに行こうとしたところ、感極まって帽子を目深に被りなおしている瞬間だったのでそっとしておきました。

というわけで、セッションの内容 は他にいくらでも記事があると思うので、実際に手を動かしてみようと思います。

参考: https://gist.github.com/tagomoris/4392f1091f658294bd4d473d8ff631cb

作業ブランチが Namespace on readにあるのでビルドしてみましょう。

$ git clone https://github.com/tagomoris/ruby
$ cd ruby
$ git checkout namespace-on-read
$ ./autogen.sh
$ mkdir build
$ cd build
$ ../configure --prefix=$HOME/ns-ruby
$ make
$ make install

$ ~/ns-ruby/bin/ruby -v
ruby 3.4.0dev (2024-03-28T13:58:33Z namespace-on-read f0649a2577) [x86_64-linux]

どうやらうまくビルドできたようです (rubyのビルド人生で初めてやった)。

かんたんな検証コードを動かしてみましょう。

# foo.rb ------

require './bar'

class Foo
 def self.var=(val)
    @@var = val
  end

  def self.var
    @@var
  end
end

# -------------

# bar.rb ------

class Bar
end

# -------------

# nstest.rb ---

def dump(obj)
  puts "#{obj}: #{obj.object_id}"
end

require './foo'

ns = Namespace.new
ns.require './foo'

dump Foo
dump Bar

dump ns::Foo
dump ns::Bar

Foo.var = 'abc'
ns::Foo.var = 'xyz'

puts "#{Foo.var}, #{ns::Foo.var}"

# -------------

実行してみましょう。

~/ns-ruby/bin/ruby nstest.rb
Foo: 100
Bar: 120
#<Namespace:0x00007ff908cf1cd8>::Foo: 140
#<Namespace:0x00007ff908cf1cd8>::Bar: 160
abc, xyz

Foons::Foo が全く独立していることがわかります。

普段遣いのrubyでは動かないことを確認しましょう。

$ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]
$ ruby nstest.rb
nstest.rb:8:in `<main>': uninitialized constant Namespace (NameError)

ns = Namespace.new
     ^^^^^^^^^

確かセッションでは例として Oj.default_options行儀の悪い gem によって書き換えられてしまう事例が挙げられていたので試してみましょう。

# foo.rb ------

require 'oj'
Oj.default_options = { symbol_keys: true }

class Foo
  def self.oj
    Oj.load('{"key":"symbol_or_string"}'
  end
end

# -------------

# nstest.rb ---

require './foo'

ns = Namespace.new
ns.require './foo'

Oj.default_options = { symbol_keys: false }

p Foo.oj
p ns::Foo.oj

実行してみましょう。

$ ~/ns-ruby/bin/gem i oj
$ ~/ns-ruby/bin/ruby nstest.rb
$HOME/ns-ruby/lib/ruby/3.4.0+0/date.rb:51: [BUG] Segmentation fault at 0x0000000000000168
ruby 3.4.0dev (2024-03-28T13:58:33Z namespace-on-read f0649a2577) [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0013 p:0039 s:0052 e:000051 CLASS  $HOME/ns-ruby/lib/ruby/3.4.0+0/date.rb:51 
_人人人人人人人_
> 突然のSEGV <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

ちなみに date.rb:51def coerce(other) です。 なぜ・・・
そういえばセッションで @tagomoris も祈りながら実行していたな・・・というのを思い出し、何度か実行してみたところ、確率10%くらいで成功しました。

$ ~/ns-ruby/bin/ruby nstest.rb
{"key"=>"symbol_or_string"}
{:key=>"symbol_or_string"}

他のライブラリでも試してみましょう。

json

標準ライブラリで oj と同じようにC拡張を持つライブラリです。
特に何も問題なく読み込めました。

csv '<top (required)>': uninitialized constant Array (NameError)

ネームスペースの中でArrayが見つからないみたいです。
もっと単純なコードで試してみましょう。

# foo.rb -----
puts "#{Array}: #{Array.object_id}"

module Mod
  def foo
    p :foo
  end
end
Array.include(Mod)

# ------------

# nstest.rb --

puts "#{Array}: #{Array.object_id}"

ns = Namespace.new
ns.require './foo'

[].foo

# ------------
$ ~/ns-ruby/bin/ruby nstest.rb
Array: 80
Array: 80
:foo

なにかおかしいですね。
Array は見つかるものの、予想外にトップレベルの Array まで汚されてしまっています。

さらに foo.rbrequire 'csv' を追加すると

$ ~/ns-ruby/bin/ruby nstest.rb
$HOME/ns-ruby/lib/ruby/gems/3.4.0+0/specifications/csv-3.2.8.gemspec:4: [BUG] vm_cref_dup: unreachable
_人人人人人人人_
> 突然のSEGV <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

トップレベルで require ‘csv’ した後に ns.require './foo' するとuninitialized constant Array (NameError)のエラーに戻ります

jsoncsv

$ ~/ns-ruby/bin/ruby nstest.rb                                                                                                                      
$HOME/ns-ruby/lib/ruby/3.4.0+0/forwardable.rb:230: [BUG] Segmentation fault at 0x00000000000001da

-- Control frame information -----------------------------------------------
c:0017 p:---- s:0088 e:000087 CFUNC  :proc
c:0016 p:0004 s:0084 E:001920 TOP    $HOME/ns-ruby/lib/ruby/3.4.0+0/forwardable.rb:230 [FINISH] 
_人人人人人人人_
> 突然のSEGV <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

問題のない jsonと問題のある csv を両方requireするとなぜか別の問題が発生しました。
面白いですね。
(自分のrubyのビルド方法が間違ってる疑惑・・・?)
こういう開発途中の機能を触ってみるのは初めてなので、Rubyがこんなに簡単にぶっ壊れるんだ・・・って新鮮な気持ちです。

いろいろなパターンでSEGVが出たので次回はコードを読んでみたいと思います。

最後に

コロナ禍で人生が一変し、結婚 + 出産 * 2 が続き、2019年以来 実に5年ぶりのRubyKaigi参加になりました。
RubyKaigi、やっぱりドチャクソ楽しいですね・・・特にrubykaraoke・・・ッ!!
1歳と0歳を連れて参加できたのも、ひとえに託児所やイベントに子供を受け入れてくださった各社様のおかげです。この場を借りてお礼申し上げます。

なお、RubyKaigiには「Kaigi Pass」という制度を利用し参加しました。
Kaigi Passとは、世界中で開催されている全ての技術カンファレンスに無制限で参加できるタイミーの制度です。
制度について気になる方はぜひ以下の記事もご覧ください。

productpr.timee.co.jp