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 の引数である env
は CGI-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 を持っていない
- Empty の中で出力した 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 サーバーを起動している
- Rails::Command::ServerCommand#perform で Rails サーバーの起動をしていて、その中で Rails::Server#start を呼んでいる
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L147
- 環境変数を取ってきたり起動前のセットアップをしている
- Rails::Server#initialize を呼んだ後、そのインスタンスに対して Rails::Server#start を呼んでいる
- Rails::Server#initialize
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L12
- super() で Rack::Server#initialize に委譲している
- Rails::Server#start
- https://github.com/rails/rails/blob/30349de203de442676e162d3f3dd0492b29d19ac/railties/lib/rails/commands/server/server_command.rb#L21
- super() を呼んで、Rack::Server#start を呼び出している
- Rails::Server#initialize
コードを追う
example のコマンド bin/rackup -Ilib example/lobster.ru
で rack application を立ち上げるところを追う
Rack::Server
bin/rackup
は Rack::Server.start を実行するだけ- Rack::Server.start
- コンストラクタに引数(options)をそのまま渡してオブジェクトに
start
させている
- コンストラクタに引数(options)をそのまま渡してオブジェクトに
- 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 でラップしていく
- 環境ごとの middleware を Array#reverse_each していく
- Rack::Server#app
- ここでは options[:config] = confing.ru の前提で進めると、Rack::Builder.parse_file に config.ru を渡す
- Rack::Server#build_up
- Rack::Server#build_up(Rack::Server#app) を実行している
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
- Rack::Builder#run
- config.ru 中に
run <Rack middleware オブジェクト>
という DSL があるときに呼ばれる - @run に app を代入する
- config.ru 中に
- Rack::Builder#to_app
まとめ
ruby のメソッド呼び出しと method_missing
Ruby で特定のクラスのインスタンスに対してメソッドを呼び出した時、まずはクラスに存在しているインスタンスメソッドから探す。 そのクラスに該当のインスタンスメソッドがなければ、継承チェーンを上ってインスタンスメソッドを探す。 例えば下記のようなクラスがあったとする。
class Neko def nyaa "Miaumiau" end end
このクラスのインスタンスをレシーバとしてメソッドを呼び出すと Neko -> Object -> Kernel -> BasicObject の順でメソッド探索が行われる(継承チェーン)
> Neko.ancestors => [Neko, Object, Kernel, BasicObject]
このクラスのインスタンスをレシーバとしていくつかメソッド呼び出しを行ってみる
Neko クラスをオープンして #method_missing
を定義する(オープンクラス)
class Neko def method_missing(nakigoe) [:wanwan, :uho].include?(nakigoe) ? nakigoe.to_s : super end end
method_missing
を上手く使うと、メソッド呼び出しを捕まえてコードの共通化を図れる。
しかし、可読性・保守性が落ちてしまうこともあるので注意が必要。
参考文献
- メタプログラミングRuby
- Ruby にちょっと慣れたら必読
pipelining でコマンド実行を速くしよう with Ruby
redis
redis は client/server モデルを採用している TCP サーバーなので複数のコマンドを実行したい時に、
コマンドを送る・結果を受け取る・次のコマンドを送る...とやっていたら全てのコマンドが完了するのに、単純化して RTT(Round Trip Time) * リクエスト数
の時間がかかる。
これは パイプライニング(Pipelining)
によって高速化できる。
Pipelining
Pipelining とは、サーバーからのレスポンスを待たずに複数のリクエストを送り、最後にまとめてレスポンスを受け取るという手法である。
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 の導入手順とそれぞれの概要は以下
目次
- devise を rails に導入する
- 認証に利用する model を生成するための generator を生成する
- 2 で生成した generator を利用して認証用の model を生成する。同時に認証に利用できるいろんな path も追加する
- もうちょっとなんかやる
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
その他
- 認証に関わる様々なメソッドが利用可能になる。一部を下記
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つのコマンドで
- key が存在しなければ SET、存在しているなら何もしない
SETEX
- key を ttl つきで設定する
SET + expire
を1つのコマンドで
- key を ttl つきで設定する
PSETEX
- SETEX とほとんど同じだが、ミリ秒で ttl を設定できる
それが全て下の SET のオプションで済んでしまう
NX
SETNX
EX
SETEX
PX
SETPX
XX
- key が存在している場合にのみ SET する。
NX
と逆の操作
- key が存在している場合にのみ SET する。
操作例
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) の計算を行う。 素数判定を行いたい数値が複数の場合には、あらかじめ素数であることを確認できるリストを作っておくことが有効。 エラトステネスの篩を行う手順
- 要素数を素数判定を行いたい最大値(Nとする)+1、値を全て true の配列を用意する
- 2 <= i <= √N を満たす i に関して、3 の操作を行う
- i 自体を除く添字が i の倍数の値を false にする
- 全ての操作を行うと、素数判定したい数値が配列の添字となるときの値が 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は
何が起こった
かを記述するが、アプリケーションの状態の変化
は記述しない
- Actionsは
- Reducerは、以前の状態とActionを受け取り、新しい状態を返す
- Reducerは
純粋関数
でなければならない- Reducerでやってはいけないこと
- 受け取ったオブジェクト(state)に変更を加えること
- stateのコピーを作り、そのコピーに代入する
- e.g. stateとしてオブジェクトの配列が渡ってきてそれにappendしたい時、展開したstateの末尾に新たなオブジェクトを加えて返す
- return [...state, { text: action.text, completed: false }]
- e.g. stateとしてオブジェクトの配列が渡ってきてそれにappendしたい時、展開したstateの末尾に新たなオブジェクトを加えて返す
- stateのコピーを作り、そのコピーに代入する
- 副作用を引き起こすこと
- e.g. APIコール、ルーティングの変更
- 非純粋関数を呼び出すこと
- e.g. Date.now() or Math.random()
- 受け取ったオブジェクト(state)に変更を加えること
- Reducerでやってはいけないこと
- Reducerは
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
Action
とReducer
を組み合わせる役割Action
アプリケーションで何が発生したかを表すReducer
Actionに対して状態をどのように変化したかを表す
- Storeでできること
- アプリケーションの状態を保持する
getState()
を通じて、状態を取得できるようにするdispatch(action)
を通じて、状態の更新をできるようにするsubscribe(listener)
を通じて、リスナーを登録するsubscribe(listener)
の戻り値の関数を実行することで、リスナーの登録解除を行う
Data Flow
- データのライフサイクルは4つのステップに分かれている
store.dispatch(action)
の呼び出しStore
がReducer
を呼び出すroot Reducer
が複数のreducerの出力をアプリケーション全体の状態を表すオブジェクト(a single state tree
)にまとめる- root Reducerから返却された
a single state tree
をStoreが保存する
Usage with React
- ReactがReduxと接続すると、Reactは
Presentational Components
とContainer 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の設計手順が参考になりそう
- Break the UI Into A Component Hierarchy
- Build A Static Version in React
- Identify The Minimal(but complete) Representation Of UI State
- UI の state として持つべき最小限を特定する
DRY
がキーワード- 下記はstateとして持ってはならない
- 親要素からpropsとして渡されるもの
- 操作がお粉されても変わらないもの
- 他のstateやpropsから導き出せるもの
- 下記はstateとして持ってはならない
- Identify Where Your State Should Live
- Redux は single source of truth なので関係なさそう
- Add Inverse Data Flow
- Redux はデータの流れは一方向なので関係なさそう
- React Componentの設計手順が参考になりそう
- Container Componentsの設計
- Presentatinal ComponentsとReduxをつなげるContainer Componentsを設計する
- Presentational Components の設計
Componentsを作っていく手順
- Presentational Components を作る
- local state や lifecycle methods を使わずに、ステートレスな関数としてのコンポーネントを作る
- 関数でなくてもよいが、関数が簡単
- local stateやlifecycle methods、パフォーマンスの最適化を行う時に、関数をクラスに変更する
- local state や lifecycle methods を使わずに、ステートレスな関数としてのコンポーネントを作る
- 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 を返す
- connect()を使うために
- 内部的には類似の処理を行うが、パフォーマンスが最適化されている
- Conponent内でcontainerをまとめる
- 各ComponentにStoreを渡す
- 全てのComponentはRedux Storeにアクセスする必要があるがpropsとして渡していくのは煩雑
Provider
を使うと全てのContainerがStoreにアクセスできるようになる