Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1838 articles
Browse latest View live

Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)

$
0
0

前記事: Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)

Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerに関連しないツールを必要としません。

ここまでのまとめ

これで自動デプロイの構築に必要な材料がすべて揃いました。以下は最終的なコードであり、deployer.rbという名前でrootフォルダに保存できます。各行の動作を見てみましょう。

# deployer.rb
class Deployer
  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

  private

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

  def current_git_rev
    `git -C #{APPLICATION_PATH} rev-parse --short HEAD`.strip
  end

  def ssh_command
    "ssh #{HOST_USER}@#{APPLICATION_HOST}"
  end

  def git_user
    `git config user.email`.split('@').first
  end

  def deploy_user
    user = git_user
    timestamp = Time.now.utc.strftime('%d.%m.%y_%H.%M.%S')
    "#{user}-#{timestamp}"
  end
end

if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

それでは1つずつ手順を追ってみましょう。

  APPLICATION_HOST = '54.173.63.18'.freeze
  HOST_USER = 'remoteuser'.freeze
  APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze
  APPLICATION_FILE = 'application.tar.gz'.freeze
  ALLOWED_ACTIONS = %w(deploy).freeze
  APPLICATION_PATH = 'blog'.freeze

ここでは値の重複を避けるためにいくつかの定数をコードで定義しています。APPLICATION_HOSTは実行するサーバーのリモートIPアドレス、HOST_USERはリモートサーバーのユーザー名、APPLICATION_CONTAINERはアプリをラップするコンテナの名前です。APPLICATION_FILEは圧縮したアプリのファイル名なので名前は自由に変えられます。ALLOWED_ACTIONSは許可する操作の配列であり、どの操作を利用可能にするかを簡単に定義できます。最後のAPPLICATION_PATHはアプリへのパスです。今回の例ではblogとしています。

  def initialize(action)
    @action = action
    abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action
  end

  def execute!
    public_send(@action)
  end

上は、(ALLOWED_ACTIONSで)利用できる各メソッドのバリデーションと呼び出しを行うラッパーです。これを用いることで、コードをリファクタリングする必要なしに、呼び出し可能な新しいメソッドを簡単に追加できます。

  def deploy
    check_changed_files
    copy_gemfile
    compress_application
    build_application_container
    push_container
    remote_deploy
  end

上はデプロイ手順です。これらのメソッドは先の例とほぼ同じですが、わずかな変更があります。それぞれの手順を見てみましょう。

  def check_changed_files
    return unless `git -C #{APPLICATION_PATH} status --short | wc -l`
                  .to_i.positive?
    abort('Files changed, please commit before deploying.')
  end

アプリのデプロイにはローカルのコードを使っているので、ファイルが変更されているかどうかをチェックして、変更がある場合はデプロイを行わないようにするのがよい方法です。この手順ではファイルの作成や変更を検出するのにgit status --shortを使っています。-Cフラグはgitでチェックする対象(この例ではblog)を定義します。不要ならこの手順を取り除くこともできますが、おすすめしません。

  def copy_gemfile
    system("cp #{APPLICATION_PATH}/Gemfile* .")
  end

上は、デプロイのたびにblogのルートディレクトリにあるGemfileとGemfile.lockをコピーします。これによって、デプロイが完了する前にすべてのgemがインストールされるようになります。

  def compress_application
    system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}")
  end

メソッド名からわかるとおり、この手順ではアプリ全体を圧縮して1つのファイルにします。このファイルは後でコンテナに含められます。

  def build_application_container
    system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .")
  end

このメソッドは、コンテナのビルド手順を実行します。このときに依存ライブラリやgemをすべてインストールします。Gemfileが変更されるたびにDockerでそのことが検出されてインストールが行われるので、依存ライブラリの更新を気にする必要はありません。依存ライブラリが変更されるたびに多少時間がかかります。変更が何もない場合、Dockerはキャッシュを使うので手順の実行はほぼ瞬時に完了します。

  def push_container
    system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

このメソッドは、Docker Registryに新しいコンテナをアップロードします。最新のコミットハッシュをgitで取得しているこのcurrent_git_revメソッドにご注目ください。各デプロイの識別にはこのコミットハッシュを使います。アップロードしたコンテナはすべてDockerHubコンソールで確認できます。

  def remote_deploy
    system("#{ssh_command} docker pull "\
           "#{APPLICATION_CONTAINER}:#{current_git_rev}")
    system("#{ssh_command} 'docker stop \$(docker ps -q)'")
    system("#{ssh_command} docker run "\
             "--name #{deploy_user} "\
             "#{APPLICATION_CONTAINER}:#{current_git_rev}")
  end

ここでは以下の3つを行っています。

  • docker pull: リモートサーバーにアップロードしたコンテナをpullします。ssh_commandメソッド呼び出しは、リモートコマンドの送信が必要になるたびに、コードの重複を避けるための単なるラッパーです。
  • docker stop $(docker ps -q): 新しいコンテナを実行するときにポート番号が衝突しないようにするため、実行中のコンテナをすべて停止します。
  • docker run: 正しいタグを与えて新しいコンテナを起動し、現在のgitユーザーとタイムスタンプに基づいて名前を付けます。これは、現在実行中のアプリをデプロイしたユーザーを知る必要がある場合に便利です。名前を確認するには、リモートサーバーでdocker psコマンドを入力します。
CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS              PORTS                    NAMES
01d777ef8d9a        mydockeruser/application-container:aa2da7a   "/bin/sh -c 'cd /t..."   10 minutes ago      Up 10 minutes       0.0.0.0:3000->3000/tcp   mygituser-29.03.17_01.09.43
if ARGV.empty?
  abort("Please inform action: \n\s- deploy")
end
application = Deployer.new(ARGV[0])

begin
  application.execute!
rescue Interrupt
  puts "\nDeploy aborted."
end

上はCLIから引数を受け取って、アプリのデプロイを実行します。Ctrl-Cでデプロイをキャンセルすると、rescueブロックでわかりやすいメッセージが表示されます。

アプリをデプロイする

この時点でのフォルダ構造は次のようになっているはずです。

.
├── blog
│   ├── app
│   ├── bin
... (application files and folders)
├── deployer.rb
├── Dockerfile

次は、アプリを実行してデプロイしましょう。

$ ruby deployer.rb deploy

コマンドが実行されるたびに出力が表示されます。すべての出力結果は、最初の例の手動実行とほぼ同じです。

Sending build context to Docker daemon 4.846 MB
Step 1/9 : FROM ruby:2.3.1-slim
 ---> e523958caea8
Step 2/9 : COPY Gemfile* /tmp/
 ---> Using cache
 ---> f103f7b71338
Step 3/9 : WORKDIR /tmp
 ---> Using cache
 ---> f268a864efbc
Step 4/9 : RUN gem install bundler &&     apt-get update &&     apt-get install -y build-essential libsqlite3-dev rsync nodejs &&     bundle install --path vendor/bundle
 ---> Using cache
 ---> 7e9c77e52f81
Step 5/9 : RUN mkdir -p /app/vendor/bundle
 ---> Using cache
 ---> 1387419ca6ba
Step 6/9 : WORKDIR /app
 ---> Using cache
 ---> 9741744560e2
Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor
 ---> Using cache
 ---> 5467eeb53bd2
Step 8/9 : COPY application.tar.gz /tmp
 ---> b2d26619a73c
Removing intermediate container 9835c63b601b
Step 9/9 : CMD cd /tmp &&     tar -xzf application.tar.gz &&     rsync -a blog/ /app/ &&     cd /app &&     RAILS_ENV=production bundle exec rake db:migrate &&     RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000
 ---> Running in 8fafe2f238f1
 ---> c0617746e751
Removing intermediate container 8fafe2f238f1
Successfully built c0617746e751
The push refers to a repository [docker.io/mydockeruser/application-container]
e529b1dc4234: Pushed
08ee50f4f8a7: Layer already exists
33e5788c35de: Layer already exists
c3d75a5c9ca1: Layer already exists
0f94183c9ed2: Layer already exists
b58339e538fb: Layer already exists
317a9fa46c5b: Layer already exists
a9bb4f79499d: Layer already exists
9c81988c760c: Layer already exists
c5ad82f84119: Layer already exists
fe4c16cbf7a4: Layer already exists
aa2da7a: digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623 size: 2627
aa2da7a: Pulling from mydockeruser/application-container
1fad42e8a0d9: Already exists
5eb735ae5425: Already exists
b37dcb8e3fe1: Already exists
50b76574ab33: Already exists
c87fdbefd3da: Already exists
f1fe764fd274: Already exists
6c419839fcb6: Already exists
4abc761a27e6: Already exists
267a4512fe4a: Already exists
18d5fb7b0056: Already exists
219eee0abfef: Pulling fs layer
219eee0abfef: Verifying Checksum
219eee0abfef: Download complete
219eee0abfef: Pull complete
Digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623
Status: Downloaded newer image for mydockeruser/application-container:aa2da7a
01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

出力結果は、ハッシュやDockerキャッシュの違いによって異なることがあります。最後に、上のように2つのハッシュが出力されます。

01d777ef8d9a
c3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

1つ目の短いハッシュは、停止したコンテナのハッシュです。最後の長いハッシュは、新たに実行中のコンテナのハッシュです。

これで、リモートサーバーのIPアドレスにアクセスするとアプリが実行されていることを確認できます。

Semaphoreで継続的デリバリー(CD)する

本チュートリアルのスクリプトを使って、アプリをSemaphoreに自動デプロイできます。やり方を見てみましょう。

最初に、「Project Settings」でDocker support付きのプラットフォームを指定します。

SemaphoreのProjectページで、「Set Up Deployment」をクリックします。

「Generic Deployment」を選択します。

「Automatic」を選択します。

Gitのブランチを選択します(普通はmaster)。

ここではアプリをデプロイしたいだけなので、ローカルコンピュータで実行するときと同じ方法でデプロイスクリプトを実行します。

rbenv global 2.3.1
docker-cache restore
ruby deployer.rb deploy
docker-cache snapshot

2つのdocker-cacheコマンドにご注目ください。これらがビルドしたイメージを取り出しを行うので、ゼロからビルドする必要はありません。ローカルでの実行と同様、最初は少し時間がかかりますが、次回からは速くなります。詳しくはSemaphoreの公式ドキュメントをご覧ください。

また、rbenv global 2.3.1コマンドをメモしておきましょう。これは、スクリプトの実行に必要な現在のRubyのバージョンを設定するためのものです。別の言語を使う場合は、必要な環境を設定する必要があります。

次の手順では、リモートサーバーへのアクセスに使うSSHキーのアップロード(必要な場合)と、新しいサーバーへの名前付けを行っています。完了すると、コードをmasterブランチにpushするたびにこのスクリプトが実行され、定義済みのリモートサーバーにアプリがデプロイされます。

その他の自動化可能なコマンド

この後のセクションでは、便利な自動化コマンドをいくつかご紹介します。

現在のバージョン

現在実行中のアプリのバージョンをトラックするには、コンテナのTagに情報を記述します。

現在実行中のバージョンを取り出すには、以下のコードが必要です。

def current
  remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

  abort('No running application.') if remote_revision == ''

  current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip
  if current_rev.empty?
    puts 'Local revision not found, please update your master branch.'
  else
    puts current_rev
  end
  deploy_by = `#{ssh_command} docker ps --format={{.Names}}`
  puts "Deploy by: #{deploy_by}"
end

各行の動作について解説します。

remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip

上のコマンドは以下を行います。

  • docker psでリモートコンテナのステータス出力を取得
  • grep -v CONTAINERで出力からヘッダを除去
  • awk '{print $2}'で2番目のカラム(image name:tag)を取得
  • 残りのコマンドでimage nameと:を削除し、残りの部分とコミットハッシュを返す
  • 返された文字列の最終行の改行を.stripで削除
abort('No running application.') if remote_revision == ''

コンテナが1つも実行されていない場合や、コミットが1つも見つからない場合はコマンド実行をやめます。

current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\
%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\
#{running_revision} | head -1`.strip

このコマンドは、git logにマッチするコンテナハッシュを検索して書式を整えます。

if current_rev.empty?
   puts 'Local revision not found, please update your master branch.'
else
  puts current_rev
end

このコミットが現在のgit historyにない場合、ユーザーにリポジトリの更新を促します。これは、新しいコミットがローカルコピーからまだrebaseされていない場合に発生することがあります。コミットがある場合は、ログ情報を出力します。

deploy_by = `#{ssh_command} docker ps --format={{.Names}}`

このコマンドは、現在実行中のコンテナ名を返します。コンテナ名にはユーザー名とタイムスタンプが含まれます。

puts "Deploy by: #{deploy_by}"

上のコマンドは、デプロイを行ったユーザーとタイムスタンプを出力します。

ログ

多くのアプリはログを出力するので、場合によってはログの面倒も見なければなりません。Dockerに組み込まれているログシステムを使うと、シンプルなSSH接続でアプリのログに簡単にアクセスできるようになります。

アプリからログを出力するには、以下を入力します。

def logs
  puts 'Connecting to remote host'
  system("#{ssh_command} 'docker logs -f --tail 100 \$(docker ps -q)'")
end

docker logsコマンドは、アプリで生成されたログをすべて出力します。-fフラグは、接続を保持してすべてのログをストリームとして読み出せるようにします。--tailフラグは、出力する古いログの最大行数を指定します。最後の$(docker ps -q)は、リモートホストで実行中のコンテナごとにIDを返します。今はアプリを実行しているだけなので、コンテナをすべて取り出しても問題ありません。

メモ: 本記事のサンプルアプリはすべてのログをファイルに書き込むので、Dockerにはログを一切出力しません。この振る舞いは、アプリの起動時にRAILS_LOG_TO_STDOUT=true環境変数で変更できます。

Dockerのインストールとログイン

新しいホストでは、必要なインストールや設定をsetupコマンド一発でできるようにすると便利です。

インストールとログインの2つの手順を完了させます。

def docker_setup
  puts 'Installing Docker on remote host'
  system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

  puts 'Adding the remote user to Docker group'
  system("#{ssh_command} -t 'docker login}'")
end

各コマンドの動作について解説します。

system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")

このコマンドはDockerのインストールスクリプトを実行します。リモートユーザーのパスワード入力を促すには-tフラグが必要です。パスワード入力を求められたら入力します。

system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")

このコマンドは、Dockerグループにリモートユーザーを追加します。これは、sudoせずにdockerコマンドを実行する場合に必要です。

system("#{ssh_command} -t 'docker login'")

更新されたアプリをダウンロードするためにログインが必要なので、このコマンドが必要になります。-tフラグは、ログイン入力できるようにするためのものです。

ロールバック

新しいアプリの実行で何か問題が起きたら、直前のバージョンにいつでもロールバックできることが重要です。Dockerコンテナのアプローチを用いたことで、デプロイされたすべてのバージョンがホスト上に保存されているので、即座にロールバックを開始できます。

次のコードスニペットをご覧ください。

def rollback
  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''
  puts "Previous revision found: #{previous_revision}"
  puts "Restarting application!"
  system("#{ssh_command} 'docker stop \$(docker ps -q)'")
  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")
end

各手順の動作について見てみましょう。

  puts 'Fetching last revision from remote server.'
  previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip
  abort('No previous revision found.') if previous_revision == ''

このコマンドは、リモートホスト上にあるすべてのDockerイメージの中から直前のコンテナtagをgrepします。このタグはgitコミットの短いハッシュになっていて、アプリのロールバックを参照するときに使われます。直前のDockerイメージがない場合は、ロールバックをやめます。

  system("#{ssh_command} 'docker stop \$(docker ps -q)'")

このコマンドは、実行中のコンテナをすべてシャットダウンして、直前のコンテナを起動できるようにします。

  system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")

このコマンドは、直前の手順で見つかったタグを用いてアプリを起動します。デプロイメソッド(deploy_user)で使われているのと同じ命名ルールを利用できます。

まとめ

本チュートリアルのすべての手順を行うと、ソフトウェアをデプロイする自動ツールが完全に動くようになるはずです。このツールは、アプリを簡単にデプロイできなければならないが、Herokuなどの自動化された環境にホスティングできない場合に便利です。

このツールが有用だとお思いいただけましたら、お気軽に本チュートリアルを共有してください。疑問点などがございましたら、ぜひ元記事にコメントをどうぞ。

皆さまが楽しくリリースできますように。

追伸: Dockerを用いた継続的デリバリー(CD)にご関心がおありでしたら、SemaphoreのDocker platformをぜひチェックしてください。タグ付きのDockerイメージのレイヤキャッシュを完全にサポートしています。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)


Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)

Rails開発者なら誰でも、アセットの読み込みに非常に時間がかかることを痛感しています。アセット読み込みを高速化する便利なコツをご紹介します。

コツ

  • 1つの巨大なファイルに何もかもバンドルしないこと。CDNを用いて、必要なページでだけrequireしましょう(CDNはCKeditorなどのツールでも使われています)。
  • I18n-jsは翻訳を個別のファイルに保持します。必要な言語だけを設定するようにしましょう。さもないと、新しいライブラリを導入するたびに翻訳ファイルがあっという間に肥大化してしまいます。必要な言語だけを設定することでコンパイル済みファイルの肥大化を回避でき、ひいてはコンパイルとページ読み込みを高速化できます。
config.i18n.available_locales = %i(en)
  • therubyracerGemfileから削除しましょう。このgemはメモリを大量に消費するためです。代わりに、最新バージョンのNodeをインストールします。
  • SASSファイルやSCSSファイルでrequirerequire_treerequire_selfを使わないこと。これらはあまりに素朴であり、Sassファイルでうまく動作しません。代わりに、Sassネイティブの@importディレクティブを使いましょう。sass-railsはこのディレクティブをカスタマイズしてRailsプロジェクトの慣習に統合します。
  • SASSファイルやSCSSファイルでの@importディレクティブの使い方には注意が必要です。@import 'compass/css3/flexbox'のように個別にインポートできる状況であれば、@import 'compass';のようにパッケージアセットをまるごとインポートするのは避けましょう。
  • JavaScriptやCoffeeScriptのマニフェストでrequire_tree .を使うのは避けましょう。次のように、アプリの管理(admin)パネルが独自のアセットを持つ場合を考えてみます。
//** assets/javascripts/admin/admin.js

//= require admin/tab.js
//= ...

//** assets/javascripts/application.js

//= require 'something'
//= require_tree . // 悪手: adminのアセットまでrequireされてしまう
  • アセットをコンパイルするときのログをチェックしましょう。デフォルトのロガーをオーバーライドしてDEBUGモードでアクセスしたときの状態を確認するのは簡単です。
# /lib/tasks/assets.rake

require 'sprockets/rails/task'

Sprockets::Rails::Task.new(Rails.application) do |t|
  t.logger = Logger.new(STDOUT)
end

まとめ

上のルールはコンパイル済みアセットを高速化するのに有用です。この概念を実証するcintrzyk/sprockets-tipsをご覧いただければ、Railsのサンプルアプリでより詳細な部分を確認できます。

Rails 5.1以降、アセットの管理方法は多様になりました。私はYarnとWebpackによる管理を強くおすすめします。その有用性については、ぜひ私を信じてください。アセットの依存性管理、効率の高いコンパイルプロセス、ページを更新せずにコードを動的に再読み込みする機能、ES6サポート、PostCSSなど、多くのメリットを得られます。

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

Ruby 3 JITの最新情報: 現状と今後(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

画像は英語記事からの引用です。

Ruby 3 JITの最新情報: 現状と今後(翻訳)

Ruby 3にJITが導入されるというニュースは既にお聞きおよびかと思います。JITはどこからやってくるのでしょうか?いつ導入されるのでしょうか?それでどれだけ高速化されるのでしょうか?JITがあるとデバッグが心配な場合にオフにできるのでしょうか?

そもそもJITって何?

JITは「just-in-time」の略ですが、日常会話の「間に合ったー!」「滑り込みセーフ」の意味でなければ、特に実行時コンパイラ(Just-In-Time Compiler)を指します。現在のRubyはコードを逐次解釈して実行するインタプリタですが、JITを用いると、Rubyプログラムの一部をマシン語に変換して、Unixコマンドやexeファイルのように実行します。特に、JITは私たちが日頃読んでいるRubyコードを、プロセッサに合わせて最も自然かつ高速なコード(しばしば機械語やマシン語(”machine code” or “machine language”)などと呼ばれます)に変換します。

JITは「普通の」コンパイラといくつかの点で異なっています。最大の違いは、プログラム全体をコンパイルするわけではない点です。その代わり、最も頻繁に実行される部分だけをコンパイルし、そのプログラムでの使われ方にうまく合わせて最速で実行できるようにコンパイルします。JITは、プログラムでメソッドがどのように呼ばれているかについて推測する必要はありません。プログラムをしばらく監視して情報をメモし、それからコンパイルを開始します。

うう、JITのせいでこの言い訳が使えなくなってしまいました。今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」がおすすめです。この手もまだ使えます。

「プログラマーが合法的におさぼりする言い訳No.1:『コンパイル中です』」
「おらっ!仕事中だぞ」「コンパイル中でーす」「さよか」
うう、JITのせいでこの言い訳が使えなくなってしまいました。
今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」が当分おすすめです。

JITでどれだけ速くなるか

この世には「嘘」、「ひどい嘘」、そして「ベンチマーク」があります(訳注: 元ネタ)。JITによる高速化を正確な値で表すなど無理な相談です。正確な値など存在しないからです。しかし、特定のプログラムについては、完全に合理的な負荷をかけた状態で50%、150%、あるいは250%までJITで高速化できるケースが多々あります。現実的な負荷の元で、500%以上もの高速化を達成するケースすらいくつかあります。ただし、JITよりインタプリタの方が速いケースも若干あることは言うまでもありません。なぜなら、現実世界には常に最適化されているものなど存在しないからです。

現時点での無難かつシンプルなCRuby向けJIT実装では、およそ30%〜50%のパフォーマンス向上が見られ、測定方法次第では最大150%に達することもあります。30%〜50%はJITにしてはかなり控えめな値ですが、これらのブランチはまだまだシンプルですし、30%〜50%という値は過小評価のしようがありません。これは3年分から10年分の「通常」リリースに相当するスピードアップであり、わずか1〜2年のJITへの取り組みでこれほどの成果を達成したのです。そして通常のスピードアップに加えて、こうした大きなスピードアップは現在も起き続けています。さらにJITは長期に渡って改良を重ねることができます。JITは、昔ながらの「純粋なインタプリタ」のRubyでは到底不可能だった最適化の世界へと大きく扉を開いているのです。それこそが、JITを搭載したRuby実装が既に劇的な高速化の可能性を秘めている理由です。TruffleRubyなどの実装は著しいメモリオーバーヘッドを伴いますが、コードを900%あるいはそれ以上スピードアップできます。この戦略はCRubyには合いませんが、それでも高速化は確かに可能なのです。

私は、「どんだけ速くなる?」という質問にはたいていRails Ruby Benchの結果を添えて回答しています(結局私の作ったgemではありますが)。しかし現時点のMJITは、大規模な並列実行を行うRailsアプリを実行できるほどには安定していません。ご心配なく、そのときが来れば結果を公表いたします。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

CRubyのJITはどこから来たのか

RubyのJITは、しばらく前からある形を取って導入されてきました。JRubyには何年も前からJITがありますし、RubiniusにもしばらくJITがありましたがその後取り除かれました。しかし「純粋な」CRubyにJITが組み込まれたことはかつてありませんでした。その代わりさまざまな実験用ブランチとしてJITが姿を表しましたが、Rubyリリースに取り入れられたことはなかったのです。

Shyouhei Urabeによる”deoptimization”ブランチはかなりよかったのですが、大きな成功には至りませんでした。このJITは非常に純粋かつ非常にシンプルであり、可能な最適化はごくわずかにとどまりましたが、その代わり余分なメモリをほとんど必要としないことが保証されていました。Rubyコアチームはメモリ使用量にとても気を遣っています。

その後、最近になってVladimir MakarovRuby 2.4のハッシュテーブルを再構築したあのMakarovです)が、メモリを食わない強力なJIT実装「MJIT」を作りました。MJITは既存のCコンパイラ(GCCやCLang)をRuby JIT向けに強化します。MakarovがMJITの動作を解説するキーノートスピーチのためにRubyKaigiに招かれたのも、MJITへの期待の大きさゆえです。Makarovは最初にRubyを改造して、スタックベースのVMではなくレジスタベースのVMを用いるようにし、その基礎の上にJITを構築しています。しかしMJITはまだ歴史が浅く、一般的にリリースできるほどには安定していません。どんなRubyプログラムでも動かせる、クラッシュしないRubyを作るのは困難であり、MJITはまだその段階にありません。しかし最近の結果によるとMJITのCPUベンチマークはRuby 2.0.0の230%にも達しているので、大筋では間違っていないことは確かです。

MJITの登場とちょうど同じ頃、Takashi KokubunはEvan Phoenixの初期の成果にヒントを得てLLVMベースの強力なRuby JIT実装「LLRB」を作っていました。LLRBもまた、MJITと同様、Ruby世界を支えるほどには洗練されていませんでしたが、TakashiはMJITから多くの成果を取り入れて開発を続け、YARV-MJITに結実しました。

YARV-MJITは、レジスタベースVMとなるためにMJITの変更点を取り入れました。この変更を反映すれば、Rubyはさらに高速になる代わりに、すべてを安定させるために必要なテストも増やさなければなりません。変更をやめておけば、Ruby JITの機能が少なくなる代わりにリリースを早められます。皆が望んでいるのは、できるだけ小規模な機能かつできるだけ早期のリリースであることを覚えていますか?YARV-MJITはまさにそれを実践する原則です。「JITを単に追加してみてはどうか」「さほど高速にならなくても構わないからJITを追加してみてはどうか」「JITをデフォルトでオフにして、欲しい人だけがこの新しい実験的機能を使うということにしてはどうか」という具合にです。しかしこのJITは、一部の機能をオフにしたMJITと同じものです。

JITはいつ使えるのか

言うまでもなくこれは難しい質問です。どんな問題が見つかり、どれだけ修正が容易かでリリース時期は変わってくるでしょう。

YARV-MJITのissue #1782が現在オープンになっているので、Rubyへの導入は秒読み状態なのかもしれませんが、Ruby 2.5.0のクリスマスリリースに含まれることはありません。そしてそれがベストです。

YARV-MJITもMJITも着々と改良を重ねています。VladはMJITが本当に成熟するには1年ほどかかるだろうと見込んでいます。しかしYARV-MJITによって通常のRubyリリースに取り込まれるJITが完璧な状態である必要はありません。JITを有効にするよう指定すればオンになります。

狭い意味ではいつリリースされてもおかしくありません。しかし(JITが)デフォルトでオンの状態でリリースされるにはおそらく1年、またはそれ以上はかかるでしょう。イミュータブルな文字列のときと同様、Rubyはさらに多くの新機能をオプトインとして取り込んでいます。これはFeature Toggles(Feature FlagsやFeature Flippersとも呼ばれます)に近い方法で、新機能が完全な状態でなくてもインクルードできますが、その場合は新機能同士が衝突しないことの確認が必要です。私はこのアプローチについて、Ruby 1.8から1.9へ移行したときの方法よりずっと好ましいと思います。

JITの導入をいち早く知る方法/JITをオフにする方法

YARV-MJITがいつRubyに導入されるかを知りたい方は、上のプルリク#1782を追いかけることをおすすめします。

JITで何か問題が起きるのではと心配な方は、JITは自由にオンオフできることを覚えておきましょう。RUBYOPT環境変数は、MJITやYARV-MJITを含む含まないにかかわらず任意のCRubyで使えますし、Rubyを実行するたびに(Rubyコード入力中でなくても)コマンドライン引数で渡すこともできます。

現時点では、JITはYARV-MJITでもデフォルトでオフになっています。オンにするには以下のオプションを指定します。

export RUBYOPT="-j"

YARV-MJITでは、JITパラメータを渡さないようにするだけでJITを無効にできます。つまり、-jを最初に渡さなければJITは動きません。

JITを動かすためのオプションは-jの他にもあります。たとえば、-j:wを渡すとJITのwarningがすべて出力され、-j:sを渡すとJITが作成する.cソースファイルを削除せずに/tmpディレクトリに保存します。

JITでもっと遊んでみたい方は、MJITまたはYARV-MJITが有効になっているRubyでruby --helpを実行することをおすすめします。ただしこれらのオプションは、YARV-MJITがRubyに取り込まれる前に変更される可能性があるので、ローカルの(Ruby)バージョンをチェックするべきです。

MJIT options:
  s, save-temps   Save MJIT temporary files in /tmp
  c, cc           C compiler to generate native code (gcc, clang, cl)
  w, warnings     Enable printing MJIT warnings
  d, debug        Enable MJIT debugging (very slow)
  v=num, verbose=num
                  Print MJIT logs of level num or less to stderr
  n=num, num-cache=num
                  Maximum number of JIT codes in a cache

JITを支援する方法/Rubyの次のJITについて

RubyのJITを使ってみたいのであれば、試しに動かしてみるのが簡単かつ手っ取り早いでしょう。

cloneとビルドは以下の方法で行えます。

cd ~/my_src_dir
git clone git@github.com:k0kubun/yarv-mjit.git
cd yarv-mjit
autoconf
./configure
make check

ビルドが終われば、テストやインストールをローカルで行えるようになります。私は以下のrunrubyスクリプトでローカルテストするのが好みです。

cd ~/my_src_dir/yarv-mjit
./tool/runruby.rb ~/my_src_dir/my_ruby_script.rb

rvmを使えば、ローカルでビルドしたRubyインタプリタをマウントできます。

# コンパイルの後で実行すること!
rvm mount ~/my_src_dir/yarv-mjit yarv-mjit
rvm use ext-yarv-mjit

-jでJITがオンになり、-j:wでwarningがオンになることをお忘れなく。コードをYARV-MJITで実行してみた方は、ぜひお知らせください。Twitterでお知らせいただくと助かりますが、他の方法でも構いません。

JITで何か問題が生じたら、小規模な再現手順に切り分けてから、YARV-MJITのRuby bug #14235までお知らせください。よろしくお願いします。

関連記事

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

メモリを意識したRubyプログラミング(翻訳)

Rubyのヒープをビジュアル表示する(翻訳)

Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)

技術的負債を調査する10のポイント(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

技術的負債を調査する10のポイント(翻訳)

6年前に画期的にシンプルなコード品質測定基準を導入して以来、コードの品質を高め、チームの生産性を向上させるのは「明確かつ実行可能な測定基準」であることがわかってきました。次の段階として、プロジェクトを明確に理解する新しい方法を提供するために、コード品質の測定とトラッキング方法に最近改良を加えました。

Code Climateの2つの柱

私たちの新しい評価システムは、「保守のしやすさ」(「技術的負債」と反対の概念)と「テストカバレッジ」という2つの柱の上に構築されています。テストカバレッジの算出方法はシンプルです。テストでカバーされたコード行数を、カバー可能な総行数と比較してパーセントで表現し、AからFまでのランキングを割り当てています。

一方、技術的負債の測定は困難を伴います。静的解析はコードベースにおける構造に潜む問題を検査しますが、ツールが変わると対象となる問題も変わってしまう(しかも重複が多くなる)傾向があります。測定のための単一の標準というものがこれまで存在しなかったので、私たちはそのための標準の作成に取りかかりました。

技術的負債の調査を標準化する目的は次のとおりです。

  • 言語を問わず適用できること: 優れた標準は、Java/Python/JavaScriptなどのさまざま言語に適用したときにも違和感を生じてはいけません。複数言語を用いるシステムは今や当たり前であり、エンジニアや組織が扱う言語の数は現在も増加傾向が続いています。ほとんどの言語は、順次/選択/反復(sequence/selection/iteration)という素朴な概念に基づいていますが、関数型プログラミングやOOPのようにコードを組み立てる別のパラダイムも普及しています。幸いなことに、ほとんどのプログラミング言語はファイル/関数/条件文などの同じようなプリミティブに還元できます。
  • 理解しやすいこと: 技術的負債を静的解析で調査する最終目的は、エンジニアがよりよい決定を下す力を与えることです。したがって、調査の価値は、エンジニアがそのデータをどれだけ簡単に利用できるかに比例します。凝りに凝ったアルゴリズムはいかにも「高精度」な雰囲気を醸し出しますが、コード品質の改善に大きく寄与するのは「シンプルかつ実行可能な測定基準」であることがここ数年でわかってきました。

  • カスタマイズの余地があること: 当然ながら、コードの組み立てや編成方法の好みはエンジニアやチームによって違います。技術的負債の優れた調査方法は、そうした好みをサポートできるよう、フルスクラッチの設定を必要とせずにアルゴリズムを調整できなくてはいけません。アルゴリズムは変えませんが、閾値は調整可能です。

  • DRYであること: 確かな静的解析をチェックすることで、相関の高い結果を得られます。たとえば、関数の循環的複雑度(cyclomatic complexity)は、条件ロジックのネストに強く影響されます。私たちは、ある違反を見つけるたびに別の違反まで同時に見つける傾向のあるチェックシステムを避けるようにしました。開発者にチェックを促すのに必要な問題は、1つあればよいのです。

  • (相反する部分について)バランスが取れていること: 測定をトラッキングして1つの振る舞いだけを推奨すると、期待に反して修正が過剰になってしまうことがあります(「測定のゲーム化」と呼ばれることもあります)。コピペコードの存在をチェックするだけでは、エンジニアはごく単純な構造の繰り返しすら避けようとするあまり、書きたくもない無駄に複雑なコードを書いてしまうことがあります。相反する測定基準(複雑さチェックなど)の組み合わせに注力することで、DRYとシンプルさを両立できるエレガントな解決方法を生み出す機会を増やせます。

技術的負債調査の10のチェックポイント

以上の目的を念頭に置いて、ファイル(またはコードベース全体)の保守のしやすさを調査するための技術的負債チェックを以下の10項目にまとめました。

  1. 引数の個数: 引数の数が多いメソッドや関数
  2. 複雑な論理値ロジック: 理解しにくくなる可能性のある論理値ロジック

  3. ファイルの大きさ: 1つのファイルで超過しているコード行数

  4. 同一のコードブロック: 文法上同一のコード(書式が異なる可能性も含む)の重複

  5. メソッド数: 関数やメソッドが多数定義されているクラス

  6. メソッドの大きさ: 1つの関数やメソッドで超過しているコード行数

  7. 制御フローのネスト: ifcaseなどのネストが深くなっている制御構造

  8. return: return文の数が多い関数やメソッド

  9. 類似のコードブロック: 同一とまではいかないが同じ構造を共有している(変数名は違う可能性)コードの重複

  10. メソッドの複雑さ: 理解しにくい可能性のある関数やメソッド

チェックの種類

10のチェックポイントは4つの主要なカテゴリに分けられます。それぞれについて見ていきましょう。

サイズ

チェックポイントのうち、「6. メソッドの大きさ」「3. ファイルの大きさ」「1. 引数の個数」「5. メソッド数」の4つは、単にコードベース内における単位のサイズや個数をチェックします。これらは静的解析の中でも最も基本的なチェックポイントであり、コードをパースしてAST(抽象構文木)を生成する必要すらありませんが、ほとんどのプログラマーはコードにおけるこれらの項目のサイズから多くの問題を認識できます。1画面に収まりきらないメソッドのリファクタリングはつらい作業になります。

「1. 引数の個数」チェックは他と少し異なり、「data clump(データの引き回し)」や「primitive obsession(基本データ型に囚われる)」を検出する傾向があります。多くの場合、システムに新しい抽象化を導入して、システム全体に引き回されがちな個別のデータをまとめることでこの問題を解決でき、コードの意味もより明確になります。

制御フロー

「8. return文」や「7. 制御フローのネスト」チェックの目的は、サイズは適切だが追いにくくなっている可能性のあるコード片を見つけることです。コンパイラは制御が複雑になっても何も困りませんが、頭の中で制御フローを評価するはめになったメンテナンス担当者は気の毒です。

複雑さ

「2. 複雑な論理値ロジック」チェックは、見直しの必要な組合せ爆発の原因となる、演算子だらけの条件を検出します。「10. メソッドの複雑さ」チェックにもその要素が少し含まれており、サイズ/制御フロー/機能単位の複雑さの情報を組み合わせて認知複雑度アルゴリズムを適用することで、あるコード単位が人間のエンジニアにとってどのぐらい理解が困難になるかを見積もります。

コピペ検出

最後の「4. 同一のコードブロック」「9. 類似のコードブロック」チェックは、悪質なコピペコードを検出します。コードのコピー元はdiffに現れず、貼り付けた部分しかdiffに現れないので、コードレビュー中に人間がチェックするのは困難です。幸いなことに、この種の問題の分析はコンピュータが得意とする分野です。私たちの採用したコピペ検出アルゴリズムは、構文木の構造同士の類似性を調べ、あるコードブロックをコピペして変数名を変えても見つけ出してくれます。

評価システム

あるコードブロック内における技術的負債の違反(問題)をすべて特定したら、結果を整形してできるだけわかりやすく表示します。

ファイル評価

最初に、エンジニアが問題解決に要する総時間を問題ごとに見積もります。私たちはこれを「改良時間(remediation time)」と呼んでいます。それほど厳密な時間ではありませんが、問題を比較したり集約したりするのに役立ちます。

あるソースコードファイルから総改良時間を得られたら、文字で表されるランキングに割り当てます。改良時間が少ないほど好ましいのでランキングが上がります。あるファイルの総改良時間が増加すると、リファクタリングの意欲が削がれるため、その分ランキングが下がります。

改良時間ランキング一覧

リポジトリの評価

最終的に、プロジェクト全体の技術的負債にランキングを付ける評価システムを構築しました。やってみると、当然ながら歴史の長い巨大なコードベースほど、小規模な比較対象に比べて技術的負債の総量が絶対的に多くなることがわかりました。このようにして、コードの総行数(LOC)を元にコードベースから総実装時間(人月単位)を見積もり、技術的負債の総時間を総実装時間で割って「技術的負債比率」を算出します。これはパーセントで表され、少ないほどよい値です。

技術的負債算出の公式

この技術的負債比率は最終的にA〜Fの評価システムに割り当てられ、プロジェクト同士の技術的負債を簡単に比較できるようになりました。もちろん、プロジェクト開始後の早い段階で警告を発することができます。

コードベースの技術的負債を無料で調査

本記事でご紹介した、コードベースでの10の技術的負債調査をお試しになりたい方は、ぜひCode Climateをお試しください。オープンソースプロジェクトは常に無料であり、非公開プロジェクトについても14日の無料お試し期間がございます。5分もあればコードベースの抱える問題を検出できます。Code ClimateではJavaScript、Ruby、PHP、Python、Javaをサポートしています。

既にCode Climateをご利用いただいているお客様については、あと数週間ほどで「保守のしやすさ」と「テストカバレッジ」が新たに表示されるようになります。どうぞご期待ください。今すぐお使いになりたい方はメールにてお問い合わせいただければ喜んでサポートいたします。

関連記事

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

ある少年がナイフを一本もらいました。「これはとてもよく切れるナイフだから」という触れ込みでしたが、いざ少年が肉などを切ろうとしてみるとどうもうまく切れないので、少年は「このナイフ、言うほど大したことないな」と思ってしまいました。しかしナイフをくれた人が「ちょい待ち、ナイフの刃をもう少し傾けて。それからナイフの持ち方がよくないからこう持ってごらん」とアドバイスしたところ、今度は見事ナイフで肉を切ることができました。

オープンソースソフトウェアや人生はちょうどこのナイフのように、正しい使い方を学ばないとうまくいかないことがあります。しかも、ナイフそのものに避けがたい問題があることもあります。さらに、ネットで見つけたナイフの使い方の情報がひどいしろもので、始末の悪いことに特定の状況ではその情報が適切だったりすることもあります。人生が面倒なのは今に始まったことではありませんが、ともあれ本記事ではRails.application.routes.url_helpersというナイフについて書いてみたいと思います。

Railsコントローラのコンテキストの外でURLを生成する状況は非常に多いので、シリアライザやジョブなど、その機能が自動的には使えないような場所でこのモジュールの機能が必要になることも非常によくあります。ネットの情報では、このモジュールに直接アクセスすることを何年もの間気軽に勧めていて、しばらくの間これで何の問題も生じませんでした。

しかし運の悪いことに、最近のバージョンのRailsではこの方法で問題が生じるようになりました(問題を指摘しているGithub issue修正のPRを参照)。これによる問題を明らかにするため、簡単なテスト用Railsアプリをセットアップしてみました。関連するコードを以下に示します。

# routes.rb
Rails.application.routes.draw do
  resources :things do
    collection do
      get :faster
    end
  end
end
# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    things_json = (1..100).map do |i|
      {
        id: i,
        url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end

  def faster
    things_json = (1..100).map do |i|
      {
        id: i,
        url: UrlHelper.instance.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end
end

このindexアクションでは、StackOverflowのアドバイスどおりにコードでモジュールを直接呼び出しています。fasterアクションの方では、このモジュールをincludeするヘルパークラスを使っています。Railsではこのモジュールをincludeして使うことが推奨されているようです。

2つのアプローチの実行結果を並べて見てみましょう。abと単一のRailsインスタンスで小さなテストを実行してみました(不要な出力が大量にあったので省略してあります)。

➜ ab -n 1000 http://127.0.0.1:3000/things

Concurrency Level:      1
Time taken for tests:   18.155 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      4815000 bytes
HTML transferred:       4485000 bytes
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
Time per request:       18.155 [ms] (mean, across all concurrent requests)
Transfer rate:          259.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    15   18   2.2     17      42
Waiting:       15   18   2.2     17      42
Total:         15   18   2.2     18      42
 ➜  ab -n 1000 http://127.0.0.1:3000/things/faster

 Concurrency Level:      1
 Time taken for tests:   8.540 seconds
 Complete requests:      1000
 Failed requests:        0
 Total transferred:      4815000 bytes
 HTML transferred:       4485000 bytes
 Requests per second:    117.09 [#/sec] (mean)
 Time per request:       8.540 [ms] (mean)
 Time per request:       8.540 [ms] (mean, across all concurrent requests)
 Transfer rate:          550.58 [Kbytes/sec] received

 Connection Times (ms)
               min  mean[+/-sd] median   max
 Connect:        0    0   0.1      0       1
 Processing:     7    8   1.3      8      23
 Waiting:        7    8   1.3      8      23
 Total:          7    8   1.3      8      23

最初の例(indexメソッド)ではモジュールを直接呼び出していますが、平均して18msを要し、スループットは55リクエスト/秒です。本番で調子の良いときにはリクエストに5秒かかるのであれば「そんなに悪くないんじゃ?」とお思いかもしれません。しかしモジュールを直接呼び出すのではなく、単にincludeする方はどうでしょうか?こちら(fasterメソッドの方)は平均して8msでスループットは117リクエスト/秒と、最初のアプローチのほぼ倍速になっています。私は名シェフではありませんが、このナイフを正しく持てば適切に肉を切れるのです。

結論: Rails.application.routes.url_helpersを直接呼ばず、このモジュールをクラスにincludeすることで、コードは高速化します。

関連記事

Rails: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

RailsのモデルIDにUUIDを使う(翻訳)

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

@jnchitoさんの以下の記事も合わせてどうぞ。

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

RSpec 3.7登場前のfeature spec は、実物(またはヘッドレス)のブラウザ環境でJavaScriptの絡むアプリのやり取りをフルスタックでテストする手段でした。最近リリースされたRSpec 3.7では、Railsのsystem testを元にしたsystem specが追加されました。Rails 5.1ではActionDispatch::SystemTestCaseが導入され、実際のブラウザでのテストに使えるようになりました。設定済みのCapybaraラッパーが提供され、Railsフレームワークに組み込まれている多くの機能を利用できるようになりました。設定済みのCapybaraのおかげで、従来は正しく設定するためにトリッキーになりがちだった手動設定の手間が大きく軽減されました。feature specの代わりにsystem specを使うメリットは次のとおりです。

  1. テストが終わるとデータベースの変更が自動でロールバックされるので、database_cleaner gemを用いてロールバックを手動で設定する必要がない。
  2. driven_byを使うと、specごとにブラウザを簡単に切り替えられる。
  3. テストが失敗すると即座にスクリーンショットを自動撮影し、ターミナルにもスクリーンショットをインライン表示する。この機能は事前設定済みなので、capybara-screenshot gemなどが不要になる。

上述のメリットに加えて、RSpecチームがRails 5.1以降ではfeature specよりもsystem specを推奨している点も強調しておきたいと思います。

RSpecのsystem specをヘッドレスChrome向けにセットアップする

まずはRailsプロジェクトを新規作成しましょう。いつものように--skip-testオプションを追加して、RailsデフォルトのminitestではなくRSpecを使うようにします。

rails --version
Rails 5.2.0.beta2
rails new rspec-system-specs --skip-test --skip-active-storage

セットアップでGemfilerspec-railsを追加する作業以外で最も面倒なのは、ヘッドレスChromeブラウザのテストに必要なgemを見極めることでしょう。必要なgemのリストを以下に示します。

group :development, :test do
  # Capybara system testingとselenium driverのサポートを追加
  gem 'capybara', '~> 2.16.1'
  gem 'selenium-webdriver', '~> 3.8.0'
  # Chromeでのシステムテスト実行に使うchromedriverを簡易インストール
  gem 'chromedriver-helper', '~> 1.1.0'
  gem 'rspec-rails', '~> 3.7.2'
end

chromedriverがインストールされていることを確認します(訳注: 以下はMacでhomebrewを使う場合です)。

brew install chromedriver
chromedriver --version
#> ChromeDriver 2.34.522932 (4140ab217e1ca1bec0c4b4d1b148f3361eb3a03e)

spec/test_helper.rbspec/rails_helper.rbを生成します。

rails g rspec:install

system specを書く

ここでは単純なhome#indexアクションがroutes.rbのrootとして設定されていて、app/views/home/index.html.erbで以下をレンダリングするとします。

<h1 id="title">Hello World</h1>

最初のsystem specを次のようにspec/system/home_spec.rbに実装できます。

require 'rails_helper'

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello World'
  end
end

ここではヘッドレスChromeを使いたいので、:selenium_chrome_headlessドライバを設定しています。Capybaraでこの他に提供されている登録済みドライバは、:rack_test:selenium:selenium_chromeです。ブラウザの解像度などの高度な設定オプションについてのドキュメントは、driven_byをご覧ください。

ちゃんと動くかどうか確認します。

$ rspec
Puma starting in single mode...
* Version 3.11.0 (ruby 2.5.0-p0), codename: Love Song
* Min threads: 0, max threads: 4
* Environment: test
* Listening on tcp://127.0.0.1:50713
Use Ctrl-C to stop
.

Finished in 1.41 seconds (files took 2.04 seconds to load)
1 example, 0 failures

ドライバをspecごとに設定したくない場合は、spec_helper.rbでデフォルトのグローバル設定を以下のように行なえます。

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

後は必要に応じて個別のspecファイルでデフォルトのドライバ設定を上書きします。

テスト失敗時のブラウザスクリーンショット

system specで特筆したい点は、specが失敗したときにブラウザのスクリーンショットを自動で撮影し、ターミナルにインライン出力してくれる便利な機能があることです。

spec失敗時

この機能はRailsのシステムテストに組み込まれているので、feature specのようにcapybara-screenshotなどによるサポートを手動で設定する必要がありません。

JavaScriptのテスト

ここまではサーバー側でのコンテンツレンダリングのテストだけなので、今度はクライアント側のJavaScriptを追加して、JavaScriptが動くブラウザ(ここではヘッドレスChrome)がsystem specで使えることを示してみましょう。

$ ->
  $('#title').text('Hello Universe')

specのアサーションをexpect(page).to have_content 'Hello Universe'に変更すると、クライアント側でのJavaScript変更のspecテストはこれまで同様パスします。

$ rspec
Finished in 1.99 seconds (files took 2.44 seconds to load)
1 example, 0 failures

データベースの自動ロールバック

上述したように、system specでのデータベース変更は自動的にロールバックされます。ページにデータベース出力を少し追加してテストしてみましょう。

<h1 id="title">Hello World</h1>
<p>
  Planet count: <%= Planet.count %>
</p>

specを変更してレコードをseedし、出力されたレコードのcountのアサーションを行うspec exampleを新たに追加します。

describe 'Homepage' do
  before do
    driven_by :selenium_chrome_headless
    puts 'creating a planet'
    Planet.create!(name: 'Mars')
  end

  it 'shows greeting' do
    visit root_url
    expect(page).to have_content 'Hello Universe'
  end

  it 'shows planet count' do
    visit root_url
    expect(page).to have_content 'Planet count: 1'
  end
end

rspecの結果は次のようになります。

$ rspec --format d
creating a planet
  shows greeting
# spec example追加後のデータベース自動ロールバック
creating a planet
  shows planet count

Finished in 1.52 seconds (files took 1.03 seconds to load)
2 examples, 0 failures

できました!feature specではspec example実行後にデータベースの状態をクリーンアップするためにdatabase_cleaner gemの設定が必要でしたが、これで不要になりました。

本記事のデモに用いた例のソースコードはGitHubに置いてあります。

関連記事

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

ソフトウェアテストでstubを使うコストを考える(翻訳)

Rails: ActiveRecord関連付けのpreload/eager-loadをテストする2つの方法(翻訳)

週刊Railsウォッチ(20180126)Bootstrap 4登場でbootstrap_form gemが対応、PostgreSQLやnginxの設定ファイル生成サービスほか

$
0
0

こんにちは、hachi8833です。ついに東京都のインフルエンザ流行が警報を超えました。剣呑剣呑。

1月最後のRailsウォッチ、いってみましょう。

Rails: 今週の改修

今週も主に最近のcommitから見繕いました。

assert_changesが式の変更を認識するよう修正

# activesupport/lib/active_support/testing/assertions.rb#159
-        if to == UNTRACKED
-          error = "#{expression.inspect} didn't change"
-          error = "#{message}.\n#{error}" if message
-          assert before != after, error
-        else
+        error = "#{expression.inspect} didn't change"
+        error = "#{error}. It was already #{to}" if before == to
+        error = "#{message}.\n#{error}" if message
+        assert before != after, error
+
+        unless to == UNTRACKED

つっつきボイス:assert_changesに評価前の値が渡ってたということか」

nil/falseチェックをtrue/falseチェックに修正

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#86
       def initialize(connection, logger, connection_options, config)
         super(connection, logger, config)

-        @active     = nil
+        @active     = true
         @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
       end
...
       def active?
-        @active != false
+        @active
       end

つっつきボイス: 「これは直したいやつ: SQLite3ってまず使わないけど」「そういえばSQLite3ってもうRailsのデフォルトから外れてる?」「まだデフォルトはSQLite3ですね」

ActionMailer::Base.defaultのprocの引数の個数(アリティ)を1に戻した

# actionmailer/lib/action_mailer/base.rb#901
+      def compute_default(value)
+        return value unless value.is_a?(Proc)
+
+        if value.arity == 1
+          instance_exec(self, &value)
+        else
+          instance_exec(&value)
+        end
+      end

もしかしてarityって、binaryとかunaryとかtrinaryの「-ary」から来てるんでしょうか。とても造語っぽい。


つっつきボイス:Proc#arityって可変長の場合マイナスの値を返すのか!」「でも確かにRubyとしてはここでInteger以外の値を返したくないだろうな」「arityはプログラミング業界の造語っぽいですねー」「私の手元にある英語辞書串刺し検索でも類似のもの出てこないです」

参考: Ruby 2.5.0リファレンスマニュアル Proc#arity
参考: Wikipedia-ja アリティ

has_many :through関連付けでレコードがdestroyされてない状態でカウンタキャッシュを更新しないよう修正

# activerecord/lib/active_record/associations/has_many_through_association.rb#145
           case method
           when :destroy
             if scope.klass.primary_key
-              count = scope.destroy_all.length
+              count = scope.destroy_all.count(&:destroyed?)
             else
               scope.each(&:_run_destroy_callbacks)
               count = scope.delete_all

つっつきボイス: 「ふむう、scope.destroy_all.lengthの返す値がタイミングによって適切でなかったってことなのかな」

参考: ActiveRecord::Relation#destroy_all

JRuby向け: opt = options.dupを追加

# railties/lib/rails/generators/app_base.rb#316
       def convert_database_option_for_jruby
         if defined?(JRUBY_VERSION)
-          case options[:database]
-          when "postgresql" then options[:database].replace "jdbcpostgresql"
-          when "mysql"      then options[:database].replace "jdbcmysql"
-          when "sqlite3"    then options[:database].replace "jdbcsqlite3"
+          opt = options.dup
+          case opt[:database]
+          when "postgresql" then opt[:database] = "jdbcpostgresql"
+          when "mysql"      then opt[:database] = "jdbcmysql"
+          when "sqlite3"    then opt[:database] = "jdbcsqlite3"
           end
+          self.options = opt.freeze

元のPR #31641では、frozen_string_literalを外してどうにかしようとしていたのをy-yagiさんが「外さない方がいい」とdupでやる方法を勧めてこうなったようです。

changes_appliedのパフォーマンス低下を修正

PR #30985 Move Attribute and AttributeSet to ActiveModelで低下したパフォーマンスをkamipoさんが修正しました。

Before:

Warming up --------------------------------------
create_string_columns
                        73.000  i/100ms
Calculating -------------------------------------
create_string_columns
                        722.256  (± 5.8%) i/s -      3.650k in   5.073031s

After:

Warming up --------------------------------------
create_string_columns
                        96.000  i/100ms
Calculating -------------------------------------
create_string_columns
                        950.224  (± 7.7%) i/s -      4.800k in   5.084837s

つっつきボイス: 「変更量多かったので結果を貼ってみました」「変更内容は繰り返し多いですけどね: 無駄な参照を減らして毎回総ナメしなくていいようにするなどして高速化したように見える」

# activerecord/lib/active_record/attribute_methods/dirty.rb#113
       # Alias for +changed+
       def changed_attribute_names_to_save
-        changes_to_save.keys
+        mutations_from_database.changed_attribute_names
       end

HasOneThroughAssociation#build_recordを修正

最新のRails 5.2 (と5.1も)で、HasOneThroughAssociation#build_recordメソッドを使ってHasOneThroughAssociationを手動でビルドできない。
再現用のサンプルアプリでこのメソッドを呼ぶと以下がraiseされる:

NameError: undefined local variable or method `through_association' for #
<ActiveRecord::Associations::HasOneThroughAssociation:0x007fa209d2f7a0>

HasOneThroughAssociationでは、(HasManyThroughAssociationオブジェクトと異なり)#build_recordメソッドでのThroughAssociationミックスインに必要な#through_associationが定義されてないっぽい。
#31762より大意


つっつきボイス: 「ま、HasOneみんなあんまり使ってないけど」「たまに必要ー」

bulk_change_table<< procs#concatに変更

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#375
             if respond_to?(method, true)
               sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
               sql_fragments << sqls
-              non_combinable_operations << procs if procs.present?
+              non_combinable_operations.concat(procs)
             else
               execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
               non_combinable_operations.each(&:call)

つっつきボイス: 「修正は純粋な高速化」「そういえばこのbulk_change_tableってkamipoさんが最近何か追加したんじゃなかったかな?」「そういえばごく最近のウォッチ↓で扱った#31331ですね」

週刊Railsウォッチ(20180112)update_attributeが修正、ぼっち演算子`&.`は`Object#try`より高速、今年のRubyカンファレンス情報ほか

リレーションに渡していたassociation引数を削除

# activerecord/lib/active_record/association_relation.rb#3
module ActiveRecord
   class AssociationRelation < Relation
-    def initialize(klass, table, predicate_builder, association)
-      super(klass, table, predicate_builder)
+    def initialize(klass, association)
+      super(klass)
       @association = association
     end

associationはまず要らないだろうということで削除されました。


つっつきボイス: 「他の引数から辿れるならassociationなくてもいいな」「お、こうやってちゃんとキーワード引数使って修正してる↓: 修正前のは古い書き方」「{}は残ってるけど、superに渡したりするかもだしインターフェイス変えるわけにはいかないか」

# activerecord/lib/active_record/relation.rb#25
-    def initialize(klass, table, predicate_builder, values = {})
+    def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})

Rails

Bootstrap 4がついに正式リリース

先週のウォッチを公開した直後にリリースされました。


getbootstrap.comより


つっつきボイス: 「もうね、どんだけ待たされたのかと」「Bootstrapのトップページが変わってから半年経たずにリリースだから早い方かな」「4のテンプレートもとっくに販売されてますしね」

bootstrap_form gemがBootstrap 4に対応(Awesome Rubyより)

<!-- リポジトリより -->
<%= bootstrap_form_for(@user, layout: :inline) do |f| %>
  <%= f.email_field :email, hide_label: true %>
  <%= f.password_field :password, hide_label: true %>
  <%= f.check_box :remember_me %>
  <%= f.submit %>
<% end %>

つっつきボイス:bootstrap-rubyって人たちがいるのか: 本家Bootstrapのリポジトリとは別なのかな?」「どうやら本家のはtwbsにあるtwbs/bootstrap-rubygemですね: ということはbootstrap-rubyは公式ではないと」「なかなかややこしい」

このgemではDry-rbのDry-auto_injectを使っています。

Ruby: Dry-rb gemシリーズのラインナップと概要

kan: 軽量な認証/承認ライブラリ(Awesome Rubyより)

# リポジトリより
class Post::AdminAbilities
  include Kan::Abilities

  register :read, :edit, :delete { |user, _| user.admin? }
end

class Comments::Abilities
  include Kan::Abilities

  register 'read' { |_, _| true }
  register 'edit' { |user, _| user.admin? }

  register :delete do |user, comment|
    user.id == comment.user_id && comment.created_at < Time.now + TEN_MINUTES
  end
end

つっつきボイス: 「カン、って読むのかな?」「もしかするとCanCanCanのもじりか」「確かにCanCanCanっぽい匂いを感じる気がする」「まあこういうのって一度は作りたくなるんですよ: 既存のものがうまく合わないときとか」「モデルにアタッチするというよりAbilityregisterしていく、どっちかというとコントローラに結合させていく感じですかね」

rack-cache: HTTPキャッシュやバリデーションを行うRackミドルウェア(Awesome Rubyより)

# リポジトリより
# config/application.rb
config.action_dispatch.rack_cache = true
# or
config.middleware.use Rack::Cache,
   verbose:     true,
   metastore:   'file:/var/cache/rack/meta',
   entitystore: 'file:/var/cache/rack/body'

つっつきボイス:nginxの設定ファイルをRubyで書きたい人向けなのかな?」「全部Rubyで書きたい気持ちはわかる: nginxの設定ファイルの記法って結構特殊だし」「そういえば最近はnginxの設定ファイルを自動生成するサービスがありますね↓: 最終的には自分で仕上げないといけないですが」


nginxconfig.ioより

「rack-cacheが欲しい人がいるとすれば、ミドルウェアのないJava Serveletみたいな世界から来た人たちが直接下のレイヤをチューニングしたい、とかかも」

acts_as_votable: 投票機能に特化したActiveRecord向けgem(Awesome Rubyより)

# リポジトリより
class Post < ActiveRecord::Base
  acts_as_votable
end

@post = Post.new(:name => 'my post!')
@post.save

@post.liked_by @user
@post.votes_for.size # => 1

つっつきボイス: 「voteとかvotableって、最初パターンの名前か何かと思ったら違うみたいでした」「文字どおりの機能です: こういうモジュール化は割りと簡単に書けるから皆さんもぜひ一度やってみるといい: 特にメタプロの練習には絶好」「機能がここまで簡素で特化していれば大してバグらないだろうし」「きっと自分のプロジェクトで機能をgemに切り出して、せっかくだから公開したとかそういうノリなんでしょうね」
「そういえばRails 3ぐらいの頃はこういう↓acts asなんちゃらみたいなモジュール名がとても流行ってましたね: 今は流行ってないけど」

【Rails】acts_as_paranoid uniquenessでハマる

Railsでフィールドのキャッシュが無効になる問題をDSLで解決する

# 同記事より
class User < ActiveRecord::Base
  include Touchable
  touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
...
end
class Task < ActiveRecord::Base
  has_one owner, class_name: :User
end

つっつきボイス: 「キャッシュを明示的にクリアしないと残っちゃう的な問題ですね」「キャッシュを頻繁に更新したいことって多いんですか?」「多いですよー: 生成にすごく時間のかかるものとか、リクエストが来てからレンダリングしてたら到底間に合わないんで、こうやって変更時に明示的にtouchするとかしないと」

Paypal PayoutsをRailsで使う(RubyFlowより)

既存のMass Payは今後非推奨になるそうです。


cookieshq.co.ukより


つっつきボイス: 「PayPalのインターフェースって数が多すぎてマジでわからなくなったりするんだよなー」「Mass Payってちょいダサいネーミングな気が」「一括処理らしさは伝わるかな」

Rails + Reduxアプリを作ってみた


hackernoon.comより


つっつきボイス:SPA的なアプリやるならこういう構成でしょうね」

Railsのバックグランドジョブにsucker_punchを使う(RubyFlowより)

sucker_punchはメモリ上に展開するので非常に速い代わりに、ジョブキューが非常に多い場合には不向きだそうです。


つっつきボイス: 「sucker_punch、すごい名前w」「Redisサーバーとかを立てないでやりたいときにいいのかな」「昨年末のRails勉強会@Tokyoで、sucker_punchは最近メンテされてなさげって聞いた気がします」

sucker punch: いきなりのパンチ[殴打]、不意打ち、急襲

plan.io: Redmineのセキュリティチェックサービス


plan.ioより

ファクトリーで注意すべき点(Ruby Weeklyより)

# 同記事より
it "downcases the location on save" do
  user = create(:user, location: "Boston", first_name: "Joël")

  expect(user.location).to eq "boston"
end

つっつきボイス: 「ファクトリーでどこまでやるかというのはある」「ファクトリーって基本はデフォルト値の置き場かなー」「そう思う」「テストデータを一番簡単に作るやつ」「has manyとか増えてくると条件がどんどんややこしくなって結局自分でビルダー作る方が早かったり」

「Railsのfind_byにSQLインジェクションの脆弱性」に異議が唱えられる

** DISPUTED ** SQL injection vulnerability in the ‘find_by’ method in Ruby on Rails 5.1.4 and earlier allows remote attackers to execute arbitrary SQL commands via the ‘name’ parameter. NOTE: The vendor disputes this issue because the documentation states that this method is not intended for use with untrusted input.
nvd.nist.govより

参考: JVNDB-2017-011712

** 未確定 ** 本件は、脆弱性として確定していません。
jvndb.jvn.jpより


つっつきボイス:find_byにインジェクション…だと?」「disputed: 係争中ですね」「そもそもfind_byって信頼できるものしか渡さないでしょ普通」「そういえばSQLインジェクションをいっぱい集めたサイトありますよね↓」「そうそう、この辺の話」

# rails-sqli.orgより
params[:id] = "admin = 't'"
User.find_by params[:id]

巨大なRailsアプリでRubocopをパスするには

$ rubocop --format offences
11010 Metrics/LineLength
3140 Style/StringLiterals
2201 Style/FrozenStringLiteralComment
1891 Style/Documentation
…and thousands of rare offences
1 Style/ZeroLengthPredicate
 —
26154 Total

つっつきボイス: 「あるある、後からRubocopかけるとこういうエグいのが出てくる↑」「そっ閉じするヤツw」「rubocop -aで最近ヒアドキュメントが壊れたりする」「『inherit_from: .rubocop_todo.ymlするな』ってのは正しい: やったら絶望できる」「少しずつ項目を増やすのが吉」「『これだけは許せない』っていうymlだけ作って共有したいです」「プロジェクトや会社によっていろいろ宗教が違うんで大変だけど」

「line lengthのチェックが邪魔で」「自分はline length 200ぐらいあってもいいかな: あんまり長いのは確かに嫌だけど」
「gemのアルファベット順並びチェックされるのも嫌ですよね」「あれは空行入れればブロックが分かれるはず: 空行なしでべた書きすると言われるしそれはわかる」「Rubocopに言われるままの修正だけだと何のアウトプットにもならないのがなー」(以下延々続く)

「JetBrainsのUpsourceみたいな方向でIDEとRubocopが統合されたら幸せになれるかな: ちなみにUpsourceはとっても優秀で、Web画面からinspect definitionとかのJetBrainsのエディタ機能が使えるし、逆にRubyMineとかからレビューコメント付けたりもできる」「へー!」「Upsource、10人まで無料なのか」「GitLabのWeb IDEも似た路線でとてもいいんだけど一番高いエディションが必要」


jetbrains.comより

「お、モーフィアスが」「マトリックス知ってる人がいて何だかほっとしたー」

#yield_selfは思ったよりいいぞ(RubyFlowより)

# 同記事より
# before
url = construct_url
response = Faraday.get(url)
data = JSON.parse(response.body)
id = data.dig('object', 'id') || '<undefined>'
return "server:#{id}"
# or, short yet less readable form:
return "server:" +
  (JSON.parse(Faraday.get(construct_url)).dig('object', 'id') || '<undefined>')

# after:
construct_url
  .yield_self { |url| Faraday.get(url) }
  .yield_self { |response| JSON.parse(response) }
  .dig('object', 'id').yield_self { |id| id || '<undefined>' }
  .yield_self { |id| "server:#{id}" }

# with method()
construct_url
  .yield_self(&Faraday.method(:get))
  .body
  .yield_self(&JSON.method(:parse))
  .dig('object', 'id').yield_self { |id| id || '<undefined>' }
  .yield_self { |id| "server:#{id}" }

「メソッド名はクソだけど」だそうです。

参考: Ruby リファレンスマニュアル Object#yield_self

Rails小粒記事2本

めちゃ短い記事です。著者はいずれも同じ人です。


つっつきボイス: 「TechRachoの朝記事向けに短い記事も押さえておきたかったので」「(3本目)SELF JOINはSQLやってる人なら普通に使うやつ」

Ruby trunkより

C-APIからの不要なexportをなくしてパフォーマンス向上(反映->close)

Ruby 3でprime_divisionを強化&修正

使うにはrequire 'prime'が必要です。

# 14383より
-1.prime_division
 => [[-1,1]]

0.prime_division
Traceback (most recent call last):
        4: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        3: from (irb):85
        2: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/lib/ruby/2.5.0/prime.rb:30:in `prime_division'
        1: from /home/jzakiya/.rvm/rubies/ruby-2.5.0/lib/ruby/2.5.0/prime.rb:203:in `prime_division'
ZeroDivisionError (ZeroDivisionError)

つっつきボイス:prime_divisionって素因数分解か」「おーこんなメソッドあるんだ」「ゼロも整数」

参考: Rubyリファレンスマニュアル Integer#prime_division

forが要素をsplatしない

a = [Struct.new(:to_ary).new([1, 2])]
a.each {|i, j| p [i, j]} #=> [1, 2]
for i, j in a; p [i, j]; end #=> [#<struct to_ary=[1, 2]>, nil]


つっつきボイス: 「おー、forで変数が展開されないってことか」「nobuさんが投げたやつですが今のところ動きなさそう」「forだと動きにぶいでしょうね: そもそもRubocopに直されちゃうw」「直されるべき」

Ruby

Java経験者が知っておきたいRubyの特徴(RubyFlowより)

// Java
public static Object create(Class c, String value) throws Exception
{
  Constructor ctor = c.getConstructor( new Class[] { String.class } );
  return ctor.newInstance( new Object[] { "Hello" } );
}

public static void main (String args[]) throws Exception
{
  Greeting g = (Greeting) create(Greeting.class, "Hello");
  g.show();
}
# Ruby
def create(klass, value)
  klass.new(value)
end

g = create(Greeting, "Hello")
g.show

つっつきボイス: 「うん、これはなかなかいい記事」「Java出身のkazzさんがきっと好きだと思って」「ここでいうmessageってメソッド呼び出しのことなんじゃ?」「messageってSmalltalkの用語じゃん!: Javaじゃなかったのかw」「書いた人が結構いい年の予感」

参考: Wikipedia-ja Smalltalk

Ruby 25周年でメッセージ募集


つっつきボイス: 「『私とRubyの初めての出会い』みたいな感じならみんなだいたい書けるんじゃ?」「自分のときは、学部1〜2年の頃使ったCD-ROMライターPCのFedora Linuxのインストールスクリプトか何かがRubyだったなー: たぶんバージョン0.xぐらいの頃」
「そういえば未踏の一番最初がRubyだったんじゃないかな」「そうなんだ!」

Rubyのメモリ使用量を削減する

Aaron Pattersonさんの記事です。


つっつきボイス: 「ちょうど以下のGC改善gemを見つけたんですが、1年前から動きがないのと、最近のRuby JIT周りの動きが早くて時の流れを感じてしまいました」

Ruby 3 JITの最新情報: 現状と今後(翻訳)

Rubyオブジェクトのアロケーションを追ってみた(Ruby Weeklyより)

Rubyのobjspaceライブラリを使っています。

# 同記事より
require 'objspace'

ObjectSpace.trace_object_allocations do
  obj = Object.new

  puts "File: #{ObjectSpace.allocation_sourcefile(obj)}"
  puts "Line: #{ObjectSpace.allocation_sourceline(obj)}"
end

# File: trace.rb
# Line: 4

つっつきボイス: 「こう書くとアロケーションがこう変わるみたいなのがわかりますね: Stringをfreezeするとこうなるとか」「レイヤは違うけど、ちょうど今日社内勉強会でやった「データマッパー」の話にも通じるものがある: lazyに書かないとこうなるよ、みたいな」

RubyRogueポッドキャスト「Rubyデバッガー」


devchat.tv/ruby-roguesより

ゲストのDaniel Azuma氏はGoogleでRubyとElixirのチームリーダーを務めているそうです。

awesome-awesomeness: まとめ情報のまとめ


つっつきボイス: 「まとめのまとめ」「草不可避」「awesomeなんちゃらという名前のまとめ情報って、GitHubで手っ取り早く★集めたい人が作りまくってますね」「とりあえずRubyの項はなかなかよさげでした」

fir: fishシェル風の対話的Ruby REPL


同記事より

SQL

MySQLをダウンタイム最小でPostgreSQLに移行する(Postgres Weeklyより)

⭐PostgreSQLの設定ファイルをGUIで生成するサービス(Postgres Weeklyより)⭐


pgconfigurator.cybertec.atより


つっつきボイス: 「よくあるやつかなと思ったけど、How would you describe your workload?↓があるのがいい: 結局どういうデータアクセスがあるかが重要なんで」


pgconfigurator.cybertec.atより

今週の⭐を進呈いたします。おめでとうございます。

pg_trgm: ワイルドカード検索を高速化する標準ツール(Postgres Weeklyより)

pg_trgm ignores non-word characters (non-alphanumerics) when extracting trigrams from a string. Each word is considered to have two spaces prefixed and one space suffixed when determining the set of trigrams contained in the string. For example, the set of trigrams in the string “cat” is “ c”, “ ca”, “cat”, and “at ”. The set of trigrams in the string “foo|bar” is “ f”, “ fo”, “foo”, “oo ”, “ b”, “ ba”, “bar”, and “ar ”.


つっつきボイス: 「全文検索とかで使うやつですね」「trigram: 三重文字」「PostgreSQLのこういうExtensionとかプラグインってもう無数にありますね: データベースの研究者がオープンソースであるPostgreSQLにたくさん集まって実装と検証に使ったのが大きいんじゃないかな」
VLDBっていう世界最大のデータベースカンファレンスがあるんですが、プロポーザルがすっごく分厚い」「2017年で第43回!」「Very Large!」「最近追えてないけど、当時はXMLデータベースが熱かった」

Herokuが「Postgres PGX」プランをリリース(Postgres Weeklyより)


blog.heroku.comより

JavaScript

ちょっと便利なスニペット

とても短い記事です。

// 同記事より
const originalObject = { a:1, b: 2, c: 3 };
const shallowObjectClone = {...originalObject};

つっつきボイス: 「記事は短いんですが、コメントやツイートで俺も俺もと盛り上がってます」「大喜利化しとるなー」

await (await fetch('https://api.github.com/users/wesbos')).json();

信頼できないJavaScriptをSaaSとして動かすには


つっつきボイス: 「レイヤで仕切ったりDockerでサンドボックス化したり、この通りにすれば大きな被害はないだろうけど、いったいどれだけ信頼されてないんだそのコードw」「書いた人(´・ω・)カワイソス」

Reduxを使っていいときいけないとき


blog.logrocket.comより


つっつきボイス: 「BPS アプリチームもRedux使ってた気がする」

FirebaseとAngularでAuth0認証する方法

Auth0はシングルサインオンのサービスのようです。

CSS/HTML/フロントエンド

平方マイルをみたいにmileの肩に2を付けて表すには


dev.to/ice_lenorより

formatDistanceでやれるそうです。


つっつきボイス: 「一応Unicodeを探してみましたが、平方マイルの文字はないですね」

WebSubがW3C仕様でRecommendationに

WebSub provides a common mechanism for communication between publishers of any kind of Web content and their subscribers, based on HTTP web hooks. Subscription requests are relayed through hubs, which validate and verify the request. Hubs then distribute new and updated content to subscribers when it becomes available. WebSub was previously known as PubSubHubbub.
w3.orgより

前はPubSubHubbubという名前だったんですね。


つっつきボイス: 「早口言葉すぐる」「しかも最後のbubが小文字とか」「インド人の話す英語みたいな」「名前変えてよかったね~」

Chrome Devtoolsで変更をリロード後も維持できる機能が追加


developers.google.comより


つっつきボイス:babaさんが『いたずらに使えそうw』って」「CSSはともかくJSでどうやってんのかな」

CSSのコメントの書き方ベストプラクティス(Frontend Weeklyより)


webdesign.tutsplus.comより


つっつきボイス: 「うん、CSSスタイルガイドとかこういうベストプラクティスがあるのはいい: 良記事」「お、これはBootstrapのファイル↓」


webdesign.tutsplus.comより

「ところで、BootstrapのCSSを最近読む機会があったんだけど、思ったよりずっとシンプルで読みやすいんですよ: もっと死ぬほどでかいかと思ってた」「へー」「しかもブラウザハック的なコードにはちゃんと解説が書いてあるのがエライ」

CrookedStyleSheets: CSSだけでトラッキングするテクニック集

// 同リポジトリより
@supports (-webkit-appearance:none) and (not (-ms-ime-align:auto)){
    #chrome_detect::after {
        content: url("track.php?action=browser_chrome");
    }
}

つっつきボイス: 「Googleに怒られないかなとふと思って」「まあ昔の携帯Webサイトなんか、こうやるしかなかったですからね」「はーん、vender-prefixで切り替えたりとか」「JS禁止の案件なんかで使えるかもしれないけど、まあお遊びというか芸ですね」

その他

Bashシェルスクリプトをデバッグする

# 同記事より
#!/bin/bash
_DEBUG="on"
function DEBUG()
{
 [ "$_DEBUG" == "on" ] &&  $@
}

DEBUG echo 'Reading files'
for i in *
do
  grep 'something' $i > /dev/null
  [ $? -eq 0 ] && echo "Found in $i file"
done
DEBUG set -x
a=2
b=3
c=$(( $a + $b ))
DEBUG set +x
echo "$a + $b = $c"

brook: Goで書かれたマルチプラットフォームのプロキシ

CLI版とGUI版があり、設定がほとんど不要だそうです。

Kubernetesを2500ノードにスケールアップ(WebOps Weeklyより)


blog.openai.comより

minikube: ローカルで動かせるKubernetes(GitHub Trendingより)


kubernetes/minikubeより


つっつきボイス: 「これは前からあるやつ: kubernetesは複数クラスタが前提なんで、これは設定確認用ですね」

村井純先生の講演

2018年2月19日を過ぎると消されるそうですので、それまでにどうぞ。


つっつきボイス: 「休みの日に聞いてたんですがめちゃめちゃ面白かった: いろいろキワドイことやってたんだなーと」「村井先生はネットの先駆者としていろいろやってきた人なので」(以下延々省略)「あと村井先生がとても美声でした」「大学の先生は話すのも仕事のうちなので声大事ですね」

番外

movfuscator: MOV命令だけを吐き出すCコンパイラ(GitHub Trendingより)


xoreaxeaxeax/movfuscatorより


つっつきボイス: 「よーやる」「MOVはCPUにもろ依存するからプラットフォーム限定だろうけど」

この表記知らなかった


つっつきボイス: 「お、KiとかMiとか普通に使いますよ」

手作り暗号ライブラリは…

インフルエンザワクチンの効果が30%しかなくてもワクチンを接種すべき理由

参考: なぜ30%しか効果がなくてもインフルエンザワクチンは打つべきなのか?

壁に隠れている部分を撮影できる次世代カメラ


つっつきボイス: 「昔ブレードランナーで、写真に写っていない物陰にVR的に回り込んで再生するシーンにびっくりしたのを思い出しました↓」「今やスマホのカメラで指紋取れますからね」

参考: 高画質を追求してきたカメラは今後「見えない物を撮る」方向へと進化していく

ドッグイヤーどころじゃなさそう


つっつきボイス: 「今どきのAI/機械学習は質のいいデータセットもないと進めようがないっすね」

はいです


つっつきボイス: 「ズキズキッ」「はいです頑張ります」「やーホントこのとおり: 早く寝れば早く起きられますよ」


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180119)derailed_benchmarks gem、PostgreSQLをGraphQL API化するPostGraphile、機械学習でモック画像をHTML化ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Random Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Github Trending

160928_1701_Q9dJIU

開発チームを苦しめるマイクロサービス(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

なぜAmazonはマイクロサービスに舵を切ったのか?」も参考にどうぞ。企業のトップが経営判断としてマイクロサービスを強力に推進したことが重要であると思えます。

開発チームを苦しめるマイクロサービス(翻訳)

マイクロサービスは多くのチームで人気を博しています。しかし、ソフトウェア開発のパターンは、このアーキテクチャパターンの周辺で今も流動を繰り返しています。このギャップはここ数年で相当埋められてきましたが、それでも質の低い実装を生産するチームがあまりにも多すぎます。

マイクロサービスは、しばしばモノリシック(一枚板)アプリの対極に位置づけられます。この対比は有用な反面、素朴に過ぎるとも言えます。マイクロサービスというソフトウェア設計は、モジュールやコンポーネントをベースに拡張したものであり、「境界定義(boundary definition)」というアイデアを適用することで明確な「縦割り分割(vertical segmentation)」を目指して拡張されます。

マイクロサービスについて私がこれまで目にした問題点は、境界を正しく定義するという大事な作業に時間を割くチームがほとんど見当たらないということです。ここをしくじると、アプリがモノリス化したときにメンテが困難になります。そうした境界定義を欠いたマイクロサービスシステムは、数十倍始末に負えないしろものになってしまいます。境界が定義されていないマイクロサービスのエコシステムなど、ただの分散モノリスでしかありません。よほど経験豊富なチームでなければこの現実を正しく理解できないでしょう。

コストを織り込む

独立した粒度の高いデプロイが行えるのは(マイクロサービスの)最大のメリットですが、多くのチームはそんなものを必要としていません。このメリットは、分散システムにおける複雑な運用というコストと引き換えに得られるものです。メッセージングプロトコルからイベント/メッセージチャンネル、協調動作やサービス検出に至るまで、分散システムにはさまざまな運用コストが伴います。ごくささやかなエコシステムですら、運用指向の特殊な知識やスキルが求められる複雑なツールを必要とします。

マイクロサービスの別の大きなメリットとして、モジュールの境界を強固にできるという点が挙げられます。しかし、モノリシックアプリで境界を満足に定義することもできないチームがあまりに多すぎます。マイクロサービスと同じぐらい複雑な別のアーキテクチャを使ったところで、この困難は軽減されません。マイクロサービスを採用するチームには、境界を見つけ出し、定義するアーキテクトとしてのスキルが求められます。最初にこのスキルをモノリシックアプリで蓄積する方がよほど楽なのですが、このアプローチに投資しようというチームはほとんど見かけません。

境界が定義されていないマイクロサービスのエコシステムなど、ただの分散モノリスでしかありません。

技術の多様性は、チームの取り組みを盛り上げてくれます。マイクロサービスは多様な技術を導入しやすくしてくれますが、独立したデプロイの場合と同様、そのメリットは「操作が複雑になる」というコストと引き換えに得られるものです。しかも、ガイドがなければチームの管理能力を超えた技術を導入してしまう可能性もあります。

問題を解決する

マイクロサービスは、他のアーキテクチャ的アプローチと同様に、特定の問題だけを解決するものであり、他の技術に対してオープンになっています。どんなアプローチを採用するにしても、プロジェクトやチームのニーズを把握することが重要です。多くの場合、マイクロサービスは出発点としてふさわしくありません。独立したデプロイをチームが本当に必要とし、かつサービスの明確な境界を見定めて維持するスキルがチームに蓄積されるまでは、モノリスを選ぶのがベストです。誤った理由でマイクロサービスを追求し、本当に解決したい問題を特定することもできないチームがあまりに多すぎます。

マップを作成する

どんなチームでも使える方法のひとつは、ドメイン駆動設計(DDD)から拝借した「コンテキストマップ(Context Map)」という概念に基いて作業することです。

コンテキストマップは、システム内に存在するさまざまなBounded Context(コンテキスト境界)を定義します。これは技術であり、実装からは独立しています。この手法は、システムのどこに境界が存在すべきかという洞察をチームが得るのに有用です。また、それらの境界がどのように重複しているか、どのようにやりとりを行う必要があるかを調べるのにも有用です。チームや経営陣がこうした発見を行えば、マイクロサービスに本当の価値があるかどうかを定めるのに役に立つでしょう。


ソフトウェアアーキテクチャに関連して頻繁に引用されるコンウェイの法則(Conway’s Law)は、肝に銘じる価値のある法則です。しかし、組織がシステムに及ぼす影響は一方向だけではありません。チームが取り組んでいるシステムもまた、組織に影響を与えるのです。分散システムに取り組むチームには、部門の縦割りや疎結合を指向する傾向も生じます。こうした傾向を管理できる手腕がなければ、チームが縄張り意識で凝り固まってしまう可能性があります。すなわち、マイクロサービスは技術であると同時に(企業)文化でもあるのです。そうした点を考慮に入れておかなければ、マイクロサービスはチームの助けになるどころか害をもたらすかもしれません。

関連記事

DroidKaigi 2017に行ってきました

[保存版]人間が読んで理解できるデザインパターン解説#1: 作成系(翻訳)

Rubyスタイルガイドを読む: クラスとモジュール(2)クラス設計・アクセサ・ダックタイピングなど


Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: マイグレーションを実行せずにマイグレーションのSQLを表示する(翻訳)

「マイグレーションを実行しないでSQLを取る方法はありますか?」という質問を何度か目にしたことがあります。芸のない回答としては、質問の「マイグレーションを実行しないで」を無視してマイグレーションを実行し、ログファイルをgrepしてSQL出力を取り出し、db:rollbackを実行せよというのが考えられます。しかしこれはズルですし手間もかかります。もっとマシな方法はないものでしょうか。

私の最初のアプローチは、ActiveRecordスタックの相当深いところでメソッド呼び出しをインターセプトし、欲しいマイグレーションの場合は実行せずにSQLを出力するというものでした。私はPostgreSQLを使っているので、ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#executeメソッドをインターセプトしてみたいと思います。以下はプロキシです。

module MyTweak
  def execute(sql, name=nil)
    if caller.detect {|x| x =~ /20171010151334/ } && sql !~ /SHOW TIME ZONE/
      puts sql
    else
      super
    end
  end
end

しかし実際にこれを使ってみるとお世辞にも美しいとは言えませんでした。コンソールでこのコード変更を適用し、マイグレーションを明示的に呼び出さないといけません。

class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
  prepend MyTweak
end
require "#{Rails.root}/db/migrate/20171010151334_add_wing_count_to_jets"
AddWingCountToJets.new.change

サンプルの出力結果をいくつかGistに置きました。しかし見てのとおり、この方法は相当イケてないうえに何というか苦痛です。

StackOverflowのこのスレでもっとよいアプローチをいくつか見つけました。1つ目の回答はalias_methodActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#executeを再定義していますが、私が上のMyTweakでやったことと大差はなく、特定のメソッドをショートさせてSQLを出力しています。しかし本当の改善は、fake_db_migrateというRakeタスクを定義してモンキーパッチを当ててからdb:migrateを実行するという方法でした。これならハードコードも不要ですし、どんなマイグレーションでも動きます。

そのスレの別の回答はもっとうまく動くのですが、少々不安定な感じでもありました。その方法ではSQLの取得にトランザクションとロールバックを使っていました。この方法もコンソールで特定のマイグレーションをrequireしなければなりませんが、コードにパッチを当てるのではなく、マイグレーションをトランザクションで実行して明示的にロールバック例外をraiseしています。

ActiveRecord::Base.connection.transaction do
  AddWingCountToJets.new.migrate :up
  raise ActiveRecord::Rollback
end

このアプローチも、質問の「マイグレーションを実行しないで」を無視していますが、その点はおそらく大丈夫でしょう。やりたいのは、データベースを元の状態のままSQLを取り出すことだからです。SQLを実行してからロールバックすればこの目的を達成できます。

Railsコアプロジェクトで、この機能のためのプルリク#31630がオープンされました。このプルリクのアプローチでは、「dry run」フラグを取り入れたロールバック戦略を用いています。この実装の今後の移り変わりや、ActiveRecordコアに取り入れられるかどうかについては興味を惹かれます。

マイグレーションのArel AST(抽象構文木)を取り出してto_sqlを呼べばいいのになぜそうしないのかとお思いの方もいるかもしれませんが、これはマイグレーションの実際の動作とは異なっています。マイグレーションは、ActiveRecordクエリのように途中でツリーを生成したりプロセスをフルスキャンしたりするのではなく、必要に応じてSQLからビルドされます。たとえば以下は、マイグレーションの非常に便利なメソッドであるchange_column_defaultのPostgreSQLアダプタ版から抜粋したものです。ここから文字列が結合される様子がわかります。

  alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
  if default.nil?
    # DEFAULT NULLの振る舞いはDROP DEFAULTと同じ結果になる。
    # ただしPostgreSQLはデフォルトをカラム型にキャストし、
    # "default NULL::character varying"のようにデフォルトにする
    execute alter_column_query % "DROP DEFAULT"
  else
    execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
  end

ArelはDDL ASTをサポートしませんが、DDL文法が存在しているので、これらがマイグレーションの中間層にまとまっていることは想像できます。しかし、現在のアプローチでこの作業を長年に渡って完了できているので、私にはこの部分で頑張るのがよいとは思えません。

結論としては、ロールバック戦略が明確さにおいてベストではないかと思います。データベースアダプタにモンキーパッチを当てたりしないからです。しかし一回こっきりの雑なハックで構わないのであれば、モンキーパッチで切り抜けるのも悪くないでしょう。これがRailsのコアに取り入れられるかどうか、今後もRailsのchangelogに注目しましょう。

訳注: #31630はマージされずにクローズしました。

関連記事

[Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

私の記事『肥大化したActiveRecordモデルをリファクタリングする7つの方法』に対して、「クラスメソッドでできることをなぜわざわざインスタンスでやるんですか?」という質問をよくいただきました。お答えしましょう。要するに以下が理由です。

私がクラスメソッドよりオブジェクトインスタンスを好む理由は、クラスメソッドはリファクタリングに抵抗するからです

詳しく説明するために、データを外部アナリティクスサービスと同期するバックグラウンドジョブを例に取ることにします。次をご覧ください。

class SyncToAnalyticsService
  ConnectionFailure = Class.new(StandardError)

  def self.perform(data)
    data              = data.symbolize_keys
    account           = Account.find(data[:account_id])
    analytics_client  = Analytics::Client.new(CC.config[:analytics_api_key])

    account_attributes = {
      account_id:         account.id,
      account_name:       account.name,
      account_user_count: account.users.count
    }

    account.users.each do |user|
      analytics_client.create_or_update({
        id:             user.id,
        email:          user.email,
        account_admin:  account.administered_by?(user)
      }.merge(account_attributes))
    end
  rescue SocketError => ex
    raise ConnectionFailure.new(ex.message)
  end
end

このジョブは、ユーザーごとに属性のハッシュをHTTP POSTとして繰り返し送信します。SocketErrorraiseされるとSyncToAnalyticsService::ConnectionFailureでラップし、こちら側のエラートラッキングシステムで正しく分類されるようにします。

このSyncToAnalyticsService.performメソッドはなかなか複雑で、責務をいくつも抱えています。単一責任原則(SRP)はちょうどフラクタルのように、より細かいレベルでアプリ全体/モジュール/クラス/メソッドに渡って適用されると考えることができます。SyncToAnalyticsService.performは、そのメソッドのさまざまな動作が必ずしも同じ抽象化レベルにないため、Composed Methodパターンには該当しません。

訳注: Composed Methodパターンについては以下もどうぞ。
「文章のように読めるメソッドを作る」Composed Method パターン

解決法のひとつは、Extract Methodパターンを何回か適用してメソッドを切り出すことです。結果は以下のような感じになります。

class SyncToAnalyticsService
  ConnectionFailure = Class.new(StandardError)

  def self.perform(data)
    data                = data.symbolize_keys
    account             = Account.find(data[:account_id])
    analytics_client    = Analytics::Client.new(CC.config[:analytics_api_key])

    sync_users(analytics_client, account)
  end

  def self.sync_users(analytics_client, account)
    account_attributes  = account_attributes(account)

    account.users.each do |user|
      sync_user(analytics_client, account_attributes, user)
    end
  end

  def self.sync_user(analytics_client, account_attributes, user)
    create_or_update_user(analytics_client, account_attributes, user)
  rescue SocketError => ex
    raise ConnectionFailure.new(ex.message)
  end

  def self.create_or_update_user(analytics_client, account_attributes, user)
    attributes = user_attributes(user, account).merge(account_attributes)
    analytics_client.create_or_update(attributes)
  end

  def self.user_attributes(user, account)
    {
      id:             user.id,
      email:          user.email,
      account_admin:  account.administered_by?(user)
    }
  end

  def self.account_attributes(account)
    {
      account_id:         account.id,
      account_name:       account.name,
      account_user_count: account.users.count
    }
  end
end

元のコードと比べれば少しはマシになりましたが、どうも今ひとつです。これではオブジェクト指向にならず、手続き型プログラミングと関数型プログラミングの不気味な折衷案がオブジェクトベースの世界で立ち往生しているような感じです。さらに、切り出したメソッドはどれもクラスレベルなので、private宣言も簡単にはできそうにありません(おそらくclass << selfのような見苦しい方法に切り替えざるを得ないでしょう)。

私だったら、SyncToAnalyticsServiceの元の実装を目にしたとしてもこんな形でリファクタリングを完了する気にはとてもならないでしょう。代わりに、次のようにリファクタリングを始めるでしょう。

class SyncToAnalyticsService
  ConnectionFailure = Class.new(StandardError)

  def self.perform(data)
    new(data).perform
  end

  def initialize(data)
    @data = data.symbolize_keys
  end

  def perform
    account           = Account.find(@data[:account_id])
    analytics_client  = Analytics::Client.new(CC.config[:analytics_api_key])

    account_attributes = {
      account_id:         account.id,
      account_name:       account.name,
      account_user_count: account.users.count
    }

    account.users.each do |user|
      analytics_client.create_or_update({
        id:             user.id,
        email:          user.email,
        account_admin:  account.administered_by?(user)
      }.merge(account_attributes))
    end
  rescue SocketError => ex
    raise ConnectionFailure.new(ex.message)
  end
end

元のコードとほとんど変わらないように見えますが、今度は機能をクラスメソッドではなくインスタンスメソッドにしている点が異なります。ここに再びExtract Methodパターンを適用すると次のような感じになります。

class SyncToAnalyticsService
  ConnectionFailure = Class.new(StandardError)

  def self.perform(data)
    new(data).perform
  end

  def initialize(data)
    @data = data.symbolize_keys
  end

  def perform
    account.users.each do |user|
      create_or_update_user(user)
    end
  rescue SocketError => ex
    raise ConnectionFailure.new(ex.message)
  end

private

  def create_or_update_user(user)
    attributes = user_attributes(user).merge(account_attributes)
    analytics_client.create_or_update(attributes)
  end

  def user_attributes(user)
    {
      id:             user.id,
      email:          user.email,
      account_admin:  account.administered_by?(user)
    }
  end

  def account_attributes
    @account_attributes ||= {
      account_id:         account.id,
      account_name:       account.name,
      account_user_count: account.users.count
    }
  end

  def analytics_client
    @analytics_client ||= Analytics::Client.new(CC.config[:analytics_api_key])
  end

  def account
    @account ||= Account.find(@data[:account_id])
  end
end

操作を完了するために直接変数(immediate variable)を引き回さなければならないクラスメソッドを追加するのではなく、結果をメモ化する#account_attributesのようなメソッドを追加しました。これは私のお気に入りの手法です。メソッドを分割するときに、メモ化したアクセサとして直接変数を切り出す方法は、私の大好きなリファクタリングです。このクラスは開始時にいかなるステートも持ちませんが、分割されているおかげでそこに何か追加するのも簡単でした

今度の結果は私にとってずっと明確になりました。こういうリファクタリングは完全勝利の気分になれます。ステートとロジックが1つのオブジェクトにきっちりカプセル化されていますし、(与えられた)オブジェクトの作成が操作の呼び出し(のタイミング)と分離されているので、テストも簡単です。しかも、AccountAnalytics::Clientといった変数をどこにも引き回していません。

さらに、このロジックを用いるどのコード片も(グローバルな)クラス名と結合していません。これらを新しいクラスで差し替えるのは大変ですが、新しいインスタンスでなら簡単に差し替えられます。このおかげで、追加の動作をコンポジションで組み立てやすくなります。変更のたびにクラスを再オープンして拡張する必要はありません。

リファクタリングメモ: 私なら、上の最終的なソースの状態でクラスの実装をやめておくでしょう。しかしロジックがさらに複雑になった場合、このジョブは単一ユーザーを同期するための別のクラスを欲しがるでしょう。

さて、このことは本記事で最初に述べた前提とどんな関係があるのでしょうか?クラスメソッドを分割するとコードが見苦しくなるので、私がクラスメソッドをリファクタリングする機会はあまりないでしょう。最初からインスタンス形式で書いておけばリファクタリングの選択肢も明確になりますし、必要な対策を取るときの抵抗も減らせます。この効果は私自身のコーディングでも何度となく体験していますし、ここ数年のさまざまなRubyチームを外から眺めていてもやはりそうです。

想定反論

YAGNIではないか?

YAGNI: You Aren’t Going To Need It(後で必要になるかもしれないという理由でやるべきではない)

YAGNIが重要な法則であることはもちろんですが、ここで適用するのは筋違いです。これらのクラスをエディタで開いてみれば、そうでないクラスと比べて複雑さはさほど変わりません。「このオブジェクトはYAGNIだ」という指摘は、「タブ文字1つではなくスペース2文字にするのはYAGNIだ」という指摘と大して変わりません。違いはスタイル上のものでしかありません。オブジェクト指向設計にYAGNIを適用する意味があるのは、わかりやすさに違いが生じる場合だけです(使うクラスが1つなのか2つなのか、など)。

オブジェクトが1つ余分になる

インスタンス形式だとオブジェクトが1つ余分に作成されるという根拠で反対する人もいます。オブジェクトが作成されることについてはそのとおりですが、実用上は何の影響もありません。Railsのリクエストやバックグラウンドジョブではおびただしい数のRubyオブジェクトが作成されます。オブジェクト作成を最適化してRubyのガベージコレクタの負担を軽減するのは正統な手法ですが、それが意味を持つ場合に行うべきです。そしてそれは測定によってのみ確認できます。そのインスタンスの変種がたったひとつの追加オブジェクトを作成しただけでシステムのパフォーマンスに大きく影響するとは考えられません(データ付きの反例をお持ちの方がいたらぜひ拝見したいと思います)。

呼び出しが面倒

最後の想定反論は、クラスメソッドの方が入力文字数が少なくて済むというものです。

Job.perform(args)
#  どっちがいいか?
Job.new(args).perform

入力文字数が少ないのはそのとおりです。私なら、オブジェクトをビルドする手頃なクラスメソッドを1つこしらえ、それに委譲して済ませるでしょう。実際、これは私が認める数少ないクラスメソッドの使い方の1つです。こうやって自分で作って自分でおいしくいただく分には構いません。

まとめ

常にオブジェクトのインスタンスから出発しましょう。ステートや複数のメソッドが当分使われないとしても、そうすべきです。いずれ変更のときが来れば、あなたや同僚がリファクタリングしたくなります。コードが今後も決して変更されないのであれば、クラスメソッド方式かインスタンスかという違いは無視して構わない性質のものであり、今後そのコードが改悪されることもまずありません。

私がコードでクラスメソッドを使って幸せになれたケースはほぼ皆無です。あるとすれば、インスタンスの初期化手順をラップして一発で呼び出せるお便利メソッドか、連携する他のオブジェクトからオブジェクトをより簡単に構成できる徹底的にシンプルなファクトリーメソッド(多くとも2行以内)ぐらいです。

皆さまはどう思われますか?どちらがお好みですか?そしてその理由は?私が見落としたメリットやデメリットはありますでしょうか?ぜひコメント欄でお知らせください。

追伸: こうした問題に興味をお持ちで、他の記事を読んでみたい方は、元記事末尾のフォームでCodeClimateニュースレターをぜひご購読ください。Rubyに特化したリファクタリングやオブジェクト設計に関する話題を月に一度メール配信いたします。

詳しく知りたい方へ

本記事をレビューしてくれたDoug Cole、Don Morrison、Josh Susserに感謝いたします。

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails: モデルのクエリをカプセル化する2つの方法(翻訳)

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)

RubyでインラインCコードを書いてメモリ共有&爆速化してみた(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。
楽しい画像はすべて英語記事からの引用です。

RubyでインラインCコードを書いてメモリ共有&爆速化してみた(翻訳)

はじめに

「科学の名において無茶する」、今回のエピソードではRubyでC拡張を書く方法を見てみることにします。さほど害はなさそうなので、Rubyプロセス間でメモリを共有して相互通信するところまでやってみます。もちろんプロセスの分離、forkの分離など何でもござれです。

「なぜそんなことを?どうしてどうして?」と言われそうですね。もちろん『科学のために:コノバケモノメ』。

For science, you monster
訳注: ゲーム『Portal 2』の有名なセリフだそうです。

免責条項: 本記事では、今後変更される可能性のあるAPIに基づいたコードを多用していますので、productionでは使わないでください、絶対に。絶対に絶対にですよ。何が起きてもおかしくありません。かわいい子犬を死なせたくはないでしょう。確かに警告しましたからね。

はじめの一歩: 例から始めよう

Rubyコード内でCのコードを使う場合、FFIRiceなどさまざまな方法が考えられますが、中でも最もシンプルなのは今回のネタに仕込んだrubyinlineです。

rubyinlineは普通のgemと同様gem install rubyinlineでインストールできます。それでは早速、Rubyコード内で2+2をC言語で書いてみましょう。

require 'inline'
class CHello
  inline do |builder|
    builder.include '<stdio.h>'
    builder.c 'int sumThem() {
      return 2 + 2;
    }'
  end
end

このシンプルなプログラムを解剖してみましょう。ただし着目するのは本物の「CMeat」の部分です(まったく某ステーキハウスもうまい名前を付けたものです)。ここではすべてのCコードを.rbファイル内で定義しており、inlineという名前のブロック内に配置しています。Cコードは、builder.include呼び出し(このコードは実際にはここでは不要ですが、完全な形式をお見せするためにあえて書いています)と、関数などのコード片を含むbuilder.c呼び出しで構成されます。

さて、その結果は?

>> CHello.new.sumThem #=> 4

やった!動きました。次はパラメータを渡してみましょう。

require 'inline'
class CHello
  inline do |builder|
    builder.include '<stdio.h>'
    builder.c 'int sumThem(int a, int b) {
      return a + b;
    }'
  end
end

今度はどうでしょうか。

CHello.new.sumThem(100, 200) #=> 300

やりました!動いています。素晴らしい、もうRubyなんか二度と使う気になれませんね。しかしちょっと待った、もっとでかい数値を食わせたらどうなるでしょうか。私も答えを知らない「220と230」で試してみると…

CHello.new.sumThem(2**30, 2**30) #=> -2147483648

おっと、C言語でお馴染みのintegerオーバーフローです。アリスならぬRubyワンダーランドをさまようときは、こういう落とし穴を1つも見落とさないようにしないといけませんね。

C言語で書きたい理由って?

この一言に尽きます。

Speed

RubyとC言語のそれぞれで、1からNまでの数値をすべてカウントする同等のコードを書いてベンチマークしてみましょう。

Nには任意のlong(ただし231-1より小さいこと)の数値を設定します(216など)。ベンチマークの秘密兵器にはbenchmark-ipsを使いました。

require 'inline'
require 'benchmark/ips'
class Counter
  inline do |builder|
    builder.include '<stdio.h>'
    builder.c 'int cCount(int n) {
      int counter = 0;
      for(int i = 0; i < n; i++) {
        counter += i;
      }
      return counter;
    }'
  end

  def rCount(n)
    counter = 0
    i = 0
    while i < n
      counter += i
      i += 1
    end
    counter
  end
end

tested_class = Counter.new

Benchmark.ips do |x|
  x.report("C Method") { tested_class.cCount(2**16) }
  x.report("Ruby Method") { tested_class.rCount(2**16) }
  x.compare!
end

結果は、ぶっ飛びのぶっちぎりの大爆速です。

$ ruby sumthem.rb
Warming up --------------------------------------
            C Method   215.194k i/100ms
         Ruby Method    45.000  i/100ms
Calculating -------------------------------------
            C Method      6.306M (±10.3%) i/s -     31.203M in   5.003825s
         Ruby Method    473.080  (± 5.3%) i/s -      2.385k in   5.055964s

Comparison:
            C Method:  6305730.9 i/s
         Ruby Method:      473.1 i/s - 13329.09x  slower

ぶ・っ・飛・び

そしてこれは、パフォーマンスが重要な数値計算をRubyで行うべきでない理由でもあります。もちろんRubyではintegerオーバーフローを気にしないで済む点や、必要に応じてさらに巨大なデータ型に変換できるといった点が便利なのは間違いありません。

神に見捨てられたまいし場所

ここからが本当の修羅です。いたいけな子どもたちが泣き叫び、シスアドが悲鳴を上げて逃げ惑う修羅の地、そう、メモリ共有です。

そのために、まずはCのツールがいくつか必要です。典型的なUnixシステム(POSIX標準)でプロセス間メモリ共有を行う方法はいくつかあります。ここではshmgetshmatによるアプローチを採用しました。これは最もシンプルかつ今回のユースケースにぴったりでした。

最初に、共有メモリセグメントに名前を付ける方法が必要です。ここでは123というキーを使います。これをshmget呼び出しに渡す方法が必要なので、呼び出しはshmget(key, 1024, 0644 | IPC_CREAT)のような感じになります。最初の引数は共有メモリセグメントの一意の識別子、2つ目の引数はその長さです(もちろん私は1 KBにしましたけどね)。3つ目の引数はアクセス制御の指定です。詳しくはIPC:Shared Memoryをご覧ください。

shmgetから受け取るのは別の識別子です。今回はこれをshmatに渡す必要があります。shmatは、プロセスの指定の(または新規の)アドレスに共有メモリをアタッチします。最後の呼び出しは常にshmdtでなければなりません(メモリをデタッチする)。

それではコードを書いてみましょう。覚悟は完了ですか?

require 'inline'
class Sharer
  inline do |builder|
    builder.include '<sys/types.h>'
    builder.include '<sys/ipc.h>'
    builder.include '<sys/shm.h>'
    builder.c 'int getShmid(int key) {
      return shmget(key, 1024, 0644 | IPC_CREAT);
    }'
    builder.c 'int getMem(int id) {
      return shmat(id, NULL, 0);
    }'
    builder.c 'int removeMem(long id) {
      return shmdt(id);
    }'
  end
end

(型変換のせいで警告が少々表示されますが、気にする必要はありません)。

これで共有メモリの作成とアタッチの準備は整いました。ん、後は何が必要でしたっけ?

Johnny Fiddle::Pointerでございまーす!

Hereeeeeee's Johnny

訳注: このネタ画像については週刊Railsウォッチ(20180112)をどうぞ。

Rubyツールボックスに潜むFiddleはほとんど知られていませんが、非常に鋭利なナイフです。なぜナイフなのかというと、非常に鋭く尖り、切れ味がシャープで、自分をぶっ刺すこともできてしまうからです。でもこういうのが楽しいんですよね!

Fiddleは、CポインタのレベルでRubyとやりとりできる品の良いライブラリです。これこそ今回のユースケースです。何だかゾクゾクしてきました。

ついでに申し上げると、Fiddle::Pointerを使うとRubyオブジェクトをunfreezeすることだってできてしまいます。もちろんとんでもないことですが、このダーティハックをご覧に入れましょう。

str = 'water'.freeze
str.frozen? # true

# Rubyオブジェクトを指すこのCポインタは
# object_idを1ビット左にシフトした(値を倍にした)ものと等しい
memory_address = str.object_id << 1

Fiddle::Pointer.new(memory_address)[1] &= ~8
str.frozen? # false

このコードが動作する理由がおわかりでしょうか。これは、Rubyオブジェクトのfrozenフラグを保持するメモリビットを反転しているのです。良い子の皆さんは真似しないでくださいね。

このメモリ変更は、forkのスコープで使うことにします(この方法はコードで示すときにいちばん楽なので)。このコードは、forkブロックから別のRubyプロセスに移動してもそのまま動作します。

basic_id = Sharer.new.getShmid(123)
address = Sharer.new.getMem(basic_id)
pointer = Fiddle::Pointer.new(address, 1024)

このpointerには、真新しい共有メモリへの参照が含まれています。最初のバイトに何か値を設定してみましょう。私は4が好きなので、4を設定してみます。

pointer[0] = 4

ここからがマジックです。このプロセスをforkし、次にこのセグメントをアタッチしてメモリ空間をforkし(元のプロセスを使えない理由がおわかりでしょうか?)、pointer[0]の値を変更して、元の値が変更されたかどうかをチェックしてみましょう。

pid = fork do
  basic_id = Sharer.new.getShmid(123)
  address = Sharer.new.getMem(basic_id)
  pointer = Fiddle::Pointer.new(address, 1024)

  pointer[0] = 44

  Sharer.new.removeMem(address)
end
Process.wait(pid)
puts pointer[0]

さて、結果は…?

puts pointer[0] # => 44

やりました!ついにforkで双方向にやりとりを行えるようになりました。

Bravo

オブジェクトを渡せるか?

今度は本記事で最もトリッキーな部分です。あるforkしたプロセスのRubyオブジェクトをfork元のプロセスに渡すことができるでしょうか?答えは「できるものとできないものがあります」。単純なオブジェクトなら渡せますが、たとえばハッシュのようなオブジェクトは、内部に参照を多数抱えています(keyやvalueなど)。つまり、こうしたオブジェクトを渡すにはかなりのハックが必要です。しかしシンブルな小さいオブジェクトなら可能です。

ここでご注意いただきたいのは、新しく作成したオブジェクトのメモリを設定できなかったことです。しかしFiddleを使って、あるセグメントから別のセグメントへメモリ全体をコピーすることはできます。そこで、新しいオブジェクトを1つ作成してそのポインタを取得し、そのオブジェクトのメモリ全体を共有メモリにコピーすることにします。

先のコードのforkの部分を変更しましょう。

pid = fork do
  basic_id = Sharer.new.getShmid(123)
  address = Sharer.new.getMem(basic_id)
  pointer = Fiddle::Pointer.new(address, 13) # 13 bytes is enough, tested

  obj = Object.new
  second_pointer = Fiddle::Pointer.new(obj.object_id << 1)
  pointer[0, 13] = second_pointer

  Sharer.new.removeMem(address)
end

Process.wait(pid)
puts pointer.to_value

Sharer.new.removeMem(address)

さてその結果は…

puts pointer.to_value # => #<Object:0x0000010d2c5000>

うまくいきました!

これって何か使い道あるの?

私が思いつける便利なアプリは1つしかありません。任意のRubyオブジェクトを共有メモリに書き出して別のプロセスからこのメモリにアクセスできるとしたら、何らかのCプログラムを使って共有メモリの中を素早く覗き込み、注意深く選んだオブジェクトをRubyからダンプできるでしょう。

そのようなCコードの例を見てみましょう。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>

int main(void) {
  int basicId = shmget(123, 1024, 0644 | IPC_CREAT);
  char *address = shmat(basicId, 0, 0);
  getchar();
  char *s;
  int i;
  for (s = address+15, i = 0; i < 14; i++) {
  // 典型的な状況では次のように書くのが普通
  // (s = address, i = 0; i < 1024; i++)
  // ただしここではフラグなどを使わずに
  // 文字列の内容だけを表示するようパラメータを調整してある
  // これは別記事向きのトピックですね;)
    putchar(*s);
    s++;
  }
  putchar('\n');

  shmdt(address);
}

簡単のために、ある文字列を共有メモリに書き出すだけのコードを書いてみましょう。

basic_id = Sharer.new.getShmid(123)
address = Sharer.new.getMem(basic_id)
pointer = Fiddle::Pointer.new(address, 1024)

string = "Hello Rebased"
second_pointer = Fiddle::Pointer.new(string.object_id << 1)
pointer[0, 1024] = second_pointer

gets

Sharer.new.removeMem(address)

Cコードをコンパイルします。

gcc --std=c11 dump_me.c -o dump_me.o

それではRubyコードを実行してdumpプログラムを実行してみましょう。「見るがよい!」

$ ./dump_me.o

Hello Rebased

できました。

まとめ

本記事では、RubyコードにCコードをうまく埋め込む方法を見い出し、これが高速化に役立つことを発見しました。そして何よりも、禁断の地である共有メモリを攻略できました。

共有メモリの完全なコード例

require 'inline'
require 'fiddle'

class Sharer
  inline do |builder|
    builder.include '<sys/types.h>'
    builder.include '<sys/ipc.h>'
    builder.include '<sys/shm.h>'
    builder.c 'int getShmid(int key) {
      return shmget(key, 1024, 0644 | IPC_CREAT);
    }'
    builder.c 'int getMem(int id) {
      return shmat(id, NULL, 0);
    }'
    builder.c 'int removeMem(long id) {
      return shmdt(id);
    }'
  end
end

basic_id = Sharer.new.getShmid(123)
address = Sharer.new.getMem(basic_id)
pointer = Fiddle::Pointer.new(address, 1024)

pid = fork do
  basic_id = Sharer.new.getShmid(123)
  address = Sharer.new.getMem(basic_id)
  pointer = Fiddle::Pointer.new(address, 13) # 13 bytes is enough, tested

  obj = Object.new
  second_pointer = Fiddle::Pointer.new(obj.object_id << 1)

  pointer[0, 13] = second_pointer

  Sharer.new.removeMem(address)
end

Process.wait(pid)
puts pointer.to_value

Sharer.new.removeMem(address)

関連記事

メモリを意識したRubyプログラミング(翻訳)

Ruby 3 JITの最新情報: 現状と今後(翻訳)

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

データベースのランダム読み出しは要注意(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

データベースのランダム読み出しは要注意(翻訳)

ORM(Object-Relation Mapper、またはObject-Relational Mapper)を初めて使ったとき、「ORMにrandom()メソッドがないのはどうしてなんだろうか?」と不思議に思ったものでした。こんなメソッドなら楽勝で追加できそうなものです。データベースのレコードをランダムに取り出したい理由はいろいろ考えられますが、順序をランダムにしたいレコード数がよほど少ないときでもない限り、SQLのORDER BY RANDOM()でやるべきではありません。本記事では、一見シンプルなSQL演算子によってどれほどパフォーマンスが低下するか、およびいくつかの修正方法について調査したいと思います。

ご存じの方もいらっしゃるかと思いますが、私はオープンソースを支援するのに最適なCodeTriageというサイトを運営しており、このサイトのデータベースパフォーマンス改善についていくつか記事を書きました(訳注: 以下はいずれも仮の日本語タイトルです)。

最近私はheroku pg:outliersコマンドを実行して最適化後の様子を調べていたところ、RANDOM()を含むたった2つのクエリがデータベース時間の32%を占めていたことに気づいて驚きました。

$ heroku pg:outliers
14:52:35.890252 | 19.9%          | 186,846    | 02:38:39.448613 | SELECT  "repos".* FROM "repos" WHERE (repos.id not in (?,?)) ORDER BY random() LIMIT $1
08:59:35.017667 | 12.1%          | 2,532,339  | 00:01:13.506894 | SELECT  "users".* FROM "users" WHERE ("users"."github_access_token" IS NOT NULL) ORDER BY RANDOM() LIMIT $1

これほど遅くなった原因を理解するために、最初のクエリをちょっと見てみましょう。

SELECT
  "repos".*
FROM "repos"
WHERE
  (repos.id not in (?,?))
ORDER BY
  random()
LIMIT $1

このクエリは週に一度実行され、オープンソースのリポジトリにアカウントを持っているがまだサイトに登録していないユーザーに、サイトに登録してissueを「トリアージ」するようユーザーに勧めます。ユーザーに送信されるメールには、ランダムなリポジトリを含む3つのおすすめリポジトリが含まれています。最終的にランダムな結果を得られたのですからRANDOM()の使い方としてはうまくいっているように思えますが、どこがまずいのでしょうか?

PostgreSQLへのリクエストORDER BY random() LIMIT 1ではレコードを1件しか要求していませんが、このクエリが行っているのはそれだけではありません。その1件を返す前にすべてのレコードを並べ替えているのです。

このクエリはArray#sampleのようなものだろうと考える人もいるかもしれませんが、実際にやっているのはArray#shuffle.firstです。私がこのコードを書いたときは、データベースに登録されているリポジトリがほんの数件どまりだったので、かなり高速でした。しかし今やリポジトリ数は2761件に増加しています。そのため、このクエリを実行するたびにデータベースはリポジトリごとに多数の行を読み込み、CPUパワーをリポジトリの並べ替えに使わなければなりません。

もうひとつのクエリでは、同じことがusersテーブルで起きているのがわかります。

=> EXPLAIN ANALYZE SELECT  "users".* FROM "users" \n
WHERE ("users"."github_access_token" IS NOT NULL) \n
ORDER BY RANDOM() LIMIT 1;

                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1471.00..1471.01 rows=1 width=2098) (actual time=12.747..12.748 rows=1 loops=1)
   ->  Sort  (cost=1471.00..1475.24 rows=8464 width=2098) (actual time=12.745..12.745 rows=1 loops=1)
         Sort Key: (random())
         Sort Method: top-N heapsort  Memory: 26kB
         ->  Seq Scan on users  (cost=0.00..1462.54 rows=8464 width=2098) (actual time=0.013..7.327 rows=8726 loops=1)
               Filter: (github_access_token IS NOT NULL)
               Rows Removed by Filter: 13510
 Total runtime: 12.811 ms
(8 rows)

比較的小さなクエリを1つ実行するたびに13ms近くかかっています。

ではRANDOM()が犯人だとしたら、どう修正すればよいのでしょうか?これは驚くほど難しい質問です。アプリやデータへのアクセス方法によって大きく異なります。

私の場合、ランダムなIDを生成し、それを使ってコードを取り出すことで問題を修正しました。このインスタンスのIDは比較的連続していることがわかっていたので、@@max_idで最も大きなIDを取り出し、1からそのIDの間のランダムな数値を1つ得てから、IDが>= idになるレコードを取り出すクエリを実行するようにしました。

これで速くなったでしょうか?もちろん。以下は上のクエリをRANDOM()に置き換えた同じクエリです。

=> EXPLAIN ANALYZE SELECT  "users".* FROM "users"\n
WHERE ("users"."github_access_token" IS NOT NULL) \n
AND id >= 55 LIMIT 1;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.00..0.17 rows=1 width=2098) (actual time=0.009..0.009 rows=1 loops=1)
   ->  Seq Scan on users  (cost=0.00..1469.36 rows=8459 width=2098) (actual time=0.009..0.009 rows=1 loops=1)
         Filter: ((github_access_token IS NOT NULL) AND (id >= 55))
 Total runtime: 0.039 ms

クエリ実行時間は13msから1msのコンマ以下にまで短縮されました。

: 連続性が要求される点に興味がおありの方は、私がRedditに書いたコメントをご覧ください。

この方法にはかなり重大な注意点がいくつかあります。私の実装では最大IDをキャッシュするので、この方法は私のユースケースには合いますが、皆さんのユースケースに合うとは限りません。次のようにすることで、これを完全にSQLで実行できます。

WHERE
  /* 略 */
  AND id IN (
    SELECT
    FLOOR(
      RANDOM() * (SELECT MAX(id) FROM issues)
    ) + 1
  )

いつものように、最適化の前後にはSQLクエリのベンチマークを取ってください。この実装ではID値が(連続せずに)まばらになっているとうまく扱えず、WHERE条件を使ってそれより大きな最大IDをランダムに選ぶことは考慮されていません。これを正しく行いたいのであれば、本質的には同じWHERE条件をメインクエリと同様にMAX(id)のサブクエリにも適用する必要があるでしょう。

私の場合、少々失敗があっても問題はありませんし、単に最も基本的なWHERE条件が適用されていることも認識していました。皆さんのニーズはここまで柔軟ではない可能性があります。

「ビルトイン機能で対処する方法ってあるの?」とお思いの方に耳寄りな情報です。Postgres 9.5で導入されたTABLESAMPLEがあることを知りました。情報をお寄せいただいた@HotFusionManに感謝します。

私の知る限り、TABLESAMPLEの使い方を紹介する最良の記事はTablesample In PostgreSQL 9.5です。欠点があるとすれば、「真のランダム」ではない(アプリで必要ならばですが)ことと、1件だけ取り出すのには使えないことです。私は、テーブルの1%だけをサンプリングするクエリを使ってこれを切り抜けられました。そして次のようにその1%からIDを取り出し、LIMITで最初のレコードだけを取り出しました。

SELECT
  *
FROM repos
WHERE
  id IN (
    SELECT
      id
    FROM repos
    TABLESAMPLE SYSTEM(1) /* 1 パーセント */
  )
LIMIT 1

Redditの/r/postgresスレで別の方法があることも教わりました。

この方法はうまくいき、大量のデータ(数百万行レベル)を返すクエリでORDER BY RANDOM()よりもはるかに高速になりましたが、その代わりデータ量の非常に少ないクエリが著しく遅くなります。

私がhttps://www.codetriage.comを最適化したとき、RANDOM()が使われているクエリをもうひとつ見つけました。これは、特定の1つのリポジトリについて開いているソースissueを検索するのに使われています。issueの保存方法が原因でIDの連続性がいまひとつなので、先ほどの手(ランダムなIDを>=でサンプリング)があまり通用しないようでした。データのランダム化にもっと確かな方法が必要であり、TABALESAMPLEならうまくいくかもしれないと考えました。

リポジトリによってはissueが数千件にのぼるものもありますが、リポジトリの50%は数件のissueしかありません。このクエリにTABLESAMPLEを導入したところ、小規模なクエリは目に見えて遅くなり、遅かったクエリは速くなりました。クエリで扱うissueの数は少ない方に偏っているので、トータルでは改善されませんでした。というわけで元のRANDOM()方式に戻しました。

追記: 念のため申し上げると、random()そのものは決して遅くないことは私もわかっていますし、本記事でもそのようなことは述べていません。遅いのはORDER BY random()の方です。速度の落ちるORDER BYを大量の記事に対して行うことが問題なのです。

追記2: いいえ、ランダムなオフセットは、IDを>=で指定する方法より速くありません。大量のレコードでオフセットを使う場合にも、パフォーマンスに深刻な問題が生じることがあります。

RANDOM()をもっと効率のよい方法で置き換えることに成功した方はいませんか?ご存知の方はぜひTwitter(@schneems)までお知らせください。

関連記事

Rails+PostgreSQLのパーティショニングを制覇する(翻訳)

ベテランRubyistがPythonコードを5倍速くした話(翻訳)

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

Go言語の内部(1)自動生成関数とその抑制方法(翻訳)

$
0
0

概要

Go言語の内部(1)自動生成関数とその抑制方法(翻訳)

皆さんもMinioでの私たちと同様、この頃Go言語のコールスタックでちょくちょく「自動生成」関数に出くわしては、これが一体何なのか気になっているのではないかと思います。

以前私たちが遭遇した事例では、次のようなコールスタックが出力されました。

   cmd.retryStorage.ListDir(0x12847c0, 0xc420402e70, 0x1, ...)
 minio/cmd/retry-storage.go:183 +0x72

cmd.(*retryStorage).ListDir(0xc4201624b0, 0xdf3b5f, 0xe, 0x25, ...)
                           :932 +0xaf

       cmd.cleanupDir.func1(0xf18f1ec540, 0x25, 0x24, 0xde7eb5)
minio/cmd/object-api-com.go:215 +0xe1

             cmd.cleanupDir(0x1284860, 0xc4201624b0, 0xdf3b5f, ...)
minio/cmd/object-api-com.go:231 +0x1d1

ご覧のとおり、2番目の関数は1番目の関数(値レシーバ、Minioのminio/cmd/retry-storage.go:183で定義)とよく似ていますが、ポインタレシーバを受け取っている点が異なります。もうひとつ疑わしいのは、2番目の関数には行番号だけが表示され、ソースコードのファイル名が空欄になっている点です。

訳注: Go言語では、レシーバを取る関数を「メソッド」と呼びますが、本記事の原文ではメソッドという呼び方をしていません。

このコールスタックを生成したコードの出どころは、以下の関数です。この関数ではstorage.ListDir()を呼び出す(再帰的)関数が宣言されており、呼び出しによってcleanupDir()storageが渡されます。

// ディレクトリを再帰的にクリーンアップする
func cleanupDir(storage StorageAPI, volume, dirPath string) error {
    var delFunc func(string) error
    // エントリを再帰的に削除する関数
    delFunc = func(entryPath string) error {

        // ディレクトリのリストを表示
        entries, err := storage.ListDir(volume, entryPath)
        if err != nil {
            return err
        }

        // 再帰して他のエントリをすべて削除
        for _, entry := range entries {
            err = delFunc(pathJoin(entryPath, entry))
            if err != nil {
                return err
            }
        }
        return nil
    }
    return delFunc(retainSlash(pathJoin(dirPath)))
}

上のコードを見た人は、storage.ListDir(値レシーバ)は、途中でポインタレシーバ版(cmd.(*retryStorage).ListDir)を呼び出す必要なしに、即座に呼び出されると仮定するかもしれません。では実際にはどうなるのでしょうか?

自動生成された関数

最初に、このポインタレシーバ関数が一体何なのかを調べてみましょう。go tool objdump -s ListDir minioを実行してこの関数の定義を取得してみると、「自動生成された」ようです。

TEXT minio/cmd.(*retryStorage).ListDir(SB) <自動生成>
 <autogenerated>:926  0x1f2e00  GS MOVQ GS:0x8a0, CX
 <autogenerated>:926  0x1f2e09  CMPQ 0x10(CX), SP
 <autogenerated>:926  0x1f2e0d  JBE 0x1f2f42
 <autogenerated>:926  0x1f2e13  SUBQ $0x78, SP
 <autogenerated>:926  0x1f2e17  MOVQ BP, 0x70(SP)
 <autogenerated>:926  0x1f2e1c  LEAQ 0x70(SP), BP
 <autogenerated>:926  0x1f2e21  MOVQ 0x20(CX), BX
 <autogenerated>:926  0x1f2e25  TESTQ BX, BX
 <autogenerated>:926  0x1f2e28  JE 0x1f2e3a
 <autogenerated>:926  0x1f2e2a  LEAQ 0x80(SP), DI
 <autogenerated>:926  0x1f2e32  CMPQ DI, 0(BX)
 <autogenerated>:926  0x1f2e35  JNE 0x1f2e3a
 <autogenerated>:926  0x1f2e37  MOVQ SP, 0(BX)
 <autogenerated>:926  0x1f2e3a  MOVQ 0x80(SP), AX
 <autogenerated>:926  0x1f2e42  TESTQ AX, AX
 <autogenerated>:926  0x1f2e45  JE 0x1f2efa
 <autogenerated>:926  0x1f2e4b  MOVQ 0x80(SP), AX
 <autogenerated>:926  0x1f2e53  MOVQ 0(AX), CX
 <autogenerated>:926  0x1f2e56  MOVQ CX, 0(SP)
 <autogenerated>:926  0x1f2e5a  LEAQ 0x8(AX), SI
 <autogenerated>:926  0x1f2e5e  LEAQ 0x8(SP), DI
 <autogenerated>:926  0x1f2e63  MOVQ BP, -0x10(SP)
 <autogenerated>:926  0x1f2e68  LEAQ -0x10(SP), BP
 <autogenerated>:926  0x1f2e6d  CALL 0x5d5a4
 <autogenerated>:926  0x1f2e72  MOVQ 0(BP), BP
 <autogenerated>:926  0x1f2e76  MOVQ 0x88(SP), AX
 <autogenerated>:926  0x1f2e7e  MOVQ AX, 0x28(SP)
 <autogenerated>:926  0x1f2e83  MOVQ 0x90(SP), AX
 <autogenerated>:926  0x1f2e8b  MOVQ AX, 0x30(SP)
 <autogenerated>:926  0x1f2e90  MOVQ 0x98(SP), AX
 <autogenerated>:926  0x1f2e98  MOVQ AX, 0x38(SP)
 <autogenerated>:926  0x1f2e9d  MOVQ 0xa0(SP), AX
 <autogenerated>:926  0x1f2ea5  MOVQ AX, 0x40(SP)
 <autogenerated>:926  0x1f2eaa  CALL cmd.retryStorage.ListDir(SB)
 <autogenerated>:926  0x1f2eaf  MOVQ 0x48(SP), AX
 <autogenerated>:926  0x1f2eb4  MOVQ 0x50(SP), CX
 <autogenerated>:926  0x1f2eb9  MOVQ 0x58(SP), DX
 <autogenerated>:926  0x1f2ebe  MOVQ 0x60(SP), BX
 <autogenerated>:926  0x1f2ec3  MOVQ 0x68(SP), SI
 <autogenerated>:926  0x1f2ec8  MOVQ AX, 0xa8(SP)
 <autogenerated>:926  0x1f2ed0  MOVQ CX, 0xb0(SP)
 <autogenerated>:926  0x1f2ed8  MOVQ DX, 0xb8(SP)
 <autogenerated>:926  0x1f2ee0  MOVQ BX, 0xc0(SP)
 <autogenerated>:926  0x1f2ee8  MOVQ SI, 0xc8(SP)
 <autogenerated>:926  0x1f2ef0  MOVQ 0x70(SP), BP
 <autogenerated>:926  0x1f2ef5  ADDQ $0x78, SP
 <autogenerated>:926  0x1f2ef9  RET
 <autogenerated>:926  0x1f2efa  LEAQ 0x96c2bb(IP), AX
 <autogenerated>:926  0x1f2f01  MOVQ AX, 0(SP)
 <autogenerated>:926  0x1f2f05  MOVQ $0x3, 0x8(SP)
 <autogenerated>:926  0x1f2f0e  LEAQ 0x976870(IP), AX
 <autogenerated>:926  0x1f2f15  MOVQ AX, 0x10(SP)
 <autogenerated>:926  0x1f2f1a  MOVQ $0xc, 0x18(SP)
 <autogenerated>:926  0x1f2f23  LEAQ 0x9707a2(IP), AX
 <autogenerated>:926  0x1f2f2a  MOVQ AX, 0x20(SP)
 <autogenerated>:926  0x1f2f2f  MOVQ $0x7, 0x28(SP)
 <autogenerated>:926  0x1f2f38  CALL runtime.panicwrap(SB)
 <autogenerated>:926  0x1f2f3d  JMP 0x1f2e4b
 <autogenerated>:926  0x1f2f42  CALL runtime.morestack_noctxt(SB)
 <autogenerated>:926  0x1f2f47  JMP cmd.(*retryStorage).ListDir(SB)

真ん中あたりのオフセットが0x1f2eaaの行でretryStorage.ListDir(値レシーバ)を呼び出しているのがわかります。この関数はminio/cmd/retry-storage.go:183にあるGo言語のコードで定義されています。そこより上の行ではスタックにすべての引数を設定しており、その中で *retryStorageの参照を解決してそのコピーをスタック上に作成しています(retryStorage.ListDirで必要になるため)。

そのCALLの次では戻り引数(return arguments)が読み込まれ(オフセット0x1f2eafから0x1f2ec3までの行)、適切なスロットにコピーされます(オフセット0x1f2ec8から0x1f2ee8までの行)。これは、この自動生成された関数から戻った後で(*retryStorage).ListDirの戻り引数になります。

つまり、(*retryStorage).ListDirが行っているのは、本質的にはretryStorage.ListDir呼び出しをラップして、ポインタのレシーバが指すオブジェクトが変更されないようにすることだけです。

Go言語がこのようにする理由

(*retryStorage).ListDirの動作については理解できました。次の疑問は、Go言語がなぜこんなことをするのかです。その理由は、StorageAPIがインターフェイス型であるという事実と関連しなければなりません。すなわち、cleanupDir()storage引数の値はインターフェイスであり、実質的に2つのポインタで構成されます。インターフェイスの2つの値は、そのインターフェイスに保存される型に関する情報へのポインタと、それに関連付けられるデータを指すポインタを与える2つのワードのペアで表されます

したがって、storage.ListDir()を呼び出すと、retryStorage()を指すポインタ(ただし値レシーバメソッド版のListDir()のみ)にアクセスできる状態になってしまいます。ここでGo言語コンパイラは手回しよく(自動)生成を行いますが、オブジェクトの参照を解決して戻り引数などをコピーしなければならない分かなり余分なコストを伴います。この点を明らかにしてくれた@thatcksのご指摘に感謝いたします。

自動生成を止めたいときは?

最初に申し上げたいのは、「修正」は必ずしも必要ではないという点です。本質的に間違ったことは行われておらず、問題なくコードを実行できるからです。

しかしどうしても修正したいのであれば、嘘のように簡単な方法で行えます。minio/cmd/retry-storage.go:183func (f retryStorage) ListDir(...)を変更してfunc (f *retryStorage) ListDir(...)にするだけで次の結果を得られます。

TEXT github.com/minio/minio/cmd.(*retryStorage).ListDir(SB)
 retry-storage.go:199   0x15edc0    GS MOVQ GS:0x8a0, CX
 retry-storage.go:199   0x15edc9    CMPQ 0x10(CX), SP
 retry-storage.go:199   0x15edcd    JBE 0x15efe5
 <以下省略>

きっとあなたは、この関数の実装が誤ってfの値を変更することのないようにしたくなると思います。いずれその可能性に気づくでしょう(たぶんこのあたりは少々気持ち悪いかもしれません: これは値レシーバ関数では発生せず、おそらく呼び出し側はfの内容を変更しようとする意図に気づくでしょう)。

もうひとつのフリーボーナスは、実行ファイルのサイズを数100バイトも削減できることです(この関数ひとつでそうなるのですから、ソースコードファイルに*をひとつ足すだけで相当いい感じになりそうです)。

まとめ

本記事が、Go言語の関数の内部動作に関する何らかの洞察となり、こうした自動生成関数の正体や、それに対して何ができるかを明らかにできれば幸いです。

本シリーズでは今後、ポインタレシーバ関数と値レシーバ関数のパフォーマンスについてもっと詳しく書きます。

Nitish TiwariHarshavardhanaに感謝いたします。

関連記事

Goby: Rubyライクな言語(1)Gobyを動かしてみる

週刊Railsウォッチ(20180202)Rails 5.2.0 RC1と5.1.5.rc1リリース、Rails 6開発開始、メソッド絵文字化gemほか

$
0
0

こんにちは、hachi8833です。生まれて初めて皆既月食をじかに見ました。

2月最初のウォッチ、いってみましょう。

つっつき中に録音ボタンを押し忘れてしまったので、今回のつっつき成分はいつもより少なめです🙇

臨時ニュース

Rails 5.2.0 RC1と5.1.5.rc1リリースとRails 6開発開始

立て続けのリリースです。RC版の表記が「RC」と「.rc」の2とおりなのが妙に気になりました。

Rails 5.2.0 RC1リリース

最終版は2月中に出したい意向だそうです。

最初のRails 5.2リリースから2か月の間、最初のRCのためにあらゆる方法で改良・調整を行ってきました。

今回の目玉機能として、Active Storageフレームワークをdeeper content-type identificationで拡張するなど多くの改良が行われました。Active StorageはBasecampなどでさらに数か月production環境での試練を重ねてきました。すぐに使える堅固なフレームワークです。

5.2ベータでは他にも改良を行いました。高速なフィクスチャー読み込みActive Job discard時のエラーハンドリングのカスタマイズActive Recordクエリでアクセス元サイトをログに出力する機能などです。Railsは止まりません!

リリースも間近になりました。Rails 5.2ベータは既にBasecampなど多くのサイトで数か月間productionで動作しています。次のRCまたは最終リリースの目標は、今後のissue次第ですが、2月末までとなります。今回はRCなので、既にrails/masterブランチをrails/5-2-stableに移行し、rails/masterはRails 6.0の開発に充てられています。

Ruby on Railsを支えてくれている皆さまに改めて感謝します。
プレスリリースの冒頭を抄訳

Rails 6ブランチ登場↓

Rails 5.1.5.rc1リリース

バグ修正です。変更があったのは以下です。

Rails 5.2ミニチュートリアル: 新機能とActive Storageの詳細(RubyFlowより)


evilmartians.comより

早くもRails 5.2のまとめ記事が出ています。来週翻訳いたします。

Rails: 今週の改修

公式は少なめで、3つのうち2つが上のDHHのプレスリリースにも含まれています。

フィクスチャ挿入時のマルチステートメントクエリをサポートして高速化、insert_fixturesは非推奨に

eachで回さなくても引数でbuild_sqlに渡せるようになりました。

  # 従来
  %w(authors dogs computers).each do |table|
    sql = build_sql(table)
    connection.query(sql)
  end

  # 変更後
  sql = build_sql(authors, dogs, computers)
  connection.query(sql)

ActiveRecordのdiscard_onがブロックを取れるようになった

# activejob/lib/active_job/exceptions.rb
       def discard_on(exception)
         rescue_from exception do |error|
-          logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+          if block_given?
+            yield self, exception
+          else
+            logger.error "Discarded #{self.class} due to a #{exception}. The original exception was #{error.cause.inspect}."
+          end
         end
       end

ActiveRecordクエリでメソッド呼び出し元の位置をdevelopmentログに出力

以下のようにapp/models/version.rb:247:in 'downloads_count'などと出力されます。

Started GET "/news/popular" for ::1 at 2016-10-19 00:57:48 +0200
Processing by NewsController#popular as HTML
  Version Load (57.3ms)  SELECT  "versions".* FROM "versions" INNER JOIN "rubygems" ON "rubygems"."id" = "versions"."rubygem_id" LEFT OUTER JOIN "gem_downloads" ON "gem_downloads"."rubygem_id" = "rubygems"."id" AND "gem_downloads"."version_id" = $1 WHERE ("versions"."created_at" BETWEEN '2016-10-11 22:57:48.145796' AND '2016-10-18 22:57:48.145965') AND "versions"."indexed" = $2  ORDER BY gem_downloads.count DESC, "versions"."created_at" DESC LIMIT 10 OFFSET 0  [["version_id", 0], ["indexed", "t"]]
  ↳ app/views/news/show.html.erb:9:in `_app_views_news_show_html_erb___2784629296874387000_70222193538980'
  Rubygem Load (0.4ms)  SELECT  "rubygems".* FROM "rubygems" WHERE "rubygems"."id" = $1 LIMIT 1  [["id", 19969]]
  ↳ app/views/news/_version.html.erb:1:in `_app_views_news__version_html_erb__2744651331114605013_70222191156360'
  Version Load (0.8ms)  SELECT  "versions".* FROM "versions" WHERE "versions"."rubygem_id" = $1 AND "versions"."latest" = $2  ORDER BY "versions"."position" ASC LIMIT 1  [["rubygem_id", 19969], ["latest", "t"]]
  ↳ app/helpers/application_helper.rb:23:in `gem_info'
  GemDownload Load (0.3ms)  SELECT  "gem_downloads".* FROM "gem_downloads" WHERE "gem_downloads"."version_id" = $1 AND "gem_downloads"."rubygem_id" = $2 LIMIT 1  [["version_id", 882133], ["rubygem_id", 19969]]
  ↳ app/models/version.rb:247:in `downloads_count'

つっつきボイス:activerecord_cause gem要らなくなったか…と思ったら昨年末のウォッチで#26815扱ってた」「いい機能!」

PostgreSQL外部テーブルをサポート

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#530
+        def foreign_tables
+          query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
+        end
+
+        def foreign_table_exists?(table_name)
+          query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
+        end

参考: 9.3で進化した外部テーブル — スライド

Rails

Rails + PostgreSQLでマルチテナントジョブキューを構築

ジョブキューのソリューションが乱立する中、PostgreSQLでやってみたそうです。

Passenger 5.2.0リリース: 非互換の変更あり


phusion.nlより

  • Ruby 2.5.0に対応
  • Apache integrationモードでPassengerResolveSymlinksInDocumentRootなどを廃止
  • 設定オプションのリファクタリング: あいまいな設定名の修正や設定の自動化など
  • etc.

EXTRACT関数を使って日付をうまく扱う(RubyFlowより)

# 同記事より
Order.where("EXTRACT(year FROM created_at) = EXTRACT(year FROM now())")

つっつきボイス: 「Karolさんの記事はいい感じのが多くて好きです」「EXTRACTは某案件で使ったことあるけど、whereEXTRACTしてるのか」「これインデックス効くんだろうか?と思ったら文中にも書いてあった↓: functional index作っとけって」

And the great thing is that you can also create a functional index for EXTRACT(year FROM created_at) to avoid sequential scanning and get much better performance.
同記事より

条件節をいい感じの名前のメソッドに切り出す(RubyFlowより)

# 同記事より
  def good?
    clean? && local? && excellent_coffee?
  end

  private

  def clean?
    !filthy?
  end
...
  def filthy?
    @name == 'Barry’s Caff'
  end

つっつきボイス: 「Andy Crollさんの記事はきっちりテンプレ化されてますね」「リファクタリングはわかるけど、filthy(不潔な)とか'Barry’s Caff'ってスゴいですね」「清潔とは『不潔じゃない』こと」

Tully’s Coffeeのもじりかもと思えてきました。

PowをPumaに切り替えた話(RubyFlowより)


つっつきボイス: 「Rails始めた頃、深く考えずにサーバーをPowとかthinとか取っ替え引っ替えしてました」「PowってMac専用なんですね」

Rails 5.2の例のCurrent


つっつきボイス: 「記事を引用いただいてうれしい: ありがとうございます」

localer: i18nの翻訳漏れを検出するgem

非常に新しいgemですが、Evil Martiansがスポンサーになっていますね。

  • 訳が抜けているymlの検出(パスの指定も可能)
$ localer check /path/to/rails/application
  • 特定のlocale/語のキーをオフにする
Locale:
  EN:
    Exclude:
      - /population\z/
      - .countries.france
  • CI統合
# .travis.yml

# other configuration options
script:
  - bundle exec bundle-audit
  - bundle exec rubocop
  - bundle exec rspec
  - bundle exec localer

i18n_generators: localeのyamlを自動生成&訳語追加

amatsudaさんによる有名なgemですが一応。Rails 5.1にも対応しています。


同リポジトリより

% rails g i18n_translation ja (de-AT, pt-BR, etc.)
# config/locales/translation_ja.ymlが生成される

つっつきボイス: 「これって自動翻訳までしてくれるのかな?」

調べてみると、2012年にはQiitaに「残念ながら現状は翻訳が全く行われない状態になってしまっています」とありましたが、手元でインストールしてrails g i18n_translation jaを実行してみたら見事自動翻訳できました。先のlocalerと合わせるとさらによさそうですね。

なお類似のhaml-i18n-extractorというローカライズ文字列抽出用gemも見つけたのですが、こちらは数年以上更新がありません。

その他Rails小物記事

Ruby trunkより

メソッド参照のシンタックスシュガーが欲しい(継続)

この間ウォッチで取り上げたyield_self is more awesome than you could thinkでこれに言及していました。method(:メソッド名)をJava 8みたいに簡潔に書けるようにしたいそうです。

#13581より
map(&Math->sqrt) (and just each(&->puts) probably?) -- Matz is explicitly against it;
map(&Math\.sqrt) (not sure about puts);
map(&Math.m(:sqrt)), each(&m(:puts)) (just shortening, no language syntax change)
map(&Math.:sqrt), each(&.:puts)
map(&Math:sqrt), each(&self:puts)
map(&Math#sqrt), each(&#puts) (it was my proposal, "just how it looks in docs", but I should reconsider: in docs it is Math::sqrt, in fact)
map(&Math:::sqrt), each(&:::puts)
map(&~>(:sqrt, Math)), each(&~>(:puts))
several by @Papierkorb:
map(&Math.>sqrt), each(&.>puts) (nobu (Nobuyoshi Nakada): conflicts with existing syntax)
map(&Math<sqrt>), each(&<puts>) (nobu (Nobuyoshi Nakada): conflicts with existing syntax)
map(&Math&>sqrt), each(&&>puts)
map(&Math|>sqrt), each(&|>puts) (too confusable with Elixir-like pipe, probably)

まだまだ続きそうな雰囲気です。


つっつきボイス: 「おーJava 8とな」「ASCIIに記号足りないですね」「ファットアロー=>の出番かな(ぶつかる)」「記号増やすとパーサーいじらないといけないから面倒」「Gobyならパーサーいじるのそんなに大変じゃないっす」

Java 8では::を使うそうです。

参考: java8 メソッド参照

URI#secure?が欲しい(継続)

uri.instance_of?(URI::HTTPS)
url.secure? # 上をこう書けたらいいな

2.3/2.4でメモリリーク(修正済み)

require 'pathname'

puts Process.pid

puts ARGV[0]
(ARGV[0] || 1).to_i.times { $LOAD_PATH.unshift(Pathname.new(__dir__) ) }

dot      = "."
filename = "ostruct"
1000.times { 1000.times { require filename }; print dot; GC.start; }

STDOUT.puts "exit?"
STDIN.gets
1 74.6 MB
2 149.5 MB
3 214 MB
4 290 MB
5 353.6 MB
9 575.4 MB
10 650.6 MB

Ruby

Reduxのストアを理解するためにRubyでちょっと再実装してみた

# 同記事より
class ReduxStore
  attr_reader :current_state

  def initialize(reducer)
    @reducer = reducer
    @listeners = []
    @current_state = nil
    dispatch({})
  end

  def dispatch(action)
    @current_state = @reducer.call(@current_state, action)
    @listeners.each { |l| l.call }
  end

  def subscribe(listener)
    @listeners.push(listener)
    ->{ @listeners.delete(listener) }
  end
end

参考: ReduxのAction、Reducer、Storeの(個人的な)整理メモ

RubyプロセスのメモリアロケーションをeBPFで調べる

著者のJulian Evansさんはrbspyというツールをつい最近公開してたちまち★900超えです。Noah Gibbs氏も絶賛。


つっつきボイス: 「eBPFはBPF(Berkeley Packet Filter)の拡張版か」「この人は後述のMacのカーネルバグを見つけた人」「レベル高い…」

参考: LinuxのBPF : (3) eBPFの基礎

abstriker: Rubyで抽象クラスを使うgem

# リポジトリより
class A1
  extend Abstriker

  abstract def foo
  end
end

class A3 < A1
  def foo
  end
end # => OK

class A2 < A1
end # => raise

Class.new(A1) do
end # => raise

つっつきボイス: 「Rubyでabstract!」「上位クラスでメソッドが宣言されてなかったらraiseするのか」「abstractの引数に埋まってるシンボルを取り出して探索してるみたいです」「Rubyだとコンパイル時に発見できないから実行しないと出ないですけどね」「マジック使わないとできないんでしょうね」「これは実装大変そう…」

Kiba: Ruby向けデータ処理&ETLライブラリ


kiba-etl.orgより

# Wikiより
job = Kiba.parse do
  source MySource
  transform MyFirstTransform
  transform { |r| r.merge(extra_field: 10) }
  destination MyDestination
end

Kiba.run(job)

つっつきボイス: 「Kibaって牙なのか(木場ジャナカッタ)」「まだ使い道がよくわからないけど、何かに使えそうな気がします」

参考: Wikipedia-ja Extract/Transform/Load(ETL)

rucc: Rubyで書かれたCコンパイラ

$ rucc -c hello.c
$ ls
hello.c  hello.o

つっつきボイス: 「何て読むのかな」「るーしーしー?」

Rubyのシンボルその後

Rubyのテスト環境

今年のRubyKaigi会場

こちらだそうです。

SQL

PostgreSQLでVACUUMしていいとき/いけないとき(Postgres Weeklyより)


つっつきボイス: 「VACUUMときたらPostgreSQL: 他では見かけない用語ですね」

参考: PostgreSQL 9.4.5 VACUUM
参考: PostgreSQL の VACUUM をなんとなくでするのはやめよう

PostgreSQLのストアドプロシージャ記事2本(Postgres Weeklyより)


つっつきボイス: 「ストアドプロシージャはORMフレームワークとかと相性悪いのでWebの人は知らなかったりするかも: 好きではないけど触ったことぐらいはある」「Railsでマイグレーションするときとかに困りそう」

ネットワーク設定のミスでテーブルが肥大化(Postgres Weeklyより)

PostgreSQL起動前にIPv6を殺すことで修正できたそうです。先週ご紹介したPostgreSQL設定ツールのCyberTecのブログです。


pgconfigurator.cybertec.atより

JavaScript

jQuery 3.3.0登場-> 3.3.1修正リリース

3.3.0に不足していたdependencyを3.3.1で追加したそうです。

参考: jQuery 3.3登場、約1年ぶりのアップデート。新機能も追加

今人気のJavaScriptリポジトリ

一番人気はFacebookのcreate-react-appでした。


同リポジトリより

JavaScriptエラートップテンと回避方法


同記事より

AutoprefixerからPostCSSへ


同記事より


つっつきボイス: 「ちょっと前に翻訳したWebpack記事↓にもPostCSSが登場してたけど、位置づけがよくわかってなかった」「PostCSSはJS版Sass的なやつ」「あ、Rubyに依存しないのか」
PostCSSの作者はロシア人だそうで、だからこのロシア正教っぽい画像↑なのかな」

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

CSS/HTML/フロントエンド

不適切に発行されたメジャーな証明書がChrome/Firefoxで順次無効になる


同記事より


つっつきボイス: 「これねー」「気が重い…」

参考: 世界30%のSSL証明書が3月と10月に強制無効化!? あなたのサイトが大丈夫か確認する3ステップ

GitHubの「草」をカスタマイズしてみた(Frontend Focusより)

See the Pen GitHub Contribution Graph in CSS Grid Layout by Ire Aderinokun (@ire) on CodePen.

同記事より

Variable Font記事2本(Frontend Focusより)

html {
  font-family: 'SourceSans' sans-serif;
  font-weight: 400;
}

@supports (font-variation-settings: "wght" 400) {
  html {
    font-family: 'SourceSansVariable', sans-serif;
    font-variation-settings: "wght" 400;
  }
}


webdesign.tutsplus.comより


つっつきボイス: 「1つのフォントでいろんな書体を使えるんですね」「日本語だとあまり要らなさそうだけどアルファベット文化圏では欲しいやつかも」

参考: Variable Fontについて

フロントエンドは複雑になる宿命(Frontend Focusより)


同記事より

Indexed Database APIの2.0が公開(Frontend Focusより)

// w3.orgより
var tx = db.transaction("books", "readwrite");
var store = tx.objectStore("books");

store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});

tx.oncomplete = function() {
  // All requests have succeeded and the transaction has committed.
};

参考: MDN IndexedDB

IndexedDB は SQL ベースの RDBMS に似たトランザクショナルデータベースシステムですが、SQL ベース の RDBMS が固定された列を持つテーブルを使用するのに対して、IndexedDB は JavaScript ベースのオブジェクト指向データベースです。IndexedDB では、キーでインデックス付けされたオブジェクトを保存および取り出すことができます。Structured Clone アルゴリズムがサポートする、任意のオブジェクトを保存できます。データベースのスキーマを定義する、データベースへの接続を確立する、そして一連のトランザクションでデータの取り出しや更新を行うことが必要です。
developer.mozilla.orgより

その他

開発者が死守すべき最低限のエチケット(Frontend Focusより)


同記事より

  • インデントをちゃんとつける
  • ファイル配置はディレクトリで構造化する
  • 必要なコメントを書く
  • ドキュメントを書く
  • fooとかbarとか使うな
  • 意味不明なファイル名を使わない
  • 大文字小文字の区別(capitalization)をきちんと行う
  • DRYを心がける

書籍『[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』予約開始

ドラフト: Javaにraw string literalを追加

Rubyのヒアドキュメント的な書き方ができるようになるそうです。

// 従来
String html = "<html>\n" +
              "    <body>\n" +
              "         <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";
// 案
String html = `<html>
                   <body>
                       <p>Hello World.</p>
                   </body>
               </html>
              `;

つっつきボイス: 「そうそう、これJavaで辛かった」「Rubyのヒアドキュメントってあまり使わなかったけど実はいいヤツなんですね」

CoreOSがRed Hat傘下に


つっつきボイス: 「これワンピースじゃないっすかw」「ああ、ワタイにわからんネタでした」「k8sって?」「Kubernetesか」

macOSでカーネルバグを見つけた

原因は不明ですが、execとtask_for_pidの間でsleepさせることで回避できるそうです。

参考: 17行のCコードでMacのシステム全体をフリーズさせられることが出来る不具合がmacOS High Sierraで確認される。

BSDは死につつある?

以下のyomoyomoさんの紹介を見るのが早いです。

参考: BSDは死につつある? 一部のセキュリティ研究者はそう考えている

TSL 1.1の廃止が延期に

参考: SSL3.0, TLS1.0~1.2の微妙な違いのまとめ

日本の改元、間が悪そう

Unicode 12のリリースが今年の2月なので、元号文字の追加はへたすると2020年のUnicode 13までかかるかもです。

おまいらの出会った最悪のコーディング文化を晒すスレ

開くたびに増えてます。

Goで書いたコードがヒープ割り当てになるかを確認する方法

この記事で絶賛されているAllocation Efficiency in High-Performance Go Servicesという記事がとても濃厚です。

番外

active_emoji: Rubyの主要メソッドを絵文字化するgem

# 同リポジトリより
class Array
  alias ⏪  <<
  alias 🈴 concat
  alias 💧 drop
  alias 🔁 each
  alias 🈳❓ empty?
  alias 🍀 sample
  alias 🎲 sample
  alias ♻ shuffle
  alias 👈 push
  alias 🍕 slice
end

class Object
  alias ⛄❓ frozen?
  alias ❄❓ frozen?
  alias ⛄ freeze
  alias ❄ freeze
  alias :"#⃣" hash
  alias 🔬 inspect
  alias 🆔 object_id
  alias 🚰 tap
end

module Kernel
  alias 🆚 <=>
  alias 📎 binding
  alias 🔲❓ block_given?
  alias 📥 gets
  alias 🔁 loop
  alias 📠 print
  alias 📤 puts
  alias 🎰 rand
  alias 👻 singleton_class
  alias 💤 sleep
  alias 💻 system
  alias ⚠ warn
  def 🔟; 10 end
  def 💯; 100 end
end

class String
  alias ⏪  <<
  alias ✖ *
  alias ➕ +
  alias 🔪 chop
  alias 🔪❗ chop!
  alias 🔡 downcase
  alias 🔡❗ downcase!
  alias 🈳❓ empty?
  alias 🍌 split
  alias 🔠 upcase
  alias 🔠❗ upcase!
  alias 📏 length
end


同リポジトリより


つっつきボイス: 「完璧にお遊びっすね」「こ、これはw」「何だかファイトが湧いてくるちきしょう、今夜読み解いてやる」「バナナ🍌が何でsplit?」「バナナスプリットってデザートありますね」「🔁🍕each_sliceとか」「🔁eachというのはちょっとー」

タイポ

/* dixyes/mianwrapperより */
#define mian main
#define ture true

つっつきボイス: 「やめて~」

ミトコンドリア


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180126)Bootstrap 4登場でbootstrap_form gemが対応、PostgreSQLやnginxの設定ファイル生成サービスほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやRSSなど)です。

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Focus

frontendfocus_banner_captured

Rails: パーシャルと`collection:`でN+1を回避してビューを高速化(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: パーシャルとcollection:でN+1を回避してビューを高速化(翻訳)

Railsでビューのレンダリング(特にパーシャル)を正しく行うことの重要性に気づいてない人をよく見かけます。本記事では、さまざまなアプローチのパフォーマンスの相対数値を比較します。このトピックは、多くのブログ記事で見落とされがちです。

N+1クエリの回避

最初の重要なトピックは「N+1クエリ」です。N+1クエリをのさばらせると速度低下が不可避になり、その他のパフォーマンス最適化も効かなくなってしまうことがあるため、ぜひとも回避しましょう!

非常にシンプルな例から見てみましょう。

<% @users.each do |*user*| %> <div class="post">
      <%= *user*.post.title %> </div>
<% end %>

usersのリストを反復して、各userpostとしています。簡単ですね。

それでは、ユーザー1000人を以下のコードでassignしてテストしてみましょう。

assign(:users, *User*.all)

結果は以下のとおりです。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
          with N+1 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
          with N+1 0.291 (± 0.0%) i/s --- 2.000 in 7.158333s

1秒あたりでレンダリング可能なビュー数は0.291です。これはかなり残念な数値なので、ビューで発生しているN+1クエリを最初に解決しましょう。現在のビューでは、イテレーションのたびにDBでSELECTを実行してuserpostを取り出しています。

N+1を今後も解決するためにBullet gemを導入します。私はBulletが大好きです❤。次の3つの理由からGoldiloaderよりもBulletが好みです。

  1. Bulletのコード自動更新は変更してよいかどうかをプロンプトで確認してくれる。
  2. Bulletはproduction環境では実行できないようになっている。
  3. BulletはN+1クエリを隠蔽せず、検出と理解を支援してくれる。

GemfileにBulletを追加してtest環境向けに設定します。これによって、N+1クエリで例外がraiseされ、解決しないとテストを完了できないようになります。

# app/config/environments/test.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.raise = true
end

テストを再実行すると、必要な情報をBulletが表示してテストを失敗させます。

Bullet::Notification::UnoptimizedQueryError:

USE eager loading detected
 User => [:post]
 Add to your finder: :includes => [:post]

そしてassignを以下のように変更します。

assign(:users, User.includes(:post))

修正後の結果は7倍ほど高速になりました。

Warming up --------------------------------------
      with N+1     1.000  i/100ms
   without N+1     1.000  i/100ms
Calculating -------------------------------------
      with N+1      1.539  (± 0.0%) i/s -      8.000  in   5.305367s
   without N+1     10.479  (± 9.5%) i/s -     52.000  in   5.057764s

Comparison:
   without N+1:       10.5 i/s
      with N+1:        1.5 i/s - 6.81x  slower

パーシャルのレンダリング

それでは本記事の本題であるパーシャルの利用に進みましょう。個別のpostをパーシャルに切り出してコードをリファクタリングすることにします。これはよい方法ですが、次のような駄目リファクタリングがあることも知っておきましょう。

1. パーシャルを切り出す。

<div class="post">
  <%= user.post.title %>
</div>

2. レンダリングする。

<% @users.each do |user| %>
    <%= render 'erb_partials/post', user: user %>
<% end %>

パフォーマンスは以下のようになります。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
 inline 1.000 i/100ms
 partial 1.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
 inline 11.776 (± 8.5%) i/s --- 59.000 in 5.085002s
 partial 5.648 (±17.7%) i/s --- 28.000 in 5.043322s

Comparison:
 inline: 11.8 i/s
 partial: 5.6 i/s --- 2.09x slower

なるほど、確かにコードが2倍も遅くなってしまいました。これはパーシャルのレンダリングを反復しているのが原因です。Railsはuserごとにパーシャルを「オープン」して評価しなければならなくなります。これを解決するにはcollectionを使います。

ビューを次のように変更します。

<%= *render* partial: 'erb_partials/post', collection: @users, as: :user %>

変更後の結果は以下のとおりです。

Warming up --------------------------------------
        inline     9.000  i/100ms
       partial     1.000  i/100ms
    collection     6.000  i/100ms
Calculating -------------------------------------
        inline     96.394  (±11.4%) i/s -    477.000  in   5.016304s
       partial      8.989  (±22.2%) i/s -     43.000  in   5.108843s
    collection     57.828  (±13.8%) i/s -    288.000  in   5.092763s

Comparison:
        inline:       96.4 i/s
    collection:       57.8 i/s - 1.67x  slower
       partial:        9.0 i/s - 10.72x  slower

パフォーマンスを落とさずにコードをパーシャルに切り出せたことがわかります。修正後のコードでは、Railsによるパーシャルの評価は1回で完了し、その後userごとにレンダリングを行います。これは、先のN+1クエリで行った修正と同じに考えることができます。すなわち、この修正によってもコードをスケール可能にできるのです。

追伸

実際には(本記事のように)同一ページに1000ユーザーを表示するのはやめ、ページネーションを実装しましょう。
本記事で用いたコードはGitHubの私のリポジトリでご覧いただけます。

ボーナス: 他のレンダリングエンジンを試してみる

先ほどはerbで比較しましたが、slimhamlではどうなのでしょうか?以下は比較の結果です。

Warming up --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
 erb inline 8.000 i/100ms
 erb partial 1.000 i/100ms
 erb collection 5.000 i/100ms
 slim inline 10.000 i/100ms
 slim partial 1.000 i/100ms
 slim collection 6.000 i/100ms
 haml inline 9.000 i/100ms
 haml partial 1.000 i/100ms
 haml collection 4.000 i/100ms
Calculating --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- -
 erb inline 97.943 (±10.2%) i/s --- 488.000 in 5.046691s
 erb partial 9.438 (±21.2%) i/s --- 46.000 in 5.026193s
 erb collection 67.090 (± 6.0%) i/s --- 335.000 in 5.015540s
 slim inline 104.373 (± 8.6%) i/s --- 530.000 in 5.122621s
 slim partial 9.836 (±20.3%) i/s --- 49.000 in 5.123851s
 slim collection 69.146 (± 7.2%) i/s --- 348.000 in 5.059607s
 haml inline 85.732 (±11.7%) i/s --- 432.000 in 5.111380s
 haml partial 8.180 (±24.4%) i/s --- 40.000 in 5.165770s
 haml collection 41.069 (±21.9%) i/s --- 196.000 in 5.084682s

Comparison:
 slim inline: 104.4 i/s
 erb inline: 97.9 i/s --- same-ish: difference falls within error
 haml inline: 85.7 i/s --- same-ish: difference falls within error
 slim collection: 69.1 i/s --- 1.51x slower
 erb collection: 67.1 i/s --- 1.56x slower
 haml collection: 41.1 i/s --- 2.54x slower
 slim partial: 9.8 i/s --- 10.61x slower
 erb partial: 9.4 i/s --- 11.06x slower
 haml partial: 8.2 i/s --- 12.76x slower

いずれの場合も、インラインのレンダリングが最もよい結果を残しています。slimのパフォーマンスはerbと同程度(またはわずかに上回る)で、hamlのレンダリング時間はパーシャルでやや落ちるようです。

結論

  • パーシャルを使いましょう。ためらう必要はありません。
  • インラインレンダリングは確かに高速ですが、コードのメンテナンス性/読みやすさ/テストのしやすさも重要です。
  • すなわち、必要に応じてビューをパーシャルに分割しましょう。
  • ただし、collectionを使って正しく行いましょう。
  • もちろんN+1クエリは避けましょう。

関連記事

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

Railsアプリのアセットプリコンパイルを高速化するコツ(翻訳)


Rails5「中級」チュートリアル(1)序章とセットアップ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

Rails5「中級」チュートリアル(1)序章とセットアップ(翻訳)

初めてアプリを作る方法を解説するチュートリアルはネット上に山ほどあります。本チュートリアルは、そこからさらに進んで、もっと複雑なRuby on Railsアプリを作る方法を詳しく解説いたします。

本チュートリアルでは、折に触れて新しい技術や概念を順次導入していきます。すなわち、どの新しいセクションにも学ぶべき新しいことが記されています。

本チュートリアルで扱うトピックには以下のものがあります。

  • Ruby On Railsの基礎
  • リファクタリング(ヘルパー、パーシャル、concern、デザインパターン)
  • テスト: TDD/BDD(RSpecとCapybara)、ファクトリー(FactoryBot)
  • ActionCable
  • ActiveJob
  • CSS、Bootstrap、JavaScript、jQuery

どんなアプリを作るか

自分と考えの近い人を探して出会えるプラットフォームを作ることにします。

このアプリの主要な機能は次のとおりです。

  • 認証(Deviseを使用)
  • 投稿の表示、検索、分類
  • インスタントメッセージ(ポップアップウィンドウと独自のメッセンジャー)
    • 非公開チャットやグループチャットを作成可能
  • ユーザーをつながり(contact)に追加する
  • リアルタイム通知

完成したアプリの外観については以下の動画でご覧いただけます。

完成したソースコードはGitHubのdomagude/collabfieldにあります。

目次

1-1 必要なもの

コードのあらゆる行について、その書き方を選んだ経緯を解説します。まったくの初心者であっても本チュートリアルを終えることは一応可能ですが、本チュートリアルのトピックの中には初心者には手に余る内容も含まれている点にご注意ください。

したがってまったくの初心者にとっては、学習曲線をかなり急上昇することになるので厳しくなるでしょう。新しい概念について触れるたびに、補足情報へのリンクを置くようにします。

理想的には、以下について基礎的な知識を持ち合わせているのがベストです。

1-2 セットアップ

基本的なRuby on Railsの開発環境が設定済みであることが前提です。まだの方はRailsInstallerをチェックしてください。

ある時期の私はWindows 10で開発していました。最初のうちはよかったのですが、やがてWindowsが引き起こすさまざまな謎障害をつぶして回る作業に疲れてしまいました。自分のアプリを動かすためにさまざまな裏技を繰り出さなければならず、これは時間の無駄だと気づきました。こうした障害を克服しても実になるスキルや知識は何も得られず、結局Windows 10でのセットアップでジタバタしただけで終わってしまったのです。

そういうわけで私はバーチャルマシン(VM)での開発に切り替えました。私が選んだのは、Vagrantに開発環境を構築してPuTTY でVMに接続する方法です。Vagrantを使ってみたい方は、こちらのチュートリアル動画が便利です。

1-3 新しいアプリを作る

データベースには、Ruby on Railsコミュニティで人気の高いPostgreSQLを使うことにします。PostgreSQLを使うRailsアプリを作ったことのない方は、こちらのチュートリアルをご覧ください。

PostgreSQLに慣れている方は、コマンドプロンプトを開いてプロジェクトを置きたいディレクトリに移動しましょう。

新しいアプリを作成するには以下を実行します。

rails new collabfield --database=postgresql

アプリの名前はCollabfieldとします。RailsはデフォルトではSQLite3を使いますが、ここではPostgreSQLを使いたいので、以下のオプションを追加することで指定する必要があります。

--database=postgresql

これで新しいアプリを生成できたはずです。

以下を実行して、作成したディレクトリに移動します。

cd collabfield

これで、以下を入力すればアプリを実行できます。

rails s

アプリを起動したので、どのように表示されるかを確認できるようになりました。ブラウザでhttp://localhost:3000を開きます。問題がなければ、Railsのwelcomeページが以下のように表示されるはずです。


  • 次回: Rails5「中級」チュートリアル(2)レイアウト(翻訳)

関連記事

Rails5「中級」チュートリアル(2)レイアウト(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(2)レイアウト(翻訳)

それではコーディングを始めます。どこから始めてもよいのですが、私はWebサイトを新しく作るときは基本的な表示構造を最初に作ってから他のものを作るのが好みなので、この方法で進めることにしましょう。

2-1 Homeページ

今はhttp://localhost:3000を開くとRailsのwelcomeページが表示されるので、これを独自のデフォルトページに変えることにします。そのためには、Pagesコントローラを生成します。Railsのコントローラに慣れていない方は、Action Controllerを読んでRailsのコントローラの概要を理解しておいてください。コマンドプロンプトで以下を実行して、新しいコントローラを生成します。

rails g controller pages

このRailsジェネレータによっていくつかのファイルが作成されます。コマンドプロンプトの出力は以下のような感じになります。

このPagesControllerを使って、特殊な静的ページを制御します。テキストエディタでCollabfieldプロジェクトを開きましょう。私はSublime Textを使っていますが、お好みのエディタで構いません。

pages_controller.rbファイルを開きます。

app/controllers/pages_controller.rb

homeページの定義はここで行います。もちろん、homeページを別の方法で他のコントローラに定義することも可能ですが、私はhomeページをPagesControllerの内部で定義するのが好みです。

pages_controller.rbを開くと以下のようになっています(Gist)。

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
end

これはPagesControllerという名前の空のクラスになっており、ApplicationControllerクラスを継承しています。ApplicationControllerのソースコードはapp/controllers/application_controller.rbにあります。

私たちが今後作成するコントローラは、すべてApplicationControllerクラスから継承します。つまり、ApplicationControllerクラスで定義したメソッドはすべてのコントローラで利用できるようになります。

indexという名前のpublicメソッドを定義しましょう。これはアクションとして呼び出せます(Gist)。

# controllers/pages_controller.rb
class PagesController < ApplicationController

  def index
  end

end

Action Controllerの概要をお読みになった方ならおわかりのように、呼び出されるコントローラとそのpublicメソッド(アクション)は「ルーティング」によって決定されます。それではルーティングを定義して、Webサイトのrootページを開いたときに呼び出されるコントローラとアクションをRailsに認識させてみましょう。app/config/routes.rbファイルを開きます。

Railsのルーティングがわからない方は、この機会にガイドのRailsのルーティングをじっくり読んでルーティングに慣れておきましょう。

ルーティングに以下のコードを追加します。

root to: 'pages#index'

追加後のroutes.rbは以下のようになります(Gist)。

# app/config/routes.rb
Rails.application.routes.draw do
  root to: 'pages#index'
end

Rubyのハッシュシンボル#はメソッドを文章の中で表記するときに使います。アクションは単なるpublicメソッドなので、pages#indexは「PagesControllerのpublicメソッド(アクション)であるindexを呼び出す」という意味です。

rootパスhttp://localhost:3000を開くと、このindexアクションが呼び出されます。しかしレンダリングするテンプレートが未定義のままなので、indexアクションに対応するテンプレートを新しく作ってみましょう。app/views/pagesディレクトリに移動してindex.html.erbファイルを開きます。このファイルには、通常のHTMLの他にERB(Embedded Ruby)コードを書けます。このファイルに以下のように書けば、テンプレートがブラウザに表示されるようになります。

<h1>Home page</h1>

http://localhost:3000を開くと、デフォルトのRails情報ページの代わりに以下のような感じで表示されるはずです。

これでやっと基本的な出発点にたどり着きました。ここからWebサイトに新しい要素を導入していきます。このあたりでgitの最初のcommitを行うのがよいでしょう。

コマンドプロンプトで以下を実行します。

git status

以下のような結果が出力されます。

参考までに、新しいRailsアプリを生成すると、新しいローカルgitリポジトリも初期化されます。

以下を実行して、現在の変更内容をすべて追加します。

git add -A

続いて以下を実行し、変更内容をすべてcommitします。

git commit -m "Generate PagesController. Initialize Home page"

試しに以下を実行してみましょう。

git status

変更点がすべてcommitされたので、「nothing to commit」と表示されます。

2-2 Bootstrap

ナビゲーションバーやレスポンシブなグリッドシステムを使えるよう、Bootstrapライブラリを使うことにします。Bootstrapを使うには、エディタでGemfileにbootstrap-sass gemを追加する必要があります。エディタでGemfileを開きます。

collabfield/Gemfile

bootstrap-sass gemをGemfileに追加します。ドキュメントにも書かれているように、sass-rails gemが存在していることも確認する必要があります。

...
gem 'bootstrap-sass', '~> 3.3.6'
gem 'sass-rails', '>= 3.2'
...

ファイルを保存して以下を実行し、追加したgemをインストールします。

bundle install

アプリを実行中の場合は、Railsサーバーを再起動して新しいgemを利用できるようにします。サーバーを再起動するには、Ctrl + Cでサーバーを停止し、rails sコマンドを再度実行してサーバーを起動するだけで完了します。

assetsディレクトリに移動してapplication.cssファイルを開きます。

app/assets/stylesheets/application.css

コメントアウトされている行の下に以下を追加します。

...
@import "bootstrap-sprockets";
@import "bootstrap";

続いてapplication.cssapplication.scssにリネームします。この変更はRailsでBootstrapライブラリを用いるのに必要です。また、これによってSassの機能を使えるようになります。

今後Sass変数を作成する場合に備えて、すべての.scssファイルがレンダリングされるよう順序を変更する必要があります。Sass変数が使われる前に定義されるよう、順序を変更したいと思います。

これを行うには、application.scssファイルの以下の2行を削除します。

*= require_self
*= require_tree .

Bootstrapライブラリが使えるようになるまであと少しです。もうひとつやっておかなければならないことがあります。bootstrap-sassドキュメントに記載されているとおり、BootstrapのJavaScriptはjQueryライブラリに依存しています。RailsでjQueryを使えるようにするには、jquery-rails gemを追加する必要があります。

訳注: Rails 5.1以降でjQueryを使う場合、gemよりもWebpackとyarnでインストールする方法が標準になりつつあります。この時点でWebpackとyarnでjQueryをインストールする場合、gem 'jquery-rails'の代わりに以下の方法が使えます。
1. Gemfileにgem 'webpacker', github: 'rails/webpacker'を追加し、bundle installを実行する
2. rails webpacker:installを実行し、Webpackerをインストールする
3. yarn install jqueryを実行し、jQueryをインストールする
サーバー再起動後の手順は同じです。

gem 'jquery-rails'

以下を実行します。

bundle install

再度サーバーを再起動します。

最後は、アプリのJavaScriptファイルでBootstrapとjQueryをrequireする手順です。application.jsファイルを開きます。

app/assets/javascripts/application.js

以下の行を追加します(訳注: require_tree .の前の行に追加します)。

//= require jquery
//= require bootstrap-sprockets

変更をgitにcommitします。

git add -A
git commit -m "Add and configure bootstrap gem"

2-3 ナビゲーションバー

ナビゲーションバーは、Bootstrapのnavbarコンポーネントを元に今後いろいろ変更を加えます。ナビゲーションバーはパーシャルテンプレートに保存します。

この時点でこれを行う理由は、アプリのあらゆるコンポーネントを別ファイルに分割するのが望ましいためです。コンポーネントを分割することで、アプリのコードのテストや管理がずっとやりやすくなりますし、コードを複製せずにコンポーネントをアプリの別の箇所で再利用することもできます。

以下のディレクトリに移動します。

views/layouts

以下のファイルを作成します。

_navigation.html.erb

パーシャルファイルの先頭にはアンダースコア_を付け、Railsフレームワークがこのファイルをパーシャルとして認識できるようにします。これを行うには、Bootstrapドキュメントのnavbarコンポーネントのコードをこのファイルにコピペして保存します。このパーシャルをWebサイトで表示するには、コードのどこかでレンダリングする必要があります。views/layouts/application.html.erbファイルを開きます。このファイルの内容は、デフォルトで常にレンダリングされます。

application.html.erbファイルには以下のメソッドがあります。

<%= yield %>

リクエストされたテンプレートはここでレンダリングされます。HTMLファイル内でRuby構文を使うには、<% %>(ERBの書式)で囲む必要があります。ERB構文の違いについて手っ取り早く知りたい方は、StackOverflowの回答をご覧ください。

2-1 Homeページセクションでは、ルーティングを設定することでroot URLが認識されるようにしました。これにより、GETリクエストを送信してrootページに移動すると、常にPagesController#indexアクションが呼び出されます。ルーティングに対応するアクション(ここではindex)は、yieldメソッドでレンダリングされるテンプレートを用いてレスポンスを返します。homeページのテンプレートがapp/views/pages/index.html.erbにあることを思い出しましょう。

ナビゲーションバーはすべてのページに表示したいので、ナビゲーションファイルのレンダリングはデフォルトのapplication.html.erbファイル内で行います。パーシャルファイルをレンダリングするには、パーシャルへのパスを引数に与えてrenderメソッドを呼び出します。以下のようにyieldメソッドの上にrenderを追加します。

...
<%= render 'layouts/navigation' %>
<%= yield %>
...

これで、http://localhost:3000をブラウザで開くと以下のようにナビゲーションバーが表示されます。

予告のとおり、このナビゲーションバーに変更を加えることにします。最初に、<li>要素と<form>要素をすべて削除します。今後ここには独自の要素を作成します。変更後の_navigation.html.erbファイルは次のようになります。

<!-- views/layouts/_navigation.html.erb -->
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- ブランド表示と、モバイル表示切り替え用のグループ化 -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">Brand</a>
    </div>

    <!-- ナビゲーションリンク/フォームなどのコンテンツをここにまとめて表示をオンオフできるようにする -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
      </ul>

      <ul class="nav navbar-nav navbar-right">
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

これでレスポンシブなナビゲーションバーの基本部分ができました。ここでgitに新しくcommitするのがよいでしょう。コマンドプロンプトで以下を実行します。

git add -A
git commit -m "Add a basic navigation bar"

今度はナビゲーションバーの名前をBrandからcollabfieldに変更します。Brandはlink要素なので、リンクの生成には[link_to](https://apidock.com/rails/ActionView/Helpers/UrlHelper/link_to)メソッドを使うべきです。その理由は、link_toメソッドを使う方がURIパスを簡単に生成できるからです。コマンドプロンプトでプロジェクトディレクトリに移動し、以下のコマンドを実行します。

rails routes

このコマンドは、routes.rbファイルで生成される利用可能なルーティングを表示します。以下が出力されます。

この時点のルーティングは先ほど定義した1つだけです。出力結果にPrefixカラムがあることにご注目ください。このprefixを使って、表示したいページへのパスを生成できます。パスの生成は、このprefix名の後ろに_pathを追加するだけでできます。たとえばroot_pathと書けば、rootページへのパスが生成されます。それでは、このlink_toメソッドとルーティングの力を借りてやってみましょう。

<a class="navbar-brand" href="#">Brand</a>

上のコードを以下に置き換えます。

<%= link_to 'collabfield', root_path, class: 'navbar-brand' %>

メソッドの使い方がよくわからなくても、適当にググればたいてい解説ドキュメントを見つけられることを覚えておきましょう。たまにハズレのドキュメントもあるので、その場合はもう少し丁寧にキーワードを指定してググれば、有用なブログ記事やStackOverflowの回答が見つかることもあります。

第1引数で渡す文字列は、<a>要素の値を追加します。第2引数はパスの指定に必要で、ここでパスを生成するのにルーティングが役立ちます。第3引数はオプションで、ここで渡したものはhtml_optionsハッシュに組み込まれます。ここでは、ナビゲーションバーにBootstrapを効かせたいので、navbar-brandクラスの追加が必要です。

ただいまの小さな変更をgitにcommitしましょう。この後のセクションでアプリのデザインに手を加える予定ですが、このナビゲーションバーから変更を開始します。

git add -A
git commit -m "Change navigation bar's brand name from Brand to collabfield"

2-4 スタイルシート

スタイルシートファイルの構成方法をご紹介します。Railsには、スタイルシートの構成方法についての厳密な規則はなく、人それぞれ少しずつ違っています。

ここでは、私が普段用いている構成方法をご紹介します。

  • baseディレクトリ: アプリ全体で使われるSass変数やスタイルはここに置いています(デフォルトのフォントサイズやデフォルト要素のスタイルなど)。
  • partialsディレクトリ: ほとんどのスタイルはここに置いています。このディレクトリでは、コンポーネントやページごとにスタイルを分割するようにしています。
  • responsiveディレクトリ: ここでは、異なる画面サイズごとに、異なるスタイルルールを定義しています(デスクトップ画面のスタイル、タブレット画面のスタイル、スマートフォン画面のスタイルなど)。

最初に、以下を実行してgitリポジトリで新しいブランチを切ります。

git checkout -b "styles"

これにより、新しいgit branchが作成され、自動的にそのブランチに切り替わります。今後、新たなコード変更を別ブランチ上で実装する場合はこのようにします。

別ブランチを切る理由は、現在動作しているバージョン(masterブランチ)から、プロジェクトに追加する新しいコードを切り離し、変更によってmasterブランチに悪影響が生じることのないようにするためです。

実装が終わったら、変更をmasterブランチにmergeできます。

最初にディレクトリをいくつか作成します。

  • app/assets/stylesheets/partials/layout

このlayoutディレクトリでnavigation.scssというファイルを作成し、以下のコードを追加します(Gist)。

//app/assets/stylesheets/partials/layout/navigation.scss
.navbar-default, .navbar-toggle:focus, .collapsed, button.navbar-toggle {
  background: $navbarColor !important;
  border: none;
  a {
    color: white !important;
  }
}

上のScssコードでは、navbarの背景色とリンクの色を変更しています。既にお気づきのように、aセレクタが別の宣言ブロックの内部でネストしていますが、これはSassの機能です。!importantは、デフォルトのBootstrapスタイルを強制的にオーバーライドするのに使います。最後に、この部分では色名の代わりにSass変数が使われていることにお気づきかと思います。Sass変数を使う理由は、アプリ全体で色を変更できるようにするためです。このSass変数を定義しましょう。

最初に以下のディレクトリを作成します。

app/assets/stylesheets/base

baseディレクトリの下にvariables.scssファイルを作成し、以下を定義します。

$navbarColor: #323738;

試しにこの時点でhttp://localhost:3000をブラウザで開いてみると、スタイルはまだ何も変更されていません。スタイルが反映されない理由は、2-2 Bootstrapセクションでapplication.scssファイルから以下を削除したためです。

*= require_self
*= require_tree .

上を削除したのは、すべてのスタイルが自動的にimportされることのないようにするためでした。

つまり、スタイルシートのファイルを新しく作成するときは、メインのapplication.scssファイルで(明示的に)importしなければならないということです。importを追加したapplication.scssファイルは次のようになります(Gist)。

//app/assets/stylesheets/application.scss
// ...デフォルトのコメント

// Bootstrap
@import "bootstrap-sprockets";
@import "bootstrap";

// Variables
@import "base/variables";

// Partials - メインのcssファイル
@import "partials/layout/*";

variables.scssをpartialよりも先にimportする理由は、partialで使われるSass変数より先にSass変数が定義されるようにするためです。

navigation.scssファイルのコードの冒頭に、さらに以下のCSSを追加します。

//app/assets/stylesheets/partials/layout/navigation.scss
nav {
  .navbar-header {
    width: 100%;
    button, .navbar-brand {
      transition: opacity 0.15s;
    }
    button {
      margin-right: 0;
    }
    button:hover, .navbar-brand:hover {
      opacity: 0.8;
    }
  }
}

好みに応じて、上のコードを冒頭に追加する代わりに末尾に追加しても構いません。個人的には、CSSセレクタの「詳細度」の順でCSSコードを配置およびグループ化するようにしています。繰り返しますが、CSSファイルの構成方法は人それぞれ少しずつ違っています。私の場合、詳細度の小さい(大雑把な)セレクタを上に、詳細度の大きいセレクタを下に置くようにしています。たとえば、要素型セレクタはクラスセレクタより上になり、クラスセレクタはIDセレクタより上になるといった具合です。

ここで変更をgitにcommitしましょう。

git add -A
git commit -m "Add CSS to the navigation bar"

今度は、画面を下にスクロールしてもナビゲーションバーが常に最上部に表示されるようにしたいと思います。現時点でスクロールするほどのコンテンツがありませんが、コンテンツは今後増えます。この段階でナビゲーションバーを固定しておくのがよいとは思いませんか?

これを行うには、Bootstrapのnavbar-fixed-topクラスを使います。次のように、nav要素にこのクラスを追加します。

<!-- views/layouts/_navigation.html.erb -->
<nav class="navbar navbar-default navbar-fixed-top">

ついでに、Bootstrap Grid Systemの左側の境界にcollabfieldを配置したいと思います。現在のクラスがcontainer-fluidになっているので、現時点のcollabfieldはviewportの左側境界に配置されています。これを変更するには、(2行目の)このクラスをcontainerに変更します。

変更後の_navigation.html.erbファイルは次のようになります。

<!-- views/layouts/_navigation.html.erb -->
<div class="container">

変更をcommitします。

git add -A
git commit -m "
- in _navigation.html.erb add navbar-fixed-top class to nav.
- Replace container-fluid class with container"

http://localhost:3000をブラウザで開くと、Home pageというテキストがナビゲーションバーの下に隠れてしまいました。これはnavbar-fixed-topクラスのせいです。この問題を解決するには、navigation.scssに以下を追加して<body>要素を下に下げます。

//app/assets/stylesheets/partials/layout/navigation.scss
body {
 margin-top: 50px;
}

これで、アプリは以下のように正常に表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Add margin-top 50px to the body"

先ほど、新しいブランチを切ってそこに切り替えて作業していたのを思い出しましょう。ここでmasterブランチに戻ることにします。

以下のコマンドを実行します。

git branch

以下のようにブランチのリストが表示されています。現在のブランチはstylesになっています。

masterブランチに切り替えるには、以下を実行します。

git checkout master

以下を実行すれば、stylesブランチで行った変更をすべてmergeできます。

git merge styles

このコマンドによって、2つのブランチがmergeされ、変更の概要が以下のように表示されます。

styleブランチが不要になったので、以下を実行して削除します。

git branch -D styles

関連記事

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)

本記事では、ファイルの新しいアップロード方法、チーム内でのCredentialの共有、Content Security Policyなど、主にActiveStorageを中心としてRails 5.1の新機能を、アプリを起動するまでの手順付きでご紹介いたします。


Rails 5.2は約束どおりの新年のプレゼントとはならず、正式リリーズまでもうひと月待つことになってしまいましたが、それでもRC1となり、今や安定してきたと考えられます。というわけで、一足早くプレゼントの包み紙を明けてしまいましょう。今ならgem install rails --prereleaseを実行することでRails 5.2をインストールできます。

訳注: Gemfileでインストールする場合はgem "rails", '>=5.2.0.rc1'を指定します。

Rails 6登場前の最後のメジャーアップデートである5.2のきらびやかな新機能の中でも、Active Storageはひときわ輝きを放っています。プロジェクトでファイルアップロードを扱える機能がRailsに組み込まれたのはこれが初めてです。DHHによるプレスリリースでは、Active StorageはBasecamp 3から切り出された、production環境生まれのフレームワークであることを謳っています。

本記事ではまずActive Storageについて解説し、続いてRails 5.2の他の機能をご紹介いたします。お時間がありましたら、どうぞ最後までお付き合いください。

Active Storageで添付ファイルを扱う

免責事項: 本記事では、Active Storageを既存のソリューション(CarrierWavePaperclipShrineなど)と比較することはせず、できるだけ初心者にわかりやすくActive Storageフレームワークを紹介するよう努めます。

Active Storageをアプリで有効にするには、最初にrakeタスクを実行します。コマンドラインでrails active_storage:installを実行してrails db/migrateで新規マイグレーションを追加することで、Active Storageで必要となる2つのテーブル(active_storage_attachmentsactive_storage_blobs)を追加します。Active StorageのREADMEによると、これらのテーブルでは以下を行います。

Active Storageでは、Attachment joinモデルを経由するポリモーフィック関連付けを使い、実際のBlobに接続します1Blobモデルは添付ファイル(attachment)のメタデータ(ファイル名やコンテンツの種類など)と識別子のキーをストレージサービスに保存します。

Active Storageのこのアプローチは、他の有名なソリューションと異なっています。Paperclip、Carrierwave、Shrineは、いずれも既存のモデルにコラムを追加する必要があります。添付ファイルを扱うgemで唯一広く使われているのは、仮想属性に依存するAttachinary gemです。これは実に使いやすいプロプライエタリなソリューションで、Cloudinaryのストレージ専用です。

Active Storageも同じような方向性ですが、ファイルの保存場所をハードウェアや著名なクラウドプロバイダなどから選べるようになっています2。Amazon S3、Google Cloud Storage、Microsoft Azureについては即座に利用できます。

scaffoldでもできる

ここでActive Storageが動くところを見てみましょう。作成済みの新規アプリがあることが前提です。Gemfilejbuilder gemをコメントアウトしてbundle installを実行しておくと、scaffoldで余分なファイルを生成せずに済みます。今はActive Storageをチェックしたいだけなので、私たちのオリジナリティを発揮するためにフル機能のCRUD構築に時間をかけないことにします。

$ rails g scaffold post title:string content:text
$ rails db:migrate  # 訳注: 補いました

投稿に画像を1つ添付できるようにしましょう。モデル定義に以下のコードを追加します。

# app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

Active Storageを使い始めるには、コードを3箇所だけ変更する必要があります。これはその最初の1つです。

次は、scaffoldジェネレータが作ってくれたposts_controller.rbです。ここで必要なのは、次のようにpost_paramsメソッドの中にimageパラメータを書いてホワイトリスト化することだけです(strong parameters)。

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, :image)
end

参考までに、コントローラのコードの他の場所で既存のモデルにファイルを添付するには、以下のようにします。

@post.image.attach(params[:image])

メモ: コントローラのアクションでリソースに対してcreateupdateを使うときに、添付ファイルを「許可されたパラメータ」として渡すのであれば、上のコードは不要です(正常に動きません)。初期のチュートリアルによってはファイルを明示的にattachする必要があるとしていることもありますが、ここでは既に該当しません。

次はビューです。生成された_form.html.erbで、送信ボタンのすぐ上にfile_fieldを追加します。

<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :image %>
  <%= form.file_field :image %>
</div>

次は、画像を表示します。

<!-- app/views/posts/show.html.erb -->
<% if @post.image.attached? %>
<!-- @post.image.present? は常にtrueを返すので、attached?で存在チェックすること -->
  <p>
    <strong>Image:</strong>
    <br>
    <%= image_tag @post.image %>
  </p>
<% end %>

これでできあがりです。マルチパートフォームでアップロードするときの本質的な詳細部分は、Railsがすべて代わりにやってくれます。rails sでサーバーを起動してlocalhost:3000/posts/newをブラウザで開き、適当な画像を選んで投稿を作成します。

First post with Active Storage

Active Storageのスモークテスト

投稿の編集や画像の変更ももちろんできますので、やってみてください。ご覧のとおりファイルアップロード機能がアプリで使えるようになりました!

ここまでの作業をまとめます。

  • モデル: 定義でhas_one_attachedメソッドを呼び出し、モデルのインスタンスごとの仮想属性とするシンボルを1つ引数として渡しました。ここでは属性名を:imageとしましたが、どんな名前でも構いません
  • コントローラ: imageを許可されたパラメータ(strong parameters)でホワイトリスト化しました。
  • ビュー: フォームにfile_fieldを追加し、アップロードした画像をimage_tagで表示しました。

今度は、フォーム送信時の舞台裏を見ていくことにしましょう。サーバーのログを見てみると、すべてのSQL文の下に、それが生成されたコードの位置が表示されていることがわかります。以前はQuery Trace gemの仕事でしたが、この機能がRailsに組み込まれました。この出力がうれしくない場合は(#31691で特定のrbenv設定で問題が生じることが報告されています)、development.rbファイルのconfig.active_record.verbose_query_logsオプションをfalseに設定してください。

Log for a POST request

添付ファイルをPOSTしたときのログ

このログから、Railsがフォームを処理し、受け取ったファイルをディスクに保存し、保存場所をエンコードしてキーを生成し、そのキーをactive_storage_blobsテーブルで参照し、postsテーブルでレコードを1件作成し、BlobPostactive_storage_attachments経由で関連付けている様子がわかります。

以下は、PostsControllershowアクションがGETで呼び出されたときの挙動です。

Log for a GET request

アップロードの結果をGETしたときのログ

1件のリクエストは3箇所で処理されます。ActiveStorage::BlobsControllerActiveStorage::DiskControllerはファイルを扱います。このようにして、画像のパブリックなURLは常に実際の場所から切り離されます。クラウドサービスを使っている場合は、BlobsControllerによってクラウド内の正しい署名済みURLにリダイレクトされます。

Postインスタンスで何ができるようになったかをrails consoleで見てみましょう。

Rails console

rails consoleでレコードをいじる

添付ファイルのURLを生成するには、urlではなくservice_urlを呼ぶ必要がありますのでご注意ください。url_forimage_tagなどのビューヘルパーはこれを認識して自動でやってくれるので、こうしたメソッドを明示的に呼ぶ必要はめったにありません。

N+1を解決する

ビューで添付ファイルが出力されるときには、少なくとも3件のデータベースクエリが発生します(親モデルで1件、active_storage_attachmentsで1件、active_storage_blobsで1件)。多数の添付ファイルをすべて含むActive Recordオブジェクトのコレクションについて出力を繰り返す場合には、何か注意が必要でしょうか?調べてみましょう。index.html.erbを変更して、投稿ごとの画像(または少なくとも画像ファイル名)を表示するようにし(post.image.filenameもそうしたクエリをすべてトリガします)、/postsをブラウザで更新してログを見てみましょう。

Active StorageのN+1クエリ

見事なまでのN+1クエリ

問題が起きているのがおわかりでしょうか?「N+1」という名のヒドラがおぞましき頭をもたげています。ありがたいことに、Active Storageにはちゃんと解決法が用意されています。この方法では、関連付けられたblobをincludesするwith_attached_imageスコープ(またはwith_attached_your_attachment_nameスコープ)を生成します。必要な作業は、PostsController#index@posts = Post.all@posts = Post.with_attached_imageに変更することだけです。結果を見てみましょう。

N+1問題の解決

問題が解決しました!

素晴らしい!しかし、Active Recordが常に正しい選択を行えることを当てにしたくないという理由で、includesではなくeager_loadpreloadを使いたい場合はどうすればよいのでしょうか?そんなときは次のようにします。

class Post < ApplicationRecord
  scope :with_eager_loaded_image, -> { eager_load(image_attachment: :blob) }
  scope :with_preloaded_image, -> { preload(image_attachment: :blob) }
end

ここで注意しておきたいのは、プリロードをネストしている点です。blobsの読み込みはattachmentsを経由します。image_attachmentは、Active Storageによって追加された関連付けの名前です。特にこの場合attachmentの名前を変更すると、関連付けの名前も変更されます。

複数の添付ファイル

CRUDで複数の添付ファイルを扱えるようにするのも朝飯前です。

  • モデル:
# app/models/post.rb
class Post < ApplicationRecord
  has_many_attached :images
  # ここで暗黙の関連付けを複数形にしていることに注意
  scope :with_eager_loaded_images, -> { eager_load(images_attachments: :blob) }
end
  • コントローラ:
# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :content, images: [])
end
  • ビュー:
<!-- app/views/posts/_form.html.erb -->
...
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true %>
</div>
...
<!-- app/views/posts/show.html.erb -->
...
<% if @post.images.attached? %>
<p>
  <strong>Images:</strong>
  <br>
  <% @post.images.each do |image| %>
    <%= image_tag(image) %>
  <% end %>
</p>
<% end %>

添付ファイルの1つ(またはすべて)を削除したい場合、purgeメソッドかpurge_laterメソッドを使えます。後者は組み込みのActiveStorage::PurgeJobを使ってバックグラウンドでファイルを削除します。親モデルを削除すると、デフォルトで非同期削除も呼び出されます。

ImageMagickでさまざまなサイズの画像を扱う

ここまでの時点では、ユーザーがアップロードした画像はそのまま表示されますが、多くの場合これは望ましくない動作です。Active Storageでは、ImageMagickによる画像変換をサポートしています。必要な準備は、Gemfileに(コメントアウトで)追加済みのmini_magick gemを有効にするだけです。これで、たとえば次のようにImageMagickの変換を使えるようになります。

<%= image_tag image.variant(resize: "500x500", monochrome: true) %>

上のコードは、指定したblobの指定したvariantを指すURLを作成しますが、変換そのものは、その画像がブラウザから最初にリクエストされるまではActiveStorage::VariantsControllerで扱われません。Active Storageでは、元のblobがActive Supportからダウンロードされ、サーバーのメモリ上で変換され、再度サーバーにアップロードされる必要があるため、コストの高い操作の遅延を試みます。

ファイルを(遅延せずに)最初に処理し、その後でURLを取得したい場合は、image.variant(resize: "100x100").processed.service_urlを呼び出します。これは、その特定のvariantが既に実行済みかどうかをチェックし、実行済みの場合は同じ処理を繰り返しません。

動画のプレビュー画像の生成(ffmpegを使用)やPDFの生成(mutoolを使用)も行えます。ただしこれらのライブラリはRailsでは標準で提供されていないので、ツールの準備は自分で行うことになります。

ImageMagickが有効になれば、アップロードされたファイルは自動的に(かつ非同期で)分析されメタデータを生成します。post.image.metadataを呼ぶことで、{"width"=>1200, "height"=>700, "analyzed"=>true}のようなハッシュを取得できます。

画像をImageMagick以外のライブラリで扱いたい方に残念なお知らせです。現時点のActive Storageにはimgproxyなどの画像処理ライブラリを使う公式の手段がまだありません。

秘密情報の扱いの改良

ここまではスイスイ進められましたが、他に何か必須の設定はあるのでしょうか?なくても大丈夫。Railsでお馴染みのomakase方式では、基本的な設定が既にconfig/storage.ymlに含まれています。このファイルは次のようになっています。

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

development.rbにも既にconfig.active_storage.service = :localが設定されているので、Active Storageをlocalhostで動かす場合にローカルディスクを使うことをRailsが認識します。yamlファイルにはAWS S3、Google GCS、マイクロソフトのAzureStorageのクラウドサービス向け設定もコメントアウト状態で含まれています。

storage.ymlは(gitなどで)ソース管理したいので、そこに秘密のキーを直接書き込むのはよくありません。Railsでは、そうしたキーをRails.application.credentialsに保存することが前提になっています。おや、そこは以前にはsecretsと呼ばれていたのではありませんか?はい、そのとおりです。しかしDHHは#30067で次のように説明しています。

config/secrets.ymlconfig/secrets.yml.encSECRET_BASE_KEYの組み合わせは混乱の元になります。これらの秘密情報に何を保存するべきなのかも、SECRET_BASE_KEYが一般的なセットアップと関連するものなのかどうかもはっきりしません。

この混乱に終止符を打つため、Railsでは「credential」という概念が新しく導入されました。

credentialは、config/credentials.yml.encファイルに暗号化された形で保存されるので、このファイルは安全にソース管理できます。もう環境変数の問題や、キー変更をチーム全員で同期する問題にわずらわされることはありません。

config/master.keyファイルは、いかなる場合であってもGitに登録してはいけません(このファイルはRails 5.2プロジェクトの.gitignoreに既に含まれています)。このファイルには、credentialを復号できる自動生成されたキーが含まれます。ドキュメントでも次のように警告されています。

このマスターキーを絶対に紛失しないこと!マスターキーは、チームがアクセス可能なパスワード管理ソフトウェアに保存すること。万一紛失すれば、あなたを含むいかなる人物も、暗号化されたcredentialに一切アクセスできなくなります。

では、内容が常に暗号化されているcredentialをどうやって編集すればよいのでしょうか? Rails 5.2から、そのための新しいタスクrails credentials:editが用意されています。このコマンドを実行すると、デフォルトのエディタで平文のテキストファイルが開くので、そこにkey_name: key_valueの形式でキーを書き込めます。Rails.application.credentials.key_nameでcredentialにアクセスできます。ネストしたキーを使っている場合はRails.application.credentials.dig(:section_name, :nested_key_name)でアクセスできます。一時ファイルを保存して閉じると、内容が暗号化されてconfig/credentials.yml.encに保存されます。ターミナルでrails credentials:showを実行するとキーを出力できます。

これで、同一のmaster.keyをチームのメンバー全員に渡せば、重要な情報が漏洩する心配をせずにGitで安全に共同作業できるようになります。production環境では、RAILS_MASTER_KEY環境変数のみ設定する必要があります。

メモ: Atomなどの外部エディタで編集する場合は、EDITOR="atom --wait" credentials:editのように実行する必要があります。修正量が少ない場合はEDITOR=vi credentials:editのようにシェルで動くエディタの方が手っ取り早いかもしれません。

Active Storageをクラウドで使う

Amazon S3を使って簡単なデモを行います。当然ながら、publicに読み出しアクセスできるS3バケット(bucket)が必要です。用意が整えば、後は以下の3つの手順を進めるだけで使えるようになります。

  1. storage.ymlamazon:セクションのコメントアウトを解除します。
  2. development.rbconfig.active_storage.service = :amazonを指定して、デフォルトのクラウドサービスをS3に切り替えます。
  3. コンソールでrails credentials:editと入力し、以下の要領でキーを入力します
aws:
  access_key_id: #idを入力
  secret_access_key: #アクセスキーを入力

これで完了です!アップロードしたファイルやvariantは自動的にAmazon S3経由で取り扱われます。post.image.service_urlは、バケットインスタンスを指す署名済みURLを生成します。

ダイレクトアップロード

Active Storageの開発が活発だった昨年夏、Fabio Akita氏は、Active Storageを有効にしたプロジェクトがHerokuなどのephemeralな(=短命な)ファイルシステムを用いるプラットフォーム上で動作する場合に、巨大なファイルを扱うことについて懸念を表明しました。Railsの作者DHHはTwitterのスレッドでブラウザからクラウドへの「ダイレクトアップロード(Direct Upload)」を実装することについて納得しました。ダイレクトアップロードは、Attachinaryが実装したCloudinaryストレージへのファイル送信と似たような方法で、アプリのバックエンドを完全にバイパスします。

ダイレクトアップロード機能については、Railsガイド(edge)にJavaScriptコードスニペットのよい説明が既に掲載されています3(最終的にCoffeeScriptではなくES6を使います!)。

ここではダイレクトアップロードを簡単に動かしてみましょう。以下の手順ではWebpackerを念頭に置いていますので、アプリでrails webpacker:installを実行しておいてください。

$ yarn add activestorage
// app/javascript/packs/application.js
import * as ActiveStorage from "activestorage";
import "../utils/direct_uploads.js"

ActiveStorage.start();
// app/javascript/utils/direct_uploads.js
// このフォルダとファイルを作成し、以下のコードをコピペする
// http://edgeguides.rubyonrails.org/active_storage_overview.html#example
<!-- app/views/posts/_form.html.erb -->
<div class="field">
  <%= form.label :images %>
  <%= form.file_field :images, multiple: true, direct_upload: true %>
</div>

上のdirect_upload: trueオプションによって、ファイルのフィールドのHTMLが以下のように生成されます。

<input multiple="multiple" data-direct-upload-url="http://localhost:3000/rails/active_storage/direct_uploads" name="post[images][]" id="post_images" type="file">

JavaScriptサンプルコードでは、Direct Upload用JSイベントを用いて、アップロードサイクルに従ってUIに応答を表示する(プログレスバーを更新するなど)方法が示されています。

EdgeガイドのDirect Uproad installationのサンプルCSSコードをコピーして、direct_uploads.cssファイルに貼り付けます(このファイルはapp/assets/stylesheetsの下に作成できます)。これで、サーバーを起動してpostページを開けば、巨大なファイルを選択できるようになり、長時間のタスクによってサーバーがブロックされていない様子を見ることができます。このアップロードは完全にXHRで行われ、プログレスバーが動的に更新されます。

Chromeでダイレクトアップロードする

ブラウザでファイルをダイレクトアップロードする

: 本記事執筆時点では、S3を使うダイレクトアップデートがFirefoxでXML Parsing Errorエラーになります。

S3を使う場合は、S3バケットの[Permissions]タブでCORS設定をオープンにする必要があります。以下はdevelopment環境で雑に動かすためだけの設定なので、production環境では設定を全開のままにしないでください!

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

「鏡よ鏡」

Active Storageのもうひとつの大きな機能はミラーリングです。ミラーリングによって、複数のクラウドストレージプロバイダの間でファイルを同期して冗長性を高めたり、クラウドの統合に役立てたりできます。たとえば、storage.ymlに以下を記述するとします。

production:
  service: Mirror
  primary: local
  mirrors:
    - amazon
    - google

続いて、使うクラウドサービスをproduction.rb:productionで設定します。これにより、アップロードされたファイルがローカルに保存されると同時にAmazon S3とGoogle Cloud Storageにもバックアップされます。ファイルを削除すると、クラウドからも削除されます。

Active Storageのクラウド関連について今のところ説明できるのは以上です。productionで通用することを示さなければならないので、これについてさらに洞察を提供できればと思います。

この辺で、Rails 5.2のその他の目玉機能についても見てみましょう。

Content-Security-Policyヘッダー設定用DSL

セキュリティはやはり重要です。ここでアプリのセキュリティをさらに強化することにしましょう4CSP(Content Security Policy)は、ブラウザが受信するネットワークリクエストを制限するために設計されています(ブラウザページで読み込んでよいものを指定するなど)が、副次的効果として、ブラウザから送信されるリクエストにも制限をかけることでたちの悪いハッキングを防止します。

Railsアプリで簡単にCSPを設定できるようになりました。グローバルに設定する場合は以下のようにします。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |p|
  # 現在のhostnameからの送信(セキュア通信のみ)をデフォルトですべて許可
  p.default_src :self, :https
  # data-urlからのフォントや画像の読み込みを許可
  p.font_src    :self, :https, :data
  # ActiveStorageアセットで使う可能性のあるhostnameは必ずここに追加すること
  p.img_src     :self, :https, :data, "cloudfront.example.com"
  # <object>タグの禁止(Flashさよなら)
  p.object_src  :none
  # インライン<style>を許可(オフにするには`:unsafe_inline`を削除)
  p.style_src   :self, :https, :unsafe_inline
end

コントローラごとに設定する場合は以下を使います(さらに動的になります)。

class PostsController < ApplicationController
  # グローバルポリシーのextend/オーバーライド
  content_security_policy do |p|
    # ユーザー固有のドメイン名をベースにするポリシーを設定
    p.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

ただし、(本記事執筆時点では)ドキュメントに記載されていない注意事項が1つあります。Webpackerとwebpack-dev-serverを使っている場合は、development環境でCSP設定を更新してhttp://localhost:3035ws://localhost:3035への接続を許可しなければなりません。これを行わないと、Webpackのホットリロード機能で必要なweb socket接続がRailsにブロックされてしまいます。詳しくはissue #31754をご覧ください。問題を発見したNick Savrovに感謝します。

# Webpackerを使う場合は以下のような感じでCSPを設定する必要がある
Rails.application.config.content_security_policy do |p|
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.style_src   :self, :https, :unsafe_inline

  if Rails.env.development?
    p.script_src :self, :https, :unsafe_eval
    p.default_src :self, :https, :unsafe_eval
    p.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035'
  else
    p.script_src :self, :https
    p.default_src :self, :https
  end
end

これは、RailsチームがCSP設定をデフォルトで無効にすることを決定した理由のひとつでもあります。

どこからでもアクセスできるCurrentシングルトン

モデルの中からcurrent_userにアクセスする方法を知りたいと思ったことはありますか?StackOverflowの“current_user”に関するすべての質問第3位は、このメソッドをモデルで使う方法についての質問です。

この状況はRails 5.2から変更される予定です。今回追加されるCurrentシングルトンのマジックは、アプリのあらゆる場所からアクセス可能なグローバルストアであるかのように振る舞います5

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

後はコントローラのどこかでuserを設定するだけで、モデル/ジョブ/メイラーなどあらゆる場所でアクセス可能になります。

class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# モデルで以下を行えるようになる
class Post < ApplicationRecord
  # post作成時のユーザー指定は不要
  # デフォルトでカレントユーザーが使われる
  belongs_to :user, default: -> { Current.user }
end

このアイデアは新しいものではありません。Steve Klabnik作のrequest_store gemを使えば、あらゆるRackアプリで同じことができます。

: 「この機能は『関心分離の法則』に違反している」と思われるかもしれません。おっしゃるとおりです。何かおかしいと思ったときは使わないでください。

訳注: 以下の記事も参考にどうぞ。

Railsの`CurrentAttributes`は有害である(翻訳)

HTTP/2 Early Hints

HTTP/2の「Early Hints」は、ブラウザのページ内でアセットを使う前にアセットを事前にダウンロードできるようにする機能です。これによってHTTP/2のパイプラインリクエストが効き、ページの読み込み時間が短縮されます。

Pumaサーバーに--early_hintsフラグを追加して起動し、HTTP/2互換プロキシ(h2oなど)をサーバーの前に配置するだけで使えるようになります。

Early Hintsについて詳しくは、この機能の作者であるEileen Uchitelleの記事『HTTP2 Early Hints』をお読みください。

訳注: 『HTTP の新しいステータスコード 103 Early Hints』も参考にどうぞ。

Bootsnap

Railsのような多機能なフレームワークで作業する場合のトレードオフのひとつが「起動時間」です。巨大なモノリシックアプリの起動やタスクの実行に1〜2分を要することがあります。

Railsアプリを事前に起動しておくSpring gemに加えて、RubyやYAMLのファイル読み込みを高速化するBootsnap(Shopifyのツール)という一片のマジックがデフォルトでGemfileに追加されたことで、コールドスタートが2〜4倍も速くなります6

Bootsnapは、Springの設定が面倒なDockerでの開発や、CIサーバーで利用する場合に特に便利です。


Rails 5.2はまだ新しいので、私たちがproduction環境で試す機会はこれまでありませんでした。皆さまの情報をTwitterで共有するときには、お気軽に私たちのアカウントにもメンションを下さい。フォームで直接お問い合わせいただくこともできます。本記事の作成で大変参考になったActive Storage記事の著者である、Mike Gunderloy氏に感謝いたします。


スタートアップをワープ速度で成長させられる地球外エンジニアよ!Evil Martiansのフォームにて待つ。

関連記事

Rails 5.2を待たずに今すぐActiveStorageを使ってみた(翻訳)

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

Rails: パーシャルと`collection:`でN+1を回避してビューを高速化(翻訳)


  1. 添付ファイルのblob(binary large object)とは、本質的に文字どおり巨大なバイナリファイルです。active_storage_blobsではバイナリファイルをデータベースに保存せず、情報(ファイルサイズ、コンテンツの種類、メタデータ)を元にファイルの置き場所だけをトラックします。 
  2. ActiveStorage::Serviceを継承することで、他のクラウドサービスのサポートも実装できます。 
  3. RailsプロジェクトでWebpackerを使う場合の詳しい設定については、以下の「新しいRailsフロントエンド開発」シリーズ記事のpart 1part 2part 3をお読みください。 
  4. 5.2より前のRailsについては、secureheaders gemをご覧ください。 
  5. これは技術的にはスレッドごとのストアであり、リクエストが終了するとクリアされるので、マルチスレッドアプリでも安全に使えます。 
  6. このマジックが好きでない場合は、rails new --skip-bootsnapを実行します。 

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: メモリ使用量を制限してHerokuのR14エラー修正&費用を節約した話(翻訳)

貯金箱でHerokuの費用節約をイメージ

理論的には、Herokuの512MB dynoが1つあればRails webサーバーとSidekiqプロセスを両方とも動かせます。トラフィックの少ないサイドプロジェクトで月7ドルを節約できればとても助かります。残念なことに、1つのdynoでRubyプロセスを2つ動かすとメモリの問題が生じることがあります。本記事では、Railsアプリのメモリ使用量を制限する方法について説明します。

最近読んだBilal Budhaniの良記事では、1つのHeroku dynoでSidekiqプロセスとPumaを同時に実行する方法について説明していました。私のサブプロジェクトの1つに適用したところ、R14エラーが大発生しました。

Error R14 (Memory quota exceeded)

Heroku R14 - Memory Quota Exceeded in Ruby errors

メモリ使用量が急上昇した後に、メモリエラーが大発生して自動的に再起動した

この問題を調査してメモリ使用量を最適化したところ、グラフは以下のようになりました。

Heroku R14 - Memory Quota Exceeded in Ruby fixed

メモリ使用量が安定し、その後ガベージコレクションが行われた

方法は次のとおりです。

1. Gemfileをダイエットする

Ruby世界にはさまざまなお便利gemがありますが、gemの削除は多くの場合最も楽チンな問題解決方法です。メモリの肥大化は、その他のコストよりも見落とされがちです。

gemごとのメモリ使用量をチェックするには、derailed benchmarksが最適です。

gem 'derailed_benchmarks', group: :development

Gemfileに上を追加してbundle exec derailed bundle:memを実行するだけでメモリ使用量をチェックできます。

私のプロジェクトではTwitterFacebookのボットプロファイルを強化します。今回驚いたのはtwitter gemが起動時に13MBものメモリを消費していたことです。最初にこのgemをより軽量なgrackleに置き換え(〜1MB)、最終的にTwitter APIへのHTTP呼び出しを行うカスタムコードを書きました。同様にkoala gem(〜1MB)も何とか取り除けました。

もうひとつ効果的だったのは、gon gem(〜6MB)をJavaScriptのカスタムデータ属性に置き換えたことです。

たった数行のJavaScriptコードを書かずに済ませたいという理由で、数MBのメモリを消費するgemファイルをいくつもインポートするのはぜひとも避けるべきです。

2. jemallocを使う

jemallocは、公式のMRIメモリアロケータの代わりに使えます。Herokuの場合、buildpackを使ってjemallocを追加できます。その結果、私のアプリではメモリ使用量が最大20%も削減されました。production環境にデプロイする前に、staging環境でjemallocを徹底的にテストしておきましょう。

3. コンカレンシーとワーカー数を制限する

トラフィックの多くないサブプロジェクトでは、スループットはさほど必要にならないでしょう。SidekiqやPumaのワーカー数やスレッド数を減らすことでメモリ使用量を制限できます。私のconfig/puma.rbは以下のとおりです。

threads_count = 1
threads threads_count, threads_count
port        ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "production" }
workers 1

preload_app!

on_worker_boot do
  @sidekiq_pid ||= spawn('bundle exec sidekiq -t 1')
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

on_restart do
  Sidekiq.redis.shutdown { |conn| conn.close }
end

plugin :tmp_restart

config/sidekiq.ymlは次のとおりです。

---
:concurrency: 1
:queues:
  - default
  - [critical, 100]

Pumaは、スレッドの最大数に1を指定しても最大7つまでスレッドを生成できます。これらの最小限の設定でも、私のSmart Wishlistアプリは100K程度のSidekiqジョブを引き続き処理可能であり、ReactフロントエンドとモバイルJSON APIの両方のサービスをこなしています。

4. JSONパーサーを最適化する

これらのSidekiqジョブは、iTunes API(一括リクエストは私のToDoリストで行います)から最新の価格をダウンロードしたりディスカウントを通知したりするのに必要です。つまり、そこではJSONのパースが相当行われているということです。こういう場合は、以下の1行修正でメモリ使用量とパフォーマンスの両方が改善されます。

gem 'yajl-ruby', require: 'yajl/json_gem'

yaji-rubyは、JSON gemと互換性のあるAPIを提供します。JSON.parse呼び出しをフックしてパフォーマンスとメモリ使用量を改善します。

まとめ

制限された環境での作業は、プログラミングのスキルを鍛えて新しい最適化方法を発見するよい方法のひとつです。理論的にはサーバーのメモリを増やせばいつでも問題を解決できますが、それよりも7ドル節約しませんか。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

データベースのランダム読み出しは要注意(翻訳)

「巨大プルリク1件vs細かいプルリク100件」問題を考える(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

「巨大プルリク1件vs細かいプルリク100件」問題を考える(翻訳)

本記事では、昔ながらの問題である「巨大なプルリク1件と超細かいプルリク100件、どっちなら戦う気になれる?」に対する回答を示したいと思います。チームの一員としてよりよいコードを書くためのガイドラインについてもある程度解説します。今回の記事は、すべて以下のツイートから触発されました。

何が問題だったか

私は、Fullscript社で行われているコードレビューが今ひとつ活用されていないことに気づきました。featureブランチが長期間取り残されていることがしょっちゅうでした。featureブランチのコードは数千行にまで肥大化し、まともなフィードバックを返すことはおろか、レビューに途方もない時間がかかる始末です。心底ゲンナリでした。

開発者は、時間とやる気が満ちてくるまでレビューを先延ばしにしたりするので、開発プロセスが停滞してしまうことがあります。レビュアーは「レビューしなければ」というプレッシャーを肌で感じつつ、コードの表面をさっと眺めて「LGTM!👍」(良さげ)などと書いて終わらせることもよくあります。

たとえレビューがうまくいったとしても、大規模なリファクタリングをかけるにはタイミング的に手遅れになることもしばしばです。初期段階の設計ミスが頑固に根を張り、修正コストはスタートアップ企業が到底負担しきれないほどに跳ね上がってしまいます。ぐらついている基礎の上で何週間も作業を重ねたこともありました。

残念なことに、コードの品質は詳細な検査が必要なレベルにすら達しませんでした。誰もそこから学んでおらず、レビュープロセスは頓挫してしまったのです。

ほほう、ではどうやって改善する?

レビューを依頼するということは、他の誰かに「責任を共有してください🙇」とお願いするということです。依頼された人は問題を理解し、あなたのコードを把握し、問題がなければ(そう願いたいものです)承認しなければなりません。私たちは開発者として、この作業をできる限り軽減すべきです。

「早期」かつ「頻繁に」

フィードバックを依頼するのは、最も重要な時期、すなわち開発プロセスの早い段階で行うようにしましょう。こうすることで、レビュアーは設計上の問題を早い段階で検出する機会を得られますし、あなたが確かな基礎の上にコードを構築していることを担保できるようになります。チームは、開発プロセスが進んでからの書き直しというコストの高い作業や、既知の欠陥を持つコードを時間や予算の制約のせいでそのままmergeする事態を回避できます。

粒度を小さくする

「小さく」というのは、限りなくゼロ行に近づけるということです。たった1行の変更のレビューを嫌がるレビュアーはいないでしょう。プルリクのサイズ(=コードの行数)に上限を設けることで、粒度を下げやすくする効果が著しく向上します。レビュアーが1行ずつ丁寧に調べやすくなるのはもちろんのこと、レビュー時間も大きく削減できます。コードを定期的にmergeできるようになり、品質にも自信を持てるようになります。

作業のスコープを絞る

コードの行数を削減するための重要なコツは、プルリクのスコープ(=機能のセット)を絞り込み、解決するタスクを1つにする(または密接に関連する少数のタスクに絞り込む)ことです。スコープを絞ることで、レビュアーの認知機能にかけられる負荷を大きく軽減できます。1件のプルリクでいくつもの問題をいっぺんに解決しようとすると、ある問題がどのコードと関連しているのかを整理する作業がつらくなります。

機能が未完成でもリリースする(ただし内緒で)

機能が完成するまでリリースを差し止めることは比較的普通に行われます。残念なことに、これは巨大なfeatureブランチがいつまで経ってもなくならない主要な原因のひとつです。masterブランチでの開発が進むに連れて、mergeのコンフリクトやrebaseなどの楽しい事件が起きがちです。たとえmergeできたとしても、大規模な変更を無事にデプロイするのはチームにとって神経を削る作業です。目玉機能や大きな依存関係のアップグレードをデプロイする場合はなおさらです。

機能を一時的に取り消すツールを使って、未完成の機能をユーザーの目から隠しておくという手があります。これなら、コードのmergeやリリースの頻度が落ちずに済みますし、心配の種も減らせます。開発が完了に近づいたら、特定のアカウントやアーリーアダプタ(訳注: 新機能を喜んで使うユーザー)にだけ新機能へのアクセスをぼちぼち許可できるようになります。デプロイ作業の心配も減りますし、機能の成熟度に応じて機能へのアクセスを制御できるようになります。

追伸: Ruby on Railsをお使いの方には、flipper gemを強くおすすめいたします。

事前の計画

開発者がこうした制約のもとで作業すると、問題を細かな単位に分割して渡すようになります。この制約は、そのような明確な計画のない開発を手がけようとする誘惑を払いのけるのにも役立ちます。これには少々経験が必要ですが、そのうちに慣れて、自然に開発プロセスの一部に組み込まれるでしょう。

「ついでのリファクタリング」はしないこと

開発者は、問題に気づくとその場でリファクタリングすることがよくあります。リファクタリングはよいことですが、別のプルリクで行うべきです。そうすることでリファクタリングを早めにmergeできますし、関係のない機能リリースに修正が結び付けられることもなくなります。「ついでの改善」は、元々のプルリクのリリースが遅れれば巻き添えで遅れてしまいますし、最悪まったくリリースされなければそのまま失われてしまうでしょう。

やってみよう

コードレビューは、チーム内でのソフトウェア作成になくてはならない作業です。コードレビューは知識を共有する場であり、コードの品質を監視する門番でもあります。上述の制約の元で作業するようになったことで、Fullscript社のコードレビューで次のような成果を得られました。

  • コードレビューが短時間で完了し、品質も向上した
  • リリースのテストが容易になり、デプロイの苦労も大きく軽減された
  • 問題が発生した場合の変更の取り消しやロールバックが簡単になった

単純な話のように見えるかもしれません(し、実際そうかもしれません)が、実践のためにはチームが一丸となって経験を積む必要があります。本記事でお伝えしているメッセージに共感いただけましたら、始め方についてチームメンバーと話してみることをおすすめします。


ご意見や疑問がありましたら、元記事のコメント欄かTwitterまでお気軽にどうぞ。

また、本記事は私が地元のミートアップで発表したスピーチを元にしています。このスピーチは元々、大規模チームでのソフトウェア開発のアプローチについてKevin McPhillipsWillem van Bergenと雑談したときに閃いたものです。お二人に感謝します。

関連記事

いい感じのコードには速攻でLGTM画像を貼ってあげよう

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

技術的負債を調査する10のポイント(翻訳)

Viewing all 1838 articles
Browse latest View live