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

週刊Railsウォッチ(20171110)dry-rbでFormObjectを作る、RailsのSQLインジェクション手法サイト、年に1度だけ起きるバグほか

$
0
0

こんにちは、hachi8833です。先週は文化の日でウォッチをお休みいたしました。

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

RubyWorld Conference 2017無事終了


2017.rubyworld-conf.orgより

今年も盛り上がったようです。皆様お疲れさまでした。


つっつきボイス: 「今年は残念ながら行けなかったんで、松江の馴染みのおでん屋食べられなかった(´・ω・`)」「今回はクックパッドの発表がいつもより多かったみたいでした」

Rails: 今週の改修

改良: beforeSendを付けずにRails.ajaxを呼び出せるようになった

// actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
-  unless options.beforeSend?(xhr, options)
+  if options.beforeSend? && !options.beforeSend(xhr, options)

// 変更前 Rails.ajax({ type: 'get', url: '/', beforeSend: function() { return true }, success: function() { // do something } // 変更後 Rails.ajax({ type: 'get', url: '/', success: function() { // do something }

つっつきボイス: 「以前は使わないときにもbeforeSend:書かないといけなかったのか」

改善: rescue画面でソースが改行されないようになった


つっつきボイス: 「行数表示と実際の行が一致するようになったのね」「エディタではワードラップする方が好きなんですが、英語圏だとエディタをワードラップしない人が割りと多い気がします」

Railsから非推奨コードを削除

以下の非推奨コードが削除されました。

  • erubis: 今後はerubiになります。
  • evented Redisアダプタ
  • ActiveRecordの以下を削除
    • #sanitize_conditions
    • #scope_chain
    • .error_on_ignored_order_or_limit設定
    • #verify!の引数
    • #indexesname引数
    • ActiveRecord::Migrator.schema_migrations_table_name
    • supports_primary_key?
    • supports_migrations?
    • initialize_schema_migrations_tableinitialize_internal_metadata_table
    • dirty recordへのlock!呼び出し
    • 関連付けでクラスに:class_nameを渡す機能
    • index_name_exists?default引数
    • ActiveRecordオブジェクトの型変換時のquoted_idサポート
  • ActiveSupportの以下を削除
    • halt_callback_chains_on_return_false
    • コールバック用文字列フィルタの:if:unlessオプション

改良: ActionDispatch::SystemTestCaseにフックを追加

y-yagiさんです。

# actionpack/lib/action_dispatch/system_test_case.rb
+
+    ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)

#redirect_backallow_other_hostを追加

# actionpack/lib/action_controller/metal/redirecting.rb
-    def redirect_back(fallback_location:, **args)
-      if referer = request.headers["Referer"]
-        redirect_to referer, **args
      else
-        redirect_to fallback_location, **args
-      end
+    def redirect_back(fallback_location:, allow_other_host: true, **args)
+      referer = request.headers["Referer"]
+      redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
+      redirect_to redirect_to_referer ? referer : fallback_location, **args
     end

つっつきボイス:redirect_backはリファラを見て前画面に戻るやつですね」「前は他のサイトに戻れないよう設定できなかったのか」

Rails

dry-rbでRailsにForm Objectを作る

cucumbersome.netより

# 同記事より
PostcardSchema = Dry::Validation.Schema do
  required(:address).filled
  required(:city).filled
  required(:zip_code).filled
  required(:content).filled
  required(:country).filled
end

つっつきボイス: 「実は今、dry-rbシリーズにどんなgemがあるのかざっくりチェックする記事を準備しているんですが、そのきっかけは同じ作者の@solnicさん(Ruby Prize 2017受賞者)のvirtusのReadmeで以下の記述を見たことでした: よく見たらもう何年も更新されてなくて、最新Rubyでの動作も確認されてないみたいなので」

さらなる進化へ
Virtusを作ったことで、Rubyにおけるデータの扱い、中でもcoercion/型安全/バリデーションについて多くのことを学べました。このプロジェクトは成功を収めて多くの人々が役立ててくれましたが、私はさらによいものを作ることを決心しました。その結果、dry-typesdry-structdry-validationが誕生しました。これらのプロジェクトはVirtusの後継者と考えるべきであり、考慮点やより優れた機能がさらにうまく分割されています。Virtusが解決しようとした同種の現代的な問題について関心がおありの方は、ぜひdryシリーズのプロジェクトもチェックしてみてください。

@solnic
https://github.com/solnic/virtus より抄訳

「なるほど、virtusはとってもいいgemだけど今後はdry-rb系gemがメンテされそう: 次にFormObject作るときはこれ参考にしよう」
「dry-validationはActiveRecordのバリデーション+strong parametersより数倍速いと謳ってますね」「でも本家のバリデーションはRailsの記法にマッチしているからそっちを使いたい」

Service Objectにちょっとうんざり(Awesome Rubyより)


avdi.codesより

# 同記事より
module Perkolator
  def self.process_ipn(params:, redemption_url_template:, email_options:)
    # ...
  end
end

post "/ipn" do
  demand_basic_auth

  redemption_url_template =
    Addressable::Template.new("#{request.base_url}/redeem?token={token}")

  Perkolator.process_ipn(
    params: params,
    redemption_url_template: redemption_url_template,
    email_options: settings.email_options)

  # Report success
  [202, "Accepted"]
end

つっつきボイス: 「Service Objectにクラスメソッドを実装する的な話か」
「Service Objectのメソッドをインスタンスメソッドとして実装するかどうかはケースバイケースなんだけど、一度クラスメソッドとして実装してしまうと後からインスタンスメソッド化するのがものすごく面倒になるのが特に厄介」
「クラスメソッドにしてしまってマルチスレッドが効かなくなるとか、あるあるですな」
「自分の場合、形はクラスメソッドだけど内部でself.newして事実上インスタンスメソッドにすることある」

ほぼほぼすべてのプロジェクトで使う25のgem(RubyFlowより)


hackernoon.comより


つっつきボイス: 「これまでRailsウォッチでいろんなgemを取り上げてきたせいか、初めて見るgemがほとんどありませんでした: それだけ定番ってことですね」「こうやって何度も見ていると、もう見ただけで機能を思い出すようになってくる」
「そうそう、ところでmoney-rails#monetizeってメソッド、これが言いたくて作ったとしか思えない」「『誰がうまいこと言えと』的なやつですね」

class Product < ActiveRecord::Base

  monetize :price_cents

end

RailsでPostgreSQLのパーティショニングを使ってみる(RubyFlowより)


evilmartians.comより

# 同記事より
CREATE OR REPLACE FUNCTION orders_partitioned_view_insert_trigger_procedure() RETURNS TRIGGER AS $BODY$
  DECLARE
    partition TEXT;
    partition_country TEXT;
    partition_date TIMESTAMP;
  BEGIN
...

つっつきボイス: 「もうパーティショニングどうこうというより、PostgreSQLの機能がそもそも凄すぎてそっちに驚く」「『こんな機能があったのか』みたいなのが次から次に出てくるし」
「記事タイトルはCommand & Conquerのもじりかな」「あー、ゲームネタなのか(ゲーム音痴なので)」

⭐rails-sqli.org: RailsのSQLインジェクション手法を一覧できるサイト⭐


rails-sqli.orgより

今気づきましたが、Rails 5Rails 4Rails 3それぞれでSQLインジェクション手法をどっさり紹介しています。

# pluckを使ったインジェクション
params[:column] = "password FROM users--"
Order.pluck(params[:column])
Query
SELECT password FROM users-- FROM "orders"
Result
["Bobpass", "Jimpass", "Sarahpass", "Tinapass", "Tonypass", "supersecretpass"]

つっつきボイス: 「おお、これ( ・∀・)イイ!!: チェックに使える」「いろんなインジェクションがあるんだなー」「何にしろparamsに入力を直接突っ込む時点でアウトですな」「翻訳したい気もするけど翻訳するところがないw」「見ればわかりますからねー」

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

barbeque: Dockerでジョブを実行する、クックパッド製ジョブキューgem

RubyWorld Conference 2017でも言及されていたようです。


つっつきボイス: 「クックパッドは多分ここ最近マイクロサービス化を進めているので、その中でバッチジョブもdocker containerの実行にすればより分散しやすくなるということかな」「Aaron Pattersonさんのインタビュー↓でもGitHubがマイクロサービス化を進めているという話がありましたね」「分散しないとRailsアプリ本体の起動が死ぬほど遅くなる」

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

RailsでのStripe.com決済処理をRSpecでテストする(RubyFlowより)


hackernoon.comより

# 同記事より
require 'rails_helper'
include ActiveJob::TestHelper
RSpec.describe Payments::InvoicePaymentSucceeded, type: :mailer do
  let(:plan) { @stripe_test_helper.create_plan(id: 'free', amount: 0) }
before(:each) do
    @admin = FactoryGirl.create(:user, email: 'awesome@dabomb.com')
    PaymentServices::Stripe::Subscription::CreationService.(
      user: @admin,
      account: @admin.account,
      plan: plan.id
    )
    @event = StripeMock.mock_webhook_event(
      'invoice.payment_succeeded',
      customer: @admin.account.subscription.stripe_customer_id,
      subscription: @admin.account.subscription.stripe_subscription_id
    )
  end
it 'job is created' do
    ActiveJob::Base.queue_adapter = :test
    expect do
      Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
    end.to have_enqueued_job.on_queue('mailers')
  end
it 'email is sent' do
    expect do
      perform_enqueued_jobs do
        Payments::InvoicePaymentSucceeded.email(@event.id).deliver_later
      end
    end.to change { ActionMailer::Base.deliveries.size }.by(1)
  end

つっつきボイス: 「Stripeは有名な決済サイトですね: こういうところにはテスト用のサンドボックス的環境が用意されているのが普通」
「そういえばPayPalのサンドボックスはよくできてるんですが、惜しいことにドキュメントがものすごく読みづらい」「私も以前試したけど結局よくわからんかった」


stripe.comより

Fat Free CRM: Railsで作られたOSSカスタマーリレーション管理(Awesome Rubyより)

www.fatfreecrm.comより


つっつきボイス: 「日本でこれをカスタマイズして使うのは考えにくいけど、ソースコードが結構きれいでよく書けている感じなので、実際に業務で動くアプリとしてソース読むとかテストコードの書き方や設計を学ぶのにいいかも」
「あまり大きくなさそうだし、git cloneしてIDEでじっくり読んでみようかな」「RedmineやGitLabだと巨大すぎて追うのが大変ですからね」「Rails 5.0.4か」

github.com/fatfreecrm/fat_free_crmより

flipper: 特定の機能を動的にオン/オフ/状態確認できるgem(RubyFlowより)

# github.com/jnunemaker/flipperより
require 'flipper'
require 'flipper/adapters/memory'

Flipper.configure do |config|
  config.default do
    # pick an adapter, this uses memory, any will do
    adapter = Flipper::Adapters::Memory.new

    # pass adapter to handy DSL instance
    Flipper.new(adapter)
  end
end

# 検索が有効かどうかをチェック
if Flipper.enabled?(:search)
  puts 'Search away!'
else
  puts 'No search for you!'
end

puts 'Enabling Search...'
Flipper.enable(:search)

# 検索が有効かどうかをチェック
if Flipper.enabled?(:search)
  puts 'Search away!'
else
  puts 'No search for you!'
end

有料版のFlipper::Cloudもあるようです。


つっつきボイス: 「クックパッドのchanko gemみたいなやつかな」


cookpad.github.io/chankoより

「ところで、flipperがなぜイルカなのかわかります?」「わかんね」「わかんね」「Officeイルカじゃなさそうだけど」「私が子供の頃『わんぱくフリッパー』っていう米国制作の番組が放映されてたんです(年即バレ)」

モデルになったとされるイルカは、不機嫌になるとすぐ芸の道具を全部ひっくり返す癖があったのでflipperというあだ名になったというのを何かで読んだ覚えがあります。

年に1度だけ起きるバグ(RubyFlowより)

とても短い記事です。

# 同記事より
event = test_organizer.create_published_event(starts_at: 25.hours.from_now)

つっつきボイス: 「年1バグとか普通にあるけど、これはどういうやつかな?以前のウォッチで扱ったActiveSupport::Durationでもなさそうだし」「25?」「あーサマータイムか」「サマータイムがある国の開発者(´・ω・)カワイソス」

週刊Railsウォッチ(20170120)Ruby 2.5.0 devリリース、古いMySQLのサポート終了、uniqメソッドが削除ほか

「そういえば米国ではsummer timeではなくDSTって書きますね: 自分もsummertimeというとスタンダードナンバーを連想します」

ジュニア開発者へのRails設計アドバイス(Awesome Rubyより)

以下は見出しから。

  • 純粋なRubyオブジェクトとデザインパターンを恐れず使うべし
  • (暗黙的でない)明示的なコードにすべし
  • アプリを水平分割すべし
  • 機能以外の本当の要件を満たす設計を
  • テストは徹底的に行うべし
  • 継承よりコンポジションを優先すべし
  • 制御フローを尊重すべし

つっつきボイス: 「うん、悪くなさそう: Railsに限らない話もいろいろある」「after_create/after_update/after_destroy/after_commitを避けろというのは大事: after_系フックを使うならちゃんと値を返すべきだし、自分自身を変えないこと」

reek: 「コードの匂い」を検出するgem(Awesome Rubyより)


github.com/troessner/reekより

gem install reekしてオレオレRailsアプリにかけてみたらこんな感じで出ました。ドキュメントのURLも出力してくれます。Rubyに絆創膏。

app/controllers/patterns_controller.rb -- 12 warnings:
  [41, 43]:DuplicateMethodCall: PatternsController#create calls 'format.html' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 87]:DuplicateMethodCall: PatternsController#update calls '@pattern[:id]' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 87]:DuplicateMethodCall: PatternsController#update calls 'edit_pattern_path(@pattern[:id])' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [80, 82]:DuplicateMethodCall: PatternsController#update calls 'format.html' 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [7]:InstanceVariableAssumption: PatternsController assumes too much for instance variable '@pattern' [https://github.com/troessner/reek/blob/master/docs/Instance-Variable-Assumption.md]
  [35]:TooManyStatements: PatternsController#create has approx 11 statements
...

つっつきボイス:rubocopのcopにして欲しいー: rubocopと他のツールのwarningを調整するの大変だし」

activerecord-import: 一括インポートgem+バリデーション(Awesome Rubyより)

# wikiより
columns = [ :title, :author ]
values = [ ['Book1', 'FooManChu'], ['Book2', 'Bob Jones'] ]

# Importing without model validations
Book.import columns, values, :validate => false

# Import with model validations
Book.import columns, values, :validate => true

# when not specified :validate defaults to true
Book.import columns, values

つっつきボイス: 「前にもウォッチで触れたことありましたが一応」「activerecord-importはbulk insert gemとしては使いやすくて有能なやつですね: バリデーションもやってくれるし」「お、ちょうど今の案件に使えそうダナ」

dckerize: RailsアプリのDockerイメージを作るgem(RubyFlowより)

# 同リポジトリより
$ rails new myapp --database=postgresql
$ cd myapp
$ dckerize up myapp

# DBファイルを設定

$ docker-compose build
$ docker-compose up

つっつきボイス: 「とりあえず動かしてみたんですが、PostgreSQLのコンテナのところでつっかえてしまいました」「うーん、PostgreSQL 9.5.3のバージョンべた書きだったり、/var/lib/postgresqlに直接つっこんだりしてるしなー: 自分専用なんじゃ?」「そんな感じですね: 自分でdocker-compose書くのがよさそう」「Dockerやったことない人がお試しに動かすきっかけにはなるかも」

Ruby trunkより

提案: Enumerator#next?

class Enumerator
  def next?
    peek
    true
  rescue StopIteration
    false
  end
end

a = [1,2,3]
e = a.to_enum
p e.next?   #=> true
p e.next    #=> 1
p e.next?   #=> true
p e.next    #=> 2
p e.next?   #=> true
p e.next    #=> 3
p e.next?   #=> false
p e.next    #raises StopIteration

つっつきボイス: 「へー、next?って今までなかったのか」「採用されるかどうかはmatz次第みたいです」

2つのArrayの間にカンマがないとnilになる => 仕様どおり(却下)

[2, 2][3, 3] # => nil

つっつきボイス: 「これ、仕様どおりですよね」「Stackoverflowレベルの内容をRubyバグに投げるのは勇者」

Ruby

RubyGems 2.7.0がリリース

今見たらもう2.7.2になっています。

  • 2.7.0
    • Update vendored bundler-1.16.0. Pull request #2051 by Samuel Giddins.
    • Use Bundler for Gem.use_gemdeps. Pull request #1674 by Samuel Giddins.
    • Add command signin to gem CLI. Pull request #1944 by Shiva Bhusal.
    • Add Logout feature to CLI. Pull request #1938 by Shiva Bhusal.
  • 2.7.1
    • Fix gem update –system with RubyGems 2.7+. Pull request #2054 by Samuel Giddins.
  • 2.7.2
    • Added template files to vendoerd bundler. Pull request #2065 by SHIBATA Hiroshi.
    • Added workaround for non-git environment. Pull request #2066 by SHIBATA Hiroshi.

#2065と#2066、ちょうど昨日踏んでしまいましたが、gem update --systemで即修正完了でした。

Ruby 2.5で速度が改善された点(RubyFlowより)


rubyguides.comより

  • 式展開
  • String#prepend
  • Enumerableのメソッド
  • Range#minRange#max
  • String#scan

つっつきボイス: 「式展開の改善は目覚ましい」「他のは誤差っぽいかなー」「何にしろRuby 3×3に向けて着々と進んでいる感じですね」

メモリを意識したRubyプログラミング


gettalong.orgより

# 同記事より
2.4.2 > require 'objspace'
 => true
2.4.2 > ObjectSpace.memsize_of(nil)
 => 0
2.4.2 > ObjectSpace.memsize_of(true)
 => 0
2.4.2 > ObjectSpace.memsize_of(false)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62-1)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62)
 => 40

つっつきボイス: 「おー、オブジェクトサイズの変わる境界がいろいろ示されていて面白い」「文字列は23バイト目から変わる、と」「ここらへんはRubyのRVALUEみたいな内部構造に関わってるはず」「近々それ系の翻訳記事出します」「この記事見てて、k0kubunさんのこのツイートを思い出しました↓: 最速のメソッドを身体がつい選んでしまうとかもう常人じゃない感」

RubyとPythonで文字列分割対決(Awesome Rubyより)


chriszetter.comより

# ruby
"".split("-") #=> []
# python
"".split("-") #=> [""]

つっつきボイス: 「Rubyが空の[]を返すのはAWKに近いのか」「著者はPythonにstr.splitの挙動をAWKに近づけるよう提案したけど通らなかったらしいです」

商用で使われるRubyのバージョン分布

何となく年の瀬が近づいた感じがしてきました。


つっつきボイス: 「おー、1.8系はほぼ消滅か」「2.0以上でもう8割ですね」「Rubyが後方互換性を大事にしているおかげでみんな結構気軽にアップグレードしてる感ある」「frozen_string_literalのような変更にもちゃんと猶予期間を設けてますね」

k0kubunさんのyarv-mjitチューンアップ


つっつきボイス: 「やっぱりoptcarrotでやってます」「60fps超えてるー」

SQL

「Mastering PostgreSQL」が発売


masteringpostgresql.comより

著者のメルマガで知りました。


つっつきボイス: 「タイムセールに乗って即買いました」「そういえば好評につきタイムセールを48時間伸ばしたって通知メールに書いてありました」
「PostgreSQL 10の後に出てるからそちらもカバーはしているけど、PostgreSQLそのものを掘り下げる内容」
「この本はボリュームディスカウントもあって、2冊分の価格(179ドル)で50人まで買えますね: 社内で3人以上買うならこれの方がお得」

PostgreSQL 10の嬉しい点5つ(Postgres Weeklyより)


10clouds.comより

  • idカラムの指定がSQL準拠に
  • ネイティブのパーティショニング機能
  • 複数カラムのstatistics
  • 並列性向上
  • JSON/JSONBの全文検索
# 同記事より
# 9
CREATE TABLE foo (id SERIAL PRIMARY KEY, val1 INTEGER);

#10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);

つっつきボイス: 「おーやっぱすごい」「JSONの全文検索もうれしいけど、バッドプラクティスになりやすいので注意かな」
「ところでPostgreSQLのexplainって、MySQLのよりずっとずっと読みやすくて助かる」「全面同意します!」「PostgreSQLはindex作成中でもクエリかけられるし」

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

JavaScript

JavaScriptのasyncとawaitがひと目でわかる動画(15秒)

社内Slackに投下してもらって知りました。


つっつきボイス: 「これマジでわかりやすい」「マジ」

5分でわかるJavaScriptのpromise


codeburst.ioより

Angular.js 5.0.0リリース


blog.angular.ioより

更新情報がいっぱいありすぎて書ききれない感じです。


つっつきボイス:CLDR対応は大きいかも」


cldr.unicode.orgより

TestCafe: Selenium要らずのWeb結合テストサービス(JavaScript Liveより)


devexpress.github.io/testcafeより

Node.js用です。


www.dedicatedcode.comより


つっつきボイス: 「この元記事の方、コードの配色がすごく読みにくい…」

Frappé Charts: GitHub風グラフ表示ライブラリ(Frontend Focusより)

依存関係なしで使えるそうです。

github.com/frappe/chartsより

CSS/HTML/フロントエンド

フロントエンドチェックリスト


codeburst.ioより

メタタグ/CSS/画像などのチェック項目リストです。


つっつきボイス: 「長い…ここまで増えたら自動化したい」

CSSだけでできる新しめのフォームデザイン(Frontend Focusより)


jonathan-harrell.comより

プレースホルダ文字を動的に移動するなどのテクニックが紹介されています。


jonathan-harrell.comより

FlexGrid: 有料のテーブル作成ライブラリ(Frontend Focusより)


grapecity.comより

非常に凝ったテーブルを作れるJSライブラリです。これも依存なしに使え、AngularとReactでも使えるそうです。


demos.wijmo.comより

その他

最も嫌われているプログラミング言語

stackoverflow.blogより

みっちりと書かれています。


stackoverflow.blogより


つっつきボイス: 「これも面白い記事」「Perlが圧勝」「CoffeeScript…(´・ω・)カワイソス」

番外

ダイヤルアップモデムの音と波形(HackerNewsより)

これはもうリンク先をご覧ください。


つっつきボイス: 「テレホタイム思い出した」「この音知らない人増えたでしょうね(年バレ)」「音は聞いたことあります」

最初のフック音が米国の電話のトーンだったので、大昔にどきどきしながら国際電話かけたときのことをつい思い出してしまいました。

足を鍛えると知能も鍛えられる?

鍛えるなら足というか下半身かなと思いました。

ニューラルネットワークをだます

この亀のフィギュアを銃と誤認させることに成功したそうです。


今週は以上です。

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

週刊Railsウォッチ(20171026)factory_girlが突然factory_botに改名、Ruby Prize最終候補者決定、PhantomJS廃止、FireFoxのFireBug終了ほか

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

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

Rails公式ニュース

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Hacker News

160928_1654_q6srdR


Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

$
0
0

概要

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

画像はすべて元記事からの引用です。

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

まとめ: アプリのサーバー設定はRuby Webアプリのスループットやコストあたりのパフォーマンスに大きな影響を与えます。設定の中でも最も重要なものについて解説します(2846 word、13分)

RubyのWebアプリサーバーは、ある意味で自動車のガソリンに似ています。よいものを使ってもそれ以上速くなりませんが、粗悪なものを使えば止まってしまいます。実際にはアプリサーバーでアプリを著しく高速化することはできません。どのサーバーもだいたい同じようなものであり、取っ替え引っ替えしたところでスループットやレスポンスタイムが向上するわけではありません。しかしダメな設定を使ったりサーバーで設定ミスしたりすれば、たちまち自分の足を撃ち抜く結果になります。クライアントのアプリでよく見かける問題のひとつがこれです。

本記事では、3つの主要なRuby アプリサーバーであるPuma、Unicorn、Passengerについてリソース(メモリやCPU)の使用状況の最適化やスループット(要するに1秒あたりのリクエスト数です)の最大化について解説します。本記事では仮想環境に特化した話はしませんので、「サーバー」と「コンテナ」という言葉を同じ意味で使います。

3つのサーバーの設計は本質的に同じなので、1つのガイドで3つの著名なアプリサーバーをカバーできます。どのサーバーもfork()システムコールを使っていくつもの子プロセスを生成してからリクエストを処理します1。これらサーバーの違いのほとんどは細かい部分にとどまります(本記事ではパフォーマンス最大化が重要な箇所でこうした詳細にも触れる予定です)。

本ガイドを通じて、コストあたりのサーバースループットの最大化を試みます。サーバーリソース(とキャッシュも)が最小の状態から、サーバーが扱える秒あたりのリクエスト数を最大化したいと思います。

パフォーマンス最大化でもっとも重要な設定

アプリサーバーには、パフォーマンスやリソース消費を決定する基本的な設定が4つあります。

  • 子プロセスの数
  • スレッドの数
  • Copy-on-Write
  • コンテナのサイズ

それぞれの設定を見ていきましょう。


* 看護師: 「あなたは31秒前からdynoですね」
* 患者: 「マジで!すぐユーザーのところに戻って、せっかく作ったこのレスポンスを返しに行かなきゃ…」
タイムアウトもそれなりに重要ですが、スループットにはそれほど関連しません。私は今後のためにタイムアウトは変更しないでおきます。

子プロセスの数

Unicorn、Puma、Passengerは、いずれもforkを使う設計になっています2。つまり、アプリのプロセスを1つ作成し、そこから多数のコピーを作成します。これらのコピーは子プロセスと呼ばれます。サーバーごとの子プロセス数は、コストあたりのスループット最大化でおそらく最も重要な設定でしょう3

私が推奨する設定は、すべてのRuby Webアプリで1つのサーバーにつきプロセスを3つ以上実行することです。この設定によってルーティングで最大のパフォーマンスを得られます。PumaとUnicornは、どちらも複数の子プロセスが1つのソケットで直接リッスンする設計になっており、プロセス間のロードバランシングはOSが行います。PassengerはnginxやApacheなどのリバースプロキシを用いて多数のリクエストを1つの子プロセスにルーティングします4。どちらのアプローチも効率はかなり高く、リクエストはアイドリング中のワーカーに素早くルーティングされます。同じことを上位レイヤでのルーティング(ロードバランサーやHerokuのHTTPメッシュを指します)で効率よく行うのは、ルーティング先のサーバーがビジーかどうかをロードバランサー側から確認できないことが多いため、かなり難しくなります5

サーバーが3個、1サーバーあたり1プロセス(つまりプロセスは全部で3個)の編成で考えてみましょう。このときロードバランサーはどのようにして1つのリクエストを3つのサーバーのいずれかに適切にルーティングするのでしょうか。「ランダムに選ぶ」方法や「ラウンドロビン」方式でも可能ですが、その場合アイドリング状態のサーバーへのルーティングは保証されません。たとえばラウンドロビン戦略で、リクエストAがサーバー#1にルーティングされるとします。リクエストBはサーバー#2にルーティングされ、リクエストCはサーバー#3にルーティングされます。


子プロセスが全部ビジーな状態でリクエストを1つ受け取ったときの私の顔。

ここで4つ目のリクエストDが来たとします。リクエストBとCの処理が首尾よく完了したおかげでサーバー#2と#3が暇になっているのに、リクエストAは誰かがCSVをエクスポートしようとしていて完了までに20秒かかるとしたらどうでしょう。ロードバランサーはサーバー#1がビジーであることには構わずリクエストを投げつけるので、リクエストAが完了するまで処理できません。サーバーが完全に死んでいるかどうかを確認する手段はどんなロードバランサーにもありますが、そうした手段はほとんどの場合かなりのタイムラグを伴います(遅延が30秒以上など)。1つのサーバーで実行するプロセス数をもっと増やせば、サーバーレベルではリクエストがビジーなプロセスに割り当てられなくなるため、多くの子プロセスが処理に時間のかかるリクエストで手一杯になってしまうリスクを断ち切ることができます。代わりに、リクエストはワーカーが空くまでソケットレベルまたはリバースプロキシでバックアップされます。これを達成するには、私の経験上1サーバーあたり3プロセス以上が最小値として適切です。リソースの制約のために1サーバーで最小3プロセスを実行できないのであれば、もっと大きなサーバーにしましょう(後述)。

つまり、1つのコンテナでは子プロセスを少なくとも3つは実行すべきです。しかし最大値はどうすればよいでしょうか。これについてはリソース(メモリとCPU)で制限されます。

まずはメモリから考えてみましょう。各子プロセスはある量のメモリを利用します。明らかに、サーバーのRAMがサポートできる個数を上回る子プロセスを追加するべきではありません。


Rubyプロセスの実際のメモリ使用量は対数的に増加します。メモリ断片化が発生するため、増加は水平にならず、単に上限に向かって増加し続けます。

しかし、Rubyアプリの単体プロセスにおける実際のメモリ使用量を調べる方法は単純ではありません。PCやproduction環境でプロセス起動直後の個数を調べる方法では不十分です。理由はいろいろありますが、Ruby Webアプリのプロセスは時間とともにメモリ使用量が増加するからです。ときには生成後の2倍から3倍に達することもあります。Rubyアプリのプロセスで使われるメモリ使用量を正確に測定するには、プロセスの再起動(ワーカーキラー)を無効にしてから、12時間から24時間待ってからpsコマンドで測定します。Herokuユーザーなら、新しい[Heroku Exec]を使って実行中のdynoでpsを実行するか、単にHerokuのメモリ使用量の測定値を1 dynoあたりのプロセス数で割って求めます。多くのRubyアプリは1プロセスあたり200 MBから400 MBのメモリを使いますが、ときには1 GBに達することがあります。


Pumaのワーカーはしばらく経つと…かなり太ります。

メモリ使用量には必ず余裕を見ておいてください。何か子プロセス数を求める公式が欲しいのであれば、(TOTAL_RAM / (RAM_PER_PROCESS * 1.2))を目安にしてください。


レアキャラ「ドット絵DHH」が現れた!
5000デヴィッドに1度だけ出現し、一生使える適切なメモリ使用量を5秒かそこらで受け取る
1いいね = お祈り1回

サーバーやコンテナの上限メモリ量を超えると、メモリが限界に達してスワップが始まるため、速度が大きく低下します。アプリのメモリ使用量を予測可能かつスパイクのない平らな状態にしておきたい理由がこれです。メモリ使用量の急増は、私が「メモリ膨張」と呼んでいる条件です。この問題の解決はまたの機会に別記事で扱いますが、The Complete Guide to Rails Performanceでも扱っています。

次に、サーバーの利用可能なCPUキャパシティを超えないようにしたいと思います。理想的には、CPU使用率100%になる総割り当て時間の5%を超えないことです。これを超えている場合、利用可能なCPUキャパシティでボトルネックが発生していることを示します。多くのRuby on Railsアプリは、クラウドプロバイダのほとんどでメモリリソースがボトルネックになる傾向がありますが、CPUリソースもボトルネックを生じることがあります。どうやって検出すればよいでしょうか。それには、お好みのサーバー監視ツールが使えます。おそらくAWSのビルトインツールなら、CPU使用率が頻繁に上限に達してないかどうかのチェックは十分可能でしょう。


OSのコンテキストスイッチはコストが高いと言っとったじゃないか。productionで実際に使った結果を見ると、あんたがウソ言ってたってことだな。

「CPUの個数より多くの子プロセスを1サーバーに割り当てるべきではない」とよく言われます。その一部は本当ですし、出発点としては適切です。しかし実際のCPU使用率は、自分で監視と最適化を行うべき値です。実際には、多くのアプリのプロセス数は、利用できるハイパースレッド数の1.25〜1.5倍に落ち着くでしょう。

Herokuでは、ログに出力されたCPU負荷の測定値をlog-runtime-metricsで取得します。私は5分間〜15分間の平均負荷をチェックします。値が常に1に近かったり超えたりすることがあるようなら、CPU使用率を超えているので子プロセス数を減らす必要があります。

子プロセス数の設定はどのサーバーでも割りと簡単です。

# Puma
$ puma -w 3 # コマンドラインオプションの場合
workers 3   # config/puma.rbに書く場合

# Unicorn
worker_processes 3 # config/unicorn.rbに書く

# Passenger (nginx/Standalone)
# Passengerのワーカー数は自動で増減します: この設定はあまり便利とは思えなかったので
# 単にmaxとminを一定の数に設定しています。
passenger_max_pool_size 3;
passenger_min_instances 3;

数値を設定ファイルに書く代わりに、WEB_CONCURRENCYなどの環境変数に設定することもできます。

workers Integer(ENV["WEB_CONCURRENCY"] || 3)

まとめると、多くのアプリは使えるリソース量に応じて1サーバーあたり3〜8プロセスを割り当てます。メモリ制約の厳しいアプリや、95パーセンタイル時間(5〜10秒以上)のアプリなら、利用可能なハイパースレッド数の4倍までプロセス数を増やしてもよいでしょう。多くのアプリでは、子プロセスの数を、利用可能なハイパースレッド数の1.5倍を超えないようにすべきです。

スレッド数

PumaやPassenger Enterpriseはアプリでマルチスレッドをサポートするので、このセクションではこの2つのサーバーを対象にします。

スレッドは、アプリの並列性(ひいてはスループット)を軽量なリソースで改善する方法です。Railsは既にスレッドセーフであり、独自のスレッドを作るとかデータベース接続などの共有リソースにグローバル変数でアクセスする($redisのことだよ!)といった妙なことをするアプリはあまりありません。つまり、多くのRuby Webアプリはスレッドセーフということになります。本当にスレッドセーフかどうかを知るには、実際にやってみるしかありません。Rubyアプリのスレッドバグは例外のraiseという派手な方法で顕在化する傾向があるので、簡単に試して結果を見ることができます。

ではスレッド数はいくつにすべきでしょうか。並列性を追加して得られるスピードアップは、プログラムがどの程度並列に実行されるかに依存します。これはアムダールの法則として知られています。MRI(CRuby)の場合、IO待ち(データベースの結果待ちなど)だけが並列化可能です。これは多くのWebアプリでおそらく総時間の10〜25%を占めるでしょう。自分のアプリで、リクエストごとにデータベースで使われる総時間をチェックできます。残念なことに、アムダールの法則によれば、並列性の占める割合が小さい(50%未満)の場合、手頃なスレッド数をさらに増やすメリットはほとんど(あるいはまったく)ありません。そしてこのことは私の経験とも整合します。Noah GibbsもDiscourseホームページのベンチマークでこれをテストした結果、スレッド数は6に落ち着いたそうです。


アムダールの法則

プロセス数の場合は現在の設定による測定値を定期的にチェックして適切にチェックすることをおすすめしますが、スレッド数の場合はそれとは異なり、アプリサーバーのプロセスごとのスレッド数を5に設定して「後は忘れる」でもたいてい大丈夫です。


「設定したら忘れよう」

MRI(CRuby)の場合、スレッド数はメモリに驚くほど大規模な影響を与えることがあります。これはホスト側に複雑な理由がいくつもあるためです(これについては今後別記事を書こうかと思います)。アプリのスレッド数を増やす場合、その前後でメモリ消費を必ずチェックしましょう。各スレッドがスタック空間で余分に消費するメモリが8 MBにとどまると期待しないことです。総メモリ使用量はしばしばこれよりずっと多くなります。

スレッド数の設定方法は次のとおりです。

# Puma: 繰り返しますが、私は「自動」スピンアップ/スピンダウン機能は本当に使わないので
# minとmaxには同じ値を設定しています
$ puma -t 5:5 # コマンドライン・オプション
threads 5, 5  # config/puma.rbに書く場合

# Passenger (nginx/Standalone)
passenger_concurrency_model thread;
passenger_thread_count 5;

JRubyをお使いの方へ: スレッドは完全に並列化されるので、アムダールの法則によるメリットをすべて得られます。JRubyでのスレッド数の設定は、上述したMRIでのプロセス数の設定にむしろ似ていて、メモリやCPUリソースを使い切るところまで増やせば済みます。

Copy-on-writeの振舞い

あらゆるUnixベースのOSではメモリの挙動にcopy-on-writeが実装されています。copy-on-writeはかなりシンプルです。プロセスがforkして子プロセスが作成された時点では、その子プロセスのメモリは親プロセスと完全に共有されます。しかしメモリに変更が生じるとコピーが作成され、その子プロセス専用のメモリになります。子プロセスは(理論的には)共有ライブラリやその他の「読み取り専用」メモリを(独自のコピーを作成する代わりに)親プロセスと共有できるようになっているべきなので、(copy-on-writeは)forkを繰り返すWebサーバーでメモリ使用量を減らすうえで大変役に立ちます。

copy-on-writeは、単に発生するものです5。copy-on-writeは「オフにできません」が、効率を高めます。基本的に私達がやりたいのは、forkの前にアプリをすべて読み込むことであり、多くのRuby Webサーバーでは「プリロード」と呼ばれています。copy-on-writeがあることで変わる点は、アプリが初期化される前と後でfork呼び出しが変わるだけです。

fork後、利用しているデータベースへの再接続も必要です。ActiveRecordの例を以下に示します。

# Puma
preload_app!
on_worker_boot do
  # Rails 4.1で`config/database.yml`を使って`pool`サイズを設定するのは有効
  ActiveRecord::Base.establish_connection
end

# Unicorn
preload_app true
after_fork do |server, worker|
  ActiveRecord::Base.establish_connection
end

# Passengerはデフォルトでプリロードを行うのでオンにする必要はない
# Passengerは自動でActiveRecordへの接続を確立するが、
# 他のDBの場合は以下を行わなければならない
PhusionPassenger.on_event(:starting_worker_process) do |forked|
  if forked
    reestablish_connection_to_database # DBによって異なる
  end
end

理論上は、アプリで使われるすべてのデータベースに対してこれを行わなければなりません。しかし実際には、Sidekiqは実際に何か行うまでRedisへの接続を試行しないので、アプリ起動時にSidekiqジョブを実行しているのでなければ、fork後に再接続する必要はありません。

残念なことに、copy-on-writeのメリットには限りがあります。透過的で巨大なページでは、メモリが1ビット変更されただけでも2 MBものページ全体がコピーされますし、メモリ断片化によっても上限が生じます。しかしそれで問題が生じるわけではないので、プリロードはとにかくオンにしておきましょう。

コンテナのサイズ


もっとメモリよこせや(゚Д゚)ゴルァ!!

一般に、サーバーで利用可能なCPUやメモリの利用率は70〜80%ぐらいにとどめておきたいものです。こうしたニーズはアプリによって異なりますし、CPUコア数とメモリのGB数の比率によっても変わります。あるアプリでは、4 vCPU/4 GB RAMのサーバーでRubyプロセスが6つ動くのがもっとも良好かもしれませんし、メモリ要求がより少なくCPU負荷のより高いアプリなら8 vCPU/2GB RAMがよいかもしれません。コンテナのサイズに完全なものはありませんので、CPUとメモリの比率は実際のproductionでの測定値に基いて選択すべきです。


Rails(有名なWebフレームワーク)とHeroku(RAM 512MB)、どっちが勝つか

サーバーで利用可能な総メモリ容量は、チューニング可能なリソースのうちでおそらく非常に重要なものです。多くのプロバイダは極めて低い値が採用されており、Herokuの標準的なdynoでは512 MBとなっています。Rubyアプリ、特に複雑かつ成熟したアプリは多くのメモリを要求するので、与えるべき総メモリ容量はおそらく非常に重要なリソースでしょう。

多くのRailsアプリで使われるRAMは300 MB以下なので、1サーバーあたり3プロセス以上を常に実行しているとすれば、多くのRailsアプリのRAMは少なくとも1 GBになるでしょう。

サーバーのCPUリソースも同じくチューニング可能な設定として重要です。利用可能なCPUコア数を知る必要がありますし、同時に実行可能なスレッド数も知る必要があります(そもそもサーバーでハイパースレッディングをサポートしているかどうかも知る必要があります)。

子プロセス数のところで解説したように、コンテナは少なくとも子プロセスを3つ以上サポートすべきです。1サーバー(またはコンテナ)あたり8プロセス以上にできればさらに改善されるでしょう。1コンテナあたりのプロセス数を増やせば、リクエストのルーティング改善やレイテンシの逓減に効果を発揮します。

まとめ

Ruby Webアプリサーバーのスループットを最大化する方法の概要を以下にまとめました。短いリスト形式になっているので、以下の手順に沿って進められます。

  1. スレッド数5のワーカー1つが使うメモリ容量を特定する。Unicornをお使いの場合は、明らかにスレッドは不要です。production環境の単一サーバー上でいくつかのワーカーを実行して少なくとも12時間は再起動せずに動かし続けてから、典型的なワーカーのメモリ容量をpsで調べます。
  2. コンテナサイズの値は、上のメモリ容量の少なくとも3倍以上にする。多くのRailsアプリでは1ワーカーあたり最大300 MB〜400 MBのRAMを使いますので、多くのRailsアプリは1コンテナ(サーバー)あたり 1 GB必要になります。これによって、1サーバーあたり3プロセスを実行する余裕のあるメモリ容量になります。実行できる子プロセス数は、(TOTAL_RAM / (RAM_PER_PROCESS * 1.2))に等しくなります。

  3. CPUコア/ハイパースレッド数をチェックする。コンテナのハイパースレッド数(AWSの場合はvCPU)が、メモリがサポート可能な数より少ない場合は、メモリが少なくCPUが多いコンテナに適したコンテナサイズを選択します。実行すべき子プロセス数は、ハイパースレッド数の1.25倍〜1.5倍が理想です。

  4. デプロイ後にCPUとメモリの消費を監視する。使用量を最大化するのに適切な子プロセス数とコンテナサイズを調整します。

関連記事

https://techracho.bpsinc.jp/hachi8833/2017_06_15/41465

RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

Rails 5のWebSocket対応アプリでDoS脆弱性を見つけるまで(翻訳)


  1. 3つのアプリサーバーで子プロセスを作成するmasterプロセスは、いずれも実際にはリクエストを処理しません。Passengerでforkが最近実行されていない場合、実際にはしばらくしてからmasterのプリロード処理を終了します。 
  2. JRubyの人なら次のセクションをスキップしてもよいでしょう。 
  3. これはCRubyのGlobal VM Lock(GVL)が原因です。Rubyコードを実行できるのは1度に1つのスレッドに限られるので、Rubyの並行処理を達成するにはプロセスを複数実行する以外に方法はありません。私たちは、サーバーのリソースを超えないようにしながら、サーバーあたりのプロセス数をできるだけ多く実行したいのです。 
  4. 私はPassengerの「least-busy-process-first」ルーティングが実は大好きです。 
  5. メモリをもっと効果的に節約するためにcopy-on-writeをさらに「サポートする」といったことはできません。 

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

$
0
0

概要

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

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

Unicorn(またはPumaやEinhorn)を実行していると、誰しもある奇妙な現象に気がつくでしょう。マスタからforkした(複数の)ワーカープロセスのメモリ使用量は開始当初は低いにもかかわらず、しばらくすると親と同じぐらいにまでメモリ量が増加します。大規模な本番インストールだと各ワーカーのサイズが100MB以上まで増加することもあり、やがてメモリはサーバーでもっとも制約の厳しいリソースになってしまい、しかもCPUはアイドル状態のままです。

現代的なOSの仮想メモリ管理システムは、まさにこの状況を防止するために設計されたcopy-on-write機能を提供します。プロセスの仮想メモリは4kページごとにセグメント化されます。プロセスがforkした当初の子プロセスは、すべてのページを親プロセスと共有します。子プロセスがページの変更を開始した場合にのみ、カーネルはその呼び出しをインターセプトし、ページをコピーして新しいプロセスに再度割り当てます。

子プロセスが時間とともに共有メモリ中心からコピー中心に移行する様子

だとすると、なぜUnicornのワーカーはもっと多くのメモリを共有しないのでしょうか。多くのソフトウェアが持つ(サイズを増減可能な)静的オブジェクトのコレクションは、一度だけ初期化された後、プログラムのライフタイム終了までメモリ上で変更されないままワーカー全体で共有され続ける筆頭候補になりえます。形式的にはそのとおりなのですが、実際には何も再利用されません。その理由を理解するには、Rubyのメモリ割り当て動作を詳しく調べる必要があります。

スラブとスロット

まずはオブジェクト割り当ての概要をざっと押さえておきましょう。RubyがOSにリクエストするメモリはチャンクに分かれており、内部ではヒープページと呼ばれます。(Rubyの)ヒープページは、OSから渡される4kページ(以後「OSページ」と呼ぶことにします)とは同じではないため、ヒープページという名前は少々不運かもしれませんが、1つのヒープページは仮想メモリ上で多数のOSページにマッピングされます。Rubyは、(OSページが複数の場合も含め)OSページを専有することで、OSページを最大限利用できるようにヒープページのサイズを増減します(通常、4kのOSページ4つが16kのヒープページ1つに等しくなります)。

1つの(Ruby)ヒープ、そのヒープページ、各ページごとのスロット

1つのヒープページは「ヒープ(複数形はheaps)」と呼ばれることもあれば、「スラブ(slab)」や「アリーナ(arena)」と呼ばれることがあるのをご存知かもしれません。私としては曖昧さの少ない後者の2つが好みなのですが、Rubyのあらゆるソースで使われている呼び名に合わせて、以後1つのチャンクを「ヒープページ」、ヒープページが複数集まったコレクション1つを単に「ヒープ」と呼ぶことにします。

1つのヒープページは、1つのヘッダと多数の「スロット」でできています。各スロットにはRVALUEが1つずつあり、これはメモリ上のRubyオブジェクトです(詳しくは後述)。1つのヒープは1つのページを指し、そこから多数のヒープページが互いを指して、コレクション全体に繰り返される結合リスト(linked list)を1つ形成します。

ヒープを利用する

Rubyのヒープは、ruby_setupeval.c)から呼び出されるInit_heapgc.c)によって初期化されます。ruby_setupは1つのRubyプロセスへの主要なエントリポイントです。ruby_setupはこのヒープに沿ってスタックとVMも初期化します。

void
Init_heap(void)
{
    heap_add_pages(objspace, heap_eden,
        gc_params.heap_init_slots / HEAP_PAGE_OBJ_LIMIT);

    ...
}

Init_heapは、スロットのターゲット数を元に初期のページ数を決定します。デフォルト値は10,000ですが、設定や環境変数で調整可能です。

#define GC_HEAP_INIT_SLOTS 10000

設定に応じて、1ページあたりのスロット数が大まかに算出されます(gc.c)。ターゲットサイズは16k(2*14または1 << 14)から始まり、そこからmallocの予約(bookkeeping)1分の数バイトを引き、ヘッダー用の数バイトも引いてから、RVALUE構造体の既知のサイズで割ります。

/* default tiny heap size: 16KB */
#define HEAP_PAGE_ALIGN_LOG 14
enum {
    HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG),
    REQUIRED_SIZE_BY_MALLOC = (sizeof(size_t) * 5),
    HEAP_PAGE_SIZE = (HEAP_PAGE_ALIGN - REQUIRED_SIZE_BY_MALLOC),
    HEAP_PAGE_OBJ_LIMIT = (unsigned int)(
        (HEAP_PAGE_SIZE - sizeof(struct heap_page_header))/sizeof(struct RVALUE)
    ),
}

64ビットシステムの場合、RVALUEは40バイトを占めます。デフォルトのRubyが最初に408スロット2ごとに24ページを割り当てていることを示すために、一部の計算を省略します。メモリがもっと必要になるとヒープは増大します。

RVALUE: メモリスロット上のオブジェクト

1つのヒープページ内にあるスロット1つにつきRVALUEが1つあり、メモリ上のRubyオブジェクトを表現します。定義は以下のとおりです(gc.cより)。

typedef struct RVALUE {
    union {
        struct RBasic  basic;
        struct RObject object;
        struct RClass  klass;
        struct RFloat  flonum;
        struct RString string;
        struct RArray  array;
        struct RRegexp regexp;
        struct RHash   hash;
        struct RData   data;
        struct RTypedData   typeddata;
        struct RStruct rstruct;
        struct RBignum bignum;
        struct RFile   file;
        struct RNode   node;
        struct RMatch  match;
        struct RRational rational;
        struct RComplex complex;
    } as;

    ...
} RVALUE;

ここは私にとって、Rubyが任意の型を任意の変数に代入できるという神秘のベールが最初に剥がされる場所です。上から、RVALUEとは単にRubyがメモリ上に保持している、取り得るすべての型の巨大なリストにすぎないことが直ちにわかります。すべての型が同じメモリを共有できるようにCの共用体(union)で圧縮されています。共用体には一度に1つずつしか設定できませんが、その共用体全体のサイズは、最大でもリストの個別の型の最大サイズにしかなりません。

スロットを具体的に理解するため、そこに保持される可能性のある型の1つを見てみることにしましょう。以下は典型的なRubyの文字列です(ruby.hより)。

struct RString {
    struct RBasic basic;
    union {
        struct {
            long len;
            char *ptr;
            union {
                long capa;
                VALUE shared;
            } aux;
        } heap;
        char ary[RSTRING_EMBED_LEN_MAX + 1];
    } as;
};

RString構造体を眺めてみると、いくつか興味深い点が浮かび上がってきます。

  • RBasicを含む。これはメモリ上にあるすべてのRuby型を区別しやすくするための共通の構造体です。
  • ary[RSTRING_EMBED_LEN_MAX + 1]を含む共用体は、文字列の内容がOSヒープに保存されるのに対し、短い文字列はRString値にインラインで含まれることがわかります。全体の値は、メモリ割り当ての追加なしにスロットに収まります。

  • ある文字列は別の文字列を参照することがあり(上のVALUE sharedの部分)、割り当てられたメモリを共有します。

VALUE: ポインタ兼スカラー

RVALUEはRuby標準のさまざまな型を保持しますが、すべての型を保持するわけではありません。Ruby C拡張のコードを読んだことがあれば、VALUEというよく似た名前に見覚えがあるでしょう。これは一般にRubyのあらゆる値の受け渡しに使われる型です。VALUEの実装はRVALUEよりややシンプルであり、単なるポインタです(ruby.hより)。

typedef uintptr_t VALUE;

ここでRubyの実装が賢くなります(残念だと思う人もいるかもしれませんが)。VALUEは多くの場合RVALUEへのポインタですが、定数との比較やさまざまなビットシフトを駆使して、ポインタのサイズに収まる、ある種のスカラー型を保持することもあります。

truefalsenilについては簡単です。これらはruby.hに値として事前定義されています。

enum ruby_special_consts {
    RUBY_Qfalse = 0x00,     /* ...0000 0000 */
    RUBY_Qtrue  = 0x14,     /* ...0001 0100 */
    RUBY_Qnil   = 0x08,     /* ...0000 1000 */

    ...
}

いわゆるfixnum(ごく大ざっぱに言うと64ビットに収まる数値)はもう少し複雑です。fixnumはVALUEを1ビット左にシフトして保存し、最下位にフラグを1つ設定します。

enum ruby_special_consts {
    RUBY_FIXNUM_FLAG    = 0x01, /* ...xxxx xxx1 */

    ...
}

#define RB_INT2FIX(i) (((VALUE)(i))<<1 | RUBY_FIXNUM_FLAG)

flonum(浮動小数点など)やシンボルの保存にも類似の手法が使われています。VALUEの型の識別が必要になると、Rubyはポインタの値をフラグのリストと比較します。このリストは、スタックに紐付けられた型を知っています。どのフラグともマッチしない場合は、ヒープに進みます(ruby.hより)。

static inline VALUE
rb_class_of(VALUE obj)
{
    if (RB_IMMEDIATE_P(obj)) {
        if (RB_FIXNUM_P(obj)) return rb_cInteger;
        if (RB_FLONUM_P(obj)) return rb_cFloat;
        if (obj == RUBY_Qtrue)  return rb_cTrueClass;
        if (RB_STATIC_SYM_P(obj)) return rb_cSymbol;
    }
    else if (!RB_TEST(obj)) {
        if (obj == RUBY_Qnil)   return rb_cNilClass;
        if (obj == RUBY_Qfalse) return rb_cFalseClass;
    }
    return RBASIC(obj)->klass;
}

値の特定のいくつかの型をスタック上に保持しておくことで、ヒープ上のスロットを専有せずに済むというメリットが生じ、速度面でも有利です。flonumがRubyに追加されたのは比較的最近であり、flonumの作者は単純な浮動小数点の計算が2倍程度まで高速化すると見積もりました。

衝突の回避

VALUEのスキームは賢くできていますが、あるスカラー値がポインタと衝突しないことをどうやって保証しているのでしょうか。ここで賢さがさらにパワーアップします。RVALUEのサイズが40バイトになっている経緯を思い出しましょう。アラインされたmallocとサイズの組み合わせは、RubyがVALUEに保存する必要のあるRVALUEのアドレスが必ず40で割り切れることを示しています。

2進数では、40で割り切れる数値の最下位3ビットは必ずゼロになります(...xxxx x000)。スタックに紐付けられる型(fixnum、flonum、シンボルなど)をRubyが識別するのに使われるあらゆるフラグは、この3つのビットのいずれかに関連するので、こうした型とRVALUEポインタは確実に住み分けられます。

ポインタに別の情報を含めるのはRubyに限りません。この手法を用いた値は、より一般には「タグ付きポインタ(tagged pointer)」と呼ばれます。

オブジェクトを割り当てる

ヒープについての基礎をいくつか押さえたので、Unicornの賢いプロセスが親プロセスと何も共有しない理由にもう少し迫ってみましょう(鋭い人はもうお気づきかもしれません)。ここから先は、Rubyがあるオブジェクト(ここでは文字列)を初期化する方法を辿ってみたいと思います。

str_new0から始めます(string.cより)。

static VALUE
str_new0(VALUE klass, const char *ptr, long len, int termlen)
{
    VALUE str;

    ...

    str = str_alloc(klass);
    if (!STR_EMBEDDABLE_P(len, termlen)) {
        RSTRING(str)->as.heap.aux.capa = len;
        RSTRING(str)->as.heap.ptr = ALLOC_N(char, (size_t)len + termlen);
        STR_SET_NOEMBED(str);
    }

    if (ptr) {
        memcpy(RSTRING_PTR(str), ptr, len);
    }

    ...

    return str;
}

先ほどRStringを調べたときの推測と似て、Rubyは新しい値が十分短い場合はスロットに埋め込みます。そうでない場合はALLOC_Nを使ってOSのヒープに文字列用の新しいスペースを割り当て、内部のポインタがそのスロット(as.heap.ptr)を指して参照できるようにします。

スロットを初期化する

いくつかの間接参照の層を経て、str_allocgc.cnewobj_ofを呼び出します。

static inline VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protected)
{
    rb_objspace_t *objspace = &rb_objspace;
    VALUE obj;

    ...

    if (!(during_gc ||
          ruby_gc_stressful ||
          gc_event_hook_available_p(objspace)) &&
        (obj = heap_get_freeobj_head(objspace, heap_eden)) != Qfalse) {
        return newobj_init(klass, flags, v1, v2, v3, wb_protected, objspace, obj);
    }

    ...
}

Rubyは、スロットに空きのあるヒープを問い合わせるのにheap_get_freeobj_headを使います(gc.c)。

static inline VALUE
heap_get_freeobj_head(rb_objspace_t *objspace, rb_heap_t *heap)
{
    RVALUE *p = heap->freelist;
    if (LIKELY(p != NULL)) {
        heap->freelist = p->as.free.next;
    }
    return (VALUE)p;
}

Rubyには、スレッドがいくつあっても一度に1つだけが実行されるようにするためのグローバルなロック(GIL)があるので、次のRVALUEをヒープの空きリストから安全に外して次の空きスロットを指し直すことができます。これ以上細かいロックは不要です。

空きスロットを獲得した後は、newobj_initがいくつか一般的な初期化を行ってからstr_new0に処理を戻して文字列固有の設定を行います(実際の文字列のコピーなど)。

eden、tomb、空きリスト

Rubyが空きスロットをheap_edenに問い合わせることを既にお気づきの方もいるかもしれません。edenという名前は聖書の「エデンの園」から命名された3ヒープのことで、Rubyはそこに生きているオブジェクトがあることを知っています。Rubyは2つのヒープをトラックしていますが、そのうちの1つです。

そしてもう1つが「tomb」です。実行後に、生きているオブジェクトがヒープページに残っていないことをガベージコレクタが検出すると、そのページはedenからtombに移動します。Rubyがある時点で新しいヒープページの割り当てが必要になると、OSにメモリ追加を要求する前に、tombから呼び戻すことが優先されます。逆に、tombのヒープページが長時間死んだままの場合、RubyはページをOSに返却します(実際にはそれほど頻繁には発生せず、一瞬で終わります)。

ここまで、Rubyが新しいページを割り当てる方法について簡単に説明しました。OSによって新しいメモリが割り当てられると、Rubyは新しいページをスキャンしていくつか初期化を行います(gc.cより)。

static struct heap_page *
heap_page_allocate(rb_objspace_t *objspace)
{
    RVALUE *start, *end, *p;

    ...

    for (p = start; p != end; p++) {
        heap_page_add_freeobj(objspace, page, (VALUE)p);
    }
    page->free_slots = limit;

    return page;
}

Rubyはそのページの開始スロットから末尾のスロットまでのメモリオフセットを算出し、一方の端から他方の端まで進みながらスロットごとにheap_page_add_freeobjを呼び出します(gc.cより)。

static inline void
heap_page_add_freeobj(rb_objspace_t *objspace, struct heap_page *page, VALUE obj)
{
    RVALUE *p = (RVALUE *)obj;
    p->as.free.flags = 0;
    p->as.free.next = page->freelist;
    page->freelist = p;

    ...
}

このヒープ自身は、空いていることがわかっているスロットへの空きリストポインタを1つトラックしますが、そこからRVALUE自身のfree.nextをたどると新しい空きスロットが見つかります。既知の空きスロットはすべて、heap_page_add_freeobjが構成した長大な連結リストで互いに連鎖しています。

あるヒープにおける、空いているRVALUEを指す空きリストポインタと、連結リストの連鎖

heap_page_add_freeobjが呼ばれてページを初期化します。これはオブジェクトが開放されたときにもガベージコレクタによって呼び出されます。このようにしてスロットが空きリストに戻され、再利用できるようになります。

肥大化したワーカーの事例についての結論

Rubyは精巧なメモリ管理戦略を備えていますが、これらのコードを読んでみると、何かがOSのcopy-on-writeとうまく噛み合っていないことにお気づきかもしれません。Rubyは拡張可能なヒープページをメモリに割り当て、そこにオブジェクトを保存し、可能になったらスロットをガベージコレクションします。空きスロットは注意深くトラックされているのでランタイムは効率よく空きスロットを見つけられます。しかし、これほど洗練されているにもかかわらず、生きているスロットの位置はヒープページ内やヒープページ間でまったく変わらないのです。

オブジェクトの割り当てと解放が常に発生する実際のプログラムでは、すぐに、生きているオブジェクトと死んだオブジェクトが混じり合った状態になってしまいます。ここで思い出されるのがUnicornです。親プロセスが自分自身をセットアップしてからfork可能な状態になるまでの間、親プロセスのメモリはちょうど、利用可能なヒープページ全体に渡って生きているオブジェクトが断片化している典型的なRubyプロセスと似た状態になっています。

ワーカーは、自身のメモリ全体を親プロセスと共有した状態から開始します。残念なことに、子が最初にスロットを1つでも初期化またはGCすると、OSは呼び出しをインターセプトして背後のOSページをコピーします。まもなく、プログラムに割り当てられているあらゆるページでこれが発生し、子ワーカーは親から完全に分岐してしまったメモリのコピーで実行されます。

copy-on-writeは強力な機能ですが、Rubyプロセスのforkにはあまり向いていません。

copy-on-writeへの対策

Rubyチームはcopy-on-writeに精通しており、ある時期に最適化を進めてきました。たとえば、Ruby 2.0ではヒープの「bitmap」が導入されました。Rubyでは「マークアンドスイープ」方式のガベージコレクタが使われており、これはオブジェクト領域全体をスキャンして生きているオブジェクトを見つけるとマーキングし、死んでいるものをすべて片付ける(sweep)というものです。従来のマーキングは1つのページ内の各スロットに直接フラグを設定するのに使われていましたが、これによってあらゆるforkでGCが実行され、マーキングが渡されるとOSページが常に親プロセスからコピーされていました。

Ruby 2.0の変更によって、これらのマーキングフラグがヒープレベルの「bitmap」に移動しました。これは、そのヒープのスロットをマッピングする単一ビットの巨大なシーケンスです。GCがあるforkでこれを渡すと、bitmapで必要なOSページだけがコピーされ、共有期間がより長いメモリをさらに多く利用できるようになりました。

今後の「圧縮」機能

今後導入される変更はさらに期待できます。Aaron Patterson氏はある時期にRuby GCの圧縮の実装について話していてGitHub本番で導入した結果ある程度の成功を収めたとのことです。具体的には、ワーカーがforkする前に呼び出されるGC.compactという名前のメソッドであるようです。

# ワーカーが親からforkするときに常に呼び出される
before_fork do
  GC.compact
end

初期化の一環としてオブジェクトの大量作成を終了できた親プロセスは、まだ生きているオブジェクトを、より長期間安定しそうなページの最小セット上のスロットに移動します。forkしたワーカーは、より長期間親プロセスとメモリを共有できるようになります。

GC圧縮前後の断片化したヒープ

これは、GitHubやHeroku、または私たちがStripeでやっているような巨大なRubyインストールを実行しているすべての人にとって実に期待できる成果です。メモリを大量に使うインスタンスをデプロイしても、メモリは多くの場合、実行可能なワーカー数という限定されたリソースに収まります。GC圧縮は各ワーカーで必要なメモリのかなりの部分を削減する能力を持ちます。メモリ削減によってboxごとに実行できるワーカー数が増加し、boxの総数も削減できます。注意点もほとんどなく、多数のワーカーを実行するコストが直ちに削減されます。

本記事へのコメントや議論はHacker Newsまでどうぞ。

もし誤りがありましたら、プルリク送信をご検討ください。

関連記事

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

Ruby2.0でnil.object_idの値が4から8に変わった理由


  1. mallocの予約は、複数のOSページでヒープページが他のOSページにあふれずにうまく収まるよう埋め合わせられます。ページはOSがプロセスに割り当てる最小単位であるため、これによって非効率なメモリ利用を補えることがあります。 
  2. 鋭い方は、リクエストが10,000件であるにもかかわらず、最初に合計9,792(24 * 408)スロット「だけ」しか割り当てていないことにお気づきかと思います。 
  3. ガベージコレクション(GC)の世界ではedenという名前を付けるのが半ば慣習になっています。Java VMにも「eden space」があります。 

Vue.jsサンプルコード(22)YouTube風の[👆][👇]ボタンで1度だけGood/Bad評価する

$
0
0

22. YouTube風の[👆][👇]ボタンで1度だけGood/Bad評価する

  • Vue.jsバージョン: 2.5.2
  • [👆]または[👇]ボタンを押すと、カウントが1上がります。
  • ボタンをもう一度押してもカウントは変わりません。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


ポイント: $once()で定義したコードは、$emitで「1度だけ」実行されます。

    created: function() {
      this.$once("foo", function(e) { this[e.target.dataset.key] += 1 })
    },
    methods: {
      a: function(e) { this.$emit("foo", e) },
    },

バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

Ruby: 認証gem ‘Rodauth’ README(翻訳)

$
0
0

こんにちは、hachi8833です。

今回は、「Railsアプリの認証システムをセキュアにする4つの方法」でも取り上げられていたRodauthのREADMEを翻訳しました。

現時点では、残念ながらRodauthをRailsで使うためのroda-rails gemがRails 4.2までしか対応していないのと、ルーティングにDSLを使うことから、おそらくRails 5で使うには一手間かける必要がありそうです。

しかし、認証のセキュリティを考えるうえで参考になる情報が多く、ドキュメントの質が(少なくともDeviseと比べて)非常に高いのが特徴です。大きな概要をまず示し、必要な概念も適宜示しながら、先に進むに連れて詳細に説明するという書き方が見事です。

さらに、READMEから読み取れる筋のよい認証システム設計も参考になると思います。パスワードハッシュの保存場所を完全に隔離した上で可能な限り自由度を高めている点や、機能ごとにカラムを足したり減らしたりするのではなくテーブルを足したり減らしたりする点など、学ぶところが多そうです。

概要

MITライセンスに基いて翻訳・公開します。


http://rodauth.jeremyevans.net/より

Rodauth README(翻訳)

RodauthはRackアプリケーション向けの認証・アカウント管理フレームワークです。ビルドにはRodaとSequelを使っていますが、他のWebフレームワーク・データベースライブラリ・データベースでも利用できます。PostgreSQL・MySQL・Microsoft SQL Serverをデフォルト設定で使うと、データベース関数経由でのアクセスが保護されるようになり、パスワードハッシュのセキュリティを強化できます。

設計上のゴール

  • セキュリティ: デフォルト設定で最大のセキュリティを利用できること
  • 簡潔性: DSLで簡単に設定できること
  • 柔軟性: フレームワークのどの部分でも機能をオーバーライドできること

機能リスト

  • ログイン
  • ログアウト
  • パスワードの変更
  • ログインの変更
  • パスワードのリセット
  • アカウントの作成
  • アカウントの無効化
  • アカウントのバリデーション
  • パスワードの確認
  • パスワードの保存(トークン経由での自動ログイン)
  • ロックアウト(総当たり攻撃からの保護)
  • OTP (TOTP経由の2要素認証)
  • リカバリーコード(バックアップコード経由の2要素認証)
  • SMSコード(SMS経由の2要素認証)
  • ログイン変更のバリデーション(ログイン変更前の新規ログインバリデーション)
  • アカウントの許容期間(ログイン前のバリデーションを不要にする)
  • パスワードの許容期間(パスワードを最近入力した場合はパスワード入力を不要にする)
  • パスワードの強度(より洗練されたチェック)
  • パスワードの使い回し禁止
  • パスワードの有効期限
  • アカウントの有効期限
  • セッションの有効期限
  • シングルセッション(アカウントのアクティブセッションを1つに限定)
  • JWT(他のすべての機能でJSON APIをサポート)
  • パスワードハッシュの更新(ハッシュのcostが変更された場合)
  • HTTP BASIC認証

リソース

Webサイト
http://rodauth.jeremyevans.net
デモサイト
http://rodauth-demo.jeremyevans.net
ソースコード
http://github.com/jeremyevans/rodauth
バグ報告
http://github.com/jeremyevans/rodauth/issues
Google Group
https://groups.google.com/forum/#!forum/rodauth
IRC(チャット)
irc://chat.freenode.net/#rodauth

依存関係

Rodauthがデフォルトで依存しているgemが若干ありますが、それらについてはRodauthの開発上依存しているものであり、運用上はgemなしで実行することも可能です。

tilt、rack_csrf
すべての機能で利用(JSON API onlyモードの場合を除く)
bcrypt
パスワードの一致チェックでデフォルトで利用(カスタム認証でpassword_match?をオーバーライドすればスキップ可能)
mail
パスワードリセット時・アカウント確認時・ロックアウト機能のメール送信で利用
rotp、rqrcode
OTP機能で利用
jwt
JWT機能で利用

セキュリティ

データベース関数経由でのパスワードハッシュアクセス

RodauthでPostgreSQL・MySQL・Microsoft SQL Serverを利用する場合、デフォルトでパスワードハッシュにアクセスするためのデータベース関数を使います。これにより、アプリケーションを実行するユーザーはパスワードハッシュに直接アクセスできないようになっています。この機能によって攻撃者がパスワードハッシュにアクセスするリスクや、パスワードハッシュを他のサイトの攻撃に利用されるリスクを減らします。

本セクションでは以後この機能についてもっと詳しく説明します。なお、Rodauthはこの機能を使わなくても利用できます。異なるデータベースを利用している場合や、データベースの権限が不足している場合などには、この機能を利用できないことがあります。

パスワードはbcryptでハッシュ化され、アカウントテーブルとは別のテーブルに保存されます。また、2つのデータベース関数を追加します。1つはパスワードで使うsaltを取得する関数、もう1つは渡されたパスワードハッシュがユーザーのパスワードハッシュと一致するかどうかをチェックする関数です。

Rodauthでは2つのデータベースアカウントを使います。1つはアプリで使うアカウント用(以下「appアカウント」)、もう1つはパスワードハッシュ用(以下「phアカウント」)です。phアカウントは、渡されたパスワードのsaltを取得するデータベース関数と、渡されたアカウントでパスワードハッシュが一致するかどうかをチェックする関数を設定します。2つの関数は、appアカウントでphアカウントのパーミッションを使って実行されるようになっています。これにより、appアカウントでパスワードハッシュを読み取らずにパスワードをチェックできます。

appアカウントではパスワードハッシュを読み出すことはできない代わりに、パスワードハッシュのINSERT、UPDATE、DELETEはできるので、この機能によって追加されたセキュリティで大きな不便は生じません。

appアカウントでのパスワードハッシュ読み取りを禁止したことによって、仮にアプリのSQLインジェクションやリモートでのコード実行の脆弱性を攻撃された場合であっても、攻撃者によるパスワードハッシュの読み取りはさらに難しくなっています。

パスワードハッシュにこのようなセキュリティ対策を追加した理由は、弱いパスワードを使うユーザーやパスワードを使いまわすユーザーが後を絶たず、あるデータベースのパスワードハッシュが盗み出されると他のサイトのアカウントにまでアクセスされる可能性があるからです。そのため、たとえ保存されている他のデータの重要性が低いとしても、パスワードハッシュの保存方法はセキュリティ上きわめて重要度が高くなっています。

アプリのデータベースに機密情報が他にも保存されているのであれば、他の情報(あるいはすべての情報)についてもパスワードハッシュと同様のアプローチを行うことを検討すべきです。

トークン

アカウントの検証トークン、パスワードリセットのトークン、パスワード保存のトークン、ロックアウトのトークンでは同様のアプローチを採用しています。これらはすべてアカウントID_長いランダム文字列形式のトークンを提供します。トークンにアカウントIDを含めることで、攻撃者は全アカウントに対してトークンを総当り(ブルートフォース)攻撃で推測できなくなり、一度に1人のユーザーに対してしか総当たり攻撃を行えなくなります。なお、トークンがランダム文字列だけで構成されていると、全アカウントに対して総当たり攻撃が可能になる場合があります。

さらに、トークンの比較にはタイミング攻撃に対して安全な関数を採用し、タイミング攻撃のリスクを低減しています。

PostgreSQLデータベースの設定

PostgreSQLでRodauthのセキュリティ設計をすべて利用するには、複数のデータベースアカウントを使います。

  1. データベースのsuperuserアカウント(通常はpostgres)
  2. appアカウント(実際はアプリと同じ名前にします)
  3. phアカウント(実際はアプリ名に_passwordを追加した名前にします)

データベースのsuperuserアカウントは、データベースに関連する拡張(extension)の読み込みに使われます。アプリでは絶対にデータベースのsuperuserアカウントを使ってはいけません。

HerokuのPostgreSQLデータベースについては、上のようなシンプルな方法で複数のデータベースアカウントを設定する方法がありません。もちろん、HerokuでRodauthを使うことはできますが、セキュリティ上の利点は同じにはなりません。ただしこれはセキュリティ上危険ということではなく、パスワードハッシュの保存方法が他のメジャーな認証ソリューションと同じレベルになるということです。

データベースアカウントの作成

アプリがデータベースのsuperuserアカウントを使って実行されているのであれば、最初にappデータベースアカウントの作成が必要です。このアカウント名はデータベース名と同じにしておくのが多くの場合ベストです。

続いてphデータベースアカウントを作成します。このアカウントはパスワードハッシュへのアクセスに使われます。

  • PostgreSQLでの実行例
createuser -U postgres ${DATABASE_NAME}
createuser -U postgres ${DATABASE_NAME}_password

superuserアカウントがデータベース内の全アイテムの所有者になっている場合、上で作成したオーナーシップの変更が必要です。詳しくはhttps://gist.github.com/jeremyevans/8483320をご覧ください。

データベースの作成

一般に、アプリのアカウントはほとんどのテーブルを所有するので、アプリのアカウントがデータベースのオーナーとなります。

createdb -U postgres -O ${DATABASE_NAME} ${DATABASE_NAME}

上の方法はアプリ開発方法として最もセキュアとは言えないため、注意が必要です。セキュリティを最大化したい場合は、テーブルのオーナーとして独自のデータベースアカウントを使い、アプリのアカウントはテーブルのオーナーにならないようにし、正常動作に必要な最小限のアクセス権だけをアプリのアカウントに許可します。

拡張の読み込み

Rodauthのログイン機能で大文字小文字を区別しないログインをサポートするには、citext拡張を読み込む必要があります。

例:

psql -U postgres -c "CREATE EXTENSION citext" ${DATABASE_NAME}

Herokuの場合、citextは標準のデータベースアカウントで読み込まれます。ログインで大文字小文字を区別したいのであれば(ただし一般にはよくないとされています)、PostgreSQLのcitext拡張を読み込む必要はありません。その場合は、マイグレーション内のcitextStringに変更し、メールアドレスに対応できるようにしてください。

デフォルト以外のスキーマを使う

PostgreSQLは、デフォルトでパブリックなスキーマで新規テーブルをセットアップします。ユーザーごとに個別のスキーマを使いたい場合は、次のようにします。

psql -U postgres -c "DROP SCHEMA public;" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME} AUTHORIZATION ${DATABASE_NAME};" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME}_password AUTHORIZATION ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME} TO ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};" ${DATABASE_NAME}

スキーマを指定する拡張の読み込み部分のコードの変更が必要です。

psql -U postgres -c "CREATE EXTENSION citext SCHEMA ${DATABASE_NAME}" ${DATABASE_NAME}

phユーザーでマイグレーションを実行する場合、スキーマ変更に対応するいくつかの変更が必要です。

create_table(:account_password_hashes) do
  foreign_key :id, Sequel[:${DATABASE_NAME}][:accounts], :primary_key=>true, :type=>:Bignum
  String :password_hash, :null=>false
end
Rodauth.create_database_authentication_functions(self, :table_name=>"${DATABASE_NAME}_password.account_password_hashes")

# if using the disallow_password_reuse feature:
create_table(:account_previous_password_hashes) do
  primary_key :id, :type=>:Bignum
  foreign_key :account_id, Sequel[:${DATABASE_NAME}][:accounts], :type=>:Bignum
  String :password_hash, :null=>false
end
Rodauth.create_database_previous_password_check_functions(self, :table_name=>"${DATABASE_NAME}_password.account_previous_password_hashes")

また、次のRodauth設定メソッドを使って、アプリのアカウントが個別のスキーマで関数を呼び出すようにします。

function_name do |name|
  "${DATABASE_NAME}_password.#{name}"
end
password_hash_table Sequel[:${DATABASE_NAME}_password][:account_password_hashes]

# disallow_password_reuse でパスワード再利用を禁止する場合:
previous_password_hash_table Sequel[:${DATABASE_NAME}_password][:account_previous_password_hashes]

MySQLデータベースの設定

MySQLにはオブジェクトの所有者という概念がなく、MySQLのGRANTやREVOKEのサポートはPostgreSQLと比べて限定されています。MySQLを使う場合、以下のようにphアカウントにGRANT ALLしてすべてのパーミッションを与え、さらにWITH GRANT OPTIONphアカウントからappアカウントにGRANTできるようにすることをおすすめします。

CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';
CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';
GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;

マイグレーションの実行は常にphアカウントで行い、appアカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。

MySQLでデータベース関数を追加するには、MySQLの設定にlog_bin_trust_function_creators=1が必要になることがあります。

Microsoft SQL Serverデータベースの設定

Microsoft SQL Serverにはデータベースの所有者という概念はありますが、MySQLの場合と同様、phアカウントをデータベースのスーパーユーザーとして使い、phからGRANTでappアカウントにパーミッションを与えられるようにすることをおすすめします。

CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';
CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';
CREATE DATABASE rodauth_test;
USE rodauth_test;
CREATE USER rodauth_test FOR LOGIN rodauth_test;
GRANT CONNECT, EXECUTE TO rodauth_test;
EXECUTE sp_changedbowner 'rodauth_test_password';

マイグレーションの実行は常にphアカウントで行い、appアカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。

テーブルの作成

異なる2種類のデータベースアカウントを使っているため、マイグレーションもデータベースアカウントごとに実行する必要があります(2つの異なるマイグレーションを実行します)。マイグレーションの例を以下に示します。このマイグレーションを変更して追加カラムをサポートしたり、Rodauthの不要な機能に関連するカラムやテーブルを削除することもできます。

1回目のマイグレーション

PostgreSQLの場合はappアカウントで実行する必要があります。MySQLやMicrosoft SQL Serverの場合はphアカウントで実行する必要があります。

マイグレーションの実行にはSequel 4.35.0以降が必要です。これより前のバージョンのSequelを使っている場合は、:BignumシンボルをBignum定数に変更してください。

Sequel.migration do
  up do
    extension :date_arithmetic

    # アカウントの検証やアカウントの無効化機能で使用
    create_table(:account_statuses) do
      Integer :id, :primary_key=>true
      String :name, :null=>false, :unique=>true
    end
    from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])

    db = self
    create_table(:accounts) do
      primary_key :id, :type=>:Bignum
      foreign_key :status_id, :account_statuses, :null=>false, :default=>1
      if db.database_type == :postgres
        citext :email, :null=>false
        constraint :valid_email, :email=>/^[^,;@ rn]+@[^,@; rn]+.[^,@; rn]+$/
        index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
      else
        String :email, :null=>false
        index :email, :unique=>true
      end
    end

    deadline_opts = proc do |days|
      if database_type == :mysql
        {:null=>false}
      else
        {:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
      end
    end

    # パスワードのリセット機能で使用
    create_table(:account_password_reset_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # アカウントの検証機能で使用
    create_table(:account_verification_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # ログイン変更の検証機能で使用
    create_table(:account_login_change_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      String :login, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # パスワード保存機能で使用
    create_table(:account_remember_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[14]
    end

    # ロックアウト機能で使用
    create_table(:account_login_failures) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      Integer :number, :null=>false, :default=>1
    end
    create_table(:account_lockouts) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      DateTime :deadline, deadline_opts[1]
    end

    # パスワードの有効期限機能で使用
    create_table(:account_password_change_times) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # アカウントの有効期限機能で使用
    create_table(:account_activity_times) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      DateTime :last_activity_at, :null=>false
      DateTime :last_login_at, :null=>false
      DateTime :expired_at
    end

    # シングルセッション機能で使用
    create_table(:account_session_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
    end

    # OTP機能で使用
    create_table(:account_otp_keys) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :key, :null=>false
      Integer :num_failures, :null=>false, :default=>0
      Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    # リカバリーコード機能で使用
    create_table(:account_recovery_codes) do
      foreign_key :id, :accounts, :type=>:Bignum
      String :code
      primary_key [:id, :code]
    end

    # SMSコード機能で使用
    create_table(:account_sms_codes) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :phone_number, :null=>false
      Integer :num_failures
      String :code
      DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
    end

    case database_type
    when :postgres
      user = get{Sequel.lit('current_user')} + '_password'
      run "GRANT REFERENCES ON accounts TO #{user}"
    when :mysql, :mssql
      user = if database_type == :mysql
        get{Sequel.lit('current_user')}.sub(/_password@/, '@')
      else
        get{DB_NAME{}}
      end
      run "GRANT ALL ON account_statuses TO #{user}"
      run "GRANT ALL ON accounts TO #{user}"
      run "GRANT ALL ON account_password_reset_keys TO #{user}"
      run "GRANT ALL ON account_verification_keys TO #{user}"
      run "GRANT ALL ON account_login_change_keys TO #{user}"
      run "GRANT ALL ON account_remember_keys TO #{user}"
      run "GRANT ALL ON account_login_failures TO #{user}"
      run "GRANT ALL ON account_lockouts TO #{user}"
      run "GRANT ALL ON account_password_change_times TO #{user}"
      run "GRANT ALL ON account_activity_times TO #{user}"
      run "GRANT ALL ON account_session_keys TO #{user}"
      run "GRANT ALL ON account_otp_keys TO #{user}"
      run "GRANT ALL ON account_recovery_codes TO #{user}"
      run "GRANT ALL ON account_sms_codes TO #{user}"
    end
  end

  down do
    drop_table(:account_sms_codes,
               :account_recovery_codes,
               :account_otp_keys,
               :account_session_keys,
               :account_activity_times,
               :account_password_change_times,
               :account_lockouts,
               :account_login_failures,
               :account_remember_keys,
               :account_login_change_keys,
               :account_verification_keys,
               :account_password_reset_keys,
               :accounts,
               :account_statuses)
  end
end

2回目のマイグレーション

phアカウントで実行します。

require 'rodauth/migrations'

Sequel.migration do
  up do
    create_table(:account_password_hashes) do
      foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
      String :password_hash, :null=>false
    end
    Rodauth.create_database_authentication_functions(self)
    case database_type
    when :postgres
      user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
      run "REVOKE ALL ON account_password_hashes FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
    when :mysql
      user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
      db_name = get(Sequel.function(:database))
      run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
    when :mssql
      user = get(Sequel.function(:DB_NAME))
      run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
      run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
      run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
    end

    # disallow_password_reuse 機能で使われる
    create_table(:account_previous_password_hashes) do
      primary_key :id, :type=>:Bignum
      foreign_key :account_id, :accounts, :type=>:Bignum
      String :password_hash, :null=>false
    end
    Rodauth.create_database_previous_password_check_functions(self)

    case database_type
    when :postgres
      user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
      run "REVOKE ALL ON account_previous_password_hashes FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
      run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
      run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
      run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
    when :mysql
      user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
      db_name = get(Sequel.function(:database))
      run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
    when :mssql
      user = get(Sequel.function(:DB_NAME))
      run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
      run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
      run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
      run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
    end
  end

  down do
    Rodauth.drop_database_previous_password_check_functions(self)
    Rodauth.drop_database_authentication_functions(self)
    drop_table(:account_previous_password_hashes, :account_password_hashes)
  end
end

マイグレーションを複数ユーザーで手分けして行いたい場合、SequelのマイグレーションAPIを使ってパスワードユーザーのマイグレーションを実行できます。

Sequel.extension :migration
Sequel.postgres('DATABASE_NAME', :user=>'PASSWORD_USER_NAME') do |db|
  Sequel::Migrator.run(db, 'path/to/password_user/migrations', :table=>'schema_info_password')
end

PostgreSQL・MySQL・Microsoft SQL Server以外のデータベースを使う場合や、(データベースの)ユーザーアカウントを複数使えない場合は、単に2つのマイグレーションを1つのマイグレーションにまとめます。

上のマイグレーションを読むとわかるように、Rodauthでは1つのテーブルにさまざまなカラムを追加するのではなく、追加機能ごとにテーブルを追加する設計になっています。

使い方

基本的な使い方

RodauthはRodaのプラグインなので、以下のように他のRodaプラグインと同じ方法で読み込みます。

plugin :rodauth do
end

plugin呼び出しでは、Rodauthの設定用DSLをブロックとして受け取ります。読み込む機能を指定するenableという設定メソッドは省略できません。

plugin :rodauth do
  enable :login, :logout
end

機能が読み込まれた後は、その機能でサポートされる設定用メソッドをすべて利用できるようになります。設定用メソッドには次の2種類があります。

    1. 認証系メソッド

1つ目は認証系メソッド(auth methods)と呼ばれます。これらのメソッドはブロックを1つ取り、Rodauthのデフォルトメソッドをオーバーライドします。ブロック内でsuperを呼ぶとデフォルトの動作を取得できますが、superには明示的に引数を渡す必要があります。なお、beforeフックやafterフックではsuperを呼ぶ必要はありません。

たとえば、ユーザーのログイン時にログ出力を追加したい場合は次のようにします。

plugin :rodauth do
  enable :login, :logout
  after_login do
    LOGGER.info "#{account[:email]} logged in!"
  end
end

ブロック内は、リクエストに関連付けられたRodauth::Authインスタンスのコンテキストになります。このオブジェクトで次のメソッドを使うと、リクエストに関連するあらゆるデータにアクセスできます。

request
RodaRequestのインスタンス
response
RodaResponseのインスタンス
scope
Rodaのインスタンス
session
セッションのハッシュ
flash
flashメッセージのハッシュ
account
アカウントモデルのインスタンス(Rodauthのメソッドで事前に設定済みの場合)

ログイン中のユーザーのIPアドレスをログ出力したい場合は次のようにします。

plugin :rodauth do
  enable :login, :logout
  after_login do
    LOGGER.info "#{account[:email]} logged in from #{request.ip}"
  end
end
    1. 認証値系メソッド

設定用メソッドの2つ目は認証値(auth value)のメソッドです。認証値系メソッドは認証系メソッドと似ていますが、単にブロックを受け取るほかに、ブロック無しで引数を1つ受け取ることもできます。受け取った引数は、その値を単に返すブロックとして扱われます。

たとえば、データベースのテーブルにアカウントを保存するaccounts_tableの場合、次のようにテーブル名をシンボルで渡すことでオーバーライドできます。

plugin :rodauth do
  enable :login, :logout
  accounts_table :users
end

認証値系メソッドはブロックを1つ受け取ることもできるので、リクエストから得られる情報を使ってすべての挙動を上書きできます。

plugin :rodauth do
  enable :login, :logout
  accounts_table do
    request.ip.start_with?("192.168.1") ? :admins : :users
  end
end

Rodauthではどの設定メソッドもブロックを受け取れるので、多くのレガシーシステムを統合するのに十分な柔軟性を備えています。

各機能のドキュメント

サポートされている各機能のオプションやメソッドについては、機能ごとに別ページを設けています。もしリンクが切れていたら、ドキュメントのディレクトリで必要なファイルを参照してください。

Rodauthをルーティングツリーで呼び出す

一般に、以下のようにrodauthをルーティングブロックの早い段階で呼び出すのが普通です。

route do |r|
  r.rodauth

  # ...
end

Rodauthはこれで実行できます。ただしこのままでは、アクセスするユーザーのログインを必須にしたり、サイトにセキュリティを追加したりできません。すべてのユーザーに対してログインを必須にするには、ログインしていないユーザーを以下のように強制的にログインページにリダイレクトします。

route do |r|
  r.rodauth
  rodauth.require_authentication

  # ...
end

ログインを必須にしたいページがサイトの一部に限られている場合は、以下のようにすると、ルーティングツリーの特定のブランチについてだけユーザーがログインしていない場合にリダイレクトできます。

route do |r|
  r.rodauth

  r.on "admin" do
    rodauth.require_authentication

    # ...
  end

  # ...
end

Rodauthをルーティングツリーのルートではなく、ルーティングのブランチ内でだけ実行したい場合があります。その場合は以下のようにRodauthの設定で:prefixを設定してから、ルーティングツリーの該当するブランチでr.rodauthを呼び出します。

plugin :rodauth do
  enable :login, :logout
  prefix "auth"
end

route do |r|
  r.on "auth" do
    r.rodauth
  end

  rodauth.require_authentication

  # ...
end

rodauthメソッド

Rodauthの機能のほとんどはr.rodauth経由で公開されています。これを使って、Rodauthで自分が有効にした機能にルーティングできます(ログイン機能の/loginなど)。しかし、上述したようにこうしたメソッドをrodauthオブジェクトで呼び出したいこともあります(現在のリクエストが認証済みであるかどうかのチェックなど)。

以下のメソッドは、r.rodauthの外でもrodauthオブジェクトで呼び出せるように設計されています。

require_login
セッションでログインを必須にし、ログインしていないリクエストをログインページにリダイレクトします。
require_authentication
require_loginと似ていますが、アカウントが2要素認証用に設定されている場合は2要素認証も必須にします。ログイン済みであっても2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
logged_in?
セッションがログイン中であるかどうかを返します。
authenticated?
logged_in?と似ていますが、アカウントが2要素認証用に設定されている場合はセッションが2要素認証されているかどうかを返します。
require_two_factor_setup
(2要素認証用)セッションで2要素認証を必須にします。2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
uses_two_factor_authentication?
(2要素認証用)現在のセッションのユーザーが2要素認証を使えるよう設定されているかどうかを返します。
update_last_activity
(アカウント有効期限用)現在のアカウントの最終活動時刻を更新します。最終活動時刻を基にアカウントの有効期限が切れるようにしてある場合にのみ意味があります。
require_current_password
(アカウント有効期限用)アカウントのパスワードの有効期限が切れた場合に、パスワード変更ページにリダイレクトして現在のパスワードを入力しないと継続できないようにします。
load_memory
(パスワード保存機能用)セッションが認証されていない場合に、remember cookieがあるかどうかをチェックします。有効なremember cookieがある場合はセッションに自動ログインしますが、rememberキー経由でログインしたというマークを付けます。
logged_in_via_remember_key?
(パスワード保存機能用) rememberキーを使って現在のセッションにログインしたかどうかを返します。セキュリティ上重要な操作でパスワードの再入力を必須にしたい場合は、confirm_passwordを使えます。
check_session_expiration
(セッション有効期限用) 現在のセッションの有効期限が切れているかどうかをチェックし、期限切れの場合は自動的にログアウトします。
check_single_session
(シングルセッションの有効期限) 現在のセッションがまだ有効かどうかをチェックし、無効な場合はセッションからログアウトします。
verified_account?
(許容期間の確認の延長) 現在のアカウントが(訳注: メールなどで)確認済みかどうかを返します。falseの場合、ユーザーが「許容期間」に該当しているためにログインを許されていることを示します。
locked_out?
(ロックアウト機能) 現在のセッションのユーザーがロックアウトされているかどうかを返します。

複数の設定を使う

Rodauthでは、同じアプリケーションで複数のrodauth設定の利用をサポートしています。これは、プラグインを読み込んで2度目のログインで別設定の名前を指定するだけで行なえます。

plugin :rodauth do
end
plugin :rodauth, :name=>:secondary do
end

その後は、いつでもルーティングでrodauthを呼び、使いたい設定名を引数で指定できるようになります。

route do |r|
  r.on 'secondary' do
    r.rodauth(:secondary)
  end

  r.rodauth
end

パスワードハッシュをアカウントのテーブルに保存する

Rodauthでは、パスワードハッシュをアカウントと同じテーブルに保存することもできます。これは、パスワードハッシュを保存するカラムを指定するだけで行なえます。

plugin :rodauth do
  account_password_hash_column :password_hash
end

Rodauthでこのオプションを設定すると、パスワードハッシュのチェックをRubyで行うようになります。

PostgreSQL/MySQL/Microsoft SQL Serverでデータベース関数を使わないようにする

RodauthとPostgreSQL/MySQL/Microsoft SQL Serverで、認証用のデータベース関数を使いたくないがハッシュテーブルは従来どおり別テーブルに保存したい場合は、次のようにします。

plugin :rodauth do
  use_database_authentication_functions? false
end

言い換えると、rodauth_get_salt関数とrodauth_valid_password_hash関数を独自に実装すれば、PostgreSQL/MySQL/Microsoft SQL Server以外のデータベースでもこの値をtrueにできます。

認証をカスタマイズする

Rodauthの設定用メソッドの中には、他の種類の認証方法を使えるようにできるものもあります。

認証をカスタマイズすると、ログインの変更やパスワードの変更などのRodauthの機能の使い方がわからなくなったり、カスタム設定を追加する必要が生じたりするかもしれません。ただし以下のカスタマイズ例では、ログイン機能とログアウト機能は正常に機能します。

  • LDAP認証を使う

アカウントがデータベースに保存されている状態でLDAP認証したい場合は、simple_ldap_authenticatorライブラリを利用できます。

require 'simple_ldap_authenticator'
plugin :rodauth do
  enable :login, :logout
  require_bcrypt? false
  password_match? do |password|
    SimpleLdapAuthenticator.valid?(account.username, password)
  end
end

データベースにアカウントがない状態でLDAPの有効なユーザーがログインできるようにしたい場合は、次のようにします。

require 'simple_ldap_authenticator'
plugin :rodauth do
  enable :login, :logout

  # LDAPで認証するのでbcryptライブラリをrequireしない
  require_bcrypt? false

  # セッションの値を:loginキーに保存する
  # (デフォルトの:account_idキーだとわかりにくいため)
  session_key :login

  # セッションの値で与えられたログインを使う
  account_session_value{account}

  # このログインそのものをアカウントとして使う
  account_from_login{|l| l.to_s}

  password_match? do |password|
    SimpleLdapAuthenticator.valid?(account, password)
  end
end
  • Facebook認証を使う

JSON APIでのFacebook認証の例を以下に示します。この設定では、クライアント側にJSONでPOSTリクエストを送信するコードがあることが前提です。このPOSTリクエストは/loginに送信され、FacebookでユーザーのOAuthアクセストークンを設定するaccess_tokenパラメータを含むとします。

 require 'koala'
 plugin :rodauth do
  enable :login, :logout, :jwt

  require_bcrypt? false
  session_key :facebook_email
  account_session_value{account}

  login_param 'access_token'

  account_from_login do |access_token|
    fb = Koala::Facebook::API.new(access_token)
    if me = fb.get_object('me', :fields=>[:email])
      me['email']
    end
  end

  # there is no password!
  password_match? do |pass|
    true
  end
end
  • その他のWebフレームワーク

Rodauthは、アプリケーションでRoda Webフレームワークが使われていなくても利用できます。これは、Rodauthを使うRodaミドルウェアを追加することで行なえます。

require 'roda'

class RodauthApp < Roda
  plugin :middleware
  plugin :rodauth do
    enable :login
  end

  route do |r|
    r.rodauth
    rodauth.require_authentication
    env['rodauth'] = rodauth
  end
end

use RodauthApp

RodauthはRodaアプリに対し、Rodaがレイアウト提供の目的で使われることを期待します。そのため、Rodauthを他のアプリ用のミドルウェアとして使う場合、Rodauthから使えるviews/layout.erbファイルがないのであれば、おそらくRodaのrenderプラグインの追加も必要になります。その場合、Rodauthがアプリと同じレイアウトを使えるようプラグインを適切に設定する必要もあるでしょう

ミドルウェア内部のルーティングブロックでenv['rodauth'] = rodauthを設定すると、Rodauthメソッドを簡単に呼び出せる方法をアプリに導入できるようになります。

Rodaを使わないアプリでのRodauth導入例をいくつか示します。

Rodauthでは、TOTP(Time-Based One-Time Passwords: RFC 6238)経由での2要素認証を使えます。Rodauthで2要素認証をアプリに統合する方法は、アプリでの必要に応じてさまざまなものがあります。

2要素認証のサポートはOTP機能の一部なので、ログイン機能に加えてOTP機能も有効にする必要があります。一般に、2要素認証を実装する場合は2要素認証を2種類用意し、プライマリの2要素認証が利用できない場合にセカンダリの2要素認証を提供するべきです。RodauthではSMSコードとリカバリーコードをセカンダリ2要素認証としてサポートします。

アプリで2要素認証をサポートし、かつ2要素認証を必須にしたくない場合は次のようにします。

plugin :rodauth do
  enable :login, :logout, :otp, :recovery_codes, :sms_codes
end
route do |r|
  r.rodauth
  rodauth.require_authentication

  # ...
end

OTP認証を全ユーザーで必須にし、アカウントを持っていないユーザーに対してOTP認証の設定を要求する場合の設定です。

route do |r|
  r.rodauth
  rodauth.require_authentication
  rodauth.require_two_factor_authentication_setup

  # ...
end

認証を必須にする場合の一般的な方法と同様に、特定のブランチでのみ2要素認証を必須にし、サイトの他の場所ではログイン認証を必須することもできます。

route do |r|
  r.rodauth
  rodauth.require_login

  r.on "admin" do
    rodauth.require_two_factor_authenticated
  end

  # ...
end

JSON APIサポート

プラグインに:jsonオプションを渡してJWT機能を有効にすると、JSONレスポンス取り扱いのサポートを追加できます。

plugin :rodauth, :json=>true do
  enable :login, :logout, :jwt
end

JSON APIをビルドするのであれば、:json => :onlyを渡すことでRodauthで通常読み込まれるHTML関連のプラグイン(render、csrf、flash、h)を読み込まないようにできます。

plugin :rodauth, :json=>:only do
  enable :login, :logout, :jwt
end

ただし、メール送信機能はデフォルトでrenderプラグインに依存していることにご注意ください。:json=>:onlyを使う場合は、renderプラグインを手動で読み込むか、*_email_body設定オプションでメールの本文を指定する必要があります。

JWT機能を導入すると、Rodauthに含まれるその他のJSON APIサポートもすべて利用できるようになります。

rodauthオブジェクトにカスタムメソッドを追加する

設定のブロック内でauth_class_evalを使うと、rodauth`オブジェクトから呼び出せるカスタムメソッドを追加できます。

plugin :rodauth do
  enable :login

  auth_class_eval do
    def require_admin
      request.redirect("/") unless account[:admin]
    end
  end
end

route do |r|
  r.rodauth

  r.on "admin" do
    rodauth.require_admin
  end
end

外部の機能を使う

有効にする設定メソッドは、Rodauthの外部にある機能を読み込めます。この外部機能のファイルは、rodauth/features/feature_nameからrequireできるディレクトリに置く必要があります。このファイルは以下の基本構造をとる必要があります。

module Rodauth
  # :feature_nameは、有効にしたい機能を指定する引数
  # :FeatureNameはオプションで、inspect出力を読みやすくする定数名を設定するのに使う
  Feature.define(:feature_name, :FeatureName) do
    # 認証値系メソッドを固定値で定義するショートカット
    auth_value_method :method_name, 1 # method_value

    auth_value_methods # 認証値メソッドごとに1つの引数

    auth_methods       # 認証メソッドごとに1つの引数

    route do |r|
      # この機能のルーティングへのリクエストをこのブロックで受ける
      # ブロックはRodauth::Authインスタンスのスコープで評価される
      # rはリクエストのRoda::RodaRequestインスタンス

      r.get do
      end

      r.post do
      end
    end

    configuration_eval do
      # メソッド固有の追加設定を必要に応じてここで定義する
    end

    # auth_methodsとauth_value_methodsのデフォルトの挙動を定義する
    # ...
  end
end

機能の構成方法の例については、Rodauthの機能のコードを参照してください。

ルーティングレベルの挙動をオーバーライドする

Rodauthのすべての設定メソッドは、Rodauth::Authインスタンスの挙動を変更します。しかし場合によってはルーティング層の扱いをオーバーライドしたくなることもあります。これは、r.rodauthを呼び出す前に以下のように適切なルーティングを追加するだけで簡単に行なえます。

route do |r|
  r.post 'login' do
    # ここにカスタム POST /login ハンドリングを記述する
  end

  r.rodauth
end

Rodauthテンプレートをプリコンパイルする

Rodauthは自分自身のgemフォルダにあるテンプレートを使います。fork型のWebサーバーを使っていて、コンパイル済みテンプレートを事前に読み込んでメモリを節約したい場合や、アプリをchrootしたい場合は、Rodauthのテンプレートをプリコンパイルすることでメリットを得られます。

plugin :rodauth do
  # ...
end
precompile_rodauth_templates

0.9.xからのアップグレード

Rodauthを0.9.xから現在のバージョンにアップグレードする場合の注意点です。

account_valid_passwordデータベース関数を使っていた場合はこれを削除し、上のマイグレーションに記載されている2つのデータベース関数を追加する必要があります。以下のコードをマイグレーションに追加することでこの作業を行えます。

require 'rodauth/migrations'
run "DROP FUNCTION account_valid_password(int8, text);"
Rodauth.create_database_authentication_functions(self)
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO ${DATABASE_NAME}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO ${DATABASE_NAME}"

類似のプロジェクト

以下はすべてRailsに特化しています。

  • Devise
  • Authlogic
  • Sorcery

著者

Jeremy Evans (code@jeremyevans.net

関連記事

Railsアプリの認証システムをセキュアにする4つの方法(翻訳)

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

JavaScript: 5分でわかるPromiseの基礎(翻訳)

$
0
0

概要

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

JavaScript: 5分でわかるPromiseの基礎(翻訳)

JavaScriptのPromiseをわかりやすく解説しました。Promiseの基本を5分で学びましょう。

この記事で学べること

本チュートリアルでは、JavaScriptの「Promise」の基礎を学びます。Promiseのすべてを網羅的に説明するものではありませんが、Promiseを理解してコードで使い始めるのに必要な知識を固めることができます。

Promiseが必要になるとき

Promiseを使うと、(コールバックのように)特定のコードの実行完了を待ってから次のコード片を実行できるようになります。

Promiseが重要な理由は何でしょうか。あるWebサイトがAPIからデータを読み込んで処理し、データを整形してからユーザーに表示するところを考えてみましょう。APIから情報を取得する前にデータの処理と整形を行おうとすれば、エラーか空白ページのどちらかが表示されておしまいです。Promiseを使うと、API呼び出しが成功するまでAPIデータが処理されたり整形されたりしないようにできます。

Promiseとは何か

JavaScriptのPromiseは、同期操作の最終的な結果を表す、一種のプレースホルダと考えてください。本質的にこのプレースホルダは、コールバックをアタッチできる1つのオブジェクトです。

Promiseは、以下の3つのステートの1つを取ります。

  • pending: 非同期操作が完了していない
  • fulfilled: 操作が完了し、Promiseが値を1つ持つ
  • rejected : 操作がエラーまたは失敗で終了した

pending状態でないPromiseは安定しています。いったん安定したPromiseのステートはずっとそのままになり、他のステートに移行することはできません。

Promiseを使う

Promiseを使う場合、関数から返された既成のPromiseを使うことがほとんどですが、関数のコンストラクタからPromiseを作ることもできます。

シンプルなPromiseは以下のような感じになります。

runFunction().then(successFunc, failureFunc);

上のコード例では、最初にrunFunction()を実行しています。runFunction()はPromiseを1つ返すので、Promiseが安定した場合にのみsuccessFunc関数かfailureFunc関数のいずれかを実行できるようになります。PromiseがfulfilledになるとsucessFunc関数が呼ばれ、Promiseが失敗するとfailureFunc関数が呼ばれます。

Promiseの例

次は独自のPromiseを作成するコード例です。読んですぐわからなくても問題ありません。

function delay(t){
  return new Promise(function(resolve){
    return setTimeout(resolve, t)
  });
}
function logHi(){
  console.log('hi');
}
delay(2000).then(logHi);

このコードにはdelay()関数とlogHi()関数という2つの関数があります。logHi()関数は単にコンソールに'hi'と出力します。delay()関数はもう少し複雑で、指定のタイムフレームを経過した後で解決するPromiseを1つ返します。

then()メソッドを使って、最終的にfulfilledまたはrejectedのどちらかの値を受け取るコールバックを登録します。

以上を念頭に置いてdelay(2000).then(logHi)を呼び、2000ms(=2秒)という値をdelay関数に渡します。2秒経過するとPromiseが解決し、その場合に限ってlogHi関数が呼ばれます。

Google Chrome Developer Toolsを開いてこのコード例を入力することでお試しいただけます。

Promiseのチェイン

Promiseの主なメリットのひとつは、いくつもの非同期操作をチェインできることです。つまり、最初の操作が成功した場合に限って次の操作を開始するように指定できるということです。これをPromiseチェインと呼びます。以下の例をご覧ください。

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 2000);

}).then((result) => {
  alert(result);
  return result + 2;
}).then((result) => {
  alert(result);
  return result + 2;
}).then((result) => {
  alert(result);
  return result + 2;
});

最初のPromiseは、2000ms経過すると解決して値1を返します。解決後にthen()ハンドラが呼び出され、アラートボックスに値1が表示されます。最後に値が2に足され、新しい値3が返されます。この値が次のthen()ハンドラに渡され、この処理を繰り返します。

見てのとおり実地のコード例ではありませんが、Promiseが互いにチェインする様子がここに示されています。これは、JavaScriptで外部リソースを読み込む場合や処理前にAPIデータの到着を待つといった特定のタスクで非常に役立ちます。

エラーハンドリング

ここまでに扱ったPromiseは、どれも「解決済み」のものばかりでしたが、ここからは趣向を変えます。.catch()を使うと、Promiseチェインのエラーをすべてキャッチできます。次のコード例で.catch()の動作をご覧ください。

// ....
})
.catch((e) => {
  console.log('error: ', e)
}

上は.catch()のシンプルなコード例です。これは、返されたエラーメッセージをコンソールにログ出力します。先のコード例にエラーハンドリングを追加してみましょう。

以下のコード例は先ほどのものと2箇所しか違っていません。2番目の.then()の後ろにエラーとエラーメッセージを追加しました。また、チェインの末尾に.catch()も追加してあります。このコードを実行するとどうなるでしょうか。

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 2000);

}).then((result) => {
  alert(result);
  return result + 2;
}).then((result) => {
  throw new Error('FAILED HERE');
  alert(result);
  return result + 2;
}).then((result) => {
  alert(result);
  return result + 2;
}).catch((e) => {
  console.log('error: ', e)
});

結果は次のとおりです。

  • 2秒経過するとPromiseが値1で解決する
  • この値が最初の.thenに渡されてアラートダイアログが画面に表示される。2が足され、新しい値3が2番目の.then()に渡される。
  • 新しいErrorがスローされる。実行は直ちに停止してPromiseはrejectedステートで解決する。
  • .catch()はエラー値を受け取って画面にログを出力する。コンソールには次のように表示されます。

最後に

お読みいただきありがとうございました。Promiseのもっと詳しい情報についてはこちらこちらこちらをご覧ください。本記事がPromise導入のよいきっかけとなればと願っています。最終的にWeb開発を学ぶ用意のある方は、6か月でフルスタックWeb開発を学ぶ究極ガイドをぜひチェックしてみてください。

私はWeb開発記事を週に4本公開しています。毎週配信しているメーリングリストを購読したい方はフォームから登録いただくか、Twitterで私をフォローして下さい。

本記事が皆さまのお役に立ちましたら、元記事の下にある[👏]ボタンを数回クリックしてサポートをお願いします。⬇⬇

関連記事

JavaScriptの正規表現のコンセプトを理解する(翻訳)

JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

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

$
0
0

こんにちは、hachi8833です。

先ごろRubyWorld ConferenceRuby Prize 2017を受賞された@solnicことPiotr Solnicaさんが近年力を入れているのがdry-rbシリーズです。

@solnicさんはあのVirtusの作者でもあるのですが、現在VirtusのREADMEには、Virtusの機能を分割改良してdry-rbにしたとあります。

Virtusを作ったことで、Rubyにおけるデータの扱い、中でもcoercion/型安全/バリデーションについて多くのことを学べました。このプロジェクトは成功を収めて多くの人々が役立ててくれましたが、私はさらによいものを作ることを決心しました。その結果、dry-typesdry-structdry-validationが誕生しました。これらのプロジェクトはVirtusの後継者と考えるべきであり、考慮点やより優れた機能がさらにうまく分割されています。Virtusが解決しようとした同種の現代的な問題について関心がおありの方は、ぜひdryシリーズのプロジェクトもチェックしてみてください。

@solnic
https://github.com/solnic/virtus より抄訳

Virtusは長く使われていて枯れているせいか、何年も更新されていません。
また、Virtusの動作が確認されているのはRuby 1.9.3/2.0.0/2.1.2/jrubyであるとのことなので、今後はdry-rbを使うのがよさそうです。そこで、とりあえずどんなgemがあるのかというレベルで少し調べてみました。

なお、dry-rbにはDiscourseで作られたフォーラムがあり、質問にはこちらを使って欲しいそうです。

dry-rbシリーズのラインナップ


dry-rb.orgより

dry-rbシリーズのgem同士の依存関係だけ、とりあえず.gemspecファイルを元にがんばって図にしてみました(draw.ioで作成)。ざっと見た限りでは、dry-validationとdry-structを使えばVirtusとだいたい同じことができそうです。これらをインストールすると、dry-coreやdry-typeなどの依存gemもインストールされます。

以下はhttp://dry-rb.org/gems/の記載順です。多くのgemは互いに依存関係にあります。11月7日時点のものなので、今後変わる可能性があります。gem名直後のかっこ内はバージョン番号です。

dry-validation(0.11.1)

述語論理に基づいたバリデーションを行うgemです。ドキュメントには、ActiveRecord/ActiveModel::Validationとstrong_parametersより数倍速いとあります(使うかどうかはまた別の問題です)。以下のようなユースケースを含む多くの使いみちがあります。

  • Form params
  • “GET” params
  • JSON/YAMLドキュメント
  • アプリの設定(ENVに保存されているなど)
  • ActiveRecord/ActiveModel::Validationsの置き換え
  • strong-parametersの置き換え
# dry-rb.orgより
schema = Dry::Validation.Form do
  required(:name).filled
  required(:age).filled(:int?, gt?: 18)
end

schema.("name" => "Jane", "age" => "30").to_h
# => {name: "Jane", age: 30}

schema.("name" => "Jane", "age" => "17").messages
# => {:age=>["must be greater than 18"]}

依存gem

  • ‘concurrent-ruby’, ‘~> 1.0’
  • ‘dry-configurable’, ‘~> 0.1’, ‘>= 0.1.3’
  • ‘dry-equalizer’, ‘~> 0.2’
  • ‘dry-logic’, ‘~> 0.4’, ‘>= 0.4.0’
  • ‘dry-types’, ‘~> 0.12.0’
  • ‘dry-core’, ‘~> 0.2’, ‘>= 0.2.1’

dry-types(0.12.2)

さまざまなビルトイン型を用いて柔軟な型システムを提供します。利用できる型リストにはTypes::Form::TimeTypes::Maybe::Strict::IntTypes::Maybe::Coercible::Arrayといった膨大な種類の型が含まれています。

# dry-rb.orgより
require 'dry-types'
require 'dry-struct'

module Types
  include Dry::Types.module
end

class User < Dry::Struct
  attribute :name, Types::String
  attribute :age,  Types::Int
end

User.new(name: 'Bob', age: 35)
# => #<User name="Bob" age=35>

依存gem

  • ‘concurrent-ruby’, ‘~> 1.0’
  • ‘dry-core’, ‘~> 0.2’, ‘>= 0.2.1’
  • ‘dry-container’, ‘~> 0.3’
  • ‘dry-equalizer’, ‘~> 0.2’
  • ‘dry-configurable’, ‘~> 0.1’
  • ‘dry-logic’, ‘~> 0.4’, ‘>= 0.4.2’
  • ‘inflecto’, ‘~> 0.0.0’, ‘>= 0.0.2’

dry-struct(0.4.0)

structに似たオブジェクトを扱える属性DSLです。constructor_typeで以下を指定することでさまざまな挙動のstructを定義できます。

  • :permissive: デフォルト
  • :schema
  • :strict
  • :strict_with_defaults
  • :weak:symbolized

機能が絞り込まれており、属性のwriterは提供されていません(データオブジェクトとしての利用)。そうした目的にはdry-typesが向いているそうです。

# dry-rb.orgより
require 'dry-struct'

module Types
  include Dry::Types.module
end

class User < Dry::Struct
  attribute :name, Types::String.optional
  attribute :age, Types::Coercible::Int
end

user = User.new(name: nil, age: '21')

user.name # nil
user.age # 21

依存gem

  • ‘dry-equalizer’, ‘~> 0.2’
  • ‘dry-types’, ‘~> 0.12’, ‘>= 0.12.2’
  • ‘dry-core’, ‘~> 0.4’, ‘>= 0.4.1’
  • ‘ice_nine’, ‘~> 0.11’

dry-transaction(0.10.2)

ビジネストランザクションを記述するDSLを提供します。トランザクションをモジュール化して再利用したり、トランザクションに引数でステップを追加したりできます。

# dry-rb.orgより
class CreateUser
  include Dry::Transaction(container: Container)

  step :process, with: "operations.process"
  step :validate, with: "operations.validate"
  step :persist, with: "operations.persist"
end

依存gem

  • “dry-container”, “>= 0.2.8”
  • “dry-matcher”, “>= 0.5.0”
  • “dry-monads”, “>= 0.0.1”
  • “wisper”, “>= 1.6.0”

dry-container(0.6.0)

dry-auto_inject gemと組み合わせることで依存関係逆転の法則を利用できる、シンプルでスレッドセーフなIoC(制御の反転)コンテナです。ソフトウェアの密結合を避けるのに使います。

# dry-rb.orgより
container = Dry::Container.new
container.register(:parrot) { |a| puts a }

parrot = container.resolve(:parrot)
parrot.call("Hello World")
# Hello World
# => nil

依存gem

  • ‘concurrent-ruby’, ‘~> 1.0’
  • ‘dry-configurable’, ‘~> 0.1’, ‘>= 0.1.3’

dry-auto_inject(0.4.4)

コンテナへの依存性注入を支援するmixinです。dry-containerとの組み合わせが良好ですが、#[]に応答できるコンテナなら何でもinjectionできます。

# dry-rb.orgより
# Set up a container (using dry-container here)
class MyContainer
  extend Dry::Container::Mixin

  register "users_repository" do
    UsersRepository.new
  end

  register "operations.create_user" do
    CreateUser.new
  end
end

# Set up your auto-injection mixin
Import = Dry::AutoInject(MyContainer)

class CreateUser
  include Import["users_repository"]

  def call(user_attrs)
    users_repository.create(user_attrs)
  end
end

create_user = MyContainer["operations.create_user"]
create_user.call(name: "Jane")

依存gem

  • ‘dry-container’, ‘>= 0.3.4’

dry-equalizer(0.0.11)

等しいかどうかをチェックする各種メソッドを追加します。依存しているgemはありません。コアファイルはequalizer.rb 1つだけと、dry-rbシリーズの中では最もシンプルなgemのようです。RubyのModule Builderパターン #2で紹介されているように、equalizer.rbではModule Builderパターンが使われています。

# dry-rbより
class GeoLocation
  include Dry::Equalizer(:latitude, :longitude)

  attr_reader :latitude, :longitude

  def initialize(latitude, longitude)
    @latitude, @longitude = latitude, longitude
  end
end

point_a = GeoLocation.new(1, 2)
point_b = GeoLocation.new(1, 2)
point_c = GeoLocation.new(2, 2)

point_a.inspect    # => "#<GeoLocation latitude=1 longitude=2>"

point_a == point_b           # => true
point_a.hash == point_b.hash # => true
point_a.eql?(point_b)        # => true
point_a.equal?(point_b)      # => false

point_a == point_c           # => false
point_a.hash == point_c.hash # => false
point_a.eql?(point_c)        # => false
point_a.equal?(point_c)      # => false

dry-system(0.8.1)

システム設定の自動読み込みや依存関係の自動解決などを行います。以下のコードを見てもdry-containerやdry-auto_injectが使われているらしいことがわかります。
このライブラリはdry-webにも使われています。

# dry-rb.orgより
require 'dry/system/container'

class Application < Dry::System::Container
  configure do |config|
    config.root = Pathname('./my/app')
  end
end

# now you can register a logger
require 'logger'
Application.register('utils.logger', Logger.new($stdout))

# and access it
Application['utils.logger']

依存gem

  • ‘inflecto’, ‘>= 0.0.2’
  • ‘concurrent-ruby’, ‘~> 1.0’
  • ‘dry-core’, ‘>= 0.3.1’
  • ‘dry-equalizer’, ‘~> 0.2’
  • ‘dry-container’, ‘~> 0.6’
  • ‘dry-auto_inject’, ‘>= 0.4.0’
  • ‘dry-configurable’, ‘~> 0.7’, ‘>= 0.7.0’
  • ‘dry-struct’, ‘~> 0.3’

dry-configurable(0.7.0)

スレッドセーフな設定機能をクラスに追加するmixinです。settingというマクロで設定します。

# dry-rb.orgより
class App
  extend Dry::Configurable

  # Pass a block for nested configuration (works to any depth)
  setting :database do
    # Can pass a default value
    setting :dsn, 'sqlite:memory'
  end
  # Defaults to nil if no default value is given
  setting :adapter
  # Pre-process values
  setting(:path, 'test') { |value| Pathname(value) }
  # Passing the reader option as true will create attr_reader method for the class
  setting :pool, 5, reader: true
  # Passing the reader attributes works with nested configuration
  setting :uploader, reader: true do
    setting :bucket, 'dev'
  end
end

App.config.database.dsn
# => "sqlite:memory"

App.configure do |config|
  config.database.dsn = 'jdbc:sqlite:memory'
end

App.config.database.dsn
# => "jdbc:sqlite:memory"
App.config.adapter
# => nil
App.config.path
# => #<Pathname:test>
App.pool
# => 5
App.uploader.bucket
# => 'dev'

依存gem

  • ‘concurrent-ruby’, ‘~> 1.0’

dry-initializer(2.3.0)

パラメータやオプションの初期化を定義するDSLです。Rails向けのdry-initializer-railsもあります。依存gemはなく、これも非常にシンプルなソースです(initializer.rb)。

# dry-rb.orgより
require 'dry-initializer-rails'

class CreateOrder
  extend Dry::Initializer
  extend Dry::Initializer::Rails

  # Params and options
  param  :customer, model: 'Customer' # use either a name
  option :product,  model: Product    # or a class

  def call
    Order.create customer: customer, product: product
  end
end

customer = Customer.find(1)
product  = Product.find(2)

order = CreateOrder.new(customer, product: product).call
order.customer # => <Customer @id=1 ...>
order.product  # => <Product @id=2 ...>

dry-logic(0.4.2)

組み立て可能な述語論理機能を提供します。dry-typeやdry-validationでも使われています。

# dry-rb.orgより
require 'dry/logic'
require 'dry/logic/predicates'

include Dry::Logic

# Rule::Predicate will only apply its predicate to its input, that’s all

user_present = Rule::Predicate.new(Predicates[:key?]).curry(:user)
# here curry simply curries arguments, so we can prepare
# predicates with args without the input
# translating this into words: check the if input has the key `:user`

min_18 = Rule::Predicate.new(Predicates[:gt?]).curry(18)
# check the value is greater than 18

has_min_age = Operations::Key.new(min_18, name: [:user, :age])
# in this example the name options is been use for accessing
# the value of the input

user_rule = user_present & has_min_age

user_rule.(user: { age: 19 }).success?
# true

user_rule.(user: { age: 18 }).success?
# false

user_rule.(user: { age: 'seventeen' })
# ArgumentError: comparison of String with 18 failed

user_rule.(user: { })
# NoMethodError: undefined method `>' for nil:NilClass

user_rule.({}).success?
# false

依存gem

  • ‘dry-core’, ‘~> 0.2’
  • ‘dry-container’, ‘~> 0.2’, ‘>= 0.2.6’
  • ‘dry-equalizer’, ‘~> 0.2’

dry-matcher(0.6.0)

柔軟で高い表現力を持つパターンマッチャーを提供します。依存gemはありません。
dry-monads gemと組み合わせるとEitherMatcherというものも使えるようになります。

# dry-rb.orgより
require "dry-monads"
require "dry/matcher/either_matcher"

value = Dry::Monads::Either::Right.new("success!")

result = Dry::Matcher::EitherMatcher.(value) do |m|
  m.success do |v|
    "Yay: #{v}"
  end

  m.failure do |v|
    "Boo: #{v}"
  end
end

result # => "Yay: success!"

dry-monads(0.3.1)

Rubyの例外ハンドリングとは別のエラーハンドリングをモナド(monad)で提供します。
モナドはHaskellで中心となる概念だそうです。

# dry-rb.orgより
require 'dry-monads'

M = Dry::Monads

maybe_user = M.Maybe(user).bind do |u|
  M.Maybe(u.address).bind do |a|
    M.Maybe(a.street)
  end
end

# If user with address exists
# => Some("Street Address")
# If user or address is nil
# => None()

# You also can pass a proc to #bind

add_two = -> (x) { M.Maybe(x + 2) }

M.Maybe(5).bind(add_two).bind(add_two) # => Some(9)
M.Maybe(nil).bind(add_two).bind(add_two) # => None()

依存gem

  • ‘dry-equalizer’
  • ‘dry-core’, ‘~> 0.3’, ‘>= 0.3.3’

dry-view(0.4.0)

Webのビューをビューコントローラ/レイアウト/テンプレートの3つの構成で記述できます。

  • ビューコントローラ
# dry-rb.orgより
require "dry-view"

class HelloView < Dry::View::Controller
  configure do |config|
    config.paths = [File.join(__dir__, "templates")]
    config.layout = "app"
    config.template = "hello"
  end

  expose :greeting
end
  • レイアウト (templates/layouts/app.html.erb)
<html>
  <body>
    <%= yield %>
  </body>
</html>
  • テンプレート (templates/hello.html.erb):
<h1>Hello!</h1>
<p><%= greeting %></p>

これで、レンダリングするビューコントローラを#callします。

view = HelloView.new
view.(greeting: "Greetings from dry-rb")
# => "<html><body><h1>Hello!</h1><p>Greetings from dry-rb!</p></body></html>

依存gem

  • “tilt”, “~> 2.0”
  • “dry-core”, “~> 0.2”
  • “dry-configurable”, “~> 0.1”
  • “dry-equalizer”, “~> 0.2”

dry-core(0.4.1)

dry-rbシリーズやROM(rom-rb: Ruby Object Mapper)共通のサポート用モジュールです。

  • キャッシュ
  • クラス属性(下サンプルコード)
  • クラスビルダー
  • 特殊定数(EMPTY_ARRAYUndefinedなど)
  • 機能の非推奨化サポート
  • 指定したタイミングでの拡張の読み込み
# dry-rb
require 'dry/core/class_attributes'

class MyClass
  extend Dry::Core::ClassAttributes

  defines :one, :two

  one 1
  two 2
end

class OtherClass < MyClass
  two 'two'
end

MyClass.one # => 1
MyClass.two # => 2

OtherClass.one # => 1
OtherClass.two # => 'two'

依存gem

  • ‘concurrent-ruby’, ‘~> 1.0’

dry-web-roda(0.9.1)

dry-rbとrom-rb(データベース永続化用)をルーティングツリーキットのRoda gemと連携させたシンプルなWebスタックです。

本筋ではありませんが、ちょっとだけ動かしてみました。
以下を実行すると、Railsっぽくプロジェクトが生成されます(構成はだいぶ違いますが)。

gem install dry-web-roda
dry-web-roda new my_app

bundle install --path vendor/bundleしてからshotgun -p 3001 -o 0.0.0.0 config.ruするとhttp://0.0.0.0:3001/でアプリが開きました。shotgunって何だろうと思ったら、Rackを自動でリロードするgemでした。

依存gem

  • “dry-configurable”, “~> 0.2”
  • “inflecto”, “~> 0.0”
  • “roda”, “~> 2.14”
  • “roda-flow”, “~> 0.3”
  • “thor”, “~> 0.19”

関連記事

Railsコードを改善する7つの素敵なGem(翻訳)

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳)

週刊Railsウォッチ(20171117)Rails開発3年分のコツ集大成、PostgreSQL 10.1でセキュリティ問題修正ほか

$
0
0

こんにちは、hachi8833です。Firefox Quantumで泣きましたか?私は拡張使ってませんでした。

11月中旬のウォッチ、いってみましょう。

Rails: 今週の改修

今週の公式更新情報はありません。

タイムゾーンがあいまいになることがある問題を修正

# 修正前
"2014-10-26 01:00:00".in_time_zone("Moscow")
#=> TZInfo::AmbiguousTime: 26/10/2014 01:00 is an ambiguous local time.

# 修正後
"2014-10-26 01:00:00".in_time_zone("Moscow")
#=> Sun, 26 Oct 2014 01:00:00 MSK +03:00
# activesupport/lib/active_support/values/time_zone.rb
     def period_for_local(time, dst = true)
-      tzinfo.period_for_local(time, dst)
+      tzinfo.period_for_local(time, dst) { |periods| periods.last }
     end

ほとんど何も説明がありません。普段はしないのですが、y-yagiさんブログをこっそりチェックすると以下のように書かれています。Rubyの挙動に合わせてActiveSupportが修正されたとのことです。

Europe/Moscowのようにタイムゾーンが複数ある値を指定した場合に、TZInfo::AmbiguousTimeが発生していたのを、発生しないよう修正しています。

「同じ地域に複数のタイムゾーンがある」というのがよくわからなかったので、#31128で修正されたという#17395を見ると、「最近(2014年)ロシアのタイムゾーン変更がtzinfoに反映された」とあります。さらに調べると、Time-j.netで以下の記述を見つけました。

ロシアのタイムゾーンは2014年10月26日(日)から以下の図のようにUTC+2 ~ UTC+12 の11個のタイムゾーンになりました。世界一広い国だけあって国内の最大の時差が10時間あります。
サマータイムについては、実施していません。2010年までは実施していましたが、2011年にサマータイムを標準時間としてサマータイムを廃止し、2014年10月26日(日)に本来の標準時間に戻りました。
2016年には、以下の地域で1時間時計の針を進めるタイムゾーンの変更がありました。

www.time-j.netより

そしてtimeanddate.comで、上のソースと同じ日付の2014/10/26 2:00に変更が行われていることがわかりました。このエッジケースに対応したということのようです。

ロシアのタイムゾーンが近年こんなにガッツンガッツン変更されているとは知りませんでした。ロシア国民とライブラリメンテナの苦労が伺えます。
プーチン、無茶しよるのう…

rails newでできる.gitignoreにmaster keyが含まれていなかったのを修正

# railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb

+      def ignore_key_file_silently(key_path, ignore: key_ignore(key_path))
+        append_to_file ".gitignore", ignore if File.exist?(".gitignore")
+      end

つっつきボイス: 「おっとっと、気をつけないとmaster keyをリポジトリに突っ込んじゃうやつだ」

Rails

3年かけて培ったRails開発のコツ


blog.kollegorna.seより

たくさん載ってます。

  • トップレベルのコントローラではrescue_fromを使う
  • コントローラではload_resourceを使う
  • ほか続々

つっつきボイス: 「よく使うテクニックもいろいろありそうなので、ざっと見ておいて損はなさそう」「これ翻訳しますね」

Rack DeflateをオンにしてRailsのページサイズを80%削減(Ruby Weeklyより)


schneems.comより

Railsの設定にRack::Deflateを追加することで100msほど高速化したそうです。

# 同記事より
config.middleware.insert_after ActionDispatch::Static, Rack::Deflater

つっつきボイス: 「要するにzipしてるってことね」

なぜService Objectが思ったほど普及しないのか(Ruby Weeklyより)


aaronlasseigne.comより

この記事は、先週のウォッチでご紹介したEnough With the Service Objects Already(Service Objectにちょっとうんざり)に答える内容です。


つっつきボイス: 「確かにこのコード先週も見たなー↓」「『彼の主張には一理あるかもしれない』だそうです」「Service Objectは使いみちが広い分、増えたときに設計がぐらつかないようにしないといけないし、YAGNIになる可能性もあるかも」

# 同記事より
class IpnProcessor
  def process_ipn(...)
    # controller code here
  end
end

SprocketからWebpackに移行する方法(Ruby Weeklyより)


rossta.netより

手順が具体的でよさそうです。


つっつきボイス: 「Webpack使う機会がまだなかった…」「Webpackいいですよー」「そうそう、Webpack使うとjavascript_pack_tagが挿入される↓」

<!-- application.html.erb -->

<html>
    <body>
        <!-- ... -->
        <%= javascript_pack_tag 'vendor' %>
        <%= javascript_include_tag 'vendor' %>

        <%= javascript_pack_tag 'application' %>
        <%= javascript_include_tag 'application' %>
    </body>
</html>

:before_validateコールバックの変わった使い方(Ruby Weeklyより)


karolgalanciak.comより

# 同記事より
class MyModel
  before_validate :strip_url

  private

  def strip_url
    self.url = url.to_s.strip
  end
end

つっつきボイス::before_validateは一見バッドプラクティスに見えたりするけど、データのフォーマットはバリデーション前にやらないと意味がないこともある」

この間翻訳させていただいた記事「RailsのObject#tryがダメな理由と効果的な代替手段」の著者でもあります。

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

Ruby/Railsプログラミングでよくある5つの間違い


business2community.comより

  • method_missingの使いすぎ
  • gemに頼りすぎ
  • アプリのロジックがビューに漏出する
  • 「コントローラを薄くする」ことにこだわりすぎる
  • SQLインジェクションを放置する

つっつきボイス:method_missingって、Railsアプリではまず書かないかな: gem作るとかフレームワーク作るならともかく」「アプリのロジックがビューに漏れるというのも、このぐらいだったら許容範囲なんじゃないかなー↓: 程度問題だけど」

# 同記事より
<h2>
Congratulations
<% if winning_player %>
<%= winning_player.name %>
<% else %>
Contestant
<% end %>
</h2>

「DHHが薄いコントローラを推してるんですよね」「ファットコントローラは確かによくないけど」

あなたが知らないかもしれないRailsの5つのコツ

# 同記事より
params[:sort].presence_in(sort_options) || :by_date

つっつきボイス:Object#presence_inは知らなかったナー」「後はおなじみといえばおなじみかな」

テストは簡潔に書くべきか言葉をつくすべきか

# 元記事より
# アプローチ1
describe '#[]' do
  subject { [1, 2, 3].method(:[]) }

  its_call(0) { is_expected.to ret 1 }
  its_call(1..-1) { is_expected.to ret [2, 3] }
  its_call(:b) { is_expected.to raise_error TypeError }
end

# アプローチ2
describe '#[]' do
  subject { [1, 2, 3] }

  it 'returns the value at a given index' do
    expect(subject[0]).to eq 1
  end
  it 'returns a list of values between a range on indexes' do
    expect(subject[1..-1]).to eq [2, 3]
  end
  it 'raises TypeError when passed a symbol' do
    expect { subject[:b] }.to raise_error TypeError
  end
end

つっつきボイス: 「う、実はアプローチ1って個人的には好き」「テストでエラーになったときに、テストを修正すべきなのかコードを修正すべきなのかわからないと困っちゃう」「普通にアプローチ2かな」
「これと直接関係ないけど、過去の経緯でプロジェクトによってテストの書き方が大きく違ってたりすると大変」「its_callなんてのがあるのか」「自分はlet好きなのでlet使う派」「私はletキライ」「お、派閥が分かれてるんですね」

Rubyで「Interactor」パターン

# 同記事より

# DeleteAccount interactor
class DeleteAccount
  include Interactor
  def call
  end
end

# Controller
def destroy
  account = Account.find(params[:id])
  DeleteAccount.call(account: account) # pass whatever you want as a hash
end

つっつきボイス: 「InteractorパターンってFacadeみたいなものなのかな?」「#call使いまくるあたりがそんな感じですね」

以下の記事でもInteractor gemが取り上げられています。

Railsコードを改善する7つの素敵なGem(翻訳)

parallel_tests: テストを並列化するgem

RSpec、Test::Unit、Cucumberのテストを並列化できるそうです。

# grosser/parallel_testsより
rake parallel:test          # Test::Unit
rake parallel:spec          # RSpec
rake parallel:features      # Cucumber
rake parallel:features-spinach       # Spinach

rake parallel:test[1] --> force 1 CPU --> 86 seconds
rake parallel:test    --> got 2 CPUs? --> 47 seconds
rake parallel:test    --> got 4 CPUs? --> 26 seconds
...

つっつきボイス: 「これ使った方います?」「一応データベースもスレッド化してくれるようです: ただ、案件で追加してみたときはテスト動かなくなったんで外しました」「もしかしてテストの方に問題があったのかもw」「プロジェクトの初期段階から使うのがよさそう」

multiverse: Railsで複数のデータベースを扱うgem(Ruby Weeklyより)

ankaneさんの作です。半月足らずで★200超えてます。


つっつきボイス: 「データベース間でJOINできます?」「使ってみないとわかりませんが、さすがに無理かなー」

data-migrate: スキーマの他にデータもマイグレーションするgem

これも半月足らずで★300近くあります。

$> rake -T data
rake data:forward                 # Pushes the schema to the next version (specify steps w/ STEP=n)
rake data:migrate                 # Migrate data migrations (options: VERSION=x, VERBOSE=false)
rake data:migrate:down            # Runs the "down" for a given migration VERSION
rake data:migrate:redo            # Rollbacks the database one migration and re migrate up (options: STEP=x, VERSIO...
rake data:migrate:status          # Display status of data migrations
rake data:migrate:up              # Runs the "up" for a given migration VERSION
rake data:rollback                # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake data:version                 # Retrieves the current schema version number for data migrations
rake db:forward:with_data         # Pushes the schema to the next version (specify steps w/ STEP=n)
rake db:migrate:down:with_data    # Runs the "down" for a given migration VERSION
rake db:migrate:redo:with_data    # Rollbacks the database one migration and re migrate up (options: STEP=x, VERSIO...
rake db:migrate:status:with_data  # Display status of data and schema migrations
rake db:migrate:up:with_data      # Runs the "up" for a given migration VERSION
rake db:migrate:with_data         # Migrate the database data and schema (options: VERSION=x, VERBOSE=false)
rake db:rollback:with_data        # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake db:version:with_data         # Retrieves the current schema version numbers for data and schema migrations

つっつきボイス:rake db:migrate:down:with_dataのような感じで使うのね」

webmock: HTTPリクエストのモック/expectation gem

リクエストのheaderやbodyを細かに指定できます。★2600超えです。

require 'webmock/rspec'

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: "abc", headers: {'Content-Length' => 3}).twice

expect(WebMock).not_to have_requested(:get, "www.something.com")

expect(WebMock).to have_requested(:post, "www.example.com").
  with { |req| req.body == "abc" }
# Note that the block with `do ... end` instead of curly brackets won't work!
# Why? See this comment https://github.com/bblimke/webmock/issues/174#issuecomment-34908908

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: {"a" => ["b", "c"]})

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: hash_including({"a" => ["b", "c"]}))

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: {"a" => ["b", "c"]},
    headers: {'Content-Type' => 'application/json'})

Grill.rb: バーベキューしながらRubyカンファレンス

今年の7月にポーランドで開催されたカンファレンスです。

蚊に食われないかなと心配になってしまいます。

grill rbさん(@grill.rb)がシェアした投稿

ReactやRailsで作った28のアプリリスト(ソース付き)

作者はさまざまです。アプリ開発のヒントにしたり、作りたいアプリが思いつかない学生さんとかにもよいかもしれません。

github-awesome-autocompleteはちょっと便利そう。


github.algolia.comより

Ruby trunkより

提案: ArgumentErrorにメソッドのプロトタイプを表示

[4] pry(main)> Kerk.new.foo1
ArgumentError: wrong number of arguments (0 for 1)
Method prototype:
    def foo1(a)
from /home/esjee/src/printprototype/spec/kerk_class.rb:2:in `foo1'

つっつきボイス: 「ノンジャパニーズの方が日本語で書いているところにほだされてしまいました」「エラーにプロトタイプって必要かしらん」
「sentry-ravenって何だろ?」「raven-rubysentry.ioというエラーレポート集約サイトがありますね」


sentry.ioより

Ruby

ベテランRubyistならPythonコードを5倍速くできることもある


schneems.comより

Rubyに精通していればPythonでも同じ考えがいろいろ通用するという主旨です。Richard Schneemanさんは怒涛のように濃い記事を書いてますね。

Total time: 1.17439 s
File: perf.py
Function: get_legal_moves_fast at line 53

Total time: 5.80368 s
File: perf.py
Function: get_legal_moves_slow at line 69

ガイジン向けRubyKaigiガイド(翻訳)

requireの仕組み(Ruby Weeklyより)


ryanbigg.comより

とても短い記事です。active_support/allというファイルがないのにrequireできる理由を解説しています。
作者のRyan Biggさんは2011年にRuby Heroを受賞しています。

Railsの`CurrentAttributes`は有害である(翻訳)

一味違う方法でRubyのパフォーマンスをプロファイリング(Ruby Weeklyより)


kollegorna.seより

ruby-prof-flamegraphというgemを援用して次のようなflame graphを生成する記事です。flame graphという呼び名を初めて知りました。関係ありませんが、最近medium.comでよい記事を見かけることが多い気がします。


kollegorna.seより


つっつきボイス: 「このグラフどうやって読むの?」「一番下がThreadだから、呼び出しが上に進んでいる感じですね」

wsdirector: WebSocketをCLIとyamlで操作するgem(Ruby Weeklyより)

yamlでWebSocketのやり取りを書いて実行できます。

  # script.yml
  - client: # first clients group
      name: "publisher" # optional group name
      multiplier: ":scale" # :scale take number from -s param, and run :scale number of clients in this group
      actions: #
        - receive:
            data: "Welcome"
        - wait_all # makes all clients in all groups wait untill every client get this point (global barrier)
        - send:
            data: "test message"
  - client:
      name: "listeners"
      multiplier: ":scale * 2"
      actions:
        - receive:
            data: "Welcome"
        - wait_all
        - receive:
            multiplier: ":scale" # you can use multiplier with any action
            data: "test message"
wsdirector script.yml ws://websocket.server:9876 -s 10

#=> Group publisher: 10 clients, 0 failures
#=> Group listeners: 20 clients, 0 failures

つっつきボイス: 「テスト用なのかな」「そういえばWebSocketまだ使ったことなかったナ」

なおスポンサーはEvil Martiansです。同社ブログの以下の記事を翻訳させていただきました。

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

ぼっち演算子&.の落とし穴


antulik.comより

これもとても短い記事です。

# 同記事より
# 落とし穴を踏んだコード
if start_date &.< start_of_month && end_date.nil?
  # …
end

つっつきボイス: 「演算子結合の優先順位の罠か」「(メソッドチェーンかと思ったら違った…)」

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

PB MEMO: Rubyコミットを追いかけるブログ

Railsコミットをひたすら追うy-yagiさんのなるようになるブログのように、このブログではRubyのコミットをひたすら追いかけています。頭が下がります。

Rubyのインスタンス変数とアクセス制御

Rubyのインスタンス変数とアクセサの関係がちょっとモヤモヤしてたので貼ってみました。


つっつきボイス: 「dispだとdisplayしか思いつかないw」「attr_accessorでアクセサを作ると、代入時に同じ名前のインスタンス変数が作成されるという理解」
protectedはJavaと同じに考えるとハマるやつですな」「access controlとして作ってないんだとすると、どんな意図で作られたんだろう…」


追記: Junichi Itoさんの英語ブログ「Matz answers why Ruby lets sub classes to access private methods in super class」がわかりやすいです。

(動画)MatzとRuby 3.0について語る

30分の動画です。駆け足で聞いてみたところ、最初と最後はよもやま話で、12:02あたりからがRuby 3.0の話でした。「パフォーマンス」「concurrency」「型」の3つのコンセプトについて語っています。


つっつきボイス: 「ここ駐車場かな?」「いや、運転してるし」

SQL

PostgreSQL 10の記事が続々出ています。

PostgreSQL 10.1などリリース: セキュリティ問題修正(Postgres Weeklyより)


postgresql.orgより

バージョン9以前に影響するものもあります。

  • CVE-2017-12172: 初期化スクリプトに権限昇格の脆弱性
  • CVE-2017-15098: JSON関数でサーバーのメモリの一部が露出
  • CVE-2017-15099: INSERT ... ON CONFLICT DO UPDATEすると権限無しでSELECTできる

pglogical拡張でPostgreSQL 9.6から10にダウンタイム最小限で移行する(Postgres Weeklyより)


rosenfeld.herokuapp.comより

PostgreSQL 10のテーブル継承と宣言的パーティショニングでスケールする(Postgres Weeklyより)


timescale.comより

PostgreSQLのパーティショニングされたテーブルを10のネイティブパーティショニングに移行する(Postgres Weeklyより)


openscg.comより

PostgreSQL 11の機能を先行紹介するブログ


depesz.comより


つっつきボイス: 「何と気が早い」

JavaScript

webpack-bundle-size-analyzer: Webpackのインストール内訳を分析

Webpackでインストールされたライブラリをこんな感じで表示できます。


github.com/robertknight/webpack-bundle-size-analyzerより

CSS/HTML/フロントエンド

Unicodeについて知っておくべき5つのこと


gojko.netより

  • 画面に表示されないUnicodeポイントはたくさんある
  • 見た目が互いにそっくりなUnicodeポイントはたくさんある
  • 正規化(normalization)はそんなに簡単な話じゃない
  • 表示の長さとメモリのサイズは同じとは限らない
  • Unicodeは単なる静的なデータではない

つっつきボイス: 「右の4つから左の合字を作れる話を思い出しました↓」


http://unicode.org/emoji/charts/emoji-zwj-sequences.htmlより

Source Mapとは何か(Ruby Weeklyより)

// 同記事より
{
  "version":3,
  "file":"application.js",
  "mappings": "AAAA;AACA;AACA;#...",
    "sources": [
      "jquery.source-56e843a66b2bf7188ac2f4c81df61608843ce144bd5aa66c2df4783fba85e8ef.js",
      "jquery_ujs.source-e87806d0cf4489aeb1bb7288016024e8de67fd18db693fe026fe3907581e53cd.js",
      "local-time.source-b04c907dd31a0e26964f63c82418cbee05740c63015392ea4eb7a071a86866ab.js"
    ],
    "names":[]
}

Firefox Quantum(57)から拡張機能はWebExtensionのみになる

Firefox 57リリース後にBPS社内でも小さな悲鳴がいくつか上がっていました。


つっつきボイス: 「私も拡張全滅しましたorz」

WebAssemblyが主要なブラウザでサポート(Frontend Weeklyより)


blog.mozilla.orgより

FirefoxとChromeに続き、SafariとEdgeでもWebAssemblyがサポートされたとのことです。


つっつきボイス: 「Aaron PattersonさんがWebAssemblyに興味持ってると言ってたのを思い出しました: Rubyで動くかしら」「機械語になるから難しそうですね」

dev.toの激速が話題

記事そのものもよさそうです。


つっつきボイス:CDNの効果が大きいのかな」

その他

スレッドとは何か

10年間見逃されていたmanコマンドの脆弱性


sudosatirical.comより

バージョンアップできないAndroid端末の台数をグラフ化

danluu.comより

番外

無電力パワードスーツで工場の事故や怪我を大きく削減


今週は以上です。

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

週刊Railsウォッチ(20171110)dry-rbでFormObjectを作る、RailsのSQLインジェクション手法サイト、年に1度だけ起きるバグほか

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured


Ruby 2.5のパフォーマンス改善点(翻訳)

$
0
0

概要

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

Ruby 2.5のパフォーマンス改善点(翻訳)

Rubyは常に改善を繰り返しており、Ruby 2.5でも同様です。

Ruby 2.5でいくつかの最適化が行われました。

  • サイズの大きな文字列を作成したときの式展開が約72%高速化
  • String#prependの引数が1つだけの場合に約42%高速化
  • Enumerable#sort_byEnumerable#min_byEnumerable#max_byが約50%高速化

ベンチマークを見てみましょう。

文字列の式展開パフォーマンス

この最適化のコミットメッセージに含まれていたコード例を使いました。

require 'benchmark/ips'

Benchmark.ips do |x|
  x.report "Large string interpolation" do |t|
    a = "Hellooooooooooooooooooooooooooooooooooooooooooooooooooo"
    b = "Wooooooooooooooooooooooooooooooooooooooooooooooooooorld"

    t.times { "#{a}, #{b}!" }
  end

  x.report "Small string interpolation" do |t|
    a = "Hello"
    b = "World"

    t.times { "#{a}, #{b}!" }
  end

  x.compare!
end

以下の結果を得ました。

  • Ruby 2.4.1:
Small string interpolation:  3236291.1 i/s
Large string interpolation:  1711633.4 i/s - 上より1.89倍遅い
  • Ruby 2.5:
Small string interpolation:  3125175.1 i/s
Large string interpolation:  2555782.6 i/s - 上より1.22倍遅い

見てのとおり、サイズの大きな文字列でパフォーマンスがかなり向上しました。

String#prependのパフォーマンス

prependメソッドは、arrayの先頭にテキストを挿入します。

Ruby 2.5では、もっともよく使われるケースとして文字列を1つだけ追加する場合の最適化を行いました。

ベンチマーク結果は次のとおりです。

  • Ruby 2.4.1:
String#prepend  3.428M (± 3.2%) i/s - 17.159M in   5.011008s
  • Ruby 2.5:
String#prepend  4.638M (± 3.6%) i/s - 23.276M in   5.025562s

これはなかなかの改善です。

Enumerableのパフォーマンス改善

いくつかのEnumerableメソッドでパフォーマンスが向上しました。

この最適化では<=>メソッドのディスパッチメソッドをスキップすることでパフォーマンスが向上します。

コミットメッセージには以下の記述があります。

Fixnum/Float/Stringオブジェクトへのディスパッチで、<=>メソッドではなくOPTIMIZED_CMP()を使う
同リンクより抄訳

ベンチマーク結果は次のとおりです。

  • Ruby 2.4.2:
Enumerable#sort_by    2.395k (± 6.7%) i/s - 11.952k in   5.014422s
Enumerable#min_by     8.244k (± 6.1%) i/s - 41.405k in   5.042327s
Enumerable#max_by     8.053k (± 6.7%) i/s - 40.180k in   5.015375s
  • Ruby 2.5:
Enumerable#sort_by    5.914k (± 6.7%) i/s  - 29.786k in   5.062584s
Enumerable#min_by     15.668k (± 3.0%) i/s - 78.888k in   5.039748s
Enumerable#max_by     15.544k (± 2.3%) i/s - 78.408k in   5.046709s

50%ほど向上していますね🙂

Range#minRange#max

他にも2つのパフォーマンス改善があります。

1つはRange#minRange#maxです。

ベンチマーク結果は次のとおりです。

  • Ruby 2.4.2
Range#min    7.976M (± 3.0%) i/s - 39.950M in   5.013242s
Range#max    7.996M (± 3.4%) i/s - 40.059M in   5.015984s
  • Ruby 2.5
Range#min   13.154M (± 3.0%) i/s -  65.731M in   5.002094s
Range#max  13.021M (± 2.6%) i/s  -  65.202M in   5.010924s

コミットはこちら

String#scanの改善

コミットメッセージによると、文字列パターンで50%、正規表現パターンで10%パフォーマンスが向上したとのことです。

ベンチマークをチェックしてみましょう。

  • Ruby 2.4.2
String#scan - String pattern
       1.367M (±19.8%) i/s - 6.458M in   4.982047s
String#scan - Regex pattern
       1.228M (±17.0%) i/s - 5.881M in   4.983943s

Ruby 2.5

String#scan - String pattern
      3.944M (±24.4%) i/s - 17.739M in   4.977417s
String#scan - Regex pattern
      1.696M (±17.4%) i/s -  8.103M in   4.982614s

高速化したscanに乾杯!

まとめ

12/25にリリース予定のRuby 2.5で導入される新しい最適化について解説しました。

最適化は、文字列の式展開、EnumerableメソッドString#prependメソッド、String#scanメソッド、そしてRange#max / Range#mixで行われました。

本記事がお役に立てば幸いです。

ぜひ、好みのSNSで本記事をシェアしてください(元記事からどうぞ)🙂

関連記事

ベンチマークの詳しい理解と修正のコツ(翻訳)

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)

$
0
0

概要

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

3年以上かけて培ったRails開発のコツ集大成(翻訳)

順序は特に決まっていません。

1. トップレベルにrescue_fromを書く(permalink

ルートのコントローラにrescue_fromを書くと、その下で発生したすべての例外をキャッチできるので非常に便利です。Webアプリにこれを追加すると、リクエスト/レスポンスのサイクルで実行されるほとんどのコードが一気に便利になります。シンプルなAPIを例に考えます。rescue_fromを使えば、レコードが見つからない(ActiveRecordがActiveRecord::RecordNotFoundをスローする)場合のアプリの振る舞いを明示的に指定できます。

rescue_from ActiveRecord::RecordNotFound do
  api_error(status: 404, errors: 'Resource not found!')
end

2. コントローラにload_resourceを書く(permalink

もうひとつのパターンは、以前同僚が使っていたのを見て以来採用しているものです。必要なリソースのフェッチをコントローラのメソッド内で行う代わりに、共通のコントローラのフィルタを使い、アクションの実行に応じてフェッチするというものです。

class UsersController
  before_action :load_resource

  def index
    # @usersで何かする
  end

  def show
    # @userで何かする
  end

  def create
    # @userで何かする
  end

  def update
    # @userで何かする
  end

  def destroy
    # @userで何かする
  end

  private
    def load_resource
      case params[:action].to_sym
      when :index
        @users = paginate(apply_filters(User.all, params))
      when :create
        @user = User.new(create_params)
      when :show, :update, :destroy
        @user = User.find(params[:id])
      end
    end
end

これの発展版がdecent_exposureです。私自身はまだ使う機会がありませんが。

ところで、私は主に2つの理由から「よいコードは常にそれ自身が語る」(訳注: コメントを避けてコードに語らせる)という言説にあまり賛成できません。

  • ある開発者にとってよいコードであっても、別の開発者にとっては悪いコードになることもあります(スタイルは人それぞれなので、それ自体が悪いのではありません)。
  • 時間や予算の制約から、手早く修正してissueをクローズするしかないという状況はいくらでもありえます。最善の(そして最も自明な)ソリューションは「10倍努力する」ということがわかっていてもです。

というわけで、「コードが匂ってるな」と思ったら、恥ずかしがらずにどしどしコメントしましょう😃

3. decoratorやpresenterを使う(permalink

しばらく前から、「モデルをファットにして、その分コントローラを薄くせよ」という言説をRailsコミュニティで見かけます。「コントローラを薄くせよ」については同意しますが、ファットモデルについては同意できません😃。モデルもできるだけ薄くするべきであり、特殊の場合にしか使わないようなプロパティをモデルで自動生成しないことです。そのような場合はラッパークラスを使って(皆さん、これがdecoratorですよ!)必要なメソッドだけを公開しましょう。

presenterはdecoratorと似ていますが、複数のモデルを扱う点だけが異なります。

4. モデル配下のワーカーを名前空間化してafter_commitで呼び出す(permalink

Userというモデルがあるとしましょう。あるモデルに関連するバックグラウンドジョブの90%は、モデルの作成/更新/削除で発生します。ここでデータが変更されるからです。ここから、User::CreateWorkerUser::UpdateWorkerUser::DestroyWorkerという3つの一般的なワーカーを導き出せます。利用可能な場合にはこれらのワーカーをActiveRecordコールバックやprevious_changesと組み合わせて使ってみましょう。ワーカーの呼び出しはafter_commitで行います。理由についてはこちらをご覧ください。

5. PostgreSQLのarrayは、よほどシンプルでない限り使わないこと(permalink

PostgreSQLのarrayもクールなのですが、私の経験では、時間を節約するより問題を生み出す方が多くなります。PostgreSQLのarrayを使うと(何らかのIDを保存するなど)、後でそのテーブルを見たときに必ず私の頭が爆発しました。データベースにはJOINというものがあるのですから、テーブルを追加するコストは高くありません。

PostgreSQLのarrayは、うんと小規模な場合にしか使わないことにします。

  • テーブルに保存する要素が少数にとどまり、かつ要素の平均個数が将来増加しないことがわかっている場合(わずかな変動ならありです)
  • テーブルがIDや関連付けと一切関わりを持たないことがわかっている場合

6. Postgres JSONBはいいヤツ(permalink

PostgreSQLのarrayとは対照的に、PostgreSQLのJSONBは大好きです。データベースにスキーマがあることのメリットは明らかなので、私たちは皆スキーマを持つデータベースが大好きです。しかしながら、スキーマを事前に予測できない場合、スキーマレスデータベースのシンプルさがどうしても必要になることがあります。私は次のような場合にJSONBを使うことがよくあります。

  • 小さな属性を多数使うことがあり、しかも親属性で名前空間される可能性もある場合。普通のテーブルでこれをやると、カラムだらけになってしまいます。
  • 保存する内容が正確にわからない場合や、プロトタイプを急いで作る場合
  • オブジェクトのhydrationを作る場合: オブジェクトをJSON形式でデータベースに保存し、同じJSONからオブジェクトを再構成する

訳注: hydrationは化学用語の「水和/水和物」の借用で、シリアライズに少し似た概念です。

7. aasm gemはいいヤツ、ただしステートを変えて初期化しないこと(permalink

私はaasm gemが大好きです。ステートマシンで状態や操作を強制することができ、専用のきわめてシンプルなDSLを使えます。ただし、オブジェクトを初期状態と異なるステートで作成するとフックが動作しないという問題が生じます。aasmの内部状態とにらめっこして頑張るか、あきらめてオブジェクトの特定のステートを手動でスキャンすることになります(それ用のサービスを作ったりとか)。

8 .メールアドレスのバリデーションを頑張るよりgemを使う(permalink

メールアドレスのバリデーションに使う正規表現をググると毎回違う正規表現が見つかるのは、もう笑うしかありません。完璧な正規表現を探すのはとっととあきらめて、おとなしくgemを使うに限ります。

9. decoratorやpresenterを使って、ビューに渡すインスタンス変数をなるべく1つだけにする(permalink

私がRailsで残念に思っている部分です。コントローラからビューにコンテキストを渡すのにインスタンス変数をいくつも使うのは、バッドプラクティスだと思います。Sandi Metzの言うとおり、インスタンス化して渡すオブジェクトは常に1つだけにすべきです。

10. モデルに保存するインスタンスメソッドに!を付ける(permalink

モデルのメソッドがオブジェクトを変更してデータベースに保存する場合、メソッド名の末尾に必ず!を付けてそのことを示しましょう。簡単なことです。

クラスレベルで厳密なAPIを書くことがコードの品質を高めることにつながりますが、開発者はそのことを忘れがちです(私もですね!)。

11. 単に認証したい場合はDevise gemを使わないこと(permalink

Deviseはマジックが多すぎます。

12. Virtusを使って、ActiveRecordでないモデルの属性をより厳密に定義する(permalink

私はVirtus gemを多用していましたし、今も使っています。シンプルなPORO(素のRuby: Pure Old Ruby Object)でモデルのように振る舞うオブジェクトを構成でき、属性をある程度厳密に保つこともできます。私は、属性が増えすぎたときに次のようなVirtus向けの独自DSLを書いて属性を操作できるようにすることがよくあります。

# シリアライザなどに定義した属性を再利用できるシンプルなモジュール
module VirtusModel
  extend ActiveSupport::Concern

  included do
    include Virtus.model

    if defined?(self::ATTRIBUTES)
      self::ATTRIBUTES.each do |group|
        group[:attrs].each do |attr|
          attribute(attr, group[:type])
        end
      end
    end
  end

  class_methods do
    def all_attributes
      self::ATTRIBUTES.map{|i| i[:attrs]}.flatten
    end
  end
end
# モデルの例
class Model < ActiveModelSerializers::Model
  ATTRIBUTES = [
    {
      attrs: [
        :id, :name, :header_text, :is_visible, :filtering_control,
        :data_type, :description, :caregory, :calculation
      ],
      type: String
    },
    {
      attrs: [
        :display_index, :min_value, :max_value, :value_type,
        :number_of_forcast_years
    ],
      type: Integer
    },
    {
      attrs: [:category], type: Array
    },
    {
      attrs: [:is_multi_select],
      type: Virtus::Attribute::Boolean
    }
  ].freeze

  include VirtusModel
end

さまざまな属性の種類を列挙することも、属性のグループにある種のタグを追加することもできます。おかげで私はニッコニコです😃

なお、Railsのattributes APIができたので、これで同じか似たようなことができるのではないかと考えています😃

13. 外部API参照などの重たい処理にはメモ化(memoization)を使う(permalink

もうおわかりですよね😃

14. PostgreSQL全文検索は単純な用途に向いている(permalink

pg_searchは驚くほど簡単にセットアップできます。tvectorsなどでPostgreSQL全文検索を最適化しなければならない場合は、素直にElasticSearchを使いましょう。PostgreSQLでそれ以上時間をかけるのは無駄です。

15. 2017年にもなって未だにService Objectとは何かがちゃんと定義されていない(permalink

多くの人が同意してくれるService Objectのもっと明確な定義と、どのように実装すべきかを今も探し続けています。

私たちが最近手がけた案件では、あるパターンに従うことで再利用が楽になりました。最初に、モジュールを1つ作成します。これをincludeすると、performという名前のクラスメソッドを作成します。

次に、作成するすべてのサービスで、コンストラクタ(initialize)をprivateにします。つまり、このperformパブリッククラスメソッドだけを呼ぶということです(もちろんRubyのような動的言語ではprivateメソッドも呼ぼうと思えば呼べますが、単に呼びにくくするだけの処置です)。

module PerformerService
  def self.included(base)
    base.send(:define_singleton_method, :perform) do |url|
      begin
        return self.send(:new, url).send(:perform)
      rescue Exception => e
        Rails.logger.error("#{self.class}: Exception raised: #{e}")
      end

      return nil
    end
  end
end
class UrlParser
  include PerformerService
  private
    def initialize(url)
      @url = url
    end
    def perform
      # ここですごいことをやる
    end
end

UrlParser.perform('https://kollegorna.se')

16. ActiveRecordのエラーメッセージを好みの形に変換する(permalink

RailsでAPIを書くと、エラーメッセージはたいていJSONAPI形式に従います。つまり、メッセージ(can't be blank)とメッセージが失敗した属性(user_id)が出力されます。

この例ではJSONポインタを使っていませんが、これにも同じアイデアを適用できます。

クライアント側では好みに応じて次の2つの方法でこれらを扱います。フォームに移動してuser_id inputを赤で表示するか、メッセージを連結して「User id can’t be blank」などのように読みやすい形に変換するかです。

しかしメッセージに関連する属性がユーザーにとって意味のないものである場合はどうなるでしょうか。

このアプリで、各ユーザーは新しい投稿(post)を1つ作成できるとします。ただし投稿は1日1回までだとします。モデルで次のようにして一意性を強制します。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  }
}

(はい、DBレベルでも同じように一意性制約をかけるべきですよね、わかっております。しかしここでは仮に、ユーザーが2つの異なるサーバー(しかも同じアプリが動き、同じDBを使っている)にアクセスして、運よく(運悪く)2つのリクエストを完全に同時に受け取れないと困るので、このエラーは扱いません)

このときのメッセージは次のようになります。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "user_id",
      "message": "は既に使われています"
    }
  ]
}

ユーザーはこれを渡されても困ってしまいます。1つの方法は、messageオプションを使うことです。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  },
  message: 'さんの投稿は1日1回までです'
}

これで、メッセージは['user_id', 'さんの投稿は1日1回までです']のように多少読みやすくなりましたが、両方の属性を使う場合にあまり便利ではありません。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "user_id",
      "message": "さんの投稿は1日1回までです"
    }
  ]
}

理想は、このメッセージをbaseに移動することです。このメッセージは特定のモデル属性に依存しない、より一般的なカスタム制約だからです。これは、メッセージにカスタムDSLを追加すればできるようになります。

validates :user_id, {
  uniqueness: {
    scope: :post_id,
    conditions: -> { where('created_at >= ?', 1.days.ago) },
  },
  message: {
    replace: "user_id",
    with: {
      attribute: "base",
      message: "ユーザーの投稿は1日1回までです"
    }
  }
}
def replace_errors(errors)
  errors_array = []
  errors.messages.each do |attribute, error|
    error.each do |e|
      if e.is_a?(Hash) && e[:replace]
        errors_array << {
          attribute: e[:with][:attribute],
          message: e[:with][:message]
        }
      else
        array_hash << {attribute: attribute, message: e}
      end
    end
  end

  return errors_array
end

これで、使いたい属性に合うエラーが出力されます。

{
  "title": "リクエストを処理できませんでした",
  "message": "(エラーの詳しい説明)",
  "errors": [
    {
      "attribute": "base",
      "message": "ユーザーの投稿は1日1回までです"
    }
  ]
}

17. 値を返すメソッドでは明示的にreturnを書く(ワンライナーであっても)(permalink

Rubyコミュニティはreturn文を書かないことにこだわっていると思いますが、私はそこにこだわる理由はない気がしています。実際私は、たとえワンライナーであっても、副作用が目的ではなく戻り値を目的とすべき場合はreturn文を追加しています。

Rubyのクールさと表現力を云々することよりも、生産性と(ある種の)安全性の方が勝ります。

18. なるべくかっこ()を使う(ある種のDSLを使う場合を除く)(permalink

これも同様です。かっこを追加して困ることはありませんし、普段他の言語も使っている同僚が幸せになれます。

19. env変数に厳密な論理値型を追加する(permalink

私はconfig/sercrets.ymlで次のようなスニペットを使うのが好きです。

<%
booly_env = ->(value) {
  return false if value.blank?

  return false if (['0', 'f', 'false'].include?(value.to_s.downcase))
  return true if (['0', 't', 'true'].include?(value.to_s.downcase))
  return true
}
%>

こうすることで、論理値型のenv変数がtruefalseのどちらかだけを取るようになるので、コードで使いやすくなります。

development:
  enable_http_caching:  <%= booly_env[ENV["ENABLE_HTTP_CACHING"] || false] %>

20. PostgreSQL以外のデータベースをメインで使うのであれば十分な理由付けが必要(permalink

MongoDBはひと頃もてはやされていましたが、ほどなくしてMongoDBの欠点が知られるようになりました。

  • スキーマレスである: スキーマレスは機能の1つだと思うかもしれませんが、実際には大きな欠点です。データベースにスキーマがあることで、スキーマを必要に応じて少しずつ変更できますし、ツールや保証も得られます。たとえば、SQLにinteger型のカラムが1つあるとすると、これをstring型やtext型に変更することも、デフォルト値の設定やNULL禁止の設定もできます。これはスキーマレスなデータベースでは不可能であり、プログラミング言語を用いて高度なレベルで自作する必要があります。スキーマレスなデータベースでは、属性の追加や削除も不可能です。基本的に最初のスキーマに縛られてしまうので、一から作り直して正しく移行できることを自力で確認するか、アプリケーションレベルで扱うことになります。
  • トランザクションが使えない
  • ACIDでない
  • クエリが少し大きくなったときの速度も大したことはないと感じる

メインで使っているデータベースでこんな目に遭っても構いませんか?私はイヤです。個人的にMongoDBの唯一の目玉機能と思えるのは、親ドキュメントに多数のドキュメントを埋め込めることぐらいです。それ以外の機能はおそらくPostgreSQLで用が足ります(それにセキュリティアップデートの面倒を見なければならないデータベースシステムが1つで済みます)。

21. 動的スコープは、他に打つ手がない場合にはよいパターン(permalink

Rubyでクロージャ(proclambda)を定義すると、レキシカルなスコープや環境がクロージャにカプセル化されます。

これは、コードのAという場所でprocを定義したとしても、コードのBという場所でそれを渡して呼び出したときに、procが定義されたAのレキシカルスコープ内で定義されているものであれば変数でも何でも参照できるということです。言い方を変えると「環境について閉じている」ということです。

これを逆にしたらどうなるでしょうか。たとえばコードのAという場所でprocを1つ定義し、そこでprocを呼んでもまったく意味がないが、コードのBという場所でprocを呼びたい場合にクロージャのレキシカルスコープを変更することで、実行結果にBの環境が反映されるようにするとします。

次の例をご覧ください。

CLOSURE = proc{puts internal_name}

class Foo
  def internal_name
    'foo'
  end

  def closure
    proc{puts internal_name}
  end

  def name1
    closure.call
  end

  def name2
    CLOSURE.call
  end

end

puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> undefined local variable or method `internal_name' for main:Object (NameError)

クロージャの定義時点ではinternal_nameが定義されていないので、当然name2メソッドは失敗します。

しかし、instance_execを使うとprocのバインディング(レキシカルスコープ)を再定義できます。

CLOSURE = proc{puts internal_name}

class Foo
  def internal_name
    'foo'
  end

  def closure
    proc{puts internal_name}
  end

  def name1
    closure.call
  end

  def name2
    instance_exec(&(CLOSURE))
  end

end

puts Foo.new.name1 #=> foo
puts Foo.new.name2 #=> foo

成功です。これは、アプリのある部分に書いたコードを、まったく異なるコンテキストで実行できるということです。しかしこれはどんなときに便利なのでしょうか?このあたりをいろいろハックしてみましたが、非常に有用な使いみちの1つはRailsのルーティングでした。

次のようなルーティングがあるとします。

  namespace :api do
    namespace :v1 do
      resources :company_users, only: [:show] do
        resources :posts, only: [:index] do
          resource :stats, only: [:show]
        end
      end
    end
  end

上から以下のルーティングが生成されます。

/api/v1/company_users/:id
/api/v1/company_users/:company_user_id/posts
/api/v1/company_users/:company_user_id/posts/:post_id/stats

:company_user_idはどうやら不要なので、次のようにしてクライアント側での柔軟性を高めたいと思います。

/api/v1/stats?user_id=:company_user_id&post_id=:post_id

しかしAPIは既に本番で稼働していて変更は困難です。

  namespace :api do
    namespace :v1 do
      resources :company_users, only: [:show] do
        resources :posts, only: [:index] do
          resource :stats, only: [:show]
        end
      end

      resource :stats, only: [:show], defaults: {company_user_id: proc{params[:company_id]}}
    end
  end

ルーティングの中にparamsがある?そのとおり!理由は、次のスニペットを使って、procのコンテキストをコントローラのコンテキストに再バインドしているからです。

def reshape_hash!
    self.params = HashWithIndifferentAccess.new(params.to_unsafe_h.reshape(self))
end

これで、このルートにuser_idを送信すると、このメソッドをbefore_filterとして追加することで、company_user_idとして追加されます。

class Api::V1::StatsController < ApplicationController
  before_action :authenticate_user!
  before_action :reshape_hash!

  def index
    stats = Stats.new(current_user).all(
      user_id: params[:company_user_id], post_id: params[:post_id]
    )

    render json: stats, serializer: StatsSerializer
  end
...

このテクニックをルーティング以外で使ったこともありますが、ほとんどは最後の手段としてです。ご利用は計画的に。

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Railsのdefault_scopeは使うな、絶対(翻訳)

Ruby: ぼっち演算子`&.`の落とし穴(翻訳)

$
0
0

概要

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

Ruby: ぼっち演算子&.の落とし穴(翻訳)

Ruby 2.3で追加された便利な&.演算子(ぼっち演算子)のおかげで、コードをずいぶんきれいにできました。これはRubyの新しい&.!=演算子に記載されているデフォルト演算子と連結して使えます。

ところで、最近&.を使ったためにバグが起きてしまいました。元々のコードは次のとおりです。

if start_date && start_date < start_of_month && end_date.nil?
  # ...
end

リファクタリングして&.を使ったのが次のコードです。

if start_date &.< start_of_month && end_date.nil?
  # ...
end

書き換え後のコードはすっきりましたが、ここにバグがあるのがおわかりでしょうか?かっこを追加して実行順序を確認してみましょう。

if start_date &.< (start_of_month && end_date.nil?)
  # ...
end

問題は、導入した&.の直後にある特殊演算子<が普通のメソッドコールになってしまったことでした。&.<の右辺はメソッドの引数とみなされて最初に実行されてしまいます。

修正するには、次のように前半部分にかっこを追加する必要がありました。

if (start_date &.< start_of_month) && end_date.nil?
  # ...
end

この次リファクタリングで&.を使うときは、振る舞いが変更されないかどうか注意しましょう。

関連記事

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

$
0
0

概要

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

Rails: dry-rbでForm Objectを作る(翻訳)

現代のRailsでは、Form Objectを作るのは珍しくありません。多くのRuby開発者はVirtusActiveModel::ValidationsをincludeしてForm Objectを作成することに慣れています。本記事では、dry-typedry-validationを使ってForm Objectを作成する方法をご紹介したいと思います。

絵ハガキ(postcard)を作成する簡単なForm Objectを作ってみましょう。このアプリには次の3つのモデルがあります。

  • Country: フィールドはnameis_state_required。2つ目のフィールドは正しいアドレスの作成に使われ、米国などのユーザーは州名の入力が必要です。
  • Country::State: フィールドはnamecountry_id
  • Postcard: フィールドはstate_idcountry_idcontentaddress

完了までの作業を定義する

  • フォームで新しい絵ハガキを作成する(だいたいおわかりですね)
  • 住所、市町村、郵便番号、コンテンツ、国のバリデーションを行う
  • 郵便番号フォーマットのバリデーションを行う
  • コンテンツの長さのバリデーションを行う(ツィートやテキストメッセージ並に短くしたい場合)
  • 選択した国で州名が必要な場合、州名の存在のバリデーションも必要

属性と型

まずは属性の定義から行います。Form ObjectはDry::Types::Structから継承する必要があります。必要なゲッターやコンストラクタはDryで定義されます。

class Postcard
  module Types
    include Dry::Types.module
  end

  class CreateForm < Dry::Types::Struct
    attribute :address, Types::Coercible::String
    attribute :city, Types::Coercible::String
    attribute :zip_code, Types::Coercible::String
    attribute :content, Types::Coercible::String
  end
end

Dry::Types.moduleincludeするだけでDry-typesの型を使えるようになります。Dry-typesでは変更に応じた多くのプリミティブ型を選択できます

Railsモデルを使う場合はもう少し複雑です。これらの型で属性を作成するには、型を登録する必要があり、TypeName = Dry::Types::Definition.new(::MyRubyClass)のように行います。.constructorをブロック付きで呼び出すと、dry-typesで構成される型を指定できます。

定義は以下のような感じになります。

module Types
  include Dry::Types.module
  Country = Dry::Types::Definition.new(::Country)
  CountryState = Dry::Types::Definition.new(::Country::State)
end

これで、CountryCountryStateを型として使えるようになりました。最終的なフォームの定義は次のようになります。

class CreateForm < Dry::Types::Struct
  attribute :address, Types::Coercible::String
  attribute :city, Types::Coercible::String
  attribute :zip_code, Types::Coercible::String
  attribute :content, Types::Coercible::String
  attribute :country, Types::Country
  attribute :state, Types::CountryState
end

これでやっと、シンプルなstructを作成できました。

メモ: dry-typesのstructコンストラクタについて

コンストラクタの種類を指定しないと、strictコンストラクタが生成されます。この場合、属性が見つからないとArgumentErrorをスローします。存在のバリデーションはdry-validationで行うので、より多くの情報を含むコンストラクタであるschemaコンストラクタやsymbolizedコンストラクタを使うことになります。schemaコンストラクタを使うには、クラス本体の中でconstructor_type(:schema)を呼ぶ必要があります。

バリデーション

Form Object内部でバリデーションを実行するには、dry-validation gemを使います。これにはさまざまな述語(メソッド)が含まれており、使い方も簡単です。まずは存在のバリデーションを行ってみましょう。

PostcardSchema = Dry::Validation.Schema do
  required(:address).filled
  required(:city).filled
  required(:zip_code).filled
  required(:content).filled
  required(:country).filled
end

先ほど定義したモデルの属性を渡すスキーマを次のように定義します。

errors = PostcardSchema.call(to_hash).messages(full: true)

それではこの動作を見てみましょう。

  • to_hash(またはto_h): 属性をハッシュベースで生成する
  • .messages(full: true): 完全なエラーメッセージを返す。

フォーマットや長さなど、渡すバリデーションの要件を増やすには、単に.filledメソッドにパラメータを渡します。contentを例に取ると、存在バリデーションの他に、20文字より長いこともバリデーションされます。

required(:content).filled(min_size?: 20)

利用できる述語の全リストはこちらをご覧ください。

バリデーションロジックがさらに複雑な場合

存在や長さのバリデーション機能はdry-validationによって提供されます。残念なことに(?)、実際に動くアプリではこれだけでは不十分です。そのため、dry-validationで独自の述語を書けるようになっています。

まずは簡単なものから。バリデーションに渡されたcountrystateが必要な場合は以下のように書きます。

PostcardSchema = Dry::Validation.Schema do
  configure do
    config.messages_file = Rails.root.join('config/locales/errors.yml')
    def state_required?(country)
      country.is_state_required
    end
  end
# (...)
end

このとおり簡単です。errors.ymlに正しいエラーメッセージを書いておくのをお忘れなく。エラーファイルについて詳しくはこちらをどうぞ。

次はいよいよ、countryで必要になった場合にのみstateの存在をチェックしましょう。stateが存在するかどうかをバリデーションに伝える必要があります。これは、スキーマに以下の行を書くだけでできます。

required(:state).maybe

ルール自体を定義する

ルール自体は次のように定義します。

rule(country_requires_state: [:country, :state]) do |country, state|
  country.state_required? > state.filled?
end

これも見てのとおり簡単です。

  • ルール内で必要となるフィールドに沿ったルール名を渡します。ここではcountryとstateを使います。
  • これらの変数はブロックにyieldされます。
  • 「stateが必要な場合は、stateの存在をチェックする」というようにルールが変換されます。

より高度なルールについて詳しくはこちらをどうぞ。

完成したForm Object

class Postcard
  module Types
    include Dry::Types.module
    Country = Dry::Types::Definition
                .new(::Country)
    CountryState = Dry::Types::Definition
                     .new(::Country::State)
  end

  class CreateForm < Dry::Types::Struct
    constructor_type(:schema)

    ZIP_CODE_FORMAT = /\d{5}/
    MINIMAL_CONTENT_LENGTH = 20

    attribute :address, Types::Coercible::String
    attribute :city, Types::Coercible::String
    attribute :zip_code, Types::Coercible::String
    attribute :content, Types::Coercible::String
    attribute :country, Types::Country
    attribute :state, Types::CountryState


    def save!
      errors = PostcardSchema.call(to_hash).messages(full: true)
      raise CommandValidationFailed, errors if errors.present?
      Postcard.create!(to_hash)
    end

    private

    PostcardSchema = Dry::Validation.Schema do
      configure do
        config.messages_file = Rails.root.join('config/locales/errors.yml')
        def state_required?(country)
          country.is_state_required
        end
      end
      required(:address).filled
      required(:city).filled
      required(:zip_code).filled(format?: ZIP_CODE_FORMAT)
      required(:content).filled(min_size?: MINIMAL_CONTENT_LENGTH)
      required(:state).maybe
      required(:country).filled

      rule(country_requires_state: [:country, :state]) do |country, state|
        country.state_required? > state.filled?
      end
    end
  end
end

モデルやspecを含む完全なプロジェクトは私のGitHubに公開してあります。

適用できるリファクタリング

記事を読みやすくするため、私はオブジェクト自身に関連するものをすべてひとつのファイルに書きました。このような書き方は、おそらく実際のアプリにおけるコードベースの編成法として最適ではありません。次のリファクタリングが考えられます。

  • Typesモジュールを別のモジュールに配置する(場合によってはグローバルスコープに)
  • PostcardSchemaはForm Objectの外部に配置し、UpdateFormなどでも使う
  • ZIP_CODE_FORMATMINIMAL_CONTENT_LENGTHについても同様

まとめ

dry-typesを使うと、アプリで型安全なコンポーネントを書けるようになります。このライブラリでは多数の型が利用可能で、独自定義も簡単です。

私にとって、dry-validationによるアプローチはActiveModelを使ったものよりも明快に感じられます。バリデーションロジックをすべて明確に区切られた場所に集められます。これらのバリデーションは他のフォーム(UpdateForなど)での再利用も簡単です。

dry-rbシリーズの最大の問題は(ROMRodaにも同種の問題があるのですが)、初めてのユーザーが簡単に使えるようなドキュメントがないことです。信じていただけるかどうかはともかく、私はこのForm Objectの作成に2時間かかりました。原因のほとんどは、ドキュメントの問題と、ブログ記事がないことです。本記事が皆さまの2時間を節約するのに役立てばと願っています。

関連記事

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

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

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

RubyのModule Builderパターン #2 Module Builderパターンとは何か(翻訳)

Vue.jsサンプルコード(23)テキストフィールドの行数に応じて縦幅を自動拡張する

$
0
0

23. テキストフィールドの行数に応じて縦幅を自動拡張する

  • Vue.jsバージョン: 2.5.2
  • テキストフィールドの行数が増えるとフィールドが下に拡張し、行数が減ると上に縮小します。
  • 画面をリロードすると最初の状態に戻ります。

サンプルコード


ポイント: jsファイルはほぼ空です。HTML側のVue.jsコードでは、改行の数を行数と仮定しています。

<textarea class="form-control" v-model="a" :rows="a.split(/\n/).length"></textarea>

バックナンバー(Vue.jsサンプルコード)

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

$
0
0

概要

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

画像はすべて元記事からの引用です。

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

ここ数年、RedisやMongoDBやCassandraやMemcachedやDynamoDBといったNoSQLデータベースを非常によく見かけました。これらは過分な評価を得たにもかかわらず、古きよきリレーショナルデータベースを置き換えるまでに至ることはほとんどありませんでした。オブジェクト-リレーショナル(これについてはまた別の機会にします)データベース管理システムであるPostgreSQLは、今も新たなファンを獲得し続けています。

本記事では、10月5日にリリースされたPostgreSQLの最新バージョンで導入された機能の例をいくつかご紹介します。記事で使われているクエリのほとんどは、PostgreSQLをUbuntu 16の上で動かしたクエリです。冒頭のIDカラムについての部分では、「かつて」PostgreSQL 9.6で行っていた場合との対比も示します。

環境設定

設定手順は比較的簡単です。最初に、ご利用のシステム設定に応じたDocker CEのインストールが必要です。

# PostgreSQL 9.6
$ docker pull postgres:9.6
$ docker run --name old-postgres -d postgres:9.6
$ docker run -it --rm --link old-postgres:postgres postgres:9.6 psql -h postgres -U postgres
# PostgreSQL 10
$ docker pull postgres:10
$ docker run --name new-postgres -d postgres:10
$ docker run -it --rm --link new-postgres:postgres postgres:10 psql -h postgres -U postgres

docker pullしてイメージを起動すると、利用中のPostgreSQLのバージョンが表示されます。

# PostgreSQL 9.6
$ docker pull postgres:9.6
$ docker run --name old-postgres -d postgres:9.6
$ docker run -it --rm --link old-postgres:postgres postgres:9.6 psql -h postgres -U postgres
# PostgreSQL 10
$ docker pull postgres:10
$ docker run --name new-postgres -d postgres:10
$ docker run -it --rm --link new-postgres:postgres postgres:10 psql -h postgres -U postgres

これで準備完了です。

1. IDカラム

MS SQL Serverから移ったときに今ひとつわからなかったのがこのIDカラムです。要するに、行のIDを保存するカラムであり、一意かつ自動でカウントアップされます。

-- PostgreSQL 9.6
CREATE TABLE foo (id SERIAL PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres
-- PostgreSQL 10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres

どちらのバージョンもよく似ており、sequenceも設定されています。そんなに大きな違いがあるのでしょうか?まず、新しい文法はSQLに準拠しているので、他のデータベースでも実行しやすいコードになります。さらに、INSERT時の挙動としてALWAYSDEFAULTのいずれかを明示的に指定できるようになりました。

もうひとつ便利なのは、sequenceの次の値の変更です。私もデータベース移行ではこの作業を嫌というほどやったものです。新しいバージョンでは、PostgreSQLのマジックのお陰で、テーブルで使われているsequenceについて特別な配慮が不要になりました。

-- PostgreSQL 9.6
ALTER SEQUENCE foo_id_seq RESTART WITH 1000;
ALTER SEQUENCE
-- PostgreSQL 10
ALTER TABLE foo ALTER COLUMN id RESTART WITH 1000;
ALTER TABLE

これでも腑に落ちないのであれば、私もこれまで知らなかった素晴らしい機能をもうひとつご紹介します。テーブルfooのコピーをひとつ作成したいとします。

-- PostgreSQL 9.6
CREATE TABLE bar (LIKE foo INCLUDING ALL);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner
--------+------------+----------+----------
 public | bar        | table    | postgres
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres
-- PostgreSQL 10
CREATE TABLE foo (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, val1 INTEGER);
CREATE TABLE
\d
             List of relations
 Schema |    Name    |   Type   |  Owner
--------+------------+----------+----------
 public | foo        | table    | postgres
 public | foo_id_seq | sequence | postgres

この操作を行うとどちらのバージョンでも新しいテーブルが1つ作成されますが、PostgreSQL 10の場合だけ、sequenceも新たに作成されています。つまり、テーブルbarに新しい行をいくつかINSERTしたい場合、バージョン9.6ではidが1000(foo_id_seqを使った場合)になりますが、バージョン10ではidが1から開始されます。これは私にとっては望ましい結果です。さて、fooを削除しようとすると少々問題が起きることがあります。

DROP TABLE foo;
ERROR:  cannot drop table foo because other objects depend on it
DETAIL:  default for table bar column id depends on sequence foo_id_seq
HINT:  Use DROP ... CASCADE to drop the dependent objects too.
DROP TABLE foo;
DROP TABLE

sequenceが他で使われているため、fooは削除できません。かといって表示されるヒントに従おうとすると、barに値を挿入するのに手間がかかります。もし既存のテーブルを大量にコピーして新しいテーブルを作成していたとなれば大変な苦労が待ち構えています。

旧来の手法に潜むその他の怪物たちについて知りたい方は、ぜひこちらの記事をご覧ください。

2. ネイティブのパーティショニング機能

私はこの機能の強力さと応用範囲について非常に誇らしく感じています。この機能はまだ始まって間もないものですが、PostgreSQLにとっては使いやすさとパフォーマンスの面で9.6から大きく飛躍しました。

ところでパーティショニングとは何でしょう。いくつか例を挙げて説明します。

少し前、10Cloudsの同僚が機械学習によるスモッグレベルを予測する記事を書きました。この測定値をすべて保存するテーブルが1つあるとしましょう。最小限のモデルテーブルには、サンプル採取時のタイムスタンプ、サイトID、汚染の種類(CO2、PM10、PM2.5など)が含まれます。しばらくすると、テーブルのデータ総量は著しく増大し、パフォーマンスに影響が生じるでしょう。

もっとも頻繁に行われる計算の種類に応じて、データを分割してみます。月ごとの測定値のような別テーブルが欲しくなったら、パーティショニングの出番です。アプリからデータベースへのクエリが直近の2週間のレコードに限られるのであれば、全レコードをスキャンする代わりに、最大でも2つのパーティションをスキャンする必要があります。もちろん、スキャンするテーブルをクエリで指定することもできますが、正直申し上げるとこれは問題が生じやすいうえに美しくありません。

ここで達成したいのは次の2つです。

  • レベルの異なる抽象化の作成: クエリの対象は1つのテーブル(マスター)だけにしたい
  • サンプルのタイムスタンプに応じて、データを異なる子テーブルに分配したい

PostgreSQLでこれを行う手順は次のとおりです。

  1. マスターテーブルを作成する
  2. datetime制約を持つ子テーブルを必要な個数作成する
  3. 子テーブルにインデックス、キー、その他の制約を作成する
  4. マスターテーブルにトリガを設定し、適切な子テーブルに振り分けてからINSERTする

PostgreSQL 10からは第4項目が不要になりました。ここはDBMSがよしなにやってくれるので、文法が非常にシンプルになりました。サンプルの実装は次のような感じになります。

-- 1. マスターテーブルを作成し、パーティショニングルールを指定
CREATE TABLE measurement(
id INTEGER GENERATED ALWAYS AS IDENTITY,
datetime TIMESTAMPTZ,
site_id INTEGER,
pollutant_id INTEGER,
value FLOAT)
PARTITION BY RANGE (datetime);
-- 2. 子テーブルを複数作成し、保存するデータを制限するデータ範囲を定義
CREATE TABLE measurement_201708
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-08-01') TO ('2017-09-01');
CREATE TABLE measurement_201709
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-09-01') TO ('2017-10-01');
CREATE TABLE measurement_201710
PARTITION OF measurement(datetime)
FOR VALUES FROM ('2017-10-01') TO ('2017-11-01');
-- 3. 小テーブルごとにキーやインデックスを必要分追加
ALTER TABLE measurement_201708 ADD PRIMARY KEY (id);
ALTER TABLE measurement_201708 ADD CONSTRAINT fk_measurement_201708_site FOREIGN KEY (site_id) REFERENCES site(id);
CREATE INDEX idx_measurement_201708_datetime ON measurement_201708(datetime);

発生場所(sites)と汚染物質(pollutants)のテーブルは既に作成済みとします。以下は、測定値(measurement)テーブルにデータを追加してクエリをかけています。

-- INSERTは通常のテーブルと同じようにできる
INSERT INTO measurement(datetime, site_id, pollutant_id, value)
SELECT '2017-08-01'::TIMESTAMPTZ + ((random()*90)::int) * INTERVAL '1 day',
(1 + random()*(SELECT max(id)-1 FROM site))::int,
(1 + random()*(SELECT max(id)-1 FROM pollutant))::int,
random()
FROM generate_series(1,1000000);
-- データのSELECTも通常と同じようにできる
SELECT * FROM measurement WHERE datetime BETWEEN '2017-09-20' AND '2017-09-27';

特定範囲のデータや特定カテゴリに属するデータを簡単にパーティショニングできる構文が新しく提供されました。つまり、別のアプローチとしてどんな地域の測定値についても個別のテーブルを作成できるということです。パーティションの1つが用済みになったら、マスターテーブルからパーティションを簡単に切り離してアーカイブできます。ここでおそらくもっとも重要な点は、ネイティブパーティショニングのパフォーマンスが大きく向上したことです。これについてはdepeszが詳しく調べたブログ記事がありますので、一読をおすすめします。

一方、パーティショニングにはまだ問題がいろいろ残っています。キーやインデックスを子テーブルごとに設定しなければならないとか、異なる子テーブル全体で一意のキーを設定できないなどです。しかしパーティショニングについては懸命な作業が行われているので、今後のリリースに期待したいと思います。

3. マルチカラム統計

この機能は、過小評価された実行プランがたくさんあるデータを扱う多くの方にとって救いとなるかもしれません。架空の例として、1000人の子どもに数え歌を歌わせる(count rhymes)学級を持つビジネスを考えてみましょう。子どもたちは(強制ではなく)皆好きでやっているとします。そして何かトチ狂った理由に基いて、どの子どもがどの単語を口にしたかという情報を保存したいとします。あなたは自分が何を作っているかを知っていて、理由は聞かずにひたすらコードを書くものとします。データの保存と取り出しは次のような感じになるでしょう。

訳注: 数え歌は、日本のわらべ唄の「どちらにしようかな」に相当します。

CREATE TABLE counting_log (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, datetime TIMESTAMP WITH TIME ZONE, child_id INTEGER, word TEXT);
CREATE TABLE
INSERT INTO counting_log(datetime, child_id, word)
SELECT current_timestamp, i%1000,
CASE WHEN i%4=1 THEN 'eeny'
WHEN i%4=2 THEN 'meeny'
WHEN i%4=3 THEN 'miny'
WHEN i%4=0 THEN 'moe'
ELSE 'nope' END
FROM generate_series(1, 1000000) i;
INSERT 0 1000000
CREATE INDEX idx_counting_log_child_id on counting_log(child_id);
CREATE INDEX
CREATE INDEX idx_counting_log_datetime on counting_log(datetime);
CREATE INDEX

準備が整い、「idが123より小さい子どもが「miny」と歌ったら取り出すこと」と業務命令が下ります。PostgreSQLでは普通次のようにします。

  1. child_id=123という述語で総行数(p1)のうちどのぐらいの割合で返されるかを見積もる
  2. word = 'miny'という述語で総行数(p2)のうちどのぐらいの割合で返されるかを見積もる
  3. 両方の述語を使う行の総行数がtotal_rows*p1*p2に等しいと仮定する

この計算方法で正確な行数が返されるのは、おそらくこれらの述語のカラムが相関していない場合に限られます。ここでは相関がないことについてはおおよそ自明なので、見積もりは正確な結果を下回るでしょう。

しかし相関統計を導入すればこの点を改善できることがあります。使い方を見てみましょう。

CREATE STATISTICS st_counting_log_child_id_word ON child_id, word FROM counting_log;
CREATE STATISTICS
ANALYZE counting_log;
ANALYZE

例の表示は逆順になっています。まずは、新しく作った統計の見積もりを見てみましょう。

EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123 AND word='miny';
                                        QUERY PLAN
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.92..2696.34 rows=967 width=8)
   Recheck Cond: (child_id = 123)
   Filter: (word = 'miny'::text)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)

返される行数は967行であると予測されました。古い方法だとどう変わるでしょうか。

DROP STATISTICS st_counting_log_child_id_word;
DROP STATISTICS
EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123;
                                        QUERY PLAN
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.92..2693.92 rows=967 width=8)
   Recheck Cond: (child_id = 123)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)
EXPLAIN SELECT datetime FROM counting_log WHERE word='miny';
                             QUERY PLAN
---------------------------------------------------------------------
 Seq Scan on counting_log  (cost=0.00..19695.00 rows=202133 width=8)
   Filter: (word = 'miny'::text)

ここで少々算数のお時間です。

返される行数 = 総行数 * p1 * p2 = 1000000 * (967/1000000) * (202133/1000000) = 195.46

予測値が出ました。さて、PostgreSQLの回答ではいくつになるでしょうか。

EXPLAIN SELECT datetime FROM counting_log WHERE child_id=123 AND word='miny';
                                        QUERY PLAN
-------------------------------------------------------------------------------------------
 Bitmap Heap Scan on counting_log  (cost=19.73..2696.14 rows=195 width=8)
   Recheck Cond: (child_id = 123)
   Filter: (word = 'miny'::text)
   ->  Bitmap Index Scan on idx_counting_log_child_id  (cost=0.00..19.68 rows=967 width=0)
         Index Cond: (child_id = 123)

ドンピシャリでした。

もうひとつチェックしてみましょう。ここまでは予測値だけを扱っていましたが、実際に返される行数はどうでしょう。

SELECT count(datetime) FROM counting_log WHERE child_id=123 AND word='miny';
 count
-------
  1000

本項のまとめ: この例から、データを熟知していればそこから多くの成果を得られることがわかります。相関を無視してしまうと、予測を大きく下回ってしまいます(およそ1桁違い)。ここではそれほど深刻には見えないかもしれませんが、子どもたちのデータを保存する別のテーブルを結合(JOIN)する場合を考えていましょう。そのようなプランでは、ネステッドループ結合の場合(実際にはハッシュ結合すべき場合)が最もコストが小さいと考えられます。相関するマルチカラム統計に保存されるデータについてもっと知りたい方は、次を入力してみてください。

SELECT * FROM pg_statistic_ext WHERE stxname = 'st_counting_log_child_id_word' \gx
-[ RECORD 1 ]---+------------------------------
stxrelid        | 16555
stxname         | st_counting_log_child_id_word
stxnamespace    | 2200
stxowner        | 16385
stxkeys         | 3 4
stxkind         | {d,f}
stxndistinct    | {"3, 4": 1000}
stxdependencies | {"3 => 4": 1.000000}

本記事ではこれ以上立ち入りませんので、stxkeysがカラム数に対応する(2つより多く指定可能)ことと、stxdistinctstxdependenciesという2種類の統計が行われることだけ押さえておけば十分です。

ところで、今入力したクエリの末尾がセミコロンではなく\gxになっていたことにお気づきでしょうか。これも新機能の1つです😉

4. 並列性の強化

並列クエリの導入が始まったのはPostgreSQL 9.6からです。以来、シーケンシャルなスキャン、ハッシュ、ネステッドループJOIN戦略、集計を並列実行できるようになっていました。今回からは、マージ結合やビットマップヒープスキャン、そしておそらくもっとも重要なインデックススキャンとインデックスのみのスキャンも並列実行できるようになりました。

Postgresql.confファイルに並列クエリを用いるための新しい設定があるのはこのためです。

  • min_parallel_table_scan_size: 並列実行をトリガするテーブルの最小サイズ(デフォルトは8 MB)
  • min_parallel_index_scan_size: 上の対象がインデックスである以外は同様(デフォルトは512 kB)
  • max_parallel_workers: 使用する並列ワーカーの最大数(デフォルトは8)。PostgreSQLの以前のバージョンで導入されていたのはmax_parallel_workers_per_gatherなのでご注意ください。

これらの設定は何に使うのでしょうか。すべてはコスト次第です。並列ワーカーを設定しても、並列ワーカーなしでクエリを実行できるリソースを上回っていればまったく効果がないことがあります。繰り返しになりますが、予測されるコストが1000を上回る場合(これはparallel_setup_cost設定に対応)、デフォルト設定のクエリが複数ワーカーで実行されるよう(クエリ)プランナーで配慮されます。

私がPostgreSQL 10で多少経験した範囲では、新しい並列ワーカーは少々恥ずかしがり屋な生き物であると感じました。実際の動作を見てみましょう。さほど面白い例ではありませんが、次のように三角関数の値を保存してみます。

CREATE TABLE trigonometry AS SELECT i AS arg, sin(i) AS sine, cos(i) AS cosine, tan(i) AS tangent FROM generate_series(0, 100000, 0.01) i;
SELECT 10000001
CREATE INDEX idx_trigonometry_arg on trigonometry(arg);
CREATE INDEX
create index idx_trigonometry_sine on trigonometry(sine);
CREATE INDEX
create index idx_trigonometry_cosine on trigonometry(cosine);
CREATE INDEX

まず、9.6で導入されたいくつかの集計関数を使ってみましょう。

EXPLAIN SELECT count(arg) FROM trigonometry WHERE arg > 50000;
                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=140598.88..140598.89 rows=1 width=8)
   ->  Gather  (cost=140598.67..140598.88 rows=2 width=8)
         Workers Planned: 2
         ->  Partial Aggregate  (cost=139598.67..139598.68 rows=1 width=8)
               ->  Parallel Seq Scan on trigonometry  (cost=0.00..134437.55 rows=2064449 width=8)
                     Filter: (arg > '50000'::numeric)

簡単ですね。予測コストがかなり大きくなったので、プランナーはワーカーを2つ追加することにしました。

次は並列インデックススキャンを試すことにしましょう。

EXPLAIN SELECT * FROM trigonometry WHERE arg > 50000;
                                             QUERY PLAN
-----------------------------------------------------------------------------------------------------
 Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..201369.28 rows=4954677 width=32)
   Index Cond: (arg > '50000'::numeric)

だめですね。別のワーカーを追加するコストを少し下げてあげるとどうなるでしょうか。

SET parallel_setup_cost=100;
SET
EXPLAIN SELECT * FROM trigonometry WHERE arg > 50000;
                                             QUERY PLAN
-----------------------------------------------------------------------------------------------------
 Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..201367.27 rows=4954562 width=32)
   Index Cond: (arg > '50000'::numeric)

まだ変わりません。これは私にとって少々驚きでした。予測コストは高く、テーブルやインデックスのサイズは閾値を十分上回っているのに…今度はクエリを変えて試してみましょう。

SET parallel_setup_cost=1000;
SET
EXPLAIN SELECT arg FROM trigonometry WHERE sine > 0.999 AND arg >100 AND arg < 10000;
                                                  QUERY PLAN
---------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.43..40801.85 rows=13403 width=8)
   Workers Planned: 2
   ->  Parallel Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..38461.55 rows=5585 width=8)
         Index Cond: ((arg > '100'::numeric) AND (arg < '10000'::numeric))
         Filter: (sine > '0.999'::double precision)

ついにやりました。Parallel Index Scanが輝かしい姿を現したのです。私がこのクエリを数回実行してみたところ、平均で162msでした。もう少し遊んでみましょう。並列実行を強制し、かつ追加ワーカーを0にしてみました。

SET max_parallel_workers =0;
SET
SET force_parallel_mode=on;
SET
EXPLAIN ANALYZE SELECT arg FROM trigonometry WHERE sine > 0.999 AND arg >100 AND arg < 10000;
                                                                          QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.43..40801.85 rows=13403 width=8) (actual time=0.265..263.796 rows=14097 loops=1)
   Workers Planned: 2
   Workers Launched: 0
   ->  Parallel Index Scan using idx_trigonometry_arg on trigonometry  (cost=0.43..38461.55 rows=5585 width=8) (actual time=0.072..262.133 rows=14097 loops=1)
         Index Cond: ((arg > '100'::numeric) AND (arg < '10000'::numeric))
         Filter: (sine > '0.999'::double precision)
         Rows Removed by Filter: 975902
 Planning time: 0.164 ms
 Execution time: 264.473 ms

まず、このクエリがちゃんと実行されていることがわかります。起動されたワーカー数が0に等しいようなので、追加のワーカーは存在しないということになりますが、その点は私にははっきりとはわからなかったことをお断りしておきます。

次に、利用可能なプロセス数を制限した場合、プランナーは実際に利用できる数より多くのプロセス数を求めるかもしれません。最後に、上のクエリの結果は平均して260ms後に返されたので、並列インデックススキャンを使った場合の実行結果は速くなっています。総合すると、これは素晴らしい結果です。多くの人はこの機能の存在に気づくことはないかもしれませんが。

5. JSONとJSONBの全文検索

いよいよ最後ですが、これも重要です。JSON型やJSONB型のカラムで全文検索がサポートされるようになりました。textカラムで全文検索が使える仕組みに親しんでいる方には大したことないように思えるかもしれませんが、PostgreSQL 10ですぐ使える素敵な機能であることに違いはありません。

支払い管理に外部のシステムを使っているとしましょう。ほとんどのデータは通常のカラムに保存しますが、安全のためにシステムからのレスポンスをJSONカラムにも保存しています。これは次のような感じになります。

CREATE TABLE transaction(id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, transaction_id VARCHAR(10), user_id INTEGER, created_datetime TIMESTAMP WITH TIME ZONE, result BOOL, amount INT ,response_data JSON);
CREATE
INSERT INTO transaction(transaction_id, user_id, created_datetime, result, amount, response_data)
SELECT tran.id, ceil(random()*100), tran.datetime, tran.result, tran.amount,
('{"transaction": {"id": "'|| tran.id ||'", "transaction_datetime": "'|| tran.datetime || '","amount": '|| tran.amount::text || ',"is_success": "'|| tran.result || '", "message": "'|| tran.msg || '"}}')::json
FROM (
    SELECT substring(md5(random()::text), 1, 10) as id,
        current_timestamp + (ceil(random()*1000)-500) * INTERVAL '1 minute' as datetime,
        ceil(random()*1000) as amount,
        NOT (i%3=1) as result,
        CASE WHEN i%9=1 THEN 'insufficient funds' WHEN i%9=4 THEN 'blocked account' WHEN i%9=7 THEN 'fraud detected' ELSE 'accepted' END as msg
    FROM generate_series(1,1000) i) tran;
INSERT 0 1000

これは簡単な例ですが、テーブルが著しく増大する場合は、次のようにJSONカラムにインデックスを作るとよいかもしれません。

CREATE INDEX idx_transaction_response_data ON transaction USING GIN (to_tsvector('english', response_data));
CREATE INDEX

テーブルにデータが入りましたので、たとえばレスポンスメッセージが「insufficient funds」の場合のクエリ方法を見てみましょう。新しいインデックスは効くでしょうか。

SELECT transaction_id FROM transaction WHERE to_tsvector('english', response_data) @@ to_tsquery('english', 'insufficient') LIMIT 5;
 transaction_id
----------------
 b114b0927f
 28613b40c6
 649b9d3285
 ce9c16ec6f
 24f69de12e
EXPLAIN SELECT transaction_id FROM transaction WHERE to_tsvector('english', response_data) @@ to_tsquery('english', 'insufficient') LIMIT 5;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Limit  (cost=8.04..23.12 rows=5 width=11)
   ->  Bitmap Heap Scan on transaction  (cost=8.04..23.12 rows=5 width=11)
         Recheck Cond: (to_tsvector('english'::regconfig, response_data) @@ '''insuffici'''::tsquery)
         ->  Bitmap Index Scan on idx_transaction_response_data  (cost=0.00..8.04 rows=5 width=0)
               Index Cond: (to_tsvector('english'::regconfig, response_data) @@ '''insuffici'''::tsquery)

素晴らしい、実にクールです。なお、効率のよいクエリにするためには「insufficient」の語幹だけを取り出した「insuffici」(語彙素: lexeme)に変換する必要があります。

これで全部?

いいえ、全貌に迫ったとは言えません。冒頭でお断りしたとおり、PostgreSQL 10では膨大な新機能が導入されたため、宣伝が大きく先走っています。ご紹介した5つの嬉しい機能はほんの一部に過ぎませんが、あえてこれらを選んだのは、(意識するかどうかにかかわらず)最も頻繁に使われると信じているからです。

紹介しなかった機能には次のものがあります。

  • 論理レプリケーション
  • 同期レプリケーション用のクォーラム(quorum)コミット
  • XMLテーブル
  • SCRAM認証
  • 一部の関数名の変更
  • psqlで利用できる\if\elif\else

他にもまだまだあります。

すべての新機能についてはPostgreSQL Wikiをご覧ください。PostgreSQL 10の次のバージョンが待ち遠しい方は、depeszのブログをご覧ください。こちらでは早くも次期PostgreSQL 11の機能の紹介が始まっています。

関連記事(PostgreSQL)

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

Rails開発者のためのPostgreSQLの便利技(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

週刊Railsウォッチ(2017/11/24)GitHubにセキュリティアラート追加、RailsでVue.jsを使う、Railsテスト本2種、node-pruneで瞬間クリーンアップほか

$
0
0

こんにちは、hachi8833です。

Rails: 今週の改修

今週もcommit差分から見繕いました。

klass.all高速化のため不要なspawnを抑制

# activerecord/lib/active_record/scoping/default.rb
              # The user has defined their own default scope method, so call that
               evaluate_default_scope do
                 if scope = default_scope
-                  (base_rel ||= relation).merge(scope)
+                  (base_rel ||= relation).merge!(scope)
                 end
               end
             elsif default_scopes.any?
               base_rel ||= relation
               evaluate_default_scope do
                 default_scopes.inject(base_rel) do |default_scope, scope|
                   scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
-                  default_scope.merge(base_rel.instance_exec(&scope))
+                  default_scope.merge!(base_rel.instance_exec(&scope))
                 end
               end
             end

つっつきボイス:mergemerge!に変わったのか」

ActiveRecord::SpawnMethodsを見るとmergespawnがありました。

# actionpack/lib/action_controller/metal/strong_parameters.rb#L718
    def merge(other)
      if other.is_a?(Array)
        records & other
      elsif other
        spawn.merge!(other)
      else
        raise ArgumentError, "invalid argument: #{other.inspect}."
      end
    end

    def merge!(other) # :nodoc:
      if other.is_a?(Hash)
        Relation::HashMerger.new(self, other).merge
      elsif other.is_a?(Relation)
        Relation::Merger.new(self, other).merge
      elsif other.respond_to?(:to_proc)
        instance_exec(&other)
      else
        raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
      end
    end

after_bundleコールバックを非推奨化

fbd1e98からRailsのプラグインで生成時にbundle installが実行されなくなったため、
after_bundleコールバックはbundle後に実行されなくなった。
このコールバック名と実際の動作が合わなくなったので削除すべきと考える。
#60c550より

# railties/lib/rails/generators/rails/plugin/plugin_generator.rb
       def run_after_bundle_callbacks
+        unless @after_bundle_callbacks.empty?
+          ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ")
+        end
+
         @after_bundle_callbacks.each do |callback|
           callback.call
         end

つっつきボイス: 「Rails 5のプラグインって何だろうと思ったら、Railsガイドに書いてあった」

参考: Gem、Railtieプラグイン、Engine(full/mountable)の違いとそれぞれの基礎情報

ActiveStorageルーティングの一部でオプションが無視されていたのを修正

# activestorage/config/routes.rb
Rails.application.routes.draw do
   get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob, internal: true

-  direct :rails_blob do |blob|
-    route_for(:rails_service_blob, blob.signed_id, blob.filename)
+  direct :rails_blob do |blob, options|
+    route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
   end

-  resolve("ActiveStorage::Blob")       { |blob| route_for(:rails_blob, blob) }
-  resolve("ActiveStorage::Attachment") { |attachment| route_for(:rails_blob, attachment.blob) }
+  resolve("ActiveStorage::Blob")       { |blob, options| route_for(:rails_blob, blob) }
+  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
...

つっつきボイス: 「おー、随分抜けてたなー」「テストが見当たらないけど後から足すのかな?」

Rails

書籍『Everyday Rails Testing with RSpec』2017年版の第2章が更新

leanpub.com/everydayrailsrspecより

見逃していましたが、同書は6月にSpec 3.6とRails 5.1向けにメジャーアップデートしていて、それがさらに更新されたということです。英語版を購入した方は無料で更新版をダウンロードできます。

書籍『Rails 5 Test Prescriptions』(ベータ版)


pragprog.comより

Rails 5.1+Webpackに対応しているそうです。おおよその目次は以下です。

  • TDDの寓話
  • TDDの基本
  • RailsのTDD
  • テストを良くする要素
  • モデルのテスト(書籍サンプルPDF
  • テストで日時を扱う
  • ダブル(double)をモックやスタブとして使う
  • CapybaraとCucumberを使った結合テスト
  • JavaScriptの結合テスト
  • JavaScriptの単体テスト
  • Railsの表示要素のテスト
  • MiniTest
  • セキュリティのテスト
  • トラブルシューティング/デバッグ
  • テストの高速化
  • レガシーコードのテスト

つっつきボイス: 「prescription: 処方箋ですね」「このあたりの本、輪読会に向いてそう」

RSpecからController specを消し去る(RubyFlowより)


everydayrails.comより


つっつきボイス: 「コントローラを薄くしてコントローラのテストも薄くするという考え、とても同意できる: コントローラに業務ロジックとか書かなければ、コントローラのテストはアクセスチェックで十分なはず」「昔コントローラのテストをどこまで書くべきか悩んでました」

[Rails] RSpecをやる前に知っておきたかったこと

RailsアプリをAWS Elastic Beanstalkにデプロイする


syndicode.coより


つっつきボイス: 「Elastic Beanstalkってどんなサービスだったかしら」「論理VMレベルのDockerに近いかも: Vagrantに近いと言えばイメージ近いかな」「この記事ではRailsのSECRET_KEY_BASEも設定してますね」「最近の作って捨てるポリシのインフラ界隈では、configはハードコードせずに環境変数としてinjectionせよ、というのがベストプラクティスになりつつあるので、実はRailsのsecret.key.enc方式はそうした流れに逆行しているかな」

参考: よくある質問: AWS Elastic Beanstalk

FastRuby.io: 古いRailsのアップグレード相談サイト

以前のウォッチでもご紹介したhttps://www.upgraderails.com/とちょっと似た感じです。upgraderails.comはアップグレード請負が全面に出ていますが、fastruby.ioは「まずはご相談」という雰囲気で、無料ガイド(PDF)も配布しています。

www.upgraderails.comより

JRubyだとTime.iso8601Time.parseの14倍速かった

早すぎる最適化もたまにはいいことがあるという主旨です。

# 同記事より
# JRuby 9.0.4.0
Warming up --------------------------------------
           No format     1.111k i/100ms
          ISO format    18.031k i/100ms
Calculating -------------------------------------
           No format     16.364k (± 3.3%) i/s -     82.214k
          ISO format    237.077k (± 4.2%) i/s -      1.190M

Comparison:
          ISO format:   237076.7 i/s
           No format:    16364.4 i/s - 14.49x slower

つっつきボイス: 「CRubyだとTime.iso8601に変えても3%しか速くならなかったそうです」「へー、JavaのDateライブラリはとても使いにくくて何度も変わってたのに」「Rubyのパースは柔軟だけどその分遅いのかな」

参考: wikipedia-ja ISO8601

RailsでVue.jsを使う


classandobjects.comより

RailsにVue.jsを導入する手順の解説です。

# 同記事より
app/javascript/
├── components
│   ├── App.vue
│   └── shared
│       └── csrf.vue
├── packs
│   ├── devise
│   │   └── registrations
│   │       └── new.js
└── views
    ├── devise
        └── registrations
            └── new.vue

つっつきボイス:rails new myapp --webpack=vueでVue.jsインストールできるのか↓」「5.1からこれ使ってました」「=jqueryはないというかなしさ」

# Intalling vue
# Rails 5.1+ new application
rails new myapp --webpack=vue

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

DHHインタビュー(Ruby Weeklyより)


lifehacker.comより

子供の頃プログラミングを学ぼうとして何度も挫折し、もっぱらゲームに勤しんでたそうです。


つっつきボイス: 「いつもの写真と違いますね」「これも最近の写真じゃないだろうなきっと」「今もTextMateが好きらしいです」「TextMateはPHP時代にかなり長い間使ってたけど、日本語サポートが残念すぎて半角日本語フォントを自作ビルドして入れるとかしないとまともに表示できなかったり、TextMate3がいまいちだったりした辺りで乗り換えたな」

Service Object支援gem 2本立て

active_interaction: ビジネスロジックをCommandパターンで書くgem

# AaronLasseigne/active_interactionより
class BooleanInteraction < ActiveInteraction::Base
  boolean :kool_aid

  def execute
    'Oh yeah!' if kool_aid
  end
end

BooleanInteraction.run!(kool_aid: 1)
# ActiveInteraction::InvalidInteractionError: Kool aid is not a valid boolean
BooleanInteraction.run!(kool_aid: true)
# => "Oh yeah!"

waterfall: チェインを意識した関数型的Service Object gem(Ruby Weeklyより)

apneadiving/waterfallより

# apneadiving/waterfallより
class FetchUser
  include Waterfall

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    chain { @response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}") }
    when_falsy { @response.success? }
      .dam { "Error status #{@response.code}" }
    chain(:user) { @response.body }
  end
end

Flow.new
    .chain(user1: :user) { FetchUser.new(1) }
    .chain(user2: :user) { FetchUser.new(2) }
    .chain  {|outflow| puts(outflow.user1, outflow.user2)  } # report success
    .on_dam {|error|   puts(error)      }                    # report error


apneadiving/waterfallより


つっつきボイス: 「この間のRails開発のコツ記事にもありましたが、みんなService Objectをどうにかしたいんだなと感じました」「このWaterfallというgemの名前は機能に即してて好き: 図もわかりやすいし↑」「ワークフローっぽいですね」「JSのPromiseをちょっと連想しました」
「ところでService ObjectのServiceという言葉、意味が広すぎてあまり好きじゃないです: Domain Objectと呼んで欲しかった」「Domainも相当意味が広い気がしますね」

参考: 混乱しがちなサービスという概念について

Ruby trunkより

リクエスト: Kernel#ppをデフォルトで有効にして欲しい

賛成が集まりつつあります。


つっつきボイス:#ppって使ったことないけど何の略?」「Kernel#pp(pretty print)は普通によく使いますね: printfデバッグ的なことをするときとか」「確かにデフォルトでrequireされるようになったらありがたい」

Ruby

RubyのChain of ResponsibilityパターンとProxyパターン(RubyFlowより)

rubyblog.proより

Rubyデザインパターンの記事2本です。ProxyパターンではVirtual proxy/Protection proxy/Remote proxy/Smart referenceの4つの応用例が示されています。

Rubyでワーカープールを実装する(Ruby Weeklyより)

# 同記事より
worker_1 got #<Proc:0x007fc35a132d18@worker_pool_2.rb:40 (lambda)>
worker_0 got #<Proc:0x007fc35a130a40@worker_pool_2.rb:89 (lambda)>
worker_3 got #<Proc:0x007fc35a1309a0@worker_pool_2.rb:89 (lambda)>
worker_5 got #<Proc:0x007fc35a130950@worker_pool_2.rb:89 (lambda)>
worker_7 got #<Proc:0x007fc35a1308b0@worker_pool_2.rb:89 (lambda)>
worker_9 got #<Proc:0x007fc35a130810@worker_pool_2.rb:89 (lambda)>
worker_5 got #<Proc:0x007fc35a1305b8@worker_pool_2.rb:89 (lambda)>
# reduced output lines...
worker_4 got #<Proc:0x007fc35a130428@worker_pool_2.rb:89 (lambda)>
worker_6 got #<Proc:0x007fc35a130900@worker_pool_2.rb:89 (lambda)>
worker_2 got #<Proc:0x007fc35a130478@worker_pool_2.rb:89 (lambda)>
worker_1 got #<Proc:0x007fc35a1307c0@worker_pool_2.rb:89 (lambda)>
worker_8 got #<Proc:0x007fc35a130018@worker_pool_2.rb:89 (lambda)>
worker_4 got #<Proc:0x007fc35a1304f0@worker_pool_2.rb:89 (lambda)>
worker_0 got #<Proc:0x007fc35a1306f8@worker_pool_2.rb:89 (lambda)>

つっつきボイス: 「ワーカーというとUnicornとかPumaとか」「そういうソースを追うときに役立ちそうですね」

google_translate_diff: Google翻訳APIで巨大な文の差分だけ翻訳するgem(RubyFlowRuby Weeklyより)

# 同記事より
s = "There are 6 pcs <b>Neumann Gefell</b> tube mics MV 101 with MK 102 capsule. It is working with much difference capsules from neumann / gefell.\nAdditionally…"

GoogleTranslateDiff.translate(s, from: :en, to: :es)

=> # Tokenize

["There are 6 pcs ", :text],
 ["<b>", :markup],
 ["Neumann Gefell", :text],
 ["</b>", :markup],
 [" tube mics MV 101 with MK 102 capsule.", :text],
 ["It is working ... / gefell.\n", :text],     # NOTE: Separate sentence
 ["Additionally…", :text]]                     # NOTE: Also, separate sentence

=> # Load from cache and translate missing pieces

["Ci sono 6 pezzi ", :text],                   # <== cache
 ["<b>", :markup],
 ["Neumann Gefell", :text],                    # <== Google ==> cache
 ["</b>", :markup],
 [" Tubi MV 101 con ... ", :text],             # <== Google ==> cache
 ["Sta lavorando cn ... / gefell.\n", :text],  # <== cache
 ["Inoltre…", :text]]                          # <== cache

=> # Join back

"Ci sono 6 pezzi <b>Neumann Gefell</b> Tubi MV 101 con capsula MK 102. Sta lavorando con molte capsule di differenza da neumann / gefell.\nInoltre"

やはりというか、差分翻訳のコンテキストは失われてしまうそうです。


つっつきボイス: 「こういうオレオレ翻訳支援ツールって車輪の再発明がものすごく多い世界: ローカライズ業界では訳文の再利用に翻訳メモリというものをよく使ってるんですが、サイズが大きくなって低品質の翻訳が混じるとどんどん残念になってしまう」

Google Translator Toolkitと翻訳メモリ(ノーカット版) : RubyWorld Conference 2013より

sniffer: 外向きHTTPリクエストのアナライザgem


aderyabin/snifferより

1月足らずで★250超えです。これも含め、最近evilmartians.comがスポンサーになっているgemをときどき見かけます。

# aderyabin/snifferより
require 'http'
require 'sniffer'

Sniffer.enable!

HTTP.get('http://example.com/?lang=ruby&author=matz')
Sniffer.data[0].to_h
# => {:request=>
#   {:host=>"example.com",
#    :query=>"/?lang=ruby&author=matz",
#    :port=>80,
#    :headers=>{"Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Connection"=>"close"},
#    :body=>"",
#    :method=>:get},
#  :response=>
#   {:status=>200,
#    :headers=>
#     {"Content-Encoding"=>"gzip",
#      "Cache-Control"=>"max-age=604800",
#      "Content-Type"=>"text/html",
#      "Date"=>"Thu, 26 Oct 2017 13:47:00 GMT",
#      "Etag"=>"\"359670651+gzip\"",
#      "Expires"=>"Thu, 02 Nov 2017 13:47:00 GMT",
#      "Last-Modified"=>"Fri, 09 Aug 2013 23:54:35 GMT",
#      "Server"=>"ECS (lga/1372)",
#      "Vary"=>"Accept-Encoding",
#      "X-Cache"=>"HIT",
#      "Content-Length"=>"606",
#      "Connection"=>"close"},
#    :body=> "OK",
#    :timing=>0.23753299983218312}}

つっつきボイス: 「おーハッシュで取れる: アプリの動作確認とかでときどき欲しくなるヤツかも」「pryの中で動かせるのがいいですね」

bundlerでcombinationが原因のバグ


depfu.comより

# 同記事より
[1,2,3,4].combination(2).to_a
 => [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

([1] * 26).combination(13).size
 => 10400600

つっつきボイス:#combinationって数学の順列組み合わせの組み合わせでしたっけ」「ですです」「そういえば#productはRSpecでよく使ってます」「組み合わせが爆発してbundlerがめちゃくちゃメモリ食ったのか」「記事の最後で投げているissue #6114がわかりやすそう」「組み合わせ爆発怖い…」

SQL

PostgreSQLのAdvisory lockとその使い方


shiroyasha.ioより

-- 同記事より
SELECT pg_advisory_unlock(23);
SELECT pg_advisory_unlock(112, 345);

SELECT mode, classid, objid FROM pg_locks WHERE locktype = 'advisory';

 mode | classid | objid
------+---------+-------
(0 rows)

つっつきボイス: 「Advisory lockの訳語って勧告的ロックなのね」「自分でunlockしないといけないあたり、何だかRubyのFiberを思い出しました」「強力すぎて怖いなー」「自己責任迫られるヤツ」

Advisory: 顧問

JavaScript

JavaScriptのコスト(JavaScript Weeklyより)


medium.com/dev-channelより

JavaScriptのどこで時間やリソースを食っているかという調査です。


medium.com/dev-channelより

忙しいJS開発者のためのES6いいとこ取り(JavaScript Weeklyより)


thenewstack.ioより

// 同記事より
var id = `Your name is ${firstName} ${lastName}.`
var url = `http://localhost:8080/api/messages/${id}`
let chicken = {
     name: 'Pidgey',
     jobs:['scratch for worms', 'lay eggs', 'roost'],
     showJobs() {
        this.jobs.forEach((job) => {
        console.log(`${this.name} wants to ${job}`);
       });
    }
};
chicken.showJobs();
//Pidgey wants to scratch for worms
//Pidgey wants to lay eggs
//Pidgey wants to roost

つっつきボイス:
「ES6、バッククォートで式展開できるようになってるじゃないの!」「webpackとbabel使うなら普通に使ってよい機能なんで、最近使い始めた」
「式展開を最初に知ったのはRubyだったんですが、他の言語は?」「PHPで使ったことあった」「Perlにもあったかな」「式展開使ってる言語いろいろありますよ: メジャーなLL系言語ならほぼ持ってるんじゃないかな」

Rubyでの文字列出力に「#+」ではなく式展開「#{}」を使うべき理由

「fat arrow => はクロージャなのか」「そういえばCoffeeScriptのアローって->=>の2種類あるんですが、たまに使い分け間違えたりしてその後嫌いになったりすることあった」

参考: CoffeeScript -> と => の違い

AngularとReactとVueのどれがいいの?


objectpartners.comより

// 同記事より: Reactの例
export class Counter extends React.Component {
  render() {
    return <div>
      <h2>Current value is { this.state.value }</h2>
      <button>Increment</button>
    </div>
  }

  increment() {
    this.setState({
      value: this.state.value + 1
    });
  }
}

つっつきボイス: 「みんな悩みますよね」「Reactはrendererがあるのかー: 趣味に合わない」「大きなプロジェクトだとAngularなのかな: TypeScriptだし」

「依存性の注入(DI)」なんか知らなくてもいい(JavaScript Liveより)

面接で「DIとは何かと」いう質問があったのがきっかけで書いた記事だそうです。

// 同記事より
class Knight extends React.Component {
  static propTypes = {
    weapon: PropTypes.any.isRequired
  };
  render() {
    return `🐴 ${this.props.weapon}`;
  }
}

つっつきボイス: 「DIといえばもうJavaでしょう: Rubyだと特に必要を感じないなー」

参考: 猿でも分かる! Dependency Injection: 依存性の注入

⭐node-prune: nodeの不要なファイルを一瞬で除去(GitHub Trendingより)⭐

公開後5日しか経過していないのに★2200超えです。Go言語で書かれています。


github.com/tj/node-pruneより


つっつきボイス: 「これみんな欲しかったヤツでしょうね」「npm使ってるとファイルじゃんじゃん増やされるし」「↑図がすべてを表してるw」「node_moduleはブラックホールより重い、と」

試しに動かしてみると、本当に一瞬で完了しました。

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

CSS/HTML/フロントエンド

CSS Writing Modes Level 3がRecommendation間近?

11/23にCRが更新されていました。


w3.orgより


grid項目のアスペクト比


css-tricks.comより

gridのアスペクト比でお悩みの方向けです。

See the Pen Aspect Ratio Boxes Filling by Chris Coyier (@chriscoyier) on CodePen.

その他

GitHubにセキュリティアラート機能が追加

GitHubのPublicなリポジトリでInsights > Code Dependencyを表示するとセキュリティアラートが表示されるようになりました。現時点ではJavaScriptとRubyが対象です。


つっつきボイス: 「これいいなー」「何年も前にGitHubに放置していた自分のリポジトリで見てみたらどっと出てた…」「gem使う前にInsights > Code Dependencyチェック、が合言葉」

開発者にとって重要な5つの問題解決スキル

dev.to記事です。


dev.toより

  1. 大きくて複雑な目標をシンプルな目標に分割できる
  2. 並列を考えられる
  3. 抽象化できる(やりすぎないこと)
  4. 既存のソリューションを再利用できる
  5. データフローに即して考えられる

つっつきボイス: 「いい感じかつ実用的かも」

tmuxinator: tmuxセッションを簡単に作れる(GitHub Trendingより)


github.com/tmuxinator/tmuxinatorより

★7200超えです。


つっつきボイス: 「BPS社内は確かtmux派とbyobu派がいましたね」「(GNU) screen派もいたはず」「自分は使ってないなー」

qt: Go言語とQtバインディングでマルチプラットフォームアプリ


github.com/therecipe/qt/wiki/Galleryより

★3200超えです。WidgetとQMLのどちらでも動きます。
これが本当なら、同一ソースからWin/Mac/iOS/Androidなど向けアプリを一気にビルドできますね。Qtの商用ライセンス料と、Store登録の面倒臭さが何とかなるといいのですが。


つっつきボイス: 「Qtってキューティーじゃなくてキュートって発音するんじゃなかったでしたっけ?」「あー、そうでした(今初めて発音した…)」

ちょっと手元で動かしてみようと思ったのですがまだサンプルをセットアップできていません。QtのSDK削除するんじゃなかった…

primitive: 画像を幾何学図形の集まりで再現する(GitHub Trendingより)


github.com/fogleman/primitiveより

★8000近くあります。TechRachoの画像加工でも早速使っています。

shapecatcher.com: 手書きした文字に似ているUnicode文字をリストアップ

絵文字を絵で検索することもできます。

番外

仕事でしかコード書かない開発者ってどうよ?

記事というよりツイート並の短さですね。怒涛のようにレスが付いています。

どうしてこうなったんだっけ

プラセボは腰痛にも効く


つっつきボイス: 「そういえばデュアルディスプレイやめたら肩こり治ったんですよ」「マジで?!」「どうも首を左右に動かしていたのが肩によくなかったのかも」

おめでとうございます!


今週は以上です。

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

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured


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

$
0
0

概要

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

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

前回の記事では、Rubyのオブジェクトがどのようにメモリ上に展開されるかについて軽く触れました。そのときの情報を元に、今回はRubyヒープのダンプを取ってそのヒープの配置や断片化をビジュアル表示するプログラムを書くことにします。

Rubyオブジェクトのレイアウトをざっと復習

単なる復習: Rubyオブジェクトは固定幅です。つまり、あらゆるRubyオブジェクトのサイズは同一(40バイト)になります。オブジェクトは実際にはmallocで割り当てられるのではなく、ページの内部に配置されます。

1つのRubyプロセスには多数のページが含まれ、1つのページには多数のオブジェクトが含まれます。

このオブジェクトはどのページに属するのか?

多くのオブジェクトが1つのページに割り当てられます。各ページは2^14バイト(訳注: 16,384バイト)です。複数のRubyオブジェクトは同時に割り当てられるのではなく、GCが1つのページ(アリーナとも呼ばれます)を割り当てます。

ページのサイズは正確な2^14バイトではありません。あるページを割り当てるとき、OSのメモリページに沿ってページを配置したいので、mallocのトータルサイズは4 KB(OSのページサイズ)の倍数よりやや小さい値にする必要があります。mallocシステムコールには若干オーバーヘッドがあるため、連続するOSページにRubyのページを隙間なく収納できるよう、実際にmallocするサイズを総量から差し引かなければなりません。paddingに使うサイズはsizeof(size_t) * 5なので、1ページの実際のサイズは(2 ^ 14) - (sizeof(size_t) * 5)になります。

各ページには、ページ情報の一部を含むヘッダが1つずつあります。ヘッダのサイズはsizeof(void *)です。

つまり、1つのページに保存できるRubyオブジェクトの最大サイズは((2 ^ 14) - (sizeof(size_t) * 5) - sizeof(void *)) / 40になります。

1ページあたりのオブジェクト数には上限があるため、1つのRubyオブジェクトのアドレスの下位14ビットにビットマスクを適用し(ページサイズは2^14バイトなので、言い換えると14ビットシフトして1ビット残ります)、オブジェクトが実際に配置されるページを算出します。そのビットマスクは~0 << 14です。

あるRubyオブジェクトのアドレスが0x7fcc6c845108の場合、バイナリをASCIIアートで表すと以下のようになります。

11111111100110001101100100001000101000100001000
^---------- ページ アドレス --------^- object id ^

上図の「object id」の部分は、昔ながらのRuby object idではなく、単にそのページ上の個別のオブジェクトを表すビットの一部です。アドレス全体は昔ながらの「object id」と考えられます。

これらの数値をRubyのコードに切り出してみましょう。

require 'fiddle'

SIZEOF_HEAP_PAGE_HEADER_STRUCT = Fiddle::SIZEOF_VOIDP

SIZEOF_RVALUE           = 40
HEAP_PAGE_ALIGN_LOG     = 14
HEAP_PAGE_ALIGN         = 1 << HEAP_PAGE_ALIGN_LOG      # 2 ^ 14
HEAP_PAGE_ALIGN_MASK    = ~(~0 << HEAP_PAGE_ALIGN_LOG)  # ページアドレス取得用マスク
REQUIRED_SIZE_BY_MALLOC = Fiddle::SIZEOF_SIZE_T * 5     # mallocで必要なpadding
HEAP_PAGE_SIZE          = HEAP_PAGE_ALIGN - REQUIRED_SIZE_BY_MALLOC # 実ページサイズ
HEAP_PAGE_OBJ_LIMIT     = (HEAP_PAGE_SIZE - SIZEOF_HEAP_PAGE_HEADER_STRUCT) / SIZEOF_RVALUE

先ほど触れた部分を改めて説明します。Rubyページは、mallocで隙間なく配置されます。言い換えると、あるRubyページが割り当てられるときのアドレスは2^14で割ることができ、ページのサイズは2^14よりごくわずか小さくなります。

それでは、あるオブジェクトアドレスを渡すと、そのオブジェクトが配置されたページのアドレスを返す関数を書いてみましょう。

def page_address_from_object_address object_address
  object_address & ~HEAP_PAGE_ALIGN_MASK
end

それでは3つのオブジェクトアドレスのページアドレスを出力してみます。

p page_address_from_object_address(0x7fcc6c8367e8) # => 140515970596864
p page_address_from_object_address(0x7fcc6c836838) # => 140515970596864
p page_address_from_object_address(0x7fcc6c847b88) # => 140515970662400

この出力から、最初の2つのオブジェクトは同じページにあるが、3番目のオブジェクトは別のページにあることがわかります。

このページにオブジェクトはいくつあるか?

Rubyオブジェクトもアライン(align)されますが、既存のページの内部でアラインされます。アラインされるのは40バイト目(これはそのオブジェクトのサイズでもあります)。つまり、あらゆるRubyオブジェクトが持つ各アドレスはすべて40で割れることが保証されます(これは、数値のようにヒープに割り当てられないオブジェクトについては真ではありません)。

Rubyオブジェクトは決して(訳注: OSによって)割り当てられず、割り当て済みの1つのページ内部に置かれます。そのページは2^14に沿ってアラインされますが、2^14で割れるすべての数が40でも割れるとは限りません。つまり、あるページには他のページよりも多くのオブジェクトが保存される場合があるということです。40でも割れるページには、そうでないオブジェクトより1つ多くオブジェクトが保存されます。

ページアドレスを渡すと、そこに保存できるオブジェクトの数とオブジェクトの場所を算出し、ページの情報を表すオブジェクトを1つ返す関数を書いてみましょう。

Page = Struct.new :address, :obj_start_address, :obj_count

def page_info page_address
  limit = HEAP_PAGE_OBJ_LIMIT # ページあたりの最大オブジェクト数

  # ページには情報を持つヘッダーが1つあるので、その分も考慮する
  obj_start_address = page_address + SIZEOF_HEAP_PAGE_HEADER_STRUCT

  # オブジェクトの開始アドレスがRubyオブジェクトのサイズで割り切れない場合、
  # SIZEOF_RVALUEで割り切れる最初のアドレスを見つけるのに必要な
  # paddingの算出が必要
  if obj_start_address % SIZEOF_RVALUE != 0
    delta = SIZEOF_RVALUE - (obj_start_address % SIZEOF_RVALUE)
    obj_start_address += delta # Move forward to first address

    # このページに実際に保存されているオブジェクト数を算出
    limit = (HEAP_PAGE_SIZE - (obj_start_address - page_address)) / SIZEOF_RVALUE
  end

  Page.new page_address, obj_start_address, limit
end

これでオブジェクトが保存されているページの情報を得られるようになったので、先の例で使ったオブジェクトアドレスのページ情報を調べてみましょう。

page_address = page_address_from_object_address(0x7fcc6c8367e8)
p page_info(page_address)
# => #<struct Page address=140515970596864, obj_start_address=140515970596880, obj_count=408>

page_address = page_address_from_object_address(0x7fcc6c836838)
p page_info(page_address)
# => #<struct Page address=140515970596864, obj_start_address=140515970596880, obj_count=408>

page_address = page_address_from_object_address(0x7fcc6c847b88)
p page_info(page_address)
# => #<struct Page address=140515970662400, obj_start_address=140515970662440, obj_count=407>

同じページにある最初の2つのオブジェクトでは、そのページに408個のオブジェクトを保存できます。3番目のオブジェクトは別のページにあり、そのページには407個のオブジェクトしか保存できません。

それらしく見えないかもしれませんが、ヒープの内容をビジュアル表示するのに必要となる、重要な情報の断片はこれですべて揃いました。

データ取得

あるヒープをビジュアル表示するには、実際にビジュアル表示するためのヒープが必要です。ObjectSpaceを使ってヒープをJSONファイルにダンプし、上のコードとJSONパーサー、そしてChunkyPNGを用いてグラフを生成します。

次がテストプログラムです。

require 'objspace'

x = 100000.times.map { Object.new }
GC.start
File.open('heap.json', 'w') { |f|
  ObjectSpace.dump_all(output: f)
}

ここで行っているのは、大量のオブジェクト割り当てとGCの後、heap.jsonというJSONファイルにヒープをダンプするだけです。JSONドキュメントの各行はRubyヒープの1つのオブジェクトに相当します。

今度はJSONファイルを処理するプログラムを書きましょう。ここでは、ページ内にあるオブジェクトをトラックできるようにPageクラスを変更し、JSONドキュメント全体を列挙して、各オブジェクトを対応するページに追加します。

class Page < Struct.new :address, :obj_start_address, :obj_count
  def initialize address, obj_start_address, obj_count
    super
    @live_objects = []
  end

  def add_object address
    @live_objects << address
  end
end

# ページをトラックする
pages = {}

File.open("heap.json") do |f|
  f.each_line do |line|
    object = JSON.load line

    # rootをスキップ(今日はやりたくないので:)
    if object["type"] != "ROOT"
      # オブジェクトのアドレスは基数16で文字列として保存される
      address      = object["address"].to_i(16)

      # ページのアドレスを取得する
      page_address = page_address_from_object_address(address)

      # ページを取得するか新しいページを作成する
      page         = pages[page_address] ||= page_info(page_address)

      page.add_object address
    end
  end
end

ヒープをビジュアル表示する

これで、処理プログラムによってオブジェクトは自身が所属するページごとに分割されました。今度はこのデータをヒープのビジュアル表示に変えましょう。残念なことに、ここでは小さな問題が1つあります。ヒープのダンプから得られる情報は、システムで実際に生存しているオブジェクトの情報です。ヒープの空白領域をどうやってビジュアル表示すればよいのでしょうか。

ヒープの空白部分の割り出しに使える情報が少しばかりあります。1つ目はオブジェクトのアドレスが40で割り切れるということ、2つ目はストレージの最初のアドレスを取得できること(Page#obj_start_address)。3つ目は1つのページに保存できるオブジェクト数を取得できること(Page#obj_count)です。そこで、obj_start_addressから開始してSIZEOF_RVALUEずつ増やせば、JSONファイルから読み取ったアドレスが存在するかどうかがわかるはずです。JSONファイルからアドレスを読み取れれば、それは生存しているオブジェクトであることがわかります。読み取れなければ、そこは空白のスロットということになります。

それでは、ページ上で取得可能なオブジェクトアドレスをすべて列挙するメソッドをPageオブジェクトに1つ追加しましょう。:fullがyieldされたらオブジェクトは存在し、:emptyがyieldされたらオブジェクトは存在しません。

class Page < Struct.new :address, :obj_start_address, :obj_count
  def each_slot
    return enum_for(:each_slot) unless block_given?

    objs = @live_objects.sort

    obj_count.times do |i|
      expected = obj_start_address + (i * SIZEOF_RVALUE)
      if objs.any? && objs.first == expected
        objs.shift
        yield :full
      else
        yield :empty
      end
    end
  end
end

これで、ページからページへ空白スロットをすべてのスロットから区別できるようになりました。ChunkyPNGでPNGファイルを生成しましょう。PNGの各カラムは1つのページを表し、各ページ内の2×2ピクセルの正方形は1つのオブジェクトを表します。オブジェクトが存在する場合はオブジェクトを赤く塗り、空白の場合はそのままにします。

require 'chunky_png'

pages = pages.values

# オブジェクトを2x2ピクセルの正方形で表すので、
# PNGの高さはオブジェクトの最大数の2倍になり、
# 幅はページ数の2倍になる
height = HEAP_PAGE_OBJ_LIMIT * 2
width = pages.size * 2

png = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)

pages.each_with_index do |page, i|
  i = i * 2

  page.each_slot.with_index do |slot, j|
    # スロットが埋まっている場合は赤くする
    if slot == :full
      j = j * 2
      png[i, j] = ChunkyPNG::Color.rgba(255, 0, 0, 255)
      png[i + 1, j] = ChunkyPNG::Color.rgba(255, 0, 0, 255)
      png[i, j + 1] = ChunkyPNG::Color.rgba(255, 0, 0, 255)
      png[i + 1, j + 1] = ChunkyPNG::Color.rgba(255, 0, 0, 255)
    end
  end
end

png.save('heap.png', :interlace => true)

このコードを実行後、heap.pngというファイルが出力されるはずです。私が生成したファイルは次のとおりです。

この例ではヒープがすべて埋まっているので今ひとつです。今度は比較的空のプロセスからヒープをダンプして様子を見てみましょう。

$ ruby -robjspace -e'File.open("heap.json", "wb") { |t| ObjectSpace.dump_all(output: t) }'

このヒープを処理すれば、次のような出力になります。

これでおしまいです。お読みいただきありがとうございました。

完全なコードはここにアップしています。

<3<3<3<3<3

関連記事

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

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

[インタビュー] Aaron Patterson(後編): Rack 2、HTTP/2、セキュリティ、WebAssembly、後進へのアドバイス(翻訳)

Rails5: ActiveSupport::Durationでの数値へのパッチ

$
0
0

こんにちは、hachi8833です。

小ネタですが、RailsのActiveSupport::Durationで数値にどうやってパッチを当てているのかが気になったので見てみました。

ActiveSupport::Durationでの挙動

1.month2.daysなどでDurationになります。

require 'active_support/all'
a = 1.month
#=> 1 month
a.class
#=> ActiveSupport::Duration

ついでに1のクラス階層も見てみます。

1.class
#=> Integer
1.class.ancestors
#=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
 ActiveSupport::NumericWithFormat,
 Integer,
 JSON::Ext::Generator::GeneratorMethods::Integer,
 Numeric,
 Comparable,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 PP::ObjectMixin,
 Kernel,
 BasicObject]

どうやらNumericWithFormatでやっているようです。

numeric/conversions.rbだった

あっさり見つかりました。

module ActiveSupport::NumericWithFormat
  ...
end

# Ruby 2.4+ unifies Fixnum & Bignum into Integer.
if 0.class == Integer
  Integer.prepend ActiveSupport::NumericWithFormat
else
  Fixnum.prepend ActiveSupport::NumericWithFormat
  Bignum.prepend ActiveSupport::NumericWithFormat
end
Float.prepend ActiveSupport::NumericWithFormat
BigDecimal.prepend ActiveSupport::NumericWithFormat

Module#prependを使ってIntegerにパッチを当てていました。
Ruby 2.4以前の場合はFixnumBignumにパッチを当てています。

FloatBigDecimalにも当たっているので、1.1.hoursもできます。

関連記事

[Rails5] Active Supportの概要をつかむ

Rails 5.2ベータがリリース!内容をざっくりチェックしました

$
0
0

こんにちは、hachi8833です。

日本時間の早朝、DHHが自らRails 5.2ベータの公開をアナウンスしましたので、駆け足で内容を追っかけてみました。

過去の週刊Railsウォッチで扱った内容が多かったので、扱ったことのあるPRにはバックナンバーも貼りました。

Rails 5.2ベータの概要

プレスリリースの末尾には、Railsの5.xシリーズはこれが最後になるかもしれないとあります。既にRails 6.0を視野に入れているそうです。

Active Storageの改善

ファイルをクラウドにそのままアップロードできるようになりました。Amazon S3、Google Cloud Storage、Microsoft Azure Cloud File Storageをすぐ使うことができます。動画やPDFのプレビューも作りやすくなったそうです。

Active StorageはBasecamp 3で既に本番運用されているとのことです。

Active Storage READMEより:

  • ファイルが1つの場合も複数の場合も対応

モデルでhas_one_attachedhas_many_attachedで指定できます。

# ファイルが1つの場合
class User < ApplicationRecord
  # 添付ファイルとblobが関連付けられる。ユーザーが削除されるとデフォルトで削除される
  # (モデルが削除され、リソースファイルが削除される)
  has_one_attached :avatar
end

# ファイルが複数の場合
class Message < ApplicationRecord
  has_many_attached :images
end
  • Active StorageとJavaScriptライブラリでクラウドへのダイレクトアップロードをサポート
// asset pipelineに以下を追加
//= require activestorage
// npmパッケージを利用
import * as ActiveStorage from "activestorage"
ActiveStorage.start()
<!-- フォームでアップロードを指定 -->
<%= form.file_field :attachments, multiple: true, direct_upload: true %>

なお現時点では、ActiveStorageのドキュメントはedgeguides.rubyonrails.orgにも上がっていません。

週刊Railsウォッチ(20170707)Railsの新機能ActiveStorage、高速Rubyフォーマッタrufo gemが超便利、Railscasts全コンテンツが無料公開ほか

Redisキャッシュストアが標準で使える

純粋なRedis、hiredis、Redis::Distributedをサポートし、複数Redisでのシャーディング(sharding: 複数サーバーへのデータ分散)やMGETも利用可能です。Redisサーバーにアクセスできない場合にも例外をraiseせず、ローカルキャッシュも利用できます。

      # デフォルトは `redis://localhost:6379/0`
      config.cache_store = :redis_cache_store
      # Redis::Distributedで複数ホストをサポート
      config.cache_store = :redis_cache_store, driver: :hiredis
        namespace: 'myapp-cache', compress: true,
        url: %w[
          redis://myapp-cache-1:6379/0
          redis://myapp-cache-1:6380/0
          redis://myapp-cache-2:6379/0
          redis://myapp-cache-2:6380/0
          redis://myapp-cache-3:6379/0
          redis://myapp-cache-3:6380/0
        ]

キャッシュの改善

キーベースのキャッシュの不要な生成を抑制して再利用を促進し、1KB以上のキャッシュをデフォルトで圧縮するようになりました。イニシャライザでcompress: falseを指定すると圧縮がオフになります。

週刊Railsウォッチ(20170526)増えすぎたマイグレーションを圧縮するsquasher gem、書籍「Complete Guide to Rails Performance」ほか

HTTP/2 early hintsを導入

HTTP/2のearly hintsに対応しました。

# actionpack/lib/action_dispatch/http/request.rb
+    def send_early_hints(links)
+      return unless env["rack.early_hints"]
+
+      env["rack.early_hints"].call(links)
+    end

参考: blog.jxck.io HTTP の新しいステータスコード 103 Early Hints

週刊Railsウォッチ(20171013)Ruby 2.5.0-preview1リリース、RubyGems 2.6.14でセキュリティバグ修正、Bootstrap 4.0がついにBetaほか

Bootsnap gemが標準に

Shopify作のbootsnap gemはRails/Rubyアプリを高速で起動できます。既存のRailsアプリのGemfileへの追加も容易です。

週刊Railsウォッチ(20170728)bootsnapがRailsで正式採用、Ruby Prizeの推薦開始、PostgreSQL配列の重複を除去ほか

Content-Security-PolicyヘッダーをDSLで設定可能に

これはRailsウォッチでは扱っていなかったものでした。

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do
  p.default_src :self, :https
  p.font_src    :self, :https, :data
  p.img_src     :self, :https, :data
  p.object_src  :none
  p.script_src  :self, :https
  p.style_src   :self, :https, :unsafe_inline

  # Specify URI for violation reports
  # p.report_uri  "/csp-violation-report-endpoint"
end

参考: MDN Content-Security-Policy

config/secrets.yml.encで秘密情報をリポジトリで一元管理可能に

EncryptedConfigurationクラスも導入されました。

週刊Railsウォッチ(20170929)特集: RubyKaigi 2017セッションを振り返る(2)Ruby 2.3.5リリースほか

Webpacker 3.0が利用可能に

そういえばWebpacker 2.0が6月頃、3.0が8月末にリリースされていました。asset pipelineの出番が減りそうです。

週刊Railsウォッチ(20170602)チームが喜ぶ19のgem、Bundler 1.15が高速化&機能追加、Deviseに挑戦する新認証gem「Rodauth」ほか

番外

関連は不明ですが、早くもRails 5.1.2がらみのバグとおぼしきissueがRubyの方に上がりました。

Unicodeで絶対知っておくべき5つの注意(翻訳)

$
0
0

概要

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

画像はすべて元記事からの引用です。

Unicodeで絶対知っておくべき5つの注意(翻訳)

先週の「WhatsApp Androidの偽アプリ出現」のニュースによると、公式アプリと同じ開発者に見える名前で偽アプリが提供されました。この詐欺師は、印刷に現れないスペース文字を開発者名に混ぜ込むことでバリデーションのすり抜けに成功しました。このハックによって、Play Storeのメンテナーが気づくまでに100万人以上が騙されました。

Unicodeの標準としての価値は計り知れません。UnicodeのおかげでPCやスマホ、ウォッチを問わず同じメッセージを同じ方法で世界中どこでも表示できます。残念なことに、Unicodeの複雑さが原因となって、詐欺師やいたずら小僧どもにとって格好の金の鉱脈になってしまっています。Unicodeで引き起こされる基本的な問題をGoogleなどの巨大企業が食い止めることができなかったら、中小企業にとっては勝ち目のない戦いに感じられてしまうのではないでしょうか。しかし、問題を引き起こしているのは、ほとんどが一握りの乱用です。詐欺行為を防ぐために、開発者なら誰もが知っておくべきUnicodeの知識トップ5をご紹介いたします。

1. 画面に表示されないUnicodeポイントがたくさんある

Unicodeにはゼロ幅のコードポイントがいくつもあります。たとえばzero-width joiner(U+200D)zero-width non-joiner(U+200C)はハイフネーションのヒントとして使われます。これらの文字は画面上には表示されませんが、文字列を比較するときに影響を与えます。WhatAppアプリの詐欺が長期間気づかれなかった理由はこれです。こうした文字の多くはGeneral Punctuationブロック( U+2000〜U+206F)に含まれています。一般に、IDの文字でこのブロックのコードポイントを利用する必然性はないので、最も簡単にフィルタできます。しかし、Mongolian Vowel Separatorのように、この範囲外にも特殊な非表示コードがいくつか存在します。

一般に、Unicodeの一意性制約に単純な文字列比較を使うのは危険です。考えられる回避法としては、悪用される可能性のあるIDやその他のデータに許可されている文字セットを制限することです。残念ながら、これですべての問題を解決できるわけではありません。

2. 見た目が極めて似ているコードポイントがたくさんある

Unicodeは世界中のあらゆる手書き言語で使われるシンボルをすべてカバーしようとしているため、人間には区別不可能なほど見た目が互いによく似ている文字がどうしても増えてしまいます。しかしコンピュータならあっさりと区別できます。この問題を利用したいたずらとしてMimicというお遊びユーティリティがあり、これはソフトウェア開発でよく使われるシンボル(コロンやセミコロンなど)を見た目のよく似た別のUnicode文字に差し替えてしまいます。コード編集ツールはカオスと化し、開発者は混乱の中に取り残されてしまいます。

見た目のよく似たシンボルの問題は、単なるいたずらにとどまりません。homomorphicという造語で呼ばれる、これを悪用した攻撃手法によって深刻なセキュリティ問題が生じることがあります。2017年4月、セキュリティの研究者は異なる文字セットを混ぜ込むことでapple.comと見た目のそっくりなドメインを登録し、さらにSSL証明書まで取得してみせました。主要なブラウザは何も知らずにSSL鍵を表示し、このドメインを「安全」なものとして扱ってしまいました。

表示可能文字と非表示文字の混在と同様、ID(特にドメイン名)の文字に別の文字セット名の混用を許可する正当性はまずありません。ほとんどのブラウザでは、文字セットの混在したドメイン名を16進Unicode値で表示することでペナルティをかけ、ユーザーが簡単に惑わされないようにしています。ユーザーにIDを表示しようとする場合(検索結果など)は、混乱防止のため同様の手法を検討してください。しかし、これも完全な解決方法ではありません。sap.comやchase.comといった一部のドメインは、非ラテン文字セットのブロック1つだけで完全に構成できてしまいます

Unicodeコンソーシアムは紛らわしい文字一覧を公開しており、詐欺の可能性を自動チェックするときの資料として有用かもしれません。その他に、紛らわしい文字の簡単な作り方をお探しの方はShapecatcherというサイトをチェックしてみてください。このツールは、手書きの文字に似ているUnicodeシンボルのリストを表示します。

3. 正規化で正しく正規化されるとは限らない

ユーザー名などのIDで、ユーザーのID入力方法が異なる場合にも処理を統一するためには、正規化(normalization)が重要です。IDの正規化によく使われる方法のひとつは、すべて小文字に変換することです。たとえばJamesBondはjamesbondと同じになります。

互いによく似た文字や重複したセットが多数存在するため、(自然)言語が異なっていたりUnicode処理ライブラリが異なっていたりすると、適用される正規化戦略もそれによって変わってしまうことがあります。正規化が複数箇所で行われていると、セキュリティ上のリスクにつながる可能性があります。要するに、アプリのさまざまな部分で行われる小文字変換の動作がすべて同じであると仮定すべきではないということです。SpotifyのMikael Goldmann氏は、同社のユーザーのひとりがアカウントハイジャック手法を発見した後、この問題について2013年に見事なインシデント分析を行いました。攻撃者が他人のユーザー名をUnicode異体字で登録すると(ᴮᴵᴳᴮᴵᴿᴰなど)、同じ正規アカウント名(bigbird)に変換される可能性があります。語の正規化手法がアプリのレイヤごとに異なる場合、偽アカウントを登録してパスワードをリセットできてしまいます。

4. 画面上に表示される文字の長さとメモリサイズはまったく別物

基本的なラテン文字や大半のヨーロッパ文字セットでは、画面や紙の上のあるテキスト片の占めるスペースは、シンボルの個数におおむね比例し、テキストのメモリ上のサイズにもおおむね比例します。EM(「M」という文字の幅)やEN(「N」という文字の幅)が長さの単位としてよく使われる理由はこれです。しかしUnicodeで同じようなことを仮定するのは危険です。Bismallah Ar-Rahman Ar-Raheem(U+FDFD)のように、たった1文字でそこらの英単語よりも長くなる愛すべきシンボルは山ほどあり、Webサイトで仮定される表示上の囲みからあっさりはみ出してしまいます。つまり、文字列の文字長をベースとするワードラップやテキスト折り返しのアルゴリズムはことごとく破綻してしまうということです。ターミナルで動作するCLIプログラムのほとんどは固定長フォントを前提としているので、ターミナルにこうした文字を表示すると、引用符の位置が完璧にずれてしまうのがわかります。

これをいたずらに使ったzalgo text generatorというサイトは、入力したテキスト片の周りにゴミ文字を大量に追加して、テキストが上下方向に大きくはみ出すように変えてしまいます。

もちろん、非表示コードポイント全体の問題によってメモリサイズと画面上のサイズの関連は失われてしまうので、画面サイズに収まるよう巧妙に調節したテキストをフィールドに入力すればデータベースのフィールドがパンクするかもしれません。固有のスペース(幅)を持たない例は他にも膨大にあるので、非表示文字をフィルタするだけでは不十分です。

U+036BU+036Cなどの「combining latin character」は直前の文字の上に配置されるので、1行のテキスト内に複数行のテキストを書くことができます(N\u036BO\u036CはNͫOͬと表示される)。ヘブライ語聖書の経典朗唱向けのイントネーションを表すカンティレーションマーク(参考)は、同じ表示スペースにいくつでも重ねられるので、画面上のたった1つの文字に大量の情報をエンコードするという悪用が簡単にできてしまいます。Kartin Kleppeは、古典的なライフゲーム(参考)のブラウザ版実装にカンティレーションマークをエンコードしてみせました。このページのソースコードを見た方は相当なショックを受けるでしょう。

5. Unicodeは単なるパッシブデータではない

コードポイントによっては、表示可能な文字の表示方法に影響を与えるよう設計されているものがあります。つまり、ユーザーがコピペしているのは単なるデータ以上のものであり、処理インストラクションも入力できるということです。よくあるいたずらとして、right-to-left override(U+202E)文字でテキストの進行方向を変えてしまうというのがあります。たとえば、Google MapsでNinjasを検索してみてください。このクエリ文字列は実際には検索ワードを逆順にしたものになっているので、ページには「ninjas」と表示されていますが、実際には「sajnin」を検索しています。

この手口はXKCDでネタとして使われて有名になりました。

「これはまだましな方だ!最悪なのは」「U+202e」「らすれそ」「?たき起が何」「たっがやし何めて」

データと処理インストラクション(事実上の実行可能コード)の混在は絶対によくありません。ユーザーが処理インストラクションを直接入力できてしまう場合は特にそうです。ユーザー入力をそのままページに表示すれば大きな問題になります。ほとんどのWeb開発者はそのことを知っているので、ユーザー入力からHTMLタグを除去してサニタイズしますが、ユーザー入力にUnicodeの制御文字が含まれる可能性にも注意が必要です。これは禁止用語やコンテンツのフィルタリングをすり抜ける初歩的な方法であり、語の文字を逆順にしてright-to-leftを語の最初に置いてオーバーライドするだけでできてしまいます。

Right-to-leftをハックして悪意のあるコードを埋め込むのはさすがに無理かもしれませんが、気をつけないとコンテンツ表示を台無しにされる、つまりページ全体の文字が逆順で表示されるかもしれません。よく行われている対策は、ユーザーが入力するコンテンツの置き場所をinputフィールドやtextエリアに限定して、処理インストラクションがせめてページの他の部分に効かなくなるようにすることです。

もうひとつ、表示の処理インストラクションで特に大きな問題として、異字体(variation)セレクタがあります。色違いの絵文字ごとにコードを作成する事態を避けるために、Unicodeでは異字体セレクタで基本シンボルと色をミックスできるようになっています。white flagにrainbow異字体セレクタを適用するとrainbow flagになります。しかし異字体がすべて有効とは限りません。2017年1月、iOSのUnicode処理のバグが原因で、ある仕掛けを施したメッセージを送りつけるだけでiPhoneをリモートでクラッシュさせるといういたずらが発生しました。このメッセージには1つのwhite flag、1つの異字体セレクタ、そして1つのゼロが含まれていました。iOS CoreTextは正しい異字体を取り出そうとしてパニックになり、iOSがクラッシュしました。このトリックはダイレクトメッセージやグループチャットの他、共有名刺にも通用し、iPadや一部のMacbookまでこの問題の影響を受けました。この悪用によるクラッシュの防止策はほとんどありませんでした。

この種のバグは数年に1度は発生します。2013年には、アラビア文字の処理でOS XやiOSをクラッシュさせる可能性のあるバグが見つかりました。こうしたバグはOSのテキスト処理モジュールの奥底に潜んでいるため、一般のクライアントアプリ開発者には防止策がまったくありません。

その他の興味深い処理インストラクションについては、GitHubで公開されているAwesome Codepointsリストで確認できます。Unicodeによって引き起こされる問題についてもっと詳しくお知りになりたい方は、拙著『Humans vs Computers』をぜひチェックしてください。

(image credits: Amador Loureiro)


関連記事

Rubyの内部文字コードはUTF-8ではない…だと…?!

Unicodeにおける日本の元号の開始日・終了日の定義について

[CSS][縦書] CSS研究部Webを更新しました

Rails: RSpecをもっとDRYに書くテクニック(翻訳)

$
0
0

概要

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

Rails: RSpecをもっとDRYに書くテクニック(翻訳)

これは何?

RSpec APIは常に可能な限りDRYで読みやすいDSLへと進化し続けています。しかし、specをさらにDRYにできる方法(トリックや追加メソッドなど)はまだまだあります。

警告: 本記事を読んで「これこれの方法を使うより先に、テストしやすいコードを書けるように設計を見直す方がよくね?」と言いたくてたまらなくなるかもしれませんが、それについてはどうか周知のこととお考えください。

更新情報(2017/08/15): saharspecはその後正式にリリースされました。テストもドキュメントもあります。ぜひお試しください。

DRYに書く方法

subjectを活用

subject文は現代的なRSpecと非常によく馴染みます。subjectを使うことで、テストの対象を明確に記述してからテストを実行できます。

# DRYかつ良い
subject { 'foo' }
it { is_expected.to eq 'foo' }

rspec-its(以前はRSpecのコアにありましたが、v3からは別gemに切り出されました)を使うと、さらに創意に満ちたチェックを行えます。

# これもDRYかつ良い
its(:size) { is_expected.to eq(3) }

しかしこのアプローチには少々限りがあります。

subjectにarrayを書く

(subjectで)値のarrayをチェックしたい場合、itsチェックでは(英語的に)うまくはまりません。

# `subject`を繰り返して長い`expect`引数を使う必要がある :(
subject { %w[foo test shenanigans] }
it { expect(subject.map(&:size)).to include(3) }

次の方法はいかがでしょうか。

subject { %w[foo test shenanigans] }
its_map(:size) { is_expected.to include(3) }

実装は次のとおりです(既にrspec/itsをrequireして名前空間を再利用している前提です)。

module RSpec
  module Its
    def its_map(attribute, *options, &block)
      describe("map(&:#{attribute})") do
        let(:__its_map_subject) do
          attribute_chain = attribute.to_s.split('.').map(&:to_sym)
          attribute_chain.inject(subject) do |inner_subject, attr|
            inner_subject.map(&attr)
          end
        end

        def is_expected
          expect(__its_map_subject)
        end

        alias_method :are_expected, :is_expected

        options << {} unless options.last.is_a?(Hash)

        example(nil, *options, &block)
      end
    end
  end
end

次のようにチェインすることもできます。

its_map(:'chars.first') { is_expected.to include('s') }
subjectにブロックを書く

次のコードで考えてみます。

describe 'addition' do
  subject { 'foo' + other }

  context 'when compatible' do
    let(:other) { 'bar' }
    # DRYかつ良い
    it { is_expected.to eq 'foobar' }
  end

  context 'when incompatible' do
    let(:other) { 5 }
    # subjectをまた書かないといけない:(
    it { expect { subject }.to raise_error(TypError) }
  end
end

上より以下の方が良いとは思いませんか?(私は思います)

subject { 'foo' + other }
# ...
context 'when incompatible' do
  let(:other) { 5 }
  its_call { is_expected.to raise_error(TypeError) } # よし、これもDRYになった
end

実装は次のとおりです(既にrspec/itsをrequireして名前空間を再利用している前提です)。

module RSpec
  module Its
    def its_call(*options, &block)
      describe("call") do
        let(:__call_subject) do
          -> { subject }
        end

        def is_expected
          expect(__call_subject)
        end

        example(nil, *options, &block)
      end
    end
  end
end

its_callは多くの便利マッチャと併用できます。

subject { hunter.shoot_at(:lion) }
its_call { is_expected.to change(Lion, :count).by(-1) }
subjectにメソッドを書く

ある比較的シンプルなメソッドについて、条件をさまざまに変えると戻り値がどのように変わるかを多数テストしなければならないとします。こんなとき、どう書きますか?

it { expect(fetch(:age)).to eq 30 }
it { expect(fetch(:weight)).to eq 50 }
it { expect(fetch(:name)).to eq 'June' }
# .... パターンがあることがわかりますか?...

以下の書き方はいかがでしょうか(これはRSpecとrspec/itsだけで追加コードなしで書けます)。

subject { ->(field) { fetch(field) } }

its([:age]) { is_expected.to eq 30 }
its([:weight]) { is_expected.to eq 50 }
its([:name]) { is_expected.to eq 'June' }

これだけで動きます。its([arg])とするとsubjectの[]が呼び出され、Ruby’のProcでは[]の定義が.callと同義になっているからです。

: 同じsubjectをテストするもうひとつの方法は、上のits_callを書き換えてits_call(:age) { is_expected.to eq 30 }のように引数を取れるようにすることです。

マッチャで楽しむ

上でご紹介したアイデアは、どちらもRSpecメンテナーたちによって検討の末rejectされました。だからといってこのアイデアが完全に役に立たないということにはなりません(正直、私はメンテナーの好みよりこちらの方がずっと明確だと思います)。

否定テスト

更新(2017年夏): この方法は非常によくないので使わないでください。演算子の優先順位が原因で、and_notを2つ連続で使うと思いもよらぬ結果が生じます。

訳注: 取り消し線の部分は訳出しませんでした。

メソッドのexpectation

#934で、「あるオブジェクトのあるメソッドを、あるコードから呼び出せるexpectation」をRSpecで1文で書けない理由がずっと議論されています(長すぎて私もあまりフォローできていません)。

訳注: 現在#934はcloseしています。

現時点では、次のどちらかの書き方が使えます。

# その1
expect(obj).to receive(:method)
some_code

# その2
obj = spy("Class")
some_code(obj)
expect(obj).to have_received(:method)

私にはどちらもあまり「アトミック」には見えません。次のソリューションはいかがでしょうか。

RSpec::Matchers.define :send_message do |object, message|
  match do |block|
    allow(object).to receive(message)
      .tap { |m| m.with(*@with) if @with }
      .tap { |m| m.and_return(@return) if @return }
      .tap { |m| m.and_call_original if @call_original }

    block.call

    expect(object).to have_received(message)
      .tap { |m| m.with(*@with) if @with }
  end

  chain :with do |*with|
    @with = with
  end

  chain :returning do |returning|
    @return = returning
  end

  chain :calling_original do
    @call_original = true
  end

  supports_block_expectations
end

これで以下のように1文で書けます。

# 引数の順序はchange()マッチャと似ている
expect { some_code }.to send_message(obj, :method).with(1, 2, 3).returning(5)

# 前述のits_callとの相性もよい
subject { some_code }
it { is_expected.to send_message(obj, :method).with(1, 2, 3).returning(5) }

クールだと思いませんか?

軽くまとめ

更新情報(2017/08/15): saharspecはその後正式にリリースされました。テストもドキュメントもあります。ぜひお試しください。

ご紹介したスニペットはいずれも私が日常的に業務で便利に使っており、ちゃんと動いています。もちろん別の方法がよいこともあるでしょう。しかしいずれにしろ、どの実装も雑なりにちゃんと動きます(「動くけど雑」と思う人もいるかもしれませんが)。私はこれらのスニペットをsaharaspecというちょっと気の利いた名前のリポジトリに置きましたが、本記事執筆時点ではまだ正式なgemになっておらず、テストやドキュメントもありません。しかし正式なgemspec付きでGitHubに置かれているので既に利用可能な状態になっていますので、ぜひ皆様のご感想をお寄せください。

Gemfileに以下を追記します(おそらくdevelopmentグループ)。

gem 'saharspec', git: 'https://github.com/zverok/saharspec.git'

なお、Redditでは本記事のワンライナーDRY specについて議論がかなり白熱しています(良し悪しはともかく)。

関連記事

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

テストを不安定にする5つの残念な書き方(翻訳)

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

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

Viewing all 1835 articles
Browse latest View live