Limaを使ってDockerのファイル同期を試す
動機
- ファイル数が増えるとMacでのDockerでVolumeの同期が遅い
- DockerDesktopのMutagenに期待していたがDockerDesktopの有料プランに取り込まれてしまった
- 他の手段としてlimaを試したいのでDockerの起動とファイルの同期までをやってみる
Limaの実行
インストール
brew install lima
Limaではなにも指定せずにstartすればDefaultという名前で環境を作ってくれるが、今回Dockerを使いたいので用意されているTemplate元に設定を作成
Docker向けののTemplate
lima/examples/docker.yaml at master · lima-vm/lima · GitHub
Templateの修正
mountType: "virtiofs" # ファイルの共有方法virtiofsに変更 vmType: "vz" # mountTypeに合わせてvmTypeもvsを指定
virtiofsとvzは現時点でexperimental
設定ファイルを指定してLimaインスタンスの作成と起動
limactl create --name=my-docker docker.yaml limactl start my-docker
起動している仮想マシンの一覧。vmTypeがvzであることが確認できる
$ limactl list NAME STATUS SSH VMTYPE ARCH CPUS MEMORY DISK DIR my-docker Running 127.0.0.1:57337 vz aarch64 4 4GiB 100GiB ~/.lima/my-docker
Dockerの動作確認
limaインスタンス起動時のメッセージを参考にDockerのcontextを切り替える
$ docker context create lima-my-docker --docker "host=unix://$HOME/.lima/my-docker/sock/docker.sock" $ docker context use lima-my-docker $ docker context show lima-my-docker
コンテナの起動
docker run --name nginx -p 127.0.0.1:8080:80 nginx:alpine
ブラウザからhttp://localhost:8080/を開くとnginxの起動を確認できる
ファイルの同期
開発環境として実行したいのでファイル同期の確認をしておく
limaのインスタンスのマウントはDockerのTemplateで以下のように指定されているので/tmp/lima/以下にサンプルのhtmlをおく
docker.yaml
mounts: - location: "~" - location: "/tmp/lima" writable: true
/tmp/lima/index.html
<!DOCTYPE html> <html> <body> <h1>Lima test</h1> </body> </html>
ファイルを置いた状態でvolumeをマウントしてdockerを起動
docker run --name nginx -p 127.0.0.1:8080:80 -v /tmp/lima/index.html:/usr/share/nginx/html/index.html nginx:alpine
ブラウザからの表示が変更されているの確認したあと
Lima test
ローカルの/tmp/lima/inde.htmlを修正してからリロードすると
... <h1>changed file</h1> ...
表示が変更される
changed file
Limaのインスタンスから変更しても同期される(ブラウザの反映にはリロード & キャッシュの削除が必要だった)
$ limactl shell my-docker bash $ sed -i 's/changed file/changed file in lima/' /tmp/lima/index.html
感想
Lima側の設定が用意されたTemplateから出来たので試したかったことは簡単に行えた
ローカル、Lima、Dockerと3つの環境の把握が必要になるのが難点だが、そもそもそういうものなのでやむをえないしアプリケーションのファイル数が増えて同期が遅くなってくるのも辛いので、改善されるなら試す価値はあると思う
環境の管理が大変なら有料でも同じことができるDesktop環境を探す選択肢をとることになりそう
DockerComposeのfileはマージできる
docker compose
コマンド実行時に -f
オプションで読み込む設定ファイルを指定出来るが、複数ファイルを指定すると内容をマージして実行してくれる
docker-compose.main.yml
services: ruby: image: ruby:3.2.1
docker-compose.other.yml
services: python: image: puthon:3.9.16
configコマンドで実行される設定を確認できる
docker compose -f docker-compose.main.yml -f docker-compose.other.yml config
name: tmp services: python: image: python:3.9.16 networks: default: null ruby: image: ruby:3.2.1 networks: default: null networks: default: name: tmp_default
docker compose -f docker-compose.main.yml -f docker-compose.other.yml up -d docker compose -f docker-compose.main.yml -f docker-compose.other.yml ps -a
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS tmp-python-1 python:3.9.16 "python3" python 25 seconds ago Exited (0) 24 seconds ago tmp-ruby-1 ruby:3.2.1 "irb" ruby 25 seconds ago Exited (0) 24 seconds ago
重複する設定がある場合には後から指定したファイルの内容に上書きされるので注意が必要
docker-compose.other.yml
services: python: image: python:3.9.16 ruby: image: ruby:3.1.0
docker compose -f docker-compose.main.yml -f docker-compose.other.yml config
name: tmp services: python: image: python:3.9.16 networks: default: null ruby: image: ruby:3.1.0 networks: default: null networks: default:
毎回 -f
オプションをつけるのが手間であれば環境変数に設定してしまうと楽
export COMPOSE_FILE="docker-compose.main.yml:docker-compose.other.yml"
セパレータはMacやLinux(:)かWindowsか(;)で異なる
参照
MySQLは設定によってはVARCHARの比較時にアルファベットの大文字小文字を区別しない
最近MySQLのVARCHARがデフォルトの設定で大文字と小文字を区別してくれていない事を知らずにバグを仕込んでしまったので軽く押さえてみた
検証
Dockerコンテナ
$ docker run -it --name mysql -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 $ docker exec -it mysql bash
MySQLにログインしてテーブル作成
$ mysql -uroot -p $ CREATE DATABASE sample_db; $ CREATE TABLE sample_db.user (id int, name varchar(10));
nameを大文字でレコードを登録
$ INSERT INTO sample_db.user VALUES(1,'JHON');
WHEREでnameを指定して絞り込み
大文字
$ SELECT id,name FROM sample_db.user WHERE name = 'JHON'; +------+------+ | id | name | +------+------+ | 1 | JHON | +------+------+ 1 row in set (0.00 sec)
小文字
$ SELECT id,name FROM sample_db.user WHERE name = 'jhon'; +------+------+ | id | name | +------+------+ | 1 | JHON | +------+------+ 1 row in set (0.00 sec)
whereでの指定が小文字のjhonにも関わらずnameが大文字のJHONであるレコードがselectされる
なぜこんなことになるかドキュメントをひいてみる
文字セットとは、記号とエンコーディングのセットです。 照合順序とは、文字セット内の文字を比較するためのルールを集めたものです。 架空の文字セットを例にして、文字セットと照合順序の違いを見てみましょう。
非バイナリ文字列の場合 (
CHAR
、VARCHAR
、TEXT
)、文字列検索では比較オペランドの照合順序が使用されます。 バイナリ文字列 (BINARY
,VARBINARY
,BLOB
) の場合、比較ではオペランド内のバイトの数値が使用されます。つまり、アルファベット文字の場合、比較では大文字と小文字が区別されます。
デフォルトの文字セットおよび照合順序は
utf8mb4
およびutf8mb4_0900_ai_ci
であるため、非バイナリ文字列比較ではデフォルトで大文字と小文字が区別されません。 これは、_`col_name`_ LIKE 'a%'
を使用して検索した場合、A
またはa
で始まるすべてのカラム値が取得されることを意味します。
かいつまんで言うとMySQLでは文字を扱うために文字セットと文字を比較するための照合順序(collate)の設定があってクエリを実行した際の結果はこの設定に左右されるらしいが、デフォルトでは大文字と小文字が区別されない設定になっているようだ。
ただし非バイナリ文字列比較とあるのでBINARY演算子によるCASTを試してみる。
CAST付きの絞り込み
小文字
SELECT id,name FROM sample_db.user WHERE BINARY name = 'jhon'; Empty set, 1 warning (0.00 sec)
大文字
SELECT id,name FROM sample_db.user WHERE BINARY name = 'JHON'; +------+------+ | id | name | +------+------+ | 1 | JHON | +------+------+ 1 row in set, 1 warning (0.00 sec)
今度は小文字ではヒットしなくなった
またクエリの実行時にcollateを指定する事も出来る
collateを使用した比較
まずは文字セットを指定したいcollateに対応したものに設定しなおす
SET character_set_connection=utf8mb4;
小文字
mysql> SELECT id,name FROM sample_db.user WHERE name = 'jhon' COLLATE utf8mb4_0900_bin; Empty set (0.01 sec)
大文字
mysql> SELECT id,name FROM sample_db.user WHERE name = 'JHON' COLLATE utf8mb4_0900_bin; +------+------+ | id | name | +------+------+ | 1 | JHON | +------+------+ 1 row in set (0.00 sec)
テーブルが使用するcollateの設定ごと変えてやればクエリごとに指定をしなくても良くなる
テーブルの設定
現在の設定の確認
SELECT TABLE_COLLATION FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='sample_db' AND TABLE_NAME='user'; +--------------------+ | TABLE_COLLATION | +--------------------+ | utf8mb4_0900_ai_ci | +--------------------+ 1 row in set (0.00 sec)
collateの変更
ALTER TABLE sample_db.user COLLATE utf8mb4_0900_bin;
create table時にcollateを指定しても良い
CREATE TABLE sample_db.user (id int, name varchar(10)) COLLATE utf8mb4_0900_bin;
ただテーブルごとの設定もそれぞれに付与するのは手間だしそのデータベースを扱うサービス全体で大文字と小文字を区別して欲しいことも多いだろうから、その場合データベース単位で設定しまう方がラクだろう。
データベースの設定
ALTER DATABASE sample_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
CREATE DATABASE sample_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
最後に
今まで既存のアプリケーションの改修が多かったことも原因かもしれないがが、MySQLを触りだして数年たつのにこの事を知らずに結構衝撃だった。 幸いリリース前にチーム内の人が指摘してくれてことなきを得たが、調べても最初BINARYのキャストの話が多く出てきてピンと来ずcollateという馴染みの薄い単語覚えておかないと調べるのに少し不便だったので記録を残しておく
おまけ
文字セットの確認
SHOW VARIABLES LIKE "chara%"; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | latin1 | | character_set_connection | latin1 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | latin1 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ 8 rows in set (0.01 sec)
GoのStructをJSONに変換する場合fieldは大文字でなければならない
GoでJSONを扱いたかったのだがStructから変換する際に変換の対象にならないFieldがあった。
type User struct { No int Name string tel string } func main() { user := User{No: 0, Name: "Bob", tel: "000-1111-2222"} json, _ := json.Marshal(user) fmt.Println(string(json)) // {"No":0,"Name":"Bob"} }
要はStructのFIeldが小文字で始まる場合はJSONへのコンバートの対象にならないようだ。
どうやらGoは定数なども頭文字を大文字にしないと外部のパッケージからアクセス出来ず、その関係なのかJSONに変換させる際にも大文字で始める必要があるらしい。
users []string // not exported
JSONに変換したい時にはStructのKeyを全部大文字にしないといけないのは無茶だろうと思ったが、StructにはTagオプションがありJSONに変換する際のKeyの指定や値が空の場合に変換の候補から外すオプション(omitempty)などが指定出来るようなのでこれを使うという事のようだ。
Name string
json:"name,omitempty"
Tagをつければ小文字始まりのKeyでもJSONとして出力できる。
type User struct { No int Name string Tel string `json:"tel"` } func main() { user := User{No: 0, Name: "Bob", Tel: "000-1111-2222"} json, _ := json.Marshal(user) fmt.Println(string(json)) // {"No":0,"Name":"Bob","tel":"000-1111-2222"} }
この状態でStructに値を指定しなければJSONに変換した際に空文字で出力されるが
user := User{No: 0, Name: "Bob"}
Tel string `json:"tel"` // {"No":0,"Name":"Bob","tel":""}
TagにomitemptyのオプションをつければJSONのKey自体が省略される
Tel string `json:"tel,omitempty"` // {"No":0,"Name":"Bob"}
Goをそんなに書き慣れていないのでJSONにした際に一部だけ落ちるという状況に最初?となったが、頭文字が大文字か小文字かでエクスポートの対象にするか分けるという事が分かればルールとしてなるほどと思うで言語の仕様としてちょっと面白いと感じた。
言語を調べる際にはなるべく公式にあたりたいと思うのだがGoのpkgは日本語のドキュメントがあるがStructに関しては適当なものが見つからず途中ちょっと困った。
ActiveRecordのlockメソッド(Postgresql)
概要
これまであまり使っていなかったActiveRecordのlock機能に触れる機会があったので少し試したことを記録しておく
Version
- Ruby 3.1.1
- ActiveRecord 7.0.2.2
- Postgresql 14.2
詳細
lock(with_lock)メソッド
AcitveRecordのロック機能のについてはRailsガイドに解説があるが、今回調べたlockメソッドはデータベースの機能を利用した悲観的ロックを扱う。transactionで囲まれたブロック内でlockを経由してからクエリメソッドを実行すると対象のレコードにロックが実行され、このトランザクションが完了するまではその他のトランザクションから更新を防ぐ事が出来る。
User.transaction do user = User.lock.first user.update(name: "change") end
もしくは
user = User.lock.first user.with_lock do user.update(name: "change") end
動作の検証
デバッガのブレイクポイントを貼ってトランザクションの途中で停止させた状態で
User.transaction do user = User.lock.first binding.pry user.update(name: "change") end
別のRails consoleなどからレコードの更新を行うとupdate
の実行が待たされる。
# 別のRails console user = User.first user.update(name: "other name") => true # デバッガを進めてlockしたtransactinoを抜けると処理が実行され結果が返ってくる
注意しないといけない点としてトランザクションを貼った中でなければロックが実行されないのだが、その状態でlockメソッドを使用してもなんの警告もエラーもでない事だ。User.transaction
をコメントアウトした以下のコードはこの箇所のupdateだけであれば問題なく実行されるが、先ほど同様にlock直後をデバッガで停止させた状態で別のコンソールなどからupdate
を実行しても即座に更新出来てしまう。
#User.transaction do user = User.lock.first binding.pry user.update(name: "change") #end
update
の実行だけなら一見正常に実行出来たように見えてしまうのでバグを防ぐためにはきちんとロックを想定したテストを書くなどの対策が必要だろう。
行ロック・テーブルロック
Postgresqlには行ロック・テーブルロックがある。 SQL文の中身を見るとわかるがActiveRecordのlockメソッドで実行されているのは行ロックだ。
User.lock.to_sql => "SELECT \"users\".* FROM \"users\" FOR UPDATE"
また
異なる種類のロックを使いたい場合は、
lock
メソッドに生SQLを渡すことも可能です。たとえば、MySQLにはLOCK IN SHARE MODE
という式があります(レコードのロック中にも他のクエリからの読み出しを許可します)。この式を指定するには、以下のように単にlockオプションの引数で渡します。 Railsガイド/Active Record クエリインターフェイス
という事なので引数にモードの指定を渡せばRailsガイドの例にあるMySQLと同様にPostgresでも行ロックのモードを指定する事が出来る。
User.transaction do # User.lock('FOR SHARE').to_sql # => "SELECT \"users\".* FROM \"users\" FOR SHARE" user = User.lock('FOR SHARE').first user.update(name: "change") end
一方のテーブルロックだがPostgresqlではLOCKコマンドを実行する必要があるので引数でなにも指定しない場合でもSELECTメソッドが実行されるActiveRecordのlockメソッドでは使用できない。もしテーブルロックが必要であるならModel経由ではなく別に生のSQLを実行する必要がある。
User.transaction do ActiveRecord::Base.connection.execute('LOCK users IN ACCESS EXCLUSIVE MODE') user = User.first binding.pry # ここで止めると別コンソールで User.first の時点で結果が返ってこない user.update(name: "change") end
lockメソッドを使用していないがtransactionを抜ければ待たされていた実行結果は返ってくるようだ。
参照
DockerのChromeでCupriteの動作を検証してみる
はじめに
Seleniumの調査だったり開発環境がApple Siliconだったりで考慮しないといけない事が多いのでSystemTestの設定を後回しにしていたRailsアプリがあったのだが、CupriteというSeleniumではなくCDP(Chrome DevTools Protocol)を使用したGemの存在を知ったのがきっかけでやっぱりきちんとやろうという事になり、まずは勉強がてら一旦Railsから離れてCupriteとDockerのChormeで環境を作って試してみた。
詳細
Gemなど
Ferrum
RubyでCDPを使うためのGem。Ferrumとはラテン語で鉄の意味らしい。
名前がラテン語である理由やGItHubのトップにあるイラストがMUCのエンドゲームでサノスが使っていたインフェニティティガントレットっぽく見えるのが気になる。
Cuprite
CapybaraでFerrumを使うためのGem。
CDP(Chrome DevTools Protocol)
Cupriteを知るまでその存在を知らなかったのだがプログラムから呼び出せるChromeに実装されたAPI。E2ETestなどではSeleniumを代替出来るのでCuprite & Ferrumはそのアプローチを取っている。
やったこと
疎通のテストなので単純なリクエストを返してくれるRackアプリをテスト用に作成。 最終的にCapybaraを通してJavaScriptの実行を試したいので検証用のボタンも追加しておく。
config.ru
class App def call(env) html = "<html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>" [200,{ "Content-Type" => "text/html" },[html]] end end run App.new
DockerはWebアプリを起動するためのRubyとChromeを別コンテナで立ち上げている。
version: "3.9" services: web: build: context: . command: ["bundle", "exec", "rackup", "-p", "80", "-o", "0.0.0.0"] ports: - "80:80" volumes: - ./app:/app depends_on: - chrome chrome: image: browserless/chrome ports: - "3333:3333" volumes: - .:/app:cached environment: PORT: 3333 CONNECTION_TIMEOUT: 600000
Rubyのコンテナでは必要なGemをbuild時にインストールしておりRuby3からWEBRickが標準ライブラリでなくなったのでPumaを追加している。
# frozen_string_literal: true source "https://rubygems.org" gem "rack" gem "puma" gem "cuprite" gem "test-unit" gem "debug"
FROM ruby:3.1.1 RUN mkdir /app WORKDIR /app ADD ./app /app RUN gem install bundler RUN bundle install
$ docker compose build $ docker compose up -d
Dockerのビルドとコンテナの起動が済んだら動作を一つずつ確認していく。
Webアプリコンテナ <-> Chromeコンテナ
Rackアプリの起動の確認とWebコンテナからChormeのコンテナにアクセス出来るかを確認しておく
$ docker compose exec web curl 'http://localhost:80' <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>% $ docker compose exec web curl 'http://chrome:3333' <!doctype html><html><head><meta charset="utf-8"/><title>browserless debugger</title>...
Chrome経由でWebアプリに接続するのでChromeのコンテナからホストネームでアクセス出来るかも確認しておく
$ docker compose exec chrome curl 'http://web:80' <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id='text'>Hello World!</h1><button onclick=changeText()>Click</button></body></html>%
Ferrum -> Chromeコンテナ
WebコンテナからFerrum経由でChromeにアクセス出来るか確認
options = { url: 'http://chrome:3333', browser_options: { 'no-sandbox': nil } } browser = Ferrum::Browser.new(options) browser.go_to("https://google.com") puts browser.current_title browser.quit
$ docker compose exec web ruby ferrum.rb Google
Capybra -> Chromeコンテナ -> Webアプリ
Capybara経由でChromeを通してRackアプリに接続出来るか確認
require "capybara/cuprite" require 'capybara/dsl' options = { url: 'http://chrome:3333', browser_options: { 'no-sandbox': nil }, base_url: "http://web" } Capybara.register_driver(:cuprite) do |app| Capybara::Cuprite::Driver.new(app,options) end Capybara.javascript_driver = :cuprite Capybara.server_host = "0.0.0.0" Capybara.app_host = "http://web:80" session = Capybara::Session.new(:cuprite) driver = session.driver browser = driver.browser page = browser.page browser.visit "/" puts page.body
今回ChromeもDockerにした上に似た名前の項目が多いので設定が紛らわしいが Capybara::Cuprite::Driver.new
のurlにはChromeの宛先をCapybara.app_host
にはWebアプリケーションの宛先を設定する。
その他はCapybaraからvisit "/"
のようにホスト名を省略してページを選択したければ
Capybara::Cuprite::Driver.new
にbasu_url
を渡す。
browser_options: { 'no-sandbox': nil }
はDocker経由の場合必要とのこと。
Capybara.server_host
もDocker経由の場合 "0.0.0.0" を指定して外部からのアクセスを許可してやる必要がある。
この内容を保存して実行してやるとCapybara経由でRackアプリに接続出来る事が確認できる。
$ docker compose exec web bundle exec ruby capybara.rb <html><head><script>function changeText() { document.querySelector('#text').textContent = 'Change Text'; }</script></head><body><h1 id="text">Hello World!</h1><button onclick="changeText()">Click</button></body></html>
TestCase
最後にテストケースを作成してSystemTestでの実行を試す。
# 必要な設定は上のCapybaraのテストとほぼ同じだが、そのままだとRackサーバーを立ち上げようとするので以下の設定を追記 Capybara.default_driver = :cuprite require 'test/unit' require 'capybara/dsl' class SystemTestCase < Test::Unit::TestCase include Capybara::DSL def test_foo visit '/' assert_equal(page.find('h1').text, 'Hello World!') page.click_on 'Click' assert_equal(page.find('h1').text, 'Change Text') end end
$ docker compose exec web ruby test_case.rb Loaded suite test_case Started . Finished in 0.319190042 seconds. -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 3.13 tests/s, 6.27 assertions/s
JavaScriptの実行も確認出来た
つまずいたところ
途中上記の設定で実行しても以下のような空のHTMLで返ってきて困った。
<html><head></head><body></body></html>
原因は意外なところでDockerComposeで指定していたサービス名が理由だった。 こちらを参考にしてapp -> webに変えたらうまく動いた。エラー内容も違うし詳細な理由はわからないのが、とりあえずはDockerComposeのService名はappにしないほうが無難なのだろうか
まとめ
SystemTestはブラウザが必要だったりUnitTestよりも複雑でそこにDockerや新しく使うGemが絡んでくると確信のないまま設定をあれこれいじくる事になりがちなので一旦小さな構成で試してみた。 とりあえず迷った時に立ちかえられる構成が出来たので次はRailsアプリに組み込んでいきたい。
参照
RailsでリクエストをWebサーバーから受け取った直後にBreakpointを貼ってデバッグする
RailsでPumaの挙動を確認したかったのだがデバッグするのにRackのMiddlewareが邪魔だったので回避策を検討してみた。
環境
詳細
Railsから見て先頭付近でリクエストを受け付けるところとして最初にApplicationControllerにデバッガを貼ってみたがBacktraceを見ると60行ほどのスタックでPumaからBreakpointまで結構な量のMiddlewareを通過していた。
class ApplicationController < ActionController::Base binding.break end
rails middleware
を打つとnewしたてのRailsアプリでもこれだけのMIddlewareが表示される。これをいちいち遡ってPumaまでたどりくのは面倒だ。
use ActionDispatch::HostAuthorization use Rack::Sendfile use ActionDispatch::Static use ActionDispatch::Executor use ActionDispatch::ServerTiming use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::ActionableExceptions use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ContentSecurityPolicy::Middleware use ActionDispatch::PermissionsPolicy::Middleware use Rack::Head use Rack::ConditionalGet use Rack::ETag use Rack::TempfileReaper run MyApp::Application.routes
アプリケーションとしてのRailsにリクエストが届くのがMiddlewareを経過した後なら、Middlewareの中にBreakpointを貼って然るべき場所に登録してやれば良いだろうという事で↓のようなただリクエストを通過させるMiddlewareを作成してBreakpointを設定。
class MyMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) binding.break [status, headers, body] end end
このMiddlewareをconfig/application.rb
から登録すれば良いのだがconfig.middleware.use
を使うとMiddlewareの最後、アプリケーションの直前に挿入されてしまうのでconfig.middleware.insert_before
を使って一番上のMiddlewareの前に登録してやる。
config.middleware.insert_before ActionDispatch::HostAuthorization, MyMiddleware
$ rails middleware use MyApp::Application::MyMiddleware use ActionDispatch::HostAuthorization use Rack::Sendfile ... run MyApp::Application.routes
この状態でRailsのseverを起動して適当なリクエストを送ると先頭に追加したMiddlewareの中でデバッガが実行されてPumaの直後ぐらいにBreakpointが止まってくれる。あとは少しスタックを昇っていけばさほど労せずに目的のPumaまでたどり着ける。
$ backtrace #0 MyApp::Application::MyMiddleware#call(env={"rack.version"=>[1, 6], "rack.errors"=>...) #1 Rails::Engine#call(env={"rack.version"=>[1, 6], "rack.errors"=>...) #2 Puma::Configuration::ConfigMiddleware#call(env={"rack.version"=>[1, 6], "rack.errors"=>...)
感想
軽い気持ちでPumaの挙動を確認しようと思ったらMiddlewareが結構邪魔で、当初回避方法を思いつかずInitializerとして登録したりして右往左往したので同じ轍を踏まないように記録しておく。 最終的にはシンプルな形で解決出来たのでPumaに限らずinsert_beforeの対象を適当なMiddlewareにすれば実際に開発しているアプリでMiddlewareのデバッグもしやすくなったので結果オーライか。