patorashのブログ

方向性はまだない

開発環境で複数の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に制御しているため、エラー起きずにちゃんと処理が終わりました。 接続数とコネクションプールの関係を考えると、デフォルトにしておくのは危険なので設定しておきましょう!

ActiveJobのコールバックは2系統に分かれている

ActiveJobの処理が時々コケることがあったので調査した。

Active Job の基礎 - Railsガイド を参照したところ、ActiveJobのコールバックは以下のようになる。

  • before_enqueue
  • around_enqueue
  • after_enqueue
  • before_perform
  • around_perform
  • after_perform

問題のあったコード

そして、元々のコードはこういう感じのことをやっていた。TaskLogモデルがあったとする。

  1. キューに入った時点でTaskLogモデルを作成する
  2. TaskJob実行直前に、ジョブIDからタスクログを取得してステータスを処理中に変更
  3. TaskJobが終了したら、ジョブIDからタスクログを取得してステータスを終了に変更
class TaskJob < ApplicationJob
  
  after_enqueue do |job|
    TaskLog.create!(job_id: job.id, status: :queued)
  end

  before_perform do |job|
    task = TaskLog.find_by! job_id: job.id
    task.status_processing!
  end

  after_perform do |job|
    task = TaskLog.find_by! job_id: job.id
    task.status_finished!
  end

  def perform
    # do something...
  end

end

しかし、いざ実行すると、たまに失敗するときがあった。before_performのTaskLog.find_by!で落ちていた。ここで落ちるということは、TaskLogが作られていないということだ。

実験

after_enqueueが終わる前にbefore_performが実行されることがある、ということだと判断したので、実験用のコードを書いた。

class TestJob < ApplicationJob

  after_enqueue do |job|
    puts "enqueued TestJob. sleep 10..."
    sleep 10
    puts "enqueued TestJob. end"
  end

  before_perform do |job|
    puts "before_perform TestJob. sleep 2"
    sleep 2
    puts "before_perform TestJob. end"
  end

  after_perform do |job|
    puts "after_perform TestJob."
  end

  def perform
    puts "perform!!!"
  end
end

これをrails cで実行した。sleepしているので10秒後にenquequd TestJob. endと出ます。

pry(main) > TestJob.perform_later
enqueued TestJob. sleep 10...
enqueued TestJob. end
Enqueued TestJob (Job ID: 0234b71e-aa4a-4644-b86e-dcf67b40d550) to Sidekiq(default)
=> #<TestJob:0x00007ff599493630
 @arguments=[],
 @executions=0,
 @job_id="0234b71e-aa4a-4644-b86e-dcf67b40d550",
 @priority=nil,
 @provider_job_id="3f7bda026dc435a015d6f81c",
 @queue_name="default">

その頃、sidekiqを実行中のターミナルでは…

2020-10-21T01:36:32.006Z 51089 TID-our2bxzcp TestJob JID-f427b7a69e619b9e45c68371 INFO: start
before_perform TestJob. sleep 2
before_perform TestJob. end
perform!!!
after_perform TestJob.
2020-10-21T01:36:34.027Z 51089 TID-our2bxzcp TestJob JID-f427b7a69e619b9e45c68371 INFO: done: 2.021 sec

早々にbefore_performが実行され、performも呼ばれ、after_enqueueが終わる前に処理が終わった。

ということは、ActiveJobのコールバックのenqueue系とperform系は完全に別々の処理と考える必要がある。

直したコード

before_enqueueでTaskLogモデルを作るのをやめればいい。

class TaskJob < ApplicationJob
  
  before_perform do |job|
    TaskLog.create!(job_id: job.id, status: :processing)
  end

  after_perform do |job|
    task = TaskLog.find_by! job_id: job.id
    task.status_finished!
  end

  def perform
    # do something...
  end

end

まとめ

enqueue系のコールバックは、使うにしても「予約しました」とか、そういう通知系をやるくらいに留めておく。perform系の処理で参照したいような前処理をenqueue系でやるべきではない!

Elasticsearchを5.6から7.9にバージョンアップ

過去にElasticsearchのバージョンアップをしたのだけれど、そのバージョンも既にEOLとなり、早く最新のバージョンにしたいと思いつつも、なかなかできずにいた。 ようやくできるタイミングがきたので、取り組んでみた。まだステージング環境では検証が終わっていないけれど、CIが通ったので修正点を一旦まとめる。

やったこと

Elasticsearch 7.9.1のDockerイメージを作成

kuromojiプラグインを入れたDockerイメージをDockerHubに作った。

https://hub.docker.com/repository/docker/patorash/elasticsearch-kuromoji

docker-compose.ymlを修正

Elasticsearchの公式のページにあるdocker-composeの設定を参考にした。

www.elastic.co

しかし、CircleCIでテストをする際にdocker-composeで行っているのだが、その際にElasticsearchのコンテナが起動しなかった。 起動しなかったときのエラーはこれ。

elasticsearch_1 | ERROR: [1] bootstrap checks failed elasticsearch_1 | [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

max_map_countの値が低すぎるから増やせという話だけれど、CircleCIのVMカーネルの値は変更できないということだった。

Running Elasticsearch 5 - Build Environment - CircleCI Discuss

このフォーラムで紹介されていた設定を追加した。environmentのところの"transport.host=localhost""bootstrap.system_call_filter=false"だ。 これで、CircleCIのdocker-composeでElasticsearchが起動するようになった。

version: '3.3'

services:
  elasticsearch:
    image: patorash/elasticsearch-kuromoji:7.9.1
    expose:
      - "9200"
      - "9300"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - cluster.initial_master_nodes=es01
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "TZ=/usr/share/zoneinfo/Asia/Tokyo"
      - "transport.host=localhost"
      - "bootstrap.system_call_filter=false"
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    healthcheck:
      test: ["CMD", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
      interval: 60s
      timeout: 30s
      retries: 3

関連gemの更新

  • elasticsearch-rails
  • elasitcsearch-model

この2つを更新した。

コードの修正

クラウドワークスさんのところの記事を参考にした。

engineer.crowdworks.jp

ここに書かれていた通り、_allが使えなくなったので削除したのと、copy_toを使うようにした。copy_toは6系で追加されたらしい。1つのキーワード検索用のフィールドを作り、そこに対して各フィールドをcopy_toで集める。今までは検索対象のフィールドをいくつも指定していたため、効率が悪かったけれど、これを使うことで1つのフィールドだけを検索対象とできるので効率がよくなるそう。

あとは、ngramの設定を変更した。以前はmin_gramを1に、max_gramを20に指定していたのだが、起動時にエラーとなった。max_ngram_diffを19に設定する必要があった(デフォルトで1のため)。

www.elastic.co

これで大体のテストは通ったのだが、ソートのテストがうまくいかなかった。

ソート順がElasticsearchへの指定順にならない

なぜかソート順がおかしいのでgemのコードを調べたところ、Elasticsearchから取得したデータをActiveRecordにするところでorderが定義されているとElasticsearchのソート条件よりもActiveRecordのソート条件のほうが優先されるように変更されていた…。

github.com

default_scopeでorderを定義していたため、elasticsearch-railsrecordsメソッドを呼んだ時点でもうダメだった。default_scopeでのorderを止めるのは変更点が多すぎて厳しいので、elasticsearch-railsから呼ばれた場合のみ、default_scopeを無効にしたいと考え、caller_locationsを使って対処した。

caller_locationsは処理が呼び出された場所を取得するので、そのパス情報の中にelasticsearch/model/adapters/active_record.rbが含まれていたらorderを無視するようにした。ただ毎回この処理が実行されるわけなので、オーバーヘッドがどれくらいになるのかが気になる…が、今はこれで。これで、Elasticsearchのソート順でデータが取れるようになった。

default_scope do
  paths = caller_locations.map(&:path)
  order(name: :asc) unless paths.any? { |path| path.end_with?('elasticsearch/model/adapters/active_record.rb') }
end

とりあえず、これでCIのテストが通るようになった👍

5にバージョンアップしたときと比べると比較的楽にバージョンアップできそう。

タブレット型Chromebookは実質Androidタブレットだった

先日、ふとChromebookが気になり始めてググっていたら、LenovoChromebookSurfaceみたいにタブレットにもなるタイプのやつを売っているのを見つけました。タブレットにもなるし、普通にキーボードも付いてくるのに4万円ちょいだったので、ボーナスが入ってから特に何も買ってなかったので思い切って買ってみました!(ここまで書いて、ボーナスが入ったから在宅勤務多いのでエアコンを買ったのを思い出した…)

買った機種は、Lenovo IdeaPad Duet Chromebook です。

www.lenovo.com

注文して3日程度で届きました。やはり新しいPCが来るとワクワクします!😁

開封の儀

f:id:patorash:20200926131139j:plain

chromebookの文字があります。

f:id:patorash:20200926131330j:plain

開けたところ。使い方のパターンが印刷されていました。

f:id:patorash:20200926131415j:plain

色はアイスブルー+アイアングレー。でも普段は立てるためにカバーつけることになるので色はあまり意識しなさそう。

f:id:patorash:20200926131628j:plain

立たせたところです。こんな感じか。カッコいい。

f:id:patorash:20200926131817j:plain

付属品は、充電用のUSB Type Cと、USBをイヤフォンジャックに変換するケーブル。インターフェースがUSB Type Cの1つのみ。 これは外部ディスプレイとか繋げないのかな?と思ってググってみたら、繋げられるらしい!アダプター買おうかな!

blog.itokoichi.com

f:id:patorash:20200926131945j:plain

そして、いよいよ起動…。うーん、速い!さすがChromebook

設定していく

ログインはPIN CODEでできるように

ログインには、当たり前だけれどGoogleアカウントが必要なので、長いパスワードとか設定していると入力が大変…。しかし、1度やってしまえば、あとはPIN CODEの設定ができるので、ログインのために長いパスワードを入力しなくてよくなります。昔にChromebook使おうとしたときはこれが辛かったので進歩してる!

Androidアプリが使える

正直、これが購入する判断に至ったものなのですが、めっちゃ便利。タブレットKindle Fire HD 8を使っていたのですが、Kindle以外にも他の会社の電子書籍サービスを使っているので、それが全部1つの端末で読めるようになりました。Kindleebook japanもGoogle Booksも読めるぞー!

また、AuthyやLastPassも入れられるので、いろんなサービスへのログインも簡単。設定もサクサクと終わらせられました。

ディスプレイの解像度を変更

初期表示だと文字がかなり大きいので、解像度を1440x900にしました。それ以上にすると、ちょっと文字が小さすぎて読みづらい…。

レビュー

半日くらいしか使っていませんが、とりあえずレビューします。

軽い

キーボードとかスタンドがあるとまぁまぁな重量がするけれど(それでも1kgは切る)、本体だけにすると450gらしいのでかなり軽いです。読書するには軽いは正義!

動画が綺麗

Chromebookなので、YouTubeは当然として、AndroidアプリでHuluやNetflixも入れられます。私はHuluを契約しているので、Google Play StoreからHuluをインストール。 10インチながら、1920x1200のWUXGAなので、フルハイビジョンで見ることができます。

縦持ちできるのがGood

キーボードを切り離したら縦持ちできるので、大きな本を読むのに便利です。また、横持ちならば漫画を見開きで見ても見やすいサイズなのでいいですね。

動作はキビキビ

CPUは8コアあるからか、まったくモッサリという印象はないです。また、ファンレスなので静か。

キーボードがやや打ちづらい

10インチに合わせるために、ホームポジション付近のよく使うキーに関してはまぁまぁいいサイズになっているのですが、記号系のキーとエンターキーが小さいです。特にエンターキーが細長くて見た目は微妙…。でも押しにくいというほどでもない…。あと、Ctrlキーがあってほしいところに検索キーがあります。WindowsでいうCapslockキーの位置です。ですが、設定からキーバインドは柔軟に変えられるので、検索キーとCtrlキーを入れ替えておきました。

Linux(Beta)が付いてた

WSLみたいな感じなのか、Linuxをインストールすることができます。Androidアプリの開発とかで使えるらしい…。ただ、コピーペーストがうまくいかないみたいで今のところの印象だと使いづらい。調べてきたコマンドがコピペできんの辛い。

メモリが少ない(4GB)ので、開発のメインマシンにはならないけれど、VSCode Onlineとかが出てきているので、今後に期待できるかも。

総評

カスタマイズで遊ぶというよりは、エンタメ系とか、執筆とかに気軽に使える端末としてすごく優秀だと思う。いわゆるノートPC型のChromebookと違ってタブレットとして使えるのがいい!なんかよく巷で「最近Androidタブレットが出ない〜。iPadは出てるのに〜!」というのをtwitterとかで見かけるが、もしかしたら、Androidタブレットは諦めてChromebookに統合していくつもりではなかろうか?Androidタブレットを探している人は、IdeaPad Duetを候補にするのがいいと思う。

なお、私はLenovo Storeで購入したが、AmazonでもAmazon限定モデルで販売していた(後で気づいた…)。違いは、Lenovo Storeだとディスク容量が128GBで、Amazonのモデルだと64GBというところ。ただし、容量が少ない分、定価はAmazonのほうが4千円くらい安い。

ストレージにGoogle Driveを使うのであれば、本体の容量は64GBでも十分かなぁと思えるので、Amazonモデルでもいいと思う。安いし。そして今さらに気づいたのだが、Amazonのモデル、9/30までの期間限定で10%オフクーポン付いてた。4万円くらいなので、3万6千円で買えるらしい!グヌヌ…!この情報を最初に知っていれば!!