patorashのブログ

方向性はまだない

ActiveJobのテスト周りの見直しで学んだことのメモ

最近テストが不安定になって、その修正をしていっていたらActiveJobが絡むあたりがまた不安定になったので原因を探ろうとしたのだけれど、ActiveJobのテストの設定周りなんて久々すぎて忘れまくっていたのでここで一旦整理しておく。

テストでActiveJobを同期的に扱う

ActiveJobのqueue_adapterの設定は、environmentsの各ファイルで行うべき、だそう。コメントでそう書いてあった。

# Use a real queuing backend for Active Job (and separate queues per environment)

production.rbなど

# Use a real queuing backend for Active Job (and separate queues per environment)
config.active_job.queue_adapter = :resque
config.active_job.queue_name_prefix = "project_#{Rails.env}"

test.rb

テストでは、queue_adapterを:asyncに設定する。以前は:inlineだったらしい。

blog.bigbinary.com

# Use a real queuing backend for Active Job (and separate queues per environment)
config.active_job.queue_adapter = :async
config.active_job.queue_name_prefix = "project_#{Rails.env}"

こうするとキューに入ったと同時に実行されるっぽい。

テストでもResqueを使いたい!

あんまりこだわることはないのかもしれないが、テストでもqueue_adapterに:resqueを使いたい場合は、:resqueを指定した上で、Resqueの設定をinlineにすればよかった。 Resqueの設定は、config/initializers/resque.rbで行なっていたのでそこに追記した。

Resque.redis = Redis.new(host: "localhost", port: ENV.fetch("REDIS_PORT", 6379)
Resque.inline = true if Rails.env.test?

こうすると、Resqueもキューに入ったと同時にジョブを実行してくれる。

ActiveJob::TestHelperを使う

もう一つ方法がある。それは、ActiveJob::TestHelperを使う方法だ。spec/rails_helper.rbなどで読み込む。

config.include ActiveJob::TestHelper

実はすでにこちらを使っていた。しかし、なぜかジョブが実行されなかったようだったので、上記の設定を色々といじったりしていたのだが、何も変わらなかった。 しかしそれは当然で、ActiveJob::TestHelperを読み込むと、queue_adapterがconfig/environments.test.rbに設定したものではなく、TestAdapterになってしまうのだ。

以下はpryの結果。

pry > ActiveJob::Base.queue_adapter => #<ActiveJob::QueueAdapters::TestAdapter:0x00007fa7646755d0 @enqueued_jobs=, @filter=nil, @perform_enqueued_at_jobs=true, @perform_enqueued_jobs=true, @performed_jobs=, @reject=nil>

どれだけconfig/environments/test.rbのほうでqueue_adapterの設定をいじり回そうと、意味がなかったのだが、これにハマってしまっていた…。

とりあえず、コードの修正によってActiveJobが突如動かなくなった理由はまだわかっていないのだが、ActiveJobをテストで動かすときの設定については理解できたのでよかった。

まとめ

  1. キューイングのテストが不要でActiveJobの結果をすぐに反映してテストをしたい場合は、ActiveJob::TestHelperを読み込まずに、config/environments/test.rbでconfig.active_job.queue_adapter = :asyncと読み込めばよい。
  2. ActiveJobのキューイングや実行タイミングを自分で制御したい場合はActiveJob::TestHelperを読み込む。その場合は、config/environments/test.rbでconfig.active_job.queue_adapterを設定しても意味はない。

あとは昔に自分がqiitaに書いていたやつの通りにやれば問題なかった。

qiita.com

docker-composeで起動したredisに繋がらなかった。

docker-composeで起動したredisに接続できないため、railsのresqueが動かなくなった。

つい最近まで、redisのバージョン指定をlatestにしていたのだけれど、heroku-redisに合わせようと思って3.2.12に変更していた。

version: "3"
services:
  redis:
    image: redis:3.2.12-alpine # 以前はlatestだった
    ports:
      - "6379:6379"
    volumes:
      - .docker/redis/data:/data
    restart: unless-stopped

普段はローカル環境でresqueを動かすときは個別にresqueを動かすようにしているので、resqueを使おうと思ったタイミングでわかった。

rails aborted! Redis::CannotConnectError: Error connecting to Redis on 127.0.0.1:6379 (Errno::ECONNREFUSED) Original Exception (Redis::CannotConnectError): Error connecting to Redis on 127.0.0.1:6379 (Errno::ECONNREFUSED)

調査

redisのバージョンをlatestに戻すと繋がり、3.2.12にすると繋がらなくなるので、gemのredisに問題があるのか?と疑ってググっていたのだけれど、何も見つからず。

docker-composeではなく、dockerコマンドで起動した後、redis-cliで繋いでみたところ、接続できた。

$ docker run -d -p 6379:6379 redis:3.2.12-alpine
21795a03ab97b7021c5d1de567ed4a16c3c0c98dbf948e1b4110bbaaedd406ca
$ docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                    NAMES
21795a03ab97        redis:3.2.12-alpine   "docker-entrypoint.s…"   8 seconds ago       Up 7 seconds        0.0.0.0:6379->6379/tcp   unruffled_gauss
$ redis-cli -h localhost -p 6379
localhost:6379> exit 
$ docker stop 21795a03ab97

接続できたのでとりあえず停止。

次はdocker-composeで起動してみる。

$ docker-compose up -d
Creating network "foo_default" with the default driver
Creating foo_redis_1         ... done
$ redis-cli -h localhost -p 6379
Could not connect to Redis at localhost:6379: Connection refused
not connected> exit

何故なのか?

原因判明

原因は気付いたらそりゃそうかって感じだけれど、volumesでマウントしていたredisのファイル(dump.rdb)にあった。latestと3.2.12ではredisのバージョンによってファイルのフォーマットに互換性がないため、latestのときに作られたファイルを3.2.12で読もうとしてエラーになって起動できないでいたのだ。

dump.rdbを削除してdocker-composeで起動したところ、redisに接続できた。

教訓

dockerのミドルウェアのメジャーバージョンを変更したらファイルを消すこと!(消しても問題なければ)

CircleCI 2.1を使うようにして設定ファイルをリファクタリングした

CircleCIはこの時に書いた設定のまま使ってました。 patorash.hatenablog.com

2018年の後半にバージョン2.1が出たという話は聞いていたものの、なかなか試せていませんでした。隙間時間ができたので、この課題にケリをつける!

CircleCI 2.1の変更点

大きな変更点は、Executors, Commands, Orbだと思います。また、それぞれで引数を設定できるようになりました。

Executors

実行環境の定義を再利用するための仕組みです。名前がつけられるのがありがたい。 基本的には、dockerのイメージを配列で指定していきます。 以下のような感じ。

version: 2.1

executors:
  default:
    working_directory: ~/project
    docker:
      - image: circleci/ruby:2.5.3-node-browsers
        environment:
          RAILS_ENV: test
          RACK_ENV: test
          TZ: "/usr/share/zoneinfo/Asia/Tokyo"
      - image: circleci/postgres:9.5-alpine-postgis-ram
        environment:
          TZ: "/usr/share/zoneinfo/Asia/Tokyo"

Commands

stepsで指定する処理に名前をつけて、まとめることができるようになりました。関数ですね。

version: 2.1

commands:
  save_node_module_cache:
    steps:
      - save_cache:
          key: v1-yarn-{{ checksum "yarn.lock" }}
          paths:
            - ./node_modules
  restore_node_module_cache:
    steps:
      - restore_cache:
          keys:
            - v1-yarn-{{  checksum "yarn.lock" }}
            - v1-yarn-

jobs:
  test:
    executor: default
    steps:
      - checkout
      - restore_node_module_cache
      - run: yarn
      - save_node_module_cache
      # 以降の処理は省略

Orb

Orbはサードパーティが公開したstepsを利用する仕組みです。ansible galaxyみたいに、よく使う設定を再利用できるので助かります!

Orbを使うには、設定のOrganizationのSecurityから、Orbを利用できるようにする必要があります。

f:id:patorash:20190125150835p:plain
CircleCIでOrbを使えるように設定する

こんな感じ。

version: 2.1

orbs:
  ruby-orbs: sue445/ruby-orbs@1.4.1

jobs:
  test:
    executor: default
    steps:
      - checkout
      - ruby-orbs/bundle-install
      # 以降の処理は省略

2.0の頃との違い

以前は設定の再利用をするために、yamlの記法でやっていました。

version: 2

references:
  restore_node_module_cache: &restore_node_module_cache
    restore_cache:
      keys:
        - v1-yarn-{{ checksum "yarn.lock" }}
        - v1-yarn

jobs:
  steps:
    - checkout
    - *restore_node_module_cache

これでも結構短くはなっていましたが、やはり見た目が煩雑…。

2.1からは普通に定義できるようになったのが嬉しいところです。

version: 2.1

commands:
  restore_node_module_cache:
    steps:
      - restore_cache:
          keys:
            - v1-yarn-{{ checksum "yarn.lock" }}
            - v1-yarn-

jobs:
  steps:
    - checkout
    - restore_node_module_cache           

前のやつをリファクタリングしてみる

ということで、yamlのマージを使っていたのをやめて、Executors, Commands, Orbを使うようにしてみました。

OrbはTLで以前に見かけたbundle updateのPRの自動化とbundle installのキャッシュ、リストアなどやってくれるやつを覚えていたので、練習も兼ねてそれを使わせてもらいました。

sue445.hatenablog.com

で、完成版がこちら。ライブラリのキャッシュを一気に行いたくなったので、準備を並列化していたのをやめました。

version: 2.1

orbs:
  ruby-orbs: sue445/ruby-orbs@1.4.1

refefences:
  default_settings: &default_settings
    working_directory: ~/project

  ruby_docker_image: &ruby_docker_image
    image: patorash/circle_ci_ruby:2.5.3-node-browsers
    environment:
      RAILS_ENV: test
      RACK_ENV: test
      TZ: "/usr/share/zoneinfo/Asia/Tokyo"

executors:
  default:
    <<: *default_settings
    docker:
      - *ruby_docker_image

  rspec:
    <<: *default_settings
    docker:
      - *ruby_docker_image
      - image: circleci/postgres:9.6-alpine-postgis
        environment:
          TZ: "/usr/share/zoneinfo/Asia/Tokyo"

commands:
  save_code_cache:
    steps:
      - save_cache:
          key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
          paths:
            - ~/project

  restore_code_cache:
    steps:
      - restore_cache:
          keys:
            - v1-repo-{{ .Environment.CIRCLE_SHA1 }}

  save_node_module_cache:
    steps:
      - save_cache:
          key: v1-yarn-{{ checksum "yarn.lock" }}
          paths:
            - ./node_modules

  restore_node_module_cache:
    steps:
      - restore_cache:
          keys:
            - v1-yarn-{{ checksum "yarn.lock" }}
            - v1-yarn

  setup_database:
    steps:
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

jobs:
  prepare_test:
    executor: default
    steps:
      - checkout
      - restore_node_module_cache
      - ruby-orbs/bundle-install
      - run: yarn
      - save_node_module_cache
      - save_code_cache

  test:
    executor: rspec
    parallelism: 4
    steps:
      - restore_code_cache
      - run: bundle --path vendor/bundle
      - setup_database

      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)"

            bundle exec rspec --format progress \
                            --format RspecJunitFormatter \
                            --out /tmp/test-results/rspec.xml \
                            --format progress \
                            $TEST_FILES

      # Save artifacts
      - store_test_results:
          path: /tmp/test-results

      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

workflows:
  build:
    jobs:
      - prepare_test
      - test:
          requires:
            - prepare_test

Commandsのおかげで何をしている処理なのかが明確になって読みやすくなりました。あとはOrbが便利ですね。gemのキャッシュとリストアの設定を書かなくて済むようになったし、別のプロジェクトでも使いまわせるのがありがたいです。

rake taskで実行確認をとるためにhighlineを使う

highlineという対話式のCLIを提供するgemがあります。

github.com

rake taskで検証用データの自動生成を行いたかったのだけれど、検証用なので本番環境で実行されたら困ります。 そこで、本当に実行するか確認を取るようにしたかったので、今回はこれを使いました。

対話式のCLIを提供してくれるので、今回使いたい確認だけでなく、入力の受付や選択肢から選んだり、単にメッセージを表示するだけなどもできます。 その際に、注意を促すためにメッセージの色を変えたり、太字にしたりなどもできます。めっちゃ便利ですね。

highlineで確認させる。

例えば、bin/rake foo:barを実行すると、確認が出て、yesと入力したら実行されるようにするには、以下のようにagreeメソッドを使います。

require 'highline/import'

namespace :foo do
  desc 'アグリーできる?'
  task bar: :environment do |x, args|
    say("<%= color('!!! 注意 !!! 本番環境では絶対に実行しないでください!', :red, BOLD) %>")
    if agree("<%= color('本当に実行しますか?', :red, BOLD) %> (y)es or (n)o")
      puts "アグリーです"
    else
      puts "アグリーできかねます"
    end
  end
end

まとめ

対話式のCLI作るのはhighlineを使うと簡単ですね〜。

WSLでpipでインストールしたモジュールが見つからなくてハマった

年末からいまだにPCのセットアップが終わらず…。 Windows Subsystem for Linuxにて、Ansibleにて環境構築手順をまとめていってるのだけれど、Windows UpdateでPCを再起動した後にWSL on Ubuntuで再びansible-playbookを実行しようとしたら、command not foundと言われた。

いやいや、動いてたじゃない…。どーしたのよ…。と思ってpipでansibleを再インストールしても状況変わらす。which ansibleしても何も表示されず。必死に「pip ansible not found」とかでググるも、イマイチわからず。

ホームディレクトリでls -laして、何気に.profileを見つけたので中身を見てみたら、pipのモジュールに対してパスを通してそうだったので、source .profileを実行したら、キタコレ!which ansibleしたらパスが表示された!

原因

原因は、.bash_profileがあったから。anyenvをインストールしたタイミングで、anyenvの初期化処理を.bash_profileに追記していた。Macのときにそうしていたから特に何も違和感を感じていなかったのだけれど、これが大問題だった。

設定ファイルの読み込み順について

参考URLに書いてありますが、bashはログインしたタイミングで設定ファイルを読み込みますが、その順番があります。

qiita.com

  1. まず、/etc/profile を読み込む
  2. その後、以下の順番で見つかったファイルのみを読み込む
    1. ~/.bash_profile
    2. ~/.bash_login
    3. ~/.profile
  3. .bashrcは読み込まないが、.profileで.bashrcがあれば読み込む設定が書いてあった。

自分はファイルの読み込み順はあるけれど全部読み込むと思い込んでいた。そのため、.bash_profileを作って満足してしまっていた。原因はpipでもなんでもない。bashの設定ファイルの読み込み設定だったのだ…。pipを疑って悪かった。ごめんなさい。

解決法

.bash_profileに追加していたのはたったの2行だったので、それを.profileに追記し、.bash_profileを削除した。

cat .bash_profile >> .profile
rm -f .bash_profile

そしてログインしなおしてみたら、which ansbleしても、ちゃんと動いた。 こういう初歩的なところもよく見落としてしまうので、ほんま勉強しなおさんとなぁ…。

Rails 5.2でcacheのkeyにTimeWithZoneを渡すと落ちる件

表題の通りなのですが、テストも通ってよっしゃー!stagingにデプロイじゃー!と意気揚々としたものの、キャッシュを使っている箇所の表示がされない不具合が…。

とはいっても一部だけで全部のキャッシュが動かなくなったわけではなさそう。調べてみた。

ローカルでキャッシュを有効にする

Railsのアップデートを真面目にやっていれば、これで開発環境でキャッシュが有効になります。

bin/rails dev:cache

もう一度実行すると、無効になります。

因みにこれが何をやっているのかというと、tmp/caching-dev.txtが作られます。config/environments/development.rbで、このファイルがある場合はキャッシュを有効にするという記述があるはずです。

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
  config.action_controller.perform_caching = true
  config.action_controller.enable_fragment_cache_logging = true
  config.cache_store = :dalli_store
  config.public_file_server.headers = {
    'Cache-Control': "public, max-age=#{2.days.to_i}"
  }
else
  config.action_controller.perform_caching = false
  config.cache_store = :null_store
end

frozen errorで落ちる

フラグメントキャッシュの存在確認で、fragment_exist?([@foo, Time.zone.now.beginning_of_month])みたいなことをしていました。今月分はずっとキャッシュさせる的なやつです。ここで落ちていました。

RubyMineのデバッガを使って調べたところ、instrument_fragment_cacheメソッド内のinstrument_payloadメソッドにTimeWithZone型のデータが渡されると落ちるようです。それでは、instrument_payloadは何をやっているのでしょうか?

rails/caching.rb at master · rails/rails · GitHub

リンクを貼っておきますが、Hashを返しているだけ…。

def instrument_payload(key)
  {
    controller: controller_name,
    action: action_name,
    key: key
  }
end

これでなぜ落ちるのが全く理解できないので、とりあえずこのままにしておきます…。できればissueを書きたいところです。

to_sすれば問題ない

fragment_exist?([@foo, Time.zone.now.beginning_of_month.to_s])すれば問題ないことは確認したので、これで乗り切ることに。Date型でもいけるようだったので、to_dateでもいいでしょう。

余談

TimeWithZone型の値をcacheのkeyに設定すると、/だらけの長いものになりました。別に害はないのですが、RailsガイドのCacheのところを読むと、keyにするには、cache_keyメソッドかto_paramメソッドが定義されている必要がある、とあります。

Rails のキャッシュ: 概要 | Rails ガイド

TimeWithZoneにはto_paramメソッドが定義されています。

Time.zone.now.to_param #=> "2019-01-17 11:19:10 +0900"

しかし、これが使われていません。掘り下げてみたところ、to_paramが呼ばれる前に、to_aメソッドがある場合はto_aメソッドを呼んだ後、再帰的に処理をしていました。

rails/cache.rb at master · rails/rails · GitHub

そして、TimeWithZoneにはto_aメソッドが定義されており、配列に変換されて処理された結果、長い文字列になっていました。

Time.zone.now.to_a #=> [22, 26, 11, 17, 1, 2019, 4, 17, false, "JST"]

うーん、to_paramで処理してほしいような…。

Rails 5.2からReferrer-Policyの値が設定されているので気をつけよう

Rails 5.1.6.1から5.2.2にアップグレードして検証していたところ、何もしていないのにJavaScriptの処理が壊れた。

その処理はCORSで有料のライブラリをダウンロードしてから実行しており、特にJS側も変更していなかったのだけれど、急にダウンロードできなくなった。 DevToolのコンソールからエラーメッセージを見たら、そのライブラリからは「リファラが間違っている」と怒られていた。

その有料のライブラリは使用するURL単位でリファラを提出する必要があったので、そのようにしてあったのだけれど、どうも何らかの理由でリファラが送られていないなと思った。

Rails 5.1.6.1に戻してみたところ、問題なく動いたので、これはバージョンの差異によるものだろうと判断し、それぞれのリクエストのヘッダを見てみた。

Railsのバージョンによる違い

5.1.6.1の場合

Referrer Policyがno-referrer-when-downgradeだった。

f:id:patorash:20190115163112p:plain
Rails 5.1.6.1の場合

5.2.2の場合

Referrer Policyがstrict-origin-when-cross-originだった。

f:id:patorash:20190115163152p:plain
Rails 5.2.2の場合

Rails 5.2.2のリリースノートを見る

Ruby on Rails 5.2 リリースノート | Rails ガイド

こちらによると、Referrer-Policyヘッダーをデフォルトのヘッダーセットに追加。というのがあった。 commitログを確認したところ、strict-origin-when-cross-originがデフォルトでセットされるようになっていた。

しかし、これがどういう設定なのかがイマイチわからない。

Referrer Policyの種類を確認する

Referrer-Policyとkindsで検索したら設定の意味が出てきた。

developer.mozilla.org

詳しくは上記のサイトの説明を読んだらわかるが、strict-origin-when-cross-originだと、要はURLのパス情報は落としたものがリファラとして送られているようだ。つまり、本来のリファラhttps://www.example.com/path/toであるが、https://www.example.com/リファラとして送られるということ。

no-referrer-when-downgradeは、プロトコルのレベルが落ちる場合(HTTPS->HTTP)は、リファラを送らないという意味で、それ以外はリファラを全部送るという設定で、特に指定されていない場合はこれがデフォルトの動作となる。

状況に合った値を設定したかったが、鑑みるにno-referrer-when-downgradeがよさそうだったのでこれを設定することにした。

RailsでReferrer Policyを設定する

設定するには、config.action_dispatch.default_headersを修正する。今回はconfig/application.rbに設定した。

config.action_dispatch.default_headers = {
    'Referrer-Policy' => 'no-referrer-when-downgrade'
}

設定後にRails 5.2.2のアプリを再起動させたところ、処理が正常に行われるようになった。

反省点

Railsのアップグレードなので影響範囲はRuby側だけでJS側には影響ないと思っていたのだけれど、そんなことはなかった。とりあえず一通りは動かしてみるっていうのはやはり大事である。