rack を理解する

https://github.com/rack/rack

rack は Ruby web application 開発のためにいい感じのインターフェースを提供してくれる。
HTTP Request/Response をラップしてくれることで、web server や web framework、その間にある middleware の API を 1 つのメソッド呼び出しにまとめてくれる。
port を開いたり、connection を受け付けたりといったこともしてくれて、web server とのやり取りを引き受けてくれるので開発者はアプリケーション開発に集中できる。

rack アプリケーションの基本

rack application は call を呼び出せる Ruby オブジェクト。環境を引数に取って、status、header、body の配列を返す。
call の引数である envCGI-like な header を持った unfrozen な Hash オブジェクトで rack アプリケーションはこの env を変えてもよい。

sample

rackup に引数として Rack::Builder DSL を書いた config.ru を渡す。
POST メソッドのみ受け付けて、リクエストボディをそのまま返す rack アプリケーションとその rack アプリケーションを 3 つの middleware で覆っている。
それぞれの middleware では呼ばれた順番とその時のアプリケーションを標準エラー出力している。

class Empty
  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: Empty"
    p @app

    res = @app.call(env)

    p "After app: Empty"

    res
  end
end

class PostDake
  class Error < StandardError; end

  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: PostDake"
    p @app

    raise Error unless env["REQUEST_METHOD"] == "POST"

    res = @app.call(env)

    p "After app: PostDake"

    res
  rescue Error => e
    [ 500,
      {"Content-Type" => "text/plain"},
      ["#{env['REQUEST_METHOD']} is not accepted!"],
    ]
  end
end

class Big
  def initialize(app)
    @app = app
  end

  def call(env)
    p "Before app: Big"
    p @app

    res = @app.call(env)

    p "After app: Big"

    [ 200,
      {"Content-Type" => "text/plain"},
      res[2].map {|s| s.upcase}
    ]
  end
end

class ReturnBody
  require "rack"

  def call(env)
    req = Rack::Request.new(env)
    [ 200,
      {"Content-Type" => "text/plain"},
      [req.body.read]
    ]
  end
end

use Empty
use PostDake
use Big
run ReturnBody.new

この config.ru を置いているディレクトリで rack アプリケーションを起動して curl すると下記のように標準エラー出力する。 ここから以下のようなことが分かる。

  • 処理の流れ
    • -(request)-> Empty -> PostDake -> Big -> ReturnBody(アプリケーション本体) -> Big -> PostDake -> Empty -(response)->
    • request では上から use した順番で middleware の処理を行ってアプリケーション本体(ReturnBody オブジェクト)に request が到達する。
    • response では反対に下から use した順番で middleware の処理を行って response を返す。
  • app の状態
    • app には処理が行われる rack middleware が rack アプリケーションに至るまで入っている
    • curl した時のそれぞれの標準エラー出力
      • Empty の中で出力した app #<PostDake:0x00007feeb1801c70 @app=#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>>
        • Empty 次には PostDake に処理が渡る。PostDake の次は Big
      • PostDake の中で出力した app #<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>
      • Big の中で出力した app #<ReturnBody:0x00007feeb1801e00>
        • Big の次は rack アプリケーションに処理が到達するので、次のオブジェクトは @app を持っていない
❯ rackup
Puma starting in single mode...
* Version 4.3.3 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
"Before app: Empty"
#<PostDake:0x00007feeb1801c70 @app=#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>>
"Before app: PostDake"
#<Big:0x00007feeb1801cc0 @app=#<ReturnBody:0x00007feeb1801e00>>
"Before app: Big"
#<ReturnBody:0x00007feeb1801e00>
"After app: Big"
"After app: PostDake"
"After app: Empty"
::1 - - [04/Apr/2020:10:54:48 +0900] "POST / HTTP/1.1" 200 10 0.0051

rails も rack 使って web サーバーを起動している

コードを追う

example のコマンド bin/rackup -Ilib example/lobster.ru で rack application を立ち上げるところを追う

Rack::Server

  • bin/rackup は Rack::Server.start を実行するだけ
  • Rack::Server.start
    • コンストラクタに引数(options)をそのまま渡してオブジェクトにstartさせている
  • Rack::Server#new
    • 起動オプションがあれば使ってなければ、なければ default_option を使う
    • Rack::Server#parse_options を実行して options[:config] に example/config.ru絶対パスを代入する
  • Rack::Server#start
    • オプションを設定して server.run を実行
      • Rack::Server.get または Rack::Server.default を使って、@_server に Handler を代入する

Rack::Server#wrapped_app

  • Rack::Server#wrapped_app
    • Rack::Server#build_up(Rack::Server#app) を実行している
      • Rack::Server#build_up
        • 環境ごとの middleware を Array#reverse_each していく
          • middlewareは、環境ごとにデフォルトでいくつか入っている
          • Rack::Server#app で生成された rack アプリケーションを 環境ごとのデフォルトの middleware でラップしていく
      • Rack::Server#app
        • ここでは options[:config] = confing.ru の前提で進めると、Rack::Builder.parse_file に config.ru を渡す

Rack::Builder

  • Rack::Builder#parse_file
    • File.read(config.ru) を実行した文字列を Rack::Builder.new_from_string に渡す
  • Rack::Builder.new_from_string
    • Rack::Builder オブジェクトのコンテキストで、つまり Rack::Builder DSL で config.ru を評価する(instance_eval)。評価して Rack::Builder#to_app を呼ぶ
      • Rack::Builder#use Rack::Builder#run が主なメソッド
        • Rack::Builder#use
          • config.ru 中に use <Rack middleware クラス> という DSL があるときに呼ばれる
          • @use の配列に Proc オブジェクトを append する
            • Proc オブジェクト: app を引数に取って、その app を引数として rack middleware をインスタンス化する
        • Rack::Builder#run
          • config.ru 中に run <Rack middleware オブジェクト> という DSL があるときに呼ばれる
          • @run に app を代入する
    • Rack::Builder#to_app
      • app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
        • use した middleware と run した rack アプリケーションを入れ子構造にしていく
          • @use は app を引数に取って middleware をインスタンス化する proc オブジェクトの配列
          • >>

        • 最終的に入れ子構造になった rack アプリケーションを返す。

まとめ

  • rack は Rack::Builder に定義された DSL を使って web サーバーを立ち上げてくれる
    • サーバーは cgi, webrick など Rack::Handler で定義されたものを使える
    • use で宣言した順番で middleware の http request を受け付ける
    • run で宣言した rack アプリケーションが http request を受け取ってそこから middleware を逆順にたどって response が返される

ruby のメソッド呼び出しと method_missing

Ruby で特定のクラスのインスタンスに対してメソッドを呼び出した時、まずはクラスに存在しているインスタンスメソッドから探す。 そのクラスに該当のインスタンスメソッドがなければ、継承チェーンを上ってインスタンスメソッドを探す。 例えば下記のようなクラスがあったとする。

class Neko
  def nyaa
    "Miaumiau"
  end
end

このクラスのインスタンスをレシーバとしてメソッドを呼び出すと Neko -> Object -> Kernel -> BasicObject の順でメソッド探索が行われる(継承チェーン)

> Neko.ancestors
=> [Neko, Object, Kernel, BasicObject]

このクラスのインスタンスをレシーバとしていくつかメソッド呼び出しを行ってみる

f:id:nekootoko3:20191127000728p:plain

f:id:nekootoko3:20191127001222p:plain

f:id:nekootoko3:20191127001244p:plain

f:id:nekootoko3:20191127001259p:plain

Neko クラスをオープンして #method_missing を定義する(オープンクラス)

class Neko
  def method_missing(nakigoe)
    [:wanwan, :uho].include?(nakigoe) ? nakigoe.to_s : super
  end
end

f:id:nekootoko3:20191127001417p:plain

f:id:nekootoko3:20191127001434p:plain

method_missing を上手く使うと、メソッド呼び出しを捕まえてコードの共通化を図れる。
しかし、可読性・保守性が落ちてしまうこともあるので注意が必要。

参考文献

pipelining でコマンド実行を速くしよう with Ruby

redis

redis は client/server モデルを採用している TCP サーバーなので複数のコマンドを実行したい時に、 コマンドを送る・結果を受け取る・次のコマンドを送る...とやっていたら全てのコマンドが完了するのに、単純化して RTT(Round Trip Time) * リクエスト数 の時間がかかる。 これは パイプライニング(Pipelining) によって高速化できる。

Pipelining

Pipelining とは、サーバーからのレスポンスを待たずに複数のリクエストを送り、最後にまとめてレスポンスを受け取るという手法である。

画像 - https://ja.wikipedia.org/wiki/HTTP%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:HTTP_pipelining2_ja.svg

wikipedia の顔像から分かるようにリクエスト毎にレスポンスを待たなくてよく全体の処理時間が短縮される。 pipelining で処理が早くなる理由としてレスポンスを待たなくてよくなること以外にも、socket I/O の回数を減らせる read()write() syscall は kernel mode で動作し、redis の処理は user mode で処理される。 pipelining を使わない時、毎リクエスト毎に 2 つのモードを行き来する必要があり、そのスイッチングコストが大きい。 それが pipelining を使うと、1 回の read()・write() だけで済むので、処理時間が短縮される。

注意点! pipelining を使ってコマンドを送る際、1 回に送るコマンドを制限したほうがよい。 というのも最大で、コマンド分のレスポンスを追加でメモリに持っておく必要があるから。 10,000 くらいまでにしておくのがよさそう。

Ruby 実装

100,000 の string 型をローカルの redis に set する。 redis には予め flushall をしておいて、実行時間も計測する。

pipelining を使わない実装

実装

require 'redis'

redis = Redis.new
100000.times do |n|
  redis.set("key_#{n}", "value_#{n}")
end

実行時間

real    0m5.038s
user    0m3.015s
sys     0m1.264s

pipelining を使った実装

実装

require 'redis'

redis = Redis.new
redis.pipelined do
  100000.times do |n|
    redis.set("key_#{n}", "value_#{n}")
  end
end

実行時間

real    0m1.857s
user    0m1.196s
sys     0m0.472s

まとめ

複数のコマンドを redis に対して発行する場合には pipelining を使おう! コマンドの数によっては 3 倍以上速くなる!

参照

devise gem で生成されるもの・できること

devise gem は色々勝手にできるらしいと聞いたしチュートリアルを見ると実際にそうらしいので自分でもやってみる devise の導入手順とそれぞれの概要は以下

目次

  1. devise を rails に導入する
  2. 認証に利用する model を生成するための generator を生成する
  3. 2 で生成した generator を利用して認証用の model を生成する。同時に認証に利用できるいろんな path も追加する
  4. もうちょっとなんかやる

1. devise を rails に導入する

Gemfile に devise を追加して bundle

2. 認証に利用する model を生成するための generator を生成する

rails g devise:install を叩く

  • ファイルが生成される
    • config/initializers/devise.rb
      • 認証に利用する model を生成するための generator
    • config/locales/devise.en.yml
      • locale ファイル

3. 2 で生成した generator を利用して認証用の model を生成する。同時に認証に利用できるいろんな path も追加する

rails g generate devise user を実行すると、認証に利用する model と認証をハンドリングする path が追加される

生成されるモデル

# == Schema Information
#
# Table name: users
#
#  id                     :integer          not null, primary key
#  email                  :string           default(""), not null
#  encrypted_password     :string           default(""), not null
#  reset_password_token   :string
#  reset_password_sent_at :datetime
#  remember_created_at    :datetime
#  created_at             :datetime         not null
#  updated_at             :datetime         not null
#
# Indexes
#
#  index_users_on_email                 (email) UNIQUE
#  index_users_on_reset_password_token  (reset_password_token) UNIQUE
#

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

追加される path

  • 例えば /users/sign_up では登録フォームが既に存在する
#                                Prefix Verb   URI Pattern                                                                              Controller#Action
#                      new_user_session GET    /users/sign_in(.:format)                                                                 devise/sessions#new
#                          user_session POST   /users/sign_in(.:format)                                                                 devise/sessions#create
#                  destroy_user_session DELETE /users/sign_out(.:format)                                                                devise/sessions#destroy
#                     new_user_password GET    /users/password/new(.:format)                                                            devise/passwords#new
#                    edit_user_password GET    /users/password/edit(.:format)                                                           devise/passwords#edit
#                         user_password PATCH  /users/password(.:format)                                                                devise/passwords#update
#                                       PUT    /users/password(.:format)                                                                devise/passwords#update
#                                       POST   /users/password(.:format)                                                                devise/passwords#create
#              cancel_user_registration GET    /users/cancel(.:format)                                                                  devise/registrations#cancel
#                 new_user_registration GET    /users/sign_up(.:format)                                                                 devise/registrations#new
#                edit_user_registration GET    /users/edit(.:format)                                                                    devise/registrations#edit
#                     user_registration PATCH  /users(.:format)                                                                         devise/registrations#update
#                                       PUT    /users(.:format)                                                                         devise/registrations#update
#                                       DELETE /users(.:format)                                                                         devise/registrations#destroy
#                                       POST   /users(.:format)                                                                         devise/registrations#create
...
Rails.application.routes.draw do
  devise_for :users
...
end

その他

  • 認証に関わる様々なメソッドが利用可能になる。一部を下記
    • authenticate_user!
      • ユーザーが sign in しているか判定。してなければ root に redirect
    • user_signed_in?
      • ユーザーがログイン済みかを bool で返す
    • current_user

4. もうちょっとなんかやる

  • デフォルトの view が plain 過ぎるのでカスタマイズしたい
    • rails generate devise:views
  • controller をカスタマイズしたい
    • rails generate devise:controllers
  • oauth を利用したい
    • omniauth 編に続く

参照

redis の SETNX, SETEX, PSETEX はもういらないらしい

redis の SET のオプションで全て済むという話。 今まで redis で string を SET する時下記のように使い分けていた

  • SETNX
    • key が存在しなければ SET、存在しているなら何もしない
      • EXISTS + SET を1つのコマンドで
  • SETEX
    • key を ttl つきで設定する
      • SET + expire を1つのコマンドで
  • PSETEX
    • SETEX とほとんど同じだが、ミリ秒で ttl を設定できる

それが全て下の SET のオプションで済んでしまう

  • NX
    • SETNX
  • EX
    • SETEX
  • PX
    • SETPX
  • XX
    • key が存在している場合にのみ SET する。NX と逆の操作

操作例

127.0.0.1:6379> SET neko otoko3 NX EX 100
OK
127.0.0.1:6379> GET neko
"otoko3"
127.0.0.1:6379> TTL neko
(integer) 89
127.0.0.1:6379> SET neko kawaii XX
OK
127.0.0.1:6379> GET neko
"kawaii"
127.0.0.1:6379> SET neko onna NX
(nil)
127.0.0.1:6379> GET neko
"kawaii"

参考

素数判定

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=ALDS1_1_C&lang=jp

試し割り法

2 以上の自然数 N (2<=N) が素数であるとは、1 と N 以外に約数が存在しないということ。 素朴に計算した場合、 2<=i<=N-1 を満たす i が N を割り切れないことで判定できる。この時の計算量は O(N)。 2 で割り切れないなら全ての偶数で割り切れないので、 i % 2 == 1 という条件を付け加えると計算が半減するがやはり計算量は O(N) のままである。

合成数 x は p<=√x を満たす素因子 p を持つという性質を利用して、O(√N)の計算量で素数判定を行うことができる。 2 <= i <= √N を満たす i が N を割り切れないことを割ってみればよい。 ここまでの1 と N 以外に約数が存在しないことを試しに割って確認してみる方法を試し割り法と呼ぶ

"""
n が素数かどうかの判定を行う関数
"""
def is_prime(n):
  if n == 2:
    return True
  for i in range(2, int(n**0.5)+1):
    if n % i == 0:
      return False
  return True

エラストテネスの篩

試し割り法は特定の数値に関してのみ素数判定を行う場合には有効だが、複数数値に関して素数判定を行う場合には有効ではない判定したい数値の数だけ O(√N) の計算を行う。 素数判定を行いたい数値が複数の場合には、あらかじめ素数であることを確認できるリストを作っておくことが有効。 エラトステネスの篩を行う手順

  1. 素数素数判定を行いたい最大値(Nとする)+1、値を全て true の配列を用意する
  2. 2 <= i <= √N を満たす i に関して、3 の操作を行う
  3. i 自体を除く添字が i の倍数の値を false にする
  4. 全ての操作を行うと、素数判定したい数値が配列の添字となるときの値が true ならば素数、false ならば合成数であることが分かる配列ができる

ここで √N までとしたのは、N が合成数ならば p <= √N を満たす素因子を持つという性質から。

"""
n までの自然数が素数かどうか示す配列を作る関数
"""
def make_is_primes(n):
  is_primes = [True for i in range(n+1)]
  is_primes[0] = False
  is_primes[1] = False
  for i in range(2, int(n**0.5)+1):
    if is_primes[i]:
      j = i + i
      while j <= n:
        is_primes[j] = False
        j += i
  return is_primes

参考文献

Redux してみる

Redux公式ドキュメントを読んだりしたまとめ

Redux

  • Reduxは、アプリケーションの状態に関することを行う
  • Reactと同じような文脈だと思っていたが、ReactとReduxは別物
    • Reduxは状態やその変化を扱い、ReactはReduxによって管理された状態に応じてUIの描画を行う
    • AngularでもjQueryでもReduxを使うことができる
  • Reduxの3つの原則
    • Single Source of Truth
      • アプリケーション全体の状態は1つのオブジェクトとして保持される
    • State is read-only
      • 状態を変更する方法は何が発生したかを表すActionを発行すること
    • Changes are Made With Pure Functions
      • Actionによって状態がどのように変化するかを表すためにReducerという純粋関数を作る
  • 全体的な大まかな流れや図は下記記事が良さそう

Actions

  • アプリケーションからStoreに送られるjson
    • 何のアクションが実行されるかを示すtypeフィールドが必須
    • storeに対して、このaction以外を送ることはない
  • store.dispatch(action)を使って送られる
  • Action Creators
    • Actionを生成する関数

Reducers

  • Storeに送られるActionによってアプリケーションの状態がどのように変化するかを明記している
    • Actionsは何が起こったかを記述するが、アプリケーションの状態の変化は記述しない
  • Reducerは、以前の状態とActionを受け取り、新しい状態を返す
    • Reducerは純粋関数でなければならない
      • Reducerでやってはいけないこと
        • 受け取ったオブジェクト(state)に変更を加えること
          • stateのコピーを作り、そのコピーに代入する
            • e.g. stateとしてオブジェクトの配列が渡ってきてそれにappendしたい時、展開したstateの末尾に新たなオブジェクトを加えて返す
              • return [...state, { text: action.text, completed: false }]
        • 副作用を引き起こすこと
          • e.g. APIコール、ルーティングの変更
        • 非純粋関数を呼び出すこと
          • e.g. Date.now() or Math.random()
 Given the same arguments, it should calculate the next state and return it.
 No surprises.
 No side effects.
 No API calls.
 No mutations.
 Just a calculation.

Store

  • ActionReducerを組み合わせる役割
    • Action アプリケーションで何が発生したかを表す
    • Reducer Actionに対して状態をどのように変化したかを表す
  • Storeでできること
    • アプリケーションの状態を保持する
    • getState()を通じて、状態を取得できるようにする
    • dispatch(action)を通じて、状態の更新をできるようにする
    • subscribe(listener)を通じて、リスナーを登録する
    • subscribe(listener)の戻り値の関数を実行することで、リスナーの登録解除を行う

Data Flow

  • データのライフサイクルは4つのステップに分かれている
    1. store.dispatch(action)の呼び出し
    2. StoreReducerを呼び出す
    3. root Reducerが複数のreducerの出力をアプリケーション全体の状態を表すオブジェクト(a single state tree)にまとめる
    4. root Reducerから返却されたa single state treeをStoreが保存する

Usage with React

  • ReactがReduxと接続すると、ReactはPresentational ComponentsContainer Componentsに分かれる
Presentational Components Container Components
目的 見た目(markup, styles) 動き(fetch data, state updates)
Aware of Redux No Yes
To Read Data props からデータを受け取る Redux State からデータを受け取る
To Change Data props から受け取った関数を実行する Redux Action を Dispatch する
Are Written プログラマが記述する 通常は React Redux によって生成される
  • それぞれのComponentsの設計
    • Presentational Components の設計
      • React Componentの設計手順が参考になりそう
        1. Break the UI Into A Component Hierarchy
        2. Build A Static Version in React
          • データモデルを受け取って描画するだけのアプリケーションを作る
            • インタラクティブな要素とは分けることがポイント
              • 静的なバージョンは考えることは少ないがコーディングは多い
              • インタラクティブな要素の追加は考えることは多いがコーディングは少ない
        3. Identify The Minimal(but complete) Representation Of UI State
          • UI の state として持つべき最小限を特定する
          • DRYがキーワード
            • 下記はstateとして持ってはならない
              • 親要素からpropsとして渡されるもの
              • 操作がお粉されても変わらないもの
              • 他のstateやpropsから導き出せるもの
        4. Identify Where Your State Should Live
          • Redux は single source of truth なので関係なさそう
        5. Add Inverse Data Flow
          • Redux はデータの流れは一方向なので関係なさそう
    • Container Componentsの設計
      • Presentatinal ComponentsとReduxをつなげるContainer Componentsを設計する

Componentsを作っていく手順

  1. Presentational Components を作る
    • local state や lifecycle methods を使わずに、ステートレスな関数としてのコンポーネントを作る
      • 関数でなくてもよいが、関数が簡単
      • local stateやlifecycle methods、パフォーマンスの最適化を行う時に、関数をクラスに変更する
  2. Container Components を作る
    • Presentational Components と Redux をつなげる
    • Comtainer Component は store.subscribe() を実行して、Redux の state の一部を読み出し、Presentational Components にプロパティとして渡す
      • 内部的には類似の処理を行うが、パフォーマンスが最適化されているconnect()を実行することが推奨される
        • connect()を使うためにmapStateToProps()を定義する必要がある
          • 現在のRedux stateがpresentational Componentにどのようなpropsとして渡されるかを記述したもの(関数名のまま)
        • 同様にmapDispatchToProps()も定義する
          • dispatch()を受け取って、presentational Componentに渡す callback props を返す
  3. Conponent内でcontainerをまとめる
  4. 各ComponentにStoreを渡す
    • 全てのComponentはRedux Storeにアクセスする必要があるがpropsとして渡していくのは煩雑
    • Providerを使うと全てのContainerがStoreにアクセスできるようになる