Posted at

Railsでサーバ立ち上げのときのみ実行するコードの配置 - Initialization Codes Executed Only in Server Mode (Rails)

More than 1 year has passed since last update.


TL;DR

config.rurun Rails.application Statement の直前に書く。

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

p "Initialization codes should come here."
p "In config.ru file, #{Rails.env}, models can also be handled #{Article.name}"
run Rails.application


経緯

rails で HTTP アプリケーションを立ち上げつつ、同時に discordrb gem を使って discord bot も立ち上げたかった。


要件


  1. RailsでHTTPアプリのServe

  2. RailsのHTTPアプリで利用するModelやJobをdiscordrbボットでも流用する


  3. rails sコマンドやrails unicorn:startコマンドの実行時に自動的にdiscord bot立ち上げる


  4. rails cコマンドやrails task起動時にはdiscord botが立ち上がってほしくない(重複するため)

初めは config/initializers/discord.rb にbot起動コードを書いていたが、これだと要件4が満たされない

かといって、単独のrubyプログラムとしてdiscord botを記述してしまうと、要件2が満たされない

さらに、下手くそなのでrails s時に自動的にbotが立ち上がらないと、忘れてしまう。(要件3)


ネット上で散見される解決策


  1. Rails::Server#initialize をオーバーライドしろ


  2. Rails::Server定数の定義の有無をチェックしてif分岐しろ

    if defined?(Rails::Server)
    
    # do something
    end


しかし、1. は、ブラックボックス触りたくない

2. はRackハンドラ (e.g. unicorn, WEBrickなど) が変わると通用しない

ので、他の方法を模索しました。


(いきなり)解決策

config.ru に書け。

これはrails appが読み込まれ、initializersの処理等も終わったタイミングで呼び出される。

しかも、rails cなど実行時には呼ばれない。


実験


準備



  1. rails new hoge で空のRails app作成


  2. rails g model Article title:string content:text published_at:datetime でそれっぽく作る


  3. rails g job ArticlePublishJob でそれっぽく(対照実験用)

  4. rails taskもそれっぽく作る

  5. config/initializers/sample.rbもそれっぽく

  6. 肝の部分はconfig.ru


  7. rails db:migrate して準備完了

# app/job/article_publish_job.rb

class ArticlePublishJobJob < ApplicationJob
queue_as :default

def perform(article)
article.update(published_at: Time.zone.now)
end
end

# lib/tasks/articles.rake

namespace :articles do
desc "publish all articles"
task :publish => :environment do
Article.all.each do |article|
ArticlePublishJob.perform_later article
end
end
end

# config/initializer/sample.rb

p "In config/initializers/sample.rb file, #{Rails.env}, #{Article.name}"
Article.create(title: "Sample article", content: "This is a sample article...")

# config.ru

# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

p "Initialization codes should come here."
p "In config.ru file, #{Rails.env}, models can also be handled #{Article.name}"
run Rails.application


いざ、試験


まず, rails s

/tmp/hoge% rails s

=> Booting Puma
=> Rails 5.0.7 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
"In config/initializers/sample.rb file, development, Article"
"Initialization codes should come here."
"In config.ru file, development, models can also be handled Article"
Puma starting in single mode...
* Version 3.11.4 (ruby 2.5.1-p57), codename: Love Song
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
^C- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2018-05-28 19:03:26 +0900 ===
- Goodbye!
Exiting

ちゃんとconfig.ru内のコードが実行されている


次はrails c

/tmp/hoge% rails c

Running via Spring preloader in process 16334
Loading development environment (Rails 5.0.7)
irb(main):001:0> Article.last
Article Load (0.6ms) SELECT "articles".* FROM "articles" ORDER BY "articles"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Article id: 1, title: "Sample article", content: "This is a sample article...", published_at: nil, created_at: "2018-05-28 10:12:06", updated_at: "2018-05-28 10:12:06">
irb(main):002:0>

config/initializers/* なファイルだけ実行される。

想定通り。


さいごに rails articles:publish

/tmp/hoge% rails articles:publish

"In config/initializers/sample.rb file, development, Article"

これもOK


さいごに

いろいろ調べたけど, config.ru はRackのエントリーポイントっぽく、RackはHTTPアプリケーションフレームワークということで、HTTP鯖upするときだけ呼ばれるという挙動は納得です。

今回の実装がベクとプラクティスかどうかはわかりませんが、いろいろググってもしっくり来る実装がなくてこの対処に落ち着きました。

他にこんな良い方法あるよ!っていうのがあれば教えて頂けると幸いです。

Rackアプリケーションの概要については @higuma さんの下記記事が大局観をつかみやすく、わかりやすかったです。ありがとうございました。

Rack解説 - Rackの構造とRack DSL