devise_for

Deviseを使うとき、 routes.rbに書く devise_for が何をやっているか。

devse_for?

devise/rails/routes.rb の devise_for のコメント欄のところ

# Includes devise_for method for routes. This method is responsible to
# generate all needed routes for devise, based on what modules you have
# defined in your model.

モデル上でDeviseのモジュール設定をベースとして、routesの設定をする。

また、

devise/mapping.rb の先頭のコメント。

# Responsible for handling devise mappings and routes configuration. Each
 # resource configured by devise_for in routes is actually creating a mapping
 # object. You can refer to devise_for in routes for usage options.

devise_for の定義は、実質はDevise::Mapping のインスタンスを作成している。

mappingオブジェクト作った後に、 devise_scope を呼び出して、 devise_#{module name} を呼び出すことで、routesを定義している。

# devise_for メソッドの最下部で、devise_scope を呼び出している箇所。
devise_scope mapping.name do
  with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
    routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
  end
end

例えば、passwordモジュールのroutesは下記のように定義されてる

def devise_password(mapping, controllers) #:nodoc:
  resource :password, only: [:new, :create, :edit, :update],
    path: mapping.path_names[:password], controller: controllers[:passwords]
end

mappingオブジェクトの設定を元に resource の設定をしてる。

devise_scope?

devise_scope :resource_name &block で、ブロックにroutesを定義する処理を指定できる。

devise_for でモジュール毎の基本的な設定と各種オプションでの調整はできる。

devise_scope はもっと自由にroutesをカスタマイズするときに使うとのこと。

constraints (配下のネストされたルートに制約をかける処理)で request.env["devise.mapping"] に Devise.mappings[scoep]を設定して、

scope :resource_name に、上記で定義したroute情報を渡している

constraints の処理は、initializeのときではなく、実際に対象のrouteにアクセスがあったときに走るので、

deviseのroutesのリクエスト毎に request.env["devise.mapping"] にMapping情報を設定している(目的は謎。)

まとめ

Railsのroutes周りの仕組みをハックして、deviseの各モジュールで必要なroutesの設定を、簡単で、柔軟な形で行えるようにしているイメージ。

Sidekiqの仕組み

Railsのジョブキューのgemで有名なSidekiq。 どのように動いているかよくわかっていないので調べてみた。

エンキュー

ActibeJob.perform_later

キューに入れたい処理はActiveJob::Baseを拡張したクラスのperformメソッドに実装し、

perform_laterを呼び出すと、エンキューの処理が走る。

Class SampleJob < ActiveJob::Base
  def perform
    p "Hello!"
  end
end

...

SampleJob.perform_later

queue_adapter.enqueue

perform_laterは ActiveJob.queue_adapter に設定されているオブジェクトの enqueue にjobインスタンスを投げます。 queue_adapterの設定はデフォルトでは、 :async(AsyncAdapter) で、configで以下のように指定すると、SidekiqAdapterが設定されます。

config.active_job.queue_adapter = :sidekiq

他にどんなアダプターがあるかは、activjob/queue_adapters.rb のコメントに結構細かく書かれています。

rails/queue_adapters.rb at master · rails/rails · GitHub

SidekiqAdapter.enqueue

def enqueue(job) #:nodoc:
  #Sidekiq::Client does not support symbols as keys
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ]
end

こんな感じ。 ジョブのクラス名や引数を Sidekiq::Client に投げている。

class として指定されている。JobWrapper の実装はかなり薄くて、

class JobWrapper #:nodoc:
  include Sidekiq::Worker

  def perform(job_data)
    Base.execute job_data.merge('provider_job_id' => jid)
  end
end

これだけです。 Base.executeは受け取ったjob_dataをdeserializeして、performを実行します。

Sidekiq.client

Sidekiq.clientのpush処理では、受け取ったジョブ情報をRedis上に保存できる状態に成形し、ジョブIDの情報を追加して、 Redisの queues:#{queue_name} キーに追加します。

サンプルで追加してみた中身はこんな感じのjsonです。

{
    "class": "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
    "wrapped": "SampleJob",
    "queue": "default",
    "args": [
        {
            "job_class": "SampleJob",
            "job_id": "ae214f55-3d84-4037-8086-be111661ec6a",
            "queue_name": "default",
            "priority": null,
            "arguments": [],
            "locale": "ja"
        }
    ],
    "retry": true,
    "jid": "d1ecd3aa4bf5340b5c2a52f6",
    "created_at": 1506733202.404303,
    "enqueued_at": 1506733202.405045
}

ジョブの取得と実行

Redisにジョブを追加するだけではジョブは実行されません。 ジョブ情報を取得して、実行する必要があります。

取得

bundle exec sidekiq を実行することで、ジョブを取得、実行するプロセスを起動できます。

プロセスが起動すると、Sidekiq::Launcher.run が実行されます。

sidekiq/launcher.rb at master · mperham/sidekiq · GitHub

まず、Sidekiq::Managerがworkerを作成し、起動します。(デフォルトの設定では25個作成されます。)

workerはそれぞれのスレッドを起動し、ジョブ情報をRedisから取得しに行きます。

wokerの中身は Sidekiq::Processor で、Processor.get_one から下記の Sidekiq::BasicFetch.retrieve_work が呼び出され、Redisからジョブ情報を取得しています。

sidekiq/fetch.rb at master · mperham/sidekiq · GitHub

実行

Redisから取得したジョブ情報を元に、ジョブのクラスと引数情報を deserialize して、worker上のスレッドで実行されます

sidekiq/processor.rb at master · mperham/sidekiq · GitHub

performが呼び出されてます。

Railsのloadは元のプロセス起動時に行われています。

sidekiq/cli.rb at master · mperham/sidekiq · GitHub

scheduled

下記のように waitwait_untill を設定した場合はどうなるか?

SampleJob.set(wait: 5.minutes).perform_later

エンキュー

指定された値から、実行する時刻を計算し、 queue_adapterの enqueue_at にジョブ情報と一緒に渡されます。

#SidekiqAdapter.enqueue_at
def enqueue_at(job, timestamp) #:nodoc:
  job.provider_job_id = Sidekiq::Client.push \
    'class'   => JobWrapper,
    'wrapped' => job.class.to_s,
    'queue'   => job.queue_name,
    'args'    => [ job.serialize ],
    'at'      => timestamp
end

Sidekiq::Client側の処理

Sidekiq::Client側の処理では、 at が指定されていると sorted set に timestampでソートする形でジョブ情報が登録されます。 (上記の通常(at指定なし)のジョブ情報リストとは違うキーに保持されます)

取得、実行

Sidekiqのプロセスは上記で説明した プロセスを実行する woker を管理する manager の他に、 at 指定で登録されているジョブを ジョブキューに登録する poller が動いています。

pollerは5秒ごとに、 sorted set から 現在時刻より前 の timestamp のジョブ情報がないかを見に行き、 対象のジョブがあった場合は Sidekiq::Client::push を呼び出し、 sorted set から ジョブキューへジョブ情報を移します。

この処理により、 at で指定したジョブも worker達が処理する対象になってくれます。

retry

プロセスでエラーが起きた時、

retry が有効な場合で、retry数がまだMAX retry数内(デフォルトは25)の場合は、ジョブ情報が retry queue に追加されます。

retry queue に保持される情報は、通常のジョブ情報に、エラー情報とリトライ回数などの情報が加えらえれたものになります。

上記で話した scheduledのキューを処理する poller は、retryのキュー情報も見るようになっているので、retryに入れられた情報は、

自然とジョブキューへ追加されます。

リトライ実行のディレイはリトライ数が多くなるごとに指数関数的に多くなるようになっています。

ディレイ時間の計算処理 sidekiq/job_retry.rb at master · mperham/sidekiq · GitHub

リトライ設定がfalseの場合と、リトライ数が上限を超えたジョブ情報は dead というキーの sorted set に 失敗時刻が sort 対象の項目として、登録されます。

dead データセットは、オプションの dead_max_jobs, dead_timeout_in_seconds の設定を元に、

失敗したジョブ情報を保持しています。

デフォルトの設定は下記となっています

deed_max_jobs: 1万
dead_timeout_in_seconds: 6ヶ月

となっています。

sidekiq-cron

Sidekiqをcron形式の設定でスケジューリングするgemが sidekiq-cron です。

github.com

設定ファイルの読み込み

YAMLの設定ファイルを読み込む場合は、 config/initializers などに

Sidekiq::Cron::Job.load_from_hash YAML.load_file(#{filepath})

のように指定することで、Redis上に、cronの設定情報が保存されます。

poller

sidekiq-cronでプロセスを持つことはなく、sidekiqのlauncher を上書きすることで実現しています。

# Sidekiq::Lancher

# remember old run
alias_method :old_run, :run

# execute normal run of launcher and run cron poller
def run
  old_run
  cron_poller.start
end

sidekiqの manager, poller とは別に、 cron_poller の処理を start しているのが解ります。

cron_poller の 処理は、上記でRedisに取り込んだ cron の設定を定期的に見に行って、ジョブキューへ追加する処理をやっています。

TCPのウィンドウ制御とコネクション識別

ウィンドウ制御

TCPは送信と応答確認によってデータ送信の信頼性を高めているが、パケット毎にそれをやっていると効率が悪い。

この問題を解決するために、ウィンドウ制御という概念を取り入れている。

具体的には、応答確認をする単位を1パケットではないく、ある一定サイズ(ウィンドウサイズ)ごとに行うようにすることで、通信を効率化している。

サイズの目安

  • 1パケット 1.5KB (イーサネットの最大パケットサイズ)

  • ウィンドウサイズの最大 64KB このサイズをスケールさせる規約もあり、近代のOSは対応しているらしい(気が向いたら調べる)

ASCII.jp:帯域を効率的に利用するTCPの仕組みとは? (1/2)|TCP/IPまるわかり

コネクション識別

TCPヘッダには接続IDのフィールドがないので、どうやっているのか気になったので調べてみた。

2つのendpointのIP:portで識別している。

The TCP/IP Guide - TCP Ports, Connections and Connection Identification

initialPropsとstate - React.js

Props in getInitialState Is an Anti-Pattern | React

getInitialState内でpropsを用いてstateを生成するやり方は、どのデータが本筋のデータかがわかりづらくなってしまう状況になってしまうことがあるので、注意して使ったほうがいい。

コメントフォームを例に考えてみる。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.comment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
    </div>
  }
})

上記のようなコードの場合、props.commentとstate.commentがあり、コードの複雑性が増してくると、どちらがどのケースで本筋のデータかがわかりづらくなってしまう可能性がある。

なので、下記のようにpropsは明示的に初期化用のデータということを表す名前にしたほうが良い。

var CommentBox = React.createClass({
  getInitialState: function(){
    return {
      comment: this.props.initialComment
    }
  },
  handleChange: function(e) {
    this.setState({comment: e.target.value})
  },
  render: function() {
    return <div className='comment-box'>
      <textarea 
        onChange={this.handleChange}
        defaultValue={this.state.comment} />
      <button onClick={this.handleUpdateClick}
    </div>
  }
})

Model周り - helpy code reading

共通部分を切り出す

CakePHPでいうBehaviorやComponent, Helper

Rails 共通部分を切り出す (View, Controller, Model) メモ - Qiita

関連したモデルのフォームでの扱い

Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for) - Qiita

helpyではtopicが複数のpostを持つ事ができる構造で、topic.rb内で 'accepts_nested_attributes_for :posts' と記述し、

viewでは下記のように定義している。

<%= f.simple_fields_for :posts do |p| %>
      <%= p.input :body, input_html: {:rows => 8, :cols => 60, placeholder: I18n.t(:how_can_we_help), label: 'Message', class: 'disable-empty form-control topic-placeholder'} %>
    <% end %>

model attachments

こういったプラグインはどのMVCフレームワークでも頻繁にお世話になりそう。

GitHub - assembler/attachinary: Attachments handler for Rails that uses Cloudinary for storage.

postgresに特化にした検索用のGem

GitHub - Casecommons/pg_search: pg_search builds ActiveRecord named scopes that take advantage of PostgreSQL’s full text search

# モデル内で下記ように定義すると
include PgSearch
pg_search_scope :admin_search,
                  against: [:id, :name, :user_name, :current_status, :post_cache]

# 下記のように呼び出せる
 @topics = Topic.admin_search(params[:q]).page params[:page]

polymorphicな関連

helpyではtopicsとpostsに投票機能が付いていて、どちらもvoteモデルの関連で実現されている。

# vote.rb

class Vote < ActiveRecord::Base

  belongs_to :voteable, :polymorphic => true

  ...

# topic.rb
class Topic < ActiveRecord::Base

  ...
  
  has_many :votes, :as => :voteable

ログイン後の初期設定 - helpy code reading

github.com

初期ログイン後に表示される初期設定画面周りのコードを見ていく。

流れ

ログイン後のリダイレクト

admin's root admin/dashboard#index to admin/topics#index

def index
    #@topics = Topic.mine(current_user.id).pending.page params[:page]

    if current_user.is_admin? || current_user.is_agent?
      redirect_to admin_topics_path
    elsif current_user.is_editor?
      redirect_to admin_categories_path
    else
      redirect_to root_url
    end
  end

ダイアログの表示

初期設定 = onboardingというキーワードで表現されていて、済みかの判定は下記のメソッドで定義されている。

# Admin::BaseController
def show_onboarding?
    User.first.email == 'admin@test.com' && current_user.email == 'admin@test.com' && current_user.is_admin?
  end

上記のメソッドをhelper_methodに登録して、html側で呼び出している。

# views/admin/topics/index.html.erb
<% if show_onboarding? %>
<div id='modal' class="modal" tabindex="-1" role="dialog" data-backdrop="static">
  <div class="modal-dialog modal-lg">
    <div class="modal-content">
      <iframe src="/admin/onboarding/index" width="100%" height="900" frameborder="no" scrolling="no"></iframe>
    </div>
  </div>
</div>
<script>
// Fire the onboarding modal if needed
$('#modal').modal('toggle');
</script>
<% end %>

onboarding modal

上記のmodal内のiframeに設定されているところを見ていく。

登録のステップとして、メッセージ表示、サービス設定、アカウント設定がある。

htmlは全てonboarding/index.html.erbに記載されていて、class onboard-panel を遷移させていく形で実装してある。

panel遷移のJS処理は assets/javascripts/admin.js に描かれている

var Helpy = Helpy || {};

...

Helpy.showPanel = function(panel) {
  var currentPanel = panel-1;
  $('.onboard-panel').addClass('hidden');
  $('#panel-' + panel).removeClass('hidden');
  $('li.step-' + currentPanel).html("<span class='glyphicon glyphicon-ok'></span>").addClass('filled-circle');
  $('li.step-' + panel).addClass('active-step');
  return true;
};

...

サービス設定のフォームはAjax化されている。

# onboarding/index.html.erb
...

<div id="panel-2" class="onboard-panel text-center hidden">
  <div class="onboard-message">
    <h2>Great! Lets Customize your Helpdesk:</h2>
  </div>
  <div class="text-left">
  <%= bootstrap_form_tag url: admin_onboard_settings_path, method: 'patch', remote: true do |f| %>

...

コントローラー側は下記のようになっている。

# admin/onboarding_controoer.rb

def update_settings

    設定保存処理

    respond_to do |format|
      format.html { redirect_to(admin_settings_path) }
      format.js {
          render js: "Helpy.showPanel(3);$('#edit_user_1').enableClientSideValidations();"
      }
    end
  end

アカウント設定のフォームはAjaxではなくて、onboarding#update_user で設定保存後にonboarding#complete リダイレクトして動作完了。

参考になる箇所

Decorator Pattern - Design Pattern in Ruby

あるオブジェクトの機能に追加の機能を付け足したい、

さらにいろいろな組み合わせで追加機能を実装したいという時に使えるパターン。

Interface

元の機能を持ったオブジェクト(ConcreteComponent)と追加機能を実装するオブジェクト(Decorator)という登場人物がいて、同じインターフェース(Component)を持つことで、クライアントからは実際にはどのオブジェクトを扱っているかを気にせずに使用できるようになる。

Traditional implementation

DecoratorオブジェクトにConcreteComponentオブジェクトの参照を渡し、追加機能以外の部分をConcreteComponentに委譲する。

Easing the Delegation

forwardableモジュールをextendすることで、delegateの処理を簡潔に書くことができる。

In Ruby

Wrapping Methods

w = SimpleWriter.new('out')

class << w
  alias old_write_line write_line
  def write_line(line)
    old_write_line("#{Time.new}: #{line}")
  end
end

元のメソッドの参照をaliasキーワードで old**に移して、old**をdecorateするメソッドを新たに定義する。

ちょっとした処理を簡単に追加できるが、複数のdecorate処理を買いたい時に、aliasに設定する名前が難しくなってくる。


Decorating with Modules

instance.extend(module) の方法でインタンスにmoduleを設定することで実現する。

簡単なテキストデコレーションを書いてみた。

class TextPrinter
  def output(str)
    print str
  end
end

module TextEmphasizeDecorator
  def output(str)
    print "** "
    print super(str)
    print " **"
  end
end

module TextBracketsDecorator
  def output(str)
    print '('
    super str
    print ')'
  end
end

to = TextPrinter.new
to.extend(TextEmphasizeDecorator)
to.extend(TextBracketsDecorator)

to.output('hoge')