Rails8お試し(Authentication GeneratorとSolidQueue/Cache/Cable)
Ruby on Rails — Rails 8.0: No PaaS RequiredにてRails8がついにリリース!という記事が公開されていた。
今回は外部依存のミドルウェア(Redisとか)が無くともRDB(デフォルトはSQLite。その他MySQLやPostgresも使える。)で何とかする仕組みがSolidX(SolidQueue/SolidCache/SolidCable)の形で実装されたり、デプロイツールのKamal2とThrusterが追加されたりとThe One Person Frameworkへの強化がメインになっていそう。他にも新たなアセットパイプラインとしてPropshaftが導入されたり、認証システム構築のための基本コンポーネントが組み込まれたりといった辺りも目玉のアップデートとなっている。
この辺りの新機能や改修を一通り試してみたい。せっかくRedis等のミドルウェアが不要になっているのでDockerも使わずホスト環境でそのまま触ってみる。
Rails8のセットアップ
とりあえずRails8をインストールしてスタートページを表示させる。
mkdir rails8-sandbox
cd rails8-sandbox
brew upgrade ruby-build
rbenv install 3.3.6
rbenv local 3.3.6
rbenv rehash
gem install rails -v 8.0.0
rails _8.0.0_ new .
bin/dev
open http://127.0.0.1:3000
認証機能をBasic Authentication Generatorで作成する
Rails 8で基本的な認証ジェネレータが導入される(翻訳)|TechRacho by BPS株式会社
Rails 8 introduces a basic authentication generator - BigBinary Blog
Add basic authentication generator · Issue #50446 · rails/rails
今回から追加されたRails提供の基本の認証機能ジェネレータを実装してみる。実行し生成されるファイルを確認してみるとこんな感じ。
$ bin/rails generate authentication
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Gemfile
modified: Gemfile.lock
modified: app/controllers/application_controller.rb
modified: config/routes.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/channels/
app/controllers/concerns/authentication.rb
app/controllers/passwords_controller.rb
app/controllers/sessions_controller.rb
app/mailers/passwords_mailer.rb
app/models/current.rb
app/models/session.rb
app/models/user.rb
app/views/passwords/
app/views/passwords_mailer/
app/views/sessions/
db/migrate/
test/fixtures/users.yml
test/mailers/previews/
test/models/user_test.rb
TableとしてはUserとSessionのテーブルを作成するマイグレーションファイルが作成されている。emailとpasswordの普通の認証処理でhas_secure_passwordを利用してセッショントークンを作成するみたい。ActiveSupport::CurrentAttributesにsessionを保持し、そのsessionにuserを委譲する設計。こんな感じ。
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end
app/controllers/concerns/authentication.rb
に具体的な認証処理が書かれている。
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id])
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
なんでメソッドごとに2行改行と1行改行があるのか謎だけど処理としてはシンプル。cookieからsession idを取り出して有効なら先ほどのCurrent.sessionに保持させて使い回す感じ。もしsession idがない場合はサインインページへリダイレクトされるだけ。あとはpasswords_controller.rb
とsessions_controller.rb
、それに対応するviewやroutes.rbも勝手に作成されている。従来のようにDevise入れてゴニョゴニョやるよりデフォルトが提供してる分スッキリしてる印象で良い。
余談だけどpasswords_controller.rb
にUser.find_by_password_reset_token!
という呼び出しがあり、このメソッドはどこで定義されてるのか調べたらhas_secure_passwordのクラスでdefine_methodされていた。#{attribute}_reset_token
という感じで動的生成されており、つまりデフォルトはpassword
だけどhoge_reset_token
という名前にもできるということっぽい。ヘぇ〜。
dbのmigrateをして実際に認証ページ( http://127.0.0.1:3000/session/new )へアクセスしてみるといい感じに表示された。
$ bin/rails db:migrate
で、じゃあ新規ユーザー作成するか〜と思ってリンク探したらなんとない!新規作成はサポートしてないらしい...。ここまでやってくれるなら新規作成までやってよくない?と思ったが仕方ないのでbin/rails console
でUse.create(email_address:, password:)
してHomeのcontrollerとviewを作りrootの設定をした。そんであとはさっきのログインページからログインすればhomeにリダイレクトされることを確認した(ちなみにcannot load such file -- bcrypt
のエラーが出たんだけどserverを再起動するだけで直るので焦るな。)
デフォルトだとAuthentication
のconcern内でrequire_authentication
が実行されており、ApplicationController
でそれがincludeされているのでこのApplicationController
を継承したページは全部ログイン必須になってた。やはり認証機能の最低限だけは適当に提供しとくけど、あとはご自由にどうぞという割り切りで作られてるな。
SolidCache/SolidQueue/SolidCableを試す
以下SolidCache/SolidQueue/SolidCableを雑に試していく。
SolidQueue
rails/solid_queue: Database-backed Active Job backend
SolidCache自体はRailsのデフォルトで入っているので追加のgemなどはいらない。設定ファイルがconfig/cache.yml
にあるのでそこにキャッシュのname_spaceやmax_sizeを設定していく模様。詳しいOptionはここにリストされている。基本はActiveJobのバックエンドとしてシームレスに利用できるようになっているから利用者としてはあまり意識しなくて良い(はず)。今までセッション管理のためだけにElastiCacheを使ってたりした部分がRDSに統合できるようなって嬉しい〜というのがおすすめポイントらしい。何かと依存するミドルウェアが多いと環境構築とか面倒だからありがたい改善ではある。
とはいえ設定がデフォルトでされているのは本番環境用の話であり、それ以外の開発環境などでは別途自分で設定が必要だった。まずconfig/development.rb
に下記を追加。
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
そんでconfig/database.yml
のdevelopmentを下記に変更。SolidQueue用に別DBを作成する必要があるのか...。パフォーマンス考えたらそりゃそう。
development:
primary:
<<: *default
database: storage/development.sqlite3
queue:
<<: *default
database: storage/development_queue.sqlite3
migrations_paths: db/queue_migrate
そしてbin/rails db:drop && bin/rails db:prepare
を実行する。これでstorage/
にdevelopment.sqlite3
とdevelopment_queue.sqlite3
が作成される。あとはbin/job
を実行するとSolidQueue
が起動する。
ひとまず適当に↓のようなJobを作成してRailsコンソールを立ち上げてSampleJob.perform_later
を実行。
class SampleJob < ApplicationJob
queue_as :default
def perform(*args)
Rails.logger.info "SampleJob is running"
sleep 5
Rails.logger.info "SampleJob is done"
end
end
するとlogs/development.log
にログが出力されるはず。実行できてた。
QueueのDBの方がどうなってるのかも気になるのでActiveJobのダッシュボードを提供してくれるmission-control-jobsを入れてroute.rbにmount MissionControl::Jobs::Engine, at: "/jobs"
を設定。
すると/jobs
というパスが生えるのでアクセスすると↓こんな感じになって実行結果がモニターできたりする。
sidekiq/web
みたいなUIの自動更新がないのでやや不便。issueは出来てたのでPRチャンス。
データ用のDBとQueue用のDBは分けることを推奨されているが一つのDBで全て実行してもいいらしい。でもデータ用とは別にDBが必要なら、普通クラウドのRDB費用はクラウド全体の費用に占める結構な割合になりがちだし、それじゃあElastiCacheみたいなものを使うのと費用面ではあんま変わんないよな〜と思ったりした。
SolidCache
rails/solid_cache: A database-backed ActiveSupport::Cache::Store
SolidCacheはRailsのキャッシュ機構のバックエンドを今までのmemcachedやRedisからRDBを使うようにできる機能。SolidQueueと同様にconfig/cache.yml
という設定ファイルがあったり、config/database.yml
にキャッシュ用のデータベースを作成する設定ができたりと同じインターフェースになっている。
今回は一番わかりやすそうなフラグメントキャッシュ(viewの部分キャッシュ)で試す。とりあえず開発環境だと有効になってないので下記のコマンドで有効にする。
rails dev:cache
次にdevelopment.rb
でcache storeをSolidCacheに変更。
# config.cache_store = :memory_store <- コメントアウト
config.cache_store = :solid_cache_store
ほんでdatabase.yml
にqueueの時と同じくcacheの設定を記述する。
development:
...
cache:
<<: *default
database: storage/development_cache.sqlite3
migrations_paths: db/cache_migrate
キャッシュの更新についても確認しやすいようにexpireする時間を短くしておく。cache.yml
に下記を追加。失効時間を30秒に設定。
development:
database: cache
store_options:
expiry_batch_size: 1
max_age: <%= 30.seconds.to_i %>
フラグメントキャッシュが動いてるのを確認するために下記をview/home/index.html
に追加
<% cache("home_index_time") do %>
<p>
<%= Time.now %>
</p>
<% end %>
これで何度かアクセスするとTime.nowの時間がキャッシュされた時間から更新されなくなるはず。サーバーログには↓こんな感じでsolid_cache_entries
テーブルからデータが取得されていることがわかった。
SolidCache::Entry Load (0.8ms) SELECT "solid_cache_entries"."key", "solid_cache_entries"."value" FROM "solid_cache_entries" WHERE "solid_cache_entries"."key_hash" IN (-398009230309464708) /*action='index',application='Rails8Sandbox',controller='home'*/
↳ app/views/home/index.html.erb:3
Read fragment views/home/index:61461968e53d2da16b5bba2b509e5932/home_index_time (1.8ms)
次にどうやってキャッシュが削除されるのかを確認する。これは少し独特なので注意が必要。というのも次回書き込み時にキャッシュに失効期限のものがあれば削除されるというアルゴリズムになっている。なので例えばcache.yml
にmax_age
というキャッシュの有効期限を設定していても、次に書き込みがある新規のキャッシュ追加更新処理がなければそのキャッシュは失効していたとしても更新されない。
先ほどのviewの場合、例えば下のように新規のキャッシュ処理を追加した時に初めて表示されるTime.now
の時間が更新される仕組み。
<% cache("home_index_time") do %>
<p>
<%= Time.now %>
</p>
<% end %>
<% cache("home_index_time2") do %>
<p>削除用のキャッシュテスト</p>
<% end %>
これがrenderingされるときに初めて下記のログが表示された。
SolidCache::Entry Upsert (0.4ms) INSERT INTO "solid_cache_entries" ("key","value","key_hash","byte_size","created_at") VALUES (x'646576656c6f706d656e743a76696577732f686f6d652f696e6465783a30666163373539643361373034356238643734363231313065616430346164612f686f6d655f696e6465785f74696d65', x'001102000000000000f0bf0a0000000408492200063a06454620203c703e0a20202020323032342d31312d31302031363a35393a3337202b303930300a20203c2f703e0a', 2662525179818617242, 285, STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) ON CONFLICT ("key_hash") DO UPDATE SET "key"=excluded."key","value"=excluded."value","byte_size"=excluded."byte_size" RETURNING "id" /*action='index',application='Rails8Sandbox',controller='home'*/
↳ app/views/home/index.html.erb:3
Write fragment views/home/index:0fac759d3a7045b8d7462110ead04ada/home_index_time (1.2ms)
次の書き込み時とか関係なく失効時間になったら自動で更新されて欲しい場合はどうすんだ...。
ちなみにデフォルトだと書き込み処理の際に失効対象のキャッシュを探し、もしあればそのスレッド中で削除処理が行われる。だから例えば削除対象のキャッシュが多い場合はそのプロセスだけ重くなってしまうはず。というわけで削除処理自体をバックグラウンドジョブに逃がすオプションがある。expiry_method: :job
とcache.yml
で設定しておけば有効になる。その他詳しいアルゴリズムについてはREADME/cache-expiryに書いてるので確認しておくとよさそう。
SolidCable
rails/solid_cable: A database backed ActionCable adapter
SolidCacheはActionCableのバックエンドをRDBにするための機能。他のSolidX同様にRailsの依存からRedisを剥がすモチベーションで使われる模様。
まずはconfig/database.yml
とconfig/cable.yml
に設定を追加する。
development:
...
cable:
<<: *default
database: storage/development_cable.sqlite3
migrations_paths: db/cable_migrate
development:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
そしてbin/rails db:drop && bin/rails db:prepare
でdbの作成。
デモとしては下記のgeneratorで雛形を作成し簡単なチャットUIを作ってみるのが早い。Rails 7でリアルタイム通信を実現! Action Cableの基本をチュートリアルとともに理解しようの記事なんかを参考に作ればok。
$ bin/rails generate channel Chat
あとはメッセージを送ってログを確認すると↓のようにSolidCacheが使われているのがわかるはず。
broadcast
ChatChannel#speak({"message"=>"hi!"})
[ActionCable] Broadcasting to chat_channel: {:message=>"hi!", :sent_at=>"19:12"}
SolidCable::Message Insert (0.3ms) INSERT INTO "solid_cable_messages" ("created_at","channel","payload","channel_hash") VALUES ('2024-11-10 10:12:07.253561', x'636861745f6368616e6e656c', x'7b226d657373616765223a22686921222c2273656e745f6174223a2231393a3132227d', 5736154075277867913) ON CONFLICT DO NOTHING RETURNING "id" /*application='Rails8Sandbox'*/
↳ app/channels/chat_channel.rb:12:in `speak'
[ActiveJob] [SolidCable::TrimJob] [86e0320c-08fa-455e-81a8-158779ecf61e] Performing SolidCable::TrimJob (Job ID: 86e0320c-08fa-455e-81a8-158779ecf61e) from SolidQueue(default)
まとめ
今回はRails8に追加されたBasic Authentication GeneratorとSolidCache/SolidQueue/SolidCableを触ってみた。他にもAssetPipelineの軽量代替実装のPropshaftや本番デプロイ楽々マンになるためのKamal2+Thrusterなんかも触りたかったけど、力尽きてしまったのでまたいつか。
今回の作業リポジトリ↓
Discussion
mkdir=>cdのとこ間違い?