flashメッセージが時々表示されないflakyテストを改善した話

Railsで

  • cookie sessionを使っている
  • 非同期でAPIをよく叩いている

という条件下で、例えば日報を投稿したあとに"投稿しました!"というflashメッセージを表示しているはずなのになぜか"投稿しました!"が表示されないという現象が時々起こっていました。

これは次のようなことが原因だと推測しています。

  • 非同期API(例: 日報のプレビューを表示する)が実行される
  • 日報投稿ボタンを押す
  • 投稿が成功して日報詳細ページへのリダイレクト用のレスポンスが返される
    • SetCookiesでflashメッセージを含んだcookie sessionが返される
  • 非同期APIのレスポンスが返る
    • SetCookiesでflashメッセージを含まないcookie sessionが返される
  • 日報詳細ページへのリクエストが実行される
    • このとき送信するCookieにはflashメッセージが含まれていないので"投稿しました!"は表示されない

長らくこの問題に悩まされていたのですが、API実行時は次のようにしてSetCookiesヘッダを返さないようにするという方法を思いついたので試してみました。おそらくこれで解決するはず。

class Api::BaseController < ApplicationController
  class NullCookieJar < ActionDispatch::Cookies::CookieJar
    def write(*)
      # nothing
    end
  end

  before_action :null_cookies

  def null_cookies
    request.cookie_jar = NullCookieJar.build(request, {})
  end
end

Rails8.0.0マイルストーンの現状

これはなに

  • 8.0.0 Milestoneを見て気になったものをまとめています
  • マイルストーンは先週くらいにできたのですがもうマージされているやつもたくさんあります
    • DHHが年末年始にめっちゃ働いている

気になったものたち

  • Ruby3.3以上のサポート
    • DHHは最初3.3以上で、という気持ちだったんだけど流石にみんな大変やろ、という意見が出て結局リリース時(2024年の予定)にサポートされているRubyのバージョン、つまり3.1以上に落ち着いた
    • PR: Bump the required Ruby version to 3.1.0 by byroot · Pull Request #50491 · rails/rails
      • ↑のPRでは「メジャーバージョンアップ時にRubyのサポートを落とす」だとRails自体のメンテも大変だしアプリケーション開発者も大変なので、毎回マイナーバージョンアップでもその時サポートしているRubyのバージョンだけサポートするようにしようぜ、という意見が出ている
      • ついでにセキュリティフィックスバージョンも落とそうぜ、という意見もある
      • このへん最終的にどうなるかわからないけど、Railsのバージョンを上げる会社はRubyのバージョンも上げるのでそんなに影響はない気がしますね
  • solid_queue, solid_cache, prop_shaft, kamalがデフォルトに
  • Action CableのアダプタのデフォルトをDBにする
    • solid_queue, solid_cacheと一緒でredisなしをデフォルトにする方針っぽい
    • もともとpostgresqlはアダプタとして使えたけれど、MySQLやsqlite3でも使えるようにしてそれをデフォルトにする、という方針
      • ONCEではすでにそれでやっている模様
  • 古いブラウザでアクセスしたときに古いよ、と出る機能
class ApplicationController < ActionController::Base
  # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting + :has 
  allow_browser versions: :modern
end

class ApplicationController < ActionController::Base
  # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
  allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
end

class MessagesController < ApplicationController
  # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
  allow_browser versions: { opera: 104, chrome: 119 }, only: :show
end
class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end

class SignupsController < ApplicationController
  rate_limit to: 1000, within: 10.seconds,
    by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups!" }, only: :new
end

turboからのリクエストに対してリダイレクトするときに気をつけること

  • turboを利用していて、destroyアクションでリダイレクトを使うときステータスコードを明示的に303(see other)にしないと、リダイレクト先にDELETEメソッドでアクセスしてしまうというハマりポイントが有る
  • Railsの現時点でのデフォルトのリダイレクト時は302なので、redirect_to @project, status: :see_otherのように明示的にステータスコードを指定するとGETでリダイレクトする
  • なぜこのような仕様になっているかというと、fetch APIがそういう仕様だから
  • fetch APIがなぜこのような仕様になっているかというと、302の仕様になるべく沿おうとしているから(要出典)
    • 302 Found - HTTP | MDN
    • 仕様書ではリダイレクトの際にメソッド (と本文) を変更しないよう要求していますが、すべてのユーザーエージェントが準拠している訳ではありません (まだこの種のバグのあるソフトウェアが見つかるでしょう)。従って、 302 コードは GET または HEAD メソッドへのレスポンスのみに使用し、 POST メソッドのままリダイレクトする場合は代わりに 307 Temporary Redirect (こちらでは明確にメソッドの変更が禁止されている) を使用することが推奨されています。

    • chromeなどのブラウザでは、GETとPOST以外のメソッドでアクセスした場合は同じメソッドをリダイレクト先にも適用する、という実装になっている模様
  • なので仕方ないっちゃ仕方ないんだけど、既存のアプリケーションに対してturboを採用しようとした場合には結構な負担になりますね
  • Rails側はこの仕様を緩和するための変更がいくつか入っていたり提案されていたりする

ぼくらは結局どうしたらいいんですかね

  • turbo-rails経由でPATCHやDELETEを発行しているぶんには気にせず302で問題なさそうではあるけど、気づかずにfetch APIを直接実行したときのことを考えるとサーバ側でも対応しておきたい
  • Add redirect_code_for_unsafe_http_methods config by jonathanhefner · Pull Request #45393 · rails/rails 相当のものをモンキーパッチして、redirect_to時のステータスコードをデフォルト303に一律で変えてしまうのが楽そうかな〜と思っています

Beats Studio Proを買ったけど返品した

ここ数年はずっとBoseのquiet comfort35を使っていて、音質などには不満は特にありませんでした。

でも複数pcを切り替える時AirPodsなどと比べると面倒*1。あとは2台同時にペアリングしている時に音を出したいデバイスではない方がアクティブになり、そちらのBluetoothを明示的にオフにしないと狙った方で音が出せなくてイライラする状況があったり。だんだんとこの状況を打破したいな、という気持ちが高まってきました。

AirPods max2がリリースされるのをずっと待っていたのですが、あと1年くらいは発売がなさそうな雰囲気。そこでApple傘下のBeats Studio Proなら発売間もないしいいんじゃないか、と思いポチってみました。

使ってみた感じとしては、Boseに感じていた諸々の不満が解消されていていいじゃん、となったのですが頭への締め付けがきつくて長時間つけていると辛い…。これまでヘッドホンの締め付けを気にしたことがなかったので盲点でした。やむなく返品することに。

Apple storeで購入したので返品処理がスムーズにできたのは不幸中の幸いでした。

もうちょっとBoseを使い続けるか、AirPods max1を買ってしまうかが目下の悩みどころです。

*1:仕事柄pcを複数台使っているので切り替える機会が多く面倒に感じやすい

テストでWebsocketのsubscribeが終わるまで待ちたい

  1. ページを開く
  2. ActionCableのブロードキャストを実行する
  3. ブロードキャストの結果画面が変更されるのを確認する

というテストがあるときに、2がActionCableの該当チャンネルのsubscribeより前に実行されてしまいテストが失敗する、という事象があった。

なのでsubscribeを待つためのヘルパーメソッドを書いてみた。asyncアダプタ想定で、subscriber情報を持っているハッシュの個数が0より大きくなればsubscribe完了しているだろう、と判断した。

  def wait_for_websocket_connection
    Timeout.timeout(Capybara.default_max_wait_time) do
      until ActionCable.server.pubsub.send(:subscriber_map).instance_variable_get(:@subscribers).count > 0
        sleep 0.5
      end
    end
  end

だいぶナイーブな実装なのでうまく動かないケースもありそうだけど、とりあえずこれで。

2024/02/06追記

ActionCableへの購読は、まず内部用のチャンネル(ActionCable::Connection::InternalChannel) を購読してから専用のチャンネルを購読する、という流れになるので、上記のコードだとタイミングによってはまだチャンネルの購読が完了していない事がある。そこを次のようなコードで改善してみた。しばらくこれで様子見

  # `stream_for current_user ` のようにして購読している前提

  def wait_for_websocket_connection(user)
    channel = NotificationChannel.broadcasting_for(user)
    block = lambda do
      s = ActionCable.server.pubsub.send(:subscriber_map).instance_variable_get(:@subscribers)
      s.key?(channel) && s[channel].count > 0
    end

    Timeout.timeout(Capybara.default_max_wait_time) do
      sleep 0.5 until block.call
    end
  end

bullet的なN+1自動検知gemのprosopiteが気になる

user = User.first
token1 = user.user_token
token2 = UserToken.find_by(user: user)
  • たぶんprosopiteはN+1と判定する
    • のでzero false positives / false negatives
    • 本当に?
    • 手元でRailsアプリケーション作って試してみよう、と思ったのだけどsqlite3は対応してなさそうなのでまた今度試してみます…

CLionでCRubyのコードを読む

  • CLionでCRubyのコードをただ開いただけだとうまくコードジャンプが効かない
  • どうやったらええんや…となっていた
  • Makefileを開くと↓のように、「Makefileプロジェクトのロード」が表示されるのでロードするとそれっぽいジャンプができるようになった
  • これでCRubyのコード読みやすくなりそう