patorashのブログ

方向性はまだない

言葉の遅れが改善する方法を読んだ

長男が発達障害と診断され、療育手帳を取得していて、療育施設にも通っている状態です。

うちの子はまだ言葉の獲得ができていないので、それのヒントがあればと思い、購入。

本を読んでほしいので本の内容について書きすぎませんが、内容を見ると本当に身に覚えがありすぎる…。小さな頃からテレビ漬けになってしまっていたのだなぁと思うし、そのせいで人の話言葉を聞く能力が育っていないというのはめちゃめちゃ腑に落ちました。自分達の呼びかけが、テレビの音と同じ雑音だと判断されているとは思いもよりませんでした。

発達障害児の増え方についてもデータで語られていて、昔は5,000人に1人だったのが、今は100人に1人という割合になっている等、激増しているのがわかります。

アナログ子育てにしましょう、と綴られており、現代にとってかなりの難題ではありますが、常になにかしらの音や光に晒されるようなことをやめればいいわけだし、工夫次第でなんとかなると思うので、テレビは当分封印し、スマホもなるべく控えるように、絵本を増やして読み聞かせして、前よりももっと一緒に遊ぶようにしていこうと思います。

これは、著者の片岡先生のサイトに掲載されていたYouTubeの動画。


テレビの子守は危ない!自閉症と診断された言葉遅れの子どもたち

Rails6.0にアップグレードしたらActiveStorageでハマった件(追記あり)

最近は担当製品の関連アプリを改修しているのですが、Railsのバージョンを5.2系から6.0にアップグレードしました。そのときにActiveStorageでハマったので備忘録を残しておきます。なお、担当製品は既にRails6にしてあります。ただし、まだActiveStorage使ってない。その時の記事はこちら。

patorash.hatenablog.com

普通にRails6へのアップグレード

先ほどの記事を読みながらどういう対応をしたかを思い出して対応。一通り、手動で動かしてみたところ、問題なさそう。そして、RSpecを実行するとめちゃくちゃ落ちる!!びっくりするくらい落ちる!!

テストが落ちる原因を探る

テストが落ちた原因は、まぁRailsのバージョンを上げたのだから、バージョンの差異が原因でしょう。ActiveStorageが絡んでいるところが落ちていたので、調査しました。Qiitaに変更点がまとめられている記事がありました。ありがたい。

qiita.com

ここの中で「3. アップロードしたファイルがストレージに保存されるタイミングがsave時に変更」という見出しが…。

Rails5.2までは、インスタンスのattributeに代入した時点で、アップロードしたファイルがストレージに保存されていました。 Rails6.0では、saveしたタイミングでストレージに保存されるよう変更になっています。saveトランザクションがコミットされた後に、ストレージに保存されます。

おぉ…、まさにこれが原因でした…。

なぜsave後にアップロードでテストが落ちるのか?

理由は、バリデーションのためにActiveStorageにアップロードしたファイルの中身を参照して検証していたからです。

class Massage < ApplicationRecord
  has_many_attached :files

  validates :files, presence: true
  validate :check_files, if: -> { self.files.attached? }

  private

  def valid_files?
    self.files.all? do |file|
      file.blob.open do |f| # => Rails6だと、ここでopenできずに落ちる!
        # 検証する
      end
    end
  end

  def check_files
    self.errors.add(:files, :invalid) unless valid_files?
  end
end

ActiveStorage::Blobモデルが保存されていないため、ActiveStorage::FileNotFoundErrorで落ちてました。

アップロード前にファイルの内容を検証できるのか?

blobをopenしても落ちるので、何かいい方法はないものかと調べましたら、Railsのissueに有力な情報がありました。

github.com

attachment_changes['files'].attachablesを使え、とあります。(has_many_attachedならattachablesで、has_one_attachedならattachableになる)

pryを使いながら検証してみたところ、attachableの中身は、{ io: Fileオブジェクト, filename: 'ファイル名' }というHashでした。 そこで、以下のように修正。

def valid_files?
  self.attachment_changes['files'].attachables.pluck(:io).all? do |file|
    # 検証する
  end
end

これでRSpecを実行したところ…。modelのテストは通りました!修正成功です!

SystemSpecが落ちる…

しかし、テストを全部流してみたらSystemSpecが落ちました。手動で画面を操作してみたところ、確かに落ちます。しかも、先ほどの変更したところで…。 self.attachment_changes['files'].attachablesの中身がHashの配列ではなく、Stringの配列になっていました。なんでやねん…。

そもそも最初は手動テストで通ってのに、RSpecのテストが落ちるから修正したら、手動テストが通らなくなったので、もしかしたら画面からの場合はモデルの保存前でもActiveStorageにファイルがアップロードされてる?と考えてpryを仕込みながら調査したところ、ビンゴ。

2020-11-21 追記

もしかしたら画面からの場合はモデルの保存前でもActiveStorageにファイルがアップロードされてる?

これは、ActiveStorageのダイレクトアップロードしているからでした…。それなら当然ですね…。

画面からファイルをアップロードした場合

Messageモデルを保存していない状態で、blobオブジェクトの中身を見た結果。idがあるので、保存済みの証拠です。つまり、ActiveStorageにアプロード済み。(2020-11-21 追記:ダイレクトアップロードしていたからです!)

[1] pry(#<Message>)> self.files.first.blob
=> #<ActiveStorage::Blob:0x00007fb56b9479b0
 id: 320,
 key: "************************",
 filename: "test.csv",
 content_type: "text/csv",
 metadata: {},
 byte_size: 1452,
 checksum: "**************************",
 created_at: Fri, 20 Nov 2020 13:45:17 JST +09:00>
RSpecでファイルをattachした場合

テストでは、attachメソッドで付けてます。

message = FactoryBot.build(:message)
file = File.open(Rails.root.join('spec', 'files', 'test.csv'), 'r')
message.files.attach(io: file, filename: File.basename(file))

pryで中身を見た結果は、以下の通り。idnilのため、ActiveStorageにはまだアップロードされていません。

[1] pry(#<Message>)> self.files.first.blob
=> #<ActiveStorage::Blob:0x00007fdcfd305d68
 id: nil,
 key: nil,
 filename: "test.csv",
 content_type: "text/csv",
 metadata: {"identified"=>true},
 byte_size: 600,
 checksum: "**************************",
 created_at: nil>

導かれる仮説は…

2020-11-21 追記

ダイレクトアップロードしていたからだったので、この仮説は間違っていました。すみません。

  • 画面を経由せずにActiveStorageにアップロードする場合は、モデルの保存が成功したタイミングでアップロード。
  • 画面を経由してActiveStorageにアップロードする場合は、モデルの保存をする以前にアップロード。(これが間違い。ダイレクトアップロードしてたから)

となると、画面を経由しているケースを想定した処理と、経由していないケースを想定した処理を書かないといけません…。ここにすごく違和感を感じる…。

修正

とりあえず、コードは以下のようになりました。

class Massage < ApplicationRecord
  has_many_attached :files

  validates :files, presence: true
  validate :check_files, if: -> { self.files.attached? }

  private

  def valid_files?
    if uploaded? # => アップロード時は今まで同様の処理
      self.files.all? do |file|
        file.blob.open do |f|
          # 検証する
        end
      end
    else
      # テストにてattachで添付時はこちら
      self.attachment_changes['files'].attachables.pluck(:io).all? do |file|
        # 検証する
      end
    end
  end

  def check_files
    self.errors.add(:files, :invalid) unless valid_files?
  end

  def uploaded?
    self.files.all? { |file| file.blob.persisted? }
  end
end

これでテストは全て通るようになりました。やった!!!

不満点

違和感を感じると書いていますが、その違和感の正体は、「これだとモデルのユニットテストでActiveStorageにファイルが保存されているケースを検証できない」という点です。attachだと保存されないから…。画面からのアップロードでも、ActiveStorageのアップロード前の状態で、デフォルトで一時ファイルへのパス情報をどっかに保存していたらいいのに…。ダイレクトアップロードでなければ一時ファイルに保存されていることを確認したので、私が間違えていました。

何か他にいい方法をご存知の方がいらっしゃいましたらコメントでもツイッターでもいいので教えてください!!

2020-11-21 追記

画面からアップロードした際に先にクラウドにアップロードされていた原因はダイレクトアップロードをしていたためでした。先にコード読めよって話ですけど、自分が作ったところじゃなかったのでテストが落ちる原因にばかり考えが巡ってしまってActiveStorageのダイレクトアップロードの存在を忘れてました。このブログを書いた後に、急にピーン!ときました。

ダイレクトアップロードではない場合、当然ながらモデルが保存されるまではアップロードされず、一時ファイルに置かれていました。attachableの内容もHashではなく、ActionDispatch::Http::UploadedFileになっていたので、これに合わせて修正すればよさそうです。

>> self.attachment_changes['files'].attachables.first
=> #<ActionDispatch::Http::UploadedFile:0x00007f9d4a54ef18
 @tempfile=#<Tempfile:/var/folders/7m/9fg43zzx58lb4715wm1dkj00yj5swh/T/RackMultipart20201121-93972-xgi34u.csv>,
 @original_filename="test.csv",
 @content_type="text/csv",
 @headers="Content-Disposition: form-data; name=\"message[files][]\"; filename=\"test.csv\"\r\nContent-Type: text/csv\r\n">

となると………

  • ダイレクトアップロードの場合のバリデーション
  • ダイレクトアップロードじゃない場合のバリデーション
  • attachメソッドを使った場合のバリデーション

の3パターンを考えないといけないわけか…。うっ、頭が…。

(これはあくまでファイルの中身をチェックして保存したい場合の話なので、普通に画像をアップロードするだけ、とかだとこんな面倒な話にはなりませんのでActiveStorageが怖いわけではありません)

2020-11-26 追記

ダイレクトアップロードじゃないケースは実装上、ないので、そこはテストで放置して、一応attachメソッドも考慮したテストを追加して対応しました。 FactoryBotのコードは、こう。

FactoryBot.define do
  factory :message do
    sequence(:content) { |n| "メッセージ#{n}" }
  end

  trait :with_direct_upload_file do
    after :build do |message|
      valid_file = File.open(Rails.root.join('spec', 'files', 'valid_file.csv'), 'r')
      blob = ActiveStorage::Blob.create_and_upload!(
          io: valid_file,
          filename: File.basename(valid_file),
          identify: false
      )
      message.files.build(blob: blob)
    end
  end

  trait :with_attach_file do
    after :build do |message|
      valid_file = File.open(Rails.root.join('spec', 'files', 'valid_file.csv'), 'r')
      message.files.attach(io: valid_file, filename: File.basename(valid_file))
    end
  end
end

そして、RSpecはこう。

RSpec.describe Message, type: :model do
  context 'ダイレクトアップロードの場合' do
    subject { FactoryBot.build(:message, :with_direct_upload_file) }
    it '保存できること' do
      # 検証する
    end
  end

  context 'attachの場合' do
    subject { FactoryBot.build(:message, :with_attach_file) }
    it '保存できること' do
      # 検証する
    end
  end
end

とりあえず、これでモデルのテストでもダイレクトアップロードを考慮したテストを行うことができました。

デジタル・ミニマリストを読んだ

丸善CHIホールディングス株主優待を使って買った本。岡山駅丸善書店があったのでよかった。この本を選んだ理由は、なかなかスマホから離れられないから…。デジタル・ミニマリストとはどういうものなのか知りたかった。この本の著者はコンピュータ科学の専門家で、単にミニマリストって言ってる人ではなさそうだったのも買った決め手。

アテンション・エコノミーを知る

人々がフェイスブックツイッターなどのSNSやニュースアプリに費やしている時間はかなりのものとなる。アテンション・エコノミーとは、日本語にすると注意経済なのだが、つまりは人々の注意を引けば引くほど儲かる仕組みということだ。PVが増えれば増えるほど、広告のビュー数が増えるので儲かる企業。向こうは注意を引けば儲かるので、多くの予算と労力をかけてこちらの時間を奪おうとしてくる。こちらの時間をそのアプリが奪えば奪うほど儲かる仕組みだからで、それに対抗するのは非常に難しい…と書いてあった。

スマホアテンション・エコノミーの最強の味方

要約すると「スマホはヤバい。便利だけど恐ろしい。なぜならばアテンション・エコノミーにとっての最強の集金マシンだからだ。あなたの注意を引くためなら何でもやってくる。あなたの時間は彼らのお金。」という感じだと思う。たしかにその通り…。ちょっと暇になるとついついスマホを見てしまい、気付いたらすぐに5分くらい経ってしまう。それの繰り返しで大量の時間をお金に替えられている。

別にお金に替えられるのが悔しいから止めようっていうふうに言ってるんじゃなくて、相手はお金に替えるのが目的だから、狡猾にあなたの時間を奪いに来るんだよっていうことを言ってる。

どう対処するか?

まずは1ヶ月、スマホからSNSを削除してみようという提案をしていた。SNSに限らず、一気に見てしまいがちなNetflixなどの動画サービスや隙間時間にやってしまいがちなスマホゲームなども含まれる。ここで重要なのは、退会は別にしない。いざとなればPCから見られるようにしておく。これだけでも、最初はスマホ依存症の禁断症状でついつい開こうとするけれど、アプリがないので何もできないで、他の事をやるようになり、だんだん有意義に過ごせるという。

戻すときは全部戻すのではなく、生活してみて本当に重要だったものとそうでないものに分けて、節度を持って再度取り入れる。不要なものはそのままさようなら。

有意義な時間の過ごし方について

スマホ依存から脱却したとして、暇な時間があるとまた逆戻りしかねないからか、有意義な時間の過ごし方についての提案が色々とあった。例えば

  • 趣味を持つ(できればデジタルでないもの)
  • コミュニティの活動に参加する
  • 手仕事を身につける

手仕事を身につけるところで面白かったのは、どうやって手仕事を学ぶのか?というのでYouTubeの動画を見たらいっぱい紹介されてるっていうふうに書いてあったところだ。つまりはデジタルであっても有意義な活動のために取り入れるならOKってことだ。たしかに魚の捌き方の動画とかもあったりするので、そういうのは面白そう。

代替手段について

ニュースアプリの代替は新聞だったり、スマホの代替はフィーチャーフォンだったりと、そういうのを薦めてる。スローメディアを活用しようということだった。スローメディアという言い方だと遅いみたいな感じに聞こえるが、ここのスローはスローライフみたいな部類のやつである。速報性ばかりを気にすると、目につけたいから過激なタイトルを付けたり速報性を重視しすぎて間違った情報を流したりすることもあり、速ければいいかというとそうでもない。スローメディアはファクトチェックをして情報を整理してから出してくるので、たくさんの速報性メディアを読むよりも短い時間でより深く物事を理解できることが多い。と書かれていた。

フィーチャーフォンでも最近はさほど困らない。なぜならPCも軽量化されてきていて、持ち運びにはさほど苦労しないので調べものがしたくなったらPC使えばいいから、だそうだ。うーん、まぁそれはそうかもしれんけれど…と思うけれど、それだけでアテンション・エコノミーから距離を置くことができるという点ではいいのかも。

全体的な感想

この本を読んでてめちゃめちゃ刺さった言葉があって、それは「孤独欠乏症」だった。いつでもSNSで繋がっていてメッセージを送りあえるせいで、本当の意味で孤独になれる時間が取れない。孤独は内省を促したり、アイデアをまとめたりするのに必要な時間なのにそれが十分に取れていないのが現代人である、というのは本当にそうだなと思った。昔はもっと色々アイデアをまとめたりしていたのに、最近は全然できていなかったので、完全に毒されていたなぁと思う。

この本を読んでいる間に、アプリは削除していないけれど、なるべくスマホに近寄らないようにしてみたが、それだけでだいぶ本を読んだり子供とちゃんと遊ぶ時間が増えたので既に効果を実感している。

個人的な取り組み

SNSアプリやニュースアプリもそうだが、個人的にヤバいのは投資系アプリで、ついつい頻繁に相場のチェックをしてしまう。損してないか?が気になってしまうのだろう。まぁ大損してるんですけど。 大損してるにも関わらず更に時間まで奪われてしまって、本当に本末転倒だな…と大いに反省したので、FX用の資金を引き揚げて株のほうに回した。これだけでとりあえず円相場を見なくて済む…。そして、株相場も頻繁に見てしまうので、株主優待がいい企業と応援している企業だけに絞って投資しようと思っている。優待のためなので、相場の上げ下げに一喜一憂しなくても済む。これで週に一回くらいのチェックで済むし、見る回数自体を減らしてもいいので、スマホから投資系のアプリを消せる。

SNSも見る回数がだいぶ減っている。特にFacebook見てない。時々近況報告を書きこむ程度でいいかもしれない。twitterは仕事中には時々見る程度だが、やはり減ってる。LINEの通知がウザいのでなんとなく入れた企業アカウントはバッサバッサと消してる。通知もサイレント通知に替えた。

FreedomというWebサイトやアプリをブロックする用アプリがあるらしいので、それのアカウントを作っておきたい。

SNSスマホとの付き合い方を見直せる本

めっちゃいい本だった。この本は別にSNSを否定していない。使われるのではなく、効果的に使うようになろう!そう言っている。 自分自身、これから取り組んでいくところなんだけれど、それでも随分有意義に時間を過ごせるようになった感じがする。そして、なんとなく時間が緩やかに過ぎるようになった気もしている…。内省する時間が増えたからだろうか…。孤独欠乏症から少し立ち直れそう。

RSpecによるRailsテスト入門を読んだ

RSpecの本はすごく前にThe RSpec Bookを読んでいたけれど、もうすでにだいぶ古いし、他の人に薦められるかってのと自分が知らないことが書いてあるかもってことで、Everyday Rails RSpecによるRailsテスト入門を買って読んでみた。

leanpub.com

とはいえ、買っていたのは何時だっただろうか…。半年前くらいだっただろうか…。積読の解消がなかなか終わりません。EBookだと尚更。

テストの書き方だけでなく、テストを書くための指針が分かる

慣れたもんだと何をテストすればいいかは自ずと分かっているのである程度雰囲気でガシガシ書いていけるのだけれど、慣れていないとどこまでテストを書くべきなのか、とか、どこからテストを書くべきなのか、がわからないかと思う。まぁRailsの場合は普通はModelから書いていくと思うが、そのあたりも押さえてある。そして、ControllerSpecとRequestSpecについてや、FeatureSpecについて、付録ではSystemSpecについても書いてあった。Viewのテストはしないってのも、現実的。まぁしないというよりはFeatureSpecでやるって話だけれども。

TDDにも踏み込んでいて、そのときはFeatureSpecから書いていってて、それもまたいいと思った。写経すればTDDを体験できる。

そして、モックとスタブの話もあってよかった。私は未だに何気に混乱する…。モックとスタブって、時々しか書かないからっていうのはあるけれど。そして、モックとスタブを使うときの指針についても書いてあった。概ね同意だけれど、まぁActiveRecordの振舞をスタブ化することはあるかなぁと思う。

FactoryBotの解説がよかった

FactoryBotでデータを準備するときの書き方が色々解説されていてよかった。traitとか、こういうのは本当に使いたいときに調べない限りなかなか目に付かないので、先に教えられると嬉しい情報だと思う。RequestSpecで使えるFactoryBot.attributes_forは多分使ったことがなかったと思うので、今後使っていきたい。

SystemSpecにしていこうと思った

うちのプロジェクトはRails 6.0系だけれど、まだFeatureSpecを使っているので、付録のSystemSpecへの移行を見ながらやっていこうと思った。こういう記事は何気に嬉しい。

感想

後輩の指導にも使えそうだし、自分にも学びがあってよかった。正直Railsをやってると、実装よりもテストを書くほうが時間がかかるときがあったりするので、テストを効率的に書けるようになるのは非常に重要だと思っている。しかしRSpecについて学ぼうとすると、この本かThe RSpec Bookだと思うのだけれど、The RSpec Bookは古すぎるので、この本を自信を持ってお薦めしていきたい。

テストのリファクタリングやっていこうかなぁ~という気持ちになった。

leanpub.com

has_manyの最新のデータをhas_oneで関連付けする方法

元ネタはこのQiitaの投稿。

qiita.com

この投稿のように、UserモデルとArticleモデルが1対多になっていて、ユーザーに紐づいた最新の記事を取得したいこととかはあると思います。私がやってるプロジェクトでも似たようなことがありました。ユーザーに紐づいたデータがバージョン管理されていて、最新のが欲しいとき、とか…。

N+1が起きるコード

以下のようなModelがあったとします。

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
  belongs_to :user
end

これで最新の記事を取るためにメソッドを定義したら、こうなります。

class User < ApplicationRecord
  has_many :articles, dependent: :destroy

  def latest_article
    articles.order(id: :desc).first
  end
end

これをViewでループすると、残念なことにN+1問題が発生します。

<ul>
<% @users.each do |user| %>
  <li><%= link_to user.latest_article.title, user.latest_article %></li>
<% end %>
</ul>

Userが3名いて、記事が登録済みの場合、このようなログが出ました。

Started GET "/users/" for 127.0.0.1 at 2020-10-31 02:58:25 +0900
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  User Load (1.2ms)  SELECT "users".* FROM "users"
  â³ app/views/users/index.html.erb:6
  Article Load (0.9ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  CACHE Article Load (0.0ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  CACHE Article Load (0.0ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 3], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  CACHE Article Load (0.0ms)  SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = ? ORDER BY "articles"."id" DESC LIMIT ?  [["user_id", 3], ["LIMIT", 1]]
  â³ app/models/user.rb:7:in `latest_article'
  Rendered users/index.html.erb within layouts/application (Duration: 87.8ms | Allocations: 17016)

ここではNが3のため、3+1=4回のクエリが発行されていることが確認できました。

latest_articleをhas_oneで定義

では、見出しの通りにhas_oneにしていきます。

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
  has_one :latest_article,
           -> { where(id: Article.group(:user_id).select('MAX(id)')) },
           class_name: 'Article'
end

肝は、whereのサブクエリで外部キーであるuser_idでGROUP BYを行い、その最新の記事IDを取るためにMAX(id)をしているところです。これで、取得されるのはユーザー毎の最新の記事のみになります。

こうなると、メソッドではなくリレーションになったため、preloadなどが使えるようになります。

使ってみる

では、Controllerでpreloadを使ってみます。

class UsersController < ApplicationController
  def index
    @users = User.all.preload(:latest_article)
  end
end

Viewは変わりません。

<ul>
<% @users.each do |user| %>
  <li><%= link_to user.latest_article.title, user.latest_article %></li>
<% end %>
</ul>

しかし、発行されるSQLは2回になっています!

Started GET "/users/" for 127.0.0.1 at 2020-10-31 03:06:53 +0900
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  User Load (0.6ms)  SELECT "users".* FROM "users"
  â³ app/views/users/index.html.erb:6
  Article Load (2.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (SELECT max(id) FROM "articles" GROUP BY "articles"."user_id") AND "articles"."user_id" IN (?, ?, ?)  [["user_id", 1], ["user_id", 2], ["user_id", 3]]
  â³ app/views/users/index.html.erb:6
  Rendered users/index.html.erb within layouts/application (Duration: 56.3ms | Allocations: 11242)

eager_loadを使えば1回でも済みますが、どういうクエリが発行されているかを確認しやすいのでpreloadにしてみました。

has_oneがこういうふうに使えるのは便利です!

開発環境で複数のRailsアプリを起動する場合はActiveJobのキュー名に気を付けよう

アプリ連携を作っていた時に起きた現象なので、複数Railsアプリを起動する場合は気をつけましょう。

FooアプリとBarアプリがあって、どちらもActiveJobを使っていました。どちらもqueueの名前はdefaultのままにしていました。そして、同じRedisを共有していました。

こっちはFooアプリのジョブ。

class FooJob < ApplicationJob
  queue_as :default

  def perform
    puts "Foo"
  end
end

こっちはBarアプリのジョブ。

class BarJob < ApplicationJob
  queue_as :default

  def perform
    puts "Bar"
  end
end

そして、FooアプリもBarアプリもsidekiqを起動。

bundle exec sidekiq

これで、BarJob.perform_laterを実行したところ、Fooアプリ側のログにclass BarJobが定義されていないというログが出てきました。 キュー名がどちらもdefaultのため、どちらのジョブか判別する術がありません。

キュー名にprefixをつける

ActiveJobは設定でキュー名にprefixをつけることができるので、つけておきましょう。

今回は開発環境だけでキュー名を分けたかったので、Fooアプリ側のconfig/environments/development.rbを以下のように修正しました。

Rails.application.configure do
  config.active_job.queue_adapter = :sidekiq
  config.active_job.queue_name_prefix = "foo_#{Rails.env}"
end

同様に、Barアプリ側のconfig/environments/development.rbも修正。

Rails.application.configure do
  config.active_job.queue_adapter = :sidekiq
  config.active_job.queue_name_prefix = "bar_#{Rails.env}"
end

sidekiqに処理するキュー名を教える

今までのままだと、キュー名がdefaultのもののみを処理しようとするので、キュー名を教える必要があります。

CLIで指定する

Fooアプリ側は以下のように。

bundle exec sidekiq -q foo_development_default

Barアプリ側も同様に。

bundle exec sidekiq -q bar_development_default

設定ファイルで指定する

CLIで毎回キュー名を打つのは面倒なので、config/sidekiq.ymlを定義しました。デフォルトはprefixなしですが、developmentの場合だけprefixが付いたキュー名にしました。

これは、Fooアプリ側。

---
:queues:
  - default

:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 5) %>

development:
  :queues:
    - foo_development_default

同様に、Barアプリ側も。

---
:queues:
  - default

:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 5) %>

development:
  :queues:
    - bar_development_default

これで、各Railsアプリ同士でキュー名が干渉することはなくなりました。

Herokuの環境変数MEMORY_AVAILABLEがなくなってた件

以前にこんな記事を書いていました。

patorash.hatenablog.com

HerokuのDyno毎にMEMRY_AVAILABLEが設定されているから便利、と思っていたのですが、Herokuのログを見るとpuma_worker_killerが512MBを超えた時点でpumaのworkerを終了させまくっていたので、もしや!?と思って調べたら、設定から無くなってました…。

自分でDyno Sizeに合わせて設定しておくのがよさそうです。