patorashのブログ

方向性はまだない

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

丸善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が付いたキュー名にしました。

2021-03-16 追記

(ここから)
ActionMailerでdeliver_laterを使って非同期メール送信する際に、以前のキューの設定では動かないことがわかりました。mailersを追記するように修正しています。
(ここまで)

これは、Fooアプリ側。

---
:queues:
  - default
  - mailers

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

development:
  :queues:
    - foo_development_default
    - foo_development_mailers

同様に、Barアプリ側も。

---
:queues:
  - default
  - mailers

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

development:
  :queues:
    - bar_development_default
    - bar_development_mailers

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

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

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

patorash.hatenablog.com

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

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

sidekiqとactiverecord-session_storeを使っている場合はWeb UIが使えない

バージョン

何が起きたのか

ActiveJobのアダプターにresqueを使っていたプロジェクトで、アダプターをresqueに変えようとしていました。そこで、諸所の設定を修正後、ActiveJobにキューイングしても、うんともすんとも言わない…。sidekiqは起動しているにも関わらず、全く何も起きない…。

調査開始

qiitaの記事を見かけました。

qiita.com

これによると、rack 2.0.7から2.0.8になると、セッション関係でエラーが起きるようになるとか…。そして、2.0.9で直った、とあります。

うちのrackのバージョンは…2.2.3。あれ?

セッションを無効にしてみる

なにはともあれ、上記の記事を参考にセッションを無効にしてみます。

require 'sidekiq/web'
Sidekiq::Web.set :sessions, false # <= セッションを無効化

Rails.application.routes.draw do
  # 他は省略
  mount Sidekiq::Web => '/sidekiq' if Rails.env.development?
end 

すると、今までキューイングされていたジョブが一斉に処理され始めました…。ActiveJobのキューの実行がSidekiq::Webのセッションに依存してるのか??? しかも別のプロジェクトではSidekiq::Webを見ても普通に見れましたし、その際にセッションを無効にする必要などありませんでした。

activerecord-session_storeのPRを発見

セッションを保存している場所に問題があるかと思い、検索キーワードを変更してみたところ、こういうPull Requestを発見。

github.com

まだOpenのままで取り込まれていない模様…。issueを見るとactiverecord-session_storeとrackのバージョンのものがチラホラと…。あまり開発が進んでいないのでしょうか?

対応

とりあえず、Sidekiq::Webさえ使わなければActiveJob自体はちゃんと動いたので、Web UIをひとまず諦めることとしました。

sidekiqでDBへのコネクションプールを使い切らないようにする

ActiveJobに大量に仕事を依頼するようにしたら、以下のようなログが出るようになりました。

could not obtain a database connection within 5.000 seconds

早速調査。

ActiveJobでsidekiqを使う場合、connection_poolの値はconcurrency + 1以上にしよう - repl.info

つまりは、ActiveJobがDBへのコネクションプールの数以上に起動して、コネクションプールが空くのを待っている間にタイムアウトしてしまう、というやつです。 そして、sidekiq起動時にDBに接続を試みるので、コネクションプールは並列数+1にしましょうということでした。

ActiveJobは、アダプタにsidekiqを使っています。

修正前

特にパラメータの指定もなく、以下のように起動。

bundle exec sidekiq

問題点

デフォルトだと、sidekiqの並列数(concurrency)は10です。しかし、DBへのコネクションプールの設定はというと…。database.ymlを見てみると…。

default: &default
  adapter: postgresql
  encoding: unicode
  username: <%= ENV.fetch("DOCKER_POSTGRES_USER") { "postgres" } %>
  port: <%= ENV.fetch("DOCKER_POSTGRES_PORT") { 5432 } %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> # <= ここ!
  host: localhost

development:
  <<: *default
  database: example_development

test:
  <<: *default
  database: example_test

production:
  <<: *default
  database: example_production

特に指定がなければ5になっています。並列数10に対してコネクションプールが5なのでそりゃ落ちますわ…。

修正していく

ということで、先の参照先のリンクの情報を元に、並列数が10の場合はコネクションプールを11にすればOK。

env RAILS_MAX_THREADS=11 bundle exec sidekiq -c 10

これで大量にActiveJobでキューイングしてみたところ、問題なく全ての処理が終了することを確認。

Herokuに対応する

本番環境はHerokuなので、herokuの記事も参考にしました。

devcenter.heroku.com

環境変数 SIDEKIQ_RAILS_MAX_THREADSSIDEKIQ_CONCURRENCY を定義して、それで調整します。

接続数の上限を意識する

heroku-postgresのstandard-0を使っている場合は、最大接続数は100なので、結構大きなアプリケーションでなければ使い切ることはありません。しかし、hobby-devやhobby-basicの場合の最大接続数は20です。

今のdatabase.ymlのままだと、WebDynoはpumaのworker毎に5の接続数を使うので、worker数を2と想定した場合、残りの接続数は10。 じゃあWorkerDynoのsidekiqは残りの10を使っていいのか?というと、heroku run rails consoleで接続することもあるし、heroku schedulerを使うこともあり得るので、3〜4接続数くらいは残しておかないと詰みます。

そのため、sidekiqの並列数は5として、コネクションプールは6とします。そうすると、接続数が4つ残る計算に。

worker: RAILS_MAX_THREADS=${SIDEKIQ_RAILS_MAX_THREADS:-6} bundle exec sidekiq -c ${SIDEKIQ_CONCURRENCY:-5}

これで、herokuに作ったステージング環境で動作検証したところ、並列数を5に制御しているため、エラー起きずにちゃんと処理が終わりました。 接続数とコネクションプールの関係を考えると、デフォルトにしておくのは危険なので設定しておきましょう!