2014年1月8日 星期三

Rails 的 i18n fallback 問題

當在 config/application.rb 中指定了 config.i18n.fallbacks 語系順序後,別忘了要 comment 掉 produciton.rb 中的 config.i18n.fallbacks = true
production 下的語系順序才會乖乖實現。

2014年1月3日 星期五

Unicorn + Nginx 配置及設定

最近部屬公司的新產品 - flyTutor 的時候,嘗試了不同的架構,從以往的 passenger-plugin on Nginx 換成了Unicorn 搭配 Nginx 的「反向代理」架構

What's "Reverse Proxy Model" ?

在提起 Unicorn 及 Nginx 的配置之前,我們得先了解一下「反向代理機制」,下面這張圖應該可以幫助你了解:

(轉自: ROR實戰聖經)


在用戶端向 server 作請求的時候,不是直接與 app server 連結 (中間那三個),而是先連到 web server ,通常是 Apache 或 Nginx,再由 web server 指派 request 給 app server、或直接 serve static assets。
所以在這樣的架構下你可以想像你的系統需要常駐著以下的服務:
  1. web server
  2. 數個 app server
  3. DB server

這樣的架構是與常見的 Apache/Nginx + Passenger 架構有所不同的,需要作一點觀念上的調適。下面我們就來介紹一下如何做相關的配置。


Nginx 配置

以下是預設的 Nginx 設定檔 - nginx.conf:
user deployer; # 定義操作 nginx 的使用者
worker_processes 1; # 定義 worker 的數量

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {

  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  access_log    /var/log/nginx/access.log;

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout  65;

  gzip  on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_proxied any;
  gzip_vary off;
  gzip_types text/plain text/css application/x-javascript text/xml application/xml application/rss+xml application/atom+xml text/javascript application/javascript application/json text/mathml;
  gzip_min_length  1000;
  gzip_disable     "MSIE [1-6]\.";

  server_names_hash_bucket_size 64;
  types_hash_max_size 2048;
  types_hash_bucket_size 64;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;

}

上面這段預設的設定檔有兩個地方需要注意:
第一行的使用者身分必須為負責 deploy 的用戶,否則常常會出現 403 權限問題,因為未經設定的話 nginx 會沒有權限存取 deployer 的檔案,如 js, css。
第二行的 worker 數量建議別設定超過 cpu 數目,多了也沒好處。

再來就是重頭戲了,我們要開始加入 app server 的設定,這段設定需要加在 http 的括號內:
  upstream unicorn {
    server 127.0.0.1:8080 fail_timeout=0;
  }

  server {
    listen 80 default deferred;
    server_name flytutor.com;
    root /var/www/flytutor/public;

    location ^~ /assets/ {
      gzip_static on;
      expires max;
      add_header Cache-Control public;
    }

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;

      if (!-f $request_filename) {
        proxy_pass http://unicorn;
      }

    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
  }


upstream:

首先我們先來講解一下第一部分的 upstream。首先,upstream 後的「unicorn」並非一定的,端看你希望怎麼命名,但是要注意的是 proxy_pass 也需作對應的設定。再來,upstream 內的 server 可以不只一個,如果你有數個 app server 在不同的 host 的話,你也可以做如下的設定:
upstream flytutor.com {
  server 192.168.0.111:8080 weight=3;
  server 192.168.0.222:8080 weight=2;
  server 192.168.0.333:8080 weight=3;
}
其中 weight 代表的是「被配發 request 的權重」,當 weight 越高的時候被分配到的機率越大,可以做 loading balance。

而除了走 TCP/IP 外,如果是在 web server 和 app server 都在同一個主機的情況下,你也可以使用 socket 來做設定,基本上效能會比 TCP/IP 好一點點(因為少了 header):
  upstream unicorn {
    server unix:/tmp/unicorn.sock fail_timeout=0;
  }

server:

接下來我們要介紹一下 server 的設定,基本上這邊指的 server 就是指 app server,一台 nginx server 可以依 server 設定派發 request ,所以可以做 vhost 的應用,例如判斷 request 的不同網域 (設定檔中的 "server_name")、或是同網域但是不同 port (設定檔中的 "listen"),來派發到不同的服務。
proxy_pass 很重要,許多網友都死在這個地方,切記要將他設置成 upstream 的命名,例如 "http://<upstream_name>"。
你可以想像當一個 request 經過 location(path), server_name(domain), listen(port) 重重篩選後,你卻不指派 proxy_pass 的目的地給他,讓這個 request 就此隱沒在數據流中,成了「digital phantom」,你於心何忍?


Unicorn 配置

unicorn 的配置通常是放在 rails project 中的 config 資料夾,長得像這樣:
app_root = "/var/www"
app_name = "flytutor"
listen "127.0.0.1:10101" #, :backlog => 2048 #這邊要跟nginx虛擬主機檔中upstream內定義的務必一樣
worker_processes 2 #看情況開
preload_app false
timeout 30
 
module Rails
  class <<self
    def root
      File.expand_path(__FILE__).split('/')[0..-3].join('/')
    end
  end
end

_working_directory = File.join(app_root, app_name)
working_directory _working_directory
logs_path = "#{_working_directory}/log"
pid "#{_working_directory}/tmp/pids/unicorn.pid"
stderr_path "#{logs_path}/unicorn.stderr.log"
stdout_path "#{logs_path}/unicorn.stdout.log"
 
GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true
 
before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
  
  old_pid = "#{Rails.root}/tmp/pids/unicorn.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      puts "Send 'QUIT' signal to unicorn error!"
    end
  end
end
 
after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

這邊要注意的地方有兩個:

  1. listen 的對象必須與 nginx.conf 中 upstream 設定的一樣,unicorn 與 nginx 就是靠這行設定連起來的。還有,如果是走 TCP/IP 的設定,盡量不要使用 1024 以下的 port 以免 deployer 沒有權限啟動 unicorn 導致失敗(你總不能老用 root 權限作 deploy 吧?)。
  2. unicorn 是採用 master-slave 機制,啟動 unicorn 後叫出  process 列表你就會了解的。而這裡的 worker_processes 的數目顧名思義就是指定 worker 的數目。開多少個才好?基本上沒有一定的標準答案,還是要依你的使用情況及環境做測試後才知道,如果懶得想的話就直接設定為「CPU 數目 + 1」吧。


操作 unicorn

許多網友在網路上搜尋到的啟動法是以 unicorn_rails 來啟動:/usr/bin/unicorn_rails -c config/unicorn.rb -E $RAILS_ENV -D。但現在 unicorn 官方已經不建議使用這種作法而是直接改用 unicorn 來啟動/重啟 unicorn

Hot restart with zero down time

許多人之所以選擇 unicorn 或許就是因為他的 hot restart 特性。如果對這有興趣的人可以參考這篇



2014年1月1日 星期三

vagrant 初體驗與故障排除

1. 30 秒認識 vagrant:

Vagrant 是一個 ruby 寫的工具,它是一個 DSL 讓開發者可以輕易控制 VirtualBox 的 VM 。用它可以輕鬆管理和制作我們理想中的開發環境。
  $ gem install vagrant #安裝 vagrant
  $ vagrant box add ubuntu http://cloud-images.ubuntu.com/vagrant/quantal/current/quantal-server-cloudimg-i386-vagrant-disk1.box #安裝新的 Vagrant Package。這裡的 ubuntu 是一個預先做好的空的 ubuntu 12.10 (intel-based)
  $ vagrant init ubuntu
  $ vagrant up
box 檔可以在 http://www.vagrantbox.es/ 下載


2. 設定:

此時就可以使用 vangrant ssh 連到vm了,預設使用者/密碼為 vangrant/vangrant
接著編輯本地資料夾中的 Vagrantfile,修改 config.vm.network :hostonly, "33.33.33.33" 將ip改成自己想要的ip,這裡以 "33.33.33.33" 為例。修改完後要執行 vangrant reload


3. 故障排除:


無法以 SSH 連接 VM:

[default] Failed to connect to VM!
Failed to connect to VM via SSH. Please verify the VM successfully booted
by looking at the VirtualBox GUI.
當出現上面的錯誤訊息時,可以照 http://vagrant.wikia.com/wiki/Usage 上的步驟來排除,基本上就是將 Vagrantfile 中的 config.vm.boot_mode = :gui 設定打開。再重新 vagrant up,在產生的 GUI 中輸入 sudo dhclient eth0


Guest additions not matchs:

[default] The guest additions on this VM do not match the install version of
VirtualBox! This may cause things such as forwarded ports, shared
folders, and more to not work properly. If any of those things fail on
this machine, please update the guest additions and repackage the
box.


Reference:

http://reality.hk/posts/2011/12/21/vagrant
http://gogojimmy.net/2013/05/26/vagrant-tutorial/


2013年12月31日 星期二

Rails query tips

Pluck - :

這個指令能讓搜尋結果的特定欄位獨立抽取出來放到 Array 裡,在很多情形下都很方便。
Ex: User.all.pluck :name # => ["Kevin", "Laura", "Yiya", "Diya"]


Subquery:

以第一個query的結果作為條件做搜尋,Ex:
Order.where( :product_id => Product.where("price<1000") )
(Reference: http://ruby-china.org/topics/10771)


Update with condition:

Ex: Model.where("state == 'decline'").update_all(:state => 'deny')


has_many relation with condition:

當兩個 model 彼此為一對多關係時,我們可以利用 condition 讓這個關係有所區分,我拿以下的情形為例:user 有很多訂單 (orders) ,有些是取消的、有些是處理中的、有些是已經結帳的。在這個情形下,用 user.orders 撈所有的訂單後再進行判斷顯然沒有效率、用 Order.where("{條件}") 的方法也就失去了 relation 的方便與直觀。這時我們可以做加上這幾個 relation:
has_many :cancled_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Cancled'" }
has_many :handling_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Handling'" }
has_many :paid_orders, :class_name=>"Order", :conditions=>proc{ "status = 'Paid'" }

這樣就可以透過如 current_user.cancled_orders 的方式取到「取消的訂單」了,相當直觀。


OR condition using Arel

Ex:
t = Post.arel_table

results = Post.where(
  t[:author].eq("Someone").
  or(t[:title].matches("%something%"))
)



2013年12月30日 星期一

動手寫一個 Rails plugin

因為開發上方便,需要一個能快速切換登入身分的小 widget,正好趁這個機會搞懂怎麼開發一個 Rails plugin。成品在這:https://github.com/kevin-shu/user_switch
以下就是步驟囉:

1. rails plugin new my_gem

首先執行 rails plugin new <plugin_name>,會產生 plugin 資料夾,結構為:
my_plugin
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── lib
│   ├── my_plugin
│   │   └── version.rb
│   ├── my_plugin.rb
│   └── tasks
│       └── my_plugin_tasks.rake
├── my_plugin.gemspec
└── test
    │
    ...(本篇不提到,忽略)
接下來就要介紹這些資料夾、檔案的用途


2. my_plugin.gemspec

這是一個 manifest 檔案,用來告訴 gem 該怎麼打包這個專案。


3. lib 資料夾

my_plugin.rb:

這就是 plugin 的核心,當裝上 plugin 後,被執行的就是這個檔案。你當然可以將所有 code 都寫在裡面,但是這樣很亂。一個功能稍微複雜且完整的 plugin 一定會有系統的做模組化。所以我們往往不在裡面寫很多 code ,而是將一個個寫好的模組 require 近來。

my_plugin 資料夾:

剛剛說的模組化的 code 通常會被分門別類放在這裡。

my_plugin/version.rb:

在初始化後,my_plugin 資料夾一開始只有這個檔案,用來記錄版本號。你如果有看一下 gemspec 的內容的話,會發現這個檔案會被 gemspec require 到。

Engine

如果你的 plugin 需要一些自定的 controller、view、assets,首先要加入這段程式碼:
module ZurbFoundation
  class Engine < Rails::Engine
  end
end
你可以將他獨立成一個檔案由 my_plugin.rb 載入,或是直接寫到 my_plugin.rb 裡。有了這段程式碼,Rails就會自動將 app 及 config 兩個資料夾下的所有檔案載入。
請注意,載入 engine 的 code 必須放在 my_plugin.rb 的最後,才能正常運作


4. app 資料夾

當你希望在安裝這個套件的專案中使用客製的 controller、assets 等檔案,就要在根目錄下建立一個 app 資料夾。結構基本上跟你熟悉的 rails 的 app 資料夾是一樣的。要注意的是,裡面的 controller 或 model 必須在 專案的 namespace 下,例如:
# CURRENT FILE :: app/controllers/my_plugin/my_controller.rb
module MyPlugin
  class MyController < ::ApplicationController
    def index
      ...
    end
  end
end


5. config

這個資料夾跟app資料夾一樣需要手動建立,最常見的用途是設定 routing,例如:
# CURRENT FILE :: config/routes.rb
Rails.application.routes.draw do
  get "team" => "team_page/team#index" , :as => :team_page
end


6. generators 資料夾

這個資料夾也是需要時才要自行建立。裡面放了一些設定檔等的 template 以及 install method,讓這個 gem 提供 install 功能,執行後會複製一份設定檔到專案中。這邊不會對 generators 多做介紹。


7. pack & push

當你的 plugin 完成後,就可以用以下指令將他打包起來,gem 會按照 gemspec 的設定做打包。
gem build /project_path/user_switch.gemspec
打包後的 gem 可以 push 到 rubygems.org:
gem push project_name-0.0.1.gem


8. 在 Rails 專案中掛上剛寫好的plugin push

我們有可以在 Gemfile 中依不同來源來做來源的設定,其中當來源是來自 local 或 git 時,是不用打包就可以直接掛上的,開發的時候來源通常是設定在 local。

(1) local: 

gem "my_plugin", :path=>"/path/to/your/plugin"

(2) git:

gem "my_plugin", :git=>"https://path/to/your/repo"

(3) rubygem.org:

gem "my_plugin"


Reference:

http://zurb.com/article/814/yetify-your-rails-new-foundation-gem-and-
http://coding.smashingmagazine.com/2011/06/23/a-guide-to-starting-your-own-rails-engine-gem/
http://guides.rubyonrails.org/plugins.html

2013年12月24日 星期二

Rails 的 Assets Convension

Rails 開發者應該都知道 assets 的路徑原則

  1. app/assets/ 放自己的 assets
  2. vendor/assetes/ 放第三方assets


但是在 app/assets 底下無法 require_tree vendor底下的資料夾,只能 require 單一檔案。要達到同樣的效果只要 require vendor的檔案,這個檔案再 require_tree 即可。

2013年12月17日 星期二

warning: toplevel constant Model referenced by NS::Model


今天遇到了這個問題,在網上搜一下找到一個滿詳盡的解釋,茅塞頓開:
Your User::File class is not loaded. You have to require it (e.g. in user.rb).
The following happens when ruby/rails sees User::Info and evaluates it (simplified; only User is defined yet).
  • check if User::Info is defined - it is not (yet)
  • check if Info is defined - it is not (yet)
  • uninitialized constant -> do rails magic to find the user/info.rb file and require it
  • return User::Info
Now lets do it again for User::File
  • check if User::File is defined - it is not (yet)
  • check if File is defined - it is (because ruby has a built in File class)!
  • produce a warning, because we've been asked for User::File but got ::File
  • return ::File

裡面提到了要手動 Require NameSpace 下的 Model,但是如果 NS 下的 model 本身有繼承到 NS 外的 Model,要確保在父類別宣告後建立才行。所以最好是在父類別檔案的結尾處 require。