Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1383 articles
Browse latest View live

週刊Railsウォッチ(20200630後編)Shopify流テスト削減、仕様化テストでレガシーコードと戦う、PostgreSQLのarray_agg()ほか

$
0
0

こんにちは、hachi8833です。ruby-jp Slack、ひと頃より落ち着いてきた感ありますが、油断すると未読たまりますね😅。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

Ruby

実行するテストを減らして「ときめく」には(Ruby Weeklyより)

つっつきボイス: 「お、Shopifyの記事」「動的解析やらいろいろ頑張ってるそうです」「Rubyでやろうとすると大変そう」


「ちょっと思ったんですけど、Shopifyが今後さらに大規模になるようなら、今後ピュアなRubyの代わりにRubyの拡張版を作って使ったりするかなと」「あ〜」「ほら、FacebookがPHPを拡張してHackという上位コンパチ言語を作ったみたいに↓」「そういえばHackってありますね!」「ググりにくそうな名前😆」

参考: Hack (プログラミング言語) - Wikipedia

「自社プロダクト向けに言語を拡張するのってゲーム業界だと割とよく行われますし、見ようによってはJetBrainsのKotlinもその一種でしょうし↓」「たしかにそれはそれでありかも!」「まあ自分たちでメンテしきれるならですけど😆」

参考: Kotlin - Wikipedia


記事見出しより:

  • テストが落ちたり落ちなかったりする原因
    • タイミング
    • データベースが不安定
    • ランダムなジェネレーター
    • テストのステートが他のステートに漏れている
  • Shopifyのモノリスティックなリポジトリ
    • テスト15万件以上
    • 年に20〜30%増加
    • Dockerコンテナを数百個パラレルにしても30〜40分かかる
  • テストの問題を解決する
  • 動的解析とは
  • 動的解析を使う理由
  • Railsで動的解析するデメリット
    • 遅い
    • 生成されたマッピングラグがHEADより遅れる
    • トレースできないファイルがある
    • メタプロで呼び出しパスがあやふやになる
    • 失敗するテストの一部をキャッチできない
  • 新しいパラレル動的解析を試す
    • 失敗するテストの「リコール率」(確実に期待通りに失敗する率)を定義
    • 速度向上
    • 計算時間の短縮
  • その他検討したアプローチ(最終的には使わず)
    • 静的解析
    • 機械学習
    • テストマシンの増強
  • テストを減らすメリット
    • 開発者に短時間でフィードバックできる
    • テストが失敗したりしなかったりする可能性を減らせる
    • CIの方がコストは低い
  • 動的分析やテストの精選への反論は、意外にも出なかった

はみだしつっつき:「ところでタイトルのspark joyって何だろうと思ってググったら、日本語の「ときめき」が由来の流行り言葉だそうです↓」「ときめきがspark joy、わかるようなそうでもないような😆」「コンマリさん、女子の世界ではえらく有名な方みたいですね」「知らない世界すぎる😆」

参考: スパーク・ジョイ! 「コンマリ」が欧米人ウケしている理由分析|THE MAINSTREAM(沢田太陽)|note

後で嫁さんに聞いたら「ときめきを感じないものはどんどん捨てる」というのがコンマリ流型付け術なんだそうです。

仕様化テストでレガシーコードと戦う(Ruby Weeklyより)


つっつきボイス:「characterization testという言葉を初めて見たんですけど説明↓が今ひとつよくわからなくて、他の記事も参考にするとどうやら『レガシーコードがどう振る舞うかを調べるためにテストを書く』ということなのかなと」「なるほどね〜」

A characterization test is a technique coined by Michael Feathers where we write tests for code where we don’t have them. It also means that, to some extent, we shouldn’t be guessing what the code should do and instead we should be in “exploratory” mode.
同記事より

# 同記事より
def mask(str)
 if str.length <= 4
   return str
 end

 # Email
 if str =~ URI::MailTo::EMAIL_REGEXP
   limit = str.length - 4
   return "#{'*' * limit}#{str[limit..-1]}"
 end

 # Phone Number or ID
 if str.is_a?(Numeric)
   limit = str.length - 4
   return "#{'*' * limit}#{str[limit..-1]}"
 end

 # String, like a name
 limit = str.length - 4
 return "#{'*' * limit}#{str[limit..-1]}"
end

「characteristic testという言葉がどのぐらい一般的なのかはわかりませんけど、記事をざっと見た感じでは、今動いているコードのブラックボックス的な部分に対してテストを書いていくというアプローチのようですね」「おぉ」

「仕様書もドキュメントもないけどメンテしないといけないときなんかに、コマンドラインで手動で動かして目検するような操作を自動化したようなイメージですね: わざとfailするテストを書いてはgot:に出た値をテストデータにコピペしてパスさせて、それを繰り返してテストパターンを増やしながら仕様を少しずつ明らかにしていく、みたいな進め方」「なるほど!」

「他にどうしても手段がないときなんかだと、ときどきこの手のテスト書くことありますヨ」「お疲れさまです…」「ヤマカンで境界値っぽい値をぶちこむのを繰り返して、最終的に以下みたいなテストを増やしていく: もちろんそれが本当に境界値なのかとか、仕様として正しいのかどうかはこれだけだとわかりませんけど、少なくともこの入力に対してこの出力が得られるという情報にはなるので、それを守ってメンテしていくと」「ふむふむ」

# 同記事より
equire_relative './../mask'

describe 'mask' do
  it "masks regular text" do
    expect(mask('simple text')).to eq('*******text')
  end

  it "masks an email address" do
    expect(mask('example@example.com')).to eq('***************.com')
  end

  it "masks numbers as strings" do
    expect(mask('635914526')).to eq('*****4526')
  end

  it "does not mask a string with 4 characters" do
    expect(mask('asdf')).to eq('asdf')
  end

  it "does not mask a string with 3 characters" do
    expect(mask('asd')).to eq('asd')
  end

  it "does not do anything with empty strings" do
    expect(mask('')).to eq('')
  end

  it "masks symbols like regular characters" do
    expect(mask('text .-@$ with symbols-')).to eq('*******************ols-')
  end
end

「そうやってレガシーコードと戦うんですね🤺」「元記事はこれでテストを固めてからリファクタリングしてますね」「こういうテストはバグが出たときにも有効ですね: エッジケースと思われる部分をテストで押さえてから修正すれば、少なくともその部分はデグレを避けられますし」「たしかに!」

「そのままだと怖くて触れないコードにこうやってアプローチするというのは割とあります☺️」「少なくともこういうテストは作業前に作らないといけませんよね」「やらずに済むのが一番ですけど😆」「仕様化テストも増やせば増やすほど当然遅くなりますし🐢」「自分もいずれこういうことしないといけなくなるのかな…😢」「そういえば今もRails 2をメンテしている知り合いいます」「マジで😆」

「なるほど、仕様化テストとも呼ばれてるのね↓」

参考: Grenning > 仕様化テスト (Characterization Test) > 仕様化テストは、チームの長期記憶としての役割も果たしてくれる - Qiita

Inkblot: 電子ペーパーをRubyで制御(Ruby Weeklyより)

# 同サイトより
require 'inkblot'
include Inkblot

Components::SimpleText.new do |st|
  st.div_height = st.div_width = 95
  st.gfonts = %w[Pacifico]
  st.font = st.gfonts.first
  st.text = 'Hello'
  st.size = 60
  st.border_size = 10
end

Components::TableList.new(
  items: %w[Apples Oranges Pears Peaches],
  fullscreen: true
)

Components::IconGrid.new do |ig|
  ig.fullscreen = true
  ig.icons = %i[alarm extension face grade]
end

Components::ScrollMenu.new do |sc|
  sc.fullscreen = true
  sc.items = (1..10).map { |x| "Option #{x}" }
end

Components::BarCode.new(
  fullscreen: true,
  code: "978054538866"
)

Components::QrCode.new do |qr|
  qr.margin_top = qr.margin_left = -5
  qr.div_height = qr.div_width = 95

  qr.message = "https://youtu.be/zt2uIhAvQZ8"
end

Components::FullScreenImage.new(
  fullscreen: true,
  path: Inkblot.vendor_path('chris_kim.bmp')
)

Components::FullScreenImage.new do |fsi|
  fsi.fullscreen = true
  fsi.url = 'https://live.staticflickr.com/'
  fsi.url << '2753/4177140189_f5fd431b26_o_d.jpg'
end

つっつきボイス:「こちらはIoT系のgemで、Waveshareは電子ペーパーデバイスのメーカーだそうです」「お、こういうの好きそうですよね?」「はい触ったことあります、電子ペーパーにArduinoが乗ったような感じのデバイスです❤️」


同サイトより

参考: 2.7inch e-Paper HAT - Waveshare Wiki

「端子にこうやって電圧を印加するといろいろ表示できたりしますよね↓」「このデバイスではそこまでプリミティブに制御したことはないかな〜」「ステッピングモーターぐらいシンプルならいいけど、こんなに複雑なのは自分ではやりたくない😆」「やらざるを得なかったことならありました😅」(以下延々)


waveshare.comより

github-ds: Active Record接続上でSQLを扱うRubyライブラリ集(Ruby Weeklyより)


つっつきボイス:「GitHubが出しているライブラリだそうです」「★500個付いてますね」

# 同リポジトリより
require "pp"

# Create new instance using ActiveRecord's default connection.
kv = GitHub::KV.new { ActiveRecord::Base.connection }

# Get a key.
pp kv.get("foo")
#<GitHub::Result:0x3fd88cd3ea9c value: nil>

# Set a key.
kv.set("foo", "bar")
# nil

# Get the key again.
pp kv.get("foo")
#<GitHub::Result:0x3fe810d06e4c value: "bar">

# Get multiple keys at once.
pp kv.mget(["foo", "bar"])
#<GitHub::Result:0x3fccccd1b57c value: ["bar", nil]>

「GitHub的に欲しいものなのかな?」「RedisインスタンスやElastiCacheなんかをわざわざ立てずにKV(キーバリュー)みたいなものをちょこっと使いたいというのは何となくワカル」「ElastiCache高いですし💵」「それにこういう形で最初に作っておけば、後でmemcachedとかに移行するときにも楽でしょうし😋」「永続化とかも考えると結局RDBでやるのが楽だったりしますよね😋」「ActiveRecordの接続があればとりあえずそういうのを作れると」

参考: Amazon ElastiCache(インメモリキャッシングシステム)| AWS

「この辺のSQL操作↓もActiveRecord::Baseconnection.executeが使いづらいから欲しかったのかも」「ストアドプロシージャっぽい機能もあるみたい」

# Select, insert, update, delete or whatever you need...
GitHub::SQL.results <<-SQL
  SELECT * FROM example_key_values
SQL

GitHub::SQL.run <<-SQL, key: "foo", value: "bar"
  INSERT INTO example_key_values (`key`, `value`)
  VALUES (:key, :value)
SQL

GitHub::SQL.value <<-SQL, key: "foo"
  SELECT value FROM example_key_values WHERE `key` = :key
SQL

# Or slowly build up a query based on conditionals...
sql = GitHub::SQL.new <<-SQL
  SELECT `value` FROM example_key_values
SQL

key = ENV["KEY"]
unless key.nil?
  sql.add <<-SQL, key: key
    WHERE `key` = :key
  SQL
end

limit = ENV["LIMIT"]
unless limit.nil?
  sql.add <<-SQL, limit: limit.to_i
    ORDER BY `key` ASC
    LIMIT :limit
  SQL
end

p sql.results

DB

PostgreSQLのhypothetical aggregates(Postgres Weeklyより)


つっつきボイス:「hypothetical aggregates言いにくい😆」「仮説的集計って言うのかな?」「へぇ〜、array_agg()っていう集約関数があるのね」

「どれどれ、rank(3.5)は、偶数の{10,2,4,6,8}をソートした配列{2,4,6,8,10}なら2と4の間に来るのでrankは2になり、奇数の{9,7,3,1,5}をソートした配列{1,3,5,7,9}なら3と5の間に来るからrankは3になると」「ふむふむ」

# 同記事より
test=# SELECT x % 2 AS grp, array_agg(x), 
              rank(3.5) WITHIN GROUP (ORDER BY x) 
       FROM   generate_series(1, 10) AS x 
       GROUP BY x % 2;
 grp |  array_agg   | rank
-----+--------------+------
   0 | {10,2,4,6,8} | 2
   1 |  {9,7,3,1,5} | 3
(2 rows)

「3.5という値はこの配列の中にはないんだけど、もし配列の中にあったとしたらrankがいくつになるだろうかというのを、配列に入れずに求められるということみたい」「あ〜やっとわかりました」「それがhypothetical aggregatesなんでしょうね」

「データを入れなくても、もし入れたら何位になるかというのがわかると」「データが確定してなくても順位が取れると」「たしかに実際のアプリケーションでそういうのが欲しいときありそう」「記事のユースケースもそういう感じですね」「データを本当に入れてしまうと他のトランザクションに影響するので、こうやってやれるのはいいですね😋」

「hypothetical aggregates、ちょっと名前カッコイイ」「怖そうな名前かな〜」「記事のこの図↓がズバリ内容を表してて秀逸👍」「わかってみるとよくわかる図ですね」「こういう機能があるということを何となくでも知っておくといつか身を助けるかも」「いつになるかはわかりませんが😆」「Rubyの機能だけでやろうとすると件数が多いときにしんどいかも」


同記事より

RDBの「配列」

「ところで、このやり方の場合array型にする必要がありそうな雰囲気ですけど、PostgreSQLなら簡単にarray型にできるのですぐ使えそう👍」「う、MySQL勢だけどぽすぐれの誘惑が〜😆」「PostgreSQLだとサブクエリの結果をarray型にしてINで取ってくるとか普通にできますけど、MySQLのarray型って機能少ないですよね」

「そんなのやれるんですかいいな〜、MySQL一筋なのにぽすぐれの話聞くたびに浮気したくなってきた😂」「まあそれをActive Recordらしく書けたりはしないんですけど、RDB単体の機能としてはそういうことができるということで☺️」

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

銀座Railsスライド「RailsとDocker」


つっつきボイス:「神速さんのスライドだ」

「現状でのRails+Dockerはこういう感じというのがわかって参考になりましたし、開発環境のDockerイメージと本番環境のDockerイメージを同じにすることは諦める、というのも収穫でしたね👍」「やっぱりそうなるのか〜」「開発環境ならちょっとコンテナの中に入って作業できますけど、本番環境は余計なものを何も入れないからほとんど何もできなくなるじゃないですか😢」「本番だとvimすら入ってなかったりしますよね😢」「Alpine Linuxだとpsコマンドすら使えなかったり😭」

「私の翻訳記事↓も取り上げていただいて感激しました😂」

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

RailsとDocker

「ところで、RailsをDocker化する機能もそろそろRails本体に入っていいんじゃないかなってときどき思いますね」「それすごく思います!」「Railsがオールインワンのフレームワークということを考えると、公式のDockerコンテナ化機能があってもよさそうですし」「rails buildで一発ビルドできたらいいな〜😂」「いっそrails sしたらコンテナが立ち上がって欲しい😆」「😆」

「まあRails環境構築の必要条件にDockerが加わったらそれはそれで面倒そうですけど、rails buildみたいなコマンドでDockerイメージ作れるような機能はそのうちできてもいいと思いますよね」

その他クラウド


つっつきボイス:「こういう機能があったらいいなという話をしてたらこの発表があったんですよね」「これ見たんですけど今ひとつわかんなくて、結局何なんでしょう?」「Slackのshared channelって今までは2拠点間でしかできなかったんですけど、その制限が緩められて20拠点までできるようになったと↓」「へぇ〜」「昨日ちょうどshared channelを3拠点でできないかなって社内で話しをしてたときだったんですよ」

本日より、1 つの Slack チャンネルを最大 20 件のオーガナイゼーションが共有できるようになりました。
同記事より

参考: Slack コネクト | Slack


「ところでこのプレスリリース、何だか読みにくいですよね」「要約すれば上の抜粋の一文だけで済むのに、長い文章に埋もれてえらく把握しづらかった😢」「前は2拠点までだったけど20拠点に増えましたと冒頭で要約してくれたらずっと読みやすくなるのに」

JavaScript

ECMAScript 2020仕様がリリース(JSer.infoより)


つっつきボイス:「お、ついに正式版の仕様が出ましたね🎉」「まあ内容は以前から出回っていますけど」「ますますJavaScriptから逃げられなくなるのかな…」

「ところでこの仕様ページ、えらく重くありません?🏋🏻‍♀️」「仕様全部乗せしてるからページがめちゃ長いのか!」「この長さはヤバい」「負荷テストに使えそう」

言語/ツール/OS/CPU

三社三様

Microsoft Defender ATP for Linux

つっつきボイス:「Microsoft Defender ATP for Linuxは便利そうですよね👍」「サーバーにセキュリティソフトを入れないといけない案件もこれで対応できそうですし」「サーバーが確実に重くなりますけど😆」

「Defender ATP for Linuxの挙動が気になるな〜、監査ログ出すぐらいならいいんですけど、もし何かあったときに通信遮断されて障害になっても困るので」「何だかSELinux↓思い出しました😆」「そうそう、SELinuxをまず止めるところから始めたりしましたよね😆」

参考: Security-Enhanced Linux - Wikipedia

「Defender ATP for Linuxがどういう形式で配布されるかは知らないんですけど、もしもバイナリ配布がライセンスで許されるなら、CIでDockerコンテナをチェックしたりAmazon Linux Extrasで入れられたりするといいですよね」「たしかにDockerコンテナのセキュリティチェックって大変ですし」「きりがない😆」「サイズ650MBなので常用は無理かな〜」「for serverとかいうライセンスが必要みたいなので再配布は難しそうですね…」「面倒くさそう😆」「じゃいいや😆」

参考: 2020年6月24日 “我々のLinuxジャーニーはまだ始まったばかり”―Microsoft Defender ATP for Linuxが一般提供開始:Linux Daily Topics|gihyo.jp … 技術評論社


Googleのセキュリティスキャナー「Tsunami」

「GoogleのTsunamiはオープンソースなのね」「nmap使ってますね〜」「うっかりAWSにかけて怒られる人出そう😆」

参考: ポートスキャンツール「Nmap」を使ったセキュリティチェック | さくらのナレッジ


ARM版Mac

「ARM版Mac、x64系はRosetta 2で動くとしても、Hypervisor系のVM拡張命令は動かないってリリースにも出ているので、少なくともx64版のWindowsやVirtualBoxとかはHypervisorでは動かないけど、逆にARM版LinuxやARM版WindowsならHypervisorフレームワーク経由で動くんじゃないかって話は出てますね」「おぉ」「そういえばParallelsがARM版Windowsの仮想環境をサポートするというニュースもありました↓」

参考: ParallelsがARMアーキテクチャ搭載Macに仮想環境を提供へ、ARM on ARMのWin環境が現実的か | TechCrunch Japan

「HypervisorフレームワークがちゃんとARMに移植されて動くようになればそのあたりはやれるかなと」「ARM版Linuxは昔からありますし、そうやってARM版が普及してくると、もしかして今は誰も使っていないAWSのARMインスタンス↓が使われるようになるのでは、なんてね😆」「誰も使ってないんですか😆」「まあARMインスタンスは値段もお安くありませんし」

参考: Amazon EC2 A1 インスタンス | AWS

「今後もしかするとARMが普及することでいろいろ広がるという夢が実現するかもしれませんし、逆にARMがコケて終わるかもしれませんけど😆」「どこかで聞いたような話😆」「少なくともIntel一強よりは夢があるかなと思いますね」


以下はつっつき後の記事です。

参考: 【笠原一輝のユビキタス情報局】IntelからArmへのシームレスな移行を実現する「macOS Big Sur」 - PC Watch


ジム・ケラー

「そういえばIntelとかAMDあたりのCPUを設計したCPUアーキテクトは全部同じ人だというツイートをどこかで見かけました」「ジム・ケラーですね↓」

参考: ジム・ケラー - Wikipedia

「ちょっと前にインテル辞めたんでしたっけ?」「インテルの最近のアーキテクチャを作り終わったところで辞めたとか何とか」「DECのAlphaもやってたのか!」「いろいろ優秀すぎて想像つかない😅」「ひとりで世の中を変えるパワーのあるエンジニア、いますよね」「まさしく偉人」

以下はつっつき後に見つけたツイートです。


番外: 音

「ある意味こっちの方がうれしいかも↓」「ジャーン」「ジャーン」「2016年まで音あったんですね」「MacbookをSMCリセットか何かで完全にリセットしたときに音が出たような覚えあります」

「ところで大昔のSad Macの音って聞いたことある人います?」「あれ音するんでしたっけ?」「すっごく情けない『チャラララ〜』みたいなメロディなんですけど、運がいいとこの音が出ることがありました」「Sad Macアイコンなら嫌になるほど見ましたけど😆」

↓これでした。

参考: 画像で見る初期Macのアイコンデザイン - 6/10 - CNET Japan

その他

BasecampのHEY.comとは

参考: シリコンバレーで話題のプライバシー重視の有料電子メールサービス、「Hey」が目指していること|市川裕康 (メディアコンサルタント)


つっつきボイス:「これは?」「Basecampの作ったHay.comという招待制のメールサービスだそうです」「そういえばFacebookも昔は招待制でしたね」「Mixiも」「プライバシー重視のメールサービスか〜」「Heyというと秋葉原のゲーセンを連想しますけど😆」

参考: 秋葉原ゲーセンの老舗にしてコアなプレイヤーが集うレトロゲームの王国 「Hey」 | ゲーム文化保存研究所

「自分はもうメールってまず開かない😆」「自分も銀行のメールしか来ません😆」「過去にサブスクしたメルマガを今一生懸命解除してます😆」


「お、Basecampは本社オフィスもなくしてフルリモートにするって参考記事に書いてますね」「へぇ〜」「もともと世界各地でリモートで仕事している会社ならやれるでしょうね」「DHHの場合自宅の仕事部屋↓がめっちゃ豪華ですし✨」

番外

今どきの高校の情報科目教科書


つっつきボイス:「はてブで話題になってましたね」「よくできてる教科書との評判」「普通に大学の授業ですね」「こんなにちゃんと学べるのはいいけど、やりたくない人にはつらそうですね」「こういうのは書いてあるとおりに打ち込めば何とかなりますヨ」「ところでPythonって書きやすいのかな?」「Rでもあんまり変わらないと思いますけど😆」

参考: R言語 - Wikipedia

「今ならもっといいのがあるんでしょうけど、自分は論文とかでどういうグラフを作ろうかと思ったとき、RのWiki(Rjpwiki)↓に掲載されているグラフ実例集を随分参考にしましたね」「お〜、こんなのがあるんですね」「グラフの種類って実際のグラフも見ないと選びようがないので、ここは随分重宝しました😋」「いいこと聞いた!」

参考: グラフィックス参考実例集 - RjpWiki
参考: グラフィックス参考実例集:イメージ図 - RjpWiki


okadajp.orgより


後編は以上です。

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

週刊Railsウォッチ(20200623後編)Bootstrap 5 alphaリリース、Lambda FunctionsとEFS、DB設計で気をつけていることほか

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

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

Ruby Weekly

Publickey

publickey_banner_captured

Postgres Weekly

postgres_weekly_banner

JSer.info

jser.info_logo_captured


Rails 5と6のHSTS設定方法

$
0
0

以下の記事の続きです。

Rails: config.force_ssl = true によるHSTSの動作をローカル環境とChromeで試してみた

参考: Rails API ActionDispatch::SSL

⚓Rails 5と6のHSTS設定方法

Rails 5以降では、config/environments/production.rbで以下の設定をコメント解除すると、production環境で強制的にHTTPS通信を試みるようになり、同時にHSTS「も」有効になります。

config.force_ssl = true

production環境のRailsサーバーでHSTS(HTTP Strict Transport Security)が有効になると、サーバーからのHTTPレスポンスに以下のようなStrict-Transport-Securityヘッダーが追加されます。

Strict-Transport-Security: includeSubDomains

ブラウザはStrict-Transport-Securityヘッダーを受け取るとそのサーバーのドメインを記憶し、以後そのドメインにアクセスするときは、たとえhttp://を指定した場合でもHTTPS通信にリダイレクトを試みます。記憶を解除する方法については前回記事の末尾をご覧ください。

⚓デフォルトのHSTS設定

Railsのssl_optionsに何も指定しない場合のデフォルトのHSTS設定は以下のようになります(railties/lib/rails/application/configuration.rb#L94)。デフォルトではincludeSubDomainsが有効になりますので、サブドメインもHSTSの対象になります。

# railties/lib/rails/application/configuration.rb#L94
self.ssl_options = { hsts: { subdomains: true } }

includeSubDomainsを明示的に無効にするには、config/environments/production.rbのconfig.ssl_optionssubdomains: falseを指定します。

config.ssl_options = { hsts: { subdomains: false } }

production環境でRailsのforce_ssl = trueを有効にする前には、HSTSの設定が本番にとって適切かどうかを確認しておきましょう。シチュエーションによってはデフォルトのHSTS設定でも問題が起きるかもしれません。

参考: ネイキッドドメイン+HTTPSで運用するRailsアプリを5.1にアップグレードしたら、サブドメインも強制的にHTTPSになってしまった話 - Qiita

⚓HSTS有効期限の設定追加

config/environments/production.rbのconfig.ssl_options{ expires: 10.days }などと追加することで、HSTSの有効期限をmax-ageで指定できます↓。max-ageの単位は秒ですが、Railsでは10.daysなどと楽に指定できます。

config.ssl_options = { hsts: { expires: 10.days } }

指定の期限を過ぎると、http://でアクセスしたときに元のようにHTTP通信を試みます。

Strict-Transport-Security: max-age=86400

⚓HSTSをオフにする

参考: Strict-Transport-Security - HTTP | MDN

試したわけではありませんが、MDNによると、HTTPS通信時にmax-age=0を指定するとStrict-Transport-Securityヘッダーを失効させることができ、ブラウザが再びHTTP通信を許すようになるそうです。

RailsでHSTSをオフにするには、config.ssl_optionsで単にhsts: falseと指定するか、hsts: { expires: 0 }を指定します。

config.ssl_options = { hsts: false }
# または
config.ssl_options = { hsts: { expires: 0 } }

⚓preload設定

production環境でのHSTS preload設定は、非暗号化通信を排除しなければならないような特殊な案件で使うことがあります。Railsではデフォルトでpreload: falseです。

preloadしないHSTSでは、暗号化されていないHTTPでサイトにアクセスするとHTTPSへの301リダイレクトが発生する可能性が残りますが、hstspreload.orgのpreloadリストに登録するとブラウザは最初からHTTPSのみで通信するようになり、HTTPからHTTPSへの301リダイレクトを排除できるようになります。

ただしhstspreload.orgのpreloadリスト登録は後からの取り消しが難しいので慎重に行いましょう。

hstspreload.orgのHSTS preloadリストに登録するには、少なくともサイト全体をもれなくHTTPSのみでアクセス可能にしておく必要があります(暗号化されていないHTTPアクセスのページが残っていると、そのページにアクセスできなくなってしまいます)。

hstspreload.orgへのドメイン名登録ではサブドメインも対象に含まれる(正確にはincludeSubDomains指定を省略できない)ので、暗号化されていないHTTPページがサブドメインにあると、やはりアクセスできなくなってしまいます。

失敗に備えて、最初はmax-ageを1日(86400)程度にして試すのが無難なのかなと思います。なお現在はmax-ageを少なくとも1年間にしなければならないとhstspreload.orgで警告されます↓。

The max-age must be at least 31536000 seconds (≈ 1 year), but the header currently only has max-age=2592000.


プリロードについてはまだ怖くて試せていないので、Railsでの設定方法のみをメモします。

config/environments/production.rbのconfig.ssl_optionspreload: trueを指定すると、以後アクセスするブラウザは、プリロードリストに従って強制的に最初からHTTPSでそのドメイン(とサブドメイン)にアクセスするようになり、設定は1年間持続します。

config.ssl_options = { hsts: { max-age: 31536000, subdomains: true, preload: true } }

参考: 3分で出来るHSTSプリロードの設定方法 – 常時SSL化後に必ず行うべき設定
参考: HTTP Strict Transport Security - Qiita

週刊Railsウォッチ(20200706前編)Railsでのマルチテナンシー実装戦略を比較、Railsでサブクエリを使う、URI.parserが非推奨化ほか

$
0
0

こんにちは、hachi8833です。RubyKaigiのチケット代返金処理が始まったそうです。


つっつきボイス:「RubyKaigiチケット代返金は参加者向けのお知らせということでしょうね」「チケットの返金始まったんですね!こないだDoorkeeperから届いたメールにRubyKaigiにようこそみたいなことが書いてあってもう返金無理かと思ってましたけど」「あ、紛らわしいメールが飛んで失礼しましたというのが2通目のツイートです↓」「なるほど😆」「完全に理解しました😆

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。先週のコミット数は珍しく少なめですね。

⚓URI.parserが非推奨化

今後はRubyのURI::DEFAULT_PARSERを使うようにとのことです。よく見るとShopifyのプルリクでした。

# actionview/lib/action_view/helpers/url_helper.rb#L542
      def current_page?(options, check_parameters: false)
        unless request
          raise "You cannot use helpers that need to determine the current " \
                "page unless your view context provides a Request object " \
                "in a #request method"
        end
        return false unless request.get? || request.head?

        check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
-       url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
+       url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)

        # We ignore any extra parameters in the request_uri if the
        # submitted URL doesn't have any either. This lets the function
        # work with things like ?order=asc
        # the behaviour can be disabled with check_parameters: true
        request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
-       request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
+       request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)

        if url_string.start_with?("/") && url_string != "/"
          url_string.chomp!("/")
          request_uri.chomp!("/")
        end
        if %r{^\w+://}.match?(url_string)
          url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
        else
          url_string == request_uri
        end
      end

つっつきボイス:「URI.parserが非推奨化?」「やべ、最近どっかで使っちゃったかも😅」「非推奨になったURI.parserはRailsのActive Supportの機能で、URI::DEFAULT_PARSERはRuby自身の機能だそうです」「なるほど〜」

これまでRegexpの重複を調べてきて、URI::Parserにはものすごい量の重複があることに気がついた。もう少し追ってみたところ、Active Supportが不要な場合であっても第2のパーサーをインスタンス化していたのが原因だった。
DEFAULT_PARSERは12年も前に追加されていたこともわかった。
URI.parserの有用性にも疑問があるが、Railsで使われている場所はほんのひと握りなので、完全に削除できるだろう。ドキュメントはないが:nodoc:も付いてないので、これがpublic APIかどうかはわからない。
@rafaelfranca
同PRより大意

「オリジナルのURI.parserは使ったことないので、使いたいニーズがどのぐらいあるのかはわかりませんけど」「上のコミットメッセージには、ほぼないだろうとありますね」「とは言えゼロではないでしょうから非推奨化して消さないとですね」「ワイ、こないだ書いたコードを見直さないと…使ってなかったよかった〜😂」「😆


「プルリクコメントにこんなの書いてありました↓」「なるほど、Ruby 1.8と1.9の頃の話だったのね」

URI.parserは、URI::ParserがまだなかったRuby 1.8とRuby 1.9との間の互換性のためにに導入されたらしい。URI.unescapeは既に5d773f8, 2f326b7, 197a995で非推奨化されている。URI.parserの非推奨化に一票。
URI.parserの振る舞いに関するドキュメントはないが、APIドキュメントには載っているのでpublic APIということになる。
同PRコメントより

⚓Ruby 1.8の頃

「まあ1.8を知るエンジニアも随分減ったかもしれませんけど」「どんな時代…?」「ほら、ハッシュの順序が維持されてなかった時代ですよ↓」「1.8やってました〜」「Ruby Enterprise Editionとかあった時代」「REEってありましたね」「2.0になってレビューで古い書き方にツッコまれまくったのを覚えてます」

参考: 要素の追加順序を保持するHashクラス (#1273692) | Ruby 1.9.0 リリース | スラド

Re: (スコア:0) by Anonymous Coward Hashが順序を保持。についてもう少し知りたい。キーの順序を保持?連想配列の実装が二分木になったとか、そういう話ですか?

Re: (スコア:0) by Anonymous Coward キーが常時ソートされた状態で保持されるという意味ではなく、each(など)で列挙すると追加した順序で出てくるという意味です。
srad.jpより

参考: Ruby Enterpriseエディションが終わる。Phusionは、Passengerに注力。 — 2012年の記事です

⚓可能な場合はLoadError#original_messageを使うようになった

こちらもShopifyのプルリクです。

LoadError#messageはRuby 2.8/3.0でDidYouMeanによって拡張されていて、$LOAD_PATHにあるものをかなりいい感じに使って訂正サジェスチョンを表示してくれる。
このおかげで、特に$LOAD_PATHの量が多い場合にメッセージへのアクセスがかなり拡張可能になる。
NameError#messageでは既に同じ問題を扱っているので、それと同じアプローチを取ることにした。
同PRより大意


つっつきボイス:「DidYouMeanをsafe_constantizeでも効かせるようにしたということですね」

# activesupport/lib/active_support/inflector/methods.rb#L329
    def safe_constantize(camel_cased_word)
      constantize(camel_cased_word)
    rescue NameError => e
      raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
        e.name.to_s == camel_cased_word.to_s)
    rescue ArgumentError => e
      raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
    rescue LoadError => e
-     raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message)
+     message = e.respond_to?(:original_message) ? e.original_message : e.message
+     raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(message)
    end

「DidYouMeanって何でしたっけ?」「ほら、『本当はこれじゃないの?』みたいなエラーメッセージですよ」「『それはタイポでは?』的なヤツ」「あ〜あれですか!何気に助かるんですよね」「『どうしてわかった?』みたいに図星だったりしますよね」「いや〜今回もどれだけ助けられたことか😂」「Rubyに後ろから見られてるような気持ちになります😆

「Rubyはこういうところがプログラマーに優しいですよね」「いちいちAPIドキュメントをひっくり返したりしなくてもわかりますし」「そういう部分がMatzが言うところの『楽しくプログラミングできる』というヤツなのかなと思いますね」「jnchitoさんが『Rubyの書き味』を引用してたのもそのあたりかも」「他の言語だとスタックトレース追ったりしないといけなくなったりしますし」「DidYouMeanでツッコまれたら取りあえず言われたとおりに変えて試すという投機的実行ができるのはいいですよね」

後で掘り起こしました↓。

⚓Marshal.load(legacy_record.dump)がMySQLで動くための後方互換性

実際にはMarshalの互換性をRailsバージョン間で維持すると明言したことは一度もないので、これまでもそれ用の型を直接削除したことがある(f1a0fa9や#29666など)が、直接削除するとキャッシュのローテーションが難しくなる。
未使用の定数を新しいバージョンで維持すれば、少なくとも1つのバージョンが続く間はキャッシュのローテーションはやりやすくなる。
同PRより大意


つっつきボイス:「なるほど、Railsのリリースバージョン間でのMarshal.loadを問題にしているということか!」「RubyのMarshalはバージョン間での互換性は保たれないものですけど、Railsでのバージョン間互換性と言われるとたしかにと思いますね」「キャッシュローテーションはたしかにMarshalでやる方が軽そう🎈

# activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L847
+       # MysqlStringエイリアスでMashal.load(File.read("legacy_record.dump"))が効くようにする
+       # TODO: Rails 6.1がリリースされたらこの定数エイリアスを削除する
+       MysqlString = Type::String # :nodoc:
+

「まあ自分はそもそもMarshalの互換性は当てにしませんけどね: 一般にバージョンが変わってもロードを維持しないといけないものならMarshalではなくJSONとかを使うべきだと思いますし」「たしかに」「MarshalはRubyのバージョンが変われば互換性が失われるものだから、そういうのを永続化的に使いたくはないですね」

⚓番外: tags_textのキャッシュ化を取り消し


つっつきボイス:「このタグはログで使うヤツみたいですね」「元のコミット↓ではメモ化||=でキャッシュしてたのを、その後のコミットで取り消したのね」

# activesupport/lib/active_support/tagged_logging.rb#L56
      def tags_text
-       tags = current_tags
-       if tags.one?
-         "[#{tags[0]}] "
-       elsif tags.any?
-         tags.collect { |tag| "[#{tag}] " }.join
+       @tags_text ||= begin
+         tags = current_tags
+         if tags.one?
+           "[#{tags[0]}] "
+         elsif tags.any?
+           tags.collect { |tag| "[#{tag}] " }.join
+         end
        end
      end

このコミットは05060ddを取り消す。
タグはファイバーごとにあるので、インスタンス変数内ではキャッシュされない。
同コミットより大意

⚓Rails

⚓提案: ハッシュ記法のショートハンド(Ruby on Rails Discussionsより)

以下のように最近のJavaScript的に書きたいという提案です。肯定否定さまざまな意見が出ています。

{topics, users, very_longly_named_objects}
# ↓
{
  topics: topics,
  users: users,
  very_longly_named_objects: very_longly_named_objects,
  even_more_very_longly_named_objects: even_more_very_longly_named_objects,
}

つっつきボイス:「このスレが割と盛り上がってるので」「あ〜、JS風のこの書き方が欲しいというのワカル!」「ローカル変数名とキー名を一致するように書くことはよくあるので、だったらこう書けるといいよねと」「特にeven_more_very_longly_named_objectsみたいに長大になるとエディタで改行して見づらくなりますし」「そうそう!」「Rubyには入れられなくても、Active Supportあたりに追加できないかな?」「ローカル変数名を取得できるならやれそうな気もしますね」「欲しい人が多いのもわかります」

後でRubyの#15236(rejected)↓を見てみました。Matzの講評を抜粋してみます。

(別のスレより)この構文についてはほとんど肯定的な気持ちになれない。理由はset構文や昔のRuby 1.8のハッシュスタイルっぽく見えるため。将来ES6の構文が普及したらこの変更を入れるチャンスはあるだろう。

JavaScriptをまったく使っていないコンサバなシニアとしては、この構文についてまだネガティブな気持ちがある。現在のRuby構文ではほぼ不可能なデストラクチャリング(代入の左側)なら最も相性がよさそう。
もちろんRubyユーザーの多くがRailsとJavaScriptを同時に使っていることは認識しているので、皆さんの意見はオープンに受け止めます。
同issueより大意


「そういえばコメントにI’m greenly jealous of JavaScriptという言い方があったんですけど、greenってたしか英語圏だと嫉妬に通じる色なんですよ」「へぇ〜」「日本語の『真っ赤な嘘』的に色の名前に含みがあるというか」

参考: ブルーは憂鬱、グリーンは嫉妬…色にまつわる英語表現(活かす!イングリッシュ Vol.12)|すぐに役立つ英会話・英語レッスン|現地情報誌ライトハウス

greenは、他にも人間を形容すると「青二才」のようなニュアンスも含むことがありますね(たぶん「初々しい」と表裏一体な感じで)。

⚓Railsでサブクエリを使う(Ruby Weeklyより)


つっつきボイス:「PostgreSQLが前提のようです」「またぽすぐれか〜😅

「このselect('avg(salary)').to_sqlみたいな書き方は自分もやったことある↓ to_sqlすれば普通にサブクエリにできるので」「ふむふむ」

# 同記事より
avg_sql = Employee.select('avg(salary)').to_sql

Employee.select(
  '*',
  "(#{avg_sql}) avg_salary",
  "salary - (#{avg_sql}) avg_difference"
)

「なるほど、FROMでサブクエリしたい場合↓」

# 同記事より
from_sql =
  PerformanceReview.select(:reviewer_id, 'avg(score) avg_score').group(
    :reviewer_id
  ).to_sql

PerformanceReview.select('avg(avg_score) reviewer_avg').from(
  "(#{from_sql}) as reviewer_avgs"
).take.reviewer_avg

「そしてHAVINGでサブクエリしたい場合↓」

# 同記事より
avg_sql = PerformanceReview.select('avg(score)').to_sql

Employee.joins(:employee_reviews).select(
  'employees.*',
  'avg(score) avg_score',
  "(#{avg_sql}) company_avg"
).group('employees.id').having("avg(score) < 0.75 * (#{avg_sql})")

「サブクエリを使う方が適切なケースは普通にありますね」「もしかすると、コンセンサスの取れる形で生SQLを書けるインターフェイスがActive Recordに公式に入ればそれで解決するのかなという気がしてきた」「そうかも」「言い換えるとArelで頑張るには限界があるということで」「あ〜」「上みたいな書き方をやっていくと今度はWITH句も使いたくなるだろうし😆」「今ならMySQLでもPostgreSQLでもWITH使えますよね」


記事見出しより:

  • RailsのActive Recordを使うということは
  • Railsにおけるサブクエリとは
  • 私たちのデータの概要
  • WHEREにサブクエリを書く
    • WHERE NOT EXISTS
  • SELECTにサブクエリを書く
  • FROMにサブクエリを書く
  • HAVINGにサブクエリを書く
  • まとめ

⚓Railsでのマルチテナンシー実装戦略を比較


つっつきボイス:「Railsで複数のテナントの扱いをどう実装するかという戦略の話なのかな」「中身読まないうちに推測すると、rowレベルは複数顧客のデータを同じテーブルに入れるし、dbレベルはデータベースを顧客ごとに分けるというヤツで、スキーマレベルは顧客ごとに別のテーブルを作るんでしょう、きっと」「たぶんそれっぽいこと書いてる気がします」「マルチテナンシーで思いつくのが取りあえずその3つなので😆」「スキーマレベルはcreate schemaとcreate tableって書いてるのでそうだと思います」

  • row(行)レベル
  • スキーマレベル
  • dbレベル

「記事にこんな感じで表が載っています↓」


同記事より

「実際マルチテナンシーをどう実装するかって悩ましいんですよ: たとえばrowレベルにはrowレベルのつらさがありますし」「見えちゃいけないものが見えてしまうとか?」「それは実装がダメすぎ😆」「rowレベルで問題になりやすいのは、パフォーマンスが落ちる問題と、テーブルがバカでかくなったときにどうするかというスケーリングの問題: 1個のテーブルが極限までデカくなってしまうとまともにメンテできなくなる可能性もあるので」「なるほど」

「テナントをdbレベルで分けてあれば、あるデータベースで問題が起きても他のテナントが死なずに済むというメリットが得られます: たとえばテナントのほとんどは小規模だけど、一部のテナントはものすごく激しく使うような案件なら、dbレベルだと障害範囲を限定できるのがいいんですよ」「ふむふむ」「その代わりインフラをメンテナンスするコストが跳ね上がるのがしんどいですけど😭

「スキーマレベルはまず使わないのが普通なので、これを検討することはほぼないかな」「自分の経験でもrowレベルかdbレベルのどっちかですね」「スキーマレベルでやっていてテナントが数千件とかになったら、テーブルが数千件できるということですよね…」「スキーマレベルはないわ〜」「/dtしてテーブルがどひゃ〜っと表示されたら死にたくなりそう」「イヤ〜😭」「表には『Extract a single tenant’s data = Easy』とか書いてあるけど、果たしてそうかな〜😅?」

「あとRails固有の問題なんですけど、rowレベルでやるとマイグレーションが激重になりがちなのが深刻なんですよ: 1個のテーブルに全テナントのデータが入るのでrails db migrateの遅さが半端なくなる」「あ〜、そうですよね」「dbレベルなら最悪でもテナントごとにマイグレーションを実行できるんですけどね」「rowレベルだと、stagingでは問題なかったのに本番でマイグレーションがなかなか終わらないということが起きがちなので、staging環境のデータベースには十分な量のダミーデータを入れておきたいですね」「たしかに!」「でないとコワすぎるので」

「エンタープライズなアプリのマルチテナンシー周りには気をつけたい」「やべマルチテナントやったことあるわ〜rowレベルだったわ〜😆」「😆」「まあ最近のMySQLやPostgreSQLはデータベースをロックしないでマイグレーションする手順も確立されているので、気をつければ大丈夫ですよ」「つまり気をつけないと死ぬってことですよね🤣」「🤣

「ただ、どの方法を選ぼうとユーザー数やデータ量が増えれば気にしなければいけないのは一緒なので、この戦略を選びさえすれば楽になるというのはないと思ってます」「そうですよね」

⚓提案: schema.rb生成中にテーブル名やカラム名などをソートする機能(Ruby on Rails Discussionsより)


つっつきボイス:「なるほど、schema.rbの項目ソートか」「ただ既存のカラム名を無断でソートするのはやめて欲しい: データベース内部の物理配置に影響するので、勝手にソートされると意味が変わっちゃう」「あ、それもそうか!」

後で見ると、提案した方はfix-db-schema-conflicts gemの作者で、RubocopによるオートコレクトとぶつからないためにこのgemのソートロジックをRailsのスキーマ生成に入れませんか(カラムの並び順に依存するアプリ用に並び順を維持するオプションも付けて)という提案でした。structure.sqlは変更されないそうです。

「テーブル名のソートはいいと思います😋」「テーブル名なら全然構わない」「ところで今自分のRailsアプリ見るとテーブル名はアルファベット順になってますね」「あ、そうなんですか?」「外部キーのソートはRDBMSに依存しそうな気がする🤔」「結局はCREATE TABLE文の中に書かれているものの順序が、データベース内部のデータ構造に影響するかどうかがポイントでしょうね」


前編は以上です。

おたより発掘

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

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

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly

週刊Railsウォッチ(20200707後編)Rubyで無名structリテラル提案、書籍『AWS認定ソリューションアーキテクト』、21世紀のC言語ほか

$
0
0

こんにちは、hachi8833です。七夕なのに悪天候…

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Ruby

⚓提案: 無名structリテラル

# 同issueより
${a: 1, b: 2}
# 上は以下とほぼ同じ
Struct.new(:a, :b).new(1, 2)
# 同issueより
s1 = ${a: 1, b: 2, c: 3}
s2 = ${a: 1, b: 2, c: 3}
assert s1 == s2

s3 = ${a: 1, c: 3, b: 2}
s4 = ${d: 4}

assert_equal false, s1 == s3
assert_equal false, s1 == s4

つっつきボイス:「Rubyに無名structリテラルがあってもいいですよね」「むしろ今までなかったのが不思議かも」「C言語ならこういうのをよくstructマクロで実現したりしますよね: Rubyはマクロがないからこういうふうに構文で実現する必要がありますけど」「なるほど」

「進行中なのでこれからでしょうね」「ついに$マークを使うのかな?」「あとStructOpenStructのあり方にどう影響するかも気になる」

「あとは今後structリテラル#{}が今のRubyのハッシュリテラル{}のようにどんどん使われるようになるかどうかでしょうね: リテラルが用意されるとみんな一気に使うようになる傾向があると思うんですよ」

「たしかに、Rubyでハッシュがつい乱用されがちなのは、Rubyのハッシュリテラルが書きやすすぎるからなのかもと思うときがあります」「それそれ、そんな感じでstructリテラル#{}が導入されたら今よりもStructが使われるかもしれませんね」

「そうなると今度はハッシュとstructリテラルをうまく使い分けられなくてわけわからないコードになる可能性もあるかも」「ふむふむ」「RubyでStructを使う人はたいていの場合あえて意識的に使っていますし」「特に何も考えないとついハッシュを使っちゃったりしますね😅

追記(2020/07/07)

記事公開後に以下のツイートにやっと気づきました。

⚓Rubyのエンコーディングエラー(Ruby Weeklyより)


つっつきボイス:「文字エンコーディングはたまにしくじることありますね」「この辺は日本人の方が知見たまってるかも🇯🇵

「エンコーディングのCompatibilityErrorは昔ちょくちょく踏んだけど最近あまり見なくなったかな〜」

# 同記事より: CompatibilityError
string_in_utf8 = "Löve"
string_in_ascii = "Löve".force_encoding('US-ASCII')
string_in_utf8.start_with?(string_in_ascii)
...
# Encoding::CompatibilityError (incompatible character encodings: UTF-8 and US-ASCII)

# == で比較するとraiseされずにfalseが返る
string_in_utf8 = "Löve"
string_in_ascii = "Löve".force_encoding('US-ASCII')
string_in_utf8 == string_in_ascii
=> false

「まあ今でもUTF-8とcp932を行き来するときなんかはエンコーディングエラーに気をつけないといけませんけど」「ですね」

参考: Microsoftコードページ932 - Wikipedia


同記事見出しより:

  • エンコーディングをざっくり知る
  • エンコーディングエラーとは
  • Encoding::ConverterNotFoundErrorがいつ起きるかと修正方法
  • Encoding::CompatibilityErrorがいつ起きるかと修正方法
  • Encoding::UndefinedConversionErrorがいつ起きるかと修正方法
  • Encoding::InvalidByteSequenceErrorがいつ起きるかと修正方法
  • エンコーディング問題のもう少し複雑な解決方法
  • その他の問題

⚓Ruby実装ごとの互換性レポート(Ruby Weeklyより)


つっつきボイス:「こんな表が掲載されてます↓」


同記事より

「CRubyの100パーセントおめでとうございます〜🎉」「当然です😆

「TruffleRubyのスコアが高いですね」「JRubyより高いのが意外だけど、考えてみたらコマンドラインの部分はJRubyが低くても仕方なさそう」


同リポジトリより


同サイトより

「Opalまである😆」「オパールって何でしたっけ?」「RubyからJSへのトランスパイラですね」

「Artichokeは?」「う、自分で記事書いておいて思い出せない…😅」「ははぁRustで書かれたRubyですか↓」

Artichoke RubyをUbuntu上でビルドしてみた

「そういえばMiniRubyっていうHaskell実装もありますけど↓、さすがにMRIコンパチではなさそうでしたね」「MiniRubyはRubyライクですって」

「ライクって便利な言葉😋」「まあ本気でライクを超えようとしたらCRubyの巨大なparse.yあたりを再現しないといけなくなるので、新しくRubyと同じものを作ろうとすると大変でしょうけど」

参考: parse.y の歩き方 - ワシの Ruby は 4 式まであるぞ -

「CRubyのコードにgccに依存する部分があったりすると別実装で対応が難しいかな?」「たしかに」「メモリ構造まで一致させないと動かないような部分もあるかもしれないので、互換性を100%にするのはかなり難しいか、もしかすると無理な可能性もあるかも🤔

参考: GNUコンパイラコレクション - Wikipedia

「でもこの記事を見た感じでは思ったより互換性高い🎉」「スゴいですね🎉

⚓AWS SDK for Ruby V2のメンテナンスモード移行&サポート終了のお知らせ


つっつきボイス:「SDK for Ruby V2も11月に終るのか〜」「SDKのV2まだ使ってる人いるのかな?」「AWS懐かし〜(今GCPの人なので)、以前V2で作りました」

「今回のSDKのV2->V3移行は、API互換さえ保たれていれば問題なく動くでしょうね: サーバー側でdeprecateされたらダメですけど」「ふむふむ」

⚓クラウドSDKのサードパーティgem依存はつらい

「AWS SDK for Rubyってあまり使ったことないんですけど、GCPにもRubyで似たようなことをやれるヤツがあって、たしかそっちは結構破壊的にインターフェイスが変わったりすることがありました😭」「マジで?」

参考: API と Ruby ライブラリ  |  Google Cloud

「しかもGCPは内部でfaraday gemに依存しているものがあったりして結構困りました」「サードパーティgemが入ってくるんですか!😳」「そこは切り離して欲しい〜😭」「GCPでちょっとRubyを使いたいだけだったのに内部で古いfaradayがロックされてて、その古いfaradayで動くように他のバージョンを下げないといけなくなったりしてつらかった…」

「なのでこの種のAWS SDKとかGCPのとかは、なるべく外部gemに依存しないように作って欲しいです!」「ほんとそうですよね…」「仮にパフォーマンスのためにサードパーティgemが必要だとしても、せめてサードパーティgemなしでも構築する道を用意して欲しいところです🙏

⚓その他Ruby


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

⚓書籍『AWS認定ソリューションアーキテクト-プロフェッショナル』


つっつきボイス:「この本早速電子書籍で買いましたよ」「きっと買うだろうなと思いました😋

参考: 【書評】ついにでた!日本初のAWS認定試験プロフェッショナルレベル対応の書籍は、充実した模擬問題と解説を使って学習できます! | Developers.IO

「そういえば自分の取ったAWSソリューションアーキテクト、そろそろ更新の時期だったかな…?」「おぉ、お持ちなんですね」「この資格が日本に上陸した一番最初に取って、たしか2回更新したから5〜6年前かな〜」「いいな〜」「期限は一応2年間ですが、去年3年間に延長されたんだったかな↓」「最近GCPなのでAWSを1年半触ってませ〜ん😅」「今の自分の資格がどうなってるか確認してみよっと(しばらくダッシュボードを操作)」

参考: AWS 認定ソリューションアーキテクト- プロフェッショナル

⚓プロフェッショナル試験とアソシエイト試験の違い

「ちなみにAWSのプロフェッショナル認定は結構難しいです」「どんなのが出るんですか?」「一応サンプル問題のPDF↓は公開されているんですけど、古い問題がずっと更新されないままなんですよ」「ありゃ😆

参考: PDF AWS-Certified-Solutions-Architect-Professional_Sample-Questions.pdf

「プロフェッショナル認定試験の難しいところは、文章を読み取る力がかなり要求されるところだと思います: 通常のアソシエイトだとそんなに問題文は長くないんですけど、プロフェッショナル試験は問題文も回答選択肢も長いので」「たしかにこれは長いですね…」「長い文章を読んで適切な選択肢を選ばないと合格できないのがプロフェッショナル」

「自分が取得したアソシエイトレベルの試験は『こういう状態のとき、この部分はどうなるか?』みたいな短めの4択問題だったりしますけど、プロフェッショナルは『こういう前提でこうしようとしているけど、その理由はなぜなのか?』とか『開発者に渡すべきポリシーは次のどれか?』みたいな感じでもっと細かい」「なるほど!」

⚓書籍を買ったらすぐ受験すべき

「この本買っちゃおうかな〜?」「興味があるならもちろんどうぞ: ただこの種の書籍はあっという間に陳腐化するので、買ったらすぐに受けるべきです」「あ〜そうでしたか!😳」「AWSの試験問題はどんどん更新されていきますし、AWSには自分も知らないような新しいサービスが続々入ってきたりサービスの内容が変わったりするので、正直昔の本は受験の役に立ちません」「そうなんですね…」

「たとえば最近だとS3のリザーブドインスタンス(RI)周りが大きく変わりましたよね: スタンダードRIの他にコンバーティブルRIというものも登場しましたし、しかも最近はスタンダードRIもインスタンスタイプを変更できるようになったんですよ」「おぉ〜!」「やべ〜AWS知らないおじさんになってしまった😅」「AWSのサービス内容は結構移り変わりが激しいので」

参考: リザーブドインスタンス(RI)- Amazon EC2 | AWS

「なのでこの種の本は出たら即買いして、そして期間を置かずに受験しないとすぐ陳腐化します」「な〜るほど!」「バカ売れする本でもないので改定される可能性はほとんどないでしょうし」「ありそう😆」「日本語で勉強したいなら即買い即受験をおすすめします」

「それにAWSの認定試験は実務を理解してないと合格が難しいですし、扱う実務の範囲も広いので知らないサービスがよく出てきてその分難しいです」「う〜む」

⚓ドメインレジストラ7社比較レビュー

つっつきボイス:「この前お名前.comからGoogleのドメインへの移行作業やりましたけどお名前.com使いづらかった😢」「お名前.comは合併や買収を繰り返しているので、サービスによってDNSの管理主体も機能も画面も違う場合がありますね」「ありゃ😅」(以下延々)

「ちなみに自分はどのドメインレジストラの場合もたいていRoute 53に向けちゃいます」「ふむふむ」「レジストラとDNSサービスは別物なので、まあこういう記事を参考に自分の好きなところを使えばいいと思います☺

参考: Amazon Route 53(スケーラブルなドメインネームシステム (DNS))| AWS

「あと最近は防弾ドメインの評判がよくないせいかWHOIS代行をやるところが減ってますけど、お名前.comは一応今もやっていますね」「へ〜」

参考: 海賊版サイト問題の解決を阻む「防弾ホスティング」 その歴史から現在までを読み解く (3/4) - ITmedia NEWS

⚓その他インフラ

⚓言語/ツール/OS/CPU

⚓PHP 8は今年後期に公開予定

union型とattributesが気になりました。


つっつきボイス:「PHPって7が超新しいみたいな印象なのに8が出るのか〜」「PHPはマイナーバージョンでも仕様が大きく変わりますよね」「もう別物ですよね」

「へ〜、PHP 8にJITコンパイラが入るのね」「JITの前はどうやってたんでしょう?」「PHPは5系までしか知りませんが高速化は昔からやってますね: PHP 5あたりからZend Engineのようなものが入ってますし」「なるほど」

参考: 【PHP】PHP と Zend Engine のバージョン - Qiita

「昔はPHPをApacheモジュールで動かすことが多かったけど、最近だとRailsっぽくPHPサーバーで動かすのが多いのかなという印象ありますね」「自分も最近のはわからないけど後ろにApacheモジュールを置いてZendにNginxを置くとかやってました」

参考: モジュール一覧 - Apache HTTP サーバ バージョン 2.4

「PHPサーバーってFPMとかですよね↓」「そういえばDocker HubにもPHPのFPMイメージがあった気がする」「FPM知らなかった😅」「まあRubyで言うWebrickのようなものと思っておけばいいでしょう」

参考: PHP: FastCGI Process Manager (FPM) - Manual
参考: WEBrick - Wikipedia

「自分はApacheモジュールでPHPを使ってた時期が長かったので、FPMで挙動が変わると怖そうで手出ししてませんが」「わかります」「特にサーバー環境変数はApacheを間に挟むかどうかで変わりそうですし」「自分もまだ勇気出ない😆」「まあFPMは既にすごく使われてるので、新しいプロジェクトならFPM版のPHPでいいんじゃないかと思います: 想像ですけど構造的にもFPM版の方がApacheモジュールよりもアプリケーションリソースを上手に使ってくれそうな雰囲気ですし」「FPMのページ見てるけどめっちゃ細かく設定できるみたい👀

参考: php - Docker Hub

⚓Dependency Injection

つっつきボイス:「『生成知識をファクトリーで隠蔽しよう』のあたりから何となくわかる気もする」「こういうのってDI(依存性の注入)って言うんだ知らなかった〜(やってたけど)😅」「いわゆるコンポジット系のパターンで、インターフェイス経由で叩こうみたいなヤツですね」

参考: 依存性の注入 - Wikipedia

「DIという概念自体は昔からあってJava方面で使われてたりしたんですけど、DIを当たり前に使おうみたいに全面的にフィーチャーしてたのはたしかC#勢だったかな〜」「へぇ〜」「あくまで印象ですけど、.NET系が割とDIを多用したことでDIが世の中に広まったのかなと」

参考: ASP.NET Core での依存関係の挿入 | Microsoft Docs

「昔はDIにするとガンガン差し替えられるぞみたいな感じで流行ったところがありますけど、最近はどちらかというとテストしやすくするためにDIを使うことが多いと思うので、昔のように同じインターフェイスを差し替えまくることってそんなにないんじゃないかなという気はしますね」「そうかも」「まあファクトリーまで作り始めると散らばりやすくなるでしょうね」

⚓21世紀のC言語

つっつきボイス:「21世紀にC言語?!」「まあCRubyがC言語で書かれてますし」「そういうことか」「C++ではないんですね?」「C++はもう全然別の言語といってもいいぐらいでしょう」「それもそうですね😆

参考: C++ - Wikipedia

「この勉強会の参加者数の上限が32767人というところにニヤリとしてしまいます😆」「32ビットsigned intの最大値🤣」「あ〜そういうことか🤣」「unsingnedではないと🤣

参考: signed と unsigned の違い | C言語 | プログラミング│C│シンメトリック公式BLOG

⚓C言語よもやま

「C言語は読めて損はないと思います: がっつり読むまでいかなくても、ヘッダーとか構造体のあたりを読む必要にかられることはたまにありますし」「C言語、昔挫折したんですよね〜: 何かいい本ないかしら?」「自分はLinuxカーネル周りを読んだりデバイスドライバをちょっといじるためにCを覚えましたね: OSのプロセスやスレッド周りを理解するには、Cをがっつり知らなくてもヘッダーや定義を読んで追うぐらいができれば上等だと思います」「ふむふむ」「あとはCの神マクロとか🤣」「神マクロですか🤣

「この間はてブで見かけたこの辺の記事↓なんかが参考になりますね: CのマクロはLinuxカーネルを追うのに読まざるを得なかったことがあったので、この記事はとてもわかりみがある」「おぉ」

参考: Linuxカーネルで学ぶC言語のマクロ - 覚書

「記事にあるような意味のよくわからないdo whileとか出てくるんですよ↓」「わ、わからん😅」「ジェネリック的なマクロとか、ビルド設定に応じて何もしないようにするマクロなんかも本当によく出てきますヨ」

/* 同記事より */
#define swap(a, b) \
        do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)

「これ!こういう構造体のコード↓を見たときにLinuxではこうやるのかと驚きましたし」「こ、これは?」「親構造体へのポインタをマクロで取りに行くというヤツで、要はメモリアドレス的にさかのぼって取りに行くだけですけど」「ひえ…😅

/* 同記事より */
/**                                                                                                                                                                                                                                           
 * container_of - cast a member of a structure out to the containing structure                                                                                                                                                                
 * @ptr:        the pointer to the member.                                                                                                                                                                                                    
 * @type:       the type of the container struct this is embedded in.                                                                                                                                                                         
 * @member:     the name of the member within the struct.                                                                                                                                                                                     
 *                                                                                                                                                                                                                                            
 */
#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

「こういうのとかメモリのアラインメントの話とかでCがちょっとわかった気持ちになれましたね」「Javaだと絶対出てこない感じ😆」「C++は知りませんけど、今はさすがにC++でもこんな書き方しないかな〜?」「しない方がよさそう😆」(以下延々)

⚓その他言語


つっつきボイス:「そういえば最近雑誌買ってないな〜」「買わないんですか?」「読みたいのはたいてい特集なので、特集が別冊になったときに買うことはあります」「今本屋に物理的に行けなくなってますし」「店頭で買うとたまに同じ号を2冊買っちゃうことがあって😭」「まあそれはそれでいいじゃないですか☺

「昔はUNIX Magazineを舐めるように読んでましたし、ぷらっとホームの広告なんかもも楽しく読んでましたし」「最近そういうのしなくなったかも」

参考: UNIX magazine 最終号 | スラド
参考: ぷらっとホーム - Wikipedia

「雑誌の特集は強い人が書いているのでやっぱり質が高いですよね👍」「雑誌だとコピペできないのが残念ですけど」「最近はたいてい記事にURL書いてますよ〜😋」「書籍もURL載せてますし」

「自分は老眼なので電子書籍じゃないと買う気がしないんですけど、Kindleで買ったときに残念なのがソースコードの行の隙間がすごく大きくて場所を取っていることとコピペしづらいこと😭」「あ〜あれはやりづらいですね」「組版のエンジニアが頑張ってくれないとどうしようもない」「ソースコードの背景色がなくて本文と区別が面倒なことも多くて😢

「それなら画像をキャプチャしてOCRでソースを取り出せばよかったりして」「そこまでやりますか😆」「その方が謎の制御文字やら全角文字やらが入らなさそうですし」

⚓その他

⚓京の形見分け


つっつきボイス:「スパコン『京』の形見分けというか」「京に寄付できるのはいいですよね」「5万円以上寄付すると京グッズがもらえるけど先着順のみか〜」「寄付自体は今も継続してますけど、グッズはもうおしまいですね」「桐の化粧箱欲しい〜」

「もらいそこなった人がせめてもとマジックで京と書いてみたのを見つけました↓」「こ、これは😆」「気持ちわかる〜」「Pentium Pro?」「Core 2 Duoって書いてるのが見えた」

参考: Intel Core 2 - Wikipedia

「ついでですが、例の富嶽のアーキテクチャが公開されているのを初めて知りました↓」「そうそう、富嶽は公開されてますヨ: ブロック図なんかもしっかり書かれているので興味ある人はどうぞ」「えらいな〜」

「よくぞここまで公開しましたね」「まあ国のプロジェクトですし」「このドキュメントだけでも相当金がかかってるはずですし、しかも日本語ですよ🇯🇵

「個人的にはA64FXという名前がちょっとAthron 64 FXみたいだな〜って😆」「それちょっと思いました😆」「初めて買ったのがAthron 64 FXでした」

参考: Athlon 64 FX - Wikipedia

「しかしこういうのを見ると、税金を投入する価値があるって思いますね」「まさに国力に直結しますよね」「もちろん技術者としては英語が読めるべきというのはありますしそれももっともなんですけど、英語の壁のせいでこういう技術のパイが広がらなかったらもったいないですし」「日本語だと読みやすさが違いますし」「ファースト1マイルのところで英語の壁でフィルタされてしまうと、『最初は日本語で勉強するけど後から頑張って英語でも読む』みたいな流れも止まってしまいますし」「たしかに」

⚓番外

⚓論文を読む理由


つっつきボイス:「いつもと毛色が違いますけど、自分が論文をちゃんと読む機会がなくて、この記事みたいに『この論文は自分に読まれるために書かれたのでは?』という気持ちにまだなったことがなくて😅」「論文は読めるようになるまでが相当つらいですけど😆」「やっぱり修練が必要なんですよね…」

⚓論文を読めるために必要なこと

「論文を読むことの難しさは『最初のうちは自分が正しく読めているかどうかがなかなかわからない』という点にあると思うんですよ: だからこの記事みたいに他の人と一緒に輪読したりしてそこを確認しながらでないと読むこと自体がなかなかできない」「おぉ」

「まず論文には、受理されるためによく見せようと書いている部分もあったりするので、そういう部分を疑いながら選り分けて読み進められるようになる必要があります」「ふむふむ」

「それから論文には紙面の限界があるので、分野にもよりますが、論文に記載されていない膨大な背景知識も要求されるんですよ: なので論文をまったく読んだことのない人がたまたま最新の論文を目にしたとしてもまず読みきれないと思います」「あ〜なるほど!」

「もちろんまともな論文であれば関連研究としてリファレンスを記載しているので、それも全部追って読めば一応読めるんですけど、1つの論文を読むために相当数のリファレンスも読まないといけなくなります」「ですよね」

「その分野の論文を読み慣れている人なら、その論文の他に後どれだけのリファレンスを読んで頑張らないといけないかが大体想像つくんですけど、読み慣れていない人だとちょうど記事にもあるようにたいてい『浅いうわっつらしか学べない』で終わっちゃう可能性大でしょうね」「う〜む😅


「今のCOVID-19関連の論文も、それだけ読んでわかるものでもなさそうですね」「まあ医療系の論文はコンピュータ系とかなり文化が違うようですけど」「あぁ、そうかも😳


「論文のスタイルは分野によって相当違うことがあります: たとえばコンピュータサイエンス(CS)系だと指導教官がlast authorになるという慣習があるんですけど、分野によっては著者名を単にアルファベット順に記載するところもあるそうです」「へぇ〜」

「論文を知らない人向けに補足すると、論文の多くは共同研究なので著者が複数であることが多くて、1番目がfirst author(筆頭著者)と呼ばれるんですけど、CS方面ではその論文に一番コミットした人がfirst authorになるという慣習があります」「へぇ〜」「そして重要なのは、first authorにならないとその人の業績とみなされない文化があること」「そうなんですね…」

「共同研究はいろいろセンシティブなところがあって、たとえばその論文に50%相当で貢献していたとしても、論文のfirst authorになるかどうかで業界での認識が大きく変わります」「なるほど」「共同研究だと論文を複数出して、論文ごとにfirst authorを交代するなどの配慮をすることもよくありますし」

「つまり論文を読むときは分野の文化的背景についても知っておく必要があるわけです🎓: たとえば論文を読み慣れてくるとlast authorを見るだけで『これはあそこの研究室で出している論文かなるほど』みたいな情報が読み取れるようになる」「な〜るほど!」

「CSだとlast authorは指導教官とか研究チームをリードする立場の人がなることが多いので、last authorの経歴を追えばどんな研究をしているかがだいたい見えてきますし、『この研究所はこういう研究に強いんだな』という情報もそこから芋づる式に見えてきます」「そうやって読むんですね」「大学名よりはlast authorの方がそういう情報にたどり着きやすかったりしますね」

「そういったわけで、論文の読み方やそうした文化的背景をがっつり教えてもらう機会がないと、独力で論文を読めるようになるのはかなり難しいと思います」「大学や大学院ってそのための場所ですよね…」「論文を読めるようになりたい人は、たとえば社会人向けの大学院のようなところで学ぶといいんじゃないかと思います」


後編は以上です。

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

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

Ruby Weekly

Rails: StripeのWebhookイベントをセキュアに扱う方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: StripeのWebhookイベントをセキュアに扱う方法(翻訳)

StripeはWebペイメントサービスでその名を広く知られたソリューションであり、私たちは支払い処理のために構築した多くのRuby on RailsアプリケーションでStripeを大規模に採用しています。これらのアプリケーションはどれも、Stripeから届くいくつかの重要なイベントに関する通知を受け取って、ワークフローの中で対応する必要があります。

Stripeはwebhookイベントを送信することで、登録されているアプリケーションに対して、さまざまなイベントの発生時に公開エンドポイントに対してHTTP POSTリクエストの形で通知を送信します。このエンドポイント(ルーティング)は一般公開されるので、その気になれば誰でも悪意を持って呼び出せます。したがって、有害なユーザーからエンドポイントを守ることが非常に重要になってきます。Stripeが送信するStripe-Signatureヘッダーはメッセージの真正性検証に利用され、これによって知らない第三者からのリクエストではなくStripeからのリクエストであることを確認します。

これはstripe_events gemを使うのが最も簡単です。

それでは実装してみましょう。とても興味深い内容になるはずです。

⚓1. gemのインストール(Gemfile)

Gemfileに以下のgemを追加します。

gem 'stripe_event'

⚓2.  ルーティングの追加(routes.rb)

mount StripeEvent::Engine, at: '/any-path-you-want'

ここではわかりやすくするために、以下のパスを使うことにします。

mount StripeEvent::Engine, at: '/stripe/webhook' # このURLは変更可能

⚓3. development環境でのテスト

まずはwebhookをローカルでテストするために、サーバーをngrokで公開しましょう。ngrokで一般からアクセス可能なURLを作成できることはもう皆さんご存知ですよね。

私のngrok URLを仮にhttps://9123ab3f.ngrok.io/だとしましょう(訳注: もちろん既に無効です)。

次に必要なのは、Stripeのwebhookを受けるhttps://9123ab3f.ngrok.io/stripe/webhookのようなエンドポイントを追加することです。

それではStripeのwebhookをセットアップしましょう。

  • Stripeアカウントにログインして、Stripeのダッシュボードに移動する
  • Developers > Webhooks を開く(リンク
  • 自分のアカウントをtestモードに切り替える
  • Stripe webhookに自分のEndPointを追加し、欲しいイベントを設定する(課金の作成や返金など)
  • 設定するEndPointはngrokのURLです(https://9123ab3f.ngrok.io/stripe/webhookなど)

liveモードのときには、自分のWebサイトのURLホストをEndPointに追加する必要があります。

webhookを作成したら、そのwebhookの署名用の秘密鍵をメモしておきます。これは後でコードに追加する必要があります。

⚓4. credentialの追加(config/initializers/stripe.rb)

Stripe.api_key             = ENV['STRIPE_SECRET_KEY']
StripeEvent.signing_secret = ENV['STRIPE_SIGNING_SECRET']

StripeEvent.configure do |events|
 events.subscribe 'charge.dispute.created', Stripe::EventHandler.new
end

ここではStripeイベントをひとつ設定します。このcharge.dispute.createdStripe::EventHandlerは、webhookが呼び出された後のアフターエフェクトを扱うために作成したサービスです。

Stripeの支払いで何らかの課金が発生すると、Stripe::EventHandlerクラスを呼び出します。

StripeEvent.configureブロックでイベントを複数定義することもできます。詳しくは以下の記事をどうぞ。

参考: Handling BigCommerce Webhooks in Ruby on Rails application - BoTree Technologies

課金に関連する以下のようなイベントは、すべてcharge.disputeで扱えます。

  1. Charge Created(課金の作成)
  2. Charge Updated(課金の更新)
  3. Charge Closed(課金のクローズ)
  4. Charge Funds Reinstated(課金額の再投入)
  5. Charge Funds Withdrawn(課金額の回収)
events.subscribe 'charge.dispute.', Stripe::EventHandler.new

上のコードは、いずれかのcharge.disputeイベントが発火したときに、エンドポイントにリダイレクトしてStripe::EventHandlerクラスを呼び出すようStripに指示します。

⚓5. webhookリクエストを扱うサービス

services/stripeフォルダの下にevent_handler.rbというサービスをひとつ作成します(app/services/stripe/event_handler.rb)。

# Stripeモジュール
module Stripe
 # stripeのメインクラスEventHandler
 class EventHandler
   def call(event)
     method = 'handle_' + event.type.tr('.', '_')
     send method, event
   rescue JSON::ParserError => e
     render json: { status: 400, error: 'Invalid payload' }
     Raven.capture_exception(e)
   rescue Stripe::SignatureVerificationError => e
     render json: { status: 400, error: 'Invalid signature' }
     Raven.capture_exception(e)
   end
 end

 def handle_charge_dispute_created(event)
   # your code goes here
 end
end

# method = 'handle_' + event.type.tr('.', '_')

# event.type
# これはイベント名を取れる。この記事の場合は「charge.dispute.created」を返して
# ‘.’ を ‘_’に置き換えることで‘charge_dispute_created’にし、そこに'handle_’をくっつける
# 最終的にhandle_charge_dispute_createdというメソッド名になる

# send method, event

# これはメソッドの変数の値に基づいてメソッドを呼び出す。この記事の場合は
# ‘handle_charge_dispute_created’を呼び出す
# 他のイベントについても同様にイベントに基づいてこのメソッドをサービス内に定義し
# 自分のサービスに置く必要がある

Stripeのイベントに基づいてこのようにサービスを定義するときに、このコンセプトを継承するとコードをシンプルに書けます。

以下の2つのStripeイベントを扱わなければならなくなったとします。

  • Charge dispute(課金の異議申し立て)
  • Charge refund(課金の返金)

この場合、以下のようなサービスを定義してコードを切り離せます。

メインのservice(EventHandler)でリクエストを扱います。

# app/services/stripe/event_handler.rb
# Stripe module
module Stripe
  # stripe main class EventHandler
  class EventHandler
    def call(event)
      method = 'handle_' + event.type.tr('.', '_')
      send method, event
    rescue JSON::ParserError => e
      render json: { status: 400, error: 'Invalid payload' }
      Raven.capture_exception(e)
    rescue Stripe::SignatureVerificationError => e
      render json: { status: 400, error: 'Invalid signature' }
      Raven.capture_exception(e)
    end
  end
end

以下のサービスは、リクエストに応じて課金の異議申し立てイベントを扱います。

# app/services/stripe/dispute_event_handler.rb
# Stripe event handler for handling webhook
module Stripe
  # This will inherite the eventHandler main class
  class DisputeEventHandler < EventHandler
    def handle_charge_dispute_created(event)
      # your code goes here
    end
  end
end

以下のサービスは、リクエストに応じて課金の返金イベントを扱います。

# app/services/stripe/refund_event_handler.rb
# Stripe event handler for handling webhook
module Stripe
  # This will inherite the eventHandler main class
  class RefundEventHandler < EventHandler
    def handle_charge_refund_created(event)
      # your code goes here
    end
  end
end

目が離せませんね!皆さまもぜひ、自分のStripe webhookをセキュアにすることを検討しましょう。

お読みいただいた皆さまに感謝いたします。


25人を超えるRuby on Railsチームを擁する私たちBoTree Technologiesは、エンタープライズアプリケーションの構築をお引き受けいたします。

弊社では、RPA、AI、Python、Django、JavaScript、ReactJSにも特化しています。

This blog is originally published at BoTree Technologies

関連記事

Stripe決済を自社サービスに導入してわかった5つの利点と2つの惜しい点

週刊Railsウォッチ(20200713前編)rspec-openapiでスキーマ自動生成、Rails Architect Conf動画、where()ハッシュキーに比較演算子条件を書ける機能ほか

$
0
0

こんにちは、hachi8833です。これが今度のAWS Summit Tokyoの目玉イベントなんですね。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

つっつきボイス:「もう今年も半分終わりか〜」「半分超えちゃいましたね」「AWS Summit Tokyo、今年はリモートでやるってどこかで見たナ🎉

「その『電笑戦 ~ AI は人を笑わせられるのか』とかいう電脳大喜利みたいな企画に募集がいっぱい来たそうです」「『ボケて』のAI版みたいな😆」「また大変そうな企画を😆」「ちゃんと生成モデルを作ってやるんですね」

「素人目にはこれで笑い取れるものを生成できるんだろうかって思いますけど」「たしかこの種の試みって他でもやっていて、一発で大ネタをキメるのは難しいけど、いくつか候補を出してその中から選ぶみたいなのだと割といい感じにできるらしいし、学習結果をフィードバックしたりして強化したりもできるので、何度も回しているとだんだん上手くなってくるという感じらしいですね」「人間様が集まって勝手にフィルターとして機能してくれると最高でしょうね」「笑いの場合、どういうコンテキストなのかにもよるでしょうね」「時代もありそうですし」

「深層学習は、人間でもなぜそれを思い付いたのか説明できないようなことを扱うのに適してますけど、その代わりAIもなぜそれが正しいのかという理由の説明はできないという」「そこですよね」「結果の説明責任がないのが深層学習ですし」「『理由はわかりませんが皆さんこうしてらっしゃいます』的な」「深層学習はその辺が誤解されやすいところ」「傍から見ると野生の勘とか女の勘みたいな挙動ですよね😆

「それに普通に考えたら難しそうなことにチャレンジするところに価値があるでしょうし」「やってみたら意外にいい結果が出るかもしれませんよね」

⚓今年のAWS Summit Tokyoは

「今年のAWS Summit Tokyoは9月にリモート開催か〜」「まあ人多すぎてしんどいイベントなので、むしろリモートでいいと思います」「同意です!」「講演聞くだけなら自宅の方が快適ですよね」「人が多いと面白そうなセッションの部屋があっという間に埋まっちゃうのもつらくて、最近行ってませんでしたし」「入れないのつらいですよね…」「どの開催地でも結局部屋足りなくなるときしかありませんでしたね」

「まあ行ったら行ったでAWS認定持っている人だけが入れる休憩ゾーンみたいなのを使えますけど😋」「そんな空港のVIPルームみたいなのがあるんですか、いいな〜」「あれでAWS認定取った甲斐はあったかなと」

「リモート開催が今後平常運転になるかもですね」「その分、会場で仕事探したり商談したい人には残念でしょうけど」「それもそうか」「興行として成功するにはそのあたりが今後の課題かも」「AWS Summitはテック系イベントですけど、ブースのあたりを見ていると来客の半分ぐらいがビジネス系寄りな印象がちょっとありますね」「へぇ〜」

⚓Rails: 先週の改修(Rails公式ニュースより)

今回は以下のコミットリストのChangelogを中心に見繕いました。ドキュメントの修正が増えてるので、そろそろ6.1が近い?

⚓ActionDispatch::SSLリダイレクトのデフォルトHTTPステータスを308に

# #L
-   def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
+   def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
      @app = app

      @redirect = redirect
      @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
      @secure_cookies = secure_cookies

      @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
+     @ssl_default_redirect_status = ssl_default_redirect_status
    end
...

      def redirection_status(request)
        if request.get? || request.head?
          301 # Issue a permanent redirect via a GET request.
+       elsif @ssl_default_redirect_status
+         @ssl_default_redirect_status
        else
          307 # Issue a fresh request redirect to preserve the HTTP method.
        end
      end

つっつきボイス:「これはこの間話題になったHTTP 308か(ウォッチ20200629)」「まだステータス名覚えられない😅」「308 parmanent redirect、っと…」

⚓新機能: whereでハッシュキーへの比較演算子条件記入をサポート


つっつきボイス:「@kamipoさんがしれっと入れてました」「おぉ、これは😍

# 同PRより
posts = Post.order(:id)

posts.where("id >": 9).pluck(:id)  # => [10, 11]
posts.where("id >=": 9).pluck(:id) # => [9, 10, 11]
posts.where("id <": 3).pluck(:id)  # => [1, 2]
posts.where("id <=": 3).pluck(:id) # => [1, 2, 3]

型キャストとテーブル名/カラム名解決という点においては、("create_at >= ?", time)よりもwhere("create_at >=": time)という書き方の方がよい。
同PRより大意

# 同PRより
class Post < ActiveRecord::Base
  attribute :created_at, :datetime, precision: 3
end

time = Time.now.utc # => 2020-06-24 10:11:12.123456 UTC

posts = Post.order(:id)
posts.create!(created_at: time) # => #<Post id: 1, created_at: "2020-06-24 10:11:12.123000">

# SELECT `posts`.* FROM `posts` WHERE (created_at >= '2020-06-24 10:11:12.123456')
posts.where("created_at >= ?", time) # => []

# SELECT `posts`.* FROM `posts` WHERE `posts`.`created_at` >= '2020-06-24 10:11:12.123000'
posts.where("created_at >=": time) # => [#<Post id: 1, created_at: "2020-06-24 10:11:12.123000">]

「これは"create_at >= ?"のようにハッシュのキーに条件を入れるというやり方を選んだのか!」「へぇ〜、そんな方法が!」「ハッシュの、しかもキーに!?」「RuboCopに怒られないかしら?😆

「DSLをいたずらに拡張するよりはこの方がうまいですね〜: これなら互換性も保たれるし、条件を文字列で直書きするのと違ってSQLインジェクションにも対策できるし」「このプルリク、ハートマークが47個も付いてますね❤」「引数で渡した文字列がそのままSQLに渡されるよりはいいよねとみんなも思ってるんじゃないかしら」

「あと型キャストについてですが、?プレースホルダで渡す従来の方式だと、渡したTimeWithZoneオブジェクトに入ってる値がそのままSQLのプレースホルダに渡されようとするので、Active Recordが本来ミリ秒で値を丸める処理が通らない: なので、Active Recordがクエリを投げる前に通常のActive Record内でRuby→SQLの間の型変換を通せるようにArelで解釈ができる仕組みにしたかったのかなと思いました」「おぉ」

# activerecord/lib/active_record/relation/predicate_builder.rb#L112
          elsif table.aggregated_with?(key)
            mapping = table.reflect_on_aggregation(key).mapping
            values = value.nil? ? [nil] : Array.wrap(value)
            if mapping.length == 1 || values.empty?
              column_name, aggr_attr = mapping.first
              values = values.map do |object|
                object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
              end
              build(table.arel_attribute(column_name), values)
            else
              queries = values.map do |object|
                mapping.map do |field_attr, aggregate_attr|
                  build(table.arel_attribute(field_attr), object.try!(aggregate_attr))
                end
              end

              grouping_queries(queries)
            end
+         elsif key.end_with?(">", ">=", "<", "<=") && /\A(?<key>.+?)\s*(?<operator>>|>=|<|<=)\z/ =~ key
+           build(table.arel_attribute(key), value, OPERATORS[-operator])
          else
            build(table.arel_attribute(key), value)
          end

「条件の中身もend_with?(">", ">=", "<", "<=")でちゃんと解析してますね↑: 何でも書けるわけじゃなくてここでチェックされる演算子しか使えないということで」「なるほど、やんちゃできないようになってると」「ぱっと見何でも書けそうに見えるけど対策済み😋

# activerecord/lib/active_record/relation/predicate_builder.rb#L138
+         OPERATORS = { ">" => :gt, ">=" => :gteq, "<" => :lt, "<=" => :lteq }.freeze

「あぁこれ見て納得した↑: もともとArelの中ではここにある演算子を使って比較演算の式をビルドするんですよ」「あ〜なるほど」

「ナイス落とし所: 引数で渡した文字列をそのままSQLに渡さずに済むという点でとても現実的な解だと思います👍」「この書き方を初めて目にしたらちょっとドキドキするかも」「そうかも😆」「あってもおかしくないけど、もしかすると見慣れない書き方に拒否反応が出るかもしれなくてやらなかったのかな?」「ハッシュのキーというとスペースを含まないスネークケースの文字という意識が先に立ちそうですし」

⚓Journeyの文字列を式展開に変えてアロケーションを削減


つっつきボイス:「こちらは小ネタですが」「+による文字列結合がよろしくないので式展開に変えたというヤツですね」

# actionpack/lib/action_dispatch/journey/path/pattern.rb#L110
          def visit_STAR(node)
-           re = @matchers[node.left.to_sym] || ".+"
-           "(#{re})"
+           re = @matchers[node.left.to_sym]
+           re ? "(#{re})" : "(.+)"
          end

Rubyでの文字列連結に「#+」ではなく式展開「#{}」を使うべき理由

⚓ガイド: Action Cableの記述を追加


つっつきボイス:「こちらも小ネタですが、Action Cableの機能説明が1個足されてました」「いいですね〜」

# guides/source/action_cable_overview.md#L757
### クライアントサイドログ出力
クライアントサイドログ出力はデフォルトで無効になっています。これは`ActionCable.logger.enabled`をtrueにすることで有効にできます。
  ```ruby
  import * as ActionCable from '@rails/actioncable'
  ActionCable.logger.enabled = true
  ```

⚓Rails

⚓大規模マイグレーションを高速化するためにコールバックを全部スキップした(Hacklinesより)

# 同記事より
# GOOD
class BackfillEmployeesWithFriendlyId < ActiveRecord::Migration[5.0]

  # 空のクラスのおかげで大規模マイグレーションを遅くするコールバックを簡単に全スキップできる
  class FriendlyIdEmployee < ActiveRecord::Base
    self.table_name = 'employees'
    extend FriendlyId
    friendly_id :slug_candidate, use: [:slugged, :finders]

    def slug_candidate
      if first_name || last_name
        "#{first_name} #{last_name}"[0, 20]
      else
        "employee"
      end + " #{SecureRandom.hex[0, 8]}"
    end
  end

  def up
    print "Updating friendly_id slug for employees"
    FriendlyIdEmployee.where(slug: nil).each do |row|
      row.save; print('.')
    end
    puts ''
  end
end

つっつきボイス:「タイトルでわかっちゃうぐらい短い記事ですが」「マイグレーション高速化のためにコールバックをスキップするという選択肢はありでしょうね」「コールバックチェインは何やっても遅くなるし」「スキップしないのが理想だけどそうせざるを得ないときはありますし」「ここではfriendly_idを入れたことで大規模マイグレーションすることになったと」「friendly_idはきっかけということですね」

何ということでしょう…Employee.all.each(&:save)がproduction環境で地獄のように遅い。
同記事より

⚓Rails Architects Conference 2020 Onlineの動画がアップ

Rails Architects Conference 2020が7/1より順次オンライン開催


つっつきボイス:「以前記事にしたArkencyさんのRails Architects Conference、結局網膜裂孔で見そびれました…😢」「そうそう、動画上がってたので後で見ようと思ってた」「うっしゃ、後で見ようっと」

「ざっとスライド見た感じでは、話すこと前提で何もかもは書かない方針かな」「英語圏とかキーノートスピーチによくあそう」「プレゼンをちゃんと聞いて欲しいなら、全部盛りしないでこうやって話す用のスライドにした方がいいでしょうね」「たしかに」


以下は同サイトに上がっている動画です(日本語タイトルは仮)。最後の2つは現時点でスライドなしの動画のみです。

⚓キーノート: 今こそ変革の時 — 次のRailsアプリを正しく始めるには

⚓Railsでのマルチテナント

⚓つらくないRailsアップグレード方法とは

⚓Railsのビューを高速化するシンプルな方法

⚓一見よさげなRubyコードにも「低凝集度」「強結合」が潜む(動画のみ)

⚓顧客が知っているのは「何が欲しい」ではなく「今欲しい」(動画のみ)

⚓pastrubies.com: 過去のRuby/Rails情報を配信(RubyFlowより)


同サイトより


つっつきボイス:「詳しい説明が見当たらないんですけど、サイト名のとおり過去のRuby/Railsに関する記事を配信するサイトのようです」「RubyConf 2005 Agenda Releaseというタイトルとかちょっと衝撃的」「当時はペチパーやってたな〜」「どういう基準で選んでるんだろう?」「開いてみたらarchive.orgだったとは」

⚓Rails Bytes: Railsアプリテンプレートを置けるリポジトリ(Hacklinesより)


同サイトより


つっつきボイス:「Everyday Railsの記事で、このRails Bytesがいいよと紹介されていました」「なるほど、Railsアプリのテンプレートを公開できるサイトね↓」「自分で作ったものもアップロードできるようです」


同サイトより

「適当にrails newで作っておいてrails app:template LOCATION="https://railsbytes.com/script/x7msKX"みたいに実行すると一発起動できるそうです」「他の人の作ったテンプレートを見られるのはよさそうですね👍


後でやってみました。

$ bundle exec rails app:template LOCATION='https://railsbytes.com/script/x7msKX'
/Users/hachi8833/deve/rails/hello_world/vendor/bundle/ruby/2.7.0/gems/thor-1.0.1/lib/thor/actions.rb:222: warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open
hello world from https://railsbytes.com 👋

⚓rspec-openapi: request specからOpenAPIスキーマを生成

# 同リポジトリより
openapi: 3.0.3
info:
  title: rspec-openapi
paths:
  "/tables":
    get:
      summary: 'tables #index'
      parameters:
      - name: page
        in: query
        schema:
          type: integer
      - name: per
        in: query
        schema:
          type: integer
      responses:
        '200':
          description: returns a list of tables
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    # ...


つっつきボイス:「この間の銀座Railsの#23でk0kubunさんがちらっとお話していたgemです」「あああ〜これ欲しかったヤツ!😂」「これはたしかに欲しいですね〜」「request specから自動生成か〜、もう作っちゃったけど使っちゃおうかな〜?」

「こういうの探してたんですよ〜」「あとは開発手順にうまくはまるかどうかでしょうね: Railsエンジニアが完全にAPI仕様の決定権を持っているならこれでやるのがよさそうだけど、フロントエンド側のエンジニアがAPI仕様を決めたいという場合だとしんどくなるかも」「あ、たしかに」「フロントエンドエンジニアもAPI仕様策定に参加するなら、これまでどおりSwagger(OpenAPI)でやる方がどちらも中身を読めるでしょうし」

参考: API Documentation & Design Tools for Teams | Swagger

「ともあれスキーマ生成は人間がやらずに済めばそれに越したことはないので、これでうまくいくなら使いたいですね」「このgem、まだ作って1か月経ってないのか」「今だとGraphQLの方が新しく使われそうな気もしますけど」「結構よさそう👍


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200707後編)Rubyで無名structリテラル提案、書籍『AWS認定ソリューションアーキテクト』、21世紀のC言語ほか

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

週刊Railsウォッチ(20200721後編)『パーフェクトRuby on Rails』増補改訂版発売間近、scan_left gemでレイジーなinjectほか

$
0
0

こんにちは、hachi8833です。今週木金は祝日のため、来週7/27、7/28の週刊Railsウォッチは通常記事となります🙇

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓臨時ニュース: 『パーフェクトRuby on Rails【増補改訂版】』が7/25より発売🎉

つっつきの後で知りました。紙の書籍と電子版同時発売だそうです。

Kindle/EPUB版もあるのがありがたいです🙏。詳しくは以下をどうぞ。

⚓Ruby

⚓Polyphony: Rubyでfine grainedコンカレンシー(Ruby Weeklyより)


同サイトより

# 同サイトより
require 'polyphony'

# Kernel#spin returns a Fiber instance
counter = spin do
  count = 1
  loop do
    sleep 1
    puts "count: #{count}"
    count += 1
  end
end

puts "Press return to stop this program"
gets

名前が好きです。


つっつきボイス:「RubyのFiberをいい感じに使えるらしい」「fine-grainedって日本語だと高粒度?この方面ではあまり日本語では言わないかな〜」

「Railsアプリのビジネスロジックでコンカレンシーをふんだんに使うことはあまりないと思いますけど、それこそRackサーバーを自分で書いて複雑なことをさせたいときとか、RubyでSocketをリッスンするようなアプリを書くときとか、後はサイトにもあるようにデータベースコネクタのコネクションプール↓なんかだとコンカレンシーを制御したくなるでしょうね」「そういえばPolyphonyにはHTTPSもできるフル機能のWebサーバーも含まれているそうです」「Webサーバーはまさにコンカレンシーが求められるところですね」

# 同サイトより
DB_CONNECTIONS = Polyphony::ResourcePool.new(limit: 5) do
  PG.connect(DB_OPTS)
end

def query_records(sql)
  DB_CONNECTIONS.acquire do |db|
    db.query(sql).to_a
  end
end

「通常のコンカレンシーだとスレッドの管理になりますけど、PolyphonyはFiberまで使うところがfine-grainedということなんでしょう」「Fiberレベルのコンカレンシーgemということですね」

⚓Huginn: IFTTTやZapier的なWeb連携自動化(Ruby Weeklyより)


同リポジトリより

参考: Webサービスの連携を自動化する Huginn の紹介 - Qiita


つっつきボイス:「ずっと前のウォッチでごく簡単に触れたことがありますが、IFTTTやZapier的なことをやれるRailsアプリだそうです」「オープンソースなのね」

参考: IFTTT: Every thing works better together
参考: Zapier | The easiest way to automate your work

「こういうWeb連携アプリって自分で運用すると大変なので誰かにやって欲しい😆」「そうかも」「インスタンス管理とかOSアップデートとかの面倒見るのがほんとつらいし、止まったときにいろんなところでお困りが発生しますし」「それを考えると、高くてもZapierを使いたくなるかな〜」

「まあ今だとKubernetesが流行ってますし、このHuginnをコンテナでサクッとそこにデプロイできるのであればそんなに大変じゃないかもしれませんけど」「コンテナならやりやすくなりそうですね」

⚓scan_left: injectをレイジー&インクリメンタルに(Ruby Weeklyより)

# 同リポジトリより
require "scan_left"

# 比較のために#injectも記載

ScanLeft.new([]).scan_left(0) { |s, x| s + x } == [0]
[].inject(0) { |s, x| s + x }                  == 0

ScanLeft.new([1]).scan_left(0) { |s, x| s + x } == [0, 1]
[1].inject(0) { |s, x| s + x }                  == 1

ScanLeft.new([1, 2, 3]).scan_left(0) { |s, x| s + x } == [0, 1, 3, 6]
[1, 2, 3].inject(0) { |s, x| s + x }                  == 6

# オプション: `ScanLeft`クラスの利用を明示的に回避したい場合は
# 以下のようにEnumerableでrefinementを使う手もある
#
# このrefinementは`#scan_left`メソッドをEnumerableに直接追加して
# 構文をより明瞭にできる

using EnumerableWithScanleft

[].scan_left(0) { |s, x| s + x }        => [0]
[1].scan_left(0) { |s, x| s + x }       => [0, 1]
[1, 2, 3].scan_left(0) { |s, x| s + x } => [0, 1, 3, 6]

つっつきボイス:「injectのオルタナですか」「そういえばここにもinjectの好きな人が約1名」「ワイのことだ〜😋」「記事にあるように、Enumerableでlazy的なinjectをやれそうですね😋

⚓遅延評価

「遅延評価は、使う側がどこかで処理を止めたいときには速くなりますね」「遅かれ早かれやらなければならない処理はスキップしようがないので、遅延評価にすれば速くなるわけではないでしょうけど」「やらなくてよかった計算をスキップできれば効果が大きい」

「@kamipoさんがRailsの改修でよくやっていますけど、Active Recordベースのカラムって参照されないものの方が多いなんてこともよくあるじゃないですか」「そうそうっ」「カラムはいっぱいあるけど実はidしか取ってなかったとか」「そういうオブジェクトを毎回作ると無駄が大きい」

「ライブラリコードなら使うかどうかわからないようなものを遅延評価で書いておけば効果は大きいと思いますけど、サービスのコードだと、ある程度下のレイヤならともかく、遅延評価を取り入れたところでどうせ全部実行されることの方が多そうですし😆」「おっしゃるとおり😆

「逆にそこら辺を遅延評価で書いてしまうとプロファイラで見ても評価が後ろにずれてしまってどこが遅いのかわかりにくくなりますし」「遅延評価でデバッグしづらくなるの、あるある」「まあ遅延評価にすればいいってもんじゃないよということで」


なお、作者はこの機能リクエストをRuby本体にも投げているそうです↓(審議中)。

⚓無名structリテラル続報(Ruby Weeklyより)


つっつきボイス:「前回も扱った無名structリテラルですが(ウォッチ20200707)、今日久しぶりにつっつきに参加いただいたkazzさんに見せたかったので」「こういうふうに${}でstructを定義できるヤツです↓」「ははぁ〜、なるほど〜😋

# 同記事より
roxie = ${name: "Roxie", breed: "whippet-cross"}

「無名structリテラル、あってもいいですよね」「私も欲しい〜」「むしろこれがなかったから今までRubyのstructがあまり使われなかったんじゃないかって思ったり」

「Rubyのstructってnewする割にはインスタンス1個のまま使われるイメージあるかな」「structはイミュータブルハッシュ的に使われることが多いですよね、自分もそうやって使ってますけど」「たしかにイミュータブルハッシュは欲しい!」

「でも考えてみれば、本来イミュータブルハッシュってハッシュでやるべきじゃないんですよね」「そうそうっ」「無名structを使うとイミュータブルであることを明示できるのがいいよね、と思うわけですよ」「あぁ〜わかりますそれ😂

「無名structリテラルで作ったものはまさにValue Objectとして使えるわけですけど、Value Objectを手軽に定義できるのはとても嬉しい」「structで書くのタイプ量多くてダルいですし😆」「それでついついハッシュに流れてしまうという😆」「無名structリテラル、入ったら使おうかな〜」「もういくつ寝ると来るんでしょうね」

Rails tips: Value Objectパターンでリファクタリング(翻訳)


同記事より:

  • 無名structリテラルが有用な理由:
    • structならキーのタイポを防げる
    • 無名structはイミュータブルであるという意図がはっきり伝わる
    • structならドット記法でシンプルにアクセスできる
    • OpenStructは遅い
  • 同プロポーザルにも「今はハッシュの方が速い」とあるが、ハッシュの利用頻度が高い分最適化が進んでいるだけではないか。

参考: class OpenStruct (Ruby 2.7.0 リファレンスマニュアル)

⚓その他Ruby


つっつきボイス:「ScientistはGitHubの公式gemなのね」「科学系か教育用かなと思ったけど、carefully refactoring critical pathとあるし、パフォーマンスをチェックしながらリファクタリングするgemっぽいですね」

# 同リポジトリより
require "scientist"

class MyWidget
  def allows?(user)
    experiment = Scientist::Default.new "widget-permissions"
    experiment.use { model.check_user?(user).valid? } # old way
    experiment.try { user.can?(:read, model) } # new way

    experiment.run
  end
end

⚓DB

⚓ENUMは銀の弾丸ではない


つっつきボイス:「ぽすぐれのenum型の話: データベースレベルで所定の値だけが入れられるようバリデーションする方法として以下が紹介されてますね」

  1. ENUM型
  2. シンプルなCHECK制約
  3. CHECK制約とFUNCTIONの組み合わせ
  4. 外部キー

「1.のENUMはスキーマの一部で、項目が増えたり減ったりするとスキーマを変更することになる: 増えるときはともかく減らすときは単純にALTER TABLEするわけにもいかなくて面倒になりがちかな」「デメリットとしてはユーザー側で項目を増やしたり減らしたりできない: すぐに変わらないアプリケーションロジックと密に絡むENUMならまあ大丈夫かと思いますが、Redmineの選択項目みたいに運用で項目が増減する可能性があるなら果たしてENUMがいいのかどうかは気にするといいでしょう」

-- 同記事より
CREATE TYPE gender AS ENUM ('male', 'female');

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    gender gender NOT NULL
);

「なおRailsのenumだと文字列やシンボルを数値に置き換えますが、ぽすぐれのENUMはENUM型なのでSQLレベルではちゃんと要素がmaleとかfemaleみたいに現れますね」「なるほど、RailsのenumとPostgreSQLのENUMは違うんですね」「Railsのenumを使うとSQLクエリで数値になるのが読みづらくて、あまり好きでないかな〜」

「他にPostgreSQLのENUMのデメリットとしては、ENUMの値はそのままでは文字列を結合したりサブストリングを取り出したりできない: ぽすぐれだと一応キャストはできるようですが、意識しておく必要はありますね」

「ENUMに向いているデータとして記事では性別が挙げられてますが、バッチのステータスなんかもまず増減しないので向いているでしょうね」


「2.のCHECK制約で書く方法」

-- 同記事より
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    gender TEXT CHECK (gender IN ('male', 'female'))
);

「2.のデメリットとして項目が増えすぎると破綻することがあると、なるほど↓」「ぽすぐれにはENUMがあるからこの書き方はせずに済みますけど、ENUMが入る前のMySQLでこの書き方を使った覚えがありますね」

-- 同記事より

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    country TEXT CHECK (country IN (
        'ad',
        'ae',
        'af',
        'ag',
        'ai',
        'al',
        'am',
        'an',
        'ao',
        ...
    ))
);

参考: ENUM型 | MySQLの使い方


「3.はCHECK制約の参照先をFUNCTIONにするというもので、単なるCHECK制約だと同じ項目を別のところでも使うとDRYでなくなってしまいますけど、FUNCTIONにすることで回避できます」

-- 同記事より

CREATE OR REPLACE FUNCTION valid_gender(TEXT) RETURNS BOOLEAN AS $$
BEGIN
    RETURN ($1 IN ('male', 'female'));
END
$$ LANGUAGE plpgsql;


CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    gender TEXT CHECK (valid_gender(gender))
);

「4.は正規化レベルを1つ上げて外部キーとして扱うというもの: これが一番RDBらしい解決方法かなと思います」「デメリットとして追加テーブルがめちゃくちゃ増える可能性があるというのもごもっとも」

-- 同記事より
CREATE TABLE countries (
    code TEXT PRIMARY KEY
);

INSERT INTO countries (code) VALUES ('ad'), ('ae'), ('af'), ('ag'), ('ai'), ('al'), ('am'), ('an'), ('ao'), ...

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    country TEXT NOT NULL,

    FOREIGN KEY (country) REFERENCES countries(code)
);

「記事の末尾にまとまっているこの表↓は実装方法を複数知っておくうえでなかなかいいですね」「Railsの場合、Railsのenumという列がもうひとつ加わるかな」「タイトルの銀の弾丸云々も、どちらかというとENUM以外の方法もあるよという点に重きを置いている感じですね」「実践的でいい記事だと思います👍

⚓JavaScript

⚓Elmとは

Haskellライクな言語だそうです。

// https://guide.elm-lang.orgより
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Browser.sandbox { init = 0, update = update, view = view }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

参考: Railsで愉快な言語Elmを使う - Qiita


つっつきボイス:「エラーメッセージが読みやすいとかTSとReduxより楽という噂を聞いたのでElmをエントリしてみました」「まあ使いたい人が使うヤツでしょう」「一応Webpackerで入れられるそうですが、Railsでの利用例の記事は今のところあまり見かけない感じでした」「最近だとフロントエンドとバックエンドでリポジトリを分けて、RailsはAPIとかGraphSQLを担当するみたいなのが増えてる感じはあるので、ElmみたいなのをRailsと同じリポジトリに入れることはそんなになさそうな気もしますけどね」

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓QUIC


つっつきボイス:「この記事はてブで見ましたけど、よくここまでやったなという感じ」「最近はDNSもQUIC実装が進んでたりしますけど、この記事みたいにBGP over QUICやるのはアツい」「まあBGPを触る機会がそもそもありませんけど😆

参考: BGP(Border Gateway Protocol)とは

「結局、TCPは便利だけどあらゆるニーズに応えられるとは限らないということでしょうね: フローコントロールを自分でやりたいならTCPはいらないでしょうし」

⚓その他フロントエンド


つっつきボイス:「前にも話題にした気がしますけど(ウォッチ20200204)ついに始まったのね」「受験料1万円か〜」「試験を受ける金銭的なメリットはぱっと思いつきませんけど、セキュリティを勉強するエンジニアが理解度を確かめるのには有用だと思います👍」「徳丸先生のことだから問題もそれなりに難しそうだし、範囲も相当広いんじゃないかしら」

⚓番外

⚓ゲーム音楽サイトはRails+React製


同サイトより


つっつきボイス:「ゲーム音楽って知らない世界なんですが、はてブ民が泣いて喜んでたので」「泣かないけど全俺が喜んだ: ありがとうございます、ありがとうございます😂

「そうそう、このサイトのソースコードがGitHubリポジトリに上がってますよ↓: しかもRailsベースのGraphQLとReactベースのフロントエンド」「お〜マジですか!」「作者のブログもありますし↓」

「しかしいつの間に?」「いや単に自分の好きな曲がなかったので、どうやってデータをクローリングしてるのかなと思って昨日ソースコードを見てた🤣」「そうでしたか🤣」「シューティングの曲が少ないな〜と思いながらざっと見た感じでは、どうやらクローラじゃなくて自分でデータ入れてるみたい」「それもスゴい💪


後編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200720前編)10月開催「Kaigi on Rails」CFP募集中、enumにデフォルト値設定機能、RailsでBitemporal Data Modelほか

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

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

Ruby Weekly

ActiveStorageでアップロードしたファイルとプレビュー画像に認証をかける

$
0
0

ActiveStorageでアップロードしたファイルに認証をかける

こんにちは。そしてはじめまして。srockstyleといいます。

僕は普段はフリーランスとして活動しておりBPSさんの社員ではありません。ある日普段から仲良くさせていただいていているhachi8833さんに「よかったら記事書いてみない?」とお誘いをいただき、TechRachoのライターとして記事を書かせていただけることになりました。

よろしくお願いします。

はじめに

最近関わっていたプロジェクトでActiveStorageを使う案件があったのですが、ふと以下のことに気づきました。

ActiveStorageは基本ファイルに対する認証はサポートしてません。もしアップロードしたファイル、またはプレビューするために生成した画像ファイルをログインしたユーザだけに見せたいなどという場合は、自前で認証を実装することになります。

ファイル本体に認証をかける

こちらのファイルを編集します。

このファイルを編集するには、apps/controllers配下に直接ファイルを配置し、元のクラスを上書きする必要があります。そのためapp/controllers配下にActiveStorageのディレクトリを作成します。

$ mkdir [project_name]/app/controllers/active_storage/

作成したディレクトリの下にblobs_controller.rbを作成、編集していきます。

[project_name]/app/controllers/active_storage/blobs_controller.rb

class ActiveStorage::BlobsController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob

  def show
    ## 認証してなかったら403
    unless authenticate_for_files
       head :forbidden
       return
    end

    expires_in ActiveStorage.service_urls_expire_in
    redirect_to @blob.service_url(disposition: params[:disposition])
  end

  private

  def authenticate_for_files
    ## 認証処理
  end
end

プレビューするファイルに認証をかける。

ActiveStorageにはPDFや動画などのファイルをアップロードしたときに、そのプレビューを画像として表示する機能があります。

プレビュー画像にも認証をかけたい場合は以下のファイルを使います。

[project_name]/app/controllers/active_storage/representations_controller.rbを作成し、編集していきます。

class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob

  def show
    ## 認証してなかったら403
    unless authenticate_for_files
       head :forbidden
       return
    end

    expires_in ActiveStorage.service_urls_expire_in
    redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
  end

  private
  def authenticate_for_files
    ## 認証処理
  end
end

ActiveStorage::BaseControllerにメソッドを書く

同じロジックなのであればActiveStorage::BaseControllerに認証のメソッドを書くのも選択肢のひとつです。

ファイルを作成、メソッドを追加します。
[project_name]/app/controllers/active_storage/base_controller.rb

class ActiveStorage::BaseController < ActionController::Base
  include ActiveStorage::SetCurrent

  protect_from_forgery with: :exception

  def authenticate_for_files
    ## 認証処理
  end
end

他の二つからはbefore_actionで呼び出します。
[project_name]/app/controllers/active_storage/blobs_controller.rb

class ActiveStorage::BlobsController < ActiveStorage::BaseController
  before_action :authenticate_for_files
  include ActiveStorage::SetBlob
  ### 中略
end

[project_name]/app/controllers/active_storage/representations_controller.rb

class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
  before_action :authenticate_for_files
  include ActiveStorage::SetBlob
  ### 中略
end

ただ、base_controllerを上書きしてしまうと触る必要のないActiveStorageの処理にも影響がでるので、冗長になってしまいますが、認証をかけたいControllerだけファイルを作成、それぞれに認証のメソッドを書くのがおすすめです。

終わりに

重要なファイルを扱うWebサービスを作っていると、見せたくない画像ファイルやドキュメントなどは期限つきURLだけでは心許ない場合などもやはりあるかと思います。期限つきでかつ認証がかかっているファイルであれば安心して扱っていけるかと思います。
この記事が役にたてば幸いです。


AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

晴れてAnyCable 1.0のリリースをお知らせできるときがやってまいりました。AnyCableはAction Cableに投入するだけで使えるターボ拡張であり、Action Cableと同じAPIに依存しつつRailsの外部でも動作できます。私の「いかれた(demented: 私の名前Dementyevのもじり)」アイデアを、リアルタイムRubyアプリケーション向けの頑丈なバックボーンとして結実するのに4年もの歳月を要しました。皆さんが本記事でAnyCableの新しい機能を発見し、私たちの成功と失敗から学び、AnyCableの未来を垣間見、そしてRubyとGoを併用して2つの世界にとってベストなものを構築する方法を知っていただければと思います。

AnyCableは、RubyやRailsで構築されたリアルタイムアプリケーションに高パフォーマンスとスケーラビリティをもたらします。低レベルのWebSocketハンドリング処理をRubyからGoに移し、しかもRails(または純粋なRuby)コードベースにあるビジネスロジックをすべて手つかずのままにできるようにしました。言い換えると、AnyCableは生産性を犠牲にすることなくアプリケーションのパフォーマンスを高められるわけです。

AnyCableのアーキテクチャやパフォーマンスの特徴についてはAnyCableの紹介記事を、完全な説明については公式Webサイトをどうぞ。

このプロジェクトを長くやっている間に、Ruby gemとGoバイナリをツールファミリーに編成したり、ひとりでメンテしていたのが多くのコラボレーターやコントリビューターからなる素晴らしいチームに移行したり、200ワード足らずのREADMEを50件を超える記事を擁する立派なドキュメントWebサイトにしたりしました。まあ昔を懐かしむのはこのぐらいにして、本日のトピックについてご説明しましょう。

⚓AnyCable 1.0のハイライト

AnyCable v1の背後に隠れている主なアイデアは、Action CableからAnyCableにできるかぎりスムーズに移行できるようにすることでした。当初はAnyCableをまさにRailsアプリケーション向けのプラグアンドプレイソリューションとして作り始めましたが、これはほとんどのユースケースにおいて真ではありませんでした。多くの人々が潜在的な注意点について長い記事を書いたりissueを投げたり回避方法やハックを共有したりしてくれました。そしてv0.6.0リリースで少し明るい未来へと小さな一歩を進め、そしてv1.0で大きく羽ばたきました!

AnyCable 1.0は、Action CableからAnyCableへの可能な限りスムーズな移行を目指しています。

詳しくは公式のリリースノートに全部書いてありますが、皆さまのためにいくつかハイライトを拾ってみましょう。

インタラクティブなクイックスタート

既存のプロジェクトでAnyCableを使うための設定はなかなか一筋縄でいきませんでした。こうした高度なツールでは「gemを追加してbundle installしておしまい」というアプローチは当てはまりません。WebSocketサーバーをインストールし、development環境とproduction環境のフレームワーク設定を更新し、潜在的な非互換性についてユーザーに通知する必要があります。

その結果「インタラクティブなRailsジェネレータ」(rails g anycable:setup)というアイデアにたどり着きました。以下を含む初期セットアップタスクのほとんどがジェネレータで自動化されます。

  • AnyCableに必要な設定ファイルやセッテイングを追加する
  • プロジェクトでDeviseを使っている場合、AnyCableの認証サポートを事前に設定する
  • anycable-goサーバーのインストール(またはdevelopment環境でDockerを検出したらdocker-compose.ymlにこのサーバーを追加する)
  • アプリケーションをHerokuで実行するための準備(必要な場合)
  • 静的な互換性チェックの実行

AnyCableセットアップジェネレーターを動かしているところ
このスクリプトの設計では「ヒストリカルなAnyCableセットアップを添えてあるissueについてはすべてジェネレーターでカバーする」というルールに従いました。

なので、anycable:setupを実行して何か問題があったらお知らせいただければ修正いたします!

Action Cableとの互換性をさらに向上

私たちは、リモート切断機能のサポート(ActionCable.server.remote_connections.where(user: user).disconnect)や、チャネルのステートをアクション間で維持する機能(通常はAction Cableのインスタンス変数経由で行う)を導入することで、サポート外のAction Cable機能を4つからわずか2つに減らしました。

class RoomChannel < ApplicationCable::Channel
  # AnyCable API similar to attr_accessor
  state_attr_accessor :room

  def subscribed
    self.room = Room.find(params["room_id"])
    stream_for room
  end

  def speak(data)
    broadcast_to room, message: data["message"]
  end
end

当初は、インスタンス変数やattributeリーダー/ライターをうまいこと検出してハイジャックすることでAnyCableステート管理機能にこれらを追加することを構想していました。実際以下のようなアルゴリズムを実装してみたりもしたのです。

  • チャネルクラスのソースコードを解析して既知のインスタンス変数名をすべて抽出する(明示的に定義されているものやattr_{reader|writer|accessor}で定義されているもの)
  • それらの変数名をステートアクセサとして暗黙に登録する(state_attr_accessorを利用)
  • リーダー/ライターによるインスタンス変数の直接利用部分をすべて@room → self.roomのように書き直す(トランスパイルが好きでしょうがないものですから)

ここにどれだけのエッジケースがありうるか想像できますか?幸いその時点で引き返すことが可能だったので、引き返すことに決めました。そしてstate_attr_accessorがAnyCableがAction Cableに追加するメインかつ唯一のAPIメソッドになりました。

互換性に関連するもうひとつの大きな改善は、Rackミドルウェアを用いてRackのリクエスト情報を拡張する機能です。

Rackミドルウェアの典型的なユースケースはDevise認証です。Deviseの背後にはWardenがあり、これが最終的にミドルウェアに依存します。このミドルウェアはWarden::Managerインスタンスを初期化したものをrequest.env["warden"]に保存し、これが後でDeviseで使われます。

v1.0以前は、AnyCable内のリクエストオブジェクトはアプリケーションミドルウェアがまったく考慮されていませんでした。このため、認証のようなよくあるシナリオが以下のコード例のように複雑になっていたのです。

# AnyCable <1.0
def connect
  self.user = find_verified_user || reject_unauthorized_connection
end

def find_verified_user
  app_cookies_key = Rails.application.config.session_options[:key] ||
                    raise("No session cookies key in config")

  env["rack.session"] = cookies.encrypted[app_cookies_key]
  Warden::SessionSerializer.new(env).fetch(:user)
end

AnyCable 1.0でRackミドルウェアがサポートされたことで、上のコードが以下のようにAction Cable向けに既にお使いであろうコードと非常に近いものになりました。

# AnyCable >=1.0
def connect
  self.user = env["warden"].user(:user) || reject_unauthorized_connection
end

この機能によって、request.sessionもAnyCable内でいつでもアクセス可能になりました。おかげで、次にお話しする別の互換性問題の解決にも役立ちました。

AnyCableと相性抜群のStimulus Reflex

Stimulus Reflexおよびその関連プロジェクトであるCableReadyはRuby on Railsコミュニティで続々と人気を得ていますが、無理もないでしょう。いかなるフロントエンドフレームワークにもロックインされることのない「極めてシンプル」「サーバーサイドレンダリング」「HTML over wire」アプローチというアイデアに皆さんが興奮しないわけがありませんよね。

残念ながら、AnyCable v0.6の時点でStimulus Reflexを最初に実行してみようとしたところ、「クラシックな」Action Cableアプリでは見たこともないようなissueが続々見つかりました。私はいかなる代償を払ってでもこれらを修正することを決意し、そして今やStimulusReflexExpoはAnyCable上で何の問題もなくすいすい動いています!

新しいデモアプリ

1個のよいサンプルアプリは、100万行のドキュメントに優る。

私たちは最初のリリース時からデモアプリを用意していたのですが、あっという間に役立たずの化け物に変わってしまいました。最も大きな過ちは、1つのコードベースにあらゆるユースケースを全部盛りにしてしまったことと、正しいテストカバレッジもCI設定もなかったことでした(おかげでアップグレードのつらかったこと…)。

そこで、古いアプリを修正するのではなく、まったく新しいアプリをスクラッチから作り直すことに決めました。AnyWork: AnyCable Rails Demoをご覧ください。

AnyCable Railsデモアプリより
構築にRails 6、StimulusTailwindCSSといったモダンなツールを用いたことで、このサンプルアプリはさながら「オムニアプリ」とでもいうべき仕上がりとなりました。つまりアプリのブランチごとにさまざまなバリエーションが用意され、それぞれが異なる利用シナリオを表すようになりましたし、どのバリエーションのプルリクにも詳細な記述を用意してあります。たとえば「From Action to Any」をご覧ください。

そういうわけで、デモアプリはあたかも実際のコードに沿った別の形のドキュメントのようなものになっています。私たちは、ドキュメント記事のほとんどをその記事専用のデモアプリのバリエーションにリンクすることを計画中です。更新情報を見逃したくない方はリポジトリにご登録ください!

Herokuへのデプロイ手順の改良

現在も、AnyCableをHerokuにデプロイするには2つのアプリケーションが必要になります。(Herokuアドオンも含めて)これを回避する方法も検討しつつ、私たちがproductionで蓄積したHerokuの経験を元にして既存のAnyCableドキュメントの改良を進めているところです。

Y Combinatorが設立したスタートアップがHerokuでスケールするために私たちが提供した支援については「Big on Heroku: Scaling Fountain without losing a drop」をご覧ください。

個人的に最も興味深い追加ドキュメントは「Choosing the right formation」です。この章には以下のように、アプリケーションの負荷に応じて必要なdyno数を算出する公式が提供されています。

Heroku formation formula

Heroku設定の算出

Cableとの4年間で得た教訓

私はAnyCable以外にも、RailsのAction Cableテスト、Railsの外の世界に向けたLite Cable、GUIよりもターミナル操作が好きな人向けのAction Cable CLIを手掛けてきました。

この4年というもの、AnyCableを始めとするcableと名の付くものたちに相当な時間を捧げてきました。その間にさまざまな決定を迫られ、中にはその決定で最終的に自分が幸せになりそこねたこともありました。

まぶしく輝く未来を築き上げるには、過去の振り返りが必要です。それでは始めましょう!

「実にクールなプロジェクトだよ」DHH

AnyCableという名前を最初にドキュメントにしたためた日は、今を去ること2016年06月10日のことでした。開催日の迫ったカンファレンス発表のネタをあれこれ考えながらAnyCableをこしらえたのです。当時の私はRailsClub Moscow(現在はRubyRussiaに改名)で発表したいと思っていたのですが、話すネタに困っていました。そこでEvil Martiansの同僚たちといくつかのアイデアを共有し、その中でウケのよかった「改良版Action Cable」を発表してみたくなり、プロトタイプ制作意欲も高まりました。こうして私はカンファレンスで発表するようになり、カンファレンスのネタのためにオープンソースプロジェクトをメンテナンスするようになったというわけです。

実を言うと、2017年後半まで、私はもちろんMartiansのメンバーの誰もAnyCableをproductionで使ってなどいませんでした(素のAction Cableすら使ってなかったのですが)。その1年半ちょっとの間、私はずっと手探り状態が続いていました。私にあるものといえば、ユーザーがGitHubに投げたいくつかのissueとGitterのチャンネルだけでした。DHHが「実にクールなプロジェクトだよ」と絶賛したプロジェクト↓にもかかわらず、当時の私はモチベの熱もほとんど冷めてしまい、いつしか「カンファレンスドリブンな」開発者と成り果てていました。v0.5.0は私がRubyConfMYで発表したタイミングでリリースされ、0.6.0はRubyConfのタイミングでリリースされました。Q&Aタイムで最も答えに窮したのは「AnyCableをどんなふうにproductionで使ってますか?」という質問を受けたときでした。「A dinnae, ye ken」と答えたときの私は、インチキ薬のセールスマンにでもなったような心持ちでした(訳注: A dinnae, ye kenは「I don’t know」のスコットランド方言をおどけて使ったと思われます)。

この状況が変わったのは、2018年の暮れのことでした。多くのプロジェクトが続々とAction Cableを採用し始めるようになり、Evil MartiansもeBayなどの顧客で採用した後も勢いは止まらず、プロジェクトのいくつかは大きく成長し、高負荷をうまく扱うにはAnyCableが必要だということが世に知られるようになりました。Evil Martiansにも商用サポートの引き合いが増え始め、数年前の「楽しいプロジェクト」にもとうとうバトルテストの試練を受ける機会が巡ってきたのです。

「オープンソースのためのオープンソース」は楽しくない。

AnyCableは試練を乗り越えたものの、自分のつらみに直接関連しない巨大オープンソースプロジェクトには今後関わるまいと当時ひとりごちたものでした。

AnyCable-Goですべてが変わった

AnyCableの最初のサーバー実装はErlangで書かれていましたが、優れたgRPCツール、配布の容易さ(シングルバイナリはシンプルの極みでしょう)、コミュニティの大きさ(つまりコントリビューターも多い)を備えているGo言語に乗り換えたのは正しい選択でした。

それにGoでの開発は滑り出しから驚くほど短期間で書けました。最初に動くようになったバージョンをビルドしたときはわずか1週間、コミット数にいたってはたった8つです!当時の私はGoの初心者同然でしたし、Goの歴史も現在より浅かったこともあって、(Rubyコミュニティと比べて)コード編成のベストプラクティスを見い出すのにかなり手こずりました。そして選んだのが「典型的なGo way」です。mainパッケージは1つだけ、リポジトリのルートディレクトリにいくつかファイルを配置、強結合、テストは「なし」または少なめ(なお後述のブラックボックステストは行いましたが)、という具合です。さて、こんなことをしてたら「メンテほぼ不能」への道に迷い込むでしょうか?

v0.6.0では多くの機能を盛り込む計画になっていましたが、後に実現したそれらの機能をアーキテクチャとして実装するのは、明らかに当時の私には無理そうでした(そもそもアーキテクチャ不在だったとも言えます)。そして大リファクタリング大会が始まったのです。

The Code City visualization for AnyCable-Go

GoCityによるAnyCable-Goビジュアル表示: v0.5.0(左)、v1.0.0(右)
このリファクタリングは「Structuring applications in Go」という他とは一味違うブログ記事にヒントを得て、Goコードのマイクロオプティマイゼーション、エラーハンドリング、ポインタの利用について学びました。さらに設計(私のRub目線で申し上げるならアーキテクチャ)の優れたGoのオープンソースプロジェクトを物色し始め、その結果faktorycentrifugotelegrafというプロジェクトを見い出しました。他の人が書いたコードを参考にしたり(ときには拝借したり)することで、散らかったコードを現在楽しく作業できるコードベースに変えるのに役立ちました。

複雑で信頼性の高いソフトウェアを書くのは決して容易ではない。

今思えば、その「先人の知恵」を最初から取り入れておけばよさそうなものなのに、なぜ私はそうしなかったのか。おそらく、Go言語は学びやすく短期間にリリースできるという評判に釣られたのでしょう(実際のアプローチは「一発しばいてデプロイ完了(訳注: 「slap shit together and deploy」はロシアで最近流行りのITジョークで、ろくにテストせずにデプロイする行為を指すそうです)」に近いことがわかってきました)。Goといえども、複雑で信頼性の高いソフトウェアを書くのは決して容易ではありません。

日の目を見なかった「AnyCable-bility」という名前

Action Cable互換のWebSocketサーバー向けにコンフォーマンステスト(訳注: コンプライアンステストとも呼ばれます)のツール、すなわちAnyTを書いたことは、私が決して後悔していないことのひとつです。AnyTはクライアント〜サーバー間のさまざまな通信シナリオを記述する結合テストのコレクションであり、テスト対象サーバーに沿って実行するCLIです。

開発ツールへの投資は、長期的には報われるものである。

こうしたツールがなかったら、AnyCable Rack serverのような新しいサーバーの実装を書くことも、既存のサーバーのリファクタリングも、はるかに難しい作業になったことでしょう。AnyTのおかげでリグレッション(=バグの再発)を避け、AnyCableの開発をAction Cableと同期させることができました(私たちはAction Cableでもテストを実行することにしています)。

AnyTについてひとつ面白い話があります。当初このツールの名前は「anycablebility」で、その名前でリリースまで行いましたが、その後奇妙極まりないことが起きました。私のRubyGemsサイトのオーナーシップが盗まれたのです🙀!アクセス権を取り返せなかったので、いっそ名前を変えることにしました。皆さんもきっと新しい名前の方がいいですよね?

Rails互換性の二面性

前述したとおり、AnyCableはAction Cableを置き換えないアドオンとして設計されました。実際私たちはAction CableのRubyコードやクライアントのJavaScriptライブラリに今も依存しています。

Action Cableを意識的にサポートしていなかったら、ここまで多くのユーザーを勝ち得ることはなかったでしょう。つまり戦略は正しかったというわけです。

一方、AnyCableが進化するに連れて依存性は低下しつつあります。互換性に関するリソースを調査する必要がありますが、欲しい機能をすべて追加するというわけにはいきません。その場合、Action Cableのコードのハックやカスタムクライアントの作成が必須になるでしょう。

現在のこの状況は、ちょうどGitHub向けCLIであるhubで起きた話と似ています。私たちも同じ教訓を身をもって思い知ったのでした。

AnyからManyへ: ケーブルの未来のために皆さんのお力を求めています

v1.0リリースの目的は2つあります。1つ目はAnyCableが安定し、productionで使えることを知らしめる(実際は割と前からそうなっていますが)。2つ目は私にとって重要な点である、v2の開発を始められる状態になったということです!

ここからはAnyCableの将来について願望ベースで語っていきたいと思います。

AnyCable 2: プロトコルの改善、Rubyやその他の言語で使える独自の「チャネル」フレームワークを自前で持つ、JSクライアントの現代化、WebSocketゲートウェイ。

AnyCable 2.0は革命的なパラダイムシフトになります。私たちは今後Action Cableの猿真似をやめます。

まず第1に、プロトコルを見直したいと思っています。たとえば、一意のセッションID、ストリーム内で単調に増加するメッセージID、アクション(perform)のACK、バッチ操作などを考えています。これらの変更が完了すれば、信頼性の高いデリバリーや真のRPCエクスペリエンスといった機能を追加しやすくなります。

プロトコルを変更するとなると、クライアント側のコードも新たに書く必要が生じるでしょうし、新しいプロトコルをサポートするためだけではなく、開発エクスペリエンスを改善するためにも既存コードの書き直しが必要になるでしょう。

以下は、AnyCable JSクライアントの理論上のコード例です。

// channels/chat.js
import { Channel } from 'anycable'

export default class extends Channel {
  static identifier = 'chat';

  fetchHistory = () => this.perform('fetchHistory')
}

// index.js
import ChatChannel from 'channels/chat'

const roomId = 42
// JSのcamelCase形式キーはサーバー側で自動的にRubyのsnake_case形式に変換される
const channel = new ChatChannel({roomId})

// async呼び出しはすべてPromiseベースとすることでawaitが使えるようになる
// (もちろんasync関数の内部で)
await channel.connect()

// 別形式のイベントAPI
channel.on('connect', () => console.log('Connected'))

// メッセージのACKはここで使われる
const messages = await channel.fetchHistory()

// 受信メッセージへサブスクライブする
channel.on('message', message => console.log(message))

さらに別の便利機能として、(Loguxでやっているような)コネクションをブラウザタブ間で共有できる機能を最初からサポートする計画もあります。

上のコード例では、チャネルIDにRubyのクラス名ではなく"chat"のようなシンプルな文字列を用いていることにご注目ください。抽象化の漏洩(leaky abstraction)よりは明示的に定義されたチャネルIDの方がずっとよいと信じています。クライアントアプリはこちら側のRubyコードの詳細について関知すべきではありません。

他の多くの問題と同様、この問題を排除するために「チャネル」用のカスタムフレームワークを提供する予定ですが、APIはAction Cableと「十分近い」ものにする予定です。そうすることで、ほとんどの場合application_cable/channel.rbのActionCable::Channel::BaseAnyCable::Channel::Baseに変更するだけでシンプルに移行できるようになります。

つまりAnyCableのRuby APIは、現在サポートされているAction Cable APIの上位セットになる。

このカスタムフレームワークは「ケーブルの種類を問わない」ものになります。フレームワークの責務はビジネスロジックに限定され、特定の転送手段やサーバー実装に関する知識を一切持ち合わせません。これによって、AnyCable以外にFalconIodineなどでもフレームワークを利用できるようになります。

人気の高いリアルタイム機能のいくつかは、すぐ使える形で同梱されるか、プラグインとして提供されます。こうした機能として、「存在トラッキング」や「チャネルレス・サブスクリプション」などを考えています。

しかしそれだけでは終わりません。

私たちが素晴らしい新プロトコルを手にしてRailsへの依存を切り離すときが来たら、おそらく次はRuby以外への移植も検討するときでしょう。AnyCable for PythonやAnyCable for PHPがあったらいいと思いませんか?

AnyCableをさまざまな言語で動かすことを検討するうちに、AnyCableをWebSocketゲートウェイとして使うという、くらくらするような素晴らしいアイデアをもうひとつ思いつきました。AnyCable-Goにルーティング機構を追加すれば、さまざまなバックエンドでさまざまなチャネルをさばけるようになるでしょう。クライアント側はマイクロサービスアーキテクチャの詳細について一切知識を持つべきではなく、1つのコネクションですべてを消費できるようになるのです!Apollo FederationでやれるようなことをWebSocketで行えるというわけです。

少々先走りすぎたようなのでこの辺にしておきましょう。私はAction Cableのアドオンに長い年月を捧げてきましたが、上で述べた夢が実現するには果たしてどのぐらいの年月がかかるでしょうか?10年かそこらでしょうか?しかしこれはオープンソースプロジェクトですし、自分が「退屈な」商用開発に従事していることを忘れないようにしないと…

AnyCableがGitHub Sponsorsに登録しました

そういったわけで、GitHub SponsorsプログラムでAnyCableのスポンサーシッププログラムを立ち上げてみる気になりました。スポンサーシッププログラムによって、私や他のコントリビュータが業務時間の外で貴重な時間をAnyCableに捧げられるようになります。私たちと一緒に素敵なリアルタイムの未来を築き上げましょう❤

AnyCableを真に自分のものにしましょう

AnyCableは拡張も設定も簡単です。特定のスタートアップ事業向けのファインチューニングを必要とする方にはカスタムソリューションや商用サポートを提供しています。ぜひAnyCable公式Webサイトをチェックしてください。そしてproduction環境で必要なものについて議論したい方は、いつでもお気軽にEvil Martiansのフォームまでご一報ください。


AnyCableは、使いやすいAction Cable APIのスピードアップと信頼性の向上をもたらします。だからこそ、AnyCableが当初「強化版Action Cable(Action Cable on steroid)」と呼ばれていたのです。皆さんのRuby on Railsアプリケーションがリアルタイム機能に依存しているのであれば、AnyCableの導入こそがインフラコストを節約しつつユーザーに素晴らしいリアルタイム機能を提供する上で最もシンプルな方法です。AnyCableには、「分析機能」「Prometheusとの統合」「無切断デプロイ」「Rails以外のアプリのサポート」といったproduction環境で必要となる多くの機能がすぐ使える状態で同梱されています。

おたより発掘

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

「一発しばいてデプロイ完了」は味わい深い

2020/07/29 17:48

関連記事

Ruby NextトランスパイラでRubyの新機能を使おう(翻訳)

Fullstaq Rubyの第一印象とDocker/Kubenetes Rubyアプリとの統合(翻訳)

週刊Railsウォッチ(20200803前編)書籍『パーフェクトRuby on Rails』増補改訂版、マルチDBで抽象クラスをscaffold生成、GitLabがPumaに乗り換えほか

$
0
0

一週間ぶりのご無沙汰です、hachi8833です。医師がまとめた以下のPDFを知人の医者が推薦しておりました。


つっつきボイス:「今年も半分以上過ぎましたね」「やめて〜聞きたくない😆

「上のスライドざっと見ましたけどわかりやすくていいですよね」「でも読んで欲しい人ほど読んでくれなかったりするという😆」「永遠の課題ですね…」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。

⚓新機能: マルチデータベースで抽象クラスをscaffold自動生成

  • マルチデータベースで抽象クラスを自動生成する

マルチプルデータベースのアプリでscaffoldによる生成を行う場合、現在のRailsでは--database=引数を渡しても抽象クラスを生成しない。この抽象クラスには設定を書き込むための接続情報が含まれ、そのデータベース用に生成されるあらゆるモデルが自動的にこの抽象クラスを継承するようになる。

使い方

  • 以下はanimalsコネクションから抽象クラスを1つ生成する
rails generate scaffold Pet name:string --database=animals
class AnimalsRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :animals }
end
  • このAnimalsRecordを継承するPetモデルを生成する
class Pet < AnimalsRecord
end
  • 既に抽象クラスが作成済みで、Railsデフォルトと異なるパターンに従う場合、--database=を引数に取る親クラスを渡せるようになる
rails generate scaffold Pet name:string --database=animals --parent=SecondaryBase
  • これにより、AnimalsRecrdではなくSecondaryBase`という親から継承できるようになる
class Pet < SecondaryBase
end

Changelogより大意


つっつきボイス:「マルチデータベース、今使いまくってます」「おぉ、勇気ある!」「メインのデータベースをリードオンリーでやってるんですけど、クラスを上書きしないといけないとか、ハマりましたよ」「ApplicationRecordに相当するものを複数作って、接続先に応じて継承元を変えるというのは、自分もswitch_pointでそういう実装してました」

「データベースが切り替わったときにActive Storageのattachやblogも切り替えるようにしたかったんですけど」「あ、それやると面倒になりがちです」「はい、とても面倒くさくなりました😅」「内部でjoinするコードが自動生成されたりするとややこしくなりそうですし」「やりながら技術的負債になるよなこれって思いました…後で触りたくない…😇


「で、このプルリクは、まさに今話したような、switch_point gemなんかでよく行われる、抽象クラスの作成をジェネレーターでサポートしたということですね: self.abstract_class = trueで抽象クラスを有効にしたものを作るところまでやってくれると↓」

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :animals }
end

「何と!そんなことしてくれるようになったとは…それ使いたかった〜😢」「そこは自分で抽象クラスを手書きすればいいじゃないですか😆」「まあそうなんですけど、ちょっと悔しい」「見事入れ違いになっちゃいましたね」「まあここで生成しているものはswitch_pointを使ってる人なら普通にやってることですし、みんなだいたいこう実装するんだからジェネレートをサポートしたということで」

「ここではApplicationRecordを継承する形で抽象クラスを生成してますね: ちなみに自分はApplicationRecordと並列する形で接続先に応じた抽象クラスを書いてますけど、まあそこは好みでしょうね」「なるほど、AnimalRecordを継承すると抽象クラスで指定したデータベースに切り替わるんですね↓: writingを指定する度胸はまだありませんけど😅」「writingにするかどうかは用途次第かな」

class Pet < AnimalsRecord
end

⚓Active Storage: 添付ファイルがパージされたときに親モデルをtouchできるようになった

現在のdeletepurgepurge_laterメソッドで用いられているが、そのせいで添付ファイルがパージされたときに親モデルを更新するコールバックをトリガできない。この振る舞いが原因で、#39858で報告されているようなキャッシュ戦略上の問題がいくつか発生している。
変更点:

  • attachment#purgerecord&.touchを追加
  • attachment#purge_laterrecord&.touchを追加
  • ついでにattachment.rbの無駄な空行を削除
  • 変更前だとfailし、変更後ならパスするテストを追加

その他情報:
deletedestroyに変更することはしなかった(after_destroy_commit :purge_dependent_blob_laterのトリガを回避するため)。
同PRより大意


つっつきボイス:「touchされてないとカウンタキャッシュなんかが正しく更新されなくなってしまうので、これは必要な修正でしょうね」


「最近すっかりActive Storageおじさんになっちゃいました」「この間の記事↓ありがとうございます🙏」「いえいえ〜Active Storageネタまだまだあるんでまた書きますよ」「お〜😂

ActiveStorageでアップロードしたファイルとプレビュー画像に認証をかける

「この記事でやってるような認証は、他のストレージエンジンでもだいたいこうせざるを得ないですよね」「S3なんかだと期限付きURLがあったりとか、ストレージエンジンによって挙動が違う部分もありますし」

⚓Redisキャッシュストアでの複数の問題を修正

つっつきボイス:「Redisがらみで似たような修正が3つほどあったのでまとめて貼りました」


Redisキャッシュストアでfetch_multiに渡したオプションがwrite_multiには正しく渡されるがread_multiに正しく渡されなかった。このためnamespace:オプションを渡すと常に失われてしまっていた。このプルリクで修正された問題を再現するスニペットは以下のとおり(テストにも追加済み)。
同PRより大意

store = ActiveSupport::Cache::RedisCacheStore.new
store.fetch_multi("ab", "xy", namespace: "mynamespace") { |key| puts "missed cache for #{key}"; key }
store.fetch_multi("ab", "xy", namespace: "mynamespace") { |key| puts "missed cache for #{key}"; key }

「なるほど、read_multi_mgetにオプションを渡しそびれてたのね↓」

# activesupport/lib/active_support/cache/redis_cache_store.rb#L352
        def read_multi_entries(names, **options)
          if mget_capable?
-           read_multi_mget(*names)
+           read_multi_mget(*names, **options)
          else
            super
          end
        end

fetch_multiread_multiがそれぞれあるというのが何とも」「Redisの内部では最終的にMGETあたりになるんでしょうけど↓」


  • MemCacheStoreDalli gem↓の圧縮を無効にした
  • Dalliで余分な圧縮がかかる問題によって圧縮が2回行われたり、指定の圧縮スレッショルド以下にもかかわらず圧縮がかかったりする問題を修正した
    同PRより大意

  • Redisやmemcachedのキャッシュストアでraw:trueを指定して読み出すと値が圧縮される問題を修正
  • CPUに負荷をかける不要な操作を防いだことでrawキャッシュの読み出しも高速化したはず
    同PRより大意

「次の2つは、Dalliで無駄な圧縮が発生しないようにしたそうです」「Dalli最近使ってなかったな〜」「Redisが使える状況ならあえてmemcachedベースのストアを使う理由もありませんけどね」「ちなみにDalliはmemcachedストアのクライアント」「Redisにrawモードがあるって知らなかった」「修正前はrawでないときは圧縮かけてたのか」

petergoldstein/dalli - GitHub

「そもそも最近memcached使ってませんし」「Redisがあるから😆」「たしかにmemcachedは構築が楽なんですけど、後からRedis的なこともやりたくなるんだったら最初からRedisにしておく方がいいでしょうし」「memcachedのメリットは超絶シンプルなことぐらいですし」「memcachedはクラッシュすると飛んじゃいますし」「そうなんですよ…」

「でもRedisはRedisで、永続化データベースのつもりで使われまくって後で地獄を見るというのもよくありますし👹」「ちょっと前に流行った、NoSQLをMySQLみたいに使うのと似てますよね」「それそれ」「Redisを更新しないといけなくなると、アップデートの間はサービスが数時間停止したり」「まあmemcachedはプロセスを再起動しただけで消えちゃいますけど」


「たしかmemcachedってRailsよりもさらに昔からあった気がしますね」「めちゃレガシー!」「Wikipediaの英語版↓見ると2003年からですって」「GitHubより昔」「ボクの職歴より長い😆」「大学の課題で組み込み機器の内蔵フラッシュストレージに書き込めないという条件を回避するためにmemcachedに保存してたことがあったぐらいだから相当昔ですね〜」「そんなことやってたんですか😆」「そのぐらいmemcachedはシンプルですしコードベースも小さかったと思います」

参考: Memcached - Wikipedia

⚓Action MailboxでX-Original-ToにSendgrid envelopeのrecipientを設定するようになった

SendgridではMailgunと同様、オリジナルのペイロードにあるenvelope JSONにあるパラメータとしてのみBCC受信者を渡すらしい。
このプルリクはSendgridペイロードからのrecipientをprependするコードをX-Original-Toヘッダー以下のraw_emailに追加する。
#38738に多大なヒントを得た。
関連: #38446
同PRより大意


つっつきボイス:「あら、これからSendgrid実装しようと思ってたのに」

「Action Mailboxを使う予定なんですか?」「よく考えたらAction Mailboxでやらなくてもいいかな…」「他にいろんなやり方もあるので今ならそんなにこだわらなくてもいいでしょうし、Webhookでも普通にやれますし」「Action Mailbox自身もSendgridと連携するときにはWebhook使いますし」

参考: Webhookとは? - Qiita

「Sendgridなら使っている人多いし、いいんじゃないでしょうか」「障害多いですけど🤣」「ありゃ😆」「この頃月1に近いペースで落ちるってbabaさんもぼやいてました」「まあメールサービスに障害は付き物なので」「メールの到着が遅れるだけならまだいいんですけど、APIが死ぬとメール自体が到着しなくなるのが厄介」「メールをキューに入れてから死んで欲しい😆

⚓コレクション関連付けがきっかり1度だけオートセーブされるよう修正

再現手順

この振る舞いはRails 6.0.3で新しく発生した。
has_many through:関連付けが用いられ、かつjoinテーブルの2つの外部キーにuniqueness制約がある場合、作成直後のレコード1件を更新するとsaveが2回行われてしまい、uniqueness制約違反が発生する。

class Team < ApplicationRecord
  has_many :memberships
  has_many :players, through: :memberships
end

class Player < ApplicationRecord
  has_many :memberships
  has_many :teams, through: :memberships
end

class Membership < ApplicationRecord
  # membershipsのplayer_idとteam_idにuniquenessインデックスがあるとする
  belongs_to :player
  belongs_to :team 
end

player = Player.create!(teams: team])
player.update!(updated_at: Time.current)

期待される動作

Rails 6.0.2.2では、新しいPlayerMembershipが1つずつsaveされ、かつPlayerからTeamへもリンクされる。以後の更新も成功する。

実際の動作

2回目のMembership#update!で書き込まれたタイミングでActiveRecord::RecordNotUnique例外が発生する。
この問題はどうやら#39124の変更に関連しているらしい。この問題は回避可能だが、これはRailsの以前の振る舞いとは異なる予想外の変更だ。
同PRより大意


つっつきボイス:「この間(ウォッチ20200720)で取り上げた#39173があの後修正されたそうです」「裏で自動的にsaveされるのってちょい怖いですよね」

「テストコードで久しぶりにHABTM見た↓」「まだ動くんだな〜😆

# activerecord/test/models/post.rb#L289
class PostWithAfterCreateCallback < ActiveRecord::Base
  self.inheritance_column = :disabled
  self.table_name = "posts"
+ has_many :comments, foreign_key: :post_id
  has_and_belongs_to_many :categories, foreign_key: :post_id

  after_create do |post|
    update_attribute(:author_id, comments.first.id)
  end
end

参考: 2.8 has_many :throughhas_and_belongs_to_manyのどちらを選ぶか — Active Record の関連付け - Railsガイド
参考: HABTMリレーションシップは悪であるという論争 | A-Listers

⚓Rails

⚓書籍『パーフェクトRuby on Rails【増補改訂版】』


つっつきボイス:「前回のウォッチでは号外止まりだったので」「皆さん続々購入かけてるみたい」「まだ1/3ぐらいしか読んでません😅

「@joker1007さんがDockerの章をたくさん書いてくれたらしいですし↓」「お〜🎉

「こういう最新情報が書籍という形にまとまって手に入るというのはいいことだと思います!」「ホントありがたい🙏」「Railsに参入する初心者がたくさんいるからこそこういう本が出せて売れるわけですし」「Railsの強い人もこんなふうに読んでるそうです↓」「目次見ただけでもわかりみしかない」


「今日ちょうど昼の勉強会でも少し話したんですけど、Linuxカーネルの最新情報がまとまった日本語の本って(自分の観測範囲では)ここ十数年見かけていないんですよ」「そんなに🤣」「なのでLinux方面でそういう情報を追おうとするとホント大変」「自分が学生の頃に読んだ『詳解Linuxカーネル』のLinuxカーネルは2.6で、3.0が出るか出ないかの頃、あとは『Linuxデバイスドライバ』を読んでなるほどと思ったりしたんですけど、今同じ方法で勉強すると書いてあるとおりには動かない部分も出てくるでしょうし」「たしかに😢

参考: Linuxカーネル - Wikipedia

「こういう渋い本ってやっぱり売れないのかな」「『詳解Linuxカーネル』は少なくとも日本語の情報としてあそこまで網羅している本が今のところ見当たらなくて」

「Railsでも、どこにどういう機能があるかという脳内マップが一度出来上がれば、あとは差分を追いかければ済むけど、脳内マップがまだできてない人は、パーフェクトRailsやRailsガイドの他にも、まとまった本を何冊も読んで脳内マップを育てて機能を追いかけられるようにしておくのがよいと思います💪」「1冊で終わらせないと」「そういうときに古い本しかないと悲しいですよね」

⚓lambdaじゃなくてService ObjectやQuery Objectを使うメリットは?(Ruby on Rails Discussionsより)


つっつきボイス:「lambdaでどんだけでかいコードを書きたいのかと」「レスでも、lambdaだと大きすぎるときにService Objectとかを使うとありますね」「lambdaだとエラー時に吐くものがProcになるからデバッグ大変になるし」「処理をまとめるだけならlambdaでもできますけど、クラスや関数を使わないで全部lambdaで書いてもいいかというとそんなことはありませんし」

参考: class Proc (Ruby 2.7.0 リファレンスマニュアル)

「上の他にもdiscuss.rubyonrails.orgでいろいろ盛り上がってて、たとえば以下はapp/javascriptというディレクトリ名はちょっと違うのではという話題でした↓」

app/frontendとかapp/packsにしようぜって言ってる人も😆」「気持ちはわかるけど」「自分で好きな名前にカスタマイズすればいいと思いますけど」「この中ではapp/assets/webpackとかapp/assets/sprocketsがマシかな〜: それならwebpackerやsprocketsみたいなものを通る前のデータがここにあるというアピールになりそうですし、こういうディレクトリに置いたものはそのままの形では公開されないわけですし」

⚓GitLabがUnicornからPumaに乗り換えた話(Ruby Weeklyより)


つっつきボイス:「GitLabのWebサーバーがPumaになったのね」「いつの間に」「世間の多くもいつの間にかPumaに乗り換えてた感ありますね」「Pumaがこんなに普及したのって何ででしょう?」「たぶんUnicornの頃はスレッドセーフでないコードが残っていたからなかなか乗り換えられなかったんでしょうけど、今はスレッドセーフでないコードを書く人がほとんどいなくなったからPumaに乗り換えても大丈夫という流れになったのかもですね」「なるほど」「乗り換えた理由はやっぱりメモリ使用量か」

Pumaにした理由
メモリ肥大化の問題を多少なりとも解決できるのではないかと信じて初期調査を開始した。Unicornのシングルスレッドプロセスから乗り換えれば、実行されているプロセス数や各プロセスのメモリオーバーヘッドを削減できる。Rubyのプロセスはかなりのメモリを消費するが、スレッドのメモリ消費量は、アプリケーションメモリの大半を占めるワーカーよりずっと小さくなる。I/Oが発生するとスレッドは一時停止するが、別のスレッドは引き続きアプリケーションのリクエストを処理できる。そういうわけで、マルチスレッドはメモリやCPUをベストな形で活用し、メモリ消費をおよそ40%も削減できる。
同記事より抜粋・大意

「そういえばGitHubはUnicorn使ってた気がしますね」「今も500エラーページに怒ったユニコーン出ますよね」「GitHubともなるとUnicornもチューニングしまくってるでしょうし、おいそれとは乗り換えられないでしょうね」

後で調べましたが、GitHubの現在のWebサーバーが何なのかは裏が取れませんでした。

参考: [GitHub] プルリクするとユニコーンが怒り出すのだが ? - Atuweb 開発 Log

⚓GoodJob: PostgreSQLベースのマルチスレッドActive Jobバックエンド(Ruby Weeklyより)

bensheldon/good_job - GitHub


つっつきボイス:「ぐっじょぶっていう名前が😆」「ぽすぐれでDelayed::Jobみたいなことをやるそうで、ポーリングの設定もありました」

「実はPostgreSQLってこういう裏で定期的に何かするみたいなジョブバックエンドに使える機能もあるんですよ」「へ〜ぽすぐれにそんな機能があるなんて」「ぽすぐれには何でもあります」「MySQL勢だけどまたつっつき恒例のぽすぐれ乗り換え誘惑が〜😆」「使わない機能もいっぱいありますけどね」「選択肢があるのは大事: まあPostgreSQLのそういう機能にロックインされるとメンテが大変になったりもしますけど」

「このツールを使うと、データベースがぽすぐれならジョブキューもぽすぐれでやれるってことなんですね」「ですです、Redisが要らなくなる😆

⚓HashWithIndifferentAccess


つっつきボイス:「ついさっき公開された記事です」「ThorでHashWithIndifferentAccessexceptメソッドの挙動が微妙に変わったのか: 踏んだことはないけど」「区別しないはずなのに区別されてたんですね」

# 同記事より
# Rails 5.2以前
h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"bar"=>2}
# Rails 6.0
h = Thor::CoreExt::HashWithIndifferentAccess.new(foo: 1, bar: 2)
h.except(:foo) #=> {"foo"=>1, "bar"=>2}

参考: RailsのAPI ActiveSupport::HashWithIndifferentAccess

以下のプルリクが現在オープンされています。


追記(2020/08/04): koicさんから補足いただきました🙇

⚓その他Rails


つっつきボイス:「これも大事」「テストのメールドメインにはexample.comを使いましょう皆さん」「それ用でない他のドメインを使ったりすると攻撃されるきっかけになることもありますし、たとえ自分で持っているドメインでも何かのはずみで失効する可能性がありますし」「記事にもありますけど、テスト用に使っていいドメイン名はRFCとかで定義されてたと思います」

参考: 例示/実験用として利用できるドメイン名 - @IT
参考: JPドメイン名の活用について | よくある質問 | JPRS

「開発環境に限定するなら他のを使ってもいいのかなという気もしますけど」「でもそうする理由がありませんよね」「どうしてもexample.com以外でやりたいなら自分のメアドでやって欲しい🤣」「ドメイン階層をたどらないといけないテストは@localhostだとできないので、どっちみち@example.comとかにしないといけなくなりますし」

「ところでhoge.comって本当に実在するって知らなかった〜😆」「ありますよ〜😆

参考: hogeとは (ホゲとは) [単語記事] - ニコニコ大百科


以下はつっつき後に見つけたツイートです。


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200721後編)『パーフェクトRuby on Rails』増補改訂版発売間近、scan_left gemでレイジーなinjectほか

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

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

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly

2020年のRailsでブラウザテストを「正しく」行う方法(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。画像は原文からの引用です。

2020年のRailsでブラウザテストを「正しく」行う方法(翻訳)

本記事で、Ruby on Railsアプリケーションにおけるエンドツーエンドのブラウザテストのベストプラクティス集を学び、自分たちのプロジェクトに採用しましょう。JavaベースのSeleniumとおさらばし、純粋なRubyから直接Chrome DevToolsプロトコルを用いる、より薄く簡素なFerrumとCupriteの合せ技を学びましょう。Dockerで開発しているそこのお方、本記事ではその点もカバーしています!

Rubyコミュニティは熱心にテストを実施しています。Rubyにはおびただしいテスト用ライブラリがあり、テストをお題にしたブログ記事も何百件あるかわからないほどですし、その名もThe Ruby Testing podcastというRubyのテスト専門のポッドキャストまであるほどです。凄まじいことに、最多ダウンロードgemトップ3を独占しているのはいずれもRSpecテスティングフレームワークのコンポーネントです。

Rubyのテスト高速化については、私たちのブログ記事をご覧ください。
TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)
TestProf II: Factory therapy for your Ruby tests

Rubyでテストがこれほど盛んな理由のひとつはRailsの存在にあると私は信じています。Railsフレームワークはテストをできる限り楽しく書けるようにしてくれます。ほとんどの場合、網羅的なRailsテスティングガイドを参照すれば事足ります(少なくとも最初のうちは)。しかし物事に例外はつきものです。私たちの場合「システムテスト」がそれでした。

Railsアプリケーションでシステムテストを書いたりメンテしたりするのは、正直「楽しい」からほど遠い作業です。この問題に対処するために用いた私のアプローチは、今を去ること2013年に初めてCucumberドリブンのテストツイートを導入してからというもの、随分と進化を遂げました。そして2020年となった今、ついにこれまでノウハウの粋を蓄積した現在の私のセットアップを皆さまと共有できるまでになりました。本記事では以下のトピックを扱います。

⚓「システムテスト」とは何か

Rails界隈におけるシステムテスト(system test)は、主にエンドツーエンドテストの自動化を指すのに用いられます。この名称がRailsで「採用される」前は「フィーチャーテスト」だの「ブラウザテスト」だの、しまいには「受け入れテスト」(一般の受け入れテストとは理念からして違います)などといった不統一な用語が飛び交っていたものです。

Martin Fowlerのテスティングピラミッドテスティングダイヤモンドでもいいのですが)を思い出してみると、システムテストはその頂点に位置します。システムテストはプログラム全体をブラックボックスとして扱い、エンドユーザーの操作や期待事項をエミュレートするのが普通です。Webアプリケーションではそうしたテストをブラウザで実行する(あるいはRack Testなどのエミュレーションが)必要が生じる理由がこれです。

システムテストの典型的なアーキテクチャをちょっと見てみましょう。

システムテストのアーキテクチャ
そのためには、「アプリケーションを実行するWebサーバー」「ブラウザ」そして「テストランナー自身」という少なくとも3つの「プロセス」を管理しなくてはなりません(そのうちいくつかはRubyのスレッドかもしれませんが)。これはあくまで最小限であり、実践上はブラウザ制御APIを提供するツールも別途必要になります。テストに特化したブラウザをビルドすることでセットアップをシンプルにし、すぐ使えるAPIを提供する試みもいくつかありました(capybara-webkitPhantomJSなど)が、結局「現実の」ブラウザとの熾烈な互換性競争を勝ち抜いて生き残ったものはひとつもありません。

Railsでエンドツーエンドのテストを書く方法はこれだけではありません。たとえばCypressというJSフレームワークとIDEを使う方法もあります。私がこのCypressによるアプローチに踏み切らなかった理由は、マルチセッションのサポートがないという点に尽きます。マルチセッションのサポートは(私の作ったAnyCableのような😉)リアルタイムアプリケーションのテストになくてはならないからです。

言うまでもありませんが、テストに用いるツールをつなぎ合わせるためにはテストの依存関係にRuby gemをいくつも足さなければなりません。依存関係が膨れ上がれば、その分問題も増えます。たとえば必須のアドオンとして長年活躍してきたDatabase Cleaner gemがそうです。データベースのステートを自動ロールバックしたくても、スレッドごとに独自のデータベースコネクションが使われるので、トランザクションを使うわけにはいきません。そのため、かつては各テーブルでTRUNCATE ...DELETE FROM ...を使うしかありませんでしたが、これをやるとかなり速度が落ちてしまいます。私たちEvil Martiansは、全スレッドで共有されるコネクションを用いることでこの問題を解決しました(TestProf拡張)。やがてそれに近い機能をすぐ使える形でRails 5.1がリリースされました(#28083)。

そういうわけで、システムテストを追加すると開発環境やCI環境のメンテナンスコストが上昇するようになり、テストが失敗する可能性のある不安定な箇所が紛れ込むようになったのです。複雑怪奇なセットアップのせいで、今や不安定なテストはエンドツーエンドのテストで最もよく見かける問題となっています。そしてテストの不安定性のほとんどが、ブラウザとのやりとりで発生します。

私が2019年に標準的に用いていたsystem_helper.rbには、200行を超えるコードが含まれていました。皆さんが今後もSeleniumを使い続けたいなら、この設定は今も有効です。

Rails 5.1で導入されたシステムテストのおかげでシンプルなブラウザテストを維持できるようにはなったものの、スムーズに動かすためには未だに設定のいくつかを頑張る必要があります。

  • Webドライバの指定が必要(RailsではSeleniumの利用が前提とされている)
  • コンテナ化された環境(つまり開発用Docker環境を使う場合)ではシステムテストを独自に設定する必要がある
  • 柔軟に設定できない(スクリーンショットの保存パスなど)

2020年にふさわしい、システムテストを楽しくやれる方法をコードで詳しく見てみることにしましょう。

⚓Cupriteを用いたモダンなシステムテスト

Railsは、システムテストをSeleniumで実行することを前提にしています。SeleniumはWebブラウザ自動化方面で勝ち残ってきた実績のあるソフトウェアで、あらゆるブラウザに対応するユニバーサルなAPIや最も現実的なエクスペリエンスを提供することを目的としています。ユーザーによるブラウザ操作のエミュレーションでは、切れば血の出る本物の人間に勝るものはありません。

Seleniumのパワーを手に入れるには代償が必要です。ブラウザごとに固有のドライバをインストールしないといけませんし、現実のユーザー操作によるオーバーヘッドは、規模が大きくなるに連れて深刻になります(Seleniumのテストは相当遅いのが普通です)。

Seleniumが作られたのはだいぶ前の話であり、当時のブラウザには自動化の仕組みがまったく提供されていませんでした。この状況が変わったのは、何年か前にChrome用のCDPプロトコルが提供されてからです。CDPを使うことでブラウザセッションを直接操作できるようになり、途中に抽象レイヤやツールを挟む必要もありません。

それ以来CDPを活用したプロジェクトが山ほど登場しましたが、中でも最もよく知られているものといえば、Node.js用ブラウザ自動化ライブラリのPuppeteerでしょう。ではRuby方面ではどうでしょう?実はFerrumというRuby向けのCDPライブラリがあるのです。Ferrumは比較的歴史が浅いのですが、Puppeteerエクスペリエンスとの互換性も保たれています。そして私たちにとって重要な点は、Cupriteという付随プロジェクトも同梱されていることです。CupriteはピュアRuby製Capybaraドライバで、CDPを用いています。

訳注: cuprite(赤銅鉱)やferrum(鉄)は、Seleniumと同じく元素名や鉱物名つながりの命名と思われます。

私がCupriteを積極的に使い始めたのは2020年初頭からに過ぎません(実は昨年もやってみたのですが、当時はDocker環境でいくつか問題にぶち当たりました)が、Cupriteに賭けてみたことはまったく後悔していません。何しろシステムテストのセットアップが信じられないほどシンプルになりましたし(必要なのは愛しのChromeだけですよ!)、実行も目覚ましく高速です。実際、SeleniumからCupriteに移行してからいくつかテストが失敗したことがあったのですが、テストが落ちた理由は、単に従来のテストでは非同期関連のexpectationが正しく書かれてなかったというものでした。以前はSeleniumがめちゃめちゃ遅かったせいでテストがパスしていたのです。

いよいよ、私がCupriteで使っているピカピカな最新システムテスト用設定をご覧に入れることにしましょう。

⚓設定例(解説付き)

この例は、このところ私が手掛けているRuby on RailsのオープンソースプロジェクトであるAnyCable Rails Demoのものを使いました。このプロジェクトの目的は、つい先頃リリースされたAnyCable 1.0をRailsアプリで使う方法をデモすることですが、何しろ最新のシステムテストカバレッジがありますので、そのまま本記事でも使えました。

同プロジェクトではRSpecと、RSpecのシステムテスト用ラッパーを用いていますが、使われているアイデアのほとんどはそのままMinitestにも通用します。

まずはローカルPCでも十分動く、最小限のテスト例から。このコードはAnyCable Rails Demoのdemo/dockerlessブランチにあります。

最初はとりあえずGemfileを見てみましょう。

group :test do
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'cuprite'
end

「あれ?selenium-webdriver要るの?」「Seleniumって要らないんじゃなかったけ?」とお思いの方へ。Railsでは、皆さんの使うドライバとは独立にこのgemが必須扱いされていることがわかりました。私がRailsに投げた#39179の修正が首尾よくマージされていれば、Rails 6.1ではこのgemを削除できるようになります(訳注: マージ済みです)。

なお、私はシステムテストの設定ファイルをspec/system/support/フォルダの下に、以下のように複数ファイルに分割して配置し、専用のsystem_helper.rbで読み込んで使っています。

spec/
  system/
    support/
      better_rails_system_tests.rb
      capybara_setup.rb
      cuprite_setup.rb
      precompile_assets.rb
      ...
  system_helper.rb

それぞれの設定ファイルについて目的を見ていきましょう。

⚓system_helper.rb

system_helper.rbには、システムテストで用いるRSpecの一般的な設定の一部を含むこともありますが、通常は以下のようにシンプルです。

# RSpec Railsの一般的な設定を読み込む
require "rails_helper.rb"

# 設定ファイルとヘルパーを読み込む
Dir[File.join(__dir__, "system/support/**/*.rb")].sort.each { |file| require file }

続いて、自分のシステムspecファイルにrequire "system_helper"を追加して、この設定を有効にします。

私たちの場合、システムテストで使うヘルパーファイルとsupport/フォルダは他のものとは別にしてあります。これは、単体テストを1つ実行したいだけの場合に設定を増やしすぎないためです。

⚓capybara_setup.rb

このファイルにはCapybaraフレームワーク用の設定を置きます。

# spec/system/support/capybara_setup.rb

# 特にSeleniumを使う場合、開発者はこのmax wait timeを増やす傾向がよく見られる
# Cupriteの場合そうした設定は不要
# ここではCapybaraのデフォルト値を明示的に指定している
Capybara.default_max_wait_time = 2

# `has_text?`や同種のマッチャーでホワイトスペースを正規化する
# (つまり改行や末尾スペースなどは無視される)
# こうすることで、UIにささいな変更が入っても影響されにくくなる
Capybara.default_normalize_ws = true

# システムテストで生成されるファイル(スクショやダウンロードファイルなど)の置き場所
# このパスを外部から設定可能にしておくとCIなどで便利かもしれない
Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara")

このファイルには、Capybaraに当てる便利パッチも含まれています。パッチの利用目的については後ほど解説します。

# spec/system/support/capybara_setup.rb

Capybara.singleton_class.prepend(Module.new do
  attr_accessor :last_used_session

  def using_session(name, &block)
    self.last_used_session = name
    super
  ensure
    self.last_used_session = nil
  end
end)

Capybara.using_sessionを使うと、別のブラウザセッションを制御できるようになります。それによって、1つのテストシナリオで複数の独立セッションを制御できます。これはリアルタイム機能のテストで特に有用です(WebSocketで何かする場合など)。

上のパッチは、最後に使われたセッション名をトラッキングします。後でこの情報を元に、マルチセッションのテストで失敗時のスクリーンショットを撮ります。

⚓cuprite_setup.rb

このファイルはCupriteの設定を担当します。

# spec/system/support/cuprite_setup.rb

# 最初にCuprite〜Capybara統合を読み込む
require "capybara/cuprite"

# 続いてドライバを登録し、後で使えるようにする
# ここでは#driven_byメソッドを利用する
Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(
    app,
    **{
      window_size: [1200, 800],
      # Docker化環境向けの追加設定については、本記事の対応するセクションを参照のこと
      browser_options: {},
      # Chrome起動時のwait timeを増やす(CIビルドを安定させるのに必要)
      process_timeout: 10,
      # デバッグ機能を有効にする
      inspector: true,
      # HEADLESS環境変数をfalsyな値に設定することで
      # Chromeを「ヘッドフルモード」で実行できるようにする
      headless: !ENV["HEADLESS"].in?(%w[n 0 no false])
    }
  )
end

# Capybaraで:cupriteドライバをデフォルトで使うよう設定する
Capybara.default_driver = Capybara.javascript_driver = :cuprite

Cupriteでよく使われるAPIメソッドのショートカットもいくつか定義しています。

module CupriteHelpers
  # 実行を停止する#pauseメソッドはテストのどこにでも置ける
  # ヘッドフルモードのテスト中にWebページの表示内容をチェックしたい場合に便利
  def pause
    page.driver.pause
  end

  # Chromeインスペクタを開いて実行を一時停止する#debugメソッドは
  # テストのどこにでも置ける
  def debug(*args)
    page.driver.debug(*args)
  end
end

RSpec.configure do |config|
  config.include CupriteHelpers, type: :system
end

#debugヘルパーの動作については以下のデモをどうぞ。

システムテストのデバッグ

⚓better_rails_system_tests.rb

このファイルには、Railsシステムテストの内部に当てるパッチがいくつかと、一般的な設定が若干含まれています(詳しくはコードの説明をどうぞ)。

# spec/system/support/better_rails_system_tests.rb

module BetterRailsSystemTests
  # スクショやその他の生成物の保存場所に`Capybara.save_path`を指定する
  # (Railsのスクショのパスは設定不可能)
  # https://github.com/rails/rails/blob/49baf092439fc74fc3377b12e3334c3dd9d0752f/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L79
  def absolute_image_path
    Rails.root.join("#{Capybara.save_path}/screenshots/#{image_name}.png")
  end

  # 失敗したときのスクショをマルチセッションのセットアップと互換にする
  # 上で導入したCapybara.last_used_sessionはここで使う
  def take_screenshot
    return super unless Capybara.last_used_session

    Capybara.using_session(Capybara.last_used_session) { super }
  end
end

RSpec.configure do |config|
  config.include BetterRailsSystemTests, type: :system

  # メイラー内のURLに正しいサーバーホストが含まれるようにする
  # メールに含まれるリンクのテストで必要(capybara-emaiなど)
  config.around(:each, type: :system) do |ex|
    was_host = Rails.application.default_url_options[:host]
    Rails.application.default_url_options[:host] = Capybara.server_host
    ex.run
    Rails.application.default_url_options[:host] = was_host
  end

  # このフックが他のものより先に実行されるようにする
  config.prepend_before(:each, type: :system) do
    # 常にJSドライバを使うようにする
    driven_by Capybara.javascript_driver
  end
end

⚓precompile_assets.rb

このファイルは、システムテストの実行前にアセットのプリコンパイルを担当します(このファイルは長いので、最も興味深い部分のみを貼っておきます)。

RSpec.configure do |config|
  # システムspecを除外する場合はアセットプリコンパイルをスキップする
  # (以下のコマンドでシステムテスト以外を実行する場合など)
  #
  #    rspec --tag ~type:system
  #
  # なお、ここではアセットプリコンパイルは不要
  next if config.filter.opposite.rules[:type] == "system" || config.exclude_pattern.match?(%r{spec/system})

  config.before(:suite) do
    # テストでwebpack-dev-serverも使う
    # フロントエンドのコード修正をシステムテストで確認したい場合に便利
    if Webpacker.dev_server.running?
      $stdout.puts "\n⚙  Webpack dev server is running! Skip assets compilation.\n"
      next
    else
      $stdout.puts "\n🐢  Precompiling assets.\n"

      # webpacker:compile Rakeタスクを実行する
      # ...
    end
  end
end

Railsがアセットを自動でプリコンパイルしてくれるのであれば、手動でアセットをプリコンパイルする理由は何でしょうか?ここで問題なのは、Railsのアセットプリコンパイルが遅延実行されることです(つまりアセットに初めてリクエストをかけるとき)。最初のテストexampleでこれが発生すると実行が非常に遅くなり、タイムアウト例外がランダムに発生することすらあります。

もうひとつご注目いただきたい点は、webpack-dev-serverをシステムテストで利用できるという事実です。これはフロントエンドコードのリファクタリングで実にありがたい機能です!テストをpauseで一時停止してブラウザで開き、フロントエンドのコードを編集すればホットリロードされるのですから。

webpack-dev-serverをテストで使うには、test環境のwebpacker.ymlにdev_server設定を追加し、RAILS_ENV=test ./bin/webpack-dev-serverでテストを実行する必要があります。

⚓システムテストをDocker化する

それでは設定を次のレベルに進めて、Evil Martians流Docker開発環境と互換性を取れるようにしましょう。Docker化環境向けのテストのセットアップは、先のAnyCable Rails Demoリポジトリのデフォルトブランチにありますので、ご自由にチェックアウトいただけますが、本記事では同設定の興味深い点すべてをこの後取り上げます。

Docker版セットアップでの最大の違いは、ブラウザのインスタンスを別コンテナで実行する点です。Chromeは、ベースとなるRailsのDockerイメージに追加することもできますし、(おそらくですが)コンテナからホストマシンのブラウザを動かすことも可能でしょう(SeleniumとChromeDriverでできたように)。しかしこういうときには、docker-compose.ymlに専用のブラウザサービスを定義するのが正しい「Docker way」であると私は思っています。

現時点の私は、browserless.ioにあるChrome Dockerイメージを使っています。これには便利なデバッグビューアが付属しているので、ヘッドレスブラウザのセッションをデバッグできます(本記事末尾に簡単な動画を用意しましたのでお楽しみに!)。

services:
  # ...
  chrome:
    image: browserless/chrome:1.31-chrome-stable
    ports:
      - "3333:3333"
    # アプリケーションのソースコードをマウントしてファイルアップロードをサポート
    # (そうしないとファイルを見つけられない)
    # 注: `#attach_file`では絶対パスを用いること
    volumes:
      - .:/app:cached
    environment:
      # なおRailsではデフォルトに3000を設定するのが普通
      PORT: 3333
      # コネクションタイムアウトを設定して、デバッグ中のタイムアウト例外を回避する
      # https://docs.browserless.io/docs/docker.html#connection-timeout
      CONNECTION_TIMEOUT: 600000

CHROME_URL: http://chrome:3333を自分のRailsサービスの環境に追加し、以下のようにChromeをバックグラウンドで実行します。

docker-compose up -d chrome

次はCupriteを設定して、URLを与えられたときにCupriteからリモートブラウザを使えるようにします。

# cuprite_setup.rb

# URLをパースする
# 注意: 以下のいずれかを使う場合はREMOTE_CHROME_HOSTをWebmock/VCR許可リストに
# 追加すること
REMOTE_CHROME_URL = ENV["CHROME_URL"]
REMOTE_CHROME_HOST, REMOTE_CHROME_PORT =
  if REMOTE_CHROME_URL
    URI.parse(REMOTE_CHROME_URL).yield_self do |uri|
      [uri.host, uri.port]
    end
  end

# リモートのChromeが実行中かどうかをチェック
remote_chrome =
  begin
    if REMOTE_CHROME_URL.nil?
      false
    else
      Socket.tcp(REMOTE_CHROME_HOST, REMOTE_CHROME_PORT, connect_timeout: 1).close
      true
    end
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
    false
  end

remote_options = remote_chrome ? { url: REMOTE_CHROME_URL } : {}

上の設定では、「CHROME_URLが設定されていない場合」「ブラウザが応答しない場合」はユーザーがローカルインストール済みのChromeを使いたいはずだという前提になっています。

このようにして設定のローカルChromeとの後方互換性を保ちます(なお私たちは基本的にDockerを開発環境で使うことを強制しません: Docker嫌いマンの皆さまは独自のローカルセットアップを自力で頑張ってくださいまし😈)。

ドライバの登録部分は以下のような感じになります。

# spec/system/support/cuprite_setup.rb

Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(
    app,
    **{
      window_size: [1200, 800],
      browser_options: remote_chrome ? { "no-sandbox" => nil } : {},
      inspector: true
    }.merge(remote_options)
  )
end

もうひとつ、#debugヘルパーを更新して、ブラウザを開こうとする代わりにデバッグビューアのURLを出力するよう変更します。

module CupriteHelpers
  # ...

  def debug(binding = nil)
    $stdout.puts "🔎 Open Chrome inspector at http://localhost:3333"
    return binding.pry if binding

    page.driver.pause
  end
end

ブラウザは別の「マシン」で動作するのですから、テストサーバーに到達する方法を知っておく必要があります(テストサーバーはlocalhostをリッスンしなくなります)。

そのためには、Capybaraサーバーホストに以下の設定が必要です。

# spec/system/support/capybara_setup.rb

# 外部世界からサーバーにアクセスできるようにする
Capybara.server_host = "0.0.0.0"
# 内部のDockerネットワークで解決可能なhostnameを使うこと
# 注意: 6.1より前のRailsはCapybara.appをオーバーライドするので
# 以下のように別の方法で保存する必要がある
CAPYBARA_APP_HOST = `hostname`.strip&.downcase || "0.0.0.0"
# Rails 6.1以降は以下の設定でやれるはず
# Capybara.app_host = "http://#{`hostname`.strip&.downcase || "0.0.0.0"}"

最後に、better_rails_system_tests.rbファイルに軽く調整を加えます。

まず、スクリーンショットの通知をVSCodeでクリッカブルにしましょう🙂(Dockerの絶対パスがホストシステムのそれと異なるため)。

# spec/system/support/better_rails_system_tests.rb

module BetterRailsSystemTests
  # ...

  # スクリーンショットのメッセージに相対パスを使う
  def image_path
    absolute_image_path.relative_path_from(Rails.root).to_s
  end
end

お次は、正しいサーバーホストがテストで使われるようにします(なおRails 6.1では#d415eb4で修正済み)。

# spec/system/support/better_rails_system_tests.rb

config.prepend_before(:each, type: :system) do
  # Railsはデフォルトであらゆるテストのhostを`127.0.0.1`に設定する
  # しかしこれはリモートブラウザでは無効
  host! CAPYBARA_APP_HOST
  # 常にJSドライバを使う
  driven_by Capybara.javascript_driver
end

Dipならさらに便利に

訳注: Dipについては以下の記事もどうぞ。

docker-composeを便利にするツール「dip」を使ってみた

Docker化された開発環境の管理にdipをお使いであれば(このツールを強くおすすめします: dipを使えば、Dockerの長ったらしいCLIコマンドを覚える苦労なしにコンテナのパワーを発揮できるようになります)、dip.ymlに以下のようにカスタムコマンドを追加し、docker-compose.ymlにもサービス定義を追加することで、chromeサービスをいちいち手で起動しなくても済むようになります。

# docker-compose.yml

# Chromeを依存関係に追加する定義をシステムテスト用に分けておく
rspec_system:
  <<: *backend
  depends_on:
    <<: *backend_depends_on
    chrome:
      condition: service_started
# dip.yml
rspec:
  description: Run Rails unit tests
  service: rails
  environment:
    RAILS_ENV: test
  command: bundle exec rspec --exclude-pattern spec/system/**/*_spec.rb
  subcommands:
    system:
      description: Run Rails system tests
      service: rspec_system
      command: bundle exec rspec --pattern spec/system/**/*_spec.rb

あとは以下のコマンドを実行するだけでシステムテストが走ります。

dip rspec system

以上でおしまいです!

最後に、Browserless.ioのDockerイメージをデバッグビューアでデバッグするときの様子を動画でご紹介いたします。

Dockerで動くシステムテストをデバッグする

皆さんのエンジニアチームで(技術経験の多寡にかかわらず)確固たる開発ノウハウを確立したいとお考えの方は、どうぞお気軽にEvil Martiansのフォームにてお問い合わせください。企業のエンジニアリング文化の改善は私たちの大好物のひとつです!

原文Changelog

  • 1.0.1 (2020-07-30): Docker化セットアップでchromeサービスにボリュームの設定を追加。

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)

週刊Railsウォッチ(20200811山の日短縮版)RSpec Queueでパラレルテスト、カロリーメイトとRubyのコラボ、Rubyのcoercionほか

$
0
0

こんにちは、hachi8833です。昨日は山の日ということで短縮版でお送りします。

回答しそびれましたが、Ruby 2.7のirbがとてもよくなったので自分も最近pryを使わなくなってました。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Rails: 先週の改修(Rails公式ニュースより)

今回はchangelogの更新はありませんでした。

⚓validates_inclusion_ofメソッドにeach_object:オプションを追加

特定の属性に対応する値が配列の場合、バリデーションでは指定のメンバーにその配列が含まれているかどうかをチェックする。:each_objectを使うと配列内の各オブジェクトが指定のメンバの中になければならないということを指定できる。
これは、たとえばデータベースカラムでarray: trueオプションを使っていて、指定の有効な値リストが配列のメンバーに含まれているかどうかを指定したい場合に有用。
変更前: features属性が["Bluetooth"]を含む配列の場合、以下のバリデーションは失敗する(["IR", "Bluetooth", "Wireless"].include?(["Bluetooth"])がfalseになるので)。

validates_inclusion_of :features, in: %w(IR Bluetooth Wireless)

変更後: 以下はパスする(featuresの各オブジェクトが指定のメンバーに含まれているかどうかが実際にチェックされるので)。

validates_inclusion_of :features, in: %w(IR Bluetooth Wireless), each_object: true

Railsに初めて投げたプルリクにつき、フィードバックは大歓迎です。
同PRより大意


つっつきボイス:「だいぶ昔のプルリクがマージされていました」「2015年って書いてますけど互換性とか大丈夫だったのかしら?」「さすがにgit rebaseとかやってるでしょうけど」

validates_inclusion_ofっていう書き方がそもそも古いですし: Rails 3ぐらいじゃなかったっけ?」「あ、もっと簡潔な書き方があるんですね」「もう随分前からvalidates :age, inclusion:みたいに書くのが普通です」「道理でvalidates_inclusion_ofって見覚えないと思った😆

# api.rubyonrails.orgより
validates :terms, acceptance: true
validates :password, confirmation: true
validates :username, exclusion: { in: %w(admin superuser) }
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
validates :age, inclusion: { in: 0..9 }
validates :first_name, length: { maximum: 30 }
validates :age, numericality: true
validates :username, presence: true

「このプルリクは、今までだと:featuresがstringならうまくいくんですけど、array of stringだと期待どおりに動かなかったのをeach_object: trueを指定することで動くようにしたということでしょうね」

# 変更前
validates_inclusion_of :features, in: %w(IR Bluetooth Wireless)

# 変更後
validates_inclusion_of :features, in: %w(IR Bluetooth Wireless), each_object: true

「それにしても5年越しのマージとは」「こんなに塩漬けにされてたのって何でだろ?」「改善内容はわかるけど、たぶんこれで困る人が少なかったからじゃないですかね」「必要なものなら色んな人が推すでしょうし」「これなら自分でバリデーター作れるでしょうし」「コメント見ると年に1度ぐらいレスやstaleタグが付けられてますね」「七夕みたい💫」「そして4日前にマージされたと」「初プルリクがついにマージ、お疲れさまです…」「これでRailsのプルリクも1個減りましたね」

⚓マルチプルDBのdb:migrate:redoで名前を指定できるようになった

» bin/rails db:migrate:redo:secondary
== 20200728162820 CreateAnimals: reverting ====================================
-- drop_table(:animals)
   -> 0.0025s
== 20200728162820 CreateAnimals: reverted (0.0047s) ===========================

== 20200728162820 CreateAnimals: migrating ====================================
-- create_table(:animals)
   -> 0.0028s
== 20200728162820 CreateAnimals: migrated (0.0029s) ===========================

つっつきボイス:「ああマルチプルDBのdb:migrate:redoね」「これは?」「マルチプルDBのrakeタスクは指定先データベースの識別子を渡せるようになっているんですけど、redoで対応していなかったのを対応したんでしょうね」「redoの対応が漏れてたのか」

db:migrate:redoを開発で使う機会ってめったにないと思いません?」「自分もないな〜」「もちろん機能としては前からありますし、CIで使いたいときもありそうですけどね」

⚓StiClass.allで暗黙のcreate_withを回避するようにした

現在は暗黙のcreate_withを使っても使わなくても、ensure_proper_typeによってsti_nameが設定されている。

      def initialize_internals_callback
        super
        ensure_proper_type
      end

      # Sets the attribute used for single table inheritance to this class name if this is not the
      # ActiveRecord::Base descendant.
      # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
      # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
      # No such attribute would be set for objects of the Message class in that example.
      def ensure_proper_type
        klass = self.class
        if klass.finder_needs_type_condition?
          _write_attribute(klass.inheritance_column, klass.sti_name)
        end
      end

create_withtype_conditionを抑制したりオーバーライドするのが目的。type_conditionscope_for_createですべてのSTIサブクラス名の配列になるので、scope_for_createでは望ましい振る舞いにならず、find_sti_classが失敗する。
この想定外の振る舞いは、ArelのInノードが誤ってEqualityノードのサブクラスになっていたことによる。私見ではほぼバグと思われるが、この振る舞いに依存している人もいる(#39288も参照)。
残念ながらこの暗黙のcreate_withには、orがSTIサブクラスのリレーションで失敗するという不本意な副作用があった(#39956)。
今回の変更は、type_conditionを抑制したりオーバーライドする方法が変わる。#39956orの問題を修正するため、暗黙のcreate_withは、scope_for_createtype_conditionで除外される。
同PRより大意


つっつきボイス:「@kamipoさんによるArel周りの修正です」「Arel難しくて黙り込んでしまう自分😅」「説明にも書いてある#39956の問題↓を解決するためのプルリクなのでこっちを見る方がわかりやすそう」

「昔Arelのコードを叩いてゴニョゴニョするコードを見たことがあってですね」「あ〜それはつらいヤツ」「コードレビューに出すのがとてもつらくて、それ以来Arelの直いじりは止めて欲しいと思ってます」「その書き方は止めましょうと自信を持って言いたい😆」「少なくともメンテしやすいコードにはなりませんし」

参考: Rails 6.0で不要なArel.sqlを減らす - koicの日記

⚓selenium-webdriver gemが必須でなくなった

# actionpack/lib/action_dispatch/system_testing/driver.rb#L6
      def initialize(name, **options, &capabilities)
        @name = name
        @browser = Browser.new(options[:using])
        @screen_size = options[:screen_size]
        @options = options[:options] || {}
        @capabilities = capabilities

-       @browser.preload unless name == :rack_test
+       if name == :selenium
+         require "selenium/webdriver"
+         @browser.preload
+       end
      end

つっつきボイス:「これだけ5月にマージされたプルリクなんですけど、以下の翻訳記事の原著者が投げたプルリクとのことだったので拾ってみました↓」

2020年のRailsでブラウザテストを「正しく」行う方法(翻訳)

「Selenium使わないときでもドライバが入ってきてたので必須でないようにしたと」「poltergeistとかちょっと懐かしい名前が見えた」「他のWebブラウザドライバを使うならSelenium要らないので、もっともな修正」

teampoltergeist/poltergeist - GitHub

「Seleniumに限らないけど、使わないものが入ってくると違和感ありますね」「Seleniumって依存関係もサイズも大きそうな印象ありますし」「Webブラウザのドライバだけならそこまででかくなさそうですけど、どうなんだろう?」

参考: Seleniumプロジェクトとツール :: Seleniumドキュメント

Seleniumのロゴがいつの間にかフラットなデザインに変わってますね↓(参考: 以前のロゴ)。


selenium.devより

⚓Rails

⚓Railsのマイグレーションはスレッドセーフであるべきか(Ruby on Rails Discussionsより)


つっつきボイス:「Railsディスカッションから拾いました」「マイグレーションは稼働中のアプリでも動作させる前提になっていることを考えるとスレッドセーフであって欲しいですけど」「そりゃその方がいいですよね😆

「そうか、マルチプルDBだと複数のマイグレーションが同時に走る可能性もないわけじゃないのか」「schema migrations tableもDBごとにありそうですし」「この辺あんまり考えたことなかったな〜」

参考: Active Record マイグレーション - Railsガイド

「DB単位で同時にマイグレーションが走ることはないけど、DBをまたいだアプリケーション全体レベルで考えると2つのDBのマイグレーションが同時に実行されることはありそう」「そのときにスレッドセーフに書くべきかどうかということですか、やっと理解」「自分は踏んだことないんですけど、2つのデータベースが密に連携してたりするとデッドロックしたりするのかな?」「いわゆる競合状態のお手本にありそうなシチュエーションっぽい」

「この辺は推測になりますけど、Railsレベルでは複数DBを跨いだマイグレーションの同時実行制御はしてなさそうな気がします: DBをまたいで保証しようとしたら相当大変そうですし」「一般的にDBをまたいで相互にロックするようなマイグレーションはあんまりしないと思いますけど、やったらどうなるのかな?」

「マルチじゃない場合、schema migrations tableをトランザクションでロックするぐらいはしてるかも: そしたらrakeタスクが複数動いてもスレッドが同時にクリティカルセクションに入ることはなくなるのでrakeタスクを実行しても大丈夫でしょうけど」「それがマルチDBになると、複数のDBをまたぐ処理を書いたらスレッドアンセーフになる可能性はありそう」「あ、ちょっと見えてきたかも」「1つのデータベース単位ではマイグレーションがスレッドセーフでも、データベースが複数になったらクリティカルセクションを同時に通るときに何か起きそうな感じですね」


追いかけボイス: Railsレベルではマイグレーションロックがあるみたいですね。

参考: Railsのmigrationにロックが掛かっているのか調べた(Mysqlの場合) - Qiita

⚓bundle execすべきかどうか、それが問題だ(Ruby Weeklyより)


つっつきボイス: 「記事は見出し冒頭にあるようにBundler入門という感じですね」「割と基本的な話みたいでした」「サブタイトルの方が目立ってる😆

⚓Railsのbinstub

「ところで今のRails wayではbundle execよりbin/railsみたいなbinstubから実行するのが正式じゃなかったっけ?」「はい、最近はRailsガイドでもそうなってるはずです」「まあ自分は今もbundle execでやっちゃいますけど😆」「実は私も😆

参考: Rails のコマンドラインツール - Railsガイド

bin/railsって気にしたことなかったんですけど、bundle exec railsと同じということなんでしょうか?」「同等でよかったと思います」「へ〜」

「今のRails wayではbin/の下にあるbinstubもGitにコミットすることになってますし、.gitignoreでもbinstubは無視されないようになってますし」「え〜、自分まったく気づかずにずっとbundle execでやってた😭」「bundle execでも間違いではないと思います」「使っても大丈夫ですよ〜」

「あくまでRails wayとしてはbinstubが正式ということなので、Railsの教科書やリファレンスを書くならbin/railsとかで書くべきでしょうね」「なるほど、そういう本でbundle execを使いまくってたら違うと」「まあ細かい話なのでそんなに気にしすぎることはありませんけど、一応ということで」

「ところでいつ頃からRailsでbinstubが正式になったんだっけ?」「Rails 4あたり?」「そんな昔からだったんですね…」「binstubの話は当時ことさら話題になってなかったような覚えあります」

後で調べると、どうやらRails 4の頃みたいです↓。

参考: Rails 4.0 と bundler install --binstubs について - おもしろwebサービス開発日記


【翻訳+解説】binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動

⚓RSpec Queue: RSpecをパラレルに分散実行(Ruby Weeklyより)

skroutz/rspecq - GitHub


つっつきボイス:「Rspecをキューイングしてパラレルに実行できるヤツか」「RSpec Queueのgem名がrspecqってなっているのが個人的に好き❤」「短くてよくわかりますし」「こういうネーミングセンスを身に付けたい」

「こうやってワーカーを分けてビルドidを付けるといい感じにキューイングしてくれるみたい↓」「おぉ〜」

# 同リポジトリより
$ rspecq --build=123 --worker=foo1 spec/

「RSpecってそのままだとパラレルにならないから、こういう形でパラレルにすることになるのかな?」「完全おまかせでパラレルにするよりも多少制御はしやすいでしょうね」「すごく大きいテストだとうれしいかも」「前処理が極端に重いテストなんかも制御しやすいかも」「テストのワーカーをいっぱい立ち上げたときなんかだと、完全にラウンドロビンにするんじゃなくて多少制御したいこともありそうですし」

参考: ラウンドロビン - Wikipedia

⚓Rubyと「Firefox Send」で巨大ファイルをセキュアに転送する

Sendは無料の暗号化ファイル転送サービスで、任意のブラウザから安全かつシンプルにファイルを共有できます。エンドツーエンド暗号化を用いて、ファイルを共有した瞬間からファイルを開くまでの間のデータをセキュアに保ちます。
同記事より


つっつきボイス:「Firefox Sendって何でしょう?」「ブラウザ内のサービスなのか外部サービスなのかと思ったら、どうやらMozillaがやってる外部サービスみたい」「ブラウザ同士の認証不要なファイル転送をサポートするインフラというかサービスっぽい」「コマンドライン版もあるのね↓」

timvisee/ffsend - GitHub

「any browserってあるからどのブラウザでもやれそう」「記事ではOpen3でRubyから操作してますね↓」「デカいファイルを渡すときに四苦八苦することがときどきあるので、こういう手段があると知ってたら使うかも」「あとはそれを思い出せるかどうか😆

参考: module Open3 (Ruby 2.7.0 リファレンスマニュアル)


動画では期限を設定できて、最大2.5GBまで扱えるとありますね。なお、後で見たら「改装中」となっていました↓🚧


send.firefox.comより

⚓その他Rails


つっつきボイス:「ついさっき流れてきたツイートです」「Everyday Rails買うとこんなおまけがついてくるのね」「Rails独自のMinitest拡張ってあんまり気にしたことなかったかも」

参考: Everyday Rails… Aaron Sumner 著 et al. [Leanpub PDF/iPad/Kindle]

⚓RailsとMinitest

「自分はRailsアプリでMinitest使おうと思ったことないな〜」「お、自分は新しいRailsプロジェクトはしれっとMinitestで書き始めましたよ😋」「私も今度はこっそりMinitestにしようと思ってます」「え〜、マジですか?😳

参考: library minitest/unit (Ruby 2.0.0)

「RSpecってセットアップも毎回地味に面倒じゃないですか: 今やってるのは自分がrails newから始めてるので速攻でMinitestでセットアップしましたし」「やべ〜今までRSpecちまちま入れてたけど、今度は自分もMinitestでやってみようかな…😅」「作っているのはAPIサーバーなのでそんな高度なことがしたいわけでもありませんし、それならMinitestのassertでいいよねって思いますし」

RSpecえかきうた


つっつき後のツイートです。

⚓Ruby

⚓Ruby 3の型付け進捗


つっつきボイス:「Rubyの型付け進捗気になってた」「soutaroさんとSorbetブログで2つ記事が出てました」「RBSという型付け用DSL」「何の略だろう…?」

ruby/rbs - GitHub

RBSとはRubyプログラムの構造を記述する言語で、これを用いてクラスやモジュールの定義(クラス内で定義されたメソッド、インスタンス変数とそれらの型、継承関係やmix-in関係)を書き下せます。定数やグローバル変数も定義できます。
同リポジトリより大意

# 同リポジトリより
module ChatApp
  VERSION: String

  class User
    attr_reader login: String
    attr_reader email: String

    def initialize: (login: String, email: String) -> void
  end

  class Bot
    attr_reader name: String
    attr_reader email: String
    attr_reader owner: User

    def initialize: (name: String, owner: User) -> void
  end

  class Message
    attr_reader id: String
    attr_reader string: String
    attr_reader from: User | Bot                     # `|` means union types: `#from` can be `User` or `Bot`
    attr_reader reply_to: Message?                   # `?` means optional type: `#reply_to` can be `nil`

    def initialize: (from: User | Bot, string: String) -> void

    def reply: (from: User | Bot, string: String) -> Message
  end

  class Channel
    attr_reader name: String
    attr_reader messages: Array[Message]
    attr_reader users: Array[User]
    attr_reader bots: Array[Bot]

    def initialize: (name: String) -> void

    def each_member: () { (User | Bot) -> void } -> void  # `{` and `}` means block.
                   | () -> Enumerable[User | Bot, void]   # Method can be overloaded.
  end
end

「RBSは型定義を別ファイルにするのね」「Rubyにもいよいよ型が入ってくるのか〜」「別ファイルにするのは懐かしのC言語開発で.hファイルとかを思い出すのでそんなに違和感はないかな」「すべてに型を付けなくても必要なところにだけ型を書ければいいんじゃないかなって思いますし」「自分もそう思います」

「このソルベットって?」「ソルベ(Sorbet)はShopifyが以前から手掛けているRubyの型付けですね」「アイコンが可愛い❤

速報: Ruby向け型チェッカー「Sorbet」をStripeがオープンソース化


1本目記事流し読み:

  • Ruby 3のRBSという型シグネチャ用言語を発表
  • 背景:
    • 動的型付けvs静的型付けは古くから問題にされている: 静的型付けは大規模プロジェクト向きだが柔軟性が低下し、動的型付けは短期開発に向いているがコードベースの規模や人数の拡大が難しい
    • 4年前にMatzがRuby 3で静的型チェックを導入すると宣言以来、さまざまな言語の型チェックを調べたうえでRubyコミュニティは型チェッカー構築基盤の開発を決定した
  • RBSは以下のような感じ
# 同記事より
# sig/merchant.rbs

class Merchant
  attr_reader token: String
  attr_reader name: String
  attr_reader employees: Array[Employee]

  def initialize: (token: String, name: String) -> void

  def each_employee: () { (Employee) -> void } -> void
                   | () -> Enumerator[Employee, void]
end
  • RBSの主な機能とRubyの重要な特性
    • ダックタイピング
    • 不統一性(non-uniformity)
  • RubyプログラムでRBSの型を使ってできること
    • バグを見つけやすくなる
    • nil安全性
    • IDE統合の向上
    • ダックタイプをより安全に
  • SorbetとRBS
    • RubyコミュニティはSorbetチームとも連携し、RBSがSorbetやSorbet独自のRBI型シグネチャ形式とぶつからないようにしている
    • SorbetやSteep(Ruby製でRBSを使用)のような静的型チェッカーもRBSの型定義を使える
    • 相互利用のためRBS gemにはRBI->RBS変換機能も付属(逆変換は開発中)
  • まとめ

soutaro/steep - GitHub
pocke/rbs_rails - GitHub

⚓カロリーメイトとQuine

既にあちこちでバズってますが。


つっつきボイス:「かなり盛り上がってますね」「mameさんの書いたQuine、マーケティング用のサイトにこういうのをしれっと仕込むオーバーキル感がすごい」「読める気がしない…」

# otsuka.co.jpより
caloriemateliquid .Quine $ cat CML_quine.rb
    n=2;eval$s=%q{Z=?\s;eval"$><<S=Z*4"+(%w{+"n=#{-~n%3};eval$s=%q{#$s}#YE";$>.isat ty&&
   (r="\e[43;3#{C="#{n*5%9+1}m"}#{T}\e[4"+C+S[1568,79]+E="\e[0m";r[81,21]="\e[37m# {(["Ca
  f\u00e9_au_lait","Yogurt","Fruit_mix"][n].chars*Z).tr(?_,"").center(21)}\e[3"+C ;a=%~POS
  A[`ER]`PASX1cTc22V6NNP.QOYGMXXIG7KK:bCCaVN8WZ[]UQMMS`cBFFJJHHY`QTUIUURRPTOcRV_a LLUT`WXL
 W]a_c_bV`XXYa_9}+[T='  B  A  L  A  N  C  E  D   F  O  O  D  ']*0+%w{bZZYb_][9cc ????`9^acG
 G,,N9DU`DKcUKU3K4!4!4!QXTSSS""9`9`#U`KcK--S;;/GOT<QE$U=>F==Q0@%U/P/B=S0Q`PM&XVV V15CMRHMSH
RKO>==QMQVR    'b`&DK>BS<XE$T>T33DDDUM<V@@E(((TCT0A<0A"')5CXPcQa54X@@Y#KcK--S;; /GOT<Q`$)T)T
:a4A%%#X   VS6a   '   b`&DK>BS<T7**]  ^^b6+++]~;   P=Str    uct.new(:x,:d,:p,:v );M=(-5**7..
b=0).m   ap{[]};A=s  =[];t=Time.now;q=?y.succ;(    s=S     .scan(/.+/ );M[0]<<P [25i-b%3*5i-
9,0,0  ,2+1  i] ;6  0.t   ime  s  {  |i    |j=  i  %2  0  ;i<  40 ?    [    M[j -1],m=M[j],M
[j+1  ]].  ea  ch  {|  n|  m.   ea  ch  {|  p| n. ea ch  {|  q|  d=p .x  -q  .x ;w=d.abs-4;w
<0&   &(  i<2 0?  p.  d+=  w  *w:  p.     p+=  w    *(   d  *(3 -p. d-     q.d) +(p.v-q.v)*4
)/p   .d  )}  }   }   :M  .  shif  t   .ea c  h{   |p|  y,  x=  (   p.  x+= p.v +=p.p/10).re
ct;   p.p   =  [4  3-   b/9 .0-y,1  ].    m  in- [x,p  .d=0   ,  x   -9    2].s ort[1]*2i;p.
v/=[   1,p.v.a   bs/2].max;M[20-j+[0  ,(x+  4).div(5   ),19].sort[1]]<<p;35.tim es{|w|v=x.to
_i-3+w         %7;c=s[w=y.div(2)-2+        w/7];(x-v)**2+(y-w*2)**2<16&&0<=w&&c &&(k=(w*2-21
 )**2/99)<=v&&c[v]&&k+79!=v&&c[v]=q}}};(24-b/18..21).map{|k|s[k]=Z*(k=(k*2-21)** 2/99)+q*79
 +Z+q*2*(6-k)};s*="\e[B\r";"  Your favorite flavor  ";b+=1;A<<"\e[A\r"*21+s.gsub (/\172+/){
  "\e[43m"+$&.tr(q,Z)+E})while+s.count(q)<1950;A.map{|q|sleep([t-Time.now+3,2e-2] .max);$>
  <<s=q};$><<s.gsub(?m,";33m").gsub(Z){S.slice!(/./)};b=?]*33.upto(91){|i|a=~/../ ;a=$'.gs
   ub(i.chr,$&)}*2;Z<<8;(b+a.gsub(?^,"^]"*41)+b).bytes{|c|c-=86;c<8?sleep(3e-2):$> <<(c<(
    'CalorieMate-Liquid-Quine';9)?r.slice!(/\e.*?m|./):c>9?"\e[%X"%c:Z)});puts})*"" }#YE

ちょっと整形しようとして諦めました😅


直接関係ありませんが、mameさんご本人が別のコードで音楽付き動画をアップしているのをruby-jp Slackで知りました。なるほどの選曲です。

参考: 水上の音楽 - Wikipedia

⚓Rubyのcoercion系メソッドには注意(Ruby Weeklyより)


つっつきボイス:「nilのcoercion(強制型変換)に注意しようという記事みたいです」「Rubyのcoercionってそんなにあったかなと思ったらto_iみたいなヤツね」

# 同記事より
nil.to_h => {}
nil.to_a => []
nil.to_f => 0.0
nil.to_r => (0/1)
nil.to_c => (0+0i)

参考: 型変換 - Wikipedia

「文字列のto_iはたしかに予想外のことになりやすい↓」「まあ使い方がよくないという説もありますけど」

# 同記事より
"312".to_i
# 312

"312 oh hai".to_i
# 312

「大文字で始まるInteger()みたいなcoercionメソッド↓、そういえばこういう機能あったな〜」

# 同記事より
Integer(nil)
# TypeError (can't convert nil into Integer)

Float(nil)
# TypeError (can't convert nil into Float))

「昔は使った覚えあるけど基本的にはcoercionってそんなに使わないかも」「Integer(nil)ってダメなんだ😳」「ホントだ、今やってみたらエラーになった」「こういうのを型チェックで救えるといいですよね」「書いた時点で見逃すとなかなか見つからなくてproductionでnilエラーになったりするヤツ」

後で気づきましたが、記事を書いた人はvirtusやdry-typeを作った方でした。

solnic/virtus - GitHub
dry-rb/dry-types - GitHub

以下の翻訳記事も趣旨が似ていますね。

Rubyの明示的/暗黙的な型変換についてのメモ(翻訳)

⚓Rubyのrescueでやんちゃしてやった(Ruby Weeklyより)


つっつきボイス:「パーサーの目を盗んでrescueにクラス定義を書いたということみたいです」「壊しに行く気満々のコード😆

# 同記事より
begin
  raise "omg"
rescue =>
  (class Box
   class << self
     attr_accessor :contents
   end
end; Box).contents
end

puts Box.contents

# => "omg"

「こういう凶悪なコード、よく考えつくな〜」「やったらできちゃうんですね」「Ruby公式とかに投げたら盛り上がりそう」

⚓その他

⚓コントのような


つっつきボイス:「200mばかし離れたところに物理サーバーを無停止で移設せよというミッションを頑張ってクリアしたそうです」「本当にそういうミッションがあったんですか?」「5分ぐらいサーバー止めたらダメ?って聞いたら一瞬たりともダメと言われてますね😆

「移設元サーバーのインターフェイス次第ですけど、意外にやりようはありそう: 電源は今の時代ならUPSつないで発電機回せば割とどうにでもなるんですけど、それよりネットワークが切れないようにする方が大変そうかな🤔」「そんな感じですね」 「移設元と移設先で上流の同じスイッチにつなげられるならやりやすいんですけど」「DNSアップデートとか書いてるから別ネットワークなのかも」「ネットワークが変わると途端に大変になるんですよ…完全なゼロダウンタイムは難しいでしょうし」「お、記事見ると移設先から延々ネットワークケーブルを引いて、先にネットワークを移行してからカートに乗せて移動してるのか、それならやれるかも」

「それにしても一瞬たりとも止められないサーバーって…」「それならどこかクラウドのデータセンターに置けばいいのにって思っちゃいますけど」「作業料の他にコンサル料ももらったって書いてあってよかったよかった」「じゃいいか😆

「こういうのを考えること自体は結構楽しいですよね😋」「やった人お疲れさまです!」「こういう移行方法の問題点は、途中で何かあったときのリカバリープランがないことですけど」「雨が降ったらどうするとか」「ネコがケーブルかじったりとか」「子どもがぶつかってサーバー落っことしたりとか」「周りを用心棒で固めないと」「途中に道路があったらさらに大変そう…」

「こういうの最近あんまりやってないけど、たまにやると楽しいんですよ」「責任抜きでやれるなら😆」「ISUCONの物理サーバー版みたいな企画あったら面白いかも」

参考: ISUCON公式Blog

後で日本語記事も出ましたね↓。

参考: 物理サーバーを稼働させたまま引っ越しさせた意外な方法がネットで話題に - GIGAZINE


今回は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200804後編)「RubyKaigi Takeout 2020」9月オンライン開催、メールバリデータtruemail、Gitのmasterが変更可能にほか

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

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

Rails公式ニュース

Ruby on Rails Discussions

Ruby Weekly

「モノイド」マジックでRubyとRailsをパワーアップしよう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

「モノイド」マジックでRubyとRailsをパワーアップしよう(翻訳)

自分たちが今本当に欲しかったもの

Rubyには、オブジェクト指向プログラミング由来でないさまざまなパターンもあります。関数型の世界からやってきたパターンもあれば、面白いことに、おそらくまったくそれと気づかずに既に使っているパターンもあります。

今そうしたパターンを学ぶ理由は何でしょう?パターンの名前はそれだけで強力ですし、それらのコンセプトを認識すれば、より強力で素晴らしい抽象化を構築できるようになります。

その中でも特に最近のRubyのあちこちで見かけるようになったパターンがあります。そしてこのパターンが、聖なる祝福を受けたHaskellやIdrisから降臨した関数型コード100%でなくてもめちゃくちゃ便利であることに気づきました。

そのパターンのアイデアや抽象化の核心は次のとおりです。そのパターンは言語を超越し、これまで言葉でうまく表せなかったアイデアを表現するのに役立ちます。

必要な知識

reduceがよくわからない方や、RubyのEnumerableメソッドの再実装周りがよくわからない方は、本記事を読む前に「Enumerableの縮小(reduce)」について予習しておくことを強くおすすめします。

昨年のRubyConfカンファレンスでの以下のトークを動画でご覧いただけます。

あるいは以下のテキスト版シリーズ記事もどうぞ。

「Enumerableの縮小」の秘密とは

上述のセッションで私がまったく触れていない楽しい秘密がひとつあります。本記事では親愛なる読者の皆さんに、まったく新しい別の概念と、それに関する直感を養う方法についてご紹介します。

先に進む前に、コアとなる概念について簡単におさらいしておきましょう。

reduceのおさらい

reduceは、以下を用いて項目のコレクションを1つの項目に「まとめます」。

  • 1つのコレクション([1, 2, 3]
  • コレクションの要素を結合して同じ型で返す方法(+
  • 初期値(多くの場合空)(0

つまり、あるリストの合計を得るには、以下のような長い書き方になります(便利なsumメソッドがあることはこの際無視します)。

[1, 2, 3].reduce(0) { |acc, v| acc + v }

ここで0を渡している理由がおわかりでしょうか?0ならそれにどんな数値を足しても同じ数値になるので、値の開始値にふさわしい「空の値」になります。

パターンの誕生

ここからが楽しいところです。これは「繰り返し可能なパターン」と「ルールのセット」に非常に似ています。同じことを乗算でやってみるとどうなるでしょうか?

[1, 2, 3].reduce(1) { |acc, v| acc * v }

乗算の場合は初期値が0だと無意味になってしまいます。0は何倍しても0にしかならないからです。初期値を1にした理由はこれです。

これで、*の関数でつなげた数値のリストと、空の値1ができます。

パターンには十分な事例が必要ですし、実際まだまだ多くの事例があるのです。

今度は文字列のリストについて考えてみましょう。空文字は""で、結合には+を使います。

list_of_strings.reduce('') { |acc, v| acc + v }

お馴染みの形になっていますよね。では配列[]+ではどうでしょう?ハッシュ{}mergeではどうでしょう?Procv { v }composeではどうでしょう?

どのやり方についても、「その型の空の項目を表す方法」と「2つの項目をつないで同じ型の別の項目を得る方法」が存在しています。これはあたかもルールのセットのようです。

そして親愛なる皆さん、秘密はここにあります。

「Enumerableの縮小(reduce)」とは、薄いベールをまとったモノイド(Monoid)のチュートリアルなのです

私たちの新たな友となるルール、それが「モノイド」

以下のルールは「モノイド」と呼ばれる概念、すなわち「1個の項目の形への」ゆるい変換を形成します。

連結(join)
(closure: 閉包)
2つの項目を連結して、同じ型の項目を1つ返す方法
零(empty)
(identity: 単位法則)
空の項目、つまり同じ型のどんな他の項目と結合しても、同じ項目を必ず返す
順序(order)
(associativity: 結合法則)
項目の順序が保たれている限り、どんなに好きなようにグループ化しても必ず同じ結果を返す

結合法則は新しい言葉ですが、要するに以下のようなものです。

1 + 2 + 3 + 4 == 1 + (2 + 3) + 4 == 1 + 2 + (3 + 4) == ....

上はどれも同じ結果になります。そして加算という操作には、反転(inversion)や交換法則(commutativity)のようなものを実現する特性もいくつか含まれます。

反転(inversion)
あらゆる操作に反転が存在する(+ 1- 1など)
交換法則(commutativity)
操作の順序に関わらず同じ結果を得られる

モノイドに反転(逆元)を追加すると数学における群(group)になり、交換法則を追加するとアーベル群(Abelian group)になります。本記事ではこれより詳しい知識について気にする必要はありませんが、数値の加法がアーベル群を形成するということだけ覚えておけばよいでしょう。

「なるほど完全に理解した、しかしどうしてまたこんな斬新な抽象概念についてここまで精密に気にしないといけないのか?」とお思いでしょう。実は、Rails経験者にはとっくにお馴染みのある振る舞いが、驚くほどモノイドの振る舞いに似ているのです。

Active Recordのクエリ

コントローラで以下のようなコードを見かけたことがあるでしょう。

class PeopleController
  def index
    @people = Person.where(name: params[:name]) if params[:name]
    @people = @people.where(birthday: params[:birthday_start]..params[:birthday_end]) if params[:birthday_start] && params[:birthday_end]
    @people = @people.where(gender: params[:gender]) if params[:gender]
  end
end

突然話題が飛んだのではと思われるかもしれませんが、私にとってはつながった話題です。そして私は以下の事実に気づいて初めて、これらを修正する方法に開眼しました。

  1. スコープは「昔ながらのピュアなRubyコード」そのものである
  2. Active Recordのクエリには「零(empty)」の概念が存在する
  3. これらはモノイドに実によく似ている

スコープとは

Railsにおけるスコープは、クエリに「名前」を与える手段のひとつです。

class Person
  # マクロヘルパー
  scope :me, -> { where(name: 'Brandon') }

  # 上と同種のアイデア
  def self.me
    where(name: 'Brandon')
  end
end

スコープは単なるRubyコードなので、以下のようにどんな引数でも自由に追加できます。

class Person
  def self.born_between(start_date, end_date)
    where(birthday: start_date..end_date)
  end
end

条件をいくつか適用する

先ほどのコントローラのコードに戻りましょう。日付が渡されたかどうかに応じて誕生日のクエリを送信するかどうかを制御していたのを覚えていますか?

@people = @people.where(birthday: params[:birthday_start]..params[:birthday_end]) if params[:birthday_start] && params[:birthday_end]

スコープで条件が使えないなどということはありませんし、モノイドではいかなるものも「零(empty)」値と連結すれば同じものが返ります。すなわち、条件が満たされない場合はスコープを効果的に無視できるということです。

class Person
  def self.born_between(start_date, end_date)
    if start_date && end_date
      where(birthday: start_date..end_date)
    else
      all
    end
  end
end

ここでいう連結は、「.」、つまりメソッド呼び出しです。Active Recordのクエリはビルダーのように振る舞うので、値を問い合わせるまでは実行されません。つまり、そのままメソッドチェインの追加を繰り返せます。

allは「零(empty)」とみなされます。つまりメソッドチェインのどこかに置いたallは、既に適用済みのすべてを対象とするallを意味するので、現在のクエリは以下のように引き続き問題なく動作します。

Model.where(a: 1).all.where(b: 2) ==
Model.where(a: 1, b: 2).all ==
Model.all.where(a: 1, b: 2) ==
...

これは実に便利なテクニックです。「no-op(=何もしない)」という条件を表現できるようにすると、さらに強力なスコープを作り出せるようになるのです。

結合法則

スコープは追加を繰り返すことも組み合わせも可能で、しかもActive Recordのどこでも利用できるのが素晴らしい点です。つまりjoinincludesも同様に使えるということです。これらのメソッドの動作は結合法則を満たすので、以下のようにかなり高い自由度でグループ化できます。

class Model
  def self.a; where(a: 1) end
  def self.b; where(b: 2) end
  def self.c; a.b.where(c: 3) end
end

このテクニックをjoinincludeorといった概念と組み合わせれば、実に強力なことができるようになります。条件付きスコープというアイデアと組み合わせれば、この型を含めるかどうかをパラメータに応じて決めることができ、コントローラがさらに柔軟になります。

警告: この方法はpluckselectto_aのようなメソッドには使えません。これらはクエリメソッドではなく、クエリを強制実行して自分自身を評価するので、メソッドチェインの末尾に置く必要があります。

方法をまとめる

以下は元のコードです。

class PeopleController
  def index
    @people = Person.where(name: params[:name]) if params[:name]
    @people = @people.where(birthday: params[:birthday_start]..params[:birthday_end]) if params[:birthday_start] && params[:birthday_end]
    @people = @people.where(gender: params[:gender]) if params[:gender]
  end
end

スコープを用いて上をリファクタリングすると以下のような感じになります。

class Person
  def self.born_between(start_date, end_date)
    if start_date && end_date
      where(birthday: start_date..end_date)
    else
      all
    end
  end

  def self.with_name(name)
    name ? where(name: params[:name]) : all
  end

  def self.with_gender(gender)
    gender ? where(gender: params[:gender]) : all
  end
end

class PeopleController
  def index
    @people = Person
      .with_name(params[:name])
      .born_between(params[:birthday_start], params[:birthday_end])
      .with_gender(params[:gender])
  end
end

以下のPostsコントローラのようなコードももっと高度な方法に書き換えられます。

class PostsController
  def index
    @posts = Post.where(params.permit(:name))
    @posts = @posts.join(:users).where(users: {id: params[:user_id]}) if params[:user_id]
    @posts = @posts.includes(:comments) if params[:show_comments]
    @posts = @posts.includes(:tags).where(tag: {name: JSON.parse(params[:tags])}) if params[:tags]
  end
end

この方法を用いれば、以下のようにモデルにincludesをラップすることもできます。

class Post
  def self.by_user(user)
    return all unless user
    join(:users).where(users: {id: user})
  end

  def self.with_comments(comments)
    return all unless comments
    includes(:comments)
  end

  def self.with_tags(tags)
    return all unless tags
    includes(:tags).where(tag: { name: JSON.parse(tags) })
  end
end

これでコントローラを以下のような感じに書き換えられます。

class PostsController
  def index
    @posts =
      Post
        .where(params.permit(:name))
        .by_user(params[:user_id])
        .with_comments(params[:show_comments])
        .with_tags(params[:tags])
  end
end

さらに、これらの一部だけをグループ化することもできますし、パラメータを渡すこともできます。可能性が大いに広がりますね。

まとめ: 可能性の領域を広げよう

私がこんな抽象概念についての記事を書いた理由、そして「モノイド」などという名前を与えた理由は、私たちの視点を大きく広げ、よくある問題を(おそらく)さらに明確な方法で解決する新しいソリューションを見つけるのに、こうした抽象概念が役に立つからです。

これこそプログラミングの醍醐味です。ちょうど、パズルのピースをひっくり返しながら悩むうちにピースがぴったりはまったときの嬉しさにも似ています。ピースを表裏や上下にひっくり返したり回転させたりしながら、思いつく限りのあらゆる角度からピースをじっと見つめているようなものでしょう。

プログラミングをやっていると、普段の私たちには思いもよらないような角度からのアプローチがいろいろ見つかりますが、これこそがプログラミングの楽しさでしょう。学ぶべきことは常にありますし、多くの斬新な概念たちが私たちに発見される日をいつも心待ちにしています。

おたより発掘

「モノイド」マジックでRubyとRailsをパワーアップしよう(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

数学の抽象的な概念が一般のプログラミングに降りてくるとアガるよね

2020/08/17 11:23

関連記事

Ruby: Enumerableを`reduce`で徹底理解する#1 基本編(翻訳)

Ruby: Enumerableを`reduce`で徹底理解する#2 — No-OpとBoolean(翻訳)

Rails: 最近のRuboCop更新をrubocop.ymlで有効にした

$
0
0

自分のRails環境でしばらくぶりにrubocop -aをかけてみたところ、以下のメッセージがどどどっと表示されました。

The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file:
 - Layout/EmptyLinesAroundAttributeAccessor (0.83)
 - Layout/SpaceAroundMethodCallOperator (0.82)
 - Lint/DeprecatedOpenSSLConstant (0.84)
 - Lint/DuplicateElsifCondition (0.88)
 - Lint/MixedRegexpCaptureTypes (0.85)
 - Lint/RaiseException (0.81)
 - Lint/StructNewOverride (0.81)
 - Style/AccessorGrouping (0.87)
 - Style/ArrayCoercion (0.88)
 - Style/BisectedAttrAccessor (0.87)
 - Style/CaseLikeIf (0.88)
 - Style/ExponentialNotation (0.82)
 - Style/HashAsLastArrayItem (0.88)
 - Style/HashLikeCase (0.88)
 - Style/RedundantAssignment (0.87)
 - Style/RedundantFetchBlock (0.86)
 - Style/RedundantFileExtensionInRequire (0.88)
 - Style/RedundantRegexpCharacterClass (0.85)
 - Style/RedundantRegexpEscape (0.85)
 - Style/SlicingWithRange (0.83)
 - Performance/AncestorsInclude (1.7)
 - Performance/BigDecimalWithNumericArgument (1.7)
 - Performance/RedundantSortBlock (1.7)
 - Performance/RedundantStringChars (1.7)
 - Performance/ReverseFirst (1.7)
 - Performance/SortReverse (1.7)
 - Performance/Squeeze (1.7)
 - Performance/StringInclude (1.7)
 - Rails/ActiveRecordCallbacksOrder (2.7)
 - Rails/FindById (2.7)
 - Rails/Inquiry (2.7)
 - Rails/MailerName (2.7)
 - Rails/MatchRoute (2.7)
 - Rails/NegateInclude (2.7)
 - Rails/Pluck (2.7)
 - Rails/PluckInWhere (2.7)
 - Rails/RenderInline (2.7)
 - Rails/RenderPlainText (2.7)
 - Rails/ShortI18n (2.7)
 - Rails/WhereExists (2.7)
For more information: https://docs.rubocop.org/rubocop/versioning.html
# (略)

なお、10日ほど後にもう一度かけてみるとさらに追加されていました。0.89で追加されたものにはunsafeなcopが多めだったようです。

The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file:
 - Lint/BinaryOperatorWithIdenticalOperands (0.89)
 - Lint/DuplicateRescueException (0.89)
 - Lint/EmptyConditionalBody (0.89)
 - Lint/FloatComparison (0.89)
 - Lint/MissingSuper (0.89)
 - Lint/OutOfRangeRegexpRef (0.89)
 - Lint/SelfAssignment (0.89)
 - Lint/TopLevelReturnWithArgument (0.89)
 - Lint/UnreachableLoop (0.89)
 - Style/ExplicitBlockArgument (0.89)
 - Style/GlobalStdStream (0.89)
 - Style/OptionalBooleanParameter (0.89)
 - Style/SingleArgumentDig (0.89)
 - Style/StringConcatenation (0.89)
For more information: https://docs.rubocop.org/rubocop/versioning.html

一部では何ごとかとissueを投げていた人もいたようですが、これはRuboCop 0.79#7567)で導入されたpendingステータスに関連するメッセージなのだそうです。

参考: RuboCopの”The following cops were added to RuboCop, but are not configured.”の対処方法 - Qiita

逆に言うと、RuboCopを更新しても基本的に新しいcopはひとりでには有効にならなくなったということなので、どうやら新しいcopを使うには自分でrubocop.yamlに反映しないといけなさそうです。

チームのrubocop.ymlだと勝手なことはできませんが、自分の環境なので以下のサイトを参考に遠慮なくrubocop.ymlを更新しました。

yamlにちまちま移し替えるのが割と面倒だったので、もったいない精神で記事にもyamlの追加部分を貼り付けてみました。すべてEnable: trueにしてあるので、コピペする方は適宜falseにするなどしてください。

なお、RuboCopドキュメントで「Safe: yes」と書いてあっても、yamlにSafe: trueと書くとwarningが表示されるcopがかなりあったので、Safe: trueはwarningが出ないものだけ書いてあります。

Layout部門のcop

クリックしてyamlを表示
Layout/EmptyLinesAroundAttributeAccessor:
  Description: "Checks for a newline after an attribute accessor or a group of them."
  Enabled: true
  VersionAdded: "0.83"
  VersionChanged: "0.84"

Layout/SpaceAroundMethodCallOperator:
  Description: "Checks method call operators to not have spaces around them."
  Enabled: true
  VersionAdded: "0.82"

Lint部門のcop

クリックしてyamlを表示
Lint/DeprecatedOpenSSLConstant:
  Description: "Algorithmic constants for OpenSSL::Cipher and OpenSSL::Digest deprecated since OpenSSL version 2.2.0. Prefer passing a string instead."
  Enabled: true
  VersionAdded: "0.84"

Lint/DuplicateElsifCondition:
  Description: "This cop checks that there are no repeated conditions used in if 'elsif'."
  Enabled: true
  VersionAdded: "0.88"

Lint/MixedRegexpCaptureTypes:
  Description: "Do not mix named captures and numbered captures in a Regexp literal because numbered capture is ignored if they’re mixed."
  Enabled: true
  VersionAdded: "0.85"

Lint/RaiseException:
  Description: "This cop checks for raise or fail statements which are raising Exception class."
  Enabled: true
  Safe: true
  VersionAdded: "0.81"

Lint/StructNewOverride:
  Description: "This cop checks unexpected overrides of the Struct built-in methods via Struct.new."
  Enabled: true
  VersionAdded: "0.81"

Lint/BinaryOperatorWithIdenticalOperands:
  Description: "This cop checks for places where binary operator has identical operands."
  Enabled: true
  VersionAdded: "0.89"

Lint/DuplicateRescueException:
  Description: "This cop checks that there are no repeated exceptions used in 'rescue' expressions."
  Enabled: true
  VersionAdded: "0.89"

Lint/EmptyConditionalBody:
  Description: "This cop checks for the presence of if, elsif and unless branches without a body."
  Enabled: true
  VersionAdded: "0.89"

Lint/FloatComparison:
  Description: "This cop checks for the presence of precise comparison of floating point numbers."
  Enabled: true
  VersionAdded: "0.89"

Lint/MissingSuper:
  Description: "This cop checks for the presence of constructors and lifecycle callbacks without calls to super."
  Enabled: true
  VersionAdded: "0.89"

Lint/OutOfRangeRegexpRef:
  Description: "This cops looks for references of Regexp captures that are out of range and thus always returns nil."
  Enabled: true
  VersionAdded: "0.89"

Lint/SelfAssignment:
  Description: "This cop checks for self-assignments."
  Enabled: true
  VersionAdded: "0.89"

Lint/TopLevelReturnWithArgument:
  Description: "This cop checks for top level return with arguments. If there is a top-level return statement with an argument, then the argument is always ignored. This is detected automatically since Ruby 2.7."
  Enabled: true
  VersionAdded: "0.89"

Lint/UnreachableLoop:
  Description: "This cop checks for loops that will have at most one iteration."
  Enabled: true
  VersionAdded: "0.89"

Style部門のcop

クリックしてyamlを表示
Style/AccessorGrouping:
  Description: "This cop checks for grouping of accessors in class and module bodies."
  Enabled: true
  VersionAdded: "0.87"

Style/ArrayCoercion:
  Description: "This cop enforces the use of Array() instead of explicit Array check or [*var]."
  Enabled: true
  VersionAdded: "0.88"

Style/BisectedAttrAccessor:
  Description: "This cop checks for places where attr_reader and attr_writer for the same method can be combined into single attr_accessor."
  Enabled: true
  VersionAdded: "0.87"

Style/CaseLikeIf:
  Description: "This cop identifies places where if-elsif constructions can be replaced with case-when."
  Enabled: true
  VersionAdded: "0.88"

Style/ExponentialNotation:
  Description: "This cop enforces consistency when using exponential notation for numbers in the code (eg 1.2e4)."
  Enabled: true
  VersionAdded: "0.82"

Style/HashAsLastArrayItem:
  Description: "Checks for presence or absence of braces around hash literal as a last array item depending on configuration."
  Enabled: true
  VersionAdded: "0.88"

Style/HashLikeCase:
  Description: "This cop checks for places where case-when represents a simple 1:1 mapping and can be replaced with a hash look"
  Enabled: true
  VersionAdded: "0.88"

Style/RedundantAssignment:
  Description: "This cop checks for redundant assignment before returning."
  Enabled: true
  VersionAdded: "0.87"

Style/RedundantFetchBlock:
  Description: "This cop identifies places where fetch(key) { value } can be replaced by fetch(key, value)"
  Enabled: true
  Safe: false
  VersionAdded: "0.86"

Style/RedundantFileExtensionInRequire:
  Description: "This cop checks for the presence of superfluous .rb extension in the filename provided to require and require_relative."
  Enabled: true
  VersionAdded: "0.88"

Style/RedundantRegexpCharacterClass:
  Description: "This cop checks for unnecessary single-element Regexp character classes."
  Enabled: true
  VersionAdded: "0.85"

Style/RedundantRegexpEscape:
  Description: "This cop checks for redundant escapes inside Regexp literals."
  Enabled: true
  VersionAdded: "0.85"

Style/SlicingWithRange:
  Description: "This cop checks that arrays are sliced with endless ranges instead of ary[start..-1] on Ruby 2.6+."
  Enabled: true
  Safe: false
  VersionAdded: "0.83"

Style/ExplicitBlockArgument:
  Description: "This cop enforces the use of explicit block argument to avoid writing block literal that just passes its arguments to another block."
  Enabled: true
  VersionAdded: "0.89"

Style/GlobalStdStream:
  Description: "This cop enforces the use of $stdout/$stderr/$stdin instead of STDOUT/STDERR/STDIN. STDOUT/STDERR/STDIN are constants, and while you can actually reassign (possibly to redirect some stream) constants in Ruby, you’ll get an interpreter warning if you do so."
  Enabled: true
  VersionAdded: "0.89"

Style/OptionalBooleanParameter:
  Description: "This cop checks for places where keyword arguments can be used instead of boolean arguments when defining methods."
  Enabled: true
  VersionAdded: "0.89"

Style/SingleArgumentDig:
  Description: "Sometimes using dig method ends up with just a single argument. In such cases, dig should be replaced with []."
  Enabled: true
  VersionAdded: "0.89"

Style/StringConcatenation:
  Description: "This cop checks for places where string concatenation can be replaced with string interpolation."
  Enabled: true
  VersionAdded: "0.89"

Performance部門のcop

クリックしてyamlを表示
#################### Performance ###############################

Performance/AncestorsInclude:
  Description: "This cop is used to identify usages of ancestors.include? and change them to use ⇐ instead."
  Enabled: true
  Safe: false
  VersionAdded: "1.7"

Performance/BigDecimalWithNumericArgument:
  Description: "This cop identifies places where numeric argument to BigDecimal should be converted to string. Initializing from String is faster than from Numeric for BigDecimal."
  Enabled: true
  VersionAdded: "1.7"

Performance/RedundantSortBlock:
  Description: "This cop identifies places where sort { |a, b| a <⇒ b } can be replaced with sort."
  Enabled: true
  VersionAdded: "1.7"

Performance/RedundantStringChars:
  Description: "This cop checks for redundant String#chars."
  Enabled: true
  VersionAdded: "1.7"

Performance/ReverseFirst:
  Description: "This cop identifies places where reverse.first(n) and reverse.first can be replaced by last(n).reverse and last."
  Enabled: true
  VersionAdded: "1.7"

Performance/SortReverse:
  Description: "This cop identifies places where sort { |a, b| b <⇒ a } can be replaced by a faster sort.reverse."
  Enabled: true
  VersionAdded: "1.7"

Performance/Squeeze:
  Description: "This cop identifies places where gsub(/a+/, 'a') and gsub!(/a+/, 'a') can be replaced by squeeze('a') and squeeze!('a')."
  Enabled: true
  VersionAdded: "1.7"

Performance/StringInclude:
  Description: "This cop identifies unnecessary use of a regex where String#include? would suffice."
  Enabled: true
  AutoCorrect: true
  VersionAdded: "1.7"

Rails部門のcop

クリックしてyamlを表示
#################### Rails #####################################

Rails/ActiveRecordCallbacksOrder:
  Description: "This cop checks that Active Record callbacks are declared in the order in which they will be executed."
  Enabled: true
  VersionAdded: "2.7"

Rails/FindById:
  Description: "This cop enforces that ActiveRecord#find is used instead of where.take!, find_by!, and find_by_id! to retrieve a single record by primary key when you expect it to be found."
  Enabled: true
  VersionAdded: "2.7"

Rails/Inquiry:
  Description: "This cop checks that Active Support’s inquiry method is not used."
  Enabled: true
  VersionAdded: "2.7"

Rails/MailerName:
  Description: "This cop enforces that mailer names end with Mailer suffix."
  Enabled: true
  VersionAdded: "2.7"

Rails/MatchRoute:
  Description: "This cop identifies places where defining routes with match can be replaced with a specific HTTP method."
  Enabled: true
  VersionAdded: "2.7"

Rails/NegateInclude:
  Description: "This cop enforces the use of collection.exclude?(obj) over !collection.include?(obj)."
  Enabled: true
  VersionAdded: "2.7"

Rails/Pluck:
  Description: "This cop enforces the use of pluck over map."
  Enabled: true
  VersionAdded: "2.7"

Rails/PluckInWhere:
  Description: "This cop identifies places where pluck is used in where query methods and can be replaced with select."
  Enabled: true
  VersionAdded: "2.7"

Rails/RenderInline:
  Description: "This cop looks for inline rendering within controller actions."
  Enabled: true
  VersionAdded: "2.7"

Rails/RenderPlainText:
  Description: "This cop identifies places where render text: can be replaced with render plain:."
  Enabled: true
  ContentTypeCompatibility: true
  VersionAdded: "2.7"

Rails/ShortI18n:
  Description: "This cop checks for consistent uses of request.referer or request.referrer, depending on the cop’s configuration."
  Enabled: true
  EnforcedStyle: "conservative"
  VersionAdded: "2.7"

Rails/WhereExists:
  Description: "This cop enforces the use of exists?(…​) over where(…​).exists?."
  Enabled: true
  VersionAdded: "2.7"

以上を反映して動作を確認できたら、念のためmry↓も実行して、古い部署名が残ってないことを確認します。

参考: RuboCop の Cop 名をマイグレーションする - koicの日記

今後のために、上の記事に従って既存のMigration/DepartmentNameも有効にしておきましょう(ドキュメントではデフォルトで有効となっていますが、私の.rubocop.ymlが古かったせいでオフになっていました)。

################## Migration #############################

Migration/DepartmentName:
  Description: >-
    Check that cop names in rubocop:disable (etc) comments are
    given with department name.
  Enabled: true

おまけ: EnabledByDefault

ここまで記事を書いた後で、ruby-jp SlackでEnabledByDefaultという設定があることを知りました。

AllCops:
  EnabledByDefault: false

この設定をtrueにすると、個別に無効にしたcopも含めてすべて有効になります(デフォルトはfalse)。プロジェクトでは普段falseのままにしておいて、最近どんなcopが増えたか知りたいと思ったときに一時的にtrueにするとよさそうです。

実際にEnabledByDefault: trueにしてみると、以下のConstantResolutionという新しいcopが上のwarningに含まれていなかったことに気づいたので、一応rubocop.ymlに追加しましたが、デフォルトがfalseなので、これだけはfalseにしておきました。 only:ignore:でチェック対象の定数名を絞り込んで使う前提のようで、そうした設定なしでこのcopをおそるおそるtrueにするとほとんどの定数名で大量の怒られが発生しました。

Lint/ConstantResolution:
  Description: "Check that certain constants are fully qualified."
  Enabled: false
  VersionAdded: "0.86"

ところで、RuboCopのドキュメントにyaml形式の設定も表示されていたらそのままyamlにコピペできて便利なのにと思いました。

関連記事

docker-composeを便利にするツール「dip」を使ってみた

週刊Railsウォッチ(20200817前編)お盆も続くRails改修、Rails 6.1にManyモナドが入る?rails-auth gemでクライアント認証ほか

$
0
0

こんにちは、hachi8833です。皆さま熱中症にはお互い気をつけましょう。

参考: 熱中症を防ぐためには(環境庁PDF)


つっつきボイス:「昨日急に体調つらくなって、自分でもびっくりするぐらい丸一日寝てたんですけど、もう一日の記憶がありませんし😇」「これだけ暑いと冷房の効いた部屋にいても体調悪くなりそうですよね…」「いやホントお大事に💊」「ウォッチも熱中症対策ということでエントリを減らし目にしました」

私も猛暑になるととりあえず梅干ししゃぶってクエン酸補給してます。

「そうそう、『室温28℃はエアコンの設定温度ではありません』ってよく注意喚起されてますよね」「自分はとりあえずエアコン27℃にしてますけど」「私も」「結局28℃は目安でしかなくて、西日が差すとか部屋の立地や構造などの条件でいくらでも変わってきますし」(以下エアコン工事や百葉箱の話題など延々)

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。お盆休みのせいか少なめです。


つっつきボイス:「お、6.1.0のマイルストーンですか」「少し前からできてたんですが、その中から1つをこの後ピックアップしました🍡」


なおつっつきの時点では6.1.0マイルストーンに余分なフィルタをかけてしまって残り2件だと思いこんでました😅。本日確認したところ、まだ25件がオープンです。

joinスコープのthrough関連付けが重複するとeager loadingの結果が正しくなくなる問題を修正

キリ番ゲットでした。


つっつきボイス:「ううめんどくさそう…」「こういう感じの現象前にも見た気がする」

# 同PRより
class Author < ActiveRecord::Base
  has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
  has_many :general_posts, through: :general_categorizations, source: :post
end

authors = Author.eager_load(:general_categorizations, :general_posts).to_a

「この辺がややこしくなってしまうのは、ActiveRecordのコードから導出されるSQLが、期待しているSQLと一致しているかどうかを考えるのが難しいからというのもありますね」「両方を一致させるのってえらく大変そう…」「お互い違う言語ですから」

「へ〜、general_categorizations_authors_joinっていう名前になってる↓」

-- 同PRより
SELECT "authors"."id" AS t0_r0, ... FROM "authors"

-- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?

-- `has_many :general_posts, through: :general_categorizations, source: :post`
---- duplicated `through: :general_categorizations` part
LEFT OUTER JOIN "categorizations" "general_categorizations_authors_join" ON "general_categorizations_authors_join"."author_id" = "authors"."id"
INNER JOIN "categories" "categories_categorizations" ON "categories_categorizations"."id" = "general_categorizations_authors_join"."category_id" AND "categories"."name" = ? -- <-- filtering `"categories"."name" = ?` won't work
---- `source: :post` part
LEFT OUTER JOIN "posts" ON "posts"."id" = "general_categorizations_authors_join"."post_id"

「上のクエリの1つ目のLEFT OUTER JOINにあるcategorizationsには別名が付いてなくて、その直後のINNER JOINにあるcategoriesにも別名が付いてないんですけど、その次のLEFT OUTER JOINにはgeneral_categorizations_authors_joinという別名が付いている: ポイントは、この別名とJOIN句がどうやって導出されたのか?という点です」「な、長い😅」

「導出方法がわからないと、JOINしたときにcategorizationsがどう紐付けられるかを予測できないんですよ」「う〜む」「categories_categorizationsっていう名前も自動生成なんですね」「なんという名前😆」

「というふうに、ActiveRecordで書かれたコードから生成されるSQLを読み解くのって難しいんですよ」「難しい〜😭」「何だか空中ブランコ見ているみたいで大丈夫なの?って気がしてきます」「動くと言われても不安になってきますよね…」

「修正内容もどんなふうに修正されたのか、コードをちょっと見ただけだとわからない…」「とりあえず修正後を見るとJOINするテーブル数が減ってるから↓、プルリクに書いてあるとおりthrough関連付けをdeduplicateして再利用したということなんでしょう」「デデュプリケート…」「さっきのgeneral_categorizations_authors_joinが消えてる🎉」「ホントだ」「これはたしかにグレートワークカミポ」「余人が手出しできなさそう…」

# 同PRより
SELECT "authors"."id" AS t0_r0, ... FROM "authors"

-- `has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }`
LEFT OUTER JOIN "categorizations" ON "categorizations"."author_id" = "authors"."id"
INNER JOIN "categories" ON "categories"."id" = "categorizations"."category_id" AND "categories"."name" = ?

-- `has_many :general_posts, through: :general_categorizations, source: :post`
---- `through: :general_categorizations` part is deduplicated / re-used
LEFT OUTER JOIN "posts" ON "posts"."id" = "categorizations"."post_id"

「たしかにActive Recordでスコープを使っていると無意識にこういうの書いちゃうことありますけど」「joinedスコープ的な書き方では気をつけないといけないでしょうね」「これに似た問題は過去にもちょくちょくあった気がします: joinのときとか、mergeのときとか」


長い名前というと、ついメリー・ポピンズを思い出します。

参考: スーパーカリフラジリスティックエクスピアリドーシャス - Wikipedia

assert _recognizesがマウントしたルーティングでも使えるように修正

ルーティングのassert_recognizesアサーションをマウントしたrootルーティングでも使えるようにする。4a9d4c8と似ているが、テストのアサーションにあるパスを認識する点が異なる。
現状のルーティングのrecognizeでは、ActionDispatch::Journey::Patch::Pattern::MatchData#post_matchを用いてrootルーティング(/)を/\A\//という正規表現でチェックすると空文字列のPATH_INFO ("")が生成される。これではマウントしたエンジンのルーティングのforward先が空文字になるのでルーティングを見つけられない。
同PRより大意

# actionpack/lib/action_dispatch/journey/router.rb#L65
      def recognize(rails_req)
        find_routes(rails_req).each do |match, parameters, route|
          unless route.path.anchored
            rails_req.script_name = match.to_s
-           rails_req.path_info   = match.post_match.sub(/^([^\/])/, '/\1')
+           rails_req.path_info   = match.post_match
+           rails_req.path_info   = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
          end

          parameters = route.defaults.merge parameters
          yield(route, parameters)
        end
      end

つっつきボイス:「assert_recognizesって使ったことないな〜」「Railsガイドにもあった↓」

「ガイドのassert_generatesアサーションはそのまんまでわかりやすい、つかこれがあればいい気がする」「でassert_recognizesアサーションはその逆をテストするのね」「assert_routingは使ったことあったかも」

# Railsガイドより
assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')

「ルーティング難しいから、複雑なルーティングを書いたときにはこういうアサーションを使うとよさそう」「普通のresoucesルーティングならわざわざテストは書かないかな〜」


「ところで今ググって出てきたrailsdoc.comって初めて見たんですけど何でしょう?」「はて?」「知らない〜」

後で調べました↓。昨年のツイートですが、今は動いています(運営主体が同じかどうかはわかりません)。参照するときは公式ではない点に留意しておこうと思います。

@controllerというインスタンス変数が初期化されない場合があったのを修正

#39937で指摘されたように、@controllerというインスタンス変数がルーティングのテストで定義されないことがある。

rails/actionpack/lib/action_dispatch/testing/assertions/routing.rb#202
 request = ActionController::TestRequest.create @controller.class 

上が一部のインスタンスでinstance variable @controller not initialized warningを出すことがある。
インスタンス変数にアクセスする前にdefined?(controller)をチェックするといいだろう。

controller = @controller if defined?(@controller)
request = ActionController::TestRequest.create controller&.class

@ioquatixのレポートと解決に感謝🙂。
同PRより大意


つっつきボイス:「@controllerという変数名にちょっとドキッとした」「@controllerが初期化されないことがあったら結構困るし」「コントローラを直接参照したいことがどのぐらいあるかですけど、まあテストだし、本来はこのパスで触れないはずのものでも状態のテストはしたいこともあるでしょうし」

establish_connectionのパラメータをリネームしてキーワードに変更

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1034
-     def establish_connection(config, pool_key = Base.default_pool_key, owner_name = Base.name)
+     def establish_connection(config, owner_name: Base.name, shard: Base.default_shard)
        owner_name = config.to_s if config.is_a?(Symbol)

        pool_config = resolve_pool_config(config, owner_name)
        db_config = pool_config.db_config
        # Protects the connection named `ActiveRecord::Base` from being removed
        # if the user calls `establish_connection :primary`.
        if owner_to_pool_manager.key?(pool_config.connection_specification_name)
-         remove_connection_pool(pool_config.connection_specification_name, pool_key)
+         remove_connection_pool(pool_config.connection_specification_name, shard: shard)
        end

        message_bus = ActiveSupport::Notifications.instrumenter
        payload = {}
        if pool_config
          payload[:spec_name] = pool_config.connection_specification_name
          payload[:config] = db_config.configuration_hash
        end

        owner_to_pool_manager[pool_config.connection_specification_name] ||= PoolManager.new
        pool_manager = get_pool_manager(pool_config.connection_specification_name)
-       pool_manager.set_pool_config(pool_key, pool_config)
+       pool_manager.set_pool_config(shard, pool_config)

        message_bus.instrument("!connection.active_record", payload) do
          pool_config.pool
        end
      end

つっつきボイス:「poolという名前が変わった」「ついでにキーワード引数に変わってる」「publicなestablish_connectionで使う名前としては、これがふさわしいという判断なんでしょうね」

  • current_pool_key -> current_shard
  • default_pool_key -> default_shard
  • pool_key -> shard

「poolという言葉だと自分はほぼコネクションプールを連想しますし、poolだと排他制御されてるように見えてしまうかもしれませんね: コネクションプールは自分だけが使えるリソースをもらって、終わったら返却する印象がありますけど、public云々と言っているのはもしかすると同時に使われる可能性も加味してるんじゃないかと推測してみました🤔」「ははぁ、なるほど」

参考: コネクションプーリング(コネクションプール)とは - IT用語辞典 e-Words

  • pool_keyからshardへのリネーム

もともとはshardpool_keyという名前にするつもりだった(内部のprivate APIを実装するときにシャーディングに使われるかどうかがわからなかったのと、public APIにせずに振る舞いを実装したかったので)。シャーディングのpublic APIができることになったので、private向けの名前にするより同じ名前をpublic APIで使う方がよい。今後のコード改修やバグ追跡がやりやすくなるはず。

このプルリクでdeprecationは不要(シャーディングAPIはまだリリースされておらず、内部コードはすべてpool_keyを使うようになっているので)。

  • 接続メソッドでキーワード引数を使うよう更新
    この変更では位置引数ではなくキーワード引数を接続用メソッドで使うようになる。この変更は一見必要ではなさそうに見えるが、今自分たちは接続管理をより頑丈かつ柔軟にするためにリファクタリング中なので、この時点でメソッドシグネチャを変更しておけば今後の変更がやりやすくなる。
    このコミットではリリース済みのpublic APIは変更されない(shardowner_name引数も6.1に追加されている)。キーワード引数にすることで柔軟性が高まり、内部動作を変更しても見苦しくならなくなる。このキーワード引数ではデフォルトで複数の「非位置引数」をサポートする。
    (@seejohnrunとの共作)
    同PRより大意

issue: Active SupportやActive RecordリレーションにManyモナドを導入したい

上のRails 6.1マイルストーンのissueの1つです。

@tomstuartのRubyでモナドを使う素敵なスピーチに刺激を受けたので、「Rails on Many monad」に足を踏み入れてみたいと思う。
Tomのmonads gemにある彼の成果を必要に応じてActive Supportにコピーすることについて快諾いただいた。このAPIの最もイケてる点は、以下のようにActive RecordリレーションでMany monadを使えるようになるという点だ。

Blogs.all.with_many.categories.posts.comments.body.split(/\s+/).values

同じことを以下のようにflat_mapでやるよりはるかにレベルアップできる。

Blogs.all.flat_map(:categories).flat_map(:posts).flat_map(:comments).flat_map(:body).split(/\s+/).values

これはずっと前からやりたかったのだが、これを正しく説明する方法について考えてみていいだろう。目指すはモナド!
これを動かすために、まずActive SupportにMaybeモナドを入れ、続いてcore extensionでwith_manyというEnumerableを追加し、それからActive Recordリレーションでもうまく動くようにすることを提案する(たぶんさくっとやれるはず)。
同issueより大意

tomstuart/monads - GitHub


つっつきボイス:「ちょうど昨日モノイド記事↓を公開したので、DHHがあげたマイルストーンissueを貼ってみました」

「モノイド」マジックでRubyとRailsをパワーアップしよう(翻訳)

「モナドか〜、こういうwith_manyをやりたいということなのね↓」「あ、今気づいたんですけど、Manyが大文字なのはモナドの名前なのか😅: モナドってMaybeとかOptionみたいな名前付けられがち」

Blogs.all.with_many.categories.posts.comments.body.split(/\s+/).values

「まあ書き方が増える分にはいいんじゃないかな」「他のテストがコケたりしなければ😆」「まだ作り中みたいですけど、6.1.0のマイルストーンにこのissueが入っているということは入れるつもりなんでしょうね」

参考: Maybeモナド と Listモナド - Qiita

「モナドはまずActive Supportに入れるつもりみたい: Active Recordにいきなり入れるよりはいいかも」「モナド大好きなkazzさんに見せたいな〜」「kazzさん最近つっつきになかなか参加できないんですよね…」

Rails

RailsアプリをDocker化する(Ruby Weeklyより)


つっつきボイス:「Rails Docker化を割と基本的なところから説明してる感じでした」「RailsをDocker Compose化するのはそんなに大変じゃないんですけど、ECSやEKSに乗せようとするといろいろ難しくなりがち: コンテナオーケストレーションツールに乗せると条件も変わってくるし考えないといけないことも増えてくるので」


記事見出しより:

  • RailsアプリケーションをDocker化するメリット
    • development環境でののメリット
    • production環境でのメリット
  • チュートリアルの概要
  • 前提条件
  • Dockerの基本概念
    • イメージ
    • コンテナ
  • DockerとDocker Composeの違い
  • アプリのDocker化に関連するファイル
    • Dockerfile
    • docker-compose.yml
  • アプリをDocker化する
    • Dockerfile
    • docker-compose.yml
    • init.sql
    • config/database.yml
  • Docker化したアプリのビルドと実行

RailsのログをRSpecでテストする方法2種(Ruby Weeklyより)

# 同記事より
# expect
it "logs a message" do
  allow(Rails.logger).to receive(:info)
  expect(Rails.logger).to receive(:info).with("Someone visited the site!")

  visit root_path

  expect(page).to have_content "Welcome to my site!"
end

つっつきボイス:「Everyday Railブログです」「spyを使う方法もあるか、なるほど」「ログの中身をテストすることってあるんですね」「当然あります、重要なシステムでログが正常に出力されてなくて復旧できなくなったら洒落にならないので」「なるほど」「そうなったのを見たことならあります😆」

Rails tips: RSpecの「スパイ(spy)」の解説(翻訳)

rails-auth: authenticationとauthorizationをミドルウェアベースで実現(Ruby Weeklyより)

square/rails-auth - GitHub


つっつきボイス:「authentication(認証)とauthorization(認可)をどっちもやれるgemみたいです」

Rails::Authは、authentication(以下AuthN)やauthorization(以下AuthZ)でRackミドルウェアを用いるよう設計された柔軟なライブラリです。AuthNの手順とAuthZの手順を別々のミドルウェアクラスに分割し、最初にAuthNのミドルウェアでcredential(X.509証明書やcookieなど)を検証し、続いてcredential(アクセス制御リストなど)を使うリクエストを別のAuthZ用ミドルウェアで認可します。
Rails::Authによる認証や認可は、ブラウザcookieを用いてエンドユーザーに対して使うことも、X.509クライアント証明書を用いてサービス間リクエストに対して使うことも、適切な認証ミドルウェアを持つcredentialを用いてその他のクライアントに対して使うこともできます。
名前とはうらはらに、SinatraなどのRackベースのフレームワークでも使えます。
同READMEより大意

# 同リポジトリより
module MyApp
  class Application < Rails::Application
    [...]

    Rails::Auth::ConfigBuilder.application(config, matchers: { allow_x509_subject: Rails::Auth::X509::Matcher })
  end
end

「X.509マッチャーとかあるんだ、やるな〜」「あれ、X.509って何でしたっけ?」「OpenSSLの証明書とかでよく使われているヤツですね」

参考: X.509 - Wikipedia

「OpenSSLとかで証明書を表示するとこういうのがどひゃ〜っと出力されるんですよ↓」「あ〜これですか😳」「ちらっと見た感じでは、このgemはクライアント証明書も使えるという触れ込みなんでしょうね」

# Wikipediaより
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            10:e6:fc:62:b7:41:8a:d5:00:5e:45:b6
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=BE, O=GlobalSign nv-sa, CN=GlobalSign Organization Validation CA - SHA256 - G2
        Validity
            Not Before: Nov 21 08:00:00 2016 GMT
            Not After : Nov 22 07:59:59 2017 GMT
        Subject: C=US, ST=California, L=San Francisco, O=Wikimedia Foundation, Inc., CN=*.wikipedia.org
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
            pub: 
                    00:c9:22:69:31:8a:d6:6c:ea:da:c3:7f:2c:ac:a5:
                    af:c0:02:ea:81:cb:65:b9:fd:0c:6d:46:5b:c9:1e:
                    9d:3b:ef
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
(省略)

「READMEにX.509クライアント証明書を使うって書いてあるところからして、このgemはもしかするとRailsにSSLというかTLSを解釈させるものなんだろうか?」

参考: クライアント証明書とは - IT用語辞典 e-Words

「コード覗いてみると↓、このgemはX.509証明書を受け取るようになってる」「証明書を直接扱ってるような感じがありますね」「でないとSubject Alternate Names(SANs)とか取り出せないし」

# lib/rails/auth/x509/certificate.rb#10
        def initialize(certificate)
          unless certificate.is_a?(OpenSSL::X509::Certificate)
            raise TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
          end

          @certificate = certificate.freeze
          @subject = {}

          @certificate.subject.to_a.each do |name, data, _type|
            @subject[name.freeze] = data.freeze
          end
          @subject_alt_names = SubjectAltNameExtension.new(certificate)
          @subject_alt_names.freeze
          @subject.freeze
        end

「Deviseとかとは使いみちが少し違う感じでしょうか?」「Wikiに書いてあるような、マイクロサービスでクライアント証明書を使うみたいな用途なら、接続先が限定されるし偽装がほぼ不可能になるという意味でわかるような気がするけど」

「gemspecを見ると↓、やっぱりこのgemはTLSをしゃべるのか」「opensslもrequireしてるし」「TLSをしゃべらせるのって大変そうだけど、よく作ったな〜」

lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "rails/auth/version"

Gem::Specification.new do |spec|
  spec.name          = "rails-auth"
  spec.version       = Rails::Auth::VERSION
  spec.authors       = ["Tony Arcieri"]
  spec.email         = ["tonyarcieri@squareup.com"]
  spec.homepage      = "https://github.com/square/rails-auth/"
  spec.licenses      = ["Apache-2.0"]

  spec.summary       = "Modular resource-oriented authentication and authorization for Rails/Rack"
  spec.description   = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
    A plugin-based framework for supporting multiple authentication and
    authorization systems in Rails/Rack apps. Supports resource-oriented
    route-by-route access control lists with TLS authentication.
  DESCRIPTION
# (省略)

「どんなふうに使うのかがまだピンとこない」「READMEにもうちょっと情報があるといいんですけど」


「ところでこのリポジトリのSquareってあの決済システムのSquareなのかな?」「最初違うかなと思ったけど、アイコンも見覚えあるし、どうやらそうみたい」

「iPhoneのヘッドフォン端子に挿して使うSquareの決済システムがコミケとかでも使われてたりしますね」

参考: サークル参加者と語る、「同人サークルとクレジットカード決済」

「rails-authはもしかするとSquareが主に内部で使っているのかも?🤔」「あ〜そうかも」「クライアント認証をRubyで書きたかったのかも」「Rackミドルウェアでやってるみたいだからまあいいけど、もし名前のとおりにRailsでクライアント認証とかやってたら速度とかつらそうですけどね」


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200811山の日短縮版)RSpec Queueでパラレルテスト、カロリーメイトとRubyのコラボ、Rubyのcoercionほか

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

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

Rails公式ニュース

Ruby Weekly


週刊Railsウォッチ(20200818後編)ruby_jardデバッガがスゴい、RubyオンラインマニュアルにEdit機能が追加、Ruby 2.7のBundlerを消す方法ほか

$
0
0

こんにちは、hachi8833です。今日はほんのちょっと暑さがましになりましたね。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Ruby

⚓@_ko1さんのD論審査発表スライド『高速なRUBY用仮想マシンの開発』


同スライドより


つっつきボイス:「YARVがRubyに入ったのが1.9の頃なのでかなり昔のスライドなんですが、RubyのYARVはこういう理由でこういう設計にしたということが詳しく述べられているのを今頃知りました」「スライドじゃなかったけど読んだ覚えあるかも」「YARVをスタックマシンにしたのはRubyではメソッド呼び出しが多いからとか、そういう設計の意図を今になって知りました」

参考: YARV - Wikipedia

「@_ko1さんのように実装中心で博士論文を取るのって、最近ならともかく当時はもっと大変だったと思います」「そういえば前にも話題に出ましたね(ウォッチ20190128)」「独自性ももちろんですけど、相当丁寧にやらないと実装でドクターを取るのは難しかったと思います」「そうですよね😅

「実際RubyにYARVが導入されたことで速くなったというのはあちこちで見聞きしますね」「ところが研究の世界だと、高速化は実装という技術の話であって研究ではないとみなす界隈もあってですね」「あ〜思い当たります」「昔は未踏に通っても論文誌に通らないとかありましたけど、最近は実装寄りの論文もだいぶ通るようになりましたね」

参考: YARV Maniacs 【第 1 回】 『Ruby ソースコード完全解説』不完全解説
参考: IPA:2004年度「未踏ユース」採択概要:05笹田

「論文のタイトル↓も『アーキテクチャ』や『評価』という言葉を中心に使ってますけど、こういうタイトルの方が通りやすかったりします」「そうそう」「他の言語との比較もきちんとやってたりしますし」


同スライドより

⚓機能リクエスト: SetをRubyにビルトインして欲しい


つっつきボイス:「たしかruby-jp Slackで見かけたと思うんですけど、RubyのSetを標準として加えようという提案です」「他の言語だとSet(集合)が標準で入っていることもありますね」「RubyのSetというと重複のないArrayという印象ですけど」「要素の順序性を問わないときに使うヤツですね」

参考: class Set (Ruby 2.7.0 リファレンスマニュアル)

「順序性を保証しなくてよければ速くできるケースであればSetは有効でしょうね」「自分はSetArrayの使い分けをそこまで意識しないかな」「何となくArrayの方が速そうな印象があるかも」

「私はJavaでSet使った経験ならあるので、そっちのSetを想像しちゃいます」「数学的には集合って重複を認めないんだっけ?」「そのはずです」

参考: Set (Java Platform SE 8 )
参考: 集合 - Wikipedia
参考: 多重集合 - Wikipedia — 重複を許す集合の概念

後でやってみると、RubyのSetはたしかに要素が重複しませんでした。

require 'set'

s1 = Set[10, 20, 30]
s2 = Set[10, 20, 40]
s = s1 + s2
p s     #=> #<Set: {10, 20, 30, 40}>
s.add(10) #=> #<Set: {10, 20, 30, 40}>
s << 10   #=> #<Set: {10, 20, 30, 40}>

自分はSetに関する一連のフィーチャーリクエストのissueを立てているが、いずれもこのユースケースに基づいている

主に念頭にあるユースケースは、RuboCopで最近遭遇したものだ。後でinclude?を呼ぶためだけにfrozen arrayが大量に使われていることに気づいたのだが、これはO(1)ではなくO(n)である。

これらをSetに変換しようとすると大きな互換問題が発生した。これは実にいまいましい状況であり、ものによっては変換によって効率がかなり落ちる。

こうした非互換性問題をきっかけに、RuboCopではArrayをベースにして最適化済みのinclude?===を装備したカスタムクラスを作っている。RuboCopではRubyコードに対してさまざまなチェックを走らせ、そうしたチェックはcopと呼ばれている。RuboCopのパフォーマンスは(私見では)かなり低く、一部のcopではO(n^2)になっているものすらある(nは検査するコードのサイズ)。このような効率の極端に低いcopが渡されたときでも、100を超えるarrayを最適化すると(といってもほとんどはささやかなものだが)、5%ほどスピードが向上した。

参考までにRuboCopのプルリクを貼っておく:
[fixes #22] Introduce FastArray, a frozen Array with fast inclusion lookup by marcandre · Pull Request #29 · rubocop-hq/rubocop-ast
Use FastArray by marcandre · Pull Request #8133 · rubocop-hq/rubocop

自分の経験に基づいて考えると、Setを用いればよいところにSetが使われていないことがまだまだたくさんあると思われる。その理由はSetがRubyに標準で組み込まれていないためにSetの知名度が低いのと、Setを手軽に書ける記法がないからだと思う。

以下のリクエストについての議論はこのissueにまとめたいと思う。SetをRubyのコアオブジェクトに加えるべきかどうか(かつてコアになかったComplexがその後コアに入ったように)。今後のフィーチャーリクエストの中にも、SetがRubyに組み込まれていればもっと簡単にやれる(あるいはそうしないと実現できない)ものがあるだろう。
同issueより大意

⚓Idiosyncratic Rubyの記事2本(Ruby Weeklyより)


つっつきボイス:「1つ目はinを代入っぽく使う方法が紹介されてました」「Ruby 2.7のパターンマッチングで入ってきたヤツ」「inの機能そのものという感じですね」

# 同記事1より(Ruby 2.7ではwarningが出ます)
[1, 2, 3, 4] in [first, second, *other]

puts first  # => 1
puts second # => 2
puts other  # => [3,4]

参考: プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ

「2つ目はNULLバイトを引用符の中で表す書き方をこんなに見つけたという記事です」「NULLバイトのリテラル書式を43個も見つけるとは、ようやる〜」「まあ自分は普通に"\0"でやるのが好きかな」

⚓ruby_jard: ByeBugベースのRubyデバッガ


同リポジトリより


つっつきボイス:「ruby_jard、はてブにあがってましたね」「まるでIDEみたい」

asciicast

「ruby_jardをRailsコンソールで使えたらありがたいかも!」「使いみち結構ありそうですね」「本番環境でどうしても値を調べないといけない状況になったときとか」「本番環境でRailsコンソールを叩くのはどうよというのは置いといて😆」「まあ自分は基本的にRubyMine使いますけど😆

「ベースはbyebugみたいですね」「それを単体gemのデバッグツールとしてここまで作り込んだのがスゴい」「あ、これプラグインとかじゃないのか」「なお試しに手元のirbでちょっとやってみたらたちまち吹っ飛びました😇

deivid-rodriguez/byebug - GitHub

「ここまで仕上げたのはスゴいですね」「偉大なモチベーション」「カラースキームまで装備してるし」「そこは既存のライブラリとか使ってるんでしょうけど」「ランタイムの依存gemも意外に少ない↓」「じゃそれ以外はほとんど自分で作り上げたのね」

  spec.add_runtime_dependency 'byebug', '>= 9.1', '< 12.0'
  spec.add_runtime_dependency 'pry', '~> 0.13.0'
  spec.add_runtime_dependency 'tty-screen', '~> 0.8.1'

⚓Ruby 2.7にデフォルトで入るBundlerを消し去る方法(Ruby Weeklyより)


つっつきボイス:「Goby↓の@st0012さんの記事をRuby Weeklyで見かけたんですけど、ちょうど自分もRuby組み込みのBundlerの削除方法が気になってたので」「え、そんなことできたっけ?」「正規の方法ではなさそうですけどやったらできたそうです」

goby-lang/goby - GitHub

「今は別バージョンのbundlerを--default付きで入れてもconfig defaultしても組み込みのbundlerが使われる↓」「自分も以前同じ結果になって諦めました」

$ bundle -v
Bundler version 2.1.4

$ gem install bundler:1.17.3 --default
Successfully installed bundler-1.17.3 as a default gem
Done installing documentation for bundler after 0 seconds
1 gem installed

$ bundle -v
Bundler version 2.1.4

$ bundle config default 1.17.3
$ bundle -v
Bundler version 2.1.4

「で方法はbundler-2.1.4.gemspecを削除するということなのね」「gempathの探索から外すということか、なるほど〜」

「これやりたくなる気持ちワカル: bundler本体をアップグレードしたときに『bundlerのバージョンが違います』みたいになってデプロイがたまにコケることがありますし」「たしかに」「まあ今はコンテナでデプロイすることが増えてきたからあんまり問題にはならなくなってきてますけど」

⚓その他Ruby

つっつきボイス:「そうそう、これはマジスゴいと思った: 俺たちの欲しかったものはまさにこれ🎉」「これいいですよね〜」「この仕組みがあれば自分もドキュメントに修正投げようという気持ちになれますし」


「応募しようと思ったら地域.rbが対象でした😅」「RubyKaigiで発注するから量も質もスゴいんだろうなと想像」「めちゃめちゃ美味しいという評判です」

地域.rbの皆さまぜひ!

⚓DB

⚓大量のレコードでOutOfMemoryになるとき


つっつきボイス:「はてブで上がってました」「JDBCの仕様らしいという話でしたっけ」「ぽすぐれでautoCommit=trueって使うんだろうか?MySQLでやるとどうなるんだろう?🤔

「autoCommitってちょっと怖そう…」「MySQLとかだと、トランザクションを書かないときはコミットするみたいな機能があった気がする」「まあこんな設定されてたら全部フェッチして死ぬでしょうね😇

参考: MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.1 START TRANSACTION、COMMIT、および ROLLBACK 構文

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

⚓Docker Hubの利用規約が更新


つっつきボイス:「無料プランは6か月以上pushまたはpullされないとinactiveにされる…だと?」「Docker Hubに何も考えずにイメージをばんばんアップロードする人がよほどたくさんいたんでしょうね」「リポジトリのサイズがペタクラスに達してるそうですし、Docker Hubとしては勘弁して欲しいでしょうね」「15ペタバイトのうち4.5ペタバイトがpushもpullもなしか…」

「GitHub Actionsとかでビルドしたイメージを自動でDocker Hubに投げてるとか割とありそうですし」「あ〜」「自動化されてるとリソースのことって気にしなくなりがちですよね」

「対象は無料プランだけだから有料プランは無事か」「有料プランは個人向けのProが月5ドルだからまあまあですかね」「でも個人用だからアカウントを共有できませんし」「あ、そうか」「無限のリソースはやっぱりないということで」

「怖いのは、自分のプロジェクトで依存しているDockerイメージが突然消えたりすること」「それですよね」「6か月以上pushまたはpullされなければ、ということはイメージが変更されなければ消えるということですし」

「自分たちが依存しているDockerイメージは、たとえばdocker rmiでイメージを消してリビルドするだけのGitHub Actionsなんかを作っておけばpullはされることになるから生存報告はできるかな」「う〜む」「Docker Hubがこうするのは無理もないし、とてもよくわかるんですけど、プロジェクトが依存している元イメージが消えるとプロジェクトが死ぬ可能性があるんですよね」「規約更新は11月からだから、来年4月ぐらいにどこかで悲鳴が上がるのかな…」

⚓その他インフラ

つっつきボイス:「へ〜、GitHub ActionsはIPv6サポートしてるのね」「そういえばAWSのVPCってIPv6サポートしてるのかな?2016年からだから割と前からサポートしてるのか↓」「IPv6のテストが必要なときってありますよね」「IPv6のCIDRsも作れるのか、やらないけど😆」「😆

参考: Amazon VPC の IPv6 の使用開始 - Amazon Virtual Private Cloud
参考: Classless Inter-Domain Routing - Wikipedia

⚓自宅でIPoE+IPv6

「ところで、今一般人が自宅で最も速いインターネットを使いたければIPoE(IP over Ethernet)でIPv6を使えという話はよくありますよね」「ですね」「プロバイダ直結のIPoEでつなぐヤツで、知人には実効で800Mbps出てる人もいますし」

参考: IPoE接続とPPPoE接続との違い | NTTコミュニケーションズ 法人のお客さま

「これをやるには自宅まで光ファイバーがやってこないとできないんですけど、うちのマンションは残念ながらVDSL銅線だからできなくって😢」「うちの団地も同じくVDSLなのでできません😭」「自宅までファイバーが届いてるところはまだそんなにありませんし」「うう、窓の外の電信柱には光ケーブル通ってるのが今も見えてるのに…」「2階だか3階までだったらNTTに依頼して窓から直接引き込むことってできたと思いますけどね」「うちは3階だから可能だと思います、後は団地の管理組合に拒否されなければですけど」

参考: VDSL方式と光回線方式の違い!変更する方法とできないときの対処法は? | NURO 光

⚓言語/ツール/OS/CPU

⚓PHP7の定数配列

<?php
// 同記事より
function foo($bar) {
    $arr = [
        "x1"=>["foo"=>1,"bar"=>1,"baz"=>3,"qux"=>4,"quux"=>5,"corge"=>6,"grault"=>7,"garply"=>8,"waldo"=>9,"fred"=>10,"plugh"=>11,"xyzzy"=>12,"thud"=>13],
        "x2"=>["foo"=>2,"bar"=>1,"baz"=>3,"qux"=>4,"quux"=>5,"corge"=>6,"grault"=>7,"garply"=>8,"waldo"=>9,"fred"=>10,"plugh"=>11,"xyzzy"=>12,"thud"=>13],
        /* 約30000行省略 */
        "x29999"=>["foo"=>29999,"bar"=>1,"baz"=>3,"qux"=>4,"quux"=>5,"corge"=>6,"grault"=>7,"garply"=>8,"waldo"=>9,"fred"=>10,"plugh"=>11,"xyzzy"=>12,"thud"=>13],
        "x30000"=>["foo"=>30000,"bar"=>1,"baz"=>3,"qux"=>4,"quux"=>5,"corge"=>6,"grault"=>7,"garply"=>8,"waldo"=>9,"fred"=>10,"plugh"=>11,"xyzzy"=>12,"thud"=>13],
    ];
    return $arr;
}
foo(1);

つっつきボイス:「定数配列、PHP 7ということはもう使えるのか」「使い放題というのはどうかな〜?😆

「定数配列ってどういうときに使うんでしょう?」「Railsでもこんな感じでyamlの設定ファイルにネステッドハッシュで設定を置いたりしますし、Railsのコンフィグ↓もまさにそれですね」「たしかに」

参考: Rails アプリケーションを設定する - Railsガイド

⚓その他

⚓これなら欲しいかも


つっつきボイス:「ムーンランダー!」「@tenderloveさんがこれ薄くていいよって盛り上がってたので」「このキートップの横が浮いてるのってゴミが入っちゃうからあんまり好きじゃないかな〜」「自分もゴミが入らない構造ならいいのにって思っちゃいました」「でなければキートップを簡単に外せて掃除できるとありがたいですよね」「これならキートップ外せると思いますよ」「自分がずっと使ってるKINESISもごくたまに全部引っこ抜いて掃除してますけど、そんなに大変でもないですね」

⚓夏のセール明後日まで

例のエンタープライズアプリケーションアーキテクチャパターンも入ってました。

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要


つっつきボイス:「翔泳社がGoogleスプレッドシートでセール本のリストを公開してたのをruby-jp Slackで知りました」「これ見ましたけど書影がなくて文字だけだと意外と探しにくいんですよね」「同じく😢」「シート複製してフィルタかけました」「お、そのままでもフィルタかけられますよ」

「買ってもきっと積ん読ですヨ🤣」「ああっほんとのこと言わないでください〜🤣」「最近は今すぐ読む本以外はAmazonのウイッシュリストに入れてますし」「私もウイッシュリストに積んでたんですが4000冊ぐらいになって顧みなくなりました…」「電子書籍ですし、読みたくなったら買うでいいんじゃないでしょうか」「まあまあ、買って積んどけばそのうち読みたくなるかもしれないじゃないですか、その可能性に投資しているんですよ〜」「ならないならない🤣

「昔の回線が細くて高かった時代は、旅行するとネットのない世界に行く可能性もあったから、Kindleにあらかじめダウンロードして持っていくとかやってましたけど、今どきは回線がつながらない環境がほぼ見当たらなくなりましたよね」「そうなんですよね、私にとってフェリーに乗ってるときが数少ないそういう世界だったんですけど、最近は高知県沖以外だとネットつながるんですよ」「なぜ高知県沖なんでしょう?」「あそこは海岸が大きく湾曲してて陸地から離れてるせいかつながりませんでしたね」「へ〜」「5年ぐらい前の話なので今は違うかもしれませんけど」(以下延々)


後編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200817前編)お盆も続くRails改修、Rails 6.1にManyモナドが入る?rails-auth gemでクライアント認証ほか

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

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

Ruby Weekly

週刊Railsウォッチ(20200824前編)「Active Jobスタイルガイド」は有用、SiderがGitLabに対応、eager loading時のselectを修正ほか

$
0
0

こんにちは、hachi8833です。先週Gmailがコケてたみたいですね。

参考: G Suite Status Dashboard

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

今週もエントリ数は抑え気味にしました。

⚓Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。いつもより少ないところに夏を感じます。

⚓rails db:structure:{dump,load}rails db:schema:{dump,load}に統合

RailsのA May of WTFにも書いたとおり。
このプルリクに実装したdeprecationの修正方法はよくわからない。
このプルリクの前は、config.active_record.schema_formatの値にかかわらずrails db:schema:{dump,load}はdump/load db/schema.rbを出力し、rails db:structure:{dump,load}はdump/load db/structure.sqlを出力していた)
このプルリクをマージすると、...:schema:...structure:のどちらのコマンドもconfig.active_record.schema_formatに従って...:schema:...コマンドを実行するようになる。
なおdb:test:load_schemaは本当に必要なのか、あるいはdb:test:loadに組み入れられるだろうか。
同PRより大意


つっつきボイス:「config.active_record.schema_formatが効いてなかったようですね」「前からdb:structuredb:schemaの2とおりがあったけど、やってることは同じだったので片方に統合したのか」

Railsガイドの更新を見るほうがわかりやすそうです↓。

# guides/source/active_record_multiple_databases.md#L139
$ bin/rails -T
rails db:create                          # Creates the database from DATABASE_URL or config/database.yml for the ...
rails db:create:animals                  # Create animals database for current environment
rails db:create:primary                  # Create primary database for current environment
rails db:drop                            # Drops the database from DATABASE_URL or config/database.yml for the cu...
rails db:drop:animals                    # Drop animals database for current environment
rails db:drop:primary                    # Drop primary database for current environment
rails db:migrate                         # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rails db:migrate:animals                 # Migrate animals database for current environment
rails db:migrate:primary                 # Migrate primary database for current environment
rails db:migrate:status                  # Display status of migrations
rails db:migrate:status:animals          # Display status of migrations for animals database
rails db:migrate:status:primary          # Display status of migrations for primary database
rails db:rollback                        # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rails db:rollback:animals                # Rollback animals database for current environment (specify steps w/ STEP=n)
rails db:rollback:primary                # Rollback primary database for current environment (specify steps w/ STEP=n)
-rails db:schema:dump                     # Creates a db/schema.rb file that is portable against any DB supported  ...
-rails db:schema:dump:animals             # Creates a db/schema.rb file that is portable against any DB supported  ...
+rails db:schema:dump                     # Creates a database schema file (either db/schema.rb or db/structure.sql  ...
+rails db:schema:dump:animals             # Creates a database schema file (either db/schema.rb or db/structure.sql  ...
rails db:schema:dump:primary             # Creates a db/schema.rb file that is portable against any DB supported  ...
-rails db:schema:load                     # Loads a schema.rb file into the database
-rails db:schema:load:animals             # Loads a schema.rb file into the animals database
-rails db:schema:load:primary             # Loads a schema.rb file into the primary database
-rails db:structure:dump                  # Dumps the database structure to db/structure.sql. Specify another file ...
-rails db:structure:dump:animals          # Dumps the animals database structure to sdb/structure.sql. Specify another ...
-rails db:structure:dump:primary          # Dumps the primary database structure to db/structure.sql. Specify another ...
-rails db:structure:load                  # Recreates the databases from the structure.sql file
-rails db:structure:load:animals          # Recreates the animals database from the structure.sql file
-rails db:structure:load:primary          # Recreates the primary database from the structure.sql file
+rails db:schema:load                     # Loads a database schema file (either db/schema.rb or db/structure.sql  ...
+rails db:schema:load:animals             # Loads a database schema file (either db/schema.rb or db/structure.sql  ...
+rails db:schema:load:primary             # Loads a database schema file (either db/schema.rb or db/structure.sql  ...

「統合されてとりあえずdeprecationになった段階だから、従来の書き方もしばらくは使えるということで」

⚓eager loadingでselectの値を無視しないよう修正

# Changelogより
post = Post.select("UPPER(title) AS title").first
post.title # => "WELCOME TO THE WEBLOG"
post.body  # => ActiveModel::MissingAttributeError

# Rails 6.0 (ignore the `select` values)
post = Post.select("UPPER(title) AS title").eager_load(:comments).first
post.title # => "Welcome to the weblog"
post.body  # => "Such a lovely day"

# Rails 6.1 (respect the `select` values)
post = Post.select("UPPER(title) AS title").eager_load(:comments).first
post.title # => "WELCOME TO THE WEBLOG"
post.body  # => ActiveModel::MissingAttributeError

つっつきボイス:「あ〜、selectで絞り込んだあとで別テーブルをeager_loadすると、selectして更新したSELECT句が無効になってたのか」 「selectしなかったのと同じになっちゃってたんですね」「Post.eager_loadとほぼ同義というか」「これはたしかによくない」

関連: #35210
パフォーマンス上の理由で、使わないカラムの数をselectで減らすことがときどきある。
たとえばGET /posts/1(postの詳細)では(ほぼ)すべてのカラムを使うが、GET /posts(postのリスト)ではすべてのカラムを使うとは限らない(idtitleはリストビューに使うがbodyは使わない場合など)。
ある関連付けがeager loadingされると、selectのカラム数削減が期待どおりに動かず、selectのカラム読み込みのほかにeager loadingでモデルの全カラムも読み込む。この動作は通常のロードやプリロードと異なる。つまり通常のロードやプリロードをeager loadingする(あるいはその逆)のは安全ではない。
このプルリクでは、「selectのカラムも読み込むほかに、eager loadingで常にモデルの全カラムも読み込む」という振る舞いを、他と同様にselectしたカラムを尊重するよう修正する。
同PRより大意

⚓datetime_select APIドキュメントのデフォルト値を修正

# actionview/lib/action_view/helpers/date_helper.rb#L
      # * <tt>:date_separator</tt>    - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
-     # * <tt>:time_separator</tt>    - Specifies a string to separate the time fields. Default is "" (i.e. nothing).
-     # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing).
+     # * <tt>:time_separator</tt>    - Specifies a string to separate the time fields. Default is " : ".
+     # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is " — ".

つっつきボイス:「datetime_selectかぁ〜、あれで表示したセレクトボックスって使いにくいんですよね😆」「それそれ😆」「生成されるAPIドキュメントの記述が違ってたそうです↑」

# 正しい記述
datetime_separator: " — "
time_separator: " : "
date_separator: ""

参考: 4 日付時刻フォームヘルパーを使う — Action View フォームヘルパー - Railsガイド
参考: datetime_select — ActionView::Helpers::DateHelper

こんな感じで表示されますね↓(1つ目がdatetime_select)。


ruby on rails - Materialize plugin breaks date and datetime selectors - Stack Overflowより

⚓Linkヘッダーをスタイルシートやスクリプトごとに自動追加するようになった

Shopifyからのプルリクです。


つっつきボイス:「HTTPヘッダーの話みたい」「何か具体例が欲しいです…」「preloadのLink要素ってカンマ区切りで書けるんだ、あんまり自分で書いたことなかったから知らなかったけど」「この辺が差分っぽい↓」

# actionview/lib/action_view/helpers/asset_tag_helper.rb#L486
+       def send_preload_links_header(preload_links)
+         if respond_to?(:request) && request
+           request.send_early_hints("Link" => preload_links.join("\n"))
+         end
+
+         if respond_to?(:response) && response
+           response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
+         end
+       end

「リクエストの場合はrequestヘッダーに入れて返して、レスポンスの場合はresponseヘッダーに入れて返してる感じ」「MDN見ると、こういう感じにLinkヘッダーでリンクを複数返せるのね↓」「これ見てちょっとわかった気がしてきたかも」

参考: Link - HTTP | MDN

# developer.mozilla.orgより
Link: <https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preconnect", <https://three.example.com>; rel="preconnect"

「テストコードもresponseヘッダーにリンクが付いたかどうかをチェックしてる↓」

# actionview/test/template/asset_tag_helper_test.rb#L513
+ def test_should_set_preload_links
+   stylesheet_link_tag("http://example.com/style.css")
+   javascript_include_tag("http://example.com/all.js")
+   expected = "<http://example.com/style.css>; rel=preload; as=style,<http://example.com/all.js>; rel=preload; as=script"
+   assert_equal expected, @response.headers["Link"]
+ end

preloadのelementLinkヘッダーでシリアライズしてHTMLのbodyがパースされる前にブラウザでプリロードできる。
これはドキュメント末尾に含まれるスクリプトでは特に便利。
実装:
この機能はEarly Hintsに乗っかっているが、原理的にはどちらも同じ機能なので、Early Hintsでは単にLinkヘッダーがどんなふうになりそうかを事前にブラウザに通知している。
シリアライゼーションにおいては、Linkヘッダーを複数送ることも、1つのヘッダーにカンマ区切りで複数の値を入れることもできる。後者は少々コンパクトになるので、自分は後者にした。
同PRより大意

⚓APIドキュメントのデフォルトインデックス名を修正


つっつきボイス:「これはAPIドキュメントの自動生成の内容が違ってたということね」「実際のものに合わせたと」

# activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L
-     #   CREATE INDEX suppliers_name_index ON suppliers(name)
+     #   CREATE INDEX index_suppliers_on_name ON suppliers(name)
...
-     #   CREATE INDEX IF NOT EXISTS suppliers_name_index ON suppliers(name)
+     #   CREATE INDEX IF NOT EXISTS index_suppliers_on_name ON suppliers(name)
...
-     #   CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
+     #   CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id)
...
-     #   add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
+     #   add_index(:accounts, [:branch_id, :party_id, :surname], name: 'by_branch_desc_party', order: {branch_id: :desc, party_id: :asc})

ActiveRecord::ConnectionAdapters::SchemaStatements#add_indexのAPIドキュメントで、生成されたSQL文のデフォルトインデックス名が正しくない例があった。
その他:
デフォルトのインデックス名は以下のフォーマットになっている。

index_<TABLE>_on_<COL1>_and_<COL2>

add_indexのAPIドキュメントには既にこれが反映されている。このプルリクは、フォーマットに準じてないSQL文が残っていたのを単に修正したのみ。なおデフォルトのインデックス名は以下の環境のRailsアプリで検証した。
Rails 6.0.3.2
Ruby 2.5.5
PostgreSQL 11.5

例の中には、add_indexnameオプションを指定したことでデフォルトインデックス名のフォーマットがないものもある。とりわけ、このプルリクで修正した「ソート順でインデックスを作成する(昇順(デフォルト)または降順)」セクションの最後のSQL文では、インデックス名がby_branch_desc_partyとなっており、これはnameオプションが渡されたことを示しているが、Rubyコードを見てみるとnameオプションは渡されていない。

add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})

このSQL文を修正して、デフォルトインデックス名がindex_accounts_on_branch_id_and_party_id_and_surnameになるようにした。今回は行わなかったが、add_indexコードを更新してnameオプションにby_branch_desc_partyを渡す別修正も考えられる。その場合、SQL文で同じインデックス名が使われる。別修正の方がよければ知らせて欲しい。
同PRより大意

⚓Rails

⚓Active Jobスタイルガイド(Ruby Weeklyより)

toptal/active-job-style-guide - GitHub


つっつきボイス:「これってスタイルガイドなんですか?」「どちらかというとActive JobでSidekiqを使うときのノウハウ集といった趣かも」「Active Jobを使うときはこう書こうというスタイルガイドでいいと思います」

mperham/sidekiq - GitHub


「Active JobではRails内部のGlobalIDを使うという話はたしかにある」「前に話題にしたGlobalIDですね(ウォッチ20181203)」

参考: 10 GlobalID — Active Job の基礎 - Railsガイド

「1個のジョブキューに重いバッチと軽いバッチを混ぜて入れると、なるべくすぐ終わって欲しい軽いバッチが日次バッチみたいな重いヤツに止められてしまうの、あるある」

# 同記事より
# bad - no queue specified
class SomeJob < ApplicationJob
  def perform
    # ...
  end
end

# bad - the wrong queue specified
class SomeJob < ApplicationJob
  queue_as :hgh_prioriti # nonexistent queue specified

  def perform
    # ...
  end
end

# good
class SomeJob < ApplicationJob
  queue_as :high_priority

  def perform
    # ...
  end
end

「ジョブは冪等かつリトライ可能に書く、みたいな定番のノウハウもあるし」

「ジョブの中にはなるべくビジネスロジックは書かないこと、ごもっとも」「せ、せやな😆」「現実にはビジネスロジックをジョブに書くのは割とありがちなんですけど、そうするとジョブの形でしか呼び出せなくなるから分けましょうと」

「ジョブからジョブを呼び出すかどうかの話、なるほど」「これはダメそうですけど、やりたくなるときってあるのかな…」「普通にあると思いますよ: たとえばメールのジョブでメール送信部分だけをマイクロなジョブにして、ちょうどここでやっているようにdeliver_laterするとか↓」「あ〜なるほどそういうことですか」

# 同記事より
# good - error kernel pattern
# bad - additional jobs are spawned
class SomeJob < ApplicationJob
  def perform
    SomeMailer.some_notification.deliver_later
    OtherJob.perform_later
  end
end

# good - no additional jobs
# bad - if `OtherJob` fails, `SomeMailer` will be re-executed on retry as well
class SomeJob < ApplicationJob
  def perform
    SomeMailer.some_notification.deliver_now
    OtherJob.perform_now
  end
end

「ジョブからジョブを呼ぶときはこれとこれを気をつけろとか書いてますね」「ジョブが発行したジョブのエラーをトラップするかどうかとか、ジョブが発行したジョブのエラーを追いかけたときになぜそのジョブが起動したのかが追いかけづらくなるとか、いろいろハマりどころがあるんですよ」「たしかに」

「ジョブからジョブ呼び出しは、やってはダメというよりは、やるなら気をつけろという感じだと思います」「ジョブはジョブ単位で管理できるようにしておかないと後で面倒なことになりますし」

「たくさんのジョブを1個にまとめる話↓」

# 同記事より
# acceptable
def perform
  batch = Sidekiq::Batch.new
  batch.description = 'Send weekly reminders'
  batch.jobs do
    User.find_each do |user|
      WeeklyReminderJob.perform_later(user)
    end
  end
end

Kernel.sleepは使うなとありますね」「ジョブの中でKernel.sleepを使うのは割とアンチなパターンです」「なるほど」「それをやると、ワーカーを握ったままsleepしたときに、ワーカー数を使い尽くした時点でジョブキューが詰まっちゃいます」「あ〜そういうことですか」

参考: Kernel.#sleep (Ruby 2.7.0 リファレンスマニュアル)

「APIの秒間呼び出し数がlimit exceededになったときとかの対応って割と面倒なんですよ: ジョブを登録し直すにしても大変なのでsleep書いちゃうこともありますけど」「あ〜わかります」「あと30秒待たないと再開できないとか」「それそれ」「そのときにジョブがワーカーを解放するように書く方がお行儀がいいのはたしかなんですけど、それも大変なので、痛し痒し感がある 」

sleepを書くべきでないのはわかるんですけど、書かざるを得ないときってありますよね」「わかります、それ」「そのためにはジョブキューが詰まらないようにするのが大事で、さっきのジョブを分ける話ともつながってくるところもあると思います」

「他にもone process per coreとか、Redisのメモリあふれに気をつけようとか、いろいろわかりみがある」

「ジョブの引数を増やしすぎるなとありますね」「う、今の案件でこれやっちゃってたかも…😅」「😆」「Active Jobのジョブに渡した引数って、たしかActive Job内の変数として持ってたような覚えがあるんですけど、そういうのもあって引数をいっぱい渡すのはあんまりよくないですね」「なるほど」

「以下みたいなuser_statusとかuser_infoみたいな引数↓って、ジョブを投入したときとジョブが実行されるときで状態が変わる可能性があるんですよ」「あ〜そうか!」

# 同記事より
# bad
SomeJob.perform_later(user_name, user_status, user_url, user_info: huge_json)

# good
SomeJob.perform_later(user, user_url)

「こういう情報は引数で渡すよりも実行時にチェックする方がより安全でしょうね」「ジョブ単体の中で取れる情報はジョブで取った方がいいと」「ジョブ投入時のスレッドとジョブ実行時のスレッドで状態を共有しようと思ったら1回シリアライズしないといけないですし、そういうところは注意が必要ですね」「よし、後で直そう😆」「😆

「これはこの間も話題になったジャストの時刻にバッチを起動するのを避けるヤツ」「開始時刻を少しずつ前後にずらしたりできるという話もありましたね」

「Sidekiqの有料機能を使うとこんなことができるという話」「APIアクセス頻度を制限する機能なんてのもあるんですね」「あると便利な機能😋

参考: Ent Rate Limiting · mperham/sidekiq Wiki

「Active Jobはジョブエンジンを限定しないつくりなんですけど、それがゆえにキューを細かく管理する機能があまりないので、こんなふうにSidekiqの高度な機能を使えるといいですよね: まあそうするしかないとも言えますけど」

参考: Active Job の基礎 - Railsガイド


「こういうスタイルガイドあると助かるかも😋」「なかなかいいドキュメント👍」「Creative CommonsのBYなので翻訳できそう」

⚓SiderがGitLabに対応開始


つっつきボイス:「今日出たニュースです」「何と、SiderがついにGitLab対応ですか!」「そうなんですよ、以前自動レビューツールのときにSiderも取り上げましたね(ウォッチ20190304)」「BPSはGitLabがメインなので、GitLabで使えて欲しいですよね」


sider.reviewより

「以前問い合わせたときはGitLab対応の予定がないとのことだったので使わなかったけど、やっとか〜」「ちょっとお高いかな💵」「10ユーザー単位か、う〜む」「自動レビューはあるとありがたいんですけどね」

以下は昨年のツイートです。

⚓後置のif


つっつきボイス:「後置のifってたしかに横に長くなるとわかりにくくなる傾向はありますよね」「個人的には三項演算子より後置ifの方が割と好きですけど」「自分もそんなにキライじゃないかな〜」「最終的には人間が読んだときに読みやすいかどうかですし」

「デフォルトのRuboCopは一行で書けるときは後置のif推奨なのね」

参考: Style/GuardClause — Style :: RuboCop Docs


つっつき後にふと思い出したのですが、以前、例のGoby言語に後置のifがない理由を@st0012さんに尋ねたところ、以下のようなよくない書き方ができてしまうから入れなかったとのことでした。

foo do
  # すごく長い処理
end if bar?

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!

⚓その他Rails

つっつきボイス:「AppSignalの記事なんですけど、割と基本的な内容かなと」「『まずRubyをきちんと学んでからRailsをやろう』、ごもっとも」

「一般によく言われているノウハウや注意点をまとめた感じの記事ですね: あとは、こういう記事を読んで欲しい人に読んでもらったときにどこまでわかってもらえるかでしょうね」「いろんな罠が説明されてますけど、読んだ人が罠を踏まなくなるかどうかはまた別というか」

「ところで記事の見出しにbread & butterってあるんですけど、ここでは『メシのタネ』みたいなニュアンスですね」「朝食メニューじゃないのか🌭


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200818後編)ruby_jardデバッガがスゴい、RubyオンラインマニュアルにEdit機能が追加、Ruby 2.7のBundlerを消す方法ほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20200825後編)Rubyクラスライブラリをgem化、Rubyテストフレームワークrr、ChromebookでWindowsが動くほか

$
0
0

こんにちは、hachi8833です。jnchitoさんのブログで週刊Railsウォッチをおすすめいただきました。ありがとうございます!

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Ruby

⚓Rubyのキーワード引数でdouble splat **を使う(Hacklinesより)

# 同記事より
class Action::AnalyseDiff
  def initialize(before: nil, after:, repository:, **kwargs)
    @before = before
    @after = after
    @repository = repository
  end

  def call
    # git_diffで何か解析する
  end

  private

  def git_diff
    @git_diff ||= if single_commit?
      # コミットを1件取得
    else
      # beforeとafterの間のコミットを取得
    end
  end

  def single_commit?
    @before.present?
  end
end

event_1 = {
  action: 'push',
  after: 'a-sha-of-a-git-commit',
  repository: 'MikeRogers0/SampleRepo',
  # ..
}

event_2 = {
  action: 'pull_request',
  before: 'a-sha-of-a-git-commit',
  after: 'a-sha-of-a-git-commit',
  repository: 'MikeRogers0/SampleRepo',
  # ..
}

Action::AnalyseDiff.new(**event_1.to_h).call
Action::AnalyseDiff.new(**event_2.to_h).call

つっつきボイス:「ハッシュを渡すときはdouble splat(**)を付けないと互換性を取れないというのはこれまでもさんざん指摘されてましたね↓」「最近になってRubyを2.7にアップグレードした人が慌ててこの辺の記事を見に来てたりして」

参考: Separation of positional and keyword arguments in Ruby 3.0

Ruby 3.0のキーワード引数変更のスケジュールが変更に

⚓rr: Rubyのテストダブルフレームワーク

rr/rr - GitHub

以下のツイートで知りました。


つっつきボイス:「rrっていうテストダブルフレームワークを初めて知ったので」「使ったことはないかもだけど見たことはあるかも」「モックとかスタブとかはひととおり持ってるみたい」

# メソッドにexpectationを1つ作成
mock(object).foo
mock(MyClass).foo

# メソッドにexpectationを1つ作成し、常に値を返すようスタブ化
mock(object).foo { 'bar' }
mock(MyClass).foo { 'bar' }

# メソッドにexpectationを特定の引数付きで1つ作成し、それで呼ばれたときに常に値を返すようスタブ化
mock(object).foo(1, 2) { 'bar' }
mock(MyClass).foo(1, 2) { 'bar' }

「どこで使われているんでしょう?」「Wikiを見ると、RubyGems.orgとかKaminariとかCanCanとかで使われてるということはそれなりにメジャーなのかも?」「お〜」「Wikiの日付が2014年なので今はもっとあるかもしれませんけど」「使わなくなったものもあったりして」

「上のツイート↑がrrがAMDで動かなかった理由だそうです」「あ〜なるほどCPUのパフォーマンスカウンタね!」

「パフォーマンス測定のときに通常の時分秒を使うと粒度が荒すぎて正確に測定できないので、インテルCPUのパフォーマンスカウンタのレジスタから値を取ってきて実行の命令数を調べるというのはよく行われるんですよ」「そうそう」「インテルCPUのレジスタから値を取ってきているので、当然インテルCPUじゃないと動かないでしょうし」「rrにはまだこのissueが上がってないみたい👀

「こうやってCPUのレジスタから値を取るというのは一種のベストプラクティスとして広く行われているので、AMDで動かない問題は他のところでも影響出るかもしれませんね」

「回答者はこのネタでRubyKaigiで話すんですって」


追記(2020/08/25): 以下のご指摘をいただきました。冒頭のrrとrr-projectは別物でした。訂正いたします。

⚓Rubyの変数


つっつきボイス:「ああ、この質問は定期的に話題になりますね」「それにMatzが自ら答えているのがスゴい」「いきなり天からのお告げを受けたみたいな⛩」「未定義のローカル変数を呼ぶとエラーだけど未定義のインスタンス変数だとnilになるというのは、もう『Rubyではそうなっているから』としか言いようがない」「今さら変えられなさそうな仕様ですね…」

A: Ruby開発開始時からの基本的な原則として、宣言されるもの(定数、ローカル変数、メソッド)は未定義アクセスはエラー、宣言されないもの(グローバル変数、インスタンス変数、HashやArrayの要素)の未定義アクセスはnilという風に決めました。すごく深い理由があったわけではなくて、Perlを参考にしただけですが。
今思えば、ここをnilでなく、エラーにしておけば間違いが見つけやすかったかもしれませんね。でも、initializeで毎回すべてのインスタンス変数の初期化が強制されるのは、やっぱりRuby的ではなかったかもしれません。
「ローカル変数の宣言?」と思った人もいるかもしれませんが、Rubyの文法上、そのスコープでの最初の代入がローカル変数の宣言になります。
Quoraより

⚓Rubyのクラスライブラリをgem化しよう(Hacklinesより)


つっつきボイス:「libの下とかに置いているクラスライブラリをgem化しようぜという記事ね」「可能ならgemにするのはいいでしょうね👍」「kazzさんもRailsでこうやってgemに切り出すのが好きですね」「プライベートgemにするとアクセス権限の扱いとかがちょい面倒になったりはしますけど、それでもgemにする方が美しいし、Railsアプリのコードがひたすらファットになるよりはgem化する方がいいでしょうね」

⚓その他Ruby

つっつきボイス:「おっと、ノベルティまだ買ってなかった」「ピザグッズ買うとRubyKaigiに貢献できるんですよね」

「Tシャツの周りにアイコンが飛び交ってる↓けど、買うと出るのかな?それとも何か押すと出るのかな?」「どっちだろう?」「ちりばめられたい方はご購入よろしく〜」


suzuri.jpより

⚓言語/ツール/OS/CPU

⚓書籍『作ろう!CPU』(予約受付中)


つっつきボイス:「まだ発売されてないんですけど、サポートページがえらく充実してたので買う気になっちゃいました」「FPGAのキットを買えば自分もCPUを組めるようになるという感じの本みたい」「老眼なので本当はKindleで欲しいんですけど」「みんなKindleでいいと思われると紙の本が出版できなくなったりして」「私は紙の本がいいです(キリッ」

参考: FPGA - Wikipedia

「そういえば同人誌でCPU自作する人たちがいましたけど、もしかすると彼ら…?」「それこそ東大にはガチでCPUを作る授業があったりしますね↓」「『ほんとうの』が付いてるのがスゴい😳

参考: 東大 理学部情報科学科/大学院情報理工学系研究科|情報科学科NAVIgation
参考: CPU実験 — ほんとうのコンピュータ自作(PDF)

「通称『CPU実験』、CPU作るだけじゃなくてコンパイラまで作ったりするという東大の名物授業」「ハードもソフトも全部作るって、クレイのスパコンみたいなことやってるんですね」「最後はチームごとに速度を競ってるし🏎」「いいな〜楽しそうだな〜」

参考: Cray-1 - Wikipedia

⚓その他

⚓ChromebookでWindows


つっつきボイス:「ChromebookでWindowsが動くというのは、要するにParallels DesktopがChromebookで動くようになるということね」

Mac: Parallels Desktop for MacでUbuntu Server LTS環境を構築する

⚓Chromebookよもやま話

「ところでChromebook Enterpriseって、Chromebookでここまでやるか?みたいなスペックですよね」「高いんでしょうか?」「メモリ32GBとか、スペックがもうノートPC並ですし」「32ですか〜?!」「Chromebookの安かろう遅かろうみたいなイメージを吹っ飛ばしてくるところはありますね」「インテルCPUだけかと思ったらx64のAMDもあるのね」「値段が気になります〜」

参考: ビジネス向け Chrome 搭載デバイス  |  Chrome Enterprise  |  Google Cloud
参考: x64 - Wikipedia

「社内でも話題にしたんですけど、Chromebookって通常のノートPCと比べてキッティングがすごくやりやすいじゃないですか: ノートPCを作業者に渡すとできることが多すぎるので、アプリのほかにセキュリティソフトやらシステム管理ソフトやらを1台ずつセットアップしてからじゃないと渡せなくて管理が面倒なんですけど、ChromebookならG-Suiteかストアを使ってもっと楽にキッティングできるんでしょう、きっと」

参考: キッティングとは - 意味の解説|ITトレンドのIT用語集
参考: Chromebooks for Business — 管理された Chromebook を使う(PDF)

「社員がChromebookで十分作業できるなら、大きな企業や組織のシステム管理部門は確実に今までより楽できるでしょうね」「特に昨今のようにコロナでリモートワーク化した企業で大変そうに思えたのはそういうキッティング部門ですし」「物理PCをキッティングしてからユーザーに配布するとなると担当者はどうしても出社しないといけなくなりますし」「ですよね」「しかもリモートワーク前なら社内で使うからよかったけど、今だと社員の自宅に発送する業務まで乗っかってきますし」「キッティングでノートPCに機密データを入れてたら、それの管理やらトラッキングやらでさらに大変そう…」

「Chromebook、企業のキッティング担当者が泣いて喜びそうですよね」「Chromebookなら紛失や盗難のときもOSの標準機能で管理者がリモートでロックしたりデータを消去したりできるでしょうし」「Chrome OSはOSレベルでそういう機能があるからいいですよね: Windowsでもそれ用のソフトを入れればやれますけど」

参考: 紛失したスマートフォンまたはパソコンをロックする、データを消去する - Google Chrome ヘルプ
参考: Google Chrome OS - Wikipedia

⚓番外

⚓数学市民化プロジェクト


つっつきボイス:「数学系の動画チャンネルって思ったよりあるんだなと思って、あとでサブスクしようかなと」「数学系YouTuberとは一体😆」「そこ気になります😆」「数学市民化プロジェクトという企画の一環みたいですね」

参考: 数学市民化プロジェクト – はじまりはKan拡張

「数学の市民化って想像がつきませんけど」「いわゆる『リベラルアーツ』の一科目としての数学ならありそうですけどね」「それならありかも」「リベラルアーツでは、隷属しない自由な市民になるための必要な科目をリストアップしてますけど、そのなかに数学もあったはず」「Wikipediaを見ると文法学・修辞学・論理学の3学、算術・幾何・天文学・音楽、の自由七科ですって」「音楽も入ってるとは」「物理とか化学みたいな科目はリベラルアーツを修めてからだったという」「話はそれからだ、みたいな」「やべ〜何の話だかわからない〜😆」(以下リベラルアーツで延々)

参考: リベラル・アーツ - Wikipedia


後編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200824前編)「Active Jobスタイルガイド」は有用、SiderがGitLabに対応、eager loading時のselectを修正ほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20200831前編)GitHubがRuby 2.7にアップグレード、Durationに変換メソッドが追加、hair_triggerでデータベーストリガほか

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2020はもう今週の金曜土曜ですね。YouTubeのRubyKaigiチャンネルでリマインダーを設定できるそうです。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

⚓Rails: 先週の改修(Rails公式ニュースより)

以下のコミットリストのChangelogを中心に見繕いました。


つっつきボイス:「そうそう、GitのコミットにはAuthorDateCommitDateの2つがありますね」「そういえばあった」「たしか普通にコミットでオプションを付けるとAuthorDateを変えられるんですけど、CommitDateは自動的にシステムの日付になるので、CommitDateを変えようとすると何か一工夫必要だった覚えがあります」「そうそう、--dateオプションはAuthorDateを変えるとき」「CommitDateを任意に変えようとすると面倒だった気がする」

参考: gitのコミット時間を変更する - Qiita
参考: Git のコミットのタイムスタンプには author date と committer date の 2 種類があるという話 - ひだまりソケットは壊れない

⚓新機能: ActionView::Helpers::TranslationHelper#translateがブロックを受け取って訳文と解決済み訳文キーを取れるようになった

このコミットはActionView::Helpers#translateヘルパーメソッド(ちなみにエイリアスは#t)がブロックを受け取れるよう拡張する。
このメソッドをブロック付きで呼び出すと、translate呼び出しが訳文を第1ブロック引数として、解決済み訳文キーを第2ブロック引数としてyieldする。

<%= translate(".key") do |translation, resolved_key| %>
  <span data-i18n-key="<%= resolved_key %>"><%= translation %></span>
<% end %>

相対的な訳文キーが完全修飾キーより先行する場合や、呼び出し側が解決済みキーに関心がない場合は、第2ブロック引数を省略可能。

<%= translate("action.template.key") do |translation| %>
  <p><%= translation %></p>
  <p><%= translation %>, but a second time</p>
<% end %>

訳文をyieldするメリットは、テンプレートローカルな変数がこれによって再利用できること。RubyのObject#tapも使える。

ただしこのコミットより先んじて、訳文キーの解決がActionView内部で行われているために、呼び出し側から利用できなかった(解決済みキー自体を明示的に決定するつもりがない限り限り)。これをブロックパラメータとして利用できるようにしたことで、生成する要素内で翻訳された値をアノテートできるようになる。
同コミットより大意

参考: translate — ActionView::Helpers::TranslationHelper


つっつきボイス:「なるほど、tヘルパーに機能が追加された」「translateにブロックを付けると複雑になるのであんまりやりすぎない方がいいかなとは思いますけど」「できるようになったのはいいこと👍」「想像ですけど、ブロック渡しにするとブロックをラムダとして解釈することになってキャッシュの高速化が効かなくなったりするのかな?それほど影響しないでしょうけど」

「Rubyのコードはこういうふうにブロックを付けるといろいろ融通を利かせられる実装が多いですよね」「デバッグでも便利なことがありますし」

⚓STI以外の型でもdemodulizeされたクラス名の保存をサポート

ポリモーフィックな型でdemodulizeされたクラス名の保存をサポートする。
Rails 6.1より前は、STI型でしかstore_full_sti_classクラス属性を用いてdemodulizeされたクラス名を保存できなかった。
このプルリクによって、STI型でもポリモーフィック型でもstore_full_class_nameクラス属性を扱えるようになった。
同PRより大意


つっつきボイス:「でもじゅらいず?」「どう日本語にしようか悩んでます」「ポリモーフィックなクラスでも動くようになったということかな」「関連するプルリクを見てみる方がいいかも↓」

「#29601が関連しているあたり、eager loadingがらみなのかも」「この辺はどう動いてるのかよくわからない…」

⚓STIよもやま話

「STIの挙動ってすぐには予測がつきにくいことがありますよね、親クラスに対してeachしたときにそのクラスが返ってくるのか親クラスが返ってくるのかとか: まあinstance_ofでチェックすればいいんですけど」

「この辺の記事↓を見てみると、モデルのインスタンスをnewするとサブクラスがあればサブクラスとして、それ以外は普通のActiveRecord::Baseとしてnewするのね」「find_sti_classはどっかで調べたことあるかも」

参考: ActiveRecord では STI をどう実装しているかを調べたメモ - Qiita
参考: find_sti_class (ActiveRecord::Inheritance::ClassMethods) - APIdock

「STIはたまにうまく設計にはまるときがありますけど、それ以外ではあんまり使わないかな」「自分もそうかも」「継承がとてもうまく作用するケースみたいに、STIがキレイに合うときはたしかにある」「STIのいい点は1個のテーブルに入るところですよね」「横断しているtypeをSQLのクエリで一発で引っ張りたい、しかもインデックスも効かせたいというときは、結果としてSTIがデータ構造的にもうまく当てはまりやすい気がします」「ふむふむ」

参考: STI — Active Record の関連付け - Railsガイド

「以前よく作ったのが通知機能でのSTIなんですけど、通知にはいろんなタイプがあるけどメッセージはだいたい共通していて、でもクリックしたときのジャンプ先は通知のタイプによって違う、みたいな機能」「ふむふむ」「Notificationみたいな親クラスを作ってそれぞれの通知タイプを作る感じでSTIにしたんですけど、理由は通知が軽く数千万レコードぐらいに増えてしまって毎回複数テーブルから引っ張ってくるのがまったく現実的でなくなってしまうから」「なるほど〜」

⚓新機能: ActiveSupport::Durationに各種変換メソッドを追加

以下が追加されたそうです。

  • in_seconds
  • in_minutes
  • in_hours
  • in_days
  • in_weeks
  • in_months
  • in_years

つっつきボイス:「Active SupportのDurationは便利ですし、よく作ったと思いますね」「in_*で変換を表すのか」「たとえば『1日は何分か』を1.day.in_minutesって書けるのね」「12.hours.in_daysは0.5だからたしかに半日」

# activesupport/lib/active_support/duration.rb#L352
+   alias :in_seconds :to_i
+
+   # Returns the amount of minutes a duration covers as a float
+   #
+   #   1.day.in_minutes # => 1440.0
+   def in_minutes
+     in_seconds / SECONDS_PER_MINUTE.to_f
+   end
+
+   # Returns the amount of hours a duration covers as a float
+   #
+   #   1.day.in_hours # => 24.0
+   def in_hours
+     in_seconds / SECONDS_PER_HOUR.to_f
+   end
+
+   # Returns the amount of days a duration covers as a float
+   #
+   #   12.hours.in_days # => 0.5
+   def in_days
+     in_seconds / SECONDS_PER_DAY.to_f
+   end
+
+   # Returns the amount of weeks a duration covers as a float
+   #
+   #   2.months.in_weeks # => 8.696
+   def in_weeks
+     in_seconds / SECONDS_PER_WEEK.to_f
+   end
+
+   # Returns the amount of months a duration covers as a float
+   #
+   #   9.weeks.in_months # => 2.07
+   def in_months
+     in_seconds / SECONDS_PER_MONTH.to_f
+   end
+
+   # Returns the amount of years a duration covers as a float
+   #
+   #   30.days.in_years # => 0.082
+   def in_years
+     in_seconds / SECONDS_PER_YEAR.to_f
+   end

「カレンダー的な処理では便利そう」「このぐらいささやかだと自分で書いてもいいでしょうけど」「こういうメソッドがあることに気づかずに実装しちゃいそう😆」「そうかも😆

⚓rails secretsを非推奨化、5.2〜のrails credentialsを推奨


つっつきボイス:「rails secretsはちょっと前からrails credentialsへの移行が進んでた気がする」「rails secretsはとりあえずsoft deprecateされたんですね」

  • Rails::Applicationにcredential用のattr_writerを追加
  • ドキュメントからsecrets.ymlへの参照を削除
  • Railsのsecretsが非推奨化されたことを示すヘッダーをsecretsコマンドのUSAGEに追加
    同PRより大意

参考: 【Rails5.2】秘匿情報はsecret.ymlではなくcredentials.yml.encで管理する【初心者】 - Qiita

⚓Railsガイド「Active Recordクエリガイド」のサンプルのモデルをBookstoreに変更

現実に即したサンプルにしたとのことです。

# guides/source/active_record_querying.md#L32
-class Client < ApplicationRecord
-  has_one :address
-  has_many :orders
-  has_and_belongs_to_many :roles
+class Author < ApplicationRecord
+  has_many :books, -> { order(year_published: :desc) }
+end

+class Book < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :author
+  has_many :reviews
+  has_and_belongs_to_many :orders, join_table: 'books_orders'
+
+  scope :in_print, -> { where(out_of_print: false) }
+  scope :out_of_print, -> { where(out_of_print: true) }
+  scope :old, -> { where('year_published < ?', 50.years.ago )}
+  scope :out_of_print_and_expensive, -> { out_of_print.where('price > 500') }
+  scope :costs_more_than, ->(amount) { where('price > ?', amount) }
end

つっつきボイス:「こちらはドキュメントの更新」「Active Recordクエリインターフェイスガイドのサンプルコードで使うモデルを書店ベースのものに書き換えたのね」「ClientAddressAuthorCustomerになったりBookSupplierが追加されたりと前より複雑になった感」「図も更新されてますね」「もっと複雑で現実的な事例をサポートできるようにしたと」「説明により適したサンプルになるならいいと思います👍

参考: Active Record クエリインターフェイス - Railsガイド

⚓Rails

⚓GitHubがRubyを2.7に上げた

Upgrading GitHub to Ruby 2.7 - The GitHub Blog

環境変数でRuby 2.6とRuby 2.7を切り替えられるデュアルブートにしてアップグレードを進めたそうです。


つっつきボイス:「ついにGitHubがRubyを2.7に!」「これは偉大!🏔」「2.7は昨年クリスマスのリリースだったから、半年以上かかったということか」「やっぱりこのぐらいはかかりますよね」「アップグレードしたことでいろいろ速くなったらしいことが書かれてる↓」


github.blogより: 起動時間(単位は秒)


github.blogより: オブジェクトのアロケーション数

「GitHubがRubyを使い続けていることでRubyがいろいろよくなっているのはいいですよね」「GitHub向けの修正もちょいちょい入ったりしてるようですし」


なお、今度のRubyKaigi Takeout 2020では2.7での最適化の話↓も聞けるそうなので楽しみにしています。


rubykaigi.orgより

⚓hair_trigger: データベース側でトリガするマイグレーションを生成

jenseng/hair_trigger - GitHub

hair-trigger: (形)(引き金が軽いことから)すぐカッとなりやすい


つっつきボイス:「ヘアトリガー?」「なるほど、モデルにtrigger.afterとかでトリガーを書いてマイグレーションを生成するとデータベース側のトリガにできるのか↓」「あら、そうみたい」「なかなかアグレッシブなgem」

# 同リポジトリより
class AccountUser < ActiveRecord::Base
  trigger.after(:insert) do
    "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
  end

  trigger.after(:update).of(:name) do
    "INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);"
  end
end
# 同リポジトリより
rake db:generate_trigger_migration
-- 同リポジトリより: MySQLの場合
CREATE TRIGGER account_users_after_insert_row_tr AFTER INSERT ON account_users
FOR EACH ROW
BEGIN
    UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;
END;

CREATE TRIGGER account_users_after_update_on_name_row_tr AFTER UPDATE ON account_users
FOR EACH ROW
BEGIN
    IF NEW.name <> OLD.name OR (NEW.name IS NULL) <> (OLD.name IS NULL) THEN
        INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);
    END IF;
END;

「整合性制約をデータベース側に置くというのはデータベース的には本来あるべき姿ではありますね」「たしかに」「基本的にRDBMSは整合性制約を保証する機能があるものですし」

参考: 参照整合性 - Wikipedia

「こういうgemは割と好きかも」「整合性制約をRailsでやりたい人はそうじゃないかもしれませんね」

「ただこのトリガーにはあくまでSQLしか書けないので、Rubyで処理したデータをブロックに渡すみたいな処理はそのままでは書けないことになりますよね」「あ〜そうなっちゃうのか」「なにしろトリガーなので」「どうしてもやりたければストアドプロシージャでRubyでやるのと同じ処理を書くとかになるでしょうし: その意味でこのgemはSQL脳の人じゃないと使いこなすのが大変かも」「Rubyでトリガーのロジックを書きたい人には悩ましいですね…」「最近のRailsエンジニアだとデータベーストリガーを使ったことない人もいるかも」

参考: ストアドプロシージャ - Wikipedia
参考: データベーストリガ - Wikipedia

「限定的な状況では有用だと思いますけど、いつもこれでやれるとは限らないかな: まあ自分は好きですけど❤

⚓カーソルベースとオフセットベースのページネーション


つっつきボイス:「たしかにカーソルベースとオフセットベースのページネーションは全然違う」「カーソルでやれるならその方がめちゃくちゃ大きいページのページネーションで明らかに有利ですけど」「ですよね」「そういえばOracleなんかだとセッションの範囲で使えるテンポラリテーブルを作ったりできますね」

参考: ページネーションとは|Web用語(意味・説明) | プロモニスタ

「実際、カーソルにしないと遅くてどうしようもないというケースはたしかにありますし」「使わざるを得ないときはありますね」「カーソルを自力で操作するのはちょっとしんどいですけど」

参考: カーソル (データベース) - Wikipedia

「このネタはruby-jp Slackでも見かけましたね」「ところでページングと書いてあると違うものを想像しません?」「OSのメモリ管理の方のページング?」「はい😆」「まあSQLの文脈なので意味はわかりますけど」「記事にはページャーという言葉も出てきてますけどlessとかmoreみたいなページャーコマンドを思い出しちゃいます」

参考: ページング方式 - Wikipedia
参考: ページャとは - IT用語辞典 e-Words


追いかけボイス: 「一応MySQLにもCREATE TEMPORARY TABLEというのがあります: PostgreSQLでもできる気がすると思ったら普通にありました↓」「temporary table、WITH句だと書ききれない複雑な処理結果とかをtemporary tableに突っ込んで処理する、みたいな用途に使えるのでたまに使いたいことあるんですけど、CREATE TABLEなpriviledgeが必要なのでそこがやや使いにくいと思うことはありますね」

参考: 第107回 CREATE TEMPORARY TABLEによる一時テーブルの利用:MySQL道普請便り|gihyo.jp … 技術評論社
参考: TEMPORARY TABLE(一時テーブル)を探る - Qiita

⚓その他Rails

つっつきボイス:「ところでWindows + WSL2でやるときって、Windowsのファイルシステム側でソースコードを変更してそれをWSL2側で動かそうとすると面倒なことになりがちですよね」「やろうとしたらできなかったり、できたとしてもトラブルが多くて大変だったな〜という印象あります」「ファイルシステムが両者で違うのでもうしょうがない」「わかってはいるんですけどね…」「8ビットで表現されるパーミッションですら両者で違ってますし」「WindowsとWSL2もいろいろすごく頑張ってるのはわかるんですけど」「ファイルアクセスはもうしょうがない」


つっつきボイス:「なるほど、例のrack-mini-profiler↓をproduction環境で動かすべきと」「APMツールでうんと詳細な情報を取ろうと思ったらrack-mini-profilerを動かしておかないと取れませんし」

MiniProfiler/rack-mini-profiler - GitHub

「rack-mini-profilerをすべてのインスタンスで動かすと全体のパフォーマンスが落ちてしまうので、全体のうち数パーセントのインスタンスだけでrack-mini-profilerを走らせてそこだけAPMで取るというのもよく行われますね」「なるほど!」「New Relicが高いので💸、5つのインスタンスのうち1つだけで取るなんてことも割とやりますし」

参考: APM(アプリケーション性能管理)ツール7選 | ニーズが高まる理由・重要性を解説 - アプリケーション性能管理 | ボクシルマガジン


前編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200825後編)Rubyクラスライブラリをgem化、Rubyテストフレームワークrr、ChromebookでWindowsが動くほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20200901後編)RubyKaigi 2020 Takeout登壇者発表、Ruby開発版が2.8から3.0へ、マイクロサービス分割ほか

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2020開催を目前にして、Rubyの開発バージョンが仮の2.8から3.0に変わりましたね。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

夏のTechRachoフェア2020もどうぞよろしく🙇

⚓Ruby

⚓RubyKaigi 2020 Takeoutの登壇者が発表


つっつきボイス:「お、いよいよ揃った」「ざっと見てみましょうか」「トップはもちろんMatzのキーノート」

「@tenderloveさんの『Don’t @ me!』っていいタイトル」「インスタンス変数のパフォーマンスについてですね」


なお以下は5月にYouTubeにアップされていた@tenderloveさんの同タイトル動画です。

「『Ruby to C Translator』も気になる」「しかもby AIですか!」

「『Road to RuboCop 1.0』か」「今年も@ko1さんの「Ractor(旧Guild)の話があるし」


そういえばGuildは数か月前に名前がRactorに変わったのですが(ウォッチ20200519)、今回のレジュメにはRactorという言葉が見当たらない…と思ったらついさっきRactorに更新されていました!

参考: ruby/ractor.ja.md at ractor · ko1/ruby


「『Rinda in the real-world embedded systems』も気になる」「リアルワールドの埋め込みシステムですか」

参考: Rinda (Ruby programming language) - Wikipedia
参考: library rinda/rinda (Ruby 2.7.0 リファレンスマニュアル)

「今回もOpalの話がありますね」「Asynchronous Opalとはアツい」「OpalはRuby to JSトランスパイラで、どのぐらい使われてるかは知らないんですが熱心にメンテされてるみたいですね」

opal/opal - GitHub

mrubyでドリキャスアプリですか!」「これはビックリ😳

mrubyをWebAssemblyで動かす話もある」「誰かやってた人が他にもいた気がしますね」

mrubyをWebAssemblyで動かす(翻訳)


「リモートイベントになっても、いつものRubyKaigiらしいコアな発表が目白押しですね」「Rails要素が少ないところもいつものRubyKaigiらしいですし」「もう来週(つっつき時点で)の週末ですか」「早いな〜、カレンダーに入れておこうっと」

「RubyKaigiの発表はどれも濃厚なので、聞く方も全力で聞かないと理解が追いつかないんですよね」「カロリーめっちゃ使いますし」「ブドウ糖が音を立てて減っていきますし」「発表後の録画配信はどうなるんでしょうね?」「こういうイベントの発表は盛り上がった場で一気に見るからいいんですよね: 録画されててもよほど興味がないと結局あとで見なかったりしがちですし」「それはあります😆」「気合入れてイベントに参加する心意気でないとですね」

⚓Rubyで選択ソートを理解する


同記事より

# 同記事より
def selection_sort(array)
  n = array.length - 1
  n.times do |i|
    min_index = i
    for j in (i + 1)..n
      min_index = j if array[j] < array[min_index]
    end
    array[i], array[min_index] = array[min_index], array[i] if min_index != i
  end
  puts array
end

array = [10, 30, 27, 7, 33, 15, 40, 50]

selection_sort(array)

つっつきボイス:「Rubyでアルゴリズムを勉強するというのはひとつの方法ではありますね: パフォーマンスも測定しようとすると破綻することも多いでしょうけど、動作を学ぶにはいいと思います」「パフォーマンスで破綻することがあるんですか?」「メソッドが特定のユースケースに対して極端にチューニングされていることがあったりするんですよね」「ああなるほど😆」「アルゴリズムに忠実に書くより既存の特異メソッドを使う方が速いなんてこともあるので、アルゴリズムのパフォーマンスも知りたければ愚直にC言語とかで書く方が向いているでしょうね」「なるほど理解です」

「Rubyは組み込みメソッドの多くがC言語で書かれていて、サボって書いても速くなるようになってたりするので、アルゴリズムの速度を学ぶには不向きな面もあると思います」「つまりRubyだと魔法のような機能でうまくやってくれると」「そういうことですね、その分パフォーマンスが予想と違ってきたりすることもままありますし」「そういう目的ならCとかで勉強する方がよさそう」「コードとしてはRubyの方が読みやすいですし、アルゴリズムを学ぶためだけにCを学ぶというのも、ね」

参考: 選択ソート - Wikipedia

⚓telephone_number: 電話番号バリデーター(Ruby Weeklyより)

mobi/telephone_number - GitHub

# 同記事より
phone_object = TelephoneNumber.parse("3175082237", :us) ==>

#<TelephoneNumber::Number:0x007fe3bc146cf0
  @country=:US,
  @e164_number="13175083348",
  @national_number="3175083348",
  @original_number="3175083348">

つっつきボイス:「電話番号バリデーターはGoogleが提供しているもの↓もあって、この間JSでやらないといけない案件で使いました」

google/libphonenumber - GitHub

「電話番号のバリデーションって国によって違ってたりしますし、めちゃめちゃ難しいですよね」「そうそうっ」「Googleのは複数の国に対応してますし、Googleがやっているという安心感もありますし、何かあっても『Googleではこうなってます』って説明できますし」「権威付け大事😆

⚓その他Ruby

つっつきボイス:「WEB+DB PRESSは紙の雑誌として結構頑張ってますよね」「昔はSoftware DesignとかWEB+DB PRESSあんなに読んでたのに最近読んでない…」「WEB+DB PRESSは扱う範囲を広げつつマニアックにしすぎない編集方針がなかなかうまくやってると思いますね」「PythonもRubyもJSも扱うし、nginxみたいなインフラ寄りのものも扱うとか」「マニアックすぎないけど初心者エンジニアが手こずるぐらいの歯ごたえがありますし、比較的中級レベルをターゲットにしつつ読者層をうまくマスに広げている感ありますね」「そーだいさんのSQL本↓みたいに雑誌の特集記事を単行本化したものにもいいものが多いですし👍


つっつきボイス:「るりまの開発環境がDocker化!」「こうやって運用を近代化していくのが大事」「ちなみにTechRachoのWordPressもリニューアル後はDockerで動くようになりました😉

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

⚓Microservicesの分割(WIP)


つっつきボイス:「はてブに上がってたので」「マイクロサービス設計の指針がベンダごとにいろいろ違っているのを見ると、ベストプラクティスはまだ固まってないんだなという印象」

⚓マイクロサービスよもやま話

「マイクロサービス化は、1つ1つのマイクロサービスのバウンダリ(境界)がはっきりするところがメリットのひとつでしょうね: そうするとリソース制御も分離されてくるので個別のマイクロサービスの構築にそれほど高いスキルを要求しなくなってくるというのはあると思います」「ふむふむ」「その分システム全体を設計するアーキテクトの責務や負担は大きくなりますけど」「個別のマイクロサービスを開発する人は自分が担当するドメインだけに注目すればいいので責任分担しやすくなりますね」

「そうなると個別のマイクロサービスだけを作る人たちの今後のスキルはどうなっていくのかなというのはちょっと気になりますけど」「個別に開発しているとシステム全体への視野は持ちにくそう」「Javaでの開発がまさにそういうふうに個別の部品の開発をいろんなところに発注するようになっていますけど、それと似たようなことがもしかするとマイクロサービス化によって今後Webにもやってくるのかなと思ったり」「まだ今は方法がいろいろあったりで過渡期感はありますけど、IstioやKubernetesを使うみたいな大きなところの方法論はだんだんまとまりつつありますし」

⚓SRE

「そうなったときに、システム全体を見るアーキテクトの道を選ぶか、それともマイクロサービスの部品づくりの道を選ぶかということになってくるのかもしれませんね」「それこそバックエンド全体を統括するIstioのようなものを管理する部門は今後いわゆるSRE(Site Reliability Engineering)的な部門になっていくだろうなと思います」「SREですか」「システム全体のパフォーマンスなどの面倒を見るのがアプリケーションエンジニアからSRE部門に移っていくでしょうし、そうなると運用の姿も変わってくるでしょうし」

参考: SREって何? これまでのシステム運用やDevOpsとは何が違うの? (1/3):CodeZine(コードジン)

「SREはちょっと前にバズワード的に流行った面はありますけど、いわゆるインフラエンジニアと呼ばれていた人たちがDevOpsの文脈でCI/CDやデプロイなどもやるようになって、今度はその先にSREという考えが生まれてアプリケーションメトリクスも取ったりするようになったという感じの流れですね: まあその言葉通りにSREをきっちり回しているところはまだ少ないと思いますけど」「ふむふむ」「SREを正しくやろうとするとアプリケーションメトリクスをきちんと取っていかないといけないので」

「アクセス数とかリクエスト数みたいな項目はどちらかというとインフラエンジニアの範疇なんですけど、本当に取らないといけないのはもっとユーザー寄りでビジネスに直結する注文数とか注文単価とか離脱数のような項目で、そういったものを可視化したりするのが本当のSREだと自分は思ってますし、実際SREはもともとそういう文脈で生まれていますし」「なるほど」

「SREを真面目にやろうとすると権限移譲なども含めて相当大変になるので、トップに立つ人のスキルが相当高くないとなかなか成り立たないと思います」「そうですよね」「そしてスキルの高い人ほどすぐ転職したりするので、初期構築はよくてもその後の運用が回らなくなるみたいなことがもう少し経つといろいろ起きてくるかも」「まあまあ😆

「Javaみたいに枠組みが完全に確立していれば人が入れ替わっても回りますけど、今のマイクロサービスはまだ設計者のノウハウやスキルに左右される部分が大きいので」「そういう人が抜けちゃったら大混乱ですよね😅」「それに構築のうまい人と、構築後の運用がうまい人はまた別なんてこともよくありますし」

「そんなこんなでマイクロサービス方面はまだしばらく賑やかになるかなと予測してます」「よくあるナントカ曲線みたいに、最初はうんと期待されてその後失望して、それから持ち直して安定するみたいなヤツの途中にいる感じでしょうか」「それそれ、マイクロサービスはまだ下に落ちきってない印象がありますね」「自分もです」

「でもマイクロサービスでうまく回せている大きな組織があるのもたしかですし、一方でうまく回せていないところの話もちらほら聞きますけど、マイクロサービスという方法論自体は悪いものではないと思います」


たぶんこの曲線かなと思います↓。なおGartnerの以下のツイートはマイクロサービスとは無関係です。

参考: ハイプ・サイクル - Wikipedia

⚓その他インフラ


つっつきボイス:「このpull数の制限は厳しいですよね」「6時間あたり100回までって、CI止まりそう…」「開発者が個人で使う場合はDocker Hubにログインすればいいでしょうけど、CIはいちいちDocker Hubにログインしないし」「自分でミラー立てるとかしないといけなくなるんだろうか?🤔

⚓JavaScript

⚓Linter

つっつきボイス:「これあるある」「アノテーション付ける言語だとありがちですね: 言語の中に別の言語があるといろいろつらい」「そんな中でRubyMineなどのJetBrains IDEはこういう処理をかなり頑張ってますね」「へ〜?」「たとえば文字列リテラルの中にSQLが書かれているとすると、そのSQLがたとえばMySQLのものだと指定すればちゃんとシンタックスハイライトしてくれるんですよ」「それ見てみたいです」(しばらくJetBrains IDEでデモ)


以下のツイートは上の内容と直接の関係はありません。

⚓その他

⚓「スレッドが同じでも送信者が同じとは限らない」SMS


つっつきボイス:「SMSのメッセージって、機能としては1通1通が完全に分割されていて、ある程度以上の長さになるとメッセージを結合したりするんですけど、実はその結合方法って割とよしなにやってるだけだったりするんですよ」「ありゃ」「記事にも書いてますけどSender IDを自称できてしまいますし」「コワい…」

参考: ショートメッセージサービス - Wikipedia

「SMSは元が電話網なんですけど、たしかこの辺はSMSのキャリアによっても違っていて、インターネットみたいに統一的なルールが決まってないんですよ」「ははぁ」「たとえばこの間使ったTwilioはSMSメッセージを送信するAPIを持ってるんですけど、以下↓を見るとわかるようにメッセージにAlphanumeric Sender IDを付けられるかどうかが国によって違ってます」「ホントだ」「そして日本は基本的にそれができないんですけど、ホワイトリストオプションがあるのでお問い合わせして審査を受けて通るとできちゃったりします」「へぇ〜!」

参考: アルファニューメリック送信者IDとは? - Twilio

「なので日本から送信するときはAlphanumeric Sender IDを付けることはあんまりできないんですけど、Sender IDを送信できる国からSender IDを付けて日本に送っちゃえばできますね」「あ〜なるほど」「当然送信元は日本になりませんけどSender IDは通る」

「という具合にSMSは国ごとに事情が違ってなかなか大変」「う〜む」


後で多少関連のありそうなTwilio公式ツイートを拾ってみました。

⚓番外

⚓テラ


つっつきボイス:「ネットワーク帯域でこのスピードを出せても、取ったデータを置くところがあるのかと」「10GBイーサネットが出たときにも思いましたけど、これってSSDに入り切らないのでは?って」「それそれ、ストレージが追いつかない😆

参考: 10ギガビット・イーサネット - Wikipedia

「こういう高速なインフラって、途上国の方が後から新しい機材を入れて速くなったりしますよね」「そうそう、先進国は投資を回収するまで次のを入れられなかったり」「5Gも日本だとなかなか進んでない感」「現状にはそんなに不満はないんですけどね」「速くなればNetflixをスマホで見るようになりますよきっと」(以下延々)

参考: 第5世代移動通信システム - Wikipedia


後編は以上です。

バックナンバー(2020年度第3四半期)

週刊Railsウォッチ(20200831前編)GitHubがRuby 2.7にアップグレード、Durationに変換メソッドが追加、hair_triggerでデータベーストリガほか

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

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

Ruby Weekly

Postgres Weekly

postgres_weekly_banner

Viewing all 1383 articles
Browse latest View live