Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Rubyの実行モデル解説記事、shale gem、HTTP/3がRFC 9114にほか(20220614後編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 shale: Rubyデータ構造をJSONやYAMLやXMLなどと相互変換(Ruby Weeklyより)

kgiszczak/shale - GitHub


つっつきボイス:「shaleってシェールガスのシェールかな?」「たしかに頁岩(けつがん)のshaleと同じスペルですね」

参考: シェールガス - Wikipedia

「よくあるgemかなと思ったら、JSON SchemaやXML Schemaも扱えるらしい↓」「お〜」「ちゃんと動くならですけどね」「一応★付けておこうっと」「できるかどうかわからないけど、XSDを渡してJSONを出力できたら嬉しい」

# 同リポジトリより
require 'shale/schema'

Shale::Schema.to_xml(Person, pretty: true)

# =>
#
# {
#   'schema0.xsd' => '
#     <xs:schema
#       elementFormDefault="qualified"
#       attributeFormDefault="qualified"
#       xmlns:xs="http://www.w3.org/2001/XMLSchema"
#       xmlns:foo="http://foo.com"
#     >
#       <xs:import namespace="http://foo.com" schemaLocation="schema1.xsd"/>
#       <xs:element name="person" type="Person"/>
#       <xs:complexType name="Person">
#         <xs:sequence>
#           <xs:element name="name" type="xs:string" minOccurs="0"/>
#           <xs:element ref="foo:address" minOccurs="0"/>
#         </xs:sequence>
#       </xs:complexType>
#     </xs:schema>',
#
#   'schema1.xsd' => '
#     <xs:schema
#       elementFormDefault="qualified"
#       attributeFormDefault="qualified"
#       targetNamespace="http://foo.com"
#       xmlns:xs="http://www.w3.org/2001/XMLSchema"
#       xmlns:foo="http://foo.com"
#     >
#       <xs:element name="address" type="foo:Address"/>
#       <xs:complexType name="Address">
#         <xs:sequence>
#           <xs:element name="city" type="xs:string" minOccurs="0"/>
#         </xs:sequence>
#       </xs:complexType>
#     </xs:schema>'
# }

参考: JSON Schema | The home of JSON Schema
参考: XML Schema - Wikipedia

🔗 Rubyの実行モデル解説(Ruby Weeklyより)


つっつきボイス:「Shopifyの技術記事です」「Copy on Write(CoW)やスレッドの話がある」「ライブラリコードなどをプリロード可能で親プロセスと子プロセスで共有できるメモリをstatic memory、子プロセス実行時の処理に応じて使われるメモリをprocessing memoryと呼んでいるんですね↓」


同記事より

参考: コピーオンライト - Wikipedia

「static memoryは親子プロセスが同じように参照しますが、書き換えが発生しない前提のメモリなので、親プロセスで一度確保してしまえば子プロセスでは書き込みを行わない限り追加のメモリ確保不要で利用できます」「ふむふむ」「そして以下の図のように、static memoryを共有している2スレッドのプロセス1個の方が、1スレッドの独立したプロセス2個よりもメモリ消費が小さくなる↓」「なるほど、記事の最後で"常に可能な限りプリロードしておけ"と書かれているのはそういうことなんですね」「そうそう、オレンジのstatic memoryの部分をプリロードで大きくしておくことで、緑のprocessing memoryの追加を少なくできます」


同記事より

「記事はGVLのレイテンシの話や、RactorやFiberが銀の弾丸ではないという話もありますね」「YJITが状況を変革するかもしれないという話もある」「かなりよさそうなコアな技術記事👍」「さすがShopify」

🔗 dry-rbを探ってみた


つっつきボイス:「翻訳記事でもお世話になっているBrandon Weaverさんの記事です」「シリーズ物っぽいタイトルだけど、dry-rbのどの機能を掘っているんだろう?」

参考: dry-rb - Home

「流し読みしただけですが、前置きが割と長いかも: JSONを返すHTTPレスポンスを扱うのにEnumerableを使ってもいいけど同時に実行されるわけではないよね、というのが前半の流れっぽい」「後半でdry-monadsを使うといいよと書かれているのが本題なんでしょうね」「dry-monadsでResultモナドを作ることで同時実行できるようにしたり、パターンマッチングと組み合わせたりするとさらにいいよという感じかな」「やっぱりWeaverさんはモナドとかモノイドが好きなんですね」「モナドまだわからない😅」「このあたりに興味のある人は読んでみるといいかも👍」

# 同記事より
require "dry/monads"

extend Dry::Monads[:result]

# Pretend it's a DB of some sort
IDS = { "a" => "Red", "b" => "Blue" }

def offering_a(id)
  return Failure("ID not found: #{id}") unless IDS.key?(id)

  Success(IDS[id])
end

offering_a("a")
# => Success("Red")

offering_a("nope")
# => Failure("ID not found: nope")

# Remember Enumerable? What happens if we chain each of these?

offering_a("a").fmap { |name| "We found #{name}!" }
# => Success("We found Red!")

offering_a("nope").fmap { |name| "We found #{name}!" }
# => Failure("ID not found: nope")
# 同記事より
result = offering_a("a")

case result
in Success("Red" | "Blue") then "Found who we were looking for"
in Success then "Not who we expected, but still ok"
in Failure(/_why/) then "He's still in our hearts though"
in Failure then "I give, you win"
end

dry-rb/dry-monads - GitHub

Rubyでわかる「時計もモノイドの一種」(翻訳)

🔗 workflow: ステートマシンgem(Ruby Weeklyより)

geekq/workflow - GitHub


つっつきボイス:「このworkflowは新し目のステートマシンgemみたいですけど、★が1700超えと多いですね」「workflowという名前、ググりにくそう...」

# 同リポジトリより
class Article
  include Workflow
  workflow do
    state :new do
      event :submit, :transitions_to => :awaiting_review
    end
    state :awaiting_review do
      event :review, :transitions_to => :being_reviewed
    end
    state :being_reviewed do
      event :accept, :transitions_to => :accepted
      event :reject, :transitions_to => :rejected
    end
    state :accepted
    state :rejected
  end
end

「Rubyのステートマシンgemというとaasmが昔から有名で↓、workflowのインターフェイスもaasmと似ている感じですけど、workflowのgemspecGemfileを見るとRailsに依存していないので、そこが売りなのかもしれない」「なるほど、Rails以外でも使えるんですね」

aasm/aasm - GitHub

参考: 有限オートマトン - Wikipedia
参考: Rails で使える StateMachine Gem 3 つをしらべてみた - Qiita

🔗 その他Ruby

つっつきボイス:「そういえばselfを付けてprivateメソッドを呼び出せるようになってましたね」

「ところでこれはホントその通り↓」「その通り」「普段からインスタンス変数を直接書き換えていたから、privateメソッドの仕様変更を気にしていなかった😆」「長年Rubyでよくない書き方を避けていると、言語仕様の変更に気づきにくいというのはあるかもしれませんね」

ただ、このようなコードは混乱を招きやすいので、何か特別な理由がなければセッターメソッドを使わずにインスタンス変数を直接書き換える方がよいでしょう。
https://qiita.com/jnchito/items/451018811842c2631e1eより

🔗DB

🔗 動画: Railsのバルクインポートを100倍高速化する(Ruby Weeklyより)


つっつきボイス:「動画だと時間がかかるので、サンプルコードの差分URLを貼っておきました↓」

perform_laterで非同期化も行っているみたい↓」

# app/models/event_stream.rb
class EventStream < ActiveSupport::CurrentAttributes
  attribute :updated_movie_ids

  # effectively a pre- and post- request/job callback
  before_reset do
    if updated_movie_ids&.any?
      # enqueue a job to process any updated movies
      MovieNotificationJob.perform_later(updated_movie_ids)
    end
  end

  # stores when any given movie has been updated
  def movie_updated(movie_id)
    self.updated_movie_ids ||= []
    self.updated_movie_ids << movie_id
  end
end

参考: Rails API perform_later -- ActiveJob::Enqueuing::ClassMethods

「予想はしていたけど、やっぱり生SQLで高速化している↓」「さもありなんですね」

# https://github.com/ryantownsend/bulk-import-exercise-solution/compare/d70a5076c927a4cb68703207dd3a2b59d68d7e93...8981231a1b0bf84aa4a1dc335928d468372c4e8b#diff-41f85d153f0416158af73a3ec7531d403d459861ecaccb145ddb47a6e2383aa0R2
  UPSERT_QUERY_TEMPLATE = <<~SQL
    with movie_import_entries as (
      select
      (row_number() over()) + %{batch_offset_start} as index,
        t.id,
        t.title,
        t.description,
        t.rating,
        t.published,
        t.subscriber_emails,
        movies.id is not null as already_exists,
        (
          (movies.id is not null or t.title is not null) and
          (movies.id is not null or t.description is not null) and
          (
            (movies.id is not null and (t.rating is null or t.rating between 1 and 5)) or
            (t.rating is not null and t.rating between 1 and 5)
          )
        ) as is_valid
      from
        movie_imports,
        jsonb_to_recordset(jsonb_path_query_array(movie_imports.entries, '$[%{batch_offset_start} to %{batch_offset_end}]')) as t(
          id uuid,
          title text,
          description text,
          rating numeric(2,1),
          published boolean,
          subscriber_emails text[]
        )
      left join
        movies on movies.id = t.id
      where
        movie_imports.id = '%{movie_import_id}'
    ),
...

「UPSERT_QUERY_TEMPLATEを作っているということは、やはりINSERT INTO SELECTしていますね↓: DBの中で実行するならINSERT INTO SELECTは高速」「なるほど」「DBの外で行うなら、TSVなどの段階で前処理しておくと速いですね」

# https://github.com/ryantownsend/bulk-import-exercise-solution/compare/d70a5076c927a4cb68703207dd3a2b59d68d7e93...8981231a1b0bf84aa4a1dc335928d468372c4e8b#diff-41f85d153f0416158af73a3ec7531d403d459861ecaccb145ddb47a6e2383aa0R70
    -- where the movie doesn't exist, import it
    inserted_movies as (
      insert into
        movies (
          id,
          title,
          description,
          rating,
          publishing_status,
          subscriber_emails,
          created_at,
          updated_at
        )
      select
        movie_import_entries.id,
        movie_import_entries.title,
        movie_import_entries.description,
        movie_import_entries.rating,
        case
          when movie_import_entries.published = true then 'published'
          else 'unpublished'
        end,
        movie_import_entries.subscriber_emails,
        now() as created_at,
        now() as updated_at
      from
        movie_import_entries
      where
        movie_import_entries.already_exists = false and
        movie_import_entries.is_valid = true
      returning
        id
    ),

参考: Tab-Separated Values - Wikipedia

「全般に、教科書的な定番の高速化手法ですね👍」「なるほど」「生SQLを使うとRailsのアプリケーション層によるチェックが行われなくなりますが、高速化のためにこうすることも多い」「アプリケーション層を通すこと自体がオーバーヘッドですもんね」

🔗クラウド/コンテナ/インフラ/Serverless

🔗 HTTP/3がRFC 9114で標準化(Publickeyより)


つっつきボイス:「HTTP/3は完全にQUICに乗るんですね」「フローコントロールがTCPじゃない世界が来た」「従来でも動画配信などではTCPを使わずにUDPを使ったりしていたので、それ自体はそれほど新しくはないけど、動画配信並の即応性を一般のWebブラウジングでも求められるようになったんだなという気持ちと、従来のネットワークレイヤモデルが崩れつつあるのかなという気持ちがありますね」「たしかに」

参考: インターネット用語1分解説~QUICとは~ - JPNIC
参考: TCP/IPとは?通信プロトコルの階層モデルを図解で解説 | ITコラム|アイティーエム株式会社

「HTTP/3になるとプロトコル番号もUDPになるので、ファイアウォールなどにも結構影響がありそうな気がしますね: L4までのファイアウォールがうまく機能しなくなってアプリケーションロードバランサーでないと扱えなくなったり」「う〜む」

「アプリケーションロードバランサー周りがHTTP/3でどう変わるかは気になる: データグラムを一度フローに展開することになってアプリケーションロードバランサーの仕事がすごく増えそうな気もするけど、TLSで暗号化されればエンドツーエンド制御になりそうだし、このあたりは今後調べる必要がありそうかな」

参考: TLS(Transport Layer Security / SSL/TLS)とは - 意味をわかりやすく - IT用語辞典 e-Words
参考: Cloud CDN とロード バランシングで QUIC を使用して HTTP/3 でコンテンツを取得 | Google Cloud Blog

🔗言語/ツール/OS/CPU

🔗 M1 Macのサーバーサイド開発環境


つっつきボイス:「お〜、昨年はいったん諦めてたの↓がついに対応できたんですね🎉」「arm64に対応したものもだんだん増えてきてますね」「MySQLも対応してる!」「Oracleがやっている本家のMySQLですね: もうM2が発表されたし、対応はしておきたいでしょうね」「以前のバージョンのMySQLがarm64に対応していない可能性はあるかも」

参考: Repro のサーバーサイド開発環境を M1 Mac に対応させるまでの道のり(撤退編) - Repro Tech Blog
参考: Apple、画期的なパフォーマンスと能力を備えたM2を発表 - Apple (日本)


後編は以上です。

バックナンバー(2022年度第2四半期)

週刊Railsウォッチ: Hotwireをアプリ構築で学ぶ、Active RecordのDurationとPostgreSQL intervalデータ型ほか(20220613前編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby Weekly

Publickey

publickey_banner_captured


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。